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