diff options
author | Timothy J Fontaine <tjfontaine@gmail.com> | 2013-04-11 16:00:19 -0700 |
---|---|---|
committer | Ben Noordhuis <info@bnoordhuis.nl> | 2013-04-17 00:08:28 +0200 |
commit | 6717fdccb4a3ebf55d87ae5df1e24ebebd4081f1 (patch) | |
tree | c53e593b3c999773d09d00ee48f4af9ee2046719 | |
parent | dc9f97b7b99f27eaf21faf29c7df8c9d823ef863 (diff) | |
download | node-new-6717fdccb4a3ebf55d87ae5df1e24ebebd4081f1.tar.gz |
http: move Server and ServerResponse out
-rw-r--r-- | lib/_http_common.js | 12 | ||||
-rw-r--r-- | lib/_http_outgoing.js | 2 | ||||
-rw-r--r-- | lib/_http_server.js | 455 | ||||
-rw-r--r-- | lib/http.js | 434 | ||||
-rw-r--r-- | node.gyp | 1 |
5 files changed, 475 insertions, 429 deletions
diff --git a/lib/_http_common.js b/lib/_http_common.js index 40a4b11840..8c97886272 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -220,3 +220,15 @@ function freeParser(parser, req) { } } exports.freeParser = freeParser; + + +function ondrain() { + if (this._httpMessage) this._httpMessage.emit('drain'); +} + + +function httpSocketSetup(socket) { + socket.removeListener('drain', ondrain); + socket.on('drain', ondrain); +} +exports.httpSocketSetup = httpSocketSetup; diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 4fe0927cc7..1902cd9b38 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -585,7 +585,7 @@ OutgoingMessage.prototype._finish = function() { assert(this.connection); if (!ServerResponse) - ServerResponse = require('http').ServerResponse; + ServerResponse = require('_http_server').ServerResponse; if (!ClientRequest) ClientRequest = require('http').ClientRequest; diff --git a/lib/_http_server.js b/lib/_http_server.js new file mode 100644 index 0000000000..945baf3226 --- /dev/null +++ b/lib/_http_server.js @@ -0,0 +1,455 @@ +// 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 util = require('util'); +var net = require('net'); +var EventEmitter = require('events').EventEmitter; +var HTTPParser = process.binding('http_parser').HTTPParser; +var assert = require('assert').ok; + +var common = require('_http_common'); +var parsers = common.parsers; +var freeParser = common.freeParser; +var debug = common.debug; +var CRLF = common.CRLF; +var continueExpression = common.continueExpression; +var chunkExpression = common.chunkExpression; +var httpSocketSetup = common.httpSocketSetup; + +var OutgoingMessage = require('_http_outgoing').OutgoingMessage; + + +var STATUS_CODES = exports.STATUS_CODES = { + 100 : 'Continue', + 101 : 'Switching Protocols', + 102 : 'Processing', // RFC 2518, obsoleted by RFC 4918 + 200 : 'OK', + 201 : 'Created', + 202 : 'Accepted', + 203 : 'Non-Authoritative Information', + 204 : 'No Content', + 205 : 'Reset Content', + 206 : 'Partial Content', + 207 : 'Multi-Status', // RFC 4918 + 300 : 'Multiple Choices', + 301 : 'Moved Permanently', + 302 : 'Moved Temporarily', + 303 : 'See Other', + 304 : 'Not Modified', + 305 : 'Use Proxy', + 307 : 'Temporary Redirect', + 400 : 'Bad Request', + 401 : 'Unauthorized', + 402 : 'Payment Required', + 403 : 'Forbidden', + 404 : 'Not Found', + 405 : 'Method Not Allowed', + 406 : 'Not Acceptable', + 407 : 'Proxy Authentication Required', + 408 : 'Request Time-out', + 409 : 'Conflict', + 410 : 'Gone', + 411 : 'Length Required', + 412 : 'Precondition Failed', + 413 : 'Request Entity Too Large', + 414 : 'Request-URI Too Large', + 415 : 'Unsupported Media Type', + 416 : 'Requested Range Not Satisfiable', + 417 : 'Expectation Failed', + 418 : 'I\'m a teapot', // RFC 2324 + 422 : 'Unprocessable Entity', // RFC 4918 + 423 : 'Locked', // RFC 4918 + 424 : 'Failed Dependency', // RFC 4918 + 425 : 'Unordered Collection', // RFC 4918 + 426 : 'Upgrade Required', // RFC 2817 + 428 : 'Precondition Required', // RFC 6585 + 429 : 'Too Many Requests', // RFC 6585 + 431 : 'Request Header Fields Too Large',// RFC 6585 + 500 : 'Internal Server Error', + 501 : 'Not Implemented', + 502 : 'Bad Gateway', + 503 : 'Service Unavailable', + 504 : 'Gateway Time-out', + 505 : 'HTTP Version Not Supported', + 506 : 'Variant Also Negotiates', // RFC 2295 + 507 : 'Insufficient Storage', // RFC 4918 + 509 : 'Bandwidth Limit Exceeded', + 510 : 'Not Extended', // RFC 2774 + 511 : 'Network Authentication Required' // RFC 6585 +}; + + +function ServerResponse(req) { + OutgoingMessage.call(this); + + if (req.method === 'HEAD') this._hasBody = false; + + this.sendDate = true; + + if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) { + this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te); + this.shouldKeepAlive = false; + } +} +util.inherits(ServerResponse, OutgoingMessage); + + +exports.ServerResponse = ServerResponse; + +ServerResponse.prototype.statusCode = 200; + +function onServerResponseClose() { + // EventEmitter.emit makes a copy of the 'close' listeners array before + // calling the listeners. detachSocket() unregisters onServerResponseClose + // but if detachSocket() is called, directly or indirectly, by a 'close' + // listener, onServerResponseClose is still in that copy of the listeners + // array. That is, in the example below, b still gets called even though + // it's been removed by a: + // + // var obj = new events.EventEmitter; + // obj.on('event', a); + // obj.on('event', b); + // function a() { obj.removeListener('event', b) } + // function b() { throw "BAM!" } + // obj.emit('event'); // throws + // + // Ergo, we need to deal with stale 'close' events and handle the case + // where the ServerResponse object has already been deconstructed. + // Fortunately, that requires only a single if check. :-) + if (this._httpMessage) this._httpMessage.emit('close'); +} + +ServerResponse.prototype.assignSocket = function(socket) { + assert(!socket._httpMessage); + socket._httpMessage = this; + socket.on('close', onServerResponseClose); + this.socket = socket; + this.connection = socket; + this.emit('socket', socket); + this._flush(); +}; + +ServerResponse.prototype.detachSocket = function(socket) { + assert(socket._httpMessage == this); + socket.removeListener('close', onServerResponseClose); + socket._httpMessage = null; + this.socket = this.connection = null; +}; + +ServerResponse.prototype.writeContinue = function() { + this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii'); + this._sent100 = true; +}; + +ServerResponse.prototype._implicitHeader = function() { + this.writeHead(this.statusCode); +}; + +ServerResponse.prototype.writeHead = function(statusCode) { + var reasonPhrase, headers, headerIndex; + + if (typeof arguments[1] == 'string') { + reasonPhrase = arguments[1]; + headerIndex = 2; + } else { + reasonPhrase = STATUS_CODES[statusCode] || 'unknown'; + headerIndex = 1; + } + this.statusCode = statusCode; + + var obj = arguments[headerIndex]; + + if (obj && this._headers) { + // Slow-case: when progressive API and header fields are passed. + headers = this._renderHeaders(); + + if (Array.isArray(obj)) { + // handle array case + // TODO: remove when array is no longer accepted + var field; + for (var i = 0, len = obj.length; i < len; ++i) { + field = obj[i][0]; + if (headers[field] !== undefined) { + obj.push([field, headers[field]]); + } + } + headers = obj; + + } else { + // handle object case + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + if (k) headers[k] = obj[k]; + } + } + } else if (this._headers) { + // only progressive api is used + headers = this._renderHeaders(); + } else { + // only writeHead() called + headers = obj; + } + + var statusLine = 'HTTP/1.1 ' + statusCode.toString() + ' ' + + reasonPhrase + CRLF; + + if (statusCode === 204 || statusCode === 304 || + (100 <= statusCode && statusCode <= 199)) { + // RFC 2616, 10.2.5: + // The 204 response MUST NOT include a message-body, and thus is always + // terminated by the first empty line after the header fields. + // RFC 2616, 10.3.5: + // The 304 response MUST NOT contain a message-body, and thus is always + // terminated by the first empty line after the header fields. + // RFC 2616, 10.1 Informational 1xx: + // This class of status code indicates a provisional response, + // consisting only of the Status-Line and optional headers, and is + // terminated by an empty line. + this._hasBody = false; + } + + // don't keep alive connections where the client expects 100 Continue + // but we sent a final status; they may put extra bytes on the wire. + if (this._expect_continue && !this._sent100) { + this.shouldKeepAlive = false; + } + + this._storeHeader(statusLine, headers); +}; + +ServerResponse.prototype.writeHeader = function() { + this.writeHead.apply(this, arguments); +}; + + +function Server(requestListener) { + if (!(this instanceof Server)) return new Server(requestListener); + net.Server.call(this, { allowHalfOpen: true }); + + if (requestListener) { + this.addListener('request', requestListener); + } + + // Similar option to this. Too lazy to write my own docs. + // http://www.squid-cache.org/Doc/config/half_closed_clients/ + // http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F + this.httpAllowHalfOpen = false; + + this.addListener('connection', connectionListener); + + this.addListener('clientError', function(err, conn) { + conn.destroy(err); + }); + + this.timeout = 2 * 60 * 1000; +} +util.inherits(Server, net.Server); + + +Server.prototype.setTimeout = function(msecs, callback) { + this.timeout = msecs; + if (callback) + this.on('timeout', callback); +}; + + +exports.Server = Server; + + +function connectionListener(socket) { + var self = this; + var outgoing = []; + var incoming = []; + + function abortIncoming() { + while (incoming.length) { + var req = incoming.shift(); + req.emit('aborted'); + req.emit('close'); + } + // abort socket._httpMessage ? + } + + function serverSocketCloseListener() { + debug('server socket close'); + // mark this parser as reusable + if (this.parser) + freeParser(this.parser); + + abortIncoming(); + } + + debug('SERVER new http connection'); + + httpSocketSetup(socket); + + // If the user has added a listener to the server, + // request, or response, then it's their responsibility. + // otherwise, destroy on timeout by default + if (self.timeout) + socket.setTimeout(self.timeout); + socket.on('timeout', function() { + var req = socket.parser && socket.parser.incoming; + var reqTimeout = req && !req.complete && req.emit('timeout', socket); + var res = socket._httpMessage; + var resTimeout = res && res.emit('timeout', socket); + var serverTimeout = self.emit('timeout', socket); + + if (!reqTimeout && !resTimeout && !serverTimeout) + socket.destroy(); + }); + + var parser = parsers.alloc(); + parser.reinitialize(HTTPParser.REQUEST); + parser.socket = socket; + socket.parser = parser; + parser.incoming = null; + + // Propagate headers limit from server instance to parser + if (typeof this.maxHeadersCount === 'number') { + parser.maxHeaderPairs = this.maxHeadersCount << 1; + } else { + // Set default value because parser may be reused from FreeList + parser.maxHeaderPairs = 2000; + } + + socket.addListener('error', function(e) { + self.emit('clientError', e, this); + }); + + socket.ondata = function(d, start, end) { + var ret = parser.execute(d, start, end - start); + if (ret instanceof Error) { + debug('parse error'); + socket.destroy(ret); + } else if (parser.incoming && parser.incoming.upgrade) { + // Upgrade or CONNECT + var bytesParsed = ret; + var req = parser.incoming; + + socket.ondata = null; + socket.onend = null; + socket.removeListener('close', serverSocketCloseListener); + parser.finish(); + freeParser(parser, req); + + // This is start + byteParsed + var bodyHead = d.slice(start + bytesParsed, end); + + var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; + if (EventEmitter.listenerCount(self, eventName) > 0) { + self.emit(eventName, req, req.socket, bodyHead); + } else { + // Got upgrade header or CONNECT method, but have no handler. + socket.destroy(); + } + } + }; + + socket.onend = function() { + var ret = parser.finish(); + + if (ret instanceof Error) { + debug('parse error'); + socket.destroy(ret); + return; + } + + if (!self.httpAllowHalfOpen) { + abortIncoming(); + if (socket.writable) socket.end(); + } else if (outgoing.length) { + outgoing[outgoing.length - 1]._last = true; + } else if (socket._httpMessage) { + socket._httpMessage._last = true; + } else { + if (socket.writable) socket.end(); + } + }; + + 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 + // 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); + } + + // When we're finished writing the response, check if this is the last + // respose, 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 { + // start sending the next message + var m = outgoing.shift(); + if (m) { + m.assignSocket(socket); + } + } + }); + + if (req.headers.expect !== undefined && + (req.httpVersionMajor == 1 && req.httpVersionMinor == 1) && + continueExpression.test(req.headers['expect'])) { + res._expect_continue = true; + if (EventEmitter.listenerCount(self, 'checkContinue') > 0) { + self.emit('checkContinue', req, res); + } else { + res.writeContinue(); + self.emit('request', req, res); + } + } else { + self.emit('request', req, res); + } + return false; // Not a HEAD response. (Not even a response!) + }; +} +exports._connectionListener = connectionListener; diff --git a/lib/http.js b/lib/http.js index e0d3e339eb..3476e45740 100644 --- a/lib/http.js +++ b/lib/http.js @@ -35,218 +35,15 @@ var common = require('_http_common'); var parsers = exports.parsers = common.parsers; var freeParser = common.freeParser; var debug = common.debug; -var CRLF = common.CRLF; -var continueExpression = common.continueExpression; -var chunkExpression = common.chunkExpression; - - -var STATUS_CODES = exports.STATUS_CODES = { - 100 : 'Continue', - 101 : 'Switching Protocols', - 102 : 'Processing', // RFC 2518, obsoleted by RFC 4918 - 200 : 'OK', - 201 : 'Created', - 202 : 'Accepted', - 203 : 'Non-Authoritative Information', - 204 : 'No Content', - 205 : 'Reset Content', - 206 : 'Partial Content', - 207 : 'Multi-Status', // RFC 4918 - 300 : 'Multiple Choices', - 301 : 'Moved Permanently', - 302 : 'Moved Temporarily', - 303 : 'See Other', - 304 : 'Not Modified', - 305 : 'Use Proxy', - 307 : 'Temporary Redirect', - 400 : 'Bad Request', - 401 : 'Unauthorized', - 402 : 'Payment Required', - 403 : 'Forbidden', - 404 : 'Not Found', - 405 : 'Method Not Allowed', - 406 : 'Not Acceptable', - 407 : 'Proxy Authentication Required', - 408 : 'Request Time-out', - 409 : 'Conflict', - 410 : 'Gone', - 411 : 'Length Required', - 412 : 'Precondition Failed', - 413 : 'Request Entity Too Large', - 414 : 'Request-URI Too Large', - 415 : 'Unsupported Media Type', - 416 : 'Requested Range Not Satisfiable', - 417 : 'Expectation Failed', - 418 : 'I\'m a teapot', // RFC 2324 - 422 : 'Unprocessable Entity', // RFC 4918 - 423 : 'Locked', // RFC 4918 - 424 : 'Failed Dependency', // RFC 4918 - 425 : 'Unordered Collection', // RFC 4918 - 426 : 'Upgrade Required', // RFC 2817 - 428 : 'Precondition Required', // RFC 6585 - 429 : 'Too Many Requests', // RFC 6585 - 431 : 'Request Header Fields Too Large',// RFC 6585 - 500 : 'Internal Server Error', - 501 : 'Not Implemented', - 502 : 'Bad Gateway', - 503 : 'Service Unavailable', - 504 : 'Gateway Time-out', - 505 : 'HTTP Version Not Supported', - 506 : 'Variant Also Negotiates', // RFC 2295 - 507 : 'Insufficient Storage', // RFC 4918 - 509 : 'Bandwidth Limit Exceeded', - 510 : 'Not Extended', // RFC 2774 - 511 : 'Network Authentication Required' // RFC 6585 -}; var outgoing = require('_http_outgoing'); var OutgoingMessage = exports.OutgoingMessage = outgoing.OutgoingMessage; - -function ServerResponse(req) { - OutgoingMessage.call(this); - - if (req.method === 'HEAD') this._hasBody = false; - - this.sendDate = true; - - if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) { - this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te); - this.shouldKeepAlive = false; - } -} -util.inherits(ServerResponse, OutgoingMessage); - - -exports.ServerResponse = ServerResponse; - -ServerResponse.prototype.statusCode = 200; - -function onServerResponseClose() { - // EventEmitter.emit makes a copy of the 'close' listeners array before - // calling the listeners. detachSocket() unregisters onServerResponseClose - // but if detachSocket() is called, directly or indirectly, by a 'close' - // listener, onServerResponseClose is still in that copy of the listeners - // array. That is, in the example below, b still gets called even though - // it's been removed by a: - // - // var obj = new events.EventEmitter; - // obj.on('event', a); - // obj.on('event', b); - // function a() { obj.removeListener('event', b) } - // function b() { throw "BAM!" } - // obj.emit('event'); // throws - // - // Ergo, we need to deal with stale 'close' events and handle the case - // where the ServerResponse object has already been deconstructed. - // Fortunately, that requires only a single if check. :-) - if (this._httpMessage) this._httpMessage.emit('close'); -} - -ServerResponse.prototype.assignSocket = function(socket) { - assert(!socket._httpMessage); - socket._httpMessage = this; - socket.on('close', onServerResponseClose); - this.socket = socket; - this.connection = socket; - this.emit('socket', socket); - this._flush(); -}; - -ServerResponse.prototype.detachSocket = function(socket) { - assert(socket._httpMessage == this); - socket.removeListener('close', onServerResponseClose); - socket._httpMessage = null; - this.socket = this.connection = null; -}; - -ServerResponse.prototype.writeContinue = function() { - this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii'); - this._sent100 = true; -}; - -ServerResponse.prototype._implicitHeader = function() { - this.writeHead(this.statusCode); -}; - -ServerResponse.prototype.writeHead = function(statusCode) { - var reasonPhrase, headers, headerIndex; - - if (typeof arguments[1] == 'string') { - reasonPhrase = arguments[1]; - headerIndex = 2; - } else { - reasonPhrase = STATUS_CODES[statusCode] || 'unknown'; - headerIndex = 1; - } - this.statusCode = statusCode; - - var obj = arguments[headerIndex]; - - if (obj && this._headers) { - // Slow-case: when progressive API and header fields are passed. - headers = this._renderHeaders(); - - if (Array.isArray(obj)) { - // handle array case - // TODO: remove when array is no longer accepted - var field; - for (var i = 0, len = obj.length; i < len; ++i) { - field = obj[i][0]; - if (headers[field] !== undefined) { - obj.push([field, headers[field]]); - } - } - headers = obj; - - } else { - // handle object case - var keys = Object.keys(obj); - for (var i = 0; i < keys.length; i++) { - var k = keys[i]; - if (k) headers[k] = obj[k]; - } - } - } else if (this._headers) { - // only progressive api is used - headers = this._renderHeaders(); - } else { - // only writeHead() called - headers = obj; - } - - var statusLine = 'HTTP/1.1 ' + statusCode.toString() + ' ' + - reasonPhrase + CRLF; - - if (statusCode === 204 || statusCode === 304 || - (100 <= statusCode && statusCode <= 199)) { - // RFC 2616, 10.2.5: - // The 204 response MUST NOT include a message-body, and thus is always - // terminated by the first empty line after the header fields. - // RFC 2616, 10.3.5: - // The 304 response MUST NOT contain a message-body, and thus is always - // terminated by the first empty line after the header fields. - // RFC 2616, 10.1 Informational 1xx: - // This class of status code indicates a provisional response, - // consisting only of the Status-Line and optional headers, and is - // terminated by an empty line. - this._hasBody = false; - } - - // don't keep alive connections where the client expects 100 Continue - // but we sent a final status; they may put extra bytes on the wire. - if (this._expect_continue && !this._sent100) { - this.shouldKeepAlive = false; - } - - this._storeHeader(statusLine, headers); -}; - -ServerResponse.prototype.writeHeader = function() { - this.writeHead.apply(this, arguments); -}; +var server = require('_http_server'); +exports.ServerResponse = server.ServerResponse; +exports.STATUS_CODES = server.STATUS_CODES; var agent = require('_http_agent'); @@ -734,235 +531,16 @@ exports.get = function(options, cb) { }; -function ondrain() { - if (this._httpMessage) this._httpMessage.emit('drain'); -} - - -function httpSocketSetup(socket) { - socket.removeListener('drain', ondrain); - socket.on('drain', ondrain); -} - - -function Server(requestListener) { - if (!(this instanceof Server)) return new Server(requestListener); - net.Server.call(this, { allowHalfOpen: true }); - - if (requestListener) { - this.addListener('request', requestListener); - } - - // Similar option to this. Too lazy to write my own docs. - // http://www.squid-cache.org/Doc/config/half_closed_clients/ - // http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F - this.httpAllowHalfOpen = false; - - this.addListener('connection', connectionListener); - - this.addListener('clientError', function(err, conn) { - conn.destroy(err); - }); - - this.timeout = 2 * 60 * 1000; -} -util.inherits(Server, net.Server); - - -Server.prototype.setTimeout = function(msecs, callback) { - this.timeout = msecs; - if (callback) - this.on('timeout', callback); -}; - - -exports.Server = Server; +var httpSocketSetup = common.httpSocketSetup; +exports._connectionListener = server._connectionListener; +var Server = exports.Server = server.Server; exports.createServer = function(requestListener) { return new Server(requestListener); }; -function connectionListener(socket) { - var self = this; - var outgoing = []; - var incoming = []; - - function abortIncoming() { - while (incoming.length) { - var req = incoming.shift(); - req.emit('aborted'); - req.emit('close'); - } - // abort socket._httpMessage ? - } - - function serverSocketCloseListener() { - debug('server socket close'); - // mark this parser as reusable - if (this.parser) - freeParser(this.parser); - - abortIncoming(); - } - - debug('SERVER new http connection'); - - httpSocketSetup(socket); - - // If the user has added a listener to the server, - // request, or response, then it's their responsibility. - // otherwise, destroy on timeout by default - if (self.timeout) - socket.setTimeout(self.timeout); - socket.on('timeout', function() { - var req = socket.parser && socket.parser.incoming; - var reqTimeout = req && !req.complete && req.emit('timeout', socket); - var res = socket._httpMessage; - var resTimeout = res && res.emit('timeout', socket); - var serverTimeout = self.emit('timeout', socket); - - if (!reqTimeout && !resTimeout && !serverTimeout) - socket.destroy(); - }); - - var parser = parsers.alloc(); - parser.reinitialize(HTTPParser.REQUEST); - parser.socket = socket; - socket.parser = parser; - parser.incoming = null; - - // Propagate headers limit from server instance to parser - if (typeof this.maxHeadersCount === 'number') { - parser.maxHeaderPairs = this.maxHeadersCount << 1; - } else { - // Set default value because parser may be reused from FreeList - parser.maxHeaderPairs = 2000; - } - - socket.addListener('error', function(e) { - self.emit('clientError', e, this); - }); - - socket.ondata = function(d, start, end) { - var ret = parser.execute(d, start, end - start); - if (ret instanceof Error) { - debug('parse error'); - socket.destroy(ret); - } else if (parser.incoming && parser.incoming.upgrade) { - // Upgrade or CONNECT - var bytesParsed = ret; - var req = parser.incoming; - - socket.ondata = null; - socket.onend = null; - socket.removeListener('close', serverSocketCloseListener); - parser.finish(); - freeParser(parser, req); - - // This is start + byteParsed - var bodyHead = d.slice(start + bytesParsed, end); - - var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; - if (EventEmitter.listenerCount(self, eventName) > 0) { - self.emit(eventName, req, req.socket, bodyHead); - } else { - // Got upgrade header or CONNECT method, but have no handler. - socket.destroy(); - } - } - }; - - socket.onend = function() { - var ret = parser.finish(); - - if (ret instanceof Error) { - debug('parse error'); - socket.destroy(ret); - return; - } - - if (!self.httpAllowHalfOpen) { - abortIncoming(); - if (socket.writable) socket.end(); - } else if (outgoing.length) { - outgoing[outgoing.length - 1]._last = true; - } else if (socket._httpMessage) { - socket._httpMessage._last = true; - } else { - if (socket.writable) socket.end(); - } - }; - - 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 - // 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); - } - - // When we're finished writing the response, check if this is the last - // respose, 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 { - // start sending the next message - var m = outgoing.shift(); - if (m) { - m.assignSocket(socket); - } - } - }); - - if (req.headers.expect !== undefined && - (req.httpVersionMajor == 1 && req.httpVersionMinor == 1) && - continueExpression.test(req.headers['expect'])) { - res._expect_continue = true; - if (EventEmitter.listenerCount(self, 'checkContinue') > 0) { - self.emit('checkContinue', req, res); - } else { - res.writeContinue(); - self.emit('request', req, res); - } - } else { - self.emit('request', req, res); - } - return false; // Not a HEAD response. (Not even a response!) - }; -} -exports._connectionListener = connectionListener; - // Legacy Interface function Client(port, host) { @@ -38,6 +38,7 @@ 'lib/_http_common.js', 'lib/_http_incoming.js', 'lib/_http_outgoing.js', + 'lib/_http_server.js', 'lib/https.js', 'lib/module.js', 'lib/net.js', |