Skip to content

Commit

Permalink
feat: custom bad gateway statuses (#316)
Browse files Browse the repository at this point in the history
  • Loading branch information
szmarczak authored Sep 29, 2022
1 parent e4367ce commit 5e4b32a
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 27 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
18 changes: 13 additions & 5 deletions src/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -162,15 +168,17 @@ 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.
if (sourceSocket.readyState === 'open') {
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);
}
}
});
Expand Down
13 changes: 4 additions & 9 deletions src/forward.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]);

Expand Down
5 changes: 3 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
60 changes: 60 additions & 0 deletions src/statuses.ts
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion test/anonymize_proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
19 changes: 10 additions & 9 deletions test/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
});
});
Expand Down Expand Up @@ -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);
});
}
}
Expand Down Expand Up @@ -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', () => {});
Expand Down Expand Up @@ -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;

Expand Down

0 comments on commit 5e4b32a

Please sign in to comment.