1use crate::api::{OgcApiInventory, OpenApiDoc};
2use crate::auth::oidc::OidcClient;
3use crate::cli::{CliArgs, CommonCommands, GlobalArgs, NoArgs, NoCommands};
4use crate::config::{ConfigError, CoreServiceCfg, WebserverCfg};
5use crate::logger;
6use crate::metrics::{init_metrics_exporter, no_metrics, NoMetrics};
7use crate::ogcapi::{ApiLink, CoreCollection};
8use crate::tls::load_rustls_config;
9use actix_cors::Cors;
10use actix_session::{config::PersistentSession, storage::CookieSessionStore, SessionMiddleware};
11use actix_web::{
12 cookie::{time::Duration, Key},
13 middleware,
14 middleware::Condition,
15 web, App, HttpServer,
16};
17use actix_web_opentelemetry::{RequestMetrics, RequestMetricsBuilder, RequestTracing};
18use async_trait::async_trait;
19use clap::{ArgMatches, Args, Parser, Subcommand};
20use log::info;
21use once_cell::sync::OnceCell;
22use opentelemetry_prometheus::PrometheusExporter;
23use prometheus::Registry;
24
25pub trait ServiceConfig: Sized {
26 fn initialize(cli: &ArgMatches) -> Result<Self, ConfigError>;
28}
29
30#[async_trait]
31pub trait OgcApiService: Clone + Send {
32 type Config: ServiceConfig;
33 type CliCommands: Subcommand + Parser + core::fmt::Debug;
34 type CliArgs: Args + core::fmt::Debug;
35 type Metrics;
36
37 async fn create(cfg: &Self::Config, core_cfg: &CoreServiceCfg) -> Self;
39 fn landing_page_links(&self, _api_base: &str) -> Vec<ApiLink> {
40 Vec::new()
41 }
42 fn conformance_classes(&self) -> Vec<String> {
43 Vec::new()
44 }
45 fn collections(&self) -> Vec<CoreCollection> {
46 Vec::new()
47 }
48 fn openapi_yaml(&self) -> Option<&str> {
49 None
50 }
51 fn metrics(&self) -> &'static Self::Metrics;
53 fn add_metrics(&self, _prometheus: &Registry) {}
55 async fn cli_run(&self, _cli: &ArgMatches) -> bool {
56 false
57 }
58}
59
60pub trait ServiceEndpoints {
61 fn register_endpoints(&self, cfg: &mut web::ServiceConfig);
62}
63
64#[derive(Clone)]
65pub struct DummyService;
66
67#[derive(Clone)]
68pub struct NoConfig;
69
70impl ServiceConfig for NoConfig {
71 fn initialize(_args: &ArgMatches) -> Result<Self, ConfigError> {
72 Ok(NoConfig)
73 }
74}
75
76#[async_trait]
77impl OgcApiService for DummyService {
78 type Config = NoConfig;
79 type CliCommands = NoCommands;
80 type CliArgs = NoArgs;
81 type Metrics = NoMetrics;
82
83 async fn create(_cfg: &Self::Config, _core_cfg: &CoreServiceCfg) -> Self {
84 DummyService
85 }
86 fn metrics(&self) -> &'static Self::Metrics {
87 no_metrics()
88 }
89}
90
91impl ServiceEndpoints for DummyService {
92 fn register_endpoints(&self, _cfg: &mut web::ServiceConfig) {}
93}
94
95#[derive(Clone)]
96pub struct CoreService {
97 pub web_config: WebserverCfg,
98 pub(crate) ogcapi: OgcApiInventory,
99 pub(crate) openapi: OpenApiDoc,
100 pub(crate) metrics: Option<PrometheusExporter>,
101 pub(crate) oidc: Option<OidcClient>,
102}
103
104impl CoreService {
105 pub fn add_service<T: OgcApiService>(&mut self, svc: &T) {
106 let api_base = "";
107
108 self.ogcapi
109 .landing_page_links
110 .extend(svc.landing_page_links(api_base));
111 self.ogcapi
112 .conformance_classes
113 .extend(svc.conformance_classes());
114 self.ogcapi.collections.extend(svc.collections());
115
116 if let Some(yaml) = svc.openapi_yaml() {
117 if self.openapi.is_empty() {
118 self.openapi = OpenApiDoc::from_yaml(yaml, api_base);
119 } else {
120 self.openapi.extend(yaml, api_base);
121 }
122 }
123
124 if let Some(metrics) = &self.metrics {
125 svc.add_metrics(metrics.registry())
126 }
127 }
128 pub fn has_cors(&self) -> bool {
129 self.web_config.cors.is_some()
130 }
131 pub fn cors(&self) -> Cors {
132 if let Some(cors_cfg) = self.web_config.cors.as_ref() {
133 let mut cors = Cors::default().allowed_methods(vec!["GET"]);
134 if cors_cfg.allow_all_origins {
135 cors = cors.allow_any_origin().send_wildcard();
136 }
137 cors
138 } else {
139 Cors::default()
140 }
141 }
142 pub fn has_metrics(&self) -> bool {
143 self.metrics.is_some()
144 }
145 pub fn middleware(&self) -> RequestTracing {
147 RequestTracing::new()
148 }
149 pub fn workers(&self) -> usize {
150 self.web_config.worker_threads()
151 }
152 pub fn tls_config(&self) -> Option<rustls::ServerConfig> {
153 if let Some(cert) = &self.web_config.tls_cert {
154 if let Some(key) = &self.web_config.tls_key {
155 return Some(load_rustls_config(cert, key));
156 }
157 }
158 None
159 }
160 pub fn server_addr(&self) -> &str {
161 &self.web_config.server_addr
162 }
163}
164
165#[async_trait]
166impl OgcApiService for CoreService {
167 type Config = CoreServiceCfg;
168 type CliCommands = CommonCommands;
169 type CliArgs = GlobalArgs;
170 type Metrics = RequestMetrics;
171
172 async fn create(cfg: &Self::Config, _core_cfg: &CoreServiceCfg) -> Self {
173 logger::init(cfg.loglevel());
174 let metrics = init_metrics_exporter();
175 let oidc = if let Some(auth_cfg) = &cfg.auth {
176 if let Some(oidc_cfg) = &auth_cfg.oidc {
177 Some(OidcClient::from_config(oidc_cfg).await)
178 } else {
179 None
180 }
181 } else {
182 None
183 };
184 CoreService {
185 web_config: cfg.webserver.clone().unwrap_or_default(),
186 ogcapi: OgcApiInventory::default(),
187 openapi: OpenApiDoc::new(),
188 metrics,
189 oidc,
190 }
191 }
192 fn landing_page_links(&self, _api_base: &str) -> Vec<ApiLink> {
193 vec![
194 ApiLink {
195 href: "/".to_string(),
196 rel: Some("self".to_string()),
197 type_: Some("application/json".to_string()),
198 title: Some("this document".to_string()),
199 hreflang: None,
200 length: None,
201 },
202 ApiLink {
203 href: "/openapi.json".to_string(),
204 rel: Some("service-desc".to_string()),
205 type_: Some("application/vnd.oai.openapi+json;version=3.0".to_string()),
206 title: Some("the API definition".to_string()),
207 hreflang: None,
208 length: None,
209 },
210 ApiLink {
211 href: "/openapi.yaml".to_string(),
212 rel: Some("service-desc".to_string()),
213 type_: Some("application/x-yaml".to_string()),
214 title: Some("the API definition".to_string()),
215 hreflang: None,
216 length: None,
217 },
218 ApiLink {
219 href: "/conformance".to_string(),
220 rel: Some("conformance".to_string()),
221 type_: Some("application/json".to_string()),
222 title: Some("OGC API conformance classes implemented by this server".to_string()),
223 hreflang: None,
224 length: None,
225 },
226 ]
227 }
228 fn conformance_classes(&self) -> Vec<String> {
229 vec![
230 "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core".to_string(),
231 ]
233 }
234 fn openapi_yaml(&self) -> Option<&str> {
235 Some(include_str!("openapi.yaml"))
236 }
237 fn metrics(&self) -> &'static Self::Metrics {
238 static METRICS: OnceCell<RequestMetrics> = OnceCell::new();
239 METRICS.get_or_init(|| {
240 RequestMetricsBuilder::new().build(opentelemetry::global::meter("bbox"))
241 })
242 }
243}
244
245#[actix_web::main]
247pub async fn run_service<T: OgcApiService + ServiceEndpoints + Sync + 'static>(
248) -> std::io::Result<()> {
249 let mut cli = CliArgs::default();
250 cli.register_service_args::<CoreService>();
251 cli.register_service_args::<T>();
252 cli.apply_global_args();
253 let matches = cli.cli_matches();
254
255 let core_cfg = CoreServiceCfg::initialize(&matches).unwrap();
256 let mut core = CoreService::create(&core_cfg, &core_cfg).await;
257
258 let service_cfg = T::Config::initialize(&matches).unwrap();
259 let service = T::create(&service_cfg, &core_cfg).await;
260
261 core.add_service(&service);
262
263 if service.cli_run(&matches).await {
264 return Ok(());
265 }
266
267 let secret_key = Key::generate();
268 let session_ttl = Duration::minutes(1);
269
270 let workers = core.workers();
271 let server_addr = core.server_addr().to_string();
272 let tls_config = core.tls_config();
273 let mut server = HttpServer::new(move || {
274 App::new()
275 .configure(|cfg| core.register_endpoints(cfg))
276 .configure(|cfg| service.register_endpoints(cfg))
277 .wrap(
278 SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
279 .cookie_name("bbox".to_owned())
280 .cookie_secure(false)
281 .session_lifecycle(PersistentSession::default().session_ttl(session_ttl))
282 .build(),
283 )
284 .wrap(Condition::new(core.has_cors(), core.cors()))
285 .wrap(middleware::Compress::default())
286 .wrap(middleware::NormalizePath::trim())
287 .wrap(middleware::Logger::default())
288 });
289 if let Some(tls_config) = tls_config {
290 info!("Starting web server at https://{server_addr}");
291 server = server.bind_rustls(server_addr, tls_config)?;
292 } else {
293 info!("Starting web server at http://{server_addr}");
294 server = server.bind(server_addr)?;
295 }
296 server.workers(workers).run().await
297}