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
54impl ServerConfig {
70 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 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 #[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}