From b86fd2c255f786bae53324c3fe732e891d35ec60 Mon Sep 17 00:00:00 2001 From: themighty1 Date: Fri, 11 Sep 2015 17:14:35 +0200 Subject: [PATCH] Socket recv() abstracted away. Add 1 second delay after receiving complete records. Support zip content type. Version bump 1.0.3 --- bootstrap.js | 1 + content/chrome/chrome_specific.js | 93 ++++++--------------- content/firefox/firefox_specific.js | 121 +++++++--------------------- content/main.js | 6 ++ content/socket.js | 107 ++++++++++++++++++++++++ content/tlsn.js | 28 ------- content/tlsn_utils.js | 12 ++- install.rdf | 2 +- manifest.json | 3 +- 9 files changed, 182 insertions(+), 191 deletions(-) create mode 100644 content/socket.js diff --git a/bootstrap.js b/bootstrap.js index 256fece..7a71bfe 100644 --- a/bootstrap.js +++ b/bootstrap.js @@ -187,6 +187,7 @@ function startup(data, reason) AddonManager.getAddonByID(data.id, function(addon function loadjs(){ jsloaded = true; var addon = thisaddon; + include(addon, "socket.js"); include(addon, "firefox/button.js"); include(addon, "tlsn_utils.js"); include(addon, "oracles.js"); diff --git a/content/chrome/chrome_specific.js b/content/chrome/chrome_specific.js index cd41f94..53c3d08 100644 --- a/content/chrome/chrome_specific.js +++ b/content/chrome/chrome_specific.js @@ -425,98 +425,53 @@ function Socket(name, port){ this.name = name; this.port = port; this.uid = Math.random().toString(36).slice(-10); + this.buffer = []; + this.recv_timeout = 20*1000; } +//inherit the base class +Socket.prototype = Object.create(AbstractSocket.prototype); +Socket.prototype.constructor = Socket; + Socket.prototype.connect = function(){ var that = this; - var is_open = false; - var uid = this.uid; return new Promise(function(resolve, reject) { chrome.runtime.sendMessage(appId, {'command':'connect', 'args':{'name':that.name, 'port':that.port}, - 'uid':uid}, + 'uid':that.uid}, function(response){ console.log('in connect response', response); + clearInterval(timer); if (response.retval === 'success'){ - is_open = true; + //endless data fetching loop for the lifetime of this Socket + var fetch = function(){ + chrome.runtime.sendMessage(appId, {'command':'recv', 'uid':that.uid}, function(response){ + console.log('fetched some data', response.data.length, that.uid); + that.buffer = [].concat(that.buffer, response.data); + setTimeout(function(){fetch()}, 100); + }); + }; + fetch(); resolve('ready'); } reject(response.retval); - }); - //dont wait too loong - var timer; - var startTime = new Date().getTime(); - var check = function(){ - var now = new Date().getTime(); - if (( (now - startTime) / 1000) >= 20){ - clearInterval(timer); - reject('connect: socket timed out'); - return; - } - if (!is_open){ - console.log('connect: Another timeout'); - return; - } - clearInterval(timer); - console.log('connect: promise resolved'); - resolve('ready'); - }; - timer = setInterval(check, 100); + //dont wait for connect for too long + var timer = setTimeout(function(){ + reject('connect: socket timed out'); + }, 1000*20); }); }; Socket.prototype.send = function(data_in){ - //Transform number array into ArrayBuffer - var ab = new ArrayBuffer(data_in.length); - var dv = new DataView(ab); - for(var i=0; i < data_in.length; i++){ - dv.setUint8(i, data_in[i]); - } chrome.runtime.sendMessage(appId, {'command':'send', 'args':{'data':data_in}, 'uid':this.uid}); }; -Socket.prototype.recv = function(){ - var uid = this.uid; - return new Promise(function(resolve, reject) { - var startTime = new Date().getTime(); - var complete_records = []; - var buf = []; - var cancelled = false; - var timer = setTimeout(function(){ - reject('recv: socket timed out'); - cancelled = true; - }, 20*1000); - - var check = function(){ - if (cancelled) return; - chrome.runtime.sendMessage(appId, {'command':'recv', 'uid':uid}, function(response){ - if (cancelled) return; - if (response.data.length > 0){ - buf = [].concat(buf, response.data); - var rv = check_complete_records(buf); - complete_records = [].concat(complete_records, rv.comprecs); - if (! rv.is_complete){ - console.log("check_complete_records failed"); - buf = rv.incomprecs; - setTimeout(function(){check()}, 100); - return; - } - clearTimeout(timer); - console.log('recv promise resolved'); - resolve(complete_records); - return; - } - console.log('Another timeout in recv'); - setTimeout(function(){check()}, 100); - }); - }; - check(); - }); -}; Socket.prototype.close = function(){ - chrome.runtime.sendMessage(appId, {'command':'close'}); + console.log('closing socket', this.uid); + chrome.runtime.sendMessage(appId, + {'command':'close', 'uid':this.uid}); }; diff --git a/content/firefox/firefox_specific.js b/content/firefox/firefox_specific.js index f6f9c4b..f14263f 100644 --- a/content/firefox/firefox_specific.js +++ b/content/firefox/firefox_specific.js @@ -775,105 +775,44 @@ function Socket(name, port){ this.name = name; this.port = port; this.sckt = null; - this.is_open = false; this.buffer = []; + this.recv_timeout = 20*1000; } +//inherit the base class +Socket.prototype = Object.create(AbstractSocket.prototype); +Socket.prototype.constructor = Socket; + Socket.prototype.connect = function(){ - //TCPSocket doesnt like to be wrapped in a Promise. We work around by making the - //promise resolve when .is_open is triggered - var TCPSocket = Components.classes["@mozilla.org/tcp-socket;1"].createInstance(Components.interfaces.nsIDOMTCPSocket); - this.sckt = TCPSocket.open(this.name, this.port, {binaryType:"arraybuffer"}); - var that = this; //inside .ondata/open etc this is lost - this.sckt.ondata = function(event){ - //transform ArrayBuffer into number array - var view = new DataView(event.data); - var int_array = []; - for(var i=0; i < view.byteLength; i++){ - int_array.push(view.getUint8(i)); - } - console.log('ondata got bytes:', view.byteLength); - that.buffer = [].concat(that.buffer, int_array); - }; - this.sckt.onopen = function() { - that.is_open = true; - console.log('onopen'); - }; - - return new Promise(function(resolve, reject) { - var timer; - var startTime = new Date().getTime(); - var check = function(){ - var now = new Date().getTime(); - if (( (now - startTime) / 1000) >= 20){ - clearInterval(timer); - reject('socket timed out'); - return; - } - if (!that.is_open){ - console.log('Another timeout'); - return; - } - clearInterval(timer); - console.log('promise resolved'); - resolve('ready'); - }; - timer = setInterval(check, 100); - }); - -}; -Socket.prototype.send = function(data_in){ - //Transform number array into ArrayBuffer - var sock = this.sckt; - var ab = new ArrayBuffer(data_in.length); - var dv = new DataView(ab); - for(var i=0; i < data_in.length; i++){ - dv.setUint8(i, data_in[i]); - } - sock.send(ab, 0, ab.byteLength); -} -Socket.prototype.recv = function(is_handshake){ - if (typeof(is_handshake) === "undefined"){ - is_handshake = false; - } var that = this; return new Promise(function(resolve, reject) { - console.log('in recv promise'); - var timer; - var startTime = new Date().getTime(); - var tmp_buf = []; - - var complete_records = []; - var buf = []; - //keep checking until either timeout or enough data gathered - var check_recv = function(){ - var now = new Date().getTime(); - if (( (now - startTime) / 1000) >= 20){ - clearInterval(timer); - console.log('rejecting'); - reject('socket timed out'); - return; - } - if (that.buffer.length === 0){ - console.log('Another timeout in recv'); - return; - } - buf = [].concat(buf, that.buffer); - that.buffer = []; - var rv = check_complete_records(buf); - complete_records = [].concat(complete_records, rv.comprecs); - if (! rv.is_complete){ - console.log("check_complete_records failed"); - buf = rv.incomprecs; - return; - } - //else - clearInterval(timer); - console.log('promise resolved'); - resolve(complete_records); + var TCPSocket = Components.classes["@mozilla.org/tcp-socket;1"].createInstance(Components.interfaces.nsIDOMTCPSocket); + that.sckt = TCPSocket.open(that.name, that.port, {binaryType:"arraybuffer"}); + that.sckt.ondata = function(event){ + var int_array = ab2ba(event.data) + console.log('ondata got bytes:', int_array.length); + that.buffer = [].concat(that.buffer, int_array); }; - timer = setInterval(check_recv, 100); + //dont wait for connect for too long + var timer = setTimeout(function(){ + reject('connect: socket timed out'); + }, 1000*20) + + that.sckt.onopen = function() { + clearInterval(timer); + console.log('onopen'); + resolve('ready'); + }; + that.sckt.onerror = function(event) { + clearInterval(timer); + console.log('onerror', event.data); + reject(event.data); + }; }); }; +Socket.prototype.send = function(data_in){ + var ab = ba2ab(data_in); + this.sckt.send(ab, 0, ab.byteLength); +}; Socket.prototype.close = function(){ this.sckt.close(); }; diff --git a/content/main.js b/content/main.js index 0e0d937..b51cb89 100644 --- a/content/main.js +++ b/content/main.js @@ -306,6 +306,10 @@ function writeDatafile(data_with_headers, session_dir){ type = 'pdf'; break; } + else if (header_lines[i].search("zip") > -1){ + type = 'zip'; + break; + } } } if (type === "html"){ @@ -675,6 +679,8 @@ function verifyCert(chain){ + + //This must be at the bottom, otherwise we'd have to define each function //before it gets used. browser_specific_init(); diff --git a/content/socket.js b/content/socket.js new file mode 100644 index 0000000..784a039 --- /dev/null +++ b/content/socket.js @@ -0,0 +1,107 @@ +//The only way to determine if the server is done sending data is to check that the receiving +//buffer has nothing but complete TLS records i.e. that there is no incomplete TLS records +//However it was observed that in cases when getting e.g. zip files, some servers first send HTTP header as one +//TLS record followed by the body as another record(s) +//That's why after receiving a complete TLS record we wait to get some more data +//This extra waiting must not be done for the handshake messages to avoid adding latency and having the handshake +//dropped by the server +function AbstractSocket(){}; +AbstractSocket.prototype.recv = function(is_handshake){ + if (typeof(is_handshake) === 'undefined'){ + is_handshake = false; + } + var that = this; + return new Promise(function(resolve, reject) { + var startTime = new Date().getTime(); + var complete_records = []; + var buf = []; + var resolved = false; + + var timer = setTimeout(function(){ + reject('recv: socket timed out'); + resolved = true; + }, that.recv_timeout); + + var check = function(){ + //console.log('check()ing for more data', uid); + if (resolved) { + console.log('returning because resolved'); + return; + } + if (that.buffer.length === 0) { + setTimeout(function(){check()}, 100); + return; + } + console.log('new data in check', that.buffer.length); + //else got new data + buf = [].concat(buf, that.buffer); + that.buffer = []; + var rv = check_complete_records(buf); + complete_records = [].concat(complete_records, rv.comprecs); + if (! rv.is_complete){ + console.log("check_complete_records failed", that.uid); + buf = rv.incomprecs; + setTimeout(function(){check()}, 100); + return; + } + else { + function finished_receiving(){ + clearTimeout(timer); + console.log('recv promise resolving', that.uid); + resolved = true; + resolve(complete_records); + }; + + console.log("got complete records", that.uid); + if (is_handshake){ + finished_receiving(); + return; + } + else { + console.log("in recv waiting for an extra second", that.uid); + buf = []; + //give the server another second to send more data + setTimeout(function(){ + if (that.buffer.length === 0){ + finished_receiving(); + return; + } + else { + console.log('more data received after waiting for a second', that.uid); + check(); + } + },1000); + } + } + }; + check(); + }); +}; + + +function check_complete_records(d){ + /*'''Given a response d from a server, + we want to know if its contents represents + a complete set of records, however many.''' + */ + var complete_records = []; + var incomplete_records = []; + + while(d){ + if (d.length < 5){ + return {'is_complete':false, 'comprecs':complete_records, 'incomprecs':d}; + } + var l = ba2int(d.slice(3,5)); + if (d.length < (l+5)){ + return {'is_complete':false, 'comprecs':complete_records, 'incomprecs':d}; + } + else if(d.length === (l+5)){ + return {'is_complete':true, 'comprecs':[].concat(complete_records, d)}; + } + else { + complete_records = [].concat(complete_records, d.slice(0, l+5)); + d = d.slice(l+5); + continue; + } + } +} \ No newline at end of file diff --git a/content/tlsn.js b/content/tlsn.js index 3ca8881..1cb4a9c 100644 --- a/content/tlsn.js +++ b/content/tlsn.js @@ -64,34 +64,6 @@ function get_cs(cs){ } - - - -function check_complete_records(d){ - /*'''Given a response d from a server, - we want to know if its contents represents - a complete set of records, however many.''' - */ - var complete_records = []; - var incomplete_records = []; - - while(d){ - var l = ba2int(d.slice(3,5)); - if (d.length < (l+5)){ - return {'is_complete':false, 'comprecs':complete_records, 'incomprecs':d}; - } - else if(d.length === (l+5)){ - return {'is_complete':true, 'comprecs':[].concat(complete_records, d)}; - } - else { - complete_records = [].concat(complete_records, d.slice(0, l+5)); - d = d.slice(l+5); - continue; - } - } -} - - function send_and_recv(command, data, expected_response){ return new Promise(function(resolve, reject) { var req = get_xhr(); diff --git a/content/tlsn_utils.js b/content/tlsn_utils.js index 7461cd6..59f5864 100644 --- a/content/tlsn_utils.js +++ b/content/tlsn_utils.js @@ -9,6 +9,16 @@ function ab2ba(ab){ } +function ba2ab(ba){ + var ab = new ArrayBuffer(ba.length); + var dv = new DataView(ab); + for(var i=0; i < ba.length; i++){ + dv.setUint8(i, ba[i]); + } + return ab; +} + + function ba2ua(ba){ var ua = new Uint8Array(ba.length); @@ -71,7 +81,7 @@ function hex2ba(str){ return ba; } -//Turn a max 4 byte array into an int. +//Turn a max 4 byte array (big-endian) into an int. function ba2int( x ){ assert(x.length <= 8, "Cannot convert bytearray larger than 8 bytes"); var retval = 0; diff --git a/install.rdf b/install.rdf index 6a0aa7b..747598a 100644 --- a/install.rdf +++ b/install.rdf @@ -5,7 +5,7 @@ pagesigner@tlsnotary - 1.0.2 + 1.0.3 2 true true diff --git a/manifest.json b/manifest.json index 98855dd..75b8215 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "PageSigner", "description": "PageSigner - a cryptographically secure webpage screenshot tool", - "version": "1.0.2", + "version": "1.0.3", "author": "TLSNotary Group", "permissions": [ @@ -43,6 +43,7 @@ "background": { "scripts": [ + "content/socket.js", "content/chrome/chrome_specific.js", "content/tlsn_utils.js", "content/oracles.js",