From 67e73752f1a937eb4d402919b57e94ff5eed90b1 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 3 Jan 2024 13:02:28 +0200 Subject: [PATCH 1/7] Move ranges stuff into separate file --- iroh-gateway/src/args.rs | 2 +- iroh-gateway/src/main.rs | 114 ++++++------------------------------- iroh-gateway/src/ranges.rs | 91 +++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 98 deletions(-) create mode 100644 iroh-gateway/src/ranges.rs diff --git a/iroh-gateway/src/args.rs b/iroh-gateway/src/args.rs index 49321b23..870d3298 100644 --- a/iroh-gateway/src/args.rs +++ b/iroh-gateway/src/args.rs @@ -15,7 +15,7 @@ pub struct Args { pub default_node: Option, /// Http or https listen addr. - /// + /// /// Will listen on http if cert_path is not specified, https otherwise. #[clap(long, default_value = "0.0.0.0:8080")] pub addr: String, diff --git a/iroh-gateway/src/main.rs b/iroh-gateway/src/main.rs index f4041cc9..70c0f296 100644 --- a/iroh-gateway/src/main.rs +++ b/iroh-gateway/src/main.rs @@ -12,11 +12,7 @@ use axum_server::tls_rustls::RustlsConfig; use bytes::Bytes; use clap::Parser; use derive_more::Deref; -use headers::{HeaderMapExt, Range}; -use iroh::bytes::{ - store::bao_tree::{ByteNum, ChunkNum}, - BlobFormat, -}; +use iroh::bytes::{store::bao_tree::ByteNum, BlobFormat}; use iroh::{ bytes::{ format::collection::Collection, @@ -31,14 +27,17 @@ use iroh::{ use lru::LruCache; use mime::Mime; use mime_classifier::MimeClassifier; -use range_collections::{range_set::RangeSetRange, RangeSet2}; +use range_collections::RangeSet2; +use ranges::parse_byte_range; use std::{ - ops::Bound, result, sync::{Arc, Mutex}, }; use url::Url; +use crate::ranges::{slice, to_byte_range, to_chunk_range}; +mod ranges; + // Make our own error that wraps `anyhow::Error`. struct AppError(anyhow::Error); @@ -64,88 +63,6 @@ where } } -/// Given a range specified as arbitrary range bounds, normalize it into a range -/// that has inclusive start and exclusive end. -fn normalize_range(start: Bound, end: Bound) -> (Option, Option) { - match (start, end) { - (Bound::Included(start), Bound::Included(end)) => (Some(start), end.checked_add(1)), - (Bound::Included(start), Bound::Excluded(end)) => (Some(start), Some(end)), - (Bound::Included(start), Bound::Unbounded) => (Some(start), None), - (Bound::Excluded(start), Bound::Included(end)) => { - (start.checked_add(1), end.checked_add(1)) - } - (Bound::Excluded(start), Bound::Excluded(end)) => (start.checked_add(1), Some(end)), - (Bound::Excluded(start), Bound::Unbounded) => (start.checked_add(1), None), - (Bound::Unbounded, Bound::Included(end)) => (None, end.checked_add(1)), - (Bound::Unbounded, Bound::Excluded(end)) => (None, Some(end)), - (Bound::Unbounded, Bound::Unbounded) => (None, None), - } -} - -/// Convert a normalized range into a `RangeSet2` that represents the byte range. -fn to_byte_range(start: Option, end: Option) -> RangeSet2 { - match (start, end) { - (Some(start), Some(end)) => RangeSet2::from(start..end), - (Some(start), None) => RangeSet2::from(start..), - (None, Some(end)) => RangeSet2::from(..end), - (None, None) => RangeSet2::all(), - } -} - -/// Convert a normalized range into a `RangeSet2` that represents the chunk range. -/// -/// Ranges are rounded up so that the given byte range is completely covered by the chunk range. -fn to_chunk_range(start: Option, end: Option) -> RangeSet2 { - let start = start.map(ByteNum); - let end = end.map(ByteNum); - match (start, end) { - (Some(start), Some(end)) => RangeSet2::from(start.full_chunks()..end.chunks()), - (Some(start), None) => RangeSet2::from(start.full_chunks()..), - (None, Some(end)) => RangeSet2::from(..end.chunks()), - (None, None) => RangeSet2::all(), - } -} - -/// Given an incoming piece of data at an offset, and a set of ranges that are being requested, -/// split the data into parts that cover only requested ranges -fn slice(offset: u64, data: Bytes, ranges: RangeSet2) -> Vec { - let len = data.len() as u64; - let data_range = to_byte_range(Some(offset), offset.checked_add(len)); - let relevant = ranges & data_range; - relevant - .iter() - .map(|range| match range { - RangeSetRange::Range(range) => { - let start = (range.start - offset) as usize; - let end = (range.end - offset) as usize; - data.slice(start..end) - } - RangeSetRange::RangeFrom(range) => { - let start = (range.start - offset) as usize; - data.slice(start..) - } - }) - .collect() -} - -/// Parse the byte range from the request headers. -async fn parse_byte_range(req: Request) -> anyhow::Result<(Option, Option)> { - Ok(match req.headers().typed_get::() { - Some(range) => { - println!("got range request {:?}", range); - let ranges = range.satisfiable_ranges(0).collect::>(); - if ranges.len() > 1 { - anyhow::bail!("multiple ranges not supported"); - } - let Some((start, end)) = ranges.into_iter().next() else { - anyhow::bail!("empty range"); - }; - normalize_range(start, end) - } - None => (None, None), - }) -} - #[derive(Debug, Clone)] struct Gateway(Arc); @@ -558,14 +475,17 @@ async fn main() -> anyhow::Result<()> { let default_node = args .default_node .map(|default_node| { - Ok(if let Ok(node_ticket) = default_node.parse::() { - node_ticket.node_addr().clone() - } else if let Ok(blob_ticket) = default_node.parse::() { - blob_ticket.node_addr().clone() - } else { - anyhow::bail!("invalid default node"); - }) - }).transpose()?; + Ok( + if let Ok(node_ticket) = default_node.parse::() { + node_ticket.node_addr().clone() + } else if let Ok(blob_ticket) = default_node.parse::() { + blob_ticket.node_addr().clone() + } else { + anyhow::bail!("invalid default node"); + }, + ) + }) + .transpose()?; let gateway = Gateway(Arc::new(Inner { endpoint, default_node, diff --git a/iroh-gateway/src/ranges.rs b/iroh-gateway/src/ranges.rs new file mode 100644 index 00000000..be2047ce --- /dev/null +++ b/iroh-gateway/src/ranges.rs @@ -0,0 +1,91 @@ +//! Utilities related to HTTP range requests. +use std::ops::Bound; + +use axum::body::Body; +use bytes::Bytes; +use headers::{HeaderMapExt, Range}; +use hyper::Request; +use iroh::bytes::store::bao_tree::{ByteNum, ChunkNum}; +use range_collections::{range_set::RangeSetRange, RangeSet2}; + +/// Given a range specified as arbitrary range bounds, normalize it into a range +/// that has inclusive start and exclusive end. +fn normalize_range(start: Bound, end: Bound) -> (Option, Option) { + match (start, end) { + (Bound::Included(start), Bound::Included(end)) => (Some(start), end.checked_add(1)), + (Bound::Included(start), Bound::Excluded(end)) => (Some(start), Some(end)), + (Bound::Included(start), Bound::Unbounded) => (Some(start), None), + (Bound::Excluded(start), Bound::Included(end)) => { + (start.checked_add(1), end.checked_add(1)) + } + (Bound::Excluded(start), Bound::Excluded(end)) => (start.checked_add(1), Some(end)), + (Bound::Excluded(start), Bound::Unbounded) => (start.checked_add(1), None), + (Bound::Unbounded, Bound::Included(end)) => (None, end.checked_add(1)), + (Bound::Unbounded, Bound::Excluded(end)) => (None, Some(end)), + (Bound::Unbounded, Bound::Unbounded) => (None, None), + } +} + +/// Convert a normalized range into a `RangeSet2` that represents the byte range. +pub fn to_byte_range(start: Option, end: Option) -> RangeSet2 { + match (start, end) { + (Some(start), Some(end)) => RangeSet2::from(start..end), + (Some(start), None) => RangeSet2::from(start..), + (None, Some(end)) => RangeSet2::from(..end), + (None, None) => RangeSet2::all(), + } +} + +/// Convert a normalized range into a `RangeSet2` that represents the chunk range. +/// +/// Ranges are rounded up so that the given byte range is completely covered by the chunk range. +pub fn to_chunk_range(start: Option, end: Option) -> RangeSet2 { + let start = start.map(ByteNum); + let end = end.map(ByteNum); + match (start, end) { + (Some(start), Some(end)) => RangeSet2::from(start.full_chunks()..end.chunks()), + (Some(start), None) => RangeSet2::from(start.full_chunks()..), + (None, Some(end)) => RangeSet2::from(..end.chunks()), + (None, None) => RangeSet2::all(), + } +} + +/// Given an incoming piece of data at an offset, and a set of ranges that are being requested, +/// split the data into parts that cover only requested ranges +pub fn slice(offset: u64, data: Bytes, ranges: RangeSet2) -> Vec { + let len = data.len() as u64; + let data_range = to_byte_range(Some(offset), offset.checked_add(len)); + let relevant = ranges & data_range; + relevant + .iter() + .map(|range| match range { + RangeSetRange::Range(range) => { + let start = (range.start - offset) as usize; + let end = (range.end - offset) as usize; + data.slice(start..end) + } + RangeSetRange::RangeFrom(range) => { + let start = (range.start - offset) as usize; + data.slice(start..) + } + }) + .collect() +} + +/// Parse the byte range from the request headers. +pub async fn parse_byte_range(req: Request) -> anyhow::Result<(Option, Option)> { + Ok(match req.headers().typed_get::() { + Some(range) => { + println!("got range request {:?}", range); + let ranges = range.satisfiable_ranges(0).collect::>(); + if ranges.len() > 1 { + anyhow::bail!("multiple ranges not supported"); + } + let Some((start, end)) = ranges.into_iter().next() else { + anyhow::bail!("empty range"); + }; + normalize_range(start, end) + } + None => (None, None), + }) +} From d17c940209eac1369186ccc4b9b15f330f679413 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 3 Jan 2024 17:00:02 +0200 Subject: [PATCH 2/7] WIP trying to hack in letsencrypt support --- iroh-gateway/Cargo.lock | 26 +++++++++++++++- iroh-gateway/Cargo.toml | 3 ++ iroh-gateway/src/args.rs | 39 ++++++++++++++++++----- iroh-gateway/src/main.rs | 67 +++++++++++++++++++++++++++++----------- 4 files changed, 109 insertions(+), 26 deletions(-) diff --git a/iroh-gateway/Cargo.lock b/iroh-gateway/Cargo.lock index 60dfab6d..b1c23c2e 100644 --- a/iroh-gateway/Cargo.lock +++ b/iroh-gateway/Cargo.lock @@ -269,6 +269,26 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-server" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447f28c85900215cc1bea282f32d4a2f22d55c5a300afdfbc661c8d6a632e063" +dependencies = [ + "arc-swap", + "bytes", + "futures-util", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", + "pin-project-lite", + "rustls 0.21.8", + "rustls-pemfile 1.0.3", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "axum-server" version = "0.6.0" @@ -2042,7 +2062,7 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", - "axum-server", + "axum-server 0.6.0", "bytes", "clap", "derive_more", @@ -2059,7 +2079,10 @@ dependencies = [ "range-collections", "rustls 0.22.1", "tokio", + "tokio-rustls-acme", + "tower", "tower-http", + "tower-service", "tracing", "tracing-subscriber", "url", @@ -4507,6 +4530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb6f50b5523d014ba161512c37457acb16fd8218c883c7152e0a67ab763f2d4" dependencies = [ "async-trait", + "axum-server 0.5.1", "base64 0.21.5", "chrono", "futures", diff --git a/iroh-gateway/Cargo.toml b/iroh-gateway/Cargo.toml index 76a23d9c..6a30ddc3 100644 --- a/iroh-gateway/Cargo.toml +++ b/iroh-gateway/Cargo.toml @@ -27,3 +27,6 @@ lru = "0.12.1" tracing-subscriber = "0.3.18" indicatif = "0.17.7" rustls = "0.22.1" +tokio-rustls-acme = { version = "0.2.0", features = ["axum"] } +tower = "0.4.13" +tower-service = "0.3.2" diff --git a/iroh-gateway/src/args.rs b/iroh-gateway/src/args.rs index 870d3298..21219568 100644 --- a/iroh-gateway/src/args.rs +++ b/iroh-gateway/src/args.rs @@ -3,6 +3,18 @@ use std::path::PathBuf; use clap::Parser; +#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)] +pub enum CertMode { + /// No certificates at all, we serve http. + None, + /// Use a self-signed certificate in the cert_path directory. + Manual, + /// Use a letsencrypt certificate, in staging mode. + LetsEncryptStaging, + /// Use a letsencrypt certificate, in production mode. + LetsEncrypt, +} + #[derive(Parser, Debug)] pub struct Args { /// Ticket for the default node. @@ -20,14 +32,27 @@ pub struct Args { #[clap(long, default_value = "0.0.0.0:8080")] pub addr: String, - /// Https certificate path. - /// - /// If this is specified, the server will listen on https. - /// The path should be a directory containing `cert.pem` and `key.pem`. - #[clap(long)] - pub cert_path: Option, - /// Magic port for the node, random if not specified. #[clap(long)] pub magic_port: Option, + + /// Certificate mode, default is none. + #[clap(long, default_value = "None")] + pub cert_mode: CertMode, + + /// Hostnames for letsencrypt. + #[clap(long, required_if_eq_any([("cert_mode", "LetsEncryptStaging"), ("cert_mode", "LetsEncrypt")]))] + pub hostname: Vec, + + /// Contact email for letsencrypt. + #[clap(long, required_if_eq_any([("cert_mode", "LetsEncryptStaging"), ("cert_mode", "LetsEncrypt")]))] + pub contact: Option, + + /// Certificate path. + /// + /// Not needed if cert_mode is None. + /// In manual mode, this is the directory containing the cert.pem and key.pem files. + /// In letsencrypt mode, this is the directory used by the acme acceptor. + #[clap(long, required_if_eq_any([("cert_mode", "LetsEncryptStaging"), ("cert_mode", "LetsEncrypt"), ("cert_mode", "Manual")]))] + pub cert_path: Option, } diff --git a/iroh-gateway/src/main.rs b/iroh-gateway/src/main.rs index 70c0f296..702e2b11 100644 --- a/iroh-gateway/src/main.rs +++ b/iroh-gateway/src/main.rs @@ -1,5 +1,6 @@ mod args; use anyhow::Context; +use args::CertMode; use axum::{ body::Body, extract::Path, @@ -29,11 +30,13 @@ use mime::Mime; use mime_classifier::MimeClassifier; use range_collections::RangeSet2; use ranges::parse_byte_range; +use tokio_rustls_acme::{AcmeConfig, caches::DirCache, axum::AxumAcceptor}; use std::{ result, sync::{Arc, Mutex}, }; use url::Url; +use tower_service::Service; use crate::ranges::{slice, to_byte_range, to_chunk_range}; mod ranges; @@ -504,24 +507,52 @@ async fn main() -> anyhow::Result<()> { .route("/ticket/:ticket/*path", get(handle_ticket_request)) .layer(Extension(gateway)); - if let Some(path) = args.cert_path { - let cert_file = path.join("cert.pem"); - let key_file = path.join("key.pem"); - let config = RustlsConfig::from_pem_file(cert_file, key_file).await?; - // Run our application with hyper - let addr = args.addr; - println!("listening on {}, https", addr); - let addr = addr.parse()?; - axum_server::bind_rustls(addr, config) - .serve(app.into_make_service()) - .await?; - } else { - // Run our application with hyper - let addr = args.addr; - println!("listening on {}, http", addr); - - let listener = tokio::net::TcpListener::bind(addr).await?; - axum::serve(listener, app).await?; + match args.cert_mode { + CertMode::None => { + // Run our application as just http + let addr = args.addr; + println!("listening on {}, http", addr); + + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + } + CertMode::Manual => { + // Run with manual certificates + let cert_path = args + .cert_path + .context("cert_path not specified")? + .canonicalize()?; + let cert_file = cert_path.join("cert.pem"); + let key_file = cert_path.join("key.pem"); + let config = RustlsConfig::from_pem_file(cert_file, key_file).await?; + // Run our application with hyper + let addr = args.addr; + println!("listening on {}, https", addr); + let addr = addr.parse()?; + axum_server::bind_rustls(addr, config) + .serve(app.into_make_service()) + .await?; + } + CertMode::LetsEncryptStaging | CertMode::LetsEncrypt => { + let is_production = args.cert_mode == CertMode::LetsEncrypt; + let hostnames = args.hostname; + let contact = args.contact.context("contact not specified")?; + let dir = args.cert_path.context("cert_path not specified")?; + let state = AcmeConfig::new(hostnames) + .contact([format!("mailto:{contact}")]) + .cache_option(Some(DirCache::new(dir))) + .directory_lets_encrypt(is_production) + .state(); + let config = todo!(); + let acceptor = state.axum_acceptor(config); + // Run our application with hyper + let addr = args.addr.parse()?; + println!("listening on {}, https", addr); + axum_server::bind(addr) + .acceptor(acceptor) + .serve(app.into_make_service()) + .await?; + } } Ok(()) From 629feb24242b44a6746f666b2834736c60bf565a Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Wed, 3 Jan 2024 18:20:24 +0200 Subject: [PATCH 3/7] Remove the actual letsencrypt code path this isn't going to work until tokio-rustls-acme is on the latest rustls --- iroh-gateway/Cargo.lock | 2 -- iroh-gateway/Cargo.toml | 4 +--- iroh-gateway/src/args.rs | 2 +- iroh-gateway/src/main.rs | 28 +++++++++++++++------------- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/iroh-gateway/Cargo.lock b/iroh-gateway/Cargo.lock index b1c23c2e..0b18abf5 100644 --- a/iroh-gateway/Cargo.lock +++ b/iroh-gateway/Cargo.lock @@ -2080,9 +2080,7 @@ dependencies = [ "rustls 0.22.1", "tokio", "tokio-rustls-acme", - "tower", "tower-http", - "tower-service", "tracing", "tracing-subscriber", "url", diff --git a/iroh-gateway/Cargo.toml b/iroh-gateway/Cargo.toml index 6a30ddc3..d5cdb362 100644 --- a/iroh-gateway/Cargo.toml +++ b/iroh-gateway/Cargo.toml @@ -27,6 +27,4 @@ lru = "0.12.1" tracing-subscriber = "0.3.18" indicatif = "0.17.7" rustls = "0.22.1" -tokio-rustls-acme = { version = "0.2.0", features = ["axum"] } -tower = "0.4.13" -tower-service = "0.3.2" +tokio-rustls-acme = { version = "0.2.0", features = ["axum"] } \ No newline at end of file diff --git a/iroh-gateway/src/args.rs b/iroh-gateway/src/args.rs index 21219568..5c0ab7ee 100644 --- a/iroh-gateway/src/args.rs +++ b/iroh-gateway/src/args.rs @@ -49,7 +49,7 @@ pub struct Args { pub contact: Option, /// Certificate path. - /// + /// /// Not needed if cert_mode is None. /// In manual mode, this is the directory containing the cert.pem and key.pem files. /// In letsencrypt mode, this is the directory used by the acme acceptor. diff --git a/iroh-gateway/src/main.rs b/iroh-gateway/src/main.rs index 702e2b11..e89b52b8 100644 --- a/iroh-gateway/src/main.rs +++ b/iroh-gateway/src/main.rs @@ -30,13 +30,12 @@ use mime::Mime; use mime_classifier::MimeClassifier; use range_collections::RangeSet2; use ranges::parse_byte_range; -use tokio_rustls_acme::{AcmeConfig, caches::DirCache, axum::AxumAcceptor}; use std::{ result, sync::{Arc, Mutex}, }; +use tokio_rustls_acme::{caches::DirCache, AcmeConfig}; use url::Url; -use tower_service::Service; use crate::ranges::{slice, to_byte_range, to_chunk_range}; mod ranges; @@ -512,7 +511,7 @@ async fn main() -> anyhow::Result<()> { // Run our application as just http let addr = args.addr; println!("listening on {}, http", addr); - + let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve(listener, app).await?; } @@ -538,20 +537,23 @@ async fn main() -> anyhow::Result<()> { let hostnames = args.hostname; let contact = args.contact.context("contact not specified")?; let dir = args.cert_path.context("cert_path not specified")?; - let state = AcmeConfig::new(hostnames) + let _state = AcmeConfig::new(hostnames) .contact([format!("mailto:{contact}")]) .cache_option(Some(DirCache::new(dir))) .directory_lets_encrypt(is_production) .state(); - let config = todo!(); - let acceptor = state.axum_acceptor(config); - // Run our application with hyper - let addr = args.addr.parse()?; - println!("listening on {}, https", addr); - axum_server::bind(addr) - .acceptor(acceptor) - .serve(app.into_make_service()) - .await?; + todo!("update tokio-rustls-acme to latest rustls"); + // let config = rustls::ServerConfig::builder() + // .with_no_client_auth() + // .with_cert_resolver(state.resolver()); + // let acceptor = state.axum_acceptor(config); + // // Run our application with hyper + // let addr = args.addr.parse()?; + // println!("listening on {}, https", addr); + // axum_server::bind(addr) + // .acceptor(acceptor) + // .serve(app.into_make_service()) + // .await?; } } From a1ae95fc252c377df1aebb0b16efd54bca2b6ae5 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Thu, 4 Jan 2024 10:28:54 +0200 Subject: [PATCH 4/7] downgrade rustls and do the axum-server stuff manually --- iroh-gateway/Cargo.lock | 98 +++++-------------------- iroh-gateway/Cargo.toml | 8 +- iroh-gateway/src/https.rs | 36 +++++++++ iroh-gateway/src/main.rs | 149 ++++++++++++++++++++++++++++++++------ 4 files changed, 186 insertions(+), 105 deletions(-) create mode 100644 iroh-gateway/src/https.rs diff --git a/iroh-gateway/Cargo.lock b/iroh-gateway/Cargo.lock index 0b18abf5..cffb539a 100644 --- a/iroh-gateway/Cargo.lock +++ b/iroh-gateway/Cargo.lock @@ -282,36 +282,13 @@ dependencies = [ "http-body 0.4.5", "hyper 0.14.27", "pin-project-lite", - "rustls 0.21.8", - "rustls-pemfile 1.0.3", + "rustls", + "rustls-pemfile", "tokio", "tokio-rustls", "tower-service", ] -[[package]] -name = "axum-server" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad46c3ec4e12f4a4b6835e173ba21c25e484c9d02b49770bf006ce5367c036" -dependencies = [ - "arc-swap", - "bytes", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "http-body-util", - "hyper 1.1.0", - "hyper-util", - "pin-project-lite", - "rustls 0.21.8", - "rustls-pemfile 2.0.0", - "tokio", - "tokio-rustls", - "tower", - "tower-service", -] - [[package]] name = "backoff" version = "0.4.0" @@ -1776,7 +1753,7 @@ dependencies = [ "futures-util", "http 0.2.9", "hyper 0.14.27", - "rustls 0.21.8", + "rustls", "tokio", "tokio-rustls", ] @@ -2062,7 +2039,6 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", - "axum-server 0.6.0", "bytes", "clap", "derive_more", @@ -2070,6 +2046,7 @@ dependencies = [ "futures", "headers", "hyper 1.1.0", + "hyper-util", "indicatif", "iroh", "lru", @@ -2077,10 +2054,12 @@ dependencies = [ "mime_classifier", "quinn", "range-collections", - "rustls 0.22.1", + "rustls", + "rustls-pemfile", "tokio", "tokio-rustls-acme", "tower-http", + "tower-service", "tracing", "tracing-subscriber", "url", @@ -2196,8 +2175,8 @@ dependencies = [ "reqwest", "ring 0.17.5", "rtnetlink", - "rustls 0.21.8", - "rustls-webpki 0.101.7", + "rustls", + "rustls-webpki", "serde", "serde_bytes", "serdect", @@ -3293,7 +3272,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.21.8", + "rustls", "thiserror", "tokio", "tracing", @@ -3309,7 +3288,7 @@ dependencies = [ "rand", "ring 0.16.20", "rustc-hash", - "rustls 0.21.8", + "rustls", "rustls-native-certs", "slab", "thiserror", @@ -3556,8 +3535,8 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.8", - "rustls-pemfile 1.0.3", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", @@ -3724,24 +3703,10 @@ checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" dependencies = [ "log", "ring 0.17.5", - "rustls-webpki 0.101.7", + "rustls-webpki", "sct", ] -[[package]] -name = "rustls" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6b63262c9fcac8659abfaa96cac103d28166d3ff3eaf8f412e19f3ae9e5a48" -dependencies = [ - "log", - "ring 0.17.5", - "rustls-pki-types", - "rustls-webpki 0.102.0", - "subtle", - "zeroize", -] - [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -3749,7 +3714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.3", + "rustls-pemfile", "schannel", "security-framework", ] @@ -3763,22 +3728,6 @@ dependencies = [ "base64 0.21.5", ] -[[package]] -name = "rustls-pemfile" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4" -dependencies = [ - "base64 0.21.5", - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9d979b3ce68192e42760c7810125eb6cf2ea10efae545a156063e61f314e2a" - [[package]] name = "rustls-webpki" version = "0.101.7" @@ -3789,17 +3738,6 @@ dependencies = [ "untrusted 0.9.0", ] -[[package]] -name = "rustls-webpki" -version = "0.102.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de2635c8bc2b88d367767c5de8ea1d8db9af3f6219eba28442242d9ab81d1b89" -dependencies = [ - "ring 0.17.5", - "rustls-pki-types", - "untrusted 0.9.0", -] - [[package]] name = "rustversion" version = "1.0.14" @@ -4517,7 +4455,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.8", + "rustls", "tokio", ] @@ -4528,7 +4466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb6f50b5523d014ba161512c37457acb16fd8218c883c7152e0a67ab763f2d4" dependencies = [ "async-trait", - "axum-server 0.5.1", + "axum-server", "base64 0.21.5", "chrono", "futures", @@ -4537,7 +4475,7 @@ dependencies = [ "rcgen", "reqwest", "ring 0.16.20", - "rustls 0.21.8", + "rustls", "serde", "serde_json", "thiserror", diff --git a/iroh-gateway/Cargo.toml b/iroh-gateway/Cargo.toml index d5cdb362..91e437b9 100644 --- a/iroh-gateway/Cargo.toml +++ b/iroh-gateway/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" [dependencies] axum = "0.7.2" -axum-server = { version = "0.6.0", features = ["tls-rustls", "tokio-rustls"] } tokio = { version = "1", features = ["full"] } headers = { version = "0.4" } hyper = "1" @@ -26,5 +25,8 @@ futures = "0.3.29" lru = "0.12.1" tracing-subscriber = "0.3.18" indicatif = "0.17.7" -rustls = "0.22.1" -tokio-rustls-acme = { version = "0.2.0", features = ["axum"] } \ No newline at end of file +rustls = "0.21" +tokio-rustls-acme = { version = "0.2.0", features = ["axum"] } +hyper-util = "0.1.2" +rustls-pemfile = "1.0.2" +tower-service = "0.3.2" diff --git a/iroh-gateway/src/https.rs b/iroh-gateway/src/https.rs new file mode 100644 index 00000000..5baa92ee --- /dev/null +++ b/iroh-gateway/src/https.rs @@ -0,0 +1,36 @@ +use anyhow::{Context, Result}; +use std::path::Path; + +pub fn load_certs(filename: impl AsRef) -> Result> { + println!("loading certs from {}", filename.as_ref().display()); + let certfile = std::fs::File::open(filename).context("cannot open certificate file")?; + let mut reader = std::io::BufReader::new(certfile); + + let certs = rustls_pemfile::certs(&mut reader)? + .iter() + .map(|v| rustls::Certificate(v.clone())) + .collect(); + + Ok(certs) +} + +pub fn load_secret_key(filename: impl AsRef) -> Result { + println!("loading secret key from {}", filename.as_ref().display()); + let keyfile = std::fs::File::open(filename.as_ref()).context("cannot open secret key file")?; + let mut reader = std::io::BufReader::new(keyfile); + + loop { + match rustls_pemfile::read_one(&mut reader).context("cannot parse secret key .pem file")? { + Some(rustls_pemfile::Item::RSAKey(key)) => return Ok(rustls::PrivateKey(key)), + Some(rustls_pemfile::Item::PKCS8Key(key)) => return Ok(rustls::PrivateKey(key)), + Some(rustls_pemfile::Item::ECKey(key)) => return Ok(rustls::PrivateKey(key)), + None => break, + _ => {} + } + } + + anyhow::bail!( + "no keys found in {} (encrypted keys not supported)", + filename.as_ref().display() + ); +} diff --git a/iroh-gateway/src/main.rs b/iroh-gateway/src/main.rs index e89b52b8..a8f22f54 100644 --- a/iroh-gateway/src/main.rs +++ b/iroh-gateway/src/main.rs @@ -9,10 +9,12 @@ use axum::{ routing::get, Extension, Router, }; -use axum_server::tls_rustls::RustlsConfig; use bytes::Bytes; use clap::Parser; use derive_more::Deref; +use futures::pin_mut; +use hyper::body::Incoming; +use hyper_util::rt::{TokioExecutor, TokioIo}; use iroh::bytes::{store::bao_tree::ByteNum, BlobFormat}; use iroh::{ bytes::{ @@ -34,10 +36,16 @@ use std::{ result, sync::{Arc, Mutex}, }; -use tokio_rustls_acme::{caches::DirCache, AcmeConfig}; +use tokio::net::TcpListener; +use tokio_rustls_acme::{caches::DirCache, tokio_rustls::TlsAcceptor, AcmeConfig}; +use tower_service::Service; use url::Url; -use crate::ranges::{slice, to_byte_range, to_chunk_range}; +use crate::{ + https::{load_certs, load_secret_key}, + ranges::{slice, to_byte_range, to_chunk_range}, +}; +mod https; mod ranges; // Make our own error that wraps `anyhow::Error`. @@ -523,37 +531,134 @@ async fn main() -> anyhow::Result<()> { .canonicalize()?; let cert_file = cert_path.join("cert.pem"); let key_file = cert_path.join("key.pem"); - let config = RustlsConfig::from_pem_file(cert_file, key_file).await?; + let certs = load_certs(cert_file)?; + let secret_key = load_secret_key(key_file)?; + let config = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(certs, secret_key)?; // Run our application with hyper let addr = args.addr; - println!("listening on {}, https", addr); - let addr = addr.parse()?; - axum_server::bind_rustls(addr, config) - .serve(app.into_make_service()) - .await?; + println!("listening on {}", addr); + println!("https with manual certificates"); + let tls_acceptor = TlsAcceptor::from(Arc::new(config)); + let tcp_listener = TcpListener::bind(addr).await?; + + pin_mut!(tcp_listener); + + loop { + let tower_service = app.clone(); + let tls_acceptor = tls_acceptor.clone(); + + // Wait for new tcp connection + let (cnx, addr) = tcp_listener.accept().await?; + + tokio::spawn(async move { + // Wait for tls handshake to happen + let Ok(stream) = tls_acceptor.accept(cnx).await else { + tracing::error!("error during tls handshake connection from {}", addr); + return; + }; + + // Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio. + // `TokioIo` converts between them. + let stream = TokioIo::new(stream); + + // Hyper has also its own `Service` trait and doesn't use tower. We can use + // `hyper::service::service_fn` to create a hyper `Service` that calls our app through + // `tower::Service::call`. + let hyper_service = + hyper::service::service_fn(move |request: Request| { + // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas + // tower's `Service` requires `&mut self`. + // + // We don't need to call `poll_ready` since `Router` is always ready. + tower_service.clone().call(request) + }); + + let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(stream, hyper_service) + .await; + + if let Err(err) = ret { + tracing::warn!("error serving connection from {}: {}", addr, err); + } + }); + } } CertMode::LetsEncryptStaging | CertMode::LetsEncrypt => { let is_production = args.cert_mode == CertMode::LetsEncrypt; let hostnames = args.hostname; let contact = args.contact.context("contact not specified")?; let dir = args.cert_path.context("cert_path not specified")?; - let _state = AcmeConfig::new(hostnames) + let state = AcmeConfig::new(hostnames) .contact([format!("mailto:{contact}")]) .cache_option(Some(DirCache::new(dir))) .directory_lets_encrypt(is_production) .state(); - todo!("update tokio-rustls-acme to latest rustls"); - // let config = rustls::ServerConfig::builder() - // .with_no_client_auth() - // .with_cert_resolver(state.resolver()); - // let acceptor = state.axum_acceptor(config); - // // Run our application with hyper - // let addr = args.addr.parse()?; - // println!("listening on {}, https", addr); - // axum_server::bind(addr) - // .acceptor(acceptor) - // .serve(app.into_make_service()) - // .await?; + let config = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_cert_resolver(state.resolver()); + // config.alpn_protocols.extend([b"h2".to_vec(), b"http/1.1".to_vec()]); + let config = Arc::new(config); + let acme_acceptor = state.acceptor(); + // Run our application with hyper + let addr = args.addr; + println!("listening on {}", addr); + println!( + "https with letsencrypt certificates, production = {}", + is_production + ); + let tcp_listener = TcpListener::bind(addr).await?; + + pin_mut!(tcp_listener); + + loop { + let tower_service = app.clone(); + let acme_acceptor = acme_acceptor.clone(); + let config = config.clone(); + + // Wait for new tcp connection + let (cnx, addr) = tcp_listener.accept().await?; + + tokio::spawn(async move { + // Wait for tls handshake to happen + let handshake = match acme_acceptor.accept(cnx).await? { + Some(handshake) => handshake, + None => { + tracing::info!("got acme tls challenge from {}", addr); + return Ok(()); + } + }; + let stream = handshake.into_stream(config.clone()).await?; + + // Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio. + // `TokioIo` converts between them. + let stream = TokioIo::new(stream); + + // Hyper has also its own `Service` trait and doesn't use tower. We can use + // `hyper::service::service_fn` to create a hyper `Service` that calls our app through + // `tower::Service::call`. + let hyper_service = + hyper::service::service_fn(move |request: Request| { + // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas + // tower's `Service` requires `&mut self`. + // + // We don't need to call `poll_ready` since `Router` is always ready. + tower_service.clone().call(request) + }); + + let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(stream, hyper_service) + .await; + + if let Err(err) = ret { + tracing::warn!("error serving connection from {}: {}", addr, err); + } + anyhow::Ok(()) + }); + } } } From 9a2961f19230df11ae32f26db43e6dd0ac8ed06e Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Thu, 4 Jan 2024 10:53:50 +0200 Subject: [PATCH 5/7] Add more logging --- iroh-gateway/src/main.rs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/iroh-gateway/src/main.rs b/iroh-gateway/src/main.rs index a8f22f54..ba945f15 100644 --- a/iroh-gateway/src/main.rs +++ b/iroh-gateway/src/main.rs @@ -621,17 +621,39 @@ async fn main() -> anyhow::Result<()> { // Wait for new tcp connection let (cnx, addr) = tcp_listener.accept().await?; + println!("got connection from {}", addr); tokio::spawn(async move { // Wait for tls handshake to happen - let handshake = match acme_acceptor.accept(cnx).await? { - Some(handshake) => handshake, - None => { + let handshake = match acme_acceptor.accept(cnx).await { + Ok(Some(handshake)) => { + tracing::info!("got tls handshake from {}", addr); + handshake + } + Ok(None) => { tracing::info!("got acme tls challenge from {}", addr); return Ok(()); } + Err(cause) => { + tracing::error!( + "error during tls handshake connection from {}: {}", + addr, + cause + ); + return Ok(()); + } + }; + let stream = match handshake.into_stream(config.clone()).await { + Ok(stream) => stream, + Err(cause) => { + tracing::error!( + "error during tls handshake connection from {}: {}", + addr, + cause + ); + return Ok(()); + } }; - let stream = handshake.into_stream(config.clone()).await?; // Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio. // `TokioIo` converts between them. From d170051d7b589375493c580f739097cf627cb0a2 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Thu, 4 Jan 2024 11:35:13 +0200 Subject: [PATCH 6/7] poll the state this needs to be done in order to drive the cert renewal process --- iroh-gateway/src/main.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/iroh-gateway/src/main.rs b/iroh-gateway/src/main.rs index ba945f15..08ffec4a 100644 --- a/iroh-gateway/src/main.rs +++ b/iroh-gateway/src/main.rs @@ -12,7 +12,7 @@ use axum::{ use bytes::Bytes; use clap::Parser; use derive_more::Deref; -use futures::pin_mut; +use futures::{pin_mut, StreamExt}; use hyper::body::Incoming; use hyper_util::rt::{TokioExecutor, TokioIo}; use iroh::bytes::{store::bao_tree::ByteNum, BlobFormat}; @@ -504,7 +504,6 @@ async fn main() -> anyhow::Result<()> { collection_cache: Mutex::new(LruCache::new(1000.try_into().unwrap())), })); - // Build our application by composing routes #[rustfmt::skip] let app = Router::new() .route("/blob/:blake3_hash", get(handle_local_blob_request)) @@ -525,6 +524,11 @@ async fn main() -> anyhow::Result<()> { } CertMode::Manual => { // Run with manual certificates + // + // Code copied from https://github.com/tokio-rs/axum/tree/main/examples/low-level-rustls/src + // + // TODO: use axum_server maybe, once tokio-rustls-acme is on the latest + // rustls. let cert_path = args .cert_path .context("cert_path not specified")? @@ -587,6 +591,12 @@ async fn main() -> anyhow::Result<()> { } } CertMode::LetsEncryptStaging | CertMode::LetsEncrypt => { + // Run with letsencrypt certificates + // + // Code copied from https://github.com/tokio-rs/axum/tree/main/examples/low-level-rustls/src and adapted + // + // TODO: use axum_server with the axum acceptor maybe, once tokio-rustls-acme is on the latest + // rustls. let is_production = args.cert_mode == CertMode::LetsEncrypt; let hostnames = args.hostname; let contact = args.contact.context("contact not specified")?; @@ -603,6 +613,18 @@ async fn main() -> anyhow::Result<()> { // config.alpn_protocols.extend([b"h2".to_vec(), b"http/1.1".to_vec()]); let config = Arc::new(config); let acme_acceptor = state.acceptor(); + // drive the acme state machine + // + // this drives the cert renewal process. + tokio::spawn(async move { + let mut state = state; + while let Some(event) = state.next().await { + match event { + Ok(ok) => tracing::debug!("acme event: {:?}", ok), + Err(err) => tracing::error!("error: {:?}", err), + } + } + }); // Run our application with hyper let addr = args.addr; println!("listening on {}", addr); From 9ece833ac9cc62c1ed289349accdecdce396d4f4 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Thu, 4 Jan 2024 12:04:21 +0200 Subject: [PATCH 7/7] rename https to cert_util --- iroh-gateway/src/{https.rs => cert_util.rs} | 1 + iroh-gateway/src/main.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename iroh-gateway/src/{https.rs => cert_util.rs} (96%) diff --git a/iroh-gateway/src/https.rs b/iroh-gateway/src/cert_util.rs similarity index 96% rename from iroh-gateway/src/https.rs rename to iroh-gateway/src/cert_util.rs index 5baa92ee..6728c88a 100644 --- a/iroh-gateway/src/https.rs +++ b/iroh-gateway/src/cert_util.rs @@ -1,3 +1,4 @@ +//! Utilities for loading certificates and keys. use anyhow::{Context, Result}; use std::path::Path; diff --git a/iroh-gateway/src/main.rs b/iroh-gateway/src/main.rs index 08ffec4a..87f820be 100644 --- a/iroh-gateway/src/main.rs +++ b/iroh-gateway/src/main.rs @@ -42,10 +42,10 @@ use tower_service::Service; use url::Url; use crate::{ - https::{load_certs, load_secret_key}, + cert_util::{load_certs, load_secret_key}, ranges::{slice, to_byte_range, to_chunk_range}, }; -mod https; +mod cert_util; mod ranges; // Make our own error that wraps `anyhow::Error`.