peep-proxy 0.1.0 → 0.2.0

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 CHANGED
@@ -158,5 +158,5 @@ export default function App({ store, port, onQuit }) {
158
158
  if (quitting) {
159
159
  return (_jsxs(Box, { flexDirection: "column", height: rows, paddingTop: 1, paddingLeft: 2, children: [_jsx(Text, { bold: true, children: "Shutting down\u2026" }), quitSteps.map((msg) => (_jsxs(Text, { dimColor: true, children: [" ", msg] }, msg)))] }));
160
160
  }
161
- return (_jsxs(Box, { flexDirection: "column", height: rows, children: [modalOpen && (_jsx(Box, { position: "absolute", marginTop: Math.max(0, Math.floor((rows - 8) / 2)), marginLeft: Math.max(0, Math.floor((columns - 22) / 2)), children: _jsx(SortModal, { sortConfig: sortConfig, onSelect: selectColumn, onClose: closeModal }) })), _jsxs(Box, { flexDirection: "row", height: available, children: [_jsx(DomainSidebar, { items: visibleItems, selectedIndex: sidebarSelectedIndex, scrollOffset: sidebarScrollOffset, viewportHeight: sidebarViewportHeight, width: SIDEBAR_WIDTH, height: available, isActive: activePanel === "sidebar" }), _jsxs(Box, { flexDirection: "column", width: contentWidth, children: [_jsx(RequestList, { entries: sortedEntries, selectedIndex: selectedIndex, scrollOffset: scrollOffset, viewportHeight: listViewportHeight, sortConfig: sortConfig, columnWidths: columnWidths, width: contentWidth, height: listHeight, isActive: activePanel === "list" }), selectedEntry && detailHeight > 0 && (_jsx(DetailPanel, { entry: selectedEntry, activePanel: activePanel, requestTab: requestTab, responseTab: responseTab, width: contentWidth, height: detailHeight }))] })] }), _jsx(StatusBar, { port: port, requestCount: sortedEntries.length, selectedIndex: selectedIndex, columns: columns, activePanel: activePanel, notification: notification })] }));
161
+ return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "row", height: available, children: [_jsx(DomainSidebar, { items: visibleItems, selectedIndex: sidebarSelectedIndex, scrollOffset: sidebarScrollOffset, viewportHeight: sidebarViewportHeight, width: SIDEBAR_WIDTH, height: available, isActive: activePanel === "sidebar" }), modalOpen ? (_jsx(Box, { width: contentWidth, height: available, alignItems: "center", justifyContent: "center", children: _jsx(SortModal, { sortConfig: sortConfig, onSelect: selectColumn, onClose: closeModal }) })) : (_jsxs(Box, { flexDirection: "column", width: contentWidth, children: [_jsx(RequestList, { entries: sortedEntries, selectedIndex: selectedIndex, scrollOffset: scrollOffset, viewportHeight: listViewportHeight, sortConfig: sortConfig, columnWidths: columnWidths, width: contentWidth, height: listHeight, isActive: activePanel === "list" }), selectedEntry && detailHeight > 0 && (_jsx(DetailPanel, { entry: selectedEntry, activePanel: activePanel, requestTab: requestTab, responseTab: responseTab, width: contentWidth, height: detailHeight }))] }))] }), _jsx(StatusBar, { port: port, requestCount: sortedEntries.length, selectedIndex: selectedIndex, columns: columns, activePanel: activePanel, notification: notification })] }));
162
162
  }
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { readFileSync } from "node:fs";
3
4
  import * as os from "node:os";
4
5
  import * as path from "node:path";
5
6
  import { render } from "ink";
@@ -14,10 +15,14 @@ const cli = meow(`
14
15
  $ peep
15
16
 
16
17
  Options
17
- --port Proxy port (default: 8080)
18
+ --port Proxy port (default: 8080)
19
+ --upstream-proxy Upstream proxy URL
20
+ --ca-cert Path to additional CA certificate (PEM)
18
21
 
19
22
  Examples
20
23
  $ peep --port=3128
24
+ $ peep --upstream-proxy=http://proxy:8080
25
+ $ peep --ca-cert=/path/to/zscaler-ca.pem
21
26
  `, {
22
27
  importMeta: import.meta,
23
28
  flags: {
@@ -25,6 +30,12 @@ const cli = meow(`
25
30
  type: "number",
26
31
  default: 8080,
27
32
  },
33
+ upstreamProxy: {
34
+ type: "string",
35
+ },
36
+ caCert: {
37
+ type: "string",
38
+ },
28
39
  },
29
40
  });
30
41
  const port = cli.flags.port;
@@ -61,7 +72,25 @@ if (findNssProfiles().length > 0 && !isNssTrusted()) {
61
72
  break;
62
73
  }
63
74
  }
64
- const proxy = new ProxyServer({ port, ca });
75
+ const upstreamProxy = cli.flags.upstreamProxy
76
+ ? new URL(cli.flags.upstreamProxy)
77
+ : undefined;
78
+ const extraCaCerts = [];
79
+ if (cli.flags.caCert) {
80
+ try {
81
+ extraCaCerts.push(readFileSync(cli.flags.caCert, "utf-8"));
82
+ }
83
+ catch {
84
+ process.stderr.write(`Failed to read CA certificate: ${cli.flags.caCert}\n`);
85
+ process.exit(1);
86
+ }
87
+ }
88
+ const proxy = new ProxyServer({
89
+ port,
90
+ ca,
91
+ upstreamProxy,
92
+ extraCaCerts: extraCaCerts.length > 0 ? extraCaCerts : undefined,
93
+ });
65
94
  const store = new TrafficStore(proxy);
66
95
  await proxy.start();
67
96
  const proxyService = enableSystemProxy(port);
@@ -1,3 +1,4 @@
1
1
  export { ProxyServer } from "./proxy-server.js";
2
2
  export { loadOrCreateCA } from "./ca.js";
3
+ export { connectThroughProxy, getUpstreamProxy, shouldBypass, } from "./upstream.js";
3
4
  export type { CaConfig, ProxyConfig, ProxyConnectEvent, ProxyErrorEvent, ProxyEventMap, ProxyRequestEvent, ProxyResponseEvent, RequestId, } from "./types.js";
@@ -1,2 +1,3 @@
1
1
  export { ProxyServer } from "./proxy-server.js";
2
2
  export { loadOrCreateCA } from "./ca.js";
3
+ export { connectThroughProxy, getUpstreamProxy, shouldBypass, } from "./upstream.js";
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _ProxyServer_instances, _ProxyServer_config, _ProxyServer_server, _ProxyServer_emitter, _ProxyServer_certCache, _ProxyServer_sockets, _ProxyServer_tunnelSockets, _ProxyServer_mitmServers, _ProxyServer_handleRequest, _ProxyServer_handleConnect, _ProxyServer_handleTunnel, _ProxyServer_handleMitm;
12
+ var _ProxyServer_instances, _ProxyServer_config, _ProxyServer_server, _ProxyServer_emitter, _ProxyServer_upstream, _ProxyServer_certCache, _ProxyServer_sockets, _ProxyServer_tunnelSockets, _ProxyServer_mitmServers, _ProxyServer_handleRequest, _ProxyServer_handleConnect, _ProxyServer_handleTunnel, _ProxyServer_handleMitm;
13
13
  import { randomUUID } from "node:crypto";
14
14
  import * as http from "node:http";
15
15
  import * as https from "node:https";
@@ -17,12 +17,14 @@ import * as net from "node:net";
17
17
  import * as tls from "node:tls";
18
18
  import { EventEmitter } from "node:events";
19
19
  import { generateHostCert } from "./ca.js";
20
+ import { connectThroughProxy, getUpstreamProxy, shouldBypass, } from "./upstream.js";
20
21
  export class ProxyServer {
21
22
  constructor(config) {
22
23
  _ProxyServer_instances.add(this);
23
24
  _ProxyServer_config.set(this, void 0);
24
25
  _ProxyServer_server.set(this, void 0);
25
26
  _ProxyServer_emitter.set(this, void 0);
27
+ _ProxyServer_upstream.set(this, void 0);
26
28
  _ProxyServer_certCache.set(this, new Map());
27
29
  _ProxyServer_sockets.set(this, new Set());
28
30
  _ProxyServer_tunnelSockets.set(this, new Set());
@@ -56,13 +58,23 @@ export class ProxyServer {
56
58
  timestamp,
57
59
  };
58
60
  __classPrivateFieldGet(this, _ProxyServer_emitter, "f").emit("request", requestEvent);
59
- const upstreamOptions = {
60
- hostname: parsed.hostname,
61
- port: parsed.port || 80,
62
- path: parsed.pathname + parsed.search,
63
- method: clientReq.method,
64
- headers: clientReq.headers,
65
- };
61
+ const upstreamHttp = __classPrivateFieldGet(this, _ProxyServer_upstream, "f").http;
62
+ const useUpstream = upstreamHttp && !shouldBypass(parsed.hostname);
63
+ const upstreamOptions = useUpstream
64
+ ? {
65
+ hostname: upstreamHttp.hostname,
66
+ port: Number(upstreamHttp.port) || 80,
67
+ path: clientReq.url,
68
+ method: clientReq.method,
69
+ headers: clientReq.headers,
70
+ }
71
+ : {
72
+ hostname: parsed.hostname,
73
+ port: parsed.port || 80,
74
+ path: parsed.pathname + parsed.search,
75
+ method: clientReq.method,
76
+ headers: clientReq.headers,
77
+ };
66
78
  const upstreamReq = http.request(upstreamOptions, (upstreamRes) => {
67
79
  const chunks = [];
68
80
  upstreamRes.on("data", (chunk) => {
@@ -103,6 +115,7 @@ export class ProxyServer {
103
115
  }
104
116
  });
105
117
  __classPrivateFieldSet(this, _ProxyServer_config, config, "f");
118
+ __classPrivateFieldSet(this, _ProxyServer_upstream, getUpstreamProxy(config.upstreamProxy), "f");
106
119
  __classPrivateFieldSet(this, _ProxyServer_emitter, new EventEmitter(), "f");
107
120
  __classPrivateFieldSet(this, _ProxyServer_server, http.createServer(__classPrivateFieldGet(this, _ProxyServer_handleRequest, "f")), "f");
108
121
  __classPrivateFieldGet(this, _ProxyServer_server, "f").on("connect", __classPrivateFieldGet(this, _ProxyServer_handleConnect, "f"));
@@ -152,7 +165,7 @@ export class ProxyServer {
152
165
  __classPrivateFieldGet(this, _ProxyServer_emitter, "f").off(event, listener);
153
166
  }
154
167
  }
155
- _ProxyServer_config = new WeakMap(), _ProxyServer_server = new WeakMap(), _ProxyServer_emitter = new WeakMap(), _ProxyServer_certCache = new WeakMap(), _ProxyServer_sockets = new WeakMap(), _ProxyServer_tunnelSockets = new WeakMap(), _ProxyServer_mitmServers = new WeakMap(), _ProxyServer_handleRequest = new WeakMap(), _ProxyServer_handleConnect = new WeakMap(), _ProxyServer_instances = new WeakSet(), _ProxyServer_handleTunnel = function _ProxyServer_handleTunnel(host, port, clientSocket, head) {
168
+ _ProxyServer_config = new WeakMap(), _ProxyServer_server = new WeakMap(), _ProxyServer_emitter = new WeakMap(), _ProxyServer_upstream = new WeakMap(), _ProxyServer_certCache = new WeakMap(), _ProxyServer_sockets = new WeakMap(), _ProxyServer_tunnelSockets = new WeakMap(), _ProxyServer_mitmServers = new WeakMap(), _ProxyServer_handleRequest = new WeakMap(), _ProxyServer_handleConnect = new WeakMap(), _ProxyServer_instances = new WeakSet(), _ProxyServer_handleTunnel = function _ProxyServer_handleTunnel(host, port, clientSocket, head) {
156
169
  const id = randomUUID();
157
170
  __classPrivateFieldGet(this, _ProxyServer_emitter, "f").emit("connect", {
158
171
  id,
@@ -160,6 +173,39 @@ _ProxyServer_config = new WeakMap(), _ProxyServer_server = new WeakMap(), _Proxy
160
173
  port,
161
174
  timestamp: Date.now(),
162
175
  });
176
+ const upstreamHttps = __classPrivateFieldGet(this, _ProxyServer_upstream, "f").https;
177
+ const useUpstream = upstreamHttps && !shouldBypass(host);
178
+ if (useUpstream) {
179
+ connectThroughProxy(upstreamHttps, host, port).then((upstreamSocket) => {
180
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
181
+ if (head.length > 0) {
182
+ upstreamSocket.write(head);
183
+ }
184
+ upstreamSocket.pipe(clientSocket);
185
+ clientSocket.pipe(upstreamSocket);
186
+ __classPrivateFieldGet(this, _ProxyServer_tunnelSockets, "f").add(upstreamSocket);
187
+ upstreamSocket.once("close", () => __classPrivateFieldGet(this, _ProxyServer_tunnelSockets, "f").delete(upstreamSocket));
188
+ upstreamSocket.on("error", (error) => {
189
+ __classPrivateFieldGet(this, _ProxyServer_emitter, "f").emit("error", {
190
+ id,
191
+ error,
192
+ phase: "connect",
193
+ });
194
+ clientSocket.end();
195
+ });
196
+ clientSocket.on("error", () => {
197
+ upstreamSocket.end();
198
+ });
199
+ }, (error) => {
200
+ __classPrivateFieldGet(this, _ProxyServer_emitter, "f").emit("error", {
201
+ id,
202
+ error: error,
203
+ phase: "connect",
204
+ });
205
+ clientSocket.end();
206
+ });
207
+ return;
208
+ }
163
209
  const upstreamSocket = net.connect(port, host, () => {
164
210
  clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
165
211
  if (head.length > 0) {
@@ -202,7 +248,7 @@ _ProxyServer_config = new WeakMap(), _ProxyServer_server = new WeakMap(), _Proxy
202
248
  clientReq.on("data", (chunk) => {
203
249
  reqChunks.push(chunk);
204
250
  });
205
- clientReq.on("end", () => {
251
+ clientReq.on("end", async () => {
206
252
  const reqBody = Buffer.concat(reqChunks);
207
253
  const requestEvent = {
208
254
  id,
@@ -222,6 +268,41 @@ _ProxyServer_config = new WeakMap(), _ProxyServer_server = new WeakMap(), _Proxy
222
268
  method: clientReq.method,
223
269
  headers: { ...clientReq.headers, host },
224
270
  };
271
+ const upstreamHttps = __classPrivateFieldGet(this, _ProxyServer_upstream, "f").https;
272
+ const useUpstream = upstreamHttps && !shouldBypass(host);
273
+ if (useUpstream) {
274
+ try {
275
+ const tunnelSocket = await connectThroughProxy(upstreamHttps, host, port);
276
+ const extraCa = __classPrivateFieldGet(this, _ProxyServer_config, "f").extraCaCerts;
277
+ upstreamOptions.createConnection = () => tls.connect({
278
+ socket: tunnelSocket,
279
+ servername: host,
280
+ ...(extraCa?.length && {
281
+ ca: [...tls.rootCertificates, ...extraCa],
282
+ }),
283
+ });
284
+ }
285
+ catch (error) {
286
+ __classPrivateFieldGet(this, _ProxyServer_emitter, "f").emit("error", {
287
+ id,
288
+ error: error,
289
+ phase: "connect",
290
+ });
291
+ if (!clientRes.headersSent) {
292
+ clientRes.writeHead(502, {
293
+ "Content-Type": "text/plain",
294
+ });
295
+ }
296
+ clientRes.end("Bad Gateway");
297
+ return;
298
+ }
299
+ }
300
+ else if (__classPrivateFieldGet(this, _ProxyServer_config, "f").extraCaCerts?.length) {
301
+ upstreamOptions.ca = [
302
+ ...tls.rootCertificates,
303
+ ...__classPrivateFieldGet(this, _ProxyServer_config, "f").extraCaCerts,
304
+ ];
305
+ }
225
306
  const upstreamReq = https.request(upstreamOptions, (upstreamRes) => {
226
307
  const chunks = [];
227
308
  upstreamRes.on("data", (chunk) => {
@@ -7,6 +7,8 @@ export type ProxyConfig = {
7
7
  port: number;
8
8
  hostname?: string;
9
9
  ca?: CaConfig;
10
+ upstreamProxy?: URL;
11
+ extraCaCerts?: string[];
10
12
  };
11
13
  export type RequestId = string;
12
14
  export type ProxyRequestEvent = {
@@ -0,0 +1,7 @@
1
+ import type * as net from "node:net";
2
+ export declare function getUpstreamProxy(explicit?: URL): {
3
+ http?: URL;
4
+ https?: URL;
5
+ };
6
+ export declare function shouldBypass(hostname: string): boolean;
7
+ export declare function connectThroughProxy(proxyUrl: URL, host: string, port: number): Promise<net.Socket>;
@@ -0,0 +1,54 @@
1
+ import * as http from "node:http";
2
+ export function getUpstreamProxy(explicit) {
3
+ if (explicit) {
4
+ return { http: explicit, https: explicit };
5
+ }
6
+ const httpsRaw = process.env["HTTPS_PROXY"] ?? process.env["https_proxy"];
7
+ const httpRaw = process.env["HTTP_PROXY"] ?? process.env["http_proxy"];
8
+ return {
9
+ http: httpRaw ? new URL(httpRaw) : undefined,
10
+ https: httpsRaw ? new URL(httpsRaw) : undefined,
11
+ };
12
+ }
13
+ export function shouldBypass(hostname) {
14
+ const raw = process.env["NO_PROXY"] ?? process.env["no_proxy"] ?? "";
15
+ if (!raw)
16
+ return false;
17
+ if (raw === "*")
18
+ return true;
19
+ const entries = raw.split(",").map((e) => e.trim().toLowerCase());
20
+ const host = hostname.toLowerCase();
21
+ for (const entry of entries) {
22
+ if (!entry)
23
+ continue;
24
+ if (host === entry)
25
+ return true;
26
+ if (entry.startsWith(".") && host.endsWith(entry))
27
+ return true;
28
+ if (host.endsWith(`.${entry}`))
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ export function connectThroughProxy(proxyUrl, host, port) {
34
+ return new Promise((resolve, reject) => {
35
+ const req = http.request({
36
+ hostname: proxyUrl.hostname,
37
+ port: Number(proxyUrl.port) || 80,
38
+ method: "CONNECT",
39
+ path: `${host}:${port}`,
40
+ headers: { Host: `${host}:${port}` },
41
+ });
42
+ req.on("connect", (res, socket) => {
43
+ if (res.statusCode === 200) {
44
+ resolve(socket);
45
+ }
46
+ else {
47
+ socket.destroy();
48
+ reject(new Error(`Upstream proxy CONNECT failed with status ${res.statusCode}`));
49
+ }
50
+ });
51
+ req.on("error", reject);
52
+ req.end();
53
+ });
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peep-proxy",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A TUI HTTP proxy for developers. Proxyman, but in your terminal.",
5
5
  "license": "MIT",
6
6
  "repository": {