// Copyright (c) 2013, Joyent, Inc. All rights reserved. 'use strict'; var EventEmitter = require('events').EventEmitter; var util = require('util'); var assert = require('assert-plus'); /** * An custom error for capturing an invalid upgrade state. * * @public * @class * @param {String} msg - an error message */ function InvalidUpgradeStateError(msg) { if (Error.captureStackTrace) { Error.captureStackTrace(this, InvalidUpgradeStateError); } this.message = msg; this.name = 'InvalidUpgradeStateError'; } util.inherits(InvalidUpgradeStateError, Error); // // The Node HTTP Server will, if we handle the 'upgrade' event, swallow any // Request with the 'Connection: upgrade' header set. While doing this it // detaches from the 'data' events on the Socket and passes the socket to // us, so that we may take over handling for the connection. // // Unfortunately, the API does not presently provide a http.ServerResponse // for us to use in the event that we do not wish to upgrade the connection. // This factory method provides a skeletal implementation of a // restify-compatible response that is sufficient to allow the existing // request handling path to work, while allowing us to perform _at most_ one // of either: // // - Return a basic HTTP Response with a provided Status Code and // close the socket. // - Upgrade the connection and stop further processing. // // To determine if an upgrade is requested, a route handler would check for // the 'claimUpgrade' method on the Response. The object this method // returns will have the 'socket' and 'head' Buffer emitted with the // 'upgrade' event by the http.Server. If the upgrade is not possible, such // as when the HTTP head (or a full request) has already been sent by some // other handler, this method will throw. // /** * Create a new upgraded response. * * @public * @function createServerUpgradeResponse * @param {Object} req - the request object * @param {Object} socket - the network socket * @param {Object} head - a buffer, the first packet of the upgraded stream * @returns {Object} an upgraded reponses */ function createServerUpgradeResponse(req, socket, head) { return new ServerUpgradeResponse(socket, head); } /** * Upgrade the http response * * @private * @class * @param {Object} socket - the network socket * @param {Object} head - a buffer, the first packet of * the upgraded stream * @returns {undefined} no return value */ function ServerUpgradeResponse(socket, head) { assert.object(socket, 'socket'); assert.buffer(head, 'head'); EventEmitter.call(this); this.sendDate = true; this.statusCode = 400; this._upgrade = { socket: socket, head: head }; this._headWritten = false; this._upgradeClaimed = false; } util.inherits(ServerUpgradeResponse, EventEmitter); /** * A function generator for all programatically attaching methods on to * the ServerUpgradeResponse class. * * @private * @function notImplemented * @param {Object} method - an object containing configuration * @returns {Function} function */ function notImplemented(method) { if (!method.throws) { return function returns() { return method.returns; }; } else { return function throws() { throw new Error('Method ' + method.name + ' is not implemented!'); }; } } var NOT_IMPLEMENTED = [ { name: 'writeContinue', throws: true }, { name: 'setHeader', throws: false, returns: null }, { name: 'getHeader', throws: false, returns: null }, { name: 'getHeaders', throws: false, returns: {} }, { name: 'removeHeader', throws: false, returns: null }, { name: 'addTrailer', throws: false, returns: null }, { name: 'cache', throws: false, returns: 'public' }, { name: 'format', throws: true }, { name: 'set', throws: false, returns: null }, { name: 'get', throws: false, returns: null }, { name: 'headers', throws: false, returns: {} }, { name: 'header', throws: false, returns: null }, { name: 'json', throws: false, returns: null }, { name: 'link', throws: false, returns: null } ]; // programatically add a bunch of methods to the ServerUpgradeResponse proto NOT_IMPLEMENTED.forEach(function forEach(method) { ServerUpgradeResponse.prototype[method.name] = notImplemented(method); }); /** * Internal implementation of `writeHead` * * @private * @function _writeHeadImpl * @param {Number} statusCode - the http status code * @param {String} reason - a message * @returns {undefined} no return value */ ServerUpgradeResponse.prototype._writeHeadImpl = function _writeHeadImpl( statusCode, reason ) { if (this._headWritten) { return; } this._headWritten = true; if (this._upgradeClaimed) { throw new InvalidUpgradeStateError('Upgrade already claimed!'); } var head = ['HTTP/1.1 ' + statusCode + ' ' + reason, 'Connection: close']; if (this.sendDate) { head.push('Date: ' + new Date().toUTCString()); } this._upgrade.socket.write(head.join('\r\n') + '\r\n'); }; /** * Set the status code of the response. * * @public * @function status * @param {Number} code - the http status code * @returns {undefined} no return value */ ServerUpgradeResponse.prototype.status = function status(code) { assert.number(code, 'code'); this.statusCode = code; return code; }; /** * Sends the response. * * @public * @function send * @param {Number} code - the http status code * @param {Object | String} body - the response to send out * @returns {undefined} no return value */ ServerUpgradeResponse.prototype.send = function send(code, body) { if (typeof code === 'number') { this.statusCode = code; } else { body = code; } if (typeof body === 'object') { if (typeof body.statusCode === 'number') { this.statusCode = body.statusCode; } if (typeof body.message === 'string') { this.statusReason = body.message; } } return this.end(); }; /** * End the response. * * @public * @function end * @returns {Boolean} always returns true */ ServerUpgradeResponse.prototype.end = function end() { this._writeHeadImpl(this.statusCode, 'Connection Not Upgraded'); this._upgrade.socket.end('\r\n'); return true; }; /** * Write to the response. * * @public * @function write * @returns {Boolean} always returns true */ ServerUpgradeResponse.prototype.write = function write() { this._writeHeadImpl(this.statusCode, 'Connection Not Upgraded'); return true; }; /** * Write to the head of the response. * * @public * @function writeHead * @param {Number} statusCode - the http status code * @param {String} reason - a message * @returns {undefined} no return value */ ServerUpgradeResponse.prototype.writeHead = function writeHead( statusCode, reason ) { assert.number(statusCode, 'statusCode'); assert.optionalString(reason, 'reason'); this.statusCode = statusCode; if (!reason) { reason = 'Connection Not Upgraded'; } if (this._headWritten) { throw new Error('Head already written!'); } return this._writeHeadImpl(statusCode, reason); }; /** * Attempt to upgrade. * * @public * @function claimUpgrade * @returns {Object} an object containing the socket and head */ ServerUpgradeResponse.prototype.claimUpgrade = function claimUpgrade() { if (this._upgradeClaimed) { throw new InvalidUpgradeStateError('Upgrade already claimed!'); } if (this._headWritten) { throw new InvalidUpgradeStateError('Upgrade already aborted!'); } this._upgradeClaimed = true; return this._upgrade; }; module.exports = { createResponse: createServerUpgradeResponse, InvalidUpgradeStateError: InvalidUpgradeStateError };