diff --git a/Cargo.lock b/Cargo.lock index b927c29..ad58c7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,7 @@ dependencies = [ "actix-codec", "actix-rt", "actix-service", + "actix-tls", "actix-utils", "base64", "bitflags 2.9.0", @@ -173,6 +174,25 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-tls" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "impl-more", + "pin-project-lite", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", +] + [[package]] name = "actix-utils" version = "3.0.1" @@ -196,6 +216,7 @@ dependencies = [ "actix-rt", "actix-server", "actix-service", + "actix-tls", "actix-utils", "actix-web-codegen", "bytes", @@ -659,7 +680,7 @@ dependencies = [ "proptest", "rand 0.9.1", "ruint", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "sha3", "tiny-keccak", @@ -1572,6 +1593,29 @@ dependencies = [ "xor_name", ] +[[package]] +name = "aws-lc-rs" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -1620,6 +1664,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools 0.10.5", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.101", + "which", +] + [[package]] name = "bip39" version = "2.1.0" @@ -1925,6 +1992,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -1987,6 +2063,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "2.34.0" @@ -2042,6 +2129,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "color-eyre" version = "0.6.4" @@ -2670,7 +2766,10 @@ dependencies = [ "mime", "open", "qstring", + "rcgen", "regex", + "rustls", + "rustls-pemfile", "serde", "serde_json", "tokio", @@ -3033,6 +3132,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -4073,12 +4178,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libloading" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" +dependencies = [ + "cfg-if", + "windows-targets 0.48.5", +] + [[package]] name = "libm" version = "0.2.15" @@ -5417,6 +5538,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +dependencies = [ + "proc-macro2", + "syn 2.0.101", +] + [[package]] name = "prettytable" version = "0.10.0" @@ -5662,7 +5793,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.1", "rustls", "socket2", "thiserror 2.0.12", @@ -5682,7 +5813,7 @@ dependencies = [ "lru-slab", "rand 0.9.1", "ring", - "rustc-hash", + "rustc-hash 2.1.1", "rustls", "rustls-pki-types", "slab", @@ -6125,6 +6256,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -6196,6 +6333,8 @@ version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ + "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -6239,6 +6378,7 @@ version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", diff --git a/dweb-cli/Cargo.toml b/dweb-cli/Cargo.toml index b09b716..6513299 100644 --- a/dweb-cli/Cargo.toml +++ b/dweb-cli/Cargo.toml @@ -58,12 +58,15 @@ hex = "0.4.3" blsttc = "8.0.2" serde = "1.0.219" serde_json = "1.0.139" -actix-web = "4.9.0" +actix-web = { version = "4.9.0", features = ["rustls-0_23"] } actix-cors = "0.7.1" actix-multipart = "0.7.2" utoipa = { version = "5.3.1", features = ["actix_extras", "non_strict_integers"] } utoipa-actix-web = "0.1.2" utoipa-swagger-ui = { version = "9.0.1", features = ["vendored", "reqwest", "actix-web"] } +rustls = "0.23" +rustls-pemfile = "2.1" +rcgen = "0.13" # patched #utoipa = { path = "../../utoipa-patch/utoipa", features = ["actix_extras", "non_strict_integers"] } # "5.3.1" diff --git a/dweb-cli/src/cli_options.rs b/dweb-cli/src/cli_options.rs index edc6c49..d1af4d8 100644 --- a/dweb-cli/src/cli_options.rs +++ b/dweb-cli/src/cli_options.rs @@ -155,6 +155,9 @@ pub enum Subcommands { /// This is only needed when not using defaults, so hidden to de-clutter the CLI help #[clap(hide = true, long, value_name = "PORT", value_parser = parse_port_number)] port: Option, + /// Enable HTTPS with automatically generated self-signed certificate + #[clap(long = "https", default_value = "false")] + https: bool, }, /// Open a browser to view a website on Autonomi (requires 'dweb serve' running) diff --git a/dweb-cli/src/commands/cmd_browse.rs b/dweb-cli/src/commands/cmd_browse.rs index a19bbe7..c599d3d 100644 --- a/dweb-cli/src/commands/cmd_browse.rs +++ b/dweb-cli/src/commands/cmd_browse.rs @@ -17,9 +17,41 @@ use std::u16; -use dweb::cache::spawn::is_main_server_with_ports_running; +use dweb::cache::spawn::{detect_server_protocol, ServerProtocol}; use dweb::web::{DWEB_SERVICE_API, LOCALHOST_STR}; +/// Shared function to determine protocol and build URL consistently +fn determine_protocol_and_build_url( + host: &str, + port: u16, + route: &str, +) -> String { + let (is_running, detected_protocol) = detect_server_protocol(); + + if !is_running { + // If no server detected, default to HTTP + return format!("http://{host}:{port}{route}"); + } + + // Use auto-detection to determine protocol + let use_https = if let Some(protocol) = detected_protocol { + let detected_https = matches!(protocol, ServerProtocol::Https); + if detected_https { + println!("Auto-detected HTTPS server on port {}", dweb::web::DEFAULT_HTTPS_PORT); + } else { + println!("Auto-detected HTTP server on port {}", dweb::web::SERVER_PORTS_MAIN_PORT); + } + detected_https + } else { + // Fallback to HTTP if detection fails + println!("Could not auto-detect protocol, defaulting to HTTP"); + false + }; + + let protocol = if use_https { "https" } else { "http" }; + format!("{protocol}://{host}:{port}{route}") +} + /// Open a browser to view a website on Autonomi. /// /// A 'with hosts' server must be running and a local DNS has been set up. @@ -34,6 +66,14 @@ pub(crate) fn handle_browse_with_hosts( host: Option<&String>, port: Option, ) { + let (is_running, _) = detect_server_protocol(); + + if !is_running { + println!("Please start the dweb server before using 'dweb open'"); + println!("For help, type 'dweb serve --help'"); + return; + } + let default_host = DWEB_SERVICE_API.to_string(); let host = host.unwrap_or(&default_host); let port = port.unwrap_or(dweb::web::SERVER_HOSTS_MAIN_PORT); @@ -49,8 +89,7 @@ pub(crate) fn handle_browse_with_hosts( // open a browser on a localhost URL at that port let route = format!("/dweb-open/v{version}/{address_name_or_link}/{remote_path}"); - - let url = format!("http://{host}:{port}{route}"); + let url = determine_protocol_and_build_url(host, port, &route); println!("DEBUG url: {url}"); let _ = open::that(url); @@ -69,9 +108,11 @@ pub(crate) fn handle_browse_with_ports( host: Option<&String>, port: Option, ) { - if !is_main_server_with_ports_running() { - println!("Please start the dweb server before using 'dweb open'"); - println!("For help, type 'dweb serve --help"); + let (is_running, detected_protocol) = detect_server_protocol(); + + if !is_running { + println!("Please start the dweb server before using 'dweb open'"); + println!("For help, type 'dweb serve --help'"); return; } @@ -79,7 +120,19 @@ pub(crate) fn handle_browse_with_ports( let default_host = LOCALHOST_STR.to_string(); let host = host.unwrap_or(&default_host); - let port = port.unwrap_or(dweb::web::SERVER_PORTS_MAIN_PORT); + + // Use auto-detection to determine the correct port if not explicitly set + let port = port.unwrap_or_else(|| { + if let Some(protocol) = detected_protocol { + match protocol { + ServerProtocol::Https => dweb::web::DEFAULT_HTTPS_PORT, + ServerProtocol::Http => dweb::web::SERVER_PORTS_MAIN_PORT, + } + } else { + dweb::web::SERVER_PORTS_MAIN_PORT + } + }); + let version = if version.is_some() { &format!("{}", version.unwrap()) } else { @@ -96,7 +149,7 @@ pub(crate) fn handle_browse_with_ports( } else { format!("/dweb-open/v{version}/{address_name_or_link}/{remote_path}") }; - let url = format!("http://{host}:{port}{route}"); + let url = determine_protocol_and_build_url(host, port, &route); println!("DEBUG url: {url}"); let _ = open::that(url); diff --git a/dweb-cli/src/commands/subcommands.rs b/dweb-cli/src/commands/subcommands.rs index a31eccc..c2d10f8 100644 --- a/dweb-cli/src/commands/subcommands.rs +++ b/dweb-cli/src/commands/subcommands.rs @@ -44,6 +44,7 @@ pub async fn cli_commands(opt: Opt) -> Result { experimental, host, port, + https, }) => { let (client, is_local_network) = connect_and_announce(opt.local, opt.alpha, api_control, true).await; @@ -52,7 +53,11 @@ pub async fn cli_commands(opt: Opt) -> Result { // Start the main server (for port based browsing), which will handle /dweb-open URLs opened by 'dweb open' let default_host = LOCALHOST_STR.to_string(); let host = host.unwrap_or(default_host); - let port = port.unwrap_or(SERVER_PORTS_MAIN_PORT); + let port = port.unwrap_or(if https { + dweb::web::DEFAULT_HTTPS_PORT + } else { + SERVER_PORTS_MAIN_PORT + }); match crate::services::serve_with_ports( &client, None, @@ -60,6 +65,7 @@ pub async fn cli_commands(opt: Opt) -> Result { Some(port), false, is_local_network, + https, ) .await { @@ -105,23 +111,22 @@ pub async fn cli_commands(opt: Opt) -> Result { if !experimental { let default_host = LOCALHOST_STR.to_string(); let host = host.unwrap_or(default_host); - let port = port.unwrap_or(SERVER_PORTS_MAIN_PORT); crate::commands::cmd_browse::handle_browse_with_ports( &address_name_or_link, version, as_name, remote_path, Some(&host), - Some(port), + port, ); } else { let default_host = dweb::web::DWEB_SERVICE_API.to_string(); let host = host.unwrap_or(default_host); let port = port.unwrap_or(SERVER_HOSTS_MAIN_PORT); - crate::commands::cmd_browse::handle_browse_with_ports( + crate::commands::cmd_browse::handle_browse_with_hosts( + None, &address_name_or_link, version, - as_name, remote_path, Some(&host), Some(port), diff --git a/dweb-cli/src/services.rs b/dweb-cli/src/services.rs index 1ef124f..50811da 100644 --- a/dweb-cli/src/services.rs +++ b/dweb-cli/src/services.rs @@ -24,12 +24,15 @@ pub(crate) mod www; use std::io; use std::time::Duration; +use std::sync::Arc; use actix_web::{dev::Service, middleware::Logger, web, web::Data, App, HttpServer}; use utoipa::OpenApi; use utoipa_actix_web::scope::scope; use utoipa_actix_web::AppExt; use utoipa_swagger_ui::SwaggerUi; +use rustls::{ServerConfig, pki_types::{CertificateDer, PrivateKeyDer}}; +use rcgen::{generate_simple_self_signed, CertifiedKey}; use dweb::cache::directory_with_port::DirectoryVersionWithPort; use dweb::client::DwebClient; @@ -41,8 +44,41 @@ pub const CONNECTION_TIMEOUT: u64 = 75; #[cfg(feature = "development")] const DWEB_SERVICE_DEBUG: &str = "debug-dweb.au"; -#[cfg(feature = "development")] -const DWEB_SERVICE_DEBUG: &str = "debug-dweb.au"; +/// Generate a self-signed certificate for HTTPS +fn generate_self_signed_cert() -> std::io::Result<(Vec>, PrivateKeyDer<'static>)> { + let subject_alt_names = vec![ + "localhost".to_string(), + "127.0.0.1".to_string(), + "::1".to_string(), + ]; + + let CertifiedKey { cert, key_pair } = generate_simple_self_signed(subject_alt_names) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to generate certificate: {}", e)))?; + + let cert_der = cert.der().to_vec(); + let key_der = key_pair.serialize_der(); + + let cert_chain = vec![CertificateDer::from(cert_der)]; + let private_key = PrivateKeyDer::try_from(key_der) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to parse private key: {}", e)))?; + + Ok((cert_chain, private_key)) +} + +/// Create rustls ServerConfig with self-signed certificate +fn create_rustls_config() -> std::io::Result { + // Install the default crypto provider if not already installed + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + + let (cert_chain, private_key) = generate_self_signed_cert()?; + + let config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cert_chain, private_key) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to create TLS config: {}", e)))?; + + Ok(config) +} /// serve_with_ports may be called as follows: /// @@ -71,6 +107,8 @@ pub async fn serve_with_ports( // Either spawn a thread for the server and return, or do server.await spawn_server: bool, is_local_network: bool, + // Enable HTTPS with self-signed certificate + https: bool, ) -> io::Result<()> { register_builtin_names(is_local_network); let directory_version_with_port_copy1 = directory_version_with_port.clone(); @@ -202,10 +240,17 @@ pub async fn serve_with_ports( Some(directory_version_with_port) => directory_version_with_port, }; - let server = server.bind((host.clone(), directory_version.port))?.run(); - actix_web::rt::spawn(server); + let server_future = if https { + let config = create_rustls_config()?; + server.bind_rustls_0_23((host.clone(), directory_version.port), config)?.run() + } else { + server.bind((host.clone(), directory_version.port))?.run() + }; + + actix_web::rt::spawn(server_future); + let protocol = if https { "https" } else { "http" }; println!( - "Started a dweb server listening on {host}:{} for version {:?} at {:?} -> {}", + "Started a dweb server listening on {protocol}://{host}:{} for version {:?} at {:?} -> {}", directory_version.port, directory_version.version, directory_version.history_address, @@ -221,7 +266,15 @@ pub async fn serve_with_ports( } Some(port) => port, }; - println!("dweb main server listening on {host}:{port}"); - server.bind((host, port))?.run().await + + let protocol = if https { "https" } else { "http" }; + println!("dweb main server listening on {protocol}://{host}:{port}"); + + if https { + let config = create_rustls_config()?; + server.bind_rustls_0_23((host, port), config)?.run().await + } else { + server.bind((host, port))?.run().await + } } } diff --git a/dweb-cli/src/services/www/dweb_open.rs b/dweb-cli/src/services/www/dweb_open.rs index 7d8e9f9..6e457ec 100644 --- a/dweb-cli/src/services/www/dweb_open.rs +++ b/dweb-cli/src/services/www/dweb_open.rs @@ -135,6 +135,14 @@ pub async fn dweb_open_as( .await } +/// Detect if the main server is running with HTTPS +fn detect_main_server_https_status() -> bool { + use dweb::cache::spawn::detect_server_protocol; + + let (_, protocol) = detect_server_protocol(); + matches!(protocol, Some(dweb::cache::spawn::ServerProtocol::Https)) +} + pub async fn handle_dweb_open( request: &HttpRequest, client: Data, @@ -176,6 +184,7 @@ pub async fn handle_dweb_open( None, true, *is_local_network.into_inner().as_ref(), + detect_main_server_https_status(), ) .await { diff --git a/dweb-lib/src/cache/spawn.rs b/dweb-lib/src/cache/spawn.rs index 0304faf..db2e9bb 100644 --- a/dweb-lib/src/cache/spawn.rs +++ b/dweb-lib/src/cache/spawn.rs @@ -20,6 +20,40 @@ // TODO web API and CLI for listing active ports and what they are serving // TODO see TODOs in serve_with_ports() +use std::net::TcpStream; +use std::time::Duration; +use crate::web::{SERVER_PORTS_MAIN_PORT, DEFAULT_HTTPS_PORT, LOCALHOST_STR}; + +#[derive(Debug, Clone, Copy)] +pub enum ServerProtocol { + Http, + Https, +} + pub fn is_main_server_with_ports_running() -> bool { return true; // TODO look-up the main server in the spawned servers struct } + +/// Detect if a dweb server is running and which protocol (HTTP/HTTPS) it uses +/// Returns (is_running, protocol) where protocol is None if no server is detected +pub fn detect_server_protocol() -> (bool, Option) { + let timeout = Duration::from_millis(500); + + // Try HTTPS port first (8443) + if let Ok(_) = TcpStream::connect_timeout( + &format!("{}:{}", LOCALHOST_STR, DEFAULT_HTTPS_PORT).parse().unwrap(), + timeout + ) { + return (true, Some(ServerProtocol::Https)); + } + + // Try HTTP port (8080) + if let Ok(_) = TcpStream::connect_timeout( + &format!("{}:{}", LOCALHOST_STR, SERVER_PORTS_MAIN_PORT).parse().unwrap(), + timeout + ) { + return (true, Some(ServerProtocol::Http)); + } + + (false, None) +}