mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
Route credentialed traffic through MITM proxy
This commit is contained in:
@@ -10,6 +10,7 @@ use crate::types::TurnAttemptsSiblingTurnsResponse;
|
||||
use anyhow::Result;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_client::chatgpt_cloudflare_cookie_header;
|
||||
use codex_client::with_chatgpt_cloudflare_cookie_store;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::default_client::get_codex_user_agent;
|
||||
@@ -21,6 +22,7 @@ use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::SpendControlLimitSnapshot;
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
use reqwest::header::COOKIE;
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::header::HeaderName;
|
||||
use reqwest::header::HeaderValue;
|
||||
@@ -450,6 +452,16 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn credential_routes_proxy_headers(&self) -> HeaderMap {
|
||||
let mut headers = self.headers();
|
||||
if let Ok(proxy_url) = reqwest::Url::parse(&self.credential_routes_proxy_url())
|
||||
&& let Some(cookie) = chatgpt_cloudflare_cookie_header(&proxy_url)
|
||||
{
|
||||
headers.insert(COOKIE, cookie);
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
/// Create a new task (user turn) by POSTing to the appropriate backend path
|
||||
/// based on `path_style`. Returns the created task id.
|
||||
pub async fn create_task(&self, request_body: serde_json::Value) -> Result<String> {
|
||||
|
||||
@@ -55,6 +55,10 @@ pub fn with_chatgpt_cloudflare_cookie_store(
|
||||
builder.cookie_provider(Arc::clone(&SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE))
|
||||
}
|
||||
|
||||
pub fn chatgpt_cloudflare_cookie_header(url: &reqwest::Url) -> Option<HeaderValue> {
|
||||
SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE.cookies(url)
|
||||
}
|
||||
|
||||
fn is_chatgpt_cookie_url(url: &reqwest::Url) -> bool {
|
||||
match url.scheme() {
|
||||
"https" => {}
|
||||
|
||||
@@ -9,6 +9,7 @@ mod sse;
|
||||
mod telemetry;
|
||||
mod transport;
|
||||
|
||||
pub use crate::chatgpt_cloudflare_cookies::chatgpt_cloudflare_cookie_header;
|
||||
pub use crate::chatgpt_cloudflare_cookies::with_chatgpt_cloudflare_cookie_store;
|
||||
pub use crate::chatgpt_hosts::is_allowed_chatgpt_host;
|
||||
pub use crate::custom_ca::BuildCustomCaTransportError;
|
||||
|
||||
@@ -26,6 +26,8 @@ pub use config::NetworkProxyConfig;
|
||||
pub use config::NetworkUnixSocketPermission;
|
||||
pub use config::NetworkUnixSocketPermissions;
|
||||
pub use config::host_and_port_from_network_addr;
|
||||
pub use mitm_hook::CredentialedRouteProxyActionConfig;
|
||||
pub use mitm_hook::CredentialedRouteProxyHeader;
|
||||
pub use mitm_hook::InjectedHeaderConfig;
|
||||
pub use mitm_hook::MitmHookActionsConfig;
|
||||
pub use mitm_hook::MitmHookBodyConfig;
|
||||
|
||||
@@ -36,6 +36,7 @@ use rama_http::Response;
|
||||
use rama_http::StatusCode;
|
||||
use rama_http::Uri;
|
||||
use rama_http::header::HOST;
|
||||
use rama_http::header::HeaderName;
|
||||
use rama_http::layer::remove_header::RemoveRequestHeaderLayer;
|
||||
use rama_http::layer::remove_header::RemoveResponseHeaderLayer;
|
||||
use rama_http_backend::server::HttpServer;
|
||||
@@ -50,6 +51,7 @@ use std::task::Context as TaskContext;
|
||||
use std::task::Poll;
|
||||
use tracing::info;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
/// State needed to terminate a CONNECT tunnel and enforce policy on inner HTTPS requests.
|
||||
pub struct MitmState {
|
||||
@@ -87,6 +89,10 @@ enum MitmPolicyDecision {
|
||||
|
||||
const MITM_INSPECT_BODIES: bool = false;
|
||||
const MITM_MAX_BODY_BYTES: usize = 4096;
|
||||
const CREDENTIAL_ROUTE_CONNECTOR_ID_HEADER: &str =
|
||||
"x-openai-internal-credential-route-connector-id";
|
||||
const CREDENTIAL_ROUTE_LINK_ID_HEADER: &str = "x-openai-internal-credential-route-link-id";
|
||||
const CREDENTIAL_ROUTE_REQUEST_URL_HEADER: &str = "x-openai-internal-credential-route-request-url";
|
||||
|
||||
impl std::fmt::Debug for MitmState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@@ -241,10 +247,27 @@ async fn forward_request(req: Request, request_ctx: &MitmRequestContext) -> Resu
|
||||
let (mut parts, body) = req.into_parts();
|
||||
apply_mitm_hook_actions(&mut parts.headers, hook_actions.as_ref());
|
||||
let authority = authority_header_value(&target_host, target_port);
|
||||
parts.uri = build_https_uri(&authority, &path)?;
|
||||
parts
|
||||
.headers
|
||||
.insert(HOST, HeaderValue::from_str(&authority)?);
|
||||
let original_request_uri = build_https_uri(&authority, &path)?;
|
||||
if let Some(actions) = hook_actions.as_ref()
|
||||
&& let Some(credentialed_route_proxy) = actions.credentialed_route_proxy.as_ref()
|
||||
{
|
||||
let (proxy_uri, proxy_authority) =
|
||||
credentialed_route_proxy_request(credentialed_route_proxy)?;
|
||||
apply_credentialed_route_proxy_headers(
|
||||
&mut parts.headers,
|
||||
credentialed_route_proxy,
|
||||
&original_request_uri,
|
||||
)?;
|
||||
parts.uri = proxy_uri;
|
||||
parts
|
||||
.headers
|
||||
.insert(HOST, HeaderValue::from_str(&proxy_authority)?);
|
||||
} else {
|
||||
parts.uri = original_request_uri;
|
||||
parts
|
||||
.headers
|
||||
.insert(HOST, HeaderValue::from_str(&authority)?);
|
||||
}
|
||||
|
||||
let inspect = mitm.inspect_enabled();
|
||||
let max_body_bytes = mitm.max_body_bytes();
|
||||
@@ -556,6 +579,48 @@ fn build_https_uri(authority: &str, path: &str) -> Result<Uri> {
|
||||
Ok(target.parse()?)
|
||||
}
|
||||
|
||||
fn credentialed_route_proxy_request(
|
||||
action: &crate::mitm_hook::CredentialedRouteProxyAction,
|
||||
) -> Result<(Uri, String)> {
|
||||
let proxy_url = Url::parse(&action.proxy_url)
|
||||
.with_context(|| format!("invalid credentialed route proxy URL {}", action.proxy_url))?;
|
||||
let proxy_authority = url_authority_header_value(&proxy_url)?;
|
||||
Ok((proxy_url.as_str().parse()?, proxy_authority))
|
||||
}
|
||||
|
||||
fn apply_credentialed_route_proxy_headers(
|
||||
headers: &mut HeaderMap,
|
||||
action: &crate::mitm_hook::CredentialedRouteProxyAction,
|
||||
request_uri: &Uri,
|
||||
) -> Result<()> {
|
||||
for proxy_header in &action.proxy_headers {
|
||||
headers.insert(proxy_header.name.clone(), proxy_header.value.clone());
|
||||
}
|
||||
headers.insert(
|
||||
HeaderName::from_static(CREDENTIAL_ROUTE_CONNECTOR_ID_HEADER),
|
||||
HeaderValue::from_str(&action.connector_id)?,
|
||||
);
|
||||
headers.insert(
|
||||
HeaderName::from_static(CREDENTIAL_ROUTE_LINK_ID_HEADER),
|
||||
HeaderValue::from_str(&action.link_id)?,
|
||||
);
|
||||
headers.insert(
|
||||
HeaderName::from_static(CREDENTIAL_ROUTE_REQUEST_URL_HEADER),
|
||||
HeaderValue::from_str(&request_uri.to_string())?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn url_authority_header_value(url: &Url) -> Result<String> {
|
||||
let host = url
|
||||
.host_str()
|
||||
.ok_or_else(|| anyhow!("URL must include a host"))?;
|
||||
let port = url
|
||||
.port_or_known_default()
|
||||
.ok_or_else(|| anyhow!("URL must include a known port"))?;
|
||||
Ok(authority_header_value(host, port))
|
||||
}
|
||||
|
||||
fn path_and_query(uri: &Uri) -> String {
|
||||
uri.path_and_query()
|
||||
.map(rama_http::uri::PathAndQuery::as_str)
|
||||
|
||||
@@ -47,6 +47,8 @@ pub struct MitmHookMatchConfig {
|
||||
pub struct MitmHookActionsConfig {
|
||||
pub strip_request_headers: Vec<String>,
|
||||
pub inject_request_headers: Vec<InjectedHeaderConfig>,
|
||||
#[serde(skip)]
|
||||
pub credentialed_route_proxy: Option<CredentialedRouteProxyActionConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
@@ -94,6 +96,7 @@ pub struct HeaderConstraint {
|
||||
pub struct MitmHookActions {
|
||||
pub strip_request_headers: Vec<HeaderName>,
|
||||
pub inject_request_headers: Vec<ResolvedInjectedHeader>,
|
||||
pub credentialed_route_proxy: Option<CredentialedRouteProxyAction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -109,6 +112,37 @@ pub enum SecretSource {
|
||||
File(AbsolutePathBuf),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct CredentialedRouteProxyActionConfig {
|
||||
pub connector_id: String,
|
||||
pub link_id: String,
|
||||
pub proxy_headers: Vec<CredentialedRouteProxyHeader>,
|
||||
pub proxy_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CredentialedRouteProxyAction {
|
||||
pub connector_id: String,
|
||||
pub link_id: String,
|
||||
pub proxy_headers: Vec<CredentialedRouteProxyHeader>,
|
||||
pub proxy_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct CredentialedRouteProxyHeader {
|
||||
pub name: HeaderName,
|
||||
pub value: HeaderValue,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for CredentialedRouteProxyHeader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CredentialedRouteProxyHeader")
|
||||
.field("name", &self.name)
|
||||
.field("value", &"<redacted>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MitmHookBodyMatcher {
|
||||
pub raw: serde_json::Value,
|
||||
@@ -315,6 +349,16 @@ where
|
||||
.with_context(|| format!("failed to compile injected header {}", header.name))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let credentialed_route_proxy =
|
||||
hook.actions
|
||||
.credentialed_route_proxy
|
||||
.as_ref()
|
||||
.map(|action| CredentialedRouteProxyAction {
|
||||
connector_id: action.connector_id.clone(),
|
||||
link_id: action.link_id.clone(),
|
||||
proxy_headers: action.proxy_headers.clone(),
|
||||
proxy_url: action.proxy_url.clone(),
|
||||
});
|
||||
|
||||
hooks_by_host
|
||||
.entry(host.clone())
|
||||
@@ -331,6 +375,7 @@ where
|
||||
actions: MitmHookActions {
|
||||
strip_request_headers,
|
||||
inject_request_headers,
|
||||
credentialed_route_proxy,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -676,6 +721,7 @@ mod tests {
|
||||
secret_file: None,
|
||||
prefix: Some("Bearer ".to_string()),
|
||||
}],
|
||||
credentialed_route_proxy: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ fn github_write_hook() -> crate::mitm_hook::MitmHookConfig {
|
||||
secret_file: None,
|
||||
prefix: Some("Bearer ".to_string()),
|
||||
}],
|
||||
credentialed_route_proxy: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -305,6 +306,7 @@ fn apply_mitm_hook_actions_replaces_authorization_header() {
|
||||
AbsolutePathBuf::try_from("/tmp/github-token").unwrap(),
|
||||
),
|
||||
}],
|
||||
credentialed_route_proxy: None,
|
||||
};
|
||||
|
||||
apply_mitm_hook_actions(&mut headers, Some(&actions));
|
||||
@@ -318,3 +320,53 @@ fn apply_mitm_hook_actions_replaces_authorization_header() {
|
||||
Some(&HeaderValue::from_static("req_123"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credentialed_route_proxy_request_rewrites_upstream_target() {
|
||||
let action = crate::mitm_hook::CredentialedRouteProxyAction {
|
||||
connector_id: "connector_123".to_string(),
|
||||
link_id: "link_123".to_string(),
|
||||
proxy_headers: vec![crate::mitm_hook::CredentialedRouteProxyHeader {
|
||||
name: HeaderName::from_static("authorization"),
|
||||
value: HeaderValue::from_static("Bearer codex-token"),
|
||||
}],
|
||||
proxy_url: "http://localhost:8080/api/codex/credential_routes/proxy".to_string(),
|
||||
};
|
||||
let request_uri: Uri = "https://api.example.com/v1/items?limit=5".parse().unwrap();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
HeaderName::from_static("authorization"),
|
||||
HeaderValue::from_static("Bearer provider-token"),
|
||||
);
|
||||
headers.insert(
|
||||
HeaderName::from_static(CREDENTIAL_ROUTE_REQUEST_URL_HEADER),
|
||||
HeaderValue::from_static("https://spoofed.example.com"),
|
||||
);
|
||||
|
||||
let (proxy_uri, proxy_authority) = credentialed_route_proxy_request(&action).unwrap();
|
||||
apply_credentialed_route_proxy_headers(&mut headers, &action, &request_uri).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
proxy_uri.to_string(),
|
||||
"http://localhost:8080/api/codex/credential_routes/proxy"
|
||||
);
|
||||
assert_eq!(proxy_authority, "localhost:8080");
|
||||
assert_eq!(
|
||||
headers.get("authorization"),
|
||||
Some(&HeaderValue::from_static("Bearer codex-token"))
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get(CREDENTIAL_ROUTE_CONNECTOR_ID_HEADER),
|
||||
Some(&HeaderValue::from_static("connector_123"))
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get(CREDENTIAL_ROUTE_LINK_ID_HEADER),
|
||||
Some(&HeaderValue::from_static("link_123"))
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get(CREDENTIAL_ROUTE_REQUEST_URL_HEADER),
|
||||
Some(&HeaderValue::from_static(
|
||||
"https://api.example.com/v1/items?limit=5"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user