mirror of https://github.com/garrytan/gstack.git
feat(browse): add extractCookiesViaCdpPipe using CDP pipe transport
Pipe-transport equivalent of extractCookiesViaCdp. Uses Target.getTargets + Target.attachToTarget(flatten: true) to acquire a page session over a single CDP pipe connection, then Network.getAllCookies. Still not wired into importCookiesViaCdp; next commit swaps the TCP path over. Refs #1136.
This commit is contained in:
parent
3bc13adabc
commit
c3f26cf7b5
|
|
@ -1151,6 +1151,72 @@ export class CdpPipeTransport {
|
|||
}
|
||||
}
|
||||
|
||||
// Minimal transport surface needed by extractCookiesViaCdpPipe. Lets tests
|
||||
// pass a scripted mock without constructing a real CdpPipeTransport.
|
||||
export interface CdpCallable {
|
||||
send(method: string, params?: object, sessionId?: string): Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract cookies over a CDP-over-pipe transport. The transport must already
|
||||
* be wired to a running Chrome process with --remote-debugging-pipe.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Target.getTargets — find a page target
|
||||
* 2. Target.attachToTarget — flatten session, get sessionId
|
||||
* 3. Network.enable — on the page session
|
||||
* 4. Network.getAllCookies — returns every cookie the browser has
|
||||
* 5. Filter + map to PlaywrightCookie[]
|
||||
*/
|
||||
export async function extractCookiesViaCdpPipe(
|
||||
transport: CdpCallable,
|
||||
domains: string[],
|
||||
): Promise<PlaywrightCookie[]> {
|
||||
if (domains.length === 0) return [];
|
||||
|
||||
const targets = await transport.send('Target.getTargets');
|
||||
const pageTarget = (targets.targetInfos as Array<{ targetId: string; type: string }>)
|
||||
.find(t => t.type === 'page');
|
||||
if (!pageTarget) {
|
||||
throw new CookieImportError(
|
||||
'No page target found in Chrome for CDP cookie extraction',
|
||||
'cdp_error',
|
||||
);
|
||||
}
|
||||
|
||||
const attached = await transport.send('Target.attachToTarget', {
|
||||
targetId: pageTarget.targetId,
|
||||
flatten: true,
|
||||
});
|
||||
const sessionId = attached.sessionId as string;
|
||||
|
||||
await transport.send('Network.enable', undefined, sessionId);
|
||||
const cookiesResp = await transport.send('Network.getAllCookies', undefined, sessionId);
|
||||
|
||||
// Normalize domain matching: `.example.com` should match `example.com` and vice versa.
|
||||
const domainSet = new Set<string>();
|
||||
for (const d of domains) {
|
||||
domainSet.add(d);
|
||||
domainSet.add(d.startsWith('.') ? d.slice(1) : '.' + d);
|
||||
}
|
||||
|
||||
const result: PlaywrightCookie[] = [];
|
||||
for (const c of cookiesResp.cookies as CdpCookie[]) {
|
||||
if (!domainSet.has(c.domain)) continue;
|
||||
result.push({
|
||||
name: c.name,
|
||||
value: c.value,
|
||||
domain: c.domain,
|
||||
path: c.path || '/',
|
||||
expires: c.expires === -1 ? -1 : c.expires,
|
||||
secure: c.secure,
|
||||
httpOnly: c.httpOnly,
|
||||
sameSite: cdpSameSite(c.sameSite),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a browser's cookie DB contains v20 (App-Bound) encrypted cookies.
|
||||
* Quick check — reads a small sample, no decryption attempted.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import { CdpPipeTransport } from '../src/cookie-import-browser';
|
||||
import { CdpPipeTransport, extractCookiesViaCdpPipe } from '../src/cookie-import-browser';
|
||||
|
||||
// ─── Test Constants ─────────────────────────────────────────────
|
||||
|
||||
|
|
@ -677,3 +677,111 @@ describe('CdpPipeTransport', () => {
|
|||
t.close();
|
||||
});
|
||||
});
|
||||
|
||||
// A mock transport that replays scripted responses per-method.
|
||||
// The test writes scripts keyed by method name; send() returns the next
|
||||
// scripted response for that method.
|
||||
class ScriptedTransport {
|
||||
private scripts: Record<string, unknown[]> = {};
|
||||
sent: Array<{ method: string; params?: unknown; sessionId?: string }> = [];
|
||||
|
||||
script(method: string, ...responses: unknown[]) {
|
||||
this.scripts[method] = (this.scripts[method] ?? []).concat(responses);
|
||||
}
|
||||
|
||||
async send(method: string, params?: object, sessionId?: string): Promise<any> {
|
||||
this.sent.push({ method, params, sessionId });
|
||||
const queue = this.scripts[method];
|
||||
if (!queue || queue.length === 0) {
|
||||
throw new Error(`No scripted response for ${method}`);
|
||||
}
|
||||
const next = queue.shift();
|
||||
if (next instanceof Error) throw next;
|
||||
return next;
|
||||
}
|
||||
}
|
||||
|
||||
describe('extractCookiesViaCdpPipe', () => {
|
||||
test('happy path: attaches to first page target, requests cookies, filters by domain', async () => {
|
||||
const transport = new ScriptedTransport();
|
||||
transport.script('Target.getTargets', {
|
||||
targetInfos: [
|
||||
{ targetId: 'BROWSER1', type: 'browser' },
|
||||
{ targetId: 'PAGE1', type: 'page', url: 'about:blank' },
|
||||
],
|
||||
});
|
||||
transport.script('Target.attachToTarget', { sessionId: 'S1' });
|
||||
transport.script('Network.enable', {});
|
||||
transport.script('Network.getAllCookies', {
|
||||
cookies: [
|
||||
{ name: 'sid', value: 'abc', domain: 'example.com', path: '/', expires: 1800000000, size: 4, httpOnly: true, secure: true, session: false, sameSite: 'Lax' },
|
||||
{ name: 'other', value: 'xyz', domain: 'other.com', path: '/', expires: -1, size: 3, httpOnly: false, secure: false, session: true, sameSite: 'None' },
|
||||
],
|
||||
});
|
||||
|
||||
const cookies = await extractCookiesViaCdpPipe(transport as any, ['example.com']);
|
||||
expect(cookies).toHaveLength(1);
|
||||
expect(cookies[0]).toEqual({
|
||||
name: 'sid',
|
||||
value: 'abc',
|
||||
domain: 'example.com',
|
||||
path: '/',
|
||||
expires: 1800000000,
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'Lax',
|
||||
});
|
||||
|
||||
expect(transport.sent.map(s => s.method)).toEqual([
|
||||
'Target.getTargets',
|
||||
'Target.attachToTarget',
|
||||
'Network.enable',
|
||||
'Network.getAllCookies',
|
||||
]);
|
||||
|
||||
expect(transport.sent[1].params).toEqual({ targetId: 'PAGE1', flatten: true });
|
||||
|
||||
expect(transport.sent[2].sessionId).toBe('S1');
|
||||
expect(transport.sent[3].sessionId).toBe('S1');
|
||||
});
|
||||
|
||||
test('matches cookies with leading-dot domain normalization', async () => {
|
||||
const transport = new ScriptedTransport();
|
||||
transport.script('Target.getTargets', { targetInfos: [{ targetId: 'P', type: 'page' }] });
|
||||
transport.script('Target.attachToTarget', { sessionId: 'S1' });
|
||||
transport.script('Network.enable', {});
|
||||
transport.script('Network.getAllCookies', {
|
||||
cookies: [
|
||||
{ name: 'a', value: '1', domain: '.example.com', path: '/', expires: -1, size: 1, httpOnly: false, secure: false, session: true, sameSite: 'Lax' },
|
||||
],
|
||||
});
|
||||
|
||||
const cookies = await extractCookiesViaCdpPipe(transport as any, ['example.com']);
|
||||
expect(cookies).toHaveLength(1);
|
||||
expect(cookies[0].domain).toBe('.example.com');
|
||||
});
|
||||
|
||||
test('throws cdp_error if no page target exists', async () => {
|
||||
const transport = new ScriptedTransport();
|
||||
transport.script('Target.getTargets', {
|
||||
targetInfos: [{ targetId: 'BROWSER1', type: 'browser' }],
|
||||
});
|
||||
|
||||
let caught: any = null;
|
||||
try {
|
||||
await extractCookiesViaCdpPipe(transport as any, ['example.com']);
|
||||
} catch (e) {
|
||||
caught = e;
|
||||
}
|
||||
expect(caught).toBeInstanceOf(CookieImportError);
|
||||
expect(caught!.code).toBe('cdp_error');
|
||||
expect(caught!.message.toLowerCase()).toContain('page target');
|
||||
});
|
||||
|
||||
test('empty domains list short-circuits to empty result', async () => {
|
||||
const transport = new ScriptedTransport();
|
||||
const cookies = await extractCookiesViaCdpPipe(transport as any, []);
|
||||
expect(cookies).toEqual([]);
|
||||
expect(transport.sent).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue