use crate::sites::{SITE_VERSION_CONFIG_FILE_NAME, SiteConfig, SiteConfigPathsMode, SiteConfigRedirectKind}; use async_std::io::WriteExt; use async_std::os::unix::net::UnixStream; use async_std::process::Command; use camino::{Utf8Path, Utf8PathBuf}; use color_eyre::Result; use color_eyre::eyre::{OptionExt, eyre}; use http_client::http_types::{Method, StatusCode, Url}; use http_client::{Request, Response}; use log::debug; use serde_json::json; use std::process::Stdio; use std::str::FromStr; use std::time::Duration; pub struct CaddyController { api_socket_path: Utf8PathBuf, admin_api_socket_path: Utf8PathBuf, } impl CaddyController { pub async fn upsert_site_configuration(&mut self, domain: &String, content_path: &Utf8Path, config: &SiteConfig) -> Result<()> { let mut routes_array = vec![]; routes_array.push(json!({ "handle": [{ "handler": "vars", "root": content_path.to_string() }] })); for redirect in &config.redirects { let handler = match redirect.kind { SiteConfigRedirectKind::Temporary => json!({ "handler": "static_response", "headers": { "Location": [redirect.to] }, "status_code": 307 }), SiteConfigRedirectKind::Permanent => json!({ "handler": "static_response", "headers": { "Location": [redirect.to] }, "status_code": 308 }), SiteConfigRedirectKind::Rewrite => json!({ "handler": "rewrite", "uri": redirect.to }), }; routes_array.push(json!({ "match": [{ "path": [redirect.from] }], "handle": [handler] })) } if config.paths_mode == SiteConfigPathsMode::Spa { // Redirect to URL without trailing slashes and "/index.html" routes_array.push(json!({ "match": [{ "path": ["*/index.html", "*/"], "not": [{ "path": ["/"] }] }], "handle": [ { "handler": "rewrite", "strip_path_suffix": "/index.html" }, { "handler": "rewrite", "strip_path_suffix": "/" }, { "handler": "static_response", "headers": { "Location": ["{http.request.uri}"] }, "status_code": 308 } ] })); routes_array.push(json!({ "match": [{ "file": { "try_files": ["{http.request.uri.path}", "{http.request.uri.path}/index.html", "/index.html"]} }], "handle": [{ "handler": "rewrite", "uri": "{http.matchers.file.relative}" }] })); } routes_array.push(json!({ "handle": [{ "handler": "file_server", "index_names": if config.paths_mode == SiteConfigPathsMode::Normal { json!(["index.html"]) } else { json!([]) }, "precompressed": { "br": {}, "gzip": {} }, "hide": [ content_path.join(SITE_VERSION_CONFIG_FILE_NAME).to_string() ], "precompressed_order": ["br", "gzip"] }] })); let configuration_object = json!({ "@id": domain, "match": [{ "host": [domain] }], "handle": [{ "handler": "subroute", "routes": [ { "group": "1", "match": [{ "path": ["/_sscdc/*"] }], "handle": [ { "handler": "rewrite", "strip_path_prefix": "/_sscdc" }, { "handler": "reverse_proxy", "upstreams": [{ "dial": format!("unix/{}", &self.api_socket_path) }], } ] }, { "group": "1", "handle": [ { "handler": "subroute", "routes": routes_array } ] } ] }], "terminal": true }); let mut request = Request::patch(Url::from_str(&format!("http://localhost/id/{}", domain))?); request.insert_header("Content-Type", "application/json"); request.set_body(configuration_object.to_string()); let mut response = request_uds(&self.admin_api_socket_path, request.clone()).await.unwrap(); match response.status() { StatusCode::Ok => { debug!("Caddy configuration for {domain} was updated.") } StatusCode::NotFound => { // The site does not yet exist. request.set_method(Method::Post); *request.url_mut() = Url::from_str("http://localhost/config/apps/http/servers/srv0/routes")?; let mut response = request_uds(&self.admin_api_socket_path, request).await.unwrap(); let status = response.status(); if !status.is_success() { return Err(eyre!( "The configuration update request to Caddy failed with status code {}:\n{}", status, response.body_string().await.unwrap() )); } debug!("Caddy configuration for {domain} was created.") } c => { return Err(eyre!( "The configuration update request to Caddy failed with status code {}:\n{}", c, response.body_string().await.unwrap() )); } } Ok(()) } } fn get_initial_caddy_configuration_object(admin_api_socket_path: &Utf8Path, api_socket_path: &Utf8Path, http_port: u16) -> serde_json::Value { json!({ "admin": { "listen": format!("unix/{}", admin_api_socket_path), "enforce_origin": false, "origins": ["localhost"], "config": { "persist": false }, }, "logging": { "sink": { "writer": { "output": "discard" } }, "logs": { "": { "writer": { "output": "discard" } } } }, "storage": { "module": "file_system", "root": "/tmp/sscdc-caddy" }, "apps": { "http": { "servers": { "srv0": { "listen": [format!(":{http_port}")], "routes": [{ "match": [{ "path": ["/_sscdc/*"] }], "handle": [ { "handler": "rewrite", "strip_path_prefix": "/_sscdc" }, { "handler": "reverse_proxy", "upstreams": [{ "dial": format!("unix/{}", api_socket_path) }], } ] }], "automatic_https": { "disable": true } } } } } }) } async fn request_uds(path: &Utf8Path, request: Request) -> http_client::http_types::Result { let stream = UnixStream::connect(path.as_std_path()).await?; async_h1::connect(stream, request).await } pub async fn start_caddy(api_socket_path: &Utf8Path, sockets_directory_path: &Utf8Path, http_port: u16) -> Result { let caddy_admin_socket_path = sockets_directory_path.join("caddy-admin-api.sock"); // Stop not properly cleaned up proxy. let _ = request_uds(&caddy_admin_socket_path, Request::new(Method::Post, "http://localhost/stop")).await; let caddy_path = option_env!("CADDY_PATH").unwrap_or("caddy"); debug!("Caddy path: {}", caddy_path); // Spawn a new proxy. let process = Command::new(caddy_path).args(&["run", "--config", "-"]).stdin(Stdio::piped()).spawn()?; let initial_configuration_object = get_initial_caddy_configuration_object(&caddy_admin_socket_path, api_socket_path, http_port); let initial_configuration_string = initial_configuration_object.to_string(); debug!("Initial caddy configuration: {}", initial_configuration_string); process .stdin .ok_or_eyre("The Caddy process STDIN is not open.")? .write_all(initial_configuration_string.as_ref()) .await?; async_std::task::sleep(Duration::from_secs(1)).await; Ok(CaddyController { api_socket_path: api_socket_path.to_path_buf(), admin_api_socket_path: caddy_admin_socket_path, }) }