relu-core
Version:
289 lines (241 loc) • 8.02 kB
JavaScript
// 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}
*/
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 notImplemented(method) {
if (!method.throws) {
return function () {
return (method.returns);
};
} else {
return function () {
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 (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}
*/
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}
*/
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}
*/
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}
*/
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
};