/*

  sshclient.c

  Authors:
        Tatu Ylonen <ylo@ssh.fi>
        Markku-Juhani Saarinen <mjos@ssh.fi>
        Timo J. Rinne <tri@ssh.fi>
        Sami Lehtinen <sjl@ssh.fi>

  Copyright (C) 1997-1999 SSH Communications Security Oy, Espoo, Finland
  All rights reserved.

  SSH client functionality for processing a connection.  Most of the
  implementation is actually shared with the server (in sshcommon.c).

*/

#include "ssh2includes.h"
#include "sshtrans.h"
#include "sshauth.h"
#include "sshconn.h"
#include "sshauthmethods.h"
#include "sshcommon.h"
#include "sshclient.h"
#include "sshuserfiles.h"
#include "sshmsgs.h"
#include "sshcipherlist.h"
#include "sshgetopt.h"
#include "sshdsprintf.h"
#include "sshfsm.h"
#include "sshreadline.h"
#include "sshappcommon.h"
#include "sshtimeouts.h"

#ifdef SSH_CHANNEL_SESSION
#include "sshchsession.h"
#endif /* SSH_CHANNEL_SESSION */

#ifdef SSH_CHANNEL_TCPFWD
#include "sshchtcpfwd.h"
#endif /* SSH_CHANNEL_TCPFWD */

#define SSH_DEBUG_MODULE "Ssh2Client"

/* Define this to test trkex-keycheck callbacks. (gives a two-second
   time-out.*/
/* #define TEST_KEYCHECK */

#ifdef TEST_KEYCHECK
#include "sshtimeouts.h"

typedef struct SshKeyCheckTestContextRec
{
  void (*callback)(Boolean result,
                   void *result_context);
  Boolean result;
  void *result_context;
} *SshKeyCheckTestContext;

void test_timeout(void *context)
{
  SshKeyCheckTestContext ctx = (SshKeyCheckTestContext) context;

  fprintf(stderr, "Delay ended, hiirr wee aar. (Calling "
          "result-callback.)\r\n");
  (*ctx->callback)(ctx->result, ctx->result_context);

  ssh_xfree(ctx);  
}

#endif /* TEST_KEYCHECK */

typedef struct SshClientKeyCheckContextRec
{
  /* Keyblob received from network. */
  const unsigned char *blob;
  size_t blob_len;

  /* Keyblob we (might) have on disk. */
  unsigned char *blob2;
  size_t blob2_len;

  /* Result callback and context to be used with it. The callback
     _must_ be called, whatever the result of the keycheck. */
  void (*result_cb)(Boolean result,
                    void *result_context);
  void *result_context;

  /* Remote servers name. */
  const char *server_name;
  
  /* Filename for the key (supposedly) in database. */
  char *key_file_name;

  /* Filename for the key (supposedly) in global
     database. (/etc/ssh2/hostkeys, typically.)  */
  char *server_key_file_name;

  /* File descriptor, from where the possible confirmations are read. */
  int readfd;

  /* File descriptor, where possible prompts etc. are written. */
  int writefd;
  
  /* TRUE if user answered "yes" and FALSE if "no". */
  Boolean confirmation;
  
  /* The Finite State Machine. */
  SshFSM fsm;

  /* State, from which to continue after 'transitional' state. */
  char *next_state;
  
  SshClient client;
} *SshClientKeyCheckContext;

/* Forward declarations. */
SSH_FSM_STEP(keycheck_begin);
SSH_FSM_STEP(keycheck_check_fd);
SSH_FSM_STEP(keycheck_key_not_found);
SSH_FSM_STEP(keycheck_continue_confirmation);
SSH_FSM_STEP(keycheck_key_different);
SSH_FSM_STEP(keycheck_warning_confirmation);
SSH_FSM_STEP(keycheck_key_match);
SSH_FSM_STEP(keycheck_failure);
SSH_FSM_STEP(keycheck_success);
SSH_FSM_STEP(keycheck_save_key);
SSH_FSM_STEP(keycheck_change_key);
SSH_FSM_STEP(keycheck_change_key_confirmation);

SshFSMStateMapItem keycheck_states[] =
{
  /* the prefix "ckc" stands for "client keycheck". */
  { "ckc_begin", "Begin keycheck", keycheck_begin },

  { "ckc_key_match", "Key found and identical", keycheck_key_match },

  { "ckc_check_fd", "Check where to read (and write) confirmations",
    keycheck_check_fd },
  { "ckc_key_not_found", "Hostkey not found from database",
    keycheck_key_not_found },
  { "ckc_continue_confirmation", "Read users answer for continuing",
    keycheck_continue_confirmation },
  { "ckc_key_different",
    "Hostkey was differs from the one in the database",
    keycheck_key_different },
  { "ckc_warning_confirmation", "Read users answer for continuing",
    keycheck_warning_confirmation },
  
  { "ckc_save_key", "Save key to disk.", keycheck_save_key },
  { "ckc_change_key", "Ask if the key should be changed", keycheck_change_key },
  { "ckc_change_key_confirmation", "Process user's response",
    keycheck_change_key_confirmation },
  
  { "ckc_failure", "Return failure", keycheck_failure },
  { "ckc_success", "Return success", keycheck_success }
};

void keycheck_destructor(void *gdata)
{
  SshClientKeyCheckContext keycheck_ctx = (SshClientKeyCheckContext) gdata;

  /* Free everything. */
  ssh_xfree(keycheck_ctx->blob2);
  ssh_xfree(keycheck_ctx->key_file_name);
  ssh_xfree(keycheck_ctx->server_key_file_name);
  if (keycheck_ctx->readfd != -1 && keycheck_ctx->readfd != fileno(stdin))
    {
      close(keycheck_ctx->readfd);
      SSH_TRACE(2, ("Closed filedescriptor %d.", keycheck_ctx->readfd));
    }
}

/* Callback function that is used to check the validity of the server
   host key.
     `server_name'  The server name as passed in when the protocol
                    was created.  This is expected to be the name that
                    the user typed.
     `blob'         The linear representation of the public key (including
                    optional certificates).
     `len'          The length of the public key blob.
     `result_cb'    This function must be called when the validity has been
                    determined.  The argument must be TRUE if the host key
                    is to be accepted, and FALSE if it is to be rejected.
     `result_context' This must be passed to the result function.
     `context'      Context argument.
   This function should call the result callback in every case.  This is not
   allowed to destroy the protocol context.  This function is allowed to
   do basically anything before calling the result callback. */

void ssh_client_key_check(const char *server_name,
                          const unsigned char *blob, size_t len,
                          void (*result_cb)(Boolean result,
                                            void *result_context),
                          void *result_context,
                          void *context)
{
  SshClientKeyCheckContext keycheck_ctx = NULL;
  SshFSM fsm;
  char *udir, *filen, *to_be_deleted, stripped_server_name[1024];
  int i, j;
  struct stat st;

  SSH_PRECOND(result_cb);
  SSH_PRECOND(context);
  SSH_PRECOND(len > 0 ? (blob != NULL) : TRUE);
  
  SSH_ASSERT(context != NULL);
  
  if (server_name == NULL || strlen(server_name) == 0)
    {
      ssh_debug("ssh_client_key_check: server_name is NULL or zero-length");
      (*result_cb)(FALSE, result_context);
      return;
    }

  fsm = ssh_fsm_allocate(sizeof(*keycheck_ctx),
                         keycheck_states,
                         SSH_FSM_NUM_STATES(keycheck_states),
                         keycheck_destructor);
  
  keycheck_ctx = ssh_fsm_get_gdata_fsm(fsm);

  memset(keycheck_ctx, 0, sizeof(*keycheck_ctx));
  
  keycheck_ctx->fsm = fsm;
  keycheck_ctx->client = (SshClient) context;
  keycheck_ctx->server_name = server_name;
  keycheck_ctx->readfd = -1;
  keycheck_ctx->result_cb = result_cb;
  keycheck_ctx->result_context = result_context;
  keycheck_ctx->blob = blob;
  keycheck_ctx->blob_len = len;
  
  if ((udir = ssh_userdir(keycheck_ctx->client->user_data,
                          keycheck_ctx->client->config, TRUE)) == NULL)
    ssh_fatal("ssh_client_key_check: no user directory.");

  ssh_dsprintf(&filen, "%s/hostkeys", udir);
  ssh_xfree(udir);  
  
  if (stat(filen, &st) < 0)
    {
      if (mkdir(filen, 0700) < 0)
        {
          ssh_warning("ssh_userdir: could not create user's ssh hostkey" 
                      "directory %s", filen);
        }
    }

  for (j = 0,i = 0; server_name[i] != '\0'; i++)
    {
      if (j > sizeof(stripped_server_name) - 10)
        break;

      if (isalpha(server_name[i]))
        {
          stripped_server_name[j++] = tolower(server_name[i]);
          continue;
        }
      if (isdigit(server_name[i]) || server_name[i] == '.' || 
          server_name[i] == '-')
        {
          stripped_server_name[j++] = server_name[i];
          continue;
        }

      /* escape this character in octal */
      stripped_server_name[j++] = '_';
      stripped_server_name[j++] = '0' + (server_name[i] >> 6);
      stripped_server_name[j++] = '0' + ((server_name[i] >> 3) & 7);
      stripped_server_name[j++] = '0' + (server_name[i] & 7);
    }
  stripped_server_name[j] = '\0';
  
  /* produce a file name from the server name */
  to_be_deleted = filen;
  ssh_dsprintf(&filen, "%s/key_%s_%s.pub", 
               filen, keycheck_ctx->client->common->config->port,
               stripped_server_name);
  ssh_xfree(to_be_deleted);

  ssh_dsprintf(&keycheck_ctx->server_key_file_name, "%s/key_%s_%s.pub",
               SSH_GLOBAL_HOSTKEYS_DIR,
               keycheck_ctx->client->common->config->port,
               stripped_server_name);
  
  SSH_DEBUG(6, ("checking key %s", filen));

  keycheck_ctx->key_file_name = filen;
  
  (void)ssh_fsm_spawn(keycheck_ctx->fsm, 0L,
                      "ckc_begin", NULL, NULL);
}

SSH_FSM_STEP(keycheck_begin)
{
  unsigned long magic;
  static Boolean tried = FALSE;
  SSH_FSM_GDATA(SshClientKeyCheckContext);

  SSH_PRECOND(gdata);
  SSH_PRECOND(gdata->key_file_name);
  SSH_PRECOND(gdata->client);
  
  magic = ssh2_key_blob_read(gdata->client->user_data, gdata->key_file_name,
                             TRUE, NULL,
                             &(gdata->blob2), &(gdata->blob2_len), NULL);

 retry:
  switch(magic)
    {
    case SSH_KEY_MAGIC_FAIL:
      if (!tried)
        {
          magic = ssh2_key_blob_read(gdata->client->user_data,
                                     gdata->server_key_file_name,
                                     TRUE, NULL,
                                     &(gdata->blob2), &(gdata->blob2_len), NULL);
          tried = TRUE;
          goto retry;
        }
      
      gdata->next_state = "ckc_key_not_found";
      SSH_FSM_SET_NEXT("ckc_check_fd");
      break;
    case SSH_KEY_MAGIC_PUBLIC:
      if (gdata->blob2_len == gdata->blob_len &&
          memcmp(gdata->blob, gdata->blob2, gdata->blob_len) == 0)
        {
          SSH_FSM_SET_NEXT("ckc_key_match");
          break;
        }
      /* FALLTHROUGH */
    default:
      gdata->next_state = "ckc_key_different";
      SSH_FSM_SET_NEXT("ckc_check_fd");
      break;
    }

  return SSH_FSM_CONTINUE;
}


SSH_FSM_STEP(keycheck_key_match)
{

  SSH_TRACE(0, ("Host key found from database."));
  SSH_FSM_SET_NEXT("ckc_success");
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(keycheck_check_fd)
{
  FILE *fi, *fo;
  SSH_FSM_GDATA(SshClientKeyCheckContext);
  SSH_PRECOND(gdata->next_state);

  if (isatty(fileno(stdin)))
    fi = stdin;
  else
    {
      /* Read the passphrase from /dev/tty to make it possible to ask it even 
         when stdin has been redirected. */
      fi = fopen("/dev/tty", "r");
      if (!fi)
        {
          ssh_warning("You have no controlling tty.  Cannot read "
                      "confirmation.");
          SSH_FSM_SET_NEXT("ckc_failure");
          return SSH_FSM_CONTINUE;
        }
    }

  if (isatty(fileno(stdout)))
    fo = stdout;
  else
    {
      /* Read the passphrase from /dev/tty to make it possible to ask it even 
         when stdin has been redirected. */
      fo = fopen("/dev/tty", "w");
      if (!fo)
        {
          ssh_warning("You have no controlling tty.  Cannot write "
                      "prompts.");
        }
    }

  gdata->readfd = fileno(fi);
  gdata->writefd = fileno(fo);
  SSH_FSM_SET_NEXT(gdata->next_state);
  return SSH_FSM_CONTINUE;
}

void readline_cb(const char *line, void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SSH_FSM_GDATA(SshClientKeyCheckContext);
  SSH_PRECOND(gdata);
  SSH_PRECOND(gdata->next_state);
  SSH_PRECOND(!gdata->client->config->batch_mode);
  
  if (line == NULL)
    {
      /* Some error occurred. */
      SSH_FSM_SET_NEXT("ckc_failure");
      return;
    }

  ssh_informational("\r\n");
  
  if (strcmp(line, "no") == 0)
    {
      gdata->confirmation = FALSE;
    }
  else if (strcmp(line, "yes") == 0)
    {
      gdata->confirmation = TRUE;
    }
  else
    {
      ssh_readline_eloop("Please answer 'yes' or 'no': ",
                         "", gdata->client->rl, readline_cb, thread);
      return;
    }

  SSH_FSM_SET_NEXT(gdata->next_state);
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(keycheck_key_not_found)
{
  SSH_FSM_GDATA(SshClientKeyCheckContext);
  ssh_informational("Host key not found from database.\r\n");

  if (gdata->client->config->strict_host_key_checking ==
      SSH_STRICT_HOSTKEY_CHECKING_YES)
    {
      /* User has requested strict host key checking.  We will not add
         the host key automatically.  The only alternative left is to
         abort. */
      ssh_informational("No host key is known for %.200s and "
                        "you have requested strict host key checking.\r\n",
                        gdata->server_name);
      SSH_FSM_SET_NEXT("ckc_failure");
      return SSH_FSM_CONTINUE;
    }
  else if (gdata->client->config->strict_host_key_checking ==
           SSH_STRICT_HOSTKEY_CHECKING_NO)
    {
      gdata->confirmation = TRUE;
      SSH_FSM_SET_NEXT("ckc_continue_confirmation");
      return SSH_FSM_CONTINUE;
    }

  if (gdata->client->config->batch_mode)
    {
      /* If we're in BatchMode, don't save the key by default. */
      SSH_FSM_SET_NEXT("ckc_failure");
      return SSH_FSM_CONTINUE;
    }
  
  gdata->next_state = "ckc_continue_confirmation";
  
  SSH_FSM_ASYNC_CALL(ssh_readline_eloop("Are you sure you want to continue " \
                                        "connecting (yes/no)? ",             \
                                        "", gdata->client->rl,               \
                                        readline_cb, thread));
}

SSH_FSM_STEP(keycheck_continue_confirmation)
{
  SSH_FSM_GDATA(SshClientKeyCheckContext);

  if (gdata->confirmation != TRUE)
    {
      SSH_FSM_SET_NEXT("ckc_failure");
    }
  else
    {
      SSH_FSM_SET_NEXT("ckc_save_key");
      gdata->next_state = "ckc_success";
    }
  
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(keycheck_save_key)
{
  SshTime now;
  char *time_str, *comment;  
  SSH_FSM_GDATA(SshClientKeyCheckContext);
  
  now = ssh_time();
  time_str = ssh_readable_time_string(now, TRUE);
  ssh_dsprintf(&comment, 
               "host key for %s, accepted by %s %s", 
               gdata->server_name, ssh_user_name(gdata->client->user_data),
               time_str);
  ssh_xfree(time_str);
  
  if (ssh2_key_blob_write(gdata->client->user_data, gdata->key_file_name,
                          0600, SSH_KEY_MAGIC_PUBLIC,
                          comment, gdata->blob, gdata->blob_len, NULL))
    {
      ssh_warning("Unable to write host key %s", gdata->key_file_name);
    }
  else
    {
      ssh_informational("Host key saved to %s\r\n", gdata->key_file_name);
      ssh_informational("%s\r\n", comment);
    }

  ssh_xfree(comment);
  
  SSH_FSM_SET_NEXT(gdata->next_state);
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(keycheck_key_different)
{
  SSH_FSM_GDATA(SshClientKeyCheckContext);

  ssh_informational
    ("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\r\n"
     "@       WARNING: HOST IDENTIFICATION HAS CHANGED!         @\r\n"
     "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\r\n"
     "IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\r\n"
     "Someone could be eavesdropping on you right now "
     "(man-in-the-middle attack)!\r\n"
     "It is also possible that the host key has just been changed.\r\n"
     "Please contact your system administrator.\r\n"
     "Add correct host key to \"%s\"\r\n"
     "to get rid of this message.",
     gdata->key_file_name);

  if (gdata->client->config->strict_host_key_checking ==
      SSH_STRICT_HOSTKEY_CHECKING_NO)
    {
      ssh_informational(" (or remove the keyfile, and connect again.)");
    }
  ssh_informational("\r\n");
  
  if (gdata->client->config->strict_host_key_checking ==
      SSH_STRICT_HOSTKEY_CHECKING_YES)
    {
      /* User has requested strict host key checking. We have to abort
         authentication. */
      ssh_informational("Host key has changed for %.200s and "
                        "you have requested strict host key checking.\r\n",
                        gdata->server_name);
      SSH_FSM_SET_NEXT("ckc_failure");
      return SSH_FSM_CONTINUE;
    }

  if (gdata->client->config->forward_agent)
    {
      gdata->client->config->forward_agent = FALSE;
      
      ssh_informational("Agent forwarding is disabled to avoid attacks by "
                        "corrupted servers.\r\n");
    }

  if (gdata->client->config->forward_x11)
    {
      gdata->client->config->forward_x11 = FALSE;

      ssh_informational("X11 forwarding is disabled to avoid attacks by "
                        "corrupted servers.\r\n");
    }

  if (gdata->client->config->strict_host_key_checking ==
           SSH_STRICT_HOSTKEY_CHECKING_NO)
    {
      gdata->confirmation = TRUE;
      SSH_FSM_SET_NEXT("ckc_warning_confirmation");
      return SSH_FSM_CONTINUE;
    }

  if (gdata->client->config->batch_mode)
    {      
      /* If we're in BatchMode, don't continue connecting by default.  */
      SSH_FSM_SET_NEXT("ckc_failure");
      return SSH_FSM_CONTINUE;
    }
  
  gdata->next_state = "ckc_warning_confirmation";
  
  SSH_FSM_ASYNC_CALL(ssh_readline_eloop("Are you sure you want to continue " \
                                        "connecting (yes/no)? ",             \
                                        "", gdata->client->rl,               \
                                        readline_cb, thread));
}

SSH_FSM_STEP(keycheck_change_key)
{
  SSH_FSM_GDATA(SshClientKeyCheckContext);
  SSH_FSM_SET_NEXT("ckc_change_key_confirmation");

  gdata->next_state = "ckc_change_key_confirmation";
  
  if (gdata->client->config->batch_mode ||
      gdata->client->config->strict_host_key_checking !=
      SSH_STRICT_HOSTKEY_CHECKING_ASK)
    {      
      /* If we're in BatchMode, don't change key by default. Also, if
         we're not in ASK mode, don't change the key. */
      gdata->confirmation = FALSE;
      return SSH_FSM_CONTINUE;
    }
  
  SSH_FSM_ASYNC_CALL(ssh_readline_eloop("Do you want to change the host key "   \
                                        "on disk (yes/no)? ",                   \
                                        "", gdata->client->rl,                  \
                                        readline_cb, thread));
}

SSH_FSM_STEP(keycheck_change_key_confirmation)
{
  SSH_FSM_GDATA(SshClientKeyCheckContext);

  if (gdata->confirmation != TRUE)
    {
      SSH_FSM_SET_NEXT("ckc_success");
    }
  else
    {
      SSH_FSM_SET_NEXT("ckc_save_key");
      gdata->next_state = "ckc_success";
    }
  
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(keycheck_warning_confirmation)
{
  SSH_FSM_GDATA(SshClientKeyCheckContext);

  if (gdata->confirmation != TRUE)
    SSH_FSM_SET_NEXT("ckc_failure");
  else
    SSH_FSM_SET_NEXT("ckc_change_key");

  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(keycheck_failure)
{
  SSH_FSM_GDATA(SshClientKeyCheckContext);

#ifdef TEST_KEYCHECK
  {
    SshKeyCheckTestContext ctx;
    
    ctx = ssh_xcalloc(1, sizeof(struct SshKeyCheckTestContextRec));
    ctx->callback = gdata->result_cb;
    ctx->result = FALSE;
    ctx->result_context = gdata->result_context;
    ssh_register_timeout(2L, 0L, test_timeout, ctx);
    SSH_TRACE(2, ("Entering (atleast) two second delay in "     \
                  "ssh_client_key_check()..."));
  }
#else
  (*gdata->result_cb)(FALSE, gdata->result_context);
#endif
  ssh_fsm_destroy(gdata->fsm);
  return SSH_FSM_FINISH;
}

SSH_FSM_STEP(keycheck_success)
{
  SSH_FSM_GDATA(SshClientKeyCheckContext);
  
#ifdef TEST_KEYCHECK
  {
    SshKeyCheckTestContext ctx;
    
    ctx = ssh_xcalloc(1, sizeof(struct SshKeyCheckTestContextRec));
    ctx->callback = gdata->result_cb;
    ctx->result = TRUE;
    ctx->result_context = gdata->result_context;
    ssh_register_timeout(2L, 0L, test_timeout, ctx);
    SSH_TRACE(2, ("Entering (atleast) two second delay in "     \
                  "ssh_client_key_check()..."));
  }
#else
  (*(gdata->result_cb))(TRUE, gdata->result_context);
#endif
  ssh_fsm_destroy(gdata->fsm);
  return SSH_FSM_FINISH;
}

/* Fetches values for the transport parameters (e.g., encryption algorithms)
   from the config data. */
Boolean ssh_client_update_transport_params(SshConfig config,
                                           SshTransportParams params)
{
  char *hlp;

  if (config->ciphers != NULL)
    {
      hlp = ssh_cipher_list_canonicalize(config->ciphers);

      if (hlp)
        {
          ssh_xfree(params->ciphers_c_to_s);
          params->ciphers_c_to_s = ssh_xstrdup(hlp);
          ssh_xfree(params->ciphers_s_to_c);
          params->ciphers_s_to_c = ssh_xstrdup(hlp);
          ssh_xfree(hlp);
        }
    }

  if (config->compression == TRUE)
    {
      ssh_xfree(params->compressions_c_to_s);
      params->compressions_c_to_s = ssh_xstrdup("zlib");
      ssh_xfree(params->compressions_s_to_c);
      params->compressions_s_to_c = ssh_xstrdup("zlib");
    }

  hlp = ssh_public_key_list_canonicalize(params->host_key_algorithms);
  ssh_xfree(params->host_key_algorithms);
  params->host_key_algorithms = hlp;

  hlp = ssh_hash_list_canonicalize(params->hash_algorithms);
  ssh_xfree(params->hash_algorithms);
  params->hash_algorithms = hlp;

  return TRUE;
}

/* Checks the remote version number, and execs a compatibility program as
   appropriate. */

Boolean ssh_client_version_check(const char *version, 
                                 void *context)
{
  SshClient client = (SshClient)context;
  char **args = NULL;
  int args_alloced = 20;  
  int args_used = 0;  
  int i;
  extern char **environ;
  char *host = NULL;
  int option;
  
  ssh_debug("Remote version: %s", version);

  if (strncmp(version, "SSH-1.", 6) == 0 &&
      strncmp(version, "SSH-1.99", 8) != 0 &&
      (client->config->ssh1compatibility == TRUE))
    {
      if ((client->config->ssh1_path == NULL) ||
          (client->config->ssh1_argv == NULL))
        return TRUE; /* Can't use exec ssh1 compatibility. */

      ssh_warning("Executing %s for ssh1 compatibility.",
                  client->config->ssh1_path);
      
      /* Close the old connection to the server. */
      close(client->config->ssh1_fd);
      
      args = ssh_xcalloc(args_alloced, sizeof(*args));

      args[0] = ssh_xstrdup("ssh");
      args_used++;
        
      /* Clear the getopt data. */
      ssh_getopt_init_data(&ssh_getopt_default_data);

      ssh_opterr = 0;
      ssh_optallowplus = 1;
      ssh_optind = 1;
        
      while(1)
        {
          if (args_alloced < args_used + 10)
            args = ssh_xrealloc(args,
                                (args_alloced + 10)*sizeof(*args));
            
          option = ssh_getopt(client->config->ssh1_argc,
                              client->config->ssh1_argv,
                              SSH2_GETOPT_ARGUMENTS, NULL);
          
          if ((option == -1) && (host == NULL))
            {
              host = client->config->ssh1_argv[ssh_optind];
                
              if (!host)
                ssh_fatal("No host name found from args to ssh1."
                          "(e.g. ssh1-argv struct has been corrupted.)");
                
              args[args_used] = ssh_xstrdup(host);
              args_used++;
              
              ssh_optind++;
              SSH_DEBUG(3, ("ssh1_args: remote host = \"%s\"", host));
              ssh_optreset = 1;
              option = ssh_getopt(client->config->ssh1_argc,
                                  client->config->ssh1_argv,
                                  SSH2_GETOPT_ARGUMENTS,NULL);
            }
          if (option == -1)
            {
              /* Rest ones are the command and arguments. */
              if (client->config->ssh1_argc > ssh_optind)
                {
                  for (i = 0;
                       i < (client->config->ssh1_argc - ssh_optind); i++)
                    {
                      args[args_used] = ssh_xstrdup(client->config->
                                                    ssh1_argv[ssh_optind
                                                             + i]);
                      args_used++;
                        
                      if (args_alloced < args_used + 10)
                        args =
                          ssh_xrealloc(args,
                                       (args_alloced
                                        + 10)*sizeof(*args));
                    }
                }
              break;
            }

          switch(option)
            {
              /* Ignored. */
            case 'S':
            case 's':
            case 'F':
              continue;
              /* Strip argument. */ 
            case 'd':
              /* Options without arguments. */
            case 'a':
            case 'C':
            case 'v':
            case 'f':
            case 'h':
            case 'n':
            case 'P':
            case 'q':
            case 'k':
            case '8':
            case 'g':
            case 'V':
              ssh_dsprintf(&args[args_used], "-%c", option);
              args_used++;
              continue;
              /* Options with arguments. */
            case 'i':
            case 'o': /* XXX conf options are different in ssh2 and ssh1 */
            case 'l':
            case 'e':
            case 'p':
            case 'L':
            case 'R':
            case 'c': /* XXX ciphers are different in ssh2 and ssh1 */
              ssh_dsprintf(&args[args_used], "-%c", option);
              args_used++;
              args[args_used] = ssh_xstrdup(ssh_optarg);
              args_used++;                
              continue;
            }
        }

      args[args_used] = NULL;
      
      for (i = 0; args[i]; i++)
        SSH_TRACE(2, ("args[%d] = %s", i, args[i]));

      /* Use ssh1 to connect. */
      execve(client->config->ssh1_path, args, environ);
      ssh_fatal("Executing ssh1 in compatibility mode failed.");
    }
  return TRUE;
}

void client_disconnect_wrap(int reason, Boolean locally_generated,
                            const char *msg, void *context)
{
  SshClient client = (SshClient) context;
  if (client->disconnect)
    (*client->disconnect)(reason, locally_generated,
                          msg, client->context);
}

void client_debug_wrap(int type, const char *msg, void *context)
{
  SshClient client = (SshClient) context;
  if (client->debug)
    (*client->debug)(type, msg, client->context);
}

void client_authentication_notify_wrap(const char *user,
                                       Boolean successful,
                                       void *context)
{
  SshClient client = (SshClient) context;
  if (successful && client->stream_wrapper_func)
    {
      (*client->stream_wrapper_func)(FALSE, client);
    }
  if (client->authentication_notify)
    (*client->authentication_notify)(user, successful, client->context);
}

/* Takes a stream, and creates an SSH client for processing that
   connection.  This closes the stream and returns NULL (without
   calling the destroy function) if an error occurs.  The random state
   is required to stay valid until the client has been destroyed.
   ``config'' must remain valid until the client is destroyed; it is
   not automatically freed.
     `stream'        the connection stream
     `config'        configuration data (not freed, must remain valid)
     `user_data'     data for client user
     `server_host_name'
                     name of the server host, as typed by the user
     `user'          (initial) user to log in as (may be changed during auth)
     `command'       the command to run in the server end
     `term'          the terminal type, needed by ssh1-emulation code
     ^stream_wrapper_func'
                     function, that is called when the stdio-streams can
                     be wrapped (this was added mainly to make ssh1-emulation
                     possible)
     `stdio_stream'  the main stdio_stream, needed mainly by ssh1-emulation
     `allocate_pty'  TRUE if pty should be allocated for command
     `disconnect'    function to call on disconnect
     `debug'         function to call on debug message (may be NULL)
     `authentication_notify'
                     function to call when authentication result is known
                     (may be NULL)
     `close_notify'  function, which is called when the underlying stream
                     is closed for some reason
     `context'       context to pass to ``destroy''
   The object should be destroyed from the ``disconnect'' callback or from
   a ``close_notify'' callback (see below).  */
SshClient ssh_client_wrap(SshStream stream, SshConfig config,
                          SshUser user_data,
                          const char *server_host_name,
                          const char *user,
                          const char *command,
                          const char *term,
                          SshClientStreamWrapperFunc stream_wrapper_func,
                          SshStream stdio_stream,
                          Boolean allocate_pty,
                          SshConnDisconnectProc disconnect,
                          SshConnDebugProc debug,
                          SshCommonAuthenticationNotify authentication_notify,
                          void (*close_notify)(SshUInt32 exit_status, 
                                               void *context),
                          void *context)
{
  SshClient client;
  SshStream trans, auth;
  SshTransportParams params;
  SshTransportCompat compat_flags;
  
  /* Create parameters. */
  params = ssh_transport_create_params();
  if (!ssh_client_update_transport_params(config, params))
    {
      ssh_stream_destroy(stream);
      ssh_transport_destroy_params(params);
      return NULL;
    }

  /* Create the client object. */
  client = ssh_xcalloc(1, sizeof(*client));
  client->user_data = user_data;
  client->config = config;
  client->being_destroyed = FALSE;

  /* Set the compatibility info. */
  client->transport_stream = stream;
  client->stream_wrapper_func = stream_wrapper_func;
  client->stdio_stream = stdio_stream;
  client->remote_server = server_host_name;
  client->remote_user = user;
  client->remote_command = command;
  client->allocate_pty = allocate_pty;
  client->terminal_type = term;
  client->close_notify = close_notify;
  client->context = context;
  client->disconnect = disconnect;
  client->debug = debug;
  client->authentication_notify = authentication_notify;

  
  /* Create a transport layer protocol object. */
  ssh_debug("ssh_client_wrap: creating transport protocol");
  trans = ssh_transport_client_wrap(stream, 
                                    SSH2_PROTOCOL_VERSION_STRING,
                                    SSH_USERAUTH_SERVICE,
                                    params, 
                                    server_host_name,
                                    ssh_client_key_check,
                                    (void *)client,
                                    (config->ssh1_path &&
                                     config->ssh1compatibility) ?
                                      ssh_client_version_check : NULL,
                                    (void *)client);

  
  ssh_transport_get_compatibility_flags(trans, &compat_flags);
  client->tr_context = ssh_stream_get_context(trans);
  
  /* Create the authentication methods array. */
  client->methods = ssh_client_authentication_initialize();
  
  /* Create an authentication protocol object. */
  SSH_TRACE(2, ("creating userauth protocol"));
  auth = ssh_auth_client_wrap(trans, user, SSH_CONNECTION_SERVICE,
                              client->methods, (void *)client);
  
  /* Create the common part of client/client objects. */
  client->common = ssh_common_wrap(stream, auth, TRUE, config, 
                                   server_host_name,
                                   client_disconnect_wrap, client_debug_wrap,
                                   client_authentication_notify_wrap,
                                   compat_flags,
                                   client);
  
  if (client->common == NULL)
    {
      ssh_client_authentication_uninitialize(client->methods);
      ssh_xfree(client);
      return NULL;
    }
  
  return client;
}

void auth_uninit_timeout(void *context)
{
  SshAuthClientMethod *methods = (SshAuthClientMethod *)context;
  SSH_PRECOND(methods);

  ssh_client_authentication_uninitialize(methods);  
}

/* Forcibly destroys the given client. */
  
void ssh_client_destroy(SshClient client)
{
  if(client->being_destroyed)
    return;
  
  client->being_destroyed = TRUE;
  ssh_common_destroy(client->common);
  ssh_register_timeout(0L, 0L, auth_uninit_timeout, (void *)client->methods);
  if (client->rl)
    {
      ssh_readline_eloop_unitialize(client->rl);
      client->rl = NULL;
    }
  
  if (!client->session_requested)
    {
      /* If session has been requested, then the lower layer destroys
         these. */
      if (client->stdio_stream)
        {
          ssh_stream_destroy(client->stdio_stream);
          client->stdio_stream = NULL;
          
        }
      if (client->stderr_stream)
        {
          ssh_stream_destroy(client->stderr_stream);
          client->stderr_stream = NULL;
        }
    }
  
  memset(client, 'F', sizeof(*client));
  ssh_xfree(client);
}

/* Starts a new command at the server.
     `client'       the client protocol object
     `stdio_stream' stream for stdin/stdout data
     `stderr_stream' stream for stderr data, or NULL to merge with stdout
     `auto_close'   automatically close stdio and stderr on channel close
     `is_subsystem' TRUE if command is a subsystem name instead of command
     `command'      command to execute, or NULL for shell
     `allocate_pty' TRUE if pty should be allocated for the command
     `term'         terminal type for pty (e.g., "vt100"), NULL otherwise
     `env'          NULL, or "name=value" strings to pass as environment
     `forward_x11'  TRUE to request X11 forwarding
     `forward_agent' TRUE to request agent forwarding
     `completion'   completion procedure to be called when done (may be NULL)
     `close_notify' function to call when ch closed (may be NULL)
     `context'      argument to pass to ``completion''.
   It is not an error if some forwarding fails, or an environment variable
   passing is denied.  The ``close_notify'' callback will be called
   regardless of the way the session is destroyed - ssh_client_destroy will
   call ``close_notify'' for all open channels.  It is also called if opening
   the cannnel fails.  It is legal to call ssh_conn_destroy from
   ``close_notify'', unless it has already been called. */

void ssh_client_start_session(SshClient client, SshStream stdio_stream,
                              SshStream stderr_stream, Boolean auto_close,
                              Boolean is_subsystem, const char *command,
                              Boolean allocate_pty, const char *term,
                              const char **env,
                              Boolean forward_x11, Boolean forward_agent,
                              void (*completion)(Boolean success,
                                                 SshChannelSession session,
                                                 void *context),
                              void (*close_notify)(SshUInt32 exit_status,
                                                   void *context),
                              void *context)
{
  /* Set, so that SshClient knows whether to destroy the stdio streams
     at ssh_client_destroy or not. */
  client->session_requested = TRUE;
  ssh_channel_start_session(client->common,
                            stdio_stream, 
                            stderr_stream,
                            auto_close, 
                            is_subsystem, 
                            command, 
                            allocate_pty,
                            term, 
                            env,
                            forward_x11, 
                            forward_agent,
                            completion, 
                            close_notify,
                            context);
}

#ifdef SSH_CHANNEL_TCPFWD

/* Requests forwarding of the given remote TCP/IP port.  If the completion
   procedure is non-NULL, it will be called when done. */

void ssh_client_remote_tcp_ip_forward(SshClient client,
                                      const char *address_to_bind,
                                      const char *port,
                                      const char *connect_to_host,
                                      const char *connect_to_port,
                                      const char *protocol,
                                      void (*completion)(Boolean success,
                                                         void *context),
                                      void *context)
{
  ssh_channel_start_remote_tcp_forward(client->common, 
                                       address_to_bind, 
                                       port,
                                       connect_to_host, 
                                       connect_to_port,
                                       protocol ? protocol : "tcp",
                                       completion, 
                                       context);
}

/* Requests forwarding of the given local TCP/IP port.  If the completion
   procedure is non-NULL, it will be called when done. */

Boolean ssh_client_local_tcp_ip_forward(SshClient client,
                                        const char *address_to_bind,
                                        const char *port,
                                        const char *connect_to_host,
                                        const char *connect_to_port,
                                        const char *protocol)
{
  return ssh_channel_start_local_tcp_forward(client->common, 
                                             address_to_bind,
                                             port, 
                                             connect_to_host,
                                             connect_to_port,
                                             protocol ? protocol : "tcp");
}

/* Opens a direct connection to the given TCP/IP port at the remote side.
   The originator values should be set to useful values and are passed
   to the other side.  ``stream'' will be used to transfer channel data. */

void ssh_client_open_remote_tcp_ip(SshClient client, SshStream stream,
                                   const char *connect_to_host,
                                   const char *connect_to_port,
                                   const char *originator_ip,
                                   const char *originator_port)
{
  ssh_channel_dtcp_open_to_remote(client->common, stream,
                                  connect_to_host, connect_to_port,
                                  originator_ip, originator_port);
}

#endif /* SSH_CHANNEL_TCPFWD */

/* Function, that informs the calling parent process of success in
   authentication. Is not called automatically by SshClient, but is
   here for convenience. */
void ssh_client_parent_notify_authentication(Boolean successful,
                                             SshStream stream)
{
  char *buffer = NULL;

  ssh_dsprintf(&buffer, "AUTHENTICATED %s\n", successful ? "YES" : "NO");

  /* XXX What if the write blocks? */
  ssh_stream_write(stream, buffer, strlen(buffer));

  ssh_xfree(buffer);
}

