From fdd2ac7e562d6cb8786a650690d904a5098b9c7f Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 24 Jan 2021 19:36:16 +0100 Subject: [PATCH 1/8] simplify meta type to &str Since this specialty is only used once and this would complicate generating the logging string from a str array without code duplication. --- src/main.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index ef4906f..0271461 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,7 +120,7 @@ async fn handle_request(stream: TcpStream) -> Result { match parse_request(stream).await { Ok(url) => send_response(url, stream).await?, - Err((status, msg)) => send_header(stream, status, &[msg]).await?, + Err((status, msg)) => send_header(stream, status, msg).await?, } stream.shutdown().await?; Ok(()) @@ -192,8 +192,7 @@ async fn send_response(url: Url, stream: &mut TlsStream) -> Result { for segment in segments { if !ARGS.serve_secret && segment.starts_with('.') { // Do not serve anything that looks like a hidden file. - return send_header(stream, 52, &["If I told you, it would not be a secret."]) - .await; + return send_header(stream, 52, "If I told you, it would not be a secret.").await; } path.push(&*percent_decode_str(segment).decode_utf8()?); } @@ -213,7 +212,7 @@ async fn send_response(url: Url, stream: &mut TlsStream) -> Result { // if client is not redirected, links may not work as expected without trailing slash let mut url = url; url.set_path(&format!("{}/", url.path())); - return send_header(stream, 31, &[url.as_str()]).await; + return send_header(stream, 31, url.as_str()).await; } } } @@ -222,7 +221,7 @@ async fn send_response(url: Url, stream: &mut TlsStream) -> Result { let mut file = match tokio::fs::File::open(&path).await { Ok(file) => file, Err(e) => { - send_header(stream, 51, &["Not found, sorry."]).await?; + send_header(stream, 51, "Not found, sorry.").await?; Err(e)? } }; @@ -232,7 +231,7 @@ async fn send_response(url: Url, stream: &mut TlsStream) -> Result { send_text_gemini_header(stream).await?; } else { let mime = mime_guess::from_path(&path).first_or_octet_stream(); - send_header(stream, 20, &[mime.essence_str()]).await?; + send_header(stream, 20, mime.essence_str()).await?; } // Send body. @@ -271,11 +270,10 @@ async fn list_directory(stream: &mut TlsStream, path: &Path) -> Resul Ok(()) } -async fn send_header(stream: &mut TlsStream, status: u8, meta: &[&str]) -> Result { +async fn send_header(stream: &mut TlsStream, status: u8, meta: &str) -> Result { use std::fmt::Write; let mut response = String::with_capacity(64); - write!(response, "{} ", status)?; - response.extend(meta.iter().copied()); + write!(response, "{} {}", status, meta)?; log::info!("Responding with status {:?}", response); response.push_str("\r\n"); stream.write_all(response.as_bytes()).await?; @@ -284,8 +282,8 @@ async fn send_header(stream: &mut TlsStream, status: u8, meta: &[&str async fn send_text_gemini_header(stream: &mut TlsStream) -> Result { if let Some(lang) = ARGS.language.as_deref() { - send_header(stream, 20, &["text/gemini;lang=", lang]).await + send_header(stream, 20, &format!("text/gemini;lang={}", lang)).await } else { - send_header(stream, 20, &["text/gemini"]).await + send_header(stream, 20, "text/gemini").await } } From 21486a0d11fa16932a075282ac9158fb0117baa7 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 24 Jan 2021 19:42:46 +0100 Subject: [PATCH 2/8] add logging for peer IP addresses --- src/main.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 0271461..1bdd3b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,6 +60,7 @@ struct Args { language: Option, silent: bool, serve_secret: bool, + log_ips: bool, } fn args() -> Result { @@ -74,6 +75,7 @@ fn args() -> Result { opts.optflag("s", "silent", "Disable logging output"); opts.optflag("h", "help", "Print this help menu"); opts.optflag("", "serve-secret", "Enable serving secret files (files/directories starting with a dot)"); + opts.optflag("", "ip", "Output IP addresses when logging"); let matches = opts.parse(&args[1..]).map_err(|f| f.to_string())?; if matches.opt_present("h") { @@ -103,6 +105,7 @@ fn args() -> Result { language: matches.opt_str("lang"), silent: matches.opt_present("s"), serve_secret: matches.opt_present("serve-secret"), + log_ips: matches.opt_present("ip"), }) } @@ -162,7 +165,19 @@ async fn parse_request(stream: &mut TlsStream) -> std::result::Result buf = &mut request[len..]; } let request = std::str::from_utf8(&request[..len - 2]).or(Err((59, "Non-UTF-8 request")))?; - log::info!("Got request for {:?}", request); + if ARGS.log_ips { + log::info!( + "Got request for {:?} from {}", + request, + stream + .get_ref() + .0 + .peer_addr() + .expect("could not get peer address") + ); + } else { + log::info!("Got request for {:?}", request); + } let url = Url::parse(request).or(Err((59, "Invalid URL")))?; From aa17b5bc17d318928db0dd574d378bf34f71ec91 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 24 Jan 2021 20:10:46 +0100 Subject: [PATCH 3/8] add RequestHandle struct --- src/main.rs | 57 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1bdd3b8..bea9ab5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -117,15 +117,21 @@ fn check_path(s: String) -> Result { } } +struct RequestHandle { + pub stream: TlsStream, +} + /// Handle a single client session (request + response). async fn handle_request(stream: TcpStream) -> Result { - let stream = &mut TLS.accept(stream).await?; + let stream = TLS.accept(stream).await?; - match parse_request(stream).await { - Ok(url) => send_response(url, stream).await?, - Err((status, msg)) => send_header(stream, status, msg).await?, + let mut handle = RequestHandle { stream }; + + match parse_request(&mut handle).await { + Ok(url) => send_response(url, &mut handle).await?, + Err((status, msg)) => send_header(&mut handle, status, msg).await?, } - stream.shutdown().await?; + handle.stream.shutdown().await?; Ok(()) } @@ -145,7 +151,7 @@ fn acceptor() -> Result { } /// Return the URL requested by the client. -async fn parse_request(stream: &mut TlsStream) -> std::result::Result { +async fn parse_request(handle: &mut RequestHandle) -> std::result::Result { // Because requests are limited to 1024 bytes (plus 2 bytes for CRLF), we // can use a fixed-sized buffer on the stack, avoiding allocations and // copying, and stopping bad clients from making us use too much memory. @@ -155,7 +161,7 @@ async fn parse_request(stream: &mut TlsStream) -> std::result::Result // Read until CRLF, end-of-stream, or there's no buffer space left. loop { - let bytes_read = stream.read(buf).await.or(Err((59, "Request ended unexpectedly")))?; + let bytes_read = handle.stream.read(buf).await.or(Err((59, "Request ended unexpectedly")))?; len += bytes_read; if request[..len].ends_with(b"\r\n") { break; @@ -169,7 +175,8 @@ async fn parse_request(stream: &mut TlsStream) -> std::result::Result log::info!( "Got request for {:?} from {}", request, - stream + handle + .stream .get_ref() .0 .peer_addr() @@ -193,7 +200,7 @@ async fn parse_request(stream: &mut TlsStream) -> std::result::Result } if let Some(port) = url.port() { // Validate that the port in the URL is the same as for the stream this request came in on. - if port != stream.get_ref().0.local_addr().unwrap().port() { + if port != handle.stream.get_ref().0.local_addr().unwrap().port() { return Err((53, "proxy request refused")); } } @@ -201,13 +208,13 @@ async fn parse_request(stream: &mut TlsStream) -> std::result::Result } /// Send the client the file located at the requested URL. -async fn send_response(url: Url, stream: &mut TlsStream) -> Result { +async fn send_response(url: Url, handle: &mut RequestHandle) -> Result { let mut path = std::path::PathBuf::from(&ARGS.content_dir); if let Some(segments) = url.path_segments() { for segment in segments { if !ARGS.serve_secret && segment.starts_with('.') { // Do not serve anything that looks like a hidden file. - return send_header(stream, 52, "If I told you, it would not be a secret.").await; + return send_header(handle, 52, "If I told you, it would not be a secret.").await; } path.push(&*percent_decode_str(segment).decode_utf8()?); } @@ -221,13 +228,13 @@ async fn send_response(url: Url, stream: &mut TlsStream) -> Result { path.push("index.gmi"); if !path.exists() && path.with_file_name(".directory-listing-ok").exists() { path.pop(); - return list_directory(stream, &path).await; + return list_directory(handle, &path).await; } } else { // if client is not redirected, links may not work as expected without trailing slash let mut url = url; url.set_path(&format!("{}/", url.path())); - return send_header(stream, 31, url.as_str()).await; + return send_header(handle, 31, url.as_str()).await; } } } @@ -236,32 +243,32 @@ async fn send_response(url: Url, stream: &mut TlsStream) -> Result { let mut file = match tokio::fs::File::open(&path).await { Ok(file) => file, Err(e) => { - send_header(stream, 51, "Not found, sorry.").await?; + send_header(handle, 51, "Not found, sorry.").await?; Err(e)? } }; // Send header. if path.extension() == Some(OsStr::new("gmi")) { - send_text_gemini_header(stream).await?; + send_text_gemini_header(handle).await?; } else { let mime = mime_guess::from_path(&path).first_or_octet_stream(); - send_header(stream, 20, mime.essence_str()).await?; + send_header(handle, 20, mime.essence_str()).await?; } // Send body. - tokio::io::copy(&mut file, stream).await?; + tokio::io::copy(&mut file, &mut handle.stream).await?; Ok(()) } -async fn list_directory(stream: &mut TlsStream, path: &Path) -> Result { +async fn list_directory(handle: &mut RequestHandle, path: &Path) -> Result { // https://url.spec.whatwg.org/#path-percent-encode-set const ENCODE_SET: AsciiSet = CONTROLS.add(b' ') .add(b'"').add(b'#').add(b'<').add(b'>') .add(b'?').add(b'`').add(b'{').add(b'}'); log::info!("Listing directory {:?}", path); - send_text_gemini_header(stream).await?; + send_text_gemini_header(handle).await?; let mut entries = tokio::fs::read_dir(path).await?; let mut lines = vec![]; while let Some(entry) = entries.next_entry().await? { @@ -280,25 +287,25 @@ async fn list_directory(stream: &mut TlsStream, path: &Path) -> Resul } lines.sort(); for line in lines { - stream.write_all(line.as_bytes()).await?; + handle.stream.write_all(line.as_bytes()).await?; } Ok(()) } -async fn send_header(stream: &mut TlsStream, status: u8, meta: &str) -> Result { +async fn send_header(handle: &mut RequestHandle, status: u8, meta: &str) -> Result { use std::fmt::Write; let mut response = String::with_capacity(64); write!(response, "{} {}", status, meta)?; log::info!("Responding with status {:?}", response); response.push_str("\r\n"); - stream.write_all(response.as_bytes()).await?; + handle.stream.write_all(response.as_bytes()).await?; Ok(()) } -async fn send_text_gemini_header(stream: &mut TlsStream) -> Result { +async fn send_text_gemini_header(handle: &mut RequestHandle) -> Result { if let Some(lang) = ARGS.language.as_deref() { - send_header(stream, 20, &format!("text/gemini;lang={}", lang)).await + send_header(handle, 20, &format!("text/gemini;lang={}", lang)).await } else { - send_header(stream, 20, "text/gemini").await + send_header(handle, 20, "text/gemini").await } } From 3353989e7e20ceb7ca5980aca4c72e4bad37a825 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 24 Jan 2021 20:31:47 +0100 Subject: [PATCH 4/8] add log_line to RequestHandle There are still some problems with this, the error handling in handle_request will have to be changed to accomodated the new log_line. --- src/main.rs | 51 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/main.rs b/src/main.rs index bea9ab5..e05d512 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use { borrow::Cow, error::Error, ffi::OsStr, + fmt::Write, fs::File, io::BufReader, net::SocketAddr, @@ -119,19 +120,37 @@ fn check_path(s: String) -> Result { struct RequestHandle { pub stream: TlsStream, + pub log_line: String, } /// Handle a single client session (request + response). async fn handle_request(stream: TcpStream) -> Result { + let log_line = format!( + "{} {}", + stream.local_addr().unwrap(), + if ARGS.log_ips { + stream + .peer_addr() + .expect("could not get peer address") + .to_string() + } else { + // Do not log IP address, but something else so columns still line up. + "-".into() + } + ); + let stream = TLS.accept(stream).await?; - let mut handle = RequestHandle { stream }; + let mut handle = RequestHandle { stream, log_line }; match parse_request(&mut handle).await { Ok(url) => send_response(url, &mut handle).await?, Err((status, msg)) => send_header(&mut handle, status, msg).await?, } handle.stream.shutdown().await?; + + log::info!("{}", handle.log_line); + Ok(()) } @@ -171,20 +190,9 @@ async fn parse_request(handle: &mut RequestHandle) -> std::result::Result Result { } async fn send_header(handle: &mut RequestHandle, status: u8, meta: &str) -> Result { - use std::fmt::Write; - let mut response = String::with_capacity(64); - write!(response, "{} {}", status, meta)?; - log::info!("Responding with status {:?}", response); - response.push_str("\r\n"); - handle.stream.write_all(response.as_bytes()).await?; + // add response status and response meta + write!(handle.log_line, " {} \"{}\"", status, meta)?; + + handle + .stream + .write_all(format!("{} {}\r\n", status, meta).as_bytes()) + .await?; Ok(()) } From 74853799c71e2a30782f9e82cc155c686bfdc96f Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 24 Jan 2021 20:45:36 +0100 Subject: [PATCH 5/8] handle errors in handle_request --- src/main.rs | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/main.rs b/src/main.rs index e05d512..d29ba1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,11 +34,7 @@ fn main() -> Result { log::info!("Listening on {:?}...", ARGS.addrs); loop { let (stream, _) = listener.accept().await?; - tokio::spawn(async { - if let Err(e) = handle_request(stream).await { - log::error!("{:?}", e); - } - }); + tokio::spawn(async { handle_request(stream).await }); } }) } @@ -123,8 +119,9 @@ struct RequestHandle { pub log_line: String, } -/// Handle a single client session (request + response). -async fn handle_request(stream: TcpStream) -> Result { +/// Handle a single client session (request + response) and any errors that +/// may occur while processing it. +async fn handle_request(stream: TcpStream) { let log_line = format!( "{} {}", stream.local_addr().unwrap(), @@ -139,19 +136,28 @@ async fn handle_request(stream: TcpStream) -> Result { } ); - let stream = TLS.accept(stream).await?; + let stream = match TLS.accept(stream).await { + Ok(stream) => stream, + Err(e) => { + log::warn!("{} error:{}", log_line, e); + return; + } + }; let mut handle = RequestHandle { stream, log_line }; - match parse_request(&mut handle).await { - Ok(url) => send_response(url, &mut handle).await?, - Err((status, msg)) => send_header(&mut handle, status, msg).await?, + let mut result = match parse_request(&mut handle).await { + Ok(url) => send_response(url, &mut handle).await, + Err((status, msg)) => send_header(&mut handle, status, msg).await, + }; + + if let Err(e) = result { + log::warn!("{} error:{}", handle.log_line, e); + } else if let Err(e) = handle.stream.shutdown().await { + log::warn!("{} error:{}", handle.log_line, e); + } else { + log::info!("{}", handle.log_line); } - handle.stream.shutdown().await?; - - log::info!("{}", handle.log_line); - - Ok(()) } /// TLS configuration. From 116c9fdcb43758b591394c06a66da7d61e08481e Mon Sep 17 00:00:00 2001 From: Johann150 Date: Mon, 25 Jan 2021 21:03:48 +0100 Subject: [PATCH 6/8] rename flag --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index d29ba1a..6e13919 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,7 +72,7 @@ fn args() -> Result { opts.optflag("s", "silent", "Disable logging output"); opts.optflag("h", "help", "Print this help menu"); opts.optflag("", "serve-secret", "Enable serving secret files (files/directories starting with a dot)"); - opts.optflag("", "ip", "Output IP addresses when logging"); + opts.optflag("", "log-ip", "Output IP addresses when logging"); let matches = opts.parse(&args[1..]).map_err(|f| f.to_string())?; if matches.opt_present("h") { From f0789921e04d1eb260c7dec3da5a66cd71c45d70 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Mon, 25 Jan 2021 21:50:59 +0100 Subject: [PATCH 7/8] make functions into methods of RequestHandle --- src/main.rs | 389 ++++++++++++++++++++++++++++------------------------ 1 file changed, 211 insertions(+), 178 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6e13919..7585a87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,17 @@ fn main() -> Result { log::info!("Listening on {:?}...", ARGS.addrs); loop { let (stream, _) = listener.accept().await?; - tokio::spawn(async { handle_request(stream).await }); + tokio::spawn(async { + match RequestHandle::new(stream).await { + Ok(handle) => match handle.handle().await { + Ok(info) => log::info!("{}", info), + Err(err) => log::warn!("{}", err), + }, + Err(log_line) => { + log::warn!("{}", log_line); + } + } + }); } }) } @@ -114,52 +124,6 @@ fn check_path(s: String) -> Result { } } -struct RequestHandle { - pub stream: TlsStream, - pub log_line: String, -} - -/// Handle a single client session (request + response) and any errors that -/// may occur while processing it. -async fn handle_request(stream: TcpStream) { - let log_line = format!( - "{} {}", - stream.local_addr().unwrap(), - if ARGS.log_ips { - stream - .peer_addr() - .expect("could not get peer address") - .to_string() - } else { - // Do not log IP address, but something else so columns still line up. - "-".into() - } - ); - - let stream = match TLS.accept(stream).await { - Ok(stream) => stream, - Err(e) => { - log::warn!("{} error:{}", log_line, e); - return; - } - }; - - let mut handle = RequestHandle { stream, log_line }; - - let mut result = match parse_request(&mut handle).await { - Ok(url) => send_response(url, &mut handle).await, - Err((status, msg)) => send_header(&mut handle, status, msg).await, - }; - - if let Err(e) = result { - log::warn!("{} error:{}", handle.log_line, e); - } else if let Err(e) = handle.stream.shutdown().await { - log::warn!("{} error:{}", handle.log_line, e); - } else { - log::info!("{}", handle.log_line); - } -} - /// TLS configuration. static TLS: Lazy = Lazy::new(|| acceptor().unwrap()); @@ -175,152 +139,221 @@ fn acceptor() -> Result { Ok(TlsAcceptor::from(Arc::new(config))) } -/// Return the URL requested by the client. -async fn parse_request(handle: &mut RequestHandle) -> std::result::Result { - // Because requests are limited to 1024 bytes (plus 2 bytes for CRLF), we - // can use a fixed-sized buffer on the stack, avoiding allocations and - // copying, and stopping bad clients from making us use too much memory. - let mut request = [0; 1026]; - let mut buf = &mut request[..]; - let mut len = 0; - - // Read until CRLF, end-of-stream, or there's no buffer space left. - loop { - let bytes_read = handle.stream.read(buf).await.or(Err((59, "Request ended unexpectedly")))?; - len += bytes_read; - if request[..len].ends_with(b"\r\n") { - break; - } else if bytes_read == 0 { - return Err((59, "Request ended unexpectedly")); - } - buf = &mut request[len..]; - } - let request = std::str::from_utf8(&request[..len - 2]).or(Err((59, "Non-UTF-8 request")))?; - - // log literal request (might be different from or not an actual URL) - write!(handle.log_line, " \"{}\"", request).unwrap(); - - let url = Url::parse(request).or(Err((59, "Invalid URL")))?; - - // Validate the URL, host and port. - if url.scheme() != "gemini" { - return Err((53, "Unsupported URL scheme")); - } - // TODO: Can be simplified by https://github.com/servo/rust-url/pull/651 - if let (Some(Host::Domain(expected)), Some(Host::Domain(host))) = (url.host(), &ARGS.hostname) { - if host != expected { - return Err((53, "Proxy request refused")); - } - } - if let Some(port) = url.port() { - // Validate that the port in the URL is the same as for the stream this request came in on. - if port != handle.stream.get_ref().0.local_addr().unwrap().port() { - return Err((53, "proxy request refused")); - } - } - Ok(url) +struct RequestHandle { + stream: TlsStream, + log_line: String, } -/// Send the client the file located at the requested URL. -async fn send_response(url: Url, handle: &mut RequestHandle) -> Result { - let mut path = std::path::PathBuf::from(&ARGS.content_dir); - if let Some(segments) = url.path_segments() { - for segment in segments { - if !ARGS.serve_secret && segment.starts_with('.') { - // Do not serve anything that looks like a hidden file. - return send_header(handle, 52, "If I told you, it would not be a secret.").await; - } - path.push(&*percent_decode_str(segment).decode_utf8()?); - } - } - - if let Ok(metadata) = tokio::fs::metadata(&path).await { - if metadata.is_dir() { - if url.path().ends_with('/') || url.path().is_empty() { - // if the path ends with a slash or the path is empty, the links will work the same - // without a redirect - path.push("index.gmi"); - if !path.exists() && path.with_file_name(".directory-listing-ok").exists() { - path.pop(); - return list_directory(handle, &path).await; - } +impl RequestHandle { + /// Creates a new request handle for the given stream. If establishing the TLS + /// session fails, returns a corresponding log line. + async fn new(stream: TcpStream) -> Result { + let log_line = format!( + "{} {}", + stream.local_addr().unwrap(), + if ARGS.log_ips { + stream + .peer_addr() + .expect("could not get peer address") + .to_string() } else { - // if client is not redirected, links may not work as expected without trailing slash - let mut url = url; - url.set_path(&format!("{}/", url.path())); - return send_header(handle, 31, url.as_str()).await; + // Do not log IP address, but something else so columns still line up. + "-".into() + } + ); + + match TLS.accept(stream).await { + Ok(stream) => Ok(Self { stream, log_line }), + Err(e) => Err(format!("{} error:{}", log_line, e)), + } + } + + /// Do the necessary actions to handle this request. If the handle is already + /// in an error state, does nothing. + /// Finally return the generated log line content. If this contains + /// the string ` error:`, the handle ended in an error state. + async fn handle(mut self) -> Result { + // not already in error condition + let result = match self.parse_request().await { + Ok(url) => self.send_response(url).await, + Err((status, msg)) => self.send_header(status, msg).await, + }; + + if let Err(e) = result { + Err(format!("{} error:{}", self.log_line, e)) + } else if let Err(e) = self.stream.shutdown().await { + Err(format!("{} error:{}", self.log_line, e)) + } else { + Ok(self.log_line) + } + } + + /// Return the URL requested by the client. + async fn parse_request(&mut self) -> std::result::Result { + // Because requests are limited to 1024 bytes (plus 2 bytes for CRLF), we + // can use a fixed-sized buffer on the stack, avoiding allocations and + // copying, and stopping bad clients from making us use too much memory. + let mut request = [0; 1026]; + let mut buf = &mut request[..]; + let mut len = 0; + + // Read until CRLF, end-of-stream, or there's no buffer space left. + loop { + let bytes_read = self + .stream + .read(buf) + .await + .or(Err((59, "Request ended unexpectedly")))?; + len += bytes_read; + if request[..len].ends_with(b"\r\n") { + break; + } else if bytes_read == 0 { + return Err((59, "Request ended unexpectedly")); + } + buf = &mut request[len..]; + } + let request = + std::str::from_utf8(&request[..len - 2]).or(Err((59, "Non-UTF-8 request")))?; + + // log literal request (might be different from or not an actual URL) + write!(self.log_line, " \"{}\"", request).unwrap(); + + let url = Url::parse(request).or(Err((59, "Invalid URL")))?; + + // Validate the URL, host and port. + if url.scheme() != "gemini" { + return Err((53, "Unsupported URL scheme")); + } + // TODO: Can be simplified by https://github.com/servo/rust-url/pull/651 + if let (Some(Host::Domain(expected)), Some(Host::Domain(host))) = + (url.host(), &ARGS.hostname) + { + if host != expected { + return Err((53, "Proxy request refused")); } } + if let Some(port) = url.port() { + // Validate that the port in the URL is the same as for the stream this request came in on. + if port != self.stream.get_ref().0.local_addr().unwrap().port() { + return Err((53, "proxy request refused")); + } + } + Ok(url) } - // Make sure the file opens successfully before sending the success header. - let mut file = match tokio::fs::File::open(&path).await { - Ok(file) => file, - Err(e) => { - send_header(handle, 51, "Not found, sorry.").await?; - Err(e)? + /// Send the client the file located at the requested URL. + async fn send_response(&mut self, url: Url) -> Result { + let mut path = std::path::PathBuf::from(&ARGS.content_dir); + if let Some(segments) = url.path_segments() { + for segment in segments { + if !ARGS.serve_secret && segment.starts_with('.') { + // Do not serve anything that looks like a hidden file. + return self + .send_header(52, "If I told you, it would not be a secret.") + .await; + } + path.push(&*percent_decode_str(segment).decode_utf8()?); + } } - }; - // Send header. - if path.extension() == Some(OsStr::new("gmi")) { - send_text_gemini_header(handle).await?; - } else { - let mime = mime_guess::from_path(&path).first_or_octet_stream(); - send_header(handle, 20, mime.essence_str()).await?; - } - - // Send body. - tokio::io::copy(&mut file, &mut handle.stream).await?; - Ok(()) -} - -async fn list_directory(handle: &mut RequestHandle, path: &Path) -> Result { - // https://url.spec.whatwg.org/#path-percent-encode-set - const ENCODE_SET: AsciiSet = CONTROLS.add(b' ') - .add(b'"').add(b'#').add(b'<').add(b'>') - .add(b'?').add(b'`').add(b'{').add(b'}'); - - log::info!("Listing directory {:?}", path); - send_text_gemini_header(handle).await?; - let mut entries = tokio::fs::read_dir(path).await?; - let mut lines = vec![]; - while let Some(entry) = entries.next_entry().await? { - let mut name = entry.file_name().into_string().or(Err("Non-Unicode filename"))?; - if name.starts_with('.') { - continue; + if let Ok(metadata) = tokio::fs::metadata(&path).await { + if metadata.is_dir() { + if url.path().ends_with('/') || url.path().is_empty() { + // if the path ends with a slash or the path is empty, the links will work the same + // without a redirect + path.push("index.gmi"); + if !path.exists() && path.with_file_name(".directory-listing-ok").exists() { + path.pop(); + return self.list_directory(&path).await; + } + } else { + // if client is not redirected, links may not work as expected without trailing slash + let mut url = url; + url.set_path(&format!("{}/", url.path())); + return self.send_header(31, url.as_str()).await; + } + } } - if entry.file_type().await?.is_dir() { - name += "/"; - } - let line = match percent_encode(name.as_bytes(), &ENCODE_SET).into() { - Cow::Owned(url) => format!("=> {} {}\n", url, name), - Cow::Borrowed(url) => format!("=> {}\n", url), // url and name are identical + + // Make sure the file opens successfully before sending the success header. + let mut file = match tokio::fs::File::open(&path).await { + Ok(file) => file, + Err(e) => { + self.send_header(51, "Not found, sorry.").await?; + Err(e)? + } }; - lines.push(line); + + // Send header. + if path.extension() == Some(OsStr::new("gmi")) { + self.send_text_gemini_header().await?; + } else { + let mime = mime_guess::from_path(&path).first_or_octet_stream(); + self.send_header(20, mime.essence_str()).await?; + } + + // Send body. + tokio::io::copy(&mut file, &mut self.stream).await?; + Ok(()) } - lines.sort(); - for line in lines { - handle.stream.write_all(line.as_bytes()).await?; + + async fn list_directory(&mut self, path: &Path) -> Result { + // https://url.spec.whatwg.org/#path-percent-encode-set + const ENCODE_SET: AsciiSet = CONTROLS + .add(b' ') + .add(b'"') + .add(b'#') + .add(b'<') + .add(b'>') + .add(b'?') + .add(b'`') + .add(b'{') + .add(b'}'); + + log::info!("Listing directory {:?}", path); + self.send_text_gemini_header().await?; + let mut entries = tokio::fs::read_dir(path).await?; + let mut lines = vec![]; + while let Some(entry) = entries.next_entry().await? { + let mut name = entry + .file_name() + .into_string() + .or(Err("Non-Unicode filename"))?; + if name.starts_with('.') { + continue; + } + if entry.file_type().await?.is_dir() { + name += "/"; + } + let line = match percent_encode(name.as_bytes(), &ENCODE_SET).into() { + Cow::Owned(url) => format!("=> {} {}\n", url, name), + Cow::Borrowed(url) => format!("=> {}\n", url), // url and name are identical + }; + lines.push(line); + } + lines.sort(); + for line in lines { + self.stream.write_all(line.as_bytes()).await?; + } + Ok(()) } - Ok(()) -} -async fn send_header(handle: &mut RequestHandle, status: u8, meta: &str) -> Result { - // add response status and response meta - write!(handle.log_line, " {} \"{}\"", status, meta)?; + async fn send_header(&mut self, status: u8, meta: &str) -> Result { + // add response status and response meta + write!(self.log_line, " {} \"{}\"", status, meta)?; - handle - .stream - .write_all(format!("{} {}\r\n", status, meta).as_bytes()) - .await?; - Ok(()) -} + self.stream + .write_all(format!("{} {}\r\n", status, meta).as_bytes()) + .await?; + Ok(()) + } -async fn send_text_gemini_header(handle: &mut RequestHandle) -> Result { - if let Some(lang) = ARGS.language.as_deref() { - send_header(handle, 20, &format!("text/gemini;lang={}", lang)).await - } else { - send_header(handle, 20, "text/gemini").await + async fn send_text_gemini_header(&mut self) -> Result { + if let Some(lang) = ARGS.language.as_deref() { + self.send_header(20, &format!("text/gemini;lang={}", lang)) + .await + } else { + self.send_header(20, "text/gemini").await + } } } From 0411a8278f269a5a94b552f5f368372044cfdcb5 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Mon, 25 Jan 2021 21:55:35 +0100 Subject: [PATCH 8/8] fix doc comment --- src/main.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7585a87..ec649f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -168,10 +168,9 @@ impl RequestHandle { } } - /// Do the necessary actions to handle this request. If the handle is already - /// in an error state, does nothing. - /// Finally return the generated log line content. If this contains - /// the string ` error:`, the handle ended in an error state. + /// Do the necessary actions to handle this request. Returns a corresponding + /// log line as Err or Ok, depending on if the request finished with or + /// without errors. async fn handle(mut self) -> Result { // not already in error condition let result = match self.parse_request().await {