diff --git a/README.md b/README.md index 9a134dd..c5d2d59 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,19 @@ Note: This sets the user to `nobody` and the group to `nobody` as well. This naming scheme is not consistent for all Unix systems... Try changing the group name to `nogroup` if the software fails to start. +Testing +------- + +As you may have spotted, I did not get around to write a test suite for this. +The server's behavior can be tested using the +[gemini-diagnostics](https://github.com/michael-lazar/gemini-diagnostics) suite +by michael-lazar. It passes all "important" tests (some malformed requests +are still handled). Most importantly: the URLDotEscape tests fails. This does +not mean you can successfully a URL escape attack against this, rather the URL +library I use already parses out any superfluous ..'s. +e.g. "localhost/../../../etc/passwd" already became "localhost/etc/passwd" once +I receive the parsed URL from the library. + Why "Sheldon Director"? ----------------------- diff --git a/doc/sheldond.conf b/doc/sheldond.conf index 7c39f06..1c04c87 100644 --- a/doc/sheldond.conf +++ b/doc/sheldond.conf @@ -5,8 +5,8 @@ default_host = localhost gem_root = ./doc # you can define as many of these as you like -listen = [::1]:1965 -listen = 127.0.0.1:1965 +listen = 0.0.0.0:1965 +listen = [::]:1965 # privilege level for the server to drop to after initializing user = nobody diff --git a/src/server/handler.rs b/src/server/handler.rs index f32a015..db45073 100644 --- a/src/server/handler.rs +++ b/src/server/handler.rs @@ -20,11 +20,10 @@ fn send_header(stream: &mut SslStream, header: &response::Header) { } pub fn handle_request(config: &ServerConfig, mut stream: SslStream) { - let mut buffer = [0; 1026]; + let mut buffer = [0; 1024]; match stream.ssl_read(&mut buffer) { Ok(s) => { - if s == 0 { - println!("received empty request buffer"); + if s == 0 || s > 1025 { send_header(&mut stream, &response::bad_request()); return; } @@ -47,7 +46,11 @@ pub fn handle_request(config: &ServerConfig, mut stream: SslStream) { let location = match Url::parse(&request) { Ok(url) => url, - Err(_) => config.default_host.join(&request).unwrap(), + Err(_) => { + println!("received invalid request url"); + send_header(&mut stream, &response::bad_request()); + return; + }, }; handle_response(config, location, &mut stream); @@ -69,25 +72,39 @@ fn write_line(line: &[u8], stream: &mut BufWriter) -> Result<(), Er fn handle_response(config: &ServerConfig, url: Url, mut stream: &mut SslStream) { println!("responding for: {}", url); - if url.scheme() != "gemini" { - send_header(&mut stream, &response::permanent_failure()); + // url scheme must be either "gemini://" or "//" (empty) + if url.scheme() != "gemini" && url.scheme() != "" { + send_header(&mut stream, &response::proxy_request_refused()); return; } + // url request host must match if url.host() != config.default_host.host() { send_header(&mut stream, &response::proxy_request_refused()); return; } + // TODO: also drop on incorrect port in request + let rel_path = match Path::new(url.path()).strip_prefix("/") { Ok(path) => path, Err(_) => { - Path::new("") + // empty path, gemini spec says to redirect client to root + send_header(&mut stream, &response::redirect_permanent( + config.default_host.as_str(), + )); + return; } }; let path = gen_path_index(&config.gem_root.join(rel_path)); + // make sure we can't escape gem_root + if !path.starts_with(&config.gem_root) { + send_header(&mut stream, &response::bad_request()); + return; + } + let file = match File::open(&path) { Ok(file) => file, Err(_) => { diff --git a/src/server/mod.rs b/src/server/mod.rs index 9909fd2..a38e194 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -24,7 +24,7 @@ pub struct ServerConfig { impl ServerConfig { pub fn new() -> ServerConfig { ServerConfig { - default_host: Url::parse("gemini://localhost").unwrap(), + default_host: Url::parse("gemini://localhost/").unwrap(), gem_root: PathBuf::from(""), addrs: Vec::new(), user: unistd::getuid(), @@ -35,7 +35,7 @@ impl ServerConfig { } pub fn set_default_host(&mut self, default_host: String) { - let mut url = Url::parse("gemini://default").unwrap(); + let mut url = Url::parse("gemini://default/").unwrap(); match url.set_host(Some(default_host.as_str())) { Ok(_) => {} diff --git a/src/server/response.rs b/src/server/response.rs index 913d881..263b2e3 100644 --- a/src/server/response.rs +++ b/src/server/response.rs @@ -3,6 +3,7 @@ use std::vec::Vec; #[derive(Copy, Clone)] pub enum Status { Success = 20, + RedirectPermanent = 31, PermanentFailure = 50, NotFound = 51, ProxyRequestRefused = 53, @@ -31,6 +32,10 @@ impl Header { } } +pub fn redirect_permanent(meta: &str) -> Header { + Header::new(Status::RedirectPermanent, meta) +} + pub fn permanent_failure() -> Header { Header::new(Status::PermanentFailure, "permanent failure") } @@ -46,3 +51,4 @@ pub fn proxy_request_refused() -> Header { pub fn bad_request() -> Header { Header::new(Status::BadRequest, "bad request") } +