Skip to main content

haste_server/
server.rs

1use crate::{
2    auth_n::{self, certificates::get_certification_provider, middleware::jwt::User},
3    config::ServerConfig,
4    fhir_client::ServerCTX,
5    fhir_http::{HTTPBody, HTTPRequest, http_request_to_fhir_request},
6    mcp,
7    middleware::{
8        errors::{log_operationoutcome_errors, operation_outcome_error_handle},
9        security_headers::SecurityHeaderLayer,
10    },
11    openapi,
12    services::{ConfigError, ServerState, create_services, get_pool},
13    static_assets::{create_static_server, root_asset_route},
14};
15use axum::{
16    Extension, Router, ServiceExt,
17    body::Body,
18    extract::{DefaultBodyLimit, OriginalUri, Path, State},
19    http::Request,
20    http::{HeaderName, HeaderValue, Method, Uri},
21    middleware::from_fn,
22    response::{IntoResponse, Response},
23    routing::{any, get, post},
24};
25use axum_client_ip::ClientIpSource;
26use haste_fhir_client::{
27    FHIRClient,
28    request::{FHIRCapabilitiesResponse, FHIRResponse},
29};
30use haste_fhir_operation_error::OperationOutcomeError;
31use haste_fhir_search::SearchEngine;
32use haste_fhir_terminology::FHIRTerminology;
33use haste_jwt::{ProjectId, TenantId};
34use haste_repository::{Repository, types::SupportedFHIRVersions, utilities::generate_id};
35use sentry::integrations::tower::NewSentryLayer;
36use serde::Deserialize;
37use std::net::SocketAddr;
38use std::sync::Arc;
39use tower::{Layer, ServiceBuilder};
40use tower_http::{catch_panic::CatchPanicLayer, normalize_path::NormalizePath};
41use tower_http::{
42    compression::CompressionLayer,
43    cors::{Any, CorsLayer},
44    normalize_path::NormalizePathLayer,
45    set_header::SetResponseHeaderLayer,
46    trace::TraceLayer,
47};
48use tower_sessions::{
49    Expiry, SessionManagerLayer,
50    cookie::{SameSite, time::Duration},
51};
52use tower_sessions_sqlx_store::PostgresStore;
53
54const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
55
56#[derive(Deserialize)]
57struct FHIRHandlerPath {
58    tenant: TenantId,
59    project: ProjectId,
60    fhir_version: SupportedFHIRVersions,
61    fhir_location: Option<String>,
62}
63
64#[derive(Deserialize)]
65struct FHIRRootHandlerPath {
66    tenant: TenantId,
67    project: ProjectId,
68    fhir_version: SupportedFHIRVersions,
69}
70
71async fn fhir_handler<
72    Repo: Repository + Send + Sync + 'static,
73    Search: SearchEngine + Send + Sync + 'static,
74    Terminology: FHIRTerminology + Send + Sync + 'static,
75>(
76    user: Arc<User>,
77    method: Method,
78    uri: Uri,
79    path: FHIRHandlerPath,
80    state: Arc<ServerState<Repo, Search, Terminology>>,
81    body: String,
82) -> Result<Response, OperationOutcomeError> {
83    let fhir_location = path.fhir_location.unwrap_or_default();
84
85    async {
86        let http_req = HTTPRequest::new(
87            method,
88            fhir_location,
89            HTTPBody::String(body),
90            uri.query()
91                .map(|q| {
92                    url::form_urlencoded::parse(q.as_bytes())
93                        .into_owned()
94                        .collect()
95                })
96                .unwrap_or_default(),
97        );
98
99        let fhir_request = http_request_to_fhir_request(SupportedFHIRVersions::R4, http_req)?;
100
101        let ctx = ServerCTX::new(
102            path.tenant,
103            path.project,
104            path.fhir_version,
105            user.clone(),
106            state.fhir_client.clone(),
107            state.rate_limit.clone(),
108        )
109        .with_tracing_id(Some(format!("rest-{}", generate_id(Some(8)))));
110
111        let ctx = Arc::new(ctx);
112
113        let response = state.fhir_client.request(ctx, fhir_request).await?;
114
115        let http_response = response.into_response();
116        Ok(http_response)
117    }
118    .await
119}
120
121async fn fhir_root_handler<
122    Repo: Repository + Send + Sync + 'static,
123    Search: SearchEngine + Send + Sync + 'static,
124    Terminology: FHIRTerminology + Send + Sync + 'static,
125>(
126    method: Method,
127    Extension(user): Extension<Arc<User>>,
128    OriginalUri(uri): OriginalUri,
129    Path(path): Path<FHIRRootHandlerPath>,
130    State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
131    body: String,
132) -> Result<Response, OperationOutcomeError> {
133    fhir_handler(
134        user,
135        method,
136        uri,
137        FHIRHandlerPath {
138            tenant: path.tenant,
139            project: path.project,
140            fhir_version: path.fhir_version,
141            fhir_location: None,
142        },
143        state,
144        body,
145    )
146    .await
147}
148
149async fn fhir_type_handler<
150    Repo: Repository + Send + Sync + 'static,
151    Search: SearchEngine + Send + Sync + 'static,
152    Terminology: FHIRTerminology + Send + Sync + 'static,
153>(
154    method: Method,
155    Extension(user): Extension<Arc<User>>,
156    OriginalUri(uri): OriginalUri,
157    Path(path): Path<FHIRHandlerPath>,
158    State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
159    body: String,
160) -> Result<Response, OperationOutcomeError> {
161    fhir_handler(user, method, uri, path, state, body).await
162}
163
164async fn public_metadata_handler<
165    Repo: Repository + Send + Sync + 'static,
166    Search: SearchEngine + Send + Sync + 'static,
167    Terminology: FHIRTerminology + Send + Sync + 'static,
168>(
169    Path(path): Path<FHIRRootHandlerPath>,
170    State(state): State<Arc<ServerState<Repo, Search, Terminology>>>,
171) -> Result<Response, OperationOutcomeError> {
172    let ctx = Arc::new(ServerCTX::system(
173        path.tenant,
174        path.project,
175        state.fhir_client.clone(),
176        state.rate_limit.clone(),
177    ));
178
179    state
180        .fhir_client
181        .capabilities(ctx)
182        .await
183        .map(|capabilities| FHIRResponse::Capabilities(FHIRCapabilitiesResponse { capabilities }))
184        .map(|fhir_response| fhir_response.into_response())
185}
186
187pub async fn server(
188    config: Arc<ServerConfig>,
189) -> Result<NormalizePath<Router>, OperationOutcomeError> {
190    let ip_source = match &config.monitoring.ip_source {
191        crate::config::IpSource::ConnectInfo => ClientIpSource::ConnectInfo,
192        crate::config::IpSource::CfConnectingIp => ClientIpSource::CfConnectingIp,
193        crate::config::IpSource::XRealIp => ClientIpSource::XRealIp,
194    };
195
196    get_certification_provider(config.as_ref());
197
198    let pool = get_pool(config.as_ref()).await;
199    let session_store = PostgresStore::new(pool.clone());
200    session_store.migrate().await.map_err(ConfigError::from)?;
201
202    let shared_state = create_services(config.clone()).await?;
203
204    let fhir_router = Router::new()
205        .route("/{fhir_version}", any(fhir_root_handler))
206        .route("/{fhir_version}/{*fhir_location}", any(fhir_type_handler));
207
208    let protected_resources_router = Router::new()
209        .nest("/fhir", fhir_router)
210        .route("/mcp", post(mcp::route::mcp_handler))
211        .layer(
212            ServiceBuilder::new()
213                .layer(axum::middleware::from_fn_with_state(
214                    shared_state.clone(),
215                    auth_n::middleware::basic_auth::basic_auth_middleware,
216                ))
217                .layer(axum::middleware::from_fn_with_state(
218                    shared_state.clone(),
219                    auth_n::middleware::jwt::token_verifcation,
220                ))
221                .layer(axum::middleware::from_fn(
222                    auth_n::middleware::project_access::project_access,
223                )),
224        );
225
226    let smart_configuration_router = Router::new()
227        // Per spec must be at root of the fhir server which is why /fhir/{fhir_version}/.well-known/smart-configuration is used as the route.
228        // Because this is publically available it is not under protected_resources_router and does not require authentication.
229        .route(
230            "/fhir/{fhir_version}/.well-known/smart-configuration",
231            get(auth_n::oidc::routes::discovery::smart_configuration),
232        )
233        .route_layer(
234            ServiceBuilder::new().layer(axum::middleware::from_fn_with_state(
235                shared_state.clone(),
236                auth_n::oidc::middleware::project_exists,
237            )),
238        );
239
240    let mut project_router = Router::new()
241        .merge(protected_resources_router)
242        .merge(smart_configuration_router)
243        .nest(
244            "/oidc",
245            auth_n::oidc::routes::create_router(shared_state.clone()),
246        );
247
248    if config.security.publicize_fhir_metadata {
249        project_router = project_router.route(
250            "/fhir/{fhir_version}/metadata",
251            get(public_metadata_handler),
252        );
253    }
254
255    let tenant_router = Router::new()
256        .route("/branding/logo", get(auth_n::tenant::routes::logo))
257        .nest("/auth", auth_n::tenant::routes::create_router())
258        .nest("/{project}/api/v1", project_router)
259        .nest(
260            "/mfa",
261            auth_n::mfa::routes::create_router(shared_state.clone()),
262        )
263        .layer(
264            // Relies on tenant (and now tenant branding) for html so moving operation outcome error handling to here.
265            ServiceBuilder::new()
266                .layer(axum::middleware::from_fn_with_state(
267                    shared_state.clone(),
268                    operation_outcome_error_handle,
269                ))
270                .layer(from_fn(log_operationoutcome_errors)),
271        );
272
273    let discovery_2_0_document_router = Router::new()
274        .route(
275            "/openid-configuration/w/{tenant}/{project}/{*resource}",
276            get(auth_n::oidc::routes::discovery::openid_configuration),
277        )
278        .route(
279            "/openid-configuration/w/{tenant}/{project}",
280            get(auth_n::oidc::routes::discovery::openid_configuration),
281        )
282        .route(
283            "/oauth-protected-resource/w/{tenant}/{project}/{*resource}",
284            get(auth_n::oidc::routes::discovery::oauth_protected_resource),
285        );
286
287    let app = Router::new()
288        .nest("/.well-known", discovery_2_0_document_router)
289        .nest(
290            "/auth",
291            auth_n::global::routes::create_router(shared_state.clone()),
292        )
293        .route("/openapi.json", get(openapi::openapi_document_handler))
294        .route(
295            "/schemas/fhir/{resource_type}",
296            get(openapi::resource_schema_handler),
297        )
298        .nest("/w/{tenant}", tenant_router)
299        .layer(
300            ServiceBuilder::new()
301                .layer(CatchPanicLayer::new())
302                .layer(ip_source.into_extension())
303                .layer(NewSentryLayer::<Request<Body>>::new_from_top())
304                .layer(TraceLayer::new_for_http())
305                // 4mb by default.
306                .layer(DefaultBodyLimit::max(config.max_request_body_size))
307                .layer(CompressionLayer::new())
308                .layer(SecurityHeaderLayer::new())
309                .layer(SetResponseHeaderLayer::overriding(
310                    HeaderName::from_static("x-api-version"),
311                    HeaderValue::from_static(SERVER_VERSION),
312                ))
313                .layer(
314                    SessionManagerLayer::new(session_store)
315                        .with_secure(true)
316                        .with_same_site(SameSite::None)
317                        .with_expiry(Expiry::OnInactivity(Duration::days(3))),
318                )
319                .layer(
320                    CorsLayer::new()
321                        .allow_methods(Any)
322                        .allow_origin(Any)
323                        .allow_headers(Any),
324                ),
325        )
326        .with_state(shared_state)
327        .nest(root_asset_route().to_str().unwrap(), create_static_server());
328
329    Ok(NormalizePathLayer::trim_trailing_slash().layer(app))
330}
331
332pub async fn serve(config: Arc<ServerConfig>, port: u16) -> Result<(), OperationOutcomeError> {
333    let server = server(config).await?;
334
335    let addr = format!("0.0.0.0:{}", port);
336    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
337
338    tracing::info!("Server started");
339    axum::serve(
340        listener,
341        <tower_http::normalize_path::NormalizePath<Router> as ServiceExt<
342            axum::http::Request<Body>,
343        >>::into_make_service_with_connect_info::<SocketAddr>(server),
344    )
345    .await
346    .unwrap();
347
348    Ok(())
349}