Skip to content

Commit a015230

Browse files
committed
Support options to start ferron with basic auth enabled and as a forward proxy.
1 parent ab763d3 commit a015230

File tree

3 files changed

+85
-11
lines changed

3 files changed

+85
-11
lines changed

docs/commands.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,15 @@ Utility command to start up a basic HTTP server
4040
Usage: ferron serve [OPTIONS]
4141
4242
Options:
43-
-l, --listen-ip <LISTEN_IP> The listening IP to use [default: 127.0.0.1]
44-
-p, --port <PORT> The port to use [default: 3000]
45-
-r, --root <ROOT> The root directory to serve [default: .]
46-
--log <LOG> Where to output logs [default: stdout] [possible values: stdout, stderr, off]
47-
--error-log <ERROR_LOG> Where to output error logs [default: stderr] [possible values: stdout, stderr, off]
48-
-h, --help Print help
43+
-l, --listen-ip <LISTEN_IP> The listening IP to use [default: 127.0.0.1]
44+
-p, --port <PORT> The port to use [default: 3000]
45+
-r, --root <ROOT> The root directory to serve [default: .]
46+
-c, --credential <CREDENTIAL> Basic authentication credentials for authorized users. The credential value must be in the form "${user}:${hashed_password}" where the "${hashed_password}" is from the ferron-passwd program or from any program using the password-auth generate_hash() macro (see https://docs.rs/password-auth/latest/password_auth/fn.generate_hash.html)
47+
--disable-brute-protection Whether to disable brute-force password protection
48+
--forward-proxy Whether to start the server as a forward proxy
49+
--log <LOG> Where to output logs [default: stdout] [possible values: stdout, stderr, off]
50+
--error-log <ERROR_LOG> Where to output error logs [default: stderr] [possible values: stdout, stderr, off]
51+
-h, --help Print help
4952
```
5053

5154
### `ferron-passwd`

docs/use-cases/ferron-serve.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
2-
title: Basic HTTP server
2+
title: 'ferron serve' subcommand
33
---
44

55
If you simply need to serve files on the local filesystem, you can
6-
use the `ferron serve` command to do so. If you are familiar with
6+
use the `ferron serve` subcommand to do so. If you are familiar with
77
the [serve NPM package](https://www.npmjs.com/package/serve), the
88
`ferron serve` command will offer similar functionality. See also
99
the help message from running `ferron serve --help` for supported

ferron/src/main.rs

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,35 @@ fn before_starting_server(
124124
"* {{\n listen_ip \"{}\"\n default_http_port {}",
125125
http_serve_args.listen_ip, http_serve_args.port
126126
);
127+
if !http_serve_args.credential.is_empty() {
128+
let mut users = Vec::<String>::new();
129+
for credential in http_serve_args.credential.iter() {
130+
let (user, hashed_password) = credential
131+
.split_once(':')
132+
.ok_or(anyhow::anyhow!("Invalid credential format: {credential}"))?;
133+
users.push(user.to_owned());
134+
config_string.push_str(format!("\n user \"{user}\" \"{hashed_password}\"").as_str());
135+
}
136+
if http_serve_args.forward_proxy {
137+
config_string.push_str(
138+
format!(
139+
"\n forward_proxy_auth users=\"{}\" brute_protection=#{}",
140+
users.join(","),
141+
http_serve_args.disable_brute_protection
142+
)
143+
.as_str(),
144+
);
145+
} else {
146+
config_string.push_str(
147+
format!(
148+
"\n status 401 users=\"{}\" brute_protection=#{}",
149+
users.join(","),
150+
http_serve_args.disable_brute_protection
151+
)
152+
.as_str(),
153+
);
154+
}
155+
}
127156
match http_serve_args.log {
128157
LogOutput::Stdout => {
129158
config_string.push_str("\n log \"/dev/stdout\"");
@@ -142,9 +171,14 @@ fn before_starting_server(
142171
}
143172
LogOutput::Off => {}
144173
}
145-
config_string
146-
.push_str(format!("\n root \"{}\"", http_serve_args.root.to_string_lossy().into_owned()).as_str());
147-
config_string.push_str("\n directory_listing #true\n}\n");
174+
if http_serve_args.forward_proxy {
175+
config_string.push_str("\n forward_proxy");
176+
} else {
177+
config_string
178+
.push_str(format!("\n root \"{}\"", http_serve_args.root.to_string_lossy().into_owned()).as_str());
179+
config_string.push_str("\n directory_listing #true");
180+
}
181+
config_string.push_str("\n}\n");
148182
temp_config_file = NamedTempFile::new()?;
149183
std::fs::write(temp_config_file.path(), config_string)?;
150184
temp_config_file.path()
@@ -894,6 +928,21 @@ struct ServeArgs {
894928
#[arg(short, long, default_value = ".")]
895929
root: PathBuf,
896930

931+
/// Basic authentication credentials for authorized users. The credential value must
932+
/// be in the form "${user}:${hashed_password}" where the "${hashed_password}" is from
933+
/// the ferron-passwd program or from any program using the password-auth generate_hash()
934+
/// macro (see https://docs.rs/password-auth/latest/password_auth/fn.generate_hash.html).
935+
#[arg(short, long)]
936+
credential: Vec<String>,
937+
938+
/// Whether to disable brute-force password protection.
939+
#[arg(long)]
940+
disable_brute_protection: bool,
941+
942+
/// Whether to start the server as a forward proxy.
943+
#[arg(long)]
944+
forward_proxy: bool,
945+
897946
/// Where to output logs.
898947
#[arg(long, default_value = "stdout")]
899948
log: LogOutput,
@@ -970,6 +1019,10 @@ fn main() {
9701019
mod tests {
9711020
use super::*;
9721021

1022+
// The hash here is for the password '123?45>6'.
1023+
const COMMON_TEST_PASSWORD: &str =
1024+
"$argon2id$v=19$m=19456,t=2,p=1$emTillHaS3OqFuvITdXxzg$G00heP8QSXk5H/ruTiLt302Xk3uETfU5QO8hBIwUq08";
1025+
9731026
#[test]
9741027
fn test_supported_args() {
9751028
let args = FerronArgs::parse_from(vec![
@@ -1045,6 +1098,9 @@ mod tests {
10451098
assert_eq!(String::from("127.0.0.1"), http_serve_args.listen_ip);
10461099
assert_eq!(3000, http_serve_args.port);
10471100
assert_eq!(PathBuf::from("."), http_serve_args.root);
1101+
assert_eq!(Vec::<String>::new(), http_serve_args.credential);
1102+
assert!(!http_serve_args.disable_brute_protection);
1103+
assert!(!http_serve_args.forward_proxy);
10481104
assert_eq!(LogOutput::Stdout, http_serve_args.log);
10491105
assert_eq!(LogOutput::Stderr, http_serve_args.error_log);
10501106
}
@@ -1062,6 +1118,12 @@ mod tests {
10621118
"8080",
10631119
"--root",
10641120
"./wwwroot",
1121+
"--credential",
1122+
format!("test:{COMMON_TEST_PASSWORD}").as_str(),
1123+
"--credential",
1124+
format!("test2:{COMMON_TEST_PASSWORD}").as_str(),
1125+
"--disable-brute-protection",
1126+
"--forward-proxy",
10651127
"--log",
10661128
"off",
10671129
"--error-log",
@@ -1078,6 +1140,15 @@ mod tests {
10781140
assert_eq!(String::from("0.0.0.0"), http_serve_args.listen_ip);
10791141
assert_eq!(8080, http_serve_args.port);
10801142
assert_eq!(PathBuf::from("./wwwroot"), http_serve_args.root);
1143+
assert_eq!(
1144+
vec![
1145+
format!("test:{COMMON_TEST_PASSWORD}"),
1146+
format!("test2:{COMMON_TEST_PASSWORD}")
1147+
],
1148+
http_serve_args.credential
1149+
);
1150+
assert!(http_serve_args.disable_brute_protection);
1151+
assert!(http_serve_args.forward_proxy);
10811152
assert_eq!(LogOutput::Off, http_serve_args.log);
10821153
assert_eq!(LogOutput::Off, http_serve_args.error_log);
10831154
}

0 commit comments

Comments
 (0)