Skip to content

Commit bb03368

Browse files
committed
Merge pull request #412 from uProxy/trevj-cloud-social-hardening
cloud social provider hardening
2 parents 5c77741 + 27898b7 commit bb03368

File tree

4 files changed

+118
-64
lines changed

4 files changed

+118
-64
lines changed

src/cloud/digitalocean/provisioner.ts

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference path='../../../../third_party/typings/browser.d.ts' />
22

3-
import promises = require('../../promises/promises');
3+
import Pinger = require('../../net/pinger');
44

55
declare const freedom: freedom.FreedomInModuleEnv;
66

@@ -110,31 +110,11 @@ class Provisioner {
110110
}
111111
}
112112

113-
// Spin until the server is truly up.
114-
// Give it one minute before declaring bankruptcy.
115-
console.log('waiting for activity on port 22');
116-
return promises.retry(() => {
117-
const socket = freedom['core.tcpsocket']();
118-
119-
const destructor = () => {
120-
try {
121-
freedom['core.tcpsocket'].close(socket);
122-
} catch (e) {
123-
console.warn('error destroying socket: ' + e.message);
124-
}
125-
};
126-
127-
// TODO: Worth thinking about timeouts here but because this times
128-
// out almost immediately if nothing is listening on the port,
129-
// it works well for our purposes.
130-
return socket.connect(this.state_.network['ipv4'], 22).then((unused: any) => {
131-
destructor();
132-
return this.state_;
133-
}, (e: Error) => {
134-
destructor();
135-
throw e;
136-
});
137-
}, 60, 1000);
113+
// It usually takes several seconds after the API reports success for
114+
// SSH on a new droplet to become responsive.
115+
return new Pinger(this.state_.network['ipv4'], 22, 60).ping().then(() => {
116+
return this.state_;
117+
});
138118
});
139119
}
140120

src/cloud/install/installer.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ require('../social/monkey/process');
55
import arraybuffers = require('../../arraybuffers/arraybuffers');
66
import linefeeder = require('../../net/linefeeder');
77
import logging = require('../../logging/logging');
8+
import Pinger = require('../../net/pinger');
89
import queue = require('../../handler/queue');
910

1011
// https://github.com/borisyankov/DefinitelyTyped/blob/master/ssh2/ssh2-tests.ts
@@ -143,6 +144,12 @@ class CloudInstaller {
143144
message: 'connection close without invitation URL'
144145
});
145146
}).connect(connectConfig);
147+
}).then((invite: any) => {
148+
// It can take several seconds before the SSH server running
149+
// in the new Docker container becomes active.
150+
return new Pinger(host, 5000).ping().then(() => {
151+
return invite;
152+
});
146153
});
147154
}
148155
}

src/cloud/social/provider.ts

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import arraybuffers = require('../../arraybuffers/arraybuffers');
66
import crypto = require('crypto');
77
import linefeeder = require('../../net/linefeeder');
88
import logging = require('../../logging/logging');
9-
import promises = require('../../promises/promises');
109
import queue = require('../../handler/queue');
1110

1211
// https://github.com/borisyankov/DefinitelyTyped/blob/master/ssh2/ssh2-tests.ts
@@ -209,24 +208,15 @@ export class CloudSocialProvider {
209208
});
210209
}
211210

212-
let numAttempts = 0;
213-
const connect = () => {
214-
log.debug('connection attempt %1...', (++numAttempts));
215-
const connection = new Connection(invite, (message: Object) => {
216-
this.dispatchEvent_('onMessage', {
217-
from: makeClientState(invite.host),
218-
// SIGNAL_FROM_SERVER_PEER,
219-
message: JSON.stringify(makeVersionedPeerMessage(3002, message))
220-
});
221-
});
222-
return connection.connect().then(() => {
223-
return connection;
211+
const connection = new Connection(invite, (message: Object) => {
212+
this.dispatchEvent_('onMessage', {
213+
from: makeClientState(invite.host),
214+
// SIGNAL_FROM_SERVER_PEER,
215+
message: JSON.stringify(makeVersionedPeerMessage(3002, message))
224216
});
225-
};
226-
227-
this.clients_[invite.host] = promises.retryWithExponentialBackoff(connect,
228-
MAX_CONNECTION_INTERVAL_MS, INITIAL_CONNECTION_INTERVAL_MS).then(
229-
(connection:Connection) => {
217+
});
218+
219+
this.clients_[invite.host] = connection.connect().then(() => {
230220
log.info('connected to zork on %1', invite.host);
231221

232222
// Fetch the banner, if available, then emit an instance message.
@@ -289,7 +279,9 @@ export class CloudSocialProvider {
289279
log.debug('saved contacts');
290280
}).catch((e) => {
291281
log.error('could not save contacts: %1', e);
292-
Promise.reject(e);
282+
Promise.reject({
283+
message: e.message
284+
});
293285
});
294286
}
295287

@@ -322,7 +314,9 @@ export class CloudSocialProvider {
322314
// the instance (safe because all we've done is run ping).
323315
log.info('new proxying session %1', payload.proxyingId);
324316
if (!(destinationClientId in this.savedContacts_)) {
325-
return Promise.reject(new Error('unknown client ' + destinationClientId));
317+
return Promise.reject({
318+
message: 'unknown client ' + destinationClientId
319+
});
326320
}
327321
return this.reconnect_(this.savedContacts_[destinationClientId].invite).then(
328322
(connection: Connection) => {
@@ -335,30 +329,39 @@ export class CloudSocialProvider {
335329
connection.sendMessage(JSON.stringify(payload));
336330
});
337331
} else {
338-
return Promise.reject(new Error('unknown client ' + destinationClientId));
332+
return Promise.reject({
333+
message: 'unknown client ' + destinationClientId
334+
});
339335
}
340336
}
341337
} else {
342-
return Promise.reject(new Error('message has no or wrong type field'));
338+
return Promise.reject({
339+
message: 'message has no or wrong type field'
340+
});
343341
}
344342
} catch (e) {
345-
return Promise.reject(new Error('could not de-serialise message: ' + e.message));
343+
return Promise.reject({
344+
message: 'could not de-serialise message: ' + e.message
345+
});
346346
}
347347
}
348348

349349
public clearCachedCredentials = (): Promise<void> => {
350-
return Promise.reject(
351-
new Error('clearCachedCredentials unimplemented'));
350+
return Promise.reject({
351+
message: 'clearCachedCredentials unimplemented'
352+
});
352353
}
353354

354355
public getUsers = (): Promise<freedom.Social.Users> => {
355-
return Promise.reject(
356-
new Error('getUsers unimplemented'));
356+
return Promise.reject({
357+
message: 'getUsers unimplemented'
358+
});
357359
}
358360

359361
public getClients = (): Promise<freedom.Social.Clients> => {
360-
return Promise.reject(
361-
new Error('getClients unimplemented'));
362+
return Promise.reject({
363+
message: 'getClients unimplemented'
364+
});
362365
}
363366

364367
public logout = (): Promise<void> => {
@@ -391,24 +394,35 @@ export class CloudSocialProvider {
391394
});
392395
}
393396

394-
// Parses an invite code, received from uProxy in JSON format.
395-
// This is the networkData field of the invite codes distributed
396-
// to uProxy users.
397+
// Parses the networkData field, serialised to JSON, of invites.
398+
// The contact is immediately saved and added to the contacts list.
397399
public acceptUserInvitation = (inviteJson: string): Promise<void> => {
398400
log.debug('acceptUserInvitation');
399401
try {
400-
var invite = <Invite>JSON.parse(inviteJson);
401-
return this.reconnect_(invite).then((connection: Connection) => {
402-
// Return nothing for type checking purposes.
402+
const invite = <Invite>JSON.parse(inviteJson);
403+
404+
this.notifyOfUser_(invite);
405+
this.savedContacts_[invite.host] = {
406+
invite: invite
407+
};
408+
this.saveContacts_();
409+
410+
// Connect in the background in order to fetch metadata such as
411+
// the banner (description).
412+
this.reconnect_(invite).catch((e: Error) => {
413+
log.warn('failed to log into cloud server during invite accept: %1', e.message);
403414
});
415+
416+
return Promise.resolve<void>();
404417
} catch (e) {
405418
return Promise.reject(new Error('could not parse invite code: ' + e.message));
406419
}
407420
}
408421

409422
public blockUser = (userId: string): Promise<void> => {
410-
return Promise.reject(
411-
new Error('blockUser unimplemented'));
423+
return Promise.reject({
424+
message: 'blockUser unimplemented'
425+
});
412426
}
413427

414428
// Removes a cloud contact from storage
@@ -599,7 +613,9 @@ class Connection {
599613
private exec_ = (command: string): Promise<string> => {
600614
log.debug('%1: execute command: %2', this.name_, command);
601615
if (this.state_ !== ConnectionState.ESTABLISHED) {
602-
return Promise.reject(new Error('can only execute commands in ESTABLISHED state'));
616+
return Promise.reject({
617+
message: 'can only execute commands in ESTABLISHED state'
618+
});
603619
}
604620
return new Promise<string>((F, R) => {
605621
this.connection_.exec(command, (e: Error, stream: ssh2.Channel) => {

src/net/pinger.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/// <reference path='../../../third_party/typings/browser.d.ts' />
2+
3+
import logging = require('../logging/logging');
4+
import promises = require('../promises/promises');
5+
6+
declare const freedom: freedom.FreedomInModuleEnv;
7+
8+
var log: logging.Log = new logging.Log('pinger');
9+
10+
const DEFAULT_TIMEOUT_SECS = 60;
11+
const DEFAULT_INTERVAL_MS = 1000;
12+
13+
// "Pings" - in an nmap sense - a port until a TCP connection can be established.
14+
class Pinger {
15+
constructor(
16+
private host_: string,
17+
private port_: number,
18+
private timeout_= DEFAULT_TIMEOUT_SECS) { }
19+
20+
// Resolves once a connection has been established, rejecting if
21+
// no connection can be made within the timeout.
22+
public ping = (): Promise<void> => {
23+
log.debug('pinging %1:%2...', this.host_, this.port_);
24+
25+
return promises.retry(() => {
26+
const socket = freedom['core.tcpsocket']();
27+
28+
const destructor = () => {
29+
try {
30+
freedom['core.tcpsocket'].close(socket);
31+
} catch (e) {
32+
console.warn('error destroying socket: ' + e.message);
33+
}
34+
};
35+
36+
// TODO: Worth thinking about timeouts here but because this times
37+
// out almost immediately if nothing is listening on the port,
38+
// it works well for our purposes.
39+
return socket.connect(this.host_, this.port_).then((unused: any) => {
40+
log.debug('connected to %1:%2', this.host_, this.port_);
41+
destructor();
42+
}, (e: Error) => {
43+
log.debug('connection failed to %1:%2', this.host_, this.port_);
44+
destructor();
45+
throw e;
46+
});
47+
}, this.timeout_, DEFAULT_INTERVAL_MS);
48+
}
49+
}
50+
51+
export = Pinger;

0 commit comments

Comments
 (0)