use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
use anyhow::{Context, Result};
use axum::{
extract::Request,
middleware,
middleware::Next,
response::{IntoResponse, Response},
routing::{delete, get, head, patch, post},
Router,
};
use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer};
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use tracing::{info, warn};
use crate::{
auth::{require_auth, require_master_key},
handlers::{
audit_events, create_secret, create_webhook, delete_secret, delete_webhook, get_secret,
head_secret, health, list_secrets, list_webhooks, patch_secret, prune_secrets,
},
license,
org_handlers::{
create_key, create_org, create_org_secret, create_org_webhook, create_principal,
create_principal_key, create_role, delete_key, delete_org, delete_org_secret,
delete_org_webhook, delete_principal, delete_role, get_me, get_org_secret, head_org_secret,
list_org_secrets, list_org_webhooks, list_orgs, list_principals, list_roles,
org_audit_events, patch_me, patch_org_secret, prune_org_secrets,
},
AppState,
};
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub api_key: Option<String>,
pub license_key: Option<String>,
pub data_dir: Option<PathBuf>,
pub sweep_interval: Duration,
pub cors_origins: Option<String>,
pub cors_methods: Option<String>,
pub audit_retention_days: u64,
pub validation_url: String,
pub validation_cache_secs: u64,
pub heartbeat: bool,
pub webhook_secret: Option<String>,
pub instance_id: Option<String>,
pub log_level: String,
pub no_banner: bool,
pub version: String,
pub redact_audit_keys: bool,
pub webhook_allowed_origins: String,
pub trusted_proxies: String,
pub rate_limit_per_second: u64,
pub rate_limit_burst: u32,
pub auto_generated_key: Option<String>,
pub no_security_banner: bool,
pub enable_public_bucket: bool,
pub auto_init: bool,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: std::env::var("SIRR_HOST").unwrap_or_else(|_| "0.0.0.0".into()),
port: std::env::var("SIRR_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(39999),
api_key: std::env::var("SIRR_MASTER_API_KEY").ok(),
license_key: std::env::var("SIRR_LICENSE_KEY").ok(),
data_dir: std::env::var("SIRR_DATA_DIR").ok().map(PathBuf::from),
sweep_interval: Duration::from_secs(300),
cors_origins: std::env::var("SIRR_CORS_ORIGINS").ok(),
cors_methods: std::env::var("SIRR_CORS_METHODS").ok(),
audit_retention_days: std::env::var("SIRR_AUDIT_RETENTION_DAYS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(30),
validation_url: std::env::var("SIRR_VALIDATION_URL")
.unwrap_or_else(|_| "https://sirrlock.com/api/validate".into()),
validation_cache_secs: std::env::var("SIRR_VALIDATION_CACHE_SECS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(3600),
heartbeat: std::env::var("SIRR_HEARTBEAT")
.map(|v| v != "false" && v != "0")
.unwrap_or(true),
webhook_secret: std::env::var("SIRR_WEBHOOK_SECRET").ok(),
instance_id: std::env::var("SIRR_INSTANCE_ID").ok(),
log_level: std::env::var("SIRR_LOG_LEVEL").unwrap_or_else(|_| "warn".into()),
version: env!("CARGO_PKG_VERSION").to_string(),
no_banner: std::env::var("SIRR_NO_BANNER")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false),
webhook_allowed_origins: std::env::var("SIRR_WEBHOOK_ALLOWED_ORIGINS")
.unwrap_or_default(),
redact_audit_keys: std::env::var("SIRR_AUDIT_REDACT_KEYS")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false),
trusted_proxies: std::env::var("SIRR_TRUSTED_PROXIES").unwrap_or_default(),
rate_limit_per_second: std::env::var("SIRR_RATE_LIMIT_PER_SECOND")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(10),
rate_limit_burst: std::env::var("SIRR_RATE_LIMIT_BURST")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(30),
auto_generated_key: None,
no_security_banner: std::env::var("SIRR_NO_SECURITY_BANNER")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false),
enable_public_bucket: std::env::var("SIRR_ENABLE_PUBLIC_BUCKET")
.map(|v| v != "false" && v != "0")
.unwrap_or(true),
auto_init: std::env::var("SIRR_AUTOINIT")
.map(|v| v == "true" || v == "1")
.unwrap_or(false),
}
}
}
pub fn read_key_file(path: &std::path::Path) -> Result<String> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("read key file: {}", path.display()))?;
let key = content.trim().to_string();
if key.is_empty() {
anyhow::bail!("key file is empty: {}", path.display());
}
Ok(key)
}
pub fn resolve_master_key() -> Result<String> {
if let Ok(path) = std::env::var("SIRR_MASTER_ENCRYPTION_KEY_FILE") {
let key = read_key_file(std::path::Path::new(&path))?;
if std::env::var("SIRR_MASTER_ENCRYPTION_KEY").is_ok() {
tracing::warn!("both SIRR_MASTER_ENCRYPTION_KEY and SIRR_MASTER_ENCRYPTION_KEY_FILE are set; using file");
}
return Ok(key);
}
std::env::var("SIRR_MASTER_ENCRYPTION_KEY")
.context("SIRR_MASTER_ENCRYPTION_KEY or SIRR_MASTER_ENCRYPTION_KEY_FILE environment variable is required")
}
pub fn resolve_data_dir(data_dir: Option<&PathBuf>) -> Result<PathBuf> {
match data_dir {
Some(d) => {
std::fs::create_dir_all(d).context("create data dir")?;
Ok(d.clone())
}
None => {
let d = std::env::var("SIRR_DATA_DIR").ok().map(PathBuf::from);
match d {
Some(d) => {
std::fs::create_dir_all(&d).context("create data dir")?;
Ok(d)
}
None => crate::dirs::data_dir(),
}
}
}
}
pub async fn run(cfg: ServerConfig) -> Result<()> {
let data_dir = resolve_data_dir(cfg.data_dir.as_ref())?;
info!(data_dir = %data_dir.display(), "using data directory");
let key_path = data_dir.join("sirr.key");
let enc_key = load_or_create_key(&data_dir)?;
let key_bytes_for_id = std::fs::read(&key_path).ok();
let db_path = data_dir.join("sirr.db");
let store = crate::store::Store::open(&db_path, enc_key).context("open store")?;
if cfg.auto_init {
auto_init_bootstrap(&store)?;
}
let webhook_instance_id = cfg
.instance_id
.clone()
.unwrap_or_else(|| gethostname().unwrap_or_else(|| "unknown".into()));
let webhook_allowed_origins: Vec<String> = cfg
.webhook_allowed_origins
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
let webhook_allowed_origins = std::sync::Arc::new(webhook_allowed_origins);
let webhook_sender = crate::webhooks::WebhookSender::new(
store.clone(),
webhook_instance_id,
cfg.webhook_secret.clone(),
webhook_allowed_origins.clone(),
);
store
.clone()
.spawn_sweep(cfg.sweep_interval, Some(webhook_sender.clone()));
let retention_secs = (cfg.audit_retention_days * 86400) as i64;
store
.clone()
.spawn_audit_sweep(cfg.sweep_interval, retention_secs);
let lic_status = license::effective_status(cfg.license_key.as_deref());
match &lic_status {
license::LicenseStatus::Free => {
info!("running on free tier (Solo limits)");
}
license::LicenseStatus::Licensed(tier) => {
info!(?tier, "license key accepted");
}
license::LicenseStatus::Invalid(reason) => {
anyhow::bail!("invalid SIRR_LICENSE_KEY: {reason}");
}
}
print_banner(&cfg, &data_dir, &lic_status);
print_security_notice(&cfg);
let heartbeat_url = cfg
.validation_url
.replace("/api/validate", "/api/instances/heartbeat");
let validator = if matches!(lic_status, license::LicenseStatus::Licensed(_)) {
if let Some(ref key) = cfg.license_key {
let v = crate::validator::OnlineValidator::new(
key.clone(),
cfg.validation_url,
cfg.validation_cache_secs,
259200, );
let valid = v.validate_startup(&store).await;
if !valid {
warn!("license rejected online — server will enforce free-tier limits above 100 secrets");
}
Some(v)
} else {
None
}
} else {
None
};
if cfg.heartbeat {
if let (Some(ref license_key), Some(ref raw_bytes)) = (&cfg.license_key, &key_bytes_for_id)
{
let instance_id = crate::heartbeat::instance_id_from_key(raw_bytes);
info!(instance_id = %instance_id, "starting instance heartbeat");
crate::heartbeat::spawn_heartbeat(crate::heartbeat::HeartbeatConfig {
endpoint: heartbeat_url,
license_key: license_key.clone(),
instance_id,
store: store.clone(),
});
}
}
let trusted_proxies: Vec<ipnet::IpNet> = cfg
.trusted_proxies
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.filter_map(|s| {
s.parse().ok().or_else(|| {
s.parse::<std::net::IpAddr>().ok().map(ipnet::IpNet::from)
})
})
.collect();
if !trusted_proxies.is_empty() {
info!(
proxies = ?trusted_proxies,
"X-Forwarded-For trusted for listed proxy CIDRs"
);
}
let enable_public_bucket = cfg.enable_public_bucket;
let state = AppState {
store,
api_key: cfg.api_key,
license: lic_status,
validator,
webhook_sender: Some(webhook_sender),
trusted_proxies: std::sync::Arc::new(trusted_proxies),
redact_audit_keys: cfg.redact_audit_keys,
webhook_allowed_origins,
enable_public_bucket,
};
let governor_conf = GovernorConfigBuilder::default()
.per_second(cfg.rate_limit_per_second)
.burst_size(cfg.rate_limit_burst)
.finish()
.expect("invalid rate-limit configuration");
let governor_limiter = governor_conf.limiter().clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60));
loop {
interval.tick().await;
governor_limiter.retain_recent();
}
});
let cors = build_cors(cfg.cors_origins.as_deref(), cfg.cors_methods.as_deref());
let public = Router::new()
.route("/health", get(health))
.route("/robots.txt", get(robots_txt))
.route("/security.txt", get(security_txt))
.route("/.well-known/security.txt", get(security_txt))
.layer(cors.clone());
let org_protected = Router::new()
.route("/orgs", post(create_org))
.route("/orgs", get(list_orgs))
.route("/orgs/{org_id}", delete(delete_org))
.route("/orgs/{org_id}/principals", post(create_principal))
.route("/orgs/{org_id}/principals", get(list_principals))
.route("/orgs/{org_id}/principals/{id}", delete(delete_principal))
.route(
"/orgs/{org_id}/principals/{id}/keys",
post(create_principal_key),
)
.route("/orgs/{org_id}/roles", post(create_role))
.route("/orgs/{org_id}/roles", get(list_roles))
.route("/orgs/{org_id}/roles/{name}", delete(delete_role))
.route("/me", get(get_me))
.route("/me", patch(patch_me))
.route("/me/keys", post(create_key))
.route("/me/keys/{key_id}", delete(delete_key))
.route("/orgs/{org_id}/secrets", post(create_org_secret))
.route("/orgs/{org_id}/secrets", get(list_org_secrets))
.route("/orgs/{org_id}/secrets/{key}", get(get_org_secret))
.route("/orgs/{org_id}/secrets/{key}", head(head_org_secret))
.route("/orgs/{org_id}/secrets/{key}", patch(patch_org_secret))
.route("/orgs/{org_id}/secrets/{key}", delete(delete_org_secret))
.route("/orgs/{org_id}/prune", post(prune_org_secrets))
.route("/orgs/{org_id}/audit", get(org_audit_events))
.route("/orgs/{org_id}/webhooks", post(create_org_webhook))
.route("/orgs/{org_id}/webhooks", get(list_org_webhooks))
.route("/orgs/{org_id}/webhooks/{id}", delete(delete_org_webhook))
.layer(middleware::from_fn_with_state(state.clone(), require_auth))
.layer(cors.clone());
let app = if enable_public_bucket {
let secret_public = Router::new()
.route("/secrets", post(create_secret))
.route("/secrets/{key}", get(get_secret))
.route("/secrets/{key}", head(head_secret));
let protected_public_bucket = Router::new()
.route("/secrets", get(list_secrets))
.route("/secrets/{key}", patch(patch_secret))
.route("/secrets/{key}", delete(delete_secret))
.route("/prune", post(prune_secrets))
.route("/audit", get(audit_events))
.route("/webhooks", post(create_webhook))
.route("/webhooks", get(list_webhooks))
.route("/webhooks/{id}", delete(delete_webhook))
.layer(middleware::from_fn_with_state(
state.clone(),
require_master_key,
))
.layer(cors);
Router::new()
.merge(secret_public)
.merge(public)
.merge(protected_public_bucket)
.merge(org_protected)
.with_state(state)
} else {
Router::new()
.merge(public)
.merge(org_protected)
.with_state(state)
}
.layer(GovernorLayer::new(governor_conf))
.layer(middleware::from_fn(add_security_headers))
.layer(TraceLayer::new_for_http());
let addr: SocketAddr = format!("{}:{}", cfg.host, cfg.port)
.parse()
.context("invalid host/port")?;
info!(%addr, "sirr server listening");
let listener = tokio::net::TcpListener::bind(addr)
.await
.context("bind listener")?;
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.context("server error")
}
fn auto_init_bootstrap(store: &crate::store::Store) -> Result<()> {
use crate::store::org::{OrgRecord, PrincipalKeyRecord, PrincipalRecord};
let orgs = store.list_orgs().context("list orgs for auto-init")?;
if !orgs.is_empty() {
info!("auto-init: orgs already exist, skipping bootstrap");
return Ok(());
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let org_id = format!("{:032x}", rand::random::<u128>());
let principal_id = format!("{:032x}", rand::random::<u128>());
let org = OrgRecord {
id: org_id.clone(),
name: "default".to_string(),
metadata: std::collections::HashMap::new(),
created_at: now,
};
store.put_org(&org).context("auto-init: create org")?;
let principal = PrincipalRecord {
id: principal_id.clone(),
org_id: org_id.clone(),
name: "admin".to_string(),
role: "admin".to_string(),
metadata: std::collections::HashMap::new(),
created_at: now,
};
store
.put_principal(&principal)
.context("auto-init: create principal")?;
let valid_before = now + 1800; let mut keys_output = Vec::new();
for i in 1..=2 {
let raw_key = {
let mut bytes = [0u8; 16];
rand::Rng::fill(&mut rand::thread_rng(), &mut bytes);
format!("sirr_key_{}", hex::encode(bytes))
};
let key_hash = {
use sha2::{Digest, Sha256};
Sha256::digest(raw_key.as_bytes()).to_vec()
};
let key_id = format!("{:016x}", rand::random::<u64>());
let key_record = PrincipalKeyRecord {
id: key_id.clone(),
principal_id: principal_id.clone(),
org_id: org_id.clone(),
name: format!("bootstrap-key-{i}"),
key_hash,
valid_after: now,
valid_before,
created_at: now,
};
store
.put_principal_key(&key_record)
.context("auto-init: create key")?;
keys_output.push((key_id, raw_key));
}
eprintln!();
eprintln!(" ╔══════════════════════════════════════════════════════════════╗");
eprintln!(" ║ AUTO-INIT BOOTSTRAP ║");
eprintln!(" ╚══════════════════════════════════════════════════════════════╝");
eprintln!();
eprintln!(" Default org created:");
eprintln!(" org_id: {org_id}");
eprintln!(" name: default");
eprintln!();
eprintln!(" Admin principal created:");
eprintln!(" principal_id: {principal_id}");
eprintln!(" name: admin");
eprintln!(" role: admin");
eprintln!();
eprintln!(" Temporary API keys (valid 30 minutes):");
for (kid, raw) in &keys_output {
eprintln!(" id={kid} key={raw}");
}
eprintln!();
warn!("auto-init keys expire in 30 minutes — create permanent keys via `sirr me create-key`");
eprintln!();
Ok(())
}
fn load_or_create_key(data_dir: &std::path::Path) -> Result<crate::store::crypto::EncryptionKey> {
let key_path = data_dir.join("sirr.key");
if key_path.exists() {
let bytes = std::fs::read(&key_path).context("read sirr.key")?;
crate::store::crypto::load_key(&bytes).ok_or_else(|| {
anyhow::anyhow!(
"sirr.key is corrupt (expected 32 bytes, got {})",
bytes.len()
)
})
} else {
let key = crate::store::crypto::generate_key();
std::fs::write(&key_path, key.as_bytes()).context("write sirr.key")?;
info!("generated new encryption key");
Ok(key)
}
}
fn gethostname() -> Option<String> {
std::process::Command::new("hostname")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
}
fn print_banner(
cfg: &ServerConfig,
data_dir: &std::path::Path,
lic_status: &license::LicenseStatus,
) {
if cfg.no_banner {
return;
}
let version = &cfg.version;
let enc_key_source = if std::env::var("SIRR_MASTER_ENCRYPTION_KEY_FILE").is_ok() {
"file"
} else {
"env"
};
let tier = match lic_status {
license::LicenseStatus::Free => "free (Solo tier)".to_string(),
license::LicenseStatus::Licensed(ref t) => format!("licensed ({t:?})"),
license::LicenseStatus::Invalid(_) => return,
};
let mask = |opt: &Option<String>| match opt {
Some(_) => "set",
None => "—",
};
let bool_str = |b: bool| if b { "true" } else { "false" };
eprintln!();
eprintln!(" ___ _ ");
eprintln!(" / __(_)_ _ _ _ ");
eprintln!(" \\__ \\ | '_| '_| ");
eprintln!(" |___/_|_| |_| ");
eprintln!();
eprintln!(" sirrd v{version} · ephemeral secret vault");
eprintln!();
eprintln!(" ── server ──────────────────────────────────");
eprintln!(" SIRR_HOST {}", cfg.host);
eprintln!(" SIRR_PORT {}", cfg.port);
eprintln!(" SIRR_DATA_DIR {}", data_dir.display());
eprintln!(" SIRR_LOG_LEVEL {}", cfg.log_level);
eprintln!(" SIRR_AUTOINIT {}", bool_str(cfg.auto_init));
eprintln!();
eprintln!(" ── security ────────────────────────────────");
eprintln!(" SIRR_MASTER_API_KEY {}", mask(&cfg.api_key));
eprintln!(" SIRR_MASTER_ENCRYPTION_KEY {enc_key_source}");
eprintln!(
" SIRR_TRUSTED_PROXIES {}",
if cfg.trusted_proxies.is_empty() {
"—"
} else {
&cfg.trusted_proxies
}
);
eprintln!(
" SIRR_ENABLE_PUBLIC_BUCKET {}",
bool_str(cfg.enable_public_bucket)
);
eprintln!();
eprintln!(" ── licensing ───────────────────────────────");
eprintln!(" SIRR_LICENSE_KEY {}", mask(&cfg.license_key));
eprintln!(" tier {tier}");
eprintln!(" SIRR_VALIDATION_URL {}", cfg.validation_url);
eprintln!(
" SIRR_VALIDATION_CACHE_SECS {}",
cfg.validation_cache_secs
);
eprintln!(" SIRR_HEARTBEAT {}", bool_str(cfg.heartbeat));
eprintln!(
" SIRR_INSTANCE_ID {}",
cfg.instance_id.as_deref().unwrap_or("(hostname)")
);
eprintln!();
eprintln!(" ── cors & rate limiting ────────────────────");
eprintln!(
" SIRR_CORS_ORIGINS {}",
cfg.cors_origins.as_deref().unwrap_or("—")
);
eprintln!(
" SIRR_CORS_METHODS {}",
cfg.cors_methods.as_deref().unwrap_or("(all)")
);
eprintln!(
" SIRR_RATE_LIMIT_PER_SECOND {}",
cfg.rate_limit_per_second
);
eprintln!(" SIRR_RATE_LIMIT_BURST {}", cfg.rate_limit_burst);
eprintln!();
eprintln!(" ── webhooks & audit ────────────────────────");
eprintln!(
" SIRR_WEBHOOK_SECRET {}",
mask(&cfg.webhook_secret)
);
eprintln!(
" SIRR_WEBHOOK_ALLOWED_ORIGINS {}",
if cfg.webhook_allowed_origins.is_empty() {
"—"
} else {
&cfg.webhook_allowed_origins
}
);
eprintln!(
" SIRR_AUDIT_RETENTION_DAYS {}",
cfg.audit_retention_days
);
eprintln!(
" SIRR_AUDIT_REDACT_KEYS {}",
bool_str(cfg.redact_audit_keys)
);
eprintln!();
}
async fn add_security_headers(req: Request, next: Next) -> Response {
let mut response = next.run(req).await;
let h = response.headers_mut();
h.insert(
axum::http::header::HeaderName::from_static("x-content-type-options"),
axum::http::HeaderValue::from_static("nosniff"),
);
h.insert(
axum::http::header::HeaderName::from_static("x-frame-options"),
axum::http::HeaderValue::from_static("DENY"),
);
h.insert(
axum::http::header::HeaderName::from_static("content-security-policy"),
axum::http::HeaderValue::from_static("default-src 'none'"),
);
h.remove(axum::http::header::SERVER);
response
}
fn print_security_notice(cfg: &ServerConfig) {
let Some(ref key) = cfg.auto_generated_key else {
return;
};
if cfg.no_security_banner {
return;
}
eprintln!();
eprintln!(" !! SECURITY NOTICE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
eprintln!(" !!");
eprintln!(" !! No SIRR_MASTER_API_KEY was set. A random key has been generated:");
eprintln!(" !!");
eprintln!(" !! SIRR_MASTER_API_KEY={key}");
eprintln!(" !!");
eprintln!(" !! Copy this key and set it in your environment before exposing");
eprintln!(" !! this server on any network. It changes on every restart");
eprintln!(" !! until you persist it as SIRR_MASTER_API_KEY.");
eprintln!(" !!");
eprintln!(" !! Suppress this notice: SIRR_NO_SECURITY_BANNER=1");
eprintln!(" !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
eprintln!();
}
async fn robots_txt() -> impl IntoResponse {
(
[(
axum::http::header::CONTENT_TYPE,
"text/plain; charset=utf-8",
)],
"User-agent: *\nDisallow: /\n",
)
}
async fn security_txt() -> impl IntoResponse {
(
[(
axum::http::header::CONTENT_TYPE,
"text/plain; charset=utf-8",
)],
concat!(
"Contact: mailto:security@sirr.dev\n",
"Expires: 2027-01-01T00:00:00.000Z\n",
"Preferred-Languages: en\n",
"Canonical: https://sirr.dev/.well-known/security.txt\n",
"Policy: https://sirr.dev/security\n",
),
)
}
fn build_cors(origins: Option<&str>, methods: Option<&str>) -> CorsLayer {
let Some(origins_str) = origins else {
return CorsLayer::new();
};
let allowed_origins: Vec<_> = origins_str
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect();
let all_methods = [
http::Method::GET,
http::Method::HEAD,
http::Method::POST,
http::Method::PATCH,
http::Method::DELETE,
http::Method::OPTIONS,
];
let allowed_methods: Vec<http::Method> = match methods {
None => all_methods.to_vec(),
Some(m) => m.split(',').filter_map(|s| s.trim().parse().ok()).collect(),
};
CorsLayer::new()
.allow_origin(allowed_origins)
.allow_methods(allowed_methods)
.allow_headers(Any)
}