/*
 * mod_site_utime, based on mod_site_misc.
 *  Basically, just provide the SITE UTIME functionality and
 *  nothing else. This module extends the SITE UTIME impl
 *  in mod_site_misc by allowing for the 2 most popular
 *  formats in use:
 *
 *    SITE UTIME YYYYMMDDhhmm[ss] path
 *    SITE UTIME path YYYYMMDDhhmm[ss] YYYYMMDDhhmm[ss] YYYYMMDDhhmm[ss] UTC
 *
 *  and fixes the bug in the 1.3.1 (and older) versions that don't
 *  allow/honor the seconds in the timestamp. Also, provide some
 *  basic error checking and use the TZ environment work-around
 *  found in src/support.c.
 *
 *  Jim Jagielski (jim@jaguNET.com)
 */
/*
 * ProFTPD: mod_site_misc -- a module implementing miscellaneous SITE commands
 *
 * Copyright (c) 2004-2006 The ProFTPD Project
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
 *
 * As a special exemption, The ProFTPD Project team and other respective
 * copyright holders give permission to link this program with OpenSSL, and
 * distribute the resulting executable, without including the source code for
 * OpenSSL in the source distribution.
 *
 * $Id: mod_site_misc.c,v 1.3 2006/06/16 02:33:43 castaglia Exp $
 */

#include "conf.h"

#define MOD_SITE_UTIME_VERSION		"mod_site_utime/1.0"

static int site_utime_check_filters(cmd_rec *cmd, const char *path) {
#if defined(HAVE_REGEX_H) && defined(HAVE_REGCOMP)
  regex_t *preg = get_param_ptr(CURRENT_CONF, "PathAllowFilter", FALSE);

  if (preg &&
      regexec(preg, path, 0, NULL, 0) != 0) {
    pr_log_debug(DEBUG2, MOD_SITE_UTIME_VERSION
      ": 'SITE %s' denied by PathAllowFilter", cmd->arg);
    return -1;
  }

  preg = get_param_ptr(CURRENT_CONF, "PathDenyFilter", FALSE);

  if (preg &&
      regexec(preg, path, 0, NULL, 0) == 0) {
    pr_log_debug(DEBUG2, MOD_SITE_UTIME_VERSION
      ": 'SITE %s' denied by PathDenyFilter", cmd->arg);
    return -1;
  }
#endif

  return 0;
}

/*
 * Returns 1 if the timestamp is the correct length
 * (either 12 or 14 chars, depending on whether or not
 * in includes secs) and contains all digits, otherwise
 * returns 0.
 *
 * NOTE: Validity of the *values* (ie: sec > 60) is not
 * done here
 */
static int site_utime_isvalidtimestamp(char *timestamp) {
  int len;
  char *p = timestamp;
  while (*p) {
    if (!isdigit(*p)) {
      return 0;
    }
    p++;
  }
  len = p - timestamp;

  if (len == 12 || len == 14) {
    return 1;
  } else {
    return 0;
  }
}

static time_t site_utime_mktime(struct tm itm) {
  struct tm tm;
  time_t res;
  char *env;
 
#ifdef HAVE_TZNAME
  char *tzname_dup[2];

  /* The mktime(3) function has a nasty habit of changing the tzname global
   * variable as a side-effect.  This can cause problems, as when the process 
   * has become chrooted, and mktime(3) sets/changes tzname wrong.  (For more
   * information on the tzname global variable, see the tzset(3) man page.)
   *
   * The best way to deal with this issue (which is especially prominent
   * on systems running glibc-2.3 or later, which is particularly ill-behaved
   * in a chrooted environment, as it assumes the ability to find system
   * timezone files at paths which are no longer valid within the chroot)
   * is to set the TZ environment variable explicitly, before starting
   * proftpd.  You can also use the SetEnv configuration directive within
   * the proftpd.conf to set the TZ environment variable, e.g.:
   *
   *  SetEnv TZ PST
   *
   * To try to help sites which fail to do this, the tzname global variable
   * will be copied prior to the mktime(3) call, and the copy restored after
   * the call.  (Note that calling the ctime(3) and localtime(3) functions also
   * causes a similar overwriting/setting of the tzname environment variable.)
   */
  memcpy(&tzname_dup, tzname, sizeof(tzname_dup));
#endif /* HAVE_TZNAME */

  env = pr_env_get(session.pool, "TZ");

  /* Set the TZ environment to be GMT, so that mktime(3) treats the timestamp
   * provided by the client as being in GMT/UTC.
   */
  if (pr_env_set(session.pool, "TZ", "GMT") < 0) {
    pr_log_debug(DEBUG8, MOD_SITE_UTIME_VERSION
      ": error setting TZ environment variable to 'GMT': %s", strerror(errno));
  }

  tm.tm_sec = itm.tm_sec;
  tm.tm_min = itm.tm_min;
  tm.tm_hour = itm.tm_hour;
  tm.tm_mday = itm.tm_mday;
  tm.tm_mon = (itm.tm_mon - 1);
  tm.tm_year = (itm.tm_year - 1900);
  tm.tm_wday = 0;
  tm.tm_yday = 0;
  tm.tm_isdst = -1;
   
  res = mktime(&tm);

  /* Restore the old TZ setting, if any. */
  if (env) {
    if (pr_env_set(session.pool, "TZ", env) < 0) {
      pr_log_debug(DEBUG8, MOD_SITE_UTIME_VERSION
        ": error setting TZ environment variable to '%s': %s", env,
        strerror(errno));
    }
  }

#ifdef HAVE_TZNAME
  /* Restore the old tzname values prior to returning. */
  memcpy(tzname, tzname_dup, sizeof(tzname_dup));
#endif /* HAVE_TZNAME */

  return res;
}

/*
 * Returns 1, and a filled out time_t struct, if the
 * parsed timestamp is correct (eg: mon between
 * 1 and 12). If the timestamp results in invalid times,
 * then return 0. Recall that we know that the timestamp is
 * all digits and has strlen of 12 or 14 at this time (we
 * still check anyway)
 *
 * Format of timestamp: YYYYMMDDhhmm[ss]
 */
static int site_utime_parsetime(time_t *stamp, char *timestamp) {
  struct tm tm;
  int ret;

  ret = sscanf(timestamp, "%4u%2u%2u%2u%2u%2u", &tm.tm_year, &tm.tm_mon,
               &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec);

  if (ret == 5) {
    tm.tm_sec = 0;
  }
  /* basic checks. Yes, mktime() should barf if these are
   * wrong, but pre-check. Use time.h as baseline
   */
  if (tm.tm_year < 1900 ||
      tm.tm_mon > 12 ||
      tm.tm_mon == 0 ||
      tm.tm_mday > 31 ||
      tm.tm_mday == 0 ||
      tm.tm_hour > 23 ||
      tm.tm_min > 59 || 
      (ret == 6 && tm.tm_sec > 60) ||
      ret < 5) {
   return 0;
  }
  
  if ((*stamp = site_utime_mktime(tm)) == (time_t) -1) {
    return 0;   /* some generic error in mktime() */
  } else {
    return 1;
  }
}

/* Command handler
 */

MODRET site_utime_utime(cmd_rec *cmd) {
  if (cmd->argc < 2)
    return PR_DECLINED(cmd);

  if (strcasecmp(cmd->argv[1], "UTIME") == 0) {
    register unsigned int i;
    char *path = "";
    struct utimbuf tmbuf;
    time_t actime, modtime;
    unsigned char *authenticated;

    if (cmd->argc < 4)
      return PR_DECLINED(cmd);

    authenticated = get_param_ptr(cmd->server->conf, "authenticated", FALSE);

    if (!authenticated || *authenticated == FALSE) {
      pr_response_add_err(R_530, "Please login with USER and PASS");
      return PR_ERROR(cmd);
    }

    /*
     * There are 2 formats for SITE UTIME floating around out
     * there among clients:
     *
     *    SITE UTIME YYYYMMDDhhmm path
     *    SITE UTIME path YYYYMMDDhhmm YYYYMMDDhhmm YYYYMMDDhhmm UTC
     *
     * Up to now, we've only supported the 1st, but we now support
     * both. So the first thing we need to do is find out which
     * format we're looking at.
     *
     * Note: We support both YYYYMMDDhhmm and YYYYMMDDhhmmss
     */
    /*
     * looks like format 2. If so, then check the 3 timestamps
     * first and look for the UTC
     */
    if ( (cmd->argc == 7) &&
         (strcmp(cmd->argv[6], "UTC") == 0) &&
         (site_utime_isvalidtimestamp(cmd->argv[3]))  &&
         (site_utime_isvalidtimestamp(cmd->argv[4]))  &&
         (site_utime_isvalidtimestamp(cmd->argv[5])) ) {
      path = pstrcat(cmd->tmp_pool, path, cmd->argv[2], NULL);
      if (!site_utime_parsetime(&actime, cmd->argv[3]) ||
          !site_utime_parsetime(&modtime, cmd->argv[4])) {
        pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(EINVAL));
        return PR_ERROR(cmd);
      }
    } else {
     /*
      * drop down to previous impl
      */
      if (!site_utime_isvalidtimestamp(cmd->argv[2])) {
        pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(EINVAL));
        return PR_ERROR(cmd);
      }
      if (!site_utime_parsetime(&modtime, cmd->argv[2])) {
        pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(EINVAL));
        return PR_ERROR(cmd);
      }
      actime = modtime;

      for (i = 3; i < cmd->argc; i++)
        path = pstrcat(cmd->tmp_pool, path, *path ? " " : "", cmd->argv[i], NULL);
    }

    path = pr_fs_decode_path(cmd->tmp_pool, path);

    if (!dir_check(cmd->tmp_pool, "SITE_UTIME", G_WRITE, path, NULL)) {
      pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(EPERM));
      return PR_ERROR(cmd);
    }

    if (site_utime_check_filters(cmd, path) < 0) {
      pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(EPERM));
      return PR_ERROR(cmd);
    }

    tmbuf.actime = actime;
    tmbuf.modtime = modtime;

    if (utime(path, &tmbuf) < 0) {
      pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(errno));
      return PR_ERROR(cmd);
    }
 
    pr_response_add(R_200, "SITE %s command successful", cmd->argv[1]);
    return PR_HANDLED(cmd);
  }

  if (strcasecmp(cmd->argv[1], "HELP") == 0) {
    pr_response_add(R_214, "UTIME <sp> YYYYMMDDhhmm[ss] <sp> path");
    pr_response_add(R_214, "UTIME path <sp> YYYYMMDDhhmm[ss] <sp> YYYYMMDDhhmm[ss] <sp> YYYYMMDDhhmm[ss] <sp> UTC");
  }
  return PR_DECLINED(cmd);
}

/* Initialization functions
 */

/* Module API tables
 */

static cmdtable site_utime_cmdtab[] = {
  { CMD, C_SITE, G_WRITE, site_utime_utime,	FALSE,	FALSE, CL_MISC },
  { 0, NULL }
};

module site_utime_module = {
  NULL, NULL,

  /* Module API version 2.0 */
  0x20,

  /* Module name */
  "site_utime",

  /* Module configuration handler table */
  NULL,

  /* Module command handler table */
  site_utime_cmdtab,

  /* Module authentication handler table */
  NULL,

  /* Module initialization function */
  NULL,

  /* Session initialization function */
  NULL,

  /* Module version */
  MOD_SITE_UTIME_VERSION
};
