From d2af2c5f08c0e83f985c14857f478495e0b5907d Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Fri, 31 Jul 2020 12:16:15 -0400 Subject: [PATCH] file serving --- Cargo.toml | 25 ++++++++++--------- shell.nix | 11 +++----- src/response.rs | 8 ++++++ src/server/files.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++ src/server/mod.rs | 39 ++++++++++++++++++++--------- 5 files changed, 113 insertions(+), 31 deletions(-) create mode 100644 src/server/files.rs diff --git a/Cargo.toml b/Cargo.toml index 2c91276..f3b1df3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "maj" -version = "0.4.2" +version = "0.5.0" authors = ["Christine Dodrill "] edition = "2018" license = "0BSD" @@ -10,22 +10,23 @@ repository = "https://tulpa.dev/cadey/maj" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-std = { version = "1.6", optional = true } +async-tls = { default-features = false, optional = true, version = "0" } async-trait = { version = "0", optional = true } +log = "0.4" +mime_guess = "2.0" num = "0.2" num-derive = "0.3" num-traits = "0.2" -rustls = { version = "0.18", optional = true, features = ["dangerous_configuration"] } -tokio-rustls = { version = "0.14", features = ["dangerous_configuration"], optional = true } -webpki = { version = "0.21.0", optional = true } -webpki-roots = { version = "0.20", optional = true } -tokio = { version = "0.2", features = ["full"], optional = true } -async-tls = { default-features = false, optional = true, version = "0" } -async-std = { version = "1.6", optional = true } -log = "0.4" -url = "2" -thiserror = "1" -structopt = "0.3" once_cell = "1.4" +rustls = { version = "0.18", optional = true, features = ["dangerous_configuration"] } +structopt = "0.3" +thiserror = "1" +tokio-rustls = { version = "0.14", features = ["dangerous_configuration"], optional = true } +tokio = { version = "0.2", features = ["full"], optional = true } +url = "2" +webpki-roots = { version = "0.20", optional = true } +webpki = { version = "0.21.0", optional = true } [dev-dependencies] pretty_env_logger = "0.4" diff --git a/shell.nix b/shell.nix index d71bcc1..cf023fd 100644 --- a/shell.nix +++ b/shell.nix @@ -1,6 +1,7 @@ -{ pkgs ? import { } }: - let + moz_overlay = import (builtins.fetchTarball + "https://github.com/mozilla/nixpkgs-mozilla/archive/master.tar.gz"); + pkgs = import { overlays = [ moz_overlay ]; }; nur = import (builtins.fetchTarball "https://github.com/nix-community/NUR/archive/master.tar.gz") { inherit pkgs; @@ -9,11 +10,7 @@ let texlive.combine { inherit (texlive) scheme-medium bitter titlesec; }; in pkgs.mkShell { buildInputs = with pkgs; [ - rustc - cargo - rls - rustfmt - cargo-watch + pkgs.latest.rustChannels.stable.rust pkg-config ncurses diff --git a/src/response.rs b/src/response.rs index 61c4fcb..dbc6d29 100644 --- a/src/response.rs +++ b/src/response.rs @@ -11,6 +11,14 @@ pub struct Response { } impl Response { + pub fn with_body(meta: String, body: Vec) -> Response { + Response { + status: StatusCode::Success, + meta: meta, + body: body, + } + } + pub fn gemini(body: Vec) -> Response { Response { status: StatusCode::Success, diff --git a/src/server/files.rs b/src/server/files.rs new file mode 100644 index 0000000..999beb0 --- /dev/null +++ b/src/server/files.rs @@ -0,0 +1,61 @@ +/// A simple handler for disk based files. Will optionally chop off a prefix. +use super::{Handler as MajHandler, Request, Result}; +use crate::Response; +use async_trait::async_trait; +use std::ffi::OsStr; + +pub struct Handler { + chop_off: String, + base_dir: String, +} + +impl Handler { + /// Serves static files from an OS directory with a given prefix chopped off. + pub fn new(chop_off: String, base_dir: String) -> Self { + Handler { + chop_off: chop_off, + base_dir: base_dir, + } + } + + fn chop(&self, path: String) -> Option { + if path.starts_with(&self.chop_off) { + path.strip_prefix(&self.chop_off).map(|val| val.to_string()) + } else { + Some(path) + } + } +} + +#[async_trait] +impl MajHandler for Handler { + async fn handle(&self, r: Request) -> Result { + let mut path = std::path::PathBuf::from(&self.base_dir); + let mut url = r.url.clone(); + url.set_path(&self.chop(r.url.path().to_string()).unwrap()); + if let Some(segments) = r.url.path_segments() { + path.extend(segments); + } + + if async_std::fs::metadata(&path).await?.is_dir() { + if url.as_str().ends_with('/') { + path.push("index.gmi"); + } else { + // Send a redirect when the URL for a directory has no trailing slash. + return Ok(Response::perm_redirect(format!("{}/", url))); + } + } + + let mut file = async_std::fs::File::open(&path).await?; + let mut buf: Vec = Vec::new(); + async_std::io::copy(&mut file, &mut buf).await?; + + // Send header. + if path.extension() == Some(OsStr::new("gmi")) { + return Ok(Response::gemini(buf)); + } + + let mime = mime_guess::from_path(&path).first_or_octet_stream(); + Ok(Response::with_body(mime.essence_str().to_string(), buf)) + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index 1ee016b..2e7fc7f 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -34,6 +34,8 @@ enum RequestParsingError { mod routes; pub use routes::*; +pub mod files; + #[async_trait] pub trait Handler { async fn handle(&self, r: Request) -> Result; @@ -78,13 +80,23 @@ async fn handle_request( Ok(url) => { if let Some(u_port) = url.port() { if port != u_port { - let _ = respond(&mut stream, "53", &["Cannot proxy"]).await; + let _ = write_header( + &mut stream, + StatusCode::ProxyRequestRefused, + "Cannot proxy to that URL", + ) + .await; return Ok(()); } } if url.scheme() != "gemini" { - let _ = respond(&mut stream, "53", &["Cannot proxy outside geminispace"]).await; + let _ = write_header( + &mut stream, + StatusCode::ProxyRequestRefused, + "Cannot proxy to that URL", + ) + .await; Err(RequestParsingError::InvalidScheme(url.scheme().to_string()))? } @@ -95,20 +107,19 @@ async fn handle_request( handle(h, req, &mut stream, addr).await; } Err(e) => { - respond(&mut stream, "59", &["Invalid request."]).await?; + let _ = write_header(&mut stream, StatusCode::BadRequest, "Invalid request.").await; log::error!("error from {}: {:?}", addr, e); } } Ok(()) } -async fn respond(mut stream: W, status: &str, meta: &[&str]) -> Result { - stream.write_all(status.as_bytes()).await?; - stream.write_all(b" ").await?; - for m in meta { - stream.write_all(m.as_bytes()).await?; - } - stream.write_all(b"\r\n").await?; +pub async fn write_header( + mut stream: W, + status: StatusCode, + meta: &str, +) -> Result { + stream.write(format!("{} {}\r\n", status as u8, meta).as_bytes()).await?; Ok(()) } @@ -145,8 +156,12 @@ async fn parse_request(mut stream: R) -> Result { Ok(url) } -async fn handle(h: Arc<(dyn Handler + Send + Sync)>, req: Request, stream: &mut T, addr: SocketAddr) -where +async fn handle( + h: Arc<(dyn Handler + Send + Sync)>, + req: Request, + stream: &mut T, + addr: SocketAddr, +) where T: Write + Unpin, { let u = req.url.clone();