Skip to content
Browse files

http: handle CONNECT response as a Upgrade

http.Server emits 'upgrade' event when a CONNECT request is received
(it is not documented, this patch adds the description), and the socket
is given to the user. However, on a client side, the socket after
a HTTP tunnel is established is not given.

After this patch is applied, http.ClientRequest emits 'upgrade' event
(I did not think that it is good API, but I did it in the same way as a
server side), and gives the socket when a CONNECT response is received.

Refs: #2474.
  • Loading branch information...
1 parent 0321adb commit 1a35e8b14c9cf1df7ca01a79ee60ebd409ae59e5 @koichik committed Jan 8, 2012
Showing with 128 additions and 26 deletions.
  1. +10 −9 doc/api/http.markdown
  2. +31 −17 lib/http.js
  3. +87 −0 test/simple/test-http-connect.js
View
19 doc/api/http.markdown
@@ -70,13 +70,14 @@ not be emitted.
`function (request, socket, head) { }`
-Emitted each time a client requests a http upgrade. If this event isn't
-listened for, then clients requesting an upgrade will have their connections
-closed.
+Emitted each time a client requests a http upgrade or CONNECT method.
+If this event isn't listened for, then clients requesting an upgrade or
+CONNECT method will have their connections closed.
-* `request` is the arguments for the http request, as it is in the request event.
-* `socket` is the network socket between the server and client.
-* `head` is an instance of Buffer, the first packet of the upgraded stream, this may be empty.
+* `request` is the arguments for the http request, as it is in the request
+* event. `socket` is the network socket between the server and client.
+* `head` is an instance of Buffer, the first packet of the upgraded stream,
+* this may be empty.
After this event is emitted, the request's socket will not have a `data`
event listener, meaning you will need to bind to it in order to handle data
@@ -597,9 +598,9 @@ Emitted after a socket is assigned to this request.
`function (response, socket, head) { }`
-Emitted each time a server responds to a request with an upgrade. If this
-event isn't being listened for, clients receiving an upgrade header will have
-their connections closed.
+Emitted each time a server responds to a request with an upgrade or CONNECT
+method. If this event isn't being listened for, clients receiving an upgrade
+header will have their connections closed.
A client server pair that show you how to listen for the `upgrade` event using `http.getAgent`:
View
48 lib/http.js
@@ -95,15 +95,16 @@ var parsers = new FreeList('parsers', 1000, function() {
parser.incoming.upgrade = info.upgrade;
- var isHeadResponse = false;
+ var skipBody = false; // response to HEAD or CONNECT
if (!info.upgrade) {
- // For upgraded connections, we'll emit this after parser.execute
+ // 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
- isHeadResponse = parser.onIncoming(parser.incoming, info.shouldKeepAlive);
+ skipBody = parser.onIncoming(parser.incoming, info.shouldKeepAlive);
}
- return isHeadResponse;
+ return skipBody;
};
parser.onBody = function(b, start, len) {
@@ -1072,7 +1073,7 @@ function ClientRequest(options, cb) {
new Buffer(options.auth).toString('base64'));
}
- if (method === 'GET' || method === 'HEAD') {
+ if (method === 'GET' || method === 'HEAD' || method === 'CONNECT') {
self.useChunkedEncodingByDefault = false;
} else {
self.useChunkedEncodingByDefault = true;
@@ -1175,12 +1176,14 @@ ClientRequest.prototype.onSocket = function(socket) {
socket.destroy(ret);
} else if (parser.incoming && parser.incoming.upgrade) {
var bytesParsed = ret;
- socket.ondata = null;
- socket.onend = null;
-
var res = parser.incoming;
req.res = res;
+ socket.ondata = null;
+ socket.onend = null;
+ parser.finish();
+ parsers.free(parser);
+
// This is start + byteParsed
var upgradeHead = d.slice(start + bytesParsed, end);
if (req.listeners('upgrade').length) {
@@ -1235,6 +1238,12 @@ ClientRequest.prototype.onSocket = function(socket) {
}
req.res = res;
+ // Responses to CONNECT request is handled as Upgrede.
+ if (req.method === 'CONNECT') {
+ res.upgrade = true;
+ return true; // skip body
+ }
+
// 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
@@ -1400,6 +1409,14 @@ function connectionListener(socket) {
// abort socket._httpMessage ?
}
+ function serverSocketCloseListener() {
+ debug('server socket close');
+ // unref the parser for easy gc
+ parsers.free(parser);
+
+ abortIncoming();
+ }
+
debug('SERVER new http connection');
httpSocketSetup(socket);
@@ -1425,10 +1442,13 @@ function connectionListener(socket) {
socket.destroy(ret);
} else if (parser.incoming && parser.incoming.upgrade) {
var bytesParsed = ret;
+ var req = parser.incoming;
+
socket.ondata = null;
socket.onend = null;
-
- var req = parser.incoming;
+ socket.removeListener('close', serverSocketCloseListener);
+ parser.finish();
+ parsers.free(parser);
// This is start + byteParsed
var upgradeHead = d.slice(start + bytesParsed, end);
@@ -1463,13 +1483,7 @@ function connectionListener(socket) {
}
};
- socket.addListener('close', function() {
- debug('server socket close');
- // unref the parser for easy gc
- parsers.free(parser);
-
- abortIncoming();
- });
+ socket.addListener('close', serverSocketCloseListener);
// The following callback is issued after the headers have been read on a
// new message. In this callback we setup the response object and pass it
View
87 test/simple/test-http-connect.js
@@ -0,0 +1,87 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('../common');
+var assert = require('assert');
+var http = require('http');
+
+var serverGotConnect = false;
+var clientGotConnect = false;
+
+var server = http.createServer(function(req, res) {
+ assert(false);
+});
+server.on('upgrade', function(req, socket, buf) {
+ assert.equal(req.method, 'CONNECT');
+ assert.equal(req.url, 'google.com:443');
+ common.debug('Server got CONNECT request');
+ serverGotConnect = true;
+
+ socket.write('HTTP/1.1 200 Connection established\r\n\r\n');
+
+ var data = buf.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);
+ });
+ req.on('upgrade', function(res, socket, buf) {
+ common.debug('Client got CONNECT request');
+ clientGotConnect = true;
+
+ var data = buf.toString();
+ socket.on('data', function(buf) {
+ data += buf.toString();
+ });
+ socket.on('end', function() {
+ assert.equal(data, 'HeadBody');
+ server.close();
+ });
+ socket.write('Body');
+ socket.end();
+ });
+
+ // It is legal for the client to send some data intended for the server
+ // before the "200 Connection established" (or any other success or
+ // error code) is received.
+ req.write('Head');
+ req.end();
+});
+
+process.on('exit', function() {
+ assert.ok(serverGotConnect);
+ assert.ok(clientGotConnect);
+
+ // 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));
+});

0 comments on commit 1a35e8b

Please sign in to comment.
Something went wrong with that request. Please try again.