diff --git a/backend/crates/gateway-api/src/lib.rs b/backend/crates/gateway-api/src/lib.rs index 4a32026d6..87d014e22 100644 --- a/backend/crates/gateway-api/src/lib.rs +++ b/backend/crates/gateway-api/src/lib.rs @@ -16,7 +16,7 @@ use gateway_core::engine::execution::ExecutionService; use gateway_core::health::{HealthProbe, WorkerHealthSource}; use gateway_core::lifecycle::ConnectionLifecycle; use serde::Deserialize; -use tower_http::cors::{Any, CorsLayer}; +use tower_http::cors::CorsLayer; use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer}; use tower_http::services::{ServeDir, ServeFile}; use tower_http::timeout::TimeoutLayer; @@ -146,11 +146,17 @@ pub fn initialize( .map_err(|_| ApiError::Config(ApiConfigError::InvalidCorsOrigin)) }) .collect::, _>>()?; + // credentials 模式禁止通配 allow_headers;显式列出鉴权与内容协商头。 router = router.layer( CorsLayer::new() .allow_origin(origins) .allow_methods([Method::GET, Method::POST]) - .allow_headers(Any) + .allow_headers([ + axum::http::header::AUTHORIZATION, + axum::http::header::CONTENT_TYPE, + HeaderName::from_static("x-api-key"), + request_id_header.clone(), + ]) .allow_credentials(true), ); } diff --git a/backend/crates/gateway-api/tests/openai/mod.rs b/backend/crates/gateway-api/tests/openai/mod.rs index 0889f6367..cdc866eaa 100644 --- a/backend/crates/gateway-api/tests/openai/mod.rs +++ b/backend/crates/gateway-api/tests/openai/mod.rs @@ -39,11 +39,18 @@ use gateway_core::routing::{ }; pub(super) async fn api_router(execution: Arc) -> axum::Router { + api_router_with_origins(execution, Vec::new()).await +} + +pub(super) async fn api_router_with_origins( + execution: Arc, + cors_allowed_origins: Vec, +) -> axum::Router { let admin = crate::admin::AdminTestFixture::new().await; gateway_api::initialize( gateway_api::ApiConfig { asset_directory: std::env::temp_dir(), - cors_allowed_origins: Vec::new(), + cors_allowed_origins, request_timeout_seconds: None, request_id_header: "x-request-id".to_owned(), }, diff --git a/backend/crates/gateway-api/tests/openai/models.rs b/backend/crates/gateway-api/tests/openai/models.rs index a72cbfb94..7aec8d66e 100644 --- a/backend/crates/gateway-api/tests/openai/models.rs +++ b/backend/crates/gateway-api/tests/openai/models.rs @@ -15,13 +15,13 @@ use tower::ServiceExt; use super::{api_router, authenticated_client}; -struct ModelsExecution { +pub(super) struct ModelsExecution { client: AuthenticatedClient, profiles: bool, } impl ModelsExecution { - fn new() -> Arc { + pub(super) fn new() -> Arc { Arc::new(Self { client: authenticated_client("sk_models_test"), profiles: false, diff --git a/backend/crates/gateway-api/tests/openai/router.rs b/backend/crates/gateway-api/tests/openai/router.rs index 982f177f5..92ed7d10e 100644 --- a/backend/crates/gateway-api/tests/openai/router.rs +++ b/backend/crates/gateway-api/tests/openai/router.rs @@ -2,12 +2,15 @@ use axum::{ Router, body::{Body, Bytes}, extract::DefaultBodyLimit, - http::{Request, StatusCode}, + http::{Method, Request, StatusCode}, routing::post, }; use gateway_api::openai::router::MAX_CLIENT_REQUEST_BODY_BYTES; use tower::ServiceExt; +use super::api_router_with_origins; +use super::models::ModelsExecution; + fn body_limit_app() -> Router { Router::new() .route( @@ -50,3 +53,50 @@ async fn responses_body_limit_should_reject_one_byte_over_sixteen_mibibytes() { assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); } + +#[tokio::test] +async fn configured_cors_origin_assembles_router_and_answers_preflight() { + let router = api_router_with_origins( + ModelsExecution::new(), + vec!["https://app.example.com".into()], + ) + .await; + + let response = router + .oneshot( + Request::builder() + .method(Method::OPTIONS) + .uri("/v1/models") + .header("origin", "https://app.example.com") + .header("access-control-request-method", "GET") + .header("access-control-request-headers", "authorization") + .body(Body::empty()) + .expect("build preflight request"), + ) + .await + .expect("route preflight request"); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response + .headers() + .get("access-control-allow-origin") + .and_then(|value| value.to_str().ok()), + Some("https://app.example.com") + ); + assert_eq!( + response + .headers() + .get("access-control-allow-credentials") + .and_then(|value| value.to_str().ok()), + Some("true") + ); + let allow_headers = response + .headers() + .get("access-control-allow-headers") + .and_then(|value| value.to_str().ok()) + .expect("allow-headers present"); + assert!(allow_headers.contains("authorization")); + assert!(allow_headers.contains("x-api-key")); + assert!(allow_headers.contains("x-request-id")); +}