Repository: static-web-server/static-web-server (language: rust)

Issue:
Help needed with tls_redirect_from feature in V2 version
I have read in #26 that your program supports tls_redirect_from feature in V1 version to redirect http to https. I would like to know how to use this feature in the current V2 version. Any help would be appreciated.


Discussion / hints:
That's correct, TLS redirection is present in v1 but unfortunately not in the latest v2.
So for now, what you can do is stay with v1 for a while.

However, this feature is on my TODO list, so there might be a chance to promote it to be a thing on the next minor release if I get the time for but not guaranteed.

> That's correct, TLS redirection is present in v1 but unfortunately not in the latest v2. So for now, what you can do is stay with v1 for a while.
> 
> However, this feature is on my TODO list, so there might be a chance to promote it to be a thing on the next minor release if I get the time for but not guaranteed.

Thank you for your prompt response. I appreciate the information you provided regarding the tls_redirect_from feature. It's good to know that it was present in V1, and I understand that it might not be available in the current V2 version.

I'll consider staying with V1 for the time being, as you suggested. It's great to hear that the feature is on your TODO list, and I'm looking forward to the possibility of it being included in a future release. I understand that it's not guaranteed, but I appreciate your consideration of this feature.

Thank you again for your help, and I wish you continued success with your project.

Best regards,

Fix patch:
diff --git a/src/https_redirect.rs b/src/https_redirect.rs
new file mode 100644
index 00000000..02f804c2
--- /dev/null
+++ b/src/https_redirect.rs
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+// This file is part of Static Web Server.
+// See https://static-web-server.net/ for more information
+// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
+
+//! Module to redirect HTTP requests to HTTPS.
+//!
+
+use headers::{HeaderMapExt, Host};
+use hyper::{header::LOCATION, Body, Request, Response, StatusCode};
+use std::sync::Arc;
+
+use crate::Result;
+
+/// HTTPS redirect options.
+pub struct RedirectOpts {
+    /// HTTPS hostname to redirect to.
+    pub https_hostname: String,
+    /// HTTPS hostname port to redirect to.
+    pub https_port: u16,
+    /// Hostnames or IPS to redirect from.
+    pub allowed_hosts: Vec<String>,
+}
+
+/// It redirects all requests from HTTP to HTTPS.
+pub async fn redirect_to_https(
+    req: &Request<Body>,
+    opts: Arc<RedirectOpts>,
+) -> Result<Response<Body>, StatusCode> {
+    if let Some(ref host) = req.headers().typed_get::<Host>() {
+        let from_hostname = host.hostname();
+        if !opts
+            .allowed_hosts
+            .iter()
+            .any(|s| s.as_str() == from_hostname)
+        {
+            tracing::debug!("redirect host is not allowed!");
+            return Err(StatusCode::BAD_REQUEST);
+        }
+
+        let url = format!(
+            "https://{}:{}{}",
+            opts.https_hostname,
+            opts.https_port,
+            req.uri()
+        );
+        tracing::debug!("https redirect to {}", url);
+
+        let mut resp = Response::new(Body::empty());
+        *resp.status_mut() = StatusCode::MOVED_PERMANENTLY;
+        resp.headers_mut().insert(LOCATION, url.parse().unwrap());
+        return Ok(resp);
+    }
+
+    tracing::debug!("redirect host was not determined!");
+    Err(StatusCode::BAD_REQUEST)
+}
diff --git a/src/lib.rs b/src/lib.rs
index d8c59d6e..3fe8c44e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -115,6 +115,8 @@ pub mod error_page;
 pub mod exts;
 pub mod fallback_page;
 pub mod handler;
+#[cfg(feature = "http2")]
+pub mod https_redirect;
 pub mod logger;
 pub mod redirects;
 pub mod rewrites;
diff --git a/src/server.rs b/src/server.rs
index a2a99159..f96b71a4 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -6,11 +6,13 @@
 //! Server module intended to construct a multi-thread HTTP or HTTP/2 web server.
 //!
 
+use hyper::server::conn::AddrStream;
 use hyper::server::Server as HyperServer;
+use hyper::service::{make_service_fn, service_fn};
 use listenfd::ListenFd;
 use std::net::{IpAddr, SocketAddr, TcpListener};
 use std::sync::Arc;
-use tokio::sync::oneshot::Receiver;
+use tokio::sync::watch::Receiver;
 
 use crate::handler::{RequestHandler, RequestHandlerOpts};
 #[cfg(any(unix, windows))]
@@ -22,7 +24,8 @@ use {
     hyper::server::conn::AddrIncoming,
 };
 
-use crate::{cors, helpers, logger, Settings};
+use crate::https_redirect;
+use crate::{cors, error, error_page, helpers, logger, Settings};
 use crate::{service::RouterService, Context, Result};
 
 /// Define a multi-thread HTTP or HTTP/2 web server.
@@ -232,6 +235,22 @@ impl Server {
         let grace_period = general.grace_period;
         tracing::info!("grace period before graceful shutdown: {}s", grace_period);
 
+        // HTTP to HTTPS redirect option
+        let https_redirect = general.https_redirect;
+        tracing::info!("http to https redirect: enabled={}", https_redirect);
+        tracing::info!(
+            "http to https redirect host: {}",
+            general.https_redirect_host
+        );
+        tracing::info!(
+            "http to https redirect from port: {}",
+            general.https_redirect_from_port
+        );
+        tracing::info!(
+            "http to https redirect from hosts: {}",
+            general.https_redirect_from_hosts
+        );
+
         // Create a service router for Hyper
         let router_service = RouterService::new(RequestHandler {
             opts: Arc::from(RequestHandlerOpts {
@@ -244,8 +263,8 @@ impl Server {
                 cors,
                 security_headers,
                 cache_control_headers,
-                page404,
-                page50x,
+                page404: page404.clone(),
+                page50x: page50x.clone(),
                 page_fallback,
                 basic_auth,
                 log_remote_address,
@@ -255,6 +274,21 @@ impl Server {
             }),
         });
 
+        #[cfg(windows)]
+        let (sender, receiver) = tokio::sync::watch::channel(());
+        // ctrl+c listening
+        #[cfg(windows)]
+        let ctrlc_task = tokio::spawn(async move {
+            if !general.windows_service {
+                tracing::info!("installing graceful shutdown ctrl+c signal handler");
+                tokio::signal::ctrl_c()
+                    .await
+                    .expect("failed to install ctrl+c signal handler");
+                tracing::info!("installing graceful shutdown ctrl+c signal handler");
+                let _ = sender.send(());
+            }
+        });
+
         // Run the corresponding HTTP Server asynchronously with its given options
         #[cfg(feature = "http2")]
         if general.http2 {
@@ -293,32 +327,155 @@ impl Server {
             #[cfg(unix)]
             let handle = signals.handle();
 
-            let server =
+            let http2_server =
                 HyperServer::builder(TlsAcceptor::new(tls, incoming)).serve(router_service);
 
             #[cfg(unix)]
-            let server =
-                server.with_graceful_shutdown(signals::wait_for_signals(signals, grace_period));
+            let http2_server = http2_server
+                .with_graceful_shutdown(signals::wait_for_signals(signals, grace_period));
+
+            #[cfg(windows)]
+            let http2_cancel_recv = Arc::new(tokio::sync::Mutex::new(_cancel_recv));
+            #[cfg(windows)]
+            let redirect_cancel_recv = http2_cancel_recv.clone();
+
             #[cfg(windows)]
-            let server = server.with_graceful_shutdown(signals::wait_for_ctrl_c(
-                _cancel_recv,
-                _cancel_fn,
-                grace_period,
-            ));
+            let http2_ctrlc_recv = Arc::new(tokio::sync::Mutex::new(Some(receiver)));
+            #[cfg(windows)]
+            let redirect_ctrlc_recv = http2_ctrlc_recv.clone();
+
+            #[cfg(windows)]
+            let http2_server = http2_server.with_graceful_shutdown(async move {
+                if general.windows_service {
+                    signals::wait_for_ctrl_c(http2_cancel_recv, grace_period).await;
+                } else {
+                    signals::wait_for_ctrl_c(http2_ctrlc_recv, grace_period).await;
+                }
+            });
 
             tracing::info!(
                 parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
-                "listening on https://{}",
+                "http2 server is listening on https://{}",
                 addr_str
             );
 
-            tracing::info!("press ctrl+c to shut down the server");
+            // HTTP to HTTPS redirect server
+            if general.https_redirect {
+                let ip = general
+                    .host
+                    .parse::<IpAddr>()
+                    .with_context(|| format!("failed to parse {} address", general.host))?;
+                let addr = SocketAddr::from((ip, general.https_redirect_from_port));
+                let tcp_listener = TcpListener::bind(addr)
+                    .with_context(|| format!("failed to bind to {addr} address"))?;
+                tracing::info!(
+                    parent: tracing::info_span!("Server::start_server", ?addr, ?threads),
+                    "http1 redirect server is listening on http://{}",
+                    addr
+                );
+                tcp_listener
+                    .set_nonblocking(true)
+                    .with_context(|| "failed to set TCP non-blocking mode")?;
+
+                #[cfg(unix)]
+                let redirect_signals = signals::create_signals()
+                    .with_context(|| "failed to register termination signals")?;
+                #[cfg(unix)]
+                let redirect_handle = redirect_signals.handle();
+
+                // Allowed redirect hosts
+                let redirect_allowed_hosts = general
+                    .https_redirect_from_hosts
+                    .split(',')
+                    .map(|s| s.trim().to_owned())
+                    .collect::<Vec<_>>();
+                if redirect_allowed_hosts.is_empty() {
+                    bail!("https redirect allowed hosts is empty, provide at least one host or IP")
+                }
 
-            server.await?;
+                let redirect_opts = Arc::new(https_redirect::RedirectOpts {
+                    https_hostname: general.https_redirect_host,
+                    https_port: general.port,
+                    allowed_hosts: redirect_allowed_hosts,
+                });
+
+                let server_redirect = HyperServer::from_tcp(tcp_listener)
+                    .unwrap()
+                    .tcp_nodelay(true)
+                    .serve(make_service_fn(move |_: &AddrStream| {
+                        let redirect_opts = redirect_opts.clone();
+                        let page404 = page404.clone();
+                        let page50x = page50x.clone();
+                        async move {
+                            Ok::<_, error::Error>(service_fn(move |req| {
+                                let redirect_opts = redirect_opts.clone();
+                                let page404 = page404.clone();
+                                let page50x = page50x.clone();
+                                async move {
+                                    let uri = req.uri();
+                                    let method = req.method();
+                                    match https_redirect::redirect_to_https(&req, redirect_opts)
+                                        .await
+                                    {
+                                        Ok(resp) => Ok(resp),
+                                        Err(status) => error_page::error_response(
+                                            uri, method, &status, &page404, &page50x,
+                                        ),
+                                    }
+                                }
+                            }))
+                        }
+                    }));
+
+                #[cfg(unix)]
+                let server_redirect = server_redirect.with_graceful_shutdown(
+                    signals::wait_for_signals(redirect_signals, grace_period),
+                );
+                #[cfg(windows)]
+                let server_redirect = server_redirect.with_graceful_shutdown(async move {
+                    if general.windows_service {
+                        signals::wait_for_ctrl_c(redirect_cancel_recv, grace_period).await;
+                    } else {
+                        signals::wait_for_ctrl_c(redirect_ctrlc_recv, grace_period).await;
+                    }
+                });
+
+                // HTTP/2 server task
+                let server_task = tokio::spawn(async move {
+                    if let Err(err) = http2_server.await {
+                        tracing::error!("http2 server failed to start up: {:?}", err);
+                        std::process::exit(1)
+                    }
+                });
+
+                // HTTP/1 redirect server task
+                let redirect_server_task = tokio::spawn(async move {
+                    if let Err(err) = server_redirect.await {
+                        tracing::error!("http1 redirect server failed to start up: {:?}", err);
+                        std::process::exit(1)
+                    }
+                });
+
+                tracing::info!("press ctrl+c to shut down the servers");
+
+                #[cfg(windows)]
+                tokio::try_join!(ctrlc_task, server_task, redirect_server_task)?;
+                #[cfg(unix)]
+                tokio::try_join!(server_task, redirect_server_task)?;
+
+                #[cfg(unix)]
+                redirect_handle.close();
+            } else {
+                tracing::info!("press ctrl+c to shut down the server");
+                http2_server.await?;
+            }
 
             #[cfg(unix)]
             handle.close();
 
+            #[cfg(windows)]
+            _cancel_fn();
+
             tracing::warn!("termination signal caught, shutting down the server execution");
             return Ok(());
         }
@@ -335,30 +492,41 @@ impl Server {
             .set_nonblocking(true)
             .with_context(|| "failed to set TCP non-blocking mode")?;
 
-        let server = HyperServer::from_tcp(tcp_listener)
+        let http1_server = HyperServer::from_tcp(tcp_listener)
             .unwrap()
             .tcp_nodelay(true)
             .serve(router_service);
 
         #[cfg(unix)]
-        let server =
-            server.with_graceful_shutdown(signals::wait_for_signals(signals, grace_period));
+        let http1_server =
+            http1_server.with_graceful_shutdown(signals::wait_for_signals(signals, grace_period));
+
+        #[cfg(windows)]
+        let http1_cancel_recv = Arc::new(tokio::sync::Mutex::new(_cancel_recv));
+        #[cfg(windows)]
+        let http1_ctrlc_recv = Arc::new(tokio::sync::Mutex::new(Some(receiver)));
+
         #[cfg(windows)]
-        let server = server.with_graceful_shutdown(signals::wait_for_ctrl_c(
-            _cancel_recv,
-            _cancel_fn,
-            grace_period,
-        ));
+        let http1_server = http1_server.with_graceful_shutdown(async move {
+            if general.windows_service {
+                signals::wait_for_ctrl_c(http1_cancel_recv, grace_period).await;
+            } else {
+                signals::wait_for_ctrl_c(http1_ctrlc_recv, grace_period).await;
+            }
+        });
 
         tracing::info!(
             parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
-            "listening on http://{}",
+            "http1 server is listening on http://{}",
             addr_str
         );
 
         tracing::info!("press ctrl+c to shut down the server");
 
-        server.await?;
+        http1_server.await?;
+
+        #[cfg(windows)]
+        _cancel_fn();
 
         #[cfg(unix)]
         handle.close();
diff --git a/src/settings/cli.rs b/src/settings/cli.rs
index a50ac5c1..2c686d93 100644
--- a/src/settings/cli.rs
+++ b/src/settings/cli.rs
@@ -26,7 +26,7 @@ pub struct General {
         long,
         short = "f",
         env = "SERVER_LISTEN_FD",
-        conflicts_with_all(&["host", "port"])
+        conflicts_with_all(&["host", "port", "https_redirect"])
     )]
     /// Instead of binding to a TCP port, accept incoming connections to an already-bound TCP
     /// socket listener on the specified file descriptor number (usually zero). Requires that the
@@ -160,6 +160,47 @@ pub struct General {
     /// Specify the file path to read the private key.
     pub http2_tls_key: Option<PathBuf>,
 
+    #[structopt(
+        long,
+        required_if("http2", "true"),
+        parse(try_from_str),
+        default_value = "false",
+        env = "SERVER_HTTPS_REDIRECT"
+    )]
+    #[cfg(feature = "http2")]
+    /// Redirect all requests with scheme "http" to "https" for the current server instance. It depends on "http2" to be enabled.
+    pub https_redirect: bool,
+
+    #[structopt(
+        long,
+        required_if("https_redirect", "true"),
+        default_value = "localhost",
+        env = "HTTPS_REDIRECT_HOST"
+    )]
+    #[cfg(feature = "http2")]
+    /// Canonical host name or IP of the HTTPS (HTTPS/2) server. It depends on "https_redirect" to be enabled.
+    pub https_redirect_host: String,
+
+    #[structopt(
+        long,
+        required_if("https_redirect", "true"),
+        default_value = "80",
+        env = "HTTPS_REDIRECT_FROM_PORT"
+    )]
+    #[cfg(feature = "http2")]
+    /// HTTP host port where the redirect server will listen for requests to redirect them to HTTPS. It depends on "https_redirect" to be enabled.
+    pub https_redirect_from_port: u16,
+
+    #[structopt(
+        long,
+        required_if("https_redirect", "true"),
+        default_value = "localhost",
+        env = "HTTPS_REDIRECT_FROM_HOSTS"
+    )]
+    #[cfg(feature = "http2")]
+    /// List of host names or IPs allowed to redirect from. HTTP requests must contain the HTTP 'Host' header and match against this list. It depends on "https_redirect" to be enabled.
+    pub https_redirect_from_hosts: String,
+
     #[cfg(feature = "compression")]
     #[cfg_attr(docsrs, doc(cfg(feature = "compression")))]
     #[structopt(
diff --git a/src/settings/file.rs b/src/settings/file.rs
index 97e11a75..5c23403b 100644
--- a/src/settings/file.rs
+++ b/src/settings/file.rs
@@ -141,6 +141,19 @@ pub struct General {
     #[cfg(feature = "http2")]
     pub http2_tls_key: Option<PathBuf>,
 
+    /// Redirect all HTTP requests to HTTPS.
+    #[cfg(feature = "http2")]
+    pub https_redirect: Option<bool>,
+    /// HTTP host port where the redirect server will listen for requests to redirect them to HTTPS.
+    #[cfg(feature = "http2")]
+    pub https_redirect_host: Option<String>,
+    /// Host port for redirecting HTTP requests to HTTPS.
+    #[cfg(feature = "http2")]
+    pub https_redirect_from_port: Option<u16>,
+    /// List of host names or IPs allowed to redirect from.
+    #[cfg(feature = "http2")]
+    pub https_redirect_from_hosts: Option<String>,
+
     /// Security headers.
     pub security_headers: Option<bool>,
 
diff --git a/src/settings/mod.rs b/src/settings/mod.rs
index 1d60d2e8..51cacaf0 100644
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -91,6 +91,14 @@ impl Settings {
         let mut http2_tls_cert = opts.http2_tls_cert;
         #[cfg(feature = "http2")]
         let mut http2_tls_key = opts.http2_tls_key;
+        #[cfg(feature = "http2")]
+        let mut https_redirect = opts.https_redirect;
+        #[cfg(feature = "http2")]
+        let mut https_redirect_host = opts.https_redirect_host;
+        #[cfg(feature = "http2")]
+        let mut https_redirect_from_port = opts.https_redirect_from_port;
+        #[cfg(feature = "http2")]
+        let mut https_redirect_from_hosts = opts.https_redirect_from_hosts;
         let mut security_headers = opts.security_headers;
         let mut cors_allow_origins = opts.cors_allow_origins;
         let mut cors_allow_headers = opts.cors_allow_headers;
@@ -173,6 +181,22 @@ impl Settings {
                     if let Some(v) = general.http2_tls_key {
                         http2_tls_key = Some(v)
                     }
+                    #[cfg(feature = "http2")]
+                    if let Some(v) = general.https_redirect {
+                        https_redirect = v
+                    }
+                    #[cfg(feature = "http2")]
+                    if let Some(v) = general.https_redirect_host {
+                        https_redirect_host = v
+                    }
+                    #[cfg(feature = "http2")]
+                    if let Some(v) = general.https_redirect_from_port {
+                        https_redirect_from_port = v
+                    }
+                    #[cfg(feature = "http2")]
+                    if let Some(v) = general.https_redirect_from_hosts {
+                        https_redirect_from_hosts = v
+                    }
                     if let Some(v) = general.security_headers {
                         security_headers = v
                     }
@@ -342,6 +366,14 @@ impl Settings {
                 http2_tls_cert,
                 #[cfg(feature = "http2")]
                 http2_tls_key,
+                #[cfg(feature = "http2")]
+                https_redirect,
+                #[cfg(feature = "http2")]
+                https_redirect_host,
+                #[cfg(feature = "http2")]
+                https_redirect_from_port,
+                #[cfg(feature = "http2")]
+                https_redirect_from_hosts,
                 security_headers,
                 cors_allow_origins,
                 cors_allow_headers,
diff --git a/src/signals.rs b/src/signals.rs
index 9679a26f..3252ba4e 100644
--- a/src/signals.rs
+++ b/src/signals.rs
@@ -15,7 +15,7 @@ use {
 };
 
 #[cfg(windows)]
-use tokio::sync::oneshot::Receiver;
+use {std::sync::Arc, tokio::sync::watch::Receiver, tokio::sync::Mutex};
 
 #[cfg(unix)]
 /// It creates a common list of signals stream for `SIGTERM`, `SIGINT` and `SIGQUIT` to be observed.
@@ -59,22 +59,9 @@ async fn delay_graceful_shutdown(grace_period_secs: u8) {
 
 #[cfg(windows)]
 /// It waits for an incoming `ctrl+c` signal on Windows.
-pub async fn wait_for_ctrl_c<F>(
-    cancel_recv: Option<Receiver<()>>,
-    cancel_fn: F,
-    grace_period_secs: u8,
-) where
-    F: FnOnce(),
-{
-    if let Some(recv) = cancel_recv {
-        if let Err(err) = recv.await {
-            tracing::error!("error during cancel recv: {:?}", err)
-        }
-        cancel_fn()
-    } else {
-        tokio::signal::ctrl_c()
-            .await
-            .expect("failed to install ctrl+c signal handler");
+pub async fn wait_for_ctrl_c(cancel_recv: Arc<Mutex<Option<Receiver<()>>>>, grace_period_secs: u8) {
+    if let Some(receiver) = &mut *cancel_recv.lock().await {
+        receiver.changed().await.ok();
     }
 
     delay_graceful_shutdown(grace_period_secs).await;
diff --git a/src/winservice.rs b/src/winservice.rs
index 08983857..ff7a81fe 100644
--- a/src/winservice.rs
+++ b/src/winservice.rs
@@ -79,7 +79,7 @@ fn run_service() -> Result {
     tracing::info!("windows service: starting service setup");
 
     // Create a channel to be able to poll a stop event from the service worker loop.
-    let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
+    let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(());
     let mut shutdown_tx = Some(shutdown_tx);
 
     // Define system service event handler that will be receiving service events.
@@ -169,7 +169,7 @@ pub fn run_server_as_service() -> Result {
 
     // Register generated `ffi_service_main` with the system and start the
     // service, blocking this thread until the service is stopped
-    service_dispatcher::start(&SERVICE_NAME, ffi_service_main)
+    service_dispatcher::start(SERVICE_NAME, ffi_service_main)
         .with_context(|| "error registering generated `ffi_service_main` with the system")?;
     Ok(())
 }


Test patch:
diff --git a/tests/toml/config.toml b/tests/toml/config.toml
index db3e0e58..70befff7 100644
--- a/tests/toml/config.toml
+++ b/tests/toml/config.toml
@@ -2,8 +2,8 @@
 
 #### Address & Root dir
 host = "::"
-port = 8787
-root = "tests/fixtures/public"
+port = 4433
+root = "docker/public"
 
 #### Logging
 log-level = "trace"
@@ -20,11 +20,15 @@ page50x = "docker/public/50x.html"
 
 #### HTTP/2 + TLS
 http2 = false
-http2-tls-cert = ""
-http2-tls-key = ""
+http2-tls-cert = "tests/tls/local.dev_cert.ecc.pem"
+http2-tls-key = "tests/tls/local.dev_key.ecc.pem"
+https-redirect = false
+https-redirect-host = "localhost"
+https-redirect-from-port = 80
+https-redirect-from-hosts = "localhost, 127.0.0.1"
 
 #### CORS & Security headers
-security-headers = true
+# security-headers = true
 cors-allow-origins = ""
 
 #### Directory listing