Subversion Repositories qbpwcf-lib(archive)

Rev

Rev 915 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
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;