Skip to content

Commit

Permalink
http: fix parsing of binary upgrade response body
Browse files Browse the repository at this point in the history
Fix a bug where a connection upgrade response with a Transfer-Encoding
header and a body whose first byte is > 127 causes said byte to be
dropped on the floor when passing the remainder of the message to
the 'upgrade' event listeners.

Fixes: #17789

PR-URL: #17806
Fixes: #17789
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Luigi Pinca <[email protected]>
Reviewed-By: Tiancheng "Timothy" Gu <[email protected]>
Reviewed-By: Richard Lau <[email protected]>
Reviewed-By: Colin Ihrig <[email protected]>
Reviewed-By: Ruben Bridgewater <[email protected]>
  • Loading branch information
bnoordhuis authored and MylesBorins committed Apr 11, 2018
1 parent c897815 commit d2a884e
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 28 deletions.
24 changes: 9 additions & 15 deletions lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,6 @@ function parserOnIncomingClient(res, shouldKeepAlive) {
var socket = this.socket;
var req = socket._httpMessage;


// propagate "domain" setting...
if (req.domain && !res.domain) {
debug('setting "res.domain"');
Expand All @@ -506,29 +505,22 @@ function parserOnIncomingClient(res, shouldKeepAlive) {
// We already have a response object, this means the server
// sent a double response.
socket.destroy();
return;
return 0; // No special treatment.
}
req.res = res;

// Responses to CONNECT request is handled as Upgrade.
if (req.method === 'CONNECT') {
const method = req.method;
if (method === 'CONNECT') {
res.upgrade = true;
return 2; // skip body, and the rest
return 2; // Skip body and treat as Upgrade.
}

// Responses to HEAD requests are crazy.
// HEAD responses aren't allowed to have an entity-body
// but *can* have a content-length which actually corresponds
// to the content-length of the entity-body had the request
// been a GET.
var isHeadResponse = req.method === 'HEAD';
debug('AGENT isHeadResponse', isHeadResponse);

if (res.statusCode === 100) {
// restart the parser, as this is a continue message.
req.res = null; // Clear res so that we don't hit double-responses.
req.emit('continue');
return true;
return 1; // Skip body but don't treat as Upgrade.
}

if (req.shouldKeepAlive && !shouldKeepAlive && !req.upgradeOrConnect) {
Expand All @@ -538,7 +530,6 @@ function parserOnIncomingClient(res, shouldKeepAlive) {
req.shouldKeepAlive = false;
}


DTRACE_HTTP_CLIENT_RESPONSE(socket, req);
LTTNG_HTTP_CLIENT_RESPONSE(socket, req);
COUNTER_HTTP_CLIENT_RESPONSE();
Expand All @@ -556,7 +547,10 @@ function parserOnIncomingClient(res, shouldKeepAlive) {
if (!handled)
res._dump();

return isHeadResponse;
if (method === 'HEAD')
return 1; // Skip body but don't treat as Upgrade.

return 0; // No special treatment.
}

// client
Expand Down
15 changes: 3 additions & 12 deletions lib/_http_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,10 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,

parser.incoming.upgrade = upgrade;

var skipBody = 0; // response to HEAD or CONNECT
if (upgrade)
return 2; // Skip body and treat as Upgrade.

if (!upgrade) {
// For upgraded connections and CONNECT method request, we'll emit this
// after parser.execute so that we can capture the first part of the new
// protocol.
skipBody = parser.onIncoming(parser.incoming, shouldKeepAlive);
}

if (typeof skipBody !== 'number')
return skipBody ? 1 : 0;
else
return skipBody;
return parser.onIncoming(parser.incoming, shouldKeepAlive);
}

// XXX This is a mess.
Expand Down
2 changes: 1 addition & 1 deletion lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
} else {
server.emit('request', req, res);
}
return false; // Not a HEAD response. (Not even a response!)
return 0; // No special treatment.
}

function resetSocketTimeout(server, socket, state) {
Expand Down
28 changes: 28 additions & 0 deletions test/parallel/test-http-upgrade-binary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';
const { mustCall } = require('../common');
const assert = require('assert');
const http = require('http');
const net = require('net');

// https://github.com/nodejs/node/issues/17789 - a connection upgrade response
// that has a Transfer-Encoding header and a body whose first byte is > 127
// triggers a bug where said byte is skipped over.
net.createServer(mustCall(function(conn) {
conn.write('HTTP/1.1 101 Switching Protocols\r\n' +
'Connection: upgrade\r\n' +
'Transfer-Encoding: chunked\r\n' +
'Upgrade: websocket\r\n' +
'\r\n' +
'\u0080', 'latin1');
this.close();
})).listen(0, mustCall(function() {
http.get({
host: this.address().host,
port: this.address().port,
headers: { 'Connection': 'upgrade', 'Upgrade': 'websocket' },
}).on('upgrade', mustCall((res, conn, head) => {
assert.strictEqual(head.length, 1);
assert.strictEqual(head[0], 128);
conn.destroy();
}));
}));

0 comments on commit d2a884e

Please sign in to comment.