aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/mod.rs')
-rw-r--r--src/api/mod.rs75
1 files changed, 74 insertions, 1 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 9bda84a..35b1a06 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -77,13 +77,17 @@ pub(crate) fn router(state: AppState, auth_state: AuthState, debug_api: bool) ->
api.layer(middleware::from_fn_with_state(auth_state, basic_auth))
// The hand-written spec (`docs/openapi.yaml`), and the Swagger UI showing it.
.merge(
- SwaggerUi::new("/swagger-ui").config(utoipa_swagger_ui::Config::from("/openapi.yaml")),
+ // Relative to the UI's page (`/swagger-ui/`), so it is found behind a proxy that
+ // serves the API under a path of its own too.
+ SwaggerUi::new("/swagger-ui")
+ .config(utoipa_swagger_ui::Config::from("../openapi.yaml")),
)
.route("/openapi.yaml", get(openapi_spec))
.route("/health", get(health::node_health))
.route("/ready", get(health::ready))
// Outermost, so authentication failures get the error body too.
.layer(middleware::from_fn(json_errors))
+ .layer(middleware::from_fn(relative_redirects))
.with_state(state)
}
@@ -107,6 +111,7 @@ pub(crate) fn relay_only_router(
.layer(middleware::from_fn_with_state(auth_state, basic_auth))
.route("/health", get(health::health))
.layer(middleware::from_fn(json_errors))
+ .layer(middleware::from_fn(relative_redirects))
.with_state(relay)
}
@@ -131,6 +136,56 @@ pub(crate) fn error_response(
(status, Json(body)).into_response()
}
+/// Redirects to a path of this API (`Location: /store/…`) made relative to the request's
+/// (`3`, `swagger-ui/`): they then lead to the same place behind a reverse proxy
+/// that serves the API under a path of its own (`/continuum/store/…`).
+async fn relative_redirects(request: Request, next: Next) -> Response {
+ let from = request.uri().path().to_owned();
+ let mut response = next.run(request).await;
+ let relative = response
+ .headers()
+ .get(axum::http::header::LOCATION)
+ .and_then(|location| location.to_str().ok())
+ .filter(|location| location.starts_with('/') && !location.starts_with("//"))
+ .map(|location| relative_to(&from, location));
+ if let Some(location) = relative.and_then(|location| HeaderValue::from_str(&location).ok()) {
+ response
+ .headers_mut()
+ .insert(axum::http::header::LOCATION, location);
+ }
+ response
+}
+
+/// `to` (a path, with its query if any) as a reference relative to the page at `from`
+/// (a path).
+fn relative_to(from: &str, to: &str) -> String {
+ let (path, query) = match to.split_once('?') {
+ Some((path, query)) => (path, Some(query)),
+ None => (to, None),
+ };
+ // The page's directory, and the target's segments (the last its name).
+ let from: Vec<&str> = from.split('/').collect();
+ let directory = &from[..from.len() - 1];
+ let target: Vec<&str> = path.split('/').collect();
+ let common = directory
+ .iter()
+ .zip(&target[..target.len() - 1])
+ .take_while(|(a, b)| a == b)
+ .count();
+ let mut relative = "../".repeat(directory.len() - common);
+ relative.push_str(&target[common..].join("/"));
+ // Not empty, and not taken for a scheme (`a:b`).
+ let first = relative.split('/').next().unwrap_or("");
+ if relative.is_empty() || first.contains(':') {
+ relative.insert_str(0, "./");
+ }
+ if let Some(query) = query {
+ relative.push('?');
+ relative.push_str(query);
+ }
+ relative
+}
+
/// Gives every error answer the same body (`error_response`). Handlers that fail with
/// a bare status or a text message, and axum's own rejections (a malformed query or
/// body), get it here: the text becomes the message, or the status's reason if there
@@ -236,3 +291,21 @@ pub(crate) fn store_status(error: StoreError, context: &str) -> ApiError {
pub(crate) fn media_type(value: &str) -> &str {
value.split(';').next().unwrap_or("").trim()
}
+
+#[cfg(test)]
+mod redirect_tests {
+ use super::relative_to;
+
+ #[test]
+ fn redirects_are_relative_to_the_request() {
+ assert_eq!(relative_to("/store/queue/-1", "/store/queue/3"), "3");
+ assert_eq!(
+ relative_to("/store/a/-1/b", "/store/a/0/b?x=1"),
+ "../0/b?x=1"
+ );
+ assert_eq!(relative_to("/store/-1", "/store/2"), "2");
+ assert_eq!(relative_to("/swagger-ui", "/swagger-ui/"), "swagger-ui/");
+ assert_eq!(relative_to("/store/a/b", "/store/a/"), "./");
+ assert_eq!(relative_to("/store/x", "/store/a:b"), "./a:b");
+ }
+}