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

#include "hackerlab/char/str.h"
#include "hackerlab/os/errno.h"
#include "hackerlab/os/errno-to-string.h"
#include "hackerlab/os/sys/wait.h"
#include "hackerlab/os/signal.h"
#include "hackerlab/arrays/ar.h"
#include "hackerlab/fmt/cvt.h"
#include "hackerlab/vu/safe.h"
#include "hackerlab/vu-network/url-socket.h"


#include "ftp-utils/with-ftp/utils.h"
#include "ftp-utils/with-ftp/with-ftp.h"
#include "ftp-utils/with-ftp/local-handler.h"




/* Remote starting directory.
 * 
 * (The entire path provided to with-ftp.)
 */
static t_uchar * remote_home;

/* rename_from_path
 * 
 * Between RNFR and RNTO, this holds the path name from RNFR.
 */
static t_uchar * rename_from_path;


ftp_handler_t
local_init_handler (t_uchar *** new_env, t_uchar * site)
{
  remote_home = str_save (0, site);
  safe_chdir (site);
  *(t_uchar **)ar_push ((void **)new_env, 0, sizeof (t_uchar *)) = str_save (0, "WFTP_SITE_ADDR=");
  return local_handle_command;
}



void
local_handle_command (int connect_in, int connect_out, t_uchar * command, int command_len)
{
  int errn;
  t_uchar * message;
  int cmd_status;

  command_len = trim_crnl (command, command_len);

  /* commands not requiring a data port */

  if (!str_cmp_prefix ("cd ", command))
    {
      /* "cd <path>" */
      if (0 > (cmd_status = vu_chdir (&errn, command + 3)))
	message = str_save (0, errno_to_string (errn));
      else
	message = str_save (0, "chdir ok");
    }
  else if (!str_cmp_prefix ("cdup", command))
    {
      /* "cdup" */
      if (0 > (cmd_status = vu_chdir (&errn, "..")))
	message = str_save (0, errno_to_string (errn));
      else
	message = str_save (0, "chdir .. ok");
    }
  else if (!str_cmp_prefix ("delete ", command))
    {
      /* "delete <path>" */
      if (0 > (cmd_status = vu_unlink (&errn, command + 7)))
	message = str_save (0, errno_to_string (errn));
      else
	message = str_save (0, "unlink ok");
    }
  else if (!str_cmp_prefix ("home", command))
    {
      /* "home" */
      if (0 > (cmd_status = vu_chdir (&errn, remote_home)))
	message = str_save (0, errno_to_string (errn));
      else
	message = str_save (0, "home ok");
    }
  else if (!str_cmp_prefix ("rmdir ", command))
    {
      /* "rmdir <path>" */
      if (0 > (cmd_status = vu_rmdir (&errn, command + 6)))
	message = str_save (0, errno_to_string (errn));
      else
	message = str_save (0, "rmdir ok");
    }
  else if (!str_cmp_prefix ("mkdir ", command))
    {
      /* "mkdir <path>" */
      if (0 > (cmd_status = vu_mkdir (&errn, command + 6, 0777)))
	message = str_save (0, errno_to_string (errn));
      else
	message = str_save (0, "rmdir ok");
    }
  else if (!str_cmp_prefix ("noop", command))
    {
      /* "noop" */
      cmd_status = 0;
      message = str_save (0, "noop ok");
    }
  else if (!str_cmp_prefix ("pwd", command))
    {
      /* There needs to be a vu_ version of getcwd, but there 
       * isn't yet, so this cheats by using the libc version.
       */
      char * path;
      size_t sizeof_path;

      sizeof_path = 4096;
      path = lim_malloc (0, sizeof_path);

      while (1)
	{
	  if (getcwd (path, sizeof_path))
	    {
	      if ((0 > printfmt (&errn, connect_out, "OK %lu\n%s\n", (unsigned long)(1 + str_length (path)), path))
		  || (0 > vfdbuf_flush (&errn, connect_out)))
		{
		  if (errn != EPIPE)
		    with_ftp_panic ("I/O error in local_handle_command (pwd OK)\n");
		}
	      lim_free (0, path);
	      return;
	    }
	  else if (errno == ERANGE)
	    {
	      sizeof_path *= 2;
	      path = lim_realloc (0, path, sizeof_path);
	    }
	  else
	    {
	      cmd_status = -1;
	      message = str_save (0, errno_to_string (errno));
	      lim_free (0, path);
	      break;
	    }
	}
    }
  else if (!str_cmp_prefix ("rnfr ", command))
    {
      if (rename_from_path)
	lim_free (0, rename_from_path);

      rename_from_path = str_save (0, command + 5);
      message = str_save (0, "rnfr ok");
      cmd_status = 0;
    }
  else if (!str_cmp_prefix ("rnto ", command))
    {
      if (!rename_from_path)
	goto bad_command;
      
      if (0 > (cmd_status = vu_rename (&errn, rename_from_path, command + 5)))
	message = str_save (0, errno_to_string (errn));
      else
	message = str_save (0, "rename ok");
    }  


  /* commands requiring a data port */

  else
    {
      t_uchar * host;
      t_uchar * comma;
      t_uchar * port;
      t_uchar * args;
      t_ulong h;
      t_uint p;
      int data_fd;

      /* <cmd> host,port args...
       * or
       * <cmd> pasv args...
       */

      host = str_chr_index (command, ' ');
      if (!host)
	goto bad_command;

      ++host;

      if (!str_cmp_prefix ("pasv ", host))
	{
	  /* passive mode */
	  /* block postconditions: args points at the list of args, data_fd ready */
	  if ((0 > printfmt (&errn, connect_out, "%s\n", my_data_url))
	      || (0 > vfdbuf_flush (&errn, connect_out)))
	    {
	      if (errn == EPIPE)
		return;
	      else
		with_ftp_panic ("I/O error in local_handle_command (PASV url send)\n");
	    }
	  data_fd = safe_open (my_data_url, O_RDWR, 0);
	  args = host + 5;	/* "pasv " */
	}
      else
	{
	  /* non-passive mode */
	  /* block postconditions: args points at the list of args, data_fd ready */

	  comma = str_chr_index (host, ',');
	  if (!comma)
	    goto bad_command;

	  if (cvt_decimal_to_ulong (&errn, &h, host, comma - host))
	    goto bad_command;

	  port = comma + 1;

	  args = str_chr_index (port, ' ');
	  if (!args)
	    goto bad_command;

	  if (cvt_decimal_to_uint (&errn, &p, port, args - port))
	    goto bad_command;

	  args += 1;

	  data_fd = url_inet_client_addr (&errn, h, p);
	  if (data_fd < 0)
	    {
	      message = str_save (0, errno_to_string (errn));
	      cmd_status = -1;
	      goto cmd_done;
	    }
	}

      if (!str_cmp_prefix ("get ", command))
	{
	  int file_fd;

	  file_fd = vu_open (&errn, args, O_RDONLY, 0);
	  if (file_fd < 0)
	    {
	      int ign;

	    bad_data_cmd:
	      vu_close (&ign, data_fd);
	      message = str_save (0, errno_to_string (errn));
	      cmd_status = -1;
	      goto cmd_done;
	    }
	  else
	    {
	      message = str_save (0, "file opened for get");
	      if ((0 > printfmt (&errn, connect_out, "OK %lu\n %s\n", (unsigned long)(2 + str_length (message)), message))
		  || (0 > vfdbuf_flush (&errn, connect_out)))
		{
		  if (errn == EPIPE)
		    goto bad_data_cmd;
		  else
		    with_ftp_panic ("I/O error in local_handle_command (get OK)\n");
		}
	    }

	  {
	    char buf[4096];
	    size_t got;

	    while (1)
	      {
		got = vu_read_retry (&errn, file_fd, buf, sizeof (buf));
		if (   (got < 0)
		    || (   (got > 0)
			&& (got != vu_write_retry (&errn, data_fd, buf, got))))
		  {
		    safe_close (file_fd);
		    goto bad_data_cmd;
		  }
		else if (got == 0)
		  {
		    if (   (0 > vu_close (&errn, file_fd))
			|| (0 > vu_close (&errn, data_fd)))
		      goto bad_data_cmd;
		    break;
		  }
	      }
	  }

	  message = str_save (0, "get command ok");
	  cmd_status = 0;
	}
      else if (!str_cmp_prefix ("put ", command))
	{
	  int file_fd;

	  file_fd = vu_open (&errn, args, O_WRONLY | O_CREAT, 0666);
	  safe_buffer_fd (file_fd, 0, O_WRONLY, 0);
	  if (file_fd < 0)
	    goto bad_data_cmd;
	  else
	    {
	      message = str_save (0, "file opened for put");
	      if ((0 > printfmt (&errn, connect_out, "OK %lu\n %s\n", (unsigned long)(2 + str_length (message)), message))
		  || (0 > vfdbuf_flush (&errn, connect_out)))
		{
		  if (errn == EPIPE)
		    goto bad_data_cmd;
		  else
		    with_ftp_panic ("I/O error in local_handle_command (put OK)\n");
		}
	    }

	  {
	    char buf[4096];
	    size_t got;

	    while (1)
	      {
		got = vu_read_retry (&errn, data_fd, buf, sizeof (buf));
		if (   (got < 0)
		    || (   (got > 0)
			&& (got != vu_write_retry (&errn, file_fd, buf, got))))
		  {
		    safe_close (file_fd);
		    goto bad_data_cmd;
		  }
		else if (got == 0)
		  {
		    if (   (0 > vu_close (&errn, file_fd))
			|| (0 > vu_close (&errn, data_fd)))
		      goto bad_data_cmd;
		    break;
		  }
	      }
	  }

	  message = str_save (0, "put command ok");
	  cmd_status = 0;
	}
      else if (!str_cmp_prefix ("nlist ", command))
	{
	  DIR * dir;
	  int l;

	  l = str_length (args);

	  if (0 > vu_opendir (&errn, &dir, l ? args : (t_uchar *)"."))
	    goto bad_data_cmd;
	  else
	    {
	      message = str_save (0, "dir opened for nlist");
	      if ((0 > printfmt (&errn, connect_out, "OK %lu\n %s\n", (unsigned long)(2 + str_length (message)), message))
		  || (0 > vfdbuf_flush (&errn, connect_out)))
		{
		  if (errn == EPIPE)
		    goto bad_data_cmd;
		  else
		    with_ftp_panic ("I/O error in local_handle_command (nlist OK)\n");
		}
	    }

	  while (1)
	    {
	      char * file;

	      file = 0;

	      if (0 > vu_readdir (&errn, 0, &file, dir))
		{
		  safe_closedir (dir);
		  if (!errn)
		    break;
		  else
		    goto bad_data_cmd;
		}
	      else 
		{
		  if (   str_cmp (".", file)
		      && str_cmp ("..", file)
		      && (0 > printfmt (&errn, data_fd, "%s\r\n", file)))
		    {
		      lim_free (0, file);
		      safe_closedir (dir);
		      goto bad_data_cmd;
		    }
		  lim_free (0, file);
		}
	    }
	  if (0 > vu_close (&errn, data_fd))
	    goto bad_data_cmd;

	  message = str_save (0, "nlist command ok");
	  cmd_status = 0;
	}
      else if (!str_cmp_prefix ("list ", command))
	{
	  int ls_pid;

	  message = str_save (0, "beginning list");
	  if ((0 > printfmt (&errn, connect_out, "OK %lu\n %s\n", (unsigned long)(2 + str_length (message)), message))
	      || (0 > vfdbuf_flush (&errn, connect_out)))
	    {
	      if (errn == EPIPE)
		goto bad_data_cmd;
	      else
		with_ftp_panic ("I/O error in local_handle_command (list OK)\n");
	    }

	  ls_pid = 0;

	  if (sigsetjmp (sigchld_jmp_tool, 1))
	    {
	      int wait_pid;
	      int wait_status;

	      safe_close (data_fd);
	      tool_running = 0;
	      
	      if (!ls_pid)
		{
		  /* SIGCHLD must have been for the primary subprocess
		   */
		  siglongjmp (sigchld_jmp, 1);
		}

	      wait_pid = waitpid (ls_pid, &wait_status, WUNTRACED);

	      if (wait_pid < 0)
		with_ftp_panic ("error waiting for ls subprocess");

	      if (wait_pid == 0)
		{
		  /* must be the main subprocess that gave SIGCHLD
		   */
		  if (ls_pid)
		    {
		      ls_pid = 0;
		      kill (ls_pid, 9);
		    }
		  siglongjmp (sigchld_jmp, 1);
		}
	      else if (WIFEXITED (wait_status) && !WEXITSTATUS (wait_status))
		{
		  message = str_save (0, "list command ok");
		  cmd_status = 0;
		  goto cmd_done;
		}
	      else
		{
		  message = str_save (0, "abnormal exit from ls");
		  cmd_status = -1;
		  goto cmd_done;
		}
	    }

	  tool_running = 1;

	  /* Between this fork and the `sigsuspend', nothing may happen that
	   * can not be asynchronously interrupted by `SIGCHLD' with a 
	   * longjmp to `sigchld_jmp_tool' which is filled above.
	   *
	   * That longjmp handler usually resumes normal execution so anything
	   * tricky here would cause reentrancy problems there.
	   */

	  ls_pid = fork ();

	  if (ls_pid < 0)
	    with_ftp_panic ("unable to fork for ls");

	  if (ls_pid > 0)
	    {
	      sigset_t mask;

	      sigemptyset (&mask);
	      sigsuspend (&mask);
	      with_ftp_panic ("SIGCHLD didn't cause longjmp?!?!?!");
	    }
	  else
	    {
	      safe_move_fd (data_fd, 1);
	      execlp ("ls", "ls", "-l", 0);
	      with_ftp_panic ("execlp of ls failed");
	    }
	}
      else
	{
	bad_command:
	  safe_printfmt (2, "with-ftp: bad command recieved (%s)\n", command);
	  with_ftp_panic ("with-ftp protocol error");
	}
    }
      
 cmd_done:
  {
    int ign;
    printfmt (&ign, connect_out, "%s %lu\n %s\n", ((cmd_status < 0) ? "ERROR" : "OK"), (unsigned long)(2 + str_length (message)), message);
    vfdbuf_flush (&ign, connect_out);
    lim_free (0, message);
  }
}




/* tag: Tom Lord Sat Jan 18 15:39:04 2003 (local-handler.c)
 */
