use std::future::Future;
use std::pin::Pin;
use std::string::ToString as _;
use std::time::Duration;
use actix_web::http::header::CACHE_CONTROL;
use actix_web::middleware::{NormalizePath, TrailingSlash};
use actix_web::web::Data;
use actix_web::{App, HttpResponse, HttpServer, Responder, middleware, route, web};
use futures::TryFutureExt as _;
#[cfg(feature = "lambda")]
use lambda_web::{is_running_on_lambda, run_actix_on_lambda};
use tracing_actix_web::TracingLogger;
#[cfg(all(feature = "webui", not(docsrs)))]
use crate::config::args::WebUiMode;
#[cfg(feature = "_catalog")]
use crate::config::file::ServerState;
use crate::config::file::srv::{KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT, SrvConfig};
use crate::srv::admin::Catalog;
use crate::{MartinError, MartinResult};
pub const RESERVED_KEYWORDS: &[&str] = &[
"_", "catalog", "config", "font", "health", "help", "index", "manifest", "metrics", "refresh",
"reload", "sprite", "status",
];
#[cfg(any(feature = "_tiles", feature = "fonts", feature = "sprites"))]
pub fn map_internal_error<T: std::fmt::Display>(e: T) -> actix_web::Error {
tracing::error!("{e}");
actix_web::error::ErrorInternalServerError(e.to_string())
}
#[cfg(feature = "_catalog")]
pub struct DebouncedWarning {
last_warning: std::sync::LazyLock<tokio::sync::Mutex<std::time::Instant>>,
}
#[cfg(feature = "_catalog")]
impl DebouncedWarning {
pub const fn new() -> Self {
Self {
last_warning: std::sync::LazyLock::new(|| {
tokio::sync::Mutex::new(std::time::Instant::now())
}),
}
}
pub async fn once_per_hour<F: FnOnce()>(&self, f: F) {
let mut last = self.last_warning.lock().await;
if last.elapsed() >= Duration::from_secs(3600) {
*last = std::time::Instant::now();
f();
}
}
}
#[route("/health", method = "GET", method = "HEAD")]
async fn get_health() -> impl Responder {
HttpResponse::Ok()
.insert_header((CACHE_CONTROL, "no-cache"))
.message_body("OK")
}
pub fn router(cfg: &mut web::ServiceConfig, usr_cfg: &SrvConfig) {
if let Some(prefix) = &usr_cfg.route_prefix {
cfg.service(web::scope(prefix).configure(|cfg| {
register_services(
cfg,
#[cfg(all(feature = "webui", not(docsrs)))]
usr_cfg,
);
}));
} else {
register_services(
cfg,
#[cfg(all(feature = "webui", not(docsrs)))]
usr_cfg,
);
}
}
fn register_services(
cfg: &mut web::ServiceConfig,
#[cfg(all(feature = "webui", not(docsrs)))] usr_cfg: &SrvConfig,
) {
cfg.service(get_health)
.service(crate::srv::admin::get_catalog);
#[cfg(feature = "_tiles")]
{
cfg.service(crate::srv::tiles::content::redirect_tile_ext)
.service(crate::srv::tiles::metadata::get_source_info)
.service(crate::srv::tiles::content::get_tile);
cfg.service(crate::srv::tiles::content::redirect_tiles);
}
#[cfg(feature = "sprites")]
cfg.service(crate::srv::sprites::get_sprite_sdf_json)
.service(crate::srv::sprites::redirect_sdf_sprites_json)
.service(crate::srv::sprites::get_sprite_json)
.service(crate::srv::sprites::redirect_sprites_json)
.service(crate::srv::sprites::get_sprite_sdf_png)
.service(crate::srv::sprites::redirect_sdf_sprites_png)
.service(crate::srv::sprites::get_sprite_png)
.service(crate::srv::sprites::redirect_sprites_png);
#[cfg(feature = "fonts")]
cfg.service(crate::srv::fonts::get_font)
.service(crate::srv::fonts::redirect_fonts);
#[cfg(feature = "styles")]
cfg.service(crate::srv::styles::get_style_json)
.service(crate::srv::styles::redirect_styles);
#[cfg(all(feature = "unstable-rendering", target_os = "linux"))]
cfg.service(crate::srv::styles_rendering::get_style_rendered);
#[cfg(all(feature = "webui", not(docsrs)))]
{
if usr_cfg.web_ui.unwrap_or_default() == WebUiMode::EnableForAll {
cfg.service(actix_web_static_files::ResourceFiles::new(
"/",
crate::srv::admin::webui::generate(),
));
} else {
cfg.service(crate::srv::admin::get_index_ui_disabled);
}
}
#[cfg(any(not(feature = "webui"), docsrs))]
cfg.service(crate::srv::admin::get_index_no_ui);
}
type Server = Pin<Box<dyn Future<Output = MartinResult<()>>>>;
pub fn new_server(
config: SrvConfig,
#[cfg(feature = "_catalog")] state: ServerState,
) -> MartinResult<(Server, String)> {
#[cfg(feature = "metrics")]
let prometheus = {
let metrics_endpoint = if let Some(prefix) = &config.route_prefix {
format!("{prefix}/_/metrics")
} else {
"/_/metrics".to_string()
};
actix_web_prom::PrometheusMetricsBuilder::new("martin")
.endpoint(&metrics_endpoint)
.mask_unmatched_patterns("UNKNOWN")
.const_labels(
config
.observability
.clone()
.unwrap_or_default()
.metrics
.unwrap_or_default()
.add_labels,
)
.build()
.map_err(|err| MartinError::MetricsIntialisationError(err))?
};
let catalog = Catalog::new(
#[cfg(feature = "_catalog")]
&state,
)?;
let keep_alive = Duration::from_secs(config.keep_alive.unwrap_or(KEEP_ALIVE_DEFAULT));
let worker_processes = config.worker_processes.unwrap_or_else(num_cpus::get);
let listen_addresses = config
.listen_addresses
.clone()
.unwrap_or_else(|| LISTEN_ADDRESSES_DEFAULT.to_string());
let cors_config = config.cors.clone().unwrap_or_default();
cors_config.validate()?;
cors_config.log_current_configuration();
let factory = move || {
let cors_middleware = cors_config.make_cors_middleware();
let app = App::new()
.app_data(Data::new(catalog.clone()))
.app_data(Data::new(config.clone()));
#[cfg(feature = "_tiles")]
let app = app
.app_data(Data::new(state.tiles.clone()))
.app_data(Data::new(state.tile_cache.clone()));
#[cfg(feature = "sprites")]
let app = app
.app_data(Data::new(state.sprites.clone()))
.app_data(Data::new(state.sprite_cache.clone()));
#[cfg(feature = "fonts")]
let app = app
.app_data(Data::new(state.fonts.clone()))
.app_data(Data::new(state.font_cache.clone()));
#[cfg(feature = "styles")]
let app = app.app_data(Data::new(state.styles.clone()));
let app = app.wrap(middleware::Condition::new(
cors_middleware.is_some(),
cors_middleware.unwrap_or_default(),
));
#[cfg(feature = "metrics")]
let app = app.wrap(prometheus.clone());
app.wrap(TracingLogger::default())
.wrap(NormalizePath::new(TrailingSlash::MergeOnly))
.configure(|c| router(c, &config))
};
#[cfg(feature = "lambda")]
if is_running_on_lambda() {
let server = run_actix_on_lambda(factory).map_err(MartinError::LambdaError);
return Ok((Box::pin(server), "(aws lambda)".into()));
}
let server = HttpServer::new(factory)
.bind(listen_addresses.clone())
.map_err(|e| MartinError::BindingError(e, listen_addresses.clone()))?
.keep_alive(keep_alive)
.shutdown_timeout(0)
.workers(worker_processes)
.run()
.err_into();
Ok((Box::pin(server), listen_addresses))
}