1use std::{
4 net::{IpAddr, Ipv4Addr, SocketAddr},
5 path::PathBuf,
6 time::Instant,
7};
8
9use axum::{
10 Json, Router,
11 extract::{ConnectInfo, Request, State},
12 handler::Handler,
13 http::Method,
14 middleware::{self, Next},
15 response::IntoResponse,
16 routing::get,
17};
18use n0_error::{Result, StdResultExt, anyerr, bail_any};
19use serde::{Deserialize, Serialize};
20use tokio::{net::TcpListener, task::JoinSet};
21use tower_http::{
22 cors::{self, CorsLayer},
23 trace::TraceLayer,
24};
25use tracing::{Level, info, span, warn};
26
27mod doh;
28mod error;
29mod pkarr;
30mod rate_limiting;
31mod tls;
32
33pub use self::{rate_limiting::RateLimitConfig, tls::CertMode};
34use crate::state::AppState;
35
36#[derive(Debug, Serialize, Deserialize, Clone)]
38#[non_exhaustive]
39pub struct HttpConfig {
40 pub port: u16,
42 pub bind_addr: Option<IpAddr>,
44}
45
46#[derive(Debug, Serialize, Deserialize, Clone)]
50#[non_exhaustive]
51pub struct HttpsConfig {
52 pub port: u16,
54 pub bind_addr: Option<IpAddr>,
56 pub domains: Vec<String>,
58 pub cert_mode: CertMode,
60 pub letsencrypt_contact: Option<String>,
65 pub letsencrypt_prod: Option<bool>,
70}
71
72#[derive(Debug)]
74pub(crate) struct HttpServer {
75 tasks: JoinSet<std::io::Result<()>>,
76 http_addr: Option<SocketAddr>,
77 https_addr: Option<SocketAddr>,
78}
79
80impl HttpServer {
81 pub(crate) async fn spawn(
83 http_config: Option<HttpConfig>,
84 https_config: Option<HttpsConfig>,
85 rate_limit_config: RateLimitConfig,
86 state: AppState,
87 cert_cache_dir: PathBuf,
88 ) -> Result<HttpServer> {
89 if http_config.is_none() && https_config.is_none() {
90 bail_any!("Either http or https config is required");
91 }
92
93 let app = create_app(state, &rate_limit_config);
94
95 let mut tasks = JoinSet::new();
96
97 let http_addr = if let Some(config) = http_config {
99 let bind_addr = SocketAddr::new(
100 config.bind_addr.unwrap_or(Ipv4Addr::UNSPECIFIED.into()),
101 config.port,
102 );
103 let app = app.clone();
104 let listener = TcpListener::bind(bind_addr)
105 .await
106 .anyerr()?
107 .into_std()
108 .anyerr()?;
109 let bound_addr = listener.local_addr().anyerr()?;
110 let fut = axum_server::from_tcp(listener)?
111 .serve(app.into_make_service_with_connect_info::<SocketAddr>());
112 info!("HTTP server listening on {bind_addr}");
113 tasks.spawn(fut);
114 Some(bound_addr)
115 } else {
116 None
117 };
118
119 let https_addr = if let Some(config) = https_config {
121 let bind_addr = SocketAddr::new(
122 config.bind_addr.unwrap_or(Ipv4Addr::UNSPECIFIED.into()),
123 config.port,
124 );
125 let acceptor = {
126 tokio::fs::create_dir_all(&cert_cache_dir)
127 .await
128 .with_std_context(|_| {
129 format!(
130 "failed to create cert cache dir at {}",
131 cert_cache_dir.display()
132 )
133 })?;
134 config
135 .cert_mode
136 .build(
137 config.domains,
138 cert_cache_dir,
139 config.letsencrypt_contact,
140 config.letsencrypt_prod.unwrap_or(false),
141 )
142 .await?
143 };
144 let listener = TcpListener::bind(bind_addr)
145 .await
146 .anyerr()?
147 .into_std()
148 .anyerr()?;
149 let bound_addr = listener.local_addr().anyerr()?;
150 let fut = axum_server::from_tcp(listener)?
151 .acceptor(acceptor)
152 .serve(app.into_make_service_with_connect_info::<SocketAddr>());
153 info!("HTTPS server listening on {bind_addr}");
154 tasks.spawn(fut);
155 Some(bound_addr)
156 } else {
157 None
158 };
159
160 Ok(HttpServer {
161 tasks,
162 http_addr,
163 https_addr,
164 })
165 }
166
167 pub(crate) fn http_addr(&self) -> Option<SocketAddr> {
169 self.http_addr
170 }
171
172 pub(crate) fn https_addr(&self) -> Option<SocketAddr> {
174 self.https_addr
175 }
176
177 pub(crate) async fn shutdown(mut self) -> Result<()> {
179 self.tasks.abort_all();
181 self.run_until_done().await?;
182 Ok(())
183 }
184
185 pub(crate) async fn run_until_done(mut self) -> Result<()> {
189 let mut final_res: Result<()> = Ok(());
190 while let Some(res) = self.tasks.join_next().await {
191 match res {
192 Ok(Ok(())) => {}
193 Err(err) if err.is_cancelled() => {}
194 Ok(Err(err)) => {
195 warn!(?err, "task failed");
196 final_res = Err(anyerr!(err, "task"));
197 }
198 Err(err) => {
199 warn!(?err, "task panicked");
200 final_res = Err(anyerr!(err, "join"));
201 }
202 }
203 }
204 final_res
205 }
206}
207
208#[derive(Serialize)]
210struct Health {
211 status: &'static str,
212 version: &'static str,
213 git_hash: &'static str,
214}
215
216async fn healthz() -> Json<Health> {
217 Json(Health {
218 status: "ok",
219 version: env!("CARGO_PKG_VERSION"),
220 git_hash: "unknown",
221 })
222}
223
224pub(crate) fn create_app(state: AppState, rate_limit_config: &RateLimitConfig) -> Router {
225 let cors = CorsLayer::new()
227 .allow_methods([Method::GET, Method::POST, Method::PUT])
229 .allow_origin(cors::Any);
231
232 let trace = TraceLayer::new_for_http().make_span_with(|request: &http::Request<_>| {
234 let conn_info = request
235 .extensions()
236 .get::<ConnectInfo<SocketAddr>>()
237 .expect("connectinfo extension to be present");
238 let span = span!(
239 Level::DEBUG,
240 "http_request",
241 method = ?request.method(),
242 uri = ?request.uri(),
243 src = %conn_info.0,
244 );
245 span
246 });
247
248 let rate_limit = rate_limiting::create(rate_limit_config);
250
251 let router = Router::new()
255 .route("/dns-query", get(doh::get).post(doh::post))
256 .route(
257 "/pkarr/{key}",
258 if let Some(rate_limit) = rate_limit {
259 get(pkarr::get).put(pkarr::put.layer(rate_limit))
260 } else {
261 get(pkarr::get).put(pkarr::put)
262 },
263 )
264 .route("/healthcheck", get(|| async { "OK" }))
266 .route("/healthz", get(healthz))
267 .route("/", get(|| async { "Hi!" }))
268 .with_state(state.clone());
269
270 router
272 .layer(cors)
273 .layer(trace)
274 .route_layer(middleware::from_fn_with_state(state, metrics_middleware))
275}
276
277async fn metrics_middleware(
286 State(state): State<AppState>,
287 req: Request,
288 next: Next,
289) -> impl IntoResponse {
290 let start = Instant::now();
291 let response = next.run(req).await;
292 let latency = start.elapsed().as_millis();
293 let status = response.status();
294 state
295 .metrics
296 .http_requests_duration_ms
297 .inc_by(latency as u64);
298 state.metrics.http_requests.inc();
299 if status.is_success() {
300 state.metrics.http_requests_success.inc();
301 } else {
302 state.metrics.http_requests_error.inc();
303 }
304 response
305}
306
307#[cfg(test)]
308mod tests {
309 use std::{
310 net::{IpAddr, Ipv4Addr},
311 sync::Arc,
312 };
313
314 use hickory_resolver::{
315 config::{NameServerConfig, ResolverConfig},
316 net::runtime::TokioRuntimeProvider,
317 };
318 use hickory_server::proto::rr::RecordType;
319 use iroh::{
320 RelayUrl, SecretKey,
321 address_lookup::{EndpointInfo, PkarrRelayClient},
322 dns::DnsResolver,
323 tls::{CaTlsConfig, default_provider},
324 };
325 use n0_error::StdResultExt;
326 use n0_tracing_test::traced_test;
327 use rand::{RngExt, SeedableRng};
328
329 use crate::{http::HttpsConfig, server::Server};
330
331 #[tokio::test]
332 #[traced_test]
333 async fn test_doh() -> n0_error::Result {
334 let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(0);
335 let dir = tempfile::tempdir()?;
336 let https_config = HttpsConfig {
337 port: 0,
338 bind_addr: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
339 domains: vec!["localhost".to_string()],
340 cert_mode: crate::http::CertMode::SelfSigned,
341 letsencrypt_contact: None,
342 letsencrypt_prod: None,
343 };
344 let server =
345 Server::spawn_for_tests_with_options(dir.path(), None, None, Some(https_config))
346 .await?;
347
348 const RELAY_URL: &str = "https://relay.example./";
349 let (name_z32, signed_packet) = {
350 let secret_key = SecretKey::from_bytes(&rng.random());
351 let endpoint_id = secret_key.public();
352 let relay_url: RelayUrl = RELAY_URL.parse().expect("valid url");
353 let endpoint_info = EndpointInfo::new(endpoint_id).with_relay_url(relay_url.clone());
354 (
355 secret_key.public().to_z32(),
356 endpoint_info.to_pkarr_signed_packet(&secret_key, 30)?,
357 )
358 };
359
360 let http_url = server.http_url().expect("http is bound");
361 let tls_config = CaTlsConfig::default()
362 .client_config(default_provider())
363 .expect("infallible");
364 let pkarr = PkarrRelayClient::new(
365 format!("{http_url}pkarr").parse().anyerr()?,
366 tls_config,
367 DnsResolver::default(),
368 );
369 pkarr.publish(&signed_packet).await?;
370
371 let client = reqwest::Client::builder()
373 .http2_prior_knowledge()
374 .use_preconfigured_tls(self::tls::insecure_tls_config())
375 .build()
376 .anyerr()?;
377
378 let url = format!(
380 "{http_url}dns-query?name={}&type=txt",
381 format_args!("_iroh.{name_z32}."),
382 );
383 let res = client
384 .get(url)
385 .header("accept", "application/dns-json")
386 .send()
387 .await
388 .anyerr()?
389 .json::<super::doh::response::DnsResponse>()
390 .await
391 .anyerr()?;
392 assert_eq!(res.answer.len(), 1);
393 assert_eq!(res.answer[0].name, format!("_iroh.{name_z32}."));
394 assert_eq!(res.answer[0].data, format!("relay={RELAY_URL}"));
395
396 let https_url = server.https_url().expect("https is bound");
398 let url = format!(
399 "{https_url}dns-query?name={}&type=txt",
400 format_args!("_iroh.{name_z32}."),
401 );
402 let res = client
403 .get(url)
404 .header("accept", "application/dns-json")
405 .send()
406 .await
407 .anyerr()?
408 .json::<super::doh::response::DnsResponse>()
409 .await
410 .anyerr()?;
411 assert_eq!(res.answer.len(), 1);
412 assert_eq!(res.answer[0].name, format!("_iroh.{name_z32}."));
413 assert_eq!(res.answer[0].data, format!("relay={RELAY_URL}"));
414
415 let client = {
417 let https_addr = server.https_addr().expect("https is bound");
418 let mut name_server =
419 NameServerConfig::https(https_addr.ip(), Arc::from("localhost"), None);
420 for connection in &mut name_server.connections {
421 connection.port = https_addr.port();
422 }
423 let config = ResolverConfig::from_parts(None, vec![], vec![name_server]);
424
425 hickory_resolver::Resolver::builder_with_config(config, TokioRuntimeProvider::default())
426 .with_tls_config(self::tls::insecure_tls_config())
427 .build()
428 .anyerr()?
429 };
430
431 let res = client
432 .txt_lookup(format!("_iroh.{name_z32}."))
433 .await
434 .anyerr()?;
435 let records = res.answers();
436 assert_eq!(records.len(), 1);
437 assert_eq!(records[0].record_type(), RecordType::TXT);
438 let txt_data = match &records[0].data {
439 hickory_server::proto::rr::RData::TXT(txt) => &txt.txt_data,
440 other => panic!("expected TXT record, got {other:?}"),
441 };
442 assert_eq!(&txt_data[0][..], format!("relay={RELAY_URL}").as_bytes());
443
444 server.shutdown().await?;
445 Ok(())
446 }
447
448 mod tls {
449 use std::sync::Arc;
450
451 use rustls::{
452 DigitallySignedStruct, RootCertStore,
453 client::{
454 ClientConfig,
455 danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
456 },
457 crypto::{
458 CryptoProvider, ring::default_provider, verify_tls12_signature,
459 verify_tls13_signature,
460 },
461 pki_types::{CertificateDer, ServerName, UnixTime},
462 };
463
464 #[derive(Debug)]
465 struct NoCertificateVerification(CryptoProvider);
466
467 impl Default for NoCertificateVerification {
468 fn default() -> Self {
469 Self(default_provider())
470 }
471 }
472
473 impl ServerCertVerifier for NoCertificateVerification {
474 fn verify_server_cert(
475 &self,
476 _end_entity: &CertificateDer<'_>,
477 _intermediates: &[CertificateDer<'_>],
478 _server_name: &ServerName<'_>,
479 _ocsp: &[u8],
480 _now: UnixTime,
481 ) -> Result<ServerCertVerified, rustls::Error> {
482 Ok(ServerCertVerified::assertion())
483 }
484
485 fn verify_tls12_signature(
486 &self,
487 message: &[u8],
488 cert: &CertificateDer<'_>,
489 dss: &DigitallySignedStruct,
490 ) -> Result<HandshakeSignatureValid, rustls::Error> {
491 verify_tls12_signature(
492 message,
493 cert,
494 dss,
495 &self.0.signature_verification_algorithms,
496 )
497 }
498
499 fn verify_tls13_signature(
500 &self,
501 message: &[u8],
502 cert: &CertificateDer<'_>,
503 dss: &DigitallySignedStruct,
504 ) -> Result<HandshakeSignatureValid, rustls::Error> {
505 verify_tls13_signature(
506 message,
507 cert,
508 dss,
509 &self.0.signature_verification_algorithms,
510 )
511 }
512
513 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
514 self.0.signature_verification_algorithms.supported_schemes()
515 }
516 }
517
518 pub(super) fn insecure_tls_config() -> ClientConfig {
519 let mut cfg = ClientConfig::builder_with_provider(Arc::new(
520 rustls::crypto::ring::default_provider(),
521 ))
522 .with_safe_default_protocol_versions()
523 .unwrap()
524 .with_root_certificates(RootCertStore::empty())
525 .with_no_client_auth();
526 cfg.dangerous()
527 .set_certificate_verifier(Arc::new(NoCertificateVerification::default()));
528 cfg
529 }
530 }
531}