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
|
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
|
||||||
|
79
lib/http.js
79
lib/http.js
@ -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
|
||||||
|
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