Skip to content

Commit

Permalink
[feature] Introduce the 'redirect' event
Browse files Browse the repository at this point in the history
Add the ability to remove confidential headers on a per-redirect basis.

Closes #2014
  • Loading branch information
lpinca committed Apr 7, 2022
1 parent 62e9b19 commit ed0c6e1
Show file tree
Hide file tree
Showing 3 changed files with 350 additions and 51 deletions.
17 changes: 17 additions & 0 deletions doc/ws.md
Expand Up @@ -24,6 +24,7 @@
- [Event: 'open'](#event-open)
- [Event: 'ping'](#event-ping)
- [Event: 'pong'](#event-pong)
- [Event: 'redirect'](#event-redirect)
- [Event: 'unexpected-response'](#event-unexpected-response)
- [Event: 'upgrade'](#event-upgrade)
- [websocket.addEventListener(type, listener[, options])](#websocketaddeventlistenertype-listener-options)
Expand Down Expand Up @@ -361,6 +362,19 @@ Emitted when a ping is received from the server.

Emitted when a pong is received from the server.

### Event: 'redirect'

- `url` {String}
- `request` {http.ClientRequest}

Emitted before a redirect is followed. `url` is the redirect URL. `request` is
the HTTP GET request with the headers queued. This event gives the ability to
inspect confidential headers and remove them on a per-redirect basis using the
[`request.getHeader()`][] and [`request.removeHeader()`][] API. The `request`
object should be used only for this purpose. When there is at least one listener
for this event, no header is removed by default, even if the redirect is to a
different domain.

### Event: 'unexpected-response'

- `request` {http.ClientRequest}
Expand Down Expand Up @@ -616,5 +630,8 @@ as configured by the `maxPayload` option.
https://nodejs.org/api/https.html#https_https_request_options_callback
[permessage-deflate]:
https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19
[`request.getheader()`]: https://nodejs.org/api/http.html#requestgetheadername
[`request.removeheader()`]:
https://nodejs.org/api/http.html#requestremoveheadername
[`socket.destroy()`]: https://nodejs.org/api/net.html#net_socket_destroy_error
[zlib-options]: https://nodejs.org/api/zlib.html#zlib_class_options
40 changes: 32 additions & 8 deletions lib/websocket.js
Expand Up @@ -30,10 +30,11 @@ const {
const { format, parse } = require('./extension');
const { toBuffer } = require('./buffer-util');

const closeTimeout = 30 * 1000;
const kAborted = Symbol('kAborted');
const protocolVersions = [8, 13];
const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
const protocolVersions = [8, 13];
const closeTimeout = 30 * 1000;

/**
* Class representing a WebSocket.
Expand Down Expand Up @@ -647,7 +648,7 @@ function initAsClient(websocket, address, protocols, options) {
hostname: undefined,
protocol: undefined,
timeout: undefined,
method: undefined,
method: 'GET',
host: undefined,
path: undefined,
port: undefined
Expand Down Expand Up @@ -701,7 +702,7 @@ function initAsClient(websocket, address, protocols, options) {

const defaultPort = isSecure ? 443 : 80;
const key = randomBytes(16).toString('base64');
const get = isSecure ? https.get : http.get;
const request = isSecure ? https.request : http.request;
const protocolSet = new Set();
let perMessageDeflate;

Expand Down Expand Up @@ -766,6 +767,8 @@ function initAsClient(websocket, address, protocols, options) {
opts.path = parts[1];
}

let req;

if (opts.followRedirects) {
if (websocket._redirects === 0) {
websocket._originalHost = parsedUrl.host;
Expand All @@ -783,7 +786,10 @@ function initAsClient(websocket, address, protocols, options) {
options.headers[key.toLowerCase()] = value;
}
}
} else if (parsedUrl.host !== websocket._originalHost) {
} else if (
websocket.listenerCount('redirect') === 0 &&
parsedUrl.host !== websocket._originalHost
) {
//
// Match curl 7.77.0 behavior and drop the following headers. These
// headers are also dropped when following a redirect to a subdomain.
Expand All @@ -803,9 +809,24 @@ function initAsClient(websocket, address, protocols, options) {
options.headers.authorization =
'Basic ' + Buffer.from(opts.auth).toString('base64');
}
}

let req = (websocket._req = get(opts));
req = websocket._req = request(opts);

if (websocket._redirects) {
//
// Unlike what is done for the `'upgrade'` event, no early exit is
// triggered here if the user calls `websocket.close()` or
// `websocket.terminate()` from a listener of the `'redirect'` event. This
// is because the user can also call `request.destroy()` with an error
// before calling `websocket.close()` or `websocket.terminate()` and this
// would result in an error being emitted on the `request` object with no
// `'error'` event listeners attached.
//
websocket.emit('redirect', websocket.url, req);
}
} else {
req = websocket._req = request(opts);
}

if (opts.timeout) {
req.on('timeout', () => {
Expand All @@ -814,7 +835,7 @@ function initAsClient(websocket, address, protocols, options) {
}

req.on('error', (err) => {
if (req === null || req.aborted) return;
if (req === null || req[kAborted]) return;

req = websocket._req = null;
emitErrorAndClose(websocket, err);
Expand Down Expand Up @@ -947,6 +968,8 @@ function initAsClient(websocket, address, protocols, options) {
skipUTF8Validation: opts.skipUTF8Validation
});
});

req.end();
}

/**
Expand Down Expand Up @@ -1007,6 +1030,7 @@ function abortHandshake(websocket, stream, message) {
Error.captureStackTrace(err, abortHandshake);

if (stream.setHeader) {
stream[kAborted] = true;
stream.abort();

if (stream.socket && !stream.socket.destroyed) {
Expand Down

0 comments on commit ed0c6e1

Please sign in to comment.