Skip to content

Commit 95d04ae

Browse files
committed
Merge pull request #350 from uProxy/trevj-cloud-social-polish
polish for cloud social provider SSH use
2 parents 0c1d8d9 + 16e2cdc commit 95d04ae

File tree

1 file changed

+63
-59
lines changed

1 file changed

+63
-59
lines changed

src/cloud/social/provider.ts

Lines changed: 63 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ const STORAGE_KEY = 'cloud-social-contacts';
2626
const ADMIN_USERNAME = 'giver';
2727
const REGULAR_USERNAME = 'getter';
2828

29+
// Timeout for establishing an SSH connection.
30+
const CONNECT_TIMEOUT_MS = 10000;
31+
2932
// Credentials for accessing a cloud instance.
3033
// The serialised, base64 form is distributed amongst users.
3134
// TODO: add (private) keys, for key-based auth
@@ -200,7 +203,7 @@ export class CloudSocialProvider {
200203
if (invite.host in this.clients_) {
201204
log.debug('closing old connection to %1', invite.host);
202205
this.clients_[invite.host].then((connection: Connection) => {
203-
connection.close();
206+
connection.end();
204207
});
205208
}
206209

@@ -350,7 +353,7 @@ export class CloudSocialProvider {
350353
log.debug('logout');
351354
for (let address in this.clients_) {
352355
this.clients_[address].then((connection: Connection) => {
353-
connection.close();
356+
connection.end();
354357
});
355358
}
356359
return Promise.resolve<void>();
@@ -367,11 +370,15 @@ export class CloudSocialProvider {
367370
public inviteUser = (clientId: string): Promise<Object> => {
368371
log.debug('inviteUser');
369372
if (!(clientId in this.savedContacts_)) {
370-
return Promise.reject(new Error('unknown cloud instance ' + clientId));
373+
return Promise.reject({
374+
message: 'unknown cloud instance ' + clientId
375+
});
371376
}
372377
if (this.savedContacts_[clientId].invite.user !== ADMIN_USERNAME) {
373-
return Promise.reject(new Error('user is logged in as non-admin user ' +
374-
this.savedContacts_[clientId].invite.user));
378+
return Promise.reject({
379+
message: 'user is logged in as non-admin user ' +
380+
this.savedContacts_[clientId].invite.user
381+
});
375382
}
376383
return this.reconnect_(this.savedContacts_[clientId].invite).then(
377384
(connection: Connection) => {
@@ -423,10 +430,10 @@ class Connection {
423430

424431
private state_ = ConnectionState.NEW;
425432

426-
private client_ = new Client();
433+
private connection_ = new Client();
427434

428435
// The tunneled connection, i.e. secure link to Zork.
429-
private stream_ :ssh2.Channel;
436+
private tunnel_ :ssh2.Channel;
430437

431438
constructor(
432439
private invite_: Invite,
@@ -436,14 +443,17 @@ class Connection {
436443
// TODO: timeout
437444
public connect = (): Promise<void> => {
438445
if (this.state_ !== ConnectionState.NEW) {
439-
return Promise.reject(new Error('can only connect in NEW state'));
446+
return Promise.reject({
447+
message: 'can only connect in NEW state'
448+
});
440449
}
441450
this.state_ = ConnectionState.CONNECTING;
442451

443452
let connectConfig: ssh2.ConnectConfig = {
444453
host: this.invite_.host,
445454
port: SSH_SERVER_PORT,
446455
username: this.invite_.user,
456+
readyTimeout: CONNECT_TIMEOUT_MS,
447457
// Remaining fields only for type-correctness.
448458
tryKeyboard: false,
449459
debug: undefined
@@ -457,20 +467,22 @@ class Connection {
457467
}
458468

459469
return new Promise<void>((F, R) => {
460-
this.client_.on('ready', () => {
470+
this.connection_.on('ready', () => {
471+
// TODO: set a timeout here, too
461472
this.setState_(ConnectionState.ESTABLISHING_TUNNEL);
462-
this.client_.forwardOut(
473+
this.connection_.forwardOut(
463474
// TODO: since we communicate using the stream, what does this mean?
464475
'127.0.0.1', 0,
465-
ZORK_HOST, ZORK_PORT, (e: Error, stream: ssh2.Channel) => {
476+
ZORK_HOST, ZORK_PORT, (e: Error, tunnel: ssh2.Channel) => {
466477
if (e) {
467-
this.close();
468-
R(new Error('error establishing tunnel: ' + e.toString()));
478+
this.end();
479+
R({
480+
message: 'error establishing tunnel: ' + e.message
481+
});
469482
return;
470483
}
471-
this.setState_(ConnectionState.WAITING_FOR_PING);
472484

473-
this.stream_ = stream;
485+
this.setState_(ConnectionState.WAITING_FOR_PING);
474486

475487
var bufferQueue = new queue.Queue<ArrayBuffer, void>();
476488
new linefeeder.LineFeeder(bufferQueue).setSyncHandler((reply: string) => {
@@ -481,9 +493,10 @@ class Connection {
481493
this.setState_(ConnectionState.ESTABLISHED);
482494
F();
483495
} else {
484-
this.close();
485-
R(new Error('did not receive ping from server on login: ' +
486-
reply));
496+
this.end();
497+
R({
498+
message: 'did not receive ping from server on login: ' + reply
499+
});
487500
}
488501
break;
489502
case ConnectionState.ESTABLISHED:
@@ -497,50 +510,36 @@ class Connection {
497510
default:
498511
log.warn('%1: did not expect message in state %2: %3',
499512
this.name_, ConnectionState[this.state_], reply);
500-
this.close();
513+
this.end();
501514
}
502515
});
503516

504-
// TODO: add error handler for stream
505-
stream.on('data', (buffer: Buffer) => {
517+
this.tunnel_ = tunnel;
518+
tunnel.on('data', (buffer: Buffer) => {
506519
bufferQueue.handle(arraybuffers.bufferToArrayBuffer(buffer));
507-
}).on('error', (e: Error) => {
508-
// This occurs when:
509-
// - host cannot be reached, e.g. non-existant hostname
510-
// TODO: does this occur outside of startup, i.e. should it always reject?
511-
log.warn('%1: tunnel error: %2', this.name_, e);
512-
this.close();
513-
R(new Error('could not establish tunnel: ' + e.toString()));
514520
}).on('end', () => {
515-
// Occurs when the stream is "over" for any reason, including
516-
// failed connection.
517521
log.debug('%1: tunnel end', this.name_);
518-
this.close();
519522
}).on('close', (hadError: boolean) => {
520-
// TODO: when does this occur? don't see it on normal close or failure
521-
log.debug('%1: tunnel close: %2', this.name_, hadError);
522-
this.close();
523+
log.debug('%1: tunnel close, with%2 error', this.name_, (hadError ? '' : 'out'));
523524
});
524525

525-
stream.write('ping\n');
526+
tunnel.write('ping\n');
526527
});
527528
}).on('error', (e: Error) => {
528529
// This occurs when:
529530
// - user supplies the wrong username or password
530531
// - host cannot be reached, e.g. non-existant hostname
531-
// TODO: does this occur outside of startup, i.e. should it always reject?
532532
log.warn('%1: connection error: %2', this.name_, e);
533-
this.close();
534-
R(new Error('could not login: ' + e.toString()));
533+
this.setState_(ConnectionState.TERMINATED);
534+
R({
535+
message: 'could not login: ' + e.message
536+
});
535537
}).on('end', () => {
536-
// Occurs when the connection is "over" for any reason, including
537-
// failed connection.
538-
log.debug('%1: connection ended', this.name_);
539-
this.close();
538+
log.debug('%1: connection end', this.name_);
539+
this.setState_(ConnectionState.TERMINATED);
540540
}).on('close', (hadError: boolean) => {
541-
// TODO: when does this occur? don't see it on normal close or failure
542-
log.debug('%1: connection close: %2', this.name_, hadError);
543-
this.close();
541+
log.debug('%1: connection close, with%2 error', this.name_, (hadError ? '' : 'out'));
542+
this.setState_(ConnectionState.TERMINATED);
544543
}).connect(connectConfig);
545544
});
546545
}
@@ -549,17 +548,14 @@ class Connection {
549548
if (this.state_ !== ConnectionState.ESTABLISHED) {
550549
throw new Error('can only connect in ESTABLISHED state');
551550
}
552-
this.stream_.write(s + '\n');
551+
this.tunnel_.write(s + '\n');
553552
}
554553

555-
public close = (): void => {
554+
public end = (): void => {
556555
log.debug('%1: close', this.name_);
557-
if (this.state_ === ConnectionState.TERMINATED) {
558-
log.debug('%1: already closed', this.name_);
559-
} else {
556+
if (this.state_ !== ConnectionState.TERMINATED) {
560557
this.setState_(ConnectionState.TERMINATED);
561-
this.client_.end();
562-
// TODO: what about the stream?
558+
this.connection_.end();
563559
}
564560
}
565561

@@ -578,21 +574,29 @@ class Connection {
578574
// Executes a command, fulfilling with the command's stdout
579575
// or rejecting if output is received on stderr.
580576
private exec_ = (command:string): Promise<string> => {
577+
log.debug('%1: execute command: %2', this.name_, command);
581578
if (this.state_ !== ConnectionState.ESTABLISHED) {
582579
return Promise.reject(new Error('can only execute commands in ESTABLISHED state'));
583580
}
584581
return new Promise<string>((F, R) => {
585-
this.client_.exec(command, (e: Error, stream: ssh2.Channel) => {
582+
this.connection_.exec(command, (e: Error, stream: ssh2.Channel) => {
586583
if (e) {
587-
R(e);
584+
R({
585+
message: 'failed to execute command: ' + e.message
586+
});
588587
return;
589588
}
590-
stream.on('data', function(data: Buffer) {
589+
590+
// TODO: There is a close event with a return code which
591+
// is probably a better indication of success.
592+
stream.on('data', (data: Buffer) => {
591593
F(data.toString());
592-
}).stderr.on('data', function(data: Buffer) {
593-
R(new Error(data.toString()));
594-
}).on('close', function(code: any, signal: any) {
595-
log.debug('exec stream closed');
594+
}).stderr.on('data', (data: Buffer) => {
595+
R({
596+
message: 'command output to STDERR: ' + data.toString()
597+
});
598+
}).on('end', () => {
599+
log.debug('%1: exec stream end', this.name_);
596600
});
597601
});
598602
});

0 commit comments

Comments
 (0)