/*
  sshfilecopy.c

  Author: Sami Lehtinen <sjl@ssh.com>

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

  Functions common for both scp and sftp.
 */
/*
  TODO(among other things, loose order):

  - comment the code better
  
  - get rid of SshFileCopyLocation, it isn't really needed with the
    addition of sub-dirs to SshFileCopyFile

  - Improve error-handling (those SSH_NOTREACHEDs have to go)

  - add 'multiple buffer'-support to
    ssh_file_copy_transfer_files(). The speed of the transfer can be
    greatly increased with several concurrent transfer to/from a file,
    so that we don't idle waiting for ACK from the server end (either
    end)

  - Find out why killing of the writer (usually ssh2) in transfer
    leads to corrupted files, but the killing of reader is handled
    correctly

  - if umask is for example 077, then the execute bits will still be
    set, if they are set in the source. Fix.
  
  - new features (like what?)  */

#include "sshincludes.h"
#include "sshfilecopy.h"
#include "sshfilecopyi.h"
#include "sshappcommon.h"
#include "sshglob.h"
#include "sshstreampair.h"

#define SSH_DEBUG_MODULE "SshFileCopy"

/* Callback used in connecting remote hosts. */
static SshFileCopyConnectCallback connect_callback = NULL;
static void *connect_context = NULL;

/* Initialize error-names. */
SshFileCopyErrorName ssh_file_copy_errors[] =
{
  { SSH_FC_OK,                      "SSH_FC_OK" },
  { SSH_FC_ERROR,                   "SSH_FC_ERROR" },
  { SSH_FC_ERROR_DEST_NOT_DIR,      "SSH_FC_ERROR_DEST_NOT_DIR" },
  { SSH_FC_ERROR_CONNECTION_FAILED, "SSH_FC_ERROR_CONNECTION_FAILED" },
  { SSH_FC_ERROR_CONNECTION_LOST,   "SSH_FC_ERROR_CONNECTION_LOST" },
  { SSH_FC_ERROR_NO_SUCH_FILE,      "SSH_FC_ERROR_NO_SUCH_FILE" },
  { SSH_FC_ERROR_PERMISSION_DENIED, "SSH_FC_ERROR_PERMISSION_DENIED" },
  { SSH_FC_ERROR_FAILURE,           "SSH_FC_ERROR_FAILURE" },
  { SSH_FC_ERROR_PROTOCOL_MISMATCH, "SSH_FC_ERROR_PROTOCOL_MISMATCH" },
  { -1, NULL}
};

/* Forward declarations. */

/* Allocate a new SshFileCopyFile structure. */
SshFileCopyFile ssh_file_copy_file_allocate(void)
{
  SshFileCopyFile file;

  SSH_DEBUG(6, ("Allocating SshFileCopyFile structure..."));
  file = ssh_xcalloc(1, sizeof(*file));
  
  return file;
}

/* Destroy SshFileCopyFile structure. This should be of type
   SshAppListNodeDeleteProc, because it used to destroy list items.*/
void ssh_file_copy_file_destroy(void *item)
{
  SshFileCopyFile file;

  file = (SshFileCopyFile) item;
  
  SSH_DEBUG(6, ("Destroying SshFileCopyFile structure..."));

  /* Close file (destroys handle) */
  if (file->handle)
    {
      SSH_DEBUG(7, ("Closing file-handle..."));
      ssh_file_client_close(file->handle, NULL, NULL);
      file->handle = NULL;
    }
  
  if (file->attributes)
    {
      SSH_DEBUG(7, ("Freeing attributes..."));
      ssh_xfree(file->attributes);
    }

  if (file->name)
    {
      SSH_DEBUG(7, ("Freeing name..."));
      ssh_xfree(file->name);
    }

  if (file->long_name)
    {
      SSH_DEBUG(7, ("Freeing long_name..."));
      ssh_xfree(file->long_name);
    }

  if (file->dir_entries)
    {
      SSH_DEBUG(7, ("Freeing dir_entries..."));
      ssh_file_copy_location_destroy(file->dir_entries);
    }
  
  memset(file, 'F', sizeof(*file));
  ssh_xfree(file);
}

/* This functions registers a new filename to 'file'. It is legal to
   call this functions multiple times for a single SshFileCopyFile
   structure _before_ it is used in any transfers. */
void ssh_file_copy_file_register_filename(SshFileCopyFile file, char *name)
{
  SSH_PRECOND(file);
  
  if (file->name)
    ssh_xfree(file->name);

  file->name = name;
}

/* Get the attributes struct of a SshFileCopyFile structure. If file
   has no attributes (it's not sttaed yet) returns NULL. */
SshFileAttributes ssh_file_copy_file_get_attributes(SshFileCopyFile file)
{
  SSH_PRECOND(file);

  return file->attributes;
}

/* This functions registers SshFileAttributes to 'file'.  */
void ssh_file_copy_file_register_attributes(SshFileCopyFile file,
                                            SshFileAttributes attrs)
{
  SSH_PRECOND(file);
  
  if (file->attributes)
    ssh_xfree(file->attributes);

  file->attributes = attrs;
}

/* Get filename from a SshFileCopyFile structure. */
const char *ssh_file_copy_file_get_name(SshFileCopyFile file)
{
  SSH_PRECOND(file);

  return file->name;
}

/* Get long filename from a SshFileCopyFile structure. */
const char *ssh_file_copy_file_get_long_name(SshFileCopyFile file)
{
  SSH_PRECOND(file);

  return file->long_name;
}

/* This functions registers a new long filename to 'file'.  */
void ssh_file_copy_file_register_long_name(SshFileCopyFile file,
                                           char *long_name)
{
  SSH_PRECOND(file);
  
  if (file->long_name)
    ssh_xfree(file->long_name);

  file->long_name = long_name;
}

/* Duplicate a SshFileCopyFile structure. Notice, however, that if the
   file had bee opened in the original struct, it will be closed now
   (ie. the SshFileHandle is NULL in the copy). */
SshFileCopyFile ssh_file_copy_file_dup(SshFileCopyFile file)
{
  SshFileCopyFile new_file;

  new_file = ssh_file_copy_file_allocate();
  
  if (ssh_file_copy_file_get_name(file))
    ssh_file_copy_file_register_filename
      (new_file, ssh_xstrdup(ssh_file_copy_file_get_name(file)));

  if (ssh_file_copy_file_get_attributes(file))
    ssh_file_copy_file_register_attributes
      (new_file, ssh_file_attributes_dup
       (ssh_file_copy_file_get_attributes(file)));
  
  if (ssh_file_copy_file_get_long_name(file))
    ssh_file_copy_file_register_long_name
      (new_file, ssh_xstrdup(ssh_file_copy_file_get_long_name(file)));

  new_file->dir_entries = file->dir_entries;
  
  return new_file;
}

/* Allocate a new SshFileCopyConnection structure. */
SshFileCopyConnection ssh_file_copy_connection_allocate(void)
{
  SshFileCopyConnection connection;

  SSH_DEBUG(6, ("Allocating SshFileCopyConnection structure..."));
  connection =  ssh_xcalloc(1, sizeof(*connection));
  
  return connection;
}

/* Free a SshFileCopyConnection structure. */
void ssh_file_copy_connection_destroy(SshFileCopyConnection connection)
{
  SSH_DEBUG(6, ("Destroying SshFileCopyConnection structure..."));

  if (connection->client)
    ssh_file_client_destroy(connection->client);
  ssh_xfree(connection->user);
  ssh_xfree(connection->host);
  ssh_xfree(connection->port);
  
  memset(connection, 'F', sizeof(*connection));
  ssh_xfree(connection);
}

/* Compare whether two SshFileCopyConnection structures are
   identical. Note that the 'client' part is not compared. Return TRUE
   if these match, or FALSE if not. Both arguments must be valid. */
Boolean ssh_file_copy_connection_compare(SshFileCopyConnection conn1,
                                         SshFileCopyConnection conn2)
{
  SSH_PRECOND(conn1);
  SSH_PRECOND(conn2);
  
  if (conn1->user != NULL && conn2->user != NULL)
    {
      if (strcmp(conn1->user, conn2->user) != 0)
        return FALSE;
    }
  else
    {
      if (!(conn1->user == NULL && conn2->user == NULL))
        return FALSE;
    }

  if (conn1->host != NULL && conn2->host != NULL)
    {
      if (strcmp(conn1->host, conn2->host) != 0)
        return FALSE;
    }
  else
    {
      if (!(conn1->host == NULL && conn2->host == NULL))
        return FALSE;
    }

  if (conn1->port != NULL && conn2->port != NULL)
    {
      if (strcmp(conn1->port, conn2->port) != 0)
        return FALSE;
    }
  else
    {
      if (!(conn1->port == NULL && conn2->port == NULL))
        return FALSE;
    }

  return TRUE;
}

/* Allocate a new SshFileCopyLocation structure. If 'source' is TRUE,
   this structure is used to represent the source file(s). Otherwise
   this is a destination location. */
SshFileCopyLocation ssh_file_copy_location_allocate(Boolean source)
{
  SshFileCopyLocation location;

  SSH_DEBUG(6, ("Allocating SshFileCopyLocation structure..."));
  location = ssh_xcalloc(1, sizeof(*location));
  
  location->file = ssh_file_copy_file_allocate();

  if (source == TRUE)
    {
      location->file_list = ssh_dllist_allocate();
      location->source = TRUE;
    }
  else
    {
      location->source = FALSE;
    }

  return location;
}

/* Destroy a SshFileCopyLocation structure.*/
void ssh_file_copy_location_destroy(SshFileCopyLocation location)
{
  SSH_DEBUG(6, ("Destroying SshFileCopyLocation structure..."));

  if (location->parent_dir == NULL)
    ssh_file_copy_file_destroy(location->file);
  else
    location->file->dir_entries = NULL;
  
  if (location->file_list != NULL)
    ssh_app_free_list(location->file_list, ssh_file_copy_file_destroy);

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

/* Add a filename to 'location->file_list' and/or 'location->file',
   depending on whether 'location' is source or destination. Only
   tranformation that is done is stripping unnecessary slashes, and
   escape characters before slashes and stripping of 'foo/../'
   constrcuts. */
SshFileCopyFile
ssh_file_copy_location_add_raw_file(SshFileCopyLocation location,
                                    const char *filename)
{
  char *filename_copy;
  SshFileCopyFile file;

  SSH_PRECOND(location);
  SSH_PRECOND(filename);

  /* Check whether we are source or destination. */
  if (location->source == TRUE)
    {
      /* Source. */
      file = ssh_file_copy_file_allocate();

      filename_copy = ssh_file_copy_strip_dot_dots(filename);
      ssh_file_copy_file_register_filename(file, filename_copy);

      /* XXX This should be removed here, and fixed in sshfc_glob.c .*/
      if (strlen(filename_copy) == 0)
        {
          ssh_xfree(filename_copy);
          filename_copy = ssh_xstrdup(".");
        }
      
      SSH_VERIFY(ssh_dllist_add_item(location->file_list,
                                     (void *)file,
                                     SSH_DLLIST_END) == SSH_DLLIST_OK);
    }
  else
    {
      /* Destination. */
      filename_copy = ssh_xstrdup(filename);
      ssh_file_copy_file_register_filename(location->file, filename_copy);
      file = location->file;
    }

  location->raw = TRUE;
  return file;
}

/* Add a filename to 'location->file_list' and/or 'location->file',
   depending on whether 'location' is source or destination. If
   'attrs' is null, it is assumed that the file hasn't been tested for
   existence or for it's attributes (ie. whether it is a directory or
   a regular file). */
void ssh_file_copy_location_add_file(SshFileCopyLocation location,
                                     SshFileAttributes attrs,
                                     const char *filename)
{
  char *filename_copy = NULL;
  SshFileCopyFile file;
  
  SSH_PRECOND(location);
  SSH_PRECOND(filename);

  if (filename[0] == '\0')
    {
      /* We were given an empty filename. */
      return;
    }
  
  filename_copy = ssh_xstrdup(filename);

  /* Remove extra '/'s from filename. */
  while (filename_copy[strlen(filename_copy) - 1] == '/')
    {    
      filename_copy[strlen(filename_copy) - 1] = '\0';
    }

  /* Check whether we are source or destination. */
  if (location->source == TRUE)
    {
      /* Source. */
      /* Get basename. */
      if (location->file->name == NULL)
        {
          /* if nothing has been added yet, initialize base dir. */
          if (attrs)
            {
              if ((attrs->permissions & S_IFMT) == S_IFDIR)
                {
                  char *basedir, *ph;

                  if (filename_copy[0] == '\0')
                    {                            
                      ssh_file_copy_file_register_filename(location->file,
                                                           ssh_xstrdup("/"));
                      
                      location->file->attributes =
                        ssh_file_attributes_dup(attrs);
                      location->raw = FALSE;
                    }
                  else
                    {
                      if ((ph = strrchr(filename_copy, '/')) != NULL)
                        {
                          *ph = '\0';
                          ph++;
                          basedir = ssh_xstrdup(filename_copy);

                          /* To identify files beginning with '/' */
                          if (basedir[0] == '\0')
                            {
                              ssh_xfree(basedir);
                              basedir = NULL;
                            }
                        }
                      else
                        {
                          basedir = ssh_xstrdup("");
                        }
                  
                      if (basedir == NULL)
                        {
                          /* Filename was of form '/etc' */
                          basedir = ssh_xstrdup("/");
                        }
                  

                      ssh_file_copy_file_register_filename(location->file,
                                                           basedir);

                      ssh_file_copy_location_add_file(location, attrs,
                                                      filename);
                    }
                }
              else
                {
                  char *temp;
                  /* File to be registered is an ordinary file; take
                     basedir, and put rest of the filename to list. */
                  if ((temp = strrchr(filename_copy, '/')) != NULL)
                    {
                      *temp = '\0';
                      temp++;

                      file =
                        ssh_file_copy_location_add_raw_file(location,
                                                            ssh_xstrdup(temp));
                      file->attributes = ssh_file_attributes_dup(attrs);

                      if (filename_copy[0] == '\0')
                        {
                          /* Filename is of form '/etc' */
                          ssh_xfree(filename_copy);
                          filename_copy = ssh_xstrdup("/");
                        }
                      
                      ssh_file_copy_file_register_filename(location->file,
                                                           ssh_xstrdup
                                                           (filename_copy));
                      location->raw = FALSE;
                    }
                  else
                    {
                      /* there was no basedir. directory will be the
                         default one (usually home directory on the
                         remote host). */
                      ssh_file_copy_file_register_filename(location->file,
                                                           ssh_xstrdup(""));
                      
                      file =
                        ssh_file_copy_location_add_raw_file(location,
                                                            filename_copy);
                      file->attributes = ssh_file_attributes_dup(attrs);
                      location->raw = FALSE;
                    }
                }
            }
          else
            {
              /* 'location' will be raw, and has to be parsed at some
                 point. */
              ssh_file_copy_location_add_raw_file(location, filename);
            }
        }
      else
        {
          /* Separate common beginning from filename. */
          if (strncmp(filename_copy, location->file->name,
                      strlen(location->file->name)) != 0)
            {
              /* XXX 'filename' isn't suitable for this location. */
              SSH_TRACE(1, ("Illegal filename to be added to list. "    \
                            "(filename = %s, basedir = %s",             \
                            filename_copy, location->file->name));
              
              SSH_NOTREACHED; /* XXX just for debugging. */
            }

          if (strcmp(filename_copy, location->file->name) != 0)
            {
              if (strcmp(ssh_file_copy_file_get_name(location->file),
                         "/") == 0)
                {
                  file = ssh_file_copy_location_add_raw_file
                    (location, &filename_copy[1]);
                }
              else
                {
                  file = ssh_file_copy_location_add_raw_file
                    (location,
                     *ssh_file_copy_file_get_name(location->file) == '\0' ?
                     filename_copy :
                     &filename_copy[strlen(ssh_file_copy_file_get_name
                                           (location->file)) + 1]);
                }
              file->attributes = ssh_file_attributes_dup(attrs);
            }
          else
            {
              /* If 'filename' is the base directory, we put the
                 attributes to the basedir */
              ssh_file_copy_file_register_attributes
                (location->file,
                 ssh_file_attributes_dup(attrs));
            }
          
          location->raw = FALSE;
        }
    }
  else
    {
      /* Destination. */
      /* Nothing special is needed. */
      file = ssh_file_copy_location_add_raw_file(location, filename_copy);

      file->attributes = ssh_file_attributes_dup(attrs);
      location->raw = FALSE;
    }
  ssh_xfree(filename_copy);
}

/* Allocate a SshFileCopyFileListItem item. */
SshFileCopyFileListItem ssh_file_copy_file_list_item_allocate(void)
{
  SshFileCopyFileListItem temp_list;

  SSH_DEBUG(4, ("Allocating SshFileCopyFileListItem structure..."));

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

  temp_list->files = ssh_file_copy_location_allocate(TRUE);

  return temp_list;
}

unsigned char *
ssh_file_copy_file_list_item_get_original_name(SshFileCopyFileListItem item)
{
  return item->original_filename;
}

unsigned char *
ssh_file_copy_file_list_item_get_directory_name(SshFileCopyFileListItem item)
{
  return item->files->file->name;
}

const char *
ssh_file_copy_file_list_item_get_name(SshFileCopyFileListItem item)
{
  ssh_dllist_rewind(item->files->file_list);
  return ssh_file_copy_file_get_name(ssh_dllist_current(item->files->file_list));
}

/* Destroy a SshFileCopyFileListItem item. */
void ssh_file_copy_file_list_item_destroy(SshFileCopyFileListItem item)
{

  SSH_DEBUG(6, ("Destroying SshFileCopyFileListItem structure..."));
  ssh_file_copy_location_destroy(item->files);
  ssh_xfree(item->original_filename);
  memset(item, 'F', sizeof(*item));
  ssh_xfree(item);
}

/* Destroy an entire SshFileCopyFileListItem list. */
void ssh_file_copy_file_list_destroy(SshDlList list)
{
  SSH_PRECOND(list);

  SSH_DEBUG(6, ("Destroying list of SshFileCopyFileListItems..."));

  ssh_dllist_rewind(list);
  
  while (ssh_dllist_is_current_valid(list))
    {
      SshFileCopyFileListItem temp_item;
      
      temp_item = ssh_dllist_delete_current(list);

      ssh_file_copy_file_list_item_destroy(temp_item);
    }

  ssh_dllist_free(list);
}

/* Get basedir from 'filename'. Return basedir. 'return_filename' will
   contain rest of the filename. */
char *ssh_file_copy_get_basedir(const char *filename,
                                     char **return_filename)
{
  char *filename_copy = NULL;
  char *base = NULL, *file = NULL;
  
  SSH_PRECOND(filename);
  SSH_PRECOND(return_filename);

  SSH_DEBUG(4, ("parsing filename \"%s\"...", filename));
  
  filename_copy = ssh_xstrdup(filename);

  /* Find next wildchar. */
  file = ssh_glob_next_unescaped_wildchar(filename_copy);

  if (!file)
    {
      /* If no wildchars, find last '/' */
      file = strrchr(filename_copy, '/');
    }
  if (!file)
    {
      /* If no last '/', the whole filename is one file. */
      *return_filename = filename_copy;
      base = ssh_xstrdup("");
      return base;
    }
  
  /* Find preceding '/'-char. */
  for (;file > filename_copy && *file != '/'; file--)
    ;

  if (file == filename_copy)
    {
      if (file[0] == '/')
        {
          /* the first part of the filename contains a wildcard, and
             it starts with a slash. */
          if (strlen(file) < 2)
            {
              *return_filename = ssh_xstrdup(".");
              base = ssh_xstrdup(file);
            }
          else
            {
              *return_filename = ssh_xstrdup(&file[1]);
              file[1] = '\0';
              base = ssh_xstrdup(filename_copy);
            }
          ssh_xfree(filename_copy);
        }
      else
        {
          /* If at start, basedir is empty string, and return_filename is
             the same as the given filename. */
          *return_filename = filename_copy;
          base = ssh_xstrdup("");
        }
    }
  else
    {
      /* Else cut it. */
      if (file != NULL)
        {
          *file = '\0';
          file++;
          *return_filename = ssh_xstrdup(file);
          
          base = ssh_xstrdup(filename_copy);
        }
      else
        {
          
        }
      
      ssh_xfree(filename_copy);
    }

  SSH_DEBUG(4, ("basedir = \"%s\", filename = \"%s\"",  \
                base, *return_filename));
  
  return base;
}

/* Removes escape characters before slashes ('\/' and the
   like). Returns a newly mallocated stripped string.*/
char *ssh_file_copy_strip_escapes_before_slashes(const char *filename)
{
  char *stripped = NULL;
  int len = 0, i = 0, str_ind = 0;
  int echar = '\\';
  
  SSH_PRECOND(filename);
  
  len = strlen(filename);
  
  stripped = ssh_xcalloc(len + 1, sizeof(char));

  for(i = 0, str_ind = 0; i < len - 1; i++, str_ind++)
    {
      if (filename[i] == echar && filename[i + 1] == '/' &&
          _ssh_glob_isescaped(&filename[i + 1], filename, echar))
        i++;

      stripped[str_ind] = filename[i];
    }

  stripped[str_ind] = filename[i];

  return stripped;
}

/* Removes extra slashes ('//' and the like) from filename. This also
   does the strip_escapes_before_slashes (those could be used to fool
   this). Returns a newly mallocated stripped string. */
char *ssh_file_copy_strip_extra_slashes(const char *filename)
{
  char *filename_copy = NULL, *ph = NULL;
  int i = 0, len = 0;
  SSH_PRECOND(filename);

  filename_copy = ssh_file_copy_strip_escapes_before_slashes(filename);

  while(filename_copy[i] != '\0')
    {
      for(;filename_copy[i] != '\0' && filename_copy[i] != '/'; i++)
        ;

      if (filename_copy[i] != '\0')
        i++;
      
      ph = &filename_copy[i];
      
      for(;filename_copy[i] != '\0' && filename_copy[i] == '/'; i++)
        ;
      
      len = strlen(&filename_copy[i]) + 1;
      memmove(ph, &filename_copy[i], len);
      
      i = ph - filename_copy;
    }
  
  return filename_copy;
}

/* Strip foo/../bar/../ combinations from filename to prevent
   malicious, or misguided, users from trashing some systems, and
   bringing the load up in others. */
char *ssh_file_copy_strip_dot_dots(const char *filename)
{
  char *filename_copy = NULL, *ph = NULL;
  int i = 0, len = 0;
  SSH_PRECOND(filename);

  filename_copy = ssh_file_copy_strip_extra_slashes(filename);

 restart:
  /* Skip first '/' characters. */
  for (;filename_copy[i] == '/' ; i++)
    ;

  /* ph points now to first non-'/' character. */
  ph = &filename_copy[i];

  
  if (strncmp(ph, "../", 3) == 0)
    {
      i += 3;
      goto restart;
    }

  if (strncmp(ph, "./", 2) == 0)
    {
      i += 2;
      goto restart;
    }

  /* Now we skip to next '/' character. */
  for (; filename_copy[i] != '\0' && filename_copy[i] != '/'; i++)
    ;

  /* If at end, return. */
  if (filename_copy[i] == '\0')
    return filename_copy;
  
  i ++;
  
  if (strncmp(&filename_copy[i], "../", 3) == 0 ||
      (strncmp(&filename_copy[i], "..", 2) == 0 &&
       filename_copy[i + 2] == '\0'))
    {
      /* Here we have a strippable case. */
      if (filename_copy[i + 2] == '\0')
        {
          *ph = '\0';
          return filename_copy;
        }
      len = strlen(&filename_copy[i + 3]) + 1;
      memmove(ph, &filename_copy[i + 3], len);

      i = ph - filename_copy;
    }

  goto restart;
}

/* Register a callback, which is used to connect to the remote host by
   SshFileCopy. 'context' is given to the callback as an argument. */
void ssh_file_copy_register_connect_callback(SshFileCopyConnectCallback
                                             callback,
                                             void *context)
{
  connect_callback = callback;
  connect_context = context;
}

typedef struct ConnectionCompletionCtxRec
{
  SshFileCopyConnection connection;
  SshFileCopyConnectCompleteCB completion_cb;
  void *completion_context;
  SshOperationHandle op_handle;
  Boolean aborted;
} *ConnectionCompletionCtx;

void stream_return_cb(SshStream stream,
                      void *context)
{      
  ConnectionCompletionCtx ctx = (ConnectionCompletionCtx) context;

  if (ctx->aborted == TRUE)
    {
      if (stream)
        ssh_stream_destroy(stream);
      ssh_xfree(ctx);
    }
  
  if (stream == NULL)
    {
      SSH_TRACE(1, ("Connection failed."));
      (*ctx->completion_cb)(NULL, ctx->completion_context);
      return;
    }
  ctx->connection->client = ssh_file_client_wrap(stream);
  (*ctx->completion_cb)(ctx->connection->client,
                        ctx->completion_context);

  ssh_operation_unregister(ctx->op_handle);
  
  ssh_xfree(ctx);
}

void connect_abort_cb(void *operation_context)
{
  ConnectionCompletionCtx ctx =
    (ConnectionCompletionCtx) operation_context;

  ctx->aborted = TRUE;
}

/* Make a connection to the remote host, or, if 'host' is NULL,
   create streampair for local action. */
SshOperationHandle
ssh_file_copy_connect(SshFileCopyConnection connection,
                      SshFileCopyConnectCompleteCB completion_cb,
                      void *completion_context)
{
  SSH_PRECOND(connection);
  SSH_PRECOND(completion_cb);
  
  if (connection->host == NULL)
    {
      SshFileClient local_client = NULL;
      SshFileServer local_server = NULL;
      SshStream stream1 = NULL, stream2 = NULL;
      
      SSH_TRACE(2, ("Making local connection."));

      /* Make local connection. */
      ssh_stream_pair_create(&stream1, &stream2);
      local_server = ssh_file_server_wrap(stream1);
      local_client = ssh_file_client_wrap(stream2);

      /* The server doesn't have to freed implicitly, as it is freed
         when ssh_file_client_destroy is called. */
      connection->client = local_client;
      (*completion_cb)(connection->client, completion_context);
      return NULL;
    }
  else
    {
      ConnectionCompletionCtx new_ctx = ssh_xcalloc(1, sizeof(*new_ctx));
      
      SSH_TRACE(2, ("Connecting to remote host. (host = %s, "       \
                    "user = %s, port = %s)", connection->host,      \
                    connection->user, connection->port));
      /* use SshFileCopyConnectCallback to connect. */
      SSH_ASSERT(connect_callback);
      new_ctx->connection = connection;
      new_ctx->completion_cb = completion_cb;
      new_ctx->completion_context = completion_context;
      new_ctx->op_handle = ssh_operation_register(connect_abort_cb,
                                                  new_ctx);
      
      (*connect_callback)(connection, connect_context,
                          stream_return_cb, new_ctx);
      return new_ctx->op_handle;
    }
}
