/* tag: Scott Parish Sun Feb  10 13:01:05 2002 (sftp-protocol.c)
 */
/* sftp-protocol.c -
 *
 ****************************************************************
 * Copyright (C) 2002 Scott Parish
 * 
 * See the file "COPYING" for further information about
 * the copyright and warranty status of this work.
 */

/*
 * Notes about sftp:
 *    string:
 *      Although not mentioned in draft [draft-ietf-secsh-filexfer-02.txt]
 *      all strings in sftp are of the form
 *
 *          uint32    length
 *          string    data (without null termination)
 *
 *    length field:
 *      The length field in each packet excludes *only* the length field
 *      itself, the size of the type field IS included in the number found
 *      in the length field.
 *
 *
 */


#include <stdio.h>
#include "hackerlab/os/errno.h"
#include "hackerlab/os/errno-to-string.h"
#include "hackerlab/char/str.h"
#include "hackerlab/mem/mem.h"
#include "hackerlab/fmt/cvt.h"
#include "hackerlab/vu/vfdbuf.h"
#include "hackerlab/vu-network/url-socket.h"
#include "hackerlab/arrays/ar.h"
#include "hackerlab/bugs/panic.h"
#include "hackerlab/vu/safe-printfmt.h"
#include "hackerlab/vu/safe-vfdbuf.h"
#include "ftp-utils/clients/sftp-protocol.h"
#include "ftp-utils/clients/sftp.h"
#include "ftp-utils/clients/misc.h"

static unsigned long request_id = 0;
static char *cwd = 0;

#define WRITE_UINT4(str, ui) do { \
  (str)[0] = (ui) >> 24; \
  (str)[1] = (ui) >> 16; \
  (str)[2] = (ui) >> 8; \
  (str)[3] = (ui); } while (0)

#define WRITE_UINT8(str, ui) do { \
  (str)[0] = (ui) >> 56; \
  (str)[1] = (ui) >> 48; \
  (str)[2] = (ui) >> 40; \
  (str)[3] = (ui) >> 32; \
  (str)[4] = (ui) >> 24; \
  (str)[5] = (ui) >> 16; \
  (str)[6] = (ui) >> 8; \
  (str)[7] = (ui); } while (0)

#define READ_UINT4(str) ( \
  ((unsigned long)(unsigned char)((str)[0]) << 24) | \
  ((unsigned long)(unsigned char)((str)[1]) << 16) | \
  ((unsigned long)(unsigned char)((str)[2]) << 8) | \
  ((unsigned long)(unsigned char)((str)[3])) )

#define READ_UINT8(str) ( \
  ((unsigned long long int)(unsigned char)((str)[0]) << 56) | \
  ((unsigned long long int)(unsigned char)((str)[1]) << 48) | \
  ((unsigned long long int)(unsigned char)((str)[2]) << 40) | \
  ((unsigned long long int)(unsigned char)((str)[3]) << 32) | \
  ((unsigned long long int)(unsigned char)((str)[4]) << 24) | \
  ((unsigned long long int)(unsigned char)((str)[5]) << 16) | \
  ((unsigned long long int)(unsigned char)((str)[6]) << 8) | \
  ((unsigned long long int)(unsigned char)((str)[7])) )



char *
sftp_abs_path (alloc_limits limits,
	       char *path)
{
  char *file;
  if (path[0] != '/')
      file = str_alloc_cat_many (limits, cwd, "/", path, str_end);
  else
      file = str_save (limits, path);

  return dirfold (limits, file);
}



int
sftp_io_retry (t_uchar **text,
	  int (*func) (),
	  int fd,
	  char *buf,
	  unsigned int count)
{
  unsigned int left;

  left = count;
  while (1)
    {
      int amt;

      amt = (*func) (fd, buf, left);
      if ((amt < 0) && (errno != EAGAIN) && (errno != EINTR))
	{
	  char *s;

          s = errno_to_string (errno);
	  *text = str_save (0, s);
	  return -1;
	}

      if (amt == left || !amt)
	return count - (left - amt);

      left -= amt;
      buf += amt;
    }
}
  



/* take the structure and create an sftp attr string, returns size of string */
int
sftp_export_attrs (alloc_limits limits,
		   struct sftp_attrs *a,
		   char **pkt,
		   int *len)
{
  int i = 0;

  *len = sizeof (a->flags) 
          + (a->flags & SSH_FILEXFER_ATTR_SIZE ? sizeof (a->size) : 0)
          + (a->flags & SSH_FILEXFER_ATTR_UIDGID ? sizeof (a->uid)*2 : 0)
          + (a->flags & SSH_FILEXFER_ATTR_PERMISSIONS ? sizeof (a->perms) : 0)
          + (a->flags & SSH_FILEXFER_ATTR_ACMODTIME ? sizeof (a->atime)*2 : 0);

  *pkt = lim_malloc (limits, *len);

  WRITE_UINT4 (*pkt, a->flags);
  i += 4;

  if (a->flags & SSH_FILEXFER_ATTR_SIZE)
    {
      WRITE_UINT8 (*pkt + i, a->size);
      i += 8;
    }

  if (a->flags & SSH_FILEXFER_ATTR_UIDGID)
    {
      WRITE_UINT4 (*pkt + i, a->uid);
      i += 4;
      WRITE_UINT4 (*pkt + i, a->gid);
      i += 4;
    }
  
  if (a->flags & SSH_FILEXFER_ATTR_PERMISSIONS)
    {
      WRITE_UINT4 (*pkt + i, a->perms);
      i += 4;
    }

  if (a->flags & SSH_FILEXFER_ATTR_ACMODTIME)
    {
      WRITE_UINT4 (*pkt + i, a->atime);
      i += 4;
      WRITE_UINT4 (*pkt + i, a->mtime);
      i += 4;
    }

  return *len;
}

/* take an sftp attr string and initialize a structure from it */
int
sftp_import_attrs (alloc_limits limits,
		   t_uchar ** text,
		   struct sftp_attrs *a,
		   char *pkt,
		   unsigned int pkt_len)
{
  int count, i, j;
  unsigned int len;

  i = 0;

  a->flags = READ_UINT4 (pkt);
  i += 4;

  len = (a->flags & SSH_FILEXFER_ATTR_SIZE ? sizeof (a->size) : 0)
      + (a->flags & SSH_FILEXFER_ATTR_UIDGID ? sizeof (a->uid)*2 : 0)
      + (a->flags & SSH_FILEXFER_ATTR_PERMISSIONS ? sizeof (a->perms) : 0)
      + (a->flags & SSH_FILEXFER_ATTR_ACMODTIME ? sizeof (a->atime)*2 : 0);

  if (len > pkt_len - i)
    {
      *text = str_save (0, "sftp_import_attrs: Packet to short: did we get garbage from server?");
      return -1;
    }

  if (a->flags & SSH_FILEXFER_ATTR_SIZE)
    {
      a->size = READ_UINT8 (pkt + i);
      i += 8;
    }

  if (a->flags & SSH_FILEXFER_ATTR_UIDGID)
    {
      a->uid = READ_UINT4 (pkt + i);
      i += 4;
      a->gid = READ_UINT4 (pkt + i);
      i += 4;
    }
  
  if (a->flags & SSH_FILEXFER_ATTR_PERMISSIONS)
    {
      a->perms = READ_UINT4 (pkt + i);
      i += 4;
    }

  if (a->flags & SSH_FILEXFER_ATTR_ACMODTIME)
    {
      a->atime = READ_UINT4 (pkt + i);
      i += 4;
      a->mtime = READ_UINT4 (pkt + i);
      i += 4;
    }

  if (a->flags & SSH_FILEXFER_ATTR_EXTENDED)
    {
      count = READ_UINT4 (pkt + i);
      for (j = 0; j < count; j++)
	{
	  i += READ_UINT4 (pkt + i);
	  i += READ_UINT4 (pkt + i);
	}
    }

  return i;
}


int
sftp_decode_status (alloc_limits limits,
		    t_uchar **text,
		    char *pkt,
		    int pkt_len,
		    int i)
{
  int code, err_len;

  code = READ_UINT4 (pkt + i);
  i += 4;

  if (code == SSH_FX_OK || code == SSH_FX_EOF)
      return 0;

  err_len = READ_UINT4 (pkt + i);
  i += 4;

  if (err_len > pkt_len - i)
    {
      *text = str_save (0, "sftp_decode_status: Packet to short: possibly garbage from server?");
      return -1;
    }

  *text = str_save_n (0, pkt + i, err_len);
  return -1;
}


unsigned int
sftp_server_response (alloc_limits limits,
		      int *errn,
		      t_uchar **text,
		      int fd,
		      char **pkt,
		      int *pkt_len,
		      int expected_response)
{
  int i;
  unsigned int len, read_len, total_read, response;
  unsigned char *p;
  unsigned char buf[4];

  read_len = sftp_io_retry (text, read, fd, buf, 4);
  if (read_len < 4 || *errn == -1)
    {
      *text = str_save (0, "Error reading from server");
      return -1;
    }

  *pkt_len = len = READ_UINT4 (buf);

  *pkt = p = lim_malloc (limits, len);

  total_read = 0;
  while (len)
    {
      read_len = sftp_io_retry (text, read, fd, p + total_read,
				MIN(len, BUFSIZ));
      if (read_len <= 0 || read_len > len || *errn == -1)
	{
	  lim_free (limits, p);
	  *text = str_save (0, "Error reading from server");
	  return -1;
	}
      total_read += read_len;
      len -= read_len;
    }

    i = 0;
    response = p[i++];

    if (response != SSH_FXP_VERSION)
      {
	if (READ_UINT4 (p + i) != request_id)
	  {
	    lim_free (limits, p);
	    *text = str_save (0, "Error: server returned wrong request id");
	    return -1;
	  }
	i += 4;
      }

    if (response != expected_response)
      {
	if (response == SSH_FXP_STATUS)
	  {
	    return sftp_decode_status (limits, text, p, *pkt_len, i);
	  }
	  
	lim_free (limits, p);
        *text = str_save (0, "Error: server returned wrong response");
	return -1;
      }
    
  return i;
}


int
sftp_write_pkt (alloc_limits limits,
	   int *errn,
	   t_uchar **text,
	   int fd,
	   char *fmt,
	   ...)
{
  va_list ap;
  struct sftp_attrs *attrs;
  char *buf;
  int i;
  unsigned int len, s_len, ret;
  unsigned long l;
  unsigned long long ll;
  char *s;
  unsigned char c;

  va_start (ap, fmt);

  len = 4;
  buf = lim_malloc (limits, len);
  i = 4;

  while (*fmt)
    switch (*fmt++)
      {
	case '8': /* int64 */
	  ll = va_arg (ap, unsigned long long);
	  len += sizeof (ll);
	  buf = lim_realloc (limits, buf, len);
	  WRITE_UINT8 (buf + i, ll);
	  i += sizeof (ll);
	  break;

	case '4': /* int32 */
	  l = va_arg (ap, unsigned long);
	  len += sizeof (l);
	  buf = lim_realloc (limits, buf, len);
	  WRITE_UINT4 (buf + i, l);
	  i += sizeof (l);
	  break;

	case '1': /* int8 */
	  c = va_arg (ap, int);
	  len += sizeof (c);
	  buf = lim_realloc (limits, buf, len);
	  buf[i] = c;
	  i += sizeof (c);
	  break;

	case 's': /* string  (IE: <int32:length> <string>) */
	  s = va_arg (ap, char *);
	  l = va_arg (ap, int);
	  len += sizeof (l) + l;
	  buf = lim_realloc (limits, buf, len);
	  WRITE_UINT4 (buf + i, l);
	  i += sizeof (l);
	  mem_move (buf + i, s, l);
	  i += l;
	  break;

	case 'a': /* attributes */
  	  attrs = va_arg (ap, struct sftp_attrs *);
	  sftp_export_attrs (limits, attrs, &s, &s_len);
	  len += s_len;
	  buf = lim_realloc (limits, buf, len);
	  mem_move (buf + i, s, s_len);
	  i += s_len;
	  break;

	default:
	  *text =  str_save (0, "Invalid fmt to sftp_write_pkt!\n");
	  return -1;
      }

  WRITE_UINT4 (buf, len - sizeof(len));
  ret = sftp_io_retry (text, write, fd, buf, len);

  lim_free (limits, buf);
  va_end (ap);

  return ret;
}


int
sftp_is_dir (alloc_limits limits,
	int * errn,
	t_uchar ** text,
	int in_fd,
	int out_fd,
	char * path)
{
  int ret, i;
  unsigned int pkt_len;
  char *pkt, *dir;
  struct sftp_attrs attrs;

  dir = sftp_abs_path (limits, path);

  ret = sftp_write_pkt (limits, errn, text, out_fd,
		        "14s", SSH_FXP_STAT, ++request_id, dir,
			str_length (dir));

  if (*errn < 0 || ret < 0) 
      return -1;
  
  lim_free (limits, dir);

  if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt,
				     &pkt_len, SSH_FXP_ATTRS)))
      return i;

  if (0 > (i = sftp_import_attrs (limits, text, &attrs, pkt + i, pkt_len - i)))
    {
      lim_free (limits, pkt);
      return i;
    }

  ret = 0;
  if (attrs.flags & SSH_FILEXFER_ATTR_PERMISSIONS && attrs.perms & S_IFDIR)
     ret = 1; 

  lim_free (limits, pkt);

  return ret;
}
	

int
sftp_client_do_init (alloc_limits limits, int * errn, int * code,
 		     t_uchar ** text, int in_fd, int out_fd)
{
  int ret;
  int pkt_len;
  char *pkt;
  int i;

  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14", SSH_FXP_INIT, SFTP_VERSION);

  if (*errn < 0 || ret < 0) 
      return -1;

  i = sftp_server_response (limits, errn, text, in_fd, &pkt, &pkt_len,
			    SSH_FXP_VERSION);

  if (i == -1 || *errn == -1)
      return -1;

  lim_free (limits, pkt);
  
  return 0;
}


int
sftp_client_connect_and_login (int * in_fd,
			      int * out_fd,
			      alloc_limits limits,
			      int * errn,
			      int * code,
			      t_uchar ** text,
			      long * text_len,
			      t_uchar * host)
{
  t_uchar *hostname;
  char **ssh_argv;
  int status, subproc;
  int pin[2], pout[2];
  int c_in_fd, c_out_fd;

  hostname = str_save (0, host);

  ssh_argv = 0;
  *(t_uchar **)ar_push ((void **)&ssh_argv, 0, sizeof (t_uchar *)) = "ssh";
  *(t_uchar **)ar_push ((void **)&ssh_argv, 0, sizeof (t_uchar *)) = "-oFallBackToRsh=no";
  *(t_uchar **)ar_push ((void **)&ssh_argv, 0, sizeof (t_uchar *)) = "-oForwardX11=no";
  *(t_uchar **)ar_push ((void **)&ssh_argv, 0, sizeof (t_uchar *)) = "-oForwardAgent=no";
/*  *(t_uchar **)ar_push ((void **)&ssh_argv, 0, sizeof (t_uchar *)) = "-oClearAllForwardings=yes"; */
  *(t_uchar **)ar_push ((void **)&ssh_argv, 0, sizeof (t_uchar *)) = "-s";
  *(t_uchar **)ar_push ((void **)&ssh_argv, 0, sizeof (t_uchar *)) = hostname;
  *(t_uchar **)ar_push ((void **)&ssh_argv, 0, sizeof (t_uchar *)) = "sftp";
  *(t_uchar **)ar_push ((void **)&ssh_argv, 0, sizeof (t_uchar *)) = 0;
  
  if ((pipe (pin) == -1) || (pipe (pout) == -1))
	panic ("Couldn't create pipe(s)");
  
  *out_fd = pout[1];
  *in_fd = pin[0];
  c_in_fd = pout[0];
  c_out_fd = pin[1]; 

  subproc = fork ();

  if (subproc < 0)
    panic ("unable to fork");

  if (subproc == 0)
    {
      if ((dup2 (c_in_fd, STDIN_FILENO) == -1) ||
	  (dup2 (c_out_fd, STDOUT_FILENO) == -1)) 
	{
	  safe_printfmt (2, "dup2: %s\n", errno_to_string (errno));
          exit (1);
	}

      close (*in_fd);
      close (*out_fd);
      close (c_in_fd);
      close (c_out_fd);
      execvp (ssh_argv[0], ssh_argv);
      safe_printfmt (2, "exec: %s: %s\n", ssh_argv[0], errno_to_string (errno));
      exit (1);
    }

  close (c_in_fd);
  close (c_out_fd);

  if (   (0 > vfdbuf_buffer_fd (errn, *in_fd, 0, O_RDONLY, 0))
      || (0 > vfdbuf_buffer_fd (errn, *out_fd, 0, O_WRONLY, 0))
      || (0 > sftp_client_do_init (limits, errn, code, text, *in_fd, *out_fd)))
    {
      int e;
      vu_close (&e, *in_fd);
      vu_close (&e, *out_fd);
      *text_len = str_length (*text);
      status = -1;
    }
  else
    {
      status = 0;
    }

  lim_free (0, hostname);

  return status;
}


/****************************************************************
 * sftp_cwd
 */

int
sftp_client_cwd (alloc_limits limits,
		 int * errn,
		 int * code,
		 t_uchar ** text,
		 long * text_len,
		 int in_fd,
		 int out_fd,
		 t_uchar * path)
{
  int ret, i;
  unsigned int pkt_len, handle_len;
  char *pkt, *dir, *handle;

  dir = sftp_abs_path (limits, path);

  /* try to open() `dir' to make somewhat sure it is valid and in our perms */
  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14s", SSH_FXP_OPENDIR, ++request_id, dir,
			str_length (dir));

  if (*errn < 0 || ret < 0) 
    {
      *text_len = str_length (*text);
      lim_free (limits, dir);
      return -1;
    }

  if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt, &pkt_len,
				     SSH_FXP_HANDLE)))
    {
      lim_free (limits, dir);
      *text_len = str_length (*text);
      return -1;
    }

  handle_len = READ_UINT4 (pkt + i);
  i += 4;
  if (handle_len > pkt_len - i)
    {
      *text = str_save (0, "sftp_client_cwd: Packet to short: did we get garbage from server?");
      lim_free (limits, pkt);
      lim_free (limits, dir);
      return -1;
    }
  handle = lim_malloc (limits, handle_len);
  mem_move (handle, pkt + i, handle_len);

  lim_free (limits, pkt);

  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14s", SSH_FXP_CLOSE, ++request_id, handle,
			handle_len);

  if (*errn < 0 || ret < 0) 
    {
      *text_len = str_length (*text);
      lim_free (limits, dir);
      return -1;
    }

  i = sftp_server_response (limits, errn, text, in_fd, &pkt, &pkt_len,
			    SSH_FXP_STATUS);
  if (i == -1)
    {
      lim_free (limits, dir);
      *text_len = str_length (*text);
      return -1;
    }

  if (cwd)
      lim_free (limits, cwd);
  cwd = dir;

  return 0;
}



/****************************************************************
 * sftp_cdup
 */

int
sftp_client_cdup (alloc_limits limits,
		 int * errn,
		 int * code,
		 t_uchar ** text,
		 int in_fd,
		 int out_fd)
{
  int status;
  long i;
  char *path;

  path = str_alloc_cat (limits, cwd, "/..");
  status = sftp_client_cwd (limits, errn, code, text, &i, in_fd, out_fd, path);
  lim_free (limits, path);

  return status;
}


/****************************************************************
 * sftp_rename
 */
int
sftp_client_rename (alloc_limits limits,
		   int * errn,
		   int * code,
		   t_uchar ** text,
		   int in_fd,
		   int out_fd,
		   t_uchar * from,
		   t_uchar * to)
{
  int ret, i;
  unsigned int pkt_len;
  char *pkt, *from2, *to2, *file, *b;

  from2 = sftp_abs_path (limits, from);
  to2 = sftp_abs_path (limits, to);

  if (0 < sftp_is_dir (limits, errn, text, in_fd, out_fd, to2))
    {
      b = file = str_save (limits, from);

filename:
      file = str_chr_rindex (file, '/');
      if (!file)
	  file = b;
      else if (!file[1])
	{
	  file[0] = 0;
	  goto filename;
       	}
      else
	  file++;

      to2 = str_realloc_cat_many (limits, to2, "/", file, str_end);
      lim_free (limits, b);
    }

  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14ss", SSH_FXP_RENAME, ++request_id, from2,
			str_length (from2), to2, str_length (to2));

  lim_free (limits, from2);
  lim_free (limits, to2);

  if (*errn < 0 || ret < 0) 
      return -1;

  if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt, &pkt_len,
				     SSH_FXP_STATUS)))
      return i;

  i = sftp_decode_status (limits, text, pkt, pkt_len, i);

  lim_free (limits, pkt);

  return i;
}


int
sftp_client_unlink (alloc_limits limits,
		   int * errn,
		   int * code,
		   t_uchar ** text,
		   int in_fd,
		   int out_fd,
		   t_uchar * path)
{
  int ret, i;
  unsigned int pkt_len;
  char *pkt, *file;

  file = sftp_abs_path (limits, path);

  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14s", SSH_FXP_REMOVE, ++request_id, file,
			str_length (file));

  lim_free (limits, file);

  if (*errn < 0 || ret < 0) 
      return -1;

  if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt, &pkt_len,
				     SSH_FXP_STATUS)))
      return i;

  i = sftp_decode_status (limits, text, pkt, pkt_len, i);

  lim_free (limits, pkt);

  return i;
}


/****************************************************************
 * sftp_rmd
 */
int
sftp_client_rmd (alloc_limits limits,
		 int * errn,
		 int * code,
		 t_uchar ** text,
		 int in_fd,
		 int out_fd,
		 t_uchar * path)
{
  int ret, i;
  unsigned int pkt_len;
  char *pkt, *dir;

  dir = sftp_abs_path (limits, path);

  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14s", SSH_FXP_RMDIR, ++request_id, dir,
			str_length (dir));

  lim_free (limits, dir);

  if (*errn < 0 || ret < 0) 
      return -1;

  if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt, &pkt_len,
				     SSH_FXP_STATUS)))
      return i;

  i = sftp_decode_status (limits, text, pkt, pkt_len, i);

  lim_free (limits, pkt);

  return i;
}


/****************************************************************
 * sftp_mkd
 */
int
sftp_client_mkd (alloc_limits limits,
		 int * errn,
		 int * code,
		 t_uchar ** text,
		 int in_fd,
		 int out_fd,
		 t_uchar * path)
{
  int ret, i;
  unsigned int pkt_len;
  char *pkt, *dir;
  struct sftp_attrs attrs;

  attrs.flags = SSH_FILEXFER_ATTR_PERMISSIONS;
  attrs.perms = SFTP_DEFAULT_DIR_MODE;

  dir = sftp_abs_path (limits, path);

  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14sa", SSH_FXP_MKDIR, ++request_id, dir,
			str_length (dir), &attrs);
  
  lim_free (limits, dir);

  if (*errn < 0 || ret < 0) 
      return -1;

  if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt, &pkt_len,
				     SSH_FXP_STATUS)))
      return i;

  i = sftp_decode_status (limits, text, pkt, pkt_len, i);

  lim_free (limits, pkt);

  return i;
}


/****************************************************************
 * sftp_client_pwd
 */
int
sftp_client_pwd (alloc_limits limits,
		 int * errn,
		 int * code,
		 t_uchar ** text,
		 int in_fd,
		 int out_fd)
{
  *text = str_save (0, cwd);

  return 0;
}


/****************************************************************
 * sftp_client_list
 */

int
sftp_client_list (alloc_limits limits,
		  int *errn, int *code,
		  t_uchar ** text,
		  int in_fd,
		  int out_fd,
		  int data_fd,
		  t_uchar * path,
		  int longfmt)
{
  struct sftp_attrs attrs;
  unsigned int pkt_len, j, count, r;
  char *pkt, *dir;
  int len, ret, handle_len, dots, i;
  char *handle;

  dir = sftp_abs_path (limits, path);

chop_slashes:
  i = str_length (dir);
  if (i > 1 && dir[i - 1] == '/')
    {
      dir[i - 1] = 0;
      goto chop_slashes;
    }

  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14s", SSH_FXP_OPENDIR, ++request_id, dir,
			str_length (dir));
    
  lim_free (limits, dir);

  if (*errn < 0 || ret < 0) 
      return -1;

  if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt, &pkt_len,
				     SSH_FXP_HANDLE)))
      return i;

  handle_len = READ_UINT4 (pkt + i);
  i += 4;
  if (handle_len > pkt_len - i)
    {
      *text = str_save (0, "sftp_client_list: Packet to short: did we get garbage from server?");
      lim_free (limits, pkt);
      return -1;
    }
  handle = lim_malloc (limits, handle_len);
  mem_move (handle, pkt + i, handle_len);

  lim_free (limits, pkt);
    
  while (1)
    {
      ret = sftp_write_pkt (limits, errn, text, out_fd,
			    "14s", SSH_FXP_READDIR, ++request_id, handle,
			    handle_len);

      if (*errn < 0 || ret < 0) 
	{
	  lim_free (limits, handle);
	  return -1;
	}

      if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt,
				         &pkt_len, SSH_FXP_NAME)))
	{
	  lim_free (limits, handle);
	  return i;
	}

      if (i == 0)
	{
	  lim_free (limits, pkt);
	  break;
	}

      count = READ_UINT4 (pkt + i);
      i += 4;

      for (j = 0; j < count; j++)
	{
	  len = READ_UINT4 (pkt + i);
	  i += 4;
	  if (len > pkt_len - i)
	    {
	      *text = str_save (0, "sftp_client_list: Packet to short: did we get garbage from server?");
	      lim_free (limits, handle);
	      lim_free (limits, pkt);
	      return -1;
	    }

	  dots = 0;
	  if ((len == 1 && pkt[i] == '.') ||
	      (len == 2 && pkt[i] == '.' && pkt[i+1] == '.'))
	    dots = 1;

	  if (!longfmt && !dots)
	    {
	      if (0 > sftp_io_retry (text, write, data_fd, pkt+i, len) ||
		  0 > sftp_io_retry (text, write, data_fd, "\r\n", 2))
		{
		  lim_free (limits, handle);
		  lim_free (limits, pkt);
		  return r;
		}
	    }
	  i += len;

	  len = READ_UINT4 (pkt + i);
	  i += 4;
	  if (len > pkt_len - i)
	    {
	      *text = str_save (0, "sftp_client_list: Packet to short: did we get garbage from server?");
	      lim_free (limits, pkt);
	      lim_free (limits, handle);
	      return -1;
	    }

	  if (longfmt && !dots)
	    {
	      if (0 > sftp_io_retry (text, write, data_fd, pkt+i, len) ||
		  0 >  sftp_io_retry (text, write, data_fd, "\r\n", 2))
		{
		  lim_free (limits, handle);
		  lim_free (limits, pkt);
		  return r;
		}
	    }
	  i += len;

	  len = sftp_import_attrs (limits, text, &attrs, pkt + i, pkt_len - i);
	  if (0 > len)
	    {
	      lim_free (limits, handle);
	      lim_free (limits, pkt);
	      return len;
	    }
	  i += len;
	}

      lim_free (limits, pkt);
    }

  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14s", SSH_FXP_CLOSE, ++request_id, handle,
			handle_len);
  lim_free (limits, handle);

  if (*errn < 0 || ret < 0) 
      return -1;

  i = sftp_server_response (limits, errn, text, in_fd,
			    &pkt, &pkt_len, SSH_FXP_HANDLE);

  if (i >= 0)
      lim_free (limits, pkt);

  return i;
}



int
sftp_client_get (alloc_limits limits,
		 int *errn,
		 int *code,
		 t_uchar ** text,
		 int in_fd,
		 int out_fd,
		 int data_fd,
		 t_uchar * path)
{
  struct sftp_attrs attrs;
  unsigned int pkt_len;
  char *pkt, *file;
  int ret, handle_len, i;
  unsigned long long total_read;
  char *handle;
  int len;

  file = sftp_abs_path (limits, path);

  attrs.flags = 0;
  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14s4a", SSH_FXP_OPEN, ++request_id, file,
			str_length (file), SSH_FXF_READ, &attrs);
  lim_free (limits, file);

  if (*errn < 0 || ret < 0) 
      return -1;
    
  if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt, &pkt_len,
				     SSH_FXP_HANDLE)))
      return i;

  handle_len = READ_UINT4 (pkt + i);
  i += 4;
  if (handle_len > pkt_len - i)
    {
      *text = str_save (0, "sftp_client_get: Packet to short: did we get garbage from server?");
      lim_free (limits, pkt);
      return -1;
    }
  handle = lim_malloc (limits, handle_len);
  mem_move (handle, pkt + i, handle_len);

  lim_free (limits, pkt);

  total_read = 0;
  while (1)
    {
      ret = sftp_write_pkt (limits, errn, text, out_fd,
			    "14s84", SSH_FXP_READ, ++request_id, handle,
			    handle_len, total_read, SFTP_RWSIZE);

      if (*errn < 0 || ret < 0) 
	{
	  lim_free (limits, handle);
	  return -1;
	}

      if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt,
					 &pkt_len, SSH_FXP_DATA)))
	{
	  lim_free (limits, handle);
	  return -1;
	}

      if (i == 0)
	{
	  lim_free (limits, pkt);
	  break;
	}

      len = READ_UINT4 (pkt + i);
      i += 4;
      if (len > pkt_len - i)
	{
	  *text = str_save (0, "sftp_client_get: Packet to short: did we get garbage from server?");
	  lim_free (limits, handle);
	  lim_free (limits, pkt);
	  return -1;
	}

      if (len != sftp_io_retry (text, write, data_fd, pkt+i, len))
	{
	  *text = str_save (0, "Error while writing!");
	  return -1;
	}
      i += len;
      total_read += len;

      lim_free (limits, pkt);
    }

  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14s", SSH_FXP_CLOSE, ++request_id, handle,
			handle_len);
  lim_free (limits, handle);

  if (*errn < 0 || ret < 0) 
      return -1;

  i = sftp_server_response (limits, errn, text, in_fd,
			    &pkt, &pkt_len, SSH_FXP_STATUS);

  if (i >= 0)
      lim_free (limits, pkt);

  return i;
}

int
sftp_client_put (alloc_limits limits,
		 int *errn,
		 int *code,
		 t_uchar ** text,
		 int in_fd,
		 int out_fd,
		 int data_fd,
		 int connect_out,
		 t_uchar * path)
{
  struct sftp_attrs attrs;
  unsigned int pkt_len;
  char *pkt, *file;
  int ret, handle_len, i;
  unsigned long long total_written;
  char *handle, *message;
  char buf[SFTP_RWSIZE];
  int len;

  file = sftp_abs_path (limits, path);

  attrs.flags = SSH_FILEXFER_ATTR_PERMISSIONS;
  attrs.perms = SFTP_DEFAULT_DIR_MODE;

  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14s4a", SSH_FXP_OPEN, ++request_id, file,
			str_length (file),
			SSH_FXF_WRITE | SSH_FXF_CREAT | SSH_FXF_TRUNC, &attrs);
  lim_free (limits, file);

  if (*errn < 0 || ret < 0) 
      return -1;
    
  if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt, &pkt_len,
				     SSH_FXP_HANDLE)))
      return i;

  /* normally this next block would have been in with-ftp.c, but we can't
   * send this until we have a file handle in hand, or errors won't be shown */
  message = str_save (limits, "file opened for put");
  safe_printfmt (connect_out, "OK %lu\n %s\n",
		 (unsigned long)(2 + str_length (message)), message);
  safe_flush (connect_out);
  lim_free (0, message);

  handle_len = READ_UINT4 (pkt + i);
  i += 4;
  if (handle_len > pkt_len - i)
    {
      *text = str_save (0, "sftp_client_put: Packet to short: did we get garbage from server?");
      lim_free (limits, pkt);
      return -1;
    }
  handle = lim_malloc (limits, handle_len);
  mem_move (handle, pkt + i, handle_len);

  lim_free (limits, pkt);

  total_written = 0;
  while (1)
    {
      len = sftp_io_retry (text, read, data_fd, buf, sizeof (buf));
      ret = sftp_write_pkt (limits, errn, text, out_fd,
			    "14s8s", SSH_FXP_WRITE, ++request_id, handle,
			    handle_len, total_written, buf, len);

      if (0 > (i = sftp_server_response (limits, errn, text, in_fd, &pkt,
					 &pkt_len, SSH_FXP_STATUS)))
	{
	  lim_free (limits, handle);
	  return i;
	}

      i = sftp_decode_status (limits, text, pkt, pkt_len, i);

      lim_free (limits, pkt);

      if (i == -1)
	{
	  lim_free (limits, handle);
	  return -1;
	}

      total_written += len;

      if (len < sizeof (buf))
        break;
    }

  ret = sftp_write_pkt (limits, errn, text, out_fd,
			"14s", SSH_FXP_CLOSE, ++request_id, handle,
			handle_len);
  lim_free (limits, handle);

  if (*errn < 0 || ret < 0) 
      return -1;

  i = sftp_server_response (limits, errn, text, in_fd,
			    &pkt, &pkt_len, SSH_FXP_STATUS);

  if (i >= 0)
      lim_free (limits, pkt);

  return i;

}
