pennmush/game/data/
pennmush/game/log/
pennmush/game/save/
pennmush/game/txt/evt/
pennmush/game/txt/nws/
pennmush/os2/
pennmush/po/
pennmush/win32/msvc.net/
pennmush/win32/msvc6/
/**
 * \file
 *
 * \brief Access control lists for PennMUSH.
 * \verbatim
 *
 * The file access.cnf in the game directory will control all 
 * access-related directives, replacing lockout.cnf and sites.cnf
 *
 * The format of entries in the file will be:
 *
 * wild-host-name    [!]option [!]option [!]option ... # comment
 *
 * A wild-host-name is a wildcard pattern to match hostnames with. 
 * The wildcard "*" will work like UNIX filename globbing, so
 * *.edu will match all sites with names ending in .edu, and
 * *.*.*.*.* will match all sites with 4 periods in their name.
 * 128.32.*.* will match all sites starting with 128.32 (UC Berkeley).
 * You can also use user@host to match specific users if you know that
 * the host is running ident and you trust its responses (nontrivial).
 *
 * The options that can be specified are:
 * *CONNECT              Allow connections to non-guest players
 * *GUEST                Allow connection to guests
 * *CREATE               Allow player creation at login screen
 * DEFAULT               All of the above
 * NONE                 None of the above
 * SUSPECT              Set all players connecting from the site suspect
 * REGISTER             Allow players to use the "register" connect command
 * DENY_SILENT          Don't log when someone's denied access from here
 * REGEXP               Treat the hostname pattern as a regular expression
 * *GOD                  God can connect from this pattern.
 * *WIZARD               Wizards can connect from this pattern.
 * *ADMIN                Admins can connect from this pattern.
 *
 * Options that are *'d can be prefaced by a !, meaning "Don't allow".
 *
 * The file is parsed line-by-line in order. This makes it possible
 * to explicitly allow only certain sites to connect and deny all others,
 * or vice versa. Sites can only do the options that are specified
 * in the first line they match.
 *
 * If a site is listed in the file with no options at all, it is
 * disallowed from any access (treated as !CONNECT, basically)
 *
 * If a site doesn't match any line in the file, it is allowed any
 * toggleable access (treated as DEFAULT) but isn't SUSPECT or REGISTER.
 *
 * "make access" produces access.cnf from lockout.cnf/sites.cnf
 *
 * @sitelock'd sites appear after the line "@sitelock" in the file
 * Using @sitelock writes out the file.
 * 
 * \endverbatim
 */

#include "config.h"
#include "copyrite.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#ifdef I_SYS_TYPES
#include <sys/types.h>
#endif
#include <fcntl.h>
#ifdef I_SYS_TIME
#include <sys/time.h>
#else
#include <time.h>
#endif
#ifdef I_UNISTD
#include <unistd.h>
#endif
#include "conf.h"
#include "externs.h"
#include "access.h"
#include "mymalloc.h"
#include "match.h"
#include "parse.h"
#include "log.h"
#include "mushdb.h"
#include "dbdefs.h"
#include "flags.h"
#include "confmagic.h"


/** An access flag. */
typedef struct a_acsflag acsflag;
/** An access flag.
 * This structure is used to build a table of access control flags.
 */
struct a_acsflag {
  const char *name;		/**< Name of the access flag */
  int toggle;			/**< Is this a negatable flag? */
  int flag;			/**< Bitmask of the flag */
};
static acsflag acslist[] = {
  {"connect", 1, ACS_CONNECT},
  {"create", 1, ACS_CREATE},
  {"guest", 1, ACS_GUEST},
  {"default", 0, ACS_DEFAULT},
  {"register", 0, ACS_REGISTER},
  {"suspect", 0, ACS_SUSPECT},
  {"deny_silent", 0, ACS_DENY_SILENT},
  {"regexp", 0, ACS_REGEXP},
  {"god", 1, ACS_GOD},
  {"wizard", 1, ACS_WIZARD},
  {"admin", 1, ACS_ADMIN},
  {NULL, 0, 0}
};

static struct access *access_top;
static int add_access_node
  (const char *host, const dbref who, const int can, const int cant,
   const char *comment);
static void free_access_list(void);

static int
add_access_node(const char *host, const dbref who, const int can,
		const int cant, const char *comment)
{
  struct access *end;
  struct access *tmp;

  tmp = (struct access *) mush_malloc(sizeof(struct access), "struct_access");
  if (!tmp)
    return 0;
  tmp->who = who;
  tmp->can = can;
  tmp->cant = cant;
  strcpy(tmp->host, host);
  if (comment)
    strcpy(tmp->comment, comment);
  else
    tmp->comment[0] = '\0';
  tmp->next = NULL;

  if (!access_top) {
    /* Add to the beginning */
    access_top = tmp;
  } else {
    end = access_top;
    while (end->next)
      end = end->next;
    end->next = tmp;
  }

  return 1;
}


/** Read the access.cnf file.
 * Initialize the access rules linked list and read in the access.cnf file.
 * Return 1 if successful, 0 if not
 */
int
read_access_file(void)
{
  FILE *fp;
  char buf[BUFFER_LEN];
  char *p;
  int can, cant;
  int retval;
  dbref who;
  char *comment;

  if (access_top) {
    /* We're reloading the file, so we've got to delete any current 
     * entries
     */
    free_access_list();
  }
  access_top = NULL;
  /* Be sure we have a file descriptor */
  release_fd();
  fp = fopen(ACCESS_FILE, FOPEN_READ);
  if (!fp) {
    do_log(LT_ERR, GOD, GOD, T("No %s file found."), ACCESS_FILE);
    retval = 0;
  } else {
    do_rawlog(LT_ERR, "Reading %s", ACCESS_FILE);
    while (fgets(buf, BUFFER_LEN, fp)) {
      /* Strip end of line if it's \r\n or \n */
      if ((p = strchr(buf, '\r')))
	*p = '\0';
      else if ((p = strchr(buf, '\n')))
	*p = '\0';
      /* Find beginning of line; ignore blank lines */
      p = buf;
      if (*p && isspace((unsigned char) *p))
	p++;
      if (*p && *p != '#') {
	can = cant = 0;
	comment = NULL;
	/* Is this the @sitelock entry? */
	if (!strncasecmp(p, "@sitelock", 9)) {
	  if (!add_access_node("@sitelock", AMBIGUOUS, ACS_SITELOCK, 0, ""))
	    do_log(LT_ERR, GOD, GOD, T("Failed to add sitelock node!"));
	} else {
	  if ((comment = strchr(p, '#'))) {
	    *comment++ = '\0';
	    while (*comment && isspace((unsigned char) *comment))
	      comment++;
	  }
	  /* Move past the host name */
	  while (*p && !isspace((unsigned char) *p))
	    p++;
	  if (*p)
	    *p++ = '\0';
	  if (!parse_access_options(p, &who, &can, &cant, NOTHING))
	    /* Nothing listed, so assume we can't do anything! */
	    cant = ACS_DEFAULT;
	  if (!add_access_node(buf, who, can, cant, comment))
	    do_log(LT_ERR, GOD, GOD, T("Failed to add access node!"));
	}
      }
    }
    retval = 1;
    fclose(fp);
  }
  reserve_fd();
  return retval;
}

/** Write the access.cnf file.
 * Writes out the access.cnf file from the linked list
 */
void
write_access_file(void)
{
  FILE *fp;
  char tmpf[BUFFER_LEN];
  struct access *ap;
  acsflag *c;

  sprintf(tmpf, "%s.tmp", ACCESS_FILE);
  /* Be sure we have a file descriptor */
  release_fd();
  fp = fopen(tmpf, FOPEN_WRITE);
  if (!fp) {
    do_log(LT_ERR, GOD, GOD, T("Unable to open %s."), tmpf);
  } else {
    for (ap = access_top; ap; ap = ap->next) {
      if (strcmp(ap->host, "@sitelock") == 0) {
	fprintf(fp, "@sitelock\n");
	continue;
      }
      fprintf(fp, "%s %d ", ap->host, ap->who);
      switch (ap->can) {
      case ACS_SITELOCK:
	break;
      case ACS_DEFAULT:
	fprintf(fp, "DEFAULT ");
	break;
      default:
	for (c = acslist; c->name; c++)
	  if (ap->can & c->flag)
	    fprintf(fp, "%s ", c->name);
	break;
      }
      switch (ap->cant) {
      case ACS_DEFAULT:
	fprintf(fp, "NONE ");
	break;
      default:
	for (c = acslist; c->name; c++)
	  if (c->toggle && (ap->cant & c->flag))
	    fprintf(fp, "!%s ", c->name);
	break;
      }
      if (ap->comment && *ap->comment)
	fprintf(fp, "# %s\n", ap->comment);
      else
	fprintf(fp, "\n");
    }
    fclose(fp);
    rename_file(tmpf, ACCESS_FILE);
  }
  reserve_fd();
  return;
}

#ifdef FORCE_IPV4
static char *
ip4_to_ip6(const char *addr)
{
  static char tbuf1[BUFFER_LEN];
  char *bp;
  bp = tbuf1;
  safe_format(tbuf1, &bp, "::ffff:%s", addr);
  *bp = '\0';
  return tbuf1;
}
#endif


/** Decide if a host can access someway.
 * \param hname a host or user+host pattern.
 * \param flag the access type we're testing.
 * \param who the player attempting access.
 * \retval 1 access permitted.
 * \retval 0 access denied.
 * \verbatim
 * Given a hostname and a flag decide if the host can do it.
 * Here's how it works:
 * We run the linked list and take the first match.
 *  (If the hostname is user@host, we try to match both user@host
 *   and just host to each line in the file.)
 * If we make a match, and the line tells us whether the site can/can't
 *   do the action, we're done.
 * Otherwise, we assume that the host can do any toggleable option
 *   (can create, connect, guest), and don't have any special
 *   flags (can't register, isn't suspect)
 * \endverbatim
 */
int
site_can_access(const char *hname, int flag, dbref who)
{
  struct access *ap;
  acsflag *c;
  char *p;

  if (!hname || !*hname)
    return 0;

  if ((p = strchr(hname, '@')))
    p++;

  for (ap = access_top; ap; ap = ap->next) {
    if (!(ap->can & ACS_SITELOCK)
	&& ((ap->can & ACS_REGEXP)
	    ? (regexp_match_case(ap->host, hname, 0)
	       || (p && regexp_match_case(ap->host, p, 0))
#ifdef FORCE_IPV4
	       || regexp_match_case(ip4_to_ip6(ap->host), hname, 0)
	       || (p && regexp_match_case(ip4_to_ip6(ap->host), p, 0))
#endif
	    )
	    : (quick_wild(ap->host, hname)
	       || (p && quick_wild(ap->host, p))
#ifdef FORCE_IPV4
	       || quick_wild(ip4_to_ip6(ap->host), hname)
	       || (p && quick_wild(ip4_to_ip6(ap->host), p))
#endif
	    ))
	&& (ap->who == AMBIGUOUS || ap->who == who)) {
      /* Got one */
      if (flag & ACS_CONNECT) {
	if ((ap->cant & ACS_GOD) && God(who))	/* God can't connect from here */
	  return 0;
	else if ((ap->cant & ACS_WIZARD) && Wizard(who))
	  /* Wiz can't connect from here */
	  return 0;
	else if ((ap->cant & ACS_ADMIN) && Hasprivs(who))
	  /* Wiz and roy can't connect from here */
	  return 0;
      }
      if (ap->cant && ((ap->cant & flag) == flag))
	return 0;
      if (ap->can && (ap->can & flag))
	return 1;

      /* Hmm. We don't know if we can or not, so continue */
      break;
    }
  }

  /* Flag was neither set nor unset. If the flag was a toggle,
   * then the host can do it. If not, the host can't */
  for (c = acslist; c->name; c++) {
    if (flag & c->flag)
      return c->toggle ? 1 : 0;
  }
  /* Should never reach here, but just in case */
  return 1;
}


/** Return the first access rule that matches a host.
 * \param hname a host or user+host pattern.
 * \param who the player attempting access.
 * \param rulenum pointer to rule position.
 * \return pointer to first matching access rule or NULL.
 */
struct access *
site_check_access(const char *hname, dbref who, int *rulenum)
{
  struct access *ap;
  char *p;

  *rulenum = 0;
  if (!hname || !*hname)
    return 0;

  if ((p = strchr(hname, '@')))
    p++;

  for (ap = access_top; ap; ap = ap->next) {
    (*rulenum)++;
    if (!(ap->can & ACS_SITELOCK)
	&& ((ap->can & ACS_REGEXP)
	    ? (regexp_match_case(ap->host, hname, 0)
	       || (p && regexp_match_case(ap->host, p, 0))
#ifdef FORCE_IPV4
	       || regexp_match_case(ip4_to_ip6(ap->host), hname, 0)
	       || (p && regexp_match_case(ip4_to_ip6(ap->host), p, 0))
#endif
	    )
	    : (quick_wild(ap->host, hname)
	       || (p && quick_wild(ap->host, p))
#ifdef FORCE_IPV4
	       || quick_wild(ip4_to_ip6(ap->host), hname)
	       || (p && quick_wild(ip4_to_ip6(ap->host), p))
#endif
	    ))
	&& (ap->who == AMBIGUOUS || ap->who == who)) {
      /* Got one */
      return ap;
    }
  }
  return NULL;
}

/** Display an access rule.
 * \param ap pointer to access rule.
 * \param rulenum access rule's number in the list.
 * \param who unused.
 * \param buff buffer to store output.
 * \param bp pointer into buff.
 * This function provides an appealing display of an access rule
 * in the list.
 */
int
format_access(struct access *ap, int rulenum,
	      dbref who __attribute__ ((__unused__)), char *buff, char **bp)
{
  if (ap) {
    safe_format(buff, bp, T("Matched line %d: %s %s"), rulenum, ap->host,
		(ap->can & ACS_REGEXP) ? "(regexp)" : "");
    safe_chr('\n', buff, bp);
    safe_format(buff, bp, T("Comment: %s"), ap->comment);
    safe_chr('\n', buff, bp);
    safe_str(T("Connections allowed by: "), buff, bp);
    if (ap->cant & ACS_CONNECT)
      safe_str(T("No one"), buff, bp);
    else if (ap->cant & ACS_ADMIN)
      safe_str(T("All but admin"), buff, bp);
    else if (ap->cant & ACS_WIZARD)
      safe_str(T("All but wizards"), buff, bp);
    else if (ap->cant & ACS_GOD)
      safe_str(T("All but God"), buff, bp);
    else
      safe_str(T("All"), buff, bp);
    safe_chr('\n', buff, bp);
    if (ap->cant & ACS_GUEST)
      safe_str(T("Guest connections are NOT allowed"), buff, bp);
    else
      safe_str(T("Guest connections are allowed"), buff, bp);
    safe_chr('\n', buff, bp);
    if (ap->cant & ACS_CREATE)
      safe_str(T("Creation is NOT allowed"), buff, bp);
    else
      safe_str(T("Creation is allowed"), buff, bp);
    safe_chr('\n', buff, bp);
    if (ap->can & ACS_REGISTER)
      safe_str(T("Email registration is allowed"), buff, bp);
    if (ap->can & ACS_SUSPECT)
      safe_str(T("Players connecting are set SUSPECT"), buff, bp);
    if (ap->can & ACS_DENY_SILENT)
      safe_str(T("Denied connections are not logged"), buff, bp);
  } else {
    safe_str(T("No matching access rule"), buff, bp);
  }
  return 0;
}


/** Add an access rule to the linked list.
 * \param player enactor.
 * \param host host pattern to add.
 * \param who player to which rule applies, or AMBIGUOUS.
 * \param can flags of allowed actions.
 * \param cant flags of disallowed actions.
 * \retval 1 success.
 * \retval 0 failure.
 * \verbatim
 * This function adds an access rule after the @sitelock entry.
 * If there is no @sitelock entry, add one to the end of the list
 * and then add the entry.
 * Build an appropriate comment based on the player and date
 * \endverbatim
 */
int
add_access_sitelock(dbref player, const char *host, dbref who, int can,
		    int cant)
{
  struct access *end;
  struct access *tmp;

  tmp = (struct access *) mush_malloc(sizeof(struct access), "struct_access");
  if (!tmp)
    return 0;

  tmp->who = who;
  tmp->can = can;
  tmp->cant = cant;
  strcpy(tmp->host, host);
  sprintf(tmp->comment, "By %s(#%d) on %s", Name(player), player,
	  show_time(mudtime, 0));
  tmp->next = NULL;

  if (!access_top) {
    /* Add to the beginning, but first add a sitelock marker */
    if (!add_access_node("@sitelock", AMBIGUOUS, ACS_SITELOCK, 0, ""))
      return 0;
    access_top->next = tmp;
  } else {
    end = access_top;
    while (end->next && end->can != ACS_SITELOCK)
      end = end->next;
    /* Now, either we're at the sitelock or the end */
    if (end->can != ACS_SITELOCK) {
      /* We're at the end and there's no sitelock marker. Add one */
      if (!add_access_node("@sitelock", AMBIGUOUS, ACS_SITELOCK, 0, ""))
	return 0;
      end = end->next;
    } else {
      /* We're in the middle, so be sure we keep the list linked */
      tmp->next = end->next;
    }
    end->next = tmp;
  }
  return 1;
}

/** Remove an access rule from the linked list.
 * \param pattern access rule host pattern to match.
 * \return number of rule removed.
 * \verbatim
 * This function removes an access rule from the list.
 * Only rules that appear after the "@sitelock" rule can be
 * removed with this function.
 * \endverbatim
 */
int
remove_access_sitelock(const char *pattern)
{
  struct access *ap, *next, *prev = NULL;
  int n = 0;

  /* We only want to be able to delete entries added with @sitelock */
  for (ap = access_top; ap; ap = ap->next)
    if (strcmp(ap->host, "@sitelock") == 0) {
      prev = ap;
      ap = ap->next;
      break;
    }

  while (ap) {
    next = ap->next;
    if (strcasecmp(pattern, ap->host) == 0) {
      n++;
      mush_free(ap, "struct_access");
      if (prev)
	prev->next = next;
      else
	access_top = next;
    } else {
      prev = ap;
    }
    ap = next;
  }

  return n;
}

/* Free the entire access list */
static void
free_access_list(void)
{
  struct access *ap, *next;
  ap = access_top;
  while (ap) {
    next = ap->next;
    mush_free((Malloc_t) ap, "struct_access");
    ap = next;
  }
  access_top = NULL;
}


/** Display the access list.
 * \param player enactor.
 * Sends the complete access list to the player.
 */
void
do_list_access(dbref player)
{
  struct access *ap;
  acsflag *c;
  char flaglist[BUFFER_LEN];
  int rulenum = 0;
  char *bp;

  for (ap = access_top; ap; ap = ap->next) {
    rulenum++;
    if (ap->can != ACS_SITELOCK) {
      bp = flaglist;
      for (c = acslist; c->name; c++) {
	if (c->flag == ACS_DEFAULT)
	  continue;
	if (ap->can & c->flag) {
	  safe_chr(' ', flaglist, &bp);
	  safe_str(c->name, flaglist, &bp);
	}
	if (c->toggle && (ap->cant & c->flag)) {
	  safe_chr(' ', flaglist, &bp);
	  safe_chr('!', flaglist, &bp);
	  safe_str(c->name, flaglist, &bp);
	}
      }
      *bp = '\0';
      notify_format(player,
		    "%3d SITE: %-20s  DBREF: %-6s FLAGS:%s", rulenum,
		    ap->host, unparse_dbref(ap->who), flaglist);
      notify_format(player, " COMMENT: %s", ap->comment);
    } else {
      notify(player,
	     T
	     ("---- @sitelock will add sites immediately below this line ----"));
    }

  }
}

/** Parse access options into fields.
 * \param opts access options to read from.
 * \param who pointer to player to whom rule applies, or AMBIGUOUS.
 * \param can pointer to flags of allowed actions.
 * \param cant pointer to flags of disallowed actions.
 * \param player enactor.
 * \return number of options successfully parsed.
 * Parse options and return the appropriate can and cant bits.
 * Return the number of options successfully parsed.
 * This makes a copy of the options string, so it's not modified.
 */
int
parse_access_options(const char *opts, dbref *who, int *can, int *cant,
		     dbref player)
{
  char myopts[BUFFER_LEN];
  char *p;
  char *w;
  acsflag *c;
  int found, totalfound, first;

  if (!opts || !*opts)
    return 0;
  strcpy(myopts, opts);
  totalfound = 0;
  first = 1;
  if (who)
    *who = AMBIGUOUS;
  p = trim_space_sep(myopts, ' ');
  while ((w = split_token(&p, ' '))) {
    found = 0;

    if (first && who) {		/* Check for a character */
      first = 0;
      if (is_integer(w)) {	/* We have a dbref */
	*who = parse_integer(w);
	if (*who != AMBIGUOUS && !GoodObject(*who))
	  *who = AMBIGUOUS;
	continue;
      }
    }

    if (*w == '!') {
      /* Found a negated warning */
      w++;
      for (c = acslist; c->name; c++) {
	if (c->toggle && !strncasecmp(w, c->name, strlen(c->name))) {
	  *cant |= c->flag;
	  found++;
	}
      }
    } else {
      /* None is special */
      if (!strncasecmp(w, "NONE", 4)) {
	*cant = ACS_DEFAULT;
	found++;
      } else {
	for (c = acslist; c->name; c++) {
	  if (!strncasecmp(w, c->name, strlen(c->name))) {
	    *can |= c->flag;
	    found++;
	  }
	}
      }
    }
    /* At this point, we haven't matched any warnings. */
    if (!found) {
      if (GoodObject(player))
	notify_format(player, T("Unknown access option: %s"), w);
      else
	do_log(LT_ERR, GOD, GOD, T("Unknown access flag: %s"), w);
    } else {
      totalfound += found;
    }
  }
  return totalfound;
}