1use crate::{
2 SERVER_VERSION,
3 auth_n::{self, certificates::get_certification_provider, middleware::jwt::User},
4 config::ServerConfig,
5 fhir_client::ServerCTX,
6 fhir_http::{HTTPBody, HTTPRequest, http_request_to_fhir_request},
7 jobs, mcp,
8 middleware::{
9 errors::{REQUEST_ID_HEADER, log_operationoutcome_errors, operation_outcome_error_handle},
10 security_headers::security_headers,
11 },
12 openapi,
13 services::{ConfigError, ServerState, create_services, get_pool},
14 static_assets::{create_static_server, root_asset_route},
15};
16use axum::{
17 Extension, Router, ServiceExt,
18 body::{Body, Bytes, to_bytes},
19 extract::{DefaultBodyLimit, OriginalUri, Path, State},
20 http::Request,
21 http::{HeaderMap, HeaderName, HeaderValue, Method, Uri, header},
22 middleware::from_fn,
23 response::{IntoResponse, Response},
24 routing::{any, get, post},
25};
26use axum_client_ip::ClientIpSource;
27use haste_fhir_client::{
28 FHIRClient,
29 request::{FHIRCapabilitiesResponse, FHIRRequest, FHIRResponse},
30};
31use haste_fhir_model::r4::generated::terminology::IssueType;
32use haste_fhir_operation_error::OperationOutcomeError;
33use haste_fhir_search::SearchEngine;
34use haste_fhir_terminology::FHIRTerminology;
35use haste_jwt::{ProjectId, TenantId};
36use haste_repository::{Repository, types::SupportedFHIRVersions, utilities::generate_id};
37use sentry::integrations::tower::NewSentryLayer;
38use serde::Deserialize;
39use std::net::SocketAddr;
40use std::sync::Arc;
41use tower::{Layer, ServiceBuilder};
42use tower_http::{catch_panic::CatchPanicLayer, cors::AllowHeaders, normalize_path::NormalizePath};
43use tower_http::{
44 compression::{
45 CompressionLayer,
46 predicate::{DefaultPredicate, NotForContentType, Predicate},
47 },
48 cors::{Any, CorsLayer},
49 normalize_path::NormalizePathLayer,
50 request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer},
51 set_header::SetResponseHeaderLayer,
52 trace::TraceLayer,
53};
54use tower_sessions::{
55 Expiry, SessionManagerLayer,
56 cookie::{SameSite, time::Duration},
57};
58use tower_sessions_sqlx_store::PostgresStore;
59
60#[derive(Deserialize)]
63struct ProjectPath {
64 tenant: TenantId,
65 project: ProjectId,
66}
67
68fn split_fhir_version(path: &str) -> (Option<SupportedFHIRVersions>, &str) {
74 let path = path.trim_start_matches('/');
75 let (head, rest) = path.split_once('/').unwrap_or((path, ""));
76
77 match SupportedFHIRVersions::from_url_segment(head) {
78 Some(version) => (Some(version), rest),
79 None => (None, path),
80 }
81}
82
83fn request_fhir_version(
90 user: &User,
91 path: &ProjectPath,
92 requested: Option<SupportedFHIRVersions>,
93) -> Result<SupportedFHIRVersions, OperationOutcomeError> {
94 let project_version = user.claims.fhir_version.clone();
95
96 if let Some(requested) = requested
97 && requested != project_version
98 {
99 return Err(OperationOutcomeError::error(
100 IssueType::not_supported(),
101 format!(
102 "Project '{}' is served as FHIR {}, not {}.",
103 path.project.as_ref(),
104 project_version,
105 requested
106 ),
107 ));
108 }
109
110 Ok(project_version)
111}
112
113fn pretty_requested(query: &str) -> Result<bool, OperationOutcomeError> {
119 let (mut format, mut pretty) = (None, None);
120 for (name, value) in url::form_urlencoded::parse(query.as_bytes()) {
121 match name.as_ref() {
122 "_format" => format = Some(value),
123 "_pretty" => pretty = Some(value),
124 _ => {}
125 }
126 }
127 if let Some(format) = format.as_deref()
128 && !matches!(
129 format,
130 "json" | "application/json" | "application/fhir+json" | "application/fhir json"
131 )
132 {
133 return Err(OperationOutcomeError::error(
134 IssueType::not_supported(),
135 format!("_format '{format}' is not supported; only JSON is served."),
136 ));
137 }
138 Ok(pretty.as_deref() == Some("true"))
139}
140
141async fn pretty_print(response: Response) -> Result<Response, OperationOutcomeError> {
142 let (mut parts, body) = response.into_parts();
143 let bytes = to_bytes(body, usize::MAX)
144 .await
145 .map_err(|e| OperationOutcomeError::error(IssueType::exception(), e.to_string()))?;
146 let bytes = serde_json::from_slice::<serde_json::Value>(&bytes)
148 .and_then(|value| serde_json::to_vec_pretty(&value))
149 .map_or(bytes, Bytes::from);
150 parts.headers.remove(axum::http::header::CONTENT_LENGTH);
151 Ok(Response::from_parts(parts, Body::from(bytes)))
152}
153
154async fn fhir_handler<
155 Repo: Repository + Send + Sync + 'static,
156 Search: SearchEngine + Send + Sync + 'static,
157 Terminology: FHIRTerminology + Send + Sync + 'static,
158>(
159 method: Method,
160 Extension(user): Extension<Arc<User>>,
161 uri: Uri,
162 OriginalUri(original_uri): OriginalUri,
163 Path(path): Path<ProjectPath>,
164 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
165 headers: HeaderMap,
166 body: Bytes,
167) -> Result<Response, OperationOutcomeError> {
168 let (requested_version, fhir_location) = split_fhir_version(uri.path());
171 let fhir_version = request_fhir_version(&user, &path, requested_version)?;
172
173 let ctx = Arc::new(
174 ServerCTX::new(
175 path.tenant,
176 path.project,
177 fhir_version,
178 user,
179 state.fhir_client.clone(),
180 state.rate_limit.clone(),
181 )
182 .with_tracing_id(Some(format!("rest-{}", generate_id(Some(8)))))
183 .with_prefer_async(jobs::prefers_async(&headers)),
184 );
185
186 let query = original_uri.query().unwrap_or_default();
187 let http_req = HTTPRequest::new(
188 method,
189 fhir_location.to_string(),
190 HTTPBody::Bytes(body),
191 query,
192 );
193 let fhir_request =
194 http_request_to_fhir_request(SupportedFHIRVersions::R4, http_req, &ctx).await?;
195
196 let pretty = matches!(fhir_request, FHIRRequest::Search(_)) && pretty_requested(query)?;
198
199 let response = state.fhir_client.request(ctx, fhir_request).await?;
200
201 let response = response.into_response();
202 if pretty {
203 pretty_print(response).await
204 } else {
205 Ok(response)
206 }
207}
208
209async fn public_metadata_handler<
210 Repo: Repository + Send + Sync + 'static,
211 Search: SearchEngine + Send + Sync + 'static,
212 Terminology: FHIRTerminology + Send + Sync + 'static,
213>(
214 Path(path): Path<ProjectPath>,
215 State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
216) -> Result<Response, OperationOutcomeError> {
217 let ctx = Arc::new(ServerCTX::system(
218 path.tenant,
219 path.project,
220 state.fhir_client.clone(),
221 state.rate_limit.clone(),
222 ));
223
224 state
225 .fhir_client
226 .capabilities(ctx)
227 .await
228 .map(|capabilities| FHIRResponse::Capabilities(FHIRCapabilitiesResponse { capabilities }))
229 .map(|fhir_response| fhir_response.into_response())
230}
231
232pub async fn server(
233 config: Arc<ServerConfig>,
234) -> Result<NormalizePath<Router>, OperationOutcomeError> {
235 let ip_source = match &config.monitoring.ip_source {
236 crate::config::IpSource::ConnectInfo => ClientIpSource::ConnectInfo,
237 crate::config::IpSource::CfConnectingIp => ClientIpSource::CfConnectingIp,
238 crate::config::IpSource::XRealIp => ClientIpSource::XRealIp,
239 };
240
241 get_certification_provider(config.as_ref());
242
243 let pool = get_pool(config.as_ref()).await;
244 let session_store = PostgresStore::new(pool.clone());
245 session_store.migrate().await.map_err(ConfigError::from)?;
246
247 let shared_state = create_services(config.clone()).await?;
248
249 let fhir_router = Router::new()
258 .route("/", any(fhir_handler))
259 .route("/{*fhir_location}", any(fhir_handler));
260
261 let protected_resources_router = Router::new()
262 .nest("/fhir", fhir_router)
263 .route("/mcp", post(mcp::route::mcp_handler))
264 .route(
266 "/jobs/{id}",
267 get(jobs::status_handler).delete(jobs::cancel_handler),
268 )
269 .layer(
270 ServiceBuilder::new()
271 .layer(axum::middleware::from_fn_with_state(
272 shared_state.clone(),
273 auth_n::middleware::basic_auth::basic_auth_middleware,
274 ))
275 .layer(axum::middleware::from_fn_with_state(
276 shared_state.clone(),
277 auth_n::middleware::jwt::token_verifcation,
278 ))
279 .layer(axum::middleware::from_fn(
280 auth_n::middleware::project_access::project_access,
281 )),
282 );
283
284 let mut smart_configuration_router = Router::new().route(
289 "/fhir/.well-known/smart-configuration",
290 get(auth_n::oidc::routes::discovery::smart_configuration),
291 );
292
293 for version in SupportedFHIRVersions::ALL {
294 smart_configuration_router = smart_configuration_router.route(
295 &format!("/fhir/{version}/.well-known/smart-configuration"),
296 get(auth_n::oidc::routes::discovery::smart_configuration),
297 );
298 }
299
300 let smart_configuration_router = smart_configuration_router.route_layer(
301 ServiceBuilder::new().layer(axum::middleware::from_fn_with_state(
302 shared_state.clone(),
303 auth_n::oidc::middleware::project_exists,
304 )),
305 );
306
307 let mut project_router = Router::new()
308 .merge(protected_resources_router)
309 .merge(smart_configuration_router)
310 .nest(
311 "/oidc",
312 auth_n::oidc::routes::create_router(shared_state.clone()),
313 );
314
315 if config.security.publicize_fhir_metadata {
316 project_router = project_router.route("/fhir/metadata", get(public_metadata_handler));
317
318 for version in SupportedFHIRVersions::ALL {
319 project_router = project_router.route(
320 &format!("/fhir/{version}/metadata"),
321 get(public_metadata_handler),
322 );
323 }
324 }
325
326 let tenant_router = Router::new()
327 .route("/branding/logo", get(auth_n::tenant::routes::logo))
328 .nest("/auth", auth_n::tenant::routes::create_router())
329 .route(
331 "/{project}/.well-known/openid-configuration",
332 get(auth_n::oidc::routes::discovery::openid_configuration),
333 )
334 .nest("/{project}/api/v1", project_router)
335 .nest(
336 "/mfa",
337 auth_n::mfa::routes::create_router(shared_state.clone()),
338 )
339 .layer(
340 ServiceBuilder::new()
342 .layer(axum::middleware::from_fn_with_state(
343 shared_state.clone(),
344 operation_outcome_error_handle,
345 ))
346 .layer(from_fn(log_operationoutcome_errors)),
347 );
348
349 let discovery_2_0_document_router = Router::new()
350 .route(
351 "/openid-configuration/w/{tenant}/{project}/{*resource}",
352 get(auth_n::oidc::routes::discovery::openid_configuration),
353 )
354 .route(
355 "/openid-configuration/w/{tenant}/{project}",
356 get(auth_n::oidc::routes::discovery::openid_configuration),
357 )
358 .route(
359 "/oauth-protected-resource/w/{tenant}/{project}/{*resource}",
360 get(auth_n::oidc::routes::discovery::oauth_protected_resource),
361 );
362
363 let app = Router::new()
364 .nest("/.well-known", discovery_2_0_document_router)
365 .nest(
366 "/auth",
367 auth_n::global::routes::create_router(shared_state.clone()),
368 )
369 .route("/openapi.json", get(openapi::openapi_document_handler))
370 .route(
371 "/schemas/fhir/{resource_type}",
372 get(openapi::resource_schema_handler),
373 )
374 .nest("/w/{tenant}", tenant_router)
375 .layer(
376 ServiceBuilder::new()
377 .layer(SetRequestIdLayer::new(REQUEST_ID_HEADER, MakeRequestUuid))
381 .layer(PropagateRequestIdLayer::new(REQUEST_ID_HEADER))
382 .layer(CatchPanicLayer::new())
383 .layer(ip_source.into_extension())
384 .layer(NewSentryLayer::<Request<Body>>::new_from_top())
385 .layer(TraceLayer::new_for_http())
386 .layer(DefaultBodyLimit::max(config.max_request_body_size))
388 .layer(CompressionLayer::new().compress_when(
395 DefaultPredicate::new().and(NotForContentType::new("text/html")),
396 ))
397 .layer(security_headers(config.signup.turnstile.is_some()))
398 .layer(SetResponseHeaderLayer::overriding(
399 HeaderName::from_static("x-api-version"),
400 HeaderValue::from_static(SERVER_VERSION),
401 ))
402 .layer(
403 SessionManagerLayer::new(session_store)
404 .with_secure(true)
405 .with_same_site(SameSite::None)
406 .with_expiry(Expiry::OnInactivity(Duration::days(3))),
407 )
408 .layer(
409 CorsLayer::new()
410 .allow_methods(Any)
411 .allow_origin(Any)
412 .allow_headers(AllowHeaders::mirror_request())
413 .expose_headers([
415 header::CONTENT_LOCATION,
416 header::ETAG,
417 header::RETRY_AFTER,
418 HeaderName::from_static("x-progress"),
419 ])
420 .max_age(std::time::Duration::from_secs(7200)),
422 ),
423 )
424 .with_state(shared_state)
425 .nest(root_asset_route().to_str().unwrap(), create_static_server());
426
427 Ok(NormalizePathLayer::trim_trailing_slash().layer(app))
428}
429
430pub async fn serve(config: Arc<ServerConfig>, port: u16) -> Result<(), OperationOutcomeError> {
431 let server = server(config).await?;
432
433 let addr = format!("0.0.0.0:{}", port);
434 let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
435
436 tracing::info!("Server started");
437 axum::serve(
438 listener,
439 <tower_http::normalize_path::NormalizePath<Router> as ServiceExt<
440 axum::http::Request<Body>,
441 >>::into_make_service_with_connect_info::<SocketAddr>(server),
442 )
443 .await
444 .unwrap();
445
446 Ok(())
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use axum::body::{Body, to_bytes};
453 use axum::http::{Request, StatusCode};
454 use tower::ServiceExt;
455
456 #[test]
457 fn format_and_pretty_parameters() {
458 for format in [
459 "json",
460 "application/json",
461 "application/fhir%2Bjson",
462 "application/fhir+json",
463 ] {
464 assert!(!pretty_requested(&format!("_format={format}")).unwrap());
465 }
466 assert!(pretty_requested("name=x&_pretty=true").unwrap());
467 assert!(!pretty_requested("_pretty=false").unwrap());
468 assert!(!pretty_requested("_pretty=true&_pretty=false").unwrap());
469 assert!(pretty_requested("_format=xml").is_err());
470 }
471
472 #[test]
473 fn version_is_split_off_when_the_url_carries_one() {
474 assert_eq!(
475 split_fhir_version("/r4/Patient/123"),
476 (Some(SupportedFHIRVersions::R4), "Patient/123")
477 );
478 assert_eq!(
479 split_fhir_version("/r4"),
480 (Some(SupportedFHIRVersions::R4), "")
481 );
482 }
483
484 #[test]
485 fn a_location_is_not_mistaken_for_a_version() {
486 assert_eq!(split_fhir_version("/Patient/123"), (None, "Patient/123"));
487 assert_eq!(split_fhir_version("/"), (None, ""));
488 assert_eq!(split_fhir_version("/r4b/Patient"), (None, "r4b/Patient"));
490 }
491
492 fn router() -> Router {
497 async fn fhir(uri: Uri) -> String {
500 let (version, location) = split_fhir_version(uri.path());
501 format!(
502 "fhir version={} location={location}",
503 version.map_or("-".to_string(), |version| version.to_string())
504 )
505 }
506
507 let fhir_router = Router::new()
508 .route("/", any(fhir))
509 .route("/{*fhir_location}", any(fhir));
510
511 let protected_resources_router = Router::new().nest("/fhir", fhir_router);
514
515 let mut project_router = Router::new()
516 .merge(protected_resources_router)
517 .route(
518 "/fhir/.well-known/smart-configuration",
519 get(|| async { "smart" }),
520 )
521 .route("/fhir/metadata", get(|| async { "metadata" }));
522
523 for version in SupportedFHIRVersions::ALL {
524 project_router = project_router
525 .route(
526 &format!("/fhir/{version}/.well-known/smart-configuration"),
527 get(|| async { "smart" }),
528 )
529 .route(
530 &format!("/fhir/{version}/metadata"),
531 get(|| async { "metadata" }),
532 );
533 }
534
535 let tenant_router = Router::new().nest("/{project}/api/v1", project_router);
536
537 Router::new().nest("/w/{tenant}", tenant_router)
538 }
539
540 async fn route_to(uri: &str) -> String {
541 let response = router()
542 .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
543 .await
544 .unwrap();
545
546 assert_eq!(response.status(), StatusCode::OK, "{uri}");
547
548 String::from_utf8(
549 to_bytes(response.into_body(), usize::MAX)
550 .await
551 .unwrap()
552 .to_vec(),
553 )
554 .unwrap()
555 }
556
557 #[tokio::test]
561 async fn both_url_shapes_address_the_same_location() {
562 let base = "/w/acme/default/api/v1/fhir";
563
564 for (uri, expected) in [
565 (base.to_string(), "fhir version=- location="),
566 (format!("{base}/Patient"), "fhir version=- location=Patient"),
567 (
568 format!("{base}/Patient/123/_history/1"),
569 "fhir version=- location=Patient/123/_history/1",
570 ),
571 (format!("{base}/r4"), "fhir version=r4 location="),
572 (
573 format!("{base}/r4/Patient"),
574 "fhir version=r4 location=Patient",
575 ),
576 (
577 format!("{base}/r4/Patient/123/_history/1"),
578 "fhir version=r4 location=Patient/123/_history/1",
579 ),
580 ] {
581 assert_eq!(route_to(&uri).await, expected, "{uri}");
582 }
583 }
584
585 #[tokio::test]
586 async fn public_routes_keep_matching_ahead_of_the_fhir_routes() {
587 let base = "/w/acme/default/api/v1/fhir";
588
589 for (uri, expected) in [
590 (format!("{base}/metadata"), "metadata"),
591 (format!("{base}/r4/metadata"), "metadata"),
592 (format!("{base}/.well-known/smart-configuration"), "smart"),
593 (
594 format!("{base}/r4/.well-known/smart-configuration"),
595 "smart",
596 ),
597 ] {
598 assert_eq!(route_to(&uri).await, expected, "{uri}");
599 }
600 }
601}