UNPKG

8.06 kBJavaScriptView Raw
1// Copyright (c) 2013, Joyent, Inc. All rights reserved.
2
3'use strict';
4
5var EventEmitter = require('events').EventEmitter;
6var util = require('util');
7var assert = require('assert-plus');
8
9/**
10 * An custom error for capturing an invalid upgrade state.
11 *
12 * @public
13 * @class
14 * @param {String} msg - an error message
15 */
16function InvalidUpgradeStateError(msg) {
17 if (Error.captureStackTrace) {
18 Error.captureStackTrace(this, InvalidUpgradeStateError);
19 }
20
21 this.message = msg;
22 this.name = 'InvalidUpgradeStateError';
23}
24util.inherits(InvalidUpgradeStateError, Error);
25
26//
27// The Node HTTP Server will, if we handle the 'upgrade' event, swallow any
28// Request with the 'Connection: upgrade' header set. While doing this it
29// detaches from the 'data' events on the Socket and passes the socket to
30// us, so that we may take over handling for the connection.
31//
32// Unfortunately, the API does not presently provide a http.ServerResponse
33// for us to use in the event that we do not wish to upgrade the connection.
34// This factory method provides a skeletal implementation of a
35// restify-compatible response that is sufficient to allow the existing
36// request handling path to work, while allowing us to perform _at most_ one
37// of either:
38//
39// - Return a basic HTTP Response with a provided Status Code and
40// close the socket.
41// - Upgrade the connection and stop further processing.
42//
43// To determine if an upgrade is requested, a route handler would check for
44// the 'claimUpgrade' method on the Response. The object this method
45// returns will have the 'socket' and 'head' Buffer emitted with the
46// 'upgrade' event by the http.Server. If the upgrade is not possible, such
47// as when the HTTP head (or a full request) has already been sent by some
48// other handler, this method will throw.
49//
50
51/**
52 * Create a new upgraded response.
53 *
54 * @public
55 * @function createServerUpgradeResponse
56 * @param {Object} req - the request object
57 * @param {Object} socket - the network socket
58 * @param {Object} head - a buffer, the first packet of the upgraded stream
59 * @returns {Object} an upgraded reponses
60 */
61function createServerUpgradeResponse(req, socket, head) {
62 return new ServerUpgradeResponse(socket, head);
63}
64
65/**
66 * Upgrade the http response
67 *
68 * @private
69 * @class
70 * @param {Object} socket - the network socket
71 * @param {Object} head - a buffer, the first packet of
72 * the upgraded stream
73 * @returns {undefined} no return value
74 */
75function ServerUpgradeResponse(socket, head) {
76 assert.object(socket, 'socket');
77 assert.buffer(head, 'head');
78
79 EventEmitter.call(this);
80
81 this.sendDate = true;
82 this.statusCode = 400;
83
84 this._upgrade = {
85 socket: socket,
86 head: head
87 };
88
89 this._headWritten = false;
90 this._upgradeClaimed = false;
91}
92util.inherits(ServerUpgradeResponse, EventEmitter);
93
94/**
95 * A function generator for all programatically attaching methods on to
96 * the ServerUpgradeResponse class.
97 *
98 * @private
99 * @function notImplemented
100 * @param {Object} method - an object containing configuration
101 * @returns {Function} function
102 */
103function notImplemented(method) {
104 if (!method.throws) {
105 return function returns() {
106 return method.returns;
107 };
108 } else {
109 return function throws() {
110 throw new Error('Method ' + method.name + ' is not implemented!');
111 };
112 }
113}
114
115var NOT_IMPLEMENTED = [
116 { name: 'writeContinue', throws: true },
117 { name: 'setHeader', throws: false, returns: null },
118 { name: 'getHeader', throws: false, returns: null },
119 { name: 'getHeaders', throws: false, returns: {} },
120 { name: 'removeHeader', throws: false, returns: null },
121 { name: 'addTrailer', throws: false, returns: null },
122 { name: 'cache', throws: false, returns: 'public' },
123 { name: 'format', throws: true },
124 { name: 'set', throws: false, returns: null },
125 { name: 'get', throws: false, returns: null },
126 { name: 'headers', throws: false, returns: {} },
127 { name: 'header', throws: false, returns: null },
128 { name: 'json', throws: false, returns: null },
129 { name: 'link', throws: false, returns: null }
130];
131
132// programatically add a bunch of methods to the ServerUpgradeResponse proto
133NOT_IMPLEMENTED.forEach(function forEach(method) {
134 ServerUpgradeResponse.prototype[method.name] = notImplemented(method);
135});
136
137/**
138 * Internal implementation of `writeHead`
139 *
140 * @private
141 * @function _writeHeadImpl
142 * @param {Number} statusCode - the http status code
143 * @param {String} reason - a message
144 * @returns {undefined} no return value
145 */
146ServerUpgradeResponse.prototype._writeHeadImpl = function _writeHeadImpl(
147 statusCode,
148 reason
149) {
150 if (this._headWritten) {
151 return;
152 }
153 this._headWritten = true;
154
155 if (this._upgradeClaimed) {
156 throw new InvalidUpgradeStateError('Upgrade already claimed!');
157 }
158
159 var head = ['HTTP/1.1 ' + statusCode + ' ' + reason, 'Connection: close'];
160
161 if (this.sendDate) {
162 head.push('Date: ' + new Date().toUTCString());
163 }
164
165 this._upgrade.socket.write(head.join('\r\n') + '\r\n');
166};
167
168/**
169 * Set the status code of the response.
170 *
171 * @public
172 * @function status
173 * @param {Number} code - the http status code
174 * @returns {undefined} no return value
175 */
176ServerUpgradeResponse.prototype.status = function status(code) {
177 assert.number(code, 'code');
178 this.statusCode = code;
179 return code;
180};
181
182/**
183 * Sends the response.
184 *
185 * @public
186 * @function send
187 * @param {Number} code - the http status code
188 * @param {Object | String} body - the response to send out
189 * @returns {undefined} no return value
190 */
191ServerUpgradeResponse.prototype.send = function send(code, body) {
192 if (typeof code === 'number') {
193 this.statusCode = code;
194 } else {
195 body = code;
196 }
197
198 if (typeof body === 'object') {
199 if (typeof body.statusCode === 'number') {
200 this.statusCode = body.statusCode;
201 }
202
203 if (typeof body.message === 'string') {
204 this.statusReason = body.message;
205 }
206 }
207
208 return this.end();
209};
210
211/**
212 * End the response.
213 *
214 * @public
215 * @function end
216 * @returns {Boolean} always returns true
217 */
218ServerUpgradeResponse.prototype.end = function end() {
219 this._writeHeadImpl(this.statusCode, 'Connection Not Upgraded');
220 this._upgrade.socket.end('\r\n');
221 return true;
222};
223
224/**
225 * Write to the response.
226 *
227 * @public
228 * @function write
229 * @returns {Boolean} always returns true
230 */
231ServerUpgradeResponse.prototype.write = function write() {
232 this._writeHeadImpl(this.statusCode, 'Connection Not Upgraded');
233 return true;
234};
235
236/**
237 * Write to the head of the response.
238 *
239 * @public
240 * @function writeHead
241 * @param {Number} statusCode - the http status code
242 * @param {String} reason - a message
243 * @returns {undefined} no return value
244 */
245ServerUpgradeResponse.prototype.writeHead = function writeHead(
246 statusCode,
247 reason
248) {
249 assert.number(statusCode, 'statusCode');
250 assert.optionalString(reason, 'reason');
251
252 this.statusCode = statusCode;
253
254 if (!reason) {
255 reason = 'Connection Not Upgraded';
256 }
257
258 if (this._headWritten) {
259 throw new Error('Head already written!');
260 }
261
262 return this._writeHeadImpl(statusCode, reason);
263};
264
265/**
266 * Attempt to upgrade.
267 *
268 * @public
269 * @function claimUpgrade
270 * @returns {Object} an object containing the socket and head
271 */
272ServerUpgradeResponse.prototype.claimUpgrade = function claimUpgrade() {
273 if (this._upgradeClaimed) {
274 throw new InvalidUpgradeStateError('Upgrade already claimed!');
275 }
276
277 if (this._headWritten) {
278 throw new InvalidUpgradeStateError('Upgrade already aborted!');
279 }
280
281 this._upgradeClaimed = true;
282
283 return this._upgrade;
284};
285
286module.exports = {
287 createResponse: createServerUpgradeResponse,
288 InvalidUpgradeStateError: InvalidUpgradeStateError
289};