From 0794bea31de960c24b0aee2fa404165e4b96f42f Mon Sep 17 00:00:00 2001 From: MedzikUser Date: Mon, 19 Sep 2022 21:13:59 +0200 Subject: [PATCH] feat(server): add tls/ssl support Added support for https server. --- .gitignore | 4 +++ Cargo.lock | 27 ++++++++++++++++ Cargo.toml | 1 + README.md | 19 ++++++++++++ config.toml | 22 +++++-------- src/server/mod.rs | 76 +++++++++++++++++++++++++++++++++++++++------ src/types/config.rs | 19 +++--------- 7 files changed, 131 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 1361d93..4bef88c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ *.db-shm *.db-wal +# Tls +cert.key +cert.pem + # IDE configs .idea .vscode diff --git a/Cargo.lock b/Cargo.lock index 600ed72..46ed0f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,12 @@ version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +[[package]] +name = "arc-swap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164" + [[package]] name = "async-compression" version = "0.3.14" @@ -155,6 +161,26 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-server" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba6170b61f7b086609dabcae68d2e07352539c6ef04a7c82980bdfa01a159d" +dependencies = [ + "arc-swap", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.66" @@ -627,6 +653,7 @@ version = "0.0.0-dev" dependencies = [ "anyhow", "axum", + "axum-server", "backtrace", "byte-unit", "crypto-utils", diff --git a/Cargo.toml b/Cargo.toml index 4f28cef..bb8a0e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ toml = "0.5" # HTTP server axum = { version = "0.6.0-rc.2", features = ["http2", "multipart"] } +axum-server = { version = "0.4", features = ["tls-rustls"] } tower-http = { version = "0.3", features = ["full"] } hyper = { version = "0.14", features = ["full"] } byte-unit = "4.0.14" diff --git a/README.md b/README.md index c3a9fd2..ac7d448 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,22 @@  

+ +## Documentation + +### 👨‍💻 Compile server + +```bash +cargo build --release +``` + +Now you can run server using command `./target/release/homedisk`. + +### 🔒 Generate development TLS certificate + +```bash +# Generate private key +openssl genrsa -out cert.key 204 +# Generate certificate +openssl req -new -x509 -key cert.key -out cert.pem -days 365 +``` diff --git a/config.toml b/config.toml index f267db3..7f1598f 100644 --- a/config.toml +++ b/config.toml @@ -1,20 +1,14 @@ [http] -# HTTP host host = "0.0.0.0" -# HTTP port -port = 8080 -# Cors domains -cors = [ - "127.0.0.1:8000", - "localhost:8000", -] +httpPort = 8080 # http server port (recommended 80) +httpsPort = 8443 # https server port (recommended 443) +cors = [ "localhost:8000" ] # CORS domains +tlsCert = "./cert.pem" # TLS certificate file +tlsKey = "./cert.key" # TLS key file [jwt] -# JWT Secret string (used to sign tokens) -secret = "secret key used to sign tokens" -# Token expiration time in hours -expires = 24 # one day +secret = "secret key used to sign tokens" # jsonwebtoken secret string used to sign tokens +expires = 24 # token expiration time in hours (default one day) [storage] -# Directory where user files will be stored -path = "/home/homedisk" +path = "/home/homedisk" # path to directory where user files will be stored diff --git a/src/server/mod.rs b/src/server/mod.rs index a32c969..9d544da 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -2,18 +2,32 @@ mod api; pub mod error; pub mod utils; +use std::path::PathBuf; + use anyhow::anyhow; -use axum::{http::HeaderValue, routing::get, Extension, Router, Server}; -use tower_http::cors::{AllowOrigin, CorsLayer}; -use tracing::info; +use axum::{ + extract::Host, + handler::HandlerWithoutStateExt, + http::{HeaderValue, StatusCode, Uri}, + response::Redirect, + routing::get, + Extension, Router, +}; +use axum_server::tls_rustls::RustlsConfig; +use tower_http::{ + cors::{AllowOrigin, CorsLayer}, + BoxError, +}; +use tracing::{debug, info}; use crate::{config::Config, database::Database}; pub async fn start_server(config: Config, db: Database) -> anyhow::Result<()> { - info!( - "🚀 Server has launched on http://{}:{}", - config.http.host, config.http.port - ); + let host = format!("{}:{}", config.http.host, config.http.https_port); + + tokio::spawn(redirect_http_to_https(config.clone())); + + info!("🚀 Server has launched on https://{host}"); // change the type from Vec to Vec so that the http server can correctly detect CORS hosts let origins = config @@ -23,7 +37,12 @@ pub async fn start_server(config: Config, db: Database) -> anyhow::Result<()> { .map(|e| e.parse().expect("Failed to parse CORS hosts")) .collect::>(); - let host = format!("{}:{}", config.http.host, config.http.port); + let tls_config = RustlsConfig::from_pem_file( + PathBuf::from("").join("").join(&config.http.tls_cert), + PathBuf::from("").join("").join(&config.http.tls_key), + ) + .await + .unwrap(); let app = Router::new() .nest("/api", api::app()) @@ -32,9 +51,48 @@ pub async fn start_server(config: Config, db: Database) -> anyhow::Result<()> { .layer(Extension(config)) .layer(Extension(db)); - Server::bind(&host.parse()?) + axum_server::bind_rustls(host.parse()?, tls_config) .serve(app.into_make_service()) .await?; Err(anyhow!("Server unexpected stopped!")) } + +async fn redirect_http_to_https(config: Config) { + fn make_https(host: String, uri: Uri, config: Config) -> Result { + let mut parts = uri.into_parts(); + + parts.scheme = Some(axum::http::uri::Scheme::HTTPS); + + if parts.path_and_query.is_none() { + parts.path_and_query = Some("/".parse().unwrap()); + } + + let https_host = host.replace( + &config.http.http_port.to_string(), + &config.http.https_port.to_string(), + ); + parts.authority = Some(https_host.parse()?); + + Ok(Uri::from_parts(parts)?) + } + + let host = format!("{}:{}", config.http.host, config.http.http_port); + + let redirect = move |Host(host): Host, uri: Uri| async move { + match make_https(host, uri, config) { + Ok(uri) => Ok(Redirect::permanent(&uri.to_string())), + Err(error) => { + tracing::warn!(%error, "Failed to convert URI to HTTPS"); + Err(StatusCode::BAD_REQUEST) + }, + } + }; + + debug!("🚀 Http redirect listening on http://{host}"); + + axum::Server::bind(&host.parse().unwrap()) + .serve(redirect.into_make_service()) + .await + .unwrap(); +} diff --git a/src/types/config.rs b/src/types/config.rs index 14b6ba5..33de56c 100644 --- a/src/types/config.rs +++ b/src/types/config.rs @@ -4,41 +4,32 @@ use std::fs; use serde::{Deserialize, Serialize}; -/// Settings configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { - /// HTTP Settings. pub http: ConfigHTTP, - /// Json Web Token Settings. pub jwt: ConfigJWT, - /// Storage Settings. pub storage: ConfigStorage, } -/// HTTP Settings. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct ConfigHTTP { - /// HTTP Host. pub host: String, - /// Port HTTP Port. - pub port: u16, - /// [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) Domains (e.g ["site1.example.com", "site2.example.com"]). + pub http_port: u16, + pub https_port: u16, pub cors: Vec, + pub tls_cert: String, + pub tls_key: String, } -/// Json Web Token Settings. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigJWT { - /// JWT Secret string (used to sign tokens). pub secret: String, - /// Token expiration time in hours. pub expires: i64, } -/// Storage Settings. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigStorage { - /// Directory where user files will be stored. pub path: String, }