From 5e4b32acb124308ebc947bb23f07552484a7490f Mon Sep 17 00:00:00 2001 From: Szymon Marczak <36894700+szmarczak@users.noreply.github.com> Date: Thu, 29 Sep 2022 14:57:22 +0200 Subject: [PATCH] feat: custom bad gateway statuses (#316) --- README.md | 52 +++++++++++++++++++++++++++++++++++ package.json | 2 +- src/chain.ts | 18 +++++++++---- src/forward.ts | 13 +++------ src/server.ts | 5 ++-- src/statuses.ts | 60 +++++++++++++++++++++++++++++++++++++++++ test/anonymize_proxy.js | 2 +- test/server.js | 19 ++++++------- 8 files changed, 144 insertions(+), 27 deletions(-) create mode 100644 src/statuses.ts diff --git a/README.md b/README.md index f1762903..1868cc8b 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,58 @@ server.on('requestFailed', ({ request, error }) => { }); ``` +## A different approach to `502 Bad Gateway` + +`502` status code is not comprehensive enough. Therefore, the server may respond with `590-599` instead: + +### `590 Non Successful` + +Upstream responded with non-200 status code. + +### `591 RESERVED` + +*This status code is reserved for further use.* + +### `592 Status Code Out Of Range` + +Upstream respondend with status code different than 100-999. + +### `593 Not Found` + +DNS lookup failed - [`EAI_NODATA`](https://github.com/libuv/libuv/blob/cdbba74d7a756587a696fb3545051f9a525b85ac/include/uv.h#L82) or [`EAI_NONAME`](https://github.com/libuv/libuv/blob/cdbba74d7a756587a696fb3545051f9a525b85ac/include/uv.h#L83). + +### `594 Connection Refused` + +Upstream refused connection. + +### `595 Connection Reset` + +Connection reset due to loss of connection or timeout. + +### `596 Broken Pipe` + +Trying to write on a closed socket. + +### `597 Auth Failed` + +Incorrect upstream credentials. + +### `598 RESERVED` + +*This status code is reserved for further use.* + +### `599 Upstream Error` + +Generic upstream error. + +--- + +`590` and `592` indicate an issue on the upstream side. \ +`593` indicates an incorrect `proxy-chain` configuration.\ +`594`, `595` and `596` may occur due to connection loss.\ +`597` indicates incorrect upstream credentials.\ +`599` is a generic error, where the above is not applicable. + ## Custom error responses To return a custom HTTP response to indicate an error to the client, diff --git a/package.json b/package.json index 36794044..c59b868e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxy-chain", - "version": "2.0.7", + "version": "2.1.0", "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": [ diff --git a/src/chain.ts b/src/chain.ts index 5f47ada0..9f70cbb6 100644 --- a/src/chain.ts +++ b/src/chain.ts @@ -6,10 +6,11 @@ import { Buffer } from 'buffer'; import { countTargetBytes } from './utils/count_target_bytes'; import { getBasicAuthorizationHeader } from './utils/get_basic'; import { Socket } from './socket'; +import { badGatewayStatusCodes, errorCodeToStatusCode } from './statuses'; -const createHttpResponse = (statusCode: number, message: string) => { +const createHttpResponse = (statusCode: number, statusMessage: string, message = '') => { return [ - `HTTP/1.1 ${statusCode} ${http.STATUS_CODES[statusCode] || 'Unknown Status Code'}`, + `HTTP/1.1 ${statusCode} ${statusMessage || http.STATUS_CODES[statusCode] || 'Unknown Status Code'}`, 'Connection: close', `Date: ${(new Date()).toUTCString()}`, `Content-Length: ${Buffer.byteLength(message)}`, @@ -118,7 +119,12 @@ export const chain = ( if (isPlain) { sourceSocket.end(); } else { - sourceSocket.end(createHttpResponse(502, '')); + const { statusCode } = response; + const status = statusCode === 401 || statusCode === 407 + ? badGatewayStatusCodes.AUTH_FAILED + : badGatewayStatusCodes.NON_200; + + sourceSocket.end(createHttpResponse(status, `UPSTREAM${response.statusCode}`)); } return; @@ -162,7 +168,7 @@ export const chain = ( }); }); - client.on('error', (error) => { + 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. @@ -170,7 +176,9 @@ export const chain = ( if (isPlain) { sourceSocket.end(); } else { - sourceSocket.end(createHttpResponse(502, '')); + const statusCode = errorCodeToStatusCode[error.code!] ?? badGatewayStatusCodes.GENERIC_ERROR; + const response = createHttpResponse(statusCode, error.code ?? 'Upstream Closed Early'); + sourceSocket.end(response); } } }); diff --git a/src/forward.ts b/src/forward.ts index d4d532b6..83f481fe 100644 --- a/src/forward.ts +++ b/src/forward.ts @@ -7,6 +7,7 @@ import { URL } from 'url'; import { validHeadersOnly } from './utils/valid_headers_only'; import { getBasicAuthorizationHeader } from './utils/get_basic'; import { countTargetBytes } from './utils/count_target_bytes'; +import { badGatewayStatusCodes, errorCodeToStatusCode } from './statuses'; const pipeline = util.promisify(stream.pipeline); @@ -84,7 +85,7 @@ export const forward = async ( // This is necessary to prevent Node.js throwing an error let statusCode = clientResponse.statusCode!; if (statusCode < 100 || statusCode > 999) { - statusCode = 502; + statusCode = badGatewayStatusCodes.STATUS_CODE_OUT_OF_RANGE; } // 407 is handled separately @@ -123,15 +124,9 @@ export const forward = async ( return; } - const statuses: {[code: string]: number | undefined} = { - ENOTFOUND: proxy ? 502 : 404, - ECONNREFUSED: 502, - ECONNRESET: 502, - EPIPE: 502, - ETIMEDOUT: 504, - }; + const statusCode = errorCodeToStatusCode[error.code!] ?? badGatewayStatusCodes.GENERIC_ERROR; - response.statusCode = statuses[error.code!] ?? 502; + response.statusCode = !proxy && error.code === 'ENOTFOUND' ? 404 : statusCode; response.setHeader('content-type', 'text/plain; charset=utf-8'); response.end(http.STATUS_CODES[response.statusCode]); diff --git a/src/server.ts b/src/server.ts index 11e7b439..52bea1c3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,6 +16,7 @@ import { direct } from './direct'; import { handleCustomResponse, HandlerOpts as CustomResponseOpts } from './custom_response'; import { Socket } from './socket'; import { normalizeUrlPort } from './utils/normalize_url_port'; +import { badGatewayStatusCodes } from './statuses'; // TODO: // - Implement this requirement from rfc7230 @@ -228,11 +229,11 @@ export class Server extends EventEmitter { */ normalizeHandlerError(error: NodeJS.ErrnoException): NodeJS.ErrnoException { if (error.message === 'Username contains an invalid colon') { - return new RequestError('Invalid colon in username in upstream proxy credentials', 502); + 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', 502); + return new RequestError('Invalid upstream proxy credentials', badGatewayStatusCodes.AUTH_FAILED); } return error; diff --git a/src/statuses.ts b/src/statuses.ts new file mode 100644 index 00000000..5defcc98 --- /dev/null +++ b/src/statuses.ts @@ -0,0 +1,60 @@ +import { STATUS_CODES } from 'http'; + +type HttpStatusCode = number; + +export const badGatewayStatusCodes = { + /** + * Upstream has timed out. + */ + TIMEOUT: 504, + /** + * Upstream responded with non-200 status code. + */ + NON_200: 590, + /** + * Upstream respondend with status code different than 100-999. + */ + STATUS_CODE_OUT_OF_RANGE: 592, + /** + * DNS lookup failed - EAI_NODATA or EAI_NONAME. + */ + NOT_FOUND: 593, + /** + * Upstream refused connection. + */ + CONNECTION_REFUSED: 594, + /** + * Connection reset due to loss of connection or timeout. + */ + CONNECTION_RESET: 595, + /** + * Trying to write on a closed socket. + */ + BROKEN_PIPE: 596, + /** + * Incorrect upstream credentials. + */ + AUTH_FAILED: 597, + /** + * Generic upstream error. + */ + GENERIC_ERROR: 599, +} as const; + +STATUS_CODES['590'] = 'Non Successful'; +STATUS_CODES['592'] = 'Status Code Out Of Range'; +STATUS_CODES['593'] = 'Not Found'; +STATUS_CODES['594'] = 'Connection Refused'; +STATUS_CODES['595'] = 'Connection Reset'; +STATUS_CODES['596'] = 'Broken Pipe'; +STATUS_CODES['597'] = 'Auth Failed'; +STATUS_CODES['599'] = 'Upstream Error'; + +// https://nodejs.org/api/errors.html#common-system-errors +export const errorCodeToStatusCode: {[errorCode: string]: HttpStatusCode | undefined} = { + ENOTFOUND: badGatewayStatusCodes.NOT_FOUND, + ECONNREFUSED: badGatewayStatusCodes.CONNECTION_REFUSED, + ECONNRESET: badGatewayStatusCodes.CONNECTION_RESET, + EPIPE: badGatewayStatusCodes.BROKEN_PIPE, + ETIMEDOUT: badGatewayStatusCodes.TIMEOUT, +} as const; diff --git a/test/anonymize_proxy.js b/test/anonymize_proxy.js index 79bfb575..16ebabc2 100644 --- a/test/anonymize_proxy.js +++ b/test/anonymize_proxy.js @@ -418,7 +418,7 @@ describe('utils.anonymizeProxy', function () { assert.fail(); }) .catch((err) => { - expect(err.message).to.contains('Received invalid response code: 502'); // Gateway error + expect(err.message).to.contains('Received invalid response code: 597'); // Gateway error expect(wasProxyCalled).to.equal(false); }) .then(() => closeAnonymizedProxy(anonymousProxyUrl, true)) diff --git a/test/server.js b/test/server.js index cef5acf8..92480c59 100644 --- a/test/server.js +++ b/test/server.js @@ -460,7 +460,7 @@ const createTestSuite = ({ assert.fail(); }) .catch((err) => { - expect(err.message).to.contain(`${expectedStatusCode}`); + expect(err.message.slice(-3)).to.contain(`${expectedStatusCode}`); }) .finally(() => { mainProxyServer.removeListener('requestFailed', onRequestFailed); @@ -639,7 +639,7 @@ const createTestSuite = ({ return requestPromised(opts) .then((response) => { if (useMainProxy) { - expect(response.statusCode).to.eql(502); + expect(response.statusCode).to.eql(592); expect(response.body).to.eql('Bad status!'); } else { expect(response.statusCode).to.eql(55); @@ -912,7 +912,7 @@ const createTestSuite = ({ const opts = getRequestOpts(`http://127.0.0.1:${server.address().port}`); return requestPromised(opts) .then((response) => { - expect(response.statusCode).to.eql(502); + expect(response.statusCode).to.eql(599); server.close(); }); }); @@ -1073,25 +1073,25 @@ const createTestSuite = ({ await requestPromised(opts); expect(false).to.be.eql(true); } catch (error) { - expect(error.message).to.be.eql('tunneling socket could not be established, statusCode=502'); + expect(error.message).to.be.eql('tunneling socket could not be established, statusCode=597'); } } else { const response = await requestPromised(opts); - expect(response.statusCode).to.be.eql(502); + expect(response.statusCode).to.be.eql(597); expect(response.body).to.be.eql('Invalid colon in username in upstream proxy credentials'); } }); it('fails gracefully on non-existent upstream proxy host', () => { const opts = getRequestOpts(`${useSsl ? 'https' : 'http'}://activate-unknown-upstream-proxy-host.gov`); - return testForErrorResponse(opts, 502); + return testForErrorResponse(opts, 593); }); if (upstreamProxyAuth) { _it('fails gracefully on bad upstream proxy credentials', () => { const opts = getRequestOpts(`${useSsl ? 'https' : 'http'}://activate-bad-upstream-proxy-credentials.gov`); - return testForErrorResponse(opts, 502); + return testForErrorResponse(opts, 597); }); } } @@ -1253,7 +1253,7 @@ describe('non-200 upstream connect response', () => { } }); - it('fails downstream with 502', (done) => { + it('fails downstream with 590', (done) => { const server = http.createServer(); server.on('connect', (_request, socket) => { socket.once('error', () => {}); @@ -1282,7 +1282,8 @@ describe('non-200 upstream connect response', () => { }, }); req.once('connect', (response, socket, head) => { - expect(response.statusCode).to.equal(502); + expect(response.statusCode).to.equal(590); + expect(response.statusMessage).to.equal('UPSTREAM403'); expect(head.length).to.equal(0); success = true;