http_srv/
config.rs

1#![allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
2
3use std::{
4    env, fs,
5    path::{Path, PathBuf},
6    process,
7    str::FromStr,
8    time::Duration,
9};
10
11use jsonrs::Json;
12use pool::PoolConfig;
13
14use crate::{
15    Result,
16    log::{self},
17    log_info, log_warn,
18};
19
20#[derive(Clone, Debug)]
21pub struct ServerConfig {
22    pub port: u16,
23    pub pool_conf: PoolConfig,
24    pub keep_alive_timeout: Duration,
25    pub keep_alive_requests: u16,
26    pub log_file: Option<String>,
27}
28
29#[cfg(not(test))]
30fn get_default_conf_file() -> Option<PathBuf> {
31    if let Ok(path) = env::var("XDG_CONFIG_HOME") {
32        let mut p = PathBuf::new();
33        p.push(path);
34        p.push("http-srv");
35        p.push("config.json");
36        Some(p)
37    } else if let Ok(path) = env::var("HOME") {
38        let mut p = PathBuf::new();
39        p.push(path);
40        p.push(".config");
41        p.push("http-srv");
42        p.push("config.json");
43        Some(p)
44    } else {
45        None
46    }
47}
48
49#[cfg(test)]
50fn get_default_conf_file() -> Option<PathBuf> {
51    None
52}
53
54/// [`crate::HttpServer`] configuration
55///
56/// # Example
57/// ```
58/// use http_srv::ServerConfig;
59/// use pool::PoolConfig;
60///
61/// let pool_conf = PoolConfig::builder()
62///                 .n_workers(120_u16)
63///                 .build();
64/// let conf =
65/// ServerConfig::default()
66///     .port(8080)
67///     .pool_config(pool_conf);
68/// ```
69impl ServerConfig {
70    /// Parse the configuration from the command line args
71    pub fn parse<S: AsRef<str>>(args: &[S]) -> Result<Self> {
72        const CONFIG_FILE_ARG: &str = "--conf";
73
74        let mut conf = Self::default();
75
76        let mut conf_file = get_default_conf_file();
77
78        /* Parse the --config-file before the rest */
79        let mut first_pass = args.iter();
80        while let Some(arg) = first_pass.next() {
81            if arg.as_ref() == CONFIG_FILE_ARG {
82                let fname = first_pass
83                    .next()
84                    .ok_or_else(|| format!("Missing argument for \"{CONFIG_FILE_ARG}\""))?;
85                let filename = PathBuf::from(fname.as_ref());
86                if filename.exists() {
87                    conf_file = Some(filename);
88                } else {
89                    log_warn!(
90                        "Config path: {} doesn't exist",
91                        filename.as_os_str().to_str().unwrap_or("[??]")
92                    );
93                }
94            }
95        }
96
97        if let Some(cfile) = conf_file {
98            conf.parse_conf_file(&cfile)?;
99        }
100
101        let mut args = args.iter();
102        while let Some(arg) = args.next() {
103            macro_rules! parse_next {
104                () => {
105                    args.next_parse().ok_or_else(|| {
106                        format!("Missing or incorrect argument for \"{}\"", arg.as_ref())
107                    })?
108                };
109                (as $t:ty) => {{
110                    let _next: $t = parse_next!();
111                    _next
112                }};
113            }
114
115            let mut pool_conf_builder = PoolConfig::builder();
116
117            match arg.as_ref() {
118                "-p" | "--port" => conf.port = parse_next!(),
119                "-n" | "-n-workers" => {
120                    pool_conf_builder.set_n_workers(parse_next!(as u16));
121                }
122                "-d" | "--dir" => {
123                    let path: String = parse_next!();
124                    env::set_current_dir(Path::new(&path))?;
125                }
126                "-k" | "--keep-alive" => {
127                    let timeout = parse_next!();
128                    conf.keep_alive_timeout = Duration::from_secs_f32(timeout);
129                }
130                "-r" | "--keep-alive-requests" => conf.keep_alive_requests = parse_next!(),
131                "-l" | "--log" => conf.log_file = Some(parse_next!()),
132                "--license" => license(),
133                "--log-level" => {
134                    let n: u8 = parse_next!();
135                    log::set_level(n.try_into()?);
136                }
137                CONFIG_FILE_ARG => {
138                    let _ = args.next();
139                }
140                "-h" | "--help" => help(),
141                unknown => return Err(format!("Unknow argument: {unknown}").into()),
142            }
143
144            conf.pool_conf = pool_conf_builder.build();
145        }
146
147        log_info!("{conf:#?}");
148        Ok(conf)
149    }
150    fn parse_conf_file(&mut self, conf_file: &Path) -> crate::Result<()> {
151        if !conf_file.exists() {
152            return Ok(());
153        }
154        let conf_str = conf_file.as_os_str().to_str().unwrap_or("");
155        let f = fs::read_to_string(conf_file).unwrap_or_else(|err| {
156            eprintln!("Error reading config file \"{conf_str}\": {err}");
157            std::process::exit(1);
158        });
159        let json = Json::deserialize(&f).unwrap_or_else(|err| {
160            eprintln!("Error parsing config file: {err}");
161            std::process::exit(1);
162        });
163        log_info!("Parsing config file: {conf_str}");
164        let Json::Object(obj) = json else {
165            return Err("Expected json object".into());
166        };
167        for (k, v) in obj {
168            macro_rules! num {
169                () => {
170                    num!(v)
171                };
172                ($v:ident) => {
173                    $v.number().ok_or_else(|| {
174                        format!("Parsing config file ({conf_str}): Expected number for \"{k}\"")
175                    })?
176                };
177                ($v:ident as $t:ty) => {{
178                    let _n = num!($v);
179                    _n as $t
180                }};
181            }
182            macro_rules! string {
183                () => {
184                    v.string()
185                        .ok_or_else(|| {
186                            format!("Parsing config file ({conf_str}): Expected string for \"{k}\"")
187                        })?
188                        .to_string()
189                };
190            }
191            macro_rules! obj {
192                () => {
193                    v.object().ok_or_else(|| {
194                        format!("Parsing config file ({conf_str}): Expected object for \"{k}\"")
195                    })?
196                };
197            }
198
199            match &*k {
200                "port" => self.port = num!() as u16,
201                "root_dir" => {
202                    let path: String = string!();
203                    let path = path.replacen(
204                        '~',
205                        env::var("HOME").as_ref().map(String::as_str).unwrap_or("~"),
206                        1,
207                    );
208                    env::set_current_dir(Path::new(&path))?;
209                }
210                "keep_alive_timeout" => self.keep_alive_timeout = Duration::from_secs_f64(num!()),
211                "keep_alive_requests" => self.keep_alive_requests = num!() as u16,
212                "log_file" => self.log_file = Some(string!()),
213                "log_level" => {
214                    let n = num!(v as u8);
215                    log::set_level(n.try_into()?);
216                }
217                "pool_config" => {
218                    for (k, v) in obj!() {
219                        match &**k {
220                            "n_workers" => self.pool_conf.n_workers = num!(v as u16),
221                            "pending_buffer_size" => {
222                                let n = v.number().map(|n| n as u16);
223                                self.pool_conf.incoming_buf_size = n;
224                            }
225                            _ => log_warn!(
226                                "Parsing config file ({conf_str}): Unexpected key: \"{k}\""
227                            ),
228                        }
229                    }
230                }
231                _ => log_warn!("Parsing config file ({conf_str}): Unexpected key: \"{k}\""),
232            }
233        }
234        Ok(())
235    }
236    #[inline]
237    #[must_use]
238    pub fn pool_config(mut self, conf: PoolConfig) -> Self {
239        self.pool_conf = conf;
240        self
241    }
242    #[inline]
243    #[must_use]
244    pub fn port(mut self, port: u16) -> Self {
245        self.port = port;
246        self
247    }
248    #[inline]
249    #[must_use]
250    pub fn keep_alive_timeout(mut self, timeout: Duration) -> Self {
251        self.keep_alive_timeout = timeout;
252        self
253    }
254    #[inline]
255    #[must_use]
256    pub fn keep_alive_requests(mut self, n: u16) -> Self {
257        self.keep_alive_requests = n;
258        self
259    }
260}
261
262fn help() -> ! {
263    println!(
264        "\
265http-srv: Copyright (C) 2025 Saúl Valdelvira
266
267This program is free software: you can redistribute it and/or modify it
268under the terms of the GNU General Public License as published by the
269Free Software Foundation, version 3.
270Use http-srv --license to read a copy of the GPL v3
271
272USAGE: http-srv [-p <port>] [-n <n-workers>] [-d <working-dir>]
273PARAMETERS:
274    -p, --port <port>    TCP Port to listen for requests
275    -n, --n-workers <n>  Number of concurrent workers
276    -d, --dir <working-dir>  Root directory of the server
277    -k, --keep-alive <sec>   Keep alive seconds
278    -r, --keep-alive-requests <num> Keep alive max requests
279    -l, --log <file>   Set log file
280    -h, --help      Display this help message
281    --log-level <n> Set log level
282    --conf <file>   Use the given config file instead of the default one
283    --license       Output the license of this program
284EXAMPLES:
285  http-srv -p 8080 -d /var/html
286  http-srv -d ~/desktop -n 1024 --keep-alive 120
287  http-srv --log /var/log/http-srv.log"
288    );
289    process::exit(0);
290}
291
292fn license() -> ! {
293    println!(include_str!("../COPYING"));
294    process::exit(0);
295}
296
297trait ParseIterator {
298    fn next_parse<T: FromStr>(&mut self) -> Option<T>;
299}
300
301impl<I, R: AsRef<str>> ParseIterator for I
302where
303    I: Iterator<Item = R>,
304{
305    fn next_parse<T: FromStr>(&mut self) -> Option<T> {
306        self.next()?.as_ref().parse().ok()
307    }
308}
309
310impl Default for ServerConfig {
311    /// Default configuration
312    ///
313    /// - Port: 80
314    /// - Nº Workers: 1024
315    /// - Keep Alive Timeout: 0s (Disabled)
316    /// - Keep Alove Requests: 10000
317    #[inline]
318    fn default() -> Self {
319        Self {
320            port: 80,
321            pool_conf: PoolConfig::default(),
322            keep_alive_timeout: Duration::from_secs(0),
323            keep_alive_requests: 10000,
324            log_file: None,
325        }
326    }
327}
328
329#[cfg(test)]
330mod test {
331    #![allow(clippy::unwrap_used)]
332
333    use crate::ServerConfig;
334
335    #[test]
336    fn valid_args() {
337        let conf = vec!["-p".to_string(), "80".to_string()];
338        ServerConfig::parse(&conf).unwrap();
339    }
340
341    macro_rules! expect_err {
342        ($conf:expr , $msg:literal) => {
343            match ServerConfig::parse(&$conf) {
344                Ok(c) => panic!("Didn't panic: {c:#?}"),
345                Err(msg) => assert_eq!(msg.get_message(), $msg),
346            }
347        };
348    }
349
350    #[test]
351    fn unknown() {
352        let conf = vec!["?"];
353        expect_err!(conf, "Unknow argument: ?");
354    }
355
356    #[test]
357    fn missing() {
358        let conf = vec!["-n"];
359        expect_err!(conf, "Missing or incorrect argument for \"-n\"");
360    }
361
362    #[test]
363    fn parse_error() {
364        let conf = vec!["-p", "abc"];
365        expect_err!(conf, "Missing or incorrect argument for \"-p\"");
366    }
367}