var Client = require('../lib/client');
var Server = require('../lib/server');
var OPEN_MODE = require('ssh2-streams').SFTPStream.OPEN_MODE;
var STATUS_CODE = require('ssh2-streams').SFTPStream.STATUS_CODE;
var utils = require('ssh2-streams').utils;

var net = require('net');
var fs = require('fs');
var crypto = require('crypto');
var path = require('path');
var join = path.join;
var inspect = require('util').inspect;
var assert = require('assert');

var t = -1;
var group = path.basename(__filename, '.js') + '/';
var fixturesdir = join(__dirname, 'fixtures');

var USER = 'nodejs';
var PASSWORD = 'FLUXCAPACITORISTHEPOWER';
var MD5_HOST_FINGERPRINT = '64254520742d3d0792e918f3ce945a64';
var KEY_RSA_BAD = fs.readFileSync(join(fixturesdir, 'bad_rsa_private_key'));
var HOST_KEY_RSA = fs.readFileSync(join(fixturesdir, 'ssh_host_rsa_key'));
var HOST_KEY_DSA = fs.readFileSync(join(fixturesdir, 'ssh_host_dsa_key'));
var HOST_KEY_ECDSA = fs.readFileSync(join(fixturesdir, 'ssh_host_ecdsa_key'));
var CLIENT_KEY_ENC_RSA_RAW = fs.readFileSync(join(fixturesdir, 'id_rsa_enc'));
var CLIENT_KEY_ENC_RSA = utils.parseKey(CLIENT_KEY_ENC_RSA_RAW, 'foobarbaz');
var CLIENT_KEY_PPK_RSA_RAW = fs.readFileSync(join(fixturesdir, 'id_rsa.ppk'));
var CLIENT_KEY_PPK_RSA = utils.parseKey(CLIENT_KEY_PPK_RSA_RAW);
var CLIENT_KEY_RSA_RAW = fs.readFileSync(join(fixturesdir, 'id_rsa'));
var CLIENT_KEY_RSA = utils.parseKey(CLIENT_KEY_RSA_RAW);
var CLIENT_KEY_RSA_NEW_RAW =
    fs.readFileSync(join(fixturesdir, 'openssh_new_rsa'));
var CLIENT_KEY_RSA_NEW = utils.parseKey(CLIENT_KEY_RSA_NEW_RAW)[0];
var CLIENT_KEY_DSA_RAW = fs.readFileSync(join(fixturesdir, 'id_dsa'));
var CLIENT_KEY_DSA = utils.parseKey(CLIENT_KEY_DSA_RAW);
var CLIENT_KEY_ECDSA_RAW = fs.readFileSync(join(fixturesdir, 'id_ecdsa'));
var CLIENT_KEY_ECDSA = utils.parseKey(CLIENT_KEY_ECDSA_RAW);
var DEBUG = false;
var DEFAULT_TEST_TIMEOUT = 30 * 1000;

var tests = [
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          privateKey: CLIENT_KEY_RSA_RAW
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'publickey',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.key.algo === 'ssh-rsa',
                 makeMsg('Unexpected key algo: ' + ctx.key.algo));
          assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(),
                           ctx.key.data,
                           makeMsg('Public key mismatch'));
          if (ctx.signature) {
            assert(CLIENT_KEY_RSA.verify(ctx.blob, ctx.signature) === true,
                   makeMsg('Could not verify PK signature'));
            ctx.accept();
          } else
            ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Authenticate with an RSA key (old OpenSSH)'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          privateKey: CLIENT_KEY_RSA_NEW_RAW
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'publickey',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.key.algo === 'ssh-rsa',
                 makeMsg('Unexpected key algo: ' + ctx.key.algo));
          assert.deepEqual(CLIENT_KEY_RSA_NEW.getPublicSSH(),
                           ctx.key.data,
                           makeMsg('Public key mismatch'));
          if (ctx.signature) {
            assert(CLIENT_KEY_RSA_NEW.verify(ctx.blob, ctx.signature) === true,
                   makeMsg('Could not verify PK signature'));
            ctx.accept();
          } else
            ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Authenticate with an RSA key (new OpenSSH)'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          privateKey: CLIENT_KEY_ENC_RSA_RAW,
          passphrase: 'foobarbaz',
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'publickey',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.key.algo === 'ssh-rsa',
                 makeMsg('Unexpected key algo: ' + ctx.key.algo));
          assert.deepEqual(CLIENT_KEY_ENC_RSA.getPublicSSH(),
                           ctx.key.data,
                           makeMsg('Public key mismatch'));
          if (ctx.signature) {
            assert(CLIENT_KEY_ENC_RSA.verify(ctx.blob, ctx.signature) === true,
                   makeMsg('Could not verify PK signature'));
            ctx.accept();
          } else
            ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Authenticate with an encrypted RSA key'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          privateKey: CLIENT_KEY_PPK_RSA_RAW
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'publickey',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.key.algo === 'ssh-rsa',
                 makeMsg('Unexpected key algo: ' + ctx.key.algo));
          if (ctx.signature) {
            assert(CLIENT_KEY_PPK_RSA.verify(ctx.blob, ctx.signature) === true,
                   makeMsg('Could not verify PK signature'));
            ctx.accept();
          } else
            ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Authenticate with an RSA key (PPK)'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          privateKey: CLIENT_KEY_DSA_RAW
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'publickey',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.key.algo === 'ssh-dss',
                 makeMsg('Unexpected key algo: ' + ctx.key.algo));
          assert.deepEqual(CLIENT_KEY_DSA.getPublicSSH(),
                           ctx.key.data,
                           makeMsg('Public key mismatch'));
          if (ctx.signature) {
            assert(CLIENT_KEY_DSA.verify(ctx.blob, ctx.signature) === true,
                   makeMsg('Could not verify PK signature'));
            ctx.accept();
          } else
            ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Authenticate with a DSA key'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          privateKey: CLIENT_KEY_ECDSA_RAW
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'publickey',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.key.algo === 'ecdsa-sha2-nistp256',
                 makeMsg('Unexpected key algo: ' + ctx.key.algo));
          assert.deepEqual(CLIENT_KEY_ECDSA.getPublicSSH(),
                           ctx.key.data,
                           makeMsg('Public key mismatch'));
          if (ctx.signature) {
            assert(CLIENT_KEY_ECDSA.verify(ctx.blob, ctx.signature) === true,
                   makeMsg('Could not verify PK signature'));
            ctx.accept();
          } else
            ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Authenticate with a ECDSA key'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: 'asdf',
          algorithms: {
            serverHostKey: ['ssh-dss']
          }
        },
        { hostKeys: [HOST_KEY_DSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'password',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.password === 'asdf',
                 makeMsg('Unexpected password: ' + ctx.password));
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Server with DSA host key'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: 'asdf'
        },
        { hostKeys: [HOST_KEY_ECDSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'password',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.password === 'asdf',
                 makeMsg('Unexpected password: ' + ctx.password));
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Server with ECDSA host key'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: 'asdf',
          algorithms: {
            serverHostKey: 'ssh-rsa'
          }
        },
        { hostKeys: [HOST_KEY_RSA, HOST_KEY_DSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'password',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.password === 'asdf',
                 makeMsg('Unexpected password: ' + ctx.password));
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Server with multiple host keys (RSA selected)'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: 'asdf',
          algorithms: {
            serverHostKey: 'ssh-dss'
          }
        },
        { hostKeys: [HOST_KEY_RSA, HOST_KEY_DSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'password',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.password === 'asdf',
                 makeMsg('Unexpected password: ' + ctx.password));
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Server with multiple host keys (DSA selected)'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var hostname = 'foo';
      var username = 'bar';

      r = setup(
        this,
        { username: USER,
          privateKey: CLIENT_KEY_RSA_RAW,
          localHostname: hostname,
          localUsername: username
        },
        { hostKeys: [ HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method !== 'hostbased')
            return ctx.reject();
          assert(ctx.method === 'hostbased',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.key.algo === 'ssh-rsa',
                 makeMsg('Unexpected key algo: ' + ctx.key.algo));
          assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(),
                           ctx.key.data,
                           makeMsg('Public key mismatch'));
          assert(ctx.signature,
                 makeMsg('Expected signature'));
          assert(ctx.localHostname === hostname,
                 makeMsg('Wrong local hostname'));
          assert(ctx.localUsername === username,
                 makeMsg('Wrong local username'));
          assert(CLIENT_KEY_RSA.verify(ctx.blob, ctx.signature) === true,
                 makeMsg('Could not verify hostbased signature'));
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Authenticate with hostbased'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'password',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.password === PASSWORD,
                 makeMsg('Unexpected password: ' + ctx.password));
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Authenticate with a password'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var calls = 0;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD,
          privateKey: CLIENT_KEY_RSA_RAW,
          authHandler: function(methodsLeft, partial, cb) {
            assert(calls++ === 0, makeMsg('authHandler called multiple times'));
            assert(methodsLeft === null, makeMsg('expected null methodsLeft'));
            assert(partial === null, makeMsg('expected null partial'));
            return 'none';
          }
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      var attempts = 0;
      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          assert(++attempts === 1, makeMsg('too many auth attempts'));
          assert(ctx.method === 'none',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Custom authentication order (sync)'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var calls = 0;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD,
          privateKey: CLIENT_KEY_RSA_RAW,
          authHandler: function(methodsLeft, partial, cb) {
            assert(calls++ === 0, makeMsg('authHandler called multiple times'));
            assert(methodsLeft === null, makeMsg('expected null methodsLeft'));
            assert(partial === null, makeMsg('expected null partial'));
            process.nextTick(cb, 'none');
          }
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      var attempts = 0;
      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          assert(++attempts === 1, makeMsg('too many auth attempts'));
          assert(ctx.method === 'none',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Custom authentication order (async)'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var cliError;
      var calls = 0;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD,
          privateKey: CLIENT_KEY_RSA_RAW,
          authHandler: function(methodsLeft, partial, cb) {
            assert(calls++ === 0, makeMsg('authHandler called multiple times'));
            assert(methodsLeft === null, makeMsg('expected null methodsLeft'));
            assert(partial === null, makeMsg('expected null partial'));
            return false;
          }
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      // Remove default client error handler added by `setup()` since we are
      // expecting an error in this case
      client.removeAllListeners('error');

      client.on('error', function(err) {
        cliError = err;
        assert.strictEqual(err.level, 'client-authentication');
        assert(/configured authentication methods failed/i.test(err.message),
               makeMsg('Wrong error message'));
      }).on('close', function() {
        assert(cliError, makeMsg('Expected client error'));
      });

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          assert(false, makeMsg('should not see auth attempt'));
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Custom authentication order (no methods)'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var calls = 0;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD,
          privateKey: CLIENT_KEY_RSA_RAW,
          authHandler: function(methodsLeft, partial, cb) {
            switch (calls++) {
              case 0:
                assert(methodsLeft === null,
                       makeMsg('expected null methodsLeft'));
                assert(partial === null, makeMsg('expected null partial'));
                return 'publickey';
              case 1:
                assert.deepStrictEqual(methodsLeft,
                                       ['password'],
                                       makeMsg('expected password method left'
                                               + ', saw: ' + methodsLeft));
                assert(partial === true, makeMsg('expected partial success'));
                return 'password';
              default:
                assert(false, makeMsg('authHandler called too many times'));
            }
          }
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      var attempts = 0;
      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          assert(++attempts === calls,
                 makeMsg('server<->client state mismatch'));
          switch (calls) {
            case 1:
              assert(ctx.method === 'publickey',
                     makeMsg('Unexpected auth method: ' + ctx.method));
              assert(ctx.username === USER,
                     makeMsg('Unexpected username: ' + ctx.username));
              assert(ctx.key.algo === 'ssh-rsa',
                     makeMsg('Unexpected key algo: ' + ctx.key.algo));
              assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(),
                               ctx.key.data,
                               makeMsg('Public key mismatch'));
              ctx.reject(['password'], true);
              break;
            case 2:
              assert(ctx.method === 'password',
                     makeMsg('Unexpected auth method: ' + ctx.method));
              assert(ctx.username === USER,
                     makeMsg('Unexpected username: ' + ctx.username));
              assert(ctx.password === PASSWORD,
                     makeMsg('Unexpected password: ' + ctx.password));
              ctx.accept();
              break;
            default:
              assert(false, makeMsg('bad client auth state'));
          }
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Custom authentication order (multi-step)'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var verified = false;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD,
          hostHash: 'md5',
          hostVerifier: function(hash) {
            assert(hash === MD5_HOST_FINGERPRINT,
                   makeMsg('Host fingerprint mismatch'));
            return (verified = true);
          }
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      }).on('close', function() {
        assert(verified, makeMsg('Failed to verify host fingerprint'));
      });
    },
    what: 'Verify host fingerprint'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var out = '';
      var outErr = '';
      var exitArgs;
      var closeArgs;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.once('session', function(accept, reject) {
            var session = accept();
            session.once('exec', function(accept, reject, info) {
              assert(info.command === 'foo --bar',
                     makeMsg('Wrong exec command: ' + info.command));
              var stream = accept();
              stream.stderr.write('stderr data!\n');
              stream.write('stdout data!\n');
              stream.exit(100);
              stream.end();
              conn.end();
            });
          });
        });
      });
      client.on('ready', function() {
        client.exec('foo --bar', function(err, stream) {
          assert(!err, makeMsg('Unexpected exec error: ' + err));
          stream.on('data', function(d) {
            out += d;
          }).on('exit', function(code) {
            exitArgs = new Array(arguments.length);
            for (var i = 0; i < exitArgs.length; ++i)
              exitArgs[i] = arguments[i];
          }).on('close', function(code) {
            closeArgs = new Array(arguments.length);
            for (var i = 0; i < closeArgs.length; ++i)
              closeArgs[i] = arguments[i];
          }).stderr.on('data', function(d) {
            outErr += d;
          });
        });
      }).on('end', function() {
        assert.deepEqual(exitArgs,
                         [100],
                         makeMsg('Wrong exit args: ' + inspect(exitArgs)));
        assert.deepEqual(closeArgs,
                         [100],
                         makeMsg('Wrong close args: ' + inspect(closeArgs)));
        assert(out === 'stdout data!\n',
               makeMsg('Wrong stdout data: ' + inspect(out)));
        assert(outErr === 'stderr data!\n',
               makeMsg('Wrong stderr data: ' + inspect(outErr)));
      });
    },
    what: 'Simple exec'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var serverEnv = {};
      var clientEnv = { SSH2NODETEST: 'foo' };

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.once('session', function(accept, reject) {
            var session = accept();
            session.once('env', function(accept, reject, info) {
              serverEnv[info.key] = info.val;
              accept && accept();
            }).once('exec', function(accept, reject, info) {
              assert(info.command === 'foo --bar',
                     makeMsg('Wrong exec command: ' + info.command));
              var stream = accept();
              stream.exit(100);
              stream.end();
              conn.end();
            });
          });
        });
      });
      client.on('ready', function() {
        client.exec('foo --bar',
                    { env: clientEnv },
                    function(err, stream) {
          assert(!err, makeMsg('Unexpected exec error: ' + err));
          stream.resume();
        });
      }).on('end', function() {
        assert.deepEqual(serverEnv, clientEnv,
                         makeMsg('Environment mismatch'));
      });
    },
    what: 'Exec with environment set'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var out = '';

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.once('session', function(accept, reject) {
            var session = accept();
            var ptyInfo;
            session.once('pty', function(accept, reject, info) {
              ptyInfo = info;
              accept && accept();
            }).once('exec', function(accept, reject, info) {
              assert(info.command === 'foo --bar',
                     makeMsg('Wrong exec command: ' + info.command));
              var stream = accept();
              stream.write(JSON.stringify(ptyInfo));
              stream.exit(100);
              stream.end();
              conn.end();
            });
          });
        });
      });
      var pty = {
        rows: 2,
        cols: 4,
        width: 0,
        height: 0,
        term: 'vt220',
        modes: {}
      };
      client.on('ready', function() {
        client.exec('foo --bar',
                    { pty: pty },
                    function(err, stream) {
          assert(!err, makeMsg('Unexpected exec error: ' + err));
          stream.on('data', function(d) {
            out += d;
          });
        });
      }).on('end', function() {
        assert.deepEqual(JSON.parse(out),
                         pty,
                         makeMsg('Wrong stdout data: ' + inspect(out)));
      });
    },
    what: 'Exec with pty set'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var out = '';

      r = setup(
        this,
        { username: USER,
          password: PASSWORD,
          agent: '/foo/bar/baz'
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.once('session', function(accept, reject) {
            var session = accept();
            var authAgentReq = false;
            session.once('auth-agent', function(accept, reject) {
              authAgentReq = true;
              accept && accept();
            }).once('exec', function(accept, reject, info) {
              assert(info.command === 'foo --bar',
                     makeMsg('Wrong exec command: ' + info.command));
              var stream = accept();
              stream.write(inspect(authAgentReq));
              stream.exit(100);
              stream.end();
              conn.end();
            });
          });
        });
      });
      client.on('ready', function() {
        client.exec('foo --bar',
                    { agentForward: true },
                    function(err, stream) {
          assert(!err, makeMsg('Unexpected exec error: ' + err));
          stream.on('data', function(d) {
            out += d;
          });
        });
      }).on('end', function() {
        assert(out === 'true',
               makeMsg('Wrong stdout data: ' + inspect(out)));
      });
    },
    what: 'Exec with OpenSSH agent forwarding'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var out = '';

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.once('session', function(accept, reject) {
            var session = accept();
            var x11 = false;
            session.once('x11', function(accept, reject, info) {
              assert.strictEqual(info.single,
                                 false,
                                 makeMsg('Wrong client x11.single: '
                                         + info.single));
              assert.strictEqual(info.screen,
                                 0,
                                 makeMsg('Wrong client x11.screen: '
                                         + info.screen));
              assert.strictEqual(info.protocol,
                                 'MIT-MAGIC-COOKIE-1',
                                 makeMsg('Wrong client x11.protocol: '
                                         + info.protocol));
              assert.strictEqual(info.cookie.length,
                                 32,
                                 makeMsg('Invalid client x11.cookie: '
                                         + info.cookie));
              x11 = true;
              accept && accept();
            }).once('exec', function(accept, reject, info) {
              assert(info.command === 'foo --bar',
                     makeMsg('Wrong exec command: ' + info.command));
              var stream = accept();
              conn.x11('127.0.0.1', 4321, function(err, xstream) {
                assert(!err, makeMsg('Unexpected x11() error: ' + err));
                xstream.resume();
                xstream.on('end', function() {
                  stream.write(JSON.stringify(x11));
                  stream.exit(100);
                  stream.end();
                  conn.end();
                }).end();
              });
            });
          });
        });
      });
      client.on('ready', function() {
        client.on('x11', function(info, accept, reject) {
          assert.strictEqual(info.srcIP,
                             '127.0.0.1',
                             makeMsg('Invalid server x11.srcIP: '
                                     + info.srcIP));
          assert.strictEqual(info.srcPort,
                             4321,
                             makeMsg('Invalid server x11.srcPort: '
                                     + info.srcPort));
          accept();
        }).exec('foo --bar',
                    { x11: true },
                    function(err, stream) {
          assert(!err, makeMsg('Unexpected exec error: ' + err));
          stream.on('data', function(d) {
            out += d;
          });
        });
      }).on('end', function() {
        assert(out === 'true',
               makeMsg('Wrong stdout data: ' + inspect(out)));
      });
    },
    what: 'Exec with X11 forwarding'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var out = '';
      var x11ClientConfig = {
        single: true,
        screen: 1234,
        protocol: 'YUMMY-MAGIC-COOKIE-1',
        cookie: '00112233445566778899001122334455'
      };

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.once('session', function(accept, reject) {
            var session = accept();
            var x11 = false;
            session.once('x11', function(accept, reject, info) {
              assert.strictEqual(info.single,
                                 true,
                                 makeMsg('Wrong client x11.single: '
                                         + info.single));
              assert.strictEqual(info.screen,
                                 1234,
                                 makeMsg('Wrong client x11.screen: '
                                         + info.screen));
              assert.strictEqual(info.protocol,
                                 'YUMMY-MAGIC-COOKIE-1',
                                 makeMsg('Wrong client x11.protocol: '
                                         + info.protocol));
              assert.strictEqual(info.cookie,
                                 '00112233445566778899001122334455',
                                 makeMsg('Wrong client x11.cookie: '
                                         + info.cookie));
              x11 = info;
              accept && accept();
            }).once('exec', function(accept, reject, info) {
              assert(info.command === 'foo --bar',
                     makeMsg('Wrong exec command: ' + info.command));
              var stream = accept();
              conn.x11('127.0.0.1', 4321, function(err, xstream) {
                assert(!err, makeMsg('Unexpected x11() error: ' + err));
                xstream.resume();
                xstream.on('end', function() {
                  stream.write(JSON.stringify(x11));
                  stream.exit(100);
                  stream.end();
                  conn.end();
                }).end();
              });
            });
          });
        });
      });
      client.on('ready', function() {
        client.on('x11', function(info, accept, reject) {
          assert.strictEqual(info.srcIP,
                             '127.0.0.1',
                             makeMsg('Invalid server x11.srcIP: '
                                     + info.srcIP));
          assert.strictEqual(info.srcPort,
                             4321,
                             makeMsg('Invalid server x11.srcPort: '
                                     + info.srcPort));
          accept();
        }).exec('foo --bar',
                    { x11: x11ClientConfig },
                    function(err, stream) {
          assert(!err, makeMsg('Unexpected exec error: ' + err));
          stream.on('data', function(d) {
            out += d;
          });
        });
      }).on('end', function() {
        var result = JSON.parse(out);
        assert.deepStrictEqual(result,
                               x11ClientConfig,
                               makeMsg('Wrong stdout data: ' + result));
      });
    },
    what: 'Exec with X11 forwarding (custom X11 settings)'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var out = '';

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.once('session', function(accept, reject) {
            var session = accept();
            var sawPty = false;
            session.once('pty', function(accept, reject, info) {
              sawPty = true;
              accept && accept();
            }).once('shell', function(accept, reject) {
              var stream = accept();
              stream.write('Cowabunga dude! ' + inspect(sawPty));
              stream.end();
              conn.end();
            });
          });
        });
      });
      client.on('ready', function() {
        client.shell(function(err, stream) {
          assert(!err, makeMsg('Unexpected shell error: ' + err));
          stream.on('data', function(d) {
            out += d;
          });
        });
      }).on('end', function() {
        assert(out === 'Cowabunga dude! true',
               makeMsg('Wrong stdout data: ' + inspect(out)));
      });
    },
    what: 'Simple shell'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var serverEnv = {};
      var clientEnv = { SSH2NODETEST: 'foo' };
      var sawPty = false;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.once('session', function(accept, reject) {
            var session = accept();
            session.once('env', function(accept, reject, info) {
              serverEnv[info.key] = info.val;
              accept && accept();
            }).once('pty', function(accept, reject, info) {
              sawPty = true;
              accept && accept();
            }).once('shell', function(accept, reject) {
              var stream = accept();
              stream.end();
              conn.end();
            });
          });
        });
      });
      client.on('ready', function() {
        client.shell({ env: clientEnv }, function(err, stream) {
          assert(!err, makeMsg('Unexpected shell error: ' + err));
          stream.resume();
        });
      }).on('end', function() {
        assert.deepEqual(serverEnv, clientEnv,
                         makeMsg('Environment mismatch'));
        assert.strictEqual(sawPty, true);
      });
    },
    what: 'Shell with environment set'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var expHandle = Buffer.from([1, 2, 3, 4]);
      var sawOpenS = false;
      var sawCloseS = false;
      var sawOpenC = false;
      var sawCloseC = false;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.once('session', function(accept, reject) {
            var session = accept();
            session.once('sftp', function(accept, reject) {
              if (accept) {
                var sftp = accept();
                sftp.once('OPEN', function(id, filename, flags, attrs) {
                  assert(id === 0,
                         makeMsg('Unexpected sftp request ID: ' + id));
                  assert(filename === 'node.js',
                         makeMsg('Unexpected filename: ' + filename));
                  assert(flags === OPEN_MODE.READ,
                         makeMsg('Unexpected flags: ' + flags));
                  sawOpenS = true;
                  sftp.handle(id, expHandle);
                  sftp.once('CLOSE', function(id, handle) {
                    assert(id === 1,
                           makeMsg('Unexpected sftp request ID: ' + id));
                    assert.deepEqual(handle,
                                     expHandle,
                                     makeMsg('Wrong sftp file handle: '
                                             + inspect(handle)));
                    sawCloseS = true;
                    sftp.status(id, STATUS_CODE.OK);
                    conn.end();
                  });
                });
              }
            });
          });
        });
      });
      client.on('ready', function() {
        client.sftp(function(err, sftp) {
          assert(!err, makeMsg('Unexpected sftp error: ' + err));
          sftp.open('node.js', 'r', function(err, handle) {
            assert(!err, makeMsg('Unexpected sftp error: ' + err));
            assert.deepEqual(handle,
                             expHandle,
                             makeMsg('Wrong sftp file handle: '
                                     + inspect(handle)));
            sawOpenC = true;
            sftp.close(handle, function(err) {
              assert(!err, makeMsg('Unexpected sftp error: ' + err));
              sawCloseC = true;
            });
          });
        });
      }).on('end', function() {
        assert(sawOpenS, makeMsg('Expected sftp open()'));
        assert(sawOpenC, makeMsg('Expected sftp open() callback'));
        assert(sawCloseS, makeMsg('Expected sftp open()'));
        assert(sawOpenC, makeMsg('Expected sftp close() callback'));
      });
    },
    what: 'Simple SFTP'
  },
  { run: function() {
      var client;
      var server;
      var state = {
        readies: 0,
        closes: 0
      };
      var clientcfg = {
        username: USER,
        password: PASSWORD
      };
      var servercfg = {
        hostKeys: [HOST_KEY_RSA]
      };
      var reconnect = false;

      client = new Client(),
      server = new Server(servercfg);

      function onReady() {
        assert(++state.readies <= 4,
               makeMsg('Wrong ready count: ' + state.readies));
      }
      function onClose() {
        assert(++state.closes <= 3,
               makeMsg('Wrong close count: ' + state.closes));
        if (state.closes === 2)
          server.close();
        else if (state.closes === 3)
          next();
      }

      server.listen(0, 'localhost', function() {
        clientcfg.host = 'localhost';
        clientcfg.port = server.address().port;
        client.connect(clientcfg);
      });

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', onReady);
      }).on('close', onClose);
      client.on('ready', function() {
        onReady();
        if (reconnect)
          client.end();
        else {
          reconnect = true;
          client.connect(clientcfg);
        }
      }).on('close', onClose);
    },
    what: 'connect() on connected client'
  },
  { run: function() {
      var client = new Client({
        username: USER,
        password: PASSWORD
      });

      assert.throws(function() {
        client.exec('uptime', function(err, stream) {
          assert(false, makeMsg('Callback unexpectedly called'));
        });
      });
      next();
    },
    what: 'Throw when not connected'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var calledBack = 0;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        });
      });
      client.on('ready', function() {
        function callback(err, stream) {
          assert(err, makeMsg('Expected error'));
          assert(err.message === 'No response from server',
                 makeMsg('Wrong error message: ' + err.message));
          ++calledBack;
        }
        client.exec('uptime', callback);
        client.shell(callback);
        client.sftp(callback);
        client.end();
      }).on('close', function() {
        // give the callbacks a chance to execute
        process.nextTick(function() {
          assert(calledBack === 3,
                 makeMsg('Only '
                               + calledBack
                               + '/3 outstanding callbacks called'));
        });
      });
    },
    what: 'Outstanding callbacks called on disconnect'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var calledBack = 0;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.on('session', function(accept, reject) {
            var session = accept();
            session.once('exec', function(accept, reject, info) {
              var stream = accept();
              stream.exit(0);
              stream.end();
            });
          });
        });
      });
      client.on('ready', function() {
        function callback(err, stream) {
          assert(!err, makeMsg('Unexpected error: ' + err));
          stream.resume();
          if (++calledBack === 3)
            client.end();
        }
        client.exec('foo', callback);
        client.exec('bar', callback);
        client.exec('baz', callback);
      }).on('end', function() {
        assert(calledBack === 3,
               makeMsg('Only '
                             + calledBack
                             + '/3 callbacks called'));
      });
    },
    what: 'Pipelined requests'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var calledBack = 0;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          var reqs = [];
          conn.on('session', function(accept, reject) {
            if (reqs.length === 0) {
              conn.rekey(function(err) {
                assert(!err, makeMsg('Unexpected rekey error: ' + err));
                reqs.forEach(function(accept) {
                  var session = accept();
                  session.once('exec', function(accept, reject, info) {
                    var stream = accept();
                    stream.exit(0);
                    stream.end();
                  });
                });
              });
            }
            reqs.push(accept);
          });
        });
      });
      client.on('ready', function() {
        function callback(err, stream) {
          assert(!err, makeMsg('Unexpected error: ' + err));
          stream.resume();
          if (++calledBack === 3)
            client.end();
        }
        client.exec('foo', callback);
        client.exec('bar', callback);
        client.exec('baz', callback);
      }).on('end', function() {
        assert(calledBack === 3,
               makeMsg('Only '
                             + calledBack
                             + '/3 callbacks called'));
      });
    },
    what: 'Pipelined requests with intermediate rekeying'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.on('session', function(accept, reject) {
            var session = accept();
            session.once('exec', function(accept, reject, info) {
              var stream = accept();
              stream.exit(0);
              stream.end();
            });
          });
        });
      });
      client.on('ready', function() {
        client.exec('foo', function(err, stream) {
          assert(!err, makeMsg('Unexpected error: ' + err));
          stream.on('exit', function(code, signal) {
            client.end();
          });
        });
      });
    },
    what: 'Ignore outgoing after stream close'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.on('session', function(accept, reject) {
            accept().on('sftp', function(accept, reject) {
              var sftp = accept();
              // XXX: hack to get channel ...
              var channel = sftp._readableState.pipes;
              if (Array.isArray(channel))
                channel = channel[0];

              channel.unpipe(sftp);
              sftp.unpipe(channel);

              channel.exit(127);
              channel.close();
            });
          });
        });
      });
      client.on('ready', function() {
        var timeout = setTimeout(function() {
          assert(false, makeMsg('Unexpected SFTP timeout'));
        }, 1000);
        client.sftp(function(err, sftp) {
          clearTimeout(timeout);
          assert(err, makeMsg('Expected error'));
          assert(err.code === 127,
                 makeMsg('Expected exit code 127, saw: ' + err.code));
          client.end();
        });
      });
    },
    what: 'SFTP server aborts with exit-status'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD,
          sock: new net.Socket()
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {});
      });
      client.on('ready', function() {
        client.end();
      });
    },
    what: 'Double pipe on unconnected, passed in net.Socket'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        });
        conn.on('request', function(accept, reject, name, info) {
          accept();
          conn.forwardOut('good', 0, 'remote', 12345, function(err, ch) {
            if (err) {
              assert(!err, makeMsg('Unexpected error: ' + err));
            }
            conn.forwardOut('bad', 0, 'remote', 12345, function(err, ch) {
              assert(err, makeMsg('Should receive error'));
              client.end();
            });
          });
        });
      });

      client.on('ready', function() {
        // request forwarding
        client.forwardIn('good', 0, function(err, port) {
          if (err) {
            assert(!err, makeMsg('Unexpected error: ' + err));
          }
        });
      });
      client.on('tcp connection', function(details, accept, reject) {
        accept();
      });
    },
    what: 'Client auto-rejects unrequested, allows requested forwarded-tcpip'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA],
          greeting: 'Hello world!'
        }
      );
      client = r.client;
      server = r.server;

      var sawGreeting = false;

      client.on('greeting', function(greeting) {
        assert.strictEqual(greeting, 'Hello world!\r\n');
        sawGreeting = true;
      });
      client.on('banner', function(message) {
        assert.fail(null, null, makeMsg('Unexpected banner'));
      });

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          assert(sawGreeting, makeMsg('Client did not see greeting'));
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'password',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.password === PASSWORD,
                 makeMsg('Unexpected password: ' + ctx.password));
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Server greeting'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA],
          banner: 'Hello world!'
        }
      );
      client = r.client;
      server = r.server;

      var sawBanner = false;

      client.on('greeting', function(greeting) {
        assert.fail(null, null, makeMsg('Unexpected greeting'));
      });
      client.on('banner', function(message) {
        assert.strictEqual(message, 'Hello world!\r\n');
        sawBanner = true;
      });

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          assert(sawBanner, makeMsg('Client did not see banner'));
          if (ctx.method === 'none')
            return ctx.reject();
          assert(ctx.method === 'password',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(ctx.username === USER,
                 makeMsg('Unexpected username: ' + ctx.username));
          assert(ctx.password === PASSWORD,
                 makeMsg('Unexpected password: ' + ctx.password));
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });
    },
    what: 'Server banner'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var fastRejectSent = false;

      function sendAcceptLater(accept) {
        if (fastRejectSent)
          accept();
        else
          setImmediate(sendAcceptLater, accept);
      }

      r = setup(
        this,
        { username: USER },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        });

        conn.on('request', function(accept, reject, name, info) {
          if (info.bindAddr === 'fastReject') {
            // Will call reject on 'fastReject' soon
            reject();
            fastRejectSent = true;
          } else
            // but accept on 'slowAccept' later
            sendAcceptLater(accept);
        });
      });

      client.on('ready', function() {
        var replyCnt = 0;

        client.forwardIn('slowAccept', 0, function(err) {
          assert(!err, makeMsg('Unexpected error: ' + err));
          if (++replyCnt === 2)
            client.end();
        });

        client.forwardIn('fastReject', 0, function(err) {
          assert(err, makeMsg('Should receive error'));
          if (++replyCnt === 2)
            client.end();
        });
      });
    },
    what: 'Server responds to global requests in the right order'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      var timer;
      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.on('session', function(accept, reject) {
            var session = accept();
            session.once('subsystem', function(accept, reject, info) {
              assert.equal(info.name, 'netconf');

              // Prevent success reply from being sent
              conn._sshstream.channelSuccess = function() {};

              var stream = accept();
              stream.close();
              timer = setTimeout(function() {
                throw new Error(makeMsg('Expected client callback'));
              }, 50);
            });
          });
        });
      });
      client.on('ready', function() {
        client.subsys('netconf', function(err, stream) {
          clearTimeout(timer);
          assert(err);
          client.end();
        });
      });
    },
    what: 'Cleanup outstanding channel requests on channel close'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD
        },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      var timer;
      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          conn.on('session', function(accept, reject) {
            var session = accept();
            session.once('exec', function(accept, reject, info) {
              var stream = accept();
              // Write enough to bring the Client's channel window to 0
              // (currently 1MB)
              var buf = Buffer.allocUnsafe(2048);
              for (var i = 0; i < 1000; ++i)
                stream.write(buf);
              stream.exit(0);
              stream.close();
            });
          });
        });
      });
      client.on('ready', function() {
        client.exec('foo', function(err, stream) {
          var sawClose = false;
          assert(!err, makeMsg('Unexpected error'));
          client._sshstream.on('CHANNEL_CLOSE:' + stream.incoming.id, onClose);
          function onClose() {
            // This handler gets called *after* the internal handler, so we
            // should have seen `stream`'s `close` event already if the bug
            // exists
            assert(!sawClose, makeMsg('Premature close event'));
            client.end();
          }
          stream.on('close', function() {
            sawClose = true;
          });
        });
      });
    },
    what: 'Channel emits close prematurely'
  },
  { run: function() {
      var client;
      var server;
      var r;

      r = setup(
        this,
        { username: USER },
        { hostKeys: [HOST_KEY_RSA], ident: 'OpenSSH_5.3' }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        });
        conn.once('request', function(accept, reject, name, info) {
          assert(name === 'tcpip-forward',
                 makeMsg('Unexpected request: ' + name));
          accept(1337);
          conn.forwardOut('good', 0, 'remote', 12345, function(err, ch) {
            assert(!err, makeMsg('Unexpected error: ' + err));
            client.end();
          });
        });
      });

      client.on('ready', function() {
        // request forwarding
        client.forwardIn('good', 0, function(err, port) {
          assert(!err, makeMsg('Unexpected error: ' + err));
          assert(port === 1337, makeMsg('Bad bound port: ' + port));
        });
      });
      client.on('tcp connection', function(details, accept, reject) {
        assert(details.destIP === 'good',
               makeMsg('Bad incoming destIP: ' + details.destIP));
        assert(details.destPort === 1337,
               makeMsg('Bad incoming destPort: ' + details.destPort));
        assert(details.srcIP === 'remote',
               makeMsg('Bad incoming srcIP: ' + details.srcIP));
        assert(details.srcPort === 12345,
               makeMsg('Bad incoming srcPort: ' + details.srcPort));
        accept();
      });
    },
    what: 'OpenSSH 5.x workaround for binding on port 0'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var srvError;
      var cliError;

      r = setup(
        this,
        { username: USER,
          algorithms: {
            cipher: [ 'aes128-cbc' ]
          }
        },
        { hostKeys: [HOST_KEY_RSA],
          algorithms: {
            cipher: [ 'aes128-ctr' ]
          }
        }
      );
      client = r.client;
      server = r.server;

      // Remove default client error handler added by `setup()` since we are
      // expecting an error in this case
      client.removeAllListeners('error');

      function onError(err) {
        if (this === client) {
          assert(!cliError, makeMsg('Unexpected multiple client errors'));
          cliError = err;
        } else {
          assert(!srvError, makeMsg('Unexpected multiple server errors'));
          srvError = err;
        }
        assert.strictEqual(err.level, 'handshake');
        assert(/handshake failed/i.test(err.message),
               makeMsg('Wrong error message'));
      }

      server.on('connection', function(conn) {
        // Remove default server connection error handler added by `setup()`
        // since we are expecting an error in this case
        conn.removeAllListeners('error');

        function onGoodHandshake() {
          assert(false, makeMsg('Handshake should have failed'));
        }
        conn.on('authentication', onGoodHandshake);
        conn.on('ready', onGoodHandshake);

        conn.on('error', onError);
      });

      client.on('ready', function() {
        assert(false, makeMsg('Handshake should have failed'));
      });
      client.on('error', onError);
      client.on('close', function() {
        assert(cliError, makeMsg('Expected client error'));
        assert(srvError, makeMsg('Expected server error'));
      });
    },
    what: 'Handshake errors are emitted'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var cliError;

      r = setup(
        this,
        { username: USER, privateKey: KEY_RSA_BAD },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      // Remove default client error handler added by `setup()` since we are
      // expecting an error in this case
      client.removeAllListeners('error');

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          assert(ctx.method === 'publickey' || ctx.method === 'none',
                 makeMsg('Unexpected auth method: ' + ctx.method));
          assert(!ctx.signature, makeMsg('Unexpected signature'));
          if (ctx.method === 'none')
            return ctx.reject();
          ctx.accept();
        });
        conn.on('ready', function() {
          assert(false, makeMsg('Authentication should have failed'));
        });
      });

      client.on('ready', function() {
        assert(false, makeMsg('Authentication should have failed'));
      });
      client.on('error', function(err) {
        if (cliError) {
          assert(/all configured/i.test(err.message),
                 makeMsg('Wrong error message'));
        } else {
          cliError = err;
          assert(/signing/i.test(err.message), makeMsg('Wrong error message'));
        }
      });
      client.on('close', function() {
        assert(cliError, makeMsg('Expected client error'));
      });
    },
    what: 'Client signing errors are caught and emitted'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var srvError;
      var cliError;

      r = setup(
        this,
        { username: USER, password: 'foo' },
        { hostKeys: [KEY_RSA_BAD] }
      );
      client = r.client;
      server = r.server;

      // Remove default client error handler added by `setup()` since we are
      // expecting an error in this case
      client.removeAllListeners('error');

      server.on('connection', function(conn) {
        // Remove default server connection error handler added by `setup()`
        // since we are expecting an error in this case
        conn.removeAllListeners('error');

        conn.once('error', function(err) {
          assert(/signing/i.test(err.message), makeMsg('Wrong error message'));
          srvError = err;
        });
        conn.on('authentication', function(ctx) {
          assert(false, makeMsg('Handshake should have failed'));
        });
        conn.on('ready', function() {
          assert(false, makeMsg('Authentication should have failed'));
        });
      });

      client.on('ready', function() {
        assert(false, makeMsg('Handshake should have failed'));
      });
      client.on('error', function(err) {
        assert(!cliError, makeMsg('Unexpected multiple client errors'));
        assert(/KEY_EXCHANGE_FAILED/.test(err.message),
               makeMsg('Wrong error message'));
        cliError = err;
      });
      client.on('close', function() {
        assert(srvError, makeMsg('Expected server error'));
        assert(cliError, makeMsg('Expected client error'));
      });
    },
    what: 'Server signing errors are caught and emitted'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var sawReady = false;

      r = setup(
        this,
        { username: '', password: 'foo' },
        { hostKeys: [HOST_KEY_RSA] }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          assert.strictEqual(ctx.username, '',
                             makeMsg('Expected empty username'));
          ctx.accept();
        }).on('ready', function() {
          conn.end();
        });
      });

      client.on('ready', function() {
        sawReady = true;
      }).on('close', function() {
        assert.strictEqual(sawReady, true, makeMsg('Expected ready event'));
      });
    },
    what: 'Empty username string works'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var socketPath = '/foo';
      var events = [];
      var expected = [
        ['client', 'openssh_forwardInStreamLocal'],
        ['server',
          'streamlocal-forward@openssh.com',
          { socketPath: socketPath }],
        ['client', 'forward callback'],
        ['client', 'unix connection', { socketPath: socketPath }],
        ['client', 'socket data', '1'],
        ['server', 'socket data', '2'],
        ['client', 'socket end'],
        ['server',
         'cancel-streamlocal-forward@openssh.com',
         { socketPath: socketPath }],
        ['client', 'cancel callback']
      ];

      r = setup(
        this,
        { username: USER },
        { hostKeys: [HOST_KEY_RSA], ident: 'OpenSSH_7.1' }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        });
        conn.on('request', function(accept, reject, name, info) {
          events.push(['server', name, info]);
          if (name === 'streamlocal-forward@openssh.com') {
            accept();
            conn.openssh_forwardOutStreamLocal(socketPath, function(err, ch) {
              assert(!err, makeMsg('Unexpected error: ' + err));
              ch.write('1');
              ch.on('data', function(data) {
                events.push(['server', 'socket data', data.toString()]);
                ch.close();
              });
            });
          } else if (name === 'cancel-streamlocal-forward@openssh.com') {
            accept();
          } else {
            reject();
          }
        });
      });

      client.on('ready', function() {
        // request forwarding
        events.push(['client', 'openssh_forwardInStreamLocal']);
        client.openssh_forwardInStreamLocal(socketPath, function(err) {
          assert(!err, makeMsg('Unexpected error: ' + err));
          events.push(['client', 'forward callback']);
        });
        client.on('unix connection', function(info, accept, reject) {
          events.push(['client', 'unix connection', info]);
          var stream = accept();
          stream.on('data', function(data) {
            events.push(['client', 'socket data', data.toString()]);
            stream.write('2');
          }).on('end', function() {
            events.push(['client', 'socket end']);
            client.openssh_unforwardInStreamLocal(socketPath, function(err) {
              assert(!err, makeMsg('Unexpected error: ' + err));
              events.push(['client', 'cancel callback']);
              client.end();
            });
          });
        });
      });
      client.on('end', function() {
        var msg = 'Events mismatch\nActual:\n' + inspect(events)
                  + '\nExpected:\n' + inspect(expected);
        assert.deepEqual(events, expected, makeMsg(msg));
      });
    },
    what: 'OpenSSH forwarded UNIX socket connection'
  },
  { run: function() {
      var client;
      var server;
      var r;
      var calledBack = 0;

      r = setup(
        this,
        { username: USER,
          password: PASSWORD,
          algorithms: {
            cipher: [ 'aes128-gcm@openssh.com' ],
          },
        },
        { hostKeys: [HOST_KEY_RSA],
          algorithms: {
            cipher: [ 'aes128-gcm@openssh.com' ],
          },
        }
      );
      client = r.client;
      server = r.server;

      server.on('connection', function(conn) {
        conn.on('authentication', function(ctx) {
          ctx.accept();
        }).on('ready', function() {
          var reqs = [];
          conn.on('session', function(accept, reject) {
            if (reqs.length === 0) {
              conn.rekey(function(err) {
                assert(!err, makeMsg('Unexpected rekey error: ' + err));
                reqs.forEach(function(accept) {
                  var session = accept();
                  session.once('exec', function(accept, reject, info) {
                    var stream = accept();
                    stream.exit(0);
                    stream.end();
                  });
                });
              });
            }
            reqs.push(accept);
          });
        });
      });
      client.on('ready', function() {
        function callback(err, stream) {
          assert(!err, makeMsg('Unexpected error: ' + err));
          stream.resume();
          if (++calledBack === 3)
            client.end();
        }
        client.exec('foo', callback);
        client.exec('bar', callback);
        client.exec('baz', callback);
      }).on('end', function() {
        assert(calledBack === 3,
               makeMsg('Only '
                             + calledBack
                             + '/3 callbacks called'));
      });
    },
    what: 'Rekeying with AES-GCM'
  },
];

function setup(self, clientcfg, servercfg, timeout) {
  self.state = {
    clientReady: false,
    serverReady: false,
    clientClose: false,
    serverClose: false
  };

  if (DEBUG) {
    console.log('========================================================\n'
                + '[TEST] '
                + self.what
                + '\n========================================================');
    clientcfg.debug = function() {
      var args = new Array(arguments.length + 1);
      args[0] = '[CLIENT]';
      for (var i = 0; i < arguments.length; ++i)
        args[i + 1] = arguments[i];
      console.log.apply(null, args);
    };
    servercfg.debug = function() {
      var args = new Array(arguments.length + 1);
      args[0] = '[SERVER]';
      for (var i = 0; i < arguments.length; ++i)
        args[i + 1] = arguments[i];
      console.log.apply(null, args);
    };
  }

  var client = new Client();
  var server = new Server(servercfg);
  if (timeout === undefined)
    timeout = DEFAULT_TEST_TIMEOUT;
  var timer;

  server.on('error', onError)
        .on('connection', function(conn) {
          conn.on('error', onError)
              .on('ready', onReady);
          server.close();
        })
        .on('close', onClose);
  client.on('error', onError)
        .on('ready', onReady)
        .on('close', onClose);

  function onError(err) {
    var which = (this === client ? 'client' : 'server');
    assert(false, makeMsg('Unexpected ' + which + ' error: ' + err));
  }
  function onReady() {
    if (this === client) {
      assert(!self.state.clientReady,
             makeMsg('Received multiple ready events for client'));
      self.state.clientReady = true;
    } else {
      assert(!self.state.serverReady,
             makeMsg('Received multiple ready events for server'));
      self.state.serverReady = true;
    }
    if (self.state.clientReady && self.state.serverReady)
      self.onReady && self.onReady();
  }
  function onClose() {
    if (this === client) {
      assert(!self.state.clientClose,
             makeMsg('Received multiple close events for client'));
      self.state.clientClose = true;
    } else {
      assert(!self.state.serverClose,
             makeMsg('Received multiple close events for server'));
      self.state.serverClose = true;
    }
    if (self.state.clientClose && self.state.serverClose) {
      clearTimeout(timer);
      next();
    }
  }

  process.nextTick(function() {
    server.listen(0, 'localhost', function() {
      if (timeout >= 0) {
        timer = setTimeout(function() {
          assert(false, makeMsg('Test timed out'));
        }, timeout);
      }
      if (clientcfg.sock)
        clientcfg.sock.connect(server.address().port, 'localhost');
      else {
        clientcfg.host = 'localhost';
        clientcfg.port = server.address().port;
      }
      client.connect(clientcfg);
    });
  });
  return { client: client, server: server };
}

function next() {
  if (Array.isArray(process._events.exit))
    process._events.exit = process._events.exit[1];
  if (++t === tests.length)
    return;

  var v = tests[t];
  v.run.call(v);
}

function makeMsg(what, msg) {
  if (msg === undefined)
    msg = what;
  if (tests[t])
    what = tests[t].what;
  else
    what = '<Unknown>';
  return '[' + group + what + ']: ' + msg;
}

process.once('uncaughtException', function(err) {
  if (t > -1 && !/(?:^|\n)AssertionError: /i.test(''+err))
    console.log(makeMsg('Unexpected Exception:'));
  throw err;
});
process.once('exit', function() {
  assert(t === tests.length,
         makeMsg('_exit',
                 'Only finished ' + t + '/' + tests.length + ' tests'));
});

next();
