use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::cli::CliArgs;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
#[serde(rename_all = "camelCase")]
pub struct ServeJsonConfig {
pub public: Option<String>,
#[serde(default)]
pub clean_urls: CleanUrlsConfig,
#[serde(default)]
pub rewrites: Vec<RewriteRule>,
#[serde(default)]
pub redirects: Vec<RedirectRule>,
#[serde(default)]
pub headers: Vec<HeaderRule>,
#[serde(default)]
pub directory_listing: DirectoryListingConfig,
#[serde(default)]
pub unlisted: Vec<String>,
pub trailing_slash: Option<bool>,
pub render_single: Option<bool>,
pub symlinks: Option<bool>,
pub etag: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CleanUrlsConfig {
Bool(bool),
Patterns(Vec<String>),
}
impl Default for CleanUrlsConfig {
fn default() -> Self {
CleanUrlsConfig::Bool(false)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum DirectoryListingConfig {
Bool(bool),
Patterns(Vec<String>),
}
impl Default for DirectoryListingConfig {
fn default() -> Self {
DirectoryListingConfig::Bool(true)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RewriteRule {
pub source: String,
pub destination: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedirectRule {
pub source: String,
pub destination: String,
#[serde(rename = "type", default = "default_redirect_type")]
pub status_type: u16,
}
fn default_redirect_type() -> u16 {
301
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderRule {
pub source: String,
pub headers: Vec<HeaderEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderEntry {
pub key: String,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct AppConfig {
pub public: String,
pub endpoints: Vec<EndpointConfig>,
pub single: bool,
pub debug: bool,
pub no_request_logging: bool,
pub cors: bool,
pub no_clipboard: bool,
pub no_compression: bool,
pub no_etag: bool,
pub symlinks: bool,
pub ssl_cert: Option<String>,
pub ssl_key: Option<String>,
pub ssl_pass: Option<String>,
pub no_port_switching: bool,
pub clean_urls: CleanUrlsConfig,
pub rewrites: Vec<RewriteRule>,
pub redirects: Vec<RedirectRule>,
pub custom_headers: Vec<HeaderRule>,
pub directory_listing: DirectoryListingConfig,
pub unlisted: Vec<String>,
pub trailing_slash: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct EndpointConfig {
pub host: String,
pub port: u16,
}
impl Default for EndpointConfig {
fn default() -> Self {
EndpointConfig {
host: "0.0.0.0".to_string(),
port: 3000,
}
}
}
pub fn parse_listen_uri(uri: &str) -> Option<EndpointConfig> {
if let Ok(port) = uri.parse::<u16>() {
return Some(EndpointConfig {
host: "0.0.0.0".to_string(),
port,
});
}
if let Some(stripped) = uri.strip_prefix("tcp://") {
let parts: Vec<&str> = stripped.rsplitn(2, ':').collect();
let port = parts[0].parse::<u16>().ok()?;
let host = if parts.len() > 1 { parts[1].to_string() } else { "0.0.0.0".to_string() };
let host = if host.is_empty() { "0.0.0.0".to_string() } else { host };
return Some(EndpointConfig { host, port });
}
let parts: Vec<&str> = uri.rsplitn(2, ':').collect();
if parts.len() == 2 {
if let Ok(port) = parts[0].parse::<u16>() {
return Some(EndpointConfig {
host: parts[1].to_string(),
port,
});
}
}
None
}
pub fn load_config(args: &CliArgs) -> AppConfig {
let mut config = AppConfig {
public: args.directory.clone(),
endpoints: vec![],
single: args.single,
debug: args.debug,
no_request_logging: args.no_request_logging,
cors: args.cors,
no_clipboard: args.no_clipboard,
no_compression: args.no_compression,
no_etag: args.no_etag,
symlinks: args.symlinks,
ssl_cert: args.ssl_cert.clone(),
ssl_key: args.ssl_key.clone(),
ssl_pass: args.ssl_pass.clone(),
no_port_switching: args.no_port_switching,
clean_urls: CleanUrlsConfig::Bool(false),
rewrites: vec![],
redirects: vec![],
custom_headers: vec![],
directory_listing: DirectoryListingConfig::Bool(true),
unlisted: vec![],
trailing_slash: None,
};
if !args.listen.is_empty() {
config.endpoints = args
.listen
.iter()
.filter_map(|uri| parse_listen_uri(uri))
.collect();
} else if let Some(port) = args.port {
config.endpoints.push(EndpointConfig {
host: "0.0.0.0".to_string(),
port,
});
}
if config.endpoints.is_empty() {
config.endpoints.push(EndpointConfig::default());
}
let config_path = args
.config
.clone()
.unwrap_or_else(|| Path::new(&config.public).join("serve.json").to_string_lossy().to_string());
if let Ok(contents) = std::fs::read_to_string(&config_path) {
if let Ok(serve_config) = serde_json::from_str::<ServeJsonConfig>(&contents) {
if let Some(public) = serve_config.public {
config.public = public;
}
if serve_config.render_single.unwrap_or(false) {
config.single = true;
}
if let Some(symlinks) = serve_config.symlinks {
config.symlinks = symlinks;
}
if let Some(etag) = serve_config.etag {
config.no_etag = !etag;
}
if let Some(ts) = serve_config.trailing_slash {
config.trailing_slash = Some(ts);
}
config.clean_urls = serve_config.clean_urls;
config.unlisted = serve_config.unlisted;
config.directory_listing = serve_config.directory_listing;
let mut all_rewrites = serve_config.rewrites;
if config.single {
all_rewrites.insert(
0,
RewriteRule {
source: "**".to_string(),
destination: "/index.html".to_string(),
},
);
}
config.rewrites = all_rewrites;
config.redirects = serve_config.redirects;
config.custom_headers = serve_config.headers;
} else if config.debug {
eprintln!("WARN: Failed to parse {}", config_path);
}
} else if config.debug {
if args.config.is_some() {
eprintln!("WARN: Config file not found: {}", config_path);
}
}
if config.single && config.rewrites.is_empty() {
config.rewrites.push(RewriteRule {
source: "**".to_string(),
destination: "/index.html".to_string(),
});
}
config
}