1use std::net::SocketAddr;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use crate::fs::PinnedRoot;
8use crate::limits::Limits;
9use crate::policy::{DirectoryListingPolicy, DotfilePolicy, StaticPolicy, SymlinkPolicy};
10use crate::primitives::canonical::is_hop_by_hop_header;
11use crate::primitives::header_block::{HeaderName, HeaderValue};
12
13#[derive(Debug, Clone)]
14#[must_use]
15pub struct ServeConfig {
16 pub bind: SocketAddr,
17 pub root: PathBuf,
18 pub limits: Limits,
19 pub static_policy: StaticPolicy,
20 pub default_content_type: String,
21 pub extra_response_headers: Vec<(String, String)>,
22}
23
24impl Default for ServeConfig {
25 fn default() -> Self {
26 Self {
27 bind: "127.0.0.1:8000".parse().unwrap(),
28 root: PathBuf::from("."),
29 limits: Limits::default(),
30 static_policy: StaticPolicy::safe_default(),
31 default_content_type: "application/octet-stream".to_string(),
32 extra_response_headers: Vec::new(),
33 }
34 }
35}
36
37pub fn validate_static_metadata(
39 default_content_type: &str,
40 extra_response_headers: &[(String, String)],
41) -> Result<(), String> {
42 if default_content_type.is_empty()
43 || default_content_type
44 .bytes()
45 .any(|b| b == b'\r' || b == b'\n' || b == 0)
46 {
47 return Err("default content type must be a non-empty value without CR/LF/NUL".into());
48 }
49 for (name, value) in extra_response_headers {
50 HeaderName::new(name.clone()).map_err(|e| format!("invalid extra response header: {e}"))?;
51 HeaderValue::new(value.clone())
52 .map_err(|e| format!("invalid extra response header: {e}"))?;
53 let lower = name.to_ascii_lowercase();
54 if is_hop_by_hop_header(&lower)
55 || matches!(
56 lower.as_str(),
57 "content-length"
58 | "date"
59 | "server"
60 | "content-type"
61 | "content-range"
62 | "accept-ranges"
63 | "etag"
64 | "last-modified"
65 | "x-content-type-options"
66 )
67 {
68 return Err(format!(
69 "extra response header is runtime- or representation-owned: {name}"
70 ));
71 }
72 }
73 Ok(())
74}
75
76#[derive(Debug, Clone, Copy)]
77#[must_use]
78pub struct StartupSummary {
79 pub bind_is_unspecified: bool,
80 pub directory_listing_enabled: bool,
81 pub symlinks_followed: bool,
82 pub dotfiles_served: bool,
83 pub max_connections: usize,
84 pub max_file_streams: usize,
85}
86
87impl ServeConfig {
88 pub fn startup_summary(&self) -> StartupSummary {
93 StartupSummary {
94 bind_is_unspecified: self.bind.ip().is_unspecified(),
95 directory_listing_enabled: matches!(
96 self.static_policy.directory_listing,
97 DirectoryListingPolicy::Enabled
98 ),
99 symlinks_followed: matches!(self.static_policy.symlinks, SymlinkPolicy::Follow),
100 dotfiles_served: matches!(self.static_policy.dotfiles, DotfilePolicy::Serve),
101 max_connections: self.limits.max_connections,
102 max_file_streams: self.limits.max_file_streams,
103 }
104 }
105}
106
107#[derive(Clone)]
108pub struct ServeState {
109 pub(crate) config: Arc<ServeConfig>,
110 pub(crate) pinned_root: Arc<PinnedRoot>,
111}
112
113impl ServeState {
114 pub fn new(config: Arc<ServeConfig>) -> Result<Self, std::io::Error> {
115 validate_static_metadata(&config.default_content_type, &config.extra_response_headers)
116 .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
117 let pinned_root = Arc::new(PinnedRoot::new(&config.root)?);
118 Ok(Self {
119 config,
120 pinned_root,
121 })
122 }
123
124 pub fn config(&self) -> &Arc<ServeConfig> {
125 &self.config
126 }
127
128 pub(crate) fn pinned_root(&self) -> &Arc<PinnedRoot> {
129 &self.pinned_root
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn default_config_binds_loopback() {
139 let config = ServeConfig::default();
140 assert!(config.bind.ip().is_loopback());
141 }
142
143 #[test]
144 fn default_config_binds_port_8000() {
145 let config = ServeConfig::default();
146 assert_eq!(config.bind.port(), 8000);
147 }
148
149 #[test]
150 fn default_startup_summary_is_safe() {
151 let summary = ServeConfig::default().startup_summary();
152 assert!(!summary.bind_is_unspecified);
153 assert!(!summary.directory_listing_enabled);
154 assert!(!summary.symlinks_followed);
155 assert!(!summary.dotfiles_served);
156 assert_eq!(summary.max_connections, 64);
157 assert_eq!(summary.max_file_streams, 32);
158 }
159}