/*

  sftp2.c

  Author: Tomi Salo <ttsalo@ssh.fi>
          Sami Lehtinen <sjl@ssh.fi>
          
  Copyright (C) 1999-2000 SSH Communications Security Oy, Espoo, Finland
  All rights reserved.

  Secure FTP. Similar to scp, but uses an ftp-like interface. 
 
 */

#include "sshincludes.h"
#include "sshfilecopy.h"
#include "ssh2includes.h"
#include "sshtty.h"
#include "ssheloop.h"
#include "sshgetopt.h"
#include "sshappcommon.h"
#include "sshreadline.h"
#include "sshfsm.h"
#include "sshunixpipestream.h"
#include "sigchld.h"
#include "sshgetopt.h"
#include "sftpcwd.h"
#include "sftppager.h"
#include "sshfdstream.h"
#include "sshfileio.h"
#include "sshtimeouts.h"
#ifdef HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif /* HAVE_SYS_IOCTL_H */

#define SSH_DEBUG_MODULE "Sftp2"

#define SFTP2_DEFAULT_SCREEN_WIDTH 78
#define SFTP2_DEFAULT_SCREEN_HEIGHT 25

int exit_value;

typedef enum
{
  SSH_SFTP_GET,
  SSH_SFTP_PUT
} SshSftpGetputDirection;

/* Global data structure */
typedef struct SshSftp2GDataRec {
  unsigned char *debug_level; 
  Boolean verbose_mode;
  
  int screen_width;
  int screen_height;
  unsigned char *ssh_path; /* Main ssh2 binary */
  SshFSM fsm;
  SshFSMThread main_thread;
  Boolean need_rl_initialization;
  unsigned char *initial_connect_target;
  
  /* Required for ssh_readline_eloop(). */
  SshReadLineCtx rl;
  SshStream stdio_stream;
  
  /* Of the variables below, the ones starting with remote_ or local_
     are the real ones and are copied into the other two, depending
     on whether we are doing a remote or local command. lls/ls,
     lpwd/pwd, lcd/cd etc. will simply use connection and cwd_ctx. */
  SshSftpCwdContext remote_cwd_ctx; 
  SshSftpCwdContext local_cwd_ctx; 
  SshSftpCwdContext cwd_ctx; 
  SshFileCopyConnection remote_connection;
  SshFileCopyConnection local_connection;
  SshFileCopyConnection connection;

  SshSftpPagerCtx pager_ctx;
  
  SshFileCopyRecurseAttrs recurse_attrs;
  SshFileCopyTransferAttrs transfer_attrs;
  /* for aborting the transfer. */
  SshOperationHandle transfer_op_handle;
  Boolean exit_attempted;

  /* Batchmode */
  unsigned char *batchfile_name;
  Boolean batchmode;
  unsigned char *batchfile_data;
  unsigned char *batchfile_ptr;
  unsigned int batchfile_len;

  /* Stuff for command_ls */
  Boolean cmd_ls_recursive;
  Boolean cmd_ls_long_format;
  Boolean cmd_ls_no_dot_prefix;
  Boolean cmd_ls_recurse_ready;
  SshFileCopyLocation cmd_ls_files_pre_glob;
  SshDlList cmd_ls_files_pre_recurse;
  /* Stuff for command_cd */
  unsigned char *cmd_cd_new_path;
  /* Stuff for command_pwd */
  unsigned char *cmd_pwd_path;
  /* Stuff for command_rm */
  unsigned char *cmd_rm_target;
  Boolean cmd_rm_directory;
  /* Stuff for command_mkdir */
  unsigned char *cmd_mkdir_target;
  struct SshFileAttributesRec cmd_mkdir_attrs;
  /* Stuff for command_getput */
  SshSftpGetputDirection cmd_getput_direction; 
  SshFileCopyLocation cmd_getput_files_pre_glob;
  SshDlList cmd_getput_files_pre_recurse;
  SshDlList cmd_getput_files_pre_transfer;
  SshFileCopyLocation cmd_getput_dest_location;
} *Sftp2GData;


/* FUNCTION PROTOTYPES */

void ssh_sftp_print_help(unsigned char *topic);

void sftp2_fsm_gdata_destructor(void *global_data);

/* FUNCTION IMPLEMENTATIONS */

void
sftp2_debug(const char *msg, void *context)
{
  Sftp2GData sftp2_gdata = (Sftp2GData) context;

  SSH_PRECOND(sftp2_gdata);

  if (sftp2_gdata->debug_level != NULL)
    fprintf(stderr, "%s\r\n", msg);
}

void
sftp2_warning(const char *msg, void *context)
{
  Sftp2GData sftp2_gdata = (Sftp2GData) context;

  SSH_PRECOND(sftp2_gdata);

  fprintf(stderr, "Warning: %s\r\n", msg);
}

void
sftp2_fatal(const char *msg, void *context)
{
  Sftp2GData sftp2_gdata = (Sftp2GData) context;
  /*  fprintf(stderr, "%s: FATAL: %s\n", session->scp2_name, msg); */
  fprintf(stderr, "FATAL: %s\r\n", msg);

  if (sftp2_gdata->rl)
    ssh_readline_eloop_unitialize(sftp2_gdata->rl);

  ssh_leave_raw_mode(-1);
  ssh_leave_non_blocking(-1);  

  exit(-1);
}

void
sftp2_fatal_signal_handler(int signal, void *context)
{
  ssh_fatal("Received signal %d.", signal);
}

#if defined(TIOCGWINSZ)

/* SIGWINCH (window size change signal) handler.  This sends a window
   change request to the server. */

void
sftp2_win_dim_change(int sig, void *context)
{
  struct winsize ws;
  Sftp2GData sftp2_gdata = (Sftp2GData) context;

  SSH_PRECOND(sftp2_gdata);
  
  if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) >= 0)
    {
      sftp2_gdata->screen_width = ws.ws_col;
      sftp2_gdata->screen_height = ws.ws_row;
    }    
  else
    {
      SSH_TRACE(2, ("unable to get window size parameters."));
    }
}
#endif /* TIOCGWINSZ */

void
sftp2_sigint_handler(int sig, void *context)
{
  Sftp2GData gdata = (Sftp2GData) context;

  if (gdata->transfer_op_handle)
    {
      SSH_TRACE(2, ("Aborting the transfer..."));
      ssh_operation_abort(gdata->transfer_op_handle);
      gdata->transfer_op_handle = NULL;
      ssh_fsm_set_next(gdata->main_thread, "get_command");
      SSH_FSM_CONTINUE_AFTER_CALLBACK(gdata->main_thread);
    }
  else
    {
      if (gdata->remote_connection->client == NULL)
        {
          ssh_fsm_set_next(gdata->main_thread, "finish");
        }
      else
        {
          if (gdata->exit_attempted == TRUE)
            {
              ssh_fsm_set_next(gdata->main_thread, "finish");
              ssh_fsm_continue(gdata->main_thread);
            }
          else
            {
              printf("\r\nYou have an active connection. Press again to terminate.\r\n");
              gdata->exit_attempted = TRUE;
            }
        }
    }
}

/* Finalizing the initialization, eg. async stuff, which can't be done
   in main(). */
void local_connect_complete_cb(SshFileClient client,
                               void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SSH_FSM_GDATA(Sftp2GData);

  if (!client)
    ssh_fatal("Local connection failed.");
  
  gdata->local_connection->client = client;

  /* Ugly, maybe, but I wouldn't add a state for this. -sjl */
  gdata->local_cwd_ctx =
    ssh_sftp_cwd_init(gdata->local_connection->client);

  gdata->rl = ssh_readline_eloop_initialize(gdata->stdio_stream);
  gdata->need_rl_initialization = FALSE;

  if (!gdata->rl)
    ssh_fatal("Couldn't initialize SshReadLine. termcap entrys are "
              "probably wrong, or missing for your terminal type.");

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}


SSH_FSM_STEP(sftp2_finalize_initialization)
{
  SSH_FSM_GDATA(Sftp2GData);

  SSH_FSM_SET_NEXT("get_command");
  gdata->local_connection->host = NULL;

  SSH_FSM_ASYNC_CALL(ssh_file_copy_connect(gdata->local_connection,     \
                                           local_connect_complete_cb,   \
                                           thread));
}

/* If condition is true, sets the cwd and connection variables
   for remote action, if it is false, for local action. Returns
   true if there is an error. */
int
sftp_set_cwd_and_conn(Sftp2GData gdata, int condition)
{
  if (condition)
    {
      if (gdata->remote_connection->client == NULL)
        {
          printf("Error: Not connected.\r\n");
          return -1;
        }
      gdata->connection = gdata->remote_connection;
      gdata->cwd_ctx = gdata->remote_cwd_ctx;
    }
  else
    {
      gdata->connection = gdata->local_connection;
      gdata->cwd_ctx = gdata->local_cwd_ctx;
    }
  return 0;
}

/* Callback from the readline library. Receives a line from the user
   and the thread in the context. Parses the arguments, and if
   a valid command is recognized, stores relevant information in the
   gdata structure and passes control to a command-specific state.
   */
void
rl_test_cb(const char *line, void *context)
{
#define SSH_SFTP2_CMDLINE_WORDS 256
  char *cmdline = NULL, *cmdline_words[SSH_SFTP2_CMDLINE_WORDS];
  int words, k;
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata((SshFSMThread)context);
  SshFSMThread thread = (SshFSMThread)context;
  Boolean quote_active, backslash_active;

  if (!gdata->batchmode)
    SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);

  for (k = 0; k < SSH_SFTP2_CMDLINE_WORDS; k++)
    cmdline_words[k] = NULL;
  
  if (line) {
    int i = 0, j = 0;

    SSH_TRACE(10, ("Got line: %s", line));
    printf("\r\n");

    if (strlen(line) == 0)
      return;

    /* Do some command line processing: split the line into words,
       taking quotation marks and backslashes into account.

       cmdline is a tmp var for storing the processed words.
       i is index into cmdline_words (array of pointers)
       j is index into cmdline
       k is index into line
       */

    cmdline = ssh_xmalloc(strlen(line) + 32);
    i = 0; j = 0; k = 0;
    quote_active = FALSE;
    backslash_active = FALSE;
    while (i < SSH_SFTP2_CMDLINE_WORDS) 
      {
        /* Skip through the whitespace until something
           is encountered. */
        while (line[k] == ' ')
          k++;
        /* Now, whatever we have here, it'll cause a new word
           to be recorded. */
        cmdline_words[i++] = &(cmdline[j]);
        /* Copy the characters until the end of the word. */
        do
          {
            backslash_active = FALSE;
            if (line[k] == '\\')
              {
                backslash_active = TRUE;
                k++;
              }
            if (!backslash_active && line[k] == '"')
              {
                if (quote_active)
                  quote_active = FALSE;
                else
                  quote_active = TRUE;
                k++;
                continue;
              }
            if (line[k] != 0 && (quote_active || backslash_active || line[k] != ' '))
              cmdline[j++] = line[k++];
          }
        while (line[k] != 0 && (quote_active || backslash_active || line[k] != ' '));
        cmdline[j++] = 0;
        if (line[k] == 0)
          {
            if (quote_active == TRUE)
              {
                printf("Error: Unterminated quotes\r\n");
                goto exit;
              }
            break;
          }
      }
    cmdline_words[i] = NULL;
    words = i;

#if 0
    i = 0;
    while (cmdline_words[i] != NULL) {
      printf("%d: %s\r\n", i, cmdline_words[i]);
      i++;
    }
#endif

    gdata->exit_attempted = FALSE;
    
    /* Command: QUIT */
    /* Quit the program by going to the 'finish' state which
       cleans things up and exits. */
    if (!strcmp(cmdline_words[0], "quit")) {
      ssh_fsm_set_next(thread, "finish");
      goto exit;
    }

    /* Command: OPEN */
    if (!strcmp(cmdline_words[0], "open") ||
        !strcmp(cmdline_words[0], "localopen")) {
      /* Free the previous hostname in any case */
      if (gdata->remote_connection->host != NULL) {
        ssh_xfree(gdata->remote_connection->host);
        gdata->remote_connection->host = NULL;
      }
      
      /* Handle the local connection (no hostname given)
         and remote connection (dup the given hostname) */
      if (!strcmp(cmdline_words[0], "open"))
        {
          if (cmdline_words[1] == NULL) {
            printf("Error: no hostname given.\r\n");
            goto exit;
          } else {
            printf("Opening connection to %s\r\n", cmdline_words[1]);
            gdata->remote_connection->host = ssh_xstrdup(cmdline_words[1]);
          }
        }
      else
        printf("Opening a local connection\r\n");
      /* hostname is now set (to NULL for local connection),
         so just go to state command_open */
      ssh_fsm_set_next(thread, "command_open");
      goto exit;
    }

    /* Allocate a remote SftpCwd context if it does not exist yet and
       the connection has been opened. (If the connection is not open,
       every command that needs the SftpCwd ctx will complain about
       connection not being open). */
    if (gdata->remote_cwd_ctx == NULL &&
        gdata->remote_connection->client != NULL)
      {
        gdata->remote_cwd_ctx =
          ssh_sftp_cwd_init(gdata->remote_connection->client);
      }

    /* Command: CLOSE */
    if (!strcmp(cmdline_words[0], "close")) {
      /* XXX remember to deallocate and zero the cwd context */
      ssh_file_client_destroy(gdata->remote_connection->client);
      gdata->remote_connection->client = NULL;
      ssh_sftp_cwd_uninit(gdata->remote_cwd_ctx);
      gdata->remote_cwd_ctx = NULL;
      goto exit;
    }

    if (!strcmp(cmdline_words[0], "help")) {
      ssh_sftp_print_help(cmdline_words[1]);
      goto exit;
    }
    
    /* Command: CD */
    if (!strcmp(cmdline_words[0], "cd") || !strcmp(cmdline_words[0], "lcd")) {
      if (cmdline_words[1] == NULL)
        {
          printf("Error: No directory given to command 'cd'.\r\n");
          goto exit;
        }
      /* Set the connection and cwd variables. */
      if (sftp_set_cwd_and_conn(gdata, !strcmp(cmdline_words[0], "cd")))
        goto exit;

      gdata->cmd_cd_new_path = ssh_xstrdup(cmdline_words[1]);
      ssh_fsm_set_next(thread, "command_cd_start");
      goto exit;
    }
    
    /* Command: RM and RMDIR */
    if (!strcmp(cmdline_words[0], "rm") || !strcmp(cmdline_words[0], "lrm")
        || !strcmp(cmdline_words[0], "rmdir")
        || !strcmp(cmdline_words[0], "lrmdir")) {
      if (cmdline_words[1] == NULL)
        {
          printf("Error: No argument given.\r\n");
          goto exit;
        }
      if (cmdline_words[2] != NULL)
        printf("Warning: This command only takes one argument (rest are ignored).\r\n");
      /* Set the connection and cwd variables. */
      if (sftp_set_cwd_and_conn(gdata,(!strcmp(cmdline_words[0], "rm") ||
                                       !strcmp(cmdline_words[0], "rmdir"))))
        goto exit;

      gdata->cmd_rm_target =
        ssh_sftp_cwd_add(cmdline_words[1], gdata->cwd_ctx);

      if (!strcmp(cmdline_words[0], "rmdir")
          || !strcmp(cmdline_words[0], "lrmdir"))
        gdata->cmd_rm_directory = TRUE;
      else
        gdata->cmd_rm_directory = FALSE;

      ssh_fsm_set_next(thread, "command_rm_start");
      goto exit;
    }
    
    /* Command: MKDIR */
    if (!strcmp(cmdline_words[0], "mkdir") ||
        !strcmp(cmdline_words[0], "lmkdir")) {
      if (cmdline_words[1] == NULL)
        {
          printf("Error: No argument given to command 'mkdir'.\r\n");
          goto exit;
        }
      if (cmdline_words[2] != NULL)
        printf("Warning: This command only takes one argument (rest are ignored).\r\n");
      /* Set the connection and cwd variables. */
      if (sftp_set_cwd_and_conn(gdata, !strcmp(cmdline_words[0], "mkdir")))
        goto exit;

      gdata->cmd_mkdir_target =
        ssh_sftp_cwd_add(cmdline_words[1], gdata->cwd_ctx);
      gdata->cmd_mkdir_attrs.flags = 0;
      ssh_fsm_set_next(thread, "command_mkdir_start");
      goto exit;
    }
    
    /* Command: PWD */
    if (!strcmp(cmdline_words[0], "pwd") ||
        !strcmp(cmdline_words[0], "lpwd")) {
      
      /* Set the connection and cwd variables. */
      if (sftp_set_cwd_and_conn(gdata, !strcmp(cmdline_words[0], "pwd")))
        goto exit;

      ssh_fsm_set_next(thread, "command_pwd_start");
      goto exit;
    }

    /* Command: LS */
    if (!strcmp(cmdline_words[0], "ls") || !strcmp(cmdline_words[0], "lls")) {
      SshGetOptData getopt_data;
      int ch;

      /* Set the connection and cwd variables. */
      if (sftp_set_cwd_and_conn(gdata, !strcmp(cmdline_words[0], "ls")))
        goto exit;

      /* We'll use our own getopt data structure */
      getopt_data = ssh_xmalloc(sizeof(struct SshGetOptDataRec));
      ssh_getopt_init_data(getopt_data);
      
      /* Go to state command_ls_pre_glob unless option parsing below
         reports an error and directs us back to get_command. */
      ssh_fsm_set_next(thread, "command_ls_pre_glob");
      
      /* Parse the options and construct the filelist (given
         after the command). Put these in the gdata structure. */
      gdata->cmd_ls_recursive = FALSE;
      gdata->cmd_ls_long_format = FALSE;
      gdata->cmd_ls_no_dot_prefix = FALSE;
      while ((ch = ssh_getopt(words, cmdline_words, "Rl",
                              getopt_data)) != -1)
        {
          switch(ch)
            {
            case 'R':
              gdata->cmd_ls_recursive = TRUE;
              break;
            case 'l':
              gdata->cmd_ls_long_format = TRUE;
              break;
            default:
              ssh_fsm_set_next(thread, "get_command");
              ssh_xfree(getopt_data);
              fprintf(stderr, "\r");
              goto exit;
              break;
            }
        }
      if (gdata->cmd_ls_recursive == TRUE)
        gdata->cmd_ls_long_format = FALSE; /* XXX temporary fix */

      /* After the options we (may) have a list of filenames.
         Construct a filecopylocation structure from the filenames. */
      gdata->cmd_ls_files_pre_glob = ssh_file_copy_location_allocate(TRUE);
      
      /* If there are no arguments, dot is the directory to list. */
      if (getopt_data->ind == words)
        {
          unsigned char *tmp_str = ssh_sftp_cwd_add(".", gdata->cwd_ctx);
          ssh_file_copy_location_add_raw_file(gdata->cmd_ls_files_pre_glob,
                                              tmp_str);
          gdata->cmd_ls_no_dot_prefix = TRUE;
          ssh_xfree(tmp_str);
        }
      
      while (getopt_data->ind < words)
        {
          unsigned char *tmp_str;
          /* If path is not absolute, prepend it with our current path */
          if (cmdline_words[getopt_data->ind][0] != '/')
            {
              tmp_str = ssh_sftp_cwd_add(cmdline_words[getopt_data->ind],
                                         gdata->cwd_ctx);
              ssh_file_copy_location_add_raw_file(gdata->cmd_ls_files_pre_glob,
                                                  tmp_str);
              ssh_xfree(tmp_str);
            }
          else
            {
              ssh_file_copy_location_add_raw_file(gdata->cmd_ls_files_pre_glob,
                                                  cmdline_words[getopt_data->ind]);
            }
          getopt_data->ind++;
        }
      
      ssh_xfree(getopt_data);
      goto exit;
    }

    /* Command: GET, MGET */
    if (!strcmp(cmdline_words[0], "get") ||
        !strcmp(cmdline_words[0], "mget") ||
        !strcmp(cmdline_words[0], "put") ||
        !strcmp(cmdline_words[0], "mput")) {
      SshGetOptData getopt_data;
      int ch;

      if (gdata->remote_connection->client == NULL)
        {
          printf("Error: Not connected.\r\n");
          goto exit;
        }

      /* Mark the direction of the transfer. */
      if (!strcmp(cmdline_words[0], "get") ||
          !strcmp(cmdline_words[0], "mget"))
        gdata->cmd_getput_direction = SSH_SFTP_GET;
      else
        gdata->cmd_getput_direction = SSH_SFTP_PUT;

      /* Set the connection and cwd variables. */
      if (gdata->cmd_getput_direction == SSH_SFTP_GET)
        {
          gdata->connection = gdata->remote_connection;
          gdata->cwd_ctx = gdata->remote_cwd_ctx;
        }
      else
        {
          gdata->connection = gdata->local_connection;
          gdata->cwd_ctx = gdata->local_cwd_ctx;
        }

      /* We'll use our own getopt data structure */
      getopt_data = ssh_xmalloc(sizeof(struct SshGetOptDataRec));
      ssh_getopt_init_data(getopt_data);

      /* Go to state command_ls_pre_glob unless option parsing below
         reports an error and directs us back to get_command. */
      ssh_fsm_set_next(thread, "command_getput_pre_glob");
      
      /* Parse the options and construct the filelist (given
         after the command). Put these in the gdata structure. */
      while ((ch = ssh_getopt(words, cmdline_words, "",
                              getopt_data)) != -1)
        {
          switch(ch)
            {
            default:
              ssh_fsm_set_next(thread, "get_command");
              ssh_xfree(getopt_data);
              goto exit;
              break;
            }
        }
      
      /* If there are no arguments, complain. */
      if (getopt_data->ind == words)
        {
          printf("Error: No arguments given to %s.\r\n", cmdline_words[0]);
          ssh_fsm_set_next(thread, "get_command");
          ssh_xfree(getopt_data);
          goto exit;
        }
      
      /* After the options we have a list of filenames.
         Construct a filecopylocation structure from the filenames. */
      gdata->cmd_getput_files_pre_glob =
        ssh_file_copy_location_allocate(TRUE);
      
      while (getopt_data->ind < words)
        {
          unsigned char *tmp_str;
          /* If path is not absolute, prepend it with our current path */
          if (cmdline_words[getopt_data->ind][0] != '/')
            {
              tmp_str = ssh_sftp_cwd_add(cmdline_words[getopt_data->ind],
                                         gdata->cwd_ctx);
              ssh_file_copy_location_add_raw_file(gdata->cmd_getput_files_pre_glob,
                                                  tmp_str);
              ssh_xfree(tmp_str);
            }
          else
            {
              ssh_file_copy_location_add_raw_file(gdata->cmd_getput_files_pre_glob,
                                                  cmdline_words[getopt_data->ind]);
            }
          getopt_data->ind++;
        }
      
      ssh_xfree(getopt_data);
      goto exit;
    }

    printf("Unrecognized command line: '%s'\r\n", line);
  }
  else
    {
      printf("\r\n");
      ssh_fsm_set_next(thread, "finish");
    }
  
exit:
  if (cmdline)
    ssh_xfree(cmdline);
  return;
}

SSH_FSM_STEP(sftp2_get_command)
{
  SSH_FSM_GDATA(Sftp2GData);
  unsigned char *tmp_ptr;

  /* If the initial connection target was specified on the command
     line, try to connect before reading any commands from the user. */
  if (gdata->initial_connect_target != NULL)
    {
      gdata->remote_connection->host =
        ssh_xstrdup(gdata->initial_connect_target);
      gdata->initial_connect_target = NULL;
      SSH_FSM_SET_NEXT("command_open");
      return SSH_FSM_CONTINUE;
    }

  if (gdata->batchmode)
    {
      if (gdata->batchfile_data == NULL)
        {
          ssh_read_file(gdata->batchfile_name, &gdata->batchfile_data,
                        &gdata->batchfile_len);
          if (gdata->batchfile_data == NULL)
            {
              printf("Error: Could not read the batchfile.\r\n");
              SSH_FSM_SET_NEXT("finish");
              return SSH_FSM_CONTINUE;
            }
          gdata->batchfile_ptr = gdata->batchfile_data;
        }
      /* Now the batchfile_ptr points to the start of the line to be
         executed. */
      tmp_ptr = gdata->batchfile_ptr;
      while (gdata->batchfile_ptr < gdata->batchfile_data + gdata->batchfile_len
             && *gdata->batchfile_ptr != '\n')
        {
          gdata->batchfile_ptr++;
        }
      if (gdata->batchfile_ptr == gdata->batchfile_data + gdata->batchfile_len)
        {
          SSH_FSM_SET_NEXT("finish");
          return SSH_FSM_CONTINUE;
        }
      *gdata->batchfile_ptr = 0;
      gdata->batchfile_ptr++;
      printf("sftp> %s", tmp_ptr);
      rl_test_cb(ssh_xstrdup(tmp_ptr), thread);
      return SSH_FSM_CONTINUE;
    }
  else
    {
      SSH_FSM_ASYNC_CALL(ssh_readline_eloop("sftp> ", NULL, gdata->rl,
                                            rl_test_cb, thread));
    }
}

/* CD IMPLEMENTATION */

void
command_cd_cb(SshSftpCwdResult result, void *context)
{
  SshFSMThread thread = (SshFSMThread)context;
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata(thread);

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
  ssh_fsm_set_next(thread, "command_pwd_start");
  /*  ssh_fsm_set_next(thread, "get_command"); */

  ssh_xfree(gdata->cmd_cd_new_path);
  if (result == SSH_SFTP_CWD_ERROR)
    {
      printf("CD failed.\r\n");
      /* Do not try to continue if cd failed in batchmode. */
      if (gdata->batchmode)
        {
          printf("Aborting batchmode.\r\n");
          ssh_fsm_set_next(thread, "finish");
          return;
        }
    }
  return;
}

SSH_FSM_STEP(sftp2_command_cd_start)
{
  SSH_FSM_GDATA(Sftp2GData);

  SSH_FSM_ASYNC_CALL(ssh_sftp_cwd_change(gdata->cmd_cd_new_path,
                                         gdata->cwd_ctx,
                                         command_cd_cb, thread));
}

/* RESTART FUNCTION */

/* This will receive a timeout callback and restart the thread,
   for continuing a FileXfer command that has failed with
   SSH_FX_NO_CONNECTION. (This can happen for a while after
   opening a new connection). */
void
command_restart_cb(void *context)
{
  SshFSMThread thread = (SshFSMThread)context;
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
  return;
}

/* PWD IMPLEMENTATION */

void
command_pwd_cb(SshFileClientError error,
              const char *name,
              const char *long_name,
              SshFileAttributes attrs,
              void *context)
{
  SshFSMThread thread = (SshFSMThread)context;
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata(thread);

  if (error == SSH_FX_NO_CONNECTION)
    {
      /* Schedule a timeout to try again. */
      ssh_fsm_set_next(thread, "command_pwd_start");
      ssh_register_timeout(1L, 0L, command_restart_cb, context);
      return;
    }

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
  ssh_fsm_set_next(thread, "get_command");

  ssh_xfree(gdata->cmd_pwd_path);
  if (error != SSH_FX_OK)
    {
      printf("PWD failed.\r\n");
      return;
    }
  printf("%s\r\n", name);
  return;
}

SSH_FSM_STEP(sftp2_command_pwd_start)
{
  SSH_FSM_GDATA(Sftp2GData);

  /* Get the sftp-internal cwd and feed it to the realpath
     resolver, and print the results in the callback. */

  gdata->cmd_pwd_path = ssh_sftp_cwd_add(NULL, gdata->cwd_ctx);
  SSH_FSM_ASYNC_CALL(ssh_file_client_realpath(gdata->connection->client,
                                              gdata->cmd_pwd_path,
                                              command_pwd_cb, thread));
}

/* OPEN IMPLEMENTATION */

void open_completion_cb(SshFileClient client,
                        void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SSH_FSM_GDATA(Sftp2GData);
  
  gdata->remote_connection->client = client;

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(sftp2_command_open)
{
  SSH_FSM_GDATA(Sftp2GData);

  SSH_FSM_SET_NEXT("command_finalize_open");

  SSH_FSM_ASYNC_CALL(ssh_file_copy_connect(gdata->remote_connection,    \
                                           open_completion_cb,          \
                                           thread));
}

SSH_FSM_STEP(sftp2_command_finalize_open)
{
  SSH_FSM_GDATA(Sftp2GData);

  if (gdata->remote_connection->client == NULL)
    {
      if (gdata->remote_connection->host == NULL)
        printf("Local connection failed.\r\n");
      else
        printf("Connecting to %s failed.\r\n",
               gdata->remote_connection->host);
    }

  ssh_fsm_set_next(thread, "get_command");
  return SSH_FSM_CONTINUE;
}

/* RM AND RMDIR IMPLEMENTATION */

void
command_rm_cb(SshFileClientError error, void *context)
{
  SshFSMThread thread = (SshFSMThread)context;
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata(thread);

  if (error == SSH_FX_NO_CONNECTION)
    {
      /* Schedule a timeout to try again. */
      ssh_fsm_set_next(thread, "command_rm_start");
      ssh_register_timeout(1L, 0L, command_restart_cb, context);
      return;
    }

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
  ssh_fsm_set_next(thread, "get_command");

  ssh_xfree(gdata->cmd_rm_target);
  if (error != SSH_FX_OK)
    printf("Command failed.\r\n");
  return;
}

SSH_FSM_STEP(sftp2_command_rm_start)
{
  SSH_FSM_GDATA(Sftp2GData);
  if (gdata->cmd_rm_directory)
    SSH_FSM_ASYNC_CALL(ssh_file_client_rmdir(gdata->connection->client,
                                              gdata->cmd_rm_target,
                                              command_rm_cb, thread));
  else
    SSH_FSM_ASYNC_CALL(ssh_file_client_remove(gdata->connection->client,
                                              gdata->cmd_rm_target,
                                              command_rm_cb, thread));
}

/* MKDIR IMPLEMENTATION */

void
command_mkdir_cb(SshFileClientError error, void *context)
{
  SshFSMThread thread = (SshFSMThread)context;
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata(thread);

  if (error == SSH_FX_NO_CONNECTION)
    {
      /* Schedule a timeout to try again. */
      ssh_fsm_set_next(thread, "command_mkdir_start");
      ssh_register_timeout(1L, 0L, command_restart_cb, context);
      return;
    }

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
  ssh_fsm_set_next(thread, "get_command");

  ssh_xfree(gdata->cmd_mkdir_target);
  if (error != SSH_FX_OK)
    printf("Command failed.\r\n");
  return;
}

SSH_FSM_STEP(sftp2_command_mkdir_start)
{
  SSH_FSM_GDATA(Sftp2GData);
  SSH_FSM_ASYNC_CALL(ssh_file_client_mkdir(gdata->connection->client,
                                           gdata->cmd_mkdir_target,
                                           &gdata->cmd_mkdir_attrs,
                                           command_mkdir_cb, thread));
}

/* GETPUT IMPLEMENTATION */

/* Receive the munged filelist from the globbing function. Go to
   command_getput_pre_recurse and call the recurse_dirs function
   from there. */
void
command_getput_glob_ready_cb(SshFileCopyError error,
                             const char *error_message,
                             SshFileCopyConnection connection,
                             /* list of items of type SshFileCopyFileListItem. */
                             SshDlList file_list, void *context)
{
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata((SshFSMThread)context);
  SshFSMThread thread = (SshFSMThread)context;
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
  if (error == SSH_FC_OK)
    {
      ssh_fsm_set_next(thread, "command_getput_pre_recurse");
      gdata->cmd_getput_files_pre_recurse = file_list;
    }
  else
    {
      printf("Error while globbing.\r\n");
      printf("error message: \"%s\"\r\n", error_message);
      ssh_fsm_set_next(thread, "get_command");
    }
  return;
}

void
command_getput_glob_error_cb(SshFileCopyError error,
                             const char *error_message,
                             void *context)
{
  printf("%s\r\n", error_message);  
}


SSH_FSM_STEP(sftp2_command_getput_pre_glob)
{
  SSH_FSM_GDATA(Sftp2GData);

  SSH_FSM_ASYNC_CALL(ssh_file_copy_glob(gdata->connection,
                                        gdata->cmd_getput_files_pre_glob,
                                        NULL,
                                        command_getput_glob_ready_cb,
                                        command_getput_glob_error_cb,
                                        thread));
}

void
command_getput_recurse_ready_cb(SshFileCopyError error,
                                const char *error_message,
                                SshFileCopyConnection connection,
                                /* List of items of type
                                   SshFileCopyFileListItem. */
                                SshDlList file_list,
                                void *context)
{
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata((SshFSMThread)context);
  SshFSMThread thread = (SshFSMThread)context;
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
  if (error == SSH_FC_OK)
    {
      gdata->cmd_getput_files_pre_transfer = file_list;
      ssh_fsm_set_next(thread, "command_getput_pre_transfer");
    }
  else
    {
      ssh_fsm_set_next(thread, "get_command");
    }
  return;
}

void
command_getput_recurse_error_cb(SshFileCopyError error,
                                const char *error_message,
                                void *context)
{
  /*  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata((SshFSMThread)context); */
  /*  SshFSMThread thread = (SshFSMThread)context; */
  printf("%s\r\n", error_message);
}

SSH_FSM_STEP(sftp2_command_getput_pre_recurse)
{
  SSH_FSM_GDATA(Sftp2GData);
  SSH_FSM_ASYNC_CALL(ssh_file_copy_recurse_dirs(gdata->connection,
                                      gdata->cmd_getput_files_pre_recurse,
                                      gdata->recurse_attrs, NULL,
                                      command_getput_recurse_ready_cb,
                                      command_getput_recurse_error_cb,
                                      thread));
}

void
command_getput_transfer_ready_cb(SshFileCopyError error,
                                const char *error_message,
                                void *context)
{
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata((SshFSMThread)context);
  SshFSMThread thread = (SshFSMThread)context;
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
  gdata->transfer_op_handle = NULL;
  ssh_fsm_set_next(thread, "get_command");
  ssh_file_copy_location_destroy(gdata->cmd_getput_dest_location);
}

void
command_getput_transfer_error_cb(SshFileCopyError error,
                                const char *error_message,
                                void *context)
{
  /* Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata((SshFSMThread)context);
  SshFSMThread thread = (SshFSMThread)context; */
  printf("%s\r\n", error_message);
}

char *stat_eta(SshUInt64 secs)
{
  static char stat_result[9];
  int hours, mins;

   hours = secs / 3600L;
   secs %= 3600L;
   mins = secs / 60L;
   secs %= 60L;

   snprintf(stat_result, sizeof(stat_result),
            "%02d:%02d:%02d", hours, mins, (int)secs);
   return(stat_result);
}

void
command_getput_transfer_progress_cb(SshFileCopyConnection source,
                                    SshFileCopyFile source_file,
                                    SshFileCopyConnection dest,
                                    SshFileCopyFile dest_file,
                                    off_t read_bytes,
                                    off_t written_bytes,
                                    SshUInt64 elapsed_time,
                                    void *context)
{
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata((SshFSMThread)context);
  SshFileAttributes attrs;
  double transfer_rate;
  Boolean at_end = FALSE;
  char *eta_string;
  static int prev_width = -1;
  static char format_string[1024];

  attrs = ssh_file_copy_file_get_attributes(source_file);

  at_end = (written_bytes >= attrs->size) ? TRUE : FALSE;

  if (elapsed_time <= 0)
    elapsed_time = 1L;
  
  transfer_rate = (double) written_bytes / elapsed_time;
  
  if (!at_end)
    eta_string = stat_eta((attrs->size - written_bytes) /
                          (transfer_rate < 1.0 ? 1.0 : transfer_rate));
  else
    eta_string = stat_eta(elapsed_time);

  if (gdata->screen_width < SFTP2_DEFAULT_SCREEN_WIDTH)
    gdata->screen_width = SFTP2_DEFAULT_SCREEN_WIDTH;

    /* If screen width is changed, reconstruct the format string. */
  if (prev_width != gdata->screen_width)
    {
      prev_width = gdata->screen_width;
      snprintf(format_string, sizeof(format_string),
           "\r%%-%d.%ds | %%10ld kB | %%5.1f kB/s | %%3.3s: %%s | %%3d%%%%",
               gdata->screen_width - 53, gdata->screen_width - 53);
    }
  
  /* Stolen from scp2 (which in turn stole this from scp1...) */

  fprintf(stderr, format_string,
          ssh_file_copy_file_get_name(source_file),
          (SshUInt32)written_bytes / 1024,
          (double)(transfer_rate / 1024),
          (written_bytes < attrs->size) ? "ETA" : "TOC",
          eta_string,
          attrs->size ? (int) (100.0 * (double) written_bytes /
                               (SshUInt32) attrs->size) : 100);

  if (written_bytes >= attrs->size)
    {
      /* This file is at it's end. */
      fprintf(stderr, "\r\n");
    }
}

SSH_FSM_STEP(sftp2_command_getput_pre_transfer)
{
  SSH_FSM_GDATA(Sftp2GData);
  unsigned char *tmp_str;

  /* Allocate the destination location structure. Free this in the
     ready/error callback. */
  gdata->cmd_getput_dest_location = ssh_file_copy_location_allocate(FALSE);

  /* Choose the connections depending on the command. */
  if (gdata->cmd_getput_direction == SSH_SFTP_GET)
    {
      tmp_str = ssh_sftp_cwd_add(".", gdata->local_cwd_ctx);
      ssh_file_copy_location_add_raw_file(gdata->cmd_getput_dest_location,
                                          tmp_str);
      ssh_xfree(tmp_str);
      SSH_FSM_ASYNC_CALL(gdata->transfer_op_handle =
                         ssh_file_copy_transfer_files
                         (gdata->remote_connection,
                          gdata->cmd_getput_files_pre_transfer,
                          gdata->local_connection,
                          gdata->cmd_getput_dest_location,
                          gdata->transfer_attrs,
                          command_getput_transfer_ready_cb,
                          command_getput_transfer_error_cb,
                          command_getput_transfer_progress_cb,
                          NULL,
                          thread));
    }
  else
    {
      /* SSH_SFTP_PUT */
      tmp_str = ssh_sftp_cwd_add(".", gdata->remote_cwd_ctx);
      ssh_file_copy_location_add_raw_file(gdata->cmd_getput_dest_location,
                                          tmp_str);
      ssh_xfree(tmp_str);
      SSH_FSM_ASYNC_CALL(gdata->transfer_op_handle =
                         ssh_file_copy_transfer_files
                         (gdata->local_connection,
                          gdata->cmd_getput_files_pre_transfer,
                          gdata->remote_connection,
                          gdata->cmd_getput_dest_location,
                          gdata->transfer_attrs,
                          command_getput_transfer_ready_cb,
                          command_getput_transfer_error_cb,
                          command_getput_transfer_progress_cb,
                          NULL,
                          thread));
    }
}

/* LS IMPLEMENTATION */

/* The state-callback-mess may be a little hard to follow. */
void
command_ls_glob_ready_cb(SshFileCopyError error,
                         const char *error_message,
                         SshFileCopyConnection connection,
                         /* List of items of type SshFileCopyFileListItem. */
                         SshDlList file_list, void *context)
{
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata((SshFSMThread)context);
  SshFSMThread thread = (SshFSMThread)context;
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
  if (error == SSH_FC_OK)
    {
      ssh_fsm_set_next(thread, "command_ls_pre_recurse");
      gdata->cmd_ls_files_pre_recurse = file_list;
    }
  else
    {
      printf("Error while globbing.\r\n");
      printf("error message: \"%s\"", error_message);
      ssh_fsm_set_next(thread, "get_command");
    }
  return;
}

void command_ls_glob_error_cb(SshFileCopyError error,
                              const char *error_message,
                              void *context)
{
  printf("%s\r\n", error_message);
}

struct SshSftpCmdLsCtxRec
{
  Boolean name_found;
  unsigned char *name;
};

typedef struct SshSftpCmdLsCtxRec *SshSftpCmdLsCtx;

void *
command_ls_list_examiner(void *item, void *ctx)
{
  SshSftpCmdLsCtx ls_ctx = (SshSftpCmdLsCtx)ctx;
  SSH_TRACE(10, ("Comparing %s and %s, result %d", ssh_file_copy_file_list_item_get_name((SshFileCopyFileListItem)item), ls_ctx->name, strcmp(ssh_file_copy_file_list_item_get_name((SshFileCopyFileListItem)item), ls_ctx->name)));
  if (!strcmp(ssh_file_copy_file_list_item_get_name((SshFileCopyFileListItem)item), ls_ctx->name))
    ls_ctx->name_found = TRUE;
  return item;
}

Boolean
command_ls_recurse_file_cb(const char *filename,
                           const char *long_name,
                           SshFileAttributes
                           file_attributes,
                           SshFileCopyRecurseAttrs attrs,
                           void *context)
{
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata((SshFSMThread)context);
  unsigned char *tmp_str;
  Boolean retval;
  
  /* XXX Tuning needed (mainly the output format with -R)
     SshFileCopy does not allow a sane listing from ls -lR,
     so we'll forget that for a while.
     XXX: Rewrite the ls to use sshfilexfer instead of
     sshfilecopy. */

  /* This will make the thread visit the post_recurse state when
     it has a chance. There it will kick the pager thread. */
  ssh_fsm_continue((SshFSMThread)context);
  
  tmp_str = ssh_sftp_cwd_strip(filename, gdata->cwd_ctx,
                               gdata->cmd_ls_no_dot_prefix);
  if (gdata->cmd_ls_long_format == TRUE)
    {
      if (long_name != NULL)
        {
          ssh_sftp_pager_write_string(gdata->pager_ctx, long_name);
          ssh_sftp_pager_write_string(gdata->pager_ctx, "\r\n");
        }
    }
  else
    {
      if (tmp_str != NULL)
        {
          ssh_sftp_pager_write_string(gdata->pager_ctx, tmp_str);
          ssh_sftp_pager_write_string(gdata->pager_ctx, "\r\n");
        }
      else
        {
          ssh_sftp_pager_write_string(gdata->pager_ctx, filename);
          ssh_sftp_pager_write_string(gdata->pager_ctx, "\r\n");
        }
    }

  /* Recurse the directory if in recursive mode or if it was given
     on the command line. */
  if (gdata->cmd_ls_recursive == TRUE)
    retval = TRUE;
  else
    {
      struct SshSftpCmdLsCtxRec ls_ctx;
      ls_ctx.name_found = FALSE;
      /* ls_ctx.name = tmp_str; */
      ls_ctx.name = (char *)filename;
      ssh_dllist_mapcar(gdata->cmd_ls_files_pre_recurse, 
                        command_ls_list_examiner, &ls_ctx);
      retval = ls_ctx.name_found;
    }
  if (tmp_str != NULL)
    ssh_xfree(tmp_str);
  return retval;
}

void
command_ls_recurse_ready_cb(SshFileCopyError error,
                            const char *error_message,
                            SshFileCopyConnection connection,
                            /* List of items of type
                               SshFileCopyFileListItem. */
                            SshDlList file_list,
                            void *context)
{
  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata((SshFSMThread)context);
  SshFSMThread thread = (SshFSMThread)context;
  SSH_TRACE(4, ("Recursing ready"));
  gdata->cmd_ls_recurse_ready = TRUE;
  ssh_fsm_continue(thread);
  return;
}

void
command_ls_recurse_error_cb(SshFileCopyError error,
                            const char *error_message,
                            void *context)
{
  /*  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata((SshFSMThread)context); */
  SshFSMThread thread = (SshFSMThread)context;
  SSH_TRACE(4, ("Error while recursing"));
  printf("%s\r\n", error_message);
  /*gdata->cmd_ls_recurse_ready = TRUE;*/
  ssh_fsm_continue(thread);
}

SSH_FSM_STEP(sftp2_command_ls_pre_glob)
{
  SSH_FSM_GDATA(Sftp2GData);

  SSH_FSM_ASYNC_CALL(ssh_file_copy_glob(gdata->connection,
                                        gdata->cmd_ls_files_pre_glob,
                                        NULL,
                                        command_ls_glob_ready_cb,
                                        command_ls_glob_error_cb,
                                        thread));
}

SSH_FSM_STEP(sftp2_command_ls_pre_recurse)
{
  SSH_FSM_GDATA(Sftp2GData);

  gdata->cmd_ls_recurse_ready = FALSE;
  
  ssh_fsm_set_next(thread, "command_ls_post_recurse");

  gdata->pager_ctx = ssh_sftp_pager_init(gdata->stdio_stream, gdata->fsm,
                                         gdata->screen_width,
                                         gdata->screen_height);
  
  ssh_file_copy_recurse_dirs(gdata->connection,
                             gdata->cmd_ls_files_pre_recurse,
                             gdata->recurse_attrs,
                             command_ls_recurse_file_cb,
                             command_ls_recurse_ready_cb,
                             command_ls_recurse_error_cb,
                             thread);
  return SSH_FSM_SUSPENDED;
}

void
command_ls_pager_finish_cb(SshSftpPagerResult result,
                           void *context)
{
  SshFSMThread thread = (SshFSMThread)context;
  /*  Sftp2GData gdata = (Sftp2GData) ssh_fsm_get_gdata(thread); */
  SSH_TRACE(4, ("Pager Finished"));
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
  ssh_fsm_set_next(thread, "get_command");
}

SSH_FSM_STEP(sftp2_command_ls_post_recurse)
{
  SSH_FSM_GDATA(Sftp2GData);

  if (gdata->cmd_ls_recurse_ready == TRUE)
    {
      ssh_sftp_pager_wakeup(gdata->pager_ctx, thread);
      ssh_fsm_set_next(thread, "get_command");
      SSH_TRACE(4, ("Asking the pager to finish."));
      SSH_FSM_ASYNC_CALL(ssh_sftp_pager_uninit(gdata->pager_ctx,
                                               command_ls_pager_finish_cb,
                                               thread));
    }
  else
    {
      ssh_sftp_pager_wakeup(gdata->pager_ctx, thread);
      return SSH_FSM_SUSPENDED;
    }
}

SSH_FSM_STEP(sftp2_finish)
{
  SSH_FSM_GDATA(Sftp2GData);
  
  ssh_fsm_destroy(gdata->fsm);  

  return SSH_FSM_FINISH;
}

SshFSMStateMapItem sftp2_states [] =
{
#include "sftppager_states.h"
  { "finalize_initialization", "Finalize initialization",
    sftp2_finalize_initialization },
  { "get_command", "Prepare to read a command from user", sftp2_get_command },
  { "command_open", "Open a connection to destination host", sftp2_command_open },
  { "command_finalize_open", "Finalize open", sftp2_command_finalize_open },
  { "command_cd_start", "Cd: Start the operation",
    sftp2_command_cd_start },
  { "command_pwd_start", "Pwd: Start the operation",
    sftp2_command_pwd_start },
  { "command_rm_start", "Rm: Start the operation",
    sftp2_command_rm_start },
  { "command_mkdir_start", "Mkdir: Start the operation",
    sftp2_command_mkdir_start },
  { "command_ls_pre_glob", "Ls: prepare for globbing",
    sftp2_command_ls_pre_glob },
  { "command_ls_pre_recurse", "Ls: prepare for recurse_dirs",
    sftp2_command_ls_pre_recurse },
  { "command_ls_post_recurse", "Ls: State after recurse_dirs",
    sftp2_command_ls_post_recurse },
  { "command_getput_pre_glob", "Getput: prepare for globbing",
    sftp2_command_getput_pre_glob },
  { "command_getput_pre_recurse", "Getput: prepare for recurse_dirs",
    sftp2_command_getput_pre_recurse },
  { "command_getput_pre_transfer", "Getput: prepare for transfer of files",
    sftp2_command_getput_pre_transfer },
  { "finish", "Finish", sftp2_finish }
};  

void
sftp2_usage(Sftp2GData sftp2_gdata)
{
  SSH_PRECOND(sftp2_gdata);
  
  fprintf(stderr, "Usage: sftp2 [-D debug_level_spec] [-b batchfile] [-S path] [-h] [-V] host\n");

  sftp2_fsm_gdata_destructor(sftp2_gdata);

  exit(-1);
}

void
sftp2_fsm_gdata_destructor(void *global_data)
{
  Sftp2GData gdata = (Sftp2GData) global_data;
  /* Do sftp2-specific cleanup */
  if (gdata->ssh_path)
    ssh_xfree(gdata->ssh_path);
  if (gdata->recurse_attrs)
    ssh_xfree(gdata->recurse_attrs);
  if (gdata->transfer_attrs)
    ssh_xfree(gdata->transfer_attrs);
  if (gdata->remote_cwd_ctx)
    {
      ssh_sftp_cwd_uninit(gdata->remote_cwd_ctx);
      gdata->remote_cwd_ctx = NULL;
    }
  if (gdata->local_cwd_ctx)
    {
      ssh_sftp_cwd_uninit(gdata->local_cwd_ctx);
      gdata->local_cwd_ctx = NULL;
    }
  if (gdata->remote_connection)
    {
      ssh_file_copy_connection_destroy(gdata->remote_connection);
      gdata->remote_connection = NULL;
    }
  if (gdata->local_connection)
    {
      ssh_file_copy_connection_destroy(gdata->local_connection);
      gdata->local_connection = NULL;
    }
  if (gdata->batchfile_data)
    ssh_xfree(gdata->batchfile_data);
  if (gdata->batchfile_name)
    ssh_xfree(gdata->batchfile_name);
  if (gdata->rl)
    ssh_readline_eloop_unitialize(gdata->rl);
  if (gdata->stdio_stream)
    ssh_stream_destroy(gdata->stdio_stream);
  ssh_leave_raw_mode(-1);
}

void
sftp2_thread_destructor(void *tdata)
{
  /* XXX */
}

typedef struct SftpConnectionCtxRec
{
  SshStream stream;
  Sftp2GData gdata;
  pid_t child_pid;
  char buffer[200];
  int read_bytes;
  SshFileCopyStreamReturnCB completion_cb;
  void *completion_context;
} *SftpConnectionCtx;

void
sftp2_sigchld_handler(pid_t pid, int status, void *context)
{
  SftpConnectionCtx ctx = (SftpConnectionCtx) context;

  SSH_PRECOND(ctx);
  
  ssh_warning("child process (%s) exited with code %d.",
              ctx->gdata->ssh_path, status);

  if (ctx->gdata->rl)
    {
      ssh_readline_eloop_unitialize(ctx->gdata->rl);
      ctx->gdata->rl = NULL;
    }

  ssh_leave_raw_mode(-1);
        
  exit(SSH_FC_ERROR);
}

void new_stream_callback(SshStreamNotification notification, void *context)
{
  SftpConnectionCtx ctx = (SftpConnectionCtx) context;
  int ret = 0;

  SSH_TRACE(2, ("notification: %d", notification));

  switch (notification)
    {
    case SSH_STREAM_INPUT_AVAILABLE:

      while ((ret = ssh_stream_read(ctx->stream,
                                    &(ctx->buffer[ctx->read_bytes]), 1)) > 0 &&
             ctx->read_bytes < sizeof(ctx->buffer) - 1)
        {
          ctx->read_bytes += ret;

          SSH_TRACE(2, ("read char: %c", ctx->buffer[strlen(ctx->buffer) - 1]));
          SSH_TRACE(2, ("read_bytes: %d, buffer len: %d",       \
                        ctx->read_bytes, strlen(ctx->buffer)));
          SSH_DEBUG_HEXDUMP(2, ("received message:"),           \
                            ctx->buffer, ctx->read_bytes);
          fflush(stderr);
          if (ctx->buffer[strlen(ctx->buffer) - 1] == '\n')
            {
              SSH_TRACE(2, ("buffer: '%s'", ctx->buffer));
              if (strcmp(ctx->buffer, "AUTHENTICATED YES\n") == 0)
                {
                  (*ctx->completion_cb)(ctx->stream, ctx->completion_context);
                  return;
                }
              else
                {
                  SSH_DEBUG_HEXDUMP(3, ("received message:"),           \
                                    ctx->buffer, ctx->read_bytes);
                  ssh_fatal("ssh2 client failed to authenticate. (or you "
                            "have too old ssh2 installed, check with ssh2 -V)");
                }
            }
          
        }
      if (ret == 0)
        {
          ssh_fatal("EOF received from ssh2. ");
        }
      if (ctx->read_bytes >= sizeof(ctx->buffer) - 1)
        {
          ssh_fatal("Received corrupted (or wrong type of) data from "
                    "ssh2-client.");
        }
      break;
    case SSH_STREAM_CAN_OUTPUT:
      break;
    case SSH_STREAM_DISCONNECTED:
      SSH_TRACE(2, ("Received SSH_STREAM_DISCONNECTED from wrapped stream."));
      ssh_fatal("ssh2 client failed to authenticate. (or you "
                "have too old ssh2 installed, check with ssh2 -V)");
      /* XXX */
      break;
    }
}

void connect_callback(SshFileCopyConnection connection, void *context,
                      SshFileCopyStreamReturnCB completion_cb,
                      void *completion_context)
{
#define SSH_ARGV_SIZE   64
  SshStream client_stream;
  char *ssh_argv[SSH_ARGV_SIZE];
  int i;
  Sftp2GData gdata = (Sftp2GData) context;
  SftpConnectionCtx new_ctx;
  
#if 0
  char port_buf[16];
  ScpCipherName cipher;
  
  SSH_ASSERT(host != NULL);
  SSH_ASSERT(session != NULL);
  SSH_ASSERT(session->ssh_path != NULL);
  SSH_ASSERT(session->ssh1_path != NULL);
#endif

  i = 0;

#if 0
  if (! session->use_ssh1)
    ssh_argv[i++] = session->ssh_path;
  else 
    ssh_argv[i++] = session->ssh1_path;
#endif

  ssh_argv[i++] = gdata->ssh_path;
    
  if (connection->user != NULL)
    {
      ssh_argv[i++] = "-l";
      ssh_argv[i++] = connection->user;
    }
  if (connection->port != NULL)
    {
      ssh_argv[i++] = "-p";
      ssh_argv[i++] = connection->port;
    }  

  if (gdata->verbose_mode)
    ssh_argv[i++] = "-v";

  ssh_argv[i++] = "-x";
  ssh_argv[i++] = "-a";
  
#if 0
  if (! session->use_ssh1)
    {
#endif
      ssh_argv[i++] = "-o";
      ssh_argv[i++] = "passwordprompt %U@%H's password: ";
      ssh_argv[i++] = "-o";
      ssh_argv[i++] = "nodelay yes";
      ssh_argv[i++] = "-o";
      ssh_argv[i++] = "authenticationnotify yes";
#if 0      
    }
  else
    {
      ssh_argv[i++] = "-o";
      ssh_argv[i++] = "PasswordPromptHost yes";
      ssh_argv[i++] = "-o";
      ssh_argv[i++] = "PasswordPromptLogin yes";
    }
#endif
#if 0
  if (! session->use_ssh1)
    {
      for (cipher = session->cipher_list; 
           cipher != NULL; 
           cipher = cipher->next)
        {
          SSH_ASSERT(i < SSH_ARGV_SIZE);
          
          ssh_argv[i++] = "-c";
          ssh_argv[i++] = cipher->name;
        }
    }
  else
    {
      if (session->cipher_list != NULL)
        {
          ssh_argv[i++] = "-c";
          ssh_argv[i++] = session->cipher_list->name;
        }
    }
#endif

  if (gdata->batchmode)
    {
      ssh_argv[i++] = "-o";
      ssh_argv[i++] = "BatchMode yes";
    }
  
  ssh_argv[i++] = connection->host;

#if 0
  if (! session->use_ssh1)
    {
#endif
      
      ssh_argv[i++] = "-s";
      ssh_argv[i++] = "sftp";
#if 0
    }
  else
    {
      ssh_argv[i++] = "sftp-server";
    }
#endif
  
  ssh_argv[i] = NULL;

  SSH_ASSERT(i < SSH_ARGV_SIZE);

  if (gdata->verbose_mode)
    {
      SSH_DEBUG_INDENT;
      for (i = 0; ssh_argv[i]; i++)
        SSH_DEBUG(2, ("argv[%d] = %s", i, ssh_argv[i]));
      SSH_DEBUG_UNINDENT;
    }
  
  switch (ssh_pipe_create_and_fork(&client_stream, NULL))
    {
    case SSH_PIPE_ERROR:
      ssh_fatal("ssh_pipe_create_and_fork() failed");
      
    case SSH_PIPE_PARENT_OK:      
      new_ctx = ssh_xcalloc(1, sizeof(*new_ctx));
      new_ctx->child_pid = ssh_pipe_get_pid(client_stream);
      new_ctx->stream = client_stream;
      new_ctx->gdata = gdata;
      new_ctx->completion_cb = completion_cb;
      new_ctx->completion_context = completion_context;
      ssh_sigchld_register(new_ctx->child_pid, sftp2_sigchld_handler,
                           new_ctx);

      ssh_stream_set_callback(client_stream, new_stream_callback,
                              new_ctx);
      new_stream_callback(SSH_STREAM_INPUT_AVAILABLE,
                          new_ctx);
#ifdef SUSPEND_AFTER_FORK
      kill(getpid(), SIGSTOP);
#endif /* SUSPEND_AFTER_FORK */

      return;
    case SSH_PIPE_CHILD_OK:
      execvp(ssh_argv[0], ssh_argv);
      fprintf(stderr, "Executing ssh2 failed. Command:'");
      for (i = 0;ssh_argv[i] != NULL; i++)
        fprintf(stderr," %s", ssh_argv[i]);

      fprintf(stderr,"' System error message: '%s'\r\n", strerror(errno));
      exit(-1);
    }
  SSH_NOTREACHED;
}

int
main(int argc, char **argv)
{
  Sftp2GData gdata;
  SshFSM fsm;
  int ch;

  /* Initialize the FSM */
  fsm = ssh_fsm_allocate(sizeof(*gdata),
                         sftp2_states,
                         SSH_FSM_NUM_STATES(sftp2_states),
                         sftp2_fsm_gdata_destructor);

  gdata = ssh_fsm_get_gdata_fsm(fsm);
  memset(gdata, 0, sizeof(*gdata));
  
  gdata->fsm = fsm;
  
  /* Initialize gdata */
  gdata->ssh_path = ssh_xstrdup("ssh2");
  gdata->remote_connection = ssh_file_copy_connection_allocate();
  gdata->local_connection = ssh_file_copy_connection_allocate();
  gdata->recurse_attrs =
    ssh_xmalloc(sizeof(struct SshFileCopyRecurseAttrsRec));
  gdata->transfer_attrs =
    ssh_xmalloc(sizeof(struct SshFileCopyTransferAttrsRec));
  gdata->batchmode = FALSE;
  /*  memset(gdata->connection, 0, sizeof(SshFileCopyConnection)); */

  ssh_event_loop_initialize();
  
  /* Handle the command line options. */
  while ((ch = ssh_getopt(argc, argv, "D:S:Vhb:", NULL)) != -1)
    {
      if (!ssh_optval)
        {
          sftp2_usage(gdata);
        }
      switch(ch)
        {
        case 'D':
          gdata->debug_level = ssh_xstrdup(ssh_optarg);
          ssh_debug_set_level_string(gdata->debug_level);
          gdata->verbose_mode = TRUE;
          break;
        case 'S':
          ssh_xfree(gdata->ssh_path);
          gdata->ssh_path = ssh_xstrdup(ssh_optarg);
          break;
        case 'V':
          ssh2_version(argv[0]);
          exit(0);
          break;
        case 'h':
          sftp2_usage(gdata);
          exit(0);
          break;
        case 'b':
          gdata->batchmode = TRUE;
          gdata->batchfile_name = ssh_xstrdup(ssh_optarg);
          break;
        default:
          sftp2_usage(gdata);
          break;
        }
    }

  gdata->initial_connect_target = NULL;
  if (ssh_optind < argc - 1)
    {
      printf("Too many arguments.\n");
      sftp2_usage(gdata);
    }
  if (ssh_optind == argc - 1)
    gdata->initial_connect_target = argv[ssh_optind];

  
  ssh_debug_register_callbacks(sftp2_fatal, sftp2_warning, sftp2_debug,
                               (void *)(gdata));

  ssh_file_copy_register_connect_callback(connect_callback,
                                          gdata);

  ssh_register_signal(SIGHUP, sftp2_fatal_signal_handler,
                      (void *)gdata);
  /* Capture Ctrl-Cs, so that we can abort transfers. */
  ssh_register_signal(SIGINT, sftp2_sigint_handler,
                      (void *)gdata);

  ssh_register_signal(SIGTERM, sftp2_fatal_signal_handler,
                      (void *)gdata);
  ssh_register_signal(SIGABRT, sftp2_fatal_signal_handler,
                      (void *)gdata);

  gdata->screen_width = SFTP2_DEFAULT_SCREEN_WIDTH;
  gdata->screen_height = SFTP2_DEFAULT_SCREEN_HEIGHT;

#ifdef SIGIO
  ssh_register_signal(SIGIO, NULL, NULL);
  /* This has to be done, because for example in NetBSD, the system
     calls get interrupted without this. */
  (void)signal(SIGIO, SIG_IGN);
#endif /* SIGIO */
  
  /* ReadLine stuff. */
  gdata->stdio_stream = ssh_stream_fd_stdio();

  if (isatty(fileno(stdout)))
    {
#if defined(SIGWINCH) && defined(TIOCGWINSZ)
      /* Register a signal handler for SIGWINCH to send window change
         notifications to the server. */
      ssh_register_signal(SIGWINCH, sftp2_win_dim_change, gdata); 
#endif /* SIGWINCH && TIOCGWINSZ*/
#ifdef TIOCGWINSZ
      /* SIGWINCH not defined, call the handler now, once */
      sftp2_win_dim_change(0, gdata);
#endif /* TIOCGWINSZ */
    }
  
  gdata->transfer_attrs->destination_must_be_dir = TRUE;
  gdata->transfer_attrs->preserve_attributes = FALSE;
  gdata->transfer_attrs->unlink_source = FALSE;
  gdata->transfer_attrs->recurse_dirs = TRUE;

  gdata->exit_attempted = FALSE;

  gdata->main_thread = ssh_fsm_spawn(fsm, 0, "finalize_initialization",
                                           NULL /* XXX (??) */,
                                           sftp2_thread_destructor);

  ssh_enter_non_blocking(-1);
  
  ssh_enter_raw_mode(-1, TRUE);

  ssh_event_loop_run();

  return exit_value;
}
