From 6b1c34e27f1885538136ae94a0c2a48ee8aa110d Mon Sep 17 00:00:00 2001 From: thevickypedia Date: Sun, 18 Feb 2024 21:31:46 -0600 Subject: [PATCH] Take an optional argument to secure the `session-token` Update HTML templates and README.md --- README.md | 21 ++++++++++++++++++++- src/lib.rs | 7 ++++++- src/routes/auth.rs | 29 +++++++++++++++++++++-------- src/routes/video.rs | 6 +++--- src/squire/middleware.rs | 8 ++++---- src/squire/settings.rs | 22 ++++++++++++++-------- src/templates/logout.rs | 6 +++++- src/templates/session.rs | 6 +++++- src/templates/unauthorized.rs | 6 +++++- 9 files changed, 83 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index f1c054b..4c88c65 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,25 @@ async fn main() { #### Config file [RuStream][repo] requires a JSON file with secrets loaded as key-value paris. +**Mandatory** +- **authorization**: Dictionary of key-value pairs with `username` as key and `password` as value. +- **video_source**: Source path for video files. +> Files starting with `_` _(underscore)_ and `.` _(dot)_ will be ignored + +**Optional** +- **video_host**: IP address to host the video. Defaults to `127.0.0.1` / `localhost` +- **video_port**: Port number to host the application. Defaults to `8000` +- **session_duration**: Time _(in seconds)_ each authenticated session should last. Defaults to `3600` +- **file_formats**: Vector of supported video file formats. Defaults to `[.mp4, .mov]` +- **workers**: Number of workers to spin up for the server. Defaults to the number of physical cores. +- **max_connections**: Maximum number of concurrent connections per worker. Defaults to `3` +- **websites**: Vector of websites (_supports regex_) to add to CORS configuration. _Required only if tunneled via CDN_ +- **key_file**: Path to the private key file for SSL certificate. Defaults to `None` +- **cert_file**: Path to the full chain file for SSL certificate. Defaults to `None` +- **secure_session**: Boolean flag to secure the cookie `session_token`. Defaults to `false` +> If `SECURE_SESSION` to set to `true`, the cookie `session_token` will only be sent via HTTPS
+> This means that the server can **ONLY** be hosted via `HTTPS` or `localhost` +
Sample content of JSON file @@ -89,7 +108,7 @@ rustup component add clippy ``` ### Usage ```shell -cargo clippy --no-deps --fix --allow-dirty +cargo clippy --no-deps --fix ``` ## Docs diff --git a/src/lib.rs b/src/lib.rs index 7974b61..5c62fd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,11 @@ pub async fn start() -> io::Result<()> { println!("{}", arts.choose(&mut rand::thread_rng()).unwrap()); let config = squire::startup::get_config(args); + if config.secure_session { + log::warn!( + "Secure session is turned on! This means that the server can ONLY be hosted via HTTPS or localhost" + ); + } let template = jinja::environment(); // Create a dedicated clone, since it will be used within closure let config_clone = config.clone(); @@ -64,7 +69,7 @@ pub async fn start() -> io::Result<()> { App::new() // Creates a new Actix web application .app_data(web::Data::new(config_clone.clone())) .app_data(web::Data::new(template_clone.clone())) - .wrap(squire::middleware::get_cors(config_clone.website.clone())) + .wrap(squire::middleware::get_cors(config_clone.websites.clone())) .wrap(middleware::Logger::default()) // Adds a default logger middleware to the application .service(routes::basics::health) // Registers a service for handling requests .service(routes::basics::root) diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 7c5c67d..83ae845 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -51,13 +51,19 @@ pub async fn login(config: web::Data>, request: Ht let cookie_duration = Duration::seconds(config.session_duration as i64); let expiration = OffsetDateTime::now_utc() + cookie_duration; - let cookie = Cookie::build("session_token", encrypted_payload) + let base_cookie = Cookie::build("session_token", encrypted_payload) .http_only(true) .same_site(SameSite::Strict) .max_age(cookie_duration) - .expires(expiration) - .finish(); + .expires(expiration); + let cookie; + if config.secure_session { + log::info!("Marking 'session_token' cookie as secure!!"); + cookie = base_cookie.secure(true).finish(); + } else { + cookie = base_cookie.finish(); + } log::info!("Session for '{}' will be valid until {}", mapped.get("username").unwrap(), expiration); let mut response = HttpResponse::Ok().json(RedirectResponse { @@ -136,7 +142,7 @@ pub async fn home(config: web::Data>, request: HttpRequest) -> HttpResponse { let auth_response = squire::authenticator::verify_token(&request, &config); if !auth_response.ok { - return failed_auth(auth_response); + return failed_auth(auth_response, &config); } squire::logger::log_connection(&request); log::debug!("{}", auth_response.detail); @@ -191,16 +197,23 @@ pub async fn error(environment: web::Data HttpResponse { +pub fn failed_auth(auth_response: squire::authenticator::AuthToken, + config: &squire::settings::Config) -> HttpResponse { let mut response = HttpResponse::build(StatusCode::FOUND); let detail = auth_response.detail; let age = Duration::new(3, 0); - let cookie = Cookie::build("detail", detail) + let base_cookie = Cookie::build("detail", detail) .path("/error") .http_only(true) .same_site(SameSite::Strict) - .max_age(age) - .finish(); + .max_age(age); + let cookie; + if config.secure_session { + log::debug!("Marking 'detail' cookie as secure!!"); + cookie = base_cookie.secure(true).finish(); + } else { + cookie = base_cookie.finish(); + } response.cookie(cookie); response.append_header(("Location", "/error")); response.finish() diff --git a/src/routes/video.rs b/src/routes/video.rs index b39a3e9..53e521c 100644 --- a/src/routes/video.rs +++ b/src/routes/video.rs @@ -82,7 +82,7 @@ pub async fn track(config: web::Data>, request: HttpRequest, info: web::Query) -> HttpResponse { let auth_response = squire::authenticator::verify_token(&request, &config); if !auth_response.ok { - return routes::auth::failed_auth(auth_response); + return routes::auth::failed_auth(auth_response, &config); } squire::logger::log_connection(&request); log::debug!("{}", auth_response.detail); @@ -116,7 +116,7 @@ pub async fn stream(config: web::Data>, request: HttpRequest, video_path: web::Path) -> HttpResponse { let auth_response = squire::authenticator::verify_token(&request, &config); if !auth_response.ok { - return routes::auth::failed_auth(auth_response); + return routes::auth::failed_auth(auth_response, &config); } squire::logger::log_connection(&request); log::debug!("{}", auth_response.detail); @@ -212,7 +212,7 @@ pub async fn streaming_endpoint(config: web::Data> request: HttpRequest, info: web::Query) -> HttpResponse { let auth_response = squire::authenticator::verify_token(&request, &config); if !auth_response.ok { - return routes::auth::failed_auth(auth_response); + return routes::auth::failed_auth(auth_response, &config); } squire::logger::log_connection(&request); let host = request.connection_info().host().to_owned(); diff --git a/src/squire/middleware.rs b/src/squire/middleware.rs index 981433e..d6567ae 100644 --- a/src/squire/middleware.rs +++ b/src/squire/middleware.rs @@ -5,15 +5,15 @@ use actix_web::http::header; /// /// # Arguments /// -/// * `website` - A vector of allowed website origins for CORS. +/// * `websites` - A vector of allowed website origins for CORS. /// /// # Returns /// /// A configured `Cors` middleware instance. -pub fn get_cors(website: Vec) -> Cors { +pub fn get_cors(websites: Vec) -> Cors { let mut origins = vec!["http://localhost.com".to_string(), "https://localhost.com".to_string()]; - if !website.is_empty() { - origins.extend_from_slice(&website); + if !websites.is_empty() { + origins.extend_from_slice(&websites); } // Create a clone to append /* to each endpoint, and further extend the same vector let cloned = origins.clone().into_iter().map(|x| format!("{}/{}", x, "*")); diff --git a/src/squire/settings.rs b/src/squire/settings.rs index 7420dcf..e186a58 100644 --- a/src/squire/settings.rs +++ b/src/squire/settings.rs @@ -32,15 +32,19 @@ pub struct Config { #[serde(default = "default_max_connections")] pub max_connections: i32, /// List of websites (supports regex) to add to CORS configuration. - #[serde(default = "default_website")] - pub website: Vec, + #[serde(default = "default_websites")] + pub websites: Vec, + // Boolean flag to restrict session_token to be sent only via HTTPS + #[serde(default = "default_secure_session")] + pub secure_session: bool, + + // Path to the private key file for SSL certificate + #[serde(default = "default_ssl")] + pub key_file: path::PathBuf, // Path to the full certificate chain file for SSL certificate #[serde(default = "default_ssl")] pub cert_file: path::PathBuf, - // Path to the private key file for SSL certificate - #[serde(default = "default_ssl")] - pub key_file: path::PathBuf } /// Returns the default value for ssl files @@ -91,12 +95,14 @@ fn default_workers() -> i32 { } } -/// Returns the default maximum number of concurrent connections (300). +/// Returns the default maximum number of concurrent connections (3). fn default_max_connections() -> i32 { - 300 + 3 } /// Returns an empty list as the default website (CORS configuration). -fn default_website() -> Vec { +fn default_websites() -> Vec { Vec::new() } + +fn default_secure_session() -> bool { false } diff --git a/src/templates/logout.rs b/src/templates/logout.rs index 2e1226e..583f534 100644 --- a/src/templates/logout.rs +++ b/src/templates/logout.rs @@ -11,8 +11,12 @@ pub fn get_content() -> String { r###" - Rustic video streaming + Rustic video streaming + + + +