Skip to content

Commit 08a91ac

Browse files
committed
http: better support for CONNECT method.
Introduces 'connect' event on both client (http.ClientRequest) and server (http.Server). Refs: #2259, #2474. Fixes #1576.
1 parent c1a63a9 commit 08a91ac

3 files changed

Lines changed: 223 additions & 37 deletions

File tree

doc/api/http.markdown

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ request body.
6666
Note that when this event is emitted and handled, the `request` event will
6767
not be emitted.
6868

69+
### Event: 'connect'
70+
71+
`function (request, socket, head) { }`
72+
73+
Emitted each time a client requests a http CONNECT method. If this event isn't
74+
listened for, then clients requesting a CONNECT method will have their
75+
connections closed.
76+
77+
* `request` is the arguments for the http request, as it is in the request
78+
event.
79+
* `socket` is the network socket between the server and client.
80+
* `head` is an instance of Buffer, the first packet of the tunneling stream,
81+
this may be empty.
82+
83+
After this event is emitted, the request's socket will not have a `data`
84+
event listener, meaning you will need to bind to it in order to handle data
85+
sent to the server on that socket.
86+
6987
### Event: 'upgrade'
7088

7189
`function (request, socket, head) { }`
@@ -74,9 +92,11 @@ Emitted each time a client requests a http upgrade. If this event isn't
7492
listened for, then clients requesting an upgrade will have their connections
7593
closed.
7694

77-
* `request` is the arguments for the http request, as it is in the request event.
95+
* `request` is the arguments for the http request, as it is in the request
96+
event.
7897
* `socket` is the network socket between the server and client.
79-
* `head` is an instance of Buffer, the first packet of the upgraded stream, this may be empty.
98+
* `head` is an instance of Buffer, the first packet of the upgraded stream,
99+
this may be empty.
80100

81101
After this event is emitted, the request's socket will not have a `data`
82102
event listener, meaning you will need to bind to it in order to handle data
@@ -593,6 +613,69 @@ Options:
593613

594614
Emitted after a socket is assigned to this request.
595615

616+
### Event: 'connect'
617+
618+
`function (response, socket, head) { }`
619+
620+
Emitted each time a server responds to a request with a CONNECT method. If this
621+
event isn't being listened for, clients receiving a CONNECT method will have
622+
their connections closed.
623+
624+
A client server pair that show you how to listen for the `connect` event.
625+
626+
var http = require('http');
627+
var net = require('net');
628+
var url = require('url');
629+
630+
// Create an HTTP tunneling proxy
631+
var proxy = http.createServer(function (req, res) {
632+
res.writeHead(200, {'Content-Type': 'text/plain'});
633+
res.end('okay');
634+
});
635+
proxy.on('connect', function(req, cltSocket, head) {
636+
// connect to an origin server
637+
var srvUrl = url.parse('http://' + req.url);
638+
var srvSocket = net.connect(srvUrl.port, srvUrl.hostname, function() {
639+
cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +
640+
'Proxy-agent: Node-Proxy\r\n' +
641+
'\r\n');
642+
srvSocket.write(head);
643+
srvSocket.pipe(cltSocket);
644+
cltSocket.pipe(srvSocket);
645+
});
646+
});
647+
648+
// now that proxy is running
649+
proxy.listen(1337, '127.0.0.1', function() {
650+
651+
// make a request to a tunneling proxy
652+
var options = {
653+
port: 1337,
654+
host: '127.0.0.1',
655+
method: 'CONNECT',
656+
path: 'www.google.com:80'
657+
};
658+
659+
var req = http.request(options);
660+
req.end();
661+
662+
req.on('connect', function(res, socket, head) {
663+
console.log('got connected!');
664+
665+
// make a request over an HTTP tunnel
666+
socket.write('GET / HTTP/1.1\r\n' +
667+
'Host: www.google.com:80\r\n' +
668+
'Connection: close\r\n' +
669+
'\r\n');
670+
socket.on('data', function(chunk) {
671+
console.log(chunk.toString());
672+
});
673+
socket.on('end', function() {
674+
proxy.close();
675+
});
676+
});
677+
});
678+
596679
### Event: 'upgrade'
597680

598681
`function (response, socket, head) { }`
@@ -601,25 +684,22 @@ Emitted each time a server responds to a request with an upgrade. If this
601684
event isn't being listened for, clients receiving an upgrade header will have
602685
their connections closed.
603686

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

606689
var http = require('http');
607-
var net = require('net');
608690

609691
// Create an HTTP server
610692
var srv = http.createServer(function (req, res) {
611693
res.writeHead(200, {'Content-Type': 'text/plain'});
612694
res.end('okay');
613695
});
614-
srv.on('upgrade', function(req, socket, upgradeHead) {
696+
srv.on('upgrade', function(req, socket, head) {
615697
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
616698
'Upgrade: WebSocket\r\n' +
617699
'Connection: Upgrade\r\n' +
618-
'\r\n\r\n');
700+
'\r\n');
619701

620-
socket.ondata = function(data, start, end) {
621-
socket.write(data.toString('utf8', start, end), 'utf8'); // echo back
622-
};
702+
socket.pipe(socket); // echo back
623703
});
624704

625705
// now that server is running

lib/http.js

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,16 @@ var parsers = new FreeList('parsers', 1000, function() {
9595

9696
parser.incoming.upgrade = info.upgrade;
9797

98-
var isHeadResponse = false;
98+
var skipBody = false; // response to HEAD or CONNECT
9999

100100
if (!info.upgrade) {
101-
// For upgraded connections, we'll emit this after parser.execute
101+
// For upgraded connections and CONNECT method request,
102+
// we'll emit this after parser.execute
102103
// so that we can capture the first part of the new protocol
103-
isHeadResponse = parser.onIncoming(parser.incoming, info.shouldKeepAlive);
104+
skipBody = parser.onIncoming(parser.incoming, info.shouldKeepAlive);
104105
}
105106

106-
return isHeadResponse;
107+
return skipBody;
107108
};
108109

109110
parser.onBody = function(b, start, len) {
@@ -1072,7 +1073,7 @@ function ClientRequest(options, cb) {
10721073
new Buffer(options.auth).toString('base64'));
10731074
}
10741075

1075-
if (method === 'GET' || method === 'HEAD') {
1076+
if (method === 'GET' || method === 'HEAD' || method === 'CONNECT') {
10761077
self.useChunkedEncodingByDefault = false;
10771078
} else {
10781079
self.useChunkedEncodingByDefault = true;
@@ -1174,22 +1175,26 @@ ClientRequest.prototype.onSocket = function(socket) {
11741175
debug('parse error');
11751176
socket.destroy(ret);
11761177
} else if (parser.incoming && parser.incoming.upgrade) {
1178+
// Upgrade or CONNECT
11771179
var bytesParsed = ret;
1178-
socket.ondata = null;
1179-
socket.onend = null;
1180-
11811180
var res = parser.incoming;
11821181
req.res = res;
11831182

1183+
socket.ondata = null;
1184+
socket.onend = null;
1185+
parser.finish();
1186+
parsers.free(parser);
1187+
11841188
// This is start + byteParsed
1185-
var upgradeHead = d.slice(start + bytesParsed, end);
1186-
if (req.listeners('upgrade').length) {
1187-
// Emit 'upgrade' on the Agent.
1188-
req.upgraded = true;
1189-
req.emit('upgrade', res, socket, upgradeHead);
1189+
var bodyHead = d.slice(start + bytesParsed, end);
1190+
1191+
var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
1192+
if (req.listeners(eventName).length) {
1193+
req.upgradeOrConnect = true;
1194+
req.emit(eventName, res, socket, bodyHead);
11901195
socket.emit('agentRemove');
11911196
} else {
1192-
// Got upgrade header, but have no handler.
1197+
// Got Upgrade header or CONNECT method, but have no handler.
11931198
socket.destroy();
11941199
}
11951200
}
@@ -1235,6 +1240,12 @@ ClientRequest.prototype.onSocket = function(socket) {
12351240
}
12361241
req.res = res;
12371242

1243+
// Responses to CONNECT request is handled as Upgrade.
1244+
if (req.method === 'CONNECT') {
1245+
res.upgrade = true;
1246+
return true; // skip body
1247+
}
1248+
12381249
// Responses to HEAD requests are crazy.
12391250
// HEAD responses aren't allowed to have an entity-body
12401251
// but *can* have a content-length which actually corresponds
@@ -1250,7 +1261,8 @@ ClientRequest.prototype.onSocket = function(socket) {
12501261
return true;
12511262
}
12521263

1253-
if (req.shouldKeepAlive && res.headers.connection !== 'keep-alive' && !req.upgraded) {
1264+
if (req.shouldKeepAlive && res.headers.connection !== 'keep-alive' &&
1265+
!req.upgradeOrConnect) {
12541266
// Server MUST respond with Connection:keep-alive for us to enable it.
12551267
// If we've been upgraded (via WebSockets) we also shouldn't try to
12561268
// keep the connection open.
@@ -1400,6 +1412,14 @@ function connectionListener(socket) {
14001412
// abort socket._httpMessage ?
14011413
}
14021414

1415+
function serverSocketCloseListener() {
1416+
debug('server socket close');
1417+
// unref the parser for easy gc
1418+
parsers.free(parser);
1419+
1420+
abortIncoming();
1421+
}
1422+
14031423
debug('SERVER new http connection');
14041424

14051425
httpSocketSetup(socket);
@@ -1424,19 +1444,24 @@ function connectionListener(socket) {
14241444
debug('parse error');
14251445
socket.destroy(ret);
14261446
} else if (parser.incoming && parser.incoming.upgrade) {
1447+
// Upgrade or CONNECT
14271448
var bytesParsed = ret;
1449+
var req = parser.incoming;
1450+
14281451
socket.ondata = null;
14291452
socket.onend = null;
1430-
1431-
var req = parser.incoming;
1453+
socket.removeListener('close', serverSocketCloseListener);
1454+
parser.finish();
1455+
parsers.free(parser);
14321456

14331457
// This is start + byteParsed
1434-
var upgradeHead = d.slice(start + bytesParsed, end);
1458+
var bodyHead = d.slice(start + bytesParsed, end);
14351459

1436-
if (self.listeners('upgrade').length) {
1437-
self.emit('upgrade', req, req.socket, upgradeHead);
1460+
var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
1461+
if (self.listeners(eventName).length) {
1462+
self.emit(eventName, req, req.socket, bodyHead);
14381463
} else {
1439-
// Got upgrade header, but have no handler.
1464+
// Got upgrade header or CONNECT method, but have no handler.
14401465
socket.destroy();
14411466
}
14421467
}
@@ -1463,13 +1488,7 @@ function connectionListener(socket) {
14631488
}
14641489
};
14651490

1466-
socket.addListener('close', function() {
1467-
debug('server socket close');
1468-
// unref the parser for easy gc
1469-
parsers.free(parser);
1470-
1471-
abortIncoming();
1472-
});
1491+
socket.addListener('close', serverSocketCloseListener);
14731492

14741493
// The following callback is issued after the headers have been read on a
14751494
// new message. In this callback we setup the response object and pass it

test/simple/test-http-connect.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
var common = require('../common');
23+
var assert = require('assert');
24+
var http = require('http');
25+
26+
var serverGotConnect = false;
27+
var clientGotConnect = false;
28+
29+
var server = http.createServer(function(req, res) {
30+
assert(false);
31+
});
32+
server.on('connect', function(req, socket, firstBodyChunk) {
33+
assert.equal(req.method, 'CONNECT');
34+
assert.equal(req.url, 'google.com:443');
35+
common.debug('Server got CONNECT request');
36+
serverGotConnect = true;
37+
38+
socket.write('HTTP/1.1 200 Connection established\r\n\r\n');
39+
40+
var data = firstBodyChunk.toString();
41+
socket.on('data', function(buf) {
42+
data += buf.toString();
43+
});
44+
socket.on('end', function() {
45+
socket.end(data);
46+
});
47+
});
48+
server.listen(common.PORT, function() {
49+
var req = http.request({
50+
port: common.PORT,
51+
method: 'CONNECT',
52+
path: 'google.com:443'
53+
}, function(res) {
54+
assert(false);
55+
});
56+
req.on('connect', function(res, socket, firstBodyChunk) {
57+
common.debug('Client got CONNECT request');
58+
clientGotConnect = true;
59+
60+
var data = firstBodyChunk.toString();
61+
socket.on('data', function(buf) {
62+
data += buf.toString();
63+
});
64+
socket.on('end', function() {
65+
assert.equal(data, 'HeadBody');
66+
server.close();
67+
});
68+
socket.write('Body');
69+
socket.end();
70+
});
71+
72+
// It is legal for the client to send some data intended for the server
73+
// before the "200 Connection established" (or any other success or
74+
// error code) is received.
75+
req.write('Head');
76+
req.end();
77+
});
78+
79+
process.on('exit', function() {
80+
assert.ok(serverGotConnect);
81+
assert.ok(clientGotConnect);
82+
83+
// Make sure this request got removed from the pool.
84+
var name = 'localhost:' + common.PORT;
85+
assert(!http.globalAgent.sockets.hasOwnProperty(name));
86+
assert(!http.globalAgent.requests.hasOwnProperty(name));
87+
});

0 commit comments

Comments
 (0)