diff --git a/.gitignore b/.gitignore index a7a7de644f..b0855379bb 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ tests/complement/logs.jsonl # Editor crashfiles *.save *.swp + +# Snaps packed +tuwunel_*_*.snap diff --git a/Cargo.lock b/Cargo.lock index 0daa90d3b6..0e2dd18798 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -473,7 +473,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -491,6 +491,12 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -581,6 +587,12 @@ version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -885,7 +897,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3" dependencies = [ - "crossterm", + "crossterm 0.29.0", ] [[package]] @@ -996,7 +1008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04a63daf06a168535c74ab97cdba3ed4fa5d4f32cb36e437dcceb83d66854b7c" dependencies = [ "crokey-proc_macros", - "crossterm", + "crossterm 0.29.0", "once_cell", "serde", "strict", @@ -1008,7 +1020,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "847f11a14855fc490bd5d059821895c53e77eeb3c2b73ee3dded7ce77c93b231" dependencies = [ - "crossterm", + "crossterm 0.29.0", "proc-macro2", "quote", "strict", @@ -1071,18 +1083,34 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.11.0", "crossterm_winapi", "derive_more", "document-features", "futures-core", - "mio", + "mio 1.1.1", "parking_lot", "rustix", "signal-hook", @@ -1270,7 +1298,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags", + "bitflags 2.11.0", "objc2", ] @@ -1315,6 +1343,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ed25519" version = "2.2.3" @@ -1675,6 +1709,24 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2230,6 +2282,23 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.11.0", + "crossterm 0.25.0", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "insta" version = "1.46.3" @@ -2770,6 +2839,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.1.1" @@ -2815,19 +2896,40 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", "memoffset", ] +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -2983,7 +3085,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags", + "bitflags 2.11.0", "objc2", "objc2-foundation", ] @@ -3004,7 +3106,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.11.0", "dispatch2", "objc2", ] @@ -3015,7 +3117,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags", + "bitflags 2.11.0", "dispatch2", "objc2", "objc2-core-foundation", @@ -3048,7 +3150,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3066,7 +3168,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -3079,7 +3181,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", ] @@ -3090,7 +3192,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3102,7 +3204,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags", + "bitflags 2.11.0", "block2", "objc2", "objc2-cloud-kit", @@ -3435,7 +3537,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags", + "bitflags 2.11.0", "crc32fast", "fdeflate", "flate2", @@ -3544,7 +3646,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14104c5a24d9bcf7eb2c24753e0f49fe14555d8bd565ea3d38e4b4303267259d" dependencies = [ - "bitflags", + "bitflags 2.11.0", "memchr", "pulldown-cmark-escape", "unicase", @@ -3816,7 +3918,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -4225,7 +4327,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -4320,7 +4422,7 @@ name = "rustyline-async" version = "0.4.6" source = "git+https://github.com/matrix-construct/rustyline-async?rev=ba743cae940659e95d7b8604bbd6fdd755171c8c#ba743cae940659e95d7b8604bbd6fdd755171c8c" dependencies = [ - "crossterm", + "crossterm 0.29.0", "futures-util", "pin-project", "thingbuf", @@ -4383,7 +4485,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.11.0", "core-foundation", "core-foundation-sys", "libc", @@ -4482,7 +4584,7 @@ version = "0.46.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e200860daf76e09f9ad111bce25928f96bedbb84bc5934b37f05bb445727c70e" dependencies = [ - "bitflags", + "bitflags 2.11.0", "log", "sentry-core", ] @@ -4517,7 +4619,7 @@ version = "0.46.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b07eefe04486316c57aba08ab53dd44753c25102d1d3fe05775cc93a13262d9" dependencies = [ - "bitflags", + "bitflags 2.11.0", "sentry-backtrace", "sentry-core", "tracing-core", @@ -4715,7 +4817,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", - "mio", + "mio 0.8.11", + "mio 1.1.1", "signal-hook", ] @@ -5110,7 +5213,7 @@ checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", - "mio", + "mio 1.1.1", "pin-project-lite", "signal-hook-registry", "socket2 0.6.3", @@ -5312,7 +5415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags", + "bitflags 2.11.0", "bytes", "futures-core", "futures-util", @@ -5468,6 +5571,17 @@ dependencies = [ "webpki-root-certs", ] +[[package]] +name = "tuwunel-init" +version = "1.5.1" +dependencies = [ + "anyhow", + "clap", + "inquire", + "regex", + "tuwunel_core", +] + [[package]] name = "tuwunel_admin" version = "1.6.0" @@ -6034,7 +6148,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -6581,7 +6695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap", "log", "serde", diff --git a/snap/local/check-config.sh b/snap/local/check-config.sh new file mode 100755 index 0000000000..0e1872b75f --- /dev/null +++ b/snap/local/check-config.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if [ ! -f "$SNAP_COMMON/tuwunel.toml" ]; then + echo "Configuration file not found at $SNAP_COMMON/tuwunel.toml" + echo "Please initialize the configuration by running:" + echo " sudo tuwunel.init" + sleep 60 + exit 1 +fi + +exec "$@" diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000000..2a0a9f7f95 --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,80 @@ +name: tuwunel +title: Tuwunel Matrix Server +base: core24 # Using core24 as the base for a modern Rust toolchain (1.94.0+) +version: 'git' +summary: High performance Matrix homeserver in Rust +description: | + Tuwunel is a featureful Matrix homeserver you can use with your favorite + client, bridge or bot. It is written entirely in Rust to be scalable, + low-cost, enterprise-ready, and community-driven. + + Install the snap and run `sudo tuwunel.init`, then direct traffic to the + server which is usually listening on 127.0.0.1:8008 +website: https://tuwunel.chat/ +source-code: https://github.com/matrix-construct/tuwunel +issues: https://github.com/matrix-construct/tuwunel/issues +contact: https://github.com/matrix-construct/tuwunel/discussions +license: Apache-2.0 +donation: https://github.com/matrix-construct/tuwunel/discussions + +grade: stable +confinement: strict + +apps: + daemon: + command: tuwunel + command-chain: [bin/check-config.sh] + daemon: simple # This creates the background service + install-mode: enable # Automatically start the service when the snap is installed + plugs: + - network + - network-bind + environment: + # By default, snap services are heavily sandboxed. + # You can point Tuwunel to look for its configuration inside the snap's data directory. + TUWUNEL_CONFIG: $SNAP_COMMON/tuwunel.toml + + tuwunel: + command: tuwunel + plugs: + - network + - network-bind + environment: + # By default, snap services are heavily sandboxed. + # You can point Tuwunel to look for its configuration inside the snap's data directory. + TUWUNEL_CONFIG: $SNAP_COMMON/tuwunel.toml + + init: + command: tuwunel-init + plugs: + - network + - network-bind + environment: + TUWUNEL_CONFIG: $SNAP_COMMON/tuwunel.toml + +parts: + wrapper: + plugin: dump + source: snap/local + organize: + check-config.sh: bin/check-config.sh + + tuwunel: + after: [wrapper] + plugin: rust + source: . + build-packages: + - build-essential + - pkg-config + - libssl-dev + - liburing-dev + - clang + - libclang-dev + - cmake + - make + stage-packages: + - liburing2 + prime: + - "-usr/lib/*/liburing-ffi.so*" + # The rust plugin automatically runs `cargo build --release` + # and installs the resulting binaries into the snap. diff --git a/src/init/Cargo.toml b/src/init/Cargo.toml new file mode 100644 index 0000000000..d9c3a1f58d --- /dev/null +++ b/src/init/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tuwunel-init" +version = "1.5.1" +edition = "2024" +authors.workspace = true + +[dependencies] +tuwunel_core = { path = "../core" } +clap = { workspace = true } +inquire = "0.7" +anyhow = "1.0" +regex = { workspace = true } diff --git a/src/init/src/main.rs b/src/init/src/main.rs new file mode 100644 index 0000000000..942917ab16 --- /dev/null +++ b/src/init/src/main.rs @@ -0,0 +1,257 @@ +use anyhow::{Context, Result}; +use clap::Parser; +use inquire::{Text, Password, PasswordDisplayMode, validator::Validation}; +use regex::Regex; +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +#[derive(Parser)] +#[command(name = "tuwunel-init")] +#[command(about = "Initialize tuwunel configuration and create the first user")] +struct Cli { + #[arg(short, long)] + path: Option, +} + +fn estimate_entropy(password: &str) -> f64 { + let mut pool = 0; + if password.chars().any(|c| c.is_ascii_lowercase()) { pool += 26; } + if password.chars().any(|c| c.is_ascii_uppercase()) { pool += 26; } + if password.chars().any(|c| c.is_ascii_digit()) { pool += 10; } + if password.chars().any(|c| !c.is_ascii_alphanumeric()) { pool += 32; } + + if pool == 0 { return 0.0; } + (pool as f64).log2() * (password.chars().count() as f64) +} + +fn classify_entropy(entropy: f64) -> &'static str { + if entropy < 40.0 { "Poor" } + else if entropy < 60.0 { "Ok" } + else if entropy < 80.0 { "Good" } + else { "Excellent" } +} + +fn main() -> Result<()> { + let args = Cli::parse(); + + let config_path = args.path.or_else(|| { + env::var("TUWUNEL_CONFIG").ok().map(PathBuf::from) + }).unwrap_or_else(|| PathBuf::from("tuwunel.toml")); + + if config_path.exists() { + println!("Configuration file already exists at {}. Exiting gracefully.", config_path.display()); + return Ok(()); + } + + if let Some(parent) = config_path.parent() { + if !parent.as_os_str().is_empty() && !parent.exists() { + fs::create_dir_all(parent).map_err(|e| { + anyhow::anyhow!("Unable to write to {}, perhaps try sudo. ({})", config_path.display(), e) + })?; + } + } + + match std::fs::OpenOptions::new().write(true).create_new(true).open(&config_path) { + Ok(_) => { + let _ = std::fs::remove_file(&config_path); + } + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { + println!("Configuration file already exists at {}. Exiting gracefully.", config_path.display()); + return Ok(()); + } + Err(e) => { + return Err(anyhow::anyhow!("Unable to write to {}, perhaps try sudo. ({})", config_path.display(), e)); + } + } + + println!("Tuwunel initialization."); + println!("We will create a minimal configuration file for your matrix server.\n"); + + let domain_regex = Regex::new(r"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+$").unwrap(); + + let server_name = Text::new("Server name (e.g., matrix.example.com):") + .with_help_message("This must be identical to the domain used for reverse proxying.") + .with_validator(move |input: &str| { + if input.trim().is_empty() { + Ok(Validation::Invalid("Server name cannot be empty.".into())) + } else if !domain_regex.is_match(input.trim()) { + Ok(Validation::Invalid("Please enter a valid fully qualified domain name.".into())) + } else { + Ok(Validation::Valid) + } + }) + .prompt() + .context("Failed to read server name")?; + + let server_name = server_name.trim(); + + let registration_token = Password::new("Registration token:") + .with_display_mode(PasswordDisplayMode::Masked) + .with_help_message("A static registration token users will have to provide when creating an account.") + .with_validator(move |input: &str| { + if input.trim().is_empty() { + return Ok(Validation::Invalid("Registration token cannot be empty.".into())); + } + let ent = estimate_entropy(input.trim()); + if ent < 40.0 { + Ok(Validation::Invalid("Registration is insecure. Please choose a more secure token.".into())) + } else { + Ok(Validation::Valid) + } + }) + .prompt() + .context("Failed to read registration token")?; + + let registration_token = registration_token.trim(); + println!("Registration token entropy: {}", classify_entropy(estimate_entropy(registration_token))); + + let create_user_decision = Text::new("Would you like to create an initial admin user? [Yes/No/Quit]:") + .with_validator(|input: &str| { + let s = input.trim().to_lowercase(); + match s.as_str() { + "y" | "yes" | "n" | "no" | "q" | "quit" | "" => Ok(Validation::Valid), + _ => Ok(Validation::Invalid("Please enter yes, no, or quit.".into())) + } + }) + .prompt() + .context("Failed to read user creation decision")?; + + let decision = create_user_decision.trim().to_lowercase(); + if decision == "q" || decision == "quit" { + println!("Exiting without writing configuration."); + return Ok(()); + } + + let create_user = decision == "y" || decision == "yes" || decision == ""; + + let mut admin_username_opt = None; + let mut admin_password_opt = None; + + if create_user { + let admin_username = Text::new("Admin username (without domain portion, eg. 'tom'):") + .with_validator(|input: &str| { + if input.trim().is_empty() { + Ok(Validation::Invalid("Admin username cannot be empty.".into())) + } else if input.contains(':') || input.contains('@') { + Ok(Validation::Invalid("Just the local username, no @ or domain.".into())) + } else { + Ok(Validation::Valid) + } + }) + .prompt() + .context("Failed to read admin username")?; + + let admin_username = admin_username.trim().to_string(); + + let admin_password = Password::new("Admin password:") + .with_display_mode(PasswordDisplayMode::Masked) + .with_validator(move |input: &str| { + if input.trim().is_empty() { + return Ok(Validation::Invalid("Admin password cannot be empty.".into())); + } + let ent = estimate_entropy(input.trim()); + if ent < 40.0 { + Ok(Validation::Invalid(format!("Admin password entropy is {}. Please choose a stronger password.", classify_entropy(ent)).into())) + } else { + Ok(Validation::Valid) + } + }) + .prompt() + .context("Failed to read admin password")?; + + let admin_password = admin_password.trim().to_string(); + println!("Admin password security: {}", classify_entropy(estimate_entropy(&admin_password))); + + admin_username_opt = Some(admin_username); + admin_password_opt = Some(admin_password); + } + + // Read the build-time generated config example (this embeds the file directly into the binary at compile time!) + let mut config_text = include_str!("../../../tuwunel-example.toml").to_string(); + + config_text = config_text.replace( + "#server_name =\n", + &format!("server_name = \"{}\"\n", server_name) + ); + config_text = config_text.replace( + "#registration_token =\n", + &format!("registration_token = \"{}\"\n", registration_token) + ); + + // If running inside a Snap, we must change the default database path to the writable SNAP_COMMON directory + if let Ok(snap_common) = env::var("SNAP_COMMON") { + config_text = config_text.replace( + "#database_path = \"/var/lib/tuwunel\"", + &format!("database_path = \"{snap_common}/db\"") + ); + } + + if let Ok(snap_data) = env::var("SNAP_DATA") { + let backup_path = std::path::Path::new(&snap_data) + .parent() + .map(|p| p.join("current").join("db.bak").display().to_string()) + .unwrap_or_else(|| format!("{snap_data}/db.bak")); + + config_text = config_text.replace( + "#database_backup_path =\n", + &format!("database_backup_path = \"{backup_path}\"\n") + ); + } + + fs::write(&config_path, config_text).context("Failed to write tuwunel config file")?; + println!("\nConfiguration file successfully written to {}", config_path.display()); + + if !create_user { + println!("\nInitialization Complete! \u{1F389}"); + println!("The daemon can now be started. If using snap, the service will pick up the config momentarily."); + return Ok(()); + } + + // Attempt to invoke the Daemon binary to create the admin user + // We expect `tuwunel` binary to be situated in the exact same directory as this tool. + let mut tuwunel_bin = PathBuf::from("tuwunel"); + if let Ok(current_exe) = env::current_exe() { + if let Some(parent) = current_exe.parent() { + let adjacent = parent.join("tuwunel"); + if adjacent.exists() { + tuwunel_bin = adjacent; + } + } + } + + // Pass the config path directly to tuwunel as --config + println!("Initializing database and to create the first administrator..."); + let admin_username = admin_username_opt.unwrap(); + let admin_password = admin_password_opt.unwrap(); + let status = Command::new(&tuwunel_bin) + .arg("--config") + .arg(&config_path) + .arg("--maintenance") + .arg("--execute") + .arg(format!("users create_user @{}:{} {}", admin_username, server_name, admin_password)) + .arg("--execute") + .arg("server shutdown") + .status() + .with_context(|| format!("Failed to evaluate tuwunel binary: {}", tuwunel_bin.display()))?; + + if status.success() { + println!("\nInitialization Complete! \u{1F389}"); + println!("The daemon can now be started, or will start automatically if you installed the Tuwunel snap."); + println!("It will by default listen only on 127.0.0.1:8008, you will probably want to direct traffic"); + println!("there by reverse proxy. See the docs for more information."); + println!(""); + println!("You can sign into the server at:"); + println!(""); + println!(" Homeserver URL: https://{server_name}"); + println!(" Username: @{admin_username}:{server_name}"); + println!(" Password: "); + } else { + println!("\nUser creation failed unexpectedly (exited with: {status})."); + println!("You can attempt to retry creating the admin manually by running:"); + println!(" {} --maintenance --execute 'user create ...'", tuwunel_bin.display()); + } + + Ok(()) +}