/** * \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; }