@@ -26,6 +26,9 @@ const STORAGE_KEY = 'cloud-social-contacts';
2626const ADMIN_USERNAME = 'giver' ;
2727const 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