1use std::fs::File;
9use std::io::BufReader;
10use std::net::SocketAddr;
11use std::sync::Arc;
12use std::time::Duration;
13
14use anyhow::{Context, Result};
15use axum::http::{HeaderValue, Request, StatusCode};
16use axum::response::IntoResponse;
17use axum::Router;
18use hyper::body::Incoming;
19use hyper_util::rt::{TokioExecutor, TokioIo};
20use hyper_util::server::conn::auto::Builder as ConnBuilder;
21use rustls::pki_types::{CertificateDer, PrivateKeyDer};
22use rustls::ServerConfig;
23use tokio::net::TcpListener;
24use tokio::sync::watch;
25use tokio_rustls::TlsAcceptor;
26use tower::{Service, ServiceExt};
27use tracing::{debug, info, warn};
28
29pub fn init_crypto() {
37 let _ = rustls::crypto::ring::default_provider().install_default();
38}
39
40pub fn load_server_config(cert_path: &str, key_path: &str) -> Result<Arc<ServerConfig>> {
44 let certs = load_certs(cert_path)?;
45 let key = load_key(key_path)?;
46
47 let mut config =
48 ServerConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
49 .with_safe_default_protocol_versions()
50 .context("selecting TLS protocol versions")?
51 .with_no_client_auth()
52 .with_single_cert(certs, key)
53 .context("building rustls ServerConfig (does the key match the certificate?)")?;
54 config.alpn_protocols = vec![b"http/1.1".to_vec()];
55 Ok(Arc::new(config))
56}
57
58fn load_certs(path: &str) -> Result<Vec<CertificateDer<'static>>> {
59 let file = File::open(path).with_context(|| format!("opening certificate file {path}"))?;
60 let mut reader = BufReader::new(file);
61 let certs = rustls_pemfile::certs(&mut reader)
62 .collect::<Result<Vec<_>, _>>()
63 .with_context(|| format!("parsing certificates from {path}"))?;
64 anyhow::ensure!(!certs.is_empty(), "no certificates found in {path}");
65 Ok(certs)
66}
67
68fn load_key(path: &str) -> Result<PrivateKeyDer<'static>> {
69 let file = File::open(path).with_context(|| format!("opening private key file {path}"))?;
70 let mut reader = BufReader::new(file);
71 rustls_pemfile::private_key(&mut reader)
72 .with_context(|| format!("parsing private key from {path}"))?
73 .with_context(|| format!("no private key found in {path}"))
74}
75
76pub async fn serve(
80 listener: TcpListener,
81 config: Arc<ServerConfig>,
82 app: Router,
83 mut shutdown: watch::Receiver<bool>,
84) -> Result<()> {
85 let acceptor = TlsAcceptor::from(config);
86 let mut make_service = app.into_make_service_with_connect_info::<SocketAddr>();
89
90 info!(listen = %listener.local_addr().map(|a| a.to_string()).unwrap_or_default(), "TLS listener up");
91
92 loop {
93 let (stream, peer) = tokio::select! {
94 _ = shutdown.changed() => {
95 if *shutdown.borrow() { break; }
96 continue;
97 }
98 accepted = listener.accept() => match accepted {
99 Ok(v) => v,
100 Err(e) => { warn!(error = %e, "TLS accept error"); continue; }
101 },
102 };
103
104 let acceptor = acceptor.clone();
105 let tower_service = unwrap_infallible(make_service.call(peer).await);
107
108 tokio::spawn(async move {
109 let tls_stream = match tokio::time::timeout(
112 Duration::from_secs(10),
113 acceptor.accept(stream),
114 )
115 .await
116 {
117 Ok(Ok(s)) => s,
118 Ok(Err(e)) => {
119 debug!(error = %e, %peer, "TLS handshake failed");
120 return;
121 }
122 Err(_) => {
123 debug!(%peer, "TLS handshake timed out");
124 return;
125 }
126 };
127 let io = TokioIo::new(tls_stream);
128 let hyper_service = hyper::service::service_fn(move |request: Request<Incoming>| {
129 tower_service.clone().oneshot(request)
130 });
131 if let Err(e) = ConnBuilder::new(TokioExecutor::new())
132 .serve_connection_with_upgrades(io, hyper_service)
133 .await
134 {
135 debug!(error = %e, %peer, "error serving TLS connection");
136 }
137 });
138 }
139 Ok(())
140}
141
142const ACME_CHALLENGE_PREFIX: &str = "/.well-known/acme-challenge/";
148
149pub fn redirect_location(
160 host_header: Option<&str>,
161 path_and_query: &str,
162 tls_port: u16,
163 allowed: &[String],
164) -> Option<String> {
165 let host = host_header?;
166 let bare = split_authority(host)?;
167 if !allowed.is_empty() && !allowed.iter().any(|a| a.eq_ignore_ascii_case(bare)) {
168 return None;
169 }
170 Some(if tls_port == 443 {
171 format!("https://{bare}{path_and_query}")
172 } else {
173 format!("https://{bare}:{tls_port}{path_and_query}")
174 })
175}
176
177fn split_authority(host: &str) -> Option<&str> {
186 let (bare, port) = match host.rfind(']') {
190 Some(close) => {
193 let rest = &host[close + 1..];
194 match rest.strip_prefix(':') {
195 Some(p) => (&host[..=close], Some(p)),
196 None if rest.is_empty() => (&host[..=close], None),
197 None => return None,
198 }
199 }
200 None => match host.split_once(':') {
201 Some((h, p)) => (h, Some(p)),
202 None => (host, None),
203 },
204 };
205 if let Some(digits) = port {
206 if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
207 return None;
208 }
209 }
210 is_valid_host(bare).then_some(bare)
211}
212
213fn is_valid_host(host: &str) -> bool {
217 if host.is_empty() || host.len() > 253 {
218 return false;
219 }
220 if let Some(inner) = host.strip_prefix('[').and_then(|h| h.strip_suffix(']')) {
221 return !inner.is_empty() && inner.parse::<std::net::Ipv6Addr>().is_ok();
222 }
223 !host.starts_with(['.', '-'])
225 && !host.ends_with(['.', '-'])
226 && host
227 .bytes()
228 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')
229}
230
231fn redirect_router(tls_port: u16, status: StatusCode, allowed: Vec<String>) -> Router {
233 let allowed = Arc::new(allowed);
234 Router::new().fallback(move |req: Request<axum::body::Body>| {
235 let allowed = Arc::clone(&allowed);
236 async move {
237 let path_and_query = req
238 .uri()
239 .path_and_query()
240 .map(|pq| pq.as_str())
241 .unwrap_or("/");
242
243 if path_and_query.starts_with(ACME_CHALLENGE_PREFIX) {
244 return (StatusCode::NOT_FOUND, "not found\n").into_response();
245 }
246
247 let host = req
248 .headers()
249 .get(axum::http::header::HOST)
250 .and_then(|h| h.to_str().ok());
251
252 match redirect_location(host, path_and_query, tls_port, &allowed) {
253 Some(location) => match HeaderValue::from_str(&location) {
254 Ok(value) => (status, [(axum::http::header::LOCATION, value)]).into_response(),
255 Err(_) => (StatusCode::BAD_REQUEST, "bad host\n").into_response(),
256 },
257 None => (
258 StatusCode::BAD_REQUEST,
259 "this port serves only an HTTPS redirect; send a valid Host header\n",
260 )
261 .into_response(),
262 }
263 }
264 })
265}
266
267pub fn parse_redirect_status(status: u16) -> Result<StatusCode> {
275 let parsed = StatusCode::from_u16(status)
276 .with_context(|| format!("invalid tls.redirect_status {status}"))?;
277 anyhow::ensure!(
283 matches!(status, 301 | 302 | 303 | 307 | 308),
284 "tls.redirect_status must be 301, 302, 303, 307 or 308 (got {parsed}); \
285 308 preserves the method and body, 301 is the older browser-facing convention"
286 );
287 Ok(parsed)
288}
289
290pub async fn serve_redirect(
300 listener: TcpListener,
301 tls_port: u16,
302 status: u16,
303 allowed: Vec<String>,
304 shutdown: watch::Receiver<bool>,
305) -> Result<()> {
306 let status = parse_redirect_status(status)?;
307
308 info!(
309 listen = %listener.local_addr().map(|a| a.to_string()).unwrap_or_default(),
310 to_port = tls_port,
311 %status,
312 "HTTP→HTTPS redirect listener up"
313 );
314
315 axum::serve(
316 listener,
317 redirect_router(tls_port, status, allowed).into_make_service(),
318 )
319 .with_graceful_shutdown(async move {
320 let mut shutdown = shutdown;
321 while shutdown.changed().await.is_ok() {
322 if *shutdown.borrow() {
323 break;
324 }
325 }
326 })
327 .await
328 .context("HTTP→HTTPS redirect server error")
329}
330
331fn unwrap_infallible<T>(result: Result<T, std::convert::Infallible>) -> T {
332 match result {
333 Ok(value) => value,
334 Err(never) => match never {},
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn load_server_config_errors_on_missing_files() {
344 assert!(load_server_config("/no/such/cert.pem", "/no/such/key.pem").is_err());
345 }
346
347 fn loc(host: Option<&str>, pq: &str, port: u16) -> Option<String> {
348 redirect_location(host, pq, port, &[])
349 }
350
351 #[test]
352 fn redirects_preserving_path_and_query() {
353 assert_eq!(
354 loc(Some("app.example.com"), "/a/b?x=1&y=2", 443).as_deref(),
355 Some("https://app.example.com/a/b?x=1&y=2")
356 );
357 }
358
359 #[test]
360 fn rewrites_to_the_tls_port_when_it_is_not_443() {
361 assert_eq!(
364 loc(Some("localhost:80"), "/", 8443).as_deref(),
365 Some("https://localhost:8443/")
366 );
367 }
368
369 #[test]
370 fn strips_the_plaintext_port_from_the_host_header() {
371 assert_eq!(
372 loc(Some("app.example.com:80"), "/", 443).as_deref(),
373 Some("https://app.example.com/")
374 );
375 }
376
377 #[test]
378 fn keeps_ipv6_literals_bracketed() {
379 assert_eq!(
380 loc(Some("[::1]:80"), "/health", 8443).as_deref(),
381 Some("https://[::1]:8443/health")
382 );
383 assert_eq!(loc(Some("[not-an-ip]"), "/", 443), None);
384 }
385
386 #[test]
387 fn refuses_a_missing_or_malformed_host() {
388 assert_eq!(loc(None, "/", 443), None);
391 assert_eq!(loc(Some(""), "/", 443), None);
392 assert_eq!(loc(Some("evil.example/@good.example"), "/", 443), None);
393 assert_eq!(loc(Some("host\r\nX-Injected: 1"), "/", 443), None);
394 assert_eq!(loc(Some("has space"), "/", 443), None);
395 assert_eq!(loc(Some("user@evil.example"), "/", 443), None);
396 assert_eq!(
401 loc(Some("attacker.example:443@victim.example"), "/", 443),
402 None
403 );
404 assert_eq!(loc(Some("host.example:80:81"), "/", 443), None);
405 assert_eq!(loc(Some("host.example:"), "/", 443), None);
406 assert_eq!(loc(Some("host.example:notaport"), "/", 443), None);
407 assert_eq!(loc(Some("[::1]:junk"), "/", 443), None);
408 assert_eq!(loc(Some(".leading-dot"), "/", 443), None);
409 assert_eq!(loc(Some(&"a".repeat(254)), "/", 443), None);
410 }
411
412 #[test]
413 fn allow_list_pins_the_redirect_target() {
414 let allowed = vec!["app.example.com".to_string()];
415 assert!(redirect_location(Some("app.example.com"), "/", 443, &allowed).is_some());
416 assert_eq!(
418 redirect_location(Some("attacker.example"), "/", 443, &allowed),
419 None
420 );
421 assert!(redirect_location(Some("APP.Example.com:80"), "/", 443, &allowed).is_some());
423 }
424
425 #[tokio::test]
426 async fn redirect_router_answers_end_to_end() {
427 use axum::body::Body;
428 use tower::ServiceExt;
429
430 let app = redirect_router(8443, StatusCode::PERMANENT_REDIRECT, vec![]);
431
432 let res = app
433 .clone()
434 .oneshot(
435 Request::builder()
436 .method("POST")
437 .uri("/submit?a=1")
438 .header("host", "localhost")
439 .body(Body::empty())
440 .unwrap(),
441 )
442 .await
443 .unwrap();
444 assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
446 assert_eq!(
447 res.headers().get("location").unwrap(),
448 "https://localhost:8443/submit?a=1"
449 );
450
451 let res = app
455 .clone()
456 .oneshot(
457 Request::builder()
458 .uri("/.well-known/acme-challenge/tok")
459 .header("host", "localhost")
460 .body(Body::empty())
461 .unwrap(),
462 )
463 .await
464 .unwrap();
465 assert_eq!(res.status(), StatusCode::NOT_FOUND);
466 assert!(res.headers().get("location").is_none());
467
468 let res = app
470 .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
471 .await
472 .unwrap();
473 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
474 }
475
476 #[test]
477 fn redirect_status_accepts_only_navigational_redirects() {
478 for ok in [301, 302, 303, 307, 308] {
479 assert!(parse_redirect_status(ok).is_ok(), "{ok} should be accepted");
480 }
481 for bad in [200, 404, 0] {
483 assert!(
484 parse_redirect_status(bad).is_err(),
485 "{bad} should be rejected"
486 );
487 }
488 for not_navigational in [300, 304, 305, 306] {
492 assert!(
493 parse_redirect_status(not_navigational).is_err(),
494 "{not_navigational} is 3xx but does not navigate; it must be rejected"
495 );
496 }
497 }
498
499 #[tokio::test]
500 async fn serve_redirect_rejects_a_non_3xx_status() {
501 let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
502 let (_tx, rx) = watch::channel(false);
503 assert!(serve_redirect(listener, 443, 200, vec![], rx)
506 .await
507 .is_err());
508 }
509}