From 60fd130bc87934b8e14e2556eb24d208ecc2dabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Morav=C4=8D=C3=ADk?= Date: Sat, 17 Jan 2026 13:25:58 +0100 Subject: [PATCH] fix: fix bugs --- CLAUDE.md | 84 +++++++++++++++++++++++++ README.md | 12 ++-- src/anonymize_proxy.ts | 8 +-- src/chain.ts | 8 ++- src/chain_socks.ts | 17 ++++- src/custom_response.ts | 2 +- src/direct.ts | 17 ++++- src/forward.ts | 4 +- src/forward_socks.ts | 2 +- src/server.ts | 12 ++-- src/statuses.ts | 2 +- src/tcp_tunnel_tools.ts | 8 ++- src/utils/is_hop_by_hop_header.ts | 2 +- src/utils/parse_authorization_header.ts | 2 +- src/utils/redact_url.ts | 6 +- test/anonymize_proxy.js | 35 +++++------ test/server.js | 2 +- test/socks.js | 2 +- test/tcp_tunnel.js | 22 +++---- test/tools.js | 13 ++-- 20 files changed, 192 insertions(+), 68 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..e0bc38bb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,84 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +proxy-chain is a programmable HTTP/HTTPS proxy server for Node.js with support for SSL/TLS, authentication, upstream proxy chaining (HTTP/HTTPS/SOCKS), custom HTTP responses, and traffic statistics. It's used by Apify Proxy and the Crawlee web scraping library. + +## Build & Development Commands + +```bash +npm run build # Compile TypeScript to dist/ +npm run build:watch # Watch mode compilation +npm run lint # ESLint check +npm run lint:fix # ESLint auto-fix +npm run local-proxy # Run local proxy server for testing +``` + +## Testing + +Docker-based testing is recommended due to Linux/macOS socket handling differences: + +```bash +npm run test:docker # Run all tests in Docker +npm run test:docker test/server.js # Run specific test file +npm run test:docker -- --grep "direct ipv6" # Run tests matching pattern +``` + +Local testing requires `/etc/hosts` entry: +``` +127.0.0.1 localhost-test +``` + +```bash +npm run test # Run all tests locally +npm run test test/anonymize_proxy.js # Run specific test file +``` + +Tests use Mocha with ts-node. Coverage via nyc. + +## Architecture + +### Request Flow + +1. `Server` class (server.ts) receives HTTP requests or CONNECT tunnels +2. User-provided `prepareRequestFunction` determines authentication and routing +3. Request is dispatched to the appropriate handler based on protocol and upstream type + +### Handler Modules + +- **direct.ts** - Direct CONNECT tunneling to target (no upstream proxy) +- **forward.ts** - HTTP forwarding to target or upstream HTTP/HTTPS proxy +- **chain.ts** - CONNECT tunneling through upstream HTTP/HTTPS proxy +- **chain_socks.ts** - CONNECT tunneling through SOCKS proxy +- **forward_socks.ts** - HTTP forwarding through SOCKS proxy +- **custom_response.ts** - Generate custom HTTP responses without contacting upstream +- **custom_connect.ts** - Route CONNECT requests to custom HTTP server + +### Key Source Files + +- **server.ts** - Main `Server` class (EventEmitter), handles connection lifecycle +- **statuses.ts** - Custom HTTP status codes 590-599 for proxy-specific errors +- **request_error.ts** - `RequestError` class for custom error responses +- **anonymize_proxy.ts** - Helper to create local anonymous proxy for authenticated upstreams +- **tcp_tunnel_tools.ts** - `createTunnel`/`closeTunnel` for TCP tunneling + +### Connection Tracking + +Each connection gets a unique ID. Statistics tracked per connection: `srcTxBytes`, `srcRxBytes`, `trgTxBytes`, `trgRxBytes`. Access via `server.getConnectionStats(connectionId)`. + +### Server Types + +- `serverType: 'http'` (default) - Standard HTTP proxy +- `serverType: 'https'` - HTTPS proxy requiring `httpsOptions: { key, cert }` + +## Custom Status Codes + +- 590: Upstream non-200 response +- 593: DNS lookup failed +- 594: Connection refused +- 595: Connection reset +- 596: Broken pipe +- 597: Auth failed +- 599: Generic upstream error diff --git a/README.md b/README.md index cb682c1b..c89cc570 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ For details, read [How to make headless Chrome and Puppeteer use a proxy server The proxy-chain package is developed by [Apify](https://apify.com/), the full-stack web scraping and data extraction platform, to support their [Apify Proxy](https://apify.com/proxy) product, which provides an easy access to a large pool of datacenter and residential IP addresses all around the world. The proxy-chain package is also used by [Crawlee](https://crawlee.dev/), -the world's most popular web craling library for Node.js. +the world's most popular web crawling library for Node.js. The proxy-chain package currently supports HTTP/SOCKS forwarding and HTTP CONNECT tunneling to forward arbitrary protocols such as HTTPS or FTP ([learn more](https://blog.apify.com/tunneling-arbitrary-protocols-over-http-proxy-with-static-ip-address-b3a2222191ff)). The HTTP CONNECT tunneling also supports the SOCKS protocol. Also, proxy-chain only supports the Basic [Proxy-Authorization](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authorization). @@ -64,7 +64,7 @@ const server = new ProxyChain.Server({ // * connectionId - Unique ID of the HTTP connection. It can be used to obtain traffic statistics. prepareRequestFunction: ({ request, username, password, hostname, port, isHttp, connectionId }) => { return { - // If set to true, the client is sent HTTP 407 resposne with the Proxy-Authenticate header set, + // If set to true, the client is sent HTTP 407 response with the Proxy-Authenticate header set, // requiring Basic authentication. Here you can verify user credentials. requestAuthentication: username !== 'bob' || password !== 'TopSecret', @@ -78,7 +78,7 @@ const server = new ProxyChain.Server({ // Applies to HTTPS upstream proxy. If set to true, requests made to the proxy will // ignore certificate errors. Useful when upstream proxy uses self-signed certificate. By default "false". - ignoreUpstreamProxyCertificate: true + ignoreUpstreamProxyCertificate: true, // If "requestAuthentication" is true, you can use the following property // to define a custom error message to return to the client instead of the default "Proxy credentials required" @@ -131,7 +131,7 @@ const ProxyChain = require('proxy-chain'); // -> listen for 'connection' events to track raw TCP sockets // // https: - // -> listen for 'securedConnection' events (instead of 'connection') to track only post-TLS-handshake sockets + // -> listen for 'secureConnection' events (instead of 'connection') to track only post-TLS-handshake sockets // -> additionally listen for 'tlsError' events to handle TLS handshake errors // // Default value is 'http' @@ -263,7 +263,7 @@ Upstream responded with non-200 status code. ### `592 Status Code Out Of Range` -Upstream respondend with status code different than 100-999. +Upstream responded with status code different than 100-999. ### `593 Not Found` @@ -482,7 +482,7 @@ server.on('tunnelConnectResponded', ({ proxyChainId, response, socket, head, cus }); ``` -Alternatively a [helper function](##helper-functions) may be used: +Alternatively a [helper function](#helper-functions) may be used: ```javascript listenConnectAnonymizedProxy(anonymizedProxyUrl, ({ response, socket, head }) => { diff --git a/src/anonymize_proxy.ts b/src/anonymize_proxy.ts index 9b7cbd8e..424652d1 100644 --- a/src/anonymize_proxy.ts +++ b/src/anonymize_proxy.ts @@ -22,7 +22,7 @@ export interface AnonymizeProxyOptions { */ export const anonymizeProxy = async ( options: string | AnonymizeProxyOptions, - callback?: (error: Error | null) => void, + callback?: (error: Error | null, result?: string) => void, ): Promise => { let proxyUrl: string; let port = 0; @@ -32,11 +32,11 @@ export const anonymizeProxy = async ( proxyUrl = options; } else { proxyUrl = options.url; - port = options.port; + port = options.port ?? 0; if (port < 0 || port > 65535) { throw new Error( - 'Invalid "port" option: only values equals or between 0-65535 are valid', + 'Invalid "port" option: only values between 0-65535 are valid', ); } @@ -88,7 +88,7 @@ export const anonymizeProxy = async ( /** * Closes anonymous proxy previously started by `anonymizeProxy()`. * If proxy was not found or was already closed, the function has no effect - * and its result if `false`. Otherwise the result is `true`. + * and its result is `false`. Otherwise the result is `true`. * @param closeConnections If true, pending proxy connections are forcibly closed. */ export const closeAnonymizedProxy = async ( diff --git a/src/chain.ts b/src/chain.ts index 1cf64b58..8b2348d4 100644 --- a/src/chain.ts +++ b/src/chain.ts @@ -163,7 +163,13 @@ export const chain = ( head: clientHead, }); - sourceSocket.write(isPlain ? '' : `HTTP/1.1 200 Connection Established\r\n\r\n`); + try { + sourceSocket.write(isPlain ? '' : `HTTP/1.1 200 Connection Established\r\n\r\n`); + } catch (error) { + sourceSocket.destroy(error as Error); + targetSocket.destroy(); + return; + } sourceSocket.pipe(targetSocket); targetSocket.pipe(sourceSocket); diff --git a/src/chain_socks.ts b/src/chain_socks.ts index 1b03c70c..d4b36d8e 100644 --- a/src/chain_socks.ts +++ b/src/chain_socks.ts @@ -56,7 +56,7 @@ export const chainSocks = async ({ const proxy: SocksProxy = { host: hostname, - port: Number(port), + port: port ? Number(port) : 1080, // Default SOCKS port is 1080 type: socksProtocolToVersionNumber(handlerOpts.upstreamProxyUrlParsed.protocol), userId: decodeURIComponent(username), password: decodeURIComponent(password), @@ -74,9 +74,14 @@ export const chainSocks = async ({ } const url = new URL(`connect://${request.url}`); + let host = url.hostname; + // Strip IPv6 brackets if present (e.g., [::1] -> ::1) + if (host[0] === '[') { + host = host.slice(1, -1); + } const destination = { port: Number(url.port), - host: url.hostname, + host, }; let targetSocket: net.Socket; @@ -89,7 +94,13 @@ export const chainSocks = async ({ }); targetSocket = client.socket; - sourceSocket.write(`HTTP/1.1 200 Connection Established\r\n\r\n`); + try { + sourceSocket.write(`HTTP/1.1 200 Connection Established\r\n\r\n`); + } catch (writeError) { + sourceSocket.destroy(writeError as Error); + targetSocket.destroy(); + return; + } } catch (error) { const socksError = error as SocksClientError; server.log(proxyChainId, `Failed to connect to upstream SOCKS proxy ${socksError.stack}`); diff --git a/src/custom_response.ts b/src/custom_response.ts index 8058f877..44dc3630 100644 --- a/src/custom_response.ts +++ b/src/custom_response.ts @@ -28,7 +28,7 @@ export const handleCustomResponse = async ( throw new Error('The user-provided "customResponseFunction" must return an object.'); } - response.statusCode = customResponse.statusCode || 200; + response.statusCode = customResponse.statusCode ?? 200; if (customResponse.headers) { for (const [key, value] of Object.entries(customResponse.headers)) { diff --git a/src/direct.ts b/src/direct.ts index f4c7d68d..b24bdc6f 100644 --- a/src/direct.ts +++ b/src/direct.ts @@ -5,6 +5,7 @@ import net from 'node:net'; import { URL } from 'node:url'; import type { Socket } from './socket'; +import { badGatewayStatusCodes, createCustomStatusHttpResponse, errorCodeToStatusCode } from './statuses'; import { countTargetBytes } from './utils/count_target_bytes'; export interface HandlerOpts { @@ -67,6 +68,7 @@ export const direct = ( sourceSocket.write(`HTTP/1.1 200 Connection Established\r\n\r\n`); } catch (error) { sourceSocket.destroy(error as Error); + targetSocket.destroy(); } }); @@ -96,11 +98,22 @@ export const direct = ( }); const { proxyChainId } = sourceSocket; + let connected = false; - targetSocket.on('error', (error) => { + targetSocket.once('connect', () => { + connected = true; + }); + + targetSocket.on('error', (error: NodeJS.ErrnoException) => { server.log(proxyChainId, `Direct Destination Socket Error: ${error.stack}`); - sourceSocket.destroy(); + // If we haven't connected yet, send an error response to the client + if (!connected && sourceSocket.writable) { + const statusCode = errorCodeToStatusCode[error.code!] ?? badGatewayStatusCodes.GENERIC_ERROR; + sourceSocket.end(createCustomStatusHttpResponse(statusCode, error.code ?? 'Connection Failed')); + } else { + sourceSocket.destroy(); + } }); sourceSocket.on('error', (error) => { diff --git a/src/forward.ts b/src/forward.ts index 2c9b6ab9..edf1eadd 100644 --- a/src/forward.ts +++ b/src/forward.ts @@ -35,8 +35,8 @@ export interface HandlerOpts { /** * The request is read from the client and is resent. - * This is similar to Direct / Chain, however it uses the CONNECT protocol instead. - * Forward uses standard HTTP methods. + * Unlike Direct / Chain which use the CONNECT protocol for tunneling, + * Forward uses standard HTTP methods (GET, POST, etc.). * * ``` * Client -> Apify (HTTP) -> Web diff --git a/src/forward_socks.ts b/src/forward_socks.ts index 95867586..9ea8c5ad 100644 --- a/src/forward_socks.ts +++ b/src/forward_socks.ts @@ -53,7 +53,7 @@ export const forwardSocks = async ( agent, }; - // Only handling "http" here - since everything else is handeled by tunnelSocks. + // Only handling "http" here - since everything else is handled by chainSocks. // We have to force cast `options` because @types/node doesn't support an array. const client = http.request(request.url!, options as unknown as http.ClientRequestArgs, async (clientResponse) => { try { diff --git a/src/server.ts b/src/server.ts index 6295c724..05220466 100644 --- a/src/server.ts +++ b/src/server.ts @@ -130,10 +130,11 @@ export type ServerOptions = HttpServerOptions | HttpsServerOptions; /** * Represents the proxy server. - * It emits the 'requestFailed' event on unexpected request errors, with the following parameter `{ error, request }`. + * It emits the 'requestFailed' event on unexpected request errors, with parameter `{ error, request }`. * It emits the 'connectionClosed' event when connection to proxy server is closed, with parameter `{ connectionId, stats }`. * It emits the 'tlsError' event on TLS handshake failures (HTTPS servers only), with parameter `{ error, socket }`. - * with parameter `{ connectionId, reason, hasParent, parentType }`. + * It emits the 'tunnelConnectResponded' event on successful CONNECT tunnel establishment, with parameter `{ proxyChainId, response, customTag, socket, head }`. + * It emits the 'tunnelConnectFailed' event when upstream proxy rejects CONNECT request, with parameter `{ proxyChainId, response, customTag, socket, head }`. */ export class Server extends EventEmitter { port: number; @@ -474,7 +475,7 @@ export class Server extends EventEmitter { throw new RequestError(`Target "${request.url}" could not be parsed`, 400); } - // Only HTTP is supported, other protocols such as HTTP or FTP must use the CONNECT method + // Only HTTP is supported, other protocols such as HTTPS or FTP must use the CONNECT method if (parsed.protocol !== 'http:') { throw new RequestError(`Only HTTP protocol is supported (was ${parsed.protocol})`, 400); } @@ -558,7 +559,7 @@ export class Server extends EventEmitter { try { handlerOpts.upstreamProxyUrlParsed = new URL(funcResult.upstreamProxyUrl); } catch (error) { - throw new Error(`Invalid "upstreamProxyUrl" provided: ${error} (was "${funcResult.upstreamProxyUrl}"`); + throw new Error(`Invalid "upstreamProxyUrl" provided: ${error} (was "${funcResult.upstreamProxyUrl}")`); } if (!['http:', 'https:', ...SOCKS_PROTOCOLS].includes(handlerOpts.upstreamProxyUrlParsed.protocol)) { @@ -740,11 +741,12 @@ export class Server extends EventEmitter { closeConnections(): void { this.log(null, 'Closing pending sockets'); + const count = this.connections.size; for (const socket of this.connections.values()) { socket.destroy(); } - this.log(null, `Destroyed ${this.connections.size} pending sockets`); + this.log(null, `Destroyed ${count} pending sockets`); } /** diff --git a/src/statuses.ts b/src/statuses.ts index ecb22eb0..d71b8b10 100644 --- a/src/statuses.ts +++ b/src/statuses.ts @@ -12,7 +12,7 @@ export const badGatewayStatusCodes = { */ NON_200: 590, /** - * Upstream respondend with status code different than 100-999. + * Upstream responded with status code different than 100-999. */ STATUS_CODE_OUT_OF_RANGE: 592, /** diff --git a/src/tcp_tunnel_tools.ts b/src/tcp_tunnel_tools.ts index c2cb9ff0..8f729e9d 100644 --- a/src/tcp_tunnel_tools.ts +++ b/src/tcp_tunnel_tools.ts @@ -2,10 +2,13 @@ import net from 'node:net'; import { URL } from 'node:url'; import { chain } from './chain'; +import type { Socket } from './socket'; import { nodeify } from './utils/nodeify'; const runningServers: Record }> = {}; +let lastConnectionId = 0; + const getAddress = (server: net.Server) => { const { address: host, port, family } = server.address() as net.AddressInfo; @@ -51,9 +54,12 @@ export async function createTunnel( server.log = log; - server.on('connection', (sourceSocket) => { + server.on('connection', (sourceSocket: Socket) => { const remoteAddress = `${sourceSocket.remoteAddress}:${sourceSocket.remotePort}`; + // Assign a unique ID for logging purposes (similar to Server.registerConnection) + sourceSocket.proxyChainId = lastConnectionId++; + const { connections } = runningServers[getAddress(server)]; log(`new client connection from ${remoteAddress}`); diff --git a/src/utils/is_hop_by_hop_header.ts b/src/utils/is_hop_by_hop_header.ts index a2b9ef08..a5a7b0f4 100644 --- a/src/utils/is_hop_by_hop_header.ts +++ b/src/utils/is_hop_by_hop_header.ts @@ -1,4 +1,4 @@ -// As per HTTP specification, hop-by-hop headers should be consumed but the proxy, and not forwarded +// As per HTTP specification, hop-by-hop headers should be consumed by the proxy, and not forwarded const hopByHopHeaders = [ 'connection', 'keep-alive', diff --git a/src/utils/parse_authorization_header.ts b/src/utils/parse_authorization_header.ts index ab9c52bc..a8d9e16f 100644 --- a/src/utils/parse_authorization_header.ts +++ b/src/utils/parse_authorization_header.ts @@ -2,7 +2,7 @@ import { Buffer } from 'node:buffer'; const splitAt = (string: string, index: number) => { return [ - index === -1 ? '' : string.substring(0, index), + index === -1 ? string : string.substring(0, index), index === -1 ? '' : string.substring(index + 1), ]; }; diff --git a/src/utils/redact_url.ts b/src/utils/redact_url.ts index ca384717..8b7e0dbf 100644 --- a/src/utils/redact_url.ts +++ b/src/utils/redact_url.ts @@ -6,7 +6,11 @@ export const redactUrl = (url: string | URL, passwordReplacement = '') } if (url.password) { - return url.href.replace(`:${url.password}`, `:${passwordReplacement}`); + // Use the URL's internal encoded password representation for replacement + // We need to rebuild the userinfo part to handle URL-encoded passwords correctly + const redactedUrl = new URL(url.href); + redactedUrl.password = passwordReplacement; + return redactedUrl.href; } return url.href; diff --git a/test/anonymize_proxy.js b/test/anonymize_proxy.js index 9ceeef3a..3e1fc0e7 100644 --- a/test/anonymize_proxy.js +++ b/test/anonymize_proxy.js @@ -105,24 +105,19 @@ const requestPromised = (opts) => { describe('utils.anonymizeProxy', function () { // Need larger timeout for Travis CI this.timeout(5 * 1000); - it('throws for invalid args', () => { - expectThrowsAsync(async () => { await anonymizeProxy(null); }); - expectThrowsAsync(async () => { await anonymizeProxy(); }); - expectThrowsAsync(async () => { await anonymizeProxy({}); }); - - expectThrowsAsync(async () => { await closeAnonymizedProxy({}); }); - expectThrowsAsync(async () => { await closeAnonymizedProxy(); }); - expectThrowsAsync(async () => { await closeAnonymizedProxy(null); }); + it('throws for invalid args', async () => { + await expectThrowsAsync(async () => { await anonymizeProxy(null); }); + await expectThrowsAsync(async () => { await anonymizeProxy(); }); + await expectThrowsAsync(async () => { await anonymizeProxy({}); }); + + await expectThrowsAsync(async () => { await closeAnonymizedProxy({}); }); + await expectThrowsAsync(async () => { await closeAnonymizedProxy(); }); + await expectThrowsAsync(async () => { await closeAnonymizedProxy(null); }); }); - it('throws for unsupported https: protocol', () => { - expectThrowsAsync(async () => { await anonymizeProxy('https://whatever.com'); }); - expectThrowsAsync(async () => { await anonymizeProxy({ url: 'https://whatever.com' }); }); - }); - - it('throws for invalid ports', () => { - expectThrowsAsync(async () => { await anonymizeProxy({ url: 'http://whatever.com', port: -16 }); }); - expectThrowsAsync(async () => { + it('throws for invalid ports', async () => { + await expectThrowsAsync(async () => { await anonymizeProxy({ url: 'http://whatever.com', port: -16 }); }); + await expectThrowsAsync(async () => { await anonymizeProxy({ url: 'http://whatever.com', port: 4324324324, @@ -130,11 +125,9 @@ describe('utils.anonymizeProxy', function () { }); }); - it('throws for invalid URLs', () => { - expectThrowsAsync(async () => { await anonymizeProxy('://whatever.com'); }); - expectThrowsAsync(async () => { await anonymizeProxy('https://whatever.com'); }); - expectThrowsAsync(async () => { await anonymizeProxy({ url: '://whatever.com' }); }); - expectThrowsAsync(async () => { await anonymizeProxy({ url: 'https://whatever.com' }); }); + it('throws for invalid URLs', async () => { + await expectThrowsAsync(async () => { await anonymizeProxy('://whatever.com'); }); + await expectThrowsAsync(async () => { await anonymizeProxy({ url: '://whatever.com' }); }); }); it('keeps already anonymous proxies (both with callbacks and promises)', () => { diff --git a/test/server.js b/test/server.js index 8089dd08..faeccbab 100644 --- a/test/server.js +++ b/test/server.js @@ -828,7 +828,7 @@ const createTestSuite = ({ const stats = mainProxyServer.getConnectionStats(Number(lastConnectionId)) || mainProxyServerConnectionId2Stats[lastConnectionId]; - // 5% range because network negotiation adds to network trafic + // 5% range because network negotiation adds to network traffic expect(stats.srcTxBytes).to.be.within(expectedSize, expectedSize * 1.05); expect(stats.trgRxBytes).to.be.within(expectedSize, expectedSize * 1.05); } diff --git a/test/socks.js b/test/socks.js index 2a9e8dc8..e7a81ed4 100644 --- a/test/socks.js +++ b/test/socks.js @@ -44,7 +44,7 @@ describe('SOCKS protocol', () => { }); }).timeout(10 * 1000); - it('work with auth', (done) => { + it('works with auth', (done) => { portastic.find({ min: 50250, max: 50500 }).then((ports) => { const [socksPort, proxyPort] = ports; socksServer = socksv5.createServer((info, accept) => { diff --git a/test/tcp_tunnel.js b/test/tcp_tunnel.js index 1b9705b4..c01207ef 100644 --- a/test/tcp_tunnel.js +++ b/test/tcp_tunnel.js @@ -33,7 +33,7 @@ const connect = (port) => new Promise((resolve, reject) => { const closeServer = (server, connections) => new Promise((resolve, reject) => { if (!server || !server.listening) return resolve(); - Promise.all(connections, destroySocket).then(() => { + Promise.all(connections.map(destroySocket)).then(() => { server.close((err) => { if (err) return reject(err); return resolve(); @@ -42,17 +42,17 @@ const closeServer = (server, connections) => new Promise((resolve, reject) => { }); describe('tcp_tunnel.createTunnel', () => { - it('throws error if proxyUrl is not in correct format', () => { - 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/); + it('throws error if proxyUrl is not in correct format', async () => { + await expectThrowsAsync(async () => { await createTunnel('socks://user:password@whatever.com:123', 'localhost:9000'); }, /must have the "http" protocol/); + await expectThrowsAsync(async () => { await createTunnel('socks5://user:password@whatever.com', 'localhost:9000'); }, /must have the "http" protocol/); }); - it('throws error if target is not in correct format', () => { - expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12'); }, 'Missing target hostname'); - expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12', null); }, 'Missing target hostname'); - expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12', ''); }, 'Missing target hostname'); - 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/); + it('throws error if target is not in correct format', async () => { + await expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12'); }, 'Missing target hostname'); + await expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12', null); }, 'Missing target hostname'); + await expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12', ''); }, 'Missing target hostname'); + await expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12', 'whatever'); }, 'Missing target port'); + await expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12', 'whatever:'); }, 'Missing target port'); + await expectThrowsAsync(async () => { await createTunnel('http://user:password@whatever.com:12', ':whatever'); }, /Invalid URL/); }); 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..ab35e5a2 100644 --- a/test/tools.js +++ b/test/tools.js @@ -114,8 +114,9 @@ describe('tools.parseAuthorizationHeader()', () => { expect(parse('')).to.eql(null); expect(parse(' ')).to.eql(null); + // When there's no space, the entire string is the type (auth-scheme) expect(parse('whatever')).to.eql({ - type: '', + type: 'whatever', data: '', }); @@ -124,13 +125,17 @@ describe('tools.parseAuthorizationHeader()', () => { data: 'bla bla', }); + // authStr('Basic', '') produces "Basic " - empty base64 expect(parse(authStr('Basic', ''))).to.eql({ - type: '', + type: 'Basic', + username: '', + password: '', data: '', }); + // When there's no space, the entire string is the type expect(parse('123124')).to.eql({ - type: '', + type: '123124', data: '', }); }); @@ -177,7 +182,7 @@ describe('tools.nodeify()', () => { const promise = asyncFunction(true); await new Promise((resolve) => { nodeify(promise, (error, result) => { - expect(result, undefined); + expect(result).to.be.undefined; expect(error.message).to.eql('Test error'); resolve(); });