use std::borrow::Cow; use std::path::Path; use std::path::PathBuf; use crate::command_runner::CommandRunner; use crate::storage::{SimpleStorage, Storage}; use crate::symbols::acme::Factory as AcmeFactory; use crate::symbols::cron::Cron; use crate::symbols::file::File; use crate::symbols::git::checkout::GitCheckout; use crate::symbols::hook::Hook; use crate::symbols::list::List; use crate::symbols::mariadb::{DatabaseDump, MariaDBDatabase, MariaDBUser}; use crate::symbols::nginx::server::{server_config, NginxServer}; use crate::symbols::owner::Owner; use crate::symbols::stored_directory::{StorageDirection, StoredDirectory}; use crate::symbols::systemd::reload::ReloadService; use crate::symbols::systemd::user_service::UserService; use crate::symbols::tls::SelfSignedTlsCert; use crate::symbols::Symbol; pub trait Policy { fn user_name_for_host(&self, host_name: &'static str) -> String { host_name.split('.').rev().fold(String::new(), |result, part| if result.is_empty() { result } else { result + "_" } + part) } fn home_for_user(&self, user_name: &str) -> PathBuf { format!("/home/{}", user_name).into() } fn socket_path(&self, user_name: &str, service_name: &str) -> PathBuf { format!("/var/tmp/{}-{}.socket", user_name, service_name).into() } fn acme_user(&self) -> Cow<'_, str> { "acme".into() } fn php_version(&self) -> &'static str { "7.0" } } pub struct DefaultPolicy; impl Policy for DefaultPolicy {} pub struct SymbolFactory<'a, C: CommandRunner, P: Policy> { command_runner: &'a C, acme_factory: AcmeFactory<'a, Cow<'a, str>, PathBuf, &'a str, C>, policy: &'a P, } impl<'b, C: 'b + CommandRunner, P: 'b + Policy> SymbolFactory<'b, C, P> { pub fn new(command_runner: &'b C, policy: &'b P, cert: &'b str) -> Self { let acme_user = policy.acme_user(); let acme_home = policy.home_for_user(&acme_user); let acme_factory = AcmeFactory::new(acme_user, acme_home, cert, command_runner); SymbolFactory { command_runner, acme_factory, policy, } } pub fn get_cert<'a, H: 'a + AsRef + Clone>(&'a self, host: H) -> impl Symbol + 'a { self.acme_factory.get_cert(host) } pub fn get_nginx_acme_server<'a, S: 'a + Symbol>( &'a self, host: &'a str, nginx_server_symbol: S, ) -> impl Symbol + 'a { List::from(( SelfSignedTlsCert::new(host, self.command_runner), Hook::new( nginx_server_symbol, ReloadService::new("nginx", self.command_runner), ), Hook::new( self.get_cert(host), ReloadService::new("nginx", self.command_runner), ), )) } pub fn get_nginx_acme_challenge_config<'a>(&'a self) -> impl Symbol + 'a { File::new( "/etc/nginx/snippets/acme-challenge.conf", format!( "location ^~ /.well-known/acme-challenge/ {{ alias {}/; try_files $uri =404; }}", self.acme_factory.get_challenges_dir().to_str().unwrap() ), ) } fn get_php_fpm_pool_socket_path<'a>(&'a self, user_name: &str) -> PathBuf { format!("/run/php/{}.sock", user_name).into() } fn get_php_fpm_pool<'a>(&'a self, user_name: &str, max_children: usize) -> impl Symbol + 'a { let socket = self.get_php_fpm_pool_socket_path(user_name); let php_version = self.policy.php_version(); Hook::new( File::new( format!("/etc/php/{}/fpm/pool.d/{}.conf", php_version, user_name), format!( "[{0}] user = {0} group = www-data listen = {1} listen.owner = www-data pm = ondemand pm.max_children = {2} catch_workers_output = yes env[PATH] = /usr/local/bin:/usr/bin:/bin ", user_name, socket.to_str().unwrap(), max_children ), ), ReloadService::new(format!("php{}-fpm", php_version), self.command_runner), ) } pub fn serve_php<'a, ROOT: AsRef>( &'a self, host_name: &'static str, root_dir: ROOT, max_children: usize, additional_config: &'a str, ) -> impl Symbol + 'a { let user_name = self.policy.user_name_for_host(host_name); let socket = self.get_php_fpm_pool_socket_path(&user_name); List::from(( self.get_php_fpm_pool(&user_name, max_children), self.get_nginx_acme_server( host_name, NginxServer::new_php( host_name, socket, root_dir, self.command_runner, additional_config, ), ), )) } pub fn serve_wordpress<'a, ROOT: 'a + AsRef>( &'a self, host_name: &'static str, root_dir: ROOT, ) -> impl Symbol + 'a { self.serve_php( host_name, root_dir, 10, " location / {{ try_files $uri $uri/ /index.php?$args; }} ", ) } pub fn serve_dokuwiki<'a>( &'a self, host_name: &'static str, root_dir: &'static str, ) -> impl Symbol + 'a { let user_name = self.policy.user_name_for_host(host_name); let socket = self.get_php_fpm_pool_socket_path(&user_name); List::from(( self.get_php_fpm_pool(&user_name, 10), self.get_nginx_acme_server(host_name, NginxServer::new( host_name, server_config(host_name, &format!(" root {}; index doku.php; location ~ [^/]\\.php(/|$) {{ fastcgi_pass unix:{}; include \"snippets/fastcgi-php.conf\"; }} location ~ /(data/|conf/|bin/|inc/|install.php) {{ deny all; }} location / {{ try_files $uri $uri/ @dokuwiki; }} location @dokuwiki {{ # rewrites \"doku.php/\" out of the URLs if you set the userewrite setting to .htaccess in dokuwiki config page rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last; rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last; rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last; rewrite ^/(.*) /doku.php?id=$1&$args last; }} ", root_dir, socket.to_str().unwrap())), self.command_runner )) )) } pub fn serve_nextcloud<'a, ROOT: 'a + AsRef>( &'a self, host_name: &'static str, root_dir: ROOT, ) -> impl Symbol + 'a { self.serve_php( host_name, root_dir, 25, " client_max_body_size 500M; # Disable gzip to avoid the removal of the ETag header gzip off; rewrite ^/caldav(.*)$ /remote.php/caldav$1 redirect; rewrite ^/carddav(.*)$ /remote.php/carddav$1 redirect; rewrite ^/webdav(.*)$ /remote.php/webdav$1 redirect; error_page 403 /core/templates/403.php; error_page 404 /core/templates/404.php; location = /robots.txt { allow all; log_not_found off; access_log off; } location ~ ^/(?:\\.htaccess|data|config|db_structure\\.xml|README) { deny all; } location / { # The following 2 rules are only needed with webfinger rewrite ^/.well-known/host-meta /public.php?service=host-meta last; rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last; rewrite ^/.well-known/carddav /remote.php/carddav/ redirect; rewrite ^/.well-known/caldav /remote.php/caldav/ redirect; rewrite ^(/core/doc/[^\\/]+/)$ $1/index.html; try_files $uri $uri/ /index.php; } # Adding the cache control header for js and css files # Make sure it is BELOW the location ~ \\.php(?:$|/) { block location ~* \\.(?:css|js)$ { add_header Cache-Control \"public, max-age=7200\"; # Optional: Don't log access to assets access_log off; } # Optional: Don't log access to other assets location ~* \\.(?:jpg|jpeg|gif|bmp|ico|png|swf)$ { access_log off; } ", ) } pub fn serve_redir<'a>( &'a self, host_name: &'static str, target: &'static str, ) -> impl Symbol + 'a { self.get_nginx_acme_server( host_name, NginxServer::new_redir(host_name, target, self.command_runner), ) } pub fn serve_static<'a>(&'a self, host_name: &'static str, dir: &'a str) -> impl Symbol + 'a { self.get_nginx_acme_server( host_name, NginxServer::new_static(host_name, dir, self.command_runner), ) } pub fn get_stored_directory<'a, T: Into>( &'a self, storage_name: &'static str, target: T, ) -> (impl Symbol + 'a, impl Symbol + 'a) { let data = SimpleStorage::new("/root/data".to_string(), storage_name.to_string()); let string_target = target.into(); ( StoredDirectory::new( string_target.clone(), data.clone(), StorageDirection::Save, self.command_runner, ), StoredDirectory::new( string_target, data, StorageDirection::Load, self.command_runner, ), ) } pub fn get_mariadb_database<'a>(&'a self, name: &'static str) -> impl Symbol + 'a { let db_dump = SimpleStorage::new("/root/data".to_string(), format!("{}.sql", name)); List::from(( MariaDBDatabase::new( name, db_dump.read_filename().expect("Initial db dump missing"), self.command_runner, ), DatabaseDump::new(name, db_dump, self.command_runner), )) } pub fn get_mariadb_user<'a>(&'a self, user_name: &'static str) -> impl Symbol + 'a { MariaDBUser::new(user_name, self.command_runner) } pub fn get_git_checkout<'a, T: 'a + AsRef>( &'a self, target: T, source: &'a str, branch: &'a str, ) -> impl Symbol + 'a { GitCheckout::new(target, source, branch, self.command_runner) } pub fn get_owner<'a, U: 'a + AsRef, F: 'a + AsRef>( &'a self, file: F, user: U, ) -> impl Symbol + 'a { Owner::new(file, user, self.command_runner) } pub fn get_file<'a, F: 'a + AsRef, Q: 'a + AsRef>( &'a self, path: Q, content: F, ) -> impl Symbol + 'a { File::new(path, content) } pub fn get_cron<'a, T: 'a + AsRef, U: 'a + AsRef>( &'a self, user: U, content: T, ) -> impl Symbol + 'a { Cron::new(user, content, self.command_runner) } pub fn get_acme_user<'a>(&'a self) -> impl Symbol + 'a { self.acme_factory.get_init() } pub fn get_systemd_user_service<'a, U: 'a + AsRef + Clone>( &'a self, user_name: U, service_name: &'a str, config: String, ) -> impl Symbol + 'a { let socket_path = self.policy.socket_path(user_name.as_ref(), service_name); let home = self.policy.home_for_user(user_name.as_ref()); UserService::new( socket_path, &home, user_name, service_name, self.command_runner, config, ) } fn get_nodejs_systemd_user_service<'a, U: 'a + AsRef + Clone>( &'a self, user_name: U, service_name: &'a str, path: &'a Path, ) -> impl Symbol + 'a { let socket_path = self.policy.socket_path(user_name.as_ref(), service_name); let home = self.policy.home_for_user(user_name.as_ref()); UserService::new_nodejs( &home, user_name, service_name, path, self.command_runner, socket_path, ) } pub fn proxy_socket<'a, T: AsRef>( &'a self, host_name: &'static str, service_name: &'a str, root_dir: T, ) -> impl Symbol + 'a { let user_name = self.policy.user_name_for_host(host_name); let socket_path = self.policy.socket_path(&user_name, service_name); self.get_nginx_acme_server( host_name, NginxServer::new_proxy(host_name, &socket_path, root_dir, self.command_runner), ) } pub fn serve_nodejs<'a, 'c: 'a, S: 'a + Symbol, SP: 'a + AsRef>( &'c self, host: &'static str, service_name: &'a str, path: &'a Path, static_path: SP, nodejs_symbol: S, ) -> impl Symbol + 'a { let user_name = self.policy.user_name_for_host(host); List::from(( Hook::new( nodejs_symbol, self.get_nodejs_systemd_user_service(user_name, service_name, path), ), self.proxy_socket(host, service_name, static_path), )) } }