Files
PageSigner/core/Socket.js
themighty1 bca431a91e enforce same input labels for c4 and c6
dont perform OT on the same inputs of c4 and c6
use fetch() built-in download progress monitor
query the notary about fetch() upload progress
renumber circuits (we have new circuit 4)
traverse circuit faster with wasm
2022-03-08 16:30:43 +03:00

259 lines
8.5 KiB
JavaScript

/* global chrome*/
// 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 are 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 causing a
// handshake timeout by the server.
// We do not communicate directly with the server but we send messages to the
// helper app. It is the helper app who opens a TCP socket and sends/receives
// data.
import {globals} from './globals.js';
import {ba2str, b64decode, concatTA, b64encode, str2ba, ba2int} from './utils.js';
export class Socket {
constructor(name, port){
this.name = name;
this.port = port;
this.uid = Math.random().toString(36).slice(-10);
this.buffer = new Uint8Array();
// connect will throw if we couldnt establish a connection to the server for this long
this.connectTimeout = 5 * 1000;
// recv() will reject if no data was seen for this long
this.noDataTimeout = 5 * 1000;
// close the socket after this time, even if it is in the middle of receiving data
this.lifeTime = 40 * 1000;
// delay after which we make a final check of the receicing buffer and if there was no data,
// from the server, the we consider the data transmission finished
this.delayBeforeFinalIteration = 500;
this.wasClosed = false;
this.backendPort = 20022;
}
async connect() {
const that = this;
let timer;
// eslint-disable-next-line no-async-promise-executor
const response = await new Promise(async function(resolve, reject) {
// dont wait for connect for too long
timer = setTimeout(function() {
reject('Unable to connect to the webserver. Please check your internet connection.');
return;
}, that.connectTimeout);
const msg = {'command': 'connect', 'args': {'name': that.name, 'port': that.port}, 'uid': that.uid};
if (globals.usePythonBackend){
const url = 'http://127.0.0.1:' + that.backendPort;
const payload = JSON.stringify(msg);
try{
var req = await fetch (url, {method:'POST', body: str2ba(payload).buffer, cache: 'no-store'});
}
catch (error) {
reject('connection error');
return;
}
const text = new Uint8Array (await req.arrayBuffer());
const response = ba2str(text);
resolve(JSON.parse(response));
return;
}
else {
chrome.runtime.sendMessage(globals.appId, msg, function(response) {resolve(response);});
}
})
.catch(function(e){
throw(e);
});
// we need to access runtime.lastError to prevent Chrome from complaining
// about unchecked error
chrome.runtime.lastError;
clearTimeout(timer);
if (response == undefined){
throw (undefined);
}
if (response.retval != 'success') {
// throw(response.retval)
}
// else if (response.retval == 'success') {
setTimeout(function() {
if (! that.wasClosed)
that.close();
}, that.lifeTime);
// endless data fetching loop for the lifetime of this Socket
that.fetchLoop();
return 'ready';
}
async send(data_in) {
var msg = {'command': 'send', 'args': {'data': Array.from(data_in)}, 'uid': this.uid};
if (globals.usePythonBackend){
msg.args.data = Array.from(b64encode(msg.args.data));
await fetch('http://127.0.0.1:20022', {method:'POST', body: JSON.stringify(msg),
cache: 'no-store'});
}
else{
chrome.runtime.sendMessage(globals.appId, msg);
}
}
// poll the backend for more data
async fetchLoop() {
if (this.wasClosed) {
return;
}
var that = this;
// eslint-disable-next-line no-async-promise-executor
var response = await new Promise(async function(resolve){
var msg = {'command': 'recv', 'uid': that.uid};
if (globals.usePythonBackend){
var req = await fetch('http://127.0.0.1:20022', {method:'POST', body: JSON.stringify(msg),
cache: 'no-store'});
var text = new Uint8Array(await req.arrayBuffer());
var response = JSON.parse(ba2str(text));
if (response.data.length > 0){
response.data = Array.from(b64decode(response.data));
}
resolve(response);
}
else {
chrome.runtime.sendMessage(globals.appId, msg, function(response) {resolve(response);});
}
});
if (response.data.length > 0){
console.log('fetched some data', response.data.length, that.uid);
that.buffer = concatTA(that.buffer, new Uint8Array(response.data));
}
setTimeout(function() {
that.fetchLoop();
}, 100);
}
// fetchLoop has built up the recv buffer. Check if there are complete
// records in the buffer, return them if yes or wait some more if no.
recv (is_handshake = false) {
const that = this;
return new Promise(function(resolve, reject) {
var dataLastSeen = new Date().getTime();
var complete_records = new Uint8Array();
var buf = new Uint8Array();
var resolved = false;
var lastIteration = false;
function finished_receiving() {
console.log('recv promise resolving', that.uid);
resolved = true;
resolve(complete_records);
}
var check = function() {
var now = new Date().getTime();
if ((now - dataLastSeen) > that.noDataTimeout){
reject('recv: no data timeout');
return;
}
// console.log('check()ing for more data', uid);
if (resolved) {
console.log('returning because resolved');
return;
}
if (that.buffer.length === 0) {
if (lastIteration){
finished_receiving();
return;
}
setTimeout(function() {check();}, 100);
return;
}
// else got new data
if (lastIteration){
console.log('more data received on last iteration', that.uid);
lastIteration = false;
}
console.log('new data in check', that.buffer.length);
dataLastSeen = now;
buf = concatTA(buf, that.buffer);
that.buffer = new Uint8Array();
const rv = that.check_complete_records(buf);
complete_records = concatTA(complete_records, rv.comprecs);
if (!rv.is_complete) {
console.log('waiting for complete records...', that.uid);
buf = rv.incomprecs;
setTimeout(function() {check();}, 100);
return;
}
else {
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 = new Uint8Array();
// give the server another second to send more data
lastIteration = true;
setTimeout(function() {check();}, that.delayBeforeFinalIteration);
}
}
};
check();
})
.catch(function(error){
throw(error);
});
}
async close() {
this.wasClosed = true;
var msg = {'command': 'close', 'uid': this.uid};
console.log('closing socket', this.uid);
if (globals.usePythonBackend){
await fetch('http://127.0.0.1:20022', {method:'POST', body: JSON.stringify(msg),
cache: 'no-store'});
}
else {
chrome.runtime.sendMessage(globals.appId, msg);
}
}
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.'''
*/
let complete_records = new Uint8Array();
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': concatTA(complete_records, d)
};
} else {
complete_records = concatTA(complete_records, d.slice(0, l + 5));
d = d.slice(l + 5);
continue;
}
}
}
}