| 1 |
liveuser |
1 |
/************************************************************************
|
|
|
2 |
* Copyright 2010-2015 Brian McKelvey.
|
|
|
3 |
*
|
|
|
4 |
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
5 |
* you may not use this file except in compliance with the License.
|
|
|
6 |
* You may obtain a copy of the License at
|
|
|
7 |
*
|
|
|
8 |
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
9 |
*
|
|
|
10 |
* Unless required by applicable law or agreed to in writing, software
|
|
|
11 |
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
12 |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
13 |
* See the License for the specific language governing permissions and
|
|
|
14 |
* limitations under the License.
|
|
|
15 |
***********************************************************************/
|
|
|
16 |
|
|
|
17 |
var crypto = require('crypto');
|
|
|
18 |
var util = require('util');
|
|
|
19 |
var url = require('url');
|
|
|
20 |
var EventEmitter = require('events').EventEmitter;
|
|
|
21 |
var WebSocketConnection = require('./WebSocketConnection');
|
|
|
22 |
|
|
|
23 |
var headerValueSplitRegExp = /,\s*/;
|
|
|
24 |
var headerParamSplitRegExp = /;\s*/;
|
|
|
25 |
var headerSanitizeRegExp = /[\r\n]/g;
|
|
|
26 |
var xForwardedForSeparatorRegExp = /,\s*/;
|
|
|
27 |
var separators = [
|
|
|
28 |
'(', ')', '<', '>', '@',
|
|
|
29 |
',', ';', ':', '\\', '\"',
|
|
|
30 |
'/', '[', ']', '?', '=',
|
|
|
31 |
'{', '}', ' ', String.fromCharCode(9)
|
|
|
32 |
];
|
|
|
33 |
var controlChars = [String.fromCharCode(127) /* DEL */];
|
|
|
34 |
for (var i=0; i < 31; i ++) {
|
|
|
35 |
/* US-ASCII Control Characters */
|
|
|
36 |
controlChars.push(String.fromCharCode(i));
|
|
|
37 |
}
|
|
|
38 |
|
|
|
39 |
var cookieNameValidateRegEx = /([\x00-\x20\x22\x28\x29\x2c\x2f\x3a-\x3f\x40\x5b-\x5e\x7b\x7d\x7f])/;
|
|
|
40 |
var cookieValueValidateRegEx = /[^\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]/;
|
|
|
41 |
var cookieValueDQuoteValidateRegEx = /^"[^"]*"$/;
|
|
|
42 |
var controlCharsAndSemicolonRegEx = /[\x00-\x20\x3b]/g;
|
|
|
43 |
|
|
|
44 |
var cookieSeparatorRegEx = /[;,] */;
|
|
|
45 |
|
|
|
46 |
var httpStatusDescriptions = {
|
|
|
47 |
100: 'Continue',
|
|
|
48 |
101: 'Switching Protocols',
|
|
|
49 |
200: 'OK',
|
|
|
50 |
201: 'Created',
|
|
|
51 |
203: 'Non-Authoritative Information',
|
|
|
52 |
204: 'No Content',
|
|
|
53 |
205: 'Reset Content',
|
|
|
54 |
206: 'Partial Content',
|
|
|
55 |
300: 'Multiple Choices',
|
|
|
56 |
301: 'Moved Permanently',
|
|
|
57 |
302: 'Found',
|
|
|
58 |
303: 'See Other',
|
|
|
59 |
304: 'Not Modified',
|
|
|
60 |
305: 'Use Proxy',
|
|
|
61 |
307: 'Temporary Redirect',
|
|
|
62 |
400: 'Bad Request',
|
|
|
63 |
401: 'Unauthorized',
|
|
|
64 |
402: 'Payment Required',
|
|
|
65 |
403: 'Forbidden',
|
|
|
66 |
404: 'Not Found',
|
|
|
67 |
406: 'Not Acceptable',
|
|
|
68 |
407: 'Proxy Authorization Required',
|
|
|
69 |
408: 'Request Timeout',
|
|
|
70 |
409: 'Conflict',
|
|
|
71 |
410: 'Gone',
|
|
|
72 |
411: 'Length Required',
|
|
|
73 |
412: 'Precondition Failed',
|
|
|
74 |
413: 'Request Entity Too Long',
|
|
|
75 |
414: 'Request-URI Too Long',
|
|
|
76 |
415: 'Unsupported Media Type',
|
|
|
77 |
416: 'Requested Range Not Satisfiable',
|
|
|
78 |
417: 'Expectation Failed',
|
|
|
79 |
426: 'Upgrade Required',
|
|
|
80 |
500: 'Internal Server Error',
|
|
|
81 |
501: 'Not Implemented',
|
|
|
82 |
502: 'Bad Gateway',
|
|
|
83 |
503: 'Service Unavailable',
|
|
|
84 |
504: 'Gateway Timeout',
|
|
|
85 |
505: 'HTTP Version Not Supported'
|
|
|
86 |
};
|
|
|
87 |
|
|
|
88 |
function WebSocketRequest(socket, httpRequest, serverConfig) {
|
|
|
89 |
// Superclass Constructor
|
|
|
90 |
EventEmitter.call(this);
|
|
|
91 |
|
|
|
92 |
this.socket = socket;
|
|
|
93 |
this.httpRequest = httpRequest;
|
|
|
94 |
this.resource = httpRequest.url;
|
|
|
95 |
this.remoteAddress = socket.remoteAddress;
|
|
|
96 |
this.remoteAddresses = [this.remoteAddress];
|
|
|
97 |
this.serverConfig = serverConfig;
|
|
|
98 |
|
|
|
99 |
// Watch for the underlying TCP socket closing before we call accept
|
|
|
100 |
this._socketIsClosing = false;
|
|
|
101 |
this._socketCloseHandler = this._handleSocketCloseBeforeAccept.bind(this);
|
|
|
102 |
this.socket.on('end', this._socketCloseHandler);
|
|
|
103 |
this.socket.on('close', this._socketCloseHandler);
|
|
|
104 |
|
|
|
105 |
this._resolved = false;
|
|
|
106 |
}
|
|
|
107 |
|
|
|
108 |
util.inherits(WebSocketRequest, EventEmitter);
|
|
|
109 |
|
|
|
110 |
WebSocketRequest.prototype.readHandshake = function() {
|
|
|
111 |
var self = this;
|
|
|
112 |
var request = this.httpRequest;
|
|
|
113 |
|
|
|
114 |
// Decode URL
|
|
|
115 |
this.resourceURL = url.parse(this.resource, true);
|
|
|
116 |
|
|
|
117 |
this.host = request.headers['host'];
|
|
|
118 |
if (!this.host) {
|
|
|
119 |
throw new Error('Client must provide a Host header.');
|
|
|
120 |
}
|
|
|
121 |
|
|
|
122 |
this.key = request.headers['sec-websocket-key'];
|
|
|
123 |
if (!this.key) {
|
|
|
124 |
throw new Error('Client must provide a value for Sec-WebSocket-Key.');
|
|
|
125 |
}
|
|
|
126 |
|
|
|
127 |
this.webSocketVersion = parseInt(request.headers['sec-websocket-version'], 10);
|
|
|
128 |
|
|
|
129 |
if (!this.webSocketVersion || isNaN(this.webSocketVersion)) {
|
|
|
130 |
throw new Error('Client must provide a value for Sec-WebSocket-Version.');
|
|
|
131 |
}
|
|
|
132 |
|
|
|
133 |
switch (this.webSocketVersion) {
|
|
|
134 |
case 8:
|
|
|
135 |
case 13:
|
|
|
136 |
break;
|
|
|
137 |
default:
|
|
|
138 |
var e = new Error('Unsupported websocket client version: ' + this.webSocketVersion +
|
|
|
139 |
'Only versions 8 and 13 are supported.');
|
|
|
140 |
e.httpCode = 426;
|
|
|
141 |
e.headers = {
|
|
|
142 |
'Sec-WebSocket-Version': '13'
|
|
|
143 |
};
|
|
|
144 |
throw e;
|
|
|
145 |
}
|
|
|
146 |
|
|
|
147 |
if (this.webSocketVersion === 13) {
|
|
|
148 |
this.origin = request.headers['origin'];
|
|
|
149 |
}
|
|
|
150 |
else if (this.webSocketVersion === 8) {
|
|
|
151 |
this.origin = request.headers['sec-websocket-origin'];
|
|
|
152 |
}
|
|
|
153 |
|
|
|
154 |
// Protocol is optional.
|
|
|
155 |
var protocolString = request.headers['sec-websocket-protocol'];
|
|
|
156 |
this.protocolFullCaseMap = {};
|
|
|
157 |
this.requestedProtocols = [];
|
|
|
158 |
if (protocolString) {
|
|
|
159 |
var requestedProtocolsFullCase = protocolString.split(headerValueSplitRegExp);
|
|
|
160 |
requestedProtocolsFullCase.forEach(function(protocol) {
|
|
|
161 |
var lcProtocol = protocol.toLocaleLowerCase();
|
|
|
162 |
self.requestedProtocols.push(lcProtocol);
|
|
|
163 |
self.protocolFullCaseMap[lcProtocol] = protocol;
|
|
|
164 |
});
|
|
|
165 |
}
|
|
|
166 |
|
|
|
167 |
if (!this.serverConfig.ignoreXForwardedFor &&
|
|
|
168 |
request.headers['x-forwarded-for']) {
|
|
|
169 |
var immediatePeerIP = this.remoteAddress;
|
|
|
170 |
this.remoteAddresses = request.headers['x-forwarded-for']
|
|
|
171 |
.split(xForwardedForSeparatorRegExp);
|
|
|
172 |
this.remoteAddresses.push(immediatePeerIP);
|
|
|
173 |
this.remoteAddress = this.remoteAddresses[0];
|
|
|
174 |
}
|
|
|
175 |
|
|
|
176 |
// Extensions are optional.
|
|
|
177 |
var extensionsString = request.headers['sec-websocket-extensions'];
|
|
|
178 |
this.requestedExtensions = this.parseExtensions(extensionsString);
|
|
|
179 |
|
|
|
180 |
// Cookies are optional
|
|
|
181 |
var cookieString = request.headers['cookie'];
|
|
|
182 |
this.cookies = this.parseCookies(cookieString);
|
|
|
183 |
};
|
|
|
184 |
|
|
|
185 |
WebSocketRequest.prototype.parseExtensions = function(extensionsString) {
|
|
|
186 |
if (!extensionsString || extensionsString.length === 0) {
|
|
|
187 |
return [];
|
|
|
188 |
}
|
|
|
189 |
var extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp);
|
|
|
190 |
extensions.forEach(function(extension, index, array) {
|
|
|
191 |
var params = extension.split(headerParamSplitRegExp);
|
|
|
192 |
var extensionName = params[0];
|
|
|
193 |
var extensionParams = params.slice(1);
|
|
|
194 |
extensionParams.forEach(function(rawParam, index, array) {
|
|
|
195 |
var arr = rawParam.split('=');
|
|
|
196 |
var obj = {
|
|
|
197 |
name: arr[0],
|
|
|
198 |
value: arr[1]
|
|
|
199 |
};
|
|
|
200 |
array.splice(index, 1, obj);
|
|
|
201 |
});
|
|
|
202 |
var obj = {
|
|
|
203 |
name: extensionName,
|
|
|
204 |
params: extensionParams
|
|
|
205 |
};
|
|
|
206 |
array.splice(index, 1, obj);
|
|
|
207 |
});
|
|
|
208 |
return extensions;
|
|
|
209 |
};
|
|
|
210 |
|
|
|
211 |
// This function adapted from node-cookie
|
|
|
212 |
// https://github.com/shtylman/node-cookie
|
|
|
213 |
WebSocketRequest.prototype.parseCookies = function(str) {
|
|
|
214 |
// Sanity Check
|
|
|
215 |
if (!str || typeof(str) !== 'string') {
|
|
|
216 |
return [];
|
|
|
217 |
}
|
|
|
218 |
|
|
|
219 |
var cookies = [];
|
|
|
220 |
var pairs = str.split(cookieSeparatorRegEx);
|
|
|
221 |
|
|
|
222 |
pairs.forEach(function(pair) {
|
|
|
223 |
var eq_idx = pair.indexOf('=');
|
|
|
224 |
if (eq_idx === -1) {
|
|
|
225 |
cookies.push({
|
|
|
226 |
name: pair,
|
|
|
227 |
value: null
|
|
|
228 |
});
|
|
|
229 |
return;
|
|
|
230 |
}
|
|
|
231 |
|
|
|
232 |
var key = pair.substr(0, eq_idx).trim();
|
|
|
233 |
var val = pair.substr(++eq_idx, pair.length).trim();
|
|
|
234 |
|
|
|
235 |
// quoted values
|
|
|
236 |
if ('"' === val[0]) {
|
|
|
237 |
val = val.slice(1, -1);
|
|
|
238 |
}
|
|
|
239 |
|
|
|
240 |
cookies.push({
|
|
|
241 |
name: key,
|
|
|
242 |
value: decodeURIComponent(val)
|
|
|
243 |
});
|
|
|
244 |
});
|
|
|
245 |
|
|
|
246 |
return cookies;
|
|
|
247 |
};
|
|
|
248 |
|
|
|
249 |
WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) {
|
|
|
250 |
this._verifyResolution();
|
|
|
251 |
|
|
|
252 |
// TODO: Handle extensions
|
|
|
253 |
|
|
|
254 |
var protocolFullCase;
|
|
|
255 |
|
|
|
256 |
if (acceptedProtocol) {
|
|
|
257 |
protocolFullCase = this.protocolFullCaseMap[acceptedProtocol.toLocaleLowerCase()];
|
|
|
258 |
if (typeof(protocolFullCase) === 'undefined') {
|
|
|
259 |
protocolFullCase = acceptedProtocol;
|
|
|
260 |
}
|
|
|
261 |
}
|
|
|
262 |
else {
|
|
|
263 |
protocolFullCase = acceptedProtocol;
|
|
|
264 |
}
|
|
|
265 |
this.protocolFullCaseMap = null;
|
|
|
266 |
|
|
|
267 |
// Create key validation hash
|
|
|
268 |
var sha1 = crypto.createHash('sha1');
|
|
|
269 |
sha1.update(this.key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
|
|
|
270 |
var acceptKey = sha1.digest('base64');
|
|
|
271 |
|
|
|
272 |
var response = 'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
|
273 |
'Upgrade: websocket\r\n' +
|
|
|
274 |
'Connection: Upgrade\r\n' +
|
|
|
275 |
'Sec-WebSocket-Accept: ' + acceptKey + '\r\n';
|
|
|
276 |
|
|
|
277 |
if (protocolFullCase) {
|
|
|
278 |
// validate protocol
|
|
|
279 |
for (var i=0; i < protocolFullCase.length; i++) {
|
|
|
280 |
var charCode = protocolFullCase.charCodeAt(i);
|
|
|
281 |
var character = protocolFullCase.charAt(i);
|
|
|
282 |
if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) {
|
|
|
283 |
this.reject(500);
|
|
|
284 |
throw new Error('Illegal character "' + String.fromCharCode(character) + '" in subprotocol.');
|
|
|
285 |
}
|
|
|
286 |
}
|
|
|
287 |
if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) {
|
|
|
288 |
this.reject(500);
|
|
|
289 |
throw new Error('Specified protocol was not requested by the client.');
|
|
|
290 |
}
|
|
|
291 |
|
|
|
292 |
protocolFullCase = protocolFullCase.replace(headerSanitizeRegExp, '');
|
|
|
293 |
response += 'Sec-WebSocket-Protocol: ' + protocolFullCase + '\r\n';
|
|
|
294 |
}
|
|
|
295 |
this.requestedProtocols = null;
|
|
|
296 |
|
|
|
297 |
if (allowedOrigin) {
|
|
|
298 |
allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, '');
|
|
|
299 |
if (this.webSocketVersion === 13) {
|
|
|
300 |
response += 'Origin: ' + allowedOrigin + '\r\n';
|
|
|
301 |
}
|
|
|
302 |
else if (this.webSocketVersion === 8) {
|
|
|
303 |
response += 'Sec-WebSocket-Origin: ' + allowedOrigin + '\r\n';
|
|
|
304 |
}
|
|
|
305 |
}
|
|
|
306 |
|
|
|
307 |
if (cookies) {
|
|
|
308 |
if (!Array.isArray(cookies)) {
|
|
|
309 |
this.reject(500);
|
|
|
310 |
throw new Error('Value supplied for "cookies" argument must be an array.');
|
|
|
311 |
}
|
|
|
312 |
var seenCookies = {};
|
|
|
313 |
cookies.forEach(function(cookie) {
|
|
|
314 |
if (!cookie.name || !cookie.value) {
|
|
|
315 |
this.reject(500);
|
|
|
316 |
throw new Error('Each cookie to set must at least provide a "name" and "value"');
|
|
|
317 |
}
|
|
|
318 |
|
|
|
319 |
// Make sure there are no \r\n sequences inserted
|
|
|
320 |
cookie.name = cookie.name.replace(controlCharsAndSemicolonRegEx, '');
|
|
|
321 |
cookie.value = cookie.value.replace(controlCharsAndSemicolonRegEx, '');
|
|
|
322 |
|
|
|
323 |
if (seenCookies[cookie.name]) {
|
|
|
324 |
this.reject(500);
|
|
|
325 |
throw new Error('You may not specify the same cookie name twice.');
|
|
|
326 |
}
|
|
|
327 |
seenCookies[cookie.name] = true;
|
|
|
328 |
|
|
|
329 |
// token (RFC 2616, Section 2.2)
|
|
|
330 |
var invalidChar = cookie.name.match(cookieNameValidateRegEx);
|
|
|
331 |
if (invalidChar) {
|
|
|
332 |
this.reject(500);
|
|
|
333 |
throw new Error('Illegal character ' + invalidChar[0] + ' in cookie name');
|
|
|
334 |
}
|
|
|
335 |
|
|
|
336 |
// RFC 6265, Section 4.1.1
|
|
|
337 |
// *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) | %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
|
|
338 |
if (cookie.value.match(cookieValueDQuoteValidateRegEx)) {
|
|
|
339 |
invalidChar = cookie.value.slice(1, -1).match(cookieValueValidateRegEx);
|
|
|
340 |
} else {
|
|
|
341 |
invalidChar = cookie.value.match(cookieValueValidateRegEx);
|
|
|
342 |
}
|
|
|
343 |
if (invalidChar) {
|
|
|
344 |
this.reject(500);
|
|
|
345 |
throw new Error('Illegal character ' + invalidChar[0] + ' in cookie value');
|
|
|
346 |
}
|
|
|
347 |
|
|
|
348 |
var cookieParts = [cookie.name + '=' + cookie.value];
|
|
|
349 |
|
|
|
350 |
// RFC 6265, Section 4.1.1
|
|
|
351 |
// 'Path=' path-value | <any CHAR except CTLs or ';'>
|
|
|
352 |
if(cookie.path){
|
|
|
353 |
invalidChar = cookie.path.match(controlCharsAndSemicolonRegEx);
|
|
|
354 |
if (invalidChar) {
|
|
|
355 |
this.reject(500);
|
|
|
356 |
throw new Error('Illegal character ' + invalidChar[0] + ' in cookie path');
|
|
|
357 |
}
|
|
|
358 |
cookieParts.push('Path=' + cookie.path);
|
|
|
359 |
}
|
|
|
360 |
|
|
|
361 |
// RFC 6265, Section 4.1.2.3
|
|
|
362 |
// 'Domain=' subdomain
|
|
|
363 |
if (cookie.domain) {
|
|
|
364 |
if (typeof(cookie.domain) !== 'string') {
|
|
|
365 |
this.reject(500);
|
|
|
366 |
throw new Error('Domain must be specified and must be a string.');
|
|
|
367 |
}
|
|
|
368 |
invalidChar = cookie.domain.match(controlCharsAndSemicolonRegEx);
|
|
|
369 |
if (invalidChar) {
|
|
|
370 |
this.reject(500);
|
|
|
371 |
throw new Error('Illegal character ' + invalidChar[0] + ' in cookie domain');
|
|
|
372 |
}
|
|
|
373 |
cookieParts.push('Domain=' + cookie.domain.toLowerCase());
|
|
|
374 |
}
|
|
|
375 |
|
|
|
376 |
// RFC 6265, Section 4.1.1
|
|
|
377 |
//'Expires=' sane-cookie-date | Force Date object requirement by using only epoch
|
|
|
378 |
if (cookie.expires) {
|
|
|
379 |
if (!(cookie.expires instanceof Date)){
|
|
|
380 |
this.reject(500);
|
|
|
381 |
throw new Error('Value supplied for cookie "expires" must be a vaild date object');
|
|
|
382 |
}
|
|
|
383 |
cookieParts.push('Expires=' + cookie.expires.toGMTString());
|
|
|
384 |
}
|
|
|
385 |
|
|
|
386 |
// RFC 6265, Section 4.1.1
|
|
|
387 |
//'Max-Age=' non-zero-digit *DIGIT
|
|
|
388 |
if (cookie.maxage) {
|
|
|
389 |
var maxage = cookie.maxage;
|
|
|
390 |
if (typeof(maxage) === 'string') {
|
|
|
391 |
maxage = parseInt(maxage, 10);
|
|
|
392 |
}
|
|
|
393 |
if (isNaN(maxage) || maxage <= 0 ) {
|
|
|
394 |
this.reject(500);
|
|
|
395 |
throw new Error('Value supplied for cookie "maxage" must be a non-zero number');
|
|
|
396 |
}
|
|
|
397 |
maxage = Math.round(maxage);
|
|
|
398 |
cookieParts.push('Max-Age=' + maxage.toString(10));
|
|
|
399 |
}
|
|
|
400 |
|
|
|
401 |
// RFC 6265, Section 4.1.1
|
|
|
402 |
//'Secure;'
|
|
|
403 |
if (cookie.secure) {
|
|
|
404 |
if (typeof(cookie.secure) !== 'boolean') {
|
|
|
405 |
this.reject(500);
|
|
|
406 |
throw new Error('Value supplied for cookie "secure" must be of type boolean');
|
|
|
407 |
}
|
|
|
408 |
cookieParts.push('Secure');
|
|
|
409 |
}
|
|
|
410 |
|
|
|
411 |
// RFC 6265, Section 4.1.1
|
|
|
412 |
//'HttpOnly;'
|
|
|
413 |
if (cookie.httponly) {
|
|
|
414 |
if (typeof(cookie.httponly) !== 'boolean') {
|
|
|
415 |
this.reject(500);
|
|
|
416 |
throw new Error('Value supplied for cookie "httponly" must be of type boolean');
|
|
|
417 |
}
|
|
|
418 |
cookieParts.push('HttpOnly');
|
|
|
419 |
}
|
|
|
420 |
|
|
|
421 |
response += ('Set-Cookie: ' + cookieParts.join(';') + '\r\n');
|
|
|
422 |
}.bind(this));
|
|
|
423 |
}
|
|
|
424 |
|
|
|
425 |
// TODO: handle negotiated extensions
|
|
|
426 |
// if (negotiatedExtensions) {
|
|
|
427 |
// response += 'Sec-WebSocket-Extensions: ' + negotiatedExtensions.join(', ') + '\r\n';
|
|
|
428 |
// }
|
|
|
429 |
|
|
|
430 |
// Mark the request resolved now so that the user can't call accept or
|
|
|
431 |
// reject a second time.
|
|
|
432 |
this._resolved = true;
|
|
|
433 |
this.emit('requestResolved', this);
|
|
|
434 |
|
|
|
435 |
response += '\r\n';
|
|
|
436 |
|
|
|
437 |
var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig);
|
|
|
438 |
connection.webSocketVersion = this.webSocketVersion;
|
|
|
439 |
connection.remoteAddress = this.remoteAddress;
|
|
|
440 |
connection.remoteAddresses = this.remoteAddresses;
|
|
|
441 |
|
|
|
442 |
var self = this;
|
|
|
443 |
|
|
|
444 |
if (this._socketIsClosing) {
|
|
|
445 |
// Handle case when the client hangs up before we get a chance to
|
|
|
446 |
// accept the connection and send our side of the opening handshake.
|
|
|
447 |
cleanupFailedConnection(connection);
|
|
|
448 |
}
|
|
|
449 |
else {
|
|
|
450 |
this.socket.write(response, 'ascii', function(error) {
|
|
|
451 |
if (error) {
|
|
|
452 |
cleanupFailedConnection(connection);
|
|
|
453 |
return;
|
|
|
454 |
}
|
|
|
455 |
|
|
|
456 |
self._removeSocketCloseListeners();
|
|
|
457 |
connection._addSocketEventListeners();
|
|
|
458 |
});
|
|
|
459 |
}
|
|
|
460 |
|
|
|
461 |
this.emit('requestAccepted', connection);
|
|
|
462 |
return connection;
|
|
|
463 |
};
|
|
|
464 |
|
|
|
465 |
WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) {
|
|
|
466 |
this._verifyResolution();
|
|
|
467 |
|
|
|
468 |
// Mark the request resolved now so that the user can't call accept or
|
|
|
469 |
// reject a second time.
|
|
|
470 |
this._resolved = true;
|
|
|
471 |
this.emit('requestResolved', this);
|
|
|
472 |
|
|
|
473 |
if (typeof(status) !== 'number') {
|
|
|
474 |
status = 403;
|
|
|
475 |
}
|
|
|
476 |
var response = 'HTTP/1.1 ' + status + ' ' + httpStatusDescriptions[status] + '\r\n' +
|
|
|
477 |
'Connection: close\r\n';
|
|
|
478 |
if (reason) {
|
|
|
479 |
reason = reason.replace(headerSanitizeRegExp, '');
|
|
|
480 |
response += 'X-WebSocket-Reject-Reason: ' + reason + '\r\n';
|
|
|
481 |
}
|
|
|
482 |
|
|
|
483 |
if (extraHeaders) {
|
|
|
484 |
for (var key in extraHeaders) {
|
|
|
485 |
var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, '');
|
|
|
486 |
var sanitizedKey = key.replace(headerSanitizeRegExp, '');
|
|
|
487 |
response += (sanitizedKey + ': ' + sanitizedValue + '\r\n');
|
|
|
488 |
}
|
|
|
489 |
}
|
|
|
490 |
|
|
|
491 |
response += '\r\n';
|
|
|
492 |
this.socket.end(response, 'ascii');
|
|
|
493 |
|
|
|
494 |
this.emit('requestRejected', this);
|
|
|
495 |
};
|
|
|
496 |
|
|
|
497 |
WebSocketRequest.prototype._handleSocketCloseBeforeAccept = function() {
|
|
|
498 |
this._socketIsClosing = true;
|
|
|
499 |
this._removeSocketCloseListeners();
|
|
|
500 |
};
|
|
|
501 |
|
|
|
502 |
WebSocketRequest.prototype._removeSocketCloseListeners = function() {
|
|
|
503 |
this.socket.removeListener('end', this._socketCloseHandler);
|
|
|
504 |
this.socket.removeListener('close', this._socketCloseHandler);
|
|
|
505 |
};
|
|
|
506 |
|
|
|
507 |
WebSocketRequest.prototype._verifyResolution = function() {
|
|
|
508 |
if (this._resolved) {
|
|
|
509 |
throw new Error('WebSocketRequest may only be accepted or rejected one time.');
|
|
|
510 |
}
|
|
|
511 |
};
|
|
|
512 |
|
|
|
513 |
function cleanupFailedConnection(connection) {
|
|
|
514 |
// Since we have to return a connection object even if the socket is
|
|
|
515 |
// already dead in order not to break the API, we schedule a 'close'
|
|
|
516 |
// event on the connection object to occur immediately.
|
|
|
517 |
process.nextTick(function() {
|
|
|
518 |
// WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006
|
|
|
519 |
// Third param: Skip sending the close frame to a dead socket
|
|
|
520 |
connection.drop(1006, 'TCP connection lost before handshake completed.', true);
|
|
|
521 |
});
|
|
|
522 |
}
|
|
|
523 |
|
|
|
524 |
module.exports = WebSocketRequest;
|