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 utils = require('./utils');
18
var extend = utils.extend;
19
var util = require('util');
20
var EventEmitter = require('events').EventEmitter;
21
var http = require('http');
22
var https = require('https');
23
var url = require('url');
24
var crypto = require('crypto');
25
var WebSocketConnection = require('./WebSocketConnection');
26
 
27
var protocolSeparators = [
28
    '(', ')', '<', '>', '@',
29
    ',', ';', ':', '\\', '\"',
30
    '/', '[', ']', '?', '=',
31
    '{', '}', ' ', String.fromCharCode(9)
32
];
33
 
34
function WebSocketClient(config) {
35
    // Superclass Constructor
36
    EventEmitter.call(this);
37
 
38
    // TODO: Implement extensions
39
 
40
    this.config = {
41
        // 1MiB max frame size.
42
        maxReceivedFrameSize: 0x100000,
43
 
44
        // 8MiB max message size, only applicable if
45
        // assembleFragments is true
46
        maxReceivedMessageSize: 0x800000,
47
 
48
        // Outgoing messages larger than fragmentationThreshold will be
49
        // split into multiple fragments.
50
        fragmentOutgoingMessages: true,
51
 
52
        // Outgoing frames are fragmented if they exceed this threshold.
53
        // Default is 16KiB
54
        fragmentationThreshold: 0x4000,
55
 
56
        // Which version of the protocol to use for this session.  This
57
        // option will be removed once the protocol is finalized by the IETF
58
        // It is only available to ease the transition through the
59
        // intermediate draft protocol versions.
60
        // At present, it only affects the name of the Origin header.
61
        webSocketVersion: 13,
62
 
63
        // If true, fragmented messages will be automatically assembled
64
        // and the full message will be emitted via a 'message' event.
65
        // If false, each frame will be emitted via a 'frame' event and
66
        // the application will be responsible for aggregating multiple
67
        // fragmented frames.  Single-frame messages will emit a 'message'
68
        // event in addition to the 'frame' event.
69
        // Most users will want to leave this set to 'true'
70
        assembleFragments: true,
71
 
72
        // The Nagle Algorithm makes more efficient use of network resources
73
        // by introducing a small delay before sending small packets so that
74
        // multiple messages can be batched together before going onto the
75
        // wire.  This however comes at the cost of latency, so the default
76
        // is to disable it.  If you don't need low latency and are streaming
77
        // lots of small messages, you can change this to 'false'
78
        disableNagleAlgorithm: true,
79
 
80
        // The number of milliseconds to wait after sending a close frame
81
        // for an acknowledgement to come back before giving up and just
82
        // closing the socket.
83
        closeTimeout: 5000,
84
 
85
        // Options to pass to https.connect if connecting via TLS
86
        tlsOptions: {}
87
    };
88
 
89
    if (config) {
90
        var tlsOptions;
91
        if (config.tlsOptions) {
92
          tlsOptions = config.tlsOptions;
93
          delete config.tlsOptions;
94
        }
95
        else {
96
          tlsOptions = {};
97
        }
98
        extend(this.config, config);
99
        extend(this.config.tlsOptions, tlsOptions);
100
    }
101
 
102
    this._req = null;
103
 
104
    switch (this.config.webSocketVersion) {
105
        case 8:
106
        case 13:
107
            break;
108
        default:
109
            throw new Error('Requested webSocketVersion is not supported. Allowed values are 8 and 13.');
110
    }
111
}
112
 
113
util.inherits(WebSocketClient, EventEmitter);
114
 
115
WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, headers, extraRequestOptions) {
116
    var self = this;
117
    if (typeof(protocols) === 'string') {
118
        if (protocols.length > 0) {
119
            protocols = [protocols];
120
        }
121
        else {
122
            protocols = [];
123
        }
124
    }
125
    if (!(protocols instanceof Array)) {
126
        protocols = [];
127
    }
128
    this.protocols = protocols;
129
    this.origin = origin;
130
 
131
    if (typeof(requestUrl) === 'string') {
132
        this.url = url.parse(requestUrl);
133
    }
134
    else {
135
        this.url = requestUrl; // in case an already parsed url is passed in.
136
    }
137
    if (!this.url.protocol) {
138
        throw new Error('You must specify a full WebSocket URL, including protocol.');
139
    }
140
    if (!this.url.host) {
141
        throw new Error('You must specify a full WebSocket URL, including hostname. Relative URLs are not supported.');
142
    }
143
 
144
    this.secure = (this.url.protocol === 'wss:');
145
 
146
    // validate protocol characters:
147
    this.protocols.forEach(function(protocol) {
148
        for (var i=0; i < protocol.length; i ++) {
149
            var charCode = protocol.charCodeAt(i);
150
            var character = protocol.charAt(i);
151
            if (charCode < 0x0021 || charCode > 0x007E || protocolSeparators.indexOf(character) !== -1) {
152
                throw new Error('Protocol list contains invalid character "' + String.fromCharCode(charCode) + '"');
153
            }
154
        }
155
    });
156
 
157
    var defaultPorts = {
158
        'ws:': '80',
159
        'wss:': '443'
160
    };
161
 
162
    if (!this.url.port) {
163
        this.url.port = defaultPorts[this.url.protocol];
164
    }
165
 
166
    var nonce = new Buffer(16);
167
    for (var i=0; i < 16; i++) {
168
        nonce[i] = Math.round(Math.random()*0xFF);
169
    }
170
    this.base64nonce = nonce.toString('base64');
171
 
172
    var hostHeaderValue = this.url.hostname;
173
    if ((this.url.protocol === 'ws:' && this.url.port !== '80') ||
174
        (this.url.protocol === 'wss:' && this.url.port !== '443'))  {
175
        hostHeaderValue += (':' + this.url.port);
176
    }
177
 
178
    var reqHeaders = headers || {};
179
    extend(reqHeaders, {
180
        'Upgrade': 'websocket',
181
        'Connection': 'Upgrade',
182
        'Sec-WebSocket-Version': this.config.webSocketVersion.toString(10),
183
        'Sec-WebSocket-Key': this.base64nonce,
184
        'Host': hostHeaderValue
185
    });
186
 
187
    if (this.protocols.length > 0) {
188
        reqHeaders['Sec-WebSocket-Protocol'] = this.protocols.join(', ');
189
    }
190
    if (this.origin) {
191
        if (this.config.webSocketVersion === 13) {
192
            reqHeaders['Origin'] = this.origin;
193
        }
194
        else if (this.config.webSocketVersion === 8) {
195
            reqHeaders['Sec-WebSocket-Origin'] = this.origin;
196
        }
197
    }
198
 
199
    // TODO: Implement extensions
200
 
201
    var pathAndQuery;
202
    // Ensure it begins with '/'.
203
    if (this.url.pathname) {
204
        pathAndQuery = this.url.path;
205
    }
206
    else if (this.url.path) {
207
        pathAndQuery = '/' + this.url.path;
208
    }
209
    else {
210
        pathAndQuery = '/';
211
    }
212
 
213
    function handleRequestError(error) {
214
        self._req = null;
215
        self.emit('connectFailed', error);
216
    }
217
 
218
    var requestOptions = {
219
        agent: false
220
    };
221
    if (extraRequestOptions) {
222
        extend(requestOptions, extraRequestOptions);
223
    }
224
    // These options are always overridden by the library.  The user is not
225
    // allowed to specify these directly.
226
    extend(requestOptions, {
227
        hostname: this.url.hostname,
228
        port: this.url.port,
229
        method: 'GET',
230
        path: pathAndQuery,
231
        headers: reqHeaders
232
    });
233
    if (this.secure) {
234
       for (var key in self.config.tlsOptions) {
235
           if (self.config.tlsOptions.hasOwnProperty(key)) {
236
               requestOptions[key] = self.config.tlsOptions[key];
237
           }
238
       }
239
    }
240
 
241
    var req = this._req = (this.secure ? https : http).request(requestOptions);
242
    req.on('upgrade', function handleRequestUpgrade(response, socket, head) {
243
        self._req = null;
244
        req.removeListener('error', handleRequestError);
245
        self.socket = socket;
246
        self.response = response;
247
        self.firstDataChunk = head;
248
        self.validateHandshake();
249
    });
250
    req.on('error', handleRequestError);
251
 
252
    req.on('response', function(response) {
253
        self._req = null;
254
        if (utils.eventEmitterListenerCount(self, 'httpResponse') > 0) {
255
            self.emit('httpResponse', response, self);
256
            if (response.socket) {
257
                response.socket.end();
258
            }
259
        }
260
        else {
261
            var headerDumpParts = [];
262
            for (var headerName in response.headers) {
263
                headerDumpParts.push(headerName + ': ' + response.headers[headerName]);
264
            }
265
            self.failHandshake(
266
                'Server responded with a non-101 status: ' +
267
                response.statusCode +
268
                '\nResponse Headers Follow:\n' +
269
                headerDumpParts.join('\n') + '\n'
270
            );
271
        }
272
    });
273
    req.end();
274
};
275
 
276
WebSocketClient.prototype.validateHandshake = function() {
277
    var headers = this.response.headers;
278
 
279
    if (this.protocols.length > 0) {
280
        this.protocol = headers['sec-websocket-protocol'];
281
        if (this.protocol) {
282
            if (this.protocols.indexOf(this.protocol) === -1) {
283
                this.failHandshake('Server did not respond with a requested protocol.');
284
                return;
285
            }
286
        }
287
        else {
288
            this.failHandshake('Expected a Sec-WebSocket-Protocol header.');
289
            return;
290
        }
291
    }
292
 
293
    if (!(headers['connection'] && headers['connection'].toLocaleLowerCase() === 'upgrade')) {
294
        this.failHandshake('Expected a Connection: Upgrade header from the server');
295
        return;
296
    }
297
 
298
    if (!(headers['upgrade'] && headers['upgrade'].toLocaleLowerCase() === 'websocket')) {
299
        this.failHandshake('Expected an Upgrade: websocket header from the server');
300
        return;
301
    }
302
 
303
    var sha1 = crypto.createHash('sha1');
304
    sha1.update(this.base64nonce + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
305
    var expectedKey = sha1.digest('base64');
306
 
307
    if (!headers['sec-websocket-accept']) {
308
        this.failHandshake('Expected Sec-WebSocket-Accept header from server');
309
        return;
310
    }
311
 
312
    if (headers['sec-websocket-accept'] !== expectedKey) {
313
        this.failHandshake('Sec-WebSocket-Accept header from server didn\'t match expected value of ' + expectedKey);
314
        return;
315
    }
316
 
317
    // TODO: Support extensions
318
 
319
    this.succeedHandshake();
320
};
321
 
322
WebSocketClient.prototype.failHandshake = function(errorDescription) {
323
    if (this.socket && this.socket.writable) {
324
        this.socket.end();
325
    }
326
    this.emit('connectFailed', new Error(errorDescription));
327
};
328
 
329
WebSocketClient.prototype.succeedHandshake = function() {
330
    var connection = new WebSocketConnection(this.socket, [], this.protocol, true, this.config);
331
 
332
    connection.webSocketVersion = this.config.webSocketVersion;
333
    connection._addSocketEventListeners();
334
 
335
    this.emit('connect', connection);
336
    if (this.firstDataChunk.length > 0) {
337
        connection.handleSocketData(this.firstDataChunk);
338
    }
339
    this.firstDataChunk = null;
340
};
341
 
342
WebSocketClient.prototype.abort = function() {
343
    if (this._req) {
344
        this._req.abort();
345
    }
346
};
347
 
348
module.exports = WebSocketClient;