mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
fix(api): stop CORS assembly from panicking on configured origins
Replace the wildcard allow_headers with an explicit whitelist (authorization, content-type, x-api-key, and the configured request-id header): tower-http asserts against wildcard headers combined with allow_credentials, so any non-empty cors_allowed_origins previously panicked inside initialize() before the server could listen. Add a preflight assembly test covering a configured origin. Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ use gateway_core::engine::execution::ExecutionService;
|
|||||||
use gateway_core::health::{HealthProbe, WorkerHealthSource};
|
use gateway_core::health::{HealthProbe, WorkerHealthSource};
|
||||||
use gateway_core::lifecycle::ConnectionLifecycle;
|
use gateway_core::lifecycle::ConnectionLifecycle;
|
||||||
use serde::Deserialize;
|
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::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer};
|
||||||
use tower_http::services::{ServeDir, ServeFile};
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
use tower_http::timeout::TimeoutLayer;
|
use tower_http::timeout::TimeoutLayer;
|
||||||
@@ -146,11 +146,17 @@ pub fn initialize(
|
|||||||
.map_err(|_| ApiError::Config(ApiConfigError::InvalidCorsOrigin))
|
.map_err(|_| ApiError::Config(ApiConfigError::InvalidCorsOrigin))
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
// credentials 模式禁止通配 allow_headers;显式列出鉴权与内容协商头。
|
||||||
router = router.layer(
|
router = router.layer(
|
||||||
CorsLayer::new()
|
CorsLayer::new()
|
||||||
.allow_origin(origins)
|
.allow_origin(origins)
|
||||||
.allow_methods([Method::GET, Method::POST])
|
.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),
|
.allow_credentials(true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,18 @@ use gateway_core::routing::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub(super) async fn api_router(execution: Arc<dyn ExecutionService>) -> axum::Router {
|
pub(super) async fn api_router(execution: Arc<dyn ExecutionService>) -> axum::Router {
|
||||||
|
api_router_with_origins(execution, Vec::new()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn api_router_with_origins(
|
||||||
|
execution: Arc<dyn ExecutionService>,
|
||||||
|
cors_allowed_origins: Vec<String>,
|
||||||
|
) -> axum::Router {
|
||||||
let admin = crate::admin::AdminTestFixture::new().await;
|
let admin = crate::admin::AdminTestFixture::new().await;
|
||||||
gateway_api::initialize(
|
gateway_api::initialize(
|
||||||
gateway_api::ApiConfig {
|
gateway_api::ApiConfig {
|
||||||
asset_directory: std::env::temp_dir(),
|
asset_directory: std::env::temp_dir(),
|
||||||
cors_allowed_origins: Vec::new(),
|
cors_allowed_origins,
|
||||||
request_timeout_seconds: None,
|
request_timeout_seconds: None,
|
||||||
request_id_header: "x-request-id".to_owned(),
|
request_id_header: "x-request-id".to_owned(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ use tower::ServiceExt;
|
|||||||
|
|
||||||
use super::{api_router, authenticated_client};
|
use super::{api_router, authenticated_client};
|
||||||
|
|
||||||
struct ModelsExecution {
|
pub(super) struct ModelsExecution {
|
||||||
client: AuthenticatedClient,
|
client: AuthenticatedClient,
|
||||||
profiles: bool,
|
profiles: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModelsExecution {
|
impl ModelsExecution {
|
||||||
fn new() -> Arc<Self> {
|
pub(super) fn new() -> Arc<Self> {
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
client: authenticated_client("sk_models_test"),
|
client: authenticated_client("sk_models_test"),
|
||||||
profiles: false,
|
profiles: false,
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ use axum::{
|
|||||||
Router,
|
Router,
|
||||||
body::{Body, Bytes},
|
body::{Body, Bytes},
|
||||||
extract::DefaultBodyLimit,
|
extract::DefaultBodyLimit,
|
||||||
http::{Request, StatusCode},
|
http::{Method, Request, StatusCode},
|
||||||
routing::post,
|
routing::post,
|
||||||
};
|
};
|
||||||
use gateway_api::openai::router::MAX_CLIENT_REQUEST_BODY_BYTES;
|
use gateway_api::openai::router::MAX_CLIENT_REQUEST_BODY_BYTES;
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
use super::api_router_with_origins;
|
||||||
|
use super::models::ModelsExecution;
|
||||||
|
|
||||||
fn body_limit_app() -> Router {
|
fn body_limit_app() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.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);
|
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"));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user