Skip to content

Commit 547d37d

Browse files
authored
Merge pull request #2798 from uProxy/dborkan-cloud-ping
Check if cloud server is online
2 parents a9a3f1c + 53cd82a commit 547d37d

File tree

4 files changed

+131
-68
lines changed

4 files changed

+131
-68
lines changed

src/generic_core/remote-user.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,9 @@ var log :logging.Log = new logging.Log('remote-user');
574574
// handshake is sent to the peer.
575575
log.error('Attempting to send instance handshake before ready');
576576
return;
577+
} else if (this.network.name === 'Cloud') {
578+
// Don't send instance handshake to cloud servers.
579+
return;
577580
}
578581
// Ensure that the user is loaded so that we have correct consent bits.
579582
return this.onceLoaded.then(() => {

src/generic_core/uproxy_core.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -862,22 +862,6 @@ export class uProxyCore implements uproxy_core_api.CoreApi {
862862
networkName: args.networkName,
863863
userId: args.userId
864864
});
865-
}).then(() => {
866-
// If we removed the only cloud friend, logout of the cloud network
867-
if (args.networkName === 'Cloud') {
868-
return this.logoutIfRosterEmpty_(network);
869-
}
870865
});
871866
}
872-
873-
private logoutIfRosterEmpty_ = (network :social.Network) : Promise<void> => {
874-
if (Object.keys(network.roster).length === 0) {
875-
return this.logout({
876-
name: network.name
877-
}).then(() => {
878-
log.info('Successfully logged out of %1 network because roster is empty', network.name);
879-
});
880-
}
881-
return Promise.resolve();
882-
}
883867
} // class uProxyCore

src/lib/cloud/social/provider.ts

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as crypto from 'crypto';
55
import * as linefeeder from '../../net/linefeeder';
66
import * as logging from '../../logging/logging';
77
import * as queue from '../../handler/queue';
8+
import Pinger from '../../net/pinger';
89

910
// https://github.com/borisyankov/DefinitelyTyped/blob/master/ssh2/ssh2-tests.ts
1011
import * as ssh2 from 'ssh2';
@@ -121,18 +122,6 @@ function makeInstanceMessage(address:string, description?:string): any {
121122
};
122123
}
123124

124-
// To see how these fields are handled, see
125-
// generic_core/social.ts#handleClient in the uProxy repo.
126-
function makeClientState(address: string): freedom.Social.ClientState {
127-
return {
128-
userId: address,
129-
clientId: address,
130-
// https://github.com/freedomjs/freedom/blob/master/interface/social.json
131-
status: 'ONLINE',
132-
timestamp: Date.now()
133-
};
134-
}
135-
136125
// To see how these fields are handled, see
137126
// generic_core/social.ts#handleUserProfile in the uProxy repo. We omit
138127
// the status field since remote-user.ts#update will use FRIEND as a default.
@@ -179,6 +168,15 @@ export class CloudSocialProvider {
179168
// SSH connections, keyed by host.
180169
private clients_: { [host: string]: Promise<Connection> } = {};
181170

171+
// Map from host to whether it is online. Hosts not in the map are assumed
172+
// to be offline.
173+
private onlineHosts_: { [host: string]: boolean } = {};
174+
175+
// Map from host to intervalId used for monitoring online presence.
176+
private onlinePresenceMonitorIds_: { [host: string]: NodeJS.Timer } = {};
177+
178+
private static PING_INTERVAL_ = 60000;
179+
182180
constructor(private dispatchEvent_: (name: string, args: Object) => void) { }
183181

184182
// Emits the messages necessary to make the user appear online
@@ -187,16 +185,18 @@ export class CloudSocialProvider {
187185
this.dispatchEvent_('onUserProfile', makeUserProfile(
188186
contact.invite.host, contact.invite.isAdmin));
189187

190-
var clientState = makeClientState(contact.invite.host);
188+
var clientState = this.makeClientState_(contact.invite.host);
191189
this.dispatchEvent_('onClientState', clientState);
192190

193-
// Pretend that we received a message from a remote uProxy client.
194-
this.dispatchEvent_('onMessage', {
195-
from: clientState,
196-
// INSTANCE
197-
message: JSON.stringify(makeVersionedPeerMessage(3000, makeInstanceMessage(
198-
contact.invite.host, contact.description), contact.version))
199-
});
191+
if (this.isOnline_(contact.invite.host)) {
192+
// Pretend that we received a message from a remote uProxy client.
193+
this.dispatchEvent_('onMessage', {
194+
from: clientState,
195+
// INSTANCE
196+
message: JSON.stringify(makeVersionedPeerMessage(3000, makeInstanceMessage(
197+
contact.invite.host, contact.description), contact.version))
198+
});
199+
}
200200
}
201201

202202
// Establishes an SSH connection to a server, first shutting down
@@ -213,8 +213,10 @@ export class CloudSocialProvider {
213213
}
214214

215215
const connection = new Connection(invite, (message: Object) => {
216+
// Set the server to online, since we are receiving messages from them.
217+
this.setOnlineStatus_(invite.host, true);
216218
this.dispatchEvent_('onMessage', {
217-
from: makeClientState(invite.host),
219+
from: this.makeClientState_(invite.host),
218220
// SIGNAL_FROM_SERVER_PEER,
219221
message: JSON.stringify(makeVersionedPeerMessage(3002,
220222
message, connection.getVersion()))
@@ -224,6 +226,9 @@ export class CloudSocialProvider {
224226
this.clients_[invite.host] = connection.connect().then(() => {
225227
log.info('connected to zork on %1', invite.host);
226228

229+
// Cloud server is online if a connection has succeeded.
230+
this.setOnlineStatus_(invite.host, true);
231+
227232
// Fetch the banner, if available, then emit an instance message.
228233
connection.getBanner().then((banner: string) => {
229234
if (banner.length < 1) {
@@ -265,6 +270,7 @@ export class CloudSocialProvider {
265270
if (savedContacts.contacts) {
266271
for (let contact of savedContacts.contacts) {
267272
this.savedContacts_[contact.invite.host] = contact;
273+
this.startMonitoringPresence_(contact.invite.host);
268274
this.notifyOfUser_(contact);
269275
}
270276
}
@@ -298,7 +304,7 @@ export class CloudSocialProvider {
298304
// TODO: emit an onUserProfile event, which can include an image URL
299305
// TODO: base this on the user's public key?
300306
// (shown in the "connected accounts" page)
301-
return Promise.resolve(makeClientState('me'));
307+
return Promise.resolve(this.makeClientState_('me'));
302308
}
303309

304310
public sendMessage = (destinationClientId: string, message: string): Promise<void> => {
@@ -410,15 +416,9 @@ export class CloudSocialProvider {
410416
this.savedContacts_[invite.host] = {
411417
invite: invite
412418
};
419+
this.startMonitoringPresence_(invite.host);
413420
this.notifyOfUser_(this.savedContacts_[invite.host]);
414421
this.saveContacts_();
415-
416-
// Connect in the background in order to fetch metadata such as
417-
// the banner (description).
418-
this.reconnect_(invite).catch((e: Error) => {
419-
log.warn('failed to log into cloud server during invite accept: %1', e.message);
420-
});
421-
422422
return Promise.resolve();
423423
} catch (e) {
424424
return Promise.reject(new Error('could not parse invite code: ' + e.message));
@@ -439,12 +439,84 @@ export class CloudSocialProvider {
439439
log.warn('cloud contact %1 is not in %2 - cannot remove from storage', host, STORAGE_KEY);
440440
return Promise.resolve();
441441
}
442+
this.stopMonitoringPresence_(host);
442443
// Remove host from savedContacts and clients
443444
delete this.savedContacts_[host];
444445
delete this.clients_[host];
445446
// Update storage with this.savedContacts_
446447
return this.saveContacts_();
447448
}
449+
450+
private startMonitoringPresence_ = (host: string) => {
451+
if (this.onlinePresenceMonitorIds_[host]) {
452+
log.error('unexpected call to startMonitoringPresence_ for ' + host);
453+
return;
454+
}
455+
// Ping server every minute to see if it is online. A server is considered
456+
// online if a connection can be established with the SSH port. We stop
457+
// pinging for presence once the host is online, so as to not give away
458+
// that we are pinging uProxy cloud servers with a regular heartbeat.
459+
const ping = () : Promise<boolean> => {
460+
var pinger = new Pinger(host, SSH_SERVER_PORT);
461+
return pinger.pingOnce().then(() => {
462+
return true;
463+
}).catch(() => {
464+
return false;
465+
}).then((newOnlineValue: boolean) => {
466+
var oldOnlineValue = this.isOnline_(host);
467+
this.setOnlineStatus_(host, newOnlineValue);
468+
if (newOnlineValue !== oldOnlineValue) {
469+
// status changed, emit a new onClientState.
470+
this.notifyOfUser_(this.savedContacts_[host]);
471+
if (newOnlineValue) {
472+
// Connect in the background in order to fetch metadata such as
473+
// the banner (description).
474+
const invite = this.savedContacts_[host].invite;
475+
this.reconnect_(invite).catch((e: Error) => {
476+
log.error('failed to log into cloud server once online: %1', e.message);
477+
});
478+
}
479+
}
480+
});
481+
}
482+
this.onlinePresenceMonitorIds_[host] = setInterval(ping, CloudSocialProvider.PING_INTERVAL_);
483+
// Ping server immediately (so we don't have to wait 1 min for 1st result).
484+
ping();
485+
}
486+
487+
private stopMonitoringPresence_ = (host: string) => {
488+
if (!this.onlinePresenceMonitorIds_[host]) {
489+
// We may have already stopped monitoring presence, e.g. because the
490+
// host has come online.
491+
return;
492+
}
493+
clearInterval(this.onlinePresenceMonitorIds_[host]);
494+
delete this.onlinePresenceMonitorIds_[host];
495+
}
496+
497+
private isOnline_ = (host: string) => {
498+
return host === 'me' || this.onlineHosts_[host] === true;
499+
}
500+
501+
private setOnlineStatus_ = (host: string, isOnline: boolean) => {
502+
this.onlineHosts_[host] = isOnline;
503+
if (isOnline) {
504+
// Stop monitoring presence once the client is online.
505+
this.stopMonitoringPresence_(host);
506+
}
507+
}
508+
509+
// To see how these fields are handled, see
510+
// generic_core/social.ts#handleClient in the uProxy repo.
511+
private makeClientState_ = (address: string) : freedom.Social.ClientState => {
512+
return {
513+
userId: address,
514+
clientId: address,
515+
// https://github.com/freedomjs/freedom/blob/master/interface/social.json
516+
status: this.isOnline_(address) ? 'ONLINE' : 'OFFLINE',
517+
timestamp: Date.now()
518+
};
519+
}
448520
}
449521

450522
enum ConnectionState {

src/lib/net/pinger.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,32 @@ export default class Pinger {
2020
public ping = (): Promise<void> => {
2121
log.debug('pinging %1:%2...', this.host_ , this.port_);
2222

23-
return promises.retry(() => {
24-
const socket = freedom['core.tcpsocket']();
25-
26-
const destructor = () => {
27-
try {
28-
freedom['core.tcpsocket'].close(socket);
29-
} catch (e) {
30-
log.warn('error destroying socket: ' + e.message);
31-
}
32-
};
33-
34-
// TODO: Worth thinking about timeouts here but because this times
35-
// out almost immediately if nothing is listening on the port,
36-
// it works well for our purposes.
37-
return socket.connect(this.host_, this.port_).then((unused: any) => {
38-
log.debug('connected to ' + this.host_ + ':' + this.port_ + '...');
39-
destructor();
40-
}, (e: Error) => {
41-
log.debug('connection failed to ' + this.host_ + ':' + this.port_ + '...');
42-
destructor();
43-
throw e;
44-
});
45-
}, this.timeout_, DEFAULT_INTERVAL_MS);
23+
return promises.retry(this.pingOnce, this.timeout_, DEFAULT_INTERVAL_MS);
24+
}
25+
26+
// Resolves if a connection has been established, or rejects if the connection
27+
// fails. Does not retry.
28+
public pingOnce = () : Promise<void> => {
29+
const socket = freedom['core.tcpsocket']();
30+
31+
const destructor = () => {
32+
try {
33+
freedom['core.tcpsocket'].close(socket);
34+
} catch (e) {
35+
log.warn('error destroying socket: ' + e.message);
36+
}
37+
};
38+
39+
// TODO: Worth thinking about timeouts here but because this times
40+
// out almost immediately if nothing is listening on the port,
41+
// it works well for our purposes.
42+
return socket.connect(this.host_, this.port_).then((unused: any) => {
43+
log.debug('connected to ' + this.host_ + ':' + this.port_ + '...');
44+
destructor();
45+
}, (e: Error) => {
46+
log.debug('connection failed to ' + this.host_ + ':' + this.port_ + '...');
47+
destructor();
48+
throw e;
49+
});
4650
}
4751
}

0 commit comments

Comments
 (0)