@fairwords/websocket 1.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,541 @@
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._socketErrorTempHandler = this._handleSocketErrorBeforeAccept.bind(this);
103
+ this.socket.on('error', this._socketErrorTempHandler);
104
+ this.socket.on('end', this._socketCloseHandler);
105
+ this.socket.on('close', this._socketCloseHandler);
106
+
107
+ this._resolved = false;
108
+ }
109
+
110
+ util.inherits(WebSocketRequest, EventEmitter);
111
+
112
+ WebSocketRequest.prototype.readHandshake = function() {
113
+ var self = this;
114
+ var request = this.httpRequest;
115
+
116
+ // Decode URL
117
+ this.resourceURL = url.parse(this.resource, true);
118
+
119
+ this.host = request.headers['host'];
120
+ if (!this.host) {
121
+ throw new Error('Client must provide a Host header.');
122
+ }
123
+
124
+ this.key = request.headers['sec-websocket-key'];
125
+ if (!this.key) {
126
+ throw new Error('Client must provide a value for Sec-WebSocket-Key.');
127
+ }
128
+
129
+ this.webSocketVersion = parseInt(request.headers['sec-websocket-version'], 10);
130
+
131
+ if (!this.webSocketVersion || isNaN(this.webSocketVersion)) {
132
+ throw new Error('Client must provide a value for Sec-WebSocket-Version.');
133
+ }
134
+
135
+ switch (this.webSocketVersion) {
136
+ case 8:
137
+ case 13:
138
+ break;
139
+ default:
140
+ var e = new Error('Unsupported websocket client version: ' + this.webSocketVersion +
141
+ 'Only versions 8 and 13 are supported.');
142
+ e.httpCode = 426;
143
+ e.headers = {
144
+ 'Sec-WebSocket-Version': '13'
145
+ };
146
+ throw e;
147
+ }
148
+
149
+ if (this.webSocketVersion === 13) {
150
+ this.origin = request.headers['origin'];
151
+ }
152
+ else if (this.webSocketVersion === 8) {
153
+ this.origin = request.headers['sec-websocket-origin'];
154
+ }
155
+
156
+ // Protocol is optional.
157
+ var protocolString = request.headers['sec-websocket-protocol'];
158
+ this.protocolFullCaseMap = {};
159
+ this.requestedProtocols = [];
160
+ if (protocolString) {
161
+ var requestedProtocolsFullCase = protocolString.split(headerValueSplitRegExp);
162
+ requestedProtocolsFullCase.forEach(function(protocol) {
163
+ var lcProtocol = protocol.toLocaleLowerCase();
164
+ self.requestedProtocols.push(lcProtocol);
165
+ self.protocolFullCaseMap[lcProtocol] = protocol;
166
+ });
167
+ }
168
+
169
+ if (!this.serverConfig.ignoreXForwardedFor &&
170
+ request.headers['x-forwarded-for']) {
171
+ var immediatePeerIP = this.remoteAddress;
172
+ this.remoteAddresses = request.headers['x-forwarded-for']
173
+ .split(xForwardedForSeparatorRegExp);
174
+ this.remoteAddresses.push(immediatePeerIP);
175
+ this.remoteAddress = this.remoteAddresses[0];
176
+ }
177
+
178
+ // Extensions are optional.
179
+ if (this.serverConfig.parseExtensions) {
180
+ var extensionsString = request.headers['sec-websocket-extensions'];
181
+ this.requestedExtensions = this.parseExtensions(extensionsString);
182
+ } else {
183
+ this.requestedExtensions = [];
184
+ }
185
+
186
+ // Cookies are optional
187
+ if (this.serverConfig.parseCookies) {
188
+ var cookieString = request.headers['cookie'];
189
+ this.cookies = this.parseCookies(cookieString);
190
+ } else {
191
+ this.cookies = [];
192
+ }
193
+ };
194
+
195
+ WebSocketRequest.prototype.parseExtensions = function(extensionsString) {
196
+ if (!extensionsString || extensionsString.length === 0) {
197
+ return [];
198
+ }
199
+ var extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp);
200
+ extensions.forEach(function(extension, index, array) {
201
+ var params = extension.split(headerParamSplitRegExp);
202
+ var extensionName = params[0];
203
+ var extensionParams = params.slice(1);
204
+ extensionParams.forEach(function(rawParam, index, array) {
205
+ var arr = rawParam.split('=');
206
+ var obj = {
207
+ name: arr[0],
208
+ value: arr[1]
209
+ };
210
+ array.splice(index, 1, obj);
211
+ });
212
+ var obj = {
213
+ name: extensionName,
214
+ params: extensionParams
215
+ };
216
+ array.splice(index, 1, obj);
217
+ });
218
+ return extensions;
219
+ };
220
+
221
+ // This function adapted from node-cookie
222
+ // https://github.com/shtylman/node-cookie
223
+ WebSocketRequest.prototype.parseCookies = function(str) {
224
+ // Sanity Check
225
+ if (!str || typeof(str) !== 'string') {
226
+ return [];
227
+ }
228
+
229
+ var cookies = [];
230
+ var pairs = str.split(cookieSeparatorRegEx);
231
+
232
+ pairs.forEach(function(pair) {
233
+ var eq_idx = pair.indexOf('=');
234
+ if (eq_idx === -1) {
235
+ cookies.push({
236
+ name: pair,
237
+ value: null
238
+ });
239
+ return;
240
+ }
241
+
242
+ var key = pair.substr(0, eq_idx).trim();
243
+ var val = pair.substr(++eq_idx, pair.length).trim();
244
+
245
+ // quoted values
246
+ if ('"' === val[0]) {
247
+ val = val.slice(1, -1);
248
+ }
249
+
250
+ cookies.push({
251
+ name: key,
252
+ value: decodeURIComponent(val)
253
+ });
254
+ });
255
+
256
+ return cookies;
257
+ };
258
+
259
+ WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) {
260
+ this._verifyResolution();
261
+
262
+ // TODO: Handle extensions
263
+
264
+ var protocolFullCase;
265
+
266
+ if (acceptedProtocol) {
267
+ protocolFullCase = this.protocolFullCaseMap[acceptedProtocol.toLocaleLowerCase()];
268
+ if (typeof(protocolFullCase) === 'undefined') {
269
+ protocolFullCase = acceptedProtocol;
270
+ }
271
+ }
272
+ else {
273
+ protocolFullCase = acceptedProtocol;
274
+ }
275
+ this.protocolFullCaseMap = null;
276
+
277
+ // Create key validation hash
278
+ var sha1 = crypto.createHash('sha1');
279
+ sha1.update(this.key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
280
+ var acceptKey = sha1.digest('base64');
281
+
282
+ var response = 'HTTP/1.1 101 Switching Protocols\r\n' +
283
+ 'Upgrade: websocket\r\n' +
284
+ 'Connection: Upgrade\r\n' +
285
+ 'Sec-WebSocket-Accept: ' + acceptKey + '\r\n';
286
+
287
+ if (protocolFullCase) {
288
+ // validate protocol
289
+ for (var i=0; i < protocolFullCase.length; i++) {
290
+ var charCode = protocolFullCase.charCodeAt(i);
291
+ var character = protocolFullCase.charAt(i);
292
+ if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) {
293
+ this.reject(500);
294
+ throw new Error('Illegal character "' + String.fromCharCode(character) + '" in subprotocol.');
295
+ }
296
+ }
297
+ if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) {
298
+ this.reject(500);
299
+ throw new Error('Specified protocol was not requested by the client.');
300
+ }
301
+
302
+ protocolFullCase = protocolFullCase.replace(headerSanitizeRegExp, '');
303
+ response += 'Sec-WebSocket-Protocol: ' + protocolFullCase + '\r\n';
304
+ }
305
+ this.requestedProtocols = null;
306
+
307
+ if (allowedOrigin) {
308
+ allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, '');
309
+ if (this.webSocketVersion === 13) {
310
+ response += 'Origin: ' + allowedOrigin + '\r\n';
311
+ }
312
+ else if (this.webSocketVersion === 8) {
313
+ response += 'Sec-WebSocket-Origin: ' + allowedOrigin + '\r\n';
314
+ }
315
+ }
316
+
317
+ if (cookies) {
318
+ if (!Array.isArray(cookies)) {
319
+ this.reject(500);
320
+ throw new Error('Value supplied for "cookies" argument must be an array.');
321
+ }
322
+ var seenCookies = {};
323
+ cookies.forEach(function(cookie) {
324
+ if (!cookie.name || !cookie.value) {
325
+ this.reject(500);
326
+ throw new Error('Each cookie to set must at least provide a "name" and "value"');
327
+ }
328
+
329
+ // Make sure there are no \r\n sequences inserted
330
+ cookie.name = cookie.name.replace(controlCharsAndSemicolonRegEx, '');
331
+ cookie.value = cookie.value.replace(controlCharsAndSemicolonRegEx, '');
332
+
333
+ if (seenCookies[cookie.name]) {
334
+ this.reject(500);
335
+ throw new Error('You may not specify the same cookie name twice.');
336
+ }
337
+ seenCookies[cookie.name] = true;
338
+
339
+ // token (RFC 2616, Section 2.2)
340
+ var invalidChar = cookie.name.match(cookieNameValidateRegEx);
341
+ if (invalidChar) {
342
+ this.reject(500);
343
+ throw new Error('Illegal character ' + invalidChar[0] + ' in cookie name');
344
+ }
345
+
346
+ // RFC 6265, Section 4.1.1
347
+ // *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) | %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
348
+ if (cookie.value.match(cookieValueDQuoteValidateRegEx)) {
349
+ invalidChar = cookie.value.slice(1, -1).match(cookieValueValidateRegEx);
350
+ } else {
351
+ invalidChar = cookie.value.match(cookieValueValidateRegEx);
352
+ }
353
+ if (invalidChar) {
354
+ this.reject(500);
355
+ throw new Error('Illegal character ' + invalidChar[0] + ' in cookie value');
356
+ }
357
+
358
+ var cookieParts = [cookie.name + '=' + cookie.value];
359
+
360
+ // RFC 6265, Section 4.1.1
361
+ // 'Path=' path-value | <any CHAR except CTLs or ';'>
362
+ if(cookie.path){
363
+ invalidChar = cookie.path.match(controlCharsAndSemicolonRegEx);
364
+ if (invalidChar) {
365
+ this.reject(500);
366
+ throw new Error('Illegal character ' + invalidChar[0] + ' in cookie path');
367
+ }
368
+ cookieParts.push('Path=' + cookie.path);
369
+ }
370
+
371
+ // RFC 6265, Section 4.1.2.3
372
+ // 'Domain=' subdomain
373
+ if (cookie.domain) {
374
+ if (typeof(cookie.domain) !== 'string') {
375
+ this.reject(500);
376
+ throw new Error('Domain must be specified and must be a string.');
377
+ }
378
+ invalidChar = cookie.domain.match(controlCharsAndSemicolonRegEx);
379
+ if (invalidChar) {
380
+ this.reject(500);
381
+ throw new Error('Illegal character ' + invalidChar[0] + ' in cookie domain');
382
+ }
383
+ cookieParts.push('Domain=' + cookie.domain.toLowerCase());
384
+ }
385
+
386
+ // RFC 6265, Section 4.1.1
387
+ //'Expires=' sane-cookie-date | Force Date object requirement by using only epoch
388
+ if (cookie.expires) {
389
+ if (!(cookie.expires instanceof Date)){
390
+ this.reject(500);
391
+ throw new Error('Value supplied for cookie "expires" must be a vaild date object');
392
+ }
393
+ cookieParts.push('Expires=' + cookie.expires.toGMTString());
394
+ }
395
+
396
+ // RFC 6265, Section 4.1.1
397
+ //'Max-Age=' non-zero-digit *DIGIT
398
+ if (cookie.maxage) {
399
+ var maxage = cookie.maxage;
400
+ if (typeof(maxage) === 'string') {
401
+ maxage = parseInt(maxage, 10);
402
+ }
403
+ if (isNaN(maxage) || maxage <= 0 ) {
404
+ this.reject(500);
405
+ throw new Error('Value supplied for cookie "maxage" must be a non-zero number');
406
+ }
407
+ maxage = Math.round(maxage);
408
+ cookieParts.push('Max-Age=' + maxage.toString(10));
409
+ }
410
+
411
+ // RFC 6265, Section 4.1.1
412
+ //'Secure;'
413
+ if (cookie.secure) {
414
+ if (typeof(cookie.secure) !== 'boolean') {
415
+ this.reject(500);
416
+ throw new Error('Value supplied for cookie "secure" must be of type boolean');
417
+ }
418
+ cookieParts.push('Secure');
419
+ }
420
+
421
+ // RFC 6265, Section 4.1.1
422
+ //'HttpOnly;'
423
+ if (cookie.httponly) {
424
+ if (typeof(cookie.httponly) !== 'boolean') {
425
+ this.reject(500);
426
+ throw new Error('Value supplied for cookie "httponly" must be of type boolean');
427
+ }
428
+ cookieParts.push('HttpOnly');
429
+ }
430
+
431
+ response += ('Set-Cookie: ' + cookieParts.join(';') + '\r\n');
432
+ }.bind(this));
433
+ }
434
+
435
+ // TODO: handle negotiated extensions
436
+ // if (negotiatedExtensions) {
437
+ // response += 'Sec-WebSocket-Extensions: ' + negotiatedExtensions.join(', ') + '\r\n';
438
+ // }
439
+
440
+ // Mark the request resolved now so that the user can't call accept or
441
+ // reject a second time.
442
+ this._resolved = true;
443
+ this.emit('requestResolved', this);
444
+
445
+ response += '\r\n';
446
+
447
+ var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig);
448
+ connection.webSocketVersion = this.webSocketVersion;
449
+ connection.remoteAddress = this.remoteAddress;
450
+ connection.remoteAddresses = this.remoteAddresses;
451
+
452
+ var self = this;
453
+
454
+ if (this._socketIsClosing) {
455
+ // Handle case when the client hangs up before we get a chance to
456
+ // accept the connection and send our side of the opening handshake.
457
+ cleanupFailedConnection(connection);
458
+ }
459
+ else {
460
+ this.socket.write(response, 'ascii', function(error) {
461
+ if (error) {
462
+ cleanupFailedConnection(connection);
463
+ return;
464
+ }
465
+
466
+ self._removeSocketCloseListeners();
467
+ connection._addSocketEventListeners();
468
+ });
469
+ }
470
+
471
+ this.emit('requestAccepted', connection);
472
+ return connection;
473
+ };
474
+
475
+ WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) {
476
+ this._verifyResolution();
477
+
478
+ // Mark the request resolved now so that the user can't call accept or
479
+ // reject a second time.
480
+ this._resolved = true;
481
+ this.emit('requestResolved', this);
482
+
483
+ if (typeof(status) !== 'number') {
484
+ status = 403;
485
+ }
486
+ var response = 'HTTP/1.1 ' + status + ' ' + httpStatusDescriptions[status] + '\r\n' +
487
+ 'Connection: close\r\n';
488
+ if (reason) {
489
+ reason = reason.replace(headerSanitizeRegExp, '');
490
+ response += 'X-WebSocket-Reject-Reason: ' + reason + '\r\n';
491
+ }
492
+
493
+ if (extraHeaders) {
494
+ for (var key in extraHeaders) {
495
+ var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, '');
496
+ var sanitizedKey = key.replace(headerSanitizeRegExp, '');
497
+ response += (sanitizedKey + ': ' + sanitizedValue + '\r\n');
498
+ }
499
+ }
500
+
501
+ response += '\r\n';
502
+ this.socket.end(response, 'ascii');
503
+
504
+ this.emit('requestRejected', this);
505
+ };
506
+
507
+ WebSocketRequest.prototype._handleSocketErrorBeforeAccept = function() {
508
+ //If error happen before accept, do nothing here. socket will close right after this.
509
+ //But we need this, or the app crash without socket error handler (if no default error handler)
510
+ //see https://github.com/theturtle32/WebSocket-Node/issues/407
511
+ };
512
+
513
+ WebSocketRequest.prototype._handleSocketCloseBeforeAccept = function() {
514
+ this._socketIsClosing = true;
515
+ this._removeSocketCloseListeners();
516
+ };
517
+
518
+ WebSocketRequest.prototype._removeSocketCloseListeners = function() {
519
+ this.socket.removeListener('error', this._socketErrorTempHandler);
520
+ this.socket.removeListener('end', this._socketCloseHandler);
521
+ this.socket.removeListener('close', this._socketCloseHandler);
522
+ };
523
+
524
+ WebSocketRequest.prototype._verifyResolution = function() {
525
+ if (this._resolved) {
526
+ throw new Error('WebSocketRequest may only be accepted or rejected one time.');
527
+ }
528
+ };
529
+
530
+ function cleanupFailedConnection(connection) {
531
+ // Since we have to return a connection object even if the socket is
532
+ // already dead in order not to break the API, we schedule a 'close'
533
+ // event on the connection object to occur immediately.
534
+ process.nextTick(function() {
535
+ // WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006
536
+ // Third param: Skip sending the close frame to a dead socket
537
+ connection.drop(1006, 'TCP connection lost before handshake completed.', true);
538
+ });
539
+ }
540
+
541
+ module.exports = WebSocketRequest;
@@ -0,0 +1,157 @@
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 extend = require('./utils').extend;
18
+ var util = require('util');
19
+ var EventEmitter = require('events').EventEmitter;
20
+ var WebSocketRouterRequest = require('./WebSocketRouterRequest');
21
+
22
+ function WebSocketRouter(config) {
23
+ // Superclass Constructor
24
+ EventEmitter.call(this);
25
+
26
+ this.config = {
27
+ // The WebSocketServer instance to attach to.
28
+ server: null
29
+ };
30
+ if (config) {
31
+ extend(this.config, config);
32
+ }
33
+ this.handlers = [];
34
+
35
+ this._requestHandler = this.handleRequest.bind(this);
36
+ if (this.config.server) {
37
+ this.attachServer(this.config.server);
38
+ }
39
+ }
40
+
41
+ util.inherits(WebSocketRouter, EventEmitter);
42
+
43
+ WebSocketRouter.prototype.attachServer = function(server) {
44
+ if (server) {
45
+ this.server = server;
46
+ this.server.on('request', this._requestHandler);
47
+ }
48
+ else {
49
+ throw new Error('You must specify a WebSocketServer instance to attach to.');
50
+ }
51
+ };
52
+
53
+ WebSocketRouter.prototype.detachServer = function() {
54
+ if (this.server) {
55
+ this.server.removeListener('request', this._requestHandler);
56
+ this.server = null;
57
+ }
58
+ else {
59
+ throw new Error('Cannot detach from server: not attached.');
60
+ }
61
+ };
62
+
63
+ WebSocketRouter.prototype.mount = function(path, protocol, callback) {
64
+ if (!path) {
65
+ throw new Error('You must specify a path for this handler.');
66
+ }
67
+ if (!protocol) {
68
+ protocol = '____no_protocol____';
69
+ }
70
+ if (!callback) {
71
+ throw new Error('You must specify a callback for this handler.');
72
+ }
73
+
74
+ path = this.pathToRegExp(path);
75
+ if (!(path instanceof RegExp)) {
76
+ throw new Error('Path must be specified as either a string or a RegExp.');
77
+ }
78
+ var pathString = path.toString();
79
+
80
+ // normalize protocol to lower-case
81
+ protocol = protocol.toLocaleLowerCase();
82
+
83
+ if (this.findHandlerIndex(pathString, protocol) !== -1) {
84
+ throw new Error('You may only mount one handler per path/protocol combination.');
85
+ }
86
+
87
+ this.handlers.push({
88
+ 'path': path,
89
+ 'pathString': pathString,
90
+ 'protocol': protocol,
91
+ 'callback': callback
92
+ });
93
+ };
94
+ WebSocketRouter.prototype.unmount = function(path, protocol) {
95
+ var index = this.findHandlerIndex(this.pathToRegExp(path).toString(), protocol);
96
+ if (index !== -1) {
97
+ this.handlers.splice(index, 1);
98
+ }
99
+ else {
100
+ throw new Error('Unable to find a route matching the specified path and protocol.');
101
+ }
102
+ };
103
+
104
+ WebSocketRouter.prototype.findHandlerIndex = function(pathString, protocol) {
105
+ protocol = protocol.toLocaleLowerCase();
106
+ for (var i=0, len=this.handlers.length; i < len; i++) {
107
+ var handler = this.handlers[i];
108
+ if (handler.pathString === pathString && handler.protocol === protocol) {
109
+ return i;
110
+ }
111
+ }
112
+ return -1;
113
+ };
114
+
115
+ WebSocketRouter.prototype.pathToRegExp = function(path) {
116
+ if (typeof(path) === 'string') {
117
+ if (path === '*') {
118
+ path = /^.*$/;
119
+ }
120
+ else {
121
+ path = path.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
122
+ path = new RegExp('^' + path + '$');
123
+ }
124
+ }
125
+ return path;
126
+ };
127
+
128
+ WebSocketRouter.prototype.handleRequest = function(request) {
129
+ var requestedProtocols = request.requestedProtocols;
130
+ if (requestedProtocols.length === 0) {
131
+ requestedProtocols = ['____no_protocol____'];
132
+ }
133
+
134
+ // Find a handler with the first requested protocol first
135
+ for (var i=0; i < requestedProtocols.length; i++) {
136
+ var requestedProtocol = requestedProtocols[i].toLocaleLowerCase();
137
+
138
+ // find the first handler that can process this request
139
+ for (var j=0, len=this.handlers.length; j < len; j++) {
140
+ var handler = this.handlers[j];
141
+ if (handler.path.test(request.resourceURL.pathname)) {
142
+ if (requestedProtocol === handler.protocol ||
143
+ handler.protocol === '*')
144
+ {
145
+ var routerRequest = new WebSocketRouterRequest(request, requestedProtocol);
146
+ handler.callback(routerRequest);
147
+ return;
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ // If we get here we were unable to find a suitable handler.
154
+ request.reject(404, 'No handler is available for the given request.');
155
+ };
156
+
157
+ module.exports = WebSocketRouter;