truflux
Version:
Secure socket connection module
536 lines (500 loc) • 18.8 kB
JavaScript
(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);