-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
http: tell the parser about CONNECT responses #6198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
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
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
||
| args.GetReturnValue().Set(value->ToBoolean()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
|
@@ -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")); | ||
|
|
||
|
|
@@ -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()); | ||
| } | ||
|
|
||
| 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'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these can be |
||
|
|
||
| var serverGotConnect = false; | ||
| var clientGotConnect = false; | ||
|
|
||
| var server = http.createServer(function(req, res) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Each of the callback functions should be wrapped in which would also let you eliminate the |
||
| assert(false); | ||
| }); | ||
| server.on('connect', function(req, socket, firstBodyChunk) { | ||
| assert.equal(req.method, 'CONNECT'); | ||
| assert.equal(req.url, 'google.com:443'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you switch this to something generic like |
||
| 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'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be a legal response this would need a |
||
|
|
||
| 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); | ||
| }); | ||
There was a problem hiding this comment.
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.upgradeis only supposed to be read, not written, by clients of libhttp_parser.