bbox_core/
endpoints.rs

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/// Middleware for content negotiation
31#[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            // Return JSON response to all requests
43            true
44        }
45    }
46}
47
48/// Absolute request base URL e.g. `http://localhost:8080`
49pub fn abs_req_baseurl(req: &HttpRequest) -> String {
50    let conninfo = req.connection_info();
51    format!("{}://{}", conninfo.scheme(), conninfo.host())
52}
53
54/// Request parent path
55/// `/xzy/tileset.json` -> `/xyz`
56pub 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
65/// Absolute URL from path
66pub 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
84/// landing page
85async fn index(ogcapi: web::Data<OgcApiInventory>, req: HttpRequest) -> HttpResponse {
86    // Make links absolute. Some clients (like OGC conformance tester) expect it.
87    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
104/// information about specifications that this API conforms to
105async 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
112/// Serve openapi.yaml
113async 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
124/// Serve openapi.json
125async 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            // OGC validator checks "{URL}/" and "{URL}/conformance" based on server URL from openapi.json
170            .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            //TODO: path from MetricsCfg
207            cfg.route("/metrics", web::get().to(metrics_handler));
208        }
209    }
210}