Downloads containing WebSocketClient.js

Downloads
Name Author Game Mode Rating
WebJCS 1.3.3Featured Download djazz Utility 10 Download file

File preview

  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.  
  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));
  20.  
  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');
  29.  
  30. const INIT = -1;
  31. const CONNECTING = 0;
  32. const OPEN = 1;
  33. const CLOSING = 2;
  34. const CLOSED = 3;
  35.  
  36. var ID_COUNTER = 0;
  37.  
  38. var protocolSeparators = [
  39.     "(", ")", "<", ">", "@",
  40.     ",", ";", ":", "\\", "\"",
  41.     "/", "[", "]", "?", "=",
  42.     "{", "}", " ", String.fromCharCode(9)
  43. ];
  44.  
  45. function 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.  
  112. util.inherits(WebSocketClient, EventEmitter);
  113.  
  114. WebSocketClient.prototype.connect = function(requestUrl, protocols, origin) {
  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.         'Upgrade': 'websocket',
  174.         'Connection': 'Upgrade',
  175.         'Sec-WebSocket-Version': this.config.websocketVersion.toString(10),
  176.         'Sec-WebSocket-Key': this.base64nonce,
  177.         'Host': hostHeaderValue
  178.     };
  179.     if (this.protocols.length > 0) {
  180.         reqHeaders['Sec-WebSocket-Protocol'] = this.protocols.join(', ');
  181.     }
  182.     if (this.origin) {
  183.         if (this.config.websocketVersion === 13) {
  184.             reqHeaders['Origin'] = this.origin;
  185.         }
  186.         else if (this.config.websocketVersion === 8) {
  187.             reqHeaders['Sec-WebSocket-Origin'] = this.origin;
  188.         }
  189.     }
  190.     // TODO: Implement extensions
  191.    
  192.     var pathAndQuery = this.url.pathname;
  193.     if (this.url.search) {
  194.         pathAndQuery += this.url.search;
  195.     }
  196.    
  197.     function handleRequestError(error) {
  198.         self.emit('connectFailed', error);
  199.     }
  200.    
  201.     if (isNode0_4_x) {
  202.         // Using old http.createClient interface since the new Agent-based API
  203.         // is buggy in Node 0.4.x.
  204.         if (this.secure) {
  205.             throw new Error("TLS connections are not supported under Node 0.4.x.  Please use 0.6.2 or newer.");
  206.         }
  207.         var client = http.createClient(this.url.port, this.url.hostname);
  208.         client.on('error', handleRequestError);
  209.         client.on('upgrade', function handleClientUpgrade(response, socket, head) {
  210.             client.removeListener('error', handleRequestError);
  211.             self.socket = socket;
  212.             self.response = response;
  213.             self.firstDataChunk = head;
  214.             self.validateHandshake();
  215.         });
  216.         var req = client.request(pathAndQuery, reqHeaders);
  217.     }
  218.     else if (isGreaterThanNode0_4_x) {
  219.         var requestOptions = {
  220.             hostname: this.url.hostname,
  221.             port: this.url.port,
  222.             method: 'GET',
  223.             path: pathAndQuery,
  224.             headers: reqHeaders,
  225.             agent: false
  226.         };
  227.         if (this.secure) {
  228.             ['key','passphrase','cert','ca'].forEach(function(key) {
  229.                 if (self.config.tlsOptions.hasOwnProperty(key)) {
  230.                     requestOptions[key] = self.config.tlsOptions[key];
  231.                 }
  232.             });
  233.             var req = https.request(requestOptions);
  234.         }
  235.         else {
  236.             var req = http.request(requestOptions);
  237.         }
  238.         req.on('upgrade', function handleRequestUpgrade(response, socket, head) {
  239.             req.removeListener('error', handleRequestError);
  240.             self.socket = socket;
  241.             self.response = response;
  242.             self.firstDataChunk = head;
  243.             self.validateHandshake();
  244.         });
  245.         req.on('error', handleRequestError);
  246.     }
  247.     else {
  248.         throw new Error("Unsupported Node version " + process.version);
  249.     }
  250.    
  251.     req.on('response', function(response) {
  252.         self.failHandshake("Server responded with a non-101 status: " + response.statusCode);
  253.     });
  254.     req.end();
  255. };
  256.  
  257. WebSocketClient.prototype.validateHandshake = function() {
  258.     var headers = this.response.headers;
  259.    
  260.     if (this.protocols.length > 0) {
  261.         this.protocol = headers['sec-websocket-protocol'];
  262.         if (this.protocol) {
  263.             if (this.protocols.indexOf(this.protocol) === -1) {
  264.                 this.failHandshake("Server did not respond with a requested protocol.");
  265.                 return;
  266.             }
  267.         }
  268.         else {
  269.             this.failHandshake("Expected a Sec-WebSocket-Protocol header.");
  270.             return;
  271.         }
  272.     }
  273.    
  274.     if (!(headers['connection'] && headers['connection'].toLocaleLowerCase() === 'upgrade')) {
  275.         this.failHandshake("Expected a Connection: Upgrade header from the server");
  276.         return;
  277.     }
  278.    
  279.     if (!(headers['upgrade'] && headers['upgrade'].toLocaleLowerCase() === 'websocket')) {
  280.         this.failHandshake("Expected an Upgrade: websocket header from the server");
  281.         return;
  282.     }
  283.    
  284.     var sha1 = crypto.createHash('sha1');
  285.     sha1.update(this.base64nonce + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
  286.     var expectedKey = sha1.digest('base64');
  287.    
  288.     if (!headers['sec-websocket-accept']) {
  289.         this.failHandshake("Expected Sec-WebSocket-Accept header from server");
  290.         return;
  291.     }
  292.    
  293.     if (!(headers['sec-websocket-accept'] === expectedKey)) {
  294.         this.failHandshake("Sec-WebSocket-Accept header from server didn't match expected value of " + expectedKey);
  295.         return;
  296.     }
  297.    
  298.     // TODO: Support extensions
  299.    
  300.     this.succeedHandshake();
  301. };
  302.  
  303. WebSocketClient.prototype.failHandshake = function(errorDescription) {
  304.     if (this.socket && this.socket.writable) {
  305.         this.socket.end();
  306.     }
  307.     this.emit('connectFailed', errorDescription);
  308. };
  309.  
  310. WebSocketClient.prototype.succeedHandshake = function() {
  311.     var connection = new WebSocketConnection(this.socket, [], this.protocol, true, this.config);
  312.     connection.websocketVersion = this.config.websocketVersion;
  313.    
  314.     this.emit('connect', connection);
  315.     if (this.firstDataChunk.length > 0) {
  316.         connection.handleSocketData(this.firstDataChunk);
  317.         this.firstDataChunk = null;
  318.     }
  319. };
  320.  
  321. module.exports = WebSocketClient;