~zethra/stargazer

a6ad2542353c48c5cfc1b9261549be671231df15 β€” Ben Aaron Goldberg a month ago 0d90acd + dcad5ff
Merge branch 'f/cert-conf'

Signed-off-by: Ben Aaron Goldberg <ben@benaaron.dev>
M doc/stargazer-ini.scd => doc/stargazer-ini.scd +16 -0
@@ 85,6 85,12 @@ The following keys are accepted under the *[:tls]* section:
	the name of the organization responsible for the host and it will be
	filled in as the X.509 /O name.

*gen-certs*
	Set to false to turn off automatic certificate generation.

*regen-certs*
	Set to false to turn off automatic regeneration of expired certificates.

## ROUTING KEYS

To configure *stargazer* to service requests, routing keys must be defined. The


@@ 221,6 227,16 @@ Within each routing section, the following keys are used to configure how
	whole number of seconds. By default CGI processes have no timeout and
	stargazer will wait indefinitely for them to finish.

*cert-path*
	Path to this certificate for this route, overrides the default store path.
	This is intended to be used for certs not managed by stargazer. As such
	stargazer will not attempt to generate or regenerate these certs.
	If this is set, *key-path* must also be set.

*key-path*
	Path to the key that goes with the cert set by the *cert-path*. If this is
	set, *cert-path* must also be set.

# CGI Support

*stargazer* supports a limited version of CGI, compatible with the Jetforce

M doc/stargazer.1.txt => doc/stargazer.1.txt +1 -1
@@ 34,4 34,4 @@ AUTHORS
       bugs/patches can be submitted by email to
       ~zethra/stargazer@lists.sr.ht.

			      2021-07-13		  stargazer(1)
			      2021-08-10		  stargazer(1)

M doc/stargazer.ini.5.txt => doc/stargazer.ini.5.txt +19 -1
@@ 85,6 85,13 @@ CONFIGURATION KEYS
	   this in with the name of the organization responsible for
	   the host and it will be filled in as the X.509 /O name.

       gen-certs
	   Set to false to turn off automatic certificate generation.

       regen-certs
	   Set to false to turn off automatic regeneration of expired
	   certificates.

   ROUTING KEYS
       To configure stargazer to service requests, routing keys must
       be defined. The name of the configuration section is used to


@@ 229,6 236,17 @@ CONFIGURATION KEYS
	   number of seconds. By default CGI processes have no timeout
	   and stargazer will wait indefinitely for them to finish.

       cert-path
	   Path to this certificate for this route, overrides the de‐
	   fault store path. This is intended to be used for certs not
	   managed by stargazer. As such stargazer will not attempt to
	   generate or regenerate these certs. If this is set, key-
	   path must also be set.

       key-path
	   Path to the key that goes with the cert set by the cert-
	   path. If this is set, cert-path must also be set.

CGI Support
       stargazer supports a limited version of CGI, compatible with
       the Jetforce server. It is not a faithful implementation of RFC


@@ 358,4 376,4 @@ AUTHORS
       bugs/patches can be submitted by email to
       ~zethra/stargazer@lists.sr.ht.

			      2021-07-13	      stargazer.ini(5)
			      2021-08-10	      stargazer.ini(5)

M src/config.rs => src/config.rs +38 -0
@@ 46,6 46,10 @@ pub struct Config {
    pub organization: String,
    /// Logging connections and other info to stdout
    pub conn_logging: bool,
    /// Generate certs?
    pub generate_certs: bool,
    /// Regenerate expired certs?
    pub regen_certs: bool,
}

pub fn load(config_path: impl AsRef<Path>) -> Result<Config> {


@@ 114,10 118,13 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> {
        let organization = tls_section
            .remove("organization")
            .unwrap_or_else(|| "stargazer".to_owned());
        let generate_certs = tls_section.remove_yn_true("gen-certs");
        let regen_certs = tls_section.remove_yn_true("regen-certs");
        check_section_empty(":tls", &tls_section)?;

        let mut sites = Vec::with_capacity(5);

        // Load router configs
        for (section, props) in conf.iter_mut() {
            let section = match section {
                Some(section) => section,


@@ 165,6 172,18 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> {
                None => None,
            };

            let cert_path = props.remove("cert-path");
            let key_path = props.remove("key-path");
            let cert_key_path = match (cert_path, key_path) {
                (Some(cert_path), Some(key_path)) => {
                    Some((cert_path.into(), key_path.into()))
                }
                (None, None) => None,
                _ => {
                    bail!("If either `cert-path` or `key-path` are set, both must be set");
                }
            };

            let route_type = if scgi {
                let scgi_addr = props.remove("scgi-address").context(
                    "Routes with `scgi` on must include an `scg-address`",


@@ 245,6 264,7 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> {
                route_type,
                lang: props.remove("lang"),
                charset: props.remove("charset"),
                cert_key_path,
            });
            check_section_empty(section, props)?;
        }


@@ 265,6 285,8 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> {
            request_timeout,
            response_timeout,
            organization,
            generate_certs,
            regen_certs,
        })
    })();
    res.with_context(|| {


@@ 298,12 320,15 @@ pub fn dev_config() -> Result<Config> {
                auto_index: true,
            }
            .into(),
            cert_key_path: None,
        }],
        worker_threads: num_cpus::get(),
        request_timeout: 5,
        response_timeout: 10,
        organization: "stargazer".to_owned(),
        conn_logging: false,
        generate_certs: true,
        regen_certs: true,
    };
    fs::create_dir_all(&conf.store).with_context(|| {
        format!(


@@ 360,6 385,7 @@ fn check_section_empty(name: &str, section: &Properties) -> Result<()> {

trait RemoveYN {
    fn remove_yn(self, name: &str) -> bool;
    fn remove_yn_true(self, name: &str) -> bool;
}

impl RemoveYN for &mut Properties {


@@ 375,6 401,18 @@ impl RemoveYN for &mut Properties {
            })
            .unwrap_or(false)
    }
    fn remove_yn_true(self, name: &str) -> bool {
        self.remove(name)
            .map(|s| match s.to_lowercase().as_str() {
                "on" | "true" | "yes" => true,
                "off" | "false" | "no" => false,
                s => {
                    warn!("Invalid value for `{}`: '{}'. Turing on", name, s);
                    false
                }
            })
            .unwrap_or(true)
    }
}

fn count_true(bools: &[bool]) -> usize {

M src/main.rs => src/main.rs +1 -6
@@ 114,12 114,7 @@ fn main() {
    }

    // Configure tls acceptor for all domain names in config
    let domains: Vec<_> = CONF
        .routes
        .iter()
        .map(|site| site.domain.to_owned())
        .collect();
    let acceptor = TlsAcceptor::from(match tls::load_config(&domains) {
    let acceptor = TlsAcceptor::from(match tls::load_config(&CONF.routes) {
        Ok(conf) => Arc::new(conf),
        Err(e) => {
            error!("{:#}", e);

M src/router.rs => src/router.rs +1 -0
@@ 34,6 34,7 @@ pub struct Route {
    pub lang: Option<String>,
    pub charset: Option<String>,
    pub route_type: RouteType,
    pub cert_key_path: Option<(PathBuf, PathBuf)>,
}

#[derive(Debug, Clone)]

M src/tls.rs => src/tls.rs +35 -17
@@ 14,8 14,9 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use crate::router::Route;
use crate::CONF;
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, bail, Context, Result};
use async_rustls::rustls::internal::msgs::handshake::DigitallySignedStruct;
use async_rustls::rustls::{
    internal::pemfile::{certs, pkcs8_private_keys},


@@ 134,21 135,24 @@ fn load_key(path: &Path) -> Result<PrivateKey> {
/// See https://docs.rs/rustls/0.16.0/rustls/struct.ServerConfig.html for details
///
/// A TLS server needs a certificate and a fitting private key
pub fn load_config(domains: &[String]) -> Result<ServerConfig> {
pub fn load_config(routes: &[Route]) -> Result<ServerConfig> {
    let mut resolver = ResolvesServerCertUsingSNI::new();
    for domain in domains {
        let (cert_chain, key) = get_cert_and_key(domain)?;
    for route in routes {
        let (cert_chain, key) = get_cert_and_key(route)?;

        let signing_key = sign::any_supported_type(&key)
            .map_err(|_| anyhow!("Invalid key for domain {}", domain))?;
            .map_err(|_| anyhow!("Invalid key for domain {}", route.domain))?;

        log::debug!("Loaded cert+key for domain {}", domain);
        log::debug!("Loaded cert+key for domain {}", route.domain);
        resolver
            .add(domain, CertifiedKey::new(cert_chain, Arc::new(signing_key)))
            .add(
                &route.domain,
                CertifiedKey::new(cert_chain, Arc::new(signing_key)),
            )
            .with_context(|| {
                format!(
                    "Error adding cert/key for domain `{}` to tls config",
                    domain
                    route.domain
                )
            })?;
    }


@@ 161,19 165,27 @@ pub fn load_config(domains: &[String]) -> Result<ServerConfig> {
    Ok(config)
}

fn get_cert_and_key(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {
    let cert_path = CONF.store.join(format!("{}.crt", domain));
    let key_path = CONF.store.join(format!("{}.key", domain));
    if !cert_path.exists() || !key_path.exists() {
        let (cert_path, key_path) = gen_cert_and_key(domain)?;
fn get_cert_and_key(route: &Route) -> Result<(Vec<Certificate>, PrivateKey)> {
    let (cert_path, key_path, default_paths) = match route.cert_key_path.clone() {
        Some((cert_path, key_path)) => (cert_path, key_path, false),
        None => (
            CONF.store.join(format!("{}.crt", route.domain)),
            CONF.store.join(format!("{}.key", route.domain)),
            true,
        ),
    };
    if default_paths && (!cert_path.exists() || !key_path.exists()) {
        let (cert_path, key_path) = gen_cert_and_key(&route.domain)?;
        return Ok((load_cert(&cert_path)?, load_key(&key_path)?));
    }
    let cert_chain = load_cert(&cert_path)?;
    for cert in &cert_chain {
        let (_, cert) = parse_x509_certificate(cert.as_ref())
            .with_context(|| format!("Error parsing cert for {}", domain))?;
        if !cert.validity().is_valid() {
            let (cert_path, key_path) = gen_cert_and_key(domain)?;
        let (_, cert) =
            parse_x509_certificate(cert.as_ref()).with_context(|| {
                format!("Error parsing cert for {}", route.domain)
            })?;
        if CONF.regen_certs && !cert.validity().is_valid() {
            let (cert_path, key_path) = gen_cert_and_key(&route.domain)?;
            return Ok((load_cert(&cert_path)?, load_key(&key_path)?));
        }
    }


@@ 181,6 193,12 @@ fn get_cert_and_key(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {
}

fn gen_cert_and_key(domain: &str) -> Result<(PathBuf, PathBuf)> {
    if !CONF.generate_certs {
        bail!(
            "Cert not found for domain {} and cert generation is disabled",
            domain
        );
    }
    debug!("Generating cert+key for {}", domain);
    let mut params = CertificateParams::new(vec![domain.to_owned()]);
    let mut distinguished_name = DistinguishedName::new();

M test_data/testing.ini => test_data/testing.ini +2 -0
@@ 16,6 16,8 @@ charset = ascii
root = ./test_data/test_site
lang = en
charset = ascii
cert-path = ./test_data/store/localhost.crt
key-path = ./test_data/store/localhost.key

[localhost:/cgi-bin]
root = ./test_data