@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.
- package/.github/workflows/websocket-tests.yml +16 -0
- package/.jshintrc +88 -0
- package/CHANGELOG.md +291 -0
- package/Jenkinsfile +7 -0
- package/LICENSE +177 -0
- package/Makefile +5 -0
- package/README.md +255 -0
- package/gulpfile.js +14 -0
- package/index.js +1 -0
- package/lib/Deprecation.js +32 -0
- package/lib/W3CWebSocket.js +257 -0
- package/lib/WebSocketClient.js +361 -0
- package/lib/WebSocketConnection.js +896 -0
- package/lib/WebSocketFrame.js +280 -0
- package/lib/WebSocketRequest.js +541 -0
- package/lib/WebSocketRouter.js +157 -0
- package/lib/WebSocketRouterRequest.js +54 -0
- package/lib/WebSocketServer.js +256 -0
- package/lib/browser.js +54 -0
- package/lib/utils.js +66 -0
- package/lib/version.js +1 -0
- package/lib/websocket.js +11 -0
- package/package.json +58 -0
- package/vendor/FastBufferList.js +191 -0
|
@@ -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;
|