mirror of
https://github.com/localsend/localsend.git
synced 2026-06-23 04:10:07 +00:00
feat: extend http server structure
This commit is contained in:
Generated
+27
@@ -61,6 +61,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
@@ -613,6 +619,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -811,6 +823,11 @@ name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
@@ -1198,6 +1215,7 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"lru",
|
||||
"pem",
|
||||
"rand 0.9.1",
|
||||
"reqwest",
|
||||
@@ -1235,6 +1253,15 @@ version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
|
||||
@@ -13,6 +13,7 @@ futures-util = { version = "0.3.31", features = ["sink"] }
|
||||
http-body-util = { version = "0.1.3", optional = true }
|
||||
hyper = { version = "1.6.0", optional = true }
|
||||
hyper-util = { version = "0.1.15", features = ["server"], optional = true }
|
||||
lru = "0.16.0"
|
||||
pem = { version = "3.0.5", optional = true }
|
||||
reqwest = { version = "0.12.22", features = ["json", "rustls-tls-webpki-roots-no-provider", "stream"], optional = true }
|
||||
rand = "0.9.1"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::http::StatusCodeError;
|
||||
use crate::model::discovery::{ProtocolType, RegisterDto, RegisterResponseDto};
|
||||
use crate::model::transfer::{PrepareUploadRequestDto, PrepareUploadResponseDto};
|
||||
use futures_util::StreamExt;
|
||||
@@ -6,6 +5,7 @@ use reqwest::{Response, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use crate::http::StatusCodeError;
|
||||
|
||||
const BASE_PATH: &str = "/api/localsend/v2";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct NonceRequest {
|
||||
/// The nonce string.
|
||||
pub nonce: String,
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
pub mod dto;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{status};{message:?}")]
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::sync::Arc;
|
||||
use rustls::pki_types::pem::PemObject;
|
||||
use rustls::{DigitallySignedStruct, DistinguishedName, Error, RootCertStore, SignatureScheme};
|
||||
use rustls::client::danger::HandshakeSignatureValid;
|
||||
use rustls::pki_types::{CertificateDer, UnixTime};
|
||||
use rustls::server::danger::{ClientCertVerified, ClientCertVerifier};
|
||||
use rustls::server::WebPkiClientVerifier;
|
||||
use x509_parser::nom::AsBytes;
|
||||
|
||||
/// Enables client certificate verification.
|
||||
pub(crate) struct CustomClientCertVerifier {
|
||||
inner: Arc<dyn ClientCertVerifier>,
|
||||
}
|
||||
|
||||
impl CustomClientCertVerifier {
|
||||
pub(crate) fn try_new(cert: &str) -> anyhow::Result<Self> {
|
||||
// We add the certificate of the server itself just so that no "empty" error is returned.
|
||||
// We don't care about the authority of the certificate, just that it is valid.
|
||||
let mut root_cert_store = RootCertStore::empty();
|
||||
root_cert_store.add(PemObject::from_pem_slice(cert.as_bytes())?)?;
|
||||
|
||||
Ok(Self {
|
||||
inner: WebPkiClientVerifier::builder(Arc::new(root_cert_store)).build()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for CustomClientCertVerifier {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientCertVerifier for CustomClientCertVerifier {
|
||||
fn offer_client_auth(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn client_auth_mandatory(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn root_hint_subjects(&self) -> &[DistinguishedName] {
|
||||
self.inner.root_hint_subjects()
|
||||
}
|
||||
|
||||
fn verify_client_cert(
|
||||
&self,
|
||||
cert: &CertificateDer<'_>,
|
||||
_: &[CertificateDer<'_>],
|
||||
_: UnixTime,
|
||||
) -> Result<ClientCertVerified, Error> {
|
||||
// We trust any certificate that is valid.
|
||||
crate::crypto::cert::verify_cert_from_der(cert.as_bytes(), None).map_err(|e| {
|
||||
tracing::warn!("Client certificate verification failed: {e:#}");
|
||||
Error::InvalidCertificate(rustls::CertificateError::ApplicationVerificationFailure)
|
||||
})?;
|
||||
Ok(ClientCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, Error> {
|
||||
self.inner.verify_tls12_signature(message, cert, dss)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, Error> {
|
||||
self.inner.verify_tls13_signature(message, cert, dss)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
self.inner.supported_verify_schemes()
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,40 @@
|
||||
mod client_cert_verifier;
|
||||
|
||||
use crate::crypto::cert::public_key_from_cert_der;
|
||||
use crate::http::server::client_cert_verifier::CustomClientCertVerifier;
|
||||
use bytes::Bytes;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::body::Incoming;
|
||||
use hyper::body::{Body, Incoming};
|
||||
use hyper::{Method, Request, Response, StatusCode};
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use hyper_util::server::conn::auto::Builder;
|
||||
use rustls::client::danger::HandshakeSignatureValid;
|
||||
use lru::LruCache;
|
||||
use rustls::pki_types::pem::PemObject;
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, UnixTime};
|
||||
use rustls::server::danger::{ClientCertVerified, ClientCertVerifier};
|
||||
use rustls::server::WebPkiClientVerifier;
|
||||
use rustls::{DigitallySignedStruct, DistinguishedName, Error, RootCertStore, SignatureScheme};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use std::fmt::Debug;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{oneshot, Mutex};
|
||||
use x509_parser::nom::AsBytes;
|
||||
use uuid::Uuid;
|
||||
use crate::http::dto::NonceRequest;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
local_nonce_map: Arc<Mutex<LruCache<String, Vec<u8>>>>,
|
||||
remote_nonce_map: Arc<Mutex<LruCache<String, Vec<u8>>>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
local_nonce_map: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(200).unwrap()))),
|
||||
remote_nonce_map: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(200).unwrap()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LsHttpServer {
|
||||
stop_tx: Arc<Mutex<Option<oneshot::Sender<()>>>>,
|
||||
@@ -41,7 +59,7 @@ impl LsHttpServer {
|
||||
if start_server_with_addr(ipv6_socket_addr, tls_config).await.is_err() {
|
||||
tracing::warn!("Failed to start server on: {}", ipv6_socket_addr);
|
||||
|
||||
// Keep the future running forever so we continue using ipv4 only.
|
||||
// Keep the future running forever, so we continue using ipv4 only even if ipv6 fails.
|
||||
tokio::time::sleep(std::time::Duration::from_secs(u64::MAX)).await;
|
||||
}
|
||||
} => {}
|
||||
@@ -80,6 +98,7 @@ async fn start_server_with_addr(
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
let incoming = tokio::net::TcpListener::bind(socket_addr).await?;
|
||||
let app_state = AppState::new();
|
||||
|
||||
let tls_acceptor = match tls_config {
|
||||
Some(tls_config) => Some(create_tls_config(&tls_config).inspect_err(|err| {
|
||||
@@ -95,9 +114,10 @@ async fn start_server_with_addr(
|
||||
);
|
||||
|
||||
loop {
|
||||
let (tcp_stream, _remote_addr) = incoming.accept().await?;
|
||||
let (tcp_stream, remote_addr) = incoming.accept().await?;
|
||||
|
||||
let tls_acceptor = tls_acceptor.clone();
|
||||
let app_state = app_state.clone();
|
||||
tokio::spawn(async move {
|
||||
let res = match tls_acceptor {
|
||||
Some(tls_acceptor) => {
|
||||
@@ -109,9 +129,10 @@ async fn start_server_with_addr(
|
||||
}
|
||||
};
|
||||
|
||||
let client_cert = {
|
||||
let client_info = {
|
||||
let (_, server_connection) = tls_stream.get_ref();
|
||||
ClientCertExt {
|
||||
ClientInfo {
|
||||
ip: remote_addr.ip(),
|
||||
cert: server_connection
|
||||
.deref()
|
||||
.deref()
|
||||
@@ -125,8 +146,9 @@ async fn start_server_with_addr(
|
||||
TokioIo::new(tls_stream),
|
||||
hyper::service::service_fn(move |mut req: Request<Incoming>| {
|
||||
req.extensions_mut()
|
||||
.insert::<ClientCertExt>(client_cert.clone());
|
||||
echo(req)
|
||||
.insert::<ClientInfo>(client_info.clone());
|
||||
req.extensions_mut().insert::<AppState>(app_state.clone());
|
||||
handle_request(req)
|
||||
}),
|
||||
)
|
||||
.await
|
||||
@@ -135,7 +157,14 @@ async fn start_server_with_addr(
|
||||
Builder::new(TokioExecutor::new())
|
||||
.serve_connection(
|
||||
TokioIo::new(tcp_stream),
|
||||
hyper::service::service_fn(move |req: Request<Incoming>| echo(req)),
|
||||
hyper::service::service_fn(move |mut req: Request<Incoming>| {
|
||||
req.extensions_mut().insert::<ClientInfo>(ClientInfo {
|
||||
ip: remote_addr.ip(),
|
||||
cert: None,
|
||||
});
|
||||
req.extensions_mut().insert::<AppState>(app_state.clone());
|
||||
handle_request(req)
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -162,29 +191,11 @@ fn create_tls_config(tls_config: &TlsConfig) -> anyhow::Result<tokio_rustls::Tls
|
||||
Ok(tokio_rustls::TlsAcceptor::from(Arc::new(config)))
|
||||
}
|
||||
|
||||
async fn echo(req: Request<Incoming>) -> Result<Response<Full<Bytes>>, hyper::Error> {
|
||||
let mut response = Response::new(Full::default());
|
||||
|
||||
match (req.method(), req.uri().path()) {
|
||||
(&Method::GET, "/") => {
|
||||
let cert = req.extensions().get::<ClientCertExt>();
|
||||
println!("{:?}", cert.extract_public_key());
|
||||
|
||||
*response.body_mut() = Full::from("Try POST /echo\n");
|
||||
}
|
||||
(&Method::POST, "/echo") => {
|
||||
*response.body_mut() = Full::from(req.into_body().collect().await?.to_bytes());
|
||||
}
|
||||
// Catch-all 404.
|
||||
_ => {
|
||||
*response.status_mut() = StatusCode::NOT_FOUND;
|
||||
}
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ClientCertExt {
|
||||
struct ClientInfo {
|
||||
/// The IP address of the client.
|
||||
ip: IpAddr,
|
||||
|
||||
/// The client certificate in DER format.
|
||||
cert: Option<Vec<u8>>,
|
||||
}
|
||||
@@ -193,82 +204,72 @@ trait PublicCertExt {
|
||||
fn extract_public_key(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
impl PublicCertExt for Option<&ClientCertExt> {
|
||||
impl PublicCertExt for &ClientInfo {
|
||||
fn extract_public_key(&self) -> Option<String> {
|
||||
self.as_ref()
|
||||
.map(|cert| public_key_from_cert_der(cert.cert.as_ref().unwrap()).unwrap())
|
||||
match &self.cert {
|
||||
Some(cert) => match public_key_from_cert_der(cert) {
|
||||
Ok(public_key) => Some(public_key),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to extract public key from certificate: {err:#}");
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomClientCertVerifier {
|
||||
inner: Arc<dyn ClientCertVerifier>,
|
||||
fn build_response(
|
||||
status: StatusCode,
|
||||
body: Option<serde_json::Value>,
|
||||
) -> Result<Response<Full<Bytes>>, hyper::Error> {
|
||||
let mut response = Response::new(Full::default());
|
||||
*response.status_mut() = status;
|
||||
|
||||
if let Some(body) = body {
|
||||
*response.body_mut() = Full::from(Bytes::from(serde_json::to_string(&body).unwrap_or_else(|_| "{}".to_string())));
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
impl CustomClientCertVerifier {
|
||||
fn try_new(cert: &str) -> anyhow::Result<Self> {
|
||||
// We add the certificate of the server itself just so that no "empty" error is returned.
|
||||
// We don't care about the authority of the certificate, just that it is valid.
|
||||
let mut root_cert_store = RootCertStore::empty();
|
||||
root_cert_store.add(PemObject::from_pem_slice(cert.as_bytes())?)?;
|
||||
async fn handle_request(mut req: Request<Incoming>) -> Result<Response<Full<Bytes>>, hyper::Error> {
|
||||
let Some(state) = req.extensions_mut().remove::<AppState>() else {
|
||||
return build_response(StatusCode::INTERNAL_SERVER_ERROR, None);
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
inner: WebPkiClientVerifier::builder(Arc::new(root_cert_store)).build()?,
|
||||
})
|
||||
let Some(client_info) = req.extensions_mut().remove::<ClientInfo>() else {
|
||||
return build_response(StatusCode::INTERNAL_SERVER_ERROR, None);
|
||||
};
|
||||
|
||||
match (req.method(), req.uri().path()) {
|
||||
(&Method::POST, "/api/localsend/v3/nonce") => {
|
||||
nonce_exchange(req.into_body(), state, client_info).await
|
||||
}
|
||||
_ => {
|
||||
let mut res = Response::new(Full::default());
|
||||
*res.status_mut() = StatusCode::NOT_FOUND;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for CustomClientCertVerifier {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientCertVerifier for CustomClientCertVerifier {
|
||||
fn offer_client_auth(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn client_auth_mandatory(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn root_hint_subjects(&self) -> &[DistinguishedName] {
|
||||
self.inner.root_hint_subjects()
|
||||
}
|
||||
|
||||
fn verify_client_cert(
|
||||
&self,
|
||||
cert: &CertificateDer<'_>,
|
||||
_: &[CertificateDer<'_>],
|
||||
_: UnixTime,
|
||||
) -> Result<ClientCertVerified, Error> {
|
||||
// We trust any certificate that is valid.
|
||||
crate::crypto::cert::verify_cert_from_der(cert.as_bytes(), None).map_err(|e| {
|
||||
tracing::warn!("Client certificate verification failed: {e:#}");
|
||||
Error::InvalidCertificate(rustls::CertificateError::ApplicationVerificationFailure)
|
||||
})?;
|
||||
Ok(ClientCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, Error> {
|
||||
self.inner.verify_tls12_signature(message, cert, dss)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, Error> {
|
||||
self.inner.verify_tls13_signature(message, cert, dss)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
self.inner.supported_verify_schemes()
|
||||
}
|
||||
async fn nonce_exchange(
|
||||
body: Incoming,
|
||||
state: AppState,
|
||||
client_info: ClientInfo,
|
||||
) -> Result<Response<Full<Bytes>>, hyper::Error> {
|
||||
let bytes = body.collect().await?.to_bytes();
|
||||
let request = match serde_json::from_slice::<NonceRequest>(&bytes) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to parse JSON body: {err:#}");
|
||||
return build_response(StatusCode::BAD_REQUEST, Some(serde_json::json!({
|
||||
"message": "Invalid JSON body"
|
||||
})));
|
||||
}
|
||||
};
|
||||
Ok(Response::new(Full::from(
|
||||
"Nonce exchange successful".to_string(),
|
||||
)))
|
||||
}
|
||||
+6
-5
@@ -28,7 +28,7 @@ async fn main() -> Result<()> {
|
||||
.with_max_level(Level::DEBUG)
|
||||
.init();
|
||||
|
||||
webrtc_test().await?;
|
||||
server_test().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -117,10 +117,11 @@ MCowBQYDK2VwAyEAZmdXP230oqK92o65ra3XaF2F8r3+fK5DEBK4c40qVts=
|
||||
async fn server_test() -> Result<()> {
|
||||
let server = http::server::LsHttpServer::start_with_port(
|
||||
53317,
|
||||
Some(TlsConfig {
|
||||
cert: CERT.to_string(),
|
||||
private_key: PRIVATE_KEY.to_string(),
|
||||
}),
|
||||
// Some(TlsConfig {
|
||||
// cert: CERT.to_string(),
|
||||
// private_key: PRIVATE_KEY.to_string(),
|
||||
// }),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(u64::MAX)).await;
|
||||
|
||||
Reference in New Issue
Block a user