[Add] TCP server to core MTP (HTTP/1.1 & HTTP/2) compatibility
All checks were successful
CI / checks (push) Successful in 5m29s
All checks were successful
CI / checks (push) Successful in 5m29s
This commit is contained in:
parent
04760fd88d
commit
00f0aaeeff
21 changed files with 1627 additions and 677 deletions
120
example/Cargo.lock
generated
120
example/Cargo.lock
generated
|
|
@ -62,6 +62,12 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
|
|
@ -520,6 +526,12 @@ version = "0.1.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
|
|
@ -666,6 +678,25 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h3"
|
||||
version = "0.0.8"
|
||||
|
|
@ -768,6 +799,41 @@ dependencies = [
|
|||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.13"
|
||||
|
|
@ -778,6 +844,41 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.2.0"
|
||||
|
|
@ -1254,6 +1355,9 @@ dependencies = [
|
|||
"h3-quinn",
|
||||
"h3-webtransport",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"mtp-codec",
|
||||
"mtp-common",
|
||||
"mtp-crypto",
|
||||
|
|
@ -1264,6 +1368,8 @@ dependencies = [
|
|||
"rustls",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
|
@ -1880,11 +1986,10 @@ version = "0.2.0"
|
|||
dependencies = [
|
||||
"base64",
|
||||
"hex",
|
||||
"http",
|
||||
"mtp",
|
||||
"rustls",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
|
|
@ -2221,6 +2326,17 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@ path = "src/main.rs"
|
|||
[dependencies]
|
||||
mtp = { version = "0.2.0", path = "../../", features = ["crypto", "tls", "web-server", "files", "pipes"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-rustls = "0.26"
|
||||
rustls = "0.23"
|
||||
http = "1"
|
||||
serde_json = { version = "1" }
|
||||
hex = "0.4"
|
||||
base64 = "0.22"
|
||||
|
|
|
|||
|
|
@ -133,17 +133,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
Box::new(complete_register),
|
||||
);
|
||||
|
||||
let _https = web_server::spawn_https(
|
||||
std::net::SocketAddr::new(config.ip, config.port),
|
||||
&config.tls_fullchain,
|
||||
&config.tls_key,
|
||||
)
|
||||
.await?;
|
||||
let mut host = mtp::webserver::MTPWebServer::new(config, web_server::config()?).await?;
|
||||
println!(
|
||||
"Server listening on https://{} (HTTPS + UDP WebTransport)",
|
||||
host.local_addr()
|
||||
);
|
||||
println!("Server listening on https://{}", host.local_addr());
|
||||
println!("TCP: HTTP/1.1 and HTTP/2");
|
||||
println!("UDP: HTTP/3 and WebTransport");
|
||||
|
||||
while let Some(conn) = host.accept().await? {
|
||||
let decrypt_keyring = Arc::clone(&decrypt_keyring);
|
||||
|
|
|
|||
|
|
@ -1,98 +1,47 @@
|
|||
use mtp::webserver::{Http3Request, Http3Response, RouteParams, WebServerConfig};
|
||||
use rustls::pki_types::{PrivateKeyDer, pem::PemObject};
|
||||
use mtp::webserver::{HttpRequest, HttpResponse, RouteParams, WebServerConfig};
|
||||
use std::{
|
||||
io,
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
path::{Component, Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
|
||||
net::TcpListener,
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
|
||||
async fn ok(request: Http3Request, response: Http3Response) -> Http3Response {
|
||||
async fn health(request: HttpRequest, response: HttpResponse) -> HttpResponse {
|
||||
response
|
||||
.header("content-type", "text/plain; charset=utf-8")
|
||||
.body(format!("OK\nclient: {}\n", request.remote_addr))
|
||||
}
|
||||
|
||||
async fn profile(
|
||||
request: Http3Request,
|
||||
response: Http3Response,
|
||||
request: HttpRequest,
|
||||
response: HttpResponse,
|
||||
params: RouteParams,
|
||||
) -> Http3Response {
|
||||
) -> HttpResponse {
|
||||
let Some(user) = params.get("user") else {
|
||||
return response.body("missing user");
|
||||
};
|
||||
|
||||
let body = serde_json::json!({
|
||||
"user": user,
|
||||
"remote_addr": request.remote_addr.to_string(),
|
||||
"profile": {
|
||||
"display_name": format!("Example user {user}"),
|
||||
"status": "active"
|
||||
}
|
||||
"profile": { "display_name": format!("Example user {user}"), "status": "active" }
|
||||
});
|
||||
|
||||
response
|
||||
.header("content-type", "application/json; charset=utf-8")
|
||||
.body(body.to_string())
|
||||
}
|
||||
|
||||
pub fn config() -> Result<WebServerConfig, mtp::webserver::RouterError> {
|
||||
WebServerConfig::new()
|
||||
.route("/", ok)?
|
||||
.route_pattern("/api/get/{user}/profile", profile)
|
||||
}
|
||||
|
||||
/// Starts the conventional HTTPS side of the example host. WebTransport uses
|
||||
/// UDP/QUIC on the same port; browsers still need TCP/TLS to navigate to a URL.
|
||||
pub async fn spawn_https(
|
||||
address: SocketAddr,
|
||||
certificate_pem: &[u8],
|
||||
key_pem: &[u8],
|
||||
) -> io::Result<JoinHandle<()>> {
|
||||
// The TCP listener is created before the QUIC endpoint, so it must select
|
||||
// rustls' process-wide provider itself.
|
||||
mtp::crypto::ensure_crypto_provider();
|
||||
let certificates = rustls::pki_types::CertificateDer::pem_slice_iter(certificate_pem)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(io::Error::other)?;
|
||||
let key = PrivateKeyDer::from_pem_slice(key_pem).map_err(io::Error::other)?;
|
||||
let tls = rustls::ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certificates, key)
|
||||
.map_err(io::Error::other)?;
|
||||
let listener = TcpListener::bind(address).await?;
|
||||
let acceptor = TlsAcceptor::from(Arc::new(tls));
|
||||
let asset_root = web_client_dist();
|
||||
match &asset_root {
|
||||
Some(_) => println!(
|
||||
"HTTPS web client available at https://localhost:{}",
|
||||
address.port()
|
||||
),
|
||||
None => eprintln!(
|
||||
"Web client build not found; HTTPS will show setup instructions. Run `pnpm --dir example/web-client build`."
|
||||
),
|
||||
let root = Arc::new(web_client_dist());
|
||||
if root.is_none() {
|
||||
eprintln!(
|
||||
"Web client build not found; requests will show setup instructions. Run `pnpm --dir example/web-client build`."
|
||||
);
|
||||
}
|
||||
Ok(tokio::spawn(async move {
|
||||
loop {
|
||||
let Ok((stream, _)) = listener.accept().await else {
|
||||
break;
|
||||
};
|
||||
let acceptor = acceptor.clone();
|
||||
let asset_root = asset_root.clone();
|
||||
tokio::spawn(async move {
|
||||
let Ok(mut stream) = acceptor.accept(stream).await else {
|
||||
return;
|
||||
};
|
||||
let _ = serve_https_request(&mut stream, &asset_root).await;
|
||||
});
|
||||
}
|
||||
}))
|
||||
WebServerConfig::new()
|
||||
.route("/health", health)?
|
||||
.route_pattern("/api/get/{user}/profile", profile)?
|
||||
.fallback(move |request, response| {
|
||||
let root = Arc::clone(&root);
|
||||
async move { static_assets(request, response, root).await }
|
||||
})
|
||||
}
|
||||
|
||||
fn web_client_dist() -> Option<PathBuf> {
|
||||
|
|
@ -104,60 +53,78 @@ fn web_client_dist() -> Option<PathBuf> {
|
|||
.find(|path| path.join("index.html").is_file())
|
||||
}
|
||||
|
||||
async fn serve_https_request<S>(stream: &mut S, asset_root: &Option<PathBuf>) -> io::Result<()>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let mut request = [0; 16 * 1024];
|
||||
let size = stream.read(&mut request).await?;
|
||||
let request = std::str::from_utf8(&request[..size]).unwrap_or_default();
|
||||
let path = request
|
||||
.lines()
|
||||
.next()
|
||||
.and_then(|line| line.split_whitespace().nth(1))
|
||||
.unwrap_or("/")
|
||||
.split('?')
|
||||
.next()
|
||||
.unwrap_or("/");
|
||||
let (status, content_type, body) = match asset_root {
|
||||
Some(asset_root) => {
|
||||
let relative = path.trim_start_matches('/');
|
||||
let candidate = asset_root.join(relative);
|
||||
let file = if relative.is_empty() || !candidate.is_file() || relative.contains("..") {
|
||||
asset_root.join("index.html")
|
||||
async fn static_assets(
|
||||
request: HttpRequest,
|
||||
response: HttpResponse,
|
||||
root: Arc<Option<PathBuf>>,
|
||||
) -> HttpResponse {
|
||||
if request.method != http::Method::GET && request.method != http::Method::HEAD {
|
||||
return response.status(http::StatusCode::METHOD_NOT_ALLOWED);
|
||||
}
|
||||
let Some(root) = root.as_ref() else {
|
||||
return response
|
||||
.status(http::StatusCode::SERVICE_UNAVAILABLE)
|
||||
.header("content-type", "text/html; charset=utf-8")
|
||||
.body("<!doctype html><title>MTP web client not built</title><p>Run <code>pnpm --dir example/web-client build</code>.</p>");
|
||||
};
|
||||
let relative = request.uri.path().trim_start_matches('/');
|
||||
let path = Path::new(relative);
|
||||
if relative.contains('\\')
|
||||
|| path.components().any(|part| {
|
||||
matches!(
|
||||
part,
|
||||
Component::ParentDir | Component::RootDir | Component::Prefix(_)
|
||||
)
|
||||
})
|
||||
{
|
||||
return response
|
||||
.status(http::StatusCode::BAD_REQUEST)
|
||||
.body("Invalid path");
|
||||
}
|
||||
let requested = if relative.is_empty() {
|
||||
root.join("index.html")
|
||||
} else {
|
||||
root.join(path)
|
||||
};
|
||||
let file = if requested.is_file() {
|
||||
requested
|
||||
} else if path.extension().is_none() {
|
||||
root.join("index.html")
|
||||
} else {
|
||||
return response
|
||||
.status(http::StatusCode::NOT_FOUND)
|
||||
.body("Not found");
|
||||
};
|
||||
match tokio::fs::read(&file).await {
|
||||
Ok(body) => {
|
||||
let response = response.header("content-type", content_type(&file));
|
||||
if request.method == http::Method::HEAD {
|
||||
response.header("content-length", &body.len().to_string())
|
||||
} else {
|
||||
candidate
|
||||
};
|
||||
let content_type = content_type(&file);
|
||||
match tokio::fs::read(&file).await {
|
||||
Ok(body) => ("200 OK", content_type, body),
|
||||
Err(_) => ("404 Not Found", "text/plain; charset=utf-8", b"Not found".to_vec()),
|
||||
response.body(body)
|
||||
}
|
||||
}
|
||||
None => (
|
||||
"503 Service Unavailable",
|
||||
"text/html; charset=utf-8",
|
||||
b"<!doctype html><title>MTP web client not built</title><p>Run <code>pnpm --dir example/web-client build</code>.</p>".to_vec(),
|
||||
),
|
||||
};
|
||||
|
||||
let response = format!(
|
||||
"HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
body.len()
|
||||
);
|
||||
stream.write_all(response.as_bytes()).await?;
|
||||
stream.write_all(&body).await?;
|
||||
stream.shutdown().await
|
||||
Err(_) => response
|
||||
.status(http::StatusCode::NOT_FOUND)
|
||||
.body("Not found"),
|
||||
}
|
||||
}
|
||||
|
||||
fn content_type(file: &Path) -> &'static str {
|
||||
match file.extension().and_then(|extension| extension.to_str()) {
|
||||
Some("html") => "text/html; charset=utf-8",
|
||||
Some("js") => "text/javascript; charset=utf-8",
|
||||
Some("js" | "mjs") => "text/javascript; charset=utf-8",
|
||||
Some("css") => "text/css; charset=utf-8",
|
||||
Some("wasm") => "application/wasm",
|
||||
Some("svg") => "image/svg+xml",
|
||||
Some("json") => "application/json",
|
||||
Some("json" | "map") => "application/json",
|
||||
Some("png") => "image/png",
|
||||
Some("jpg" | "jpeg") => "image/jpeg",
|
||||
Some("gif") => "image/gif",
|
||||
Some("webp") => "image/webp",
|
||||
Some("ico") => "image/x-icon",
|
||||
Some("woff") => "font/woff",
|
||||
Some("woff2") => "font/woff2",
|
||||
_ => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue