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 Apache;
const BASE_MODULES: &[(&str, &str)] = &[
("mpm_event_module", "mod_mpm_event.so"),
("authz_core_module", "mod_authz_core.so"),
("unixd_module", "mod_unixd.so"),
("log_config_module", "mod_log_config.so"),
("mime_module", "mod_mime.so"),
("dir_module", "mod_dir.so"),
("proxy_module", "mod_proxy.so"),
("proxy_fcgi_module", "mod_proxy_fcgi.so"),
("rewrite_module", "mod_rewrite.so"),
("headers_module", "mod_headers.so"),
("setenvif_module", "mod_setenvif.so"),
];
const SSL_MODULES: &[(&str, &str)] = &[
("ssl_module", "mod_ssl.so"),
("socache_shmcb_module", "mod_socache_shmcb.so"),
];
impl Apache {
fn server_root(brew: &Brew) -> PathBuf {
brew.opt("httpd")
}
fn modules_dir(brew: &Brew) -> PathBuf {
Self::server_root(brew).join("lib/httpd/modules")
}
fn conffile(server: &Server) -> Result<PathBuf> {
Ok(paths::generated_dir()?
.join("apache")
.join(format!("{}.conf", server.name)))
}
fn httpd_bin(brew: &Brew) -> PathBuf {
brew.opt("httpd").join("bin/httpd")
}
fn render_conf(
server: &Server,
vhosts: &[&Vhost],
state: &State,
cfg: &Config,
brew: &Brew,
) -> Result<String> {
let needs_ssl = vhosts.iter().any(|v| v.ssl) || server.default_site;
let mdir = Self::modules_dir(brew);
let mut out = String::new();
out.push_str("# Generated by reeve — do not edit by hand.\n");
out.push_str(&format!(
"ServerRoot {}\n",
q(&Self::server_root(brew).display().to_string())
));
out.push_str("ServerName localhost\n");
out.push_str(&format!("Timeout {}\n", server.setting("timeout", "300")));
out.push_str("KeepAlive On\n");
out.push_str(&format!(
"KeepAliveTimeout {}\n",
server.setting("keepalive_timeout", "5")
));
out.push_str(&format!(
"MaxKeepAliveRequests {}\n",
server.setting("max_keepalive_requests", "100")
));
out.push_str(&format!(
"LimitRequestBody {}\n",
server.setting("limit_request_body", "0")
));
out.push_str(&format!(
"PidFile {}\n",
q(&paths::run_dir()?
.join(format!("httpd-{}.pid", server.name))
.display()
.to_string())
));
out.push_str(&format!("Listen {}\n", server.http_port));
if needs_ssl {
out.push_str(&format!("Listen {}\n", server.https_port));
}
let mut mods: Vec<(&str, &str)> = BASE_MODULES.to_vec();
if needs_ssl {
mods.extend_from_slice(SSL_MODULES);
}
for (name, file) in mods {
let path = mdir.join(file);
if path.exists() {
out.push_str(&format!(
"LoadModule {name} {}\n",
q(&path.display().to_string())
));
}
}
out.push_str(&format!(
"ErrorLog {}\n",
q(&paths::logs_dir()?
.join(format!("server-{}-error.log", server.name))
.display()
.to_string())
));
out.push_str("LogLevel warn\n");
out.push_str("<IfModule mime_module>\n");
out.push_str(&format!(
" TypesConfig {}\n",
q(&brew.etc("httpd").join("mime.types").display().to_string())
));
out.push_str(" AddType application/x-httpd-php .php\n");
out.push_str("</IfModule>\n");
out.push_str(
"<Directory />\n AllowOverride none\n Require all denied\n</Directory>\n\n",
);
if needs_ssl {
out.push_str("<IfModule ssl_module>\n SSLSessionCache \"shmcb:");
out.push_str(
&paths::run_dir()?
.join(format!("ssl_scache-{}", server.name))
.display()
.to_string(),
);
out.push_str("(512000)\"\n</IfModule>\n\n");
}
for v in vhosts {
let port = if v.ssl {
server.https_port
} else {
server.http_port
};
out.push_str(&format!("<VirtualHost *:{port}>\n"));
out.push_str(&format!(" ServerName {}\n", v.server_name));
if let Some(target) = &v.proxy_target {
out.push_str(" ProxyPreserveHost On\n");
out.push_str(&format!(" ProxyPass / {}/\n", q(target)));
out.push_str(&format!(" ProxyPassReverse / {}/\n", q(target)));
} else {
let php = state.get_php(&v.php_version).ok_or_else(|| {
anyhow::anyhow!(
"vhost '{}' references uninstalled PHP {}",
v.server_name,
v.php_version
)
})?;
let handler = format!("proxy:unix:{}|fcgi://localhost", php.fpm_socket);
let docroot = v.effective_docroot();
out.push_str(&format!(" DocumentRoot {}\n", q(&docroot)));
out.push_str(&format!(" <Directory {}>\n", q(&docroot)));
out.push_str(" Options Indexes FollowSymLinks\n");
out.push_str(" AllowOverride All\n");
out.push_str(" Require all granted\n");
out.push_str(" </Directory>\n");
out.push_str(" DirectoryIndex index.php index.html\n");
out.push_str(" <FilesMatch \\.php$>\n");
out.push_str(&format!(" SetHandler {}\n", q(&handler)));
out.push_str(" </FilesMatch>\n");
}
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(" SSLEngine on\n");
out.push_str(&format!(
" SSLCertificateFile {}\n",
q(&cert.display().to_string())
));
out.push_str(&format!(
" SSLCertificateKeyFile {}\n",
q(&key.display().to_string())
));
}
out.push_str("</VirtualHost>\n\n");
}
if server.default_site {
let root = server.effective_default_root(&cfg.sites_root);
let php_sock = super::default_php_socket(state, cfg);
let body = |out: &mut String| {
out.push_str(" ServerName localhost\n");
out.push_str(&format!(" DocumentRoot {}\n", q(root)));
out.push_str(&format!(" <Directory {}>\n", q(root)));
out.push_str(" Options Indexes FollowSymLinks\n");
out.push_str(" AllowOverride All\n");
out.push_str(" Require all granted\n");
out.push_str(" </Directory>\n");
out.push_str(" DirectoryIndex index.php index.html\n");
if let Some(sock) = &php_sock {
let handler = format!("proxy:unix:{sock}|fcgi://localhost");
out.push_str(" <FilesMatch \\.php$>\n");
out.push_str(&format!(" SetHandler {}\n", q(&handler)));
out.push_str(" </FilesMatch>\n");
}
};
out.push_str(&format!("<VirtualHost *:{}>\n", server.http_port));
body(&mut out);
out.push_str("</VirtualHost>\n\n");
let cert = paths::certs_dir()?.join(format!("{}.pem", super::DEFAULT_SITE_HOST));
let key = paths::certs_dir()?.join(format!("{}-key.pem", super::DEFAULT_SITE_HOST));
out.push_str(&format!("<VirtualHost *:{}>\n", server.https_port));
body(&mut out);
out.push_str(" SSLEngine on\n");
out.push_str(&format!(
" SSLCertificateFile {}\n",
q(&cert.display().to_string())
));
out.push_str(&format!(
" SSLCertificateKeyFile {}\n",
q(&key.display().to_string())
));
out.push_str("</VirtualHost>\n\n");
}
Ok(out)
}
}
fn q(s: &str) -> String {
if s.chars().any(|c| c.is_whitespace()) {
format!("\"{s}\"")
} else {
s.to_string()
}
}
impl WebServerBackend for Apache {
fn id(&self) -> Backend {
Backend::Apache
}
fn formula(&self) -> &'static str {
"httpd"
}
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<()> {
let content = Self::render_conf(server, vhosts, state, cfg, brew)?;
let path = Self::conffile(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::conffile(server)?;
if !path.exists() {
bail!(
"No generated config for '{}'. Run `reeve apply` first.",
server.name
);
}
let out = Command::new(Self::httpd_bin(brew))
.arg("-t")
.arg("-f")
.arg(&path)
.output()
.context("Failed to run `httpd -t`")?;
if !out.status.success() {
bail!(
"httpd config test 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 conf = Self::conffile(server)?;
Ok(ServiceSpec {
service: super::server_service_id(server),
program: Self::httpd_bin(brew),
args: vec![
"-D".into(),
"FOREGROUND".into(),
"-f".into(),
conf.display().to_string(),
],
log: paths::logs_dir()?.join(format!("server-{}.log", server.name)),
keep_alive: true,
run_at_load: true,
})
}
}