agate

Simple gemini server for static files
git clone https://github.com/mbrubeck/agate.git
Log | Files | Refs | README

commit 8fbbec2b4bad05442f18463e6c10c4f4fa5642a2
parent 4ae9cd5826ab6deb1e38e1a82ea442499130ee60
Author: Johann150 <johann.galle@protonmail.com>
Date:   Mon,  8 Feb 2021 10:14:58 +0100

implement simple vhosts

closes #28

Diffstat:
MREADME.md | 8++++++++
Msrc/main.rs | 38+++++++++++++++++++++++---------------
2 files changed, 31 insertions(+), 15 deletions(-)

diff --git a/README.md b/README.md @@ -89,6 +89,14 @@ gone.gmi:52 This file is no longer here, sorry. Agate uses the `env_logger` crate and allows you to set the logging verbosity by setting the default `RUST_LOG` environment variable. For more information, please see the [documentation of `env_logger`]. +### Virtual Hosts + +Agate has basic support for virtual hosts. If you specify multiple `--hostname`s, Agate will look in a directory with the respective hostname within the content root directory. +For example if one of the hostnames is `example.com`, and the content root directory is set to the default `./content`, and `gemini://example.com/file.gmi` is requested, then Agate will look for `./content/example.com/file.gmi`. This behaviour is only enabled if multiple `--hostname`s are specified. +Agate does not support different certificates for different hostnames, you will have to use a single certificate for all domains (multi domain certificate). + +If you want to serve the same content for multiple domains, you can instead disable the hostname check by not specifying `--hostname`. In this case Agate will disregard a request's hostname apart from checking that there is one. + [Gemini]: https://gemini.circumlunar.space/ [Rust]: https://www.rust-lang.org/ [home]: gemini://gem.limpet.net/agate/ diff --git a/src/main.rs b/src/main.rs @@ -69,7 +69,7 @@ struct Args { content_dir: String, cert_file: String, key_file: String, - hostname: Option<Host>, + hostnames: Vec<Host>, language: Option<String>, silent: bool, serve_secret: bool, @@ -103,10 +103,10 @@ fn args() -> Result<Args> { "Address to listen on (multiple occurences possible, default 0.0.0.0:1965 and [::]:1965)", "IP:PORT", ); - opts.optopt( + opts.optmulti( "", "hostname", - "Domain name of this Gemini server (optional)", + "Domain name of this Gemini server (optional; simple vhosts if there are multiple occurences)", "NAME", ); opts.optopt( @@ -127,12 +127,12 @@ fn args() -> Result<Args> { let matches = opts.parse(&args[1..]).map_err(|f| f.to_string())?; if matches.opt_present("h") { let usage = opts.usage(&format!("Usage: {} [options]", &args[0])); - return Err(usage.into()) + return Err(usage.into()); + } + let mut hostnames = vec![]; + for s in matches.opt_strs("hostname") { + hostnames.push(Host::parse(&s)?); } - let hostname = match matches.opt_str("hostname") { - Some(s) => Some(Host::parse(&s)?), - None => None, - }; let mut addrs = vec![]; for i in matches.opt_strs("addr") { addrs.push(i.parse()?); @@ -148,7 +148,7 @@ fn args() -> Result<Args> { content_dir: check_path(matches.opt_get_default("content", "content".into())?)?, cert_file: check_path(matches.opt_get_default("cert", "cert.pem".into())?)?, key_file: check_path(matches.opt_get_default("key", "key.rsa".into())?)?, - hostname, + hostnames, language: matches.opt_str("lang"), silent: matches.opt_present("s"), serve_secret: matches.opt_present("serve-secret"), @@ -268,14 +268,16 @@ impl RequestHandle { 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 { + + if let Some(host) = url.host() { + // TODO: to_owned can be removed in next version of url https://github.com/servo/rust-url/pull/651 + if !ARGS.hostnames.is_empty() && !ARGS.hostnames.contains(&host.to_owned()) { return Err((53, "Proxy request refused")); } + } else { + return Err((59, "URL does not contain a host")); } + 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() { @@ -288,6 +290,12 @@ impl RequestHandle { /// 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 ARGS.hostnames.len() > 1 { + // basic vhosts, existence of host_str was checked by parse_request already + path.push(url.host_str().expect("no hostname")); + } + if let Some(segments) = url.path_segments() { for segment in segments { if !ARGS.serve_secret && segment.starts_with('.') { @@ -332,7 +340,7 @@ impl RequestHandle { Ok(file) => file, Err(e) => { self.send_header(51, "Not found, sorry.").await?; - return Err(e.into()) + return Err(e.into()); } };