1mod mime;
10
11use std::{
12 convert::Infallible,
13 env::current_dir,
14 fmt::{Display, Formatter},
15 fs::File,
16 io::{BufRead, BufReader, Cursor, Error, ErrorKind, Read, Seek, SeekFrom, Write},
17 net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream},
18 path::{Component, Path, PathBuf},
19 time::UNIX_EPOCH,
20};
21use mime::path_to_mime_type;
22
23fn relative_path_components(path: &Path) -> impl Iterator<Item = impl AsRef<Path> + '_> {
24 path.components().filter_map(|comp| if let Component::Normal(r) = comp {
25 Some(r)
26 } else {
27 None
28 })
29}
30
31type Result<T = (), E = std::io::Error> = std::result::Result<T, E>;
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Request {
36 pub sent: Response,
38 etag: Option<u64>,
40}
41
42impl Request {
43 pub fn client_cache_reused(&self) -> bool {
49 self.etag.is_some()
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum Response {
58 Path(PathBuf),
60 Data(Vec<u8>),
65}
66
67enum ResponseReader<'response> {
69 Path(File),
70 Data(Cursor<&'response [u8]>),
71}
72
73macro_rules! fwd {
74 { $(fn $name:ident (&mut $self:ident $(,)? $($arg:ident: $argty:ty),*) -> $ret:ty;)+ } => {
75 $(
76 fn $name(&mut $self, $($arg: $argty),*) -> $ret {
77 match $self {
78 ResponseReader::Path(file) => file.$name($($arg),*),
79 ResponseReader::Data(file) => file.$name($($arg),*),
80 }
81 }
82 )+
83 }
84}
85
86impl Read for ResponseReader<'_> {
87 fwd! {
88 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
89 fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()>;
90 fn read_to_end(&mut self, buf: &mut Vec<u8>) -> std::io::Result<usize>;
91 fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize>;
92 }
93}
94
95impl Seek for ResponseReader<'_> {
96 fwd! {
97 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64>;
98 fn rewind(&mut self) -> std::io::Result<()>;
99 fn seek_relative(&mut self, offset: i64) -> std::io::Result<()>;
100 fn stream_position(&mut self) -> std::io::Result<u64>;
101 }
102}
103
104impl From<PathBuf> for Response {
105 fn from(value: PathBuf) -> Self {
106 Self::Path(value)
107 }
108}
109
110impl Response {
111 pub fn display(&self) -> impl Display + '_ {
118 struct ResponseDisplay<'path>(Option<std::path::Display<'path>>);
119
120 impl Display for ResponseDisplay<'_> {
121 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
122 match &self.0 {
123 Some(display) => write!(f, "{display}"),
124 None => f.write_str("<in-memory>"),
125 }
126 }
127 }
128
129 ResponseDisplay(match self {
130 Response::Path(path) => Some(path.display()),
131 Response::Data(_) => None,
132 })
133 }
134
135 fn etag(&self) -> Result<Option<u64>> {
136 Ok(match self {
137 Self::Path(path) => path.metadata()?.modified()?
138 .duration_since(UNIX_EPOCH)
139 .map_err(|_| Error::other(format!("mtime of {path:?} is before the Unix epoch")))?
140 .as_secs()
141 .into(),
142 Self::Data(_) => None,
143 })
144 }
145
146 fn to_reader(&self) -> Result<ResponseReader<'_>> {
147 Ok(match self {
148 Self::Path(path) => ResponseReader::Path(File::open(path)?),
149 Self::Data(vec) => ResponseReader::Data(Cursor::new(vec)),
150 })
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum RequestResult {
158 Ok(Request),
160 InvalidHttpMethod,
164 NoRequestedPath,
166 InvalidHttpVersion,
170 InvalidHeader,
175 FileNotFound(Box<str>),
179}
180
181#[derive(Debug)]
184pub enum ServerError<E> {
185 Io(std::io::Error),
186 Callback(E),
187}
188
189impl<E: Display> Display for ServerError<E> {
190 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
191 match self {
192 ServerError::Io(err) => write!(f, "IO error: {err}"),
193 ServerError::Callback(err) => Display::fmt(err, f),
194 }
195 }
196}
197
198impl<E> From<std::io::Error> for ServerError<E> {
199 fn from(value: std::io::Error) -> Self {
200 Self::Io(value)
201 }
202}
203
204impl<E: std::error::Error> std::error::Error for ServerError<E> {}
205
206pub struct Server {
208 root: PathBuf,
209 listener: TcpListener,
210 line_buf: String,
211 misc_buf: String,
212}
213
214impl Server {
215 pub const DEFAULT_PORT: u16 = 6969;
218
219 pub fn current_dir() -> Result<Self> {
224 Self::_new(current_dir()?, Self::DEFAULT_PORT)
225 }
226
227 pub fn new(root: impl AsRef<Path>) -> Result<Self> {
232 Self::_new(root.as_ref().canonicalize()?, Self::DEFAULT_PORT)
233 }
234
235 pub fn current_dir_at(port: u16) -> Result<Self> {
240 Self::_new(current_dir()?, port)
241 }
242
243 pub fn new_at(root: impl AsRef<Path>, port: u16) -> Result<Self> {
248 Self::_new(root.as_ref().canonicalize()?, port)
249 }
250
251 fn _new(root: PathBuf, port: u16) -> Result<Self> {
252 Ok(Self {
253 root,
254 listener: TcpListener::bind((Ipv4Addr::LOCALHOST, port))?,
255 line_buf: String::new(),
256 misc_buf: String::new(),
257 })
258 }
259
260 fn read_http_line(reader: &mut impl BufRead, dst: &mut String) -> Result<()> {
261 dst.clear();
262 reader.read_line(dst)?;
263 if dst.pop() == Some('\n') && dst.ends_with('\r') {
264 dst.pop();
265 }
266 Ok(())
267 }
268
269 fn send_file(
272 &self,
273 mut dst: impl Write,
274 data: &Response,
275 content_type: &'static str,
276 etag_to_match: Option<u64>,
277 ) -> Result<bool> {
278 let etag = data.etag()?;
279 if let Some(etag) = etag.filter(|&x| Some(x) == etag_to_match) {
280 write!(dst, "HTTP/1.1 304 Not Modified\r\n\
281 Connection: close\r\n\
282 ETag: \"{etag:x}\"\r\n\
283 Cache-Control: public; must-revalidate\r\n\
284 \r\n")?;
285 return Ok(false)
286 }
287 let mut file = data.to_reader()?;
288 let content_length = file.seek(SeekFrom::End(0))?;
289 file.rewind()?;
290 write!(dst, "HTTP/1.1 200 OK\r\n\
291 Connection: close\r\n\
292 Content-Type: {content_type}\r\n\
293 Content-Length: {content_length}\r\n\
294 Cache-Control: public; must-revalidate\r\n")?;
295 if let Some(etag) = etag {
296 write!(dst, "ETag: \"{etag:x}\"\r\n")?;
297 }
298 write!(dst, "\r\n")?;
299 std::io::copy(&mut file, &mut dst)?;
300 Ok(true)
301 }
302
303 fn send_400(mut dst: impl Write) -> Result {
304 write!(dst, "HTTP/1.1 400 Bad Request\r\n\
305 Connection: close\r\n\
306 \r\n")
307 }
308
309 fn send_404(mut dst: impl Write) -> Result {
310 write!(dst, "HTTP/1.1 404 Not Found\r\n\
311 Connection: close\r\n\
312 Content-Type: text/html\r\n\
313 Content-Length: 18\r\n\
314 \r\n\
315 <h1>Not Found</h1>")
316 }
317
318 fn respond<E>(
320 &mut self,
321 conn: &mut BufReader<TcpStream>,
322 ) -> Result<RequestResult, ServerError<E>> {
323 Self::read_http_line(conn, &mut self.line_buf)?;
324 let mut etag = None;
325 loop {
326 Self::read_http_line(conn, &mut self.misc_buf)?;
327 if let Some(etag_raw) = self.misc_buf.strip_prefix("If-None-Match: ") {
328 etag = match u64::from_str_radix(etag_raw.trim_matches('"'), 16) {
329 Ok(x) => Some(x),
330 Err(_) => return Ok(RequestResult::InvalidHeader),
331 }
332 } else if self.misc_buf.is_empty() {
333 break;
334 }
335 }
336
337 let Some(path_and_version) = self.line_buf.strip_prefix("GET ") else {
338 return Ok(RequestResult::InvalidHttpMethod)
339 };
340 let Some((path, http_version)) = path_and_version.split_once(' ') else {
341 return Ok(RequestResult::NoRequestedPath)
342 };
343 if http_version != "HTTP/1.1" {
344 return Ok(RequestResult::InvalidHttpVersion)
345 }
346 if path.contains("..") {
347 return Ok(RequestResult::FileNotFound(Box::from(path)))
348 }
349
350
351 let path = match path.split_once('?').map_or(path, |(path, _query)| path) {
352 "/" => "/index.html",
353 path => path,
354 };
355 let mut n_comps = 0usize;
356 self.root.extend(relative_path_components(path.as_ref()).inspect(|_| n_comps += 1));
357 if self.root.extension().is_none() {
358 self.root.set_extension("html");
359 }
360 let actual_path = self.root.canonicalize();
361 for _ in 0 .. n_comps {
362 self.root.pop();
363 }
364 let Ok(actual_path) = actual_path else {
365 return Ok(RequestResult::FileNotFound(Box::from(path)))
366 };
367
368 Ok(RequestResult::Ok(Request { sent: Response::Path(actual_path), etag }))
369 }
370
371 fn handle_conn<E>(
372 &mut self,
373 conn: TcpStream,
374 addr: &SocketAddr,
375 mut on_pending_request: impl FnMut(&SocketAddr, PathBuf) -> Result<Response, E>,
376 mut on_request: impl FnMut(&SocketAddr, RequestResult) -> Result<(), E>,
377 ) -> Result<(), ServerError<E>> {
378 let mut conn = BufReader::new(conn);
379
380 while match conn.get_ref().peek(&mut [0; 4]) {
381 Ok(n) => n > 0,
382 Err(err) => match err.kind() {
383 ErrorKind::ConnectionReset | ErrorKind::BrokenPipe => false,
384 _ => return Err(err.into()),
385 }
386 } {
387 let res = match self.respond(&mut conn) {
388 Ok(RequestResult::Ok(Request { sent, mut etag })) => {
389 let Response::Path(path) = sent else {
390 unreachable!("Server::respond returned in-memory data instead of path");
391 };
392 let content_type = path_to_mime_type(&path);
393 let res = on_pending_request(addr, path).map_err(ServerError::Callback)?;
394 if self.send_file(conn.get_mut(), &res, content_type, etag)? {
395 etag = None;
396 }
397 RequestResult::Ok(Request { sent: res, etag })
398 }
399
400 Ok(RequestResult::FileNotFound(path)) => {
401 Self::send_404(conn.get_mut())?;
402 RequestResult::FileNotFound(path)
403 }
404
405 Ok(res @(| RequestResult::NoRequestedPath
406 | RequestResult::InvalidHeader
407 | RequestResult::InvalidHttpVersion
408 | RequestResult::InvalidHttpMethod)) => {
409 Self::send_400(conn.get_mut())?;
410 res
411 }
412
413 Err(ServerError::Io(err)) if err.kind() == ErrorKind::ConnectionReset => break,
414
415 Err(err) => return Err(err),
416 };
417 on_request(addr, res).map_err(ServerError::Callback)?
418 }
419 Ok(())
420 }
421
422 pub fn try_serve_with_callback<E>(
441 &mut self,
442 mut on_pending_request: impl FnMut(&SocketAddr, PathBuf) -> Result<Response, E>,
443 mut on_request: impl FnMut(&SocketAddr, RequestResult) -> Result<(), E>,
444 ) -> Result<Infallible, ServerError<E>> {
445 loop {
446 let (conn, addr) = self.listener.accept()?;
447 self.handle_conn(
448 conn,
449 &addr,
450 &mut on_pending_request,
451 &mut on_request,
452 )?;
453 }
454 }
455
456 pub fn serve_with_callback(
474 &mut self,
475 mut on_pending_request: impl FnMut(&SocketAddr, PathBuf) -> Response,
476 mut on_request: impl FnMut(&SocketAddr, RequestResult),
477 ) -> Result<Infallible> {
478 self.try_serve_with_callback::<Infallible>(
479 |addr, path| Ok(on_pending_request(addr, path)),
480 |addr, req| Ok(on_request(addr, req)),
481 ).map_err(|err| match err {
482 ServerError::Io(err) => err,
483 ServerError::Callback(err) => match err {},
484 })
485 }
486
487 pub fn serve(&mut self) -> Result<Infallible> {
493 self.serve_with_callback(|_, path| path.into(), |_, _| ())
494 }
495}
496
497pub fn print_request_result(addr: &SocketAddr, res: RequestResult) {
512 match res {
513 RequestResult::Ok(req) if req.client_cache_reused() =>
514 println!("{addr}:\n -> GET {}\n <- 304 Not Modified", req.sent.display()),
515 RequestResult::Ok(req) =>
516 println!("{addr}:\n -> GET {}\n <- 200 OK", req.sent.display()),
517 RequestResult::InvalidHttpMethod =>
518 println!("{addr}:\n -> <invalid HTTP method>\n <- 400 Bad Request"),
519 RequestResult::NoRequestedPath =>
520 println!("{addr}:\n -> <no requested path>\n <- 400 Bad Request"),
521 RequestResult::InvalidHttpVersion =>
522 println!("{addr}:\n -> <invalid HTTP version>\n <- 400 Bad Request"),
523 RequestResult::InvalidHeader =>
524 println!("{addr}:\n -> <invalid header(s)>\n <- 400 Bad Request"),
525 RequestResult::FileNotFound(path) =>
526 println!("{addr}:\n -> GET {path}\n <- 404 Not Found"),
527 }
528}
529
530pub fn serve_current_dir() -> Result<Infallible> {
535 Server::current_dir()?.serve()
536}
537
538pub fn serve(root: impl AsRef<Path>) -> Result<Infallible> {
543 Server::new(root)?.serve()
544}
545
546pub fn serve_current_dir_at(port: u16) -> Result<Infallible> {
551 Server::current_dir_at(port)?.serve()
552}
553
554pub fn serve_at(root: impl AsRef<Path>, port: u16) -> Result<Infallible> {
559 Server::new_at(root, port)?.serve()
560}