1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
//  WIZARD.rs
//    by Lut99
//
//  Created:
//    01 Jun 2023, 12:43:20
//  Last edited:
//    07 Mar 2024, 09:54:57
//  Auto updated?
//    Yes
//
//  Description:
//!   Implements a CLI wizard for setting up nodes, making the process
//!   _even_ easier.
//

use std::borrow::Cow;
use std::collections::HashMap;
use std::error;
use std::fmt::{Display, Formatter, Result as FResult};
use std::fs::{self, File};
use std::io::Write as _;
use std::path::{Path, PathBuf};

use brane_cfg::info::Info;
use brane_cfg::node::{self, NodeConfig, NodeKind, NodeSpecificConfig};
use brane_cfg::proxy::{ForwardConfig, ProxyConfig, ProxyProtocol};
use brane_shr::input::{FileHistory, confirm, input, input_map, input_path, select};
use console::style;
use dirs::config_dir;
use enum_debug::EnumDebug as _;
use log::{debug, info};
use specifications::address::Address;
use validator::{FromStrValidator, MapValidator, PortValidator, RangeValidator};

pub mod validator;

use crate::spec::InclusiveRange;

type PortRangeValidator = RangeValidator<PortValidator>;
type AddressValidator = FromStrValidator<Address>;
type PortMapValidator = MapValidator<PortValidator, AddressValidator>;

/***** HELPER MACROS *****/
/// Generates a FileHistory that points to some branectl-specific directory in the [`config_dir()`].
macro_rules! hist {
    ($name:literal) => {{
        let hist = FileHistory::new(config_dir().unwrap().join("branectl").join("history").join($name));
        debug!("{hist:?}");
        hist
    }};
}

/// Writes a few lines that generate a directory, with logging statements.
///
/// # Arguments
/// - `[$name, $value]`: The name and subsequent value of the variable that contains the given path.
macro_rules! generate_dir {
    ($value:ident) => {
        if !$value.exists() {
            debug!("Generating '{}'...", $value.display());
            if let Err(err) = fs::create_dir(&$value) {
                return Err(Error::GenerateDir { path: $value, err });
            }
        }
    };

    ($name:ident, $value:expr) => {
        let $name: PathBuf = $value;
        generate_dir!($name);
    };
}





/***** ERRORS *****/
/// Defines errors that relate to the wizard.
#[derive(Debug)]
pub enum Error {
    /// Failed to query the user for the node config file.
    NodeConfigQuery { err: Box<Self> },
    /// Failed to write the node config file.
    NodeConfigWrite { err: Box<Self> },
    /// Failed to query the user for the proxy config file.
    ProxyConfigQuery { err: Box<Self> },
    /// Failed to write the proxy config file.
    ProxyConfigWrite { err: Box<Self> },

    /// Failed to create a new file.
    ConfigCreate { path: PathBuf, err: std::io::Error },
    /// Failed to generate a configuration file.
    ConfigSerialize { path: PathBuf, err: brane_cfg::info::YamlError },
    /// Failed to write to the config file.
    ConfigWrite { path: PathBuf, err: std::io::Error },
    /// Failed to generate a directory.
    GenerateDir { path: PathBuf, err: std::io::Error },
    /// Failed the query the user for input.
    ///
    /// The `what` should fill in: `Failed to query the user for ...`
    Input { what: &'static str, err: brane_shr::input::Error },
}
impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> FResult {
        use Error::*;
        match self {
            NodeConfigQuery { .. } => write!(f, "Failed to query node configuration"),
            NodeConfigWrite { .. } => write!(f, "Failed to write node config file"),
            ProxyConfigQuery { .. } => write!(f, "Failed to query proxy service configuration"),
            ProxyConfigWrite { .. } => write!(f, "Failed to write proxy service config file"),

            ConfigCreate { path, .. } => write!(f, "Failed to create config file '{}'", path.display()),
            ConfigSerialize { path, .. } => write!(f, "Failed to serialize config to '{}'", path.display()),
            ConfigWrite { path, .. } => write!(f, "Failed to write to config file '{}'", path.display()),
            GenerateDir { path, .. } => write!(f, "Failed to generate directory '{}'", path.display()),
            Input { what, .. } => write!(f, "Failed to query the user for {what}"),
        }
    }
}
impl error::Error for Error {
    fn source(&self) -> Option<&(dyn 'static + error::Error)> {
        use Error::*;
        match self {
            NodeConfigQuery { err } => Some(err),
            NodeConfigWrite { err } => Some(err),
            ProxyConfigQuery { err } => Some(err),
            ProxyConfigWrite { err } => Some(err),

            ConfigCreate { err, .. } => Some(err),
            ConfigSerialize { err, .. } => Some(err),
            ConfigWrite { err, .. } => Some(err),
            GenerateDir { err, .. } => Some(err),
            Input { err, .. } => Some(err),
        }
    }
}





/***** HELPER FUNCTIONS *****/
/// Writes a given [`Config`] to disk.
///
/// This wraps the default [`Config::to_path()`] function to also include a nice header.
///
/// # Arguments
/// - `config`: The [`Config`]-file to write.
/// - `path`: The path to write the file to.
/// - `url`: The wiki-URL to write in the file.
///
/// # Errors
/// This function may error if we failed to write any of this.
///
/// # Panics
/// This function may panic if the given path has no filename.
fn write_config<C>(config: C, path: impl AsRef<Path>, url: impl AsRef<str>) -> Result<(), Error>
where
    C: Info<Error = serde_yaml::Error>,
{
    let path: &Path = path.as_ref();
    let url: &str = url.as_ref();
    debug!("Generating config file '{}'...", path.display());

    // Deduce the filename
    let filename: Cow<str> = match path.file_name() {
        Some(filename) => filename.to_string_lossy(),
        None => {
            panic!("No filename found in '{}'", path.display());
        },
    };

    // Convert the filename to nice header
    let mut header_name: String = String::with_capacity(filename.len());
    let mut saw_lowercase: bool = false;
    let mut ext: bool = false;
    for c in filename.chars() {
        if !ext && c == '.' {
            // Move to extension mode
            header_name.push('.');
            ext = true;
        } else if !ext && (c == ' ' || c == '-' || c == '_') {
            // Write it as a space
            header_name.push(' ');
        } else if !ext && saw_lowercase && c.is_ascii_uppercase() {
            // Write is with a space, since we assume it's a word boundary in camelCase
            header_name.push(' ');
            header_name.push(c);
        } else if !ext && c.is_ascii_lowercase() {
            // Capitalize it
            header_name.push((c as u8 - b'a' + b'A') as char);
        } else {
            // The rest is pushed as-is
            header_name.push(c);
        }

        // Update whether we saw a lowercase last step
        saw_lowercase = c.is_ascii_lowercase();
    }

    // Create a file, now
    let mut handle: File = match File::create(path) {
        Ok(handle) => handle,
        Err(err) => {
            return Err(Error::ConfigCreate { path: path.into(), err });
        },
    };

    // Write the header to a string
    if let Err(err) = writeln!(handle, "# {header_name}") {
        return Err(Error::ConfigWrite { path: path.into(), err });
    };
    if let Err(err) = writeln!(handle, "#   by branectl") {
        return Err(Error::ConfigWrite { path: path.into(), err });
    };
    if let Err(err) = writeln!(handle, "# ") {
        return Err(Error::ConfigWrite { path: path.into(), err });
    };
    if let Err(err) = writeln!(handle, "# This file has been generated using the `branectl wizard` subcommand. You can") {
        return Err(Error::ConfigWrite { path: path.into(), err });
    };
    if let Err(err) = writeln!(handle, "# manually change this file after generation; it is just a normal YAML file.") {
        return Err(Error::ConfigWrite { path: path.into(), err });
    };
    if let Err(err) = writeln!(handle, "# Documentation for how to do so can be found here:") {
        return Err(Error::ConfigWrite { path: path.into(), err });
    };
    if let Err(err) = writeln!(handle, "# {url}") {
        return Err(Error::ConfigWrite { path: path.into(), err });
    };
    if let Err(err) = writeln!(handle, "# ") {
        return Err(Error::ConfigWrite { path: path.into(), err });
    };
    if let Err(err) = writeln!(handle) {
        return Err(Error::ConfigWrite { path: path.into(), err });
    };

    // Write the remainder of the file
    if let Err(err) = config.to_writer(handle, true) {
        return Err(Error::ConfigSerialize { path: path.into(), err });
    }
    Ok(())
}

/***** QUERY FUNCTIONS *****/
/// Queries the user for the proxy services configuration.
///
/// # Returns
/// A new [`ProxyConfig`] that reflects the user's choices.
///
/// # Errors
/// This function may error if we failed to query the user.
pub fn query_proxy_config() -> Result<ProxyConfig, Error> {
    // Query the user for the range
    let range: InclusiveRange<u16> = match input(
        "port range",
        "P1. Enter the range of ports allocated for outgoing connections",
        Some(InclusiveRange::new(4200, 4299)),
        Some(PortRangeValidator::default()),
        Some(hist!("prx-outgoing_range.hist")),
    ) {
        Ok(range) => range,
        Err(err) => {
            return Err(Error::Input { what: "outgoing range", err });
        },
    };
    debug!("Outgoing range: [{}, {}]", range.0.start(), range.0.end());
    println!();

    // Read the map of incoming ports
    let incoming: HashMap<u16, Address> = match input_map(
        "port",
        "address",
        "P2.1. Enter an incoming port map as '<incoming port>:<destination address>:<destination port>' (or leave empty to specify none)",
        "P2.%I. Enter an additional incoming port map as '<port>:<destination address>' (or leave empty to finish)",
        ":",
        // None::<NoValidator>,
        Some(PortMapValidator { allow_empty: true, ..Default::default() }),
        Some(hist!("prx-incoming.hist")),
    ) {
        Ok(incoming) => incoming,
        Err(err) => {
            return Err(Error::Input { what: "outgoing range", err });
        },
    };
    debug!("Incoming ports map:\n{:#?}", incoming);
    println!();

    // Finally, read any proxy
    let to_proxy_or_not_to_proxy: bool = match confirm("P3. Do you want to route outgoing traffic through a SOCKS proxy?", Some(false)) {
        Ok(yesno) => yesno,
        Err(err) => {
            return Err(Error::Input { what: "proxy confirmation", err });
        },
    };
    let forward: Option<ForwardConfig> = if to_proxy_or_not_to_proxy {
        // Query the address
        let address: Address = match input(
            "address",
            "P3a. Enter the target address (including port) to route the traffic to",
            None::<Address>,
            Some(AddressValidator::default()),
            Some(hist!("prx-forward-address.hist")),
        ) {
            Ok(address) => address,
            Err(err) => {
                return Err(Error::Input { what: "forwarding address", err });
            },
        };

        // Query the protocol
        let protocol: ProxyProtocol =
            match select("P3b. Enter the protocol to use to route traffic", vec![ProxyProtocol::Socks5, ProxyProtocol::Socks6], Some(0)) {
                Ok(prot) => prot,
                Err(err) => {
                    return Err(Error::Input { what: "forwarding protocol", err });
                },
            };

        // Construct the config
        Some(ForwardConfig { address, protocol })
    } else {
        None
    };
    debug!("Using forward config: {:?}", forward);
    println!();

    // Construct the ProxyConfig to return it
    Ok(ProxyConfig { outgoing_range: range.0, incoming, forward })
}

/// Queries the user for the node file configuration.
///
/// # Returns
/// A new [`NodeConfig`] that reflects the user's choices.
///
/// # Errors
/// This function may error if we failed to query the user.
pub fn query_proxy_node_config() -> Result<NodeConfig, Error> {
    // Construct the ProxyConfig to return it
    Ok(NodeConfig {
        hostnames: HashMap::new(),
        namespace: String::new(),
        node:      NodeSpecificConfig::Proxy(node::ProxyConfig {
            paths:    node::ProxyPaths { certs: "".into(), proxy: "".into() },
            services: node::ProxyServices {
                prx: node::PublicService {
                    name: "brane-prx".into(),
                    address: Address::Hostname("test.com".into(), 42),
                    bind: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::new(0, 0, 0, 0), 0)),
                    external_address: Address::Hostname("test.com".into(), 42),
                },
            },
        }),
    })
}





/***** LIBRARY *****/
/// Main handler for the `branectl wizard setup` (or `branectl wizard node`) subcommand.
///
/// # Arguments
///
/// # Errors
/// This function may error if any of the wizard steps fail.
pub fn setup() -> Result<(), Error> {
    info!("Running wizard to setup a new node...");

    // Let us setup the history structure
    generate_dir!(_path, config_dir().unwrap().join("branectl"));
    generate_dir!(_path, config_dir().unwrap().join("branectl").join("history"));

    // Do an intro prompt
    println!();
    println!(
        "{}{}{}",
        style("Welcome to ").bold(),
        style("Node Setup Wizard").bold().green(),
        style(format!(" for BRANE v{}", env!("CARGO_PKG_VERSION"))).bold()
    );
    println!();
    println!("This wizard will guide you through the process of setting up a node interactively.");
    println!("Simply answer the questions, and the required configuration files will be generated as you go.");
    println!();
    println!("You can abort the wizard at any time by pressing {}.", style("Ctrl+C").bold().green());
    println!();

    // Select the path where we will go to
    let mut prompt: Cow<str> = Cow::Borrowed("1. Select the location of the node configuration files");
    let path: PathBuf = loop {
        // Query the path
        let path: PathBuf = match input_path(prompt, Some("./"), Some(hist!("output_path.hist"))) {
            Ok(path) => path,
            Err(err) => {
                return Err(Error::Input { what: "config path", err });
            },
        };

        // Ask to create it if it does not exist
        if !path.exists() {
            // Do the question
            let ok: bool = match confirm("Directory '{}' does not exist. Create it?", Some(true)) {
                Ok(ok) => ok,
                Err(err) => {
                    return Err(Error::Input { what: "directory creation confirmation", err });
                },
            };

            // Create it, lest continue and try again
            if ok {
                generate_dir!(path);
            }
        }

        // Assert it's a directory
        if path.is_dir() {
            break path;
        }
        prompt = Cow::Owned(format!("Path '{}' does not point to a directory; specify another", path.display()));
    };
    debug!("Configuration directory: '{}'", path.display());
    println!();

    // Generate the configuration directories already
    generate_dir!(config_dir, path.join("config"));
    generate_dir!(certs_dir, config_dir.join("certs"));

    // Let us query the user for the type of node
    let kind: NodeKind = match select("2. Select the type of node to generate", [NodeKind::Central, NodeKind::Worker, NodeKind::Proxy], None) {
        Ok(kind) => kind,
        Err(err) => {
            return Err(Error::Input { what: "node kind", err });
        },
    };
    debug!("Building for node kind '{}'", kind.variant());
    println!();

    // Do a small intermittent text, which will be finished by node-specific contexts
    println!("You have selected to create a new {} node.", style(kind).bold().green());
    println!("For this node type, the following configuration files have to be generated:");

    // The rest is node-dependent
    match kind {
        NodeKind::Central => {},

        NodeKind::Worker => {},

        NodeKind::Proxy => {
            println!(" - {}", style(config_dir.join("proxy.yml").display()).bold());
            println!();

            // Note: we don't check if the user wants a custom config, since they very likely want it if they are setting up a proxy node
            // For the proxy, we only need to read the proxy config
            println!("=== proxy.yml===");
            let cfg: ProxyConfig = match query_proxy_config() {
                Ok(cfg) => cfg,
                Err(err) => {
                    return Err(Error::ProxyConfigQuery { err: Box::new(err) });
                },
            };
            let proxy_path: PathBuf = config_dir.join("proxy.yml");
            if let Err(err) = write_config(cfg, proxy_path, "https://wiki.enablingpersonalizedinterventions.nl/user-guide/config/admins/proxy.html") {
                return Err(Error::ProxyConfigWrite { err: Box::new(err) });
            }

            // Now we generate the node.yml file
            println!("=== node.yml ===");
            let node: NodeConfig = match query_proxy_node_config() {
                Ok(node) => node,
                Err(err) => {
                    return Err(Error::NodeConfigQuery { err: Box::new(err) });
                },
            };
            let node_path: PathBuf = path.join("node.yml");
            if let Err(err) = write_config(node, node_path, "https://wiki.enablingpersonalizedinterventions.nl/user-guide/config/admins/node.html") {
                return Err(Error::NodeConfigWrite { err: Box::new(err) });
            }
        },
    }

    // Done!
    Ok(())
}