2025-09-09 14:23:23 -07:00
|
|
|
use reqwest::header::HeaderValue;
|
|
|
|
|
use std::sync::LazyLock;
|
2025-09-03 10:11:02 -07:00
|
|
|
|
2025-09-09 14:23:23 -07:00
|
|
|
pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct Originator {
|
|
|
|
|
pub value: String,
|
|
|
|
|
pub header_value: HeaderValue,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub static ORIGINATOR: LazyLock<Originator> = LazyLock::new(|| {
|
|
|
|
|
let default = "codex_cli_rs";
|
|
|
|
|
let value = std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR)
|
|
|
|
|
.unwrap_or_else(|_| default.to_string());
|
|
|
|
|
|
|
|
|
|
match HeaderValue::from_str(&value) {
|
|
|
|
|
Ok(header_value) => Originator {
|
|
|
|
|
value,
|
|
|
|
|
header_value,
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::error!("Unable to turn originator override {value} into header value: {e}");
|
|
|
|
|
Originator {
|
|
|
|
|
value: default.to_string(),
|
|
|
|
|
header_value: HeaderValue::from_static(default),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
pub fn get_codex_user_agent() -> String {
|
2025-09-03 10:11:02 -07:00
|
|
|
let build_version = env!("CARGO_PKG_VERSION");
|
|
|
|
|
let os_info = os_info::get();
|
|
|
|
|
format!(
|
|
|
|
|
"{}/{build_version} ({} {}; {}) {}",
|
2025-09-09 14:23:23 -07:00
|
|
|
ORIGINATOR.value.as_str(),
|
2025-09-03 10:11:02 -07:00
|
|
|
os_info.os_type(),
|
|
|
|
|
os_info.version(),
|
|
|
|
|
os_info.architecture().unwrap_or("unknown"),
|
|
|
|
|
crate::terminal::user_agent()
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a reqwest client with default `originator` and `User-Agent` headers set.
|
2025-09-09 14:23:23 -07:00
|
|
|
pub fn create_client() -> reqwest::Client {
|
2025-09-03 10:11:02 -07:00
|
|
|
use reqwest::header::HeaderMap;
|
|
|
|
|
|
|
|
|
|
let mut headers = HeaderMap::new();
|
2025-09-09 14:23:23 -07:00
|
|
|
headers.insert("originator", ORIGINATOR.header_value.clone());
|
|
|
|
|
let ua = get_codex_user_agent();
|
2025-09-03 10:11:02 -07:00
|
|
|
|
2025-09-09 14:23:23 -07:00
|
|
|
reqwest::Client::builder()
|
2025-09-03 10:11:02 -07:00
|
|
|
// Set UA via dedicated helper to avoid header validation pitfalls
|
|
|
|
|
.user_agent(ua)
|
|
|
|
|
.default_headers(headers)
|
|
|
|
|
.build()
|
2025-09-09 14:23:23 -07:00
|
|
|
.unwrap_or_else(|_| reqwest::Client::new())
|
2025-09-03 10:11:02 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_get_codex_user_agent() {
|
2025-09-09 14:23:23 -07:00
|
|
|
let user_agent = get_codex_user_agent();
|
2025-09-03 10:11:02 -07:00
|
|
|
assert!(user_agent.starts_with("codex_cli_rs/"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_create_client_sets_default_headers() {
|
|
|
|
|
use wiremock::Mock;
|
|
|
|
|
use wiremock::MockServer;
|
|
|
|
|
use wiremock::ResponseTemplate;
|
|
|
|
|
use wiremock::matchers::method;
|
|
|
|
|
use wiremock::matchers::path;
|
|
|
|
|
|
2025-09-09 14:23:23 -07:00
|
|
|
let client = create_client();
|
2025-09-03 10:11:02 -07:00
|
|
|
|
|
|
|
|
// Spin up a local mock server and capture a request.
|
|
|
|
|
let server = MockServer::start().await;
|
|
|
|
|
Mock::given(method("GET"))
|
|
|
|
|
.and(path("/"))
|
|
|
|
|
.respond_with(ResponseTemplate::new(200))
|
|
|
|
|
.mount(&server)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let resp = client
|
|
|
|
|
.get(server.uri())
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.expect("failed to send request");
|
|
|
|
|
assert!(resp.status().is_success());
|
|
|
|
|
|
|
|
|
|
let requests = server
|
|
|
|
|
.received_requests()
|
|
|
|
|
.await
|
|
|
|
|
.expect("failed to fetch received requests");
|
|
|
|
|
assert!(!requests.is_empty());
|
|
|
|
|
let headers = &requests[0].headers;
|
|
|
|
|
|
|
|
|
|
// originator header is set to the provided value
|
|
|
|
|
let originator_header = headers
|
|
|
|
|
.get("originator")
|
|
|
|
|
.expect("originator header missing");
|
2025-09-09 14:23:23 -07:00
|
|
|
assert_eq!(originator_header.to_str().unwrap(), "codex_cli_rs");
|
2025-09-03 10:11:02 -07:00
|
|
|
|
|
|
|
|
// User-Agent matches the computed Codex UA for that originator
|
2025-09-09 14:23:23 -07:00
|
|
|
let expected_ua = get_codex_user_agent();
|
2025-09-03 10:11:02 -07:00
|
|
|
let ua_header = headers
|
|
|
|
|
.get("user-agent")
|
|
|
|
|
.expect("user-agent header missing");
|
|
|
|
|
assert_eq!(ua_header.to_str().unwrap(), expected_ua);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
fn test_macos() {
|
|
|
|
|
use regex_lite::Regex;
|
2025-09-09 14:23:23 -07:00
|
|
|
let user_agent = get_codex_user_agent();
|
2025-09-03 10:11:02 -07:00
|
|
|
let re = Regex::new(
|
|
|
|
|
r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\) (\S+)$",
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(re.is_match(&user_agent));
|
|
|
|
|
}
|
|
|
|
|
}
|