agate

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

commit 4c2d33491d5df8744a202c9f170a1d7ca1bd4af4
parent d1d3f0cbb540b74bd45024edd7790fda7350e9c3
Author: Johann150 <johann@qwertqwefsday.eu>
Date:   Tue, 23 Mar 2021 23:28:16 +0100

add capability for multiple certificates (#40)


Diffstat:
MCHANGELOG.md | 6++++++
MCargo.toml | 2+-
MREADME.md | 44++++++++++++++++++++++++++++++++++++++------
Asrc/certificates.rs | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.rs | 50++++++++++++++------------------------------------
Rtests/data/cert.pem -> tests/data/.certificates/cert.pem | 0
Rtests/data/key.rsa -> tests/data/.certificates/key.rsa | 0
Rtests/data/key.rsa -> tests/data/cert_missing/key.rsa | 0
Rtests/data/cert.pem -> tests/data/key_missing/cert.pem | 0
Atests/data/multicert/create_certs.sh | 36++++++++++++++++++++++++++++++++++++
Atests/data/multicert/example.com/cert.pem | 28++++++++++++++++++++++++++++
Atests/data/multicert/example.com/key.rsa | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/data/multicert/example.org/cert.pem | 28++++++++++++++++++++++++++++
Atests/data/multicert/example.org/key.rsa | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/tests.rs | 247++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mtools/debian/install.sh | 6++++--
Atools/debian/uninstall.sh | 21+++++++++++++++++++++
17 files changed, 690 insertions(+), 111 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +* The ability to specify a certificate and key with `--cert` and `--key` respectively has been replaced with the `--certs` option. + Certificates are now stored in a special directory. To migrate to this version, the keys should be stored in the `.certificates` directory (or any other directory you specify). + This enables us to use multiple certificates for multiple domains. + ### Fixed +* Agate now requires the use of SNI by any connecting client. * All log lines are in the same format now: `<local ip>:<local port> <remote ip or dash> "<request>" <response status> "<response meta>" [error:<error>]` If the connection could not be established correctly (e.g. because of TLS errors), the status code `00` is used. diff --git a/Cargo.toml b/Cargo.toml @@ -24,11 +24,11 @@ rustls = "0.19.0" url = "2.2.1" glob = "0.3" configparser = "2.0" +webpki = "0.21.4" [dev-dependencies] gemini-fetch = "0.2.1" anyhow = "1.0" -webpki = "0.21.4" [profile.release] lto = true diff --git a/README.md b/README.md @@ -39,19 +39,20 @@ You can use the install script in the `tools` directory for the remaining steps If there is none, please consider contributing one to make it easier for less tech-savvy users! *** -2. Generate a self-signed TLS certificate and private key. For example, if you have OpenSSL 1.1 installed, you can use a command like the following. (Replace the hostname `example.com` with the address of your Gemini server.) +2. Generate a self-signed TLS certificate and private key in the `.certificates` directory. For example, if you have OpenSSL 1.1 installed, you can use a command like the following. (Replace the *two* occurences of `example.com` in the last line with the domain of your Gemini server.) ``` -openssl req -x509 -newkey rsa:4096 -keyout key.rsa -out cert.pem \ - -days 3650 -nodes -subj "/CN=example.com" +mkdir -p .certificates + +openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \ + -keyout .certificates/key.rsa -out .certificates/cert.pem \ + -subj "/CN=example.com" -addext "subjectAltName = DNS:example.com" ``` 3. Run the server. You can use the following arguments to specify the locations of the content directory, certificate and key files, IP address and port to listen on, host name to expect in request URLs, and default language code(s) to include in the MIME type for for text/gemini files: (Again replace the hostname `example.com` with the address of your Gemini server.) ``` agate --content path/to/content/ \ - --key key.rsa \ - --cert cert.pem \ --addr [::]:1965 \ --addr 0.0.0.0:1965 \ --hostname example.com \ @@ -107,7 +108,7 @@ Rules can overwrite other rules, so if a file is matched by multiple rules, the If a line violates the format or looks like case 3, but is incorrect, it might be ignored. You should check your logs. Please know that this configuration file is first read when a file from the respective directory is accessed. So no log messages after startup does not mean the `.meta` file is okay. Such a configuration file might look like this: -```text +``` # This line will be ignored. **/*.de.gmi: ;lang=de nl/**/*.gmi: ;lang=nl @@ -142,6 +143,33 @@ Agate does not support different certificates for different hostnames, you will 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. +### Multiple certificates + +Agate has support for using multiple certificates with the `--certs` option. Agate will thus always require that a client uses SNI, which should not be a problem since the Gemini specification also requires SNI to be used. + +Certificates are by default stored in the `.certificates` directory. This is a hidden directory for the purpose that uncautious people may set the content root directory to the currrent director which may also contain the certificates directory. In this case, the certificates and private keys would still be hidden. The certificates are only loaded when Agate is started and are not reloaded while running. The certificates directory may directly contain a key and certificate pair, this is the default pair used if no other matching keys are present. The certificates directory may also contain subdirectories for specific domains, for example a folder for `example.org` and `portal.example.org`. Note that the subfolders for subdomains (like `portal.example.org`) should not be inside other subfolders but directly in the certificates directory. Agate will select the certificate/key pair whose name matches most closely. For example take the following directory structure: + +``` +.certificates +|-- cert.pem (1) +|-- key.rsa (1) +|-- example.org +| |-- cert.pem (2) +| `-- key.rsa (2) +`-- portal.example.org + |-- cert.pem (3) + `-- key.rsa (3) +``` + +This would be understood like this: +* The certificate/key pair (1) would be used for the entire domain tree (exceptions below). +* The certificate/key pair (2) would be used for the entire domain tree of `example.org`, so also including subdomains like `secret.example.org`. It overrides the pair (1) for this subtree (exceptions below). +* The certificate/key pair (3) would be used for the entire domain tree of `portal.example.org`, so also inclduding subdomains like `test.portal.example.org`. It overrides the pairs (1) and (2) for this subtree. + +Using a directory named just `.` causes undefined behaviour as this would have the same meaning as the top level certificate/key pair (pair (1) in the example above). + +The files for a certificate/key pair have to be named `cert.pem` and `key.rsa` respectively. The certificate has to be a X.509 certificate in a PEM file and has to include a subject alt name of the domain name. The private key has to be in PKCS#8 format. For an example of how to create such certificates see Installation and Setup, step 2. + ## Logging All requests will be logged using this format: @@ -152,6 +180,10 @@ The "error:" part will only be logged if an error occurred. This should only be There are some lines apart from these that might occur in logs depending on the selected log level. For example the initial "Listening on..." line or information about listing a particular directory. +## Security considerations + +If you want to run agate on a multi-user system, you should be aware that all certificate and key data is loaded into memory and stored there until the server stops. Since the memory is also not explicitly overwritten or zeroed after use, the sensitive data might stay in memory after the server has terminated. + [Gemini]: https://gemini.circumlunar.space/ [Rust]: https://www.rust-lang.org/ [home]: gemini://qwertqwefsday.eu/agate.gmi diff --git a/src/certificates.rs b/src/certificates.rs @@ -0,0 +1,229 @@ +use { + rustls::{ + internal::pemfile::{certs, pkcs8_private_keys}, + sign::{CertifiedKey, RSASigningKey}, + ResolvesServerCert, + }, + std::{ + ffi::OsStr, + fmt::{Display, Formatter}, + fs::File, + io::BufReader, + path::Path, + sync::Arc, + }, + webpki::DNSNameRef, +}; + +/// A struct that holds all loaded certificates and the respective domain +/// names. +pub(crate) struct CertStore { + /// Stores the certificates and the domains they apply to, sorted by domain + /// names, longest matches first + certs: Vec<(String, CertifiedKey)>, +} + +static CERT_FILE_NAME: &str = "cert.pem"; +static KEY_FILE_NAME: &str = "key.rsa"; + +#[derive(Debug)] +pub enum CertLoadError { + /// could not access the certificate root directory + NoReadCertDir, + /// the specified domain name cannot be processed correctly + BadDomain(String), + /// The key file for the given domain does not contain any suitable keys. + NoKeys(String), + /// the key file for the specified domain is bad (e.g. does not contain a + /// key or is invalid) + BadKey(String), + /// The certificate file for the specified domain is bad (e.g. invalid) + /// The second parameter is the error message. + BadCert(String, String), + /// the key file for the specified domain is missing (but a certificate + /// file was present) + MissingKey(String), + /// the certificate file for the specified domain is missing (but a key + /// file was present) + MissingCert(String), + /// neither a key file nor a certificate file were present for the given + /// domain (but a folder was present) + EmptyDomain(String), +} + +impl Display for CertLoadError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::NoReadCertDir => write!(f, "Could not read from certificate directory."), + Self::BadDomain(domain) if !domain.is_ascii() => write!( + f, + "The domain name {} cannot be processed, it must be punycoded.", + domain + ), + Self::BadDomain(domain) => write!(f, "The domain name {} cannot be processed.", domain), + Self::NoKeys(domain) => write!( + f, + "The key file for {} does not contain any suitable key.", + domain + ), + Self::BadKey(domain) => write!(f, "The key file for {} is malformed.", domain), + Self::BadCert(domain, e) => { + write!(f, "The certificate file for {} is malformed: {}", domain, e) + } + Self::MissingKey(domain) => write!(f, "The key file for {} is missing.", domain), + Self::MissingCert(domain) => { + write!(f, "The certificate file for {} is missing.", domain) + } + Self::EmptyDomain(domain) => write!( + f, + "A folder for {} exists, but there is no certificate or key file.", + domain + ), + } + } +} + +impl std::error::Error for CertLoadError {} + +fn load_domain(certs_dir: &Path, domain: String) -> Result<CertifiedKey, CertLoadError> { + let mut path = certs_dir.to_path_buf(); + path.push(&domain); + // load certificate from file + path.push(CERT_FILE_NAME); + if !path.is_file() { + return Err(if !path.with_file_name(KEY_FILE_NAME).is_file() { + CertLoadError::EmptyDomain(domain) + } else { + CertLoadError::MissingCert(domain) + }); + } + + let cert_chain = match certs(&mut BufReader::new(File::open(&path).unwrap())) { + Ok(cert) => cert, + Err(()) => return Err(CertLoadError::BadCert(domain, String::new())), + }; + + // load key from file + path.set_file_name(KEY_FILE_NAME); + if !path.is_file() { + return Err(CertLoadError::MissingKey(domain)); + } + let key = match pkcs8_private_keys(&mut BufReader::new(File::open(&path).unwrap())) { + Ok(mut keys) if !keys.is_empty() => keys.remove(0), + Ok(_) => return Err(CertLoadError::NoKeys(domain)), + Err(()) => return Err(CertLoadError::BadKey(domain)), + }; + + // transform key to correct format + let key = match RSASigningKey::new(&key) { + Ok(key) => key, + Err(()) => return Err(CertLoadError::BadKey(domain)), + }; + Ok(CertifiedKey::new(cert_chain, Arc::new(Box::new(key)))) +} + +impl CertStore { + /// Load certificates from a certificate directory. + /// Certificates should be stored in a folder for each hostname, for example + /// the certificate and key for `example.com` should be in the files + /// `certs_dir/example.com/{cert.pem,key.rsa}` respectively. + /// + /// If there are `cert.pem` and `key.rsa` directly in certs_dir, these will be + /// loaded as default certificates. + pub fn load_from(certs_dir: &Path) -> Result<Self, CertLoadError> { + // load all certificates from directories + let mut certs = vec![]; + + // Try to load fallback certificate and key directly from the top level + // certificate directory. It will be loaded as the `.` domain. + match load_domain(certs_dir, ".".to_string()) { + Err(CertLoadError::EmptyDomain(_)) => { /* there are no fallback keys */ } + Err(CertLoadError::NoReadCertDir) => unreachable!(), + Err(CertLoadError::BadDomain(_)) => unreachable!(), + Err(CertLoadError::NoKeys(_)) => { + return Err(CertLoadError::NoKeys("fallback".to_string())) + } + Err(CertLoadError::BadKey(_)) => { + return Err(CertLoadError::BadKey("fallback".to_string())) + } + Err(CertLoadError::BadCert(_, e)) => { + return Err(CertLoadError::BadCert("fallback".to_string(), e)) + } + Err(CertLoadError::MissingKey(_)) => { + return Err(CertLoadError::MissingKey("fallback".to_string())) + } + Err(CertLoadError::MissingCert(_)) => { + return Err(CertLoadError::MissingCert("fallback".to_string())) + } + // For the fallback keys there is no domain name to verify them + // against, so we can skip that step and only have to do it for the + // other keys below. + Ok(key) => certs.push((String::new(), key)), + } + + for file in certs_dir + .read_dir() + .or(Err(CertLoadError::NoReadCertDir))? + .filter_map(Result::ok) + .filter(|x| x.path().is_dir()) + { + let path = file.path(); + + // the filename should be the domain name + let filename = path + .file_name() + .and_then(OsStr::to_str) + .unwrap() + .to_string(); + + let dns_name = match DNSNameRef::try_from_ascii_str(&filename) { + Ok(name) => name, + Err(_) => return Err(CertLoadError::BadDomain(filename)), + }; + + let key = load_domain(certs_dir, filename.clone())?; + key.cross_check_end_entity_cert(Some(dns_name)) + .map_err(|e| CertLoadError::BadCert(filename.clone(), e.to_string()))?; + + certs.push((filename, key)); + } + + certs.sort_unstable_by(|(a, _), (b, _)| { + // Try to match as many domain segments as possible. If one is a + // substring of the other, the `zip` will only compare the smaller + // length of either a or b and the for loop will not decide. + for (a_part, b_part) in a.split('.').rev().zip(b.split('.').rev()) { + if a_part != b_part { + // Here we have to make sure that the empty string will + // always be sorted to the end, so we reverse the usual + // ordering of str. + return a_part.cmp(b_part).reverse(); + } + } + // Sort longer domains first. + a.len().cmp(&b.len()).reverse() + }); + + Ok(Self { certs }) + } +} + +impl ResolvesServerCert for CertStore { + fn resolve(&self, client_hello: rustls::ClientHello<'_>) -> Option<CertifiedKey> { + if let Some(name) = client_hello.server_name() { + let name: &str = name.into(); + // The certificate list is sorted so the longest match will always + // appear first. We have to find the first that is either this + // domain or a parent domain of the current one. + self.certs + .iter() + .find(|(s, _)| name.ends_with(s)) + // only the key is interesting + .map(|(_, k)| k) + .cloned() + } else { + // This kind of resolver requires SNI. + None + } + } +} diff --git a/src/main.rs b/src/main.rs @@ -1,22 +1,18 @@ #![forbid(unsafe_code)] +mod certificates; mod metadata; use metadata::{FileOptions, PresetMeta}; use { once_cell::sync::Lazy, percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS}, - rustls::{ - internal::pemfile::{certs, pkcs8_private_keys}, - Certificate, NoClientAuth, PrivateKey, ServerConfig, - }, + rustls::{NoClientAuth, ServerConfig}, std::{ borrow::Cow, error::Error, ffi::OsStr, fmt::Write, - fs::File, - io::BufReader, net::SocketAddr, path::{Path, PathBuf}, sync::Arc, @@ -80,8 +76,7 @@ static ARGS: Lazy<Args> = Lazy::new(|| { struct Args { addrs: Vec<SocketAddr>, content_dir: PathBuf, - cert_chain: Vec<Certificate>, - key: PrivateKey, + certs: Arc<certificates::CertStore>, hostnames: Vec<Host>, language: Option<String>, silent: bool, @@ -102,15 +97,9 @@ fn args() -> Result<Args> { ); opts.optopt( "", - "cert", - "TLS certificate PEM file (default ./cert.pem)", - "FILE", - ); - opts.optopt( - "", - "key", - "PKCS8 private key file (default ./key.rsa)", - "FILE", + "certs", + "folder for certificate files (default ./.certificates/)", + "DIR", ); opts.optmulti( "", @@ -174,25 +163,14 @@ fn args() -> Result<Args> { ]; } - let cert_file = File::open(check_path( - matches.opt_get_default("cert", "cert.pem".into())?, - )?)?; - let cert_chain = certs(&mut BufReader::new(cert_file)).or(Err("bad cert"))?; - - let key_file = File::open(check_path( - matches.opt_get_default("key", "key.rsa".into())?, - )?)?; - let key = pkcs8_private_keys(&mut BufReader::new(key_file)) - .or(Err("bad key file"))? - .drain(..) - .next() - .ok_or("no keys found")?; + let certs = Arc::new(certificates::CertStore::load_from(&check_path( + matches.opt_get_default("certs", ".certificates".into())?, + )?)?); Ok(Args { addrs, content_dir: check_path(matches.opt_get_default("content", "content".into())?)?, - cert_chain, - key, + certs, hostnames, language: matches.opt_str("lang"), silent: matches.opt_present("s"), @@ -213,15 +191,15 @@ fn check_path(s: String) -> Result<PathBuf, String> { } /// TLS configuration. -static TLS: Lazy<TlsAcceptor> = Lazy::new(|| acceptor().unwrap()); +static TLS: Lazy<TlsAcceptor> = Lazy::new(acceptor); -fn acceptor() -> Result<TlsAcceptor> { +fn acceptor() -> TlsAcceptor { let mut config = ServerConfig::new(NoClientAuth::new()); if ARGS.only_tls13 { config.versions = vec![rustls::ProtocolVersion::TLSv1_3]; } - config.set_single_cert(ARGS.cert_chain.clone(), ARGS.key.clone())?; - Ok(TlsAcceptor::from(Arc::new(config))) + config.cert_resolver = ARGS.certs.clone(); + TlsAcceptor::from(Arc::new(config)) } struct RequestHandle { diff --git a/tests/data/cert.pem b/tests/data/.certificates/cert.pem diff --git a/tests/data/key.rsa b/tests/data/.certificates/key.rsa diff --git a/tests/data/key.rsa b/tests/data/cert_missing/key.rsa diff --git a/tests/data/cert.pem b/tests/data/key_missing/cert.pem diff --git a/tests/data/multicert/create_certs.sh b/tests/data/multicert/create_certs.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +mkdir -p example.com example.org + +for domain in "example.com" "example.org" +do +# create private key +openssl genpkey -out $domain/key.rsa -algorithm RSA -pkeyopt rsa_keygen_bits:4096 + +# create config file: +# the generated certificates must not be CA-capable, otherwise rustls complains +cat >openssl.conf <<EOT +[req] +default_bits = 4096 +distinguished_name = req_distinguished_name +req_extensions = req_ext +prompt = no + +[v3_ca] +basicConstraints = critical, CA:false + +[req_distinguished_name] +commonName = $domain + +[req_ext] +subjectAltName = DNS:$domain +EOT + +openssl req -new -sha256 -out request.csr -key $domain/key.rsa -config openssl.conf + +openssl x509 -req -sha256 -days 3650 -in request.csr -out $domain/cert.pem \ + -extensions req_ext -extfile openssl.conf -signkey $domain/key.rsa +done + +# clean up +rm openssl.conf request.csr diff --git a/tests/data/multicert/example.com/cert.pem b/tests/data/multicert/example.com/cert.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE1DCCArygAwIBAgIUDZjgSq0hJCJPEjCho3BZxrR0S2AwDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjEwMzExMjEyMDA5WhcNMzEw +MzA5MjEyMDA5WjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAKCvZP1hCE3kzcjHrRpmsB22qmz1zq5AyiviJfKA +bL/CoPxiH/t53ZxtKH2SKwF9l3YpMc+pVP0BsgNOXIaQyX0cS5Nq0mgrPnShGQlq +8M/8DzqKaoOYBBxxODaw25BWTq0ljWj0Sz3ksa91ayxK/whfK1rmNzKjyIYzvbdO +j6qIEXYFHyFqzLXOpunj2s+nZKwKxX0GWkM2qB8mwLHOq0JNufYDkzPvyiZPru1N +23SoMiCEGu0uRjvHalg8ehuRito20UeD1HHfy6Gr/S1nzNumRTD0iMpySNMpz/cf +d6/9u/C7pC73qc4hfXerS02ffwEm+ulQEDwZ7QiS8CbSIfkLyG93O1LXr6fhqLPi +505wCia9V2Iq93zlGUBP/zDqaMoISwohKFvrvpYlQG4LI8b0j+To9YOFwSsekPZf +4rfOLIBwPsE8knAycnN7D2GYXeuo3tMh4lPKgV7IoG6CUzRLoZ8pGIZnjVqO0I5R +1w5CU48RqlL+tsYdNSphBBKPLnUro9WOG9UIKo3C0ccij86vLU/9L2gGT//oTIUN +suR9e8eoK8Yos+fef3+vOWgbeK+Isi09Hq70j0vatACtWBE6Z7ROhW2ytiD3LD5g +MN7Yk5mEV6t/y0dhDffIjvBS4tS+idWr2jvt0KgRE2JzV6iMAg7C0fdCYg1WuQer +1GWZAgMBAAGjGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEB +CwUAA4ICAQAmfm4cDU6SVxzMgInZcwPIW7NTSzly7WzCHhaGYqkgCrEmu+2DEKzq +c8lIe5SBHQCs6y8LRdNAJ9I3dMnXw9x97/lrKwg9+C6xnjqcDYRG3DzGsAz0OF1A +AWboIdnPSnHeicmDK0C4i7/VghJmpozcOF0HCnfo0zDhhyCOAhv7fT3TCk/kDCJd +uQA2EJRwnvrfbuhddFzpdkoZJQeSsPmJvTqQoAap8tKxmrkDo6GZSVRMFelCL5e0 +6nQQQfb5Qo97/lHHXfP9JycYc+AqRzvuc6bZiEJG2FeWJb3QCOTULfAbpksem6WA +tNtJ4Vv/snX0Wcy6zmTxXH0HbFmXM2Xdd2adiZVQU++Ck8jrzxHUtiG08E0VnXvQ +nABixB7aWQkDWCCR3k4QBBsGSPCIQ15d8RsC1HGxTs7zmGE2ic3rWEgzGkpaIvIN +aai5cmRey/mRIauWrG+juOHemSQ4WlLeSnDixcrlQP63WkKf9j4yKgUjbRULT/LO +QjH4n7ckoj3d+CGIvlQN94tn0xM38iya+43ytC0LUFNYd+BjxkFZI5kKaFh6vYmZ +tKjrL496Jd/L3knjIRTPh1L6TpINSYFhMqzXHFJNsRViI/4YtiilJlo5NX18ejMO +mYLcKU4xS9u7rmBpQyIFmwlLCYAh8X1yKAvezLzeGy0x9+iramaOPg== +-----END CERTIFICATE----- diff --git a/tests/data/multicert/example.com/key.rsa b/tests/data/multicert/example.com/key.rsa @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCgr2T9YQhN5M3I +x60aZrAdtqps9c6uQMor4iXygGy/wqD8Yh/7ed2cbSh9kisBfZd2KTHPqVT9AbID +TlyGkMl9HEuTatJoKz50oRkJavDP/A86imqDmAQccTg2sNuQVk6tJY1o9Es95LGv +dWssSv8IXyta5jcyo8iGM723To+qiBF2BR8hasy1zqbp49rPp2SsCsV9BlpDNqgf +JsCxzqtCTbn2A5Mz78omT67tTdt0qDIghBrtLkY7x2pYPHobkYraNtFHg9Rx38uh +q/0tZ8zbpkUw9IjKckjTKc/3H3ev/bvwu6Qu96nOIX13q0tNn38BJvrpUBA8Ge0I +kvAm0iH5C8hvdztS16+n4aiz4udOcAomvVdiKvd85RlAT/8w6mjKCEsKIShb676W +JUBuCyPG9I/k6PWDhcErHpD2X+K3ziyAcD7BPJJwMnJzew9hmF3rqN7TIeJTyoFe +yKBuglM0S6GfKRiGZ41ajtCOUdcOQlOPEapS/rbGHTUqYQQSjy51K6PVjhvVCCqN +wtHHIo/Ory1P/S9oBk//6EyFDbLkfXvHqCvGKLPn3n9/rzloG3iviLItPR6u9I9L +2rQArVgROme0ToVtsrYg9yw+YDDe2JOZhFerf8tHYQ33yI7wUuLUvonVq9o77dCo +ERNic1eojAIOwtH3QmINVrkHq9RlmQIDAQABAoICAGUFwJV4ktL+Hc60kwU9OE6G +EGHOrLFrNHAgj0EGMtjg0Xu7aWYeeRCmpEVGR1l5j2cPgSyQxkkG7tcbRhqoHrVU +u8Mj7sLlJTAINIhyPpJUY3KnoU24niUPnYrs6C23xWEgceZhaIiyJnAsf0Pqpqqp +wsU0ZdGlnSWalBUSBErvnyK3F5pX3foTwWbdBS12jVmIsB7phogpbcuf/pgLWiqm +WVrtZnfJsysg/9ZcE7QlJtbAl3k0lZ1xw09UPmTkvQpyWmL+4+rwC8NKMTOBxg72 +WxvrMbEt5tEzwXcZxpLUEHvKTO/mb1CUR6CcBgz4UM31ptxWpM5UcmzojKmrhQVi +cUklkwua4ugYhGPd3307wChD4xiHpSw/r/zEVQR3OZVYDHMF2QGvrCe5ZPgxnZvS +UZLsb1vjnTYxhUaIfWRDZATtkrCxadv5qE7NYQktd/hBQeWw2A98HHaSgUbI/Z2C +Ts4ZJHlAdxj/m6Sovu7oiZMQzMpzM88JYK7tQL3hCCX2B9Nopqml/C92sj8C0Nn9 +QiIG9X9tM8GDVPWIo+HZ0Uk5W/nuDBO+4rN2nLrULoIv9mgvfA5woVP2yBBZp+N9 +pjL080SOuuclvsnfGBPBJ/HSnUyFpiXrmnVFVkv+yLzZxTrwsCrZwllRgvd3u6iY +c/SaYaC+wkbcoE3xOtktAoIBAQDUamxIP+ajY5fu6KNXsHS6Rbg5oREpBUAdeTSQ +poWI15NgIqAVws2OtrMKpcFrVbDtIdaPEXk/lgk7uAIVC3tFEf7E3mJkxEI1msA3 +JA/BV1JiryImwAnb6NCoBkNbp4YjYRdCQJz0Q726TyPYBXs3Xk1UYRY4dzgFCxnr +iIVEeTzJkee1ANUvPWkuHyKdL2chz1TetHR6VhLdgP5u2QxVVnv/YrWT8+3SAqbB +GbNbXMewsBgNp+CCJL3dazOQD4kJyx5t6iR2FbzE6SuOTQiZG4I8lRnHMdW0cVjp +ZhyLnoFqz+SzfkudnCbc0N5Fx3Xn9B4plLFOI6eGbcy/9cB7AoIBAQDBp7cawD3M +fBvOdmY1fUaU1dJ6OPOJ6pu8bxCcP+rlg/BOyhLPaJ2pgmOeisid9GmFirDf2CVr +YnL7T9k8Twb/j78Z7eEZz3NvV8ANzC0MfsGgiW2ThNoc8j5rT1ke08B/t/0GpRkx +RugygzdQf2IGQG6aJtPVovhTU9uyhC/3NZcIbzRIkZmktk8kP316LNYuxXPqE00l ++VmSn645vfAnYXBALgOaBlqxxFChbXq5+XuIjQH013PJaGhMwBFeEo0jb34xI9ER +iRleEj2ZG8GQHH85AMlo586MwTK4ipH/1tVm9jK31cM7MOaNwiJkKEfwVGpGTg90 +yyCv1dxEXvf7AoIBAQDMDAtGgDPi4mnxswIt2zDWOuEUYvfkCsojRepLxdrisAs/ +PyO+o6nonPJymPWrUN6rfGTqfCOYBF2MQ1+kranVmMq+fM3R9IGRkr1werCzzlky +uP+6b6FI4WWG8rVD1zJQzBSWrRDYyDX6Qcmx2toZPvpTwwugZE2o8pgMnNFADKJr +E0CcrFcdkQV3q6sJiZ6taMgjQv/dANAQfbhr7Q4e7/wfQMgifyEGK0valQCpFAAz +Z4VDoO9WtUq55x/aFEJU6QyrE0/BK3JxSXdws+k9gqJh5eykX+fk9Tkuw8tKB5JU +c65DCmBC39ypI+9Q4qENl4Bd+xszb6aeyNz1zXH/AoIBACXsVB02/GMpAsEByq46 +5DGNVfR9ZqPhf7H9BgGzOqrLlam4RMq9L/LcB+oqP3M/Q9LVACI1z84hr2arkl0P +FM3DNqc7QFOvnml1g7SwATprMDvh7cVvxM7aWYLmPQueaBoay8AbYL2Xpy0NKS3o +ZCfZQk+Jvv4dNggLagChhkshAXyzWkfDy5TH5uOwU0Azu5XZMQPr17XSCMp/3ryM +B5WOrU7ENAxbpjMdwLR8HgaBZsGs628pKhGNEq/FBSGo/F6uHMY+v1hxwrf7Vni/ +SL6R9hARqV+T1Y0W4HnnGQRC6/OHzxLVF7BluSCVneqDQOM9hLpT2w8CIFqOxN3W +wzUCggEAPbS/XHzASitRWD9oYA3y+vo/BJW0hkyVaCV1uni/0/oSFZ9NrKV8iXn5 +GYXl/HCN9SlLJBV08tKb9w5evOqC/nD4B2Ape0CXvhXDDLxTDAAPyAtEWXIi6moX +7HOrttpABaUB1qK03UVsb8S23Q8mDBlv0chsbNBEvdMtl5Lrw/u4rvHH4Kbv8N9x +89pnI4DwGbcj7y+AYr+QN4WDgcAlAvjsNq6qJFlcr9SPeNeSojHHfW3ipEE5YWG+ +V+VsqdeU4E4svwYdqDejETFrrykpLkf1ISTV4xs6b0Wv94RYUEZ8FXQYj6AZpIb1 +q+T6LY6tbtpB+11HBnD4I79WCYf4DA== +-----END PRIVATE KEY----- diff --git a/tests/data/multicert/example.org/cert.pem b/tests/data/multicert/example.org/cert.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE1DCCArygAwIBAgIUVpgbY7cDU3z0VcZgYV14fgF0qcswDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5vcmcwHhcNMjEwMzExMjEyMDA5WhcNMzEw +MzA5MjEyMDA5WjAWMRQwEgYDVQQDDAtleGFtcGxlLm9yZzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK0MOqzesRj6psy6XMcQz72jUCiaKTP/bamWyaDx +AcM0S0McCl0dpXpeID6+i4l/fQKP3OjJIH7pC55mRxjJZVWdhoSdvW/5/dli/uxp +o5pHH6a9ktnvOXHgw8kkbp3DVvu4JpJvuoKd67dll931sO5nVCCTJkNwg+/QhP+R +I43JuZKZVOQpsY7f3sZl6ASJuThgifxlCF0mrsabx+Yk0Ug6SLAIson/OHeGaCiB +Xlj0+kCxFGWsFHOTy55HHJKuGvtadc/MJdGrVCUUHjq3Les8wQXuzQUG+8QJOm2h +tVkWXJRZtumUUHVi+rqWRRBO1yLih5r/Yv8qN5Ba+c4AQcT0uDyKd3ar6SjRgWDU +ZoJbA0Ar4ydGlkQu9KZVY2Z20Hx+R+xKmzz6BwnKS3X8BnOYhprwOlZNY6qCpygq +wiDtMJfI210kDeXSuURXfWvrrOQRhO4fQzmrg4o7DsF2WOU9kY7fgxY9ObTUTp24 +j2g2p/I5S0JHRpmQhYIXU1UErbb3JqUGrOueUf5fdIu0VWWjXAbGsl4WkzXZGBbS +VkKQ4oArLa7BFbJkC/+z8ttTanIyPI5SKNDGXWRRR5fB/r9t5I6X5Ep0+c+zE4so +Jj7SKv6ygvnKoHckuaBCaNJB4Pq9fv7IFZzf5bo3CGSlJi1NTtAB32I+k2ZGqVBX +Yh3HAgMBAAGjGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUub3JnMA0GCSqGSIb3DQEB +CwUAA4ICAQBBG6f0T9nQfHV0G0bmTPUVbAVJuYtiDBSBOP/uactOWsmw65GPq1k8 +UAYmqYfm5kWl8tsjNWzt8mfm+mQVVGp0c+OM2ltAQ03GPpZzkZLtHpTiOZ/1mcP3 +D3kInj5jUOp/8k7ZWawsOhJcKP5n8ZQYTjKPvo3lD3lDXQVNyHtO/2IwjFeiiD1F +fx1ZQ2ohLD9f88KkUyYrujzJyCvjyTOF195PrJBLPlho40R+A0AJTm0VBTU3I33l +G+8twaVzcspGKFOwpwuLpFXPby6H1nrmc7/En/nzYniSmCjTU/dOsHVZRHxB0T7t +WjsPrDA6SHtmvic0Avk3dxEKRHyzSwiL57lSeZnqovm06DfbGyL7rb7VIrAvNE5I +eZUforj6QsNUQENb46LqgQkQ/vStGZ7jHFM81PeW/r/J0XmjU/SG2y8op1gT98Qz +jThR5uA2fw2BTIulQq4awQC0K5uXlmMuiliip2MleDp67y9q7z9oxahY/umdw4ki +8I76mZKe+9QipBVPilLWZy9lJyPvZgpr3oZMyHKWUR1D30hVeFpRG5kyvSt115GC +EHXAUbp//AlrWNBLoPLeXoqrZNUfdDiUaQ1wqB7OgUkB1dCzp0XsSTGo6ryDyjd7 +d39ID7e3QFP8zCuFEcmB8AmE09zAGeAZXFMA+xXWV3H7DQIwVSkOwA== +-----END CERTIFICATE----- diff --git a/tests/data/multicert/example.org/key.rsa b/tests/data/multicert/example.org/key.rsa @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +BVksLv0EVzq2MyO+yTR1M8Y+0KBK5dx8 +-----END PRIVATE KEY----- diff --git a/tests/tests.rs b/tests/tests.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use gemini_fetch::{Header, Page, Status}; use std::io::Read; use std::net::{SocketAddr, ToSocketAddrs}; @@ -19,6 +20,8 @@ fn addr(port: u16) -> SocketAddr { struct Server { server: std::process::Child, buf: u8, + // is set when output is collected by stop() + output: Option<Result<(), String>>, } impl Server { @@ -44,17 +47,20 @@ impl Server { Self { server, buf: buffer[0], + output: None, } } -} -impl Drop for Server { - fn drop(&mut self) { - // try to stop the server again - match self.server.try_wait() { - Err(e) => panic!("cannot access orchestrated program: {:?}", e), + pub fn stop(&mut self) -> Result<(), String> { + // try to stop the server + if let Some(output) = self.output.clone() { + return output; + } + + self.output = Some(match self.server.try_wait() { + Err(e) => Err(format!("cannot access orchestrated program: {:?}", e)), // everything fine, still running as expected, kill it now - Ok(None) => self.server.kill().unwrap(), + Ok(None) => Ok(self.server.kill().unwrap()), Ok(Some(_)) => { // forward stderr so we have a chance to understand the problem let buffer = std::iter::once(Ok(self.buf)) @@ -62,23 +68,36 @@ impl Drop for Server { .collect::<Result<Vec<u8>, _>>() .unwrap(); - eprintln!("{}", String::from_utf8_lossy(&buffer)); - // make the test fail - panic!("program had crashed"); + Err(String::from_utf8_lossy(&buffer).into_owned()) } + }); + return self.output.clone().unwrap(); + } +} + +impl Drop for Server { + fn drop(&mut self) { + if self.output.is_none() && !std::thread::panicking() { + // a potential error message was not yet handled + self.stop().unwrap(); + } else if self.output.is_some() { + // server was already stopped + } else { + // we are panicking and a potential error was not handled + self.stop().unwrap_or_else(|e| eprintln!("{:?}", e)); } } } fn get(args: &[&str], addr: SocketAddr, url: &str) -> Result<Page, anyhow::Error> { - let _server = Server::new(args); + let mut server = Server::new(args); // actually perform the request let page = tokio::runtime::Runtime::new() .unwrap() .block_on(async { Page::fetch_from(&Url::parse(url).unwrap(), addr, None).await }); - page + server.stop().map_err(|e| anyhow!(e)).and(page) } #[test] @@ -305,65 +324,161 @@ fn explicit_tls_version() { ) } -#[test] -/// - simple vhosts are enabled when multiple hostnames are supplied -/// - the vhosts access the correct files -fn vhosts_example_com() { - let page = get( - &[ - "--addr", - "[::]:1977", - "--hostname", - "example.com", - "--hostname", - "example.org", - ], - addr(1977), - "gemini://example.com/", - ) - .expect("could not get page"); - - assert_eq!(page.header.status, Status::Success); +mod vhosts { + use super::*; + + #[test] + /// - simple vhosts are enabled when multiple hostnames are supplied + /// - the vhosts access the correct files + fn example_com() { + let page = get( + &[ + "--addr", + "[::]:1977", + "--hostname", + "example.com", + "--hostname", + "example.org", + ], + addr(1977), + "gemini://example.com/", + ) + .expect("could not get page"); + + assert_eq!(page.header.status, Status::Success); + + assert_eq!( + page.body, + Some( + std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/content/example.com/index.gmi" + )) + .unwrap() + ) + ); + } - assert_eq!( - page.body, - Some( - std::fs::read_to_string(concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/data/content/example.com/index.gmi" - )) - .unwrap() + #[test] + /// - the vhosts access the correct files + fn example_org() { + let page = get( + &[ + "--addr", + "[::]:1978", + "--hostname", + "example.com", + "--hostname", + "example.org", + ], + addr(1978), + "gemini://example.org/", ) - ); + .expect("could not get page"); + + assert_eq!(page.header.status, Status::Success); + + assert_eq!( + page.body, + Some( + std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/content/example.org/index.gmi" + )) + .unwrap() + ) + ); + } } -#[test] -/// - the vhosts access the correct files -fn vhosts_example_org() { - let page = get( - &[ - "--addr", - "[::]:1978", - "--hostname", - "example.com", - "--hostname", - "example.org", - ], - addr(1978), - "gemini://example.org/", - ) - .expect("could not get page"); +mod multicert { + use super::*; - assert_eq!(page.header.status, Status::Success); + #[test] + fn cert_missing() { + let mut server = Server::new(&["--addr", "[::]:1979", "--certs", "cert_missing"]); - assert_eq!( - page.body, - Some( - std::fs::read_to_string(concat!( + // wait for the server to stop, it should crash + let _ = server.server.wait(); + + assert!(server + .stop() + .unwrap_err() + .to_string() + .contains("certificate file for fallback is missing")); + } + + #[test] + fn key_missing() { + let mut server = Server::new(&["--addr", "[::]:1980", "--certs", "key_missing"]); + + // wait for the server to stop, it should crash + let _ = server.server.wait(); + + assert!(server + .stop() + .unwrap_err() + .to_string() + .contains("key file for fallback is missing")); + } + + #[test] + fn example_com() { + use rustls::ClientSession; + use std::io::{Cursor, Write}; + use std::net::TcpStream; + + let mut server = Server::new(&["--addr", "[::]:1981", "--certs", "multicert"]); + + let mut config = rustls::ClientConfig::new(); + config + .root_store + .add_pem_file(&mut Cursor::new(include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), - "/tests/data/content/example.org/index.gmi" - )) - .unwrap() - ) - ); + "/tests/data/multicert/example.com/cert.pem" + )))) + .unwrap(); + + let dns_name = webpki::DNSNameRef::try_from_ascii_str("example.com").unwrap(); + let mut session = ClientSession::new(&std::sync::Arc::new(config), dns_name); + let mut tcp = TcpStream::connect(addr(1981)).unwrap(); + let mut tls = rustls::Stream::new(&mut session, &mut tcp); + + write!(tls, "gemini://example.com/\r\n").unwrap(); + + let mut buf = [0; 10]; + tls.read(&mut buf).unwrap(); + + server.stop().unwrap() + } + + #[test] + fn example_org() { + use rustls::ClientSession; + use std::io::{Cursor, Write}; + use std::net::TcpStream; + + let mut server = Server::new(&["--addr", "[::]:1982", "--certs", "multicert"]); + + let mut config = rustls::ClientConfig::new(); + config + .root_store + .add_pem_file(&mut Cursor::new(include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/multicert/example.org/cert.pem" + )))) + .unwrap(); + + let dns_name = webpki::DNSNameRef::try_from_ascii_str("example.org").unwrap(); + let mut session = ClientSession::new(&std::sync::Arc::new(config), dns_name); + let mut tcp = TcpStream::connect(addr(1982)).unwrap(); + let mut tls = rustls::Stream::new(&mut session, &mut tcp); + + write!(tls, "gemini://example.org/\r\n").unwrap(); + + let mut buf = [0; 10]; + tls.read(&mut buf).unwrap(); + + server.stop().unwrap() + } } diff --git a/tools/debian/install.sh b/tools/debian/install.sh @@ -66,8 +66,10 @@ cp geminilogs /etc/logrotate.d/ echo "setting up content files..." mkdir -p /srv/gemini/content -openssl req -x509 -newkey rsa:4096 -keyout /srv/gemini/key.rsa -out /srv/gemini/cert.pem \ - -days 3650 -nodes -subj "/CN=$(uname -n)" +mkdir -p /srv/gemini/.certificates +openssl req -x509 -newkey rsa:4096 -keyout /srv/gemini/.certificates/key.rsa \ + -out /srv/gemini/.certificates/cert.pem -days 3650 -nodes \ + -subj "/CN=$(uname -n)" -addext "subjectAltName = DNS:$(uname -n)" echo "starting service..." systemctl daemon-reload diff --git a/tools/debian/uninstall.sh b/tools/debian/uninstall.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# This file is part of the Agate software and licensed under either the +# MIT license or Apache license at your option. +# +# Please keep in mind that there is not warranty whatsoever provided for this +# software as specified in the disclaimer in the MIT license or section 7 of +# the Apache license respectively. + +echo "stopping and disabling service..." +systemctl stop gemini +systemctl disable gemini + +echo "removing config files..." +rm -f /etc/systemd/system/gemini.service /etc/rsyslog.d/gemini.conf /etc/logrotate.d/geminilogs + +echo "deleting certificates..." +rm -rf /srv/gemini/.certificates +# do not delete content files, user might want to use them still or can delete them manually +echo "NOTE: content files at /srv/gemini/content not deleted" +# cannot uninstall executable since we did not install it +echo "NOTE: agate executable at $(which agate) not uninstalled"