prismcast 1.10.0 → 1.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +6 -2
- package/dist/app.js.map +1 -1
- package/dist/browser/index.js +2 -2
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/tuning/hbo.js +31 -123
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.js +70 -7
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/video.js +1 -1
- package/dist/browser/video.js.map +1 -1
- package/dist/config/index.d.ts +6 -2
- package/dist/config/index.js +35 -21
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +26 -0
- package/dist/config/paths.js +51 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/presets.js +2 -2
- package/dist/config/presets.js.map +1 -1
- package/dist/config/profiles.js +6 -6
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/userChannels.js +1 -1
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.js +1 -1
- package/dist/hdhr/discover.js +3 -4
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/identity.d.ts +2 -0
- package/dist/identity.js +19 -0
- package/dist/identity.js.map +1 -0
- package/dist/routes/cdp.d.ts +171 -0
- package/dist/routes/cdp.js +702 -0
- package/dist/routes/cdp.js.map +1 -0
- package/dist/routes/config/services.js +1 -1
- package/dist/routes/config/services.js.map +1 -1
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/root/index.js +13 -19
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/status.js +11 -3
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/service/commands.context.js +2 -2
- package/dist/service/commands.context.js.map +1 -1
- package/dist/service/commands.d.ts +1 -1
- package/dist/service/commands.js +1 -10
- package/dist/service/commands.js.map +1 -1
- package/dist/service/generators.context.js +2 -1
- package/dist/service/generators.context.js.map +1 -1
- package/dist/service/generators.js +4 -9
- package/dist/service/generators.js.map +1 -1
- package/dist/streaming/clients.js +3 -11
- package/dist/streaming/clients.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.js +1 -1
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.js +1 -1
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/mpegts.js +1 -1
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/setup.js +6 -6
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.js +2 -4
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/utils/debugFilter.js +1 -0
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/fileLogger.js +3 -3
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.js +3 -3
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +53 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/markup.d.ts +12 -0
- package/dist/utils/markup.js +54 -0
- package/dist/utils/markup.js.map +1 -0
- package/dist/utils/morganStream.js +1 -1
- package/dist/utils/morganStream.js.map +1 -1
- package/dist/utils/network.d.ts +8 -0
- package/dist/utils/network.js +22 -0
- package/dist/utils/network.js.map +1 -0
- package/dist/utils/platform.d.ts +0 -27
- package/dist/utils/platform.js +0 -57
- package/dist/utils/platform.js.map +1 -1
- package/package.json +8 -6
- package/dist/utils/html.d.ts +0 -6
- package/dist/utils/html.js +0 -22
- package/dist/utils/html.js.map +0 -1
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { LOG, formatError, getPackageVersion, isCategoryEnabled } from "../utils/index.js";
|
|
2
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
3
|
+
import { getBrowserInstance } from "../browser/index.js";
|
|
4
|
+
/* Module-level state. The WebSocketServer is created in `noServer` mode so the HTTP server's upgrade event remains the single dispatch point; we route
|
|
5
|
+
* /cdp/devtools/* upgrades into the WSS and leave any other upgrade requests untouched so future features can claim them.
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle ownership lives one layer down: each CdpProxySession subscribes to its own browser's `disconnected` event in start() and unsubscribes in cleanup().
|
|
8
|
+
* That replaces the older module-level "track the current browser, broadcast over a Set of active sessions" pattern - which encoded a per-instance concern
|
|
9
|
+
* (this WS attached to that browser) as shared state and had a latent race when listener registration outran rotation. Discovery endpoints likewise hold no
|
|
10
|
+
* disconnect subscription: the discoverySessions WeakMap is keyed by Browser identity, so rotation invalidates structurally and the old entry GCs with its
|
|
11
|
+
* browser.
|
|
12
|
+
*/
|
|
13
|
+
let wss = null;
|
|
14
|
+
/**
|
|
15
|
+
* Builds the public-facing webSocketDebuggerUrl for a target. Chrome's wire convention puts the host:port of the debugging port at the front and a path
|
|
16
|
+
* identifying the target underneath /devtools. We mirror that shape so external clients recognize the URL as standard CDP.
|
|
17
|
+
* @param req - The current Express request, used to derive host + protocol so the URL reflects how the client actually reached us.
|
|
18
|
+
* @param suffix - The target-specific path under /devtools (e.g., "browser/<id>", "page/<id>").
|
|
19
|
+
* @returns The ws:// URL.
|
|
20
|
+
*/
|
|
21
|
+
function makeWsUrl(req, suffix) {
|
|
22
|
+
// Express exposes the Host header at req.headers.host. We fall back to the configured server.host:port only if the request omits it, which should not happen
|
|
23
|
+
// for HTTP/1.1 clients but is defensive.
|
|
24
|
+
const host = req.headers.host ?? (req.hostname + ":" + String(req.socket.localPort ?? ""));
|
|
25
|
+
// The proxy speaks ws (not wss) because PrismCast's HTTP listener is plain HTTP. If a reverse proxy fronts PrismCast with TLS, the operator is responsible for
|
|
26
|
+
// adjusting tooling that needs wss.
|
|
27
|
+
return "ws://" + host + "/cdp/devtools/" + suffix;
|
|
28
|
+
}
|
|
29
|
+
/* Per-browser CDPSession cache used by the GET /cdp/json endpoints to query Chrome for the live target list. Keyed by Browser identity so rotation auto-invalidates
|
|
30
|
+
* - the entry for the previous browser is unreferenced (along with the browser itself) and the next discovery call against the new browser is a structural cache
|
|
31
|
+
* miss. Each entry is stored as a pending Promise rather than a resolved value so concurrent callers per browser serialize on the same in-flight creation; storing
|
|
32
|
+
* the resolved value would let two simultaneous requests each create their own session (the slower one overwriting the faster one's reference and leaking the
|
|
33
|
+
* orphan).
|
|
34
|
+
*/
|
|
35
|
+
const discoverySessions = new WeakMap();
|
|
36
|
+
/**
|
|
37
|
+
* Returns a working browser-level CDPSession for discovery, lazily creating one per Browser identity and re-creating it after a cached session detaches (e.g.,
|
|
38
|
+
* a transient CDP error). Concurrent callers against the same browser share the same in-flight Promise; callers against a different browser get their own.
|
|
39
|
+
* @param browser - The active Puppeteer Browser.
|
|
40
|
+
* @returns A CDPSession attached to the browser target.
|
|
41
|
+
*/
|
|
42
|
+
async function getDiscoverySession(browser) {
|
|
43
|
+
const existing = discoverySessions.get(browser);
|
|
44
|
+
if (existing) {
|
|
45
|
+
const cached = await existing;
|
|
46
|
+
if (!cached.detached) {
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const session = browser.target().createCDPSession();
|
|
51
|
+
discoverySessions.set(browser, session);
|
|
52
|
+
return session;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sends a CDP command on a given session. Wrapper around CDPSession.send that erases the method generic so we can forward arbitrary methods received over the
|
|
56
|
+
* wire.
|
|
57
|
+
* @param session - The CDP session.
|
|
58
|
+
* @param method - The CDP method name.
|
|
59
|
+
* @param params - The CDP parameters.
|
|
60
|
+
* @returns The CDP result.
|
|
61
|
+
*/
|
|
62
|
+
async function sendCdp(session, method, params) {
|
|
63
|
+
return session.send(method, params);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Looks up the underlying Connection for a CDPSession. Returns null when the session is detached.
|
|
67
|
+
* @param session - The CDP session.
|
|
68
|
+
* @returns The Connection or null.
|
|
69
|
+
*/
|
|
70
|
+
function connectionFor(session) {
|
|
71
|
+
return session.connection() ?? null;
|
|
72
|
+
}
|
|
73
|
+
/* End of wire helpers. */
|
|
74
|
+
/**
|
|
75
|
+
* Per-WebSocket CDP proxy state. One instance per attached client. Owns the browser-level CDPSession, the per-target sub-session map, and the lifecycle
|
|
76
|
+
* subscriptions to the Browser. Cleans up on WS close, target destruction, or browser disconnect. Exported for unit testing - production callers receive
|
|
77
|
+
* instances via the upgrade handler and do not construct them directly.
|
|
78
|
+
*/
|
|
79
|
+
export class CdpProxySession {
|
|
80
|
+
/* The active CDPSession attached to the browser target. Handles browser-level commands and serves as the routing channel for events the proxy emits when no
|
|
81
|
+
* sessionId is present.
|
|
82
|
+
*/
|
|
83
|
+
browserSession = null;
|
|
84
|
+
/* The underlying Puppeteer Connection that owns every CDPSession we create. Captured for the Connection.createSession path which lets us mint a Puppeteer
|
|
85
|
+
* CDPSession from a CDP TargetInfo (the recommended public API for attaching to an arbitrary target by id).
|
|
86
|
+
*/
|
|
87
|
+
connection = null;
|
|
88
|
+
/* Maps synthetic sessionId values (opaque to the client; we hand them out via Target.attachToTarget responses and Target.attachedToTarget events) to the
|
|
89
|
+
* underlying Puppeteer CDPSession for that target. Subsequent client messages with a sessionId route through this map.
|
|
90
|
+
*/
|
|
91
|
+
sessions = new Map();
|
|
92
|
+
/* Reverse-map sessionId -> CDP targetId, so the targetdestroyed lifecycle hook can emit Target.detachedFromTarget with the right targetId for the synthetic
|
|
93
|
+
* sub-session.
|
|
94
|
+
*/
|
|
95
|
+
sessionToTargetId = new Map();
|
|
96
|
+
/* Captured original emit functions so we can restore them on detach. Each entry is the EventEmitter prototype-bound original emit for the corresponding
|
|
97
|
+
* CDPSession.
|
|
98
|
+
*/
|
|
99
|
+
originalEmits = new Map();
|
|
100
|
+
/* Counter for synthetic sessionIds. Incremented per attach. The string form looks like an opaque token; we prepend a deterministic prefix so cross-referencing
|
|
101
|
+
* proxy logs against external client logs is easy when debugging.
|
|
102
|
+
*/
|
|
103
|
+
nextSessionSerial = 1;
|
|
104
|
+
/* Whether the client has called Target.setDiscoverTargets({ discover: true }). When true, we emit Target.targetCreated / Target.targetDestroyed events as
|
|
105
|
+
* browser targets come and go.
|
|
106
|
+
*/
|
|
107
|
+
discoverTargets = false;
|
|
108
|
+
/* Whether the client has called Target.setAutoAttach({ autoAttach: true, flatten: true }). When true, every existing and future target is auto-attached, and
|
|
109
|
+
* Target.attachedToTarget events are emitted with the synthetic sessionId.
|
|
110
|
+
*/
|
|
111
|
+
autoAttach = false;
|
|
112
|
+
/* Whether start() completed successfully and the "CDP client attached." log fired. cleanup() consults this so the paired "detached" log only emits when an
|
|
113
|
+
* "attached" log preceded it - keeping the audit trail honest when setup fails before the proxy is ever usable.
|
|
114
|
+
*/
|
|
115
|
+
attached = false;
|
|
116
|
+
/* The bound listener attached to `this.browser`'s "disconnected" event in start(). Held as a field so cleanup() can unsubscribe by reference - using a fresh
|
|
117
|
+
* arrow function at unsubscribe time would silently fail to remove the original listener because EventEmitter compares listener identity. Cleared back to null
|
|
118
|
+
* on unsubscribe so the field's presence is a faithful indicator of "are we still listening."
|
|
119
|
+
*/
|
|
120
|
+
onBrowserDisconnect = null;
|
|
121
|
+
ws;
|
|
122
|
+
browser;
|
|
123
|
+
constructor(ws, browser) {
|
|
124
|
+
this.ws = ws;
|
|
125
|
+
this.browser = browser;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Initializes the per-connection CDP plumbing: registers the WebSocket lifecycle handlers, subscribes to the browser's disconnect event, opens a browser-level
|
|
129
|
+
* CDPSession, captures the underlying Connection, and subscribes to CDP target events. Any failure during async setup is caught by the WS close handler
|
|
130
|
+
* (registered first), which fires when `closeWith()` closes the socket and routes cleanup through the same path the happy-path teardown takes. This invariant
|
|
131
|
+
* - lifecycle handlers wired before any async operation that can fail - is what guarantees the session is torn down (including its disconnect listener)
|
|
132
|
+
* regardless of where setup aborts. Handler bodies are null-safe so an early message arriving before browserSession is set produces a clean error frame rather
|
|
133
|
+
* than a crash.
|
|
134
|
+
*/
|
|
135
|
+
async start() {
|
|
136
|
+
/* Wire the WebSocket lifecycle handlers BEFORE any async setup. closeWith() (used by the failure paths below) closes the socket, which fires the close event,
|
|
137
|
+
* which runs cleanup() - the handler-first ordering is the invariant that guarantees cleanup runs from any failure path, including the browser-disconnect
|
|
138
|
+
* subscription registered immediately after this.
|
|
139
|
+
*/
|
|
140
|
+
this.ws.on("message", (data) => { void this.handleClientMessage(data); });
|
|
141
|
+
this.ws.on("close", () => { void this.cleanup(); });
|
|
142
|
+
this.ws.on("error", (err) => {
|
|
143
|
+
LOG.warn("CDP proxy WebSocket error: %s.", err.message);
|
|
144
|
+
void this.cleanup();
|
|
145
|
+
});
|
|
146
|
+
/* Subscribe to the browser's "disconnected" event so this session tears down cleanly when the browser PrismCast launched goes away (crash, rotation, normal
|
|
147
|
+
* shutdown). The handler closes our WS with 1001 ("going away"); the WS close handler above then runs cleanup(), which unsubscribes us. Doing this per-session
|
|
148
|
+
* - rather than a module-level broadcast over a Set - means the lifecycle concern lives on the resource it actually depends on, and a stale subscription from
|
|
149
|
+
* a previous browser can never fire against a session attached to a new one.
|
|
150
|
+
*/
|
|
151
|
+
this.onBrowserDisconnect = () => { this.closeWith(1001, "browser disconnected"); };
|
|
152
|
+
this.browser.on("disconnected", this.onBrowserDisconnect);
|
|
153
|
+
try {
|
|
154
|
+
this.browserSession = await this.browser.target().createCDPSession();
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
LOG.warn("CDP proxy could not open a browser session: %s.", formatError(error));
|
|
158
|
+
this.closeWith(1011, "browser session creation failed");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const connection = connectionFor(this.browserSession);
|
|
162
|
+
if (!connection) {
|
|
163
|
+
this.closeWith(1011, "no CDP connection");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.connection = connection;
|
|
167
|
+
/* Subscribe to CDP Target.* lifecycle events on the browser session so we can drive internal session cleanup and auto-attach. This is CDP-native: the
|
|
168
|
+
* targetId on these events matches what Chrome reports through Target.getTargets and is the same id clients send back via Target.attachToTarget. Using
|
|
169
|
+
* Puppeteer's higher-level browser.on("targetcreated") instead would force us to map Puppeteer's opaque Target objects to CDP targetIds, which Puppeteer
|
|
170
|
+
* doesn't expose. The listeners are registered before we enable discovery so no event slips past during the round-trip.
|
|
171
|
+
*/
|
|
172
|
+
this.browserSession.on("Target.targetCreated", (event) => { void this.handleCdpTargetCreated(event); });
|
|
173
|
+
this.browserSession.on("Target.targetDestroyed", (event) => { void this.handleCdpTargetDestroyed(event); });
|
|
174
|
+
// Capture every event the browser session emits and forward it to the client with no sessionId (it's a browser-level event).
|
|
175
|
+
this.attachEventListener(this.browserSession, null);
|
|
176
|
+
/* Enable target discovery on our browser session so Target.targetCreated / targetDestroyed events fire. This is required for our internal session cleanup
|
|
177
|
+
* regardless of whether the client has called Target.setDiscoverTargets - the wildcard forwarder gates client-facing emission of Target.* events on
|
|
178
|
+
* `this.discoverTargets`, so the internal subscription doesn't leak events to clients that didn't ask for them.
|
|
179
|
+
*/
|
|
180
|
+
try {
|
|
181
|
+
await sendCdp(this.browserSession, "Target.setDiscoverTargets", { discover: true });
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
LOG.warn("CDP proxy could not enable target discovery: %s.", formatError(error));
|
|
185
|
+
}
|
|
186
|
+
this.attached = true;
|
|
187
|
+
LOG.info("CDP client attached.");
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Closes the WS with a code/reason and triggers cleanup. Safe to call when the WS is already closed.
|
|
191
|
+
* @param code - WebSocket close code (1000 normal, 1001 going-away, 1011 internal-error).
|
|
192
|
+
* @param reason - Human-readable reason.
|
|
193
|
+
*/
|
|
194
|
+
closeWith(code, reason) {
|
|
195
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
196
|
+
try {
|
|
197
|
+
this.ws.close(code, reason);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// The ws library may throw if the socket is in an intermediate state. The 'close' event still fires from the underlying socket and runs cleanup() via the
|
|
201
|
+
// handler registered synchronously at the top of start() - that handler-first ordering is the invariant that guarantees cleanup runs from any failure path.
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Handles a CDP message received over the WebSocket. Parses the JSON, dispatches to the browser-level or target-level handler, and forwards the response.
|
|
207
|
+
* Malformed JSON is silently dropped (the wire protocol has no error frame for that case at the client's level).
|
|
208
|
+
* @param data - The raw WebSocket message data.
|
|
209
|
+
*/
|
|
210
|
+
async handleClientMessage(data) {
|
|
211
|
+
let raw;
|
|
212
|
+
if (typeof data === "string") {
|
|
213
|
+
raw = data;
|
|
214
|
+
}
|
|
215
|
+
else if (Buffer.isBuffer(data)) {
|
|
216
|
+
raw = data.toString("utf8");
|
|
217
|
+
}
|
|
218
|
+
else if (Array.isArray(data)) {
|
|
219
|
+
raw = Buffer.concat(data).toString("utf8");
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
let msg;
|
|
225
|
+
try {
|
|
226
|
+
msg = JSON.parse(raw);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if ((typeof msg.id !== "number") || (typeof msg.method !== "string")) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (msg.sessionId !== undefined) {
|
|
235
|
+
await this.handleTargetMessage(msg);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
await this.handleBrowserMessage(msg);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Dispatches a browser-level CDP command. Intercepts the Target domain to synthesize its semantics on top of Puppeteer's Browser API; passes everything else
|
|
243
|
+
* through to the browser session.
|
|
244
|
+
* @param msg - The parsed CDP request.
|
|
245
|
+
*/
|
|
246
|
+
async handleBrowserMessage(msg) {
|
|
247
|
+
switch (msg.method) {
|
|
248
|
+
case "Target.setDiscoverTargets": {
|
|
249
|
+
await this.handleSetDiscoverTargets(msg);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
case "Target.setAutoAttach": {
|
|
253
|
+
await this.handleSetAutoAttach(msg);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
case "Target.attachToTarget": {
|
|
257
|
+
await this.handleAttachToTarget(msg);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
case "Target.detachFromTarget": {
|
|
261
|
+
await this.handleDetachFromTarget(msg);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
case "Target.getTargets":
|
|
265
|
+
case "Target.getTargetInfo": {
|
|
266
|
+
// These are pure reads. We pass them through to the browser session which returns Chrome's authoritative answer.
|
|
267
|
+
await this.passthrough(msg, this.browserSession);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
default: {
|
|
271
|
+
await this.passthrough(msg, this.browserSession);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Dispatches a message addressed to a specific sub-session. Looks up the CDPSession by synthetic sessionId and forwards. Unknown sessionId yields a CDP error
|
|
278
|
+
* response so the client sees a clean failure rather than silently dropping.
|
|
279
|
+
* @param msg - The parsed CDP request with a sessionId.
|
|
280
|
+
*/
|
|
281
|
+
async handleTargetMessage(msg) {
|
|
282
|
+
const sessionId = msg.sessionId;
|
|
283
|
+
if (sessionId === undefined) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const session = this.sessions.get(sessionId);
|
|
287
|
+
if (!session) {
|
|
288
|
+
this.sendResponse({ error: { code: -32602, message: "session not attached: " + sessionId }, id: msg.id, sessionId });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
await this.passthrough(msg, session);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Forwards a CDP command to a session and sends the response (or error) back to the client. The sessionId on the response is the one the client provided so
|
|
295
|
+
* the client can correlate.
|
|
296
|
+
* @param msg - The parsed CDP request.
|
|
297
|
+
* @param session - The CDP session to forward to.
|
|
298
|
+
*/
|
|
299
|
+
async passthrough(msg, session) {
|
|
300
|
+
if (!session) {
|
|
301
|
+
this.sendResponse({ error: { code: -32603, message: "no session" }, id: msg.id, sessionId: msg.sessionId });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
const result = await sendCdp(session, msg.method, msg.params);
|
|
306
|
+
this.sendResponse({ id: msg.id, result, sessionId: msg.sessionId });
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
this.sendResponse({ error: { code: -32603, message: formatError(error) }, id: msg.id, sessionId: msg.sessionId });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Handles Target.setDiscoverTargets. When enabled, emits Target.targetCreated for every existing target so the client's view of the world matches Chrome's;
|
|
314
|
+
* targetcreated / targetdestroyed events thereafter flow via the Browser event subscriptions.
|
|
315
|
+
* @param msg - The parsed CDP request.
|
|
316
|
+
*/
|
|
317
|
+
async handleSetDiscoverTargets(msg) {
|
|
318
|
+
const params = (msg.params ?? {});
|
|
319
|
+
this.discoverTargets = params.discover === true;
|
|
320
|
+
this.sendResponse({ id: msg.id, result: {} });
|
|
321
|
+
if (!this.discoverTargets || !this.browserSession) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// Emit Target.targetCreated for every existing target so the client has the full picture as of attach time.
|
|
325
|
+
try {
|
|
326
|
+
const info = await sendCdp(this.browserSession, "Target.getTargets", {});
|
|
327
|
+
for (const targetInfo of info.targetInfos) {
|
|
328
|
+
this.sendEvent({ method: "Target.targetCreated", params: { targetInfo } });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
LOG.warn("CDP proxy could not enumerate targets for discovery: %s.", formatError(error));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Handles Target.setAutoAttach. When enabled, creates a per-target CDPSession for every existing target, generates a synthetic sessionId, and emits
|
|
337
|
+
* Target.attachedToTarget for each. Subsequent new targets attach via the targetcreated lifecycle hook.
|
|
338
|
+
* @param msg - The parsed CDP request.
|
|
339
|
+
*/
|
|
340
|
+
async handleSetAutoAttach(msg) {
|
|
341
|
+
const params = (msg.params ?? {});
|
|
342
|
+
// We only support flatten:true semantics - the modern mode every contemporary client uses. flatten:false is the legacy hierarchical mode where each session
|
|
343
|
+
// has its own WebSocket; we have no way to express that.
|
|
344
|
+
this.autoAttach = (params.autoAttach === true) && (params.flatten !== false);
|
|
345
|
+
this.sendResponse({ id: msg.id, result: {} });
|
|
346
|
+
if (!this.autoAttach || !this.browserSession) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const info = await sendCdp(this.browserSession, "Target.getTargets", {});
|
|
351
|
+
// Sequential attach (not Promise.all) so synthetic sessionIds are assigned and emitted in target order. Parallelism would interleave the
|
|
352
|
+
// Target.attachedToTarget events and complicate any client that asserts on the attachment sequence.
|
|
353
|
+
for (const targetInfo of info.targetInfos) {
|
|
354
|
+
// eslint-disable-next-line no-await-in-loop
|
|
355
|
+
await this.attachToTargetInfo(targetInfo);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
LOG.warn("CDP proxy could not enumerate targets for auto-attach: %s.", formatError(error));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Handles Target.attachToTarget. Looks up the target's info via the browser session, creates a Puppeteer CDPSession via Connection.createSession (the public
|
|
364
|
+
* API for attaching to a target by id), generates a synthetic sessionId, and returns it. The client uses that sessionId for subsequent messages.
|
|
365
|
+
* @param msg - The parsed CDP request.
|
|
366
|
+
*/
|
|
367
|
+
async handleAttachToTarget(msg) {
|
|
368
|
+
const params = (msg.params ?? {});
|
|
369
|
+
if (typeof params.targetId !== "string") {
|
|
370
|
+
this.sendResponse({ error: { code: -32602, message: "targetId required" }, id: msg.id });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (!this.browserSession || !this.connection) {
|
|
374
|
+
this.sendResponse({ error: { code: -32603, message: "browser session not ready" }, id: msg.id });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const info = await sendCdp(this.browserSession, "Target.getTargetInfo", { targetId: params.targetId });
|
|
379
|
+
const sessionId = await this.attachToTargetInfo(info.targetInfo, { emitEvent: false });
|
|
380
|
+
this.sendResponse({ id: msg.id, result: { sessionId } });
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
this.sendResponse({ error: { code: -32603, message: formatError(error) }, id: msg.id });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Attaches to a target given its TargetInfo: creates a Puppeteer CDPSession via Connection.createSession, generates a synthetic sessionId, wires event
|
|
388
|
+
* forwarding, and optionally emits Target.attachedToTarget for the client. Returns the sessionId so callers (manual attach via Target.attachToTarget) can
|
|
389
|
+
* include it in their response.
|
|
390
|
+
* @param targetInfo - The CDP TargetInfo identifying the target.
|
|
391
|
+
* @param options - emitEvent: whether to emit Target.attachedToTarget after attach (defaults to true; manual attach sets false and returns the sessionId via
|
|
392
|
+
* the original response).
|
|
393
|
+
* @returns The synthetic sessionId for the newly-attached session.
|
|
394
|
+
*/
|
|
395
|
+
async attachToTargetInfo(targetInfo, options = {}) {
|
|
396
|
+
if (!this.connection) {
|
|
397
|
+
throw new Error("connection unavailable");
|
|
398
|
+
}
|
|
399
|
+
const session = await this.connection.createSession(targetInfo);
|
|
400
|
+
const sessionId = "cdp-proxy-" + String(this.nextSessionSerial++);
|
|
401
|
+
this.sessions.set(sessionId, session);
|
|
402
|
+
this.sessionToTargetId.set(sessionId, targetInfo.targetId);
|
|
403
|
+
this.attachEventListener(session, sessionId);
|
|
404
|
+
if (options.emitEvent !== false) {
|
|
405
|
+
this.sendEvent({
|
|
406
|
+
method: "Target.attachedToTarget",
|
|
407
|
+
params: { sessionId, targetInfo, waitingForDebugger: false }
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return sessionId;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Handles Target.detachFromTarget. Looks up the session by sessionId, detaches it, emits Target.detachedFromTarget, and removes from the map.
|
|
414
|
+
* @param msg - The parsed CDP request.
|
|
415
|
+
*/
|
|
416
|
+
async handleDetachFromTarget(msg) {
|
|
417
|
+
const params = (msg.params ?? {});
|
|
418
|
+
if (typeof params.sessionId !== "string") {
|
|
419
|
+
this.sendResponse({ error: { code: -32602, message: "sessionId required" }, id: msg.id });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await this.detachSessionById(params.sessionId);
|
|
423
|
+
this.sendResponse({ id: msg.id, result: {} });
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Detaches a sub-session by its synthetic sessionId, restoring the patched emit, calling CDPSession.detach() (best-effort - the session may already be gone if
|
|
427
|
+
* the target closed), and emitting Target.detachedFromTarget so the client's view is consistent.
|
|
428
|
+
* @param sessionId - The synthetic sessionId.
|
|
429
|
+
*/
|
|
430
|
+
async detachSessionById(sessionId) {
|
|
431
|
+
const session = this.sessions.get(sessionId);
|
|
432
|
+
const targetId = this.sessionToTargetId.get(sessionId);
|
|
433
|
+
if (session) {
|
|
434
|
+
this.detachEventListener(session);
|
|
435
|
+
try {
|
|
436
|
+
await session.detach();
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// Detach is best-effort. A session whose target already closed will throw; the resulting state (session gone) is what we want anyway.
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
this.sessions.delete(sessionId);
|
|
443
|
+
this.sessionToTargetId.delete(sessionId);
|
|
444
|
+
if (targetId) {
|
|
445
|
+
this.sendEvent({ method: "Target.detachedFromTarget", params: { sessionId, targetId } });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Handles the CDP `Target.targetCreated` event emitted by the browser session. When autoAttach is on, mints a Puppeteer CDPSession for the new target and emits
|
|
450
|
+
* a synthetic Target.attachedToTarget to the client. Whether the client also sees the Target.targetCreated event is decided by the wildcard forwarder (which
|
|
451
|
+
* gates Target.* events on this.discoverTargets).
|
|
452
|
+
* @param event - The CDP event payload.
|
|
453
|
+
*/
|
|
454
|
+
async handleCdpTargetCreated(event) {
|
|
455
|
+
if (!this.autoAttach) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
await this.attachToTargetInfo(event.targetInfo);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
LOG.warn("CDP proxy auto-attach failed: %s.", formatError(error));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Handles the CDP `Target.targetDestroyed` event emitted by the browser session. Detaches every sub-session attached to this targetId and emits the
|
|
467
|
+
* corresponding Target.detachedFromTarget events. The targetId on the CDP event matches exactly what we stored in sessionToTargetId at attach time, so there is
|
|
468
|
+
* no fuzzy matching - this is the lookup the Puppeteer-level targetdestroyed event could not give us.
|
|
469
|
+
* @param event - The CDP event payload.
|
|
470
|
+
*/
|
|
471
|
+
async handleCdpTargetDestroyed(event) {
|
|
472
|
+
const matching = [];
|
|
473
|
+
for (const [sessionId, targetId] of this.sessionToTargetId) {
|
|
474
|
+
if (targetId === event.targetId) {
|
|
475
|
+
matching.push(sessionId);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Sequential detach so the synthesized Target.detachedFromTarget events emit in deterministic order to the client.
|
|
479
|
+
for (const sessionId of matching) {
|
|
480
|
+
// eslint-disable-next-line no-await-in-loop
|
|
481
|
+
await this.detachSessionById(sessionId);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Attaches a wildcard event listener to a CDPSession. Each CDP-domain event (matched by the "Domain.event" naming pattern - uppercase first letter, dot,
|
|
486
|
+
* lowercase next letter) is forwarded to the WS client after the session's own listeners have run, so any internal state mutation (e.g. cleanup driven by
|
|
487
|
+
* Target.targetDestroyed) is settled before the client sees the event. Target.* events are gated on this.discoverTargets so a client that has not subscribed
|
|
488
|
+
* to the Target domain does not receive its events even though our internal subscription stays on.
|
|
489
|
+
*
|
|
490
|
+
* Implementation note. CDPSession exposes no public "subscribe to every event" hook, so we monkey-patch the EventEmitter.emit method. The patch saves the
|
|
491
|
+
* original so detachEventListener can restore it cleanly. Order of operations inside the patch is: dispatch through original first (specific listeners fire,
|
|
492
|
+
* including the internal Target.* handlers we registered), then forward to the client. This ordering avoids the race where the client receives an event tied
|
|
493
|
+
* to state we haven't yet cleaned up.
|
|
494
|
+
*
|
|
495
|
+
* @param session - The CDP session to monitor.
|
|
496
|
+
* @param sessionId - The synthetic sessionId to include in the forwarded events (null for browser-level events).
|
|
497
|
+
*/
|
|
498
|
+
attachEventListener(session, sessionId) {
|
|
499
|
+
const original = session.emit.bind(session);
|
|
500
|
+
this.originalEmits.set(session, original);
|
|
501
|
+
// We alias `this` to a closure-captured reference because the patched function below is invoked as a method on the CDPSession (so its own `this` would point
|
|
502
|
+
// at the session, not this CdpProxySession). Arrow functions cannot be used here either, because the underlying EventEmitter contract expects a function whose
|
|
503
|
+
// `this` binding can be anything - matching the original emit shape we're replacing.
|
|
504
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
505
|
+
const proxy = this;
|
|
506
|
+
const patched = function (event, ...args) {
|
|
507
|
+
// Dispatch to user-registered listeners first so internal handlers run before the client sees the event.
|
|
508
|
+
const result = original(event, ...args);
|
|
509
|
+
if ((typeof event === "string") && (/^[A-Z][a-zA-Z0-9]+\.[a-z]/).test(event)) {
|
|
510
|
+
// Target.* events are gated: clients only see them when they have opted in via Target.setDiscoverTargets / setAutoAttach. Our internal subscription stays
|
|
511
|
+
// on regardless so we can drive session cleanup.
|
|
512
|
+
const isTargetEvent = event.startsWith("Target.");
|
|
513
|
+
const shouldForward = !isTargetEvent || proxy.discoverTargets;
|
|
514
|
+
if (shouldForward) {
|
|
515
|
+
proxy.sendEvent({ method: event, params: args[0], ...(sessionId === null ? {} : { sessionId }) });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return result;
|
|
519
|
+
};
|
|
520
|
+
session.emit = patched;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Restores the original emit on a session, undoing attachEventListener. Called on detach / cleanup.
|
|
524
|
+
* @param session - The CDP session whose emit to restore.
|
|
525
|
+
*/
|
|
526
|
+
detachEventListener(session) {
|
|
527
|
+
const original = this.originalEmits.get(session);
|
|
528
|
+
if (original) {
|
|
529
|
+
session.emit = original;
|
|
530
|
+
this.originalEmits.delete(session);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Sends a CDP wire response back to the client.
|
|
535
|
+
* @param response - The response object.
|
|
536
|
+
*/
|
|
537
|
+
sendResponse(response) {
|
|
538
|
+
this.sendRaw(response);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Sends a CDP wire event back to the client.
|
|
542
|
+
* @param event - The event object.
|
|
543
|
+
*/
|
|
544
|
+
sendEvent(event) {
|
|
545
|
+
this.sendRaw(event);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Serializes a frame and writes it to the WS, swallowing errors that arise from a closed socket. We do not retry; if the WS is closed, the proxy session is
|
|
549
|
+
* already on its way to cleanup.
|
|
550
|
+
* @param frame - The frame to send.
|
|
551
|
+
*/
|
|
552
|
+
sendRaw(frame) {
|
|
553
|
+
if (this.ws.readyState !== WebSocket.OPEN) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
this.ws.send(JSON.stringify(frame));
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
LOG.warn("CDP proxy WebSocket write failed: %s.", formatError(error));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Detaches every sub-session, restores patched emits, and unsubscribes from the browser-disconnect event. Safe to call multiple times: detach of an
|
|
565
|
+
* already-detached CDPSession is a no-op, and the disconnect-listener field is cleared after the first removal so a repeat call is a no-op too. Triggered by
|
|
566
|
+
* WS close, WS error, or closeWith().
|
|
567
|
+
*/
|
|
568
|
+
async cleanup() {
|
|
569
|
+
// Unsubscribe from browser disconnect first so a disconnect arriving mid-cleanup cannot drive a second closeWith() against an already-closing WS. The
|
|
570
|
+
// sequencing is conservative - the close handler is idempotent - but unsubscribing here keeps the lifecycle ledger clean.
|
|
571
|
+
if (this.onBrowserDisconnect) {
|
|
572
|
+
this.browser.off("disconnected", this.onBrowserDisconnect);
|
|
573
|
+
this.onBrowserDisconnect = null;
|
|
574
|
+
}
|
|
575
|
+
// Sequential detach to keep the final wave of Target.detachedFromTarget events ordered.
|
|
576
|
+
for (const sessionId of Array.from(this.sessions.keys())) {
|
|
577
|
+
// eslint-disable-next-line no-await-in-loop
|
|
578
|
+
await this.detachSessionById(sessionId);
|
|
579
|
+
}
|
|
580
|
+
if (this.browserSession) {
|
|
581
|
+
this.detachEventListener(this.browserSession);
|
|
582
|
+
try {
|
|
583
|
+
await this.browserSession.detach();
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
// Best-effort.
|
|
587
|
+
}
|
|
588
|
+
this.browserSession = null;
|
|
589
|
+
}
|
|
590
|
+
if (this.attached) {
|
|
591
|
+
this.attached = false;
|
|
592
|
+
LOG.info("CDP client detached.");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/* End of CdpProxySession. */
|
|
597
|
+
/**
|
|
598
|
+
* Configures the /cdp/json/version, /cdp/json, and /cdp/json/list discovery endpoints on the Express app. Each handler checks isCategoryEnabled("cdp") at request
|
|
599
|
+
* time and returns 404 when CDP is disabled, so the surface appears and disappears in sync with the toggle in /debug without requiring a restart.
|
|
600
|
+
* @param app - The Express application.
|
|
601
|
+
*/
|
|
602
|
+
export function setupCdpEndpoint(app) {
|
|
603
|
+
app.get("/cdp/json/version", async (req, res) => {
|
|
604
|
+
if (!isCategoryEnabled("cdp")) {
|
|
605
|
+
res.status(404).send("Not Found");
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const browser = getBrowserInstance();
|
|
609
|
+
if (!browser) {
|
|
610
|
+
res.status(503).send("Browser not running");
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const session = await getDiscoverySession(browser);
|
|
615
|
+
const version = await sendCdp(session, "Browser.getVersion", {});
|
|
616
|
+
res.json({
|
|
617
|
+
"Browser": version.product,
|
|
618
|
+
"Protocol-Version": version.protocolVersion,
|
|
619
|
+
"User-Agent": version.userAgent,
|
|
620
|
+
"V8-Version": version.jsVersion,
|
|
621
|
+
"WebKit-Version": version.product,
|
|
622
|
+
"prismcastVersion": getPackageVersion(),
|
|
623
|
+
"webSocketDebuggerUrl": makeWsUrl(req, "browser/prismcast")
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
LOG.warn("CDP /cdp/json/version error: %s.", formatError(error));
|
|
628
|
+
res.status(500).send("Internal error");
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
// /cdp/json and /cdp/json/list are aliases - both return the target list.
|
|
632
|
+
const listHandler = async (req, res) => {
|
|
633
|
+
if (!isCategoryEnabled("cdp")) {
|
|
634
|
+
res.status(404).send("Not Found");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const browser = getBrowserInstance();
|
|
638
|
+
if (!browser) {
|
|
639
|
+
res.status(503).send("Browser not running");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
const session = await getDiscoverySession(browser);
|
|
644
|
+
const targets = await sendCdp(session, "Target.getTargets", {});
|
|
645
|
+
const list = targets.targetInfos.map((info) => ({
|
|
646
|
+
description: "",
|
|
647
|
+
devtoolsFrontendUrl: "/devtools/inspector.html?ws=" + makeWsUrl(req, "page/" + info.targetId).replace(/^ws:\/\//, ""),
|
|
648
|
+
id: info.targetId,
|
|
649
|
+
title: info.title,
|
|
650
|
+
type: info.type,
|
|
651
|
+
url: info.url,
|
|
652
|
+
webSocketDebuggerUrl: makeWsUrl(req, "page/" + info.targetId)
|
|
653
|
+
}));
|
|
654
|
+
res.json(list);
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
LOG.warn("CDP /cdp/json error: %s.", formatError(error));
|
|
658
|
+
res.status(500).send("Internal error");
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
app.get("/cdp/json", (req, res) => { void listHandler(req, res); });
|
|
662
|
+
app.get("/cdp/json/list", (req, res) => { void listHandler(req, res); });
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Attaches the WebSocket upgrade handler for /cdp/devtools/* paths to the given HTTP server. Called from startServer after app.listen() returns the server.
|
|
666
|
+
* Upgrades for paths outside /cdp/devtools/ are ignored (left to any other upgrade handler).
|
|
667
|
+
* @param server - The Node HTTP server.
|
|
668
|
+
*/
|
|
669
|
+
export function attachCdpUpgradeHandler(server) {
|
|
670
|
+
if (wss) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
wss = new WebSocketServer({ noServer: true });
|
|
674
|
+
server.on("upgrade", (req, socket, head) => {
|
|
675
|
+
const url = req.url ?? "";
|
|
676
|
+
if (!url.startsWith("/cdp/devtools/")) {
|
|
677
|
+
// Not ours - leave the socket for any other upgrade handler. PrismCast doesn't currently install another, but this keeps the surface composable.
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (!isCategoryEnabled("cdp")) {
|
|
681
|
+
socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
682
|
+
socket.destroy();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const browser = getBrowserInstance();
|
|
686
|
+
if (!browser) {
|
|
687
|
+
socket.write("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n");
|
|
688
|
+
socket.destroy();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (!wss) {
|
|
692
|
+
socket.destroy();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
696
|
+
const proxySession = new CdpProxySession(ws, browser);
|
|
697
|
+
void proxySession.start();
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
LOG.info("CDP proxy listening at /cdp.");
|
|
701
|
+
}
|
|
702
|
+
//# sourceMappingURL=cdp.js.map
|