use std::error::Error; use std::fmt; use std::io; use std::path::{Path, PathBuf}; use crate::command_runner::CommandRunner; use crate::resources::Resource; use crate::symbols::file::File as FileSymbol; use crate::symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner}; #[derive(Debug)] pub enum NginxServerError { ExecError(E), GenericError, } impl From for NginxServerError { fn from(err: io::Error) -> Self { Self::ExecError(err) } } impl Error for NginxServerError { fn description(&self) -> &str { match self { Self::ExecError(ref e) => e.description(), Self::GenericError => "Generic error", } } fn cause(&self) -> Option<&dyn Error> { match self { Self::ExecError(ref e) => Some(e), _ => None, } } } impl fmt::Display for NginxServerError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "{}", self.description()) } } pub struct NginxServer<'a, C: CommandRunner, T: AsRef, P: AsRef> { command_runner: &'a C, file: FileSymbol, } pub fn server_config(domain: &str, content: &str) -> String { format!( "server {{ listen 443 ssl http2; server_name {0}; include \"snippets/acme-challenge.conf\"; ssl_certificate /etc/ssl/local_certs/{0}.chained.crt; ssl_certificate_key /etc/ssl/private/{0}.key; add_header Strict-Transport-Security \"max-age=31536000\"; {1} }} # Redirect all HTTP links to the matching HTTPS page server {{ listen 80; server_name {0}; include \"snippets/acme-challenge.conf\"; location / {{ return 301 https://$host$request_uri; }} }} ", domain, content ) } pub fn php_server_config_snippet, STATIC: AsRef>( socket_path: SOCKET, static_path: STATIC, ) -> String { format!( " root {}; index index.html index.php; location ~ [^/]\\.php(/|$) {{ fastcgi_pass unix:{}; include \"snippets/fastcgi-php.conf\"; }}", static_path.as_ref().to_str().unwrap(), socket_path.as_ref().to_str().unwrap() ) } pub trait SocketSpec { fn to_nginx(&self) -> String; } impl SocketSpec for T where T: AsRef, { fn to_nginx(&self) -> String { format!("unix:{}:", self.as_ref().to_str().unwrap()) } } pub struct LocalTcpSocket(usize); impl LocalTcpSocket { pub const fn new(x: usize) -> Self { Self(x) } } impl SocketSpec for LocalTcpSocket { fn to_nginx(&self) -> String { format!("localhost:{}", self.0) } } impl<'a, C: CommandRunner> NginxServer<'a, C, String, PathBuf> { pub fn new_redir(domain: &'a str, target: &'a str, command_runner: &'a C) -> Self { let content = server_config( domain, &format!( "location / {{ return 301 $scheme://{}$request_uri; }}", target ), ); Self::new(domain, content, command_runner) } pub fn new_proxy>( domain: &'a str, socket_path: &S, static_path: STATIC, command_runner: &'a C, ) -> Self { let proxy_content = format!( "location / {{ try_files $uri @proxy; }} location @proxy {{ include fastcgi_params; proxy_pass http://{}; proxy_redirect off; }}", socket_path.to_nginx() ); let content = server_config( domain, &format!( " root {}; {} ", static_path.as_ref().to_str().unwrap(), proxy_content ), ); Self::new(domain, content, command_runner) } pub fn new_php, STATIC: AsRef>( domain: &'a str, socket_path: SOCKET, static_path: STATIC, command_runner: &'a C, additional_config: &'a str, ) -> Self { let content = server_config( domain, &(php_server_config_snippet(socket_path, static_path) + additional_config), ); Self::new(domain, content, command_runner) } pub fn new_static>( domain: &'a str, static_path: S, command_runner: &'a C, ) -> Self { let content = server_config( domain, &format!( " root {}; try_files $uri $uri/ $uri.html =404; ", static_path.as_ref().to_str().unwrap() ), ); Self::new(domain, content, command_runner) } pub fn new(domain: &'a str, content: String, command_runner: &'a C) -> Self { let file_path: PathBuf = ["/etc/nginx/sites-enabled/", domain].iter().collect(); NginxServer { command_runner, file: FileSymbol::new(file_path, content), } } } impl, P: AsRef> Symbol for NginxServer<'_, C, T, P> { fn target_reached(&self) -> Result> { if !self.file.target_reached()? { return Ok(false); } // TODO: Could try to find out if the server is in the live config Ok(true) } fn execute(&self) -> Result<(), Box> { self.file.execute()?; self .command_runner .run_successfully("systemctl", args!["reload-or-restart", "nginx"]) } fn get_prerequisites(&self) -> Vec { self.file.get_prerequisites() } fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box { Box::new(SymbolAction::new(runner, self)) } fn into_action<'b>(self: Box, runner: &'b dyn SymbolRunner) -> Box where Self: 'b, { Box::new(OwnedSymbolAction::new(runner, *self)) } } impl, P: AsRef> fmt::Display for NginxServer<'_, C, T, P> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "Nginx server config") } }