mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Route credentialed traffic through MITM proxy
This commit is contained in:
@@ -177,6 +177,37 @@ impl NetworkProxySpec {
|
||||
Ok(spec)
|
||||
}
|
||||
|
||||
pub(crate) fn with_credentialed_routes(
|
||||
&self,
|
||||
credentialed_routes: &crate::credentialed_routes::CredentialedRoutesSessionConfig,
|
||||
) -> std::io::Result<Self> {
|
||||
let mut spec = self.clone();
|
||||
let credentialed_route_hooks = credentialed_routes.mitm_hooks();
|
||||
let mut allowed_domains = spec.config.network.allowed_domains().unwrap_or_default();
|
||||
for hook in &credentialed_route_hooks {
|
||||
if !allowed_domains
|
||||
.iter()
|
||||
.any(|allowed_domain| normalize_host(allowed_domain) == normalize_host(&hook.host))
|
||||
{
|
||||
allowed_domains.push(hook.host.clone());
|
||||
}
|
||||
}
|
||||
spec.config.network.set_allowed_domains(allowed_domains);
|
||||
let mut mitm_hooks = credentialed_route_hooks;
|
||||
mitm_hooks.extend(spec.config.network.mitm_hooks);
|
||||
spec.config.network.mitm_hooks = mitm_hooks;
|
||||
spec.config.network.mitm = spec.config.network.mode
|
||||
== codex_network_proxy::NetworkMode::Limited
|
||||
|| !spec.config.network.mitm_hooks.is_empty();
|
||||
validate_policy_against_constraints(&spec.config, &spec.constraints).map_err(|err| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("network proxy constraints are invalid: {err}"),
|
||||
)
|
||||
})?;
|
||||
Ok(spec)
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_to_started_proxy(
|
||||
&self,
|
||||
started_proxy: &StartedNetworkProxy,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
use codex_backend_client::CredentialRouteAuthType;
|
||||
use codex_backend_client::ResolvedCredentialRoute;
|
||||
use codex_config::NetworkDomainPermissionToml;
|
||||
use codex_config::NetworkDomainPermissionsToml;
|
||||
use codex_network_proxy::NetworkDomainPermission;
|
||||
@@ -80,6 +82,37 @@ fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() {
|
||||
assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credentialed_routes_add_runtime_allowlist_and_mitm_hooks() {
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
NetworkProxyConfig::default(),
|
||||
None,
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
|
||||
)
|
||||
.expect("config should load");
|
||||
let credentialed_routes = crate::credentialed_routes::CredentialedRoutesSessionConfig {
|
||||
routes: vec![ResolvedCredentialRoute {
|
||||
connector_id: "connector_123".to_string(),
|
||||
link_id: "link_123".to_string(),
|
||||
auth_type: CredentialRouteAuthType::OAuth,
|
||||
base_url: "https://api.example.com/v1".to_string(),
|
||||
}],
|
||||
proxy_url: Some("https://chatgpt.com/backend-api/wham/credential_routes/proxy".to_string()),
|
||||
};
|
||||
|
||||
let spec = spec
|
||||
.with_credentialed_routes(&credentialed_routes)
|
||||
.expect("credentialed routes should fit unconstrained config");
|
||||
|
||||
assert_eq!(
|
||||
spec.config.network.allowed_domains(),
|
||||
Some(vec!["api.example.com".to_string()])
|
||||
);
|
||||
assert!(spec.config.network.mitm);
|
||||
assert_eq!(spec.config.network.mitm_hooks.len(), 1);
|
||||
assert_eq!(spec.config.network.mitm_hooks[0].host, "api.example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_allowed_domains_do_not_override_user_denies_for_same_pattern() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_backend_client::ResolvedCredentialRoute;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_network_proxy::CredentialedRouteProxyActionConfig;
|
||||
use codex_network_proxy::MitmHookActionsConfig;
|
||||
use codex_network_proxy::MitmHookConfig;
|
||||
use codex_network_proxy::MitmHookMatchConfig;
|
||||
use tracing::debug;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct CredentialedRoutesSessionConfig {
|
||||
pub(crate) routes: Vec<ResolvedCredentialRoute>,
|
||||
pub(crate) proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) async fn load_for_session(
|
||||
chatgpt_base_url: &str,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> Vec<ResolvedCredentialRoute> {
|
||||
) -> CredentialedRoutesSessionConfig {
|
||||
let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) else {
|
||||
return Vec::new();
|
||||
return CredentialedRoutesSessionConfig::default();
|
||||
};
|
||||
|
||||
let client = match BackendClient::from_auth(chatgpt_base_url.to_string(), auth) {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
warn!(error = %err, "failed to initialize credentialed routes client");
|
||||
return Vec::new();
|
||||
return CredentialedRoutesSessionConfig::default();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,11 +37,126 @@ pub(crate) async fn load_for_session(
|
||||
credentialed_routes = response.routes.len(),
|
||||
"loaded credentialed routes for session"
|
||||
);
|
||||
response.routes
|
||||
CredentialedRoutesSessionConfig {
|
||||
routes: response.routes,
|
||||
proxy_url: Some(client.credential_routes_proxy_url()),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(error = %err, "failed to load credentialed routes for session");
|
||||
Vec::new()
|
||||
CredentialedRoutesSessionConfig::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CredentialedRoutesSessionConfig {
|
||||
pub(crate) fn mitm_hooks(&self) -> Vec<MitmHookConfig> {
|
||||
let Some(proxy_url) = self.proxy_url.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
self.routes
|
||||
.iter()
|
||||
.filter_map(|route| match route_mitm_hook(route, proxy_url) {
|
||||
Ok(hook) => Some(hook),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
connector_id = %route.connector_id,
|
||||
link_id = %route.link_id,
|
||||
base_url = %route.base_url,
|
||||
error = %err,
|
||||
"skipping invalid credentialed route"
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn route_mitm_hook(
|
||||
route: &ResolvedCredentialRoute,
|
||||
proxy_url: &str,
|
||||
) -> anyhow::Result<MitmHookConfig> {
|
||||
let base_url = Url::parse(&route.base_url)?;
|
||||
anyhow::ensure!(
|
||||
base_url.scheme() == "https",
|
||||
"credentialed route must use https"
|
||||
);
|
||||
let host = base_url
|
||||
.host_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("credentialed route must include a host"))?;
|
||||
anyhow::ensure!(
|
||||
base_url.username().is_empty() && base_url.password().is_none(),
|
||||
"credentialed route must not include user info"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
base_url.fragment().is_none() && base_url.query().is_none(),
|
||||
"credentialed route must not include query or fragment"
|
||||
);
|
||||
let path_prefix = match base_url.path() {
|
||||
"" => "/",
|
||||
path => path,
|
||||
};
|
||||
|
||||
Ok(MitmHookConfig {
|
||||
host: host.to_string(),
|
||||
matcher: MitmHookMatchConfig {
|
||||
methods: vec![
|
||||
"DELETE".to_string(),
|
||||
"GET".to_string(),
|
||||
"HEAD".to_string(),
|
||||
"PATCH".to_string(),
|
||||
"POST".to_string(),
|
||||
"PUT".to_string(),
|
||||
],
|
||||
path_prefixes: vec![path_prefix.to_string()],
|
||||
..MitmHookMatchConfig::default()
|
||||
},
|
||||
actions: MitmHookActionsConfig {
|
||||
credentialed_route_proxy: Some(CredentialedRouteProxyActionConfig {
|
||||
connector_id: route.connector_id.clone(),
|
||||
link_id: route.link_id.clone(),
|
||||
proxy_url: proxy_url.to_string(),
|
||||
}),
|
||||
..MitmHookActionsConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_backend_client::CredentialRouteAuthType;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn credentialed_routes_compile_to_internal_mitm_hooks() {
|
||||
let config = CredentialedRoutesSessionConfig {
|
||||
routes: vec![ResolvedCredentialRoute {
|
||||
connector_id: "connector_123".to_string(),
|
||||
link_id: "link_123".to_string(),
|
||||
auth_type: CredentialRouteAuthType::OAuth,
|
||||
base_url: "https://api.example.com/v1".to_string(),
|
||||
}],
|
||||
proxy_url: Some(
|
||||
"https://chatgpt.com/backend-api/wham/credential_routes/proxy".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
let hooks = config.mitm_hooks();
|
||||
|
||||
assert_eq!(hooks.len(), 1);
|
||||
assert_eq!(hooks[0].host, "api.example.com");
|
||||
assert_eq!(hooks[0].matcher.path_prefixes, vec!["/v1".to_string()]);
|
||||
assert_eq!(
|
||||
hooks[0].actions.credentialed_route_proxy,
|
||||
Some(CredentialedRouteProxyActionConfig {
|
||||
connector_id: "connector_123".to_string(),
|
||||
link_id: "link_123".to_string(),
|
||||
proxy_url: "https://chatgpt.com/backend-api/wham/credential_routes/proxy"
|
||||
.to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -877,7 +877,7 @@ async fn thread_title_from_thread_store(
|
||||
|
||||
struct ManagedNetworkProxyStartParams<'a> {
|
||||
spec: &'a crate::config::NetworkProxySpec,
|
||||
credentialed_routes: &'a [codex_backend_client::ResolvedCredentialRoute],
|
||||
credentialed_routes: &'a crate::credentialed_routes::CredentialedRoutesSessionConfig,
|
||||
exec_policy: &'a codex_execpolicy::Policy,
|
||||
permission_profile: &'a PermissionProfile,
|
||||
network_policy_decider: Option<Arc<dyn codex_network_proxy::NetworkPolicyDecider>>,
|
||||
@@ -961,10 +961,18 @@ impl Session {
|
||||
}: ManagedNetworkProxyStartParams<'_>,
|
||||
) -> anyhow::Result<(StartedNetworkProxy, SessionNetworkProxyRuntime)> {
|
||||
debug!(
|
||||
credentialed_routes = credentialed_routes.len(),
|
||||
credentialed_routes = credentialed_routes.routes.len(),
|
||||
"starting managed network proxy"
|
||||
);
|
||||
let spec = spec
|
||||
.with_credentialed_routes(credentialed_routes)
|
||||
.map_err(|err| {
|
||||
tracing::warn!(
|
||||
"failed to apply credentialed routes to managed proxy; continuing without credentialed routes: {err}"
|
||||
);
|
||||
err
|
||||
})
|
||||
.unwrap_or_else(|_| spec.clone())
|
||||
.with_exec_policy_network_rules(exec_policy)
|
||||
.map_err(|err| {
|
||||
tracing::warn!(
|
||||
@@ -1023,6 +1031,15 @@ impl Session {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let spec = match spec.with_credentialed_routes(&self.services.credentialed_routes) {
|
||||
Ok(spec) => spec,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"failed to apply credentialed routes while refreshing managed network proxy: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let current_exec_policy = self.services.exec_policy.current();
|
||||
let spec = match spec.with_exec_policy_network_rules(current_exec_policy.as_ref()) {
|
||||
Ok(spec) => spec,
|
||||
|
||||
@@ -811,11 +811,14 @@ impl Session {
|
||||
Arc::clone(network_policy_decider_session),
|
||||
)
|
||||
});
|
||||
let credentialed_routes = if config.permissions.network.is_some() {
|
||||
crate::credentialed_routes::load_for_session(&config.chatgpt_base_url, auth)
|
||||
.await
|
||||
} else {
|
||||
crate::credentialed_routes::CredentialedRoutesSessionConfig::default()
|
||||
};
|
||||
let (network_proxy, session_network_proxy) =
|
||||
if let Some(spec) = config.permissions.network.as_ref() {
|
||||
let credentialed_routes =
|
||||
crate::credentialed_routes::load_for_session(&config.chatgpt_base_url, auth)
|
||||
.await;
|
||||
let current_exec_policy = exec_policy.current();
|
||||
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
|
||||
ManagedNetworkProxyStartParams {
|
||||
@@ -923,6 +926,7 @@ impl Session {
|
||||
thread_extension_data,
|
||||
agent_control,
|
||||
network_proxy,
|
||||
credentialed_routes,
|
||||
network_approval: Arc::clone(&network_approval),
|
||||
state_db: state_db_ctx.clone(),
|
||||
live_thread: live_thread_init.as_ref().cloned(),
|
||||
|
||||
@@ -685,7 +685,7 @@ async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyho
|
||||
|
||||
let (started_proxy, _) = Session::start_managed_network_proxy(ManagedNetworkProxyStartParams {
|
||||
spec: &spec,
|
||||
credentialed_routes: &[],
|
||||
credentialed_routes: &crate::credentialed_routes::CredentialedRoutesSessionConfig::default(),
|
||||
exec_policy: &exec_policy,
|
||||
permission_profile: &permission_profile_for_sandbox_policy(
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
@@ -732,7 +732,7 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules()
|
||||
|
||||
let (started_proxy, _) = Session::start_managed_network_proxy(ManagedNetworkProxyStartParams {
|
||||
spec: &spec,
|
||||
credentialed_routes: &[],
|
||||
credentialed_routes: &crate::credentialed_routes::CredentialedRoutesSessionConfig::default(),
|
||||
exec_policy: &exec_policy,
|
||||
permission_profile: &permission_profile_for_sandbox_policy(
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
@@ -774,7 +774,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R
|
||||
|
||||
let (started_proxy, _) = Session::start_managed_network_proxy(ManagedNetworkProxyStartParams {
|
||||
spec: &spec,
|
||||
credentialed_routes: &[],
|
||||
credentialed_routes: &crate::credentialed_routes::CredentialedRoutesSessionConfig::default(),
|
||||
exec_policy: &exec_policy,
|
||||
permission_profile: &permission_profile_for_sandbox_policy(
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
@@ -849,7 +849,7 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow
|
||||
)?;
|
||||
let (started_proxy, _) = Session::start_managed_network_proxy(ManagedNetworkProxyStartParams {
|
||||
spec: &spec,
|
||||
credentialed_routes: &[],
|
||||
credentialed_routes: &crate::credentialed_routes::CredentialedRoutesSessionConfig::default(),
|
||||
exec_policy: &Policy::empty(),
|
||||
permission_profile: &permission_profile_for_sandbox_policy(&initial_policy),
|
||||
network_policy_decider: None,
|
||||
@@ -4352,6 +4352,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
thread_extension_data: codex_extension_api::ExtensionData::new(thread_id.to_string()),
|
||||
agent_control,
|
||||
network_proxy: None,
|
||||
credentialed_routes: crate::credentialed_routes::CredentialedRoutesSessionConfig::default(),
|
||||
network_approval: Arc::clone(&network_approval),
|
||||
state_db: None,
|
||||
live_thread: None,
|
||||
@@ -6208,6 +6209,7 @@ where
|
||||
thread_extension_data: codex_extension_api::ExtensionData::new(thread_id.to_string()),
|
||||
agent_control,
|
||||
network_proxy: None,
|
||||
credentialed_routes: crate::credentialed_routes::CredentialedRoutesSessionConfig::default(),
|
||||
network_approval: Arc::clone(&network_approval),
|
||||
state_db: state_db.clone(),
|
||||
live_thread: None,
|
||||
|
||||
@@ -66,6 +66,7 @@ pub(crate) struct SessionServices {
|
||||
pub(crate) thread_extension_data: ExtensionData,
|
||||
pub(crate) agent_control: AgentControl,
|
||||
pub(crate) network_proxy: Option<StartedNetworkProxy>,
|
||||
pub(crate) credentialed_routes: crate::credentialed_routes::CredentialedRoutesSessionConfig,
|
||||
pub(crate) network_approval: Arc<NetworkApprovalService>,
|
||||
pub(crate) state_db: Option<StateDbHandle>,
|
||||
pub(crate) live_thread: Option<LiveThread>,
|
||||
|
||||
@@ -24,6 +24,7 @@ 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::InjectedHeaderConfig;
|
||||
pub use mitm_hook::MitmHookActionsConfig;
|
||||
pub use mitm_hook::MitmHookBodyConfig;
|
||||
|
||||
@@ -47,6 +47,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 {
|
||||
@@ -228,10 +229,22 @@ 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, &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();
|
||||
@@ -547,6 +560,31 @@ fn build_https_uri(authority: &str, path: &str) -> Result<Uri> {
|
||||
Ok(target.parse()?)
|
||||
}
|
||||
|
||||
fn credentialed_route_proxy_request(
|
||||
action: &crate::mitm_hook::CredentialedRouteProxyAction,
|
||||
request_uri: &Uri,
|
||||
) -> Result<(Uri, String)> {
|
||||
let mut proxy_url = Url::parse(&action.proxy_url)
|
||||
.with_context(|| format!("invalid credentialed route proxy URL {}", action.proxy_url))?;
|
||||
proxy_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("link_id", &action.link_id)
|
||||
.append_pair("connector_id", &action.connector_id)
|
||||
.append_pair("request_url", &request_uri.to_string());
|
||||
let proxy_authority = url_authority_header_value(&proxy_url)?;
|
||||
Ok((proxy_url.as_str().parse()?, proxy_authority))
|
||||
}
|
||||
|
||||
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,20 @@ pub enum SecretSource {
|
||||
File(AbsolutePathBuf),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct CredentialedRouteProxyActionConfig {
|
||||
pub connector_id: String,
|
||||
pub link_id: String,
|
||||
pub proxy_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CredentialedRouteProxyAction {
|
||||
pub connector_id: String,
|
||||
pub link_id: String,
|
||||
pub proxy_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MitmHookBodyMatcher {
|
||||
pub raw: serde_json::Value,
|
||||
@@ -315,6 +332,15 @@ 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_url: action.proxy_url.clone(),
|
||||
});
|
||||
|
||||
hooks_by_host
|
||||
.entry(host.clone())
|
||||
@@ -331,6 +357,7 @@ where
|
||||
actions: MitmHookActions {
|
||||
strip_request_headers,
|
||||
inject_request_headers,
|
||||
credentialed_route_proxy,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -676,6 +703,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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -261,6 +262,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));
|
||||
@@ -274,3 +276,22 @@ 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_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 (proxy_uri, proxy_authority) =
|
||||
credentialed_route_proxy_request(&action, &request_uri).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
proxy_uri.to_string(),
|
||||
"http://localhost:8080/api/codex/credential_routes/proxy?link_id=link_123&connector_id=connector_123&request_url=https%3A%2F%2Fapi.example.com%2Fv1%2Fitems%3Flimit%3D5"
|
||||
);
|
||||
assert_eq!(proxy_authority, "localhost:8080");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user