1#![allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
2
3use core::fmt;
4use std::{
5 env, fs,
6 path::{Path, PathBuf},
7 process,
8 str::FromStr,
9 sync::Arc,
10 time::Duration,
11};
12
13use jsonrs::Json;
14pub use pool::PoolConfig;
15
16use crate::{
17 Result,
18 log::{self},
19 log_info, log_warn,
20};
21
22#[derive(Clone, Debug)]
23pub enum Preset {
24 Read,
25 ReadWrite,
26 Post,
27}
28
29#[derive(Clone)]
30pub struct ServerConfig {
31 pub port: u16,
32 pub pool_conf: PoolConfig,
33 pub keep_alive_timeout: Duration,
34 pub keep_alive_requests: u16,
35 pub log_file: Option<String>,
36 pub setup_lib: Option<String>,
37 pub preset: Preset,
38
39 #[cfg(feature = "tls")]
40 pub tls_config: Option<Arc<rustls::ServerConfig>>,
41}
42
43impl fmt::Debug for ServerConfig {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 let mut deb = f.debug_struct("ServerConfig");
46 deb.field("port", &self.port)
47 .field("pool_conf", &self.pool_conf)
48 .field("keep_alive_timeout", &self.keep_alive_timeout)
49 .field("keep_alive_requests", &self.keep_alive_requests)
50 .field("setup_lib", &self.setup_lib)
51 .field("preset", &self.preset)
52 .field("log_file", &self.log_file);
53
54 #[cfg(feature = "tls")]
55 deb.field("tls", &self.tls_config.is_some());
56
57 deb.finish()
58 }
59}
60
61#[cfg(not(test))]
62fn get_default_conf_file() -> Option<PathBuf> {
63 if let Ok(path) = env::var("XDG_CONFIG_HOME") {
64 let mut p = PathBuf::new();
65 p.push(path);
66 p.push("http-srv");
67 p.push("config.json");
68 Some(p)
69 } else if let Ok(path) = env::var("HOME") {
70 let mut p = PathBuf::new();
71 p.push(path);
72 p.push(".config");
73 p.push("http-srv");
74 p.push("config.json");
75 Some(p)
76 } else {
77 None
78 }
79}
80
81#[cfg(test)]
82fn get_default_conf_file() -> Option<PathBuf> {
83 None
84}
85
86#[cfg(feature = "tls")]
87#[allow(clippy::unwrap_used)]
88fn get_tls_config(cert: Option<String>, pkey: Option<String>) -> Result<Arc<rustls::ServerConfig>> {
89 use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
90
91 let Some(cert) = cert else {
92 return Err("Missing certificate file".into());
93 };
94 let Some(pkey) = pkey else {
95 return Err("Missing private key file".into());
96 };
97
98 let certs = CertificateDer::pem_file_iter(cert)
99 .unwrap()
100 .map(|cert| cert.unwrap())
101 .collect();
102 let private_key = PrivateKeyDer::from_pem_file(pkey).unwrap();
103 let config = rustls::ServerConfig::builder()
104 .with_no_client_auth()
105 .with_single_cert(certs, private_key)
106 .map_err(|err| format!("rustls: {err}"))?;
107
108 Ok(Arc::new(config))
109}
110
111fn parse_preset(arg: &str, is_from_cli: bool) -> Result<Preset> {
112 Ok(match arg {
113 "read" => Preset::Read,
114 "readwrite" => Preset::ReadWrite,
115 "post" => Preset::Post,
116 p => {
117 return Err(format!(
118 "Unknown argument for \"{}preset\": {p}.\
119 Valid values: read, readwrite, post",
120 if is_from_cli { "--" } else { "" }
121 )
122 .into());
123 }
124 })
125}
126
127impl ServerConfig {
143 pub fn parse<S: AsRef<str>>(args: &[S]) -> Result<Self> {
145 const CONFIG_FILE_ARG: &str = "--conf";
146
147 let mut conf = Self::default();
148
149 let mut conf_file = get_default_conf_file();
150
151 let mut first_pass = args.iter();
153 while let Some(arg) = first_pass.next() {
154 if arg.as_ref() == CONFIG_FILE_ARG {
155 let fname = first_pass
156 .next()
157 .ok_or_else(|| format!("Missing argument for \"{CONFIG_FILE_ARG}\""))?;
158 let filename = PathBuf::from(fname.as_ref());
159 if filename.exists() {
160 conf_file = Some(filename);
161 } else {
162 log_warn!(
163 "Config path: {} doesn't exist",
164 filename.as_os_str().to_str().unwrap_or("[??]")
165 );
166 }
167 }
168 }
169
170 if let Some(cfile) = conf_file {
171 conf.parse_conf_file(&cfile)?;
172 }
173
174 let mut pool_conf_builder = PoolConfig::builder();
175
176 #[cfg(feature = "tls")]
177 let mut tls = false;
178 #[cfg(feature = "tls")]
179 let mut cert: Option<String> = None;
180 #[cfg(feature = "tls")]
181 let mut privkey: Option<String> = None;
182
183 let mut args = args.iter();
184 while let Some(arg) = args.next() {
185 macro_rules! parse_next {
186 () => {
187 args.next_parse().ok_or_else(|| {
188 format!("Missing or incorrect argument for \"{}\"", arg.as_ref())
189 })?
190 };
191 (as $t:ty) => {{
192 let _next: $t = parse_next!();
193 _next
194 }};
195 }
196
197 match arg.as_ref() {
198 "-p" | "--port" => conf.port = parse_next!(),
199 "-n" | "-n-workers" => {
200 pool_conf_builder.set_n_workers(parse_next!(as u16));
201 }
202 "-d" | "--dir" => {
203 let path: String = parse_next!();
204 env::set_current_dir(Path::new(&path))?;
205 }
206 "-k" | "--keep-alive" => {
207 let timeout = parse_next!();
208 conf.keep_alive_timeout = Duration::from_secs_f32(timeout);
209 }
210 "-r" | "--keep-alive-requests" => conf.keep_alive_requests = parse_next!(),
211 "-l" | "--log" => conf.log_file = Some(parse_next!()),
212 "--license" => license(),
213 "--log-level" => {
214 let n: u8 = parse_next!();
215 log::set_level(n.try_into()?);
216 }
217 "--preset" => {
218 let arg = args.next().ok_or("Missing argument for \"--preset\"")?;
219 conf.preset = parse_preset(arg.as_ref(), true)?;
220 }
221
222 "--setup-lib" => conf.setup_lib = Some(parse_next!()),
223
224 #[cfg(feature = "tls")]
225 "--tls" => tls = true,
226
227 #[cfg(feature = "tls")]
228 "--cert-file" => cert = Some(parse_next!()),
229
230 #[cfg(feature = "tls")]
231 "--private-key" => privkey = Some(parse_next!()),
232
233 CONFIG_FILE_ARG => {
234 let _ = args.next();
235 }
236 "-h" | "--help" => help(),
237 unknown => return Err(format!("Unknow argument: {unknown}").into()),
238 }
239 }
240
241 conf.pool_conf = pool_conf_builder.build();
242
243 #[cfg(feature = "tls")]
244 if conf.tls_config.is_none() && tls {
245 conf.tls_config = Some(get_tls_config(cert, privkey)?);
246 }
247
248 log_info!("{conf:#?}");
249 Ok(conf)
250 }
251 #[allow(clippy::too_many_lines)]
252 fn parse_conf_file(&mut self, conf_file: &Path) -> crate::Result<()> {
253 if !conf_file.exists() {
254 return Ok(());
255 }
256 let conf_str = conf_file.as_os_str().to_str().unwrap_or("");
257 let f = fs::read_to_string(conf_file).unwrap_or_else(|err| {
258 eprintln!("Error reading config file \"{conf_str}\": {err}");
259 std::process::exit(1);
260 });
261 let json = Json::deserialize(&f).unwrap_or_else(|err| {
262 eprintln!("Error parsing config file: {err}");
263 std::process::exit(1);
264 });
265 log_info!("Parsing config file: {conf_str}");
266 let Json::Object(obj) = json else {
267 return Err("Expected json object".into());
268 };
269
270 #[cfg(feature = "tls")]
271 let mut tls = false;
272
273 #[cfg(feature = "tls")]
274 let mut cert: Option<String> = None;
275
276 #[cfg(feature = "tls")]
277 let mut privkey: Option<String> = None;
278
279 for (k, v) in obj {
280 macro_rules! num {
281 () => {
282 num!(v)
283 };
284 ($v:ident) => {
285 $v.number().ok_or_else(|| {
286 format!("Parsing config file ({conf_str}): Expected number for \"{k}\"")
287 })?
288 };
289 ($v:ident as $t:ty) => {{
290 let _n = num!($v);
291 _n as $t
292 }};
293 }
294 macro_rules! bool {
295 ($v:ident) => {
296 $v.boolean().ok_or_else(|| {
297 format!("Parsing config file ({conf_str}): Expected boolean for \"{k}\"")
298 })?
299 };
300 }
301 macro_rules! string {
302 ($v:ident) => {
303 $v.string()
304 .ok_or_else(|| {
305 format!("Parsing config file ({conf_str}): Expected string for \"{k}\"")
306 })?
307 .to_string()
308 };
309 () => {
310 string!(v)
311 };
312 }
313 macro_rules! obj {
314 () => {
315 v.object().ok_or_else(|| {
316 format!("Parsing config file ({conf_str}): Expected object for \"{k}\"")
317 })?
318 };
319 }
320
321 macro_rules! path {
322 ($v:ident) => {{
323 let path: String = string!($v);
324 path.replacen(
325 '~',
326 env::var("HOME").as_ref().map(String::as_str).unwrap_or("~"),
327 1,
328 )
329 }};
330
331 () => {
332 path!(v)
333 };
334 }
335
336 match &*k {
337 "port" => self.port = num!() as u16,
338 "root_dir" => {
339 let path = path!();
340 env::set_current_dir(Path::new(&path))?;
341 }
342 "keep_alive_timeout" => self.keep_alive_timeout = Duration::from_secs_f64(num!()),
343 "keep_alive_requests" => self.keep_alive_requests = num!() as u16,
344 "log_file" => self.log_file = Some(string!()),
345 "log_level" => {
346 let n = num!(v as u8);
347 log::set_level(n.try_into()?);
348 }
349 "preset" => {
350 let arg = string!(v);
351 self.preset = parse_preset(arg.as_str(), false)?;
352 }
353 #[cfg(feature = "tls")]
354 "tls" => {
355 for (k, v) in obj!() {
356 match &**k {
357 "enabled" => tls = bool!(v),
358 "cert_file" => cert = Some(path!(v)),
359 "private_key" => privkey = Some(path!(v)),
360 _ => log_warn!(
361 "Parsing config file ({conf_str}): Unexpected key: \"{k}\""
362 ),
363 }
364 }
365 }
366 "setup_lib" if self.setup_lib.is_none() => {
367 self.setup_lib = Some(path!(v));
368 }
369 "pool_config" => {
370 for (k, v) in obj!() {
371 match &**k {
372 "n_workers" => self.pool_conf.n_workers = num!(v as u16),
373 "pending_buffer_size" => {
374 let n = v.number().map(|n| n as u16);
375 self.pool_conf.incoming_buf_size = n;
376 }
377 _ => log_warn!(
378 "Parsing config file ({conf_str}): Unexpected key: \"{k}\""
379 ),
380 }
381 }
382 }
383 _ => log_warn!("Parsing config file ({conf_str}): Unexpected key: \"{k}\""),
384 }
385 }
386
387 #[cfg(feature = "tls")]
388 if tls {
389 self.tls_config = Some(get_tls_config(cert, privkey)?);
390 }
391
392 Ok(())
393 }
394 #[inline]
395 #[must_use]
396 pub fn pool_config(mut self, conf: PoolConfig) -> Self {
397 self.pool_conf = conf;
398 self
399 }
400 #[inline]
401 #[must_use]
402 pub fn port(mut self, port: u16) -> Self {
403 self.port = port;
404 self
405 }
406 #[inline]
407 #[must_use]
408 pub fn keep_alive_timeout(mut self, timeout: Duration) -> Self {
409 self.keep_alive_timeout = timeout;
410 self
411 }
412 #[inline]
413 #[must_use]
414 pub fn keep_alive_requests(mut self, n: u16) -> Self {
415 self.keep_alive_requests = n;
416 self
417 }
418
419 #[inline]
420 #[must_use]
421 pub fn preset(mut self, p: Preset) -> Self {
422 self.preset = p;
423 self
424 }
425}
426
427fn help() -> ! {
428 println!(
430 "\
431http-srv: Copyright (C) 2025 Saúl Valdelvira
432
433This program is free software: you can redistribute it and/or modify it
434under the terms of the GNU General Public License as published by the
435Free Software Foundation, version 3.
436Use http-srv --license to read a copy of the GPL v3
437
438USAGE: http-srv [-p <port>] [-n <n-workers>] [-d <working-dir>]
439PARAMETERS:
440 -p, --port <port> TCP Port to listen for requests
441 -n, --n-workers <n> Number of concurrent workers
442 -d, --dir <working-dir> Root directory of the server
443 -k, --keep-alive <sec> Keep alive seconds
444 -r, --keep-alive-requests <num> Keep alive max requests
445 -l, --log <file> Set log file
446 -h, --help Display this help message
447 --log-level <n> Set log level
448 --setup-lib <file> Load the given file to setup the server
449 --conf <file> Use the given config file instead of the default one
450 --license Output the license of this program
451 --preset <read|readwrite|post> Sets a default handler preset
452 read: Only GET and HEAD methods are allowed
453 readwrite: GET, HEAD, POST and DELETE are allowed
454 post: Only POST is allowed
455
456 --tls Enable TLS
457 --cert-file Certificate file for TLS
458 --private-key Private key for TLS
459EXAMPLES:
460 http-srv -p 8080 -d /var/html
461 http-srv -d ~/desktop -n 1024 --keep-alive 120
462 http-srv --log /var/log/http-srv.log"
463 );
464 process::exit(0);
465}
466
467fn license() -> ! {
468 println!(include_str!("../COPYING"));
469 process::exit(0);
470}
471
472trait ParseIterator {
473 fn next_parse<T: FromStr>(&mut self) -> Option<T>;
474}
475
476impl<I, R: AsRef<str>> ParseIterator for I
477where
478 I: Iterator<Item = R>,
479{
480 fn next_parse<T: FromStr>(&mut self) -> Option<T> {
481 self.next()?.as_ref().parse().ok()
482 }
483}
484
485impl Default for ServerConfig {
486 #[inline]
494 fn default() -> Self {
495 Self {
496 port: 80,
497 pool_conf: PoolConfig::default(),
498 keep_alive_timeout: Duration::from_secs(0),
499 keep_alive_requests: 10000,
500 log_file: None,
501 setup_lib: None,
502 preset: Preset::Read,
503 #[cfg(feature = "tls")]
504 tls_config: None,
505 }
506 }
507}
508
509#[cfg(test)]
510mod test {
511 #![allow(clippy::unwrap_used)]
512
513 use crate::ServerConfig;
514
515 #[test]
516 fn valid_args() {
517 let conf = vec!["-p".to_string(), "80".to_string()];
518 ServerConfig::parse(&conf).unwrap();
519 }
520
521 macro_rules! expect_err {
522 ($conf:expr , $msg:literal) => {
523 match ServerConfig::parse(&$conf) {
524 Ok(c) => panic!("Didn't panic: {c:#?}"),
525 Err(msg) => assert_eq!(msg.get_message(), $msg),
526 }
527 };
528 }
529
530 #[test]
531 fn unknown() {
532 let conf = vec!["?"];
533 expect_err!(conf, "Unknow argument: ?");
534 }
535
536 #[test]
537 fn missing() {
538 let conf = vec!["-n"];
539 expect_err!(conf, "Missing or incorrect argument for \"-n\"");
540 }
541
542 #[test]
543 fn parse_error() {
544 let conf = vec!["-p", "abc"];
545 expect_err!(conf, "Missing or incorrect argument for \"-p\"");
546 }
547}