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:
parent
c1a63a9e90
commit
08a91acd76
@ -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) { }`
|
||||
@ -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
|
||||
@ -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) { }`
|
||||
@ -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
|
||||
|
79
lib/http.js
79
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;
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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.
|
||||
@ -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);
|
||||
@ -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;
|
||||
socket.ondata = null;
|
||||
socket.onend = null;
|
||||
|
||||
var req = parser.incoming;
|
||||
|
||||
// This is start + byteParsed
|
||||
var upgradeHead = d.slice(start + bytesParsed, end);
|
||||
socket.ondata = null;
|
||||
socket.onend = null;
|
||||
socket.removeListener('close', serverSocketCloseListener);
|
||||
parser.finish();
|
||||
parsers.free(parser);
|
||||
|
||||
if (self.listeners('upgrade').length) {
|
||||
self.emit('upgrade', req, req.socket, upgradeHead);
|
||||
// This is start + byteParsed
|
||||
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 {
|
||||
// Got upgrade header, but have no handler.
|
||||
// Got upgrade header or CONNECT method, but have no handler.
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
87
test/simple/test-http-connect.js
Normal file
87
test/simple/test-http-connect.js
Normal 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));
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user