1use std::future::Future;
4use std::net::SocketAddr;
5use std::path::PathBuf;
6use std::sync::Arc;
7use std::time::Duration;
8
9use percent_encoding::percent_decode_str;
10use tokio::io::{AsyncReadExt, AsyncWriteExt};
11use tokio::net::{TcpListener, TcpStream};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Request {
16 pub path: String,
17 pub peer: SocketAddr,
18}
19
20pub trait Handler: Send + Sync + 'static {
23 fn handle(&self, request: Request) -> impl Future<Output = Vec<u8>> + Send;
24}
25
26impl<F, Fut> Handler for F
27where
28 F: Fn(Request) -> Fut + Send + Sync + 'static,
29 Fut: Future<Output = Vec<u8>> + Send,
30{
31 fn handle(&self, request: Request) -> impl Future<Output = Vec<u8>> + Send {
32 self(request)
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct ServerConfig {
39 pub max_request_line: usize,
42 pub timeout: Duration,
44}
45
46impl Default for ServerConfig {
47 fn default() -> Self {
48 Self {
49 max_request_line: 2048,
50 timeout: Duration::from_secs(30),
51 }
52 }
53}
54
55pub async fn serve(
58 listener: TcpListener,
59 handler: impl Handler,
60 config: ServerConfig,
61 shutdown: impl Future<Output = ()>,
62) -> std::io::Result<()> {
63 let handler = Arc::new(handler);
64 let config = Arc::new(config);
65 tokio::pin!(shutdown);
66 loop {
67 tokio::select! {
68 _ = &mut shutdown => break,
69 accepted = listener.accept() => match accepted {
70 Ok((stream, peer)) => {
71 let handler = handler.clone();
72 let config = config.clone();
73 tokio::spawn(async move {
74 if let Err(error) = handle_connection(stream, peer, handler, &config).await {
75 log::debug!("nex: connection from {peer} failed: {error}");
76 }
77 });
78 }
79 Err(error) => log::warn!("nex: accept failed: {error}"),
80 },
81 }
82 }
83 Ok(())
84}
85
86async fn handle_connection(
87 mut stream: TcpStream,
88 peer: SocketAddr,
89 handler: Arc<impl Handler>,
90 config: &ServerConfig,
91) -> std::io::Result<()> {
92 let mut line = Vec::with_capacity(64);
95 let mut byte = [0u8; 1];
96 loop {
97 let count = tokio::time::timeout(config.timeout, stream.read(&mut byte))
98 .await
99 .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "selector read"))??;
100 if count == 0 {
101 break;
102 }
103 if byte[0] == b'\n' {
104 break;
105 }
106 line.push(byte[0]);
107 if line.len() >= config.max_request_line {
108 return stream.shutdown().await;
110 }
111 }
112 let path = String::from_utf8_lossy(&line)
113 .trim_end_matches('\r')
114 .to_string();
115
116 let body = handler.handle(Request { path, peer }).await;
117 tokio::time::timeout(config.timeout, stream.write_all(&body))
118 .await
119 .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "response write"))??;
120 stream.shutdown().await
121}
122
123#[derive(Debug, Clone)]
127pub struct FileHandler {
128 root: PathBuf,
129}
130
131impl FileHandler {
132 pub fn new(root: impl Into<PathBuf>) -> Self {
133 Self { root: root.into() }
134 }
135
136 fn resolve(&self, request_path: &str) -> Option<PathBuf> {
137 let decoded = percent_decode_str(request_path).decode_utf8().ok()?;
138 let mut resolved = self.root.clone();
139 for segment in decoded.split('/') {
140 match segment {
141 "" | "." => continue,
142 ".." => return None,
143 segment if segment.contains(['\\', ':']) => return None,
144 segment => resolved.push(segment),
145 }
146 }
147 Some(resolved)
148 }
149
150 async fn listing_for(&self, dir: &PathBuf, request_path: &str) -> Vec<u8> {
151 let mut names = Vec::new();
152 if let Ok(mut entries) = tokio::fs::read_dir(dir).await {
153 while let Ok(Some(entry)) = entries.next_entry().await {
154 let mut name = entry.file_name().to_string_lossy().into_owned();
155 if entry.file_type().await.map(|kind| kind.is_dir()).unwrap_or(false) {
156 name.push('/');
157 }
158 names.push(name);
159 }
160 }
161 names.sort();
162 let mut out = format!("{request_path}\n\n");
163 for name in names {
164 out.push_str("=> ");
165 out.push_str(&name);
166 out.push('\n');
167 }
168 out.into_bytes()
169 }
170}
171
172impl Handler for FileHandler {
173 async fn handle(&self, request: Request) -> Vec<u8> {
174 let Some(path) = self.resolve(&request.path) else {
175 return b"bad path\n".to_vec();
176 };
177 if crate::is_directory_path(&request.path) || path.is_dir() {
178 let index = path.join("index.nex");
179 if let Ok(body) = tokio::fs::read(&index).await {
180 return body;
181 }
182 return self
183 .listing_for(&path, if request.path.is_empty() { "/" } else { &request.path })
184 .await;
185 }
186 match tokio::fs::read(&path).await {
187 Ok(body) => body,
188 Err(_) => format!("{} not found\n", request.path).into_bytes(),
189 }
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::client::{FetchOptions, fetch};
197 use crate::{ListingLine, parse_listing};
198
199 async fn spawn(handler: impl Handler) -> (SocketAddr, tokio::sync::oneshot::Sender<()>) {
200 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
201 let addr = listener.local_addr().unwrap();
202 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
203 tokio::spawn(async move {
204 let _ = serve(listener, handler, ServerConfig::default(), async {
205 let _ = rx.await;
206 })
207 .await;
208 });
209 (addr, tx)
210 }
211
212 fn options_for(addr: SocketAddr) -> FetchOptions {
213 FetchOptions {
214 connect_addr: Some(addr),
215 ..Default::default()
216 }
217 }
218
219 #[tokio::test]
220 async fn round_trip_echoes_the_selector() {
221 let (addr, _stop) = spawn(|request: Request| async move {
222 format!("you asked for {:?}\n", request.path).into_bytes()
223 })
224 .await;
225 let body = fetch("nex://example.test/hello.txt", &options_for(addr))
226 .await
227 .unwrap();
228 assert_eq!(body, b"you asked for \"/hello.txt\"\n");
229 }
230
231 #[tokio::test]
232 async fn file_handler_serves_files_listings_and_refuses_traversal() {
233 let dir = tempfile::TempDir::new().unwrap();
234 std::fs::write(dir.path().join("about.txt"), "hi\n").unwrap();
235 std::fs::create_dir(dir.path().join("nexlog")).unwrap();
236 std::fs::write(dir.path().join("nexlog").join("one.txt"), "post\n").unwrap();
237
238 let (addr, _stop) = spawn(FileHandler::new(dir.path())).await;
239
240 let file = fetch("nex://example.test/about.txt", &options_for(addr))
241 .await
242 .unwrap();
243 assert_eq!(file, b"hi\n");
244
245 let listing = fetch("nex://example.test/", &options_for(addr))
246 .await
247 .unwrap();
248 let lines = parse_listing(&String::from_utf8(listing).unwrap());
249 assert!(lines.contains(&ListingLine::Link {
250 url: "about.txt".to_string(),
251 label: None
252 }));
253 assert!(lines.contains(&ListingLine::Link {
254 url: "nexlog/".to_string(),
255 label: None
256 }));
257
258 let escape = crate::fetch_path("example.test", 1900, "/../secret", &options_for(addr))
261 .await
262 .unwrap();
263 assert_eq!(escape, b"bad path\n");
264 }
265
266 #[tokio::test]
267 async fn index_nex_takes_precedence_over_generated_listings() {
268 let dir = tempfile::TempDir::new().unwrap();
269 std::fs::write(dir.path().join("index.nex"), "welcome to my site\n").unwrap();
270 let (addr, _stop) = spawn(FileHandler::new(dir.path())).await;
271 let body = fetch("nex://example.test/", &options_for(addr))
272 .await
273 .unwrap();
274 assert_eq!(body, b"welcome to my site\n");
275 }
276}