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



#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/printfmt.h"
#include "hackerlab/vu-network/url-socket.h"
#include "ftp-utils/clients/dav-protocol.h"
#include "ftp-utils/clients/misc.h"
#include "ftp-utils/libneon/ne_session.h"
#include "ftp-utils/libneon/ne_basic.h"
#include "ftp-utils/libneon/ne_request.h"
#include "ftp-utils/libneon/ne_auth.h"
#include "ftp-utils/libneon/ne_props.h"
#include "ftp-utils/libneon/ne_uri.h"
#include "ftp-utils/libneon/ne_redirect.h"

static char *cwd = 0;
static char *dav_scheme = 0;
static char *dav_hostname = 0;
static int dav_port = 0;
int long_ls = 0;
ne_session *sess;
ne_server_capabilities sess_opts;
ne_server_capabilities proxy_opts;

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

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



char *
abs_path (alloc_limits limits, char *path)
{
  char *file, *tmp;


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


  dirfold (limits, file);

  tmp = uri_abspath_escape (file);
  lim_free (limits, file);
  file = str_save(limits, tmp);
  free (tmp);

  return file;
}


int
handle_redirect (char **uri)
{
  const char *new_loc;
  int ne_err;
  struct uri *uri_parts;
  struct uri defaults = {dav_scheme, dav_hostname, dav_port, 0, 0};
  
  new_loc = ne_redirect_location (sess);
  if ((ne_err = uri_parse (new_loc, uri_parts, &defaults)))
    return ne_err;

  ne_session_destroy (sess);
  sess = ne_session_create (uri_parts->scheme, uri_parts->host,
			    uri_parts->port);

  lim_free (0, dav_scheme);
  lim_free (0, dav_hostname);

  dav_scheme = str_save (0, uri_parts->scheme);
  dav_hostname = str_save (0, uri_parts->host);
  dav_port = uri_parts->port;

  *uri = uri_parts->path;

  uri_free (uri_parts);
 
  return 0;
}


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


static void
dav_is_dir_results (void *userdata,
		    const char *uri,
		    const ne_prop_result_set *set)
{
  struct dav_is_dir_ls_data *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;
    }
}


int
dav_is_dir (alloc_limits limits, t_uchar **text, char *dir)
{
  char *filename, *parent;
  struct dav_is_dir_ls_data data;
  int ne_err;

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

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

  if((ne_err = ne_simple_propfind (sess, parent, NE_DEPTH_ONE, ls_props,
				   dav_is_dir_results, &data)))
    {
      *text = str_save (limits, ne_get_error (sess));
      lim_free (limits, parent);
      return -1;
    }

  lim_free (limits, parent);
  return data.is_dir;
}


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;
}


int
redirect_confirm (void *userdata, const char *src, const char *dest) 
{
  return 1;
}


void
redirect_notify (void *userdata, const char *src, const char *dest) 
{
}


int
dav_client_connect_and_login (alloc_limits limits,
			      int * errn,
			      int * code,
			      t_uchar ** text,
			      long * text_len,
			      t_uchar * protocol,
			      t_uchar * host,
			      t_uchar * program_name,
			      t_uchar * program_version)
{
  t_uchar *user, *passwd, *hostname, *portstr, *useragent;
  t_uchar *proxy, *proxy_port;
  struct authinfo *auth = 0;
  unsigned int port;
  int i;

  hostname = str_chr_index (host, '@');
  if (!hostname)
    {
      hostname = str_save (limits, host);
    }
  else
    {
      user = host;
      auth = lim_malloc(limits, 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, "");
	}

      hostname = str_save (limits, hostname + 1);
    }

  portstr = str_chr_index (hostname, ':');
   if (portstr)
     *(portstr++) = 0;

  port = 80;
  if (portstr &&
      0 > cvt_decimal_to_uint (errn, &port, portstr, str_length (portstr)))
    {
      lim_free (limits, hostname);
      *text = str_save (0, "invalid string for port");
      return -1; 
    }

  dav_scheme = str_save (0, protocol);
  dav_hostname = str_save (0, hostname);
  dav_port = port;

  sess = ne_session_create (protocol, hostname, port);

  if (auth)
      ne_set_server_auth (sess, dav_client_auth, auth);

  useragent = str_alloc_cat_many (limits,0, program_name, "/", program_version,
				  str_end);
  ne_set_useragent (sess, useragent);

  proxy = getenv ("http_proxy");
  if (proxy && !str_cmp_prefix ("http://", proxy))
    {
      proxy += 7;

      proxy_port = str_chr_index (proxy, ':');
      if (proxy_port)
	{
 	  int ne_err;

	  *proxy_port = 0;
	  i = str_length (++proxy_port) - 1;
	  if (proxy_port[i] == '/')
	      proxy_port[i] = 0;
	  ne_session_proxy (sess, proxy, atoi (proxy_port));

 	  if ((ne_err = ne_options(sess, "*", &proxy_opts)))
 	    {
 	      lim_free (limits, hostname);
 	      lim_free (limits, useragent);
 	      *text = str_alloc_cat (limits,
 				     "Unable to detect Proxy capabilities: ",
 				     ne_get_error (sess));
 	      *text_len = str_length(*text);
 	      return -1;
 	    }
 
 	  if (! proxy_opts.dav_class1)
 	    {
 	      lim_free (limits, hostname);
 	      lim_free (limits, useragent);
 	      *text = str_save (0, "Proxy server does not support WebDAV capabilities");
 	      return -1;
 	    }

	}
    }

  ne_redirect_register (sess, NULL, redirect_notify, NULL);

  lim_free (limits, hostname);
  lim_free (limits, useragent);

  return 0;
}



/****************************************************************
 * dav_cwd
 */

int
dav_client_cwd (alloc_limits limits, int * errn, int * code, t_uchar ** text,
		long * text_len, t_uchar * path)
{
  int ne_err;
  char *tmp, *dir;

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

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

  if (sess_opts.dav_class1) /* if the server supports DAV */
    {
      if ((ne_err = ne_simple_propfind (sess, dir, NE_DEPTH_ONE, ls_props, NULL,
					NULL)))
	{
	  lim_free (limits, dir);
	  *text = str_save (limits, ne_get_error (sess));
	  return -1;
	}
    }
  else /* no dav, so look for a listing file */
    {
      time_t mtime;
      char *dirlisting = str_alloc_cat(0, dir, LISTING_FILE);

      if ((ne_err = ne_getmodtime(sess, dirlisting, &mtime)))
	{
	  lim_free (limits, dir);
	  lim_free (limits, dirlisting);
	  *text = str_save (limits, ne_get_error (sess));
	  return -1;
	}
      lim_free (limits, dirlisting);
    }

  tmp = uri_unescape (dir);
  lim_free (limits, dir);
  if (cwd)
      free (cwd);
  cwd = tmp;

  return 0;
}



/****************************************************************
 * dav_cdup
 */

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

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

  return status;
}


/****************************************************************
 * dav_rename
 */
int
dav_client_rename (alloc_limits limits,
		   int * errn,
		   int * code,
		   t_uchar ** text,
		   t_uchar * from,
		   t_uchar * to)
{
  int ne_err, ret;
  char *from2, *to2, *file, *b;

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

  ret = dav_is_dir (limits, text, from2);
  if (ret == -1)
    {
      lim_free (limits, from2);
      lim_free (limits, to2);
      return -1;
    }
  else if (ret)
    {
      if (from2[str_length (from2) - 1] != '/')
	from2 = str_realloc_cat (limits, from2, "/");
      if (to2[str_length (to2) - 1] != '/')
	to2 = str_realloc_cat (limits, to2, "/");
    }

  if (0 < dav_is_dir (limits, text, 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);
    }

  if ((ne_err = ne_move (sess, 0, from2, to2)))
    {
      lim_free (limits, from2);
      lim_free (limits, to2);
      *text = str_save (limits, ne_get_error (sess));
      return -1;
    }

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

  return 0;
}


int
dav_client_unlink (alloc_limits limits,
		   int * errn,
		   int * code,
		   t_uchar ** text,
		   t_uchar * path)
{
  int ne_err;
  char *dir;

  dir = abs_path (limits, path);

  if ((ne_err = ne_delete (sess, dir)))
    {
      lim_free (limits, dir);
      *text = str_save (limits, ne_get_error (sess));
      return -1;
    }

  lim_free (limits, dir);

  return 0;
}



/****************************************************************
 * dav_rmd
 */
int
dav_client_rmd (alloc_limits limits,
		int * errn,
		int * code,
		t_uchar ** text,
		t_uchar * path)
{
  int ne_err;
  char *dir;

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

  if ((ne_err = ne_delete (sess, dir)))
    {
      lim_free (limits, dir);
      *text = str_save (limits, ne_get_error (sess));
      return -1;
    }

  lim_free (limits, dir);

  return 0;
}


/****************************************************************
 * dav_mkd
 */
int
dav_client_mkd (alloc_limits limits,
		int * errn,
		int * code,
		t_uchar ** text,
		t_uchar * path)
{
  int ne_err;
  char *dir;

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

  if ((ne_err = ne_mkcol (sess, dir)))
    {
      lim_free (limits, dir);
      *text = str_save (limits, ne_get_error (sess));
      return -1;
    }

  lim_free (limits, dir);

  return 0;
}


/****************************************************************
 * dav_client_pwd
 */
int
dav_client_pwd (alloc_limits limits,
		int * errn,
		int * code,
		t_uchar ** path)
{
  int len = str_length (cwd);

  *path = str_save_n (0, cwd, str_length (cwd) - (len != 1));

  return 0;
}

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


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 = uri_unescape (file);
      lim_free (0, file);
      
      data->files = str_realloc_cat_many (0, data->files, tmp, "\r\n", str_end);
      free (tmp);
    }
}


/****************************************************************
 * dav_client_list
 */

int
dav_client_list (alloc_limits limits, int *errn, int *code,
		  t_uchar ** text, int data_fd,
		  t_uchar * path, int longfmt)
{
  int ne_err;
  char *dir;

  dir = abs_path (limits, path);

  if (sess_opts.dav_class1)
    {
      struct ls_data data;
      long_ls = longfmt;

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

      data.files = str_save (0, "");;
      data.uri = dir;
  
      if ((ne_err = ne_simple_propfind (sess, dir, NE_DEPTH_ONE, ls_props,
				        results, &data)))
	{
	  lim_free (limits, dir);
	  lim_free (0, data.files);
	  *text = str_save (limits, ne_get_error (sess));
	  return -1;
	}

      if (0 > printfmt (errn, data_fd, "%s", data.files))
	return -1;

      lim_free (0, data.files);
    }
  else
    {
      dir = str_realloc_cat(limits, dir, "/" LISTING_FILE);
      if ((ne_err = ne_get (sess, dir, data_fd)))
	{
	  lim_free (limits, dir);
	  *text = str_save (limits, ne_get_error (sess));
	  return -1;
	}
    }

  lim_free (limits, dir);

  return 0;
}



int
dav_client_get (alloc_limits limits, int *errn, int *code,
		 t_uchar ** text, int data_fd, t_uchar * path)
{
  int ne_err;
  char *file;

  file = abs_path (limits, path);

  if ((ne_err = ne_get (sess, file, data_fd)))
    {
      lim_free (limits, file);
      *text = str_save (limits, ne_get_error (sess));
      return -1;
    }

  lim_free (limits, file);

  return 0;
}

int
dav_client_put (alloc_limits limits, int *errn, int *code,
		 t_uchar ** text, int data_fd, t_uchar * path)
{
  int ne_err, tmp_fd, amt;
  char *file, buf[BUFSIZ], tmp_pattern[] = "/tmp/with-ftp.XXXXXX";
  mode_t old_mode;
  FILE *tmp_fh;

  old_mode = umask (077);  /* Create file with restrictive permissions */
  tmp_fd = mkstemp (tmp_pattern);
  umask (old_mode);
  if (tmp_fd == -1 || !(tmp_fh = fdopen (tmp_fd, "w+b")) ||
      -1 == unlink (tmp_pattern))
    {
      *text = str_save (limits, errno_to_string (errno));
      return -1;
    }

  while (1)
    {
      amt = read (data_fd, buf, sizeof (buf));
      if ((amt < 0) && (errno != EAGAIN) && (errno != EINTR))
	{
	  *text = str_save (limits, errno_to_string (errno));
	  return -1;
	}
      else if (!amt)
	break;
      if (0 >= fwrite (buf, amt, 1, tmp_fh))
	{
	  *text = str_save (limits, errno_to_string (errno));
	  return -1;
	}
    }

  if (0 > fseek (tmp_fh, 0, SEEK_SET))
    {
      *text = str_save (limits, errno_to_string (errno));
      return -1;
    }

  file = abs_path (limits, path);
  
  if ((ne_err = ne_put (sess, file, tmp_fd)))
    {
      lim_free (limits, file);
      *text = str_save (limits, ne_get_error (sess));
      return -1;
    }

  lim_free (limits, file);

  return 0;
}
