fix: support remote host in CDP discovery (#854)
* fix: support remote host in CDP discovery (#851) `discover_cdp_url` now accepts a host parameter instead of hardcoding 127.0.0.1, allowing `connect "http://<remote-ip>:<port>"` to query the correct remote `/json/version` endpoint. The returned webSocketDebuggerUrl is rewritten to match the requested host and port, since Chrome always reports 127.0.0.1 regardless of the interface it was reached through. * style: apply cargo fmt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: unify discover_cdp_url and discover_cdp_url_with_request_timeout Merge the two discovery functions into discover_cdp_url(host, port) and discover_cdp_url_with_timeout(host, port, timeout), eliminating duplicated logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: merge discover_cdp_url into single function with optional timeout Replace discover_cdp_url + discover_cdp_url_with_timeout with a single discover_cdp_url(host, port, timeout) where timeout is Option<Duration>. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: replace Option<Duration> with separate discover_cdp_url_with_timeout Split back into two functions for cleaner call sites: - discover_cdp_url(host, port) for default timeout - discover_cdp_url_with_timeout(host, port, timeout) for custom timeout Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: bracket IPv6 addresses in CDP discovery HTTP URL Extract bracket_ipv6 helper and apply it in fetch_cdp_info to produce valid URLs like http://[::1]:9222/json/version instead of malformed http://::1:9222/json/version. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: hyunjinee <leehj0110@kakao.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
hyunjinee
parent
8163f6cdca
commit
0883813cd3
@@ -1285,15 +1285,17 @@ async fn resolve_cdp_url(input: &str) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.starts_with("http://") || input.starts_with("https://") {
|
if input.starts_with("http://") || input.starts_with("https://") {
|
||||||
// Parse out the port and discover
|
|
||||||
let parsed = url::Url::parse(input).map_err(|e| format!("Invalid CDP URL: {}", e))?;
|
let parsed = url::Url::parse(input).map_err(|e| format!("Invalid CDP URL: {}", e))?;
|
||||||
|
let host = parsed
|
||||||
|
.host_str()
|
||||||
|
.ok_or_else(|| format!("No host in CDP URL: {}", input))?;
|
||||||
let port = parsed.port().unwrap_or(9222);
|
let port = parsed.port().unwrap_or(9222);
|
||||||
return discover_cdp_url(port).await;
|
return discover_cdp_url(host, port).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try as numeric port
|
// Try as numeric port
|
||||||
if let Ok(port) = input.parse::<u16>() {
|
if let Ok(port) = input.parse::<u16>() {
|
||||||
return discover_cdp_url(port).await;
|
return discover_cdp_url("127.0.0.1", port).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(format!(
|
Err(format!(
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ pub async fn auto_connect_cdp() -> Result<String, String> {
|
|||||||
for dir in &user_data_dirs {
|
for dir in &user_data_dirs {
|
||||||
if let Some((port, ws_path)) = read_devtools_active_port(dir) {
|
if let Some((port, ws_path)) = read_devtools_active_port(dir) {
|
||||||
// Try HTTP endpoint first (pre-M144)
|
// Try HTTP endpoint first (pre-M144)
|
||||||
if let Ok(ws_url) = discover_cdp_url(port).await {
|
if let Ok(ws_url) = discover_cdp_url("127.0.0.1", port).await {
|
||||||
return Ok(ws_url);
|
return Ok(ws_url);
|
||||||
}
|
}
|
||||||
// M144+: direct WebSocket
|
// M144+: direct WebSocket
|
||||||
@@ -461,7 +461,7 @@ pub async fn auto_connect_cdp() -> Result<String, String> {
|
|||||||
|
|
||||||
// Fallback: probe common ports
|
// Fallback: probe common ports
|
||||||
for port in [9222u16, 9229] {
|
for port in [9222u16, 9229] {
|
||||||
if let Ok(ws_url) = discover_cdp_url(port).await {
|
if let Ok(ws_url) = discover_cdp_url("127.0.0.1", port).await {
|
||||||
return Ok(ws_url);
|
return Ok(ws_url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,70 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use super::types::BrowserVersionInfo;
|
use super::types::BrowserVersionInfo;
|
||||||
|
|
||||||
pub async fn discover_cdp_url(port: u16) -> Result<String, String> {
|
/// Default timeout for CDP discovery HTTP requests.
|
||||||
discover_cdp_url_with_request_timeout(port, Duration::from_secs(2)).await
|
const DEFAULT_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(2);
|
||||||
|
|
||||||
|
/// Discover the CDP WebSocket URL by querying `/json/version` at the given host and port.
|
||||||
|
/// The returned `webSocketDebuggerUrl` has its host/port rewritten to match
|
||||||
|
/// the requested target, since Chrome always reports `127.0.0.1` regardless
|
||||||
|
/// of the interface it was reached through.
|
||||||
|
pub async fn discover_cdp_url(host: &str, port: u16) -> Result<String, String> {
|
||||||
|
discover_cdp_url_with_timeout(host, port, DEFAULT_DISCOVERY_TIMEOUT).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn discover_cdp_url_with_request_timeout(
|
/// Like [`discover_cdp_url`] but with a custom request timeout.
|
||||||
|
pub async fn discover_cdp_url_with_timeout(
|
||||||
|
host: &str,
|
||||||
port: u16,
|
port: u16,
|
||||||
request_timeout: Duration,
|
timeout: Duration,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let url = format!("http://127.0.0.1:{}/json/version", port);
|
let info = fetch_cdp_info(host, port, timeout).await?;
|
||||||
|
let ws_url = info.web_socket_debugger_url.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"No webSocketDebuggerUrl in /json/version at {}:{}",
|
||||||
|
host, port
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(rewrite_ws_host(&ws_url, host, port))
|
||||||
|
}
|
||||||
|
|
||||||
let body = tokio::time::timeout(request_timeout, async { reqwest_get_string(&url).await })
|
/// Bracket an IPv6 address for use in URLs. No-op for IPv4 or already-bracketed addresses.
|
||||||
|
fn bracket_ipv6(host: &str) -> String {
|
||||||
|
if host.contains(':') && !host.starts_with('[') {
|
||||||
|
format!("[{}]", host)
|
||||||
|
} else {
|
||||||
|
host.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch `/json/version` from the given host:port and parse the response.
|
||||||
|
async fn fetch_cdp_info(
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<BrowserVersionInfo, String> {
|
||||||
|
let url = format!("http://{}:{}/json/version", bracket_ipv6(host), port);
|
||||||
|
|
||||||
|
let body = tokio::time::timeout(timeout, reqwest_get_string(&url))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| format!("Timeout connecting to CDP on port {}", port))?
|
.map_err(|_| format!("Timeout connecting to CDP at {}:{}", host, port))?
|
||||||
.map_err(|e| format!("Failed to connect to CDP on port {}: {}", port, e))?;
|
.map_err(|e| format!("Failed to connect to CDP at {}:{}: {}", host, port, e))?;
|
||||||
|
|
||||||
let info: BrowserVersionInfo = serde_json::from_str(&body)
|
serde_json::from_str(&body).map_err(|e| format!("Invalid /json/version response: {}", e))
|
||||||
.map_err(|e| format!("Invalid /json/version response: {}", e))?;
|
}
|
||||||
|
|
||||||
info.web_socket_debugger_url
|
/// Rewrite the host and port in a WebSocket URL to match the target we
|
||||||
.ok_or_else(|| format!("No webSocketDebuggerUrl in /json/version on port {}", port))
|
/// actually connected to. Chrome's `/json/version` always returns
|
||||||
|
/// `ws://127.0.0.1:<local-port>/...` which is unreachable when the
|
||||||
|
/// browser is on a remote machine or behind a port-forward.
|
||||||
|
fn rewrite_ws_host(ws_url: &str, host: &str, port: u16) -> String {
|
||||||
|
if let Ok(mut parsed) = url::Url::parse(ws_url) {
|
||||||
|
let _ = parsed.set_host(Some(&bracket_ipv6(host)));
|
||||||
|
let _ = parsed.set_port(Some(port));
|
||||||
|
parsed.to_string()
|
||||||
|
} else {
|
||||||
|
ws_url.to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reqwest_get_string(url: &str) -> Result<String, String> {
|
async fn reqwest_get_string(url: &str) -> Result<String, String> {
|
||||||
@@ -57,8 +101,8 @@ mod tests {
|
|||||||
let (port, server) =
|
let (port, server) =
|
||||||
spawn_json_server(r#"{"webSocketDebuggerUrl":"ws://127.0.0.1:1234/"}"#).await;
|
spawn_json_server(r#"{"webSocketDebuggerUrl":"ws://127.0.0.1:1234/"}"#).await;
|
||||||
|
|
||||||
let ws_url = discover_cdp_url(port).await.unwrap();
|
let ws_url = discover_cdp_url("127.0.0.1", port).await.unwrap();
|
||||||
assert_eq!(ws_url, "ws://127.0.0.1:1234/");
|
assert_eq!(ws_url, format!("ws://127.0.0.1:{}/", port));
|
||||||
server.await.unwrap();
|
server.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,8 +110,22 @@ mod tests {
|
|||||||
async fn invalid_json_returns_parse_error() {
|
async fn invalid_json_returns_parse_error() {
|
||||||
let (port, server) = spawn_json_server("not-json").await;
|
let (port, server) = spawn_json_server("not-json").await;
|
||||||
|
|
||||||
let err = discover_cdp_url(port).await.unwrap_err();
|
let err = discover_cdp_url("127.0.0.1", port).await.unwrap_err();
|
||||||
assert!(err.contains("Invalid /json/version response"));
|
assert!(err.contains("Invalid /json/version response"));
|
||||||
server.await.unwrap();
|
server.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewrite_ws_host_replaces_host_and_port() {
|
||||||
|
let original = "ws://127.0.0.1:9222/devtools/browser/abc";
|
||||||
|
let rewritten = rewrite_ws_host(original, "10.211.55.12", 9223);
|
||||||
|
assert_eq!(rewritten, "ws://10.211.55.12:9223/devtools/browser/abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rewrite_ws_host_handles_ipv6() {
|
||||||
|
let original = "ws://127.0.0.1:9222/devtools/browser/abc";
|
||||||
|
let rewritten = rewrite_ws_host(original, "::1", 9222);
|
||||||
|
assert_eq!(rewritten, "ws://[::1]:9222/devtools/browser/abc");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::process::{Child, Command, Stdio};
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use super::discovery::discover_cdp_url_with_request_timeout;
|
use super::discovery::discover_cdp_url_with_timeout;
|
||||||
|
|
||||||
const LIGHTPANDA_STARTUP_TIMEOUT: Duration = Duration::from_secs(10);
|
const LIGHTPANDA_STARTUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
const LIGHTPANDA_POLL_INTERVAL: Duration = Duration::from_millis(100);
|
const LIGHTPANDA_POLL_INTERVAL: Duration = Duration::from_millis(100);
|
||||||
@@ -257,7 +257,7 @@ async fn wait_for_lightpanda_ready(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
match discover_cdp_url_with_request_timeout(port, LIGHTPANDA_DISCOVERY_TIMEOUT).await {
|
match discover_cdp_url_with_timeout("127.0.0.1", port, LIGHTPANDA_DISCOVERY_TIMEOUT).await {
|
||||||
Ok(ws_url) => return Ok(ws_url),
|
Ok(ws_url) => return Ok(ws_url),
|
||||||
Err(err) => last_probe_error = Some(err),
|
Err(err) => last_probe_error = Some(err),
|
||||||
}
|
}
|
||||||
@@ -365,7 +365,7 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(ws_url, "ws://127.0.0.1:9222/");
|
assert_eq!(ws_url, format!("ws://127.0.0.1:{}/", port));
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user