mirror of
https://github.com/n0-computer/noq.git
synced 2026-09-22 11:13:44 +00:00
451 lines
14 KiB
Rust
451 lines
14 KiB
Rust
use std::{
|
|
ascii, cmp,
|
|
ffi::OsStr,
|
|
fs,
|
|
net::{IpAddr, SocketAddr},
|
|
path::PathBuf,
|
|
pin::Pin,
|
|
str, sync,
|
|
task::{Context, Poll},
|
|
};
|
|
|
|
use anyhow::{anyhow, bail, Context as _, Result};
|
|
use bytes::Bytes;
|
|
use futures::{ready, AsyncReadExt, Future, StreamExt, TryFutureExt, future};
|
|
use http::{Response, StatusCode};
|
|
use http_body::Body as _;
|
|
use hyper::{body::HttpBody, service::{make_service_fn, service_fn}};
|
|
use structopt::{self, StructOpt};
|
|
use tokio::net::{TcpListener, TcpStream};
|
|
use tokio_rustls::{server::TlsStream, TlsAcceptor};
|
|
use tracing::{error, info, info_span};
|
|
use tracing_futures::Instrument as _;
|
|
|
|
use quinn::SendStream;
|
|
use quinn_h3::{self, server::RecvRequest, IntoBody};
|
|
use sync::Arc;
|
|
|
|
#[derive(StructOpt, Debug, Clone)]
|
|
#[structopt(name = "h3_server")]
|
|
struct Opt {
|
|
/// TLS private key
|
|
#[structopt(
|
|
parse(from_os_str),
|
|
short = "k",
|
|
long = "key",
|
|
requires = "cert",
|
|
default_value = "key.der"
|
|
)]
|
|
key: PathBuf,
|
|
/// TLS certificate
|
|
#[structopt(
|
|
parse(from_os_str),
|
|
short = "c",
|
|
long = "cert",
|
|
requires = "key",
|
|
default_value = "cert.der"
|
|
)]
|
|
cert: PathBuf,
|
|
/// Address to listen on
|
|
#[structopt(long = "listen", short = "l", default_value = "::")]
|
|
listen: IpAddr,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
tracing::subscriber::set_global_default(
|
|
tracing_subscriber::FmtSubscriber::builder()
|
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
|
.finish(),
|
|
)
|
|
.unwrap();
|
|
let opt = Opt::from_args();
|
|
|
|
let key = fs::read(&opt.key).context("failed to read private key")?;
|
|
let key = match opt.key.as_path().extension().and_then(OsStr::to_str) {
|
|
Some("der") => quinn::PrivateKey::from_der(&key[..])?,
|
|
_ => quinn::PrivateKey::from_pem(&key[..])?,
|
|
};
|
|
|
|
let cert_chain = fs::read(&opt.cert).context("failed to read certificate chain")?;
|
|
let cert_chain = match opt.cert.as_path().extension().and_then(OsStr::to_str) {
|
|
Some("der") => {
|
|
quinn::CertificateChain::from_certs(vec![quinn::Certificate::from_der(&cert_chain)?])
|
|
}
|
|
_ => quinn::CertificateChain::from_pem(&cert_chain)?,
|
|
};
|
|
|
|
let mut server_config = quinn::ServerConfigBuilder::default();
|
|
server_config.certificate(cert_chain, key)?;
|
|
server_config.protocols(&[quinn_h3::ALPN, b"hq-27"]);
|
|
|
|
let main = server(server_config.clone(), SocketAddr::new(opt.listen, 4433));
|
|
let default = server(server_config.clone(), SocketAddr::new(opt.listen, 443));
|
|
server_config.use_stateless_retry(true);
|
|
let retry = server(server_config.clone(), SocketAddr::new(opt.listen, 4434));
|
|
|
|
tokio::try_join!(main, default, retry, h2_server(server_config.clone()))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn server(server_config: quinn::ServerConfigBuilder, addr: SocketAddr) -> Result<()> {
|
|
let mut transport = quinn::TransportConfig::default();
|
|
transport.initial_window(1024 * 1024);
|
|
transport.send_window(1024 * 1024 * 3);
|
|
transport.receive_window(1024 * 1024);
|
|
let mut server_config = server_config.build();
|
|
server_config.transport = Arc::new(transport);
|
|
|
|
let mut endpoint_builder = quinn::Endpoint::builder();
|
|
endpoint_builder.listen(server_config);
|
|
let (_, mut incoming) = endpoint_builder.bind(&addr)?;
|
|
|
|
println!("server listening on {}", addr);
|
|
while let Some(connecting) = incoming.next().await {
|
|
tokio::spawn(async move {
|
|
let protos = connecting.authentication_data().protocol.unwrap();
|
|
println!("server received connection");
|
|
|
|
if protos == b"h3-27" {
|
|
if let Err(e) = h3_handle_connection(connecting).await {
|
|
error!("handling connection failed: {:?}", e)
|
|
}
|
|
} else if protos == b"hq-27" {
|
|
if let Err(e) = hq_handle_connection(connecting).await {
|
|
error!("handling connection failed: {:?}", e)
|
|
}
|
|
}
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn h3_handle_connection(connecting: quinn::Connecting) -> Result<()> {
|
|
let connecting = quinn_h3::server::Connecting::from(connecting);
|
|
let mut incoming = connecting.await.context("accept failed")?;
|
|
tokio::spawn(async move {
|
|
while let Some(request) = incoming.next().await {
|
|
tokio::spawn(async move {
|
|
if let Err(e) = h3_handle_request(request).await {
|
|
eprintln!("request error: {}", e)
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn h3_handle_request(recv_request: RecvRequest) -> Result<()> {
|
|
let (mut request, sender) = recv_request.await?;
|
|
println!("received request: {:?}", request);
|
|
|
|
let body = request.body_mut().read_to_end().await?;
|
|
println!("received body: {}", String::from_utf8_lossy(&body));
|
|
|
|
if let Some(trailers) = request.body_mut().trailers().await? {
|
|
println!("received trailers: {:?}", trailers);
|
|
}
|
|
|
|
match request.uri().path() {
|
|
"/" => h3_home(sender).await?,
|
|
path => match parse_size(path) {
|
|
Ok(n) => h3_payload(sender, n).await?,
|
|
Err(_) => h3_home(sender).await?,
|
|
},
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn h3_home(sender: quinn_h3::server::Sender) -> Result<()> {
|
|
let response = Response::builder()
|
|
.status(StatusCode::OK)
|
|
.header("server", VERSION)
|
|
.body(HOME.into_body())
|
|
.expect("failed to build response");
|
|
sender
|
|
.send_response(response)
|
|
.await
|
|
.map_err(|e| anyhow!("failed to send response: {:?}", e))?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn h3_payload(sender: quinn_h3::server::Sender, len: usize) -> Result<()> {
|
|
if len > 1_000_000_000 {
|
|
let response = Response::builder()
|
|
.status(StatusCode::BAD_REQUEST)
|
|
.header("server", VERSION)
|
|
.body(format!("requested {}: too large", len).into_body())
|
|
.expect("failed to build response");
|
|
sender.send_response(response).await?;
|
|
return Ok(());
|
|
}
|
|
|
|
let mut buf = TEXT.repeat(len / TEXT.len() + 1);
|
|
buf.truncate(len);
|
|
|
|
let response = Response::builder()
|
|
.status(StatusCode::OK)
|
|
.header("server", VERSION)
|
|
.body(buf.into_body())
|
|
.expect("failed to build response");
|
|
|
|
sender
|
|
.send_response(response)
|
|
.await
|
|
.map_err(|e| anyhow!("failed to send response: {:?}", e))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn hq_handle_connection(conn: quinn::Connecting) -> Result<()> {
|
|
let quinn::NewConnection {
|
|
connection,
|
|
mut bi_streams,
|
|
..
|
|
} = conn.await?;
|
|
let span = info_span!(
|
|
"connection",
|
|
remote = %connection.remote_address(),
|
|
protocol = %connection
|
|
.authentication_data()
|
|
.protocol
|
|
.map_or_else(|| "<none>".into(), |x| String::from_utf8_lossy(&x).into_owned())
|
|
);
|
|
async {
|
|
info!("established");
|
|
|
|
// Each stream initiated by the client constitutes a new request.
|
|
while let Some(stream) = bi_streams.next().await {
|
|
let stream = match stream {
|
|
Err(quinn::ConnectionError::ApplicationClosed { .. }) => {
|
|
info!("connection closed");
|
|
return Ok(());
|
|
}
|
|
Err(e) => {
|
|
return Err(e);
|
|
}
|
|
Ok(s) => s,
|
|
};
|
|
tokio::spawn(
|
|
hq_handle_request(stream)
|
|
.unwrap_or_else(move |e| error!("failed: {reason}", reason = e.to_string()))
|
|
.instrument(info_span!("request")),
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
.instrument(span)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn hq_handle_request((send, 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::<Vec<_>>();
|
|
escaped.push_str(str::from_utf8(&part).unwrap());
|
|
}
|
|
info!(content = %escaped);
|
|
// Execute the request
|
|
hq_process_get(send, &req).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn hq_process_get(mut send: SendStream, 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_else(|| x.len());
|
|
let path = str::from_utf8(&x[..end]).context("path is malformed UTF-8")?;
|
|
|
|
// Write the response
|
|
match parse_size(path) {
|
|
Ok(n) if n <= 1_000_000_000 => {
|
|
let mut remaining = n;
|
|
while remaining > 0 {
|
|
let size = cmp::min(remaining, TEXT.len());
|
|
send.write_all(&TEXT[..size])
|
|
.await
|
|
.map_err(|e| anyhow!("failed to send response: {}", e))?;
|
|
remaining -= size;
|
|
}
|
|
}
|
|
Ok(_) | Err(_) => {
|
|
send.write_all(&HOME.as_bytes())
|
|
.await
|
|
.map_err(|e| anyhow!("failed to send response: {}", e))?;
|
|
}
|
|
}
|
|
|
|
// Gracefully terminate the stream
|
|
send.finish()
|
|
.await
|
|
.map_err(|e| anyhow!("failed to shutdown stream: {}", e))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_size(literal: &str) -> Result<usize> {
|
|
if literal.is_empty() {
|
|
return Err(anyhow!("path empty"));
|
|
}
|
|
let pos = literal[1..]
|
|
.find(|c: char| !c.is_ascii_digit())
|
|
.map(|p| p + 1)
|
|
.unwrap_or_else(|| literal.len());
|
|
let num: usize = literal[1..pos]
|
|
.parse()
|
|
.map_err(|_| anyhow!("parse failed"))?;
|
|
let scale = match literal[pos..].to_uppercase().as_str() {
|
|
"K" => 1000,
|
|
"M" => 1_000_000,
|
|
"G" => 1_000_000_000,
|
|
_ => 1,
|
|
};
|
|
Ok(num * scale)
|
|
}
|
|
|
|
const ALT_SVC: &str = "h3-27=\":443\"";
|
|
|
|
fn h2_home() -> hyper::Response<hyper::Body> {
|
|
Response::builder()
|
|
.status(StatusCode::OK)
|
|
.header("Alt-Svc", ALT_SVC)
|
|
.body(HOME.into())
|
|
.expect("failed to build response")
|
|
}
|
|
|
|
fn h2_payload(len: usize) -> hyper::Response<hyper::Body> {
|
|
if len > 1_000_000_000 {
|
|
let response = Response::builder()
|
|
.status(StatusCode::BAD_REQUEST)
|
|
.header("Alt-Svc", ALT_SVC)
|
|
.body(Bytes::from(format!("requested {}: too large", len)).into())
|
|
.expect("failed to build response");
|
|
return response;
|
|
}
|
|
|
|
let mut buf = TEXT.repeat(len / TEXT.len() + 1);
|
|
buf.truncate(len);
|
|
Response::builder()
|
|
.status(StatusCode::OK)
|
|
.body(buf.into())
|
|
.expect("failed to build response")
|
|
}
|
|
|
|
async fn h2_handle(request: hyper::Request<hyper::Body>) -> Result<hyper::Response<hyper::Body>> {
|
|
Ok(match request.uri().path() {
|
|
"/" => h2_home(),
|
|
path => match parse_size(path) {
|
|
Ok(n) => h2_payload(n),
|
|
Err(_) => h2_home(),
|
|
},
|
|
})
|
|
}
|
|
|
|
async fn h2_server(server_config: quinn::ServerConfigBuilder) -> Result<()> {
|
|
let mut tls_cfg = (*server_config.build().crypto).clone();
|
|
tls_cfg.set_protocols(&[b"h2".to_vec(), b"http/1.1".to_vec()]);
|
|
let tls_acceptor = TlsAcceptor::from(sync::Arc::new(tls_cfg));
|
|
|
|
let tcp = TcpListener::bind(&SocketAddr::new([0, 0, 0, 0].into(), 443)).await?;
|
|
|
|
let service = make_service_fn(|_conn| async { Ok::<_, anyhow::Error>(service_fn(h2_handle)) });
|
|
let server = hyper::Server::builder(HyperAcceptor::new(tcp, tls_acceptor)).serve(service);
|
|
|
|
if let Err(e) = server.await {
|
|
error!("server error: {}", e);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
struct HyperAcceptor {
|
|
tcp: TcpListener,
|
|
tls: TlsAcceptor,
|
|
handshake: Option<tokio_rustls::Accept<TcpStream>>,
|
|
}
|
|
|
|
impl HyperAcceptor {
|
|
pub fn new(tcp: TcpListener, tls: TlsAcceptor) -> Self {
|
|
Self {
|
|
tls,
|
|
tcp,
|
|
handshake: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl hyper::server::accept::Accept for HyperAcceptor {
|
|
type Conn = TlsStream<TcpStream>;
|
|
type Error = anyhow::Error;
|
|
|
|
fn poll_accept(
|
|
mut self: Pin<&mut Self>,
|
|
cx: &mut Context,
|
|
) -> Poll<Option<Result<Self::Conn, Self::Error>>> {
|
|
loop {
|
|
match self.handshake {
|
|
Some(ref mut h) => {
|
|
let conn = ready!(Pin::new(h).poll(cx))?;
|
|
std::mem::replace(&mut self.handshake, None);
|
|
return Poll::Ready(Some(Ok(conn)));
|
|
}
|
|
None => {
|
|
let (stream, _) = ready!(self.tcp.poll_accept(cx))?;
|
|
self.handshake = Some(self.tls.accept(stream));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const TEXT: &[u8] =
|
|
b"It would be different if we could not step back and reflect on the process,\n\
|
|
but were merely led from impulse to impulse without self- consciousness. But human\n\
|
|
beings do not act solely on impulse. They are prudent, they reflect, they weigh\n\
|
|
consequences, they ask whether what they are doing is worth while. Not only are their\n\
|
|
lives full of particular choices that hang together in larger activities with temporal\n\
|
|
structure: they also decide in the broadest terms what to pursue and what to avoid, what\n\
|
|
the priorities among their various aims should be, and what kind of people they want to\n\
|
|
be or become. Some men are faced with such choices by the large decisions they make from\n\
|
|
time to time; some merely by reflection on the course their lives are taking as the product\n\
|
|
of countless small decisions. They decide whom to marry, what profession to follow, whether\n\
|
|
to join the Country Club, or the Resistance; or they may just wonder why they go on being\n\
|
|
salesmen or academics or taxi drivers, and then stop thinking about it after a certain period\n\
|
|
of inconclusive reflection.";
|
|
|
|
const HOME: &str = r##"
|
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
<head>
|
|
<title>Quinn H3 interop server</title>
|
|
</head>
|
|
<body>
|
|
<h1>Welcome to the quinn-h3 interop server.</h1>
|
|
<p>
|
|
<strong>Draft version:</strong> draft-24<br/>
|
|
<strong>Available tests:</strong> VHDCRZSBU3
|
|
</p>
|
|
<p>
|
|
Use '/{n}' to get <i>n</i> bytes of deep thoughts.<br/>
|
|
For example <a href="/1000000">/1000000</a>
|
|
to get 1MB. Limit: 1GB
|
|
</p>
|
|
<p>Checkout our project's <a href="https://github.com/djc/quinn">repository</a>.</p>
|
|
<p>Say hi on quickdev slack workspace at `quinn`.</p>
|
|
</body>
|
|
</html>
|
|
"##;
|
|
|
|
const VERSION: &str = "quinn-h3:0.0.1";
|