diff --git a/.env.example b/.env.example
index 90e281c..1da874c 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,11 @@
-DATABASE_URL= // the url to the database ( example local postgres: postgres://postgres:postgres@localhost/findthetime )
-EVENT_UID_SIZE= // size of snowflake ids for events ( 20 )
-LOG_LEVEL= // see https://docs.rs/tracing-subscriber/0.3.15/tracing_subscriber/struct.EnvFilter.html#. Default is info
\ No newline at end of file
+DATABASE_URL= # the url to the database ( example local postgres: postgres://postgres:postgres@localhost/findthetime )
+EVENT_UID_SIZE= # size of snowflake ids for events ( 20 )
+LOG_LEVEL= # see https://docs.rs/tracing-subscriber/0.3.15/tracing_subscriber/struct.EnvFilter.html#. Default is info
+
+HTTP_PORT= # 8080 by default
+
+SSL_ENABLED= # false by default
+SSL_REDIRECT= # true by default
+SSL_PORT= # 8443 by default
+SSL_CERT_PATH= # no default, if SSL_ENABLED is true, this must be configured, or the backend will panic. In a docker environment, this is configured by default ( see Dockerfile )
+SSL_KEY_PATH= # no default, if SSL_ENABLED is true, this must be configured, or the backend will panic. In a docker environment, this is configured by default ( see Dockerfile )
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 0b745e2..f5cddd1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/target
-.env
\ No newline at end of file
+.env
+.self-signed
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index a6515c2..de52c55 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -60,6 +60,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "arc-swap"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
+
[[package]]
name = "async-trait"
version = "0.1.77"
@@ -164,6 +170,29 @@ dependencies = [
"syn 2.0.48",
]
+[[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",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "pin-project-lite",
+ "rustls",
+ "rustls-pemfile",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-service",
+]
+
[[package]]
name = "backtrace"
version = "0.3.69"
@@ -426,9 +455,10 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "findtheti-me"
-version = "0.1.4"
+version = "0.1.7"
dependencies = [
"axum",
+ "axum-server",
"chrono",
"dotenv",
"env_logger",
@@ -1308,6 +1338,20 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+[[package]]
+name = "ring"
+version = "0.17.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74"
+dependencies = [
+ "cc",
+ "getrandom",
+ "libc",
+ "spin 0.9.8",
+ "untrusted",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "rsa"
version = "0.9.6"
@@ -1347,6 +1391,44 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "rustls"
+version = "0.21.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
+dependencies = [
+ "log",
+ "ring",
+ "rustls-webpki",
+ "sct",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4"
+dependencies = [
+ "base64",
+ "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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
[[package]]
name = "rustversion"
version = "1.0.14"
@@ -1365,6 +1447,16 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "sct"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
[[package]]
name = "serde"
version = "1.0.195"
@@ -1861,6 +1953,16 @@ dependencies = [
"syn 2.0.48",
]
+[[package]]
+name = "tokio-rustls"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
[[package]]
name = "tokio-stream"
version = "0.1.14"
@@ -2049,6 +2151,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
[[package]]
name = "url"
version = "2.5.0"
diff --git a/Cargo.toml b/Cargo.toml
index 49e21bb..0f1c224 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,12 +1,13 @@
[package]
name = "findtheti-me"
-version = "0.1.4"
+version = "0.1.7"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = { version = "0.7.3", features = ["macros", "tokio"] }
+axum-server = { version = "0.6.0", features = ["tls-rustls"] }
chrono = { version = "0.4.31", features = ["serde"] }
dotenv = "0.15.0"
env_logger = "0.10.1"
@@ -16,6 +17,6 @@ serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
sqlx = { version = "0.7.3", features = ["runtime-tokio", "postgres", "chrono"] }
tokio = {version = "1.35.1", features = ["macros", "rt-multi-thread", "rt", "net"]}
-tower-http = { version = "0.5.0", features = ["fs", "trace"] }
+tower-http = { version = "0.5.0", features = ["fs", "trace", "cors"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "std"] }
diff --git a/Dockerfile b/Dockerfile
index e645f23..b2d3ea0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -54,8 +54,14 @@ USER appuser
ENV LOG_LEVEL=info
ENV EVENT_UID_SIZE=20
+ENV HTTP_PORT=8080
+
+ENV SSL_ENABLED=false
+ENV SSL_REDIRECT=true
+ENV SSL_PORT=8443
+ENV SSL_CERT_PATH=/etc/findtheti-me/certs/server.cert
+ENV SSL_KEY_PATH=/etc/findtheti-me/certs/server.key
+
WORKDIR ./findtheti-me
-ENTRYPOINT ["./findtheti-me"]
-
-EXPOSE 8080/tcp
\ No newline at end of file
+CMD ["./findtheti-me"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 4ccf2c1..e67c927 100644
--- a/README.md
+++ b/README.md
@@ -9,15 +9,27 @@ Also, it is only compatible with PostgreSQL at the moment. It is required to hav
### Simple (With Docker)
-To use `findtheti.me` with docker, simply run
+#### Without SSL
+
```sh
docker run
-e DATABASE_URL='postgresql://{postgres user}:{postgres password}@{postgres host}/{postgres database}'
-p {port to run on}:8080
- mvv97/findthetime
+ mvv97/findthetime:latest
```
-#### Example docker-compose.yml
+#### With SSL
+```sh
+docker run
+ -e DATABASE_URL='postgresql://{postgres user}:{postgres password}@{postgres host}/{postgres database}'
+ -e SSL_ENABLED='true'
+ -v /data/findtheti-me/certs:/etc/findtheti-me/certs # Place your cert files in /data/findtheti-me/certs and ensure they have permissions of at least 644
+ -p {http port to run on}:8080 # if SSL_REDIRECT=false, this can be skipped
+ -p {ssl port to run on}:8443
+ mvv97/findthetime:latest
+```
+
+### Example docker-compose.yml
```yml
version: "3.4"
@@ -38,10 +50,20 @@ services:
restart: unless-stopped
environment:
DATABASE_URL: "postgres://${PG_USER:-findthetime}:${PG_PASS}@postgresql/${PG_DB:-findthetime}"
+ SSL_ENABLED: 'true' # when this is set to false ( default ), the ssl port is not listened to.
+ SSL_REDIRECT: 'true'
+ SSL_PORT: '8443'
+ SSL_CERT_PATH: '/etc/findtheti-me/certs/server.cert'
+ SSL_KEY_PATH: '/etc/findtheti-me/certs/server.key'
+ volumes:
+ - '/data/findtheti-me/certs:/etc/findtheti-me/certs'
ports:
- '8080:8080'
+ - '8443:8443'
```
+Ensure you have the necessary environment variables configured: `PG_DB`, `PG_USER` and `PG_PASS`.
+
### Advanced (Without Docker)
1. Compile Backend (`cargo build --release`)
@@ -60,6 +82,21 @@ installationDir/
Finally, run `./findtheti-me` in the root, and the application should start.
+### Enable SSL
+
+In order to enable SSL, configure `SSL_ENABLED=true`, `SSL_PORT` with the desired port ( `8443` by default ), and `SSL_CERT_PATH` and `SSL_KEY_PATH`
+with the paths to your certificate and key files ( `/etc/letsencrypt/live/your.domain/cert.pem` and `/etc/letsencrypt/live/your.domain/key.pem`, for example ).
+Ensure the permissions of these files are at least `644`, as the container user will need to be able to read them.
+
+**Note that there is currently no support for encrypted private keys ( those that start with `-----BEGIN ENCRYPTED PRIVATE KEY-----`).
+Attempting to use such will be met with the error:**
+
+```
+Unable to use files configured in 'SSL_CERT_PATH' or 'SSL_KEY_PATH': Custom { kind: Other, error: "private key format not supported" }
+```
+`findtheti.me` will automatically register a listener at the configured `HTTP_PORT` to redirect
+to the configured `SSL_PORT`. To disable this, configure `SSL_REDIRECT=false` ( `true` by default ).
+
## Setup For Development
### Backend
1. Create a PostgreSQL database
@@ -69,7 +106,8 @@ Finally, run `./findtheti-me` in the root, and the application should start.
### Frontend
1. `yarn install`
-2. `yarn dev` ( or `yarn build`/`yarn preview` )
+2. If using SSL on the backend, change the proxy in `vite.config.ts` to reflect that.
+3. `yarn dev` ( or `yarn build`/`yarn preview` )
### Docker Build Image
1. Do Backend and Frontend setups first
diff --git a/docker-compose.yml b/docker-compose.yml
index a728a24..58f4f19 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,6 +3,7 @@ version: "3.4"
services:
postgresql:
+ container_name: db
image: "docker.io/library/postgres:16-alpine"
restart: unless-stopped
volumes:
@@ -13,9 +14,17 @@ services:
POSTGRES_DB: ${PG_DB:-findthetime}
findthetime:
- image: "docker.io/mvv97/findthetime:latest"
+ build: .
restart: unless-stopped
environment:
- DATABASE_URL: "postgres://${PG_USER:-findthetime}:${PG_PASS}@postgresql/${PG_DB:-findthetime}"
+ DATABASE_URL: "postgres://${PG_USER:-findthetime}:${PG_PASS}@db/${PG_DB:-findthetime}"
+ SSL_ENABLED: 'true' # when this is set to false ( default ), the ssl port is not listened to.
+ SSL_REDIRECT: 'true'
+ SSL_PORT: '8443'
+ SSL_CERT_PATH: '/etc/findtheti-me/certs/server.cert'
+ SSL_KEY_PATH: '/etc/findtheti-me/certs/server.key'
+ volumes:
+ - '/home/haedhutner/Workspace/findtheti-me/.self-signed:/etc/findtheti-me/certs'
ports:
- - '8080:8080'
\ No newline at end of file
+ - '8080:8080'
+ - '8443:8443'
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 3e6d8c6..e068894 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -4,6 +4,7 @@ import RootLayout from './pages/RootLayout'
import NewEventPage from './pages/NewEventPage'
import ExistingEventPage from './pages/ExistingEventPage'
import ThankYouPage from './pages/ThankYouPage'
+import NotFoundPage from './pages/NotFoundPage'
function App() {
return (
@@ -12,6 +13,7 @@ function App() {
} />
} />
} />
+ } />
)
diff --git a/frontend/src/pages/NotFoundPage.tsx b/frontend/src/pages/NotFoundPage.tsx
new file mode 100644
index 0000000..a4d6659
--- /dev/null
+++ b/frontend/src/pages/NotFoundPage.tsx
@@ -0,0 +1,17 @@
+import { Typography } from '@mui/material';
+import Grid from '@mui/material/Unstable_Grid2';
+
+const NotFoundPage = () => {
+ return (
+
+
+ 404 Not Found!
+
+
+ Not sure what you were looking for, but you won't find it here.
+
+
+ );
+}
+
+export default NotFoundPage;
\ No newline at end of file
diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts
index fc311b5..8a57cda 100644
--- a/frontend/src/utils.ts
+++ b/frontend/src/utils.ts
@@ -12,6 +12,13 @@ const utils = {
performRequest: (url: string | URL | Request, options?: RequestInit | undefined): Promise => {
return fetch(url, options).then(async resp => {
if (!resp.ok) {
+
+ if (resp.status === 404) {
+ window.location.replace(`${window.location.origin}/not-found`)
+
+ throw 'Not Found';
+ }
+
let errorTextResult = await resp.text();
var errorMsg = errorTextResult;
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 21d76fe..b6cbfd8 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -9,7 +9,7 @@ export default defineConfig({
server: {
proxy: {
"/api": {
- target: "http://localhost:8080",
+ target: "https://localhost:8443",
changeOrigin: true,
secure: false,
},
diff --git a/src/api.rs b/src/api.rs
index 6e1cf88..e198daf 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -14,7 +14,7 @@ use axum::{
use serde::Serialize;
use sqlx::{migrate::MigrateError, PgPool};
-use crate::endpoints;
+use crate::{endpoints, config};
pub(crate) async fn routes() -> Result {
Ok(Router::new()
@@ -107,9 +107,7 @@ impl AppState {
pool
},
- event_uid_size: env::var("EVENT_UID_SIZE")?
- .parse()
- .expect("EVENT_UID_SIZE is undefined. Must be a number."),
+ event_uid_size: config::get_or_default("EVENT_UID_SIZE", 20),
})
}
}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..cccb84a
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,32 @@
+use std::{env, str::FromStr};
+
+use axum::http::StatusCode;
+
+use crate::api::ApplicationError;
+
+pub const DEFAULT_HTTP_PORT: u16 = 8080;
+pub const DEFAULT_SSL_PORT: u16 = 8443;
+
+pub const DEFAULT_SSL_ENABLED: bool = false;
+pub const DEFAULT_SSL_REDIRECT: bool = true;
+
+pub fn get(name: &str, fail_status_code: StatusCode) -> Result where T: FromStr {
+ match env::var(name).map(|r| r.parse()) {
+ Ok(Ok(a)) => Ok(a),
+ _ => Err(ApplicationError::new(format!("Unabled to get or parse environment variable '{}'.", name), fail_status_code)),
+ }
+}
+
+pub fn get_or_default(name: &str, default: T) -> T where T: FromStr {
+ match env::var(name).map(|r| r.parse()) {
+ Ok(Ok(a)) => a,
+ _ => default,
+ }
+}
+
+pub fn get_or_panic(name: &str) -> T where T: FromStr {
+ match get(name, StatusCode::INTERNAL_SERVER_ERROR) {
+ Ok(r) => r,
+ Err(e) => panic!("{}", e),
+ }
+}
\ No newline at end of file
diff --git a/src/endpoints.rs b/src/endpoints.rs
index 52f7ade..e6eee65 100644
--- a/src/endpoints.rs
+++ b/src/endpoints.rs
@@ -156,7 +156,11 @@ pub async fn fetch_event(
let res = conn
.transaction(|txn| {
Box::pin(async move {
- let event = db::fetch_event_by_snowflake_id(txn, event_snowflake_id).await?;
+ let event = match db::fetch_event_by_snowflake_id(txn, event_snowflake_id).await {
+ Ok(e) => e,
+ Err(sqlx::Error::RowNotFound) => return Err(ApplicationError::new("No such event found".to_string(), StatusCode::NOT_FOUND)),
+ Err(e) => return Err(e.into())
+ };
Ok(EventDto {
snowflake_id: event.snowflake_id,
diff --git a/src/main.rs b/src/main.rs
index 2287bc2..43754cf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,22 +1,28 @@
use std::net::SocketAddr;
-
-use axum::Router;
+use axum::extract::Host;
+use axum::handler::HandlerWithoutStateExt;
+use axum::http::uri::Scheme;
+use axum::response::Redirect;
+use axum::{Router, BoxError};
+use axum::http::{Uri, StatusCode};
+use axum_server::tls_rustls::RustlsConfig;
use dotenv::dotenv;
-use tokio::net::TcpListener;
use tower_http::services::ServeFile;
use tower_http::trace::TraceLayer;
use tower_http::{services::ServeDir, trace};
-use tracing::Level;
+use tracing::{Level, instrument, info, warn};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
mod api;
mod db;
mod endpoints;
mod entity;
+mod config;
+#[instrument(name = "findtheti-me")]
#[tokio::main(flavor = "current_thread")]
async fn main() {
- println!("Starting findtheti.me...");
+ info!("Starting findtheti.me...");
dotenv().ok();
@@ -25,37 +31,112 @@ async fn main() {
.with(EnvFilter::from_env("LOG_LEVEL"))
.init();
+ let router = init_router()
+ .await
+ .layer(
+ TraceLayer::new_for_http()
+ .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
+ .on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
+ )
+ .into_make_service_with_connect_info::();
+
+ let http_port = config::get_or_default("HTTP_PORT", config::DEFAULT_HTTP_PORT);
+
+ let ssl_enabled = config::get_or_default("SSL_ENABLED", config::DEFAULT_SSL_ENABLED);
+
+ if ssl_enabled {
+ info!("SSL marked as enabled, will create http to https redirect.");
+
+ let (ssl_port, ssl_redirect, ssl_cert_path, ssl_key_path): (u16, bool, String, String) = (
+ config::get_or_default("SSL_PORT", config::DEFAULT_SSL_PORT),
+ config::get_or_default("SSL_REDIRECT", config::DEFAULT_SSL_REDIRECT),
+ config::get_or_panic("SSL_CERT_PATH"),
+ config::get_or_panic("SSL_KEY_PATH")
+ );
+
+ let ssl_config = match RustlsConfig::from_pem_file(ssl_cert_path, ssl_key_path).await {
+ Ok(c) => c,
+ Err(e) => {
+ panic!("Unable to use files configured in 'SSL_CERT_PATH' or 'SSL_KEY_PATH': {:?}", e);
+ },
+ };
+
+ if ssl_redirect {
+ tokio::spawn(redirect_http_to_https(http_port, ssl_port));
+ }
+
+ let addr = SocketAddr::from(([0, 0, 0, 0], ssl_port));
+
+ info!("Listening on {}", addr);
+
+ axum_server::bind_rustls(addr, ssl_config)
+ .serve(router)
+ .await
+ .unwrap()
+ } else {
+ let addr = SocketAddr::from(([0, 0, 0, 0], http_port));
+
+ info!("Listening on {}", addr);
+
+ axum_server::bind(addr).serve(router).await.unwrap()
+ }
+}
+
+
+async fn init_router() -> Router {
let api_routes = api::routes().await.expect("Unable to create api routes");
let mut routes = Router::new().nest("/api", api_routes);
- // If in release mod, serve static files
+ // If in release mode, serve static files
if !cfg!(debug_assertions) {
- println!("Initializing frontend routes...");
+ info!("Initializing frontend routes...");
routes = routes
.nest_service("/assets", ServeDir::new("./frontend/dist/assets"))
.fallback_service(ServeFile::new("./frontend/dist/index.html"));
}
- println!("Routes initialized...");
+ info!("Routes initialized...");
- let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
-
- let listener = TcpListener::bind(addr).await.unwrap();
-
- println!("Starting server...");
-
- axum::serve(
- listener,
- routes
- .layer(
- TraceLayer::new_for_http()
- .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
- .on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
- )
- .into_make_service_with_connect_info::(),
- )
- .await
- .unwrap();
+ routes
}
+
+async fn redirect_http_to_https(http_port: u16, https_port: u16) {
+ fn make_https(host: String, uri: Uri, http_port: u16, https_port: u16) -> 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.to_string(), &https_port.to_string());
+ 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, http_port, https_port) {
+ Ok(uri) => Ok(Redirect::permanent(&uri.to_string())),
+ Err(error) => {
+
+ warn!(%error, "failed to convert URI to HTTPS");
+
+ Err(StatusCode::BAD_REQUEST)
+ }
+ }
+ };
+
+ let addr = SocketAddr::from(([0, 0, 0, 0], http_port));
+
+ let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
+
+ info!("HTTPS redirect listening on {}", listener.local_addr().unwrap());
+
+ axum::serve(listener, redirect.into_make_service())
+ .await
+ .unwrap();
+}
\ No newline at end of file