Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

Commit

Permalink
http: better support for CONNECT method.
Browse files Browse the repository at this point in the history
Introduces 'connect' event on both client (http.ClientRequest) and
server (http.Server).

Refs: #2259, #2474.
Fixes #1576.
  • Loading branch information
koichik committed Jan 9, 2012
1 parent c1a63a9 commit 08a91ac
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 37 deletions.
98 changes: 89 additions & 9 deletions doc/api/http.markdown
Expand Up @@ -66,6 +66,24 @@ request body.
Note that when this event is emitted and handled, the `request` event will
not be emitted.

### Event: 'connect'

`function (request, socket, head) { }`

Emitted each time a client requests a http CONNECT method. If this event isn't
listened for, then clients requesting a 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 tunneling 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
sent to the server on that socket.

### Event: 'upgrade'

`function (request, socket, head) { }`
Expand All @@ -74,9 +92,11 @@ 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.

* `request` is the arguments for the http request, as it is in the request event.
* `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.
* `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
Expand Down Expand Up @@ -593,6 +613,69 @@ Options:

Emitted after a socket is assigned to this request.

### Event: 'connect'

`function (response, socket, head) { }`

Emitted each time a server responds to a request with a CONNECT method. If this
event isn't being listened for, clients receiving a CONNECT method will have
their connections closed.

A client server pair that show you how to listen for the `connect` event.

var http = require('http');
var net = require('net');
var url = require('url');

// Create an HTTP tunneling proxy
var proxy = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('okay');
});
proxy.on('connect', function(req, cltSocket, head) {
// connect to an origin server
var srvUrl = url.parse('http://' + req.url);
var srvSocket = net.connect(srvUrl.port, srvUrl.hostname, function() {
cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: Node-Proxy\r\n' +
'\r\n');
srvSocket.write(head);
srvSocket.pipe(cltSocket);
cltSocket.pipe(srvSocket);
});
});

// now that proxy is running
proxy.listen(1337, '127.0.0.1', function() {

// make a request to a tunneling proxy
var options = {
port: 1337,
host: '127.0.0.1',
method: 'CONNECT',
path: 'www.google.com:80'
};

var req = http.request(options);
req.end();

req.on('connect', function(res, socket, head) {
console.log('got connected!');

// make a request over an HTTP tunnel
socket.write('GET / HTTP/1.1\r\n' +
'Host: www.google.com:80\r\n' +
'Connection: close\r\n' +
'\r\n');
socket.on('data', function(chunk) {
console.log(chunk.toString());
});
socket.on('end', function() {
proxy.close();
});
});
});

### Event: 'upgrade'

`function (response, socket, head) { }`
Expand All @@ -601,25 +684,22 @@ 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.

A client server pair that show you how to listen for the `upgrade` event using `http.getAgent`:
A client server pair that show you how to listen for the `upgrade` event.

var http = require('http');
var net = require('net');

// Create an HTTP server
var srv = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('okay');
});
srv.on('upgrade', function(req, socket, upgradeHead) {
srv.on('upgrade', function(req, socket, head) {
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'\r\n\r\n');
'\r\n');

socket.ondata = function(data, start, end) {
socket.write(data.toString('utf8', start, end), 'utf8'); // echo back
};
socket.pipe(socket); // echo back
});

// now that server is running
Expand Down
75 changes: 47 additions & 28 deletions lib/http.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1174,22 +1175,26 @@ ClientRequest.prototype.onSocket = function(socket) {
debug('parse error');
socket.destroy(ret);
} else if (parser.incoming && parser.incoming.upgrade) {
// Upgrade or CONNECT
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) {
// Emit 'upgrade' on the Agent.
req.upgraded = true;
req.emit('upgrade', res, socket, upgradeHead);
var bodyHead = d.slice(start + bytesParsed, end);

var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
if (req.listeners(eventName).length) {
req.upgradeOrConnect = true;
req.emit(eventName, res, socket, bodyHead);
socket.emit('agentRemove');
} else {
// Got upgrade header, but have no handler.
// Got Upgrade header or CONNECT method, but have no handler.
socket.destroy();
}
}
Expand Down Expand Up @@ -1235,6 +1240,12 @@ ClientRequest.prototype.onSocket = function(socket) {
}
req.res = res;

// Responses to CONNECT request is handled as Upgrade.
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
Expand All @@ -1250,7 +1261,8 @@ ClientRequest.prototype.onSocket = function(socket) {
return true;
}

if (req.shouldKeepAlive && res.headers.connection !== 'keep-alive' && !req.upgraded) {
if (req.shouldKeepAlive && res.headers.connection !== 'keep-alive' &&
!req.upgradeOrConnect) {
// Server MUST respond with Connection:keep-alive for us to enable it.
// If we've been upgraded (via WebSockets) we also shouldn't try to
// keep the connection open.
Expand Down Expand Up @@ -1400,6 +1412,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);
Expand All @@ -1424,19 +1444,24 @@ function connectionListener(socket) {
debug('parse error');
socket.destroy(ret);
} else if (parser.incoming && parser.incoming.upgrade) {
// Upgrade or CONNECT
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);
var bodyHead = d.slice(start + bytesParsed, end);

if (self.listeners('upgrade').length) {
self.emit('upgrade', req, req.socket, upgradeHead);
var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
if (self.listeners(eventName).length) {
self.emit(eventName, req, req.socket, bodyHead);
} else {
// Got upgrade header, but have no handler.
// Got upgrade header or CONNECT method, but have no handler.
socket.destroy();
}
}
Expand All @@ -1463,13 +1488,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
Expand Down
87 changes: 87 additions & 0 deletions 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('connect', function(req, socket, firstBodyChunk) {
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 = 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);
});
req.on('connect', function(res, socket, firstBodyChunk) {
common.debug('Client got CONNECT request');
clientGotConnect = true;

var data = firstBodyChunk.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 08a91ac

Please sign in to comment.