This repository has been archived by the owner. It is now read-only.

Response object for HTTP Upgrade/CONNECT #3036

Closed
wants to merge 3 commits into
from
Jump to file or symbol
Failed to load files and symbols.
+242 −111
Split
View
@@ -93,39 +93,37 @@ not be emitted.
### Event: 'connect'
-`function (request, socket, head) { }`
+`function (request, response) { }`
-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.
+Emitted each time a client requests a http CONNECT method. The arguments are
+the same as for the request event. 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.
+The server will treat a CONNECT method as the final request on a connection.
+Completing the request with `response.end()` will send the response, before
+finally closing the connection.
-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.
+To instead continue the connection, set the response parameters as usual, then
+use [switchProtocols](#http_response_switchprotocols_callback) instead of
+`response.end()`. This method will give away control of the socket after
+headers have been sent.
### Event: 'upgrade'
-`function (request, socket, head) { }`
+`function (request, response) { }`
-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.
+Emitted each time a client requests a http upgrade. The arguments are the same
+as for the request event. 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.
-* `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.
+The server will treat an upgrade request as the final request on a connection.
+Completing the request with `response.end()` will send the response, before
+finally closing the connection.
-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.
+To instead continue the connection, set the response parameters as usual, then
+use [switchProtocols](#http_response_switchprotocols_callback) instead of
+`response.end()`. This method will give away control of the socket after
+headers have been sent.
### Event: 'clientError'
@@ -395,6 +393,14 @@ If `data` is specified, it is equivalent to calling `response.write(data, encodi
followed by `response.end()`.
+### response.switchProtocols(callback)
+
+This method is used instead of `response.end()` when successfully completing
+a request with an upgrade or the CONNECT method. The callback receives the
+socket as its only argument, which will resume sending and receiving after
+the HTTP headers.
+
+
## http.request(options, callback)
Node maintains several connections per server to make HTTP requests.
@@ -616,16 +622,15 @@ A client server pair that show you how to listen for the `connect` event.
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('okay');
});
- proxy.on('connect', function(req, cltSocket, head) {
+ proxy.on('connect', function(req, res) {
// 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);
+ res.writeHead(200, { 'Proxy-agent': 'Node-Proxy' });
+ res.switchProtocols(function(cltSocket) {
+ srvSocket.pipe(cltSocket);
+ cltSocket.pipe(srvSocket);
+ });
});
});
@@ -677,13 +682,14 @@ A client server pair that show you how to listen for the `upgrade` event.
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('okay');
});
- 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');
-
- socket.pipe(socket); // echo back
+ srv.on('upgrade', function(req, res) {
+ res.writeHead(101, {
+ 'Connection': 'Upgrade',
+ 'Upgrade': 'Echo'
+ });
+ res.switchProtocols(function(socket) {
+ socket.pipe(socket); // echo back
+ });
});
// now that server is running
@@ -695,7 +701,7 @@ A client server pair that show you how to listen for the `upgrade` event.
hostname: '127.0.0.1',
headers: {
'Connection': 'Upgrade',
- 'Upgrade': 'websocket'
+ 'Upgrade': 'Echo'
}
};
View
@@ -287,6 +287,42 @@ function connectionListener(socket) {
var outgoing = [];
var incoming = [];
+ function createResponse(req, shouldKeepAlive) {
+ incoming.push(req);
+
+ var res = new ServerResponse(req);
+
+ res.shouldKeepAlive = shouldKeepAlive;
+ DTRACE_HTTP_SERVER_REQUEST(req, socket);
+ COUNTER_HTTP_SERVER_REQUEST();
+
+ if (socket._httpMessage) {
+ // There are already pending outgoing res, append.
+ outgoing.push(res);
+ } else {
+ res.assignSocket(socket);
+ }
+
+ res.on('finish', function() {
+ // Usually the first incoming element should be our request. it may
+ // be that in the case abortIncoming() was called that the incoming
+ // array will be empty.
+ assert(incoming.length == 0 || incoming[0] === req);
+
+ incoming.shift();
+
+ // if the user never called req.read(), and didn't pipe() or
+ // .resume() or .on('data'), then we call req._dump() so that the
+ // bytes will be pulled off the wire.
+ if (!req._consuming)
+ req._dump();
+
+ res.detachSocket(socket);
+ });
+
+ return res;
+ }
+
function abortIncoming() {
while (incoming.length) {
var req = incoming.shift();
@@ -360,14 +396,41 @@ function connectionListener(socket) {
freeParser(parser, req);
var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
- if (EventEmitter.listenerCount(self, eventName) > 0) {
- var bodyHead = d.slice(bytesParsed, d.length);
-
- self.emit(eventName, req, req.socket, bodyHead);
- } else {
+ if (EventEmitter.listenerCount(self, eventName) === 0) {
// Got upgrade header or CONNECT method, but have no handler.
socket.destroy();
+ return;
}
+
+ // This is start + byteParsed
+ var bodyHead = d.slice(bytesParsed, d.length);
+ socket.unshift(bodyHead);
+
+ // 'shouldKeepAlive == false' ensures we send 'Connection: close' for
+ // responses that don't successfully switch protocols.
+ var res = createResponse(req, false);
+ res.useChunkedEncodingByDefault = false;
+
+ // The alternate exit for a handler that accepts the upgrade.
+ var switchCb = null;
+ res.switchProtocols = function(fn) {
+ req._consuming = true;
+ switchCb = fn;
+ this.end();
+ };
+
+ // When finished, either upgrade, or close the socket.
+ res.on('finish', function() {
+ socket.removeListener('close', serverSocketCloseListener);
+
+ if (switchCb) {
+ switchCb(socket);
+ } else {
+ socket.destroySoon();
+ }
+ });
+
+ self.emit(eventName, req, res);
}
};
@@ -398,39 +461,11 @@ function connectionListener(socket) {
// new message. In this callback we setup the response object and pass it
// to the user.
parser.onIncoming = function(req, shouldKeepAlive) {
- incoming.push(req);
-
- var res = new ServerResponse(req);
-
- res.shouldKeepAlive = shouldKeepAlive;
- DTRACE_HTTP_SERVER_REQUEST(req, socket);
- COUNTER_HTTP_SERVER_REQUEST();
-
- if (socket._httpMessage) {
- // There are already pending outgoing res, append.
- outgoing.push(res);
- } else {
- res.assignSocket(socket);
- }
+ var res = createResponse(req, shouldKeepAlive);
// When we're finished writing the response, check if this is the last
- // respose, if so destroy the socket.
+ // response, if so destroy the socket.
res.on('finish', function() {
- // Usually the first incoming element should be our request. it may
- // be that in the case abortIncoming() was called that the incoming
- // array will be empty.
- assert(incoming.length == 0 || incoming[0] === req);
-
- incoming.shift();
-
- // if the user never called req.read(), and didn't pipe() or
- // .resume() or .on('data'), then we call req._dump() so that the
- // bytes will be pulled off the wire.
- if (!req._consuming)
- req._dump();
-
- res.detachSocket(socket);
-
if (res._last) {
socket.destroySoon();
} else {
@@ -37,13 +37,15 @@ var server = http.createServer(function(req, res) {
res.end(req.url);
}, 50);
});
-server.on('connect', function(req, socket, firstBodyChunk) {
+server.on('connect', function(req, res) {
common.debug('Server got CONNECT request');
serverConnected = true;
- socket.write('HTTP/1.1 200 Connection established\r\n\r\n');
- socket.resume();
- socket.on('end', function() {
- socket.end();
+ res.writeHead(200, 'Connection established');
+ res.switchProtocols(function(socket) {
+ socket.resume();
+ socket.on('end', function() {
+ socket.end();
+ });
});
});
server.listen(common.PORT, function() {
@@ -29,20 +29,21 @@ var clientGotConnect = false;
var server = http.createServer(function(req, res) {
assert(false);
});
-server.on('connect', function(req, socket, firstBodyChunk) {
+server.on('connect', function(req, res) {
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);
+ res.writeHead(200, 'Connection Established');
+ res.switchProtocols(function(socket) {
+ var data = '';
+ socket.on('data', function(buf) {
+ data += buf.toString();
+ });
+ socket.on('end', function() {
+ socket.end(data);
+ });
});
});
server.listen(common.PORT, function() {
@@ -0,0 +1,79 @@
+// 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 net = require('net');
+var http = require('http');
+
+var agent = new http.Agent({ maxSockets: 1 });
+
+var gotRegularResponse = false;
+var gotUpgradeResponse = false;
+
+var server = http.createServer(function(req, res) {
+ common.debug('Server got regular request');
+ setTimeout(function() {
+ common.debug('Server finished regular request');
+ res.writeHead(200, {'Content-Type': 'text/plain'});
+ res.end('Hello World\n');
+ }, 500);
+});
+server.on('upgrade', function(req, res) {
+ common.debug('Server got upgrade request');
+ res.writeHead(101, {
+ 'Upgrade': 'dummy',
+ 'Connection': 'Upgrade'
+ });
+ res.switchProtocols(function(sock) {
+ common.debug('Server finished update request');
+ sock.end();
+ server.close();
+ });
+});
+server.listen(common.PORT, function() {
+ var conn = net.createConnection(common.PORT);
+
+ conn.on('connect', function() {
+ conn.write('GET / HTTP/1.1\r\n' +
+ '\r\n' +
+ 'GET / HTTP/1.1\r\n' +
+ 'Upgrade: WebSocket\r\n' +
+ 'Connection: Upgrade\r\n' +
+ '\r\n');
+ });
+
+ conn.setEncoding('utf8');
+ var data = '';
+ conn.on('data', function(chunk) {
+ data += chunk;
+ });
+
+ conn.on('end', function() {
+ conn.end();
+
+ var regularIndex = data.indexOf('Hello World');
+ var upgradeIndex = data.indexOf('Upgrade');
+ assert.notEqual(regularIndex, -1);
+ assert.notEqual(upgradeIndex, -1);
+ assert.ok(regularIndex < upgradeIndex);
+ });
+});
Oops, something went wrong.