diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..229cd6ae --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": [ + "@apify/ts" + ], + "parserOptions": { + "project": "tsconfig.eslint.json" + }, + "overrides": [ + { + "files": [ + "test/**/*.js" + ], + "env": { + "jest": true + } + } + ] + } diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 4f608306..503c814a 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -14,8 +14,8 @@ jobs: strategy: matrix: - os: [ubuntu-22.04] # add windows-latest later - node-version: [14, 16, 18] + os: [ubuntu-latest] # add windows-latest later + node-version: [10, 12, 14, 16] steps: - diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ea9a4b95..9ff345d3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,8 +18,13 @@ jobs: strategy: matrix: +<<<<<<< HEAD:.github/workflows/release.yaml os: [ubuntu-22.04] # add windows-latest later node-version: [14, 16, 18] +======= + os: [ubuntu-latest] # add windows-latest later + node-version: [14, 16] +>>>>>>> f1bbe42 (release: 2.0.0 (#162)):.github/workflows/release.yml steps: - diff --git a/.gitignore b/.gitignore index f309e3b3..0731a867 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,7 @@ docs package-lock.json .nyc_output dist +<<<<<<< HEAD .vscode +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e4e1544..58b36fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,20 @@ +<<<<<<< HEAD 2.0.1 / 2022-05-02 +======= +2.0.0 / 2021-10-12 +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) ================== - Simplify code, fix tests, move to TypeScript [#162](https://github.com/apify/proxy-chain/pull/162) - Bugfix: Memory leak in createTunnel [#160](https://github.com/apify/proxy-chain/issues/160) - Bugfix: Proxy fails to handle non-standard HTTP response in HTTP forwarding mode, on certain websites [#107](https://github.com/apify/proxy-chain/issues/107) +<<<<<<< HEAD - Pass proxyChainId to tunnelConnectResponded [#173](https://github.com/apify/proxy-chain/pull/173) - feat: accept custom port for proxy anonymization [#214](https://github.com/apify/proxy-chain/pull/214) - fix: socket close race condition - feat: closeConnection by id [#176](https://github.com/apify/proxy-chain/pull/176) - feat: custom dns lookup [#175](https://github.com/apify/proxy-chain/pull/175) +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) 1.0.3 / 2021-08-17 ================== diff --git a/jest.config.ts b/jest.config.ts index dbb5dc79..e48be1fd 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,6 +1,9 @@ import type { Config } from '@jest/types'; +<<<<<<< HEAD // eslint-disable-next-line import/no-default-export +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) export default (): Config.InitialOptions => ({ verbose: true, preset: 'ts-jest', diff --git a/package.json b/package.json index 53963e2d..e73fb65c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,10 @@ { "name": "proxy-chain", +<<<<<<< HEAD "version": "2.5.9", +======= + "version": "2.0.0", +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) "description": "Node.js implementation of a proxy server (think Squid) with support for SSL, authentication, upstream proxy chaining, and protocol tunneling.", "main": "dist/index.js", "keywords": [ @@ -37,13 +41,19 @@ "prepublishOnly": "npm run build", "local-proxy": "node ./dist/run_locally.js", "test": "nyc cross-env NODE_OPTIONS=--insecure-http-parser mocha --bail", +<<<<<<< HEAD "lint": "eslint .", "lint:fix": "eslint . --fix" +======= + "lint": "eslint src", + "lint-fix": "eslint src --fix" +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) }, "engines": { "node": ">=14" }, "devDependencies": { +<<<<<<< HEAD "@apify/eslint-config": "^1.0.0", "@apify/tsconfig": "^0.1.0", "@types/jest": "^28.1.2", @@ -51,14 +61,28 @@ "basic-auth": "^2.0.1", "basic-auth-parser": "^0.0.2", "body-parser": "^1.19.0", - "chai": "^4.3.4", + "chai": "^4.5.0", "cross-env": "^7.0.3", "eslint": "^9.18.0", +======= + "@apify/eslint-config-ts": "^0.1.4", + "@apify/tsconfig": "^0.1.0", + "@types/jest": "^27.0.2", + "@types/node": "^16.10.1", + "@typescript-eslint/eslint-plugin": "^4.31.2", + "basic-auth": "^2.0.1", + "basic-auth-parser": "^0.0.2", + "body-parser": "^1.19.0", + "chai": "^4.3.4", + "cross-env": "^7.0.3", + "eslint": "^7.32.0", +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) "express": "^4.17.1", "faye-websocket": "^0.11.4", "got-scraping": "^3.2.4-beta.0", "isparta": "^4.1.1", - "mocha": "^10.0.0", +<<<<<<< HEAD + "mocha": "^10.8.2", "nyc": "^15.1.0", "portastic": "^1.0.1", "proxy": "^1.0.2", @@ -66,12 +90,25 @@ "request": "^2.88.2", "rimraf": "^4.1.2", "sinon": "^13.0.2", +======= + "mocha": "^9.1.2", + "nyc": "^15.1.0", + "phantomjs-prebuilt": "^2.1.16", + "portastic": "^1.0.1", + "proxy": "^1.0.2", + "request": "^2.88.2", + "rimraf": "^3.0.2", + "sinon": "^11.1.2", +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) "sinon-stub-promise": "^4.0.0", "socksv5": "^0.0.6", "through": "^2.3.8", "ts-node": "^10.2.1", "typescript": "^4.4.3", +<<<<<<< HEAD "typescript-eslint": "^8.20.0", +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) "underscore": "^1.13.1", "ws": "^8.2.2" }, @@ -84,10 +121,13 @@ "exclude": [ "**/test/**" ] +<<<<<<< HEAD }, "dependencies": { "socks": "^2.8.3", "socks-proxy-agent": "^8.0.3", "tslib": "^2.3.1" +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } } diff --git a/src/anonymize_proxy.ts b/src/anonymize_proxy.ts index 9b7cbd8e..5d73fb8a 100644 --- a/src/anonymize_proxy.ts +++ b/src/anonymize_proxy.ts @@ -1,14 +1,23 @@ +<<<<<<< HEAD import type { Buffer } from 'node:buffer'; import type http from 'node:http'; import type net from 'node:net'; import { URL } from 'node:url'; import { Server, SOCKS_PROTOCOLS } from './server'; +======= +import net from 'net'; +import http from 'http'; +import { Buffer } from 'buffer'; +import { URL } from 'url'; +import { Server } from './server'; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) import { nodeify } from './utils/nodeify'; // Dictionary, key is value returned from anonymizeProxy(), value is Server instance. const anonymizedProxyUrlToServer: Record = {}; +<<<<<<< HEAD export interface AnonymizeProxyOptions { url: string; port: number; @@ -52,11 +61,26 @@ export const anonymizeProxy = async ( // If upstream proxy requires no password or if there is no need to ignore HTTPS proxy cert errors, return it directly if (!parsedProxyUrl.username && !parsedProxyUrl.password && (!ignoreProxyCertificate || parsedProxyUrl.protocol !== 'https:')) { +======= +/** + * Parses and validates a HTTP proxy URL. If the proxy requires authentication, then the function + * starts an open local proxy server that forwards to the upstream proxy. + */ +export const anonymizeProxy = (proxyUrl: string, callback?: (error: Error | null) => void): Promise => { + const parsedProxyUrl = new URL(proxyUrl); + if (parsedProxyUrl.protocol !== 'http:') { + throw new Error('Invalid "proxyUrl" option: only HTTP proxies are currently supported.'); + } + + // If upstream proxy requires no password, return it directly + if (!parsedProxyUrl.username && !parsedProxyUrl.password) { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) return nodeify(Promise.resolve(proxyUrl), callback); } let server: Server & { port: number }; +<<<<<<< HEAD const startServer = async () => { return Promise.resolve().then(async () => { server = new Server({ @@ -81,6 +105,32 @@ export const anonymizeProxy = async ( anonymizedProxyUrlToServer[url] = server; return url; }); +======= + const startServer = () => { + return Promise.resolve() + .then(() => { + server = new Server({ + // verbose: true, + port: 0, + prepareRequestFunction: () => { + return { + requestAuthentication: false, + upstreamProxyUrl: proxyUrl, + }; + }, + }) as Server & { port: number }; + + return server.listen(); + }); + }; + + const promise = startServer() + .then(() => { + const url = `http://127.0.0.1:${server.port}`; + anonymizedProxyUrlToServer[url] = server; + return url; + }); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) return nodeify(promise, callback); }; @@ -91,7 +141,11 @@ export const anonymizeProxy = async ( * and its result if `false`. Otherwise the result is `true`. * @param closeConnections If true, pending proxy connections are forcibly closed. */ +<<<<<<< HEAD export const closeAnonymizedProxy = async ( +======= +export const closeAnonymizedProxy = ( +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) anonymizedProxyUrl: string, closeConnections: boolean, callback?: (error: Error | null, result?: boolean) => void, @@ -107,6 +161,7 @@ export const closeAnonymizedProxy = async ( delete anonymizedProxyUrlToServer[anonymizedProxyUrl]; +<<<<<<< HEAD const promise = server.close(closeConnections).then(() => { return true; }); @@ -122,6 +177,16 @@ type Callback = ({ socket: net.Socket; head: Buffer; }) => void; +======= + const promise = server.close(closeConnections) + .then(() => { + return true; + }); + return nodeify(promise, callback); +}; + +type Callback = ({ response, socket, head }: { response: http.IncomingMessage; socket: net.Socket; head: Buffer; }) => void; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) /** * Add a callback on 'tunnelConnectResponded' Event in order to get headers from CONNECT tunnel to proxy diff --git a/src/chain.ts b/src/chain.ts index 06cd1603..b0675e57 100644 --- a/src/chain.ts +++ b/src/chain.ts @@ -1,3 +1,4 @@ +<<<<<<< HEAD import type { Buffer } from 'node:buffer'; import type dns from 'node:dns'; import type { EventEmitter } from 'node:events'; @@ -18,15 +19,45 @@ interface Options { localAddress?: string; family?: number; lookup?: typeof dns['lookup']; +======= +import http from 'http'; +import { URL } from 'url'; +import { EventEmitter } from 'events'; +import { Buffer } from 'buffer'; +import { countTargetBytes } from './utils/count_target_bytes'; +import { getBasicAuthorizationHeader } from './utils/get_basic'; +import { Socket } from './socket'; + +const createHttpResponse = (statusCode: number, message: string) => { + return [ + `HTTP/1.1 ${statusCode} ${http.STATUS_CODES[statusCode] || 'Unknown Status Code'}`, + 'Connection: close', + `Date: ${(new Date()).toUTCString()}`, + `Content-Length: ${Buffer.byteLength(message)}`, + ``, + message, + ].join('\r\n'); +}; + +interface Options { + method: string; + headers: string[]; + path?: string; + localAddress?: string; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } export interface HandlerOpts { upstreamProxyUrlParsed: URL; +<<<<<<< HEAD ignoreUpstreamProxyCertificate: boolean; localAddress?: string; ipFamily?: number; dnsLookup?: typeof dns['lookup']; customTag?: unknown; +======= + localAddress?: string; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } interface ChainOpts { @@ -34,8 +65,14 @@ interface ChainOpts { sourceSocket: Socket; head?: Buffer; handlerOpts: HandlerOpts; +<<<<<<< HEAD server: EventEmitter & { log: (connectionId: unknown, str: string) => void }; isPlain: boolean; +======= + server: EventEmitter & { log: (...args: any[]) => void; }; + isPlain: boolean; + localAddress?: string; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } /** @@ -54,6 +91,7 @@ export const chain = ( }: ChainOpts, ): void => { if (head && head.length > 0) { +<<<<<<< HEAD // HTTP/1.1 has no defined semantics when sending payload along with CONNECT and servers can reject the request. // HTTP/2 only says that subsequent DATA frames must be transferred after HEADERS has been sent. // HTTP/3 says that all DATA frames should be transferred (implies pre-HEADERS data). @@ -62,15 +100,23 @@ export const chain = ( // There are also clients that send payload along with CONNECT to save milliseconds apparently. // Beware of upstream proxy servers that send out valid CONNECT responses with diagnostic data such as IPs! sourceSocket.unshift(head); +======= + throw new Error(`Unexpected data on CONNECT: ${head.length} bytes`); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } const { proxyChainId } = sourceSocket; +<<<<<<< HEAD const { upstreamProxyUrlParsed: proxy, customTag } = handlerOpts; +======= + const { upstreamProxyUrlParsed: proxy } = handlerOpts; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) const options: Options = { method: 'CONNECT', path: request.url, +<<<<<<< HEAD headers: { host: request.url!, }, @@ -99,6 +145,25 @@ export const chain = ( }); client.on('connect', (response, targetSocket, clientHead) => { +======= + headers: [ + 'host', + request.url!, + ], + localAddress: handlerOpts.localAddress, + }; + + if (proxy.username || proxy.password) { + options.headers.push('proxy-authorization', getBasicAuthorizationHeader(proxy)); + } + + const client = http.request(proxy.origin, options as unknown as http.ClientRequestArgs); + + client.on('connect', (response, targetSocket, clientHead) => { + countTargetBytes(sourceSocket, targetSocket); + + // @ts-expect-error Missing types +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) if (sourceSocket.readyState !== 'open') { // Sanity check, should never reach. targetSocket.destroy(); @@ -123,6 +188,7 @@ export const chain = ( if (isPlain) { sourceSocket.end(); } else { +<<<<<<< HEAD const { statusCode } = response; const status = statusCode === 401 || statusCode === 407 ? badGatewayStatusCodes.AUTH_FAILED @@ -141,10 +207,16 @@ export const chain = ( head: clientHead, }); +======= + sourceSocket.end(createHttpResponse(502, '')); + } + +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) return; } if (clientHead.length > 0) { +<<<<<<< HEAD // See comment above targetSocket.unshift(clientHead); } @@ -153,6 +225,14 @@ export const chain = ( proxyChainId, response, customTag, +======= + targetSocket.destroy(new Error(`Unexpected data on CONNECT: ${clientHead.length} bytes`)); + return; + } + + server.emit('tunnelConnectResponded', { + response, +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) socket: targetSocket, head: clientHead, }); @@ -183,17 +263,29 @@ export const chain = ( }); }); +<<<<<<< HEAD client.on('error', (error: NodeJS.ErrnoException) => { server.log(proxyChainId, `Failed to connect to upstream proxy: ${error.stack}`); // The end socket may get connected after the client to proxy one gets disconnected. +======= + client.on('error', (error) => { + server.log(proxyChainId, `Failed to connect to upstream proxy: ${error.stack}`); + + // The end socket may get connected after the client to proxy one gets disconnected. + // @ts-expect-error Missing types +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) if (sourceSocket.readyState === 'open') { if (isPlain) { sourceSocket.end(); } else { +<<<<<<< HEAD const statusCode = errorCodeToStatusCode[error.code!] ?? badGatewayStatusCodes.GENERIC_ERROR; const response = createCustomStatusHttpResponse(statusCode, error.code ?? 'Upstream Closed Early'); sourceSocket.end(response); +======= + sourceSocket.end(createHttpResponse(502, '')); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } } }); diff --git a/src/custom_response.ts b/src/custom_response.ts index 8058f877..5400a3c2 100644 --- a/src/custom_response.ts +++ b/src/custom_response.ts @@ -1,3 +1,4 @@ +<<<<<<< HEAD import type { Buffer } from 'node:buffer'; import type http from 'node:http'; @@ -5,11 +6,23 @@ export interface CustomResponse { statusCode?: number; headers?: Record; body?: string | Buffer; +======= +import http from 'http'; + +export interface Result { + statusCode?: number; + headers?: Record; + body?: string; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) encoding?: BufferEncoding; } export interface HandlerOpts { +<<<<<<< HEAD customResponseFunction: () => CustomResponse | Promise, +======= + customResponseFunction: () => Result | Promise, +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } export const handleCustomResponse = async ( diff --git a/src/direct.ts b/src/direct.ts index f4c7d68d..6953a8ed 100644 --- a/src/direct.ts +++ b/src/direct.ts @@ -1,3 +1,4 @@ +<<<<<<< HEAD import type { Buffer } from 'node:buffer'; import type dns from 'node:dns'; import type { EventEmitter } from 'node:events'; @@ -11,13 +12,28 @@ export interface HandlerOpts { localAddress?: string; ipFamily?: number; dnsLookup?: typeof dns['lookup']; +======= +import net from 'net'; +import { Buffer } from 'buffer'; +import { URL } from 'url'; +import { EventEmitter } from 'events'; +import { countTargetBytes } from './utils/count_target_bytes'; +import { Socket } from './socket'; + +export interface HandlerOpts { + localAddress?: string; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } interface DirectOpts { request: { url?: string }; sourceSocket: Socket; head: Buffer; +<<<<<<< HEAD server: EventEmitter & { log: (connectionId: unknown, str: string) => void }; +======= + server: EventEmitter & { log: (...args: any[]) => void; }; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) handlerOpts: HandlerOpts; } @@ -46,16 +62,23 @@ export const direct = ( } if (head.length > 0) { +<<<<<<< HEAD // See comment in chain.ts sourceSocket.unshift(head); +======= + throw new Error(`Unexpected data on CONNECT: ${head.length} bytes`); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } const options = { port: Number(url.port), host: url.hostname, localAddress: handlerOpts.localAddress, +<<<<<<< HEAD family: handlerOpts.ipFamily, lookup: handlerOpts.dnsLookup, +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) }; if (options.host[0] === '[') { diff --git a/src/forward.ts b/src/forward.ts index b7c656f6..e91c644c 100644 --- a/src/forward.ts +++ b/src/forward.ts @@ -1,3 +1,4 @@ +<<<<<<< HEAD import type dns from 'node:dns'; import http from 'node:http'; import https from 'node:https'; @@ -10,6 +11,16 @@ import type { SocketWithPreviousStats } from './utils/count_target_bytes'; import { countTargetBytes } from './utils/count_target_bytes'; import { getBasicAuthorizationHeader } from './utils/get_basic'; import { validHeadersOnly } from './utils/valid_headers_only'; +======= +import http from 'http'; +import https from 'https'; +import stream from 'stream'; +import util from 'util'; +import { URL } from 'url'; +import { validHeadersOnly } from './utils/valid_headers_only'; +import { getBasicAuthorizationHeader } from './utils/get_basic'; +import { countTargetBytes } from './utils/count_target_bytes'; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) const pipeline = util.promisify(stream.pipeline); @@ -19,16 +30,23 @@ interface Options { insecureHTTPParser: boolean; path?: string; localAddress?: string; +<<<<<<< HEAD family?: number; lookup?: typeof dns['lookup']; +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } export interface HandlerOpts { upstreamProxyUrlParsed: URL; +<<<<<<< HEAD ignoreUpstreamProxyCertificate: boolean; localAddress?: string; ipFamily?: number; dnsLookup?: typeof dns['lookup']; +======= + localAddress?: string; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } /** @@ -62,8 +80,11 @@ export const forward = async ( headers: validHeadersOnly(request.rawHeaders), insecureHTTPParser: true, localAddress: handlerOpts.localAddress, +<<<<<<< HEAD family: handlerOpts.ipFamily, lookup: handlerOpts.dnsLookup, +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) }; // In case of proxy the path needs to be an absolute URL @@ -80,12 +101,23 @@ export const forward = async ( } } +<<<<<<< HEAD const requestCallback = async (clientResponse: http.IncomingMessage) => { +======= + const fn = origin!.startsWith('https:') ? https.request : http.request; + + // We have to force cast `options` because @types/node doesn't support an array. + const client = fn(origin!, options as unknown as http.ClientRequestArgs, async (clientResponse) => { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) try { // This is necessary to prevent Node.js throwing an error let statusCode = clientResponse.statusCode!; if (statusCode < 100 || statusCode > 999) { +<<<<<<< HEAD statusCode = badGatewayStatusCodes.STATUS_CODE_OUT_OF_RANGE; +======= + statusCode = 502; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } // 407 is handled separately @@ -107,6 +139,7 @@ export const forward = async ( ); resolve(); +<<<<<<< HEAD } catch { // Client error, pipeline already destroys the streams, ignore. resolve(); @@ -145,4 +178,26 @@ export const forward = async ( resolve(); }); +======= + } catch (error) { + reject(error); + } + }); + + client.once('socket', (socket) => { + countTargetBytes(request.socket, socket); + }); + + try { + // `pipeline` automatically handles all the events and data + await pipeline( + request, + client, + ); + } catch (error: any) { + error.proxy = proxy; + + reject(error); + } +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) }); diff --git a/src/index.ts b/src/index.ts index f945ef87..6e0e4519 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +<<<<<<< HEAD export * from './request_error'; export * from './server'; export * from './utils/redact_url'; @@ -5,3 +6,10 @@ export * from './anonymize_proxy'; export * from './tcp_tunnel_tools'; export { CustomResponse } from './custom_response'; +======= +export { RequestError } from './request_error'; +export { Server } from './server'; +export { redactUrl } from './utils/redact_url'; +export { anonymizeProxy, closeAnonymizedProxy, listenConnectAnonymizedProxy } from './anonymize_proxy'; +export { createTunnel, closeTunnel } from './tcp_tunnel_tools'; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) diff --git a/src/server.ts b/src/server.ts index 41fba80d..8aa699b2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,3 +1,4 @@ +<<<<<<< HEAD /* eslint-disable no-use-before-define */ import { Buffer } from 'node:buffer'; import type dns from 'node:dns'; @@ -27,6 +28,24 @@ import { parseAuthorizationHeader } from './utils/parse_authorization_header'; import { redactUrl } from './utils/redact_url'; export const SOCKS_PROTOCOLS = ['socks:', 'socks4:', 'socks4a:', 'socks5:', 'socks5h:']; +======= +import net from 'net'; +import http from 'http'; +import util from 'util'; +import { URL } from 'url'; +import { EventEmitter } from 'events'; +import { Buffer } from 'buffer'; +import { parseAuthorizationHeader } from './utils/parse_authorization_header'; +import { redactUrl } from './utils/redact_url'; +import { nodeify } from './utils/nodeify'; +import { getTargetStats } from './utils/count_target_bytes'; +import { RequestError } from './request_error'; +import { chain, HandlerOpts as ChainOpts } from './chain'; +import { forward, HandlerOpts as ForwardOpts } from './forward'; +import { direct } from './direct'; +import { handleCustomResponse, HandlerOpts as CustomResponseOpts } from './custom_response'; +import { Socket } from './socket'; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) // TODO: // - Implement this requirement from rfc7230 @@ -40,7 +59,11 @@ export const SOCKS_PROTOCOLS = ['socks:', 'socks4:', 'socks4a:', 'socks5:', 'soc const DEFAULT_AUTH_REALM = 'ProxyChain'; const DEFAULT_PROXY_SERVER_PORT = 8000; +<<<<<<< HEAD export type ConnectionStats = { +======= +type ConnectionStats = { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) srcTxBytes: number; srcRxBytes: number; trgTxBytes: number | null; @@ -55,6 +78,7 @@ type HandlerOpts = { srcHead: Buffer | null; trgParsed: URL | null; upstreamProxyUrlParsed: URL | null; +<<<<<<< HEAD ignoreUpstreamProxyCertificate: boolean; isHttp: boolean; customResponseFunction?: CustomResponseOpts['customResponseFunction'] | null; @@ -67,10 +91,20 @@ type HandlerOpts = { export type PrepareRequestFunctionOpts = { connectionId: number; +======= + isHttp: boolean; + customResponseFunction: CustomResponseOpts['customResponseFunction'] | null; + localAddress?: string; +}; + +type PrepareRequestFunctionOpts = { + connectionId: unknown; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) request: http.IncomingMessage; username: string; password: string; hostname: string; +<<<<<<< HEAD port: number; isHttp: boolean; }; @@ -90,6 +124,22 @@ export type PrepareRequestFunctionResult = { type Promisable = T | Promise; export type PrepareRequestFunction = (opts: PrepareRequestFunctionOpts) => Promisable; +======= + port: string; + isHttp: boolean; +}; + +type PrepareRequestFunctionResult = { + customResponseFunction?: CustomResponseOpts['customResponseFunction']; + requestAuthentication?: boolean; + failMsg?: string; + upstreamProxyUrl?: string | null; + localAddress?: string; +}; + +type Promisable = T | Promise; +type PrepareRequestFunction = (opts: PrepareRequestFunctionOpts) => Promisable; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) /** * Represents the proxy server. @@ -99,8 +149,11 @@ export type PrepareRequestFunction = (opts: PrepareRequestFunctionOpts) => Promi export class Server extends EventEmitter { port: number; +<<<<<<< HEAD host?: string; +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) prepareRequestFunction?: PrepareRequestFunction; authRealm: unknown; @@ -113,7 +166,11 @@ export class Server extends EventEmitter { stats: { httpRequestCount: number; connectRequestCount: number; }; +<<<<<<< HEAD connections: Map; +======= + connections: Map; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) /** * Initializes a new instance of Server class. @@ -151,7 +208,10 @@ export class Server extends EventEmitter { */ constructor(options: { port?: number, +<<<<<<< HEAD host?: string, +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) prepareRequestFunction?: PrepareRequestFunction, verbose?: boolean, authRealm?: unknown, @@ -164,7 +224,10 @@ export class Server extends EventEmitter { this.port = options.port; } +<<<<<<< HEAD this.host = options.host; +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) this.prepareRequestFunction = options.prepareRequestFunction; this.authRealm = options.authRealm || DEFAULT_AUTH_REALM; this.verbose = !!options.verbose; @@ -186,8 +249,12 @@ export class Server extends EventEmitter { log(connectionId: unknown, str: string): void { if (this.verbose) { +<<<<<<< HEAD const logPrefix = connectionId != null ? `${String(connectionId)} | ` : ''; // eslint-disable-next-line no-console +======= + const logPrefix = connectionId ? `${connectionId} | ` : ''; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) console.log(`ProxyServer[${this.port}]: ${logPrefix}${str}`); } } @@ -208,7 +275,12 @@ export class Server extends EventEmitter { * Needed for abrupt close of the server. */ registerConnection(socket: Socket): void { +<<<<<<< HEAD const unique = this.lastHandlerId++; +======= + const weakId = Math.random().toString(36).slice(2); + const unique = Symbol(weakId); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) socket.proxyChainId = unique; this.connections.set(unique, socket); @@ -221,23 +293,29 @@ export class Server extends EventEmitter { this.connections.delete(unique); }); +<<<<<<< HEAD // We have to manually destroy the socket if it timeouts. // This will prevent connections from leaking and close them properly. socket.on('timeout', () => { socket.destroy(); }); +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } /** * Handles incoming sockets, useful for error handling */ onConnection(socket: Socket): void { +<<<<<<< HEAD // https://github.com/nodejs/node/issues/23858 if (!socket.remoteAddress) { socket.destroy(); return; } +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) this.registerConnection(socket); // We need to consume socket errors, because the handlers are attached asynchronously. @@ -255,11 +333,27 @@ export class Server extends EventEmitter { */ normalizeHandlerError(error: NodeJS.ErrnoException): NodeJS.ErrnoException { if (error.message === 'Username contains an invalid colon') { +<<<<<<< HEAD return new RequestError('Invalid colon in username in upstream proxy credentials', badGatewayStatusCodes.AUTH_FAILED); } if (error.message === '407 Proxy Authentication Required') { return new RequestError('Invalid upstream proxy credentials', badGatewayStatusCodes.AUTH_FAILED); +======= + return new RequestError('Invalid colon in username in upstream proxy credentials', 502); + } + + if (error.message === '407 Proxy Authentication Required') { + return new RequestError('Invalid upstream proxy credentials', 502); + } + + if (error.code === 'ENOTFOUND') { + if ((error as any).proxy) { + return new RequestError('Failed to connect to upstream proxy', 502); + } + + return new RequestError('Target website does not exist', 404); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } return error; @@ -276,6 +370,7 @@ export class Server extends EventEmitter { const { proxyChainId } = request.socket as Socket; if (handlerOpts.customResponseFunction) { +<<<<<<< HEAD this.log(proxyChainId, 'Using handleCustomResponse()'); await handleCustomResponse(request, response, handlerOpts as CustomResponseOpts); return; @@ -289,6 +384,14 @@ export class Server extends EventEmitter { this.log(proxyChainId, 'Using forward()'); await forward(request, response, handlerOpts as ForwardOpts); +======= + this.log(proxyChainId, 'Using HandlerCustomResponse'); + return await handleCustomResponse(request, response, handlerOpts as CustomResponseOpts); + } + + this.log(proxyChainId, 'Using forward'); + return await forward(request, response, handlerOpts as ForwardOpts); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } catch (error) { this.failRequest(request, this.normalizeHandlerError(error as NodeJS.ErrnoException)); } @@ -307,6 +410,7 @@ export class Server extends EventEmitter { const data = { request, sourceSocket: socket, head, handlerOpts: handlerOpts as ChainOpts, server: this, isPlain: false }; +<<<<<<< HEAD if (handlerOpts.customConnectServer) { socket.unshift(head); // See chain.ts for why we do this await customConnect(socket, handlerOpts.customConnectServer); @@ -326,6 +430,15 @@ export class Server extends EventEmitter { this.log(socket.proxyChainId, `Using direct() => ${request.url}`); direct(data); +======= + if (handlerOpts.upstreamProxyUrlParsed) { + this.log(socket.proxyChainId, `Using HandlerTunnelChain => ${request.url}`); + return await chain(data); + } + + this.log(socket.proxyChainId, `Using HandlerTunnelDirect => ${request.url}`); + return await direct(data); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } catch (error) { this.failRequest(request, this.normalizeHandlerError(error as NodeJS.ErrnoException)); } @@ -338,27 +451,41 @@ export class Server extends EventEmitter { getHandlerOpts(request: http.IncomingMessage): HandlerOpts { const handlerOpts: HandlerOpts = { server: this, +<<<<<<< HEAD id: (request.socket as Socket).proxyChainId!, +======= + id: ++this.lastHandlerId, +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) srcRequest: request, srcHead: null, trgParsed: null, upstreamProxyUrlParsed: null, +<<<<<<< HEAD ignoreUpstreamProxyCertificate: false, isHttp: false, srcResponse: null, customResponseFunction: null, customConnectServer: null, +======= + isHttp: false, + srcResponse: null, + customResponseFunction: null, +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) }; this.log((request.socket as Socket).proxyChainId, `!!! Handling ${request.method} ${request.url} HTTP/${request.httpVersion}`); if (request.method === 'CONNECT') { // CONNECT server.example.com:80 HTTP/1.1 +<<<<<<< HEAD try { handlerOpts.trgParsed = new URL(`connect://${request.url}`); } catch { throw new RequestError(`Target "${request.url}" could not be parsed`, 400); } +======= + handlerOpts.trgParsed = new URL(`connect://${request.url}`); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) if (!handlerOpts.trgParsed.hostname || !handlerOpts.trgParsed.port) { throw new RequestError(`Target "${request.url}" could not be parsed`, 400); @@ -376,7 +503,11 @@ export class Server extends EventEmitter { let parsed; try { parsed = new URL(request.url!); +<<<<<<< HEAD } catch { +======= + } catch (error) { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) // If URL is invalid, throw HTTP 400 error throw new RequestError(`Target "${request.url}" could not be parsed`, 400); } @@ -401,18 +532,32 @@ export class Server extends EventEmitter { * @param handlerOpts */ async callPrepareRequestFunction(request: http.IncomingMessage, handlerOpts: HandlerOpts): Promise { +<<<<<<< HEAD if (this.prepareRequestFunction) { const funcOpts: PrepareRequestFunctionOpts = { connectionId: (request.socket as Socket).proxyChainId!, +======= + // Authenticate the request using a user function (if provided) + if (this.prepareRequestFunction) { + const funcOpts: PrepareRequestFunctionOpts = { + connectionId: (request.socket as Socket).proxyChainId, +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) request, username: '', password: '', hostname: handlerOpts.trgParsed!.hostname, +<<<<<<< HEAD port: normalizeUrlPort(handlerOpts.trgParsed!), isHttp: handlerOpts.isHttp, }; // Authenticate the request using a user function (if provided) +======= + port: handlerOpts.trgParsed!.port, + isHttp: handlerOpts.isHttp, + }; + +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) const proxyAuth = request.headers['proxy-authorization']; if (proxyAuth) { const auth = parseAuthorizationHeader(proxyAuth); @@ -421,10 +566,14 @@ export class Server extends EventEmitter { throw new RequestError('Invalid "Proxy-Authorization" header', 400); } +<<<<<<< HEAD // https://datatracker.ietf.org/doc/html/rfc7617#page-3 // Note that both scheme and parameter names are matched case- // insensitively. if (auth.type.toLowerCase() !== 'basic') { +======= + if (auth.type !== 'Basic') { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) throw new RequestError('The "Proxy-Authorization" header must have the "Basic" type.', 400); } @@ -449,10 +598,13 @@ export class Server extends EventEmitter { const funcResult = await this.callPrepareRequestFunction(request, handlerOpts); handlerOpts.localAddress = funcResult.localAddress; +<<<<<<< HEAD handlerOpts.ipFamily = funcResult.ipFamily; handlerOpts.dnsLookup = funcResult.dnsLookup; handlerOpts.customConnectServer = funcResult.customConnectServer; handlerOpts.customTag = funcResult.customTag; +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) // If not authenticated, request client to authenticate if (funcResult.requestAuthentication) { @@ -466,6 +618,7 @@ export class Server extends EventEmitter { throw new Error(`Invalid "upstreamProxyUrl" provided: ${error} (was "${funcResult.upstreamProxyUrl}"`); } +<<<<<<< HEAD if (!['http:', 'https:', ...SOCKS_PROTOCOLS].includes(handlerOpts.upstreamProxyUrlParsed.protocol)) { throw new Error(`Invalid "upstreamProxyUrl" provided: URL must have one of the following protocols: "http", "https", ${SOCKS_PROTOCOLS.map((p) => `"${p.replace(':', '')}"`).join(', ')} (was "${funcResult.upstreamProxyUrl}")`); } @@ -475,6 +628,14 @@ export class Server extends EventEmitter { handlerOpts.ignoreUpstreamProxyCertificate = funcResult.ignoreUpstreamProxyCertificate; } +======= + if (handlerOpts.upstreamProxyUrlParsed.protocol !== 'http:') { + // eslint-disable-next-line max-len + throw new Error(`Invalid "upstreamProxyUrl" provided: URL must have the "http" protocol (was "${funcResult.upstreamProxyUrl}")`); + } + } + +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) const { proxyChainId } = request.socket as Socket; if (funcResult.customResponseFunction) { @@ -517,7 +678,12 @@ export class Server extends EventEmitter { this.emit('requestFailed', { error, request }); } +<<<<<<< HEAD this.log(proxyChainId, 'Closing because request failed with error'); +======= + // Emit 'connectionClosed' event if request failed and connection was already reported + this.log(proxyChainId, 'Closed because request failed with error'); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } /** @@ -542,6 +708,10 @@ export class Server extends EventEmitter { headers.date = (new Date()).toUTCString(); headers['content-length'] = String(Buffer.byteLength(message)); +<<<<<<< HEAD +======= + // TODO: we should use ??= here +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) headers.server = headers.server || this.authRealm; headers['content-type'] = headers['content-type'] || 'text/plain; charset=utf-8'; @@ -570,8 +740,15 @@ export class Server extends EventEmitter { /** * Starts listening at a port specified in the constructor. +<<<<<<< HEAD */ async listen(callback?: (error: NodeJS.ErrnoException | null) => void): Promise { +======= + * @param callback Optional callback + * @return {(Promise|undefined)} + */ + listen(callback?: (error: NodeJS.ErrnoException | null) => void): Promise { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) const promise = new Promise((resolve, reject) => { // Unfortunately server.listen() is not a normal function that fails on error, // so we need this trickery @@ -593,7 +770,11 @@ export class Server extends EventEmitter { this.server.on('error', onError); this.server.on('listening', onListening); +<<<<<<< HEAD this.server.listen(this.port, this.host); +======= + this.server.listen(this.port); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) }); return nodeify(promise, callback); @@ -602,14 +783,22 @@ export class Server extends EventEmitter { /** * Gets array of IDs of all active connections. */ +<<<<<<< HEAD getConnectionIds(): number[] { +======= + getConnectionIds(): unknown[] { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) return [...this.connections.keys()]; } /** * Gets data transfer statistics of a specific proxy connection. */ +<<<<<<< HEAD getConnectionStats(connectionId: number): ConnectionStats | undefined { +======= + getConnectionStats(connectionId: unknown): ConnectionStats | undefined { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) const socket = this.connections.get(connectionId); if (!socket) return undefined; @@ -626,6 +815,7 @@ export class Server extends EventEmitter { } /** +<<<<<<< HEAD * Forcibly close a specific pending proxy connection. */ closeConnection(connectionId: number): void { @@ -657,13 +847,31 @@ export class Server extends EventEmitter { * @param closeConnections If true, pending proxy connections are forcibly closed. */ async close(closeConnections: boolean, callback?: (error: NodeJS.ErrnoException | null) => void): Promise { +======= + * Closes the proxy server. + * @param closeConnections If true, pending proxy connections are forcibly closed. + */ + close(closeConnections: boolean, callback?: (error: NodeJS.ErrnoException | null) => void): Promise { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) if (typeof closeConnections === 'function') { callback = closeConnections; closeConnections = false; } if (closeConnections) { +<<<<<<< HEAD this.closeConnections(); +======= + this.log(null, 'Closing pending sockets'); + + for (const socket of this.connections.values()) { + socket.destroy(); + } + + this.connections.clear(); + + this.log(null, `Destroyed ${this.connections.size} pending sockets`); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } if (this.server) { diff --git a/src/socket.d.ts b/src/socket.d.ts new file mode 100644 index 00000000..cb0c059e --- /dev/null +++ b/src/socket.d.ts @@ -0,0 +1,7 @@ +import type net from 'net'; +import type tls from 'tls'; + +type AdditionalProps = { proxyChainId?: unknown }; + +export type Socket = net.Socket & AdditionalProps; +export type TLSSocket = tls.TLSSocket & AdditionalProps; diff --git a/src/tcp_tunnel_tools.ts b/src/tcp_tunnel_tools.ts index f3c2e001..9be66ca0 100644 --- a/src/tcp_tunnel_tools.ts +++ b/src/tcp_tunnel_tools.ts @@ -1,6 +1,11 @@ +<<<<<<< HEAD import net from 'node:net'; import { URL } from 'node:url'; +======= +import { URL } from 'url'; +import net from 'net'; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) import { chain } from './chain'; import { nodeify } from './utils/nodeify'; @@ -16,18 +21,31 @@ const getAddress = (server: net.Server) => { return `${host}:${port}`; }; +<<<<<<< HEAD export async function createTunnel( proxyUrl: string, targetHost: string, options?: { verbose?: boolean; ignoreProxyCertificate?: boolean; +======= +export function createTunnel( + proxyUrl: string, + targetHost: string, + options: { + verbose?: boolean; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) }, callback?: (error: Error | null, result?: string) => void, ): Promise { const parsedProxyUrl = new URL(proxyUrl); +<<<<<<< HEAD if (!['http:', 'https:'].includes(parsedProxyUrl.protocol)) { throw new Error(`The proxy URL must have the "http" or "https" protocol (was "${proxyUrl}")`); +======= + if (parsedProxyUrl.protocol !== 'http:') { + throw new Error(`The proxy URL must have the "http" protocol (was "${proxyUrl}")`); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } const url = new URL(`connect://${targetHost || ''}`); @@ -42,13 +60,21 @@ export async function createTunnel( const verbose = options && options.verbose; +<<<<<<< HEAD const server: net.Server & { log?: (...args: unknown[]) => void } = net.createServer(); +======= + const server = net.createServer(); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) const log = (...args: unknown[]): void => { if (verbose) console.log(...args); }; +<<<<<<< HEAD server.log = log; +======= + (server as any).log = log; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) server.on('connection', (sourceSocket) => { const remoteAddress = `${sourceSocket.remoteAddress}:${sourceSocket.remotePort}`; @@ -68,10 +94,14 @@ export async function createTunnel( chain({ request: { url: targetHost }, sourceSocket, +<<<<<<< HEAD handlerOpts: { upstreamProxyUrlParsed: parsedProxyUrl, ignoreUpstreamProxyCertificate: options?.ignoreProxyCertificate ?? false, }, +======= + handlerOpts: { upstreamProxyUrlParsed: parsedProxyUrl }, +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) server: server as net.Server & { log: typeof log }, isPlain: true, }); @@ -96,7 +126,11 @@ export async function createTunnel( return nodeify(promise, callback); } +<<<<<<< HEAD export async function closeTunnel( +======= +export function closeTunnel( +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) serverPath: string, closeConnections: boolean | undefined, callback: (error: Error | null, result?: boolean) => void, @@ -106,6 +140,7 @@ export async function closeTunnel( if (!port) throw new Error('serverPath must contain port'); const promise = new Promise((resolve) => { +<<<<<<< HEAD if (!runningServers[serverPath]) { resolve(false); return; @@ -114,16 +149,25 @@ export async function closeTunnel( resolve(true); return; } +======= + if (!runningServers[serverPath]) return resolve(false); + if (!closeConnections) return resolve(true); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) for (const connection of runningServers[serverPath].connections) { connection.destroy(); } resolve(true); }) +<<<<<<< HEAD .then(async (serverExists) => new Promise((resolve) => { if (!serverExists) { resolve(false); return; } +======= + .then((serverExists) => new Promise((resolve) => { + if (!serverExists) return resolve(false); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) runningServers[serverPath].server.close(() => { delete runningServers[serverPath]; resolve(true); diff --git a/src/utils/count_target_bytes.ts b/src/utils/count_target_bytes.ts index 983021b6..659efe14 100644 --- a/src/utils/count_target_bytes.ts +++ b/src/utils/count_target_bytes.ts @@ -1,4 +1,8 @@ +<<<<<<< HEAD import type net from 'node:net'; +======= +import net from 'net'; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) const targetBytesWritten = Symbol('targetBytesWritten'); const targetBytesRead = Symbol('targetBytesRead'); @@ -7,6 +11,7 @@ const calculateTargetStats = Symbol('calculateTargetStats'); type Stats = { bytesWritten: number | null, bytesRead: number | null }; +<<<<<<< HEAD /** * Socket object extended with previous read and written bytes. * Necessary due to target socket re-use. @@ -17,11 +22,18 @@ interface Extras { [targetBytesWritten]: number; [targetBytesRead]: number; [targets]: Set; +======= +interface Extras { + [targetBytesWritten]: number; + [targetBytesRead]: number; + [targets]: Set; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) [calculateTargetStats]: () => Stats; } // @ts-expect-error TS is not aware that `source` is used in the assertion. // eslint-disable-next-line @typescript-eslint/no-empty-function +<<<<<<< HEAD function typeSocket(source: unknown): asserts source is net.Socket & Extras {} export const countTargetBytes = ( @@ -29,12 +41,18 @@ export const countTargetBytes = ( target: SocketWithPreviousStats, registerCloseHandler?: (handler: () => void) => void, ): void => { +======= +function typeSocket(source: unknown): asserts source is net.Socket & Extras {}; + +export const countTargetBytes = (source: net.Socket, target: net.Socket): void => { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) typeSocket(source); source[targetBytesWritten] = source[targetBytesWritten] || 0; source[targetBytesRead] = source[targetBytesRead] || 0; source[targets] = source[targets] || new Set(); +<<<<<<< HEAD source[targets].add(target); const closeHandler = () => { @@ -46,6 +64,14 @@ export const countTargetBytes = ( registerCloseHandler = (handler: () => void) => target.once('close', handler); } registerCloseHandler(closeHandler); +======= + target.once('close', () => { + source[targetBytesWritten] += target.bytesWritten; + source[targetBytesRead] += target.bytesRead; + + source[targets].delete(target); + }); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) if (!source[calculateTargetStats]) { source[calculateTargetStats] = () => { @@ -53,8 +79,13 @@ export const countTargetBytes = ( let bytesRead = source[targetBytesRead]; for (const socket of source[targets]) { +<<<<<<< HEAD bytesWritten += (socket.bytesWritten - (socket.previousBytesWritten || 0)); bytesRead += (socket.bytesRead - (socket.previousBytesRead || 0)); +======= + bytesWritten += socket.bytesWritten; + bytesRead += socket.bytesRead; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } return { diff --git a/src/utils/decode_uri_component_safe.ts b/src/utils/decode_uri_component_safe.ts index b7734de0..776cf827 100644 --- a/src/utils/decode_uri_component_safe.ts +++ b/src/utils/decode_uri_component_safe.ts @@ -1,7 +1,11 @@ export const decodeURIComponentSafe = (encodedURIComponent: string): string => { try { return decodeURIComponent(encodedURIComponent); +<<<<<<< HEAD } catch { +======= + } catch (e) { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) return encodedURIComponent; } }; diff --git a/src/utils/get_basic.ts b/src/utils/get_basic.ts index 6ccbf608..94f94908 100644 --- a/src/utils/get_basic.ts +++ b/src/utils/get_basic.ts @@ -1,5 +1,9 @@ +<<<<<<< HEAD import type { URL } from 'node:url'; +======= +import { URL } from 'url'; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) import { decodeURIComponentSafe } from './decode_uri_component_safe'; export const getBasicAuthorizationHeader = (url: URL): string => { diff --git a/src/utils/nodeify.ts b/src/utils/nodeify.ts index 378124b8..a340f1b4 100644 --- a/src/utils/nodeify.ts +++ b/src/utils/nodeify.ts @@ -1,10 +1,18 @@ // Replacement for Bluebird's Promise.nodeify() +<<<<<<< HEAD export const nodeify = async (promise: Promise, callback?: (error: Error | null, result?: T) => void): Promise => { +======= +export const nodeify = (promise: Promise, callback?: (error: Error | null, result?: T) => void): Promise => { +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) if (typeof callback !== 'function') return promise; promise.then( (result) => callback(null, result), +<<<<<<< HEAD callback, +======= + callback as any, +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) ).catch((error) => { // Need to .catch because it doesn't crash the process on Node.js 14 process.nextTick(() => { diff --git a/src/utils/parse_authorization_header.ts b/src/utils/parse_authorization_header.ts index ab9c52bc..828cc3f1 100644 --- a/src/utils/parse_authorization_header.ts +++ b/src/utils/parse_authorization_header.ts @@ -1,5 +1,8 @@ +<<<<<<< HEAD import { Buffer } from 'node:buffer'; +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) const splitAt = (string: string, index: number) => { return [ index === -1 ? '' : string.substring(0, index), @@ -25,14 +28,18 @@ export const parseAuthorizationHeader = (header: string): Authorization | null = const [type, data] = splitAt(header, header.indexOf(' ')); +<<<<<<< HEAD // https://datatracker.ietf.org/doc/html/rfc7617#page-3 // Note that both scheme and parameter names are matched case- // insensitively. +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) if (type.toLowerCase() !== 'basic') { return { type, data }; } const auth = Buffer.from(data, 'base64').toString(); +<<<<<<< HEAD // https://datatracker.ietf.org/doc/html/rfc7617#page-5 // To receive authorization, the client @@ -54,6 +61,9 @@ export const parseAuthorizationHeader = (header: string): Authorization | null = // This is a non-spec behavior. At Apify there are clients that rely on this. // If you want this behavior changed, please open an issue. const [username, password] = auth.includes(':') ? splitAt(auth, auth.indexOf(':')) : [auth, '']; +======= + const [username, password] = splitAt(auth, auth.indexOf(':')); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) return { type, diff --git a/src/utils/redact_url.ts b/src/utils/redact_url.ts index ca384717..738b6736 100644 --- a/src/utils/redact_url.ts +++ b/src/utils/redact_url.ts @@ -1,4 +1,8 @@ +<<<<<<< HEAD import { URL } from 'node:url'; +======= +import { URL } from 'url'; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) export const redactUrl = (url: string | URL, passwordReplacement = ''): string => { if (typeof url !== 'object') { diff --git a/src/utils/valid_headers_only.ts b/src/utils/valid_headers_only.ts index 936851f6..569d7be7 100644 --- a/src/utils/valid_headers_only.ts +++ b/src/utils/valid_headers_only.ts @@ -1,5 +1,10 @@ +<<<<<<< HEAD import { validateHeaderName, validateHeaderValue } from 'node:http'; +======= +// @ts-expect-error Missing types +import { validateHeaderName, validateHeaderValue } from 'http'; +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) import { isHopByHopHeader } from './is_hop_by_hop_header'; /** @@ -17,16 +22,29 @@ export const validHeadersOnly = (rawHeaders: string[]): string[] => { try { validateHeaderName(name); validateHeaderValue(name, value); +<<<<<<< HEAD } catch { +======= + } catch (error) { + // eslint-disable-next-line no-continue +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) continue; } if (isHopByHopHeader(name)) { +<<<<<<< HEAD +======= + // eslint-disable-next-line no-continue +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) continue; } if (name.toLowerCase() === 'host') { if (containsHost) { +<<<<<<< HEAD +======= + // eslint-disable-next-line no-continue +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) continue; } diff --git a/test/anonymize_proxy.js b/test/anonymize_proxy.js index 9ceeef3a..c69202c6 100644 --- a/test/anonymize_proxy.js +++ b/test/anonymize_proxy.js @@ -9,7 +9,10 @@ const request = require('request'); const express = require('express'); const { anonymizeProxy, closeAnonymizedProxy, listenConnectAnonymizedProxy } = require('../src/index'); +<<<<<<< HEAD const { expectThrowsAsync } = require('./utils/throws_async'); +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) let expressServer; let proxyServer; diff --git a/test/server.js b/test/server.js index 70c0ff99..79076ffa 100644 --- a/test/server.js +++ b/test/server.js @@ -72,8 +72,25 @@ const puppeteerGet = (url, proxyUrl) => { // eslint-disable-next-line global-require const puppeteer = require('puppeteer'); +<<<<<<< HEAD return (async () => { const parsed = proxyUrl ? new URL(proxyUrl) : undefined; +======= + let proxyParams = ''; + if (proxyUrl) { + const parsed = new URL(proxyUrl); + const username = decodeURIComponent(parsed.username); + const password = decodeURIComponent(parsed.password); + + proxyParams += `--proxy-type=http --proxy=${parsed.hostname}:${parsed.port} `; + if (username || password) { + if ((username && !password) || (!username && password)) { + throw new Error('PhantomJS cannot handle proxy only username or password!'); + } + proxyParams += `--proxy-auth=${username}:${password} `; + } + } +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) const browser = await puppeteer.launch({ env: parsed ? { @@ -258,7 +275,7 @@ const createTestSuite = ({ let addToMainProxyServerConnectionIds = true; expect(request).to.be.an('object'); - expect(port).to.be.an('number'); + expect(port).to.be.an('string'); // All the fake hostnames here have a .gov TLD, because without a TLD, // the tests would fail on GitHub Actions. We assume nobody will register @@ -309,7 +326,11 @@ const createTestSuite = ({ expect(trgParsed.hostname).to.be.eql(hostname); expect(trgParsed.pathname).to.be.eql('/some/path'); expect(trgParsed.search).to.be.eql('?query=456'); +<<<<<<< HEAD expect(port).to.be.eql(1234); +======= + expect(port).to.be.eql('1234'); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) return { statusCode: 201, headers: { @@ -461,7 +482,11 @@ const createTestSuite = ({ assert.fail(); }) .catch((err) => { +<<<<<<< HEAD expect(err.message.slice(-3)).to.contain(`${expectedStatusCode}`); +======= + expect(err.message).to.contain(`${expectedStatusCode}`); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) }) .finally(() => { mainProxyServer.removeListener('requestFailed', onRequestFailed); @@ -640,7 +665,11 @@ const createTestSuite = ({ return requestPromised(opts) .then((response) => { if (useMainProxy) { +<<<<<<< HEAD expect(response.statusCode).to.eql(592); +======= + expect(response.statusCode).to.eql(502); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) expect(response.body).to.eql('Bad status!'); } else { expect(response.statusCode).to.eql(55); @@ -1079,12 +1108,20 @@ const createTestSuite = ({ await requestPromised(opts); expect(false).to.be.eql(true); } catch (error) { +<<<<<<< HEAD expect(error.message).to.be.eql('tunneling socket could not be established, statusCode=597'); +======= + expect(error.message).to.be.eql('tunneling socket could not be established, statusCode=502'); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) } } else { const response = await requestPromised(opts); +<<<<<<< HEAD expect(response.statusCode).to.be.eql(597); +======= + expect(response.statusCode).to.be.eql(502); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) expect(response.body).to.be.eql('Invalid colon in username in upstream proxy credentials'); } }); @@ -1338,6 +1375,7 @@ it('supports localAddress', async () => { } }); +<<<<<<< HEAD it('supports https proxy relay', async () => { const target = https.createServer(() => { }); @@ -1576,6 +1614,8 @@ describe('supports ignoreUpstreamProxyCertificate', () => { }); }); +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) // Run all combinations of test parameters const useSslVariants = [ false, diff --git a/test/tcp_tunnel.js b/test/tcp_tunnel.js index 1b9705b4..9ff77c9b 100644 --- a/test/tcp_tunnel.js +++ b/test/tcp_tunnel.js @@ -4,7 +4,10 @@ const http = require('http'); const proxy = require('proxy'); const { createTunnel, closeTunnel } = require('../src/index'); +<<<<<<< HEAD const { expectThrowsAsync } = require('./utils/throws_async'); +======= +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) const destroySocket = (socket) => new Promise((resolve, reject) => { if (!socket || socket.destroyed) return resolve(); @@ -43,6 +46,7 @@ const closeServer = (server, connections) => new Promise((resolve, reject) => { describe('tcp_tunnel.createTunnel', () => { it('throws error if proxyUrl is not in correct format', () => { +<<<<<<< HEAD expectThrowsAsync(async () => { await createTunnel('socks://user:password@whatever.com:123', 'localhost:9000'); }, /must have the "http" protocol/); expectThrowsAsync(async () => { await createTunnel('socks5://user:password@whatever.com', 'localhost:9000'); }, /must have the "http" protocol/); }); @@ -53,6 +57,18 @@ describe('tcp_tunnel.createTunnel', () => { expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12', 'whatever'); }, 'Missing target port'); expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12', 'whatever:'); }, 'Missing target port'); expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12', ':whatever'); }, /Invalid URL/); +======= + assert.throws(() => { createTunnel('socks://user:password@whatever.com:123', 'localhost:9000'); }, /must have the "http" protocol/); + assert.throws(() => { createTunnel('socks5://user:password@whatever.com', 'localhost:9000'); }, /must have the "http" protocol/); + }); + it('throws error if target is not in correct format', () => { + assert.throws(() => { createTunnel('http://user:password@whatever.com:12'); }, 'Missing target hostname'); + assert.throws(() => { createTunnel('http://user:password@whatever.com:12', null); }, 'Missing target hostname'); + assert.throws(() => { createTunnel('http://user:password@whatever.com:12', ''); }, 'Missing target hostname'); + assert.throws(() => { createTunnel('http://user:password@whatever.com:12', 'whatever'); }, 'Missing target port'); + assert.throws(() => { createTunnel('http://user:password@whatever.com:12', 'whatever:'); }, 'Missing target port'); + assert.throws(() => { createTunnel('http://user:password@whatever.com:12', ':whatever'); }, /Invalid URL/); +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) }); it('correctly tunnels to tcp service and then is able to close the connection', () => { const proxyServerConnections = []; diff --git a/test/tools.js b/test/tools.js index 8319453c..c31d2056 100644 --- a/test/tools.js +++ b/test/tools.js @@ -79,10 +79,16 @@ describe('tools.parseAuthorizationHeader()', () => { data: 'dXNlcm5hbWU6', }); +<<<<<<< HEAD // Do not alter this test, see comment in src/utils/parse_authorization_header.ts expect(parse(authStr('Basic', 'username'))).to.eql({ type: 'Basic', username: 'username', +======= + expect(parse(authStr('Basic', 'username'))).to.eql({ + type: 'Basic', + username: '', +>>>>>>> f1bbe42 (release: 2.0.0 (#162)) password: '', data: 'dXNlcm5hbWU=', });