http: better support for CONNECT method.

Introduces 'connect' event on both client (http.ClientRequest) and
server (http.Server).

Refs: #2259, #2474.
Fixes #1576.
This commit is contained in:
koichik 2012-01-09 03:51:06 +01:00
parent c1a63a9e90
commit 08a91acd76
3 changed files with 225 additions and 39 deletions

View File

@ -66,6 +66,24 @@ request body.
Note that when this event is emitted and handled, the `request` event will Note that when this event is emitted and handled, the `request` event will
not be emitted. 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' ### Event: 'upgrade'
`function (request, socket, head) { }` `function (request, socket, head) { }`
@ -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 listened for, then clients requesting an upgrade will have their connections
closed. 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. * `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` 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 event listener, meaning you will need to bind to it in order to handle data
@ -593,6 +613,69 @@ Options:
Emitted after a socket is assigned to this request. 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' ### Event: 'upgrade'
`function (response, socket, head) { }` `function (response, socket, head) { }`
@ -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 event isn't being listened for, clients receiving an upgrade header will have
their connections closed. 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 http = require('http');
var net = require('net');
// Create an HTTP server // Create an HTTP server
var srv = http.createServer(function (req, res) { var srv = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'}); res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('okay'); 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' + socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' + 'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' + 'Connection: Upgrade\r\n' +
'\r\n\r\n'); '\r\n');
socket.ondata = function(data, start, end) { socket.pipe(socket); // echo back
socket.write(data.toString('utf8', start, end), 'utf8'); // echo back
};
}); });
// now that server is running // now that server is running

View File

@ -95,15 +95,16 @@ var parsers = new FreeList('parsers', 1000, function() {
parser.incoming.upgrade = info.upgrade; parser.incoming.upgrade = info.upgrade;
var isHeadResponse = false; var skipBody = false; // response to HEAD or CONNECT
if (!info.upgrade) { 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 // 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) { parser.onBody = function(b, start, len) {
@ -1072,7 +1073,7 @@ function ClientRequest(options, cb) {
new Buffer(options.auth).toString('base64')); new Buffer(options.auth).toString('base64'));
} }
if (method === 'GET' || method === 'HEAD') { if (method === 'GET' || method === 'HEAD' || method === 'CONNECT') {
self.useChunkedEncodingByDefault = false; self.useChunkedEncodingByDefault = false;
} else { } else {
self.useChunkedEncodingByDefault = true; self.useChunkedEncodingByDefault = true;
@ -1174,22 +1175,26 @@ ClientRequest.prototype.onSocket = function(socket) {
debug('parse error'); debug('parse error');
socket.destroy(ret); socket.destroy(ret);
} else if (parser.incoming && parser.incoming.upgrade) { } else if (parser.incoming && parser.incoming.upgrade) {
// Upgrade or CONNECT
var bytesParsed = ret; var bytesParsed = ret;
socket.ondata = null;
socket.onend = null;
var res = parser.incoming; var res = parser.incoming;
req.res = res; req.res = res;
socket.ondata = null;
socket.onend = null;
parser.finish();
parsers.free(parser);
// This is start + byteParsed // This is start + byteParsed
var upgradeHead = d.slice(start + bytesParsed, end); var bodyHead = d.slice(start + bytesParsed, end);
if (req.listeners('upgrade').length) {
// Emit 'upgrade' on the Agent. var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
req.upgraded = true; if (req.listeners(eventName).length) {
req.emit('upgrade', res, socket, upgradeHead); req.upgradeOrConnect = true;
req.emit(eventName, res, socket, bodyHead);
socket.emit('agentRemove'); socket.emit('agentRemove');
} else { } else {
// Got upgrade header, but have no handler. // Got Upgrade header or CONNECT method, but have no handler.
socket.destroy(); socket.destroy();
} }
} }
@ -1235,6 +1240,12 @@ ClientRequest.prototype.onSocket = function(socket) {
} }
req.res = res; 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. // Responses to HEAD requests are crazy.
// HEAD responses aren't allowed to have an entity-body // HEAD responses aren't allowed to have an entity-body
// but *can* have a content-length which actually corresponds // but *can* have a content-length which actually corresponds
@ -1250,7 +1261,8 @@ ClientRequest.prototype.onSocket = function(socket) {
return true; 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. // 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 // If we've been upgraded (via WebSockets) we also shouldn't try to
// keep the connection open. // keep the connection open.
@ -1400,6 +1412,14 @@ function connectionListener(socket) {
// abort socket._httpMessage ? // abort socket._httpMessage ?
} }
function serverSocketCloseListener() {
debug('server socket close');
// unref the parser for easy gc
parsers.free(parser);
abortIncoming();
}
debug('SERVER new http connection'); debug('SERVER new http connection');
httpSocketSetup(socket); httpSocketSetup(socket);
@ -1424,19 +1444,24 @@ function connectionListener(socket) {
debug('parse error'); debug('parse error');
socket.destroy(ret); socket.destroy(ret);
} else if (parser.incoming && parser.incoming.upgrade) { } else if (parser.incoming && parser.incoming.upgrade) {
// Upgrade or CONNECT
var bytesParsed = ret; var bytesParsed = ret;
socket.ondata = null;
socket.onend = null;
var req = parser.incoming; var req = parser.incoming;
// This is start + byteParsed socket.ondata = null;
var upgradeHead = d.slice(start + bytesParsed, end); socket.onend = null;
socket.removeListener('close', serverSocketCloseListener);
parser.finish();
parsers.free(parser);
if (self.listeners('upgrade').length) { // This is start + byteParsed
self.emit('upgrade', req, req.socket, upgradeHead); var bodyHead = d.slice(start + bytesParsed, end);
var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
if (self.listeners(eventName).length) {
self.emit(eventName, req, req.socket, bodyHead);
} else { } else {
// Got upgrade header, but have no handler. // Got upgrade header or CONNECT method, but have no handler.
socket.destroy(); socket.destroy();
} }
} }
@ -1463,13 +1488,7 @@ function connectionListener(socket) {
} }
}; };
socket.addListener('close', function() { socket.addListener('close', serverSocketCloseListener);
debug('server socket close');
// unref the parser for easy gc
parsers.free(parser);
abortIncoming();
});
// The following callback is issued after the headers have been read on a // 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 // new message. In this callback we setup the response object and pass it

View File

@ -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));
});