mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Restore app-server websocket listener with auth guard (#22404)
## Why PR #21843 removed the TCP websocket app-server listener, but that also removed functionality that still needs to exist. Restoring it as-is would reopen the old remote exposure problem, so this keeps the restored listener while making remote and non-loopback usage require explicit auth. ## What Changed - Mostly reverts #21843 and reapplies the small merge-conflict resolutions needed on top of current main. - Restores ws://IP:PORT parsing, the app-server TCP websocket acceptor, websocket auth CLI flags, and the associated tests. - The only intentional behavior change from the restored code is that non-loopback websocket listeners now fail startup unless --ws-auth capability-token or --ws-auth signed-bearer-token is configured. Loopback listeners remain available for local and SSH-forwarding workflows. ## Reviewer Focus Please focus review on the small auth-enforcement delta layered on top of the revert: - codex-rs/app-server-transport/src/transport/websocket.rs: start_websocket_acceptor now rejects unauthenticated non-loopback websocket binds before accepting connections. - codex-rs/app-server-transport/src/transport/auth.rs: helper logic classifies unauthenticated non-loopback listeners. - codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs: tests cover unauthenticated ws://0.0.0.0 startup rejection and authenticated non-loopback capability-token startup. Everything else is intended to be revert/merge-conflict restoration rather than new product behavior. ## Verification - Manually verified that TUI remoting is restored and that auth is enforced for non-localhost urls.
This commit is contained in:
committed by
GitHub
Unverified
parent
d1430fd61e
commit
51bfb5f3b1
@@ -420,7 +420,7 @@ struct AppServerCommand {
|
||||
subcommand: Option<AppServerSubcommand>,
|
||||
|
||||
/// Transport endpoint URL. Supported values: `stdio://` (default),
|
||||
/// `unix://`, `unix://PATH`, `off`.
|
||||
/// `unix://`, `unix://PATH`, `ws://IP:PORT`, `off`.
|
||||
#[arg(
|
||||
long = "listen",
|
||||
value_name = "URL",
|
||||
@@ -449,6 +449,9 @@ struct AppServerCommand {
|
||||
/// See https://developers.openai.com/codex/config-advanced/#metrics for more details.
|
||||
#[arg(long = "analytics-default-enabled")]
|
||||
analytics_default_enabled: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
auth: codex_app_server::AppServerWebsocketAuthArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -894,6 +897,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
listen,
|
||||
remote_control,
|
||||
analytics_default_enabled,
|
||||
auth,
|
||||
} = app_server_cli;
|
||||
reject_remote_mode_for_app_server_subcommand(
|
||||
root_remote.as_deref(),
|
||||
@@ -903,6 +907,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
match subcommand {
|
||||
None => {
|
||||
let transport = listen;
|
||||
let auth = auth.try_into_settings()?;
|
||||
let runtime_options = codex_app_server::AppServerRuntimeOptions {
|
||||
remote_control_enabled: remote_control,
|
||||
..Default::default()
|
||||
@@ -914,6 +919,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
analytics_default_enabled,
|
||||
transport,
|
||||
codex_protocol::protocol::SessionSource::VSCode,
|
||||
auth,
|
||||
runtime_options,
|
||||
)
|
||||
.await?;
|
||||
@@ -2532,14 +2538,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_listen_websocket_url_fails_to_parse() {
|
||||
let parse_result = MultitoolCli::try_parse_from([
|
||||
"codex",
|
||||
"app-server",
|
||||
"--listen",
|
||||
"ws://127.0.0.1:4500",
|
||||
]);
|
||||
assert!(parse_result.is_err());
|
||||
fn app_server_listen_websocket_url_parses() {
|
||||
let app_server = app_server_from_args(
|
||||
["codex", "app-server", "--listen", "ws://127.0.0.1:4500"].as_ref(),
|
||||
);
|
||||
assert_eq!(
|
||||
app_server.listen,
|
||||
codex_app_server::AppServerTransport::WebSocket {
|
||||
bind_address: "127.0.0.1:4500".parse().expect("valid socket address"),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2708,6 +2716,61 @@ mod tests {
|
||||
assert!(err.to_string().contains("app-server daemon version"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_capability_token_flags_parse() {
|
||||
let app_server = app_server_from_args(
|
||||
[
|
||||
"codex",
|
||||
"app-server",
|
||||
"--ws-auth",
|
||||
"capability-token",
|
||||
"--ws-token-file",
|
||||
"/tmp/codex-token",
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
assert_eq!(
|
||||
app_server.auth.ws_auth,
|
||||
Some(codex_app_server::WebsocketAuthCliMode::CapabilityToken)
|
||||
);
|
||||
assert_eq!(
|
||||
app_server.auth.ws_token_file,
|
||||
Some(PathBuf::from("/tmp/codex-token"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_signed_bearer_flags_parse() {
|
||||
let app_server = app_server_from_args(
|
||||
[
|
||||
"codex",
|
||||
"app-server",
|
||||
"--ws-auth",
|
||||
"signed-bearer-token",
|
||||
"--ws-shared-secret-file",
|
||||
"/tmp/codex-secret",
|
||||
"--ws-issuer",
|
||||
"issuer",
|
||||
"--ws-audience",
|
||||
"audience",
|
||||
"--ws-max-clock-skew-seconds",
|
||||
"9",
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
assert_eq!(
|
||||
app_server.auth.ws_auth,
|
||||
Some(codex_app_server::WebsocketAuthCliMode::SignedBearerToken)
|
||||
);
|
||||
assert_eq!(
|
||||
app_server.auth.ws_shared_secret_file,
|
||||
Some(PathBuf::from("/tmp/codex-secret"))
|
||||
);
|
||||
assert_eq!(app_server.auth.ws_issuer.as_deref(), Some("issuer"));
|
||||
assert_eq!(app_server.auth.ws_audience.as_deref(), Some("audience"));
|
||||
assert_eq!(app_server.auth.ws_max_clock_skew_seconds, Some(9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_rejects_removed_insecure_non_loopback_flag() {
|
||||
let parse_result = MultitoolCli::try_parse_from([
|
||||
|
||||
Reference in New Issue
Block a user