From e40fa2bdee5f16d9cff0fcf9b25411ea43afa2f0 Mon Sep 17 00:00:00 2001 From: MeexReay Date: Mon, 28 Jul 2025 18:59:05 +0300 Subject: [PATCH] write initial server implementation + some client refactor --- Cargo.lock | 93 ++++++++++++++++ Cargo.toml | 1 + src/client.rs | 26 +++-- src/server.rs | 296 +++++++++----------------------------------------- 4 files changed, 163 insertions(+), 253 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 85e8fac..614a11e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,6 +346,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "dunce" version = "1.0.5" @@ -615,6 +624,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "object" version = "0.36.7" @@ -642,12 +657,28 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -683,6 +714,7 @@ dependencies = [ "bcrypt", "clap", "quinn", + "rcgen", "rustls", "tokio", ] @@ -788,6 +820,19 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rcgen" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0068c5b3cab1d4e271e0bb6539c87563c43411cad90b057b15c79958fbeb41f7" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "regex" version = "1.11.1" @@ -989,6 +1034,26 @@ dependencies = [ "libc", ] +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1090,6 +1155,25 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + [[package]] name = "tinyvec" version = "1.9.0" @@ -1481,6 +1565,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "zerocopy" version = "0.8.26" diff --git a/Cargo.toml b/Cargo.toml index d6e72de..a2c8139 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,6 @@ edition = "2024" bcrypt = "0.17.0" clap = { version = "4.5.41", features = ["derive"] } quinn = { version = "0.11.8", features = ["rustls"] } +rcgen = "0.14.3" rustls = { version = "0.23.30", features = ["ring"] } tokio = { version = "1.47.0", features = ["rt", "macros", "rt-multi-thread"] } diff --git a/src/client.rs b/src/client.rs index 2bb40be..5a112c2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -61,16 +61,13 @@ impl ServerCertVerifier for NoCertVerify { async fn open_connection( host: SocketAddr, - remote: SocketAddr, - password: &str -) -> Result<(Endpoint, Connection, SendStream, RecvStream), Box> { +) -> Result<(Endpoint, Connection), Box> { let mut client_crypto = rustls::ClientConfig::builder() .with_root_certificates(RootCertStore::empty()) .with_no_client_auth(); let verifier = Arc::new(NoCertVerify); client_crypto.dangerous().set_certificate_verifier(verifier); - client_crypto.alpn_protocols = vec![b"hq-29".into()]; let client_config = ClientConfig::new(Arc::new(QuicClientConfig::try_from(Arc::new(client_crypto))?)); @@ -81,6 +78,14 @@ async fn open_connection( .connect(host, &host.ip().to_string())? .await?; + Ok((endpoint, conn)) +} + +async fn open_request( + conn: &mut Connection, + remote: SocketAddr, + password: &str +) -> Result<(SendStream, RecvStream), Box> { let (mut send, recv) = conn .open_bi() .await?; @@ -92,17 +97,22 @@ async fn open_connection( ); send.write_all(request.as_bytes()).await?; - Ok((endpoint, conn, send, recv)) + Ok((send, recv)) } -async fn close_connection( - endpoint: Endpoint, - conn: Connection, +async fn close_request( mut send: SendStream, mut recv: RecvStream ) -> Result<(), Box> { send.finish()?; recv.stop(0u32.into())?; + Ok(()) +} + +async fn close_connection( + endpoint: Endpoint, + conn: Connection, +) -> Result<(), Box> { conn.close(0u32.into(), b"good environment"); endpoint.wait_idle().await; Ok(()) diff --git a/src/server.rs b/src/server.rs index 298562f..212a0c1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,271 +1,77 @@ -//! This example demonstrates an HTTP server that serves files from a directory. -//! -//! Checkout the `README.md` for guidance. +use std::{error::Error, net::SocketAddr, str, sync::Arc}; +use quinn::crypto::rustls::QuicServerConfig; +use rustls::pki_types::PrivatePkcs8KeyDer; -use std::{ - ascii, fs, io, - net::SocketAddr, - path::{self, Path, PathBuf}, - str, - sync::Arc, -}; - -use anyhow::{Context, Result, anyhow, bail}; -use clap::Parser; -use proto::crypto::rustls::QuicServerConfig; -use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; -use tracing::{error, info, info_span}; -use tracing_futures::Instrument as _; - -mod common; - -#[derive(Parser, Debug)] -#[clap(name = "server")] -struct Opt { - /// file to log TLS keys to for debugging - #[clap(long = "keylog")] - keylog: bool, - /// directory to serve files from - root: PathBuf, - /// TLS private key in PEM format - #[clap(short = 'k', long = "key", requires = "cert")] - key: Option, - /// TLS certificate in PEM format - #[clap(short = 'c', long = "cert", requires = "key")] - cert: Option, - /// Enable stateless retries - #[clap(long = "stateless-retry")] - stateless_retry: bool, - /// Address to listen on - #[clap(long = "listen", default_value = "[::1]:4433")] - listen: SocketAddr, - /// Client address to block - #[clap(long = "block")] - block: Option, - /// Maximum number of concurrent connections to allow - #[clap(long = "connection-limit")] - connection_limit: Option, -} - -fn main() { - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .finish(), - ) - .unwrap(); - let opt = Opt::parse(); - let code = { - if let Err(e) = run(opt) { - eprintln!("ERROR: {e}"); - 1 - } else { - 0 - } - }; - ::std::process::exit(code); -} - -#[tokio::main] -async fn run(options: Opt) -> Result<()> { - let (certs, key) = if let (Some(key_path), Some(cert_path)) = (&options.key, &options.cert) { - let key = fs::read(key_path).context("failed to read private key")?; - let key = if key_path.extension().is_some_and(|x| x == "der") { - PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)) - } else { - rustls_pemfile::private_key(&mut &*key) - .context("malformed PKCS #1 private key")? - .ok_or_else(|| anyhow::Error::msg("no private keys found"))? - }; - let cert_chain = fs::read(cert_path).context("failed to read certificate chain")?; - let cert_chain = if cert_path.extension().is_some_and(|x| x == "der") { - vec![CertificateDer::from(cert_chain)] - } else { - rustls_pemfile::certs(&mut &*cert_chain) - .collect::>() - .context("invalid PEM-encoded certificate")? - }; - - (cert_chain, key) - } else { - let dirs = directories_next::ProjectDirs::from("org", "quinn", "quinn-examples").unwrap(); - let path = dirs.data_local_dir(); - let cert_path = path.join("cert.der"); - let key_path = path.join("key.der"); - let (cert, key) = match fs::read(&cert_path).and_then(|x| Ok((x, fs::read(&key_path)?))) { - Ok((cert, key)) => ( - CertificateDer::from(cert), - PrivateKeyDer::try_from(key).map_err(anyhow::Error::msg)?, - ), - Err(ref e) if e.kind() == io::ErrorKind::NotFound => { - info!("generating self-signed certificate"); - let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); - let key = PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()); - let cert = cert.cert.into(); - fs::create_dir_all(path).context("failed to create certificate directory")?; - fs::write(&cert_path, &cert).context("failed to write certificate")?; - fs::write(&key_path, key.secret_pkcs8_der()) - .context("failed to write private key")?; - (cert, key.into()) - } - Err(e) => { - bail!("failed to read certificate: {}", e); - } - }; - - (vec![cert], key) - }; +pub async fn run_server(host: SocketAddr, password: &str) -> Result<(), Box> { + let cert = rcgen::generate_simple_self_signed(vec![ + "localhost".into(), + host.ip().to_string().into(), + ]).unwrap(); + let key = PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()).into(); + let certs = vec![cert.cert.into()]; let mut server_crypto = rustls::ServerConfig::builder() .with_no_client_auth() .with_single_cert(certs, key)?; - server_crypto.alpn_protocols = common::ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); - if options.keylog { - server_crypto.key_log = Arc::new(rustls::KeyLogFile::new()); - } - + server_crypto.alpn_protocols = vec![b"hq-29".into()]; + let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(server_crypto)?)); let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); transport_config.max_concurrent_uni_streams(0_u8.into()); - let root = Arc::::from(options.root.clone()); - if !root.exists() { - bail!("root path does not exist"); - } - - let endpoint = quinn::Endpoint::server(server_config, options.listen)?; - eprintln!("listening on {}", endpoint.local_addr()?); + let endpoint = quinn::Endpoint::server(server_config, host)?; while let Some(conn) = endpoint.accept().await { - if options - .connection_limit - .is_some_and(|n| endpoint.open_connections() >= n) - { - info!("refusing due to open connection limit"); - conn.refuse(); - } else if Some(conn.remote_address()) == options.block { - info!("refusing blocked client IP address"); - conn.refuse(); - } else if options.stateless_retry && !conn.remote_address_validated() { - info!("requiring connection to validate its address"); - conn.retry().unwrap(); - } else { - info!("accepting connection"); - let fut = handle_connection(root.clone(), conn); - tokio::spawn(async move { - if let Err(e) = fut.await { - error!("connection failed: {reason}", reason = e.to_string()) - } - }); - } + let fut = handle_connection(conn, password.to_string()); + + tokio::spawn(async move { + if let Err(e) = fut.await { + eprintln!("connection failed: {reason}", reason = e.to_string()) + } + }); } Ok(()) } -async fn handle_connection(root: Arc, conn: quinn::Incoming) -> Result<()> { +async fn handle_connection(conn: quinn::Incoming, password: String) -> Result<(), Box> { let connection = conn.await?; - let span = info_span!( - "connection", - remote = %connection.remote_address(), - protocol = %connection - .handshake_data() - .unwrap() - .downcast::().unwrap() - .protocol - .map_or_else(|| "".into(), |x| String::from_utf8_lossy(&x).into_owned()) - ); - async { - info!("established"); - // Each stream initiated by the client constitutes a new request. - loop { - let stream = connection.accept_bi().await; - let stream = match stream { - Err(quinn::ConnectionError::ApplicationClosed { .. }) => { - info!("connection closed"); - return Ok(()); + loop { + let stream = connection.accept_bi().await; + let stream = match stream { + Err(quinn::ConnectionError::ApplicationClosed { .. }) => { + eprintln!("connection closed"); + return Ok(()); + } + Err(e) => { + return Err(e.into()); + } + Ok(s) => s, + }; + let fut = handle_request( + stream.0, + stream.1, + password.clone() + ); + + tokio::spawn( + async move { + if let Err(e) = fut.await { + eprintln!("failed: {reason}", reason = e.to_string()); } - Err(e) => { - return Err(e); - } - Ok(s) => s, - }; - let fut = handle_request(root.clone(), stream); - tokio::spawn( - async move { - if let Err(e) = fut.await { - error!("failed: {reason}", reason = e.to_string()); - } - } - .instrument(info_span!("request")), - ); - } + }, + ); } - .instrument(span) - .await?; - Ok(()) } async fn handle_request( - root: Arc, - (mut send, mut recv): (quinn::SendStream, quinn::RecvStream), -) -> Result<()> { - let req = recv - .read_to_end(64 * 1024) - .await - .map_err(|e| anyhow!("failed reading request: {}", e))?; - let mut escaped = String::new(); - for &x in &req[..] { - let part = ascii::escape_default(x).collect::>(); - escaped.push_str(str::from_utf8(&part).unwrap()); - } - info!(content = %escaped); - // Execute the request - let resp = process_get(&root, &req).unwrap_or_else(|e| { - error!("failed: {}", e); - format!("failed to process request: {e}\n").into_bytes() - }); - // Write the response - send.write_all(&resp) - .await - .map_err(|e| anyhow!("failed to send response: {}", e))?; - // Gracefully terminate the stream - send.finish().unwrap(); - info!("complete"); + mut send: quinn::SendStream, + mut recv: quinn::RecvStream, + password: String +) -> Result<(), Box> { + todo!(); + Ok(()) } - -fn process_get(root: &Path, x: &[u8]) -> Result> { - if x.len() < 4 || &x[0..4] != b"GET " { - bail!("missing GET"); - } - if x[4..].len() < 2 || &x[x.len() - 2..] != b"\r\n" { - bail!("missing \\r\\n"); - } - let x = &x[4..x.len() - 2]; - let end = x.iter().position(|&c| c == b' ').unwrap_or(x.len()); - let path = str::from_utf8(&x[..end]).context("path is malformed UTF-8")?; - let path = Path::new(&path); - let mut real_path = PathBuf::from(root); - let mut components = path.components(); - match components.next() { - Some(path::Component::RootDir) => {} - _ => { - bail!("path must be absolute"); - } - } - for c in components { - match c { - path::Component::Normal(x) => { - real_path.push(x); - } - x => { - bail!("illegal component in path: {:?}", x); - } - } - } - let data = fs::read(&real_path).context("failed reading file")?; - Ok(data) -}