Skip to content

Commit 8eec596

Browse files
server: Add TLS support
1 parent 57ec91e commit 8eec596

File tree

3 files changed

+176
-16
lines changed

3 files changed

+176
-16
lines changed

crates/server/Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ jsonrs = { package = "jsonrs", version = ">=0.1.4", git = "https://github.com/sa
1717
base64 = { package = "rb64", version = ">=0.1.0", git = "https://github.com/saulvaldelvira/rb64" }
1818
url = { package = "url-utils", version = ">=0.1.0", path = "../url" }
1919

20+
[dependencies.rustls]
21+
version = ">=0.23.28"
22+
optional = true
23+
default-features = false
24+
features = ["std", "aws_lc_rs"]
25+
2026
[features]
2127
default = ["full"]
2228
regex = ["dep:regexpr"]
23-
full = ["regex"]
29+
tls = ["dep:rustls", "http/tls"]
30+
full = ["regex", "tls"]

crates/server/src/config.rs

Lines changed: 139 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
#![allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
22

3+
use core::fmt;
34
use std::{
45
env, fs,
56
path::{Path, PathBuf},
67
process,
78
str::FromStr,
9+
sync::Arc,
810
time::Duration,
911
};
1012

@@ -17,13 +19,32 @@ use crate::{
1719
log_info, log_warn,
1820
};
1921

20-
#[derive(Clone, Debug)]
22+
#[derive(Clone)]
2123
pub struct ServerConfig {
2224
pub port: u16,
2325
pub pool_conf: PoolConfig,
2426
pub keep_alive_timeout: Duration,
2527
pub keep_alive_requests: u16,
2628
pub log_file: Option<String>,
29+
30+
#[cfg(feature = "tls")]
31+
pub tls_config: Option<Arc<rustls::ServerConfig>>,
32+
}
33+
34+
impl fmt::Debug for ServerConfig {
35+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36+
let mut deb = f.debug_struct("ServerConfig");
37+
deb.field("port", &self.port)
38+
.field("pool_conf", &self.pool_conf)
39+
.field("keep_alive_timeout", &self.keep_alive_timeout)
40+
.field("keep_alive_requests", &self.keep_alive_requests)
41+
.field("log_file", &self.log_file);
42+
43+
#[cfg(feature = "tls")]
44+
deb.field("tls", &self.tls_config.is_some());
45+
46+
deb.finish()
47+
}
2748
}
2849

2950
#[cfg(not(test))]
@@ -51,6 +72,31 @@ fn get_default_conf_file() -> Option<PathBuf> {
5172
None
5273
}
5374

75+
#[cfg(feature = "tls")]
76+
#[allow(clippy::unwrap_used)]
77+
fn get_tls_config(cert: Option<String>, pkey: Option<String>) -> Result<Arc<rustls::ServerConfig>> {
78+
use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
79+
80+
let Some(cert) = cert else {
81+
return Err("Missing certificate file".into());
82+
};
83+
let Some(pkey) = pkey else {
84+
return Err("Missing private key file".into());
85+
};
86+
87+
let certs = CertificateDer::pem_file_iter(cert)
88+
.unwrap()
89+
.map(|cert| cert.unwrap())
90+
.collect();
91+
let private_key = PrivateKeyDer::from_pem_file(pkey).unwrap();
92+
let config = rustls::ServerConfig::builder()
93+
.with_no_client_auth()
94+
.with_single_cert(certs, private_key)
95+
.map_err(|err| format!("rustls: {err}"))?;
96+
97+
Ok(Arc::new(config))
98+
}
99+
54100
/// [`crate::HttpServer`] configuration
55101
///
56102
/// # Example
@@ -98,6 +144,15 @@ impl ServerConfig {
98144
conf.parse_conf_file(&cfile)?;
99145
}
100146

147+
let mut pool_conf_builder = PoolConfig::builder();
148+
149+
#[cfg(feature = "tls")]
150+
let mut tls = false;
151+
#[cfg(feature = "tls")]
152+
let mut cert: Option<String> = None;
153+
#[cfg(feature = "tls")]
154+
let mut privkey: Option<String> = None;
155+
101156
let mut args = args.iter();
102157
while let Some(arg) = args.next() {
103158
macro_rules! parse_next {
@@ -112,8 +167,6 @@ impl ServerConfig {
112167
}};
113168
}
114169

115-
let mut pool_conf_builder = PoolConfig::builder();
116-
117170
match arg.as_ref() {
118171
"-p" | "--port" => conf.port = parse_next!(),
119172
"-n" | "-n-workers" => {
@@ -134,19 +187,34 @@ impl ServerConfig {
134187
let n: u8 = parse_next!();
135188
log::set_level(n.try_into()?);
136189
}
190+
#[cfg(feature = "tls")]
191+
"--tls" => tls = true,
192+
193+
#[cfg(feature = "tls")]
194+
"--cert-file" => cert = Some(parse_next!()),
195+
196+
#[cfg(feature = "tls")]
197+
"--private-key" => privkey = Some(parse_next!()),
198+
137199
CONFIG_FILE_ARG => {
138200
let _ = args.next();
139201
}
140202
"-h" | "--help" => help(),
141203
unknown => return Err(format!("Unknow argument: {unknown}").into()),
142204
}
205+
}
206+
207+
conf.pool_conf = pool_conf_builder.build();
143208

144-
conf.pool_conf = pool_conf_builder.build();
209+
#[cfg(feature = "tls")]
210+
if conf.tls_config.is_none() && tls {
211+
conf.tls_config = Some(get_tls_config(cert, privkey)?);
145212
}
146213

147214
log_info!("{conf:#?}");
148215
Ok(conf)
149216
}
217+
#[allow(clippy::too_many_lines)]
150218
fn parse_conf_file(&mut self, conf_file: &Path) -> crate::Result<()> {
151219
if !conf_file.exists() {
152220
return Ok(());
@@ -164,6 +232,16 @@ impl ServerConfig {
164232
let Json::Object(obj) = json else {
165233
return Err("Expected json object".into());
166234
};
235+
236+
#[cfg(feature = "tls")]
237+
let mut tls = false;
238+
239+
#[cfg(feature = "tls")]
240+
let mut cert: Option<String> = None;
241+
242+
#[cfg(feature = "tls")]
243+
let mut privkey: Option<String> = None;
244+
167245
for (k, v) in obj {
168246
macro_rules! num {
169247
() => {
@@ -179,14 +257,24 @@ impl ServerConfig {
179257
_n as $t
180258
}};
181259
}
260+
macro_rules! bool {
261+
($v:ident) => {
262+
$v.boolean().ok_or_else(|| {
263+
format!("Parsing config file ({conf_str}): Expected boolean for \"{k}\"")
264+
})?
265+
};
266+
}
182267
macro_rules! string {
183-
() => {
184-
v.string()
268+
($v:ident) => {
269+
$v.string()
185270
.ok_or_else(|| {
186271
format!("Parsing config file ({conf_str}): Expected string for \"{k}\"")
187272
})?
188273
.to_string()
189274
};
275+
() => {
276+
string!(v)
277+
};
190278
}
191279
macro_rules! obj {
192280
() => {
@@ -196,15 +284,25 @@ impl ServerConfig {
196284
};
197285
}
198286

199-
match &*k {
200-
"port" => self.port = num!() as u16,
201-
"root_dir" => {
202-
let path: String = string!();
203-
let path = path.replacen(
287+
macro_rules! path {
288+
($v:ident) => {{
289+
let path: String = string!($v);
290+
path.replacen(
204291
'~',
205292
env::var("HOME").as_ref().map(String::as_str).unwrap_or("~"),
206293
1,
207-
);
294+
)
295+
}};
296+
297+
() => {
298+
path!(v)
299+
};
300+
}
301+
302+
match &*k {
303+
"port" => self.port = num!() as u16,
304+
"root_dir" => {
305+
let path = path!();
208306
env::set_current_dir(Path::new(&path))?;
209307
}
210308
"keep_alive_timeout" => self.keep_alive_timeout = Duration::from_secs_f64(num!()),
@@ -214,6 +312,19 @@ impl ServerConfig {
214312
let n = num!(v as u8);
215313
log::set_level(n.try_into()?);
216314
}
315+
#[cfg(feature = "tls")]
316+
"tls" => {
317+
for (k, v) in obj!() {
318+
match &**k {
319+
"enabled" => tls = bool!(v),
320+
"cert_file" => cert = Some(path!(v)),
321+
"private_key" => privkey = Some(path!(v)),
322+
_ => log_warn!(
323+
"Parsing config file ({conf_str}): Unexpected key: \"{k}\""
324+
),
325+
}
326+
}
327+
}
217328
"pool_config" => {
218329
for (k, v) in obj!() {
219330
match &**k {
@@ -231,6 +342,12 @@ impl ServerConfig {
231342
_ => log_warn!("Parsing config file ({conf_str}): Unexpected key: \"{k}\""),
232343
}
233344
}
345+
346+
#[cfg(feature = "tls")]
347+
if tls {
348+
self.tls_config = Some(get_tls_config(cert, privkey)?);
349+
}
350+
234351
Ok(())
235352
}
236353
#[inline]
@@ -260,7 +377,8 @@ impl ServerConfig {
260377
}
261378

262379
fn help() -> ! {
263-
println!(
380+
/* FIXME: Don't output tls options if the tls feature is disabled */
381+
println!(concat!(
264382
"\
265383
http-srv: Copyright (C) 2025 Saúl Valdelvira
266384
@@ -281,11 +399,15 @@ PARAMETERS:
281399
--log-level <n> Set log level
282400
--conf <file> Use the given config file instead of the default one
283401
--license Output the license of this program
402+
403+
--tls Enable TLS
404+
--cert-file Certificate file for TLS
405+
--private-key Private key for TLS
284406
EXAMPLES:
285407
http-srv -p 8080 -d /var/html
286408
http-srv -d ~/desktop -n 1024 --keep-alive 120
287409
http-srv --log /var/log/http-srv.log"
288-
);
410+
));
289411
process::exit(0);
290412
}
291413

@@ -322,6 +444,9 @@ impl Default for ServerConfig {
322444
keep_alive_timeout: Duration::from_secs(0),
323445
keep_alive_requests: 10000,
324446
log_file: None,
447+
448+
#[cfg(feature = "tls")]
449+
tls_config: None,
325450
}
326451
}
327452
}

crates/server/src/lib.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub type Result<T> = std::result::Result<T, HttpError>;
5656
use std::{
5757
io::{self, BufRead, BufReader},
5858
net::{TcpListener, TcpStream},
59+
sync::Arc,
5960
thread,
6061
time::{Duration, Instant},
6162
};
@@ -115,9 +116,25 @@ fn handle_connection(
115116
handlers: &Handler,
116117
keep_alive_timeout: Duration,
117118
keep_alive_requests: u16,
119+
120+
#[cfg(feature = "tls")] tls_config: Option<&Arc<rustls::ServerConfig>>,
118121
) -> Result<()> {
122+
#[cfg(feature = "tls")]
123+
let mut req = match tls_config {
124+
Some(config) => {
125+
let conn = rustls::ServerConnection::new(Arc::clone(config))
126+
.map_err(|err| format!("TLS error: {err}"))?;
127+
let tls_stream = rustls::StreamOwned::new(conn, stream);
128+
HttpRequest::parse(tls_stream)?
129+
}
130+
None => HttpRequest::parse(stream)?,
131+
};
132+
133+
#[cfg(not(feature = "tls"))]
119134
let mut req = HttpRequest::parse(stream)?;
135+
120136
handlers.handle(&mut req)?;
137+
121138
let connection = req.header("Connection");
122139
let keep_alive = keep_alive_timeout.as_millis() > 0;
123140
if connection.is_some_and(|conn| conn == "keep-alive") && keep_alive {
@@ -188,12 +205,23 @@ impl HttpServer {
188205
let timeout = config.keep_alive_timeout;
189206
let req = config.keep_alive_requests;
190207

208+
#[cfg(feature = "tls")]
209+
let tls_config = config.tls_config.as_ref();
210+
191211
println!("Sever listening on port {}", config.port);
192212

193213
pool.scope(|scope| {
194214
for stream in listener.incoming().flatten() {
195215
scope.execute(|| {
196-
handle_connection(stream, &handler, timeout, req).unwrap_or_else(|err| {
216+
handle_connection(
217+
stream,
218+
&handler,
219+
timeout,
220+
req,
221+
#[cfg(feature = "tls")]
222+
tls_config,
223+
)
224+
.unwrap_or_else(|err| {
197225
log_error!("{err}");
198226
});
199227
});

0 commit comments

Comments
 (0)