UNPKG

truflux

Version:

Secure socket connection module

536 lines (500 loc) 18.8 kB
(function(module) { var canSupportDownloads = !!(Blob&&URL), canSupportUploads = !!(Blob&&File&&FileReader), Noop = function () {}, LowerMask = 0x0000FFFF, UpperMask = 0xFFFF0000, /** * The size of a chunk in bytes. Equates to a Megabyte * @type {Number} */ FileChunkSize = 1024*1024, /** * How often we send a file in milliseconds * @type {Number} */ FileSendFrequency = 1; /** * 32bit isomorphic mixer function to map a 32bit to another 32bit * @param value Integer 32bit * @param seed Integer 32bit seed * @returns {number} */ function MixBit(value,seed)//mix bit 32 { value^=seed; var a = (value & UpperMask)>>16, b = value & LowerMask, aa = (a + b) & LowerMask, bb = (a + 2*b)& LowerMask; return (aa<<16)|bb; } /** * 32bit isomorphic mixer function to map a 32bit to another 32bit. Undoes the mixbit operation * @param value Integer 32bit * @param seed Integer 32bit seed * @returns {number} */ function unMixBit(value,seed)//unmix bit 32 { var aa =(value & UpperMask)>>16, bb= value & LowerMask, b = (bb - aa) & LowerMask, a = ((2*aa)-bb) & LowerMask; return ((a<<16)|b)^seed; } /** * Converts a string to a mix seed * @param {String} str * @return {Number} */ function StringToMixSeed(str)//String to mix seed { var j=0xFFFFFFFF; for (var i=0,c=str.length;i<c;i++) j=(j^str.charCodeAt(i))<<4; return j; } function NetworkFileWriter(header,client) { //Copy header information in for (var property in header) if (header.hasOwnProperty(property)) this[property]=header[property]; this.onErr=this.onEnd=Noop;//Noop this._client=client; this._blobs=[]; this._interacted=false; this._streamIndex=header.i; } NetworkFileWriter.prototype= { _appendBlob:function(blob)//Append new blob { this._blobs.push(blob); }, accept:function() { if (!this._interacted) { this._client.send(-15,this._streamIndex); this._interacted=true; } }, decline:function() { if (!this._interacted) { this._client.send(-14,this._streamIndex); this._interacted=true; } }, pause:function() { this._client.send(-19,this._streamIndex); }, resume:function() { this._client.send(-18,this._streamIndex); }, cancel:function() { //Remove the blob buffer we currently have this._blobs=[]; this._client.send(-16,this._streamIndex); }, _finished:function() { this.onEnd(URL.createObjectURL(new Blob(s._blobs, {type: 'application/octet-binary'}))); this._client.send(-17,this._streamIndex); this._blobs=[]; } }; function NetworkFileReader(file,client,streamIndex) { this._chunkIndex = 0; this._file = file; this._sendTimeoutHandle = 0; this._size = file.size||file.fileSize; this._streamIndex = streamIndex; this._client = client; this.paused = false; this.onEnd = this.onAccept = this.onCancel = this.onPause = this.onResume = this.onDecline = Noop; } NetworkFileReader.prototype= { _accept:function() { this.onAccept(); this._resume(); }, _decline:function() { this.onDecline(); //Discard the file this._file=null; }, _cancel:function() { this.onCancel(); clearInterval(this._sendTimeoutHandle); }, _pause:function()//pause { this.paused=true; this.onPause(); clearInterval(this._sendTimeoutHandle); }, _resume:function()//resume { var self = this, chunk, endChunkIndex; this.paused = false; this.onResume(); this._sendTimeoutHandle=setInterval(function() { //Take the minimum between the two sizes so we don't go past the bounds chunk = self._file.slice(self._chunkIndex, endChunkIndex=(Math.min(self._chunkIndex+FileChunkSize, self._size)) ); self._chunkIndex = endChunkIndex; self._client.send(-12, self._streamIndex); //send binary chunk directly self._client._s.send(chunk); if (self._chunkIndex== self._size) { //Say we have finished sending the full file self._client.send(-13, self._streamIndex); clearInterval(self._sendTimeoutHandle); } },FileSendFrequency); }, _finished:function() { //Release the file and listeners, as well as deregister from client read streams this.onEnd(); this.onEnd = this.onAccept = this.onCancel = this.onPause = this.onResume = this.onDecline = 0; this._client._readStreams[this._streamIndex] = this._client = this._file = null; } }; function Client (options)//Truflux client { options || (options = {}); if (!options.msg) throw new Error('\"msg\" required'); this._readStreams = {}; this._writeStreams = {}; this._fileIndex = 1; this.host = options.host || "ws://localhost"; this.msg = options.msg; this.session = {}; this.socket = null; this.onClose = this.onErr = this.onOpen = Noop; this.peerSecure = this.secure = false; this._crypto = options.crypto; this._cipher = this._diffieHellmanKey = this._symmetricKey = this._isomorphicKey = null; } Client.prototype= { /** * Connects to the server * @return {Client} */ open:function () { if (this.socket) return this; var self = this, expectingBinary = false, dataType, upperByte, message, decipher, rawMessage, messageIndex, expectedStreamIndex; this.socket = new WebSocket(this.host); this.socket.onmessage =function(raw) { raw= raw.data; if (!expectingBinary) { dataType = raw.charCodeAt(0); //Check to see if the id is in 2 parts upperByte = dataType&0x40; //Remove the upper bit if present dataType &= 0x3F; rawMessage = raw.slice(upperByte?3:2); messageIndex = upperByte?(raw.charCodeAt(1)<<16)|raw.charCodeAt(2):raw.charCodeAt(1); if (self.secure) { messageIndex = unMixBit(messageIndex, self._isomorphicKey); decipher = self._crypto.createDecipher(self._cipher, self._symmetricKey); rawMessage = decipher.update(rawMessage,'utf8'); rawMessage = decipher.final('utf8'); } //Transform the payload depending on the type if (dataType==2) //Object { try { message=JSON.parse(rawMessage); } catch(e) { //Parse Message Error return self.onErr({e:-8}) } } if (dataType==3) //Number message=parseFloat(rawMessage); if (dataType==4) //String message=rawMessage; if (dataType==5) //Boolean message=!!(rawMessage.charCodeAt(0)); if (messageIndex>0) { if (self.msg.has(messageIndex)) self.msg.call(messageIndex,message,this); else //Unknown user msg self.onErr({e:-1,add:messageIndex},this); return; } //Otherwise it must be a system message if (messageIndex==-2) //SecureServerUpgrade self.peerSecure = true; if (messageIndex==-3) //SecureServerDowngrade self.peerSecure = false; if (messageIndex==-4) //SecureClientUpgrade { //Acknowlege the request self.send(-6,true); self.secure=true; } if (messageIndex==-5) //SecureClientDowngrade { //Acknowlege the request self.send(-6,false); self.secure=false; } if (messageIndex==-7) //Provide security { if (!self._crypto) self.send(-8);//Can't satisfy upgrade else { //Iterate through ciphers to see which we support var publicKey, selectedCipher, ciphers = self._crypto.getCiphers(), index = message.cipher.length-1; for (; index>=0; index--) { if (ciphers.indexOf(message.cipher[i]) != -1) selectedCipher = message.cipher[i]; } self._cipher = selectedCipher; self._diffieHellmanKey = self.crypto.createDiffieHellman(message.prime,'base64'); self._diffieHellmanKey.generateKeys(); publicKey = self._diffieHellmanKey.getPublicKey('base64'); self._symmetricKey = self._diffieHellmanKey.computeSecret(message.pub,'base64','base64'); self._isomorphicKey = StringToMixSeed(self._symmetricKey); self.send(-8, { pub: publicKey, cipher: selectedCipher }); } } if (messageIndex==-9) //Start self.onOpen(self); if (messageIndex==-11) //FileOffered { if (canSupportDownloads) self.onFile(self._writeStreams[message.i] = new NetworkFileWriter(message,this)); else//Say we can't support it self.send(-10, message.i); } if (messageIndex==-12) //File chunk Incoming { expectingBinary=true; expectedStreamIndex=message; } if (messageIndex==-13) //File end if (self._writeStreams[message]) self._writeStreams[message]._finished(); if (messageIndex==-14) //File decline if (self._readStreams[message]) self._readStreams[message]._decline(); if (messageIndex==-15) //File accept if (self._readStreams[message]) self._readStreams[message]._accept(); if (messageIndex==-16) //File cancel if (self._readStreams[message]) self._readStreams[message]._cancel(); if (messageIndex==-17) //File finish if (self._readStreams[message]) self._readStreams[message]._finished(); if (messageIndex==-18) //File resume if (self._readStreams[message]) self._readStreams[message]._resume(); if (messageIndex==-19) //File resume if (self._readStreams[message]) self._readStreams[message]._pause(); } else { if (self._writeStreams[expectedStreamIndex]) self._writeStreams[expectedStreamIndex]._appendBlob(raw); expectedStreamIndex=0; } }; this.socket.onclose=function() { for (var stream in self._readStreams) if (self._readStreams.hasOwnProperty(stream)) self._readStreams[stream]._cancel(); for (var stream in self._writeStreams) if (self._writeStreams.hasOwnProperty(stream)) self._writeStreams[stream].cancel(); self._readStreams = self._writeStreams = self.socket = null; self.onClose(); }; this.socket.onerror=function(error) { //Native error self.onErr({e:-2,add:error}); }; return this; }, /** * Gets info about the socket * @return {Object} */ getInfo:function() { return { address :'localhost', port : 80, remoteAddress : this.host, remotePort : 80 }; }, /** * Closes the socket connection */ close:function() { if (!this.socket) return; this.socket.close(); this.onclose(); this._readStreams = this._writeStreams = this.socket = this.session = null; }, /** * Sends a file over the websocket * @param {File} file * @param {Object} options * @return {NetworkFileReader|undefined} */ sendFile:function(file,options) { options||(options={}); var streamIndex= this._fileIndex++; if (canSupportUploads&&this.socket)//not closed and we support file uploads { //Offer file this.send(-11, { name: options.name|| file.name||file.fileName, size: file.size ||file.fileSize, isStream:!!options.isStream, i:streamIndex }); return this._readStreams[streamIndex] = new NetworkFileReader(file,this,streamIndex) } }, /** * Sends a message over the socket * @param {Number} id * @param {Any} payload * @return {Client|undefined} */ send:function(id,payload) { if (!this._socket) return this; if (!id) throw new Error('Unsupplied message ID'); if (typeof id=='string') id = this.msg.id[id]; var dataType = typeof payload, cipher = this.secure&& this._crypto.createCipher(this._cipher,self._symmetricKey), id = this.secure?MixBit(id,this._isomorphicKey):i, upperBits = (id&UpperMask)>>>16, messageHasUpperbit = upperBits?0x40:0, message; if (upperBits) id = String.fromCharCode(upperBits)+String.fromCharCode(id&LowerMask); else id = String.fromCharCode(i&LowerMask); if (dataType=='boolean') { dataType = 5; message = payload?'1':'\0'; } else if (dataType=='string') { dataType = 4; message = payload; } else if (dataType=='number') { dataType = 3; message =''+payload; } else { dataType = payload?2:0; message = dataType?JSON.stringify(p):''; } if (this.secure) { //Data must be in utf 8 format message = cipher.update(message,'utf8'); message += cipher.final('utf8'); } this.socket.send(String.fromCharCode(dataType|messageHasUpperbit)+id+message); return this; } }; module.truflux= { Client : Client, fileDown : canSupportDownloads, fileUp : canSupportUploads }; })(this);