256 lines
9.4 KiB
Rust
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,
|
|
})
|
|
}
|