Skip to content
Clone
631aa98
151 lines 5.4 KB Rust
pub mod cleanup;
pub mod openapi;
pub mod spa;
use axum::Router;
use axum::extract::{Request, State};
use axum::http::StatusCode;
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use export::AppState;
use export::middleware::AppTraceLayer;
use tower_http::limit::RequestBodyLimitLayer;
use tower_http::trace::TraceLayer;
const MAX_REQUEST_BODY_BYTES: usize = 2 * 1024 * 1024;
pub const SERVICE_KEY_HEADER: &str = "x-gitdata-service-key";
fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
let mut diff = 0u8;
for (a, b) in left.iter().zip(right.iter()) {
diff |= a ^ b;
}
diff == 0
}
async fn require_service_key(
State(expected): State<String>,
request: Request,
next: Next,
) -> Response {
let provided = request
.headers()
.get(SERVICE_KEY_HEADER)
.and_then(|value| value.to_str().ok())
.unwrap_or_default();
if expected.is_empty() || !constant_time_eq(provided.as_bytes(), expected.as_bytes()) {
return (StatusCode::UNAUTHORIZED, "invalid service key").into_response();
}
next.run(request).await
}
fn build_cors_layer(web: &config::web::WebConfig) -> tower_http::cors::CorsLayer {
use axum::http::{HeaderName, header};
use tower_http::cors::{AllowOrigin, CorsLayer};
if web.cors_allow_origins.is_empty() {
return CorsLayer::new();
}
let origins: Vec<axum::http::HeaderValue> = web
.cors_allow_origins
.iter()
.filter_map(|o| o.parse().ok())
.collect();
CorsLayer::new()
.allow_origin(AllowOrigin::list(origins))
.allow_credentials(web.cors_allow_credentials)
.allow_methods([
axum::http::Method::GET,
axum::http::Method::POST,
axum::http::Method::PUT,
axum::http::Method::PATCH,
axum::http::Method::DELETE,
axum::http::Method::OPTIONS,
])
.allow_headers([
header::CONTENT_TYPE,
header::AUTHORIZATION,
HeaderName::from_static("x-request-id"),
])
}
pub async fn router(state: AppState) -> Result<Router, jwt::SessionError> {
let cors = build_cors_layer(&state.config.web);
let auth = auth::services::AuthService::new(state.clone());
let chat = chat::services::service::ChatService::new(state.clone());
let issues = issues::services::service::IssueService::new(state.clone());
let space = space::services::service::SpaceService::new(state.clone());
let user = user::services::service::UserService::new(state.clone());
let git = git::service::GitService::new(state.clone());
let pulls = pulls::services::service::PullService::new(state.clone());
let internal_service_key = state.config.internal.service_key.clone();
let jwt_verifier = if state.config.oauth.enabled {
match jwt::JwtVerifier::new(&state.config.oauth) {
Ok(verifier) => Some(verifier),
Err(error) => {
tracing::warn!(%error, "oauth is enabled but the JWT verifier could not be built");
None
}
}
} else {
None
};
let revalidate_state = state.clone();
let health_state = state;
// 清理接口需要跨模块调用(space/issues/git),这里的服务都还没被
// 其他 router 的 with_state 消费掉,先构造再合并。
let cleanup_router = cleanup::router(space.clone(), issues.clone(), git.clone());
let public: Router = Router::new()
.merge(auth::routes::router(auth.clone()).with_state(auth.clone()))
.merge(chat::routes::router(chat.clone(), space.clone()).with_state(chat))
.merge(issues::routes::router(issues.clone()).with_state(issues))
.merge(space::routes::router(space.clone()).with_state(space))
.merge(user::routes::router(user.clone()).with_state(user))
.merge(git::routes::router(git.clone()).with_state(git.clone()))
.merge(pulls::routes::router().with_state(pulls))
.merge(cleanup_router);
let internal: Router = Router::new()
.merge(git::routes::internal_router(git.clone()).with_state(git))
.route_layer(axum::middleware::from_fn_with_state(
internal_service_key,
require_service_key,
));
let router: Router = Router::new()
.nest("/api/v1", public)
.nest("/api/v1/internal", internal)
.nest("/pages", auth::routes::pages_router(auth.clone()))
.merge(export::health::router(health_state))
.fallback(spa::fallback)
.layer(cors)
.layer(RequestBodyLimitLayer::new(MAX_REQUEST_BODY_BYTES))
.layer(
TraceLayer::new_for_http()
.make_span_with(export::telemetry::HttpSpan)
.on_response(export::telemetry::HttpOnResponse),
)
.layer(export::telemetry::metrics_layer())
.layer(AppTraceLayer::new());
let router = match jwt_verifier {
Some(verifier) => {
let warm = verifier.clone();
tokio::spawn(async move { warm.warm_up().await });
router
.layer(axum::middleware::from_fn_with_state(
revalidate_state,
auth::oauth::revalidate::revalidate,
))
.layer(axum::Extension(verifier))
}
None => router,
};
Ok(router)
}