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,
}