mod app;
use std::time::Duration;
use ullun::tls::TlsConfig;
fn dev_certs() -> (String, String, String, String) {
use rcgen::{
BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair,
};
let dir = std::path::Path::new("dev-certs");
std::fs::create_dir_all(dir).expect("create dev-certs");
let ca_key = KeyPair::generate().unwrap();
let mut ca_params = CertificateParams::new(Vec::new()).unwrap();
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
ca_params
.distinguished_name
.push(DnType::CommonName, "ullun dev CA");
let ca_cert = ca_params.self_signed(&ca_key).unwrap();
let issuer = Issuer::new(ca_params, ca_key);
let server_key = KeyPair::generate().unwrap();
let server_params = CertificateParams::new(vec!["localhost".to_string()]).unwrap();
let server_cert = server_params.signed_by(&server_key, &issuer).unwrap();
let client_key = KeyPair::generate().unwrap();
let mut client_params = CertificateParams::new(Vec::new()).unwrap();
client_params
.distinguished_name
.push(DnType::CommonName, "dev admin");
client_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
let client_cert = client_params.signed_by(&client_key, &issuer).unwrap();
let write = |name: &str, content: String| {
std::fs::write(dir.join(name), &content).expect("write dev cert");
content
};
write("ca.pem", ca_cert.pem());
let cert = write("server.pem", server_cert.pem());
let key = write("server.key", server_key.serialize_pem());
write("client.pem", client_cert.pem());
write("client.key", client_key.serialize_pem());
let fingerprint = app::fingerprint(client_cert.der());
(
cert,
key,
std::fs::read_to_string(dir.join("ca.pem")).unwrap(),
fingerprint,
)
}
#[tokio::main]
async fn main() {
let env = |k: &str| std::env::var(k).ok();
let dev = env("DEV").as_deref() == Some("1");
let https_port = env("HTTPS_PORT").unwrap_or_else(|| "8443".into());
let http_port: u16 = env("HTTP_PORT")
.and_then(|p| p.parse().ok())
.unwrap_or(8080);
let (tls, mut fingerprints) = if dev {
let (cert, key, ca, fp) = dev_certs();
println!("dev certificates written to ./dev-certs, try:");
println!(" curl --cacert dev-certs/ca.pem https://localhost:{https_port}/api/whoami");
println!(" curl --cacert dev-certs/ca.pem --cert dev-certs/client.pem --key dev-certs/client.key https://localhost:{https_port}/admin/status");
(
TlsConfig::from_pem(cert.as_bytes(), key.as_bytes())
.and_then(|t| t.client_auth(ca.as_bytes(), false)),
vec![fp],
)
} else {
let (Some(cert), Some(key), Some(ca)) = (env("TLS_CERT"), env("TLS_KEY"), env("CLIENT_CA"))
else {
eprintln!("TLS_CERT, TLS_KEY and CLIENT_CA are required (or DEV=1)");
std::process::exit(2);
};
let ca_pem = std::fs::read(&ca).unwrap_or_else(|e| {
eprintln!("cannot read CLIENT_CA {ca}: {e}");
std::process::exit(2);
});
(
TlsConfig::from_pem_files(&cert, &key).and_then(|t| t.client_auth(&ca_pem, false)),
Vec::new(),
)
};
let tls = tls.unwrap_or_else(|e| {
eprintln!("{}", e.message());
std::process::exit(2);
});
fingerprints.extend(
env("ADMIN_FINGERPRINTS")
.unwrap_or_default()
.split(',')
.map(|s| s.trim().to_ascii_lowercase())
.filter(|s| !s.is_empty()),
);
let https = app::build(app::Config {
admin_fingerprints: fingerprints,
})
.tls(tls)
.tls_reload_every(Duration::from_secs(3600))
.run(Box::leak(format!("0.0.0.0:{https_port}").into_boxed_str()));
if http_port == 0 {
https.await.unwrap();
} else {
let redirect = app::redirect_app(https_port.parse().unwrap_or(443))
.run(Box::leak(format!("0.0.0.0:{http_port}").into_boxed_str()));
let (a, b) = tokio::join!(https, redirect);
a.unwrap();
b.unwrap();
}
}