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 Nginx;
impl Nginx {
fn conffile(server: &Server) -> Result<PathBuf> {
Ok(paths::generated_dir()?
.join("nginx")
.join(format!("{}.conf", server.name)))
}
fn nginx_bin(brew: &Brew) -> PathBuf {
brew.bin("nginx")
}
fn render_conf(
server: &Server,
vhosts: &[&Vhost],
state: &State,
cfg: &Config,
brew: &Brew,
) -> Result<String> {
let run = paths::run_dir()?;
let logs = paths::logs_dir()?;
let etc = brew.etc("nginx");
let mut out = String::new();
out.push_str("# Generated by reeve — do not edit by hand.\n");
out.push_str(&format!(
"error_log {} warn;\n",
q(&logs
.join(format!("server-{}-error.log", server.name))
.display()
.to_string())
));
out.push_str(&format!(
"pid {};\n",
q(&run
.join(format!("nginx-{}.pid", server.name))
.display()
.to_string())
));
out.push_str(&format!(
"events {{ worker_connections {}; }}\n",
server.setting("worker_connections", "256")
));
out.push_str("http {\n");
out.push_str(&format!(
" include {};\n",
q(&etc.join("mime.types").display().to_string())
));
out.push_str(" default_type application/octet-stream;\n");
out.push_str(&format!(
" client_max_body_size {};\n",
server.setting("client_max_body_size", "64m")
));
let tmp = |suffix: &str| {
q(&run
.join(format!("nginx-{}-{}", server.name, suffix))
.display()
.to_string())
};
out.push_str(&format!(" client_body_temp_path {};\n", tmp("body")));
out.push_str(&format!(" proxy_temp_path {};\n", tmp("proxy")));
out.push_str(&format!(" fastcgi_temp_path {};\n", tmp("fcgi")));
out.push_str(&format!(" uwsgi_temp_path {};\n", tmp("uwsgi")));
out.push_str(&format!(" scgi_temp_path {};\n", tmp("scgi")));
out.push_str(&format!(
" access_log {};\n",
q(&logs
.join(format!("server-{}-access.log", server.name))
.display()
.to_string())
));
let fastcgi_conf = etc.join("fastcgi.conf");
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
)
})?;
out.push_str(" server {\n");
if v.ssl {
out.push_str(&format!(" listen {} ssl;\n", server.https_port));
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!(
" ssl_certificate {};\n",
q(&cert.display().to_string())
));
out.push_str(&format!(
" ssl_certificate_key {};\n",
q(&key.display().to_string())
));
} else {
out.push_str(&format!(" listen {};\n", server.http_port));
}
out.push_str(&format!(" server_name {};\n", v.server_name));
out.push_str(&format!(" root {};\n", q(&v.docroot)));
out.push_str(" index index.php index.html;\n");
out.push_str(" location / { try_files $uri $uri/ /index.php?$query_string; }\n");
out.push_str(" location ~ \\.php$ {\n");
out.push_str(&format!(
" include {};\n",
q(&fastcgi_conf.display().to_string())
));
out.push_str(&format!(
" fastcgi_pass unix:{};\n",
php.fpm_socket
));
out.push_str(" }\n");
out.push_str(" }\n");
}
if server.default_site {
let root = q(cfg.sites_root.trim_end_matches('/'));
let php_sock = super::default_php_socket(state, cfg);
let body = |out: &mut String| {
out.push_str(" server_name _;\n");
out.push_str(&format!(" root {};\n", root));
out.push_str(" index index.php index.html;\n");
out.push_str(
" location / { try_files $uri $uri/ /index.php?$query_string; }\n",
);
if let Some(sock) = &php_sock {
out.push_str(" location ~ \\.php$ {\n");
out.push_str(&format!(
" include {};\n",
q(&fastcgi_conf.display().to_string())
));
out.push_str(&format!(" fastcgi_pass unix:{};\n", sock));
out.push_str(" }\n");
}
};
out.push_str(" server {\n");
out.push_str(&format!(
" listen {} default_server;\n",
server.http_port
));
body(&mut out);
out.push_str(" }\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(" server {\n");
out.push_str(&format!(
" listen {} ssl default_server;\n",
server.https_port
));
out.push_str(&format!(
" ssl_certificate {};\n",
q(&cert.display().to_string())
));
out.push_str(&format!(
" ssl_certificate_key {};\n",
q(&key.display().to_string())
));
body(&mut out);
out.push_str(" }\n");
}
out.push_str("}\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 Nginx {
fn id(&self) -> Backend {
Backend::Nginx
}
fn formula(&self) -> &'static str {
"nginx"
}
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::nginx_bin(brew))
.arg("-t")
.arg("-c")
.arg(&path)
.output()
.context("Failed to run `nginx -t`")?;
if !out.status.success() {
bail!(
"nginx 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::nginx_bin(brew),
args: vec![
"-c".into(),
conf.display().to_string(),
"-g".into(),
"daemon off;".into(),
],
log: paths::logs_dir()?.join(format!("server-{}.log", server.name)),
keep_alive: true,
run_at_load: true,
})
}
}