sscdc/src/caddy.rs
Moritz Ruth 0718608de1
All checks were successful
Build / build (push) Successful in 1m51s
Fix Caddy configuration patching
2025-03-02 16:30:28 +01:00

256 lines
9.4 KiB
Rust

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<Response> {
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<CaddyController> {
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,
})
}