From dc306f1ed781bc70fc0cf29d7000349f0afb3056 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 8 Jan 2024 10:38:51 +0300 Subject: [PATCH] Add HTTP redirect --- flake.nix | 44 ++++++++++++++++++++++++++++--- src/main.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 106 insertions(+), 14 deletions(-) diff --git a/flake.nix b/flake.nix index fa19c9f..6080496 100644 --- a/flake.nix +++ b/flake.nix @@ -103,12 +103,39 @@ services.site = { enable = mkEnableOption (mdDoc "site service"); - port = mkOption { + certificate = mkOption { + type = types.nullOr types.path; + default = null; + example = "/path/to/cert.pem"; + description = mdDoc '' + The path to the SSL certificate the site will use. + ''; + }; + + key = mkOption { + type = types.nullOr types.path; + default = null; + example = "/path/to/key.pem"; + description = mdDoc '' + The path to the SSL key the site will use. + ''; + }; + + httpPort = mkOption { type = types.port; default = 8080; example = 80; description = mdDoc '' - Specifies on which port the site service listens for connections. + Specifies on which port the site service listens for HTTP connections. + ''; + }; + + httpsPort = mkOption { + type = types.port; + default = 8443; + example = 80; + description = mdDoc '' + Specifies on which port the site service listens for HTTPS connections. ''; }; @@ -138,11 +165,20 @@ wantedBy = [ "multi-user.target" ]; serviceConfig = let - needsPrivilidges = cfg.port < 1024; + arguments = [ + "--http-port" cfg.httpPort + "--https-port" cfg.httpsPort + "--log-level" cfg.logLevel + ] ++ (optionals (cfg.certificate != null) [ + "--certificate" cfg.certificate + ]) ++ (optionals (cfg.key != null) [ + "--key" cfg.key + ]); + needsPrivilidges = cfg.httpPort < 1024 || cfg.httpsPort < 1024; capabilities = [ "" ] ++ optionals needsPrivilidges [ "CAP_NET_BIND_SERVICE" ]; rootDirectory = "/run/site"; in { - ExecStart = "${self.packages.${pkgs.system}.site}/bin/site --port ${cfg.port} --log-level ${cfg.logLevel}"; + ExecStart = "${self.packages.${pkgs.system}.site}/bin/site " + (concatStringsSep " " arguments); Restart = "always"; DynamicUser = true; RootDirectory = rootDirectory; diff --git a/src/main.rs b/src/main.rs index 447a95a..c6f10ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#![feature(lazy_cell, let_chains)] +#![feature(lazy_cell)] mod asset; mod errors; @@ -13,17 +13,31 @@ use std::{ }; use anyhow::Context; -use axum::Router; +use axum::{ + extract::Host, + handler::HandlerWithoutStateExt, + http::{ + uri::Scheme, + StatusCode, + Uri, + }, + response::Redirect, + BoxError, + Router, +}; use axum_server::tls_rustls::RustlsConfig; use clap::Parser; use tower_http::trace::TraceLayer; -#[derive(Parser)] +#[derive(Parser, Clone)] #[command(author, version, about)] struct Cli { - /// The port to listen for connections on + /// The HTTP port to listen for connections on #[arg(long, default_value = "8080")] - port: u16, + http_port: u16, + /// The HTTPS port to listen for connections on + #[arg(long, default_value = "8443")] + https_port: u16, /// The log level to log stuff with #[arg(long, default_value = "info")] log_level: log::LevelFilter, @@ -36,6 +50,46 @@ struct Cli { key: Option, } +async fn redirect_http(args: Cli) { + let http_port = args.http_port.to_string(); + let https_port = args.https_port.to_string(); + + let make_https = move |host: String, uri: Uri| -> Result { + let mut parts = uri.into_parts(); + + parts.scheme = Some(Scheme::HTTPS); + + if parts.path_and_query.is_none() { + parts.path_and_query = Some("/".parse().unwrap()); + } + + let https_host = host.replace(&http_port, &https_port); + parts.authority = Some(https_host.parse()?); + + Ok(Uri::from_parts(parts)?) + }; + + let redirect = move |Host(host): Host, uri: Uri| { + async move { + match make_https(host, uri) { + Ok(uri) => Ok(Redirect::permanent(&uri.to_string())), + Err(error) => { + log::warn!("Failed to convert URI to HTTPS: {}", error); + Err(StatusCode::BAD_REQUEST) + }, + } + } + }; + + let address = SocketAddr::from(([0, 0, 0, 0], args.http_port)); + + axum_server::bind(address) + .serve(redirect.into_make_service()) + .await + .with_context(|| "Failed to run redirect server") + .unwrap(); +} + #[tokio::main] async fn main() -> anyhow::Result<()> { let args = Cli::parse(); @@ -52,20 +106,22 @@ async fn main() -> anyhow::Result<()> { .layer(TraceLayer::new_for_http()) .into_make_service(); - let address = SocketAddr::from(([0, 0, 0, 0], args.port)); + if let (Some(certificate_path), Some(key_path)) = (&args.certificate, &args.key) { + tokio::spawn(redirect_http(args.clone())); - if let Some(certificate_path) = args.certificate - && let Some(key_path) = args.key - { let config = RustlsConfig::from_pem_file(certificate_path, key_path) .await .with_context(|| "Failed to create TLS configuration from PEM files")?; + let address = SocketAddr::from(([0, 0, 0, 0], args.https_port)); + axum_server::bind_rustls(address, config).serve(app).await } else { + let address = SocketAddr::from(([0, 0, 0, 0], args.http_port)); + axum_server::bind(address).serve(app).await } - .with_context(|| "Failed to run server")?; + .with_context(|| "Failed to run main server")?; Ok(()) }