pub mod artifact_routes;
pub mod error;
pub mod health;
pub mod limits;
pub mod logging;
pub mod npm_routes;
pub mod pypi_routes;
use std::sync::Arc;
use axum::Router;
use axum::extract::Request;
use axum::http::{HeaderValue, Method, StatusCode, header};
use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use tokio::sync::OwnedSemaphorePermit;
use crate::App;
use crate::http::error::ApiError;
pub fn router(app: Arc<App>) -> Router {
Router::new()
.route("/health/live", get(health::live))
.route("/health/ready", get(health::ready))
.route("/health/delivery", get(health::delivery))
.route("/npm/-/ping", get(npm_routes::ping))
.route("/npm/-", get(unknown_route))
.route("/npm/-/{*rest}", get(unknown_route))
.route("/npm/{package}", get(npm_routes::package))
.route(
"/npm/{package}/{version_or_tag}",
get(npm_routes::package_version),
)
.route("/pypi/simple/", get(pypi_routes::index))
.route("/pypi/simple/{project}/", get(pypi_routes::project))
.route(
"/pypi/simple/{project}",
get(pypi_routes::project_without_slash),
)
.route_layer(middleware::from_fn(get_only))
.route(
"/npm/artifacts/{reference_id}/{filename}",
get(artifact_routes::serve_npm).head(artifact_routes::serve_npm),
)
.route(
"/pypi/artifacts/{reference_id}/{filename}",
get(artifact_routes::serve_pypi).head(artifact_routes::serve_pypi),
)
.fallback(unknown_route)
.method_not_allowed_fallback(unsupported_method)
.layer(middleware::map_response(no_store))
.layer(middleware::from_fn_with_state(
Arc::clone(&app),
logging::decide,
))
.with_state(app)
}
fn admit(app: &App) -> Result<OwnedSemaphorePermit, ApiError> {
let permit = app.limits.active_permit().ok_or(ApiError::Overloaded)?;
if !app.store().is_healthy()
&& app
.blocklist()
.is_some_and(|snapshot| snapshot.is_valid_at(app.clock.now_utc_micros()))
{
return Err(ApiError::StorageUnusable);
}
Ok(permit)
}
async fn unknown_route() -> ApiError {
ApiError::NotFound
}
async fn unsupported_method() -> ApiError {
ApiError::MethodNotAllowed
}
async fn get_only(request: Request, next: Next) -> Response {
let mut response = if request.method() == Method::HEAD {
ApiError::MethodNotAllowed.into_response()
} else {
next.run(request).await
};
if response.status() == StatusCode::METHOD_NOT_ALLOWED {
response
.headers_mut()
.insert(header::ALLOW, HeaderValue::from_static("GET"));
}
response
}
async fn no_store(mut response: Response) -> Response {
response
.headers_mut()
.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
response
}