p2p_chat/app/
setup.rs

1//! This module handles the initial setup of the application.
2use super::args::AppArgs;
3use crate::crypto::{Identity, StorageEncryption};
4use anyhow::{anyhow, Result};
5use base64::prelude::*;
6use std::net::TcpListener;
7use std::path::Path;
8use std::sync::Arc;
9use tracing_subscriber::EnvFilter;
10
11/// Contains all the necessary components for the application to run.
12///
13/// This struct is created by the `prepare` function and passed to the
14/// appropriate `run` function (either for a client or a mailbox node).
15pub struct PreparedApp {
16    /// The command-line arguments.
17    pub args: AppArgs,
18    /// The port to listen on for P2P connections.
19    pub port: u16,
20    /// The port for the Web UI.
21    pub web_port: u16,
22    /// The user's identity.
23    pub identity: Arc<Identity>,
24    /// The database instance.
25    pub db: sled::Db,
26    /// The encryption key for the storage, if enabled.
27    pub encryption: Option<StorageEncryption>,
28}
29
30/// Prepares the application for running.
31///
32/// This function performs the following steps:
33/// 1. Finds free ports if not specified.
34/// 2. Configures logging.
35/// 3. Prints a start banner.
36/// 4. Creates the data directory.
37/// 5. Loads or generates the user's identity.
38/// 6. Prints identity information.
39/// 7. Opens the database.
40/// 8. Sets up storage encryption if enabled.
41///
42/// # Arguments
43///
44/// * `args` - The command-line arguments.
45///
46/// # Errors
47///
48/// This function will return an error if any of the setup steps fail.
49pub fn prepare(args: AppArgs) -> Result<PreparedApp> {
50    let port = args.port.unwrap_or(find_free_port()?);
51    let web_port = args.web_port.unwrap_or(find_free_port()?);
52
53    configure_logging(args.mailbox);
54    print_start_banner(&args, port, web_port);
55
56    std::fs::create_dir_all(&args.data_dir)?;
57
58    let identity_path = format!("{}/identity.json", args.data_dir);
59    let identity = Arc::new(Identity::load_or_generate(&identity_path)?);
60
61    print_identity_info(&identity);
62
63    let db_path = format!("{}/db", args.data_dir);
64    let db = sled::open(&db_path)?;
65
66    let encryption = if args.encrypt {
67        println!("🔐 Storage encryption enabled");
68
69        let password = resolve_encryption_password(&args)?;
70        let salt_path = format!("{}/encryption_salt.bin", args.data_dir);
71        let salt = load_or_create_salt(&salt_path)?;
72
73        Some(StorageEncryption::new(&password, &salt)?)
74    } else {
75        None
76    };
77
78    Ok(PreparedApp {
79        args,
80        port,
81        web_port,
82        identity,
83        db,
84        encryption,
85    })
86}
87
88/// Configures logging for the application.
89///
90/// If running in mailbox mode, it sets a more verbose logging level.
91fn configure_logging(mailbox_mode: bool) {
92    if mailbox_mode {
93        let _ = tracing_subscriber::fmt()
94            .with_env_filter(EnvFilter::new("info,p2p_chat=debug"))
95            .try_init();
96    }
97}
98
99/// Prints a banner with startup information.
100fn print_start_banner(args: &AppArgs, port: u16, web_port: u16) {
101    println!("🚀 Starting P2P E2E Messenger");
102    println!(
103        "Mode: {}",
104        if args.mailbox {
105            "Mailbox Node"
106        } else {
107            "Client"
108        }
109    );
110    println!("Port: {}", port);
111    if !args.mailbox {
112        println!("Web UI: http://127.0.0.1:{}", web_port);
113    }
114    println!("Data directory: {}", args.data_dir);
115    println!();
116}
117
118/// Prints information about the user's identity.
119fn print_identity_info(identity: &Arc<Identity>) {
120    println!("Identity loaded:");
121    println!("  Peer ID: {}", identity.peer_id);
122    println!(
123        "  E2E Public Key: {}",
124        BASE64_STANDARD.encode(identity.hpke_public_key())
125    );
126    println!();
127}
128
129/// Resolves the encryption password.
130///
131/// The password can be provided via a command-line argument or an environment variable.
132fn resolve_encryption_password(args: &AppArgs) -> Result<String> {
133    args
134        .encryption_password
135        .clone()
136        .or_else(|| std::env::var("P2P_MESSENGER_PASSWORD").ok())
137        .ok_or_else(|| {
138            anyhow!(
139                "Encryption password not provided. Supply --encryption-password or set P2P_MESSENGER_PASSWORD."
140            )
141        })
142}
143
144/// Loads an encryption salt from a file, or creates a new one if it doesn't exist.
145fn load_or_create_salt(path: &str) -> Result<[u8; 16]> {
146    if Path::new(path).exists() {
147        let bytes = std::fs::read(path)?;
148        if bytes.len() != 16 {
149            anyhow::bail!(
150                "Encryption salt at '{}' has unexpected length {} (expected 16)",
151                path,
152                bytes.len()
153            );
154        }
155        let mut salt = [0u8; 16];
156        salt.copy_from_slice(&bytes);
157        Ok(salt)
158    } else {
159        let generated = StorageEncryption::generate_salt();
160        std::fs::write(path, generated)?;
161        Ok(generated)
162    }
163}
164
165/// Finds a free TCP port on the local machine.
166fn find_free_port() -> Result<u16> {
167    let listener = TcpListener::bind("127.0.0.1:0")?;
168    Ok(listener.local_addr()?.port())
169}