feat: extend http server structure

This commit is contained in:
Tien Do Nam
2025-07-12 22:54:12 +02:00
parent d7f73b9585
commit 13ba9b7008
8 changed files with 234 additions and 112 deletions
+27
View File
@@ -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"
+1
View File
@@ -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";
+7
View File
@@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct NonceRequest {
/// The nonce string.
pub nonce: String,
}
+2
View File
@@ -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
View File
@@ -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;