1use crate::api::{OgcApiInventory, OpenApiDoc};
2use crate::auth::oidc::{AuthRequest, OidcClient};
3use crate::config::WebserverCfg;
4use crate::ogcapi::*;
5use crate::service::{CoreService, ServiceEndpoints};
6use crate::static_assets::favicon;
7use crate::TileResponse;
8use actix_session::Session;
9use actix_web::{
10 error::ErrorInternalServerError, guard, guard::Guard, guard::GuardContext, http::header,
11 http::StatusCode, web, web::Bytes, HttpRequest, HttpResponse, Responder,
12};
13use actix_web_opentelemetry::PrometheusMetricsHandler;
14use async_stream::stream;
15use futures_core::stream::Stream;
16use log::info;
17use std::convert::Infallible;
18use std::io::Read;
19use std::path::Path;
20
21impl TileResponse {
22 pub fn into_stream(self) -> impl Stream<Item = Result<Bytes, Infallible>> {
23 let bytes = self.body.bytes().map_while(|val| val.ok());
24 stream! {
25 yield Ok::<_, Infallible>(web::Bytes::from_iter(bytes));
26 }
27 }
28}
29
30#[derive(Default)]
32pub struct JsonContentGuard;
33
34impl Guard for JsonContentGuard {
35 fn check(&self, ctx: &GuardContext<'_>) -> bool {
36 if cfg!(feature = "html") {
37 match ctx.header::<header::Accept>() {
38 Some(hdr) => hdr.preference() == "application/json",
39 None => false,
40 }
41 } else {
42 true
44 }
45 }
46}
47
48pub fn abs_req_baseurl(req: &HttpRequest) -> String {
50 let conninfo = req.connection_info();
51 format!("{}://{}", conninfo.scheme(), conninfo.host())
52}
53
54pub fn req_parent_path(req: &HttpRequest) -> String {
57 Path::new(req.path())
58 .parent()
59 .expect("invalid req.path")
60 .to_str()
61 .expect("invalid req.path")
62 .to_string()
63}
64
65pub fn absurl(req: &HttpRequest, path: &str) -> String {
67 let conninfo = req.connection_info();
68 let pathbase = path.split('/').nth(1).unwrap_or("");
69 let reqbase = req
70 .path()
71 .split('/')
72 .nth(1)
73 .map(|p| {
74 if p.is_empty() || p == pathbase {
75 "".to_string()
76 } else {
77 format!("/{p}")
78 }
79 })
80 .unwrap_or("".to_string());
81 format!("{}://{}{reqbase}{path}", conninfo.scheme(), conninfo.host())
82}
83
84async fn index(ogcapi: web::Data<OgcApiInventory>, req: HttpRequest) -> HttpResponse {
86 let links = ogcapi
88 .landing_page_links
89 .iter()
90 .map(|link| {
91 let mut l = link.clone();
92 l.href = absurl(&req, &link.href);
93 l
94 })
95 .collect();
96 let landing_page = CoreLandingPage {
97 title: Some("BBOX OGC API".to_string()),
98 description: Some("BBOX OGC API landing page".to_string()),
99 links,
100 };
101 HttpResponse::Ok().json(landing_page)
102}
103
104async fn conformance(ogcapi: web::Data<OgcApiInventory>) -> HttpResponse {
106 let conforms_to = CoreConformsTo {
107 conforms_to: ogcapi.conformance_classes.to_vec(),
108 };
109 HttpResponse::Ok().json(conforms_to)
110}
111
112async fn openapi_yaml(
114 openapi: web::Data<OpenApiDoc>,
115 cfg: web::Data<WebserverCfg>,
116 req: HttpRequest,
117) -> HttpResponse {
118 let yaml = openapi.as_yaml(&cfg.public_server_url(req));
119 HttpResponse::Ok()
120 .content_type("application/x-yaml")
121 .body(yaml)
122}
123
124async fn openapi_json(
126 openapi: web::Data<OpenApiDoc>,
127 cfg: web::Data<WebserverCfg>,
128 req: HttpRequest,
129) -> HttpResponse {
130 let json = openapi.as_json(&cfg.public_server_url(req));
131 HttpResponse::Ok().json(json)
132}
133
134async fn health() -> HttpResponse {
135 HttpResponse::Ok().body("OK")
136}
137
138async fn login(oidc: web::Data<OidcClient>) -> impl Responder {
139 web::Redirect::to(oidc.authorize_url.clone()).using_status_code(StatusCode::FOUND)
140}
141
142async fn auth(
143 session: Session,
144 oidc: web::Data<OidcClient>,
145 params: web::Query<AuthRequest>,
146) -> actix_web::Result<impl Responder> {
147 let identity = params.auth(&oidc).await.map_err(ErrorInternalServerError)?;
148 info!(
149 "username: `{}` groups: {:?}",
150 identity.username, identity.groups
151 );
152
153 session.insert("username", identity.username).unwrap();
154 session.insert("groups", identity.groups).unwrap();
155
156 Ok(web::Redirect::to("/").using_status_code(StatusCode::FOUND))
157}
158
159async fn logout(session: Session) -> impl Responder {
160 session.clear();
161 web::Redirect::to("/").using_status_code(StatusCode::FOUND)
162}
163
164impl ServiceEndpoints for CoreService {
165 fn register_endpoints(&self, cfg: &mut web::ServiceConfig) {
166 cfg.app_data(web::Data::new(self.web_config.clone()))
167 .app_data(web::Data::new(self.ogcapi.clone()))
168 .app_data(web::Data::new(self.openapi.clone()))
169 .service(
171 web::resource("/")
172 .guard(JsonContentGuard)
173 .route(web::get().to(index)),
174 )
175 .service(
176 web::resource("/conformance")
177 .guard(JsonContentGuard)
178 .route(web::get().to(conformance)),
179 )
180 .service(web::resource("/favicon.ico").route(web::get().to(favicon)))
181 .service(web::resource("/openapi.yaml").route(web::get().to(openapi_yaml)))
182 .service(web::resource("/openapi.json").route(web::get().to(openapi_json)))
183 .service(
184 web::resource("/openapi")
185 .guard(guard::Acceptable::new(
186 "application/x-yaml".parse().unwrap(),
187 ))
188 .route(web::get().to(openapi_yaml)),
189 )
190 .service(
191 web::resource("/openapi")
192 .guard(JsonContentGuard)
193 .route(web::get().to(openapi_json)),
194 )
195 .service(web::resource("/health").to(health));
196
197 if let Some(oidc) = &self.oidc {
198 cfg.app_data(web::Data::new(oidc.clone()))
199 .service(web::resource("/login").route(web::get().to(login)))
200 .service(web::resource("/auth").route(web::get().to(auth)))
201 .service(web::resource("/logout").route(web::get().to(logout)));
202 }
203
204 if let Some(metrics) = &self.metrics {
205 let metrics_handler = PrometheusMetricsHandler::new(metrics.clone());
206 cfg.route("/metrics", web::get().to(metrics_handler));
208 }
209 }
210}