add capability for multiple certificates (#40)

This commit is contained in:
Johann150 2021-03-23 23:28:16 +01:00 committed by GitHub
commit 4c2d33491d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 771 additions and 111 deletions

View file

@ -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.

View file

@ -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

View file

@ -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

229
src/certificates.rs Normal file
View file

@ -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
}
}
}

View file

@ -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 {

View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
tPwNvAOh6tUMmDlbRt+xdpFmU238jA==
-----END PRIVATE KEY-----

View file

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFCTCCAvGgAwIBAgIUP60wPmdZzVFdc9c1ReFbzcWk9CwwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIwMTEwODE0MzE1NloXDTMwMTEw
NjE0MzE1NlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEA+3+qGCFlrKMqyCl7uO7QJ+ILZx94swlcxaO1Vo2plau/
rLFksNTm9EeT5kfovgE8UIXyuwQWZOoxOouaX30t3SUt1qhM2tJw+bkgN+M02LSX
i67AbjqJXWeJs+yQL64RVdM3c13Qow6EBqkOPSViISNtp09GKufKgePW6ptb8fk2
o1yA7inQicCX7YRf/bF4q5qjOBrDjswDH8W1N5Vn8XVNa1ydLQZWUh/299WZ7jQg
LgIFiJ9RL9qhtl9FlSvN3yeRnny3AX4bk+I666vmleqAB8579QCT7wcGIVatPX71
b0xZxayCZHNzcWTI4nZI2CO5b4T88F9qztM8NzBC/OvNKVPAiTrFwMnlxg3oqbe9
Evqep2Ut/n5aKjkgeWDHUI57VoAWJEfPWPKuJhIPnmokf5RRzjqGa3invivnYzD2
Jna0RMUNfA3CjVpdt/c4/59kKU3Z848uzZLrw2cmX+KZpv9LuCnfUqOnEsmwhoxr
ZaZqb8wuF7nm7aPwO85SXiIIZdppm6N1TIZTOOJ0HpDutZiKs9nZKoBoH+5KkCkr
76P0Y+bvA+DdxZ0V0Jvyar5QskJac3hbpO7xSc9cgNXytwsX7iusbnJTIY6Nr4DQ
sGogRY2/3gJ6zPv27VHykxlyMeJ3nRK5bE7k6rpCUQjrpVXb9GVbFZ1uv9LeIfkC
AwEAAaNTMFEwHQYDVR0OBBYEFI7IjwaA0gwpeZXl5x7Knw8i4STDMB8GA1UdIwQY
MBaAFI7IjwaA0gwpeZXl5x7Knw8i4STDMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
hvcNAQELBQADggIBANQJ+WuQ8UZsk86t7jTS9K2Igit22ImXhdddDHHbBrbdLXoN
g4IXij8mEJqfPyqZhT49S23booihvJOyhT/RgRB6LI/hBuV1vTwARmIRU4WhZAh0
xIwVCqniDp6Rgf6OEOYgLGk11CEv7vMfPZYbLddDY1HvmSl8l087CLEQ37WRb51M
5Quhsrny441s6aTn8I7c9WY2H/CUmlF8byoLuIl2MpR5bQN17binfsJZPYkKaMyJ
5URM0nCVUEr+o/1znYsQJYa+GSVEsJJ6OyS7TMSoFJlVFslOxbdPzNkdcL6jxpXN
B0rCrC1gTaQynxTOoQ56N8Z74V9xoXNd0ZwaSgEWfeM++YOyk3qgfvhobd0F8rTD
8+dvMN7eI+N8P+S+VCnX9YzrQIZyTwEhHK9fXLlcqoiAhpizgGhlGctiZ1MmpzT2
aqFmLOCKpcQLyofsSLSFbhV2/w8rJbS1kTlrzwQLzaJvtLVy+ZZdQFP49Vj8Lb7n
3Oos/YeNoGhJoTWX7S2nQBChYMsSUA15+IS7RN0b+cJroHESsqCkbp07M20zhztz
fDWdYFh+o4V2lF9ecnqV7MwTvz9WxpchcUfQgENrJ3dgTn35hsZOMM2anwFWrmG+
KVyFMhWNnZB1E530Nsu9cNHntqc3sFBdJebrFED9gOFErt5Vou8btjIqPm/Y
-----END CERTIFICATE-----

View file

@ -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

View file

@ -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-----

View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
q+T6LY6tbtpB+11HBnD4I79WCYf4DA==
-----END PRIVATE KEY-----

View file

@ -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-----

View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQCtDDqs3rEY+qbM
ulzHEM+9o1Aomikz/22plsmg8QHDNEtDHApdHaV6XiA+vouJf30Cj9zoySB+6Que
ZkcYyWVVnYaEnb1v+f3ZYv7saaOaRx+mvZLZ7zlx4MPJJG6dw1b7uCaSb7qCneu3
ZZfd9bDuZ1QgkyZDcIPv0IT/kSONybmSmVTkKbGO397GZegEibk4YIn8ZQhdJq7G
m8fmJNFIOkiwCLKJ/zh3hmgogV5Y9PpAsRRlrBRzk8ueRxySrhr7WnXPzCXRq1Ql
FB46ty3rPMEF7s0FBvvECTptobVZFlyUWbbplFB1Yvq6lkUQTtci4oea/2L/KjeQ
WvnOAEHE9Lg8ind2q+ko0YFg1GaCWwNAK+MnRpZELvSmVWNmdtB8fkfsSps8+gcJ
ykt1/AZzmIaa8DpWTWOqgqcoKsIg7TCXyNtdJA3l0rlEV31r66zkEYTuH0M5q4OK
Ow7BdljlPZGO34MWPTm01E6duI9oNqfyOUtCR0aZkIWCF1NVBK229yalBqzrnlH+
X3SLtFVlo1wGxrJeFpM12RgW0lZCkOKAKy2uwRWyZAv/s/LbU2pyMjyOUijQxl1k
UUeXwf6/beSOl+RKdPnPsxOLKCY+0ir+soL5yqB3JLmgQmjSQeD6vX7+yBWc3+W6
NwhkpSYtTU7QAd9iPpNmRqlQV2IdxwIDAQABAoICAAiUL6B8VclIO9awcoMH6VSc
cQ/iPKKwSg57RDmvWQgFYqnMDRN6scZ0PiL+LUq+wELNQQVlWzAPe5z5sxKegWCS
M6YFb+vKN/R7/OlZf1vZpM8OXOZi/rUPkIU7QiSeF4TZJ0hhM5zgGVx5M+M0F/Zp
tvj6co4rWM8dxkopNtsDoiiLY3MAQiY0IQYy7SK0dTM/TffuRlDf5xA/jtRxBNMQ
2KOperhup6z9Q9KmPzgnxPRKExnLQyRLsm+BVQBMk1fcrzSDCWjwlnZUHf+JL0SX
OXaC1TUnmHmqf3QJ7USiYCqWnAPOb4KySn3Pj1L0paO8GT7s5EqEHEcSy4mT5664
F6LsAkkxDY37bAQ4I6guEPb6+JjI+a7BtU82/LIb9tUIF3tLLN810JMRUFu0Pt1Z
e84AlANH47NNcCjPcoHz8lcVBlNQ4JDi9yxZwwO+brFvwHVmzV7l3wCl/70ucsJy
gwB1Eysizi41hqMpAtbhGe3nmgJs/S9+KjnjkvkzlqFaX6xaU5nTLpkEF1znsbiw
FEHKEiIFn0hPPHb24PRm4ktyBoT65qaSB/GewVbvJN/CxPin5C1I9MzvzXv+rFn1
NMkGJCmL40zwAUkm/1NTO8cvRG85s8ak7Y1IAmo67wUmuuoQQyo71Jef/JcVyk6c
xuRRdMu5sp8nyyNcYOZZAoIBAQDWmGVqUYWm5sGWUkhRv6ct/ov6rhlEw9/4KJIa
TkEN0G6rItTjCAHycywSomeeQv5waxdYjpAOOYxm166bvgGa6lkDmttIvyG2bzsX
F9Xvc4+KbMPMG0Bn5M4IP/FPM+L/hA6r4z7MmU3PIotLSGRT9CMyfQzs228YLgdb
OfRT3qxWeohWHHv91l62yJjJ2hCay89U/aq3C5oQ7RPuv8tj5V1H86kcRl0Emkly
LkZLNVf1FykVn80SL0MquXhhsv4GrRedjbMhnWtqUuIohTOVqGe2tfkBu2OYpDuO
T7eKi0I4Z1bzUOwfjoBsWuyJjhWGt9pGYS912An3r/htZirtAoIBAQDOb6gxTw5N
1aVSgcdDE0A1/UsqYrICkqstGbJYjvQIj8YMyAGG7AAd5KN/cnNpJMyrNSmUhKAK
I1OFCsloTa8VbDistxCJIqkK3wJ6/YnlBjQbyTPCbjPJ5Fj/U0AutqCYjRHifyAr
GxUtSkZM+NxV/ggcKlDaLGiIH1/NmiTXlqFHyoxC4yhB0ZJeBj8az8THP3awgqjV
idHzcYQBJEWMbn5/ysPlm3NCuQwqXpGvQ8P881P91CtnvmilxgDKt/fOOFA3n/jy
YNFjmoL/bxA8L9Lc8Zpr1vHs++01V+JFxhLWPpiIAWavkkj0BvkQDzJmvLHI9SmG
wR/DK9Z+kXEDAoIBAQDHUsw4Qbp7uTCs+IaV8AdP0HSihl2QIsQA02ZJqtAADc8N
hI/qxMBSO6n/MPw/4whE0SPhLKIfpFKGH+XeYVFKXEwL7iWqX2Xn908SdyBOhq8Y
K0h+Z/2dwsegoAv6vj4libq665ukHO1J7VMmvPn7hPPAbKi5xGRfODm7AYyw7k5z
EONb4J9GunxFGPPZ4YO01IQi9G9CEDOtbxgpldpMUnofX/J/AdhacxivRs4iA01M
qJOPs1uefWnM4HMxhDkxaEtcG4b8PSTNoGjSrE6qvr5+1m2Qr0amPD3ZRLA9rnX2
v/3iiRKZiRo+CwJUDjZuaI0E/DZCJkWz265Lpy9NAoIBAQCoJu1i1Nl67ycOEOZF
rb2k/KCocuI7FEtYnlDWsAL5olsZeCU+SKhDsUS4gHqfz7jjUJeBAZL3DxVuDn5G
dtjB43g6v5c5jUESuNrlYfZb1nTFmVuO6YNH1bfkqmRiaKJiAK7rxs9mLVZPoOuo
sSGQ7i6e+p0HShsPnjbEW+XcsjbHKqabqTrWeiX2brIiXdEU144Pcy6hWfTpjrKO
14PLQwnJgFmXgssdM2xEaunSUKmpNm9ZF+UPSVsmhSWJ+tZgZSB6XtVCYTjOIELK
XCZmUDI7hJVbeCdx+TecNuz6FsCrQSuvxSxmoQrJs5BW03ojk1phrclYmaEMsn2y
dTgPAoIBAQCRe4y76djA0MY7QCm4HO06ZpZV7g0K1wzUrJdOc7+UAklFNVttTfAJ
eTk7dVKcxJy8W/jBStuKXuBba76vn7Oas4J316HHz4w9NBgz9Gdbid9sbkDWfsF/
AjzEtrwKKCydH2bxgJlj0kC3baemSib71HNilVuApLGhdCZAdWkxqkxAmSoMuhjx
3CX66AKVyMMouk7GnuoUj67Ued5Bha54arDq1RZVWLa8JnGRoUkfzC5ukyYo3LcE
SC15WJOUEGyqXGUwSrh9t1YLpViG6dARBnLuLjdRIvLVvEtgymvRwzb1fS4/wMH8
BVksLv0EVzq2MyO+yTR1M8Y+0KBK5dx8
-----END PRIVATE KEY-----

View file

@ -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");
mod vhosts {
use super::*;
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()
#[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()
)
);
}
#[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()
}
}

View file

@ -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

21
tools/debian/uninstall.sh Executable file
View file

@ -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"