use std::collections::BTreeMap;
use crate::config::schema::Config;
use crate::error::{Error, Result};
use crate::exposure::Exposure;
use crate::registry::service_def::{AuthKind, EnvFormat, ServiceDef};
use crate::system::secret;
pub fn build_context(
config: &Config,
service_def: &ServiceDef,
host_port: Option<u16>,
auth_kind: Option<&AuthKind>,
exposure: &Exposure,
enable_smtp: bool,
) -> Result<BTreeMap<String, String>> {
let url: Option<&str> = exposure.url();
let mut ctx = BTreeMap::new();
ctx.insert("service.name".into(), service_def.service.name.clone());
if let Some(port) = host_port {
ctx.insert("service.port".into(), port.to_string());
}
let effective_port = host_port.or_else(|| service_def.ports.first().map(|p| p.container_port));
let localhost_url = match effective_port {
Some(port) => format!("http://127.0.0.1:{port}"),
None => "http://127.0.0.1".to_string(),
};
ctx.insert("service.url".into(), localhost_url.clone());
if let Some(url) = url {
let parsed = url::Url::parse(url)
.map_err(|e| Error::Template(format!("invalid service URL '{url}': {e}")))?;
let host = parsed
.host_str()
.ok_or_else(|| Error::Template(format!("service URL '{url}' has no host")))?;
ctx.insert("service.domain".into(), host.to_string());
ctx.insert("service.scheme".into(), parsed.scheme().to_string());
let authority = match parsed.port() {
Some(port) => format!("{host}:{port}"),
None => host.to_string(),
};
ctx.insert("service.external_authority".into(), authority);
ctx.insert("service.external_url".into(), url.to_string());
} else {
ctx.insert("service.scheme".into(), "http".into());
}
ctx.entry("service.external_url".into())
.or_insert(localhost_url.clone());
ctx.entry("service.url".into()).or_insert(localhost_url);
ctx.insert(
"admin.email".into(),
config
.admin_email
.clone()
.unwrap_or_else(|| "admin@example.com".to_string()),
);
if enable_smtp && let Some(smtp) = &config.smtp {
ctx.insert("smtp.host".into(), smtp.host.clone());
ctx.insert("smtp.port".into(), smtp.port.to_string());
ctx.insert("smtp.username".into(), smtp.username.clone());
ctx.insert("smtp.password".into(), smtp.password.clone());
ctx.insert("smtp.from".into(), smtp.from.clone());
ctx.insert("smtp.security".into(), smtp.security.as_str().into());
}
if let (Some(_), Some(auth)) = (auth_kind, &config.auth) {
let auth_base_url = auth.url().to_string();
let installed_for_auth = crate::list_installed().unwrap_or_default();
let caddy_installed = crate::is_service_installed("caddy");
let authelia_exposure = installed_for_auth
.iter()
.find(|s| s.name == auth.provider_name())
.map(|s| s.exposure.clone());
let mut external_url = authelia_exposure
.as_ref()
.and_then(|e| e.url())
.map(|u| u.to_string())
.unwrap_or_else(|| auth_base_url.clone());
if caddy_installed && matches!(authelia_exposure, Some(crate::Exposure::Internal { .. })) {
let port = crate::caddy_https_port(config);
external_url = with_caddy_port(&external_url, port)?;
}
let internal_url = external_url.clone();
ctx.insert("auth.url".into(), auth_base_url.clone());
ctx.insert("auth.internal_url".into(), internal_url.clone());
ctx.insert("auth.provider".into(), auth.provider_name().to_string());
ctx.insert("auth.external_url".into(), external_url.clone());
let issuer = match auth {
crate::config::schema::AuthCredentials::Authelia { .. } => external_url.clone(),
crate::config::schema::AuthCredentials::External { .. } => auth_base_url.clone(),
};
ctx.insert("auth.issuer".into(), issuer);
ctx.insert(
"auth.client_id".into(),
secret::generate(&EnvFormat::Uuid, None),
);
ctx.insert(
"auth.client_secret".into(),
secret::generate(&EnvFormat::String, Some(64)),
);
}
let _ = config; for installed in crate::list_installed().unwrap_or_default() {
let name = &installed.name;
for (port_name, port) in &installed.ports {
ctx.insert(
format!("services.{name}.port.{port_name}"),
port.to_string(),
);
}
let env_file = crate::service_home(name)?.join(".env");
let content = match std::fs::read_to_string(&env_file) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(source) => {
return Err(Error::FileRead {
path: env_file,
source,
});
}
};
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, val)) = line.split_once('=') {
ctx.insert(format!("services.{name}.env.{key}"), val.to_string());
}
}
}
let all_envs: Vec<_> = service_def
.env
.iter()
.chain(service_def.env_groups.iter().flat_map(|g| g.env.iter()))
.chain(
service_def
.choices
.iter()
.flat_map(|c| c.options.iter().flat_map(|o| o.env.iter())),
)
.collect();
let jwt_signing_keys: Vec<String> = all_envs
.iter()
.filter(|e| e.format == EnvFormat::JwtHs256)
.filter_map(|e| e.jwt_signing_key.clone())
.collect();
for env in all_envs.iter().copied() {
if env.format == EnvFormat::JwtHs256 {
continue;
}
for secret_name in crate::generate::extract_secret_refs(&env.value) {
if jwt_signing_keys.contains(&secret_name) {
let key = format!("secret.{secret_name}");
ctx.entry(key)
.or_insert_with(|| secret::generate(&env.format, env.length));
}
}
}
for env in all_envs.iter().copied() {
if env.format != EnvFormat::JwtHs256 {
continue;
}
if let (Some(claims), Some(signing_key_name)) = (&env.jwt_claims, &env.jwt_signing_key) {
let signing_key_ref = format!("secret.{signing_key_name}");
let signing_key = ctx.get(&signing_key_ref).cloned().ok_or_else(|| {
Error::Template(format!(
"JWT signing key '{signing_key_name}' not found in context — \
the referenced secret must be declared by a non-JWT env var in \
service.toml before the JWT env var that signs with it"
))
})?;
for secret_name in crate::generate::extract_secret_refs(&env.value) {
let key = format!("secret.{secret_name}");
ctx.entry(key)
.or_insert_with(|| secret::generate_jwt_hs256(&signing_key, claims));
}
}
}
for env in all_envs.iter().copied() {
if env.format == EnvFormat::JwtHs256 {
continue;
}
for secret_name in crate::generate::extract_secret_refs(&env.value) {
let key = format!("secret.{secret_name}");
ctx.entry(key)
.or_insert_with(|| secret::generate(&env.format, env.length));
}
}
Ok(ctx)
}
fn with_caddy_port(url: &str, caddy_port: u16) -> Result<String> {
let mut parsed = url::Url::parse(url)
.map_err(|e| Error::Template(format!("invalid auth provider URL '{url}': {e}")))?;
if parsed.port_or_known_default() == Some(caddy_port) {
return Ok(url.to_string());
}
parsed.set_port(Some(caddy_port)).map_err(|_| {
Error::Template(format!(
"auth provider URL '{url}' is not a base URL — can't set port"
))
})?;
let mut s = parsed.to_string();
if s.ends_with('/') {
s.pop();
}
Ok(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn with_caddy_port_already_explicit_default() {
let out = with_caddy_port("https://authelia.internal:443", 443).unwrap();
assert_eq!(out, "https://authelia.internal:443");
}
#[test]
fn with_caddy_port_no_port_needs_default() {
let out = with_caddy_port("https://authelia.internal", 443).unwrap();
assert!(out == "https://authelia.internal" || out == "https://authelia.internal:443");
}
#[test]
fn with_caddy_port_replaces_mismatched_port() {
let out = with_caddy_port("https://authelia.internal:8443", 9443).unwrap();
assert_eq!(out, "https://authelia.internal:9443");
}
#[test]
fn with_caddy_port_appends_high_port_to_bare_host() {
let out = with_caddy_port("https://authelia.internal", 8443).unwrap();
assert_eq!(out, "https://authelia.internal:8443");
}
}