mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
Refactor api-provision browser auth reuse
Move the reusable authorization-code callback server into codex-login::server, switch api-provision over to the shared PKCE/state/callback flow, and keep the TUI browser path alive even when auto-open fails. Validation: - cargo check -p codex-login --lib - cargo fmt --all --manifest-path /home/dev-user/code/codex/codex-rs/Cargo.toml - git diff --check Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -1,38 +1,26 @@
|
||||
//! Browser-based helper for onboarding login and Codex auth provisioning.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_utils_home_dir::find_codex_home;
|
||||
use rand::RngCore;
|
||||
use reqwest::Client;
|
||||
use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use sha2::Digest;
|
||||
use sha2::Sha256;
|
||||
use tiny_http::Header;
|
||||
use tiny_http::Response;
|
||||
use tiny_http::Server;
|
||||
use tiny_http::StatusCode;
|
||||
use url::Url;
|
||||
|
||||
use crate::auth::AuthDotJson;
|
||||
use crate::pkce::PkceCodes;
|
||||
use crate::server::AuthorizationCodeServer;
|
||||
use crate::server::start_authorization_code_server;
|
||||
|
||||
const AUTH_ISSUER: &str = "https://auth.openai.com";
|
||||
const PLATFORM_HYDRA_CLIENT_ID: &str = "app_2SKx67EdpoN0G6j64rFvigXD";
|
||||
@@ -85,23 +73,32 @@ pub struct PendingApiProvisioning {
|
||||
options: ApiProvisionOptions,
|
||||
redirect_uri: String,
|
||||
code_verifier: String,
|
||||
callback_server: LocalCallbackServer,
|
||||
auth_url: String,
|
||||
callback_server: AuthorizationCodeServer,
|
||||
}
|
||||
|
||||
impl PendingApiProvisioning {
|
||||
pub fn auth_url(&self) -> &str {
|
||||
&self.auth_url
|
||||
&self.callback_server.auth_url
|
||||
}
|
||||
|
||||
pub fn callback_port(&self) -> u16 {
|
||||
self.callback_server.actual_port
|
||||
}
|
||||
|
||||
pub fn open_browser(&self) -> bool {
|
||||
webbrowser::open(&self.auth_url).is_ok()
|
||||
self.callback_server.open_browser()
|
||||
}
|
||||
|
||||
pub fn open_browser_or_print(&self) -> bool {
|
||||
self.callback_server.open_browser_or_print()
|
||||
}
|
||||
|
||||
pub async fn finish(self) -> Result<ProvisionedApiKey, HelperError> {
|
||||
let code = self
|
||||
.callback_server
|
||||
.wait_for_code(Duration::from_secs(OAUTH_TIMEOUT_SECONDS))?;
|
||||
.wait_for_code(Duration::from_secs(OAUTH_TIMEOUT_SECONDS))
|
||||
.await
|
||||
.map_err(|err| HelperError::message(err.to_string()))?;
|
||||
provision_from_authorization_code(
|
||||
&self.client,
|
||||
&self.options,
|
||||
@@ -129,20 +126,24 @@ pub fn start_api_provisioning(
|
||||
) -> Result<PendingApiProvisioning, HelperError> {
|
||||
validate_api_provision_options(&options)?;
|
||||
let client = build_http_client()?;
|
||||
let pkce = generate_pkce();
|
||||
let state = generate_state();
|
||||
let callback_server =
|
||||
LocalCallbackServer::bind(options.callback_port, DEFAULT_CALLBACK_PATH, &state)?;
|
||||
let callback_server = start_authorization_code_server(
|
||||
options.callback_port,
|
||||
DEFAULT_CALLBACK_PATH,
|
||||
/*force_state*/ None,
|
||||
|redirect_uri, pkce, state| {
|
||||
build_authorize_url(&options, redirect_uri, pkce, state)
|
||||
.map_err(|err| std::io::Error::other(err.to_string()))
|
||||
},
|
||||
)
|
||||
.map_err(|err| HelperError::message(err.to_string()))?;
|
||||
let redirect_uri = callback_server.redirect_uri.clone();
|
||||
let auth_url = build_authorize_url(&options, &redirect_uri, &pkce, &state)?;
|
||||
|
||||
Ok(PendingApiProvisioning {
|
||||
client,
|
||||
options,
|
||||
redirect_uri,
|
||||
code_verifier: pkce.code_verifier,
|
||||
code_verifier: callback_server.code_verifier().to_string(),
|
||||
callback_server,
|
||||
auth_url,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -155,7 +156,7 @@ pub async fn run_from_env() -> Result<(), HelperError> {
|
||||
ParseOutcome::Run(options) => {
|
||||
let auth_path = resolve_codex_auth_path(options.codex_auth_path.as_deref())?;
|
||||
let session = start_api_provisioning(options.api_provision_options())?;
|
||||
open_browser_or_print(session.auth_url());
|
||||
session.open_browser_or_print();
|
||||
let provisioned = session.finish().await?;
|
||||
let codex_auth_synced = !options.skip_codex_auth_sync;
|
||||
if codex_auth_synced {
|
||||
@@ -380,222 +381,6 @@ fn parse_u64(raw: String, flag: &str) -> Result<u64, HelperError> {
|
||||
.map_err(|err| HelperError::message(format!("invalid value for `{flag}`: {err}")))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PkceCodes {
|
||||
code_verifier: String,
|
||||
code_challenge: String,
|
||||
}
|
||||
|
||||
fn generate_pkce() -> PkceCodes {
|
||||
let mut bytes = [0u8; 64];
|
||||
rand::rng().fill_bytes(&mut bytes);
|
||||
let code_verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
|
||||
let digest = Sha256::digest(code_verifier.as_bytes());
|
||||
let code_challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest);
|
||||
PkceCodes {
|
||||
code_verifier,
|
||||
code_challenge,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_state() -> String {
|
||||
let mut bytes = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut bytes);
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
|
||||
}
|
||||
|
||||
struct LocalCallbackServer {
|
||||
redirect_uri: String,
|
||||
rx: Receiver<Result<String, String>>,
|
||||
server: Arc<Server>,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
worker: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl LocalCallbackServer {
|
||||
fn bind(port: u16, callback_path: &str, expected_state: &str) -> Result<Self, HelperError> {
|
||||
let server = Arc::new(Server::http(format!("127.0.0.1:{port}")).map_err(|err| {
|
||||
HelperError::message(format!("failed to bind callback server: {err}"))
|
||||
})?);
|
||||
let actual_port = server
|
||||
.server_addr()
|
||||
.to_ip()
|
||||
.map(|addr| addr.port())
|
||||
.ok_or_else(|| {
|
||||
HelperError::message("unable to determine callback server port".to_string())
|
||||
})?;
|
||||
let (tx, rx) = channel();
|
||||
let callback_path = callback_path.to_string();
|
||||
let expected_state = expected_state.to_string();
|
||||
let redirect_uri = format!("http://localhost:{actual_port}{callback_path}");
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
let server_for_thread = Arc::clone(&server);
|
||||
let shutdown_for_thread = Arc::clone(&shutdown);
|
||||
let callback_path_for_thread = callback_path;
|
||||
let worker = std::thread::spawn(move || {
|
||||
callback_loop(
|
||||
server_for_thread,
|
||||
shutdown_for_thread,
|
||||
tx,
|
||||
callback_path_for_thread,
|
||||
expected_state,
|
||||
);
|
||||
});
|
||||
Ok(Self {
|
||||
redirect_uri,
|
||||
rx,
|
||||
server,
|
||||
shutdown,
|
||||
worker: Some(worker),
|
||||
})
|
||||
}
|
||||
|
||||
fn wait_for_code(self, timeout: Duration) -> Result<String, HelperError> {
|
||||
match self.rx.recv_timeout(timeout) {
|
||||
Ok(Ok(code)) => Ok(code),
|
||||
Ok(Err(message)) => Err(HelperError::message(message)),
|
||||
Err(_) => Err(HelperError::message(
|
||||
"OAuth login timed out waiting for the browser callback.".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LocalCallbackServer {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown.store(true, Ordering::Relaxed);
|
||||
self.server.unblock();
|
||||
if let Some(worker) = self.worker.take() {
|
||||
let _ = worker.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn callback_loop(
|
||||
server: Arc<Server>,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
tx: std::sync::mpsc::Sender<Result<String, String>>,
|
||||
callback_path: String,
|
||||
expected_state: String,
|
||||
) {
|
||||
while !shutdown.load(Ordering::Relaxed) {
|
||||
let request = match server.recv_timeout(Duration::from_millis(200)) {
|
||||
Ok(Some(request)) => request,
|
||||
Ok(None) => continue,
|
||||
Err(_) => break,
|
||||
};
|
||||
let response = handle_callback_request(request.url(), &callback_path, &expected_state);
|
||||
let should_exit = response.result.is_some();
|
||||
let _ = request.respond(response.response);
|
||||
if let Some(result) = response.result {
|
||||
let _ = tx.send(result);
|
||||
break;
|
||||
}
|
||||
if should_exit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CallbackResponse {
|
||||
response: Response<std::io::Cursor<Vec<u8>>>,
|
||||
result: Option<Result<String, String>>,
|
||||
}
|
||||
|
||||
fn handle_callback_request(
|
||||
url_raw: &str,
|
||||
callback_path: &str,
|
||||
expected_state: &str,
|
||||
) -> CallbackResponse {
|
||||
let parsed_url = match Url::parse(&format!("http://localhost{url_raw}")) {
|
||||
Ok(url) => url,
|
||||
Err(err) => {
|
||||
return CallbackResponse {
|
||||
response: html_response(
|
||||
/*status*/ 400,
|
||||
format!("<h1>Bad Request</h1><p>{err}</p>"),
|
||||
),
|
||||
result: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
if parsed_url.path() != callback_path {
|
||||
return CallbackResponse {
|
||||
response: html_response(/*status*/ 404, "<h1>Not Found</h1>".to_string()),
|
||||
result: None,
|
||||
};
|
||||
}
|
||||
|
||||
let params: HashMap<String, String> = parsed_url.query_pairs().into_owned().collect();
|
||||
if params.get("state").map(String::as_str) != Some(expected_state) {
|
||||
return CallbackResponse {
|
||||
response: html_response(/*status*/ 400, "<h1>State mismatch</h1>".to_string()),
|
||||
result: Some(Err("State mismatch in OAuth callback.".to_string())),
|
||||
};
|
||||
}
|
||||
if let Some(error_code) = params.get("error") {
|
||||
let message = oauth_callback_error_message(
|
||||
error_code,
|
||||
params.get("error_description").map(String::as_str),
|
||||
);
|
||||
return CallbackResponse {
|
||||
response: html_response(
|
||||
/*status*/ 403,
|
||||
"<h1>Sign-in failed</h1><p>Return to your terminal.</p>".to_string(),
|
||||
),
|
||||
result: Some(Err(message)),
|
||||
};
|
||||
}
|
||||
match params.get("code") {
|
||||
Some(code) if !code.is_empty() => CallbackResponse {
|
||||
response: html_response(
|
||||
/*status*/ 200,
|
||||
"<h1>Sign-in complete</h1><p>You can return to your terminal.</p>".to_string(),
|
||||
),
|
||||
result: Some(Ok(code.clone())),
|
||||
},
|
||||
_ => CallbackResponse {
|
||||
response: html_response(
|
||||
/*status*/ 400,
|
||||
"<h1>Missing authorization code</h1>".to_string(),
|
||||
),
|
||||
result: Some(Err(
|
||||
"Missing authorization code. Sign-in could not be completed.".to_string(),
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn html_response(status: u16, body: String) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
let mut response = Response::from_string(body).with_status_code(StatusCode(status));
|
||||
if let Ok(header) = Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..]) {
|
||||
response = response.with_header(header);
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
fn oauth_callback_error_message(error_code: &str, error_description: Option<&str>) -> String {
|
||||
match error_description {
|
||||
Some(description) if !description.is_empty() => {
|
||||
format!("OAuth callback error ({error_code}): {description}")
|
||||
}
|
||||
_ => format!("OAuth callback error ({error_code})."),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_browser_or_print(auth_url: &str) {
|
||||
let opened = webbrowser::open(auth_url).is_ok();
|
||||
if opened {
|
||||
eprintln!(
|
||||
"Starting local login server.\nIf your browser did not open, navigate to this URL to authenticate:\n\n{auth_url}"
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"Starting local login server.\nOpen this URL in your browser to authenticate:\n\n{auth_url}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_authorize_url(
|
||||
options: &ApiProvisionOptions,
|
||||
redirect_uri: &str,
|
||||
|
||||
@@ -110,6 +110,67 @@ impl LoginServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle for a running authorization-code callback server.
|
||||
pub(crate) struct AuthorizationCodeServer {
|
||||
pub auth_url: String,
|
||||
pub actual_port: u16,
|
||||
pub redirect_uri: String,
|
||||
code_verifier: String,
|
||||
server_handle: tokio::task::JoinHandle<io::Result<String>>,
|
||||
shutdown_handle: ShutdownHandle,
|
||||
}
|
||||
|
||||
impl AuthorizationCodeServer {
|
||||
pub fn open_browser(&self) -> bool {
|
||||
webbrowser::open(&self.auth_url).is_ok()
|
||||
}
|
||||
|
||||
pub fn open_browser_or_print(&self) -> bool {
|
||||
let opened = self.open_browser();
|
||||
if opened {
|
||||
eprintln!(
|
||||
"Starting local auth callback server.\nIf your browser did not open, navigate to this URL to continue:\n\n{}",
|
||||
self.auth_url
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"Starting local auth callback server.\nOpen this URL in your browser to continue:\n\n{}",
|
||||
self.auth_url
|
||||
);
|
||||
}
|
||||
opened
|
||||
}
|
||||
|
||||
pub fn code_verifier(&self) -> &str {
|
||||
&self.code_verifier
|
||||
}
|
||||
|
||||
pub async fn wait_for_code(self, timeout: Duration) -> io::Result<String> {
|
||||
let AuthorizationCodeServer {
|
||||
server_handle,
|
||||
shutdown_handle,
|
||||
..
|
||||
} = self;
|
||||
let server_handle = server_handle;
|
||||
tokio::pin!(server_handle);
|
||||
|
||||
tokio::select! {
|
||||
result = &mut server_handle => {
|
||||
result
|
||||
.map_err(|err| io::Error::other(format!("authorization-code server thread panicked: {err:?}")))?
|
||||
}
|
||||
_ = tokio::time::sleep(timeout) => {
|
||||
shutdown_handle.shutdown();
|
||||
let _ = server_handle.await;
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"OAuth login timed out waiting for the browser callback.",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle used to signal the login server loop to exit.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ShutdownHandle {
|
||||
@@ -123,6 +184,92 @@ impl ShutdownHandle {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start_authorization_code_server<F>(
|
||||
port: u16,
|
||||
callback_path: &str,
|
||||
force_state: Option<String>,
|
||||
auth_url_builder: F,
|
||||
) -> io::Result<AuthorizationCodeServer>
|
||||
where
|
||||
F: FnOnce(&str, &PkceCodes, &str) -> io::Result<String>,
|
||||
{
|
||||
let pkce = generate_pkce();
|
||||
let state = force_state.unwrap_or_else(generate_state);
|
||||
let callback_path = callback_path.to_string();
|
||||
|
||||
let server = bind_server(port)?;
|
||||
let actual_port = match server.server_addr().to_ip() {
|
||||
Some(addr) => addr.port(),
|
||||
None => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::AddrInUse,
|
||||
"Unable to determine the server port",
|
||||
));
|
||||
}
|
||||
};
|
||||
let server = Arc::new(server);
|
||||
|
||||
let redirect_uri = format!("http://localhost:{actual_port}{callback_path}");
|
||||
let auth_url = auth_url_builder(&redirect_uri, &pkce, &state)?;
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<Request>(16);
|
||||
let _server_handle = {
|
||||
let server = server.clone();
|
||||
thread::spawn(move || -> io::Result<()> {
|
||||
while let Ok(request) = server.recv() {
|
||||
match tx.blocking_send(request) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
eprintln!("Failed to send request to channel: {error}");
|
||||
return Err(io::Error::other("Failed to send request to channel"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
let shutdown_notify = Arc::new(tokio::sync::Notify::new());
|
||||
let server_handle = {
|
||||
let shutdown_notify = shutdown_notify.clone();
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = loop {
|
||||
tokio::select! {
|
||||
_ = shutdown_notify.notified() => {
|
||||
break Err(io::Error::other("Authentication was not completed"));
|
||||
}
|
||||
maybe_req = rx.recv() => {
|
||||
let Some(req) = maybe_req else {
|
||||
break Err(io::Error::other("Authentication was not completed"));
|
||||
};
|
||||
|
||||
let url_raw = req.url().to_string();
|
||||
let response =
|
||||
process_authorization_code_request(&url_raw, &callback_path, &state);
|
||||
|
||||
if let Some(result) = respond_to_request(req, response).await {
|
||||
break result;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
server.unblock();
|
||||
result
|
||||
})
|
||||
};
|
||||
|
||||
Ok(AuthorizationCodeServer {
|
||||
auth_url,
|
||||
actual_port,
|
||||
redirect_uri,
|
||||
code_verifier: pkce.code_verifier,
|
||||
server_handle,
|
||||
shutdown_handle: ShutdownHandle { shutdown_notify },
|
||||
})
|
||||
}
|
||||
|
||||
/// Starts a local callback server and returns the browser auth URL.
|
||||
pub fn run_login_server(opts: ServerOptions) -> io::Result<LoginServer> {
|
||||
let pkce = generate_pkce();
|
||||
@@ -191,30 +338,7 @@ pub fn run_login_server(opts: ServerOptions) -> io::Result<LoginServer> {
|
||||
let response =
|
||||
process_request(&url_raw, &opts, &redirect_uri, &pkce, actual_port, &state).await;
|
||||
|
||||
let exit_result = match response {
|
||||
HandledRequest::Response(response) => {
|
||||
let _ = tokio::task::spawn_blocking(move || req.respond(response)).await;
|
||||
None
|
||||
}
|
||||
HandledRequest::ResponseAndExit {
|
||||
headers,
|
||||
body,
|
||||
result,
|
||||
} => {
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
send_response_with_disconnect(req, headers, body)
|
||||
})
|
||||
.await;
|
||||
Some(result)
|
||||
}
|
||||
HandledRequest::RedirectWithHeader(header) => {
|
||||
let redirect = Response::empty(302).with_header(header);
|
||||
let _ = tokio::task::spawn_blocking(move || req.respond(redirect)).await;
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(result) = exit_result {
|
||||
if let Some(result) = respond_to_request(req, response).await {
|
||||
break result;
|
||||
}
|
||||
}
|
||||
@@ -237,13 +361,14 @@ pub fn run_login_server(opts: ServerOptions) -> io::Result<LoginServer> {
|
||||
}
|
||||
|
||||
/// Internal callback handling outcome.
|
||||
enum HandledRequest {
|
||||
enum HandledRequest<T> {
|
||||
Response(Response<Cursor<Vec<u8>>>),
|
||||
RedirectWithHeader(Header),
|
||||
ResponseAndExit {
|
||||
status: StatusCode,
|
||||
headers: Vec<Header>,
|
||||
body: Vec<u8>,
|
||||
result: io::Result<()>,
|
||||
result: io::Result<T>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -254,7 +379,7 @@ async fn process_request(
|
||||
pkce: &PkceCodes,
|
||||
actual_port: u16,
|
||||
state: &str,
|
||||
) -> HandledRequest {
|
||||
) -> HandledRequest<()> {
|
||||
let parsed_url = match url::Url::parse(&format!("http://localhost{url_raw}")) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
@@ -392,6 +517,7 @@ async fn process_request(
|
||||
"/success" => {
|
||||
let body = include_str!("assets/success.html");
|
||||
HandledRequest::ResponseAndExit {
|
||||
status: StatusCode(200),
|
||||
headers: match Header::from_bytes(
|
||||
&b"Content-Type"[..],
|
||||
&b"text/html; charset=utf-8"[..],
|
||||
@@ -404,6 +530,7 @@ async fn process_request(
|
||||
}
|
||||
}
|
||||
"/cancel" => HandledRequest::ResponseAndExit {
|
||||
status: StatusCode(200),
|
||||
headers: Vec::new(),
|
||||
body: b"Login cancelled".to_vec(),
|
||||
result: Err(io::Error::new(
|
||||
@@ -415,6 +542,117 @@ async fn process_request(
|
||||
}
|
||||
}
|
||||
|
||||
fn process_authorization_code_request(
|
||||
url_raw: &str,
|
||||
callback_path: &str,
|
||||
expected_state: &str,
|
||||
) -> HandledRequest<String> {
|
||||
let parsed_url = match url::Url::parse(&format!("http://localhost{url_raw}")) {
|
||||
Ok(u) => u,
|
||||
Err(err) => {
|
||||
return HandledRequest::Response(
|
||||
Response::from_string(format!("Bad Request: {err}")).with_status_code(400),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
match parsed_url.path() {
|
||||
"/cancel" => HandledRequest::ResponseAndExit {
|
||||
status: StatusCode(200),
|
||||
headers: Vec::new(),
|
||||
body: b"Login cancelled".to_vec(),
|
||||
result: Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"Login cancelled",
|
||||
)),
|
||||
},
|
||||
path if path == callback_path => {
|
||||
let params: std::collections::HashMap<String, String> =
|
||||
parsed_url.query_pairs().into_owned().collect();
|
||||
|
||||
if params.get("state").map(String::as_str) != Some(expected_state) {
|
||||
return HandledRequest::ResponseAndExit {
|
||||
status: StatusCode(400),
|
||||
headers: html_headers(),
|
||||
body: b"<h1>State mismatch</h1><p>Return to your terminal and try again.</p>"
|
||||
.to_vec(),
|
||||
result: Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"State mismatch in OAuth callback.",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(error_code) = params.get("error") {
|
||||
let message = oauth_callback_error_message(
|
||||
error_code,
|
||||
params.get("error_description").map(String::as_str),
|
||||
);
|
||||
return HandledRequest::ResponseAndExit {
|
||||
status: StatusCode(403),
|
||||
headers: html_headers(),
|
||||
body: b"<h1>Sign-in failed</h1><p>Return to your terminal.</p>".to_vec(),
|
||||
result: Err(io::Error::new(io::ErrorKind::PermissionDenied, message)),
|
||||
};
|
||||
}
|
||||
|
||||
match params.get("code") {
|
||||
Some(code) if !code.is_empty() => HandledRequest::ResponseAndExit {
|
||||
status: StatusCode(200),
|
||||
headers: html_headers(),
|
||||
body: b"<h1>Sign-in complete</h1><p>You can return to your terminal.</p>"
|
||||
.to_vec(),
|
||||
result: Ok(code.clone()),
|
||||
},
|
||||
_ => HandledRequest::ResponseAndExit {
|
||||
status: StatusCode(400),
|
||||
headers: html_headers(),
|
||||
body: b"<h1>Missing authorization code</h1><p>Return to your terminal.</p>"
|
||||
.to_vec(),
|
||||
result: Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Missing authorization code. Sign-in could not be completed.",
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
_ => HandledRequest::Response(Response::from_string("Not Found").with_status_code(404)),
|
||||
}
|
||||
}
|
||||
|
||||
fn html_headers() -> Vec<Header> {
|
||||
match Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..]) {
|
||||
Ok(header) => vec![header],
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn respond_to_request<T>(req: Request, response: HandledRequest<T>) -> Option<io::Result<T>> {
|
||||
match response {
|
||||
HandledRequest::Response(response) => {
|
||||
let _ = tokio::task::spawn_blocking(move || req.respond(response)).await;
|
||||
None
|
||||
}
|
||||
HandledRequest::RedirectWithHeader(header) => {
|
||||
let redirect = Response::empty(302).with_header(header);
|
||||
let _ = tokio::task::spawn_blocking(move || req.respond(redirect)).await;
|
||||
None
|
||||
}
|
||||
HandledRequest::ResponseAndExit {
|
||||
status,
|
||||
headers,
|
||||
body,
|
||||
result,
|
||||
} => {
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
send_response_with_disconnect(req, status, headers, body)
|
||||
})
|
||||
.await;
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// tiny_http filters `Connection` headers out of `Response` objects, so using
|
||||
/// `req.respond` never informs the client (or the library) that a keep-alive
|
||||
/// socket should be closed. That leaves the per-connection worker parked in a
|
||||
@@ -426,10 +664,10 @@ async fn process_request(
|
||||
/// server-side connection persistence, but it does not.
|
||||
fn send_response_with_disconnect(
|
||||
req: Request,
|
||||
status: StatusCode,
|
||||
mut headers: Vec<Header>,
|
||||
body: Vec<u8>,
|
||||
) -> io::Result<()> {
|
||||
let status = StatusCode(200);
|
||||
let mut writer = req.into_writer();
|
||||
let reason = status.default_reason_phrase();
|
||||
write!(writer, "HTTP/1.1 {} {}\r\n", status.0, reason)?;
|
||||
@@ -888,13 +1126,14 @@ fn login_error_response(
|
||||
kind: io::ErrorKind,
|
||||
error_code: Option<&str>,
|
||||
error_description: Option<&str>,
|
||||
) -> HandledRequest {
|
||||
) -> HandledRequest<()> {
|
||||
let mut headers = Vec::new();
|
||||
if let Ok(header) = Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..]) {
|
||||
headers.push(header);
|
||||
}
|
||||
let body = render_login_error_page(message, error_code, error_description);
|
||||
HandledRequest::ResponseAndExit {
|
||||
status: StatusCode(200),
|
||||
headers,
|
||||
body,
|
||||
result: Err(io::Error::new(kind, message.to_string())),
|
||||
|
||||
@@ -42,12 +42,15 @@ pub(crate) fn start_command(
|
||||
})?;
|
||||
|
||||
let options = ApiProvisionOptions::default();
|
||||
let callback_port = options.callback_port;
|
||||
let session = start_api_provisioning(options)
|
||||
.map_err(|err| format!("Failed to start API provisioning: {err}"))?;
|
||||
let _ = session.open_browser();
|
||||
let start_message =
|
||||
continue_in_browser_message(session.auth_url(), &dotenv_path, callback_port);
|
||||
let browser_opened = session.open_browser();
|
||||
let start_message = continue_in_browser_message(
|
||||
session.auth_url(),
|
||||
session.callback_port(),
|
||||
&dotenv_path,
|
||||
browser_opened,
|
||||
);
|
||||
|
||||
let app_event_tx_for_task = app_event_tx;
|
||||
let dotenv_path_for_task = dotenv_path;
|
||||
@@ -79,8 +82,9 @@ fn existing_shell_api_key_message() -> PlainHistoryCell {
|
||||
|
||||
fn continue_in_browser_message(
|
||||
auth_url: &str,
|
||||
dotenv_path: &Path,
|
||||
callback_port: u16,
|
||||
dotenv_path: &Path,
|
||||
browser_opened: bool,
|
||||
) -> PlainHistoryCell {
|
||||
let mut lines = vec![
|
||||
vec![
|
||||
@@ -91,9 +95,17 @@ fn continue_in_browser_message(
|
||||
"".into(),
|
||||
];
|
||||
|
||||
lines.push(
|
||||
" If the link doesn't open automatically, open the following link to authenticate:".into(),
|
||||
);
|
||||
if browser_opened {
|
||||
lines.push(" Codex tried to open this link for you.".dark_gray().into());
|
||||
} else {
|
||||
lines.push(
|
||||
" Codex couldn't auto-open your browser, but the provisioning flow is still waiting."
|
||||
.dark_gray()
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
lines.push("".into());
|
||||
lines.push(" Open the following link to authenticate:".into());
|
||||
lines.push("".into());
|
||||
lines.push(Line::from(vec![
|
||||
" ".into(),
|
||||
@@ -111,7 +123,7 @@ fn continue_in_browser_message(
|
||||
lines.push("".into());
|
||||
lines.push(
|
||||
format!(
|
||||
" On a remote or headless machine, forward localhost:{callback_port} to this Codex host before opening the link."
|
||||
" On a remote or headless machine, forward localhost:{callback_port} back to this Codex host before opening the link."
|
||||
)
|
||||
.dark_gray()
|
||||
.into(),
|
||||
@@ -265,8 +277,9 @@ mod tests {
|
||||
fn continue_in_browser_message_snapshot() {
|
||||
let cell = continue_in_browser_message(
|
||||
"https://auth.openai.com/oauth/authorize?client_id=abc",
|
||||
Path::new("/tmp/workspace/.env.local"),
|
||||
/*callback_port*/ 5000,
|
||||
Path::new("/tmp/workspace/.env.local"),
|
||||
/*browser_opened*/ false,
|
||||
);
|
||||
|
||||
assert_snapshot!(render_cell(&cell));
|
||||
@@ -282,6 +295,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continue_in_browser_message_always_includes_the_auth_url() {
|
||||
let cell = continue_in_browser_message(
|
||||
"https://auth.example.com/oauth/authorize?state=abc",
|
||||
5000,
|
||||
Path::new("/tmp/workspace/.env.local"),
|
||||
/*browser_opened*/ false,
|
||||
);
|
||||
|
||||
assert!(
|
||||
render_cell(&cell).contains("https://auth.example.com/oauth/authorize?state=abc")
|
||||
);
|
||||
}
|
||||
|
||||
fn render_cell(cell: &PlainHistoryCell) -> String {
|
||||
cell.display_lines(120)
|
||||
.into_iter()
|
||||
|
||||
@@ -4,10 +4,12 @@ expression: render_cell(&cell)
|
||||
---
|
||||
• Finish API provisioning via your browser.
|
||||
|
||||
If the link doesn't open automatically, open the following link to authenticate:
|
||||
Codex couldn't auto-open your browser, but the provisioning flow is still waiting.
|
||||
|
||||
Open the following link to authenticate:
|
||||
|
||||
https://auth.openai.com/oauth/authorize?client_id=abc
|
||||
|
||||
Codex will save OPENAI_API_KEY to /tmp/workspace/.env.local and hot-apply it here when allowed.
|
||||
|
||||
On a remote or headless machine, forward localhost:5000 to this Codex host before opening the link.
|
||||
On a remote or headless machine, forward localhost:5000 back to this Codex host before opening the link.
|
||||
|
||||
Reference in New Issue
Block a user