nothing-browser 0.0.19 → 0.0.21
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/LICENSE +20 -20
- package/README.md +253 -253
- package/dist/launch/detect.js +7 -2
- package/dist/launch/spawn.js +7 -2
- package/dist/piggy/launch/detect.d.ts.map +1 -1
- package/dist/piggy/launch/spawn.d.ts.map +1 -1
- package/dist/piggy/register/index.d.ts.map +1 -1
- package/dist/piggy.d.ts +0 -1
- package/dist/piggy.d.ts.map +1 -1
- package/dist/piggy.js +21 -5
- package/dist/register/index.js +14 -3
- package/nothing_browser_pig_pink.svg +58 -58
- package/package.json +4 -3
- package/piggy/cache/memory.d.ts +6 -6
- package/piggy/cache/memory.ts +37 -37
- package/piggy/client/index.d.ts +78 -78
- package/piggy/client/index.ts +567 -567
- package/piggy/human/index.d.ts +6 -6
- package/piggy/human/index.ts +52 -52
- package/piggy/intercept/scripts.d.ts +12 -12
- package/piggy/intercept/scripts.ts +152 -152
- package/piggy/launch/detect.d.ts +2 -2
- package/piggy/launch/detect.ts +59 -51
- package/piggy/launch/spawn.d.ts +5 -5
- package/piggy/launch/spawn.ts +157 -164
- package/piggy/logger/index.d.ts +2 -2
- package/piggy/logger/index.ts +58 -58
- package/piggy/open/index.d.ts +3 -3
- package/piggy/open/index.ts +4 -4
- package/piggy/pool/index.d.ts +11 -11
- package/piggy/pool/index.ts +74 -74
- package/piggy/register/index.d.ts +6 -6
- package/piggy/register/index.ts +517 -506
- package/piggy/server/index.d.ts +57 -57
- package/piggy/server/index.ts +189 -189
- package/piggy/store/index.d.ts +25 -25
- package/piggy/store/index.ts +229 -229
- package/piggy.ts +216 -221
package/piggy/client/index.ts
CHANGED
|
@@ -1,568 +1,568 @@
|
|
|
1
|
-
// piggy/client/index.ts
|
|
2
|
-
import { connect, type Socket } from "net";
|
|
3
|
-
import { writeFileSync, mkdirSync } from "fs";
|
|
4
|
-
import { dirname } from "path";
|
|
5
|
-
import { platform } from "os";
|
|
6
|
-
import logger from "../logger";
|
|
7
|
-
|
|
8
|
-
const DEFAULT_SOCKET_PATH = platform() === "win32"
|
|
9
|
-
? "\\\\.\\pipe\\piggy"
|
|
10
|
-
: "/tmp/piggy";
|
|
11
|
-
|
|
12
|
-
// ── Transport interface ────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
interface Transport {
|
|
15
|
-
send(data: string): void;
|
|
16
|
-
on(event: "data", handler: (chunk: string) => void): void;
|
|
17
|
-
on(event: "error", handler: (e: Error) => void): void;
|
|
18
|
-
on(event: "close", handler: () => void): void;
|
|
19
|
-
destroy(): void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ── Socket transport ───────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
class SocketTransport implements Transport {
|
|
25
|
-
private sock: Socket;
|
|
26
|
-
|
|
27
|
-
constructor(sock: Socket) {
|
|
28
|
-
this.sock = sock;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
send(data: string) {
|
|
32
|
-
this.sock.write(data);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
on(event: string, handler: any) {
|
|
36
|
-
this.sock.on(event, handler);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
destroy() {
|
|
40
|
-
this.sock.destroy();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ── HTTP transport ─────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
class HttpTransport implements Transport {
|
|
47
|
-
private host: string;
|
|
48
|
-
private key: string;
|
|
49
|
-
private dataHandlers: ((chunk: string) => void)[] = [];
|
|
50
|
-
private errorHandlers: ((e: Error) => void)[] = [];
|
|
51
|
-
private closeHandlers: (() => void)[] = [];
|
|
52
|
-
private _destroyed = false;
|
|
53
|
-
|
|
54
|
-
constructor(host: string, key: string) {
|
|
55
|
-
this.host = host.replace(/\/$/, "");
|
|
56
|
-
this.key = key;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
on(event: string, handler: any) {
|
|
60
|
-
if (event === "data") this.dataHandlers.push(handler);
|
|
61
|
-
if (event === "error") this.errorHandlers.push(handler);
|
|
62
|
-
if (event === "close") this.closeHandlers.push(handler);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
send(data: string) {
|
|
66
|
-
if (this._destroyed) return;
|
|
67
|
-
|
|
68
|
-
fetch(this.host, {
|
|
69
|
-
method: "POST",
|
|
70
|
-
headers: {
|
|
71
|
-
"Content-Type": "application/json",
|
|
72
|
-
"X-Piggy-Key": this.key,
|
|
73
|
-
},
|
|
74
|
-
body: data,
|
|
75
|
-
})
|
|
76
|
-
.then(async (res) => {
|
|
77
|
-
if (!res.ok) {
|
|
78
|
-
const text = await res.text().catch(() => `HTTP ${res.status}`);
|
|
79
|
-
this.errorHandlers.forEach(h =>
|
|
80
|
-
h(new Error(`HTTP ${res.status}: ${text}`))
|
|
81
|
-
);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const text = await res.text();
|
|
85
|
-
const lines = text.split("\n").filter(l => l.trim());
|
|
86
|
-
for (const line of lines) {
|
|
87
|
-
this.dataHandlers.forEach(h => h(line + "\n"));
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
.catch((e: Error) => {
|
|
91
|
-
if (!this._destroyed) {
|
|
92
|
-
this.errorHandlers.forEach(h => h(e));
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
destroy() {
|
|
98
|
-
this._destroyed = true;
|
|
99
|
-
this.closeHandlers.forEach(h => h());
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── PiggyClient ────────────────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
export class PiggyClient {
|
|
106
|
-
private socketPath: string;
|
|
107
|
-
private httpHost: string | null = null;
|
|
108
|
-
private httpKey: string | null = null;
|
|
109
|
-
private transport: Transport | null = null;
|
|
110
|
-
private reqId = 0;
|
|
111
|
-
private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
|
|
112
|
-
private buf = "";
|
|
113
|
-
private eventHandlers = new Map<string, Map<string, (data: any) => Promise<any>>>();
|
|
114
|
-
private globalEventHandlers = new Map<string, Set<(data: any) => void>>();
|
|
115
|
-
|
|
116
|
-
constructor(socketPath?: string);
|
|
117
|
-
constructor(opts: { host: string; key: string });
|
|
118
|
-
|
|
119
|
-
constructor(arg?: string | { host: string; key: string }) {
|
|
120
|
-
if (arg && typeof arg === "object") {
|
|
121
|
-
this.socketPath = "";
|
|
122
|
-
this.httpHost = arg.host.replace(/\/$/, "");
|
|
123
|
-
this.httpKey = arg.key;
|
|
124
|
-
} else {
|
|
125
|
-
this.socketPath = (arg as string | undefined) ?? DEFAULT_SOCKET_PATH;
|
|
126
|
-
}
|
|
127
|
-
this.eventHandlers.set("default", new Map());
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ── Connect ───────────────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
connect(): Promise<void> {
|
|
133
|
-
if (this.httpHost) return this._connectHttp();
|
|
134
|
-
return this._connectSocket();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private _connectSocket(): Promise<void> {
|
|
138
|
-
return new Promise((resolve, reject) => {
|
|
139
|
-
logger.info(`Connecting to socket: ${this.socketPath}`);
|
|
140
|
-
const sock = connect(this.socketPath);
|
|
141
|
-
sock.setEncoding("utf8");
|
|
142
|
-
|
|
143
|
-
sock.on("connect", () => {
|
|
144
|
-
this.transport = new SocketTransport(sock);
|
|
145
|
-
this._wireTransport();
|
|
146
|
-
logger.success("Connected to Piggy server (socket)");
|
|
147
|
-
resolve();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
sock.on("error", (e) => {
|
|
151
|
-
for (const p of this.pending.values()) p.reject(e);
|
|
152
|
-
this.pending.clear();
|
|
153
|
-
reject(e);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private async _connectHttp(): Promise<void> {
|
|
159
|
-
logger.info(`Connecting to Piggy server (HTTP): ${this.httpHost}`);
|
|
160
|
-
try {
|
|
161
|
-
const res = await fetch(this.httpHost!, {
|
|
162
|
-
method: "POST",
|
|
163
|
-
headers: {
|
|
164
|
-
"Content-Type": "application/json",
|
|
165
|
-
"X-Piggy-Key": this.httpKey!,
|
|
166
|
-
},
|
|
167
|
-
body: "hello",
|
|
168
|
-
});
|
|
169
|
-
if (res.status === 401) {
|
|
170
|
-
throw new Error("Unauthorized — invalid X-Piggy-Key");
|
|
171
|
-
}
|
|
172
|
-
this.transport = new HttpTransport(this.httpHost!, this.httpKey!);
|
|
173
|
-
this._wireTransport();
|
|
174
|
-
logger.success(`Connected to Piggy server (HTTP): ${this.httpHost}`);
|
|
175
|
-
} catch (e: any) {
|
|
176
|
-
throw new Error(`Failed to connect to Piggy HTTP server: ${e.message}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
private _wireTransport() {
|
|
181
|
-
if (!this.transport) return;
|
|
182
|
-
|
|
183
|
-
this.transport.on("data", (chunk: string) => {
|
|
184
|
-
this.buf += chunk;
|
|
185
|
-
const lines = this.buf.split("\n");
|
|
186
|
-
this.buf = lines.pop()!;
|
|
187
|
-
|
|
188
|
-
for (const line of lines) {
|
|
189
|
-
if (!line.trim()) continue;
|
|
190
|
-
try {
|
|
191
|
-
const msg = JSON.parse(line);
|
|
192
|
-
|
|
193
|
-
if (msg.type === "event") {
|
|
194
|
-
this.handleEvent(msg);
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const p = this.pending.get(msg.id);
|
|
199
|
-
if (p) {
|
|
200
|
-
this.pending.delete(msg.id);
|
|
201
|
-
msg.ok ? p.resolve(msg.data) : p.reject(new Error(msg.data ?? "command failed"));
|
|
202
|
-
}
|
|
203
|
-
} catch {
|
|
204
|
-
logger.error(`Bad JSON from server: ${line}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
this.transport.on("error", (e: Error) => {
|
|
210
|
-
for (const p of this.pending.values()) p.reject(e);
|
|
211
|
-
this.pending.clear();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
this.transport.on("close", () => {
|
|
215
|
-
for (const p of this.pending.values()) p.reject(new Error("Connection closed"));
|
|
216
|
-
this.pending.clear();
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ── Event handling ────────────────────────────────────────────────────────
|
|
221
|
-
|
|
222
|
-
private handleEvent(event: any) {
|
|
223
|
-
if (event.event === "exposed_call") {
|
|
224
|
-
const { tabId, name, callId, data } = event;
|
|
225
|
-
const effectiveTabId = tabId || "default";
|
|
226
|
-
const handlers = this.eventHandlers.get(effectiveTabId);
|
|
227
|
-
const handler = handlers?.get(name);
|
|
228
|
-
|
|
229
|
-
if (handler) {
|
|
230
|
-
let parsedData: any;
|
|
231
|
-
try {
|
|
232
|
-
parsedData = JSON.parse(data || "null");
|
|
233
|
-
} catch {
|
|
234
|
-
parsedData = data;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
Promise.resolve(handler(parsedData))
|
|
238
|
-
.then(response => {
|
|
239
|
-
if (response && typeof response === "object" && "success" in response) {
|
|
240
|
-
this.send("exposed.result", {
|
|
241
|
-
tabId: effectiveTabId,
|
|
242
|
-
callId,
|
|
243
|
-
result: response.success ? JSON.stringify(response.result) : (response.error || "Unknown error"),
|
|
244
|
-
isError: !response.success,
|
|
245
|
-
}).catch(e => logger.error(`Failed to send exposed result: ${e}`));
|
|
246
|
-
} else {
|
|
247
|
-
this.send("exposed.result", {
|
|
248
|
-
tabId: effectiveTabId,
|
|
249
|
-
callId,
|
|
250
|
-
result: JSON.stringify(response),
|
|
251
|
-
isError: false,
|
|
252
|
-
}).catch(e => logger.error(`Failed to send exposed result: ${e}`));
|
|
253
|
-
}
|
|
254
|
-
})
|
|
255
|
-
.catch(err => {
|
|
256
|
-
this.send("exposed.result", {
|
|
257
|
-
tabId: effectiveTabId,
|
|
258
|
-
callId,
|
|
259
|
-
result: err.message || "Handler error",
|
|
260
|
-
isError: true,
|
|
261
|
-
}).catch(e => logger.error(`Failed to send exposed error: ${e}`));
|
|
262
|
-
});
|
|
263
|
-
} else {
|
|
264
|
-
logger.warn(`No handler for exposed function: ${name} in tab ${effectiveTabId}`);
|
|
265
|
-
}
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (event.event === "navigate") {
|
|
270
|
-
const handlers = this.globalEventHandlers.get(`navigate:${event.tabId}`);
|
|
271
|
-
if (handlers) {
|
|
272
|
-
for (const h of handlers) {
|
|
273
|
-
try { h(event.url); } catch (e) { logger.error(`navigate handler error: ${e}`); }
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
const wildcard = this.globalEventHandlers.get("navigate:*");
|
|
277
|
-
if (wildcard) {
|
|
278
|
-
for (const h of wildcard) {
|
|
279
|
-
try { h({ url: event.url, tabId: event.tabId }); } catch {}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
onEvent(eventName: string, tabId: string, handler: (data: any) => void): () => void {
|
|
286
|
-
const key = `${eventName}:${tabId}`;
|
|
287
|
-
if (!this.globalEventHandlers.has(key)) {
|
|
288
|
-
this.globalEventHandlers.set(key, new Set());
|
|
289
|
-
}
|
|
290
|
-
this.globalEventHandlers.get(key)!.add(handler);
|
|
291
|
-
return () => this.globalEventHandlers.get(key)?.delete(handler);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
disconnect() {
|
|
295
|
-
this.transport?.destroy();
|
|
296
|
-
this.transport = null;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ── Core send ─────────────────────────────────────────────────────────────
|
|
300
|
-
|
|
301
|
-
send<T = any>(cmd: string, payload: Record<string, any> = {}): Promise<T> {
|
|
302
|
-
return new Promise((resolve, reject) => {
|
|
303
|
-
if (!this.transport) return reject(new Error("Not connected"));
|
|
304
|
-
const id = String(++this.reqId);
|
|
305
|
-
this.pending.set(id, { resolve, reject });
|
|
306
|
-
this.transport.send(JSON.stringify({ id, cmd, payload }) + "\n");
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ── Tabs ──────────────────────────────────────────────────────────────────
|
|
311
|
-
async newTab(): Promise<string> { return this.send<string>("tab.new", {}); }
|
|
312
|
-
async closeTab(tabId: string): Promise<void> { await this.send("tab.close", { tabId }); }
|
|
313
|
-
async listTabs(): Promise<string[]> { return this.send<string[]>("tab.list", {}); }
|
|
314
|
-
|
|
315
|
-
// ── Navigation ────────────────────────────────────────────────────────────
|
|
316
|
-
async navigate(url: string, tabId = "default"): Promise<void> { await this.send("navigate", { url, tabId }); }
|
|
317
|
-
async reload(tabId = "default"): Promise<void> { await this.send("reload", { tabId }); }
|
|
318
|
-
async goBack(tabId = "default"): Promise<void> { await this.send("go.back", { tabId }); }
|
|
319
|
-
async goForward(tabId = "default"): Promise<void> { await this.send("go.forward", { tabId }); }
|
|
320
|
-
|
|
321
|
-
// ── Page info ─────────────────────────────────────────────────────────────
|
|
322
|
-
async getTitle(tabId = "default"): Promise<string> { return this.send<string>("page.title", { tabId }); }
|
|
323
|
-
async getUrl(tabId = "default"): Promise<string> { return this.send<string>("page.url", { tabId }); }
|
|
324
|
-
async content(tabId = "default"): Promise<string> { return this.send<string>("page.content", { tabId }); }
|
|
325
|
-
|
|
326
|
-
// ── Eval / JS ─────────────────────────────────────────────────────────────
|
|
327
|
-
async evaluate(js: string, tabId = "default"): Promise<any> { return this.send("evaluate", { js, tabId }); }
|
|
328
|
-
async addInitScript(js: string, tabId = "default"): Promise<void> { await this.send("addInitScript", { js, tabId }); }
|
|
329
|
-
|
|
330
|
-
// ── Interactions ──────────────────────────────────────────────────────────
|
|
331
|
-
async click(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("click", { selector, tabId }); }
|
|
332
|
-
async doubleClick(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("dblclick", { selector, tabId }); }
|
|
333
|
-
async hover(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("hover", { selector, tabId }); }
|
|
334
|
-
async type(selector: string, text: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("type", { selector, text, tabId }); }
|
|
335
|
-
async select(selector: string, value: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("select", { selector, value, tabId }); }
|
|
336
|
-
async keyPress(key: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("keyboard.press", { key, tabId }); }
|
|
337
|
-
async keyCombo(combo: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("keyboard.combo", { combo, tabId }); }
|
|
338
|
-
async mouseMove(x: number, y: number, tabId = "default"): Promise<boolean> { return this.send<boolean>("mouse.move", { x, y, tabId }); }
|
|
339
|
-
async mouseDrag(from: { x: number; y: number }, to: { x: number; y: number }, tabId = "default"): Promise<boolean> { return this.send<boolean>("mouse.drag", { from, to, tabId }); }
|
|
340
|
-
|
|
341
|
-
// ── Scroll ────────────────────────────────────────────────────────────────
|
|
342
|
-
async scrollTo(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("scroll.to", { selector, tabId }); }
|
|
343
|
-
async scrollBy(px: number, tabId = "default"): Promise<boolean> { return this.send<boolean>("scroll.by", { px, tabId }); }
|
|
344
|
-
|
|
345
|
-
// ── Fetch ─────────────────────────────────────────────────────────────────
|
|
346
|
-
async fetchText(query: string, tabId = "default"): Promise<string | null> { return this.send<string | null>("fetch.text", { query, tabId }); }
|
|
347
|
-
async fetchLinks(query: string, tabId = "default"): Promise<string[]> {
|
|
348
|
-
if (query === "a" || query === "body") {
|
|
349
|
-
const result = await this.send<string[]>("fetch.links.all", { tabId });
|
|
350
|
-
return Array.isArray(result) ? result : [];
|
|
351
|
-
}
|
|
352
|
-
const result = await this.send<string[]>("fetch.links", { query, tabId });
|
|
353
|
-
return Array.isArray(result) ? result : [];
|
|
354
|
-
}
|
|
355
|
-
async fetchImages(query: string, tabId = "default"): Promise<string[]> {
|
|
356
|
-
const result = await this.send<string[]>("fetch.image", { query, tabId });
|
|
357
|
-
return Array.isArray(result) ? result : [];
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// ── Search ────────────────────────────────────────────────────────────────
|
|
361
|
-
async searchCss(query: string, tabId = "default"): Promise<any> { return this.send("search.css", { query, tabId }); }
|
|
362
|
-
async searchId(query: string, tabId = "default"): Promise<any> { return this.send("search.id", { query, tabId }); }
|
|
363
|
-
|
|
364
|
-
// ── Wait ──────────────────────────────────────────────────────────────────
|
|
365
|
-
async waitForSelector(selector: string, timeout = 30000, tabId = "default"): Promise<void> { await this.send("wait.selector", { selector, timeout, tabId }); }
|
|
366
|
-
async waitForNavigation(tabId = "default"): Promise<void> { await this.send("wait.navigation", { tabId }); }
|
|
367
|
-
async waitForResponse(urlPattern: string, timeout = 30000, tabId = "default"): Promise<void> { await this.send("wait.response", { url: urlPattern, timeout, tabId }); }
|
|
368
|
-
|
|
369
|
-
// ── Screenshot / PDF ──────────────────────────────────────────────────────
|
|
370
|
-
async screenshot(filePath?: string, tabId = "default"): Promise<string> {
|
|
371
|
-
const b64 = await this.send<string>("screenshot", { tabId });
|
|
372
|
-
if (filePath) { mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, Buffer.from(b64, "base64")); }
|
|
373
|
-
return filePath ?? b64;
|
|
374
|
-
}
|
|
375
|
-
async pdf(filePath?: string, tabId = "default"): Promise<string> {
|
|
376
|
-
const b64 = await this.send<string>("pdf", { tabId });
|
|
377
|
-
if (filePath) { mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, Buffer.from(b64, "base64")); }
|
|
378
|
-
return filePath ?? b64;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// ── Image blocking ────────────────────────────────────────────────────────
|
|
382
|
-
async blockImages(tabId = "default"): Promise<void> { await this.send("intercept.block.images", { tabId }); }
|
|
383
|
-
async unblockImages(tabId = "default"): Promise<void> { await this.send("intercept.unblock.images", { tabId }); }
|
|
384
|
-
|
|
385
|
-
// ── Cookies ───────────────────────────────────────────────────────────────
|
|
386
|
-
async setCookie(name: string, value: string, domain: string, path = "/", tabId = "default"): Promise<void> { await this.send("cookie.set", { name, value, domain, path, tabId }); }
|
|
387
|
-
async getCookie(name: string, tabId = "default"): Promise<any> { return this.send("cookie.get", { name, tabId }); }
|
|
388
|
-
async deleteCookie(name: string, tabId = "default"): Promise<void> { await this.send("cookie.delete", { name, tabId }); }
|
|
389
|
-
async listCookies(tabId = "default"): Promise<any[]> { return this.send<any[]>("cookie.list", { tabId }); }
|
|
390
|
-
|
|
391
|
-
// ── Interception ──────────────────────────────────────────────────────────
|
|
392
|
-
async addInterceptRule(action: "block" | "redirect" | "modifyHeaders", pattern: string, options: { redirectUrl?: string; headers?: Record<string, string> } = {}, tabId = "default"): Promise<void> {
|
|
393
|
-
await this.send("intercept.rule.add", { action, pattern, ...options, tabId });
|
|
394
|
-
}
|
|
395
|
-
async clearInterceptRules(tabId = "default"): Promise<void> { await this.send("intercept.rule.clear", { tabId }); }
|
|
396
|
-
|
|
397
|
-
// ── Network capture ───────────────────────────────────────────────────────
|
|
398
|
-
async captureStart(tabId = "default"): Promise<void> { await this.send("capture.start", { tabId }); }
|
|
399
|
-
async captureStop(tabId = "default"): Promise<void> { await this.send("capture.stop", { tabId }); }
|
|
400
|
-
async captureRequests(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.requests", { tabId }); }
|
|
401
|
-
async captureWs(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.ws", { tabId }); }
|
|
402
|
-
async captureCookies(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.cookies", { tabId }); }
|
|
403
|
-
async captureStorage(tabId = "default"): Promise<any> { return this.send("capture.storage", { tabId }); }
|
|
404
|
-
async captureClear(tabId = "default"): Promise<void> { await this.send("capture.clear", { tabId }); }
|
|
405
|
-
|
|
406
|
-
// ── Session ───────────────────────────────────────────────────────────────
|
|
407
|
-
async sessionExport(tabId = "default"): Promise<any> { return this.send("session.export", { tabId }); }
|
|
408
|
-
async sessionImport(data: any, tabId = "default"): Promise<void> { await this.send("session.import", { data, tabId }); }
|
|
409
|
-
|
|
410
|
-
// ── Session persistence (opt-in) ──────────────────────────────────────────
|
|
411
|
-
// WS frames and pings are NOT saved by default — you must opt in.
|
|
412
|
-
// Files are written to cwd (same folder as cookies.json / profile.json).
|
|
413
|
-
|
|
414
|
-
/** Enable or disable saving WebSocket frames to ws.json in cwd */
|
|
415
|
-
async sessionWsSave(enabled = true): Promise<void> {
|
|
416
|
-
await this.send("session.ws.save", { enabled });
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/** Enable or disable saving ping log to pings.json in cwd */
|
|
420
|
-
async sessionPingsSave(enabled = true): Promise<void> {
|
|
421
|
-
await this.send("session.pings.save", { enabled });
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/** Get all data file paths for the current session */
|
|
425
|
-
async sessionPaths(): Promise<{
|
|
426
|
-
workDir: string;
|
|
427
|
-
cookies: string;
|
|
428
|
-
profile: string;
|
|
429
|
-
ws: string;
|
|
430
|
-
pings: string;
|
|
431
|
-
}> {
|
|
432
|
-
return this.send("session.paths", {});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/** Get path to cookies.json */
|
|
436
|
-
async sessionCookiesPath(): Promise<string> {
|
|
437
|
-
return this.send("session.cookies.path", {});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/** Get path to profile.json */
|
|
441
|
-
async sessionProfilePath(): Promise<string> {
|
|
442
|
-
return this.send("session.profile.path", {});
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/** Get path to ws.json */
|
|
446
|
-
async sessionWsPath(): Promise<string> {
|
|
447
|
-
return this.send("session.ws.path", {});
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/** Get path to pings.json */
|
|
451
|
-
async sessionPingsPath(): Promise<string> {
|
|
452
|
-
return this.send("session.pings.path", {});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/** Reload cookies.json and profile.json from disk without restarting */
|
|
456
|
-
async sessionReload(): Promise<void> {
|
|
457
|
-
await this.send("session.reload", {});
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// ── Expose Function ───────────────────────────────────────────────────────
|
|
461
|
-
async exposeFunction(name: string, handler: (data: any) => Promise<any> | any, tabId = "default"): Promise<void> {
|
|
462
|
-
if (!this.eventHandlers.has(tabId)) this.eventHandlers.set(tabId, new Map());
|
|
463
|
-
this.eventHandlers.get(tabId)!.set(name, async (data: any) => {
|
|
464
|
-
try {
|
|
465
|
-
const result = await handler(data);
|
|
466
|
-
if (result && typeof result === "object" && ("success" in result || "error" in result)) return result;
|
|
467
|
-
return { success: true, result };
|
|
468
|
-
} catch (err: any) {
|
|
469
|
-
return { success: false, error: err.message || String(err) };
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
await this.send("expose.function", { name, tabId });
|
|
473
|
-
logger.success(`[${tabId}] exposed function: ${name}`);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
async unexposeFunction(name: string, tabId = "default"): Promise<void> {
|
|
477
|
-
const handlers = this.eventHandlers.get(tabId);
|
|
478
|
-
if (handlers) handlers.delete(name);
|
|
479
|
-
logger.info(`[${tabId}] unexposed function: ${name}`);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
async clearExposedFunctions(tabId = "default"): Promise<void> {
|
|
483
|
-
this.eventHandlers.set(tabId, new Map());
|
|
484
|
-
logger.info(`[${tabId}] cleared all exposed functions`);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// ── Proxy ─────────────────────────────────────────────────────────────────
|
|
488
|
-
|
|
489
|
-
async proxyLoad(path: string): Promise<void> {
|
|
490
|
-
await this.send("proxy.load", { path });
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
async proxyFetch(url: string): Promise<void> {
|
|
494
|
-
await this.send("proxy.fetch", { url });
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
async proxyOvpn(path: string): Promise<void> {
|
|
498
|
-
await this.send("proxy.ovpn", { path });
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
async proxySet(opts: {
|
|
502
|
-
host?: string;
|
|
503
|
-
port?: number;
|
|
504
|
-
type?: "http" | "https" | "socks5" | "socks4";
|
|
505
|
-
user?: string;
|
|
506
|
-
pass?: string;
|
|
507
|
-
proxy?: string;
|
|
508
|
-
}): Promise<void> {
|
|
509
|
-
await this.send("proxy.set", opts as Record<string, any>);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
async proxyTest(): Promise<void> {
|
|
513
|
-
await this.send("proxy.test", {});
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
async proxyTestStop(): Promise<void> {
|
|
517
|
-
await this.send("proxy.test.stop", {});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
async proxyNext(): Promise<void> {
|
|
521
|
-
await this.send("proxy.next", {});
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
async proxyDisable(): Promise<void> {
|
|
525
|
-
await this.send("proxy.disable", {});
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async proxyEnable(): Promise<void> {
|
|
529
|
-
await this.send("proxy.enable", {});
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
async proxyCurrent(): Promise<{
|
|
533
|
-
host: string; port: number; type: string;
|
|
534
|
-
user?: string; alive: boolean; latencyMs?: number;
|
|
535
|
-
}> {
|
|
536
|
-
return this.send("proxy.current", {});
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
async proxyStats(): Promise<{
|
|
540
|
-
total: number; alive: number; dead: number;
|
|
541
|
-
index: number; checking: boolean;
|
|
542
|
-
}> {
|
|
543
|
-
return this.send("proxy.stats", {});
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
async proxyList(limit?: number): Promise<{
|
|
547
|
-
host: string; port: number; type: string;
|
|
548
|
-
alive: boolean; latencyMs?: number;
|
|
549
|
-
}[]> {
|
|
550
|
-
return this.send("proxy.list", limit !== undefined ? { limit } : {});
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
async proxyRotation(mode: "none" | "timed" | "perrequest", interval?: number): Promise<void> {
|
|
554
|
-
await this.send("proxy.rotation", { mode, ...(interval !== undefined ? { interval } : {}) });
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
async proxyConfig(opts: { skipDead?: boolean; autoCheck?: boolean }): Promise<void> {
|
|
558
|
-
await this.send("proxy.config", opts as Record<string, any>);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
async proxySave(path: string, filter: "alive" | "dead" | "all" = "all"): Promise<void> {
|
|
562
|
-
await this.send("proxy.save", { path, filter });
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
onProxyEvent(event: string, handler: (data: any) => void): () => void {
|
|
566
|
-
return this.onEvent(event, "*", handler);
|
|
567
|
-
}
|
|
1
|
+
// piggy/client/index.ts
|
|
2
|
+
import { connect, type Socket } from "net";
|
|
3
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
4
|
+
import { dirname } from "path";
|
|
5
|
+
import { platform } from "os";
|
|
6
|
+
import logger from "../logger";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_SOCKET_PATH = platform() === "win32"
|
|
9
|
+
? "\\\\.\\pipe\\piggy"
|
|
10
|
+
: "/tmp/piggy";
|
|
11
|
+
|
|
12
|
+
// ── Transport interface ────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
interface Transport {
|
|
15
|
+
send(data: string): void;
|
|
16
|
+
on(event: "data", handler: (chunk: string) => void): void;
|
|
17
|
+
on(event: "error", handler: (e: Error) => void): void;
|
|
18
|
+
on(event: "close", handler: () => void): void;
|
|
19
|
+
destroy(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Socket transport ───────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
class SocketTransport implements Transport {
|
|
25
|
+
private sock: Socket;
|
|
26
|
+
|
|
27
|
+
constructor(sock: Socket) {
|
|
28
|
+
this.sock = sock;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
send(data: string) {
|
|
32
|
+
this.sock.write(data);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
on(event: string, handler: any) {
|
|
36
|
+
this.sock.on(event, handler);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
destroy() {
|
|
40
|
+
this.sock.destroy();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── HTTP transport ─────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
class HttpTransport implements Transport {
|
|
47
|
+
private host: string;
|
|
48
|
+
private key: string;
|
|
49
|
+
private dataHandlers: ((chunk: string) => void)[] = [];
|
|
50
|
+
private errorHandlers: ((e: Error) => void)[] = [];
|
|
51
|
+
private closeHandlers: (() => void)[] = [];
|
|
52
|
+
private _destroyed = false;
|
|
53
|
+
|
|
54
|
+
constructor(host: string, key: string) {
|
|
55
|
+
this.host = host.replace(/\/$/, "");
|
|
56
|
+
this.key = key;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
on(event: string, handler: any) {
|
|
60
|
+
if (event === "data") this.dataHandlers.push(handler);
|
|
61
|
+
if (event === "error") this.errorHandlers.push(handler);
|
|
62
|
+
if (event === "close") this.closeHandlers.push(handler);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
send(data: string) {
|
|
66
|
+
if (this._destroyed) return;
|
|
67
|
+
|
|
68
|
+
fetch(this.host, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
"X-Piggy-Key": this.key,
|
|
73
|
+
},
|
|
74
|
+
body: data,
|
|
75
|
+
})
|
|
76
|
+
.then(async (res) => {
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const text = await res.text().catch(() => `HTTP ${res.status}`);
|
|
79
|
+
this.errorHandlers.forEach(h =>
|
|
80
|
+
h(new Error(`HTTP ${res.status}: ${text}`))
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const text = await res.text();
|
|
85
|
+
const lines = text.split("\n").filter(l => l.trim());
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
this.dataHandlers.forEach(h => h(line + "\n"));
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.catch((e: Error) => {
|
|
91
|
+
if (!this._destroyed) {
|
|
92
|
+
this.errorHandlers.forEach(h => h(e));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
destroy() {
|
|
98
|
+
this._destroyed = true;
|
|
99
|
+
this.closeHandlers.forEach(h => h());
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── PiggyClient ────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export class PiggyClient {
|
|
106
|
+
private socketPath: string;
|
|
107
|
+
private httpHost: string | null = null;
|
|
108
|
+
private httpKey: string | null = null;
|
|
109
|
+
private transport: Transport | null = null;
|
|
110
|
+
private reqId = 0;
|
|
111
|
+
private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
|
|
112
|
+
private buf = "";
|
|
113
|
+
private eventHandlers = new Map<string, Map<string, (data: any) => Promise<any>>>();
|
|
114
|
+
private globalEventHandlers = new Map<string, Set<(data: any) => void>>();
|
|
115
|
+
|
|
116
|
+
constructor(socketPath?: string);
|
|
117
|
+
constructor(opts: { host: string; key: string });
|
|
118
|
+
|
|
119
|
+
constructor(arg?: string | { host: string; key: string }) {
|
|
120
|
+
if (arg && typeof arg === "object") {
|
|
121
|
+
this.socketPath = "";
|
|
122
|
+
this.httpHost = arg.host.replace(/\/$/, "");
|
|
123
|
+
this.httpKey = arg.key;
|
|
124
|
+
} else {
|
|
125
|
+
this.socketPath = (arg as string | undefined) ?? DEFAULT_SOCKET_PATH;
|
|
126
|
+
}
|
|
127
|
+
this.eventHandlers.set("default", new Map());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Connect ───────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
connect(): Promise<void> {
|
|
133
|
+
if (this.httpHost) return this._connectHttp();
|
|
134
|
+
return this._connectSocket();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private _connectSocket(): Promise<void> {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
logger.info(`Connecting to socket: ${this.socketPath}`);
|
|
140
|
+
const sock = connect(this.socketPath);
|
|
141
|
+
sock.setEncoding("utf8");
|
|
142
|
+
|
|
143
|
+
sock.on("connect", () => {
|
|
144
|
+
this.transport = new SocketTransport(sock);
|
|
145
|
+
this._wireTransport();
|
|
146
|
+
logger.success("Connected to Piggy server (socket)");
|
|
147
|
+
resolve();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
sock.on("error", (e) => {
|
|
151
|
+
for (const p of this.pending.values()) p.reject(e);
|
|
152
|
+
this.pending.clear();
|
|
153
|
+
reject(e);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async _connectHttp(): Promise<void> {
|
|
159
|
+
logger.info(`Connecting to Piggy server (HTTP): ${this.httpHost}`);
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetch(this.httpHost!, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
"X-Piggy-Key": this.httpKey!,
|
|
166
|
+
},
|
|
167
|
+
body: "hello",
|
|
168
|
+
});
|
|
169
|
+
if (res.status === 401) {
|
|
170
|
+
throw new Error("Unauthorized — invalid X-Piggy-Key");
|
|
171
|
+
}
|
|
172
|
+
this.transport = new HttpTransport(this.httpHost!, this.httpKey!);
|
|
173
|
+
this._wireTransport();
|
|
174
|
+
logger.success(`Connected to Piggy server (HTTP): ${this.httpHost}`);
|
|
175
|
+
} catch (e: any) {
|
|
176
|
+
throw new Error(`Failed to connect to Piggy HTTP server: ${e.message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private _wireTransport() {
|
|
181
|
+
if (!this.transport) return;
|
|
182
|
+
|
|
183
|
+
this.transport.on("data", (chunk: string) => {
|
|
184
|
+
this.buf += chunk;
|
|
185
|
+
const lines = this.buf.split("\n");
|
|
186
|
+
this.buf = lines.pop()!;
|
|
187
|
+
|
|
188
|
+
for (const line of lines) {
|
|
189
|
+
if (!line.trim()) continue;
|
|
190
|
+
try {
|
|
191
|
+
const msg = JSON.parse(line);
|
|
192
|
+
|
|
193
|
+
if (msg.type === "event") {
|
|
194
|
+
this.handleEvent(msg);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const p = this.pending.get(msg.id);
|
|
199
|
+
if (p) {
|
|
200
|
+
this.pending.delete(msg.id);
|
|
201
|
+
msg.ok ? p.resolve(msg.data) : p.reject(new Error(msg.data ?? "command failed"));
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
logger.error(`Bad JSON from server: ${line}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
this.transport.on("error", (e: Error) => {
|
|
210
|
+
for (const p of this.pending.values()) p.reject(e);
|
|
211
|
+
this.pending.clear();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
this.transport.on("close", () => {
|
|
215
|
+
for (const p of this.pending.values()) p.reject(new Error("Connection closed"));
|
|
216
|
+
this.pending.clear();
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Event handling ────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
private handleEvent(event: any) {
|
|
223
|
+
if (event.event === "exposed_call") {
|
|
224
|
+
const { tabId, name, callId, data } = event;
|
|
225
|
+
const effectiveTabId = tabId || "default";
|
|
226
|
+
const handlers = this.eventHandlers.get(effectiveTabId);
|
|
227
|
+
const handler = handlers?.get(name);
|
|
228
|
+
|
|
229
|
+
if (handler) {
|
|
230
|
+
let parsedData: any;
|
|
231
|
+
try {
|
|
232
|
+
parsedData = JSON.parse(data || "null");
|
|
233
|
+
} catch {
|
|
234
|
+
parsedData = data;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
Promise.resolve(handler(parsedData))
|
|
238
|
+
.then(response => {
|
|
239
|
+
if (response && typeof response === "object" && "success" in response) {
|
|
240
|
+
this.send("exposed.result", {
|
|
241
|
+
tabId: effectiveTabId,
|
|
242
|
+
callId,
|
|
243
|
+
result: response.success ? JSON.stringify(response.result) : (response.error || "Unknown error"),
|
|
244
|
+
isError: !response.success,
|
|
245
|
+
}).catch(e => logger.error(`Failed to send exposed result: ${e}`));
|
|
246
|
+
} else {
|
|
247
|
+
this.send("exposed.result", {
|
|
248
|
+
tabId: effectiveTabId,
|
|
249
|
+
callId,
|
|
250
|
+
result: JSON.stringify(response),
|
|
251
|
+
isError: false,
|
|
252
|
+
}).catch(e => logger.error(`Failed to send exposed result: ${e}`));
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
.catch(err => {
|
|
256
|
+
this.send("exposed.result", {
|
|
257
|
+
tabId: effectiveTabId,
|
|
258
|
+
callId,
|
|
259
|
+
result: err.message || "Handler error",
|
|
260
|
+
isError: true,
|
|
261
|
+
}).catch(e => logger.error(`Failed to send exposed error: ${e}`));
|
|
262
|
+
});
|
|
263
|
+
} else {
|
|
264
|
+
logger.warn(`No handler for exposed function: ${name} in tab ${effectiveTabId}`);
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (event.event === "navigate") {
|
|
270
|
+
const handlers = this.globalEventHandlers.get(`navigate:${event.tabId}`);
|
|
271
|
+
if (handlers) {
|
|
272
|
+
for (const h of handlers) {
|
|
273
|
+
try { h(event.url); } catch (e) { logger.error(`navigate handler error: ${e}`); }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const wildcard = this.globalEventHandlers.get("navigate:*");
|
|
277
|
+
if (wildcard) {
|
|
278
|
+
for (const h of wildcard) {
|
|
279
|
+
try { h({ url: event.url, tabId: event.tabId }); } catch {}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
onEvent(eventName: string, tabId: string, handler: (data: any) => void): () => void {
|
|
286
|
+
const key = `${eventName}:${tabId}`;
|
|
287
|
+
if (!this.globalEventHandlers.has(key)) {
|
|
288
|
+
this.globalEventHandlers.set(key, new Set());
|
|
289
|
+
}
|
|
290
|
+
this.globalEventHandlers.get(key)!.add(handler);
|
|
291
|
+
return () => this.globalEventHandlers.get(key)?.delete(handler);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
disconnect() {
|
|
295
|
+
this.transport?.destroy();
|
|
296
|
+
this.transport = null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── Core send ─────────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
send<T = any>(cmd: string, payload: Record<string, any> = {}): Promise<T> {
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
if (!this.transport) return reject(new Error("Not connected"));
|
|
304
|
+
const id = String(++this.reqId);
|
|
305
|
+
this.pending.set(id, { resolve, reject });
|
|
306
|
+
this.transport.send(JSON.stringify({ id, cmd, payload }) + "\n");
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Tabs ──────────────────────────────────────────────────────────────────
|
|
311
|
+
async newTab(): Promise<string> { return this.send<string>("tab.new", {}); }
|
|
312
|
+
async closeTab(tabId: string): Promise<void> { await this.send("tab.close", { tabId }); }
|
|
313
|
+
async listTabs(): Promise<string[]> { return this.send<string[]>("tab.list", {}); }
|
|
314
|
+
|
|
315
|
+
// ── Navigation ────────────────────────────────────────────────────────────
|
|
316
|
+
async navigate(url: string, tabId = "default"): Promise<void> { await this.send("navigate", { url, tabId }); }
|
|
317
|
+
async reload(tabId = "default"): Promise<void> { await this.send("reload", { tabId }); }
|
|
318
|
+
async goBack(tabId = "default"): Promise<void> { await this.send("go.back", { tabId }); }
|
|
319
|
+
async goForward(tabId = "default"): Promise<void> { await this.send("go.forward", { tabId }); }
|
|
320
|
+
|
|
321
|
+
// ── Page info ─────────────────────────────────────────────────────────────
|
|
322
|
+
async getTitle(tabId = "default"): Promise<string> { return this.send<string>("page.title", { tabId }); }
|
|
323
|
+
async getUrl(tabId = "default"): Promise<string> { return this.send<string>("page.url", { tabId }); }
|
|
324
|
+
async content(tabId = "default"): Promise<string> { return this.send<string>("page.content", { tabId }); }
|
|
325
|
+
|
|
326
|
+
// ── Eval / JS ─────────────────────────────────────────────────────────────
|
|
327
|
+
async evaluate(js: string, tabId = "default"): Promise<any> { return this.send("evaluate", { js, tabId }); }
|
|
328
|
+
async addInitScript(js: string, tabId = "default"): Promise<void> { await this.send("addInitScript", { js, tabId }); }
|
|
329
|
+
|
|
330
|
+
// ── Interactions ──────────────────────────────────────────────────────────
|
|
331
|
+
async click(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("click", { selector, tabId }); }
|
|
332
|
+
async doubleClick(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("dblclick", { selector, tabId }); }
|
|
333
|
+
async hover(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("hover", { selector, tabId }); }
|
|
334
|
+
async type(selector: string, text: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("type", { selector, text, tabId }); }
|
|
335
|
+
async select(selector: string, value: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("select", { selector, value, tabId }); }
|
|
336
|
+
async keyPress(key: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("keyboard.press", { key, tabId }); }
|
|
337
|
+
async keyCombo(combo: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("keyboard.combo", { combo, tabId }); }
|
|
338
|
+
async mouseMove(x: number, y: number, tabId = "default"): Promise<boolean> { return this.send<boolean>("mouse.move", { x, y, tabId }); }
|
|
339
|
+
async mouseDrag(from: { x: number; y: number }, to: { x: number; y: number }, tabId = "default"): Promise<boolean> { return this.send<boolean>("mouse.drag", { from, to, tabId }); }
|
|
340
|
+
|
|
341
|
+
// ── Scroll ────────────────────────────────────────────────────────────────
|
|
342
|
+
async scrollTo(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("scroll.to", { selector, tabId }); }
|
|
343
|
+
async scrollBy(px: number, tabId = "default"): Promise<boolean> { return this.send<boolean>("scroll.by", { px, tabId }); }
|
|
344
|
+
|
|
345
|
+
// ── Fetch ─────────────────────────────────────────────────────────────────
|
|
346
|
+
async fetchText(query: string, tabId = "default"): Promise<string | null> { return this.send<string | null>("fetch.text", { query, tabId }); }
|
|
347
|
+
async fetchLinks(query: string, tabId = "default"): Promise<string[]> {
|
|
348
|
+
if (query === "a" || query === "body") {
|
|
349
|
+
const result = await this.send<string[]>("fetch.links.all", { tabId });
|
|
350
|
+
return Array.isArray(result) ? result : [];
|
|
351
|
+
}
|
|
352
|
+
const result = await this.send<string[]>("fetch.links", { query, tabId });
|
|
353
|
+
return Array.isArray(result) ? result : [];
|
|
354
|
+
}
|
|
355
|
+
async fetchImages(query: string, tabId = "default"): Promise<string[]> {
|
|
356
|
+
const result = await this.send<string[]>("fetch.image", { query, tabId });
|
|
357
|
+
return Array.isArray(result) ? result : [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Search ────────────────────────────────────────────────────────────────
|
|
361
|
+
async searchCss(query: string, tabId = "default"): Promise<any> { return this.send("search.css", { query, tabId }); }
|
|
362
|
+
async searchId(query: string, tabId = "default"): Promise<any> { return this.send("search.id", { query, tabId }); }
|
|
363
|
+
|
|
364
|
+
// ── Wait ──────────────────────────────────────────────────────────────────
|
|
365
|
+
async waitForSelector(selector: string, timeout = 30000, tabId = "default"): Promise<void> { await this.send("wait.selector", { selector, timeout, tabId }); }
|
|
366
|
+
async waitForNavigation(tabId = "default"): Promise<void> { await this.send("wait.navigation", { tabId }); }
|
|
367
|
+
async waitForResponse(urlPattern: string, timeout = 30000, tabId = "default"): Promise<void> { await this.send("wait.response", { url: urlPattern, timeout, tabId }); }
|
|
368
|
+
|
|
369
|
+
// ── Screenshot / PDF ──────────────────────────────────────────────────────
|
|
370
|
+
async screenshot(filePath?: string, tabId = "default"): Promise<string> {
|
|
371
|
+
const b64 = await this.send<string>("screenshot", { tabId });
|
|
372
|
+
if (filePath) { mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, Buffer.from(b64, "base64")); }
|
|
373
|
+
return filePath ?? b64;
|
|
374
|
+
}
|
|
375
|
+
async pdf(filePath?: string, tabId = "default"): Promise<string> {
|
|
376
|
+
const b64 = await this.send<string>("pdf", { tabId });
|
|
377
|
+
if (filePath) { mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, Buffer.from(b64, "base64")); }
|
|
378
|
+
return filePath ?? b64;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── Image blocking ────────────────────────────────────────────────────────
|
|
382
|
+
async blockImages(tabId = "default"): Promise<void> { await this.send("intercept.block.images", { tabId }); }
|
|
383
|
+
async unblockImages(tabId = "default"): Promise<void> { await this.send("intercept.unblock.images", { tabId }); }
|
|
384
|
+
|
|
385
|
+
// ── Cookies ───────────────────────────────────────────────────────────────
|
|
386
|
+
async setCookie(name: string, value: string, domain: string, path = "/", tabId = "default"): Promise<void> { await this.send("cookie.set", { name, value, domain, path, tabId }); }
|
|
387
|
+
async getCookie(name: string, tabId = "default"): Promise<any> { return this.send("cookie.get", { name, tabId }); }
|
|
388
|
+
async deleteCookie(name: string, tabId = "default"): Promise<void> { await this.send("cookie.delete", { name, tabId }); }
|
|
389
|
+
async listCookies(tabId = "default"): Promise<any[]> { return this.send<any[]>("cookie.list", { tabId }); }
|
|
390
|
+
|
|
391
|
+
// ── Interception ──────────────────────────────────────────────────────────
|
|
392
|
+
async addInterceptRule(action: "block" | "redirect" | "modifyHeaders", pattern: string, options: { redirectUrl?: string; headers?: Record<string, string> } = {}, tabId = "default"): Promise<void> {
|
|
393
|
+
await this.send("intercept.rule.add", { action, pattern, ...options, tabId });
|
|
394
|
+
}
|
|
395
|
+
async clearInterceptRules(tabId = "default"): Promise<void> { await this.send("intercept.rule.clear", { tabId }); }
|
|
396
|
+
|
|
397
|
+
// ── Network capture ───────────────────────────────────────────────────────
|
|
398
|
+
async captureStart(tabId = "default"): Promise<void> { await this.send("capture.start", { tabId }); }
|
|
399
|
+
async captureStop(tabId = "default"): Promise<void> { await this.send("capture.stop", { tabId }); }
|
|
400
|
+
async captureRequests(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.requests", { tabId }); }
|
|
401
|
+
async captureWs(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.ws", { tabId }); }
|
|
402
|
+
async captureCookies(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.cookies", { tabId }); }
|
|
403
|
+
async captureStorage(tabId = "default"): Promise<any> { return this.send("capture.storage", { tabId }); }
|
|
404
|
+
async captureClear(tabId = "default"): Promise<void> { await this.send("capture.clear", { tabId }); }
|
|
405
|
+
|
|
406
|
+
// ── Session ───────────────────────────────────────────────────────────────
|
|
407
|
+
async sessionExport(tabId = "default"): Promise<any> { return this.send("session.export", { tabId }); }
|
|
408
|
+
async sessionImport(data: any, tabId = "default"): Promise<void> { await this.send("session.import", { data, tabId }); }
|
|
409
|
+
|
|
410
|
+
// ── Session persistence (opt-in) ──────────────────────────────────────────
|
|
411
|
+
// WS frames and pings are NOT saved by default — you must opt in.
|
|
412
|
+
// Files are written to cwd (same folder as cookies.json / profile.json).
|
|
413
|
+
|
|
414
|
+
/** Enable or disable saving WebSocket frames to ws.json in cwd */
|
|
415
|
+
async sessionWsSave(enabled = true): Promise<void> {
|
|
416
|
+
await this.send("session.ws.save", { enabled });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Enable or disable saving ping log to pings.json in cwd */
|
|
420
|
+
async sessionPingsSave(enabled = true): Promise<void> {
|
|
421
|
+
await this.send("session.pings.save", { enabled });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Get all data file paths for the current session */
|
|
425
|
+
async sessionPaths(): Promise<{
|
|
426
|
+
workDir: string;
|
|
427
|
+
cookies: string;
|
|
428
|
+
profile: string;
|
|
429
|
+
ws: string;
|
|
430
|
+
pings: string;
|
|
431
|
+
}> {
|
|
432
|
+
return this.send("session.paths", {});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Get path to cookies.json */
|
|
436
|
+
async sessionCookiesPath(): Promise<string> {
|
|
437
|
+
return this.send("session.cookies.path", {});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Get path to profile.json */
|
|
441
|
+
async sessionProfilePath(): Promise<string> {
|
|
442
|
+
return this.send("session.profile.path", {});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Get path to ws.json */
|
|
446
|
+
async sessionWsPath(): Promise<string> {
|
|
447
|
+
return this.send("session.ws.path", {});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Get path to pings.json */
|
|
451
|
+
async sessionPingsPath(): Promise<string> {
|
|
452
|
+
return this.send("session.pings.path", {});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Reload cookies.json and profile.json from disk without restarting */
|
|
456
|
+
async sessionReload(): Promise<void> {
|
|
457
|
+
await this.send("session.reload", {});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ── Expose Function ───────────────────────────────────────────────────────
|
|
461
|
+
async exposeFunction(name: string, handler: (data: any) => Promise<any> | any, tabId = "default"): Promise<void> {
|
|
462
|
+
if (!this.eventHandlers.has(tabId)) this.eventHandlers.set(tabId, new Map());
|
|
463
|
+
this.eventHandlers.get(tabId)!.set(name, async (data: any) => {
|
|
464
|
+
try {
|
|
465
|
+
const result = await handler(data);
|
|
466
|
+
if (result && typeof result === "object" && ("success" in result || "error" in result)) return result;
|
|
467
|
+
return { success: true, result };
|
|
468
|
+
} catch (err: any) {
|
|
469
|
+
return { success: false, error: err.message || String(err) };
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
await this.send("expose.function", { name, tabId });
|
|
473
|
+
logger.success(`[${tabId}] exposed function: ${name}`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async unexposeFunction(name: string, tabId = "default"): Promise<void> {
|
|
477
|
+
const handlers = this.eventHandlers.get(tabId);
|
|
478
|
+
if (handlers) handlers.delete(name);
|
|
479
|
+
logger.info(`[${tabId}] unexposed function: ${name}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async clearExposedFunctions(tabId = "default"): Promise<void> {
|
|
483
|
+
this.eventHandlers.set(tabId, new Map());
|
|
484
|
+
logger.info(`[${tabId}] cleared all exposed functions`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── Proxy ─────────────────────────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
async proxyLoad(path: string): Promise<void> {
|
|
490
|
+
await this.send("proxy.load", { path });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async proxyFetch(url: string): Promise<void> {
|
|
494
|
+
await this.send("proxy.fetch", { url });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async proxyOvpn(path: string): Promise<void> {
|
|
498
|
+
await this.send("proxy.ovpn", { path });
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async proxySet(opts: {
|
|
502
|
+
host?: string;
|
|
503
|
+
port?: number;
|
|
504
|
+
type?: "http" | "https" | "socks5" | "socks4";
|
|
505
|
+
user?: string;
|
|
506
|
+
pass?: string;
|
|
507
|
+
proxy?: string;
|
|
508
|
+
}): Promise<void> {
|
|
509
|
+
await this.send("proxy.set", opts as Record<string, any>);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async proxyTest(): Promise<void> {
|
|
513
|
+
await this.send("proxy.test", {});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async proxyTestStop(): Promise<void> {
|
|
517
|
+
await this.send("proxy.test.stop", {});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async proxyNext(): Promise<void> {
|
|
521
|
+
await this.send("proxy.next", {});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async proxyDisable(): Promise<void> {
|
|
525
|
+
await this.send("proxy.disable", {});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async proxyEnable(): Promise<void> {
|
|
529
|
+
await this.send("proxy.enable", {});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async proxyCurrent(): Promise<{
|
|
533
|
+
host: string; port: number; type: string;
|
|
534
|
+
user?: string; alive: boolean; latencyMs?: number;
|
|
535
|
+
}> {
|
|
536
|
+
return this.send("proxy.current", {});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async proxyStats(): Promise<{
|
|
540
|
+
total: number; alive: number; dead: number;
|
|
541
|
+
index: number; checking: boolean;
|
|
542
|
+
}> {
|
|
543
|
+
return this.send("proxy.stats", {});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async proxyList(limit?: number): Promise<{
|
|
547
|
+
host: string; port: number; type: string;
|
|
548
|
+
alive: boolean; latencyMs?: number;
|
|
549
|
+
}[]> {
|
|
550
|
+
return this.send("proxy.list", limit !== undefined ? { limit } : {});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async proxyRotation(mode: "none" | "timed" | "perrequest", interval?: number): Promise<void> {
|
|
554
|
+
await this.send("proxy.rotation", { mode, ...(interval !== undefined ? { interval } : {}) });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async proxyConfig(opts: { skipDead?: boolean; autoCheck?: boolean }): Promise<void> {
|
|
558
|
+
await this.send("proxy.config", opts as Record<string, any>);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async proxySave(path: string, filter: "alive" | "dead" | "all" = "all"): Promise<void> {
|
|
562
|
+
await this.send("proxy.save", { path, filter });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
onProxyEvent(event: string, handler: (data: any) => void): () => void {
|
|
566
|
+
return this.onEvent(event, "*", handler);
|
|
567
|
+
}
|
|
568
568
|
}
|