Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
http: tell the parser about CONNECT responses
This commit fixes a bug in HTTP CONNECT response parsing. The parser
normally continues to look for additional HTTP messages after the
first message has completed. However, in the case of CONNECT responses,
the parser should stop looking for additional messages and treat
any further data as a separate protocol.

Because of the way that HTTP messages are parsed in JavaScript, this
bug only manifests itself in the case where the socket's `data'
handler receives the end of the response message *and also* includes
non-HTTP data.

In order to implement the fix, an `.upgrade' accessor is exposed to
JavaScript on the HTTPParser object that proxies the underlying
http_parser's `upgrade' field. Likewise in JavaScript, the `http'
client module sets this value to `true' when a response is received
to a CONNECT request.

The result of this is that callbacks on HTTPParser instances can
signal that the message indicates a change in protocol and further
HTTP parsing should not occur.
  • Loading branch information
slushie committed Apr 15, 2016
commit adb1a963226fae3c3dbea3e1fb8067c118fadabc
2 changes: 2 additions & 0 deletions lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ function socketOnData(d) {
// client
function parserOnIncomingClient(res, shouldKeepAlive) {
var socket = this.socket;
var parser = socket.parser;
var req = socket._httpMessage;


Expand All @@ -432,6 +433,7 @@ function parserOnIncomingClient(res, shouldKeepAlive) {
// Responses to CONNECT request is handled as Upgrade.
if (req.method === 'CONNECT') {
res.upgrade = true;
parser.upgrade = true;
return true; // skip body
}

Expand Down
27 changes: 27 additions & 0 deletions src/node_http_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,28 @@ class Parser : public AsyncWrap {
args.GetReturnValue().Set(ret);
}

static void GetUpgrade(Local<String> property,
const PropertyCallbackInfo<Value>& args) {
Parser* parser = Unwrap<Parser>(args.Holder());

Local<Value> ret = Boolean::New(
parser->env()->isolate(),
parser->parser_.upgrade);

args.GetReturnValue().Set(ret);
}

static void SetUpgrade(Local<String> property,
Local<Value> value,
const PropertyCallbackInfo<void>& args) {
Parser* parser = Unwrap<Parser>(args.Holder());

bool upgrade = value->BooleanValue();
parser->parser_.upgrade = upgrade;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't look correct to me. http_parser.upgrade is only supposed to be read, not written, by clients of libhttp_parser.


args.GetReturnValue().Set(value->ToBoolean());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not necessary to return anything in a setter.

}

protected:
class ScopedRetainParser {
public:
Expand Down Expand Up @@ -736,6 +758,7 @@ void InitHttpParser(Local<Object> target,
void* priv) {
Environment* env = Environment::GetCurrent(context);
Local<FunctionTemplate> t = env->NewFunctionTemplate(Parser::New);

t->InstanceTemplate()->SetInternalFieldCount(1);
t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "HTTPParser"));

Expand Down Expand Up @@ -771,6 +794,10 @@ void InitHttpParser(Local<Object> target,
env->SetProtoMethod(t, "unconsume", Parser::Unconsume);
env->SetProtoMethod(t, "getCurrentBuffer", Parser::GetCurrentBuffer);

Local<v8::ObjectTemplate> o = t->InstanceTemplate();
o->SetAccessor(FIXED_ONE_BYTE_STRING(env->isolate(), "upgrade"),
Parser::GetUpgrade, Parser::SetUpgrade);

target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "HTTPParser"),
t->GetFunction());
}
Expand Down
90 changes: 90 additions & 0 deletions test/parallel/test-http-connect-head.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use strict';
var common = require('../common');
var assert = require('assert');
var http = require('http');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these can be const


var serverGotConnect = false;
var clientGotConnect = false;

var server = http.createServer(function(req, res) {
Copy link
Copy Markdown
Member

@jasnell jasnell Apr 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each of the callback functions should be wrapped in common.mustCall(fn) ...

which would also let you eliminate the serverGotConnect and clientGotConnect state and on-exit check

assert(false);
});
server.on('connect', function(req, socket, firstBodyChunk) {
assert.equal(req.method, 'CONNECT');
assert.equal(req.url, 'google.com:443');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you switch this to something generic like example.com:443

console.error('Server got CONNECT request');
serverGotConnect = true;

// It is legal for the server to send some data intended for the client
// along with the CONNECT response
socket.write('HTTP/1.1 200 Connection established\r\n\r\nHead');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be a legal response this would need a Date header also


var data = firstBodyChunk.toString();
socket.on('data', function(buf) {
data += buf.toString();
});
socket.on('end', function() {
socket.end(data);
});
});
server.listen(common.PORT, function() {
var req = http.request({
port: common.PORT,
method: 'CONNECT',
path: 'google.com:443'
}, function(res) {
assert(false);
});

var clientRequestClosed = false;
req.on('close', function() {
clientRequestClosed = true;
});

req.on('connect', function(res, socket, firstBodyChunk) {
console.error('Client got CONNECT request');
clientGotConnect = true;

// Make sure this request got removed from the pool.
var name = 'localhost:' + common.PORT;
assert(!http.globalAgent.sockets.hasOwnProperty(name));
assert(!http.globalAgent.requests.hasOwnProperty(name));

// Make sure this socket has detached.
assert(!socket.ondata);
assert(!socket.onend);
assert.equal(socket.listeners('connect').length, 0);
assert.equal(socket.listeners('data').length, 0);

// the stream.Duplex onend listener
// allow 0 here, so that i can run the same test on streams1 impl
assert(socket.listeners('end').length <= 1);

assert.equal(socket.listeners('free').length, 0);
assert.equal(socket.listeners('close').length, 0);
assert.equal(socket.listeners('error').length, 0);
assert.equal(socket.listeners('agentRemove').length, 0);

var data = firstBodyChunk.toString();

// test that the firstBodyChunk was not parsed as HTTP
assert.equal(data, 'Head');

socket.on('data', function(buf) {
data += buf.toString();
});
socket.on('end', function() {
assert.equal(data, 'HeadRequestEnd');
assert(clientRequestClosed);
server.close();
});
socket.end('End');
});

req.end('Request');
});

process.on('exit', function() {
assert.ok(serverGotConnect);
assert.ok(clientGotConnect);
});