A library for writing host-specific, single-binary configuration management and deployment tools
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

371 lines
11 KiB

6 years ago
7 years ago
6 years ago
7 years ago
7 years ago
6 years ago
7 years ago
6 years ago
5 years ago
6 years ago
5 years ago
7 years ago
5 years ago
7 years ago
6 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
7 years ago
6 years ago
5 years ago
7 years ago
6 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
6 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
5 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
5 years ago
5 years ago
6 years ago
5 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
6 years ago
5 years ago
6 years ago
7 years ago
  1. use std::borrow::Cow;
  2. use std::ops::Deref;
  3. use std::path::Path;
  4. use command_runner::{CommandRunner, SetuidCommandRunner};
  5. use storage::{SimpleStorage, Storage};
  6. use symbols::acme::{AcmeCert, AcmeCertChain};
  7. use symbols::file::File;
  8. use symbols::git::checkout::GitCheckout;
  9. use symbols::hook::Hook;
  10. use symbols::list::ListAction;
  11. use symbols::mariadb::{DatabaseDump, MariaDBDatabase, MariaDBUser};
  12. use symbols::nginx::server::{php_server_config_snippet, server_config, NginxServer};
  13. use symbols::owner::Owner;
  14. use symbols::stored_directory::{StorageDirection, StoredDirectory};
  15. use symbols::systemd::reload::ReloadService;
  16. use symbols::tls::SelfSignedTlsCert;
  17. use symbols::{Action, Symbol, SymbolRunner};
  18. pub trait Policy {
  19. fn user_name_for_host(&self, host_name: &'static str) -> String;
  20. fn home_for_user(&self, user_name: &str) -> String {
  21. format!("/home/{}", user_name)
  22. }
  23. }
  24. pub struct DefaultPolicy;
  25. impl Policy for DefaultPolicy {
  26. fn user_name_for_host(&self, host_name: &'static str) -> String {
  27. host_name.split('.').rev().fold(String::new(), |result, part| if result.is_empty() { result } else { result + "_" } + part)
  28. }
  29. }
  30. pub struct SymbolFactory<'a, C: 'a + CommandRunner, R: 'a + SymbolRunner, P: 'a + Policy> {
  31. command_runner: &'a C,
  32. acme_command_runner: SetuidCommandRunner<'a, C>,
  33. symbol_runner: &'a R,
  34. policy: &'a P,
  35. }
  36. impl<'b, C: 'b + CommandRunner, R: 'b + SymbolRunner, P: 'b + Policy> SymbolFactory<'b, C, R, P> {
  37. pub fn new(command_runner: &'b C, symbol_runner: &'b R, policy: &'b P) -> Self {
  38. let acme_user = "acme"; // FIXME: CONFIG
  39. let acme_command_runner = SetuidCommandRunner::new(acme_user, command_runner);
  40. SymbolFactory {
  41. command_runner,
  42. acme_command_runner,
  43. symbol_runner,
  44. policy,
  45. }
  46. }
  47. pub fn get_nginx_acme_server<'a, 'c: 'a, S: 'a + Symbol>(
  48. &'c self,
  49. host: &'static str,
  50. nginx_server_symbol: S,
  51. ) -> Box<dyn Action + 'a> {
  52. Box::new(ListAction::new(vec![
  53. Box::new(SelfSignedTlsCert::new(host.into(), self.command_runner))
  54. .into_action(self.symbol_runner),
  55. Box::new(Hook::new(
  56. nginx_server_symbol,
  57. ReloadService::new("nginx", self.command_runner),
  58. ))
  59. .into_action(self.symbol_runner),
  60. Box::new(AcmeCert::new(host.into(), &self.acme_command_runner))
  61. .into_action(self.symbol_runner),
  62. Box::new(Hook::new(
  63. AcmeCertChain::new(host.into(), &self.acme_command_runner),
  64. ReloadService::new("nginx", self.command_runner),
  65. ))
  66. .into_action(self.symbol_runner),
  67. ]))
  68. }
  69. pub fn get_nginx_acme_challenge_config<'a>(&'a self) -> Box<dyn Action + 'a> {
  70. Box::new(File::new(
  71. "/etc/nginx/snippets/acme-challenge.conf",
  72. "location ^~ /.well-known/acme-challenge/ {
  73. alias /home/acme/challenges/;
  74. try_files $uri =404;
  75. }",
  76. ))
  77. .into_action(self.symbol_runner)
  78. }
  79. fn get_php_fpm_pool_socket_path<'a>(&'a self, user_name: &str) -> String {
  80. format!("/run/php/{}.sock", user_name)
  81. }
  82. fn get_php_fpm_pool<'a>(&'a self, user_name: &str) -> Box<dyn Action + 'a> {
  83. let socket = self.get_php_fpm_pool_socket_path(user_name);
  84. Box::new(Hook::new(
  85. File::new(
  86. format!("/etc/php/7.0/fpm/pool.d/{}.conf", user_name),
  87. format!(
  88. "[{0}]
  89. user = {0}
  90. group = www-data
  91. listen = {1}
  92. listen.owner = www-data
  93. pm = ondemand
  94. pm.max_children = 10
  95. catch_workers_output = yes
  96. env[PATH] = /usr/local/bin:/usr/bin:/bin
  97. ",
  98. user_name, socket
  99. ),
  100. ),
  101. ReloadService::new("php7.0-fpm", self.command_runner),
  102. ))
  103. .into_action(self.symbol_runner)
  104. }
  105. pub fn serve_php<'a>(
  106. &'a self,
  107. host_name: &'static str,
  108. root_dir: Cow<'a, str>,
  109. ) -> Box<dyn Action + 'a> {
  110. let user_name = self.policy.user_name_for_host(host_name);
  111. let socket = self.get_php_fpm_pool_socket_path(&user_name);
  112. Box::new(ListAction::new(vec![
  113. self.get_php_fpm_pool(&user_name),
  114. self.get_nginx_acme_server(
  115. host_name,
  116. NginxServer::new_php(host_name, socket.into(), root_dir, self.command_runner),
  117. ),
  118. ]))
  119. }
  120. pub fn serve_wordpress<'a>(
  121. &'a self,
  122. host_name: &'static str,
  123. root_dir: Cow<'a, str>,
  124. ) -> Box<dyn Action + 'a> {
  125. let user_name = self.policy.user_name_for_host(host_name);
  126. let socket = self.get_php_fpm_pool_socket_path(&user_name);
  127. Box::new(ListAction::new(vec![
  128. self.get_php_fpm_pool(&user_name),
  129. self.get_nginx_acme_server(
  130. host_name,
  131. NginxServer::new(
  132. host_name,
  133. server_config(
  134. host_name,
  135. &format!(
  136. "{}
  137. location / {{
  138. try_files $uri $uri/ /index.php?$args;
  139. }}
  140. ",
  141. php_server_config_snippet(socket.into(), root_dir)
  142. ),
  143. ),
  144. self.command_runner,
  145. ),
  146. ),
  147. ]))
  148. }
  149. pub fn serve_dokuwiki<'a>(
  150. &'a self,
  151. host_name: &'static str,
  152. root_dir: &'static str,
  153. ) -> Box<dyn Action + 'a> {
  154. let user_name = self.policy.user_name_for_host(host_name);
  155. let socket = self.get_php_fpm_pool_socket_path(&user_name);
  156. Box::new(ListAction::new(vec![
  157. self.get_php_fpm_pool(&user_name),
  158. self.get_nginx_acme_server(host_name,
  159. NginxServer::new(
  160. host_name,
  161. server_config(host_name, &format!("
  162. root {};
  163. index doku.php;
  164. location ~ [^/]\\.php(/|$) {{
  165. fastcgi_pass unix:{};
  166. include \"snippets/fastcgi-php.conf\";
  167. }}
  168. location ~ /(data/|conf/|bin/|inc/|install.php) {{ deny all; }}
  169. location / {{ try_files $uri $uri/ @dokuwiki; }}
  170. location @dokuwiki {{
  171. # rewrites \"doku.php/\" out of the URLs if you set the userewrite setting to .htaccess in dokuwiki config page
  172. rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
  173. rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
  174. rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
  175. rewrite ^/(.*) /doku.php?id=$1&$args last;
  176. }}
  177. ",
  178. root_dir,
  179. socket)),
  180. self.command_runner
  181. ))
  182. ]))
  183. }
  184. pub fn serve_nextcloud<'a>(
  185. &'a self,
  186. host_name: &'static str,
  187. root_dir: Cow<'a, str>,
  188. ) -> Box<dyn Action + 'a> {
  189. let user_name = self.policy.user_name_for_host(host_name);
  190. let socket = self.get_php_fpm_pool_socket_path(&user_name);
  191. Box::new(ListAction::new(vec![
  192. self.get_php_fpm_pool(&user_name),
  193. self.get_nginx_acme_server(
  194. host_name,
  195. NginxServer::new(
  196. host_name,
  197. server_config(
  198. host_name,
  199. &format!(
  200. "{}
  201. client_max_body_size 500M;
  202. # Disable gzip to avoid the removal of the ETag header
  203. gzip off;
  204. rewrite ^/caldav(.*)$ /remote.php/caldav$1 redirect;
  205. rewrite ^/carddav(.*)$ /remote.php/carddav$1 redirect;
  206. rewrite ^/webdav(.*)$ /remote.php/webdav$1 redirect;
  207. error_page 403 /core/templates/403.php;
  208. error_page 404 /core/templates/404.php;
  209. location = /robots.txt {{
  210. allow all;
  211. log_not_found off;
  212. access_log off;
  213. }}
  214. location ~ ^/(?:\\.htaccess|data|config|db_structure\\.xml|README) {{
  215. deny all;
  216. }}
  217. location / {{
  218. # The following 2 rules are only needed with webfinger
  219. rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
  220. rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
  221. rewrite ^/.well-known/carddav /remote.php/carddav/ redirect;
  222. rewrite ^/.well-known/caldav /remote.php/caldav/ redirect;
  223. rewrite ^(/core/doc/[^\\/]+/)$ $1/index.html;
  224. try_files $uri $uri/ /index.php;
  225. }}
  226. # Adding the cache control header for js and css files
  227. # Make sure it is BELOW the location ~ \\.php(?:$|/) {{ block
  228. location ~* \\.(?:css|js)$ {{
  229. add_header Cache-Control \"public, max-age=7200\";
  230. # Optional: Don't log access to assets
  231. access_log off;
  232. }}
  233. # Optional: Don't log access to other assets
  234. location ~* \\.(?:jpg|jpeg|gif|bmp|ico|png|swf)$ {{
  235. access_log off;
  236. }}
  237. ",
  238. php_server_config_snippet(socket.into(), root_dir)
  239. ),
  240. ),
  241. self.command_runner,
  242. ),
  243. ),
  244. ]))
  245. }
  246. pub fn serve_redir<'a>(
  247. &'a self,
  248. host_name: &'static str,
  249. target: &'static str,
  250. ) -> Box<dyn Action + 'a> {
  251. self.get_nginx_acme_server(
  252. host_name,
  253. NginxServer::new_redir(host_name, target, self.command_runner),
  254. )
  255. }
  256. pub fn serve_static<'a>(&'a self, host_name: &'static str, dir: &'a str) -> Box<dyn Action + 'a> {
  257. self.get_nginx_acme_server(
  258. host_name,
  259. NginxServer::new_static(host_name, dir, self.command_runner),
  260. )
  261. }
  262. pub fn get_stored_directory<'a, T: Into<String>>(
  263. &'a self,
  264. storage_name: &'static str,
  265. target: T,
  266. ) -> (Box<dyn Action + 'a>, Box<dyn Action + 'a>) {
  267. let data = SimpleStorage::new("/root/data".to_string(), storage_name.to_string());
  268. let string_target = target.into();
  269. (
  270. Box::new(StoredDirectory::new(
  271. string_target.clone().into(),
  272. data.clone(),
  273. StorageDirection::Save,
  274. self.command_runner,
  275. ))
  276. .into_action(self.symbol_runner),
  277. Box::new(StoredDirectory::new(
  278. string_target.into(),
  279. data.clone(),
  280. StorageDirection::Load,
  281. self.command_runner,
  282. ))
  283. .into_action(self.symbol_runner),
  284. )
  285. }
  286. pub fn get_mariadb_database<'a>(&'a self, name: &'static str) -> Box<dyn Action + 'a> {
  287. let db_dump = SimpleStorage::new("/root/data".to_string(), format!("{}.sql", name));
  288. Box::new(ListAction::new(vec![
  289. Box::new(MariaDBDatabase::new(
  290. name.into(),
  291. db_dump
  292. .read_filename()
  293. .expect("Initial db dump missing")
  294. .into(),
  295. self.command_runner,
  296. ))
  297. .into_action(self.symbol_runner),
  298. Box::new(DatabaseDump::new(name, db_dump, self.command_runner))
  299. .into_action(self.symbol_runner),
  300. ]))
  301. }
  302. pub fn get_mariadb_user<'a>(&'a self, user_name: &'static str) -> Box<dyn Action + 'a> {
  303. Box::new(MariaDBUser::new(user_name.into(), self.command_runner))
  304. .into_action(self.symbol_runner)
  305. }
  306. pub fn get_git_checkout<'a, T: 'a + AsRef<str>>(
  307. &'a self,
  308. target: T,
  309. source: &'a str,
  310. branch: &'a str,
  311. ) -> Box<dyn Action + 'a> {
  312. Box::new(GitCheckout::new(
  313. target,
  314. source,
  315. branch,
  316. self.command_runner,
  317. ))
  318. .into_action(self.symbol_runner)
  319. }
  320. pub fn get_owner<'a, F: 'a + AsRef<str>>(
  321. &'a self,
  322. file: F,
  323. user: &'a str,
  324. ) -> Box<dyn Action + 'a> {
  325. Box::new(Owner::new(file, user.into(), self.command_runner)).into_action(self.symbol_runner)
  326. }
  327. pub fn get_file<'a, F: 'a + Deref<Target = str>, Q: 'a + AsRef<Path>>(
  328. &'a self,
  329. path: Q,
  330. content: F,
  331. ) -> Box<dyn Action + 'a> {
  332. Box::new(File::new(path, content)).into_action(self.symbol_runner)
  333. }
  334. }