use async_std::os::unix::net::UnixStream; 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; use async_std::io::WriteExt; use async_std::process::Command; 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) -> Result<()> { 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": "vars", "root": content_path.to_string() }, { "handler": "file_server", "precompressed": { "br": {}, "gzip": {} }, "precompressed_order": ["br", "gzip"] } ] } ] }], "terminal": true }); let mut request = Request::post(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.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) -> 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": [":80"], "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) -> 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); 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, }) }