/*

  scp2.c

  Author: Sami Lehtinen <sjl@ssh.fi>

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

  Secure Copy. Designed to transfer files between hosts, using ssh2.
  
 */
/* TODO:

   - lots of testing

   - -n parameter isn't yet implemented.

*/
#include "sshincludes.h"
#include "sshtty.h"
#include "ssheloop.h"
#include "sshgetopt.h"
#include "sshdllist.h"
#include "sshappcommon.h"
#include "sshfilecopy.h"
#include "sshglob.h"
#include "sshdsprintf.h"
#include "sshgetcwd.h"
#include "sshunixpipestream.h"
#include "sigchld.h"
#include "sshfsm.h"
#include "sshmiscstring.h"
#ifdef HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif /* HAVE_SYS_IOCTL_H */

#define SSH_DEBUG_MODULE "Scp2"

/* #define SUSPEND_AFTER_FORK */

#define SCP_STATS_FILE stderr
#define SCP_DEFAULT_SCREEN_WIDTH 78

/* XXX We might want to give some other errors (in addition to
   SshFileCopyErrors) out, too...*/
SshFileCopyError exit_value = SSH_FC_OK;

/* Forward declaration. */
typedef struct ScpFileListItemRec *ScpFileListItem;

/* Scp2 session record. Holds all relevant information which must be
   "globally" known. (This is passed to callbacks etc.) */
typedef struct ScpSessionRec
{
  /* program name (by which we were called), not argv[0]. */
  char *scp2_name;
  
  /* Attributes for ssh_file_copy_transfer_files(). */
  SshFileCopyTransferAttrs attrs;

  /* Set, if user wants debug output. */
  Boolean verbose_mode;

  /* If set, ssh2 is invoked with ``BatchMode yes''. */
  Boolean batch_mode;
  
  /* Set, if user wants scp to remain quiet (fatal errors are still
     displayed). */
  Boolean quiet_mode;  

  /* Width of terminal screen. */
  int screen_width;
  
  /* Remote port (if given on the command line with '-P'). */
  char *remote_port;

  /* Cipher arguments are stored in this list. This may be an empty
     list, in which case we let ssh2 use the default ciphers. */
  SshDlList cipher_list;

  /* Optional path to ssh. */
  char *ssh_path;

  /* Optional debug level as a C-string. */
  char *debug_level;

  /* If set, indicates that scp is operating in a tty, and therefore
     can print things. */
  Boolean have_tty;

  /* If set, scp doesn't show progress indicator. */
  Boolean no_statistics;

  /* If set, scp removes source file after copying (if it is
     successful, that is). */
  Boolean unlink_after_copy;

  /* If set, scp only shows what would've happened if copying had
     taken place. */
  Boolean do_not_copy;

  /* pid of the ssh child scp executes. */
  pid_t ssh_child_pid;

  /* Exit value of scp2. */
  SshFileCopyError exit_value;

  /* This the list of files and locations, that the user specifies on
     the command-line. These locations may consists of directories to
     be recursed, or there may be glob-patterns. These are parsed, and
     the results are put to final_file_list. */
  SshDlList original_file_list;

  /* This is the final file list, after everything related to globbing
     and directory recursion is done. */
  SshDlList final_file_list;

  /* The destination file. */
  ScpFileListItem destination;

  /* Pointer finite state machine internal struct. */
  SshFSM fsm;

  /* Pointer to main thread of FSM. */
  SshFSMThread main_thread;

  /* Operation handle for ssh_file_copy_transfer_files, which can be
     used to abort the operation. */
  SshOperationHandle transfer_op_handle;
} *ScpSession;

/* Structure, which holds a connection and the files to be transferred
   to/from that location. */
struct ScpFileListItemRec
{
  SshFileCopyConnection *connection;
  SshFileCopyLocation location;
};

typedef struct ScpFinalFileListItemRec
{
  SshFileCopyConnection connection;
  SshDlList file_list;
} *ScpFinalFileListItem;

typedef struct ScpThreadContextRec
{
  SshDlList new_final_file_list;
} *ScpThreadContext;

typedef struct ScpConnectionCtxRec
{
  SshStream stream;
  ScpSession session;
  pid_t child_pid;
  char buffer[200];
  int read_bytes;
  SshFileCopyStreamReturnCB completion_cb;
  void *completion_context;
} *ScpConnectionCtx;

void 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);

/* Callback for return values of ssh_file_copy_glob() */
void glob_callback(SshFileCopyError error,
                   SshFileCopyConnection connection,
                   SshDlList file_list,
                   void *context);

/* Callback used by sshfilecopy to get a stream (by connecting with
   ssh and wrapping that to stream). */
void connect_callback(SshFileCopyConnection connection, void *context,
                      SshFileCopyStreamReturnCB completion_cb,
                      void *completion_context);

/* Parse a string to a SshFileCopyConnection structure. String is of
   form [user@]host[#port] . In case of an error or if the given
   string is malformed, return TRUE. Otherwise, return FALSE. filename
   is returned in return_filename, and it is malloced inside
   scp_location_parse(). */
Boolean scp_location_parse(SshFileCopyConnection connection,
                           char **return_filename,
                           const char *orig_string);

/* Find a matching SshFileCopyConnection from file_list. file_list
   MUST contain ScpFileListItems. Return matching
   SshFileCopyConnection if found, or NULL if not. */
ScpFileListItem
scp_file_list_find_matching_connection(SshDlList file_list,
                                       SshFileCopyConnection connection);

/* Destroy a ScpFileListItem structure. */
void scp_file_list_item_destroy(void *item);

/* Destroy a ScpFinalFileListItem structure. */
void scp_final_file_list_item_destroy(void *item);

/* Initializes an ScpSession context. Does not allocate the main
   struct. */
void scp_session_initialize(ScpSession session)
{
  memset(session, 0, sizeof(*session));
  session->cipher_list = ssh_dllist_allocate();
  session->original_file_list = ssh_dllist_allocate();
  session->final_file_list = ssh_dllist_allocate();
  session->ssh_path = ssh_xstrdup("ssh2");
  session->attrs = ssh_xcalloc(1, sizeof(*session->attrs));
}

void scp_sigchld_handler(pid_t pid, int status, void *context)
{
  ScpConnectionCtx new_ctx = (ScpConnectionCtx) context;

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

  exit(SSH_FC_ERROR);
}

/* Frees ScpSession contents. Does not free the main struct. */
void scp_session_destroy(ScpSession session)
{
  ssh_xfree(session->ssh_path);
  ssh_xfree(session->debug_level);
  ssh_xfree(session->remote_port);

  ssh_app_free_list(session->cipher_list, NULL);
  ssh_app_free_list(session->original_file_list,
                    scp_file_list_item_destroy);
  if (session->final_file_list)
    ssh_app_free_list(session->final_file_list,
                      scp_final_file_list_item_destroy);

  ssh_xfree(session->attrs);
  ssh_file_copy_connection_destroy(*session->destination->connection);
  ssh_xfree(session->destination->connection);
}

/* Allocate a file list item.*/
ScpFileListItem scp_file_list_item_allocate(Boolean source)
{
  ScpFileListItem item;
  SshFileCopyConnection *connection;

  connection = ssh_xmalloc(sizeof(SshFileCopyConnection));
  
  item = ssh_xcalloc(1, sizeof(*item));

  *connection = ssh_file_copy_connection_allocate();
  item->connection = connection;
  item->location = ssh_file_copy_location_allocate(source);

  return item;
}

void scp_final_file_list_item_destroy(void *item)
{
  ScpFinalFileListItem list_item = (ScpFinalFileListItem) item;
  SSH_DEBUG(4, ("Destroying ScpFinalFileListItem..."));

  ssh_file_copy_file_list_destroy(list_item->file_list);

  memset(list_item, 'F', sizeof(*list_item));
  
  ssh_xfree(list_item);
}

void scp_file_list_item_destroy(void *item)
{
  ScpFileListItem list_item = (ScpFileListItem) item;

  SSH_DEBUG(4, ("Destroying ScpFileListItem..."));
  if (*list_item->connection)
    {
      ssh_file_copy_connection_destroy(*(list_item->connection));
      *(list_item->connection) = NULL;
    }
  ssh_file_copy_location_destroy(list_item->location);

  memset(list_item, 'F', sizeof(*list_item));
  
  ssh_xfree(list_item);  
}

void scp_debug(const char *msg, void *context)
{
  ScpSession session = (ScpSession) context;

  SSH_PRECOND(session);
  
  if (!session->verbose_mode || session->quiet_mode)
    return;

  fprintf(stderr, "%s:%s\n", session->scp2_name, msg);
}

void scp_warning(const char *msg, void *context)
{
  ScpSession session = (ScpSession) context;
  
  SSH_PRECOND(session);
    
  if (!session->quiet_mode)
    fprintf(stderr, "%s: warning: %s\n", session->scp2_name, msg);
}

void scp_fatal(const char *msg, void *context)
{
  ScpSession session = (ScpSession) context;
  /* If we would call SSH_ASSERT here for the session struct, we could
     go to a infinite loop. The reason is left as an exercise for the
     reader. */
  fprintf(stderr, "%s: FATAL: %s\n", session->scp2_name, msg);

  exit(-1);
}

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

void scp_usage(ScpSession session)
{
  SSH_PRECOND(session);
  
  fprintf(stderr, "usage: %s [-D debug_level_spec] [-d] [-p] [-n] [-u] "
                  "[-v] [-1]\n", session->scp2_name);
  fprintf(stderr, "           [-c cipher] [-S ssh2-path] [-h] "
                  "[-P ssh2-port]\n");
  fprintf(stderr, "           [[user@]host[#port]:]file ...\n");
  fprintf(stderr, "           [[user@]host[#port]:]file_or_dir\n");
  fprintf(stderr, "\n");
  fprintf(stderr, "Options:\n");
  fprintf(stderr, "  -D debug_level_spec  Set debug level.\n");
  fprintf(stderr, "  -d                   Force target to be a directory.\n");
  fprintf(stderr, "  -q                   Make scp quiet (only fatal errors "
                  "are displayed).\n");
  fprintf(stderr, "  -Q                   Don't show progress indicator.\n");
  fprintf(stderr, "  -p                   Preserve file attributes and "
                  "timestamps.\n");
  fprintf(stderr, "  -n                   Show what would've been done "
                  "without actually copying\n");
  fprintf(stderr, "                       any files.\n");
  fprintf(stderr, "  -u                   Remove source files after "
                  "copying.\n");
  fprintf(stderr, "  -v                   Verbose mode; equal to `-D 2'.\n");
  fprintf(stderr, "  -1                   Engage scp1 compatibility.\n");
  fprintf(stderr, "  -c cipher            Select encryption algorithm. "
                  "Multiple -c options are \n");
  fprintf(stderr, "                       allowed and a single -c flag "
                  "can have only one cipher.\n");
  fprintf(stderr, "  -S ssh2-path         Tell scp2 where to find ssh2.\n");
  fprintf(stderr, "  -P ssh2-port         Tell scp2 which port sshd2 "
                  "listens on the remote machine.\n");
  fprintf(stderr, "  -h                   Display this help.\n");
  fprintf(stderr, "\n");
}

void glob_ready_cb(SshFileCopyError error,
                   const char *error_message,
                   SshFileCopyConnection connection,
                   SshDlList file_list,
                   void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  ScpSession session = ssh_fsm_get_gdata(thread);
  
  SSH_DEBUG(2, ("Received error \"%s\"., msg: %s",                              \
                ssh_file_copy_errors[error].error_name, error_message));

  if (session->exit_value < error)
    session->exit_value = error;

  /* XXX FIX THIS! I mean it!! */
  switch (error)
    {
    case SSH_FC_OK:
      /* Everything went just fine. */
      {
        ScpFinalFileListItem item;

        item = ssh_xcalloc(1, sizeof(*item));

        item->connection = connection;
        item->file_list = file_list;

        ssh_dllist_add_item(session->final_file_list, item, SSH_DLLIST_END);
      }
      
      break;
    case SSH_FC_ERROR:
      /* XXX Some miscellaneous error. */
      break;
    case SSH_FC_ERROR_DEST_NOT_DIR:
      /* Globbing doesn't use this error value, so this should not happen. */
      SSH_NOTREACHED;
    case SSH_FC_ERROR_CONNECTION_FAILED:
      /* XXX Connecting to host failed. */
      SSH_NOTREACHED;
      break;
    case SSH_FC_ERROR_CONNECTION_LOST:
      /* XXX Connection lost. */
      break;
    case SSH_FC_ERROR_NO_SUCH_FILE:
      /* File doesn't exist. */
      break;
    case SSH_FC_ERROR_PERMISSION_DENIED:
      /* No permission to access file. */
      break;
    case SSH_FC_ERROR_FAILURE:
      /* Undetermined error. */
      break;
    case SSH_FC_ERROR_PROTOCOL_MISMATCH:
      /* File transfer protocol mismatch. */
      break;
    case SSH_FC_ERROR_ELOOP:
      /* Maximum symlink level exceeded. */
      break;
    }

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

void glob_error_cb(SshFileCopyError error,
                   const char *error_message,
                   void *context)
{
  SSH_TRACE(2, ("Received error %s", ssh_file_copy_errors[error].error_name));
  ssh_informational("%s\r\n", error_message);
}

SSH_FSM_STEP(scp_glob)
{
  SSH_FSM_GDATA(ScpSession);
  
  /* XXX */
  ScpFileListItem item;
    
  if (!ssh_dllist_is_current_valid(gdata->original_file_list))
    {
      /* File list is at it's end. */
      ssh_dllist_rewind(gdata->final_file_list);
  
      if (gdata->attrs->recurse_dirs)
        {
          SSH_DEBUG(1, ("Starting recursion..."));
          SSH_FSM_SET_NEXT("scp_recurse");
        }
      else
        {
          SSH_DEBUG(1, ("Starting transfer..."));
          SSH_FSM_SET_NEXT("scp_transfer");
        }
      return SSH_FSM_CONTINUE;
    }

  item = (ScpFileListItem) ssh_dllist_current(gdata->original_file_list);

  ssh_dllist_fw(gdata->original_file_list, 1);

    /* XXX local files shouldn't be globbed, as they are globbed by
       the shell. If wildcards survive to here, then the user has
       deliberately escaped them, and we should use them
       literalily. This remains here for the time being for debugging
       purposes. */

  SSH_FSM_ASYNC_CALL(ssh_file_copy_glob(*(item->connection),    \
                                        item->location,         \
                                        NULL,                   \
                                        glob_ready_cb,          \
                                        glob_error_cb,          \
                                        thread));
}

void recurse_ready_cb(SshFileCopyError error,
                      const char *error_message,
                      SshFileCopyConnection
                      connection,
                      SshDlList file_list,
                      void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  ScpThreadContext tdata = ssh_fsm_get_tdata(thread);
  ScpSession session = ssh_fsm_get_gdata(thread);
  
  SSH_TRACE(0, ("Received error %s, error message %s.", \
                ssh_file_copy_errors[error].error_name, \
                error_message));  

  if (session->exit_value < error)
    session->exit_value = error;

  switch (error)
    {
    case SSH_FC_OK:
      /* XXX Everything went just fine. */
      /* XXX Not like this, this is for testing purposes. */
      {
        ScpFinalFileListItem item;

        item = ssh_xcalloc(1, sizeof(*item));

        item->connection = connection;
        item->file_list = file_list;

        ssh_dllist_add_item(tdata->new_final_file_list, item, SSH_DLLIST_END);
      }
      
      break;
    case SSH_FC_ERROR:
      /* XXX Some miscellaneous error. */
      break;
    case SSH_FC_ERROR_DEST_NOT_DIR:
      /* Recursion doesn't use this error value, so this should not
         happen. */
      SSH_NOTREACHED;
    case SSH_FC_ERROR_CONNECTION_FAILED:
      /* XXX Connecting to host failed. */
      SSH_NOTREACHED;
      break;
    case SSH_FC_ERROR_CONNECTION_LOST:
      /* XXX Connection lost. */
      break;
    case SSH_FC_ERROR_NO_SUCH_FILE:
      /* File doesn't exist. */
      break;
    case SSH_FC_ERROR_PERMISSION_DENIED:
      /* No permission to access file. */
      break;
    case SSH_FC_ERROR_FAILURE:
      /* Undetermined error. */
      break;
    case SSH_FC_ERROR_PROTOCOL_MISMATCH:
      /* File transfer protocol mismatch. */
      break;
    case SSH_FC_ERROR_ELOOP:
      /* Maximum symlink level exceeded. */
      break;
    }

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

void recurse_nonfatal_error_cb(SshFileCopyError error,
                               const char *error_message,
                               void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SSH_FSM_GDATA(ScpSession);
  
  SSH_TRACE(2, ("Received error %s.", ssh_file_copy_errors[error].error_name));
  if (!gdata->quiet_mode)
    ssh_warning("%s", error_message);
  /* XXX scp2 shouldn't return 0 after these. */
}

void mapcar_func(SshFileCopyFile file, void *context)
{
  ssh_debug("%s", ssh_file_copy_file_get_name(file));
}

SSH_FSM_STEP(scp_recurse)
{
  SSH_FSM_DATA(ScpSession, ScpThreadContext);
  ScpFinalFileListItem item;
  
  if (!ssh_dllist_is_current_valid(gdata->final_file_list))
    {
      /* File list is at it's end. */
      /* XXX destroy filelist. */

      gdata->final_file_list = tdata->new_final_file_list;
      ssh_dllist_rewind(gdata->final_file_list);
      SSH_DEBUG(1, ("Starting transfer..."));
      SSH_FSM_SET_NEXT("scp_transfer");
      return SSH_FSM_CONTINUE;
    }
  
  item = ssh_dllist_current(gdata->final_file_list);

  ssh_dllist_fw(gdata->final_file_list, 1);

#ifdef DEBUG_LIGHT
  ssh_debug("files in file list:");
  ssh_file_copy_file_list_mapcar(item->file_list,
                                 mapcar_func,
                                 NULL);
#endif
  SSH_FSM_ASYNC_CALL(ssh_file_copy_recurse_dirs(item->connection,          \
                                                item->file_list,           \
                                                NULL,                      \
                                                NULL,                      \
                                                recurse_ready_cb,          \
                                                recurse_nonfatal_error_cb, \
                                                thread));
}

void transfer_ready_cb(SshFileCopyError error,
                      const char *error_message,
                      void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  ScpSession session = ssh_fsm_get_gdata(thread);
  
  SSH_TRACE(0, ("Received error %s, error message %s.", \
                ssh_file_copy_errors[error].error_name, \
                error_message));  

  if (session->exit_value < error)
    session->exit_value = error;
  
  switch (error)
    {
    case SSH_FC_OK:
      /* XXX Everything went just fine. */
      break;
    case SSH_FC_ERROR:
      /* XXX Some miscellaneous error. */
      if (!session->quiet_mode)
        ssh_warning("%s", error_message);
      break;
    case SSH_FC_ERROR_DEST_NOT_DIR:
      /* XXX Destination is not a directory. */
      if (!session->quiet_mode)
        ssh_warning("%s", error_message);
      break;
    case SSH_FC_ERROR_CONNECTION_FAILED:
      /* XXX Connecting to host failed. */
      if (!session->quiet_mode)
        ssh_warning("%s", error_message);
      break;
    case SSH_FC_ERROR_CONNECTION_LOST:
      /* XXX Connection lost. */
      if (!session->quiet_mode)
        ssh_warning("%s", error_message);
      break;
    case SSH_FC_ERROR_NO_SUCH_FILE:
      /* XXX Destination directory doesn't exist. */
      if (!session->quiet_mode)
        ssh_warning("%s", error_message);
      break;
    case SSH_FC_ERROR_PERMISSION_DENIED:
      /* XXX No permission to access file. */
      if (!session->quiet_mode)
        ssh_warning("%s", error_message);
      break;
    case SSH_FC_ERROR_FAILURE:
      /* XXX Undetermined error. */
      if (!session->quiet_mode)
        ssh_warning("%s", error_message);
      break;
    case SSH_FC_ERROR_PROTOCOL_MISMATCH:
      /* XXX File transfer protocol mismatch. */
      if (!session->quiet_mode)
        ssh_warning("%s", error_message);
      break;
    case SSH_FC_ERROR_ELOOP:
      /* XXX Maximum symlink level exceeded. */
      if (!session->quiet_mode)
        ssh_warning("%s", error_message);
      break;
    }

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

void transfer_nonfatal_error_cb(SshFileCopyError error,
                                const char *error_string,
                                void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  ScpSession session = ssh_fsm_get_gdata(thread);
  
  SSH_TRACE(2, ("Received error %s.", ssh_file_copy_errors[error].error_name));

  if (!session->quiet_mode)
    ssh_warning("%s", error_string);

  if (session->exit_value < error)
    session->exit_value = error;
}

char *stat_eta(SshUInt64 secs)
{
  static char stat_result[10];

#if 1
  return ssh_format_time(stat_result, sizeof(stat_result), secs);
#else
  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);
#endif
}

#if defined(SIGWINCH) && defined(TIOCGWINSZ)

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

void scp_win_dim_change(int sig, void *context)
{
  struct winsize ws;
  ScpSession session = (ScpSession) context;

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

void 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)
{
  SshFSMThread thread = (SshFSMThread) context;
  ScpSession session = ssh_fsm_get_gdata(thread);
  SshFileAttributes attrs;
  double transfer_rate;
  Boolean at_end = FALSE;
  char *eta_string;
  static int prev_width = -1;
  static char format_string[1024];
  char num_buffer[10];
  
  SSH_PRECOND(!session->no_statistics);
  
  attrs = ssh_file_copy_file_get_attributes(source_file);

  SSH_VERIFY(attrs);
  SSH_ASSERT(attrs->flags & SSH_FILEXFER_ATTR_SIZE);

  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 (session->screen_width < SCP_DEFAULT_SCREEN_WIDTH)
    session->screen_width = SCP_DEFAULT_SCREEN_WIDTH;

  /* If screen width is changed, reconstruct the format string. */
  if (prev_width != session->screen_width)
    {
      prev_width = session->screen_width;

      snprintf(format_string, sizeof(format_string),
              "\r%%-%d.%ds | %%5sB | %%5.1f kB/s | %%3.3s: %%s | %%3d%%%%",
               session->screen_width - 47, session->screen_width - 47);
    }
  
  fprintf(SCP_STATS_FILE,
          format_string,
          ssh_file_copy_file_get_name(source_file),
          ssh_format_number(num_buffer, sizeof(num_buffer),
                            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(SCP_STATS_FILE, "\n");
    }
}

SSH_FSM_STEP(scp_transfer)
{
  SSH_FSM_GDATA(ScpSession);
  ScpFinalFileListItem item;
  /* XXX */

  if (!ssh_dllist_is_current_valid(gdata->final_file_list))
    {
      /* File list is at it's end. */
      /* XXX destroy filelist. */
      
      SSH_TRACE(1, ("Transfer ready"));
      SSH_FSM_SET_NEXT("scp_finish");
      return SSH_FSM_CONTINUE;
    }

  item = ssh_dllist_current(gdata->final_file_list);

  ssh_dllist_fw(gdata->final_file_list, 1);

#ifdef DEBUG_LIGHT
  ssh_file_copy_file_list_mapcar(item->file_list,
                                 mapcar_func,
                                 NULL);
#endif

  SSH_FSM_ASYNC_CALL(gdata->transfer_op_handle =                \
                     ssh_file_copy_transfer_files               \
                     (item->connection,                         \
                      item->file_list,                          \
                      *(gdata->destination->connection),        \
                      gdata->destination->location,             \
                      gdata->attrs,                             \
                      transfer_ready_cb,                        \
                      transfer_nonfatal_error_cb,               \
                      gdata->no_statistics ?                    \
                      NULL : transfer_progress_cb,              \
                      NULL,                                     \
                      thread));
}

SSH_FSM_STEP(scp_finish)
{
  SSH_FSM_GDATA(ScpSession);
  /* XXX Do we need to do something here? */
  /* XXX Some file descriptors are still left to the event loop after
     this, so it would probably be best to abort the event loop in the
     fsm-destroy-callback. */
  ssh_app_free_list(gdata->final_file_list,
                    scp_final_file_list_item_destroy);
  gdata->final_file_list = NULL;
  
  ssh_fsm_destroy(gdata->fsm);
  return SSH_FSM_FINISH;
}

void scp_fsm_gdata_destructor(void *gdata)
{
  ScpSession session = (ScpSession) gdata;
  /* Destroy ScpSession structure. */
  exit_value = session->exit_value;
  scp_session_destroy(session);
  memset(session, 'F', sizeof(*session));
}

void scp_thread_destructor(void *tdata)
{
  /* Nothing here yet. */
}

#ifdef TEST_TRANSFER_ABORT
void scp_interrupt_signal_handler(int sig, void *context)
{
  ScpSession session = (ScpSession) context;
  
  SSH_PRECOND(sig == SIGINT);
  SSH_PRECOND(session);

  SSH_TRACE(0, ("SIGINT received."));
  
  if (session->transfer_op_handle)
    {
      ssh_operation_abort(session->transfer_op_handle);
      session->transfer_op_handle = NULL;
    }

  ssh_unregister_signal(SIGINT);
}
#endif /* TEST_TRANSFER_ABORT */
SshFSMStateMapItem scp_states[] =
{ { "scp_glob", "Glob locations", scp_glob },
  { "scp_recurse", "Recurse source directories", scp_recurse },
  { "scp_transfer", "Tranfer files", scp_transfer },
  { "scp_finish", "Finish", scp_finish } };  
    
int main(int argc, char **argv)
{
  int ch, i;
  char *filename;
  ScpSession session;
  ScpFileListItem destination;
  ScpThreadContext thread_context;
  SshFSM fsm;
  
  fsm = ssh_fsm_allocate(sizeof(*session),
                         scp_states,
                         SSH_FSM_NUM_STATES(scp_states),
                         scp_fsm_gdata_destructor);

  session = ssh_fsm_get_gdata_fsm(fsm);

  scp_session_initialize(session);

  session->fsm = fsm;
  
  ssh_event_loop_initialize();

  ssh_debug_register_callbacks(scp_fatal, scp_warning, scp_debug,
                               (void *)(session));

  if ((filename = strrchr(argv[0], '/')) != NULL)
    {
      filename++;
      session->scp2_name = ssh_xstrdup(filename);
    }
  else
    {
      session->scp2_name = ssh_xstrdup(argv[0]);
    }

  filename = NULL;
  
  while ((ch = ssh_getopt(argc, argv, "qQdpvnuhS:P:c:D:tf1rOVB", NULL)) != -1)
    {
      if (!ssh_optval)
        {
          scp_usage(session);
          exit(SSH_FC_ERROR);
        }
      switch(ch)
        {
        case 't':
        case 'f':
          /* Scp 1 compatibility mode, this is remote server for ssh 1 scp,
             exec old scp here. */
          {
            ssh_warning("Executing scp1 compatibility.");
            execvp("scp1", argv);
            ssh_fatal("Executing ssh1 in compatibility mode failed (Check "
                      "that scp1 is in your PATH).");
          }
          break;
        case '1':
          /* Scp 1 compatibility in the client */
          {
            char **av;
            int j;
            int removed_flag = FALSE;

            av = ssh_xcalloc(argc, sizeof(char *));
            ssh_warning("Executing scp1 compatibility.");
            for (i = 0, j = 0 ; i < argc ; i++)
              {

                SSH_DEBUG(3, ("argv[%d] = %s", i, argv[i]));

                /* skip first "-1" argument */
                if (strcmp("-1", argv[i]) || removed_flag)
                  {
                    av[j] = argv[i];
                    j++;
                  }
                else
                  {
                    removed_flag = TRUE;
                  }

              }

            execvp("scp1", av);
            ssh_fatal("Executing ssh1 in compatibility mode failed (Check "
                      "that scp1 is in your PATH).");
          }

          break;
        case 'p':
          session->attrs->preserve_attributes = TRUE;
          break;
        case 'r':
          session->attrs->recurse_dirs = TRUE;
          break;
        case 'P':
          session->remote_port = ssh_xstrdup(ssh_optarg);
          
          {
            int port = atoi(session->remote_port);
            
            if ((port <= 0) || (port > 65535))
              {
                ssh_warning("Port specification '%s' is invalid or out of "
                            "range.",
                            session->remote_port);
                scp_usage(session);
                exit(SSH_FC_ERROR);
              }
          }
          
          break;
        case 'c':
          if ( ssh_dllist_add_item(session->cipher_list,
                                   (void *)ssh_xstrdup(ssh_optarg),
                                   SSH_DLLIST_END) != SSH_DLLIST_OK)
            {
              ssh_fatal("Error in cipher list handling.");
            }
          break;
        case 'S':
          ssh_xfree(session->ssh_path);
          session->ssh_path = ssh_xstrdup(ssh_optarg);
          break;
        case 'd':
          session->attrs->destination_must_be_dir = TRUE;
          break;
        case 'B':
          session->batch_mode = TRUE;
          break;
        case 'D':
          session->debug_level = ssh_xstrdup(ssh_optarg);
          ssh_debug_set_level_string(session->debug_level);
          session->verbose_mode = TRUE;
          break;
        case 'v':
          session->debug_level = ssh_xstrdup("2");
          ssh_debug_set_level_string(session->debug_level);
          session->verbose_mode = TRUE;
          break;
        case 'q':
          session->quiet_mode = TRUE;
          break;
        case 'Q':
          session->no_statistics = TRUE;
          break;
        case 'u':
          session->attrs->unlink_source = TRUE;
          break;
        case 'n':
          session->do_not_copy = TRUE;
          break;
        case 'h':
          scp_usage(session);
          exit(SSH_FC_OK);
          break;
        case 'V':
          ssh2_version(argv[0]);
          exit(SSH_FC_OK);
          break;
        default:
          scp_usage(session);
          exit(SSH_FC_ERROR);
          break;
        }
    }

  ssh_register_signal(SIGHUP, scp_fatal_signal_handler, (void *)session);
#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. (In Linux, SIGIO causes the
     program to terminate. Ah, I just _love_ compatible interfaces
     like this!)  */
  (void)signal(SIGIO, SIG_IGN);
#endif /* SIGIO */
  
  if (isatty(fileno(stdout)))
    {
      session->have_tty = TRUE;
      if (!session->no_statistics)
        {          
#if defined(SIGWINCH) && defined(TIOCGWINSZ)
          /* Register a signal handler for SIGWINCH to send window change
             notifications to the server. */
          ssh_register_signal(SIGWINCH, scp_win_dim_change, session); 
#endif /* SIGWINCH && TIOCGWINSZ*/
#ifdef TIOCGWINSZ
          {
            struct winsize ws;
    
            if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) >= 0)
              {
                session->screen_width = ws.ws_col;
              }    
            else
              {
                SSH_TRACE(2, ("unable to get window size parameters."));
                session->screen_width = SCP_DEFAULT_SCREEN_WIDTH;
              }
          }
#else /* TIOCGWINSZ */
          session->screen_width = SCP_DEFAULT_SCREEN_WIDTH;
#endif /* TIOCGWINSZ */
        }
    }
  
  if (session->have_tty == FALSE)
    {
      session->no_statistics = TRUE;
    }
  
  if (argc - ssh_optind < 2)
    {
      ssh_warning("Missing destination file.");
      scp_usage(session);
      exit(SSH_FC_ERROR);
    }

  /* This is because in some cases, where ssh2 dies (ie. it is killed
     etc.) scp2 receives SIGPIPE, which kills it, too. This is not
     good, as sshfilecopy has an internal error recovery system. */
  ssh_register_signal(SIGPIPE, NULL, NULL);
  ssh_sigchld_initialize();

  /* Parse source locations */
  for (i = ssh_optind; i < argc - 1; i++)
    {
      SshFileCopyConnection *source_connection;
      ScpFileListItem temp_item;

      source_connection = ssh_xcalloc(1, sizeof(SshFileCopyConnection));
      
      *source_connection = ssh_file_copy_connection_allocate();
      
      if (scp_location_parse(*source_connection, &filename, argv[i]))
        {
          /* An error occurred. */
          /* XXX Free everything that can be freed, and exit. */

          ssh_warning("Invalid source specification '%s'.", argv[i]);
          scp_usage(session);
          exit(SSH_FC_ERROR);
        }

      /* XXX If 'connection' is local, and filename isn't absolute, it
         should be preceded by current working directory. */
      if ((*source_connection)->host == NULL)
        {
          if (*filename != '/')
            {
              char *new_filename, *cwd;

              cwd = ssh_getcwd();

              ssh_dsprintf(&new_filename, "%s/%s", cwd, filename);

              ssh_xfree(cwd);
              ssh_xfree(filename);
              filename = new_filename;
            }
        }
      
      /* Check if 'connection' is identical to a previous one, and
         if so, just add the 'filename' to the previous. */
      if ((temp_item =
           scp_file_list_find_matching_connection(session->original_file_list,
                                                  *source_connection)) != NULL)
        {
          ssh_file_copy_connection_destroy(*source_connection);
          ssh_xfree(source_connection);
          ssh_file_copy_location_add_raw_file(temp_item->location,
                                              filename);
        }
      else
        {
          ScpFileListItem source;
          source = scp_file_list_item_allocate(TRUE);

          ssh_file_copy_connection_destroy(*(source->connection));

          source->connection = source_connection;

          ssh_file_copy_location_add_raw_file(source->location,
                                              filename);
          
          /* Add the file item to the list. */
          if (ssh_dllist_add_item(session->original_file_list,
                                  source,
                                  SSH_DLLIST_END) != SSH_DLLIST_OK)
            ssh_fatal("Error in manipulating original_file_list.");
        }

      ssh_xfree(filename);
    }

  /* Parse destination location. */
  destination = scp_file_list_item_allocate(FALSE);

  if (scp_location_parse(*(destination->connection), &filename, argv[i]))
    {
      /* An error occurred. */
      /* XXX Free everything that can be freed, and exit. */
      
      ssh_warning("Invalid destination specification '%s'.", argv[i]);
      scp_usage(session);
      exit(SSH_FC_ERROR);
    }

  if ((*destination->connection)->host == NULL)
    {
      if (*filename != '/')
        {
          char *new_filename, *cwd;
          
          cwd = ssh_getcwd();

          if (cwd == NULL)
            ssh_fatal("Couldn't get path of current working directory");
          
          ssh_dsprintf(&new_filename, "%s/%s", cwd, filename);
          
          ssh_xfree(cwd);
          ssh_xfree(filename);
          filename = new_filename;
        }
    }

  ssh_file_copy_location_add_raw_file(destination->location, filename);

  ssh_xfree(filename);

  session->destination = destination;

  /* Register connection callback for sshfilecopy. */
  ssh_file_copy_register_connect_callback(connect_callback,
                                          session);

  /* Initiate wildcard parsing. */
  ssh_dllist_rewind(session->original_file_list);

  session->main_thread = ssh_fsm_spawn(session->fsm,
                                       sizeof(*thread_context),
                                       "scp_glob",
                                       NULL /* XXX (??) */,
                                       scp_thread_destructor);

  thread_context = ssh_fsm_get_tdata(session->main_thread);

  thread_context->new_final_file_list = ssh_dllist_allocate();

#ifdef TEST_TRANSFER_ABORT
  ssh_register_signal(SIGINT, scp_interrupt_signal_handler, session);
#endif /* TEST_TRANSFER_ABORT */
  
  ssh_event_loop_run();

  ssh_event_loop_uninitialize();
  
  return exit_value;
}


void new_stream_callback(SshStreamNotification notification, void *context)
{
  ScpConnectionCtx ctx = (ScpConnectionCtx) 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(4, ("read char: %c", ctx->buffer[strlen(ctx->buffer) - 1]));
          SSH_TRACE(4, ("read_bytes: %d, buffer len: %d",       \
                        ctx->read_bytes, strlen(ctx->buffer)));
          SSH_DEBUG_HEXDUMP(4, ("received message:"),           \
                            ctx->buffer, ctx->read_bytes);
          fflush(stderr);
          if (ctx->buffer[strlen(ctx->buffer) - 1] == '\n')
            {
              SSH_TRACE(3, ("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_warning("ssh2 client failed to authenticate. (or you "
                              "have too old ssh2 installed, check "
                              "with ssh2 -V)");
                }
            }
          
        }
      if (ret == -1)
        return;
      if (ret == 0)
        {
          ssh_warning("EOF received from ssh2. ");
        }
      if (ctx->read_bytes >= sizeof(ctx->buffer) - 1)
        {
          ssh_warning("Received corrupted (or wrong type of) data from "
                      "ssh2-client.");
        }
      break;
    case SSH_STREAM_CAN_OUTPUT:
      return;
    case SSH_STREAM_DISCONNECTED:
      SSH_TRACE(2, ("Received SSH_STREAM_DISCONNECTED from wrapped stream."));
      ssh_warning("ssh2 client failed to authenticate. (or you "
                  "have too old ssh2 installed, check with ssh2 -V)");
      /* XXX */
      break;
    }
  ssh_stream_destroy(ctx->stream);
  (*ctx->completion_cb)(NULL, ctx->completion_context);
}

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;
  ScpSession session = (ScpSession) context;
  ScpConnectionCtx new_ctx;
  
  i = 0;

  ssh_argv[i++] = session->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;
    }
  else if (session->remote_port != NULL)
    {
      ssh_argv[i++] = "-p";
      ssh_argv[i++] = session->remote_port;
    }
  
  if (session->verbose_mode)
    ssh_argv[i++] = "-v";

  if (session->batch_mode)
    {
      ssh_argv[i++] = "-o";
      ssh_argv[i++] = "batchmode yes";
    }
  
  ssh_argv[i++] = "-x";
  ssh_argv[i++] = "-a";
  
  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";
  
  ssh_argv[i++] = connection->host;

  ssh_argv[i++] = "-s";
  ssh_argv[i++] = "sftp";
  
  ssh_argv[i] = NULL;

  SSH_ASSERT(i < SSH_ARGV_SIZE);
     
  if (session->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->session = session;
      new_ctx->completion_cb = completion_cb;
      new_ctx->completion_context = completion_context;
      ssh_sigchld_register(new_ctx->child_pid, scp_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;
}

Boolean recurse_func(const char *filename,
                      const char *long_name,
                      SshFileAttributes
                      file_attributes,
                      SshFileCopyRecurseAttrs attrs,
                      void *context)
{
#if 0
  fprintf(stderr, "%s -- %s,perms 0%o\n", filename, long_name,
          (unsigned int) file_attributes->permissions);
#else
  fprintf(stderr, "%s\n", filename);
#endif
  return TRUE;
}

/* Find a matching SshFileCopyConnection from file_list. file_list
   MUST contain ScpFileListItems. Return matching
   ScpFileListItem if found, or NULL if not. */
ScpFileListItem
scp_file_list_find_matching_connection(SshDlList file_list,
                                       SshFileCopyConnection connection)
{
  int i, len;
  ScpFileListItem temp_item;
  
  SSH_PRECOND(file_list);
  SSH_PRECOND(connection);

  ssh_dllist_rewind(file_list);

  len = ssh_dllist_length(file_list);
  
  for(i = 0; i < len ; i++)
    {
      if ((temp_item = ssh_dllist_current(file_list)) == NULL)
        {
          /* An error occurred. */
          /* We can't do nothing about this here. It is likely that
             we'll bomb out later, because the list is probably
             corrupt. */
          SSH_TRACE(2, ("Error in list handling."));
          
          return NULL;
        }

      if (ssh_file_copy_connection_compare(*(temp_item->connection),
                                           connection))
        {
          /* Found. */
          return temp_item;
        }

      if (ssh_dllist_fw(file_list, 1) != SSH_DLLIST_OK)
        {
          /* An error occurred. */
          /* We can't do nothing about this here. It is likely that
             we'll bomb out later, because the list is probably
             corrupt. */
          SSH_TRACE(2, ("Error in list handling."));
          
          return NULL;
        }
    }

  /* Matching item not found. */
  return NULL;
}

/* Parse a string to a SshFileCopyConnection structure. String is of form
   [[user@]host[#port]:]filename . In case of an error or if the given
   string is malformed, return TRUE. Otherwise, return FALSE. */
Boolean scp_location_parse(SshFileCopyConnection connection,
                           char **return_filename,
                           const char *orig_string)
{
  char *string, *host = NULL, *user = NULL, *port = NULL, *filename = NULL;

  SSH_ASSERT(orig_string);

  if (strlen(orig_string) == 0)
    {
      return TRUE;
    }
  
  string = ssh_xstrdup(orig_string);

  /* Separate filename and [[user@]host[#port] */
  if ((filename = ssh_glob_next_unescaped_char(string, ':')) != NULL)
    {
      *filename = '\0';
      filename++;
      
      /* If filename is empty, default it to '.'.*/
      if (strlen(filename) == 0)
        filename = ssh_xstrdup(".");

      /* Separate user and host. */
      if ((host = ssh_glob_next_unescaped_char(string, '@')) != NULL)
        {
          *host = '\0';
          host++;

          if (strlen(host) == 0 || strlen(string) == 0)
            {
              /* It is an error for the hostname or username to be
                 empty. */
              /* Free everything that can be freed. */
              ssh_xfree(string);
              return TRUE;
            }
          user = string;
        }
      else
        {
          /* No '@' sign. Check for '#' nevertheless. */
          host = string;
        }

      if ((port = ssh_glob_next_unescaped_char(host, '#')) != NULL)
        {
          *port = '\0';
          port++;

          if (strlen(port) == 0)
            {
              /* It is an error for port to be empty if '#' is
                 specified. */
              /* Free everything that can be freed. */
              ssh_xfree(string);
              return TRUE;              
            }
        }
    }
  else
    {
      /* The whole string is a filename (and the file is local). We
         check, however, that there are no un-escaped '@' characters
         in it, as that would indicate that the user tried to specify
         a username and a hostname. We give an error instead. */
      if (ssh_glob_next_unescaped_char(string, '@'))
        {
          /* Free memory. */
          ssh_xfree(string);
          SSH_TRACE(2, ("user@host form used, but no colon."));
          return TRUE;
        }

      filename = string;
    }

  connection->user = ssh_glob_strip_escapes(user);
  connection->host = ssh_glob_strip_escapes(host);
  connection->port = ssh_glob_strip_escapes(port);

  *return_filename = ssh_xstrdup(filename);
  
  SSH_DEBUG(3, ("user = %s, host = %s, port = %s, filename = %s",       \
                connection->user ?                                      \
                connection->user : "NULL",                              \
                connection->host ?                                      \
                connection->host : "NULL",                              \
                connection->port ?                                      \
                connection->port : "NULL",                              \
                filename));

  ssh_xfree(string);
  
  return FALSE;
}

