/* pfs-dav.c:
 *
 ****************************************************************
 * Copyright (C) 2002, 2003 Scott Parish
 * Copyright (C) 2003 Tom Lord
 *
 * See the file "COPYING" for further information about
 * the copyright and warranty status of this work.
 */


#include <string.h>
#include "hackerlab/bugs/panic.h"
#include "hackerlab/mem/alloc-limits.h"
#include "hackerlab/mem/mem.h"
#include "hackerlab/char/str.h"
#include "hackerlab/fmt/cvt.h"
#include "hackerlab/fs/file-names.h"
#include "hackerlab/vu/safe.h"
#include "tla/libfsutils/tmp-files.h"
#include "tla/libfsutils/file-contents.h"
#include "tla/libarch/archives.h"
#include "tla/libarch/pfs-dav.h"
#include "tla/libneon/ne_session.h"
#include "tla/libneon/ne_basic.h"
#include "tla/libneon/ne_request.h"
#include "tla/libneon/ne_auth.h"
#include "tla/libneon/ne_props.h"
#include "tla/libneon/ne_uri.h"
#include "tla/libneon/ne_redirect.h"



#undef MIN
#define MIN(A,B) ((A) < (B) ? (A) : (B))
#define LISTING_FILE ".listing"

static const ne_propname ls_props[] =
{
  { "DAV:", "getcontentlength" },
  { "DAV:", "getlastmodified" },
  { "DAV:", "resourcetype" },
  { NULL }
};

struct authinfo
{
  char *username;
  char *password;
};

struct ls_data
{
  char *files;
  char *uri;
};

struct dav_is_dir_ls_data
{
  char *uri;
  int is_dir;
};


struct arch_pfs_dav_session
{
  struct arch_pfs_session pfs;

  char * cwd;
  char * dav_scheme;
  char * dav_hostname;
  int dav_port;

  ne_session * sess;
  ne_server_capabilities sess_opts;
};


/* __STDC__ prototypes for static functions */
static t_uchar * pfs_file_contents (struct arch_pfs_session * p, t_uchar * path, int soft_errors);
static rel_table pfs_directory_files (struct arch_pfs_session * p, t_uchar * path, int soft_errors);
static int pfs_file_exists (struct arch_pfs_session * p, t_uchar * path);
static int pfs_get_file (struct arch_pfs_session * p, int out_fd, t_uchar * path, int soft_errors);
static int pfs_put_file (struct arch_pfs_session * p, t_uchar * path, mode_t perms, int in_fd, int soft_errors);
static int pfs_mkdir (struct arch_pfs_session * p, t_uchar * path, mode_t mode, int soft_errors);
static int pfs_rename (struct arch_pfs_session * p, t_uchar ** errstr, t_uchar * from, t_uchar * to, int soft_errors);
static int pfs_is_dir (struct arch_pfs_session * p, t_uchar * path);
static int dav_is_dir (struct arch_pfs_dav_session * pfs, char * dir);
static int pfs_rmdir (struct arch_pfs_session * p, t_uchar * path, int soft_errors);
static int pfs_rm (struct arch_pfs_session * p, t_uchar * path, int soft_errors);
static void dav_is_dir_results (void * userdata, const char * uri, const ne_prop_result_set * set);
static int dav_client_cwd (struct arch_pfs_dav_session * pfs, t_uchar * path);
static int dav_client_auth (void * userdata, const char * realm, int attempt,
                            char * username, char * password);
static t_uchar * abs_path (t_uchar * cwd, t_uchar * path);
static t_uchar * dirfold (t_uchar *dir);
static void results (void * userdata, const char * uri, const ne_prop_result_set * set);



struct arch_pfs_vtable dav_pfs_fns =
{
  pfs_file_exists,
  pfs_is_dir,

  pfs_file_contents,
  pfs_get_file,
  pfs_directory_files,

  pfs_put_file,

  pfs_mkdir,
  pfs_rename,

  pfs_rmdir,
  pfs_rm,
};




int
arch_pfs_dav_supported_protocol (t_uchar * uri)
{
  if (!str_cmp_prefix ("http:", uri) ||
      !str_cmp_prefix ("https:", uri))
    return 1;
  else
    return 0;
}


struct arch_pfs_session *
arch_pfs_dav_connect (t_uchar * uri)
{
  int ign;
  struct arch_pfs_dav_session * answer = 0;
  t_uchar * host;
  t_uchar * user;
  t_uchar * passwd;
  t_uchar * hostname;
  t_uchar * portstr;
  t_uchar * root_path = 0;
  t_uchar * proxy;
  t_uchar * proxy_port;
  struct authinfo * auth = 0;
  int proxy_port_num;

  answer = (struct arch_pfs_dav_session *)lim_malloc (0, sizeof (*answer));
  mem_set0 ((t_uchar *)answer, sizeof (*answer));
  answer->pfs.vtable = &dav_pfs_fns;

  if (!str_cmp_prefix ("http://", uri))
    {
      answer->dav_scheme = str_save (0, "http");
      host = str_save (0, uri + sizeof ("http://") - 1);
    }
  else if (!str_cmp_prefix ("https://", uri))
    {
      answer->dav_scheme = str_save (0, "https");
      host = str_save (0, uri + sizeof ("https://") - 1);
    }
  else
    panic ("bogus uri to arch_pfs_dav_connect");

  root_path = str_chr_index (host, '/');
  if (root_path)
    {
      t_uchar * pos;
      pos = root_path;
      root_path = str_save (0, root_path);
      *pos = 0;
    }

  hostname = str_chr_index (host, '@');

  if (!hostname)
    {
      answer->dav_hostname = str_save (0, host);
    }
  else
    {
      user = host;
      auth = lim_malloc (0, sizeof (struct authinfo));

      passwd = str_chr_index_n (user, hostname - user, ':');
      if (passwd++)
        {
          auth->username = str_save_n (0, user, (passwd - 1) - user);
          auth->password = str_save_n (0, passwd, hostname - passwd);
        }
      else
        {
          auth->username = str_save_n (0, user, hostname - user);
          auth->password = str_save (0, "");
        }

      answer->dav_hostname = str_save (0, hostname + 1);
    }

  portstr = str_chr_index (answer->dav_hostname, ':');
   if (portstr)
     *(portstr++) = 0;

  answer->dav_port = 80;
  if (portstr && (0 > cvt_decimal_to_uint (&ign, &answer->dav_port, portstr, str_length (portstr))))
    {
      safe_printfmt (2, "illegal port number in uri -- %s\n", uri);
      exit (2);
    }


  answer->sess = ne_session_create (answer->dav_scheme, answer->dav_hostname, answer->dav_port);

  if (auth)
    {
      ne_set_server_auth (answer->sess, dav_client_auth, auth);
    }

  ne_set_useragent (answer->sess, "arch-client/0.1");

  /*
   * Retrieve the proxy. Do not alter the environment variable.
   * The proxy variable is formatted as
   * scheme://host:port/
   * We only support http as the scheme.
   */    
  proxy = getenv ("http_proxy");
  if (proxy)
    proxy = str_save (0, proxy);
  else
    {
      proxy = getenv ("HTTP_PROXY");
      if (proxy)
        proxy = str_save (0, proxy);
    }

  if (proxy && !str_cmp_prefix ("http://", proxy))
    {
      t_uchar * proxy_path = proxy + 7;
      proxy_port = str_chr_index (proxy_path, ':');

      if (proxy_port)
        {
	  unsigned proxy_port_len;

          *proxy_port++ = 0;

	  proxy_port_len = str_length (proxy_port);
	  /* The port may be followed by a trailing slash (according to
	     rcollins, it _must_ be followed by a slash, strictly speaking,
	     but this restriction is widely ignored both by software and by
	     users, so it's not practical to enforce it, and there's no
	     real gain from doing so).  */
	  if (proxy_port_len > 0 && proxy_port[proxy_port_len - 1] == '/')
	    proxy_port_len--;

          if (cvt_decimal_to_int (&ign, &proxy_port_num, proxy_port, proxy_port_len))
            {
              safe_printfmt (2, "ill formated http proxy port number from $http_proxy\n");
              exit (2);
            }

          ne_session_proxy (answer->sess, proxy_path, proxy_port_num);
        }
    }

  if (root_path)
    dav_client_cwd (answer, root_path);

  lim_free (0, host);
  lim_free (0, root_path);
  lim_free (0, proxy);

  return (struct arch_pfs_session *)answer;
}

static t_uchar *
pfs_file_contents (struct arch_pfs_session * p, t_uchar * path, int soft_errors)
{
  struct arch_pfs_dav_session * pfs = (struct arch_pfs_dav_session *)p;
  t_uchar * tmp_path = 0;
  int fd;
  t_uchar * dav_path = 0;
  int ne_err;
  t_uchar * answer = 0;

  tmp_path = tmp_file_name ("/tmp", ",,pfs-dav-file-contents");
  fd = safe_open (tmp_path, O_RDWR | O_CREAT | O_EXCL, 0000);
  safe_unlink (tmp_path);

  dav_path = abs_path (pfs->cwd, (char *)path);
  ne_err = ne_get (pfs->sess, dav_path, fd);
  if (ne_err)
    {
      if (!soft_errors)
        {
          safe_printfmt (2, "webdav error: %s\n", ne_get_error (pfs->sess));
          exit (2);
        }
    }
  else
    {
      safe_lseek (fd, (off_t)0, SEEK_SET);
      answer = fd_contents (fd);
    }

  safe_close (fd);
  lim_free (0, dav_path);

  return answer;
}

static rel_table
pfs_directory_files (struct arch_pfs_session * p, t_uchar * path, int soft_errors)
{
  struct arch_pfs_dav_session * pfs = (struct arch_pfs_dav_session *)p;
  int ne_err;
  t_uchar * dir = 0;
  t_uchar * file_string = 0;
  rel_table answer = 0;

  if (pfs->sess_opts.dav_class1)
    {
      struct ls_data data;

      dir = abs_path (pfs->cwd, path);

      if (dir[str_length (dir) - 1] != '/')
        dir = str_realloc_cat(0, dir, "/");

      data.files = str_save (0, "");;
      data.uri = dir;

      if ((ne_err = ne_simple_propfind (pfs->sess, dir, NE_DEPTH_ONE, ls_props, results, &data)))
        {
          if (!soft_errors)
            {
              safe_printfmt (2, "webdav error (directory_files): %s (%s)\n", dir, ne_get_error (pfs->sess));
              exit (2);
            }
        }
      else
        {
          file_string = str_save (0, data.files);
        }

      lim_free (0, data.files);
    }
  else
    {
      dir = file_name_in_vicinity (0, path, LISTING_FILE);

      file_string = pfs_file_contents (p, dir, 1);
    }


  answer = rel_ws_split (file_string);

  lim_free (0, dir);
  lim_free (0, file_string);

  return answer;
}

static int
pfs_file_exists (struct arch_pfs_session * p, t_uchar * path)
{
  t_uchar * path_dir = 0;
  t_uchar * path_tail = 0;
  rel_table files = 0;
  int answer = 0;
  int x;

  /* There is probably a better way to do this in webdav. */

  path_dir = file_name_directory_file (0, path);
  if (!path_dir)
    {
      path_dir = str_save (0, ".");
    }

  path_tail = file_name_tail (0, path);

  files = pfs_directory_files (p, path_dir, 1);

  for (x = 0; x < rel_n_records (files); ++x)
    {
      if (!str_cmp (path_tail, files[x][0]))
        {
          answer = 1;
          break;
        }
    }


  lim_free (0, path_dir);
  lim_free (0, path_tail);
  rel_free_table (files);

  return answer;
}

static int
pfs_get_file (struct arch_pfs_session * p, int out_fd, t_uchar * path, int soft_errors)
{
  struct arch_pfs_dav_session * pfs = (struct arch_pfs_dav_session *)p;
  t_uchar * dav_path = 0;
  int ne_err;
  int answer = 0;


  dav_path = abs_path (pfs->cwd, (char *)path);
  ne_err = ne_get (pfs->sess, dav_path, out_fd);
  if (ne_err)
    {
      if (!soft_errors)
        {
          safe_printfmt (2, "webdav error: %s\n", ne_get_error (pfs->sess));
          exit (2);
        }
      else
        answer = -1;
    }

  lim_free (0, dav_path);
  return answer;
}


static int
pfs_put_file (struct arch_pfs_session * p, t_uchar * path, mode_t perms, int in_fd, int soft_errors)
{
  struct arch_pfs_dav_session * pfs = (struct arch_pfs_dav_session *)p;
  int ne_err;
  t_uchar * file;
  int answer = 0;

  file = abs_path (pfs->cwd, path);

  ne_err = ne_put (pfs->sess, file, in_fd);

  if (ne_err)
    {
      if (!soft_errors)
        {
          safe_printfmt (2, "webdav error(put_file): writing to %s (%s)\n", file, ne_get_error (pfs->sess));
          exit (2);
        }
      else
        {
          answer = -1;
        }
    }

  lim_free (0, file);

  return answer;
}


static int
pfs_mkdir (struct arch_pfs_session * p, t_uchar * path, mode_t mode, int soft_errors)
{
  struct arch_pfs_dav_session * pfs = (struct arch_pfs_dav_session *)p;
  int ne_err;
  t_uchar * dir = 0;
  int answer = 0;

  dir = abs_path (pfs->cwd, path);
  if (dir[str_length (dir) - 1] != '/')
    dir = str_realloc_cat (0, dir, "/");

  if ((ne_err = ne_mkcol (pfs->sess, dir)))
    {
      if (!soft_errors)
        {
          safe_printfmt (2, "webdav error (pfs_mkdir): %s (%s)\n", path, ne_get_error (pfs->sess));
          exit (2);
        }
      else
        answer = -1;
    }

  lim_free (0, dir);
  return answer;
}


static int
pfs_rename (struct arch_pfs_session * p, t_uchar ** errstr, t_uchar * from, t_uchar * to, int soft_errors)
{
  struct arch_pfs_dav_session * pfs = (struct arch_pfs_dav_session *)p;
  int ne_err;
  int ret;
  t_uchar * from2;
  t_uchar * to2;
  int result = 0;

  from2 = abs_path (pfs->cwd, from);
  to2 = abs_path (pfs->cwd, to);

  ret = dav_is_dir (pfs, from2);
  if (ret == -1)
    {
      if (errstr)
        *errstr = str_save (0, "file does not exist for rename");
      result = -1;
    }
  else
    {
      if (ret)
        {
          if (from2[str_length (from2) - 1] != '/')
            from2 = str_realloc_cat (0, from2, "/");
          if (to2[str_length (to2) - 1] != '/')
            to2 = str_realloc_cat (0, to2, "/");
        }

      if (0 < dav_is_dir (pfs, to2))
        {
          char * b;
          char * file;

          b = file = str_save (0, 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 (0, to2, "/", file, str_end);
          lim_free (0, b);
        }


      if ((ne_err = ne_move (pfs->sess, 0, from2, to2)))
        {
          if (errstr)
            *errstr = str_save (0, ne_get_error (pfs->sess));
          result = -1;
        }
    }

  lim_free (0, from2);
  lim_free (0, to2);
  return result;
}

static int
pfs_is_dir (struct arch_pfs_session * p, t_uchar * path)
{
  struct arch_pfs_dav_session * pfs = (struct arch_pfs_dav_session *)p;
  t_uchar * abs = 0;
  int answer;

  abs = abs_path (pfs->cwd, path);

  answer = dav_is_dir (pfs, abs);

  lim_free (0, abs);

  return answer;
}

static int
dav_is_dir (struct arch_pfs_dav_session * pfs, char * dir)
{
  char * filename;
  char * parent;
  struct dav_is_dir_ls_data data;
  int ne_err;
  int answer;

  parent = str_save (0, dir);
  filename = str_chr_rindex (parent, '/');
  filename[1] = 0;

  data.uri = dir;
  data.is_dir = 0;

  if((ne_err = ne_simple_propfind (pfs->sess, parent, NE_DEPTH_ONE, ls_props, dav_is_dir_results, &data)))
    {
      answer = -1;
    }
  else
    {
      answer = data.is_dir;
    }

  lim_free (0, parent);
  return answer;
}

static int
pfs_rmdir (struct arch_pfs_session * p, t_uchar * path, int soft_errors)
{
  struct arch_pfs_dav_session * pfs = (struct arch_pfs_dav_session *)p;
  int ne_err;
  t_uchar * dir = 0;
  int answer = 0;

  dir = abs_path (pfs->cwd, path);
  if (dir[str_length (dir) - 1] != '/')
    dir = str_realloc_cat (0, dir, "/");

  if ((ne_err = ne_delete (pfs->sess, dir)))
    {
      if (!soft_errors)
        {
          safe_printfmt (2, "webdav error (pfs_rmdir): %s (%s)\n", dir, ne_get_error (pfs->sess));
          exit (2);
        }
      else
        answer = -1;
    }

  lim_free (0, dir);

  return answer;
}

static int
pfs_rm (struct arch_pfs_session * p, t_uchar * path, int soft_errors)
{
  struct arch_pfs_dav_session * pfs = (struct arch_pfs_dav_session *)p;
  int ne_err;
  t_uchar * abs = 0;
  int answer = 0;

  abs = abs_path (pfs->cwd, path);

  if ((ne_err = ne_delete (pfs->sess, abs)))
    {
      if (!soft_errors)
        {
          safe_printfmt (2, "webdav error (pfs_rm): %s (%s)\n", path, ne_get_error (pfs->sess));
          exit (2);
        }
      else
        answer = -1;
    }

  lim_free (0, abs);
  return 0;
}






static void
dav_is_dir_results (void * userdata, const char * uri, const ne_prop_result_set * set)
{
  struct dav_is_dir_ls_data * data = (struct dav_is_dir_ls_data *)userdata;
  int len;

  len = str_length (data->uri);
  if (!str_cmp_n (uri, len, data->uri, len))
    {
      if (uri[str_length (uri) - 1] == '/')
        data->is_dir = 1;
      else
        data->is_dir = 0;
    }
}

static int
dav_client_cwd (struct arch_pfs_dav_session * pfs, t_uchar * path)
{
  int ne_err;
  t_uchar * tmp;
  t_uchar * dir;

  dir = abs_path (pfs->cwd, path);
  if (dir[str_length (dir) - 1] != '/')
    dir = str_realloc_cat (0, dir, "/");

  if ((ne_err = ne_options (pfs->sess, dir, &pfs->sess_opts)))
    {
      /* if error assume that server doesn't implement OPTIONS */
      pfs->sess_opts.dav_class1 = 0;
      pfs->sess_opts.dav_class2 = 0;
      pfs->sess_opts.dav_executable = 0;
    }

  if (pfs->sess_opts.dav_class1) /* if the server supports DAV */
    {
      if ((ne_err = ne_simple_propfind (pfs->sess, dir, NE_DEPTH_ONE, ls_props, NULL, NULL)))
        {
          safe_printfmt (2, "webdav error: %s\n", ne_get_error (pfs->sess));
          exit (2);
        }
    }
  else /* no dav, so look for a listing file */
    {
      time_t mtime;
      char * dirlisting = 0;

      dirlisting = str_alloc_cat (0, dir, LISTING_FILE);

      if ((ne_err = ne_getmodtime (pfs->sess, dirlisting, &mtime)))
        {
          safe_printfmt (2, "webdav error: %s\n", ne_get_error (pfs->sess));
          exit (2);
        }
      lim_free (0, dirlisting);
    }

  tmp = ne_path_unescape (dir);
  lim_free (0, dir);
  if (pfs->cwd)
    free (pfs->cwd);
  pfs->cwd = tmp;

  return 0;
}


static int
dav_client_auth (void * userdata, const char * realm, int attempt,
                 char * username, char * password)
{
  struct authinfo *auth;
  int len;

  if (attempt > 1)
    return attempt;

  auth = userdata;
  len = str_length (auth->username);
  str_cpy_n (username, auth->username, MIN (len, NE_ABUFSIZ - 1));
  len = str_length (auth->password);
  str_cpy_n (password, auth->password, MIN (len, NE_ABUFSIZ - 1));

  return 0;
}


static t_uchar *
abs_path (t_uchar * cwd, t_uchar * path)
{
  t_uchar * file;
  t_uchar * tmp;

  if (cwd == NULL)
    cwd = "/";

  if (path[0] != '/')
    file = str_alloc_cat (0, cwd, path);
  else
    file = str_save (0, path);

  dirfold (file);

  tmp = ne_path_escape (file);
  lim_free (0, file);
  file = str_save (0, tmp);
  free (tmp);

  return file;
}


static t_uchar *
dirfold (t_uchar *dir)
{
  t_uchar * buf;
  t_uchar * this;
  t_uchar * next;
  int dir_i = 0;

  this = next = buf = str_save (0, dir);
  while ((this = str_separate (&next, "/")) != NULL)
    {
      if (str_length (this) == 0 || (str_length (this) == 1 && this[0] == '.'))
        continue;
      else if (str_length (this) == 2 && *this == '.' && this[1] == '.')
        {
          if (dir_i > 0)
            dir_i = (int)(strrchr (dir, '/') - (char *)dir);
          dir[dir_i] = 0;
        }
      else
        {
          dir[dir_i++] = '/';
          strcpy (dir + dir_i, this);
          dir_i += str_length (this);
        }
    }
  lim_free (0, buf);

  if (!str_length (dir))
      str_cpy (dir, "/");

  return dir;
}

static void
results (void * userdata, const char * uri, const ne_prop_result_set * set)
{
  int n;
  char * file, * tmp;
  struct ls_data * data = userdata;

  if (str_cmp (data->uri, uri))
    {
      if (1 == (n = str_length (uri)))
        return;

      if (uri[n - 1] == '/')
        n--;

      file = str_chr_rindex_n (uri, n, '/') + 1;

      n = str_length (file);
      if (file[n - 1] == '/')
        n--;

      file = str_save_n (0, file, n);
      tmp = ne_path_unescape (file);
      lim_free (0, file);

      data->files = str_realloc_cat_many (0, data->files, tmp, "\r\n", str_end);
      free (tmp);
    }
}



/* tag: Tom Lord Thu Jun  5 15:23:06 2003 (pfs-dav.c)
 */
