1 /************************************************************************
2 * Copyright 2010-2011 Worlize Inc.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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 ***********************************************************************/
17 var nodeVersion
= process
.version
.slice(1).split('.').map(function(item
) { return parseInt(item
, 10); });
18 var isNode0_4_x
= (nodeVersion
[0] === 0 && nodeVersion
[1] === 4);
19 var isGreaterThanNode0_4_x
= (nodeVersion
[0] > 0 || (nodeVersion
[0] === 0 && nodeVersion
[1] > 4));
21 var extend
= require('./utils').extend
;
22 var util
= require('util');
23 var EventEmitter
= require('events').EventEmitter
;
24 var http
= require('http');
25 var https
= require('https');
26 var url
= require('url');
27 var crypto
= require('crypto');
28 var WebSocketConnection
= require('./WebSocketConnection');
38 var protocolSeparators
= [
39 "(", ")", "<", ">", "@",
40 ",", ";", ":", "\\", "\"",
41 "/", "[", "]", "?", "=",
42 "{", "}", " ", String
.fromCharCode(9)
45 function WebSocketClient(config
) {
46 // TODO: Implement extensions
49 // 1MiB max frame size.
50 maxReceivedFrameSize
: 0x100000,
52 // 8MiB max message size, only applicable if
53 // assembleFragments is true
54 maxReceivedMessageSize
: 0x800000,
56 // Outgoing messages larger than fragmentationThreshold will be
57 // split into multiple fragments.
58 fragmentOutgoingMessages
: true,
60 // Outgoing frames are fragmented if they exceed this threshold.
62 fragmentationThreshold
: 0x4000,
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.
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,
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,
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.
93 // Options to pass to https.connect if connecting via TLS
97 extend(this.config
, config
);
100 switch (this.config
.websocketVersion
) {
105 throw new Error("Requested websocketVersion is not supported. " +
106 "Allowed values are 8 and 13.");
109 this.readyState
= INIT
;
112 util
.inherits(WebSocketClient
, EventEmitter
);
114 WebSocketClient
.prototype.connect = function(requestUrl
, protocols
, origin
, headers
) {
116 if (typeof(protocols
) === 'string') {
117 protocols
= [protocols
];
119 if (!(protocols
instanceof Array
)) {
122 this.protocols
= protocols
;
123 this.origin
= origin
;
125 if (typeof(requestUrl
) === 'string') {
126 this.url
= url
.parse(requestUrl
);
129 this.url
= requestUrl
; // in case an already parsed url is passed in.
131 if (!this.url
.protocol
) {
132 throw new Error("You must specify a full WebSocket URL, including protocol.");
134 if (!this.url
.host
) {
135 throw new Error("You must specify a full WebSocket URL, including hostname. Relative URLs are not supported.");
138 this.secure
= (this.url
.protocol
=== 'wss:');
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
) + "'");
156 if (!this.url
.port
) {
157 this.url
.port
= defaultPorts
[this.url
.protocol
];
160 var nonce
= new Buffer(16);
161 for (var i
=0; i
< 16; i
++) {
162 nonce
[i
] = Math
.round(Math
.random()*0xFF);
164 this.base64nonce
= nonce
.toString('base64');
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
)
173 extend(reqHeaders
, headers
|| {});
175 'Upgrade': 'websocket',
176 'Connection': 'Upgrade',
177 'Sec-WebSocket-Version': this.config
.websocketVersion
.toString(10),
178 'Sec-WebSocket-Key': this.base64nonce
,
179 'Host': hostHeaderValue
182 if (this.protocols
.length
> 0) {
183 reqHeaders
['Sec-WebSocket-Protocol'] = this.protocols
.join(', ');
186 if (this.config
.websocketVersion
=== 13) {
187 reqHeaders
['Origin'] = this.origin
;
189 else if (this.config
.websocketVersion
=== 8) {
190 reqHeaders
['Sec-WebSocket-Origin'] = this.origin
;
194 // TODO: Implement extensions
196 var pathAndQuery
= this.url
.pathname
;
197 if (this.url
.search
) {
198 pathAndQuery
+= this.url
.search
;
201 function handleRequestError(error
) {
202 self
.emit('connectFailed', error
);
206 // Using old http.createClient interface since the new Agent-based API
207 // is buggy in Node 0.4.x.
209 throw new Error("TLS connections are not supported under Node 0.4.x. Please use 0.6.2 or newer.");
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();
220 var req
= client
.request(pathAndQuery
, reqHeaders
);
222 else if (isGreaterThanNode0_4_x
) {
223 var requestOptions
= {
224 hostname
: this.url
.hostname
,
232 ['key','passphrase','cert','ca'].forEach(function(key
) {
233 if (self
.config
.tlsOptions
.hasOwnProperty(key
)) {
234 requestOptions
[key
] = self
.config
.tlsOptions
[key
];
237 var req
= https
.request(requestOptions
);
240 var req
= http
.request(requestOptions
);
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();
249 req
.on('error', handleRequestError
);
252 throw new Error("Unsupported Node version " + process
.version
);
255 req
.on('response', function(response
) {
256 self
.failHandshake("Server responded with a non-101 status: " + response
.statusCode
);
261 WebSocketClient
.prototype.validateHandshake = function() {
262 var headers
= this.response
.headers
;
264 if (this.protocols
.length
> 0) {
265 this.protocol
= headers
['sec-websocket-protocol'];
267 if (this.protocols
.indexOf(this.protocol
) === -1) {
268 this.failHandshake("Server did not respond with a requested protocol.");
273 this.failHandshake("Expected a Sec-WebSocket-Protocol header.");
278 if (!(headers
['connection'] && headers
['connection'].toLocaleLowerCase() === 'upgrade')) {
279 this.failHandshake("Expected a Connection: Upgrade header from the server");
283 if (!(headers
['upgrade'] && headers
['upgrade'].toLocaleLowerCase() === 'websocket')) {
284 this.failHandshake("Expected an Upgrade: websocket header from the server");
288 var sha1
= crypto
.createHash('sha1');
289 sha1
.update(this.base64nonce
+ "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
290 var expectedKey
= sha1
.digest('base64');
292 if (!headers
['sec-websocket-accept']) {
293 this.failHandshake("Expected Sec-WebSocket-Accept header from server");
297 if (!(headers
['sec-websocket-accept'] === expectedKey
)) {
298 this.failHandshake("Sec-WebSocket-Accept header from server didn't match expected value of " + expectedKey
);
302 // TODO: Support extensions
304 this.succeedHandshake();
307 WebSocketClient
.prototype.failHandshake = function(errorDescription
) {
308 if (this.socket
&& this.socket
.writable
) {
311 this.emit('connectFailed', errorDescription
);
314 WebSocketClient
.prototype.succeedHandshake = function() {
315 var connection
= new WebSocketConnection(this.socket
, [], this.protocol
, true, this.config
);
316 connection
.websocketVersion
= this.config
.websocketVersion
;
318 this.emit('connect', connection
);
319 if (this.firstDataChunk
.length
> 0) {
320 connection
.handleSocketData(this.firstDataChunk
);
321 this.firstDataChunk
= null;
325 module
.exports
= WebSocketClient
;