/lib/_http_server.js
JavaScript | 498 lines | 333 code | 75 blank | 90 comment | 68 complexity | 7e863076851c5cefb9199932160662fd MD5 | raw file
Possible License(s): 0BSD, Apache-2.0, MPL-2.0-no-copyleft-exception, JSON, WTFPL, CC-BY-SA-3.0, Unlicense, ISC, BSD-3-Clause, MIT, AGPL-3.0
- // 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.
- 'use strict';
- 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',
- 308 : 'Permanent Redirect', // RFC 7238
- 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);
- ServerResponse.prototype._finish = function() {
- DTRACE_HTTP_SERVER_RESPONSE(this.connection);
- COUNTER_HTTP_SERVER_RESPONSE();
- OutgoingMessage.prototype._finish.call(this);
- };
- exports.ServerResponse = ServerResponse;
- ServerResponse.prototype.statusCode = 200;
- ServerResponse.prototype.statusMessage = undefined;
- 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(cb) {
- this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii', cb);
- this._sent100 = true;
- };
- ServerResponse.prototype._implicitHeader = function() {
- this.writeHead(this.statusCode);
- };
- ServerResponse.prototype.writeHead = function(statusCode, reason, obj) {
- var headers;
- if (util.isString(reason)) {
- // writeHead(statusCode, reasonPhrase[, headers])
- this.statusMessage = reason;
- } else {
- // writeHead(statusCode[, headers])
- this.statusMessage =
- this.statusMessage || STATUS_CODES[statusCode] || 'unknown';
- obj = reason;
- }
- this.statusCode = statusCode;
- if (this._headers) {
- // Slow-case: when progressive API and header fields are passed.
- if (obj) {
- var keys = Object.keys(obj);
- for (var i = 0; i < keys.length; i++) {
- var k = keys[i];
- if (k) this.setHeader(k, obj[k]);
- }
- }
- // only progressive api is used
- headers = this._renderHeaders();
- } else {
- // only writeHead() called
- headers = obj;
- }
- var statusLine = 'HTTP/1.1 ' + statusCode.toString() + ' ' +
- this.statusMessage + 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, null, this);
- }
- 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 (util.isNumber(this.maxHeadersCount)) {
- parser.maxHeaderPairs = this.maxHeadersCount << 1;
- } else {
- // Set default value because parser may be reused from FreeList
- parser.maxHeaderPairs = 2000;
- }
- socket.addListener('error', socketOnError);
- socket.addListener('close', serverSocketCloseListener);
- parser.onIncoming = parserOnIncoming;
- socket.on('end', socketOnEnd);
- socket.on('data', socketOnData);
- // TODO(isaacs): Move all these functions out of here
- function socketOnError(e) {
- self.emit('clientError', e, this);
- }
- function socketOnData(d) {
- assert(!socket._paused);
- debug('SERVER socketOnData %d', d.length);
- var ret = parser.execute(d);
- 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;
- debug('SERVER upgrade or connect', req.method);
- socket.removeListener('data', socketOnData);
- socket.removeListener('end', socketOnEnd);
- socket.removeListener('close', serverSocketCloseListener);
- parser.finish();
- freeParser(parser, req, null);
- parser = null;
- var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
- if (EventEmitter.listenerCount(self, eventName) > 0) {
- debug('SERVER have listener for %s', eventName);
- var bodyHead = d.slice(bytesParsed, d.length);
- // TODO(isaacs): Need a way to reset a stream to fresh state
- // IE, not flowing, and not explicitly paused.
- socket._readableState.flowing = null;
- self.emit(eventName, req, socket, bodyHead);
- } else {
- // Got upgrade header or CONNECT method, but have no handler.
- socket.destroy();
- }
- }
- if (socket._paused) {
- // onIncoming paused the socket, we should pause the parser as well
- debug('pause parser');
- socket.parser.pause();
- }
- }
- function socketOnEnd() {
- var socket = this;
- 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();
- }
- }
- // 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.
- socket._paused = false;
- function socketOnDrain() {
- // If we previously paused, then start reading again.
- if (socket._paused) {
- socket._paused = false;
- socket.parser.resume();
- socket.resume();
- }
- }
- socket.on('drain', socketOnDrain);
- function parserOnIncoming(req, shouldKeepAlive) {
- incoming.push(req);
- // If the writable end isn't consuming, then stop reading
- // so that we don't become overwhelmed by a flood of
- // pipelined requests that may never be resolved.
- if (!socket._paused) {
- var needPause = socket._writableState.needDrain;
- if (needPause) {
- socket._paused = true;
- // We also need to pause the parser, but don't do that until after
- // the call to execute, because we may still be processing the last
- // chunk.
- socket.pause();
- }
- }
- 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
- // response, if so destroy the socket.
- res.on('prefinish', resOnFinish);
- function resOnFinish() {
- // 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._readableState.resumeScheduled)
- 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 (!util.isUndefined(req.headers.expect) &&
- (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;