use super::WebServerBackend;
use crate::brew::Brew;
use crate::config::Config;
use crate::daemon::ServiceSpec;
use crate::paths;
use crate::state::{Backend, Server, State, Vhost};
use anyhow::{bail, Context, Result};
use std::path::PathBuf;
use std::process::Command;
pub struct Caddy;
impl Caddy {
fn caddyfile(server: &Server) -> Result<PathBuf> {
Ok(paths::generated_dir()?
.join("caddy")
.join(format!("{}.caddyfile", server.name)))
}
fn render_caddyfile(
server: &Server,
vhosts: &[&Vhost],
state: &State,
cfg: &Config,
) -> Result<String> {
let mut out = String::from(
"# Generated by reeve — do not edit by hand.\n{\n\tadmin off\n\tauto_https off\n}\n\n",
);
for v in vhosts {
let php = state.get_php(&v.php_version).ok_or_else(|| {
anyhow::anyhow!(
"vhost '{}' references uninstalled PHP {}",
v.server_name,
v.php_version
)
})?;
let scheme_port = if v.ssl {
format!("https://{}:{}", v.server_name, server.https_port)
} else {
format!("http://{}:{}", v.server_name, server.http_port)
};
out.push_str(&format!("{scheme_port} {{\n"));
out.push_str(&format!("\troot * {}\n", quote(&v.docroot)));
out.push_str(&format!(
"\tphp_fastcgi {}\n",
quote(&format!("unix/{}", php.fpm_socket))
));
out.push_str("\tfile_server\n");
out.push_str(&format!(
"\trequest_body {{\n\t\tmax_size {}\n\t}}\n",
server.setting("max_body", "100MB")
));
if v.ssl {
let cert = paths::certs_dir()?.join(format!("{}.pem", v.server_name));
let key = paths::certs_dir()?.join(format!("{}-key.pem", v.server_name));
out.push_str(&format!(
"\ttls {} {}\n",
quote(&cert.display().to_string()),
quote(&key.display().to_string())
));
}
out.push_str("}\n\n");
}
if server.default_site {
let root = cfg.sites_root.trim_end_matches('/');
let php_sock = super::default_php_socket(state, cfg);
let body = |out: &mut String| {
out.push_str(&format!("\troot * {}\n", quote(root)));
if let Some(sock) = &php_sock {
out.push_str(&format!(
"\tphp_fastcgi {}\n",
quote(&format!("unix/{sock}"))
));
}
out.push_str("\tfile_server browse\n");
};
out.push_str(&format!(":{} {{\n", server.http_port));
body(&mut out);
out.push_str("}\n\n");
let (cert, key) = (
paths::certs_dir()?.join(format!("{}.pem", super::DEFAULT_SITE_HOST)),
paths::certs_dir()?.join(format!("{}-key.pem", super::DEFAULT_SITE_HOST)),
);
out.push_str(&format!("https://:{} {{\n", server.https_port));
body(&mut out);
out.push_str(&format!(
"\ttls {} {}\n",
quote(&cert.display().to_string()),
quote(&key.display().to_string())
));
out.push_str("}\n\n");
}
Ok(out)
}
fn caddy_bin(brew: &Brew) -> PathBuf {
brew.bin("caddy")
}
}
fn quote(s: &str) -> String {
if s.chars().any(|c| c.is_whitespace()) {
format!("\"{s}\"")
} else {
s.to_string()
}
}
impl WebServerBackend for Caddy {
fn id(&self) -> Backend {
Backend::Caddy
}
fn formula(&self) -> &'static str {
"caddy"
}
fn ensure_installed(&self, brew: &Brew) -> Result<()> {
if !brew.is_installed(self.formula()) {
brew.install(self.formula())?;
}
Ok(())
}
fn render(
&self,
server: &Server,
vhosts: &[&Vhost],
state: &State,
cfg: &Config,
_brew: &Brew,
) -> Result<()> {
for v in vhosts {
if state.get_php(&v.php_version).is_none() {
bail!(
"vhost '{}' needs PHP {} — run `reeve php install {}`",
v.server_name,
v.php_version,
v.php_version
);
}
}
let content = Self::render_caddyfile(server, vhosts, state, cfg)?;
let path = Self::caddyfile(server)?;
std::fs::write(&path, content)
.with_context(|| format!("Failed to write {}", path.display()))?;
Ok(())
}
fn validate(&self, server: &Server, brew: &Brew) -> Result<()> {
let path = Self::caddyfile(server)?;
if !path.exists() {
bail!(
"No generated Caddyfile for '{}'. Run `reeve apply` first.",
server.name
);
}
let out = Command::new(Self::caddy_bin(brew))
.args(["validate", "--adapter", "caddyfile", "--config"])
.arg(&path)
.output()
.context("Failed to run `caddy validate`")?;
if !out.status.success() {
bail!(
"caddy validate failed for '{}':\n{}",
server.name,
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(())
}
fn reload(&self, server: &Server, _brew: &Brew) -> Result<()> {
crate::daemon::restart(&super::server_service_id(server))
}
fn service_spec(&self, server: &Server, brew: &Brew) -> Result<ServiceSpec> {
let caddyfile = Caddy::caddyfile(server)?;
Ok(ServiceSpec {
service: super::server_service_id(server),
program: Caddy::caddy_bin(brew),
args: vec![
"run".into(),
"--adapter".into(),
"caddyfile".into(),
"--config".into(),
caddyfile.display().to_string(),
],
log: paths::logs_dir()?.join(format!("server-{}.log", server.name)),
keep_alive: true,
run_at_load: true,
})
}
}