Move/refactor keepalive logic and add disconnect timer

We now disconnect ourselves if we don't get the server's response to a
keepalive request within 30s. This way we will eventually disconnect if
the network goes away but the socket is not closed.*

* See code.google.com/p/chromium/issues/detail?id=197841 and
https://stackoverflow.com/questions/11755605/chrome-websocket-connection-not-closed-when-browser-closed

We will then try to reconnect once a minute (See 8a10c96);

Keepalives belong at this level anyway, since the format is defined by
both the websocket resource protocol and our specific server url
structure.

// FREEBIE
This commit is contained in:
lilia 2015-07-16 13:16:50 -07:00
parent 8a10c96ab4
commit 7d3d634a2d
6 changed files with 62 additions and 65 deletions

View file

@ -38398,7 +38398,6 @@ axolotlInternal.RecipientRecord = function() {
TextSecureWebSocket = function (url) { TextSecureWebSocket = function (url) {
'use strict'; 'use strict';
var keepAliveTimer;
var reconnectSemaphore = 0; var reconnectSemaphore = 0;
var reconnectTimeout = 1000; var reconnectTimeout = 1000;
var socket; var socket;
@ -38408,27 +38407,10 @@ TextSecureWebSocket = function (url) {
onclose : function() {}, onclose : function() {},
onerror : function() {}, onerror : function() {},
getStatus : function() { return socket.readyState; }, getStatus : function() { return socket.readyState; },
close : function() { calledClose = true; } close : function() { calledClose = true; socket.close(); }
}; };
var error; var error;
function resetKeepAliveTimer() {
clearTimeout(keepAliveTimer);
if (calledClose) { return; }
keepAliveTimer = setTimeout(function() {
if (socket.readyState === WebSocket.OPEN) {
socket.send(
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: { verb: 'GET', path: '/v1/keepalive' }
}).encode().toArrayBuffer()
);
}
resetKeepAliveTimer();
}, 55000);
};
function onclose(e) { function onclose(e) {
if (!error && !calledClose) { if (!error && !calledClose) {
reconnectSemaphore--; reconnectSemaphore--;
@ -38448,22 +38430,18 @@ TextSecureWebSocket = function (url) {
function onmessage(response) { function onmessage(response) {
socketWrapper.onmessage(response); socketWrapper.onmessage(response);
resetKeepAliveTimer();
}; };
function send(msg) { function send(msg) {
resetKeepAliveTimer();
socket.send(msg); socket.send(msg);
}; };
function connect() { function connect() {
clearTimeout(keepAliveTimer);
if (++reconnectSemaphore <= 0) { return; } if (++reconnectSemaphore <= 0) { return; }
if (socket) { socket.close(); } if (socket) { socket.close(); }
socket = new WebSocket(url); socket = new WebSocket(url);
socket.onopen = resetKeepAliveTimer;
socket.onerror = onerror socket.onerror = onerror
socket.onclose = onclose; socket.onclose = onclose;
socket.onmessage = onmessage; socket.onmessage = onmessage;
@ -38527,6 +38505,10 @@ TextSecureWebSocket = function (url) {
window.crypto.getRandomValues(bits); window.crypto.getRandomValues(bits);
this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true); this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
} }
if (this.body === undefined) {
this.body = null;
}
}; };
var IncomingWebSocketRequest = function(options) { var IncomingWebSocketRequest = function(options) {
@ -39507,9 +39489,26 @@ function generateKeys(count, progressCallback) {
} }
} }
new WebSocketResource(this.socket, this.handleRequest.bind(this)); this.wsr = new WebSocketResource(this.socket, this.handleRequest.bind(this));
this.resetKeepAliveTimer();
},
resetKeepAliveTimer: function() {
clearTimeout(this.keepAliveTimer);
clearTimeout(this.disconnectTimer);
this.keepAliveTimer = setTimeout(function() {
if (this.getStatus() === WebSocket.OPEN) {
this.wsr.sendRequest({
verb: 'GET',
path: '/v1/keepalive',
success: this.resetKeepAliveTimer.bind(this)
});
}
this.disconnectTimer = setTimeout(this.socket.close, 30000);
}.bind(this), 55000);
}, },
handleRequest: function(request) { handleRequest: function(request) {
this.resetKeepAliveTimer();
// TODO: handle different types of requests. for now we only expect // TODO: handle different types of requests. for now we only expect
// PUT /messages <encrypted IncomingPushMessageSignal> // PUT /messages <encrypted IncomingPushMessageSignal>
textsecure.crypto.decryptWebsocketMessage(request.body).then(function(plaintext) { textsecure.crypto.decryptWebsocketMessage(request.body).then(function(plaintext) {

View file

@ -44,9 +44,26 @@
} }
} }
new WebSocketResource(this.socket, this.handleRequest.bind(this)); this.wsr = new WebSocketResource(this.socket, this.handleRequest.bind(this));
this.resetKeepAliveTimer();
},
resetKeepAliveTimer: function() {
clearTimeout(this.keepAliveTimer);
clearTimeout(this.disconnectTimer);
this.keepAliveTimer = setTimeout(function() {
if (this.getStatus() === WebSocket.OPEN) {
this.wsr.sendRequest({
verb: 'GET',
path: '/v1/keepalive',
success: this.resetKeepAliveTimer.bind(this)
});
}
this.disconnectTimer = setTimeout(this.socket.close, 30000);
}.bind(this), 55000);
}, },
handleRequest: function(request) { handleRequest: function(request) {
this.resetKeepAliveTimer();
// TODO: handle different types of requests. for now we only expect // TODO: handle different types of requests. for now we only expect
// PUT /messages <encrypted IncomingPushMessageSignal> // PUT /messages <encrypted IncomingPushMessageSignal>
textsecure.crypto.decryptWebsocketMessage(request.body).then(function(plaintext) { textsecure.crypto.decryptWebsocketMessage(request.body).then(function(plaintext) {

View file

@ -45,6 +45,21 @@ describe('MessageReceiver', function() {
assert.strictEqual(signal.message.body, 'hello'); assert.strictEqual(signal.message.body, 'hello');
}); });
var messageReceiver = new textsecure.MessageReceiver(window); var messageReceiver = new textsecure.MessageReceiver(window);
messageReceiver.connect(); });
it('sends a keepalive once a minute', function(done) {
this.timeout(60000);
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/v1/keepalive');
server.close();
done();
});
});
var messageReceiver = new textsecure.MessageReceiver(window);
}); });
}); });

View file

@ -75,20 +75,4 @@ describe('TextSecureWebSocket', function() {
mockServer.close(); mockServer.close();
}); });
it('sends a keepalive once a minute', function(done) {
this.timeout(60000);
var mockServer = new MockServer('ws://localhost:8081');
mockServer.on('connection', function(server) {
server.on('message', function(data) {
var message = textsecure.protobuf.WebSocketMessage.decode(data);
assert.strictEqual(message.type, textsecure.protobuf.WebSocketMessage.Type.REQUEST);
assert.strictEqual(message.request.verb, 'GET');
assert.strictEqual(message.request.path, '/v1/keepalive');
socket.close();
server.close();
done();
});
});
var socket = new TextSecureWebSocket('ws://localhost:8081');
});
}); });

View file

@ -51,6 +51,10 @@
window.crypto.getRandomValues(bits); window.crypto.getRandomValues(bits);
this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true); this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true);
} }
if (this.body === undefined) {
this.body = null;
}
}; };
var IncomingWebSocketRequest = function(options) { var IncomingWebSocketRequest = function(options) {

View file

@ -24,7 +24,6 @@
TextSecureWebSocket = function (url) { TextSecureWebSocket = function (url) {
'use strict'; 'use strict';
var keepAliveTimer;
var reconnectSemaphore = 0; var reconnectSemaphore = 0;
var reconnectTimeout = 1000; var reconnectTimeout = 1000;
var socket; var socket;
@ -34,27 +33,10 @@ TextSecureWebSocket = function (url) {
onclose : function() {}, onclose : function() {},
onerror : function() {}, onerror : function() {},
getStatus : function() { return socket.readyState; }, getStatus : function() { return socket.readyState; },
close : function() { calledClose = true; } close : function() { calledClose = true; socket.close(); }
}; };
var error; var error;
function resetKeepAliveTimer() {
clearTimeout(keepAliveTimer);
if (calledClose) { return; }
keepAliveTimer = setTimeout(function() {
if (socket.readyState === WebSocket.OPEN) {
socket.send(
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: { verb: 'GET', path: '/v1/keepalive' }
}).encode().toArrayBuffer()
);
}
resetKeepAliveTimer();
}, 55000);
};
function onclose(e) { function onclose(e) {
if (!error && !calledClose) { if (!error && !calledClose) {
reconnectSemaphore--; reconnectSemaphore--;
@ -74,22 +56,18 @@ TextSecureWebSocket = function (url) {
function onmessage(response) { function onmessage(response) {
socketWrapper.onmessage(response); socketWrapper.onmessage(response);
resetKeepAliveTimer();
}; };
function send(msg) { function send(msg) {
resetKeepAliveTimer();
socket.send(msg); socket.send(msg);
}; };
function connect() { function connect() {
clearTimeout(keepAliveTimer);
if (++reconnectSemaphore <= 0) { return; } if (++reconnectSemaphore <= 0) { return; }
if (socket) { socket.close(); } if (socket) { socket.close(); }
socket = new WebSocket(url); socket = new WebSocket(url);
socket.onopen = resetKeepAliveTimer;
socket.onerror = onerror socket.onerror = onerror
socket.onclose = onclose; socket.onclose = onclose;
socket.onmessage = onmessage; socket.onmessage = onmessage;