proxy-chain 3.0.1-beta.5 → 3.0.1-beta.7

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/README.md CHANGED
@@ -560,18 +560,23 @@ specified by the `proxyUrl` parameter.
560
560
  The optional `options` parameter is an object with the following properties:
561
561
  - `port: Number` - Enables specifying the local port to listen at. By default `0`,
562
562
  which means a random port will be selected.
563
- - `hostname: String` - Local hostname to listen at. By default `localhost`.
563
+ - `hostname: String` - Local hostname to listen at. By default `127.0.0.1`.
564
564
  - `ignoreProxyCertificate` - For HTTPS proxy, ignore certificate errors in proxy requests. Useful for proxy with self-signed certificate. By default `false`.
565
565
  - `verbose: Boolean` - If `true`, the functions logs a lot. By default `false`.
566
566
 
567
- The result of the function is a local endpoint in a form of `hostname:port`.
567
+ The result of the function is a local endpoint in a form of `hostname:port`,
568
+ where `hostname` is the address the tunnel actually bound to. IPv6 addresses are
569
+ bracketed, e.g. `[::1]:56836`.
570
+
568
571
  All TCP connections made to the local endpoint will be tunneled through the proxy to the target host and port.
569
572
  For example, this is useful if you want to access a certain service from a specific IP address.
570
573
 
571
- The tunnel should be eventually closed by calling the `closeTunnel()` function.
574
+ The tunnel does not authenticate its clients and forwards the `proxyUrl`
575
+ credentials upstream, which is why it listens on `127.0.0.1` by default. Only
576
+ pass a non-loopback `hostname` if you restrict access to it by other means -
577
+ doing so emits a `ProxyChainSecurityWarning` via `process.emitWarning()`.
572
578
 
573
- The `createTunnel()` function returns a promise that resolves to a String with
574
- the path to the local endpoint.
579
+ The tunnel should be eventually closed by calling the `closeTunnel()` function.
575
580
 
576
581
  For more information, read this [blog post](https://blog.apify.com/tunneling-arbitrary-protocols-over-http-proxy-with-static-ip-address-b3a2222191ff).
577
582
 
@@ -579,7 +584,7 @@ Example:
579
584
 
580
585
  ```javascript
581
586
  const host = await createTunnel('http://bob:pass123@proxy.example.com:8000', 'service.example.com:356');
582
- // Prints something like "localhost:56836"
587
+ // Prints something like "127.0.0.1:56836"
583
588
  console.log(host);
584
589
  ```
585
590
 
@@ -3,7 +3,7 @@ import type http from 'node:http';
3
3
  import type net from 'node:net';
4
4
  export interface AnonymizeProxyOptions {
5
5
  url: string;
6
- port: number;
6
+ port?: number;
7
7
  ignoreProxyCertificate?: boolean;
8
8
  }
9
9
  /**
@@ -1,5 +1,6 @@
1
1
  import { URL } from 'node:url';
2
2
  import { Server, SOCKS_PROTOCOLS } from './server.js';
3
+ import { validateListenPort } from './utils/validate_listen_port.js';
3
4
  // Dictionary, key is value returned from anonymizeProxy(), value is Server instance.
4
5
  const anonymizedProxyUrlToServer = {};
5
6
  /**
@@ -16,10 +17,9 @@ export const anonymizeProxy = async (options) => {
16
17
  }
17
18
  else {
18
19
  proxyUrl = options.url;
19
- port = options.port;
20
- if (port < 0 || port > 65535) {
21
- throw new Error('Invalid "port" option: only values equals or between 0-65535 are valid');
22
- }
20
+ // Port 0 tells the OS to pick a free ephemeral port, which we read back after `listen()`.
21
+ port = options.port ?? 0;
22
+ validateListenPort(port);
23
23
  if (options.ignoreProxyCertificate !== undefined) {
24
24
  ignoreProxyCertificate = options.ignoreProxyCertificate;
25
25
  }
@@ -1,4 +1,6 @@
1
1
  export declare function createTunnel(proxyUrl: string, targetHost: string, options?: {
2
+ port?: number;
3
+ hostname?: string;
2
4
  verbose?: boolean;
3
5
  ignoreProxyCertificate?: boolean;
4
6
  }): Promise<string>;
@@ -1,7 +1,16 @@
1
1
  import net from 'node:net';
2
2
  import { URL } from 'node:url';
3
3
  import { chain } from './chain.js';
4
+ import { validateListenPort } from './utils/validate_listen_port.js';
4
5
  const runningServers = {};
6
+ // The tunnel does not authenticate its clients and forwards the proxyUrl credentials
7
+ // upstream, so it must not be reachable off the local machine by default.
8
+ const DEFAULT_LISTEN_HOSTNAME = '127.0.0.1';
9
+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '::1', '0:0:0:0:0:0:0:1']);
10
+ const isLoopbackHostname = (hostname) => {
11
+ const normalized = hostname.toLowerCase();
12
+ return LOOPBACK_HOSTNAMES.has(normalized) || normalized.startsWith('127.');
13
+ };
5
14
  const getAddress = (server) => {
6
15
  const { address: host, port, family } = server.address();
7
16
  if (family === 'IPv6') {
@@ -21,7 +30,14 @@ export async function createTunnel(proxyUrl, targetHost, options) {
21
30
  if (!url.port) {
22
31
  throw new Error('Missing target port');
23
32
  }
24
- const verbose = options && options.verbose;
33
+ const listenPort = options?.port ?? 0;
34
+ const listenHostname = options?.hostname || DEFAULT_LISTEN_HOSTNAME;
35
+ validateListenPort(listenPort);
36
+ if (!isLoopbackHostname(listenHostname)) {
37
+ process.emitWarning(`The tunnel is listening on "${listenHostname}", so it may be reachable from other machines.`
38
+ + ' It does not authenticate its clients and forwards the proxyUrl credentials upstream.', 'ProxyChainSecurityWarning');
39
+ }
40
+ const verbose = options?.verbose ?? false;
25
41
  const server = net.createServer();
26
42
  const log = (...args) => {
27
43
  // eslint-disable-next-line no-console
@@ -51,8 +67,7 @@ export async function createTunnel(proxyUrl, targetHost, options) {
51
67
  });
52
68
  const promise = new Promise((resolve, reject) => {
53
69
  server.once('error', reject);
54
- // Let the system pick a random listening port
55
- server.listen(0, () => {
70
+ server.listen(listenPort, listenHostname, () => {
56
71
  const address = getAddress(server);
57
72
  server.off('error', reject);
58
73
  runningServers[address] = { server, connections: new Set() };
@@ -0,0 +1,2 @@
1
+ /** Throws if `port` is not a valid TCP port number (0-65535). */
2
+ export declare const validateListenPort: (port: number) => void;
@@ -0,0 +1,6 @@
1
+ /** Throws if `port` is not a valid TCP port number (0-65535). */
2
+ export const validateListenPort = (port) => {
3
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
4
+ throw new Error(`The "port" option must be an integer between 0 and 65535 (was ${port})`);
5
+ }
6
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proxy-chain",
3
- "version": "3.0.1-beta.5",
3
+ "version": "3.0.1-beta.7",
4
4
  "description": "Node.js implementation of a proxy server (think Squid) with support for SSL, authentication, upstream proxy chaining, and protocol tunneling.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -51,45 +51,29 @@
51
51
  "devDependencies": {
52
52
  "@apify/eslint-config": "^1.0.0",
53
53
  "@apify/tsconfig": "^0.1.0",
54
- "@types/jest": "^28.1.2",
55
54
  "@types/node": "^20.19.29",
55
+ "@vitest/coverage-v8": "^4.1.11",
56
56
  "basic-auth": "^2.0.1",
57
57
  "basic-auth-parser": "^0.0.2",
58
58
  "body-parser": "^1.19.0",
59
- "chai": "^4.3.4",
60
59
  "cross-env": "^7.0.3",
61
60
  "eslint": "^9.18.0",
62
61
  "express": "^4.17.1",
63
62
  "faye-websocket": "^0.11.4",
64
63
  "got-scraping": "^3.2.4-beta.0",
65
- "isparta": "^4.1.1",
66
- "mocha": "^10.0.0",
67
- "nyc": "^15.1.0",
68
64
  "portastic": "^1.0.1",
69
65
  "proxy": "^1.0.2",
70
66
  "puppeteer": "^25.1.0",
71
67
  "request": "^2.88.2",
72
68
  "rimraf": "^4.1.2",
73
- "sinon": "^13.0.2",
74
- "sinon-stub-promise": "^4.0.0",
75
69
  "socksv5": "^0.0.6",
76
- "through": "^2.3.8",
77
70
  "tsx": "^4.21.0",
78
71
  "typescript": "^5.9.3",
79
72
  "typescript-eslint": "^8.20.0",
80
73
  "underscore": "^1.13.1",
74
+ "vitest": "^4.1.11",
81
75
  "ws": "^8.2.2"
82
76
  },
83
- "nyc": {
84
- "reporter": [
85
- "text",
86
- "html",
87
- "lcov"
88
- ],
89
- "exclude": [
90
- "**/test/**"
91
- ]
92
- },
93
77
  "dependencies": {
94
78
  "socks": "^2.8.3",
95
79
  "socks-proxy-agent": "^8.0.3",
@@ -100,13 +84,22 @@
100
84
  "build": "tsc",
101
85
  "clean": "rimraf dist",
102
86
  "local-proxy": "tsx test/utils/run_locally.js",
103
- "test": "nyc cross-env NODE_OPTIONS=--insecure-http-parser mocha 'test/unit/**/*.js' 'test/e2e/**/*.js'",
104
- "test:unit": "mocha 'test/unit/**/*.js'",
105
- "test:e2e": "nyc cross-env NODE_OPTIONS=--insecure-http-parser mocha 'test/e2e/**/*.js'",
106
- "test:bun": "bun --bun run mocha --no-config --exit 'test/unit/**/*.js'",
107
- "test:bun:e2e:compatible": "bun --bun run mocha --no-config --exit --grep 'throws error' test/e2e/tcp_tunnel.js",
108
- "test:bun:e2e:full": "bun --bun run mocha --no-config --exit 'test/e2e/**/*.js'",
109
- "test:docker": "docker build --tag proxy-chain-tests --file test/Dockerfile . && docker run --add-host localhost-test:127.0.0.1 proxy-chain-tests",
87
+ "test": "vitest run --coverage",
88
+ "test:unit": "vitest run --project unit",
89
+ "test:e2e": "vitest run --project e2e --coverage",
90
+ "test:bun": "bun --bun run vitest run --project unit",
91
+ "test:bun:e2e:full": "cross-env NODE_OPTIONS=--insecure-http-parser bun --bun run vitest run --project e2e",
92
+ "test:bun:e2e:compatible": "pnpm run test:bun:e2e:full test/e2e/tcp_tunnel.js -t 'throws error'",
93
+ "test:bun:all": "pnpm run test:bun && pnpm run test:bun:e2e:full",
94
+ "test:bun:supported": "pnpm run test:bun && pnpm run test:bun:e2e:compatible",
95
+ "docker:build": "docker build --tag proxy-chain-tests --file test/Dockerfile .",
96
+ "docker:run": "docker run --rm --add-host localhost-test:127.0.0.1 proxy-chain-tests",
97
+ "test:docker": "pnpm run docker:build && pnpm run docker:run -- test",
98
+ "test:docker:bun": "pnpm run docker:build && pnpm run docker:run -- test:bun:supported",
99
+ "test:docker:bun:unit": "pnpm run docker:build && pnpm run docker:run -- test:bun",
100
+ "test:docker:bun:e2e:compatible": "pnpm run docker:build && pnpm run docker:run -- test:bun:e2e:compatible",
101
+ "test:docker:bun:e2e:full": "pnpm run docker:build && pnpm run docker:run -- test:bun:e2e:full",
102
+ "test:docker:bun:full": "pnpm run docker:build && pnpm run docker:run -- test:bun:all",
110
103
  "test:docker:all": "bash scripts/test-docker-all.sh",
111
104
  "lint": "eslint .",
112
105
  "lint:fix": "eslint . --fix"