1+ import { parseBrowserHttpUrl , redactCdpUrl } from "openclaw/plugin-sdk/browser-config" ;
12import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime" ;
23import WebSocket from "ws" ;
34import { isLoopbackHost } from "../gateway/net.js" ;
67 type SsrFPolicy ,
78 resolvePinnedHostnameWithPolicy ,
89} from "../infra/net/ssrf.js" ;
9- import { redactSensitiveText } from "../logging/redact.js" ;
1010import {
1111 getDirectAgentForCdp ,
1212 withManagedProxyForCdpUrl ,
@@ -18,98 +18,7 @@ import { resolveBrowserRateLimitMessage } from "./rate-limit-message.js";
1818import { withAllowedHostname } from "./ssrf-policy-helpers.js" ;
1919
2020export { isLoopbackHost } ;
21-
22- /**
23- * Detects whether a raw URL string contains an explicitly written port.
24- *
25- * WHATWG `URL` normalizes default ports (e.g. `:80` for http, `:443` for
26- * https) to an empty `.port` string, making it impossible to distinguish
27- * "user wrote :80" from "user omitted the port". This helper inspects the
28- * raw string to preserve that intent.
29- *
30- * Handles IPv6 bracket notation and userinfo (user:pass@host) correctly.
31- */
32- function hasRawExplicitPort ( raw : string ) : boolean {
33- // Strip scheme (e.g. "http://") and take only the authority portion
34- // (everything before the first /, ?, or #).
35- const authority = raw . replace ( / ^ [ a - z ] [ a - z 0 - 9 + . - ] * : \/ \/ / i, "" ) . split ( / [ / ? # ] / , 1 ) [ 0 ] ?? "" ;
36-
37- // Strip userinfo (user:pass@); the colon there is not a port separator.
38- const hostPort = authority . includes ( "@" )
39- ? authority . slice ( authority . lastIndexOf ( "@" ) + 1 )
40- : authority ;
41-
42- // IPv6: [::1]:9222 has a port after the closing bracket.
43- if ( hostPort . startsWith ( "[" ) ) {
44- return / ^ \[ [ ^ \] ] + \] : \d + $ / . test ( hostPort ) ;
45- }
46-
47- // IPv4 / hostname: host:port
48- return / : \d + $ / . test ( hostPort ) ;
49- }
50-
51- export function parseBrowserHttpUrl ( raw : string , label : string ) {
52- const trimmed = raw . trim ( ) ;
53- const parsed = new URL ( trimmed ) ;
54- const allowed = [ "http:" , "https:" , "ws:" , "wss:" ] ;
55- if ( ! allowed . includes ( parsed . protocol ) ) {
56- throw new Error ( `${ label } must be http(s) or ws(s), got: ${ parsed . protocol . replace ( ":" , "" ) } ` ) ;
57- }
58-
59- const isSecure = parsed . protocol === "https:" || parsed . protocol === "wss:" ;
60- const port =
61- parsed . port && Number . parseInt ( parsed . port , 10 ) > 0
62- ? Number . parseInt ( parsed . port , 10 )
63- : isSecure
64- ? 443
65- : 80 ;
66-
67- // WHATWG URL rejects invalid ports (non-numeric, negative, >65535), and
68- // the ternary above falls back to 80/443 for empty or zero parsed.port,
69- // so this defensive guard is unreachable at runtime. Kept as a
70- // belt-and-braces check against parser drift.
71- /* c8 ignore next 3 */
72- if ( Number . isNaN ( port ) || port <= 0 || port > 65535 ) {
73- throw new Error ( `${ label } has invalid port: ${ parsed . port } ` ) ;
74- }
75-
76- const normalized = parsed . toString ( ) . replace ( / \/ $ / , "" ) ;
77- const hasExplicitPort = hasRawExplicitPort ( trimmed ) ;
78-
79- // When the user explicitly wrote a default port (e.g. :80 for http),
80- // WHATWG normalization drops it from the URL string. Rebuild a
81- // port-preserving normalized form so callers don't need raw-string hacks.
82- // Note: the URL .port setter silently discards protocol-default ports,
83- // so we must inject the port via string surgery on the normalized form.
84- let normalizedWithPort : string ;
85- if ( hasExplicitPort && ! parsed . port ) {
86- const proto = parsed . protocol + "//" ;
87- const rest = normalized . slice ( proto . length ) ;
88- // Skip userinfo (user:pass@) if present
89- const atIdx = rest . indexOf ( "@" ) ;
90- const hostStart = atIdx >= 0 ? atIdx + 1 : 0 ;
91- const hostPart = rest . slice ( hostStart ) ;
92- // Find the end of the host: IPv6 brackets, a path slash, or a port colon.
93- const hostLen = hostPart . startsWith ( "[" )
94- ? hostPart . indexOf ( "]" ) + 1
95- : ( ( ) => {
96- const idx = hostPart . search ( / [: / ] / ) ;
97- return idx < 0 ? hostPart . length : idx ;
98- } ) ( ) ;
99- const insertAt = hostStart + hostLen ;
100- normalizedWithPort = proto + rest . slice ( 0 , insertAt ) + ":" + port + rest . slice ( insertAt ) ;
101- } else {
102- normalizedWithPort = normalized ;
103- }
104-
105- return {
106- parsed,
107- port,
108- hasExplicitPort,
109- normalized,
110- normalizedWithPort,
111- } ;
112- }
21+ export { parseBrowserHttpUrl , redactCdpUrl } ;
11322
11423/**
11524 * Returns true when the URL uses a WebSocket protocol (ws: or wss:).
@@ -179,24 +88,6 @@ export async function assertCdpEndpointAllowed(
17988 }
18089}
18190
182- export function redactCdpUrl ( cdpUrl : string | null | undefined ) : string | null | undefined {
183- if ( typeof cdpUrl !== "string" ) {
184- return cdpUrl ;
185- }
186- const trimmed = cdpUrl . trim ( ) ;
187- if ( ! trimmed ) {
188- return trimmed ;
189- }
190- try {
191- const parsed = new URL ( trimmed ) ;
192- parsed . username = "" ;
193- parsed . password = "" ;
194- return redactSensitiveText ( parsed . toString ( ) . replace ( / \/ $ / , "" ) ) ;
195- } catch {
196- return redactSensitiveText ( trimmed ) ;
197- }
198- }
199-
20091type CdpResponse = {
20192 id : number ;
20293 result ?: unknown ;
0 commit comments