psyclpc/src/access_check.c

492 lines
16 KiB
C

/*---------------------------------------------------------------------------
* IP/time based access control.
*
* Copyright (C) 1995 by Joern Rennecke.
*---------------------------------------------------------------------------
* Access to the game is based on the user's IP address and the current time.
* These are matched against a set of rules and if a match is detected, a
* proper message is sent back and the connection is shutdown.
*
* This facility is active only if ACCESS_CONTROL has been defined in config.h
* If ACCESS_LOG is defined in config.h, all checks and their results are
* logged by comm.c in the specified file.
*
* The rules are read from the file ACCESS_FILE (defined in config.h,
* typically "ACCESS.ALLOW") which resides in the mudlib. Every line specifies
* one rule and has to follow the syntax given below. Lines with a '#' as
* first character count as comments and are ignored, as are lines which
* do not conform to the rule syntax (but except for empty lines this should
* be relied upon).
*
* The syntax for a rule is (no leading whitespace allowed!):
*
* <ipnum>:[p<port>]:<class>:<max>:<start>:<end>:<text>
* <ipnum>:[p<port>]:<class>:<max>:h<hours>:w<days>:m=<text>
*
* where
* ipnum: <byte>.<byte>.<byte>.<byte>, with byte = * or number
* There is only loose error checking - specifying an illegal
* address will have interesting consequences, but would
* most likely cause no error to occur.
* port: the port number to which the connection is made. Omission
* means 'any port'.
* class: number
* max: the maximum number of users, a number. The value -1 allows
* an unlimited number of users.
* start: hour this rule starts to be valid (0..23).
* end: hour this rule ceases to be valid (0..23).
* Setting both start and end to 0 skips any time check.
* hours: hours this rule is valid.
* This form allows several entries, separated with a ','.
* Every entry can be a single hour (0..23), or a range in the
* form '<start>-<end>'
* Omitting the entry skips any time check.
* days: the days this rule is valid.
* The syntax is similar to <hours> except for the
* allowable values: the days Sunday..Saturday are given as
* the numbers 0..6.
* Omitting the entry skips any day check.
* text: string to send if the rule matches.
*
* A class is defined by the first rule using it's number. This
* definition specifies the allowable <max>imum of users and the <text>
* to send. Subsequent rules for the same class just add new ipnumber/
* time rules, but don't change <max> or <text>
*
* ORDER MATTERS. That means if you allow 129.*.*.*, you have to put
* any restrictions on 129.132.*.* BEFORE this rule.
*
* Addresses not matching any rule at all are not allowed. To get around
* this, add an appropriate 'allow-all' rule *.*.*.* at the very end.
*
* An example rulefile:
*
* # SPARC cluster has access denied. Class 1
* 129.132.122.*:1:0:0:0:LPMUD access denied for your cluster.
*
* # CALL-1A0 has access limited to some maximum, for now 5 logins. Class 2
* 129.132.106.*:2:5:8:20:Sorry, LPMUD is currently full.
*
* # CALL-1A0 at all other times, its a 10 limit.
* # Due to the rule order, this is effectively limited to times
* # outside 8-20.
* 129.132.106.*:3:10:0:0:Sorry, LPMUD is currently full.
*
* # No more than 5 users allowed from localhost while working hours :-)
* 127.0.0.1:42:5:h8-12,13-18:w1-5:m=Pick a better time.
*
* # Everybody else is welcome.
* *.*.*.*:0:-1:0:0:This message should never be printed.
*
* The rule file is (re)read whenever the gamedriver detects a change in its
* timestamp.
*
* TODO: Make ACCESS_CONTROL a runtime option, also, when the file is
* TODO:: missing on driver startup (and just then), allow every access.
* TODO:: Alternatively, this could be made an
* TODO:: efun "string|int access_control(file, [interactive])" to be used
* TODO:: from TODO:: master.c::connect().
* TODO:: Or a driver hook with the settings "file", ({ "file", "logfile" })
* TODO:: and #'function(ip-address, port).
*---------------------------------------------------------------------------
*/
#include "driver.h"
#if defined(ACCESS_CONTROL)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#include <time.h>
#include "access_check.h"
#include "comm.h"
#include "filestat.h"
#include "xalloc.h"
#undef DEBUG_ACCESS_CHECK /* define to activate debug output */
/*-------------------------------------------------------------------------*/
#define MAX_MESSAGE_LENGTH 256
/* IP address/time rules, kept in a linked list.
* Every structure describes one rule, with a reference to the
* corresponding class structure.
*/
static struct access_address {
uint32 addr, mask; /* The IP address and a mask for the interesting parts */
int32 port; /* The port number (positive), or -1 for 'any port' */
int32 hour_mask; /* Bitmask: at which times is this rule valid? */
int32 wday_mask; /* Bitmask: at which days is this rule valid? */
struct access_class *class; /* The corresponding class */
struct access_address *next;
} *all_access_addresses = NULL;
/* Class descriptions, kept in a linked list.
* The structure is allocated big enough to keep the full message, the
* 8 characters listed in the definition are just a placeholder.
*/
static struct access_class {
long id; /* Class ID */
mp_int max_usage, usage; /* Max and current number of users */
struct access_class *next;
char message[8]; /* Placeholder for the message text buffer */
} *all_access_classes = NULL;
static time_t last_read_time = 0;
/*-------------------------------------------------------------------------*/
static struct access_class *
find_access_class (struct sockaddr_in *full_addr, int port)
/* Find and return the class structure for the given IP <full_addr> at
* the current time. Return NULL if no rule covers the IP at this time.
*/
{
uint32 addr;
struct access_address *aap;
time_t seconds;
struct tm *tm_p;
#ifdef DEBUG_ACCESS_CHECK
fprintf(stderr, "find_class for '%s':%d\n"
, inet_ntoa(*(struct in_addr*)&full_addr->sin_addr)
, port);
#endif
#ifndef USE_IPV6
addr = full_addr->sin_addr.s_addr;
#else
addr = (uint32) full_addr->sin_addr.s_addr;
/* TODO: DANGER: The above cast might break under IPv6 */
#endif
tm_p = NULL;
for (aap = all_access_addresses; aap; aap = aap->next) {
#ifdef DEBUG_ACCESS_CHECK
fprintf(stderr, " '%s':%ld, %ld %ld\n",
inet_ntoa(*(struct in_addr*)&aap->addr),
aap->port,
(long)aap->class->max_usage, (long)aap->class->usage);
#endif
if (aap->port >= 0 && aap->port != port)
continue;
if ((aap->addr ^ addr) & aap->mask)
continue;
if (aap->wday_mask >= 0) {
if (!tm_p) {
time(&seconds);
tm_p = localtime(&seconds);
#ifdef DEBUG_ACCESS_CHECK
fprintf(stderr, " h:%d w:%d\n", tm_p->tm_hour, tm_p->tm_wday);
#endif
}
if ( !((1 << tm_p->tm_hour) & aap->hour_mask) )
continue;
if ( !((1 << tm_p->tm_wday) & aap->wday_mask) )
continue;
}
#ifdef DEBUG_ACCESS_CHECK
fprintf(stderr, " found\n");
#endif
return aap->class;
}
#ifdef DEBUG_ACCESS_CHECK
fprintf(stderr, " not found\n");
#endif
return NULL;
}
/*-------------------------------------------------------------------------*/
static void
add_access_entry (struct sockaddr_in *full_addr, int login_port, long *idp)
/* Find the class structure for <full_addr> and increments its count
* of users. The id of the class is put into *idp.
* If there is no class, *idp is not changed.
*
* This function is called after the ACCESS_FILE has been (re-)read
* to reinitialize the current class usage counts.
*/
{
struct access_class *acp;
acp = find_access_class(full_addr, login_port);
if (acp) {
acp->usage++;
*idp = acp->id;
}
}
/*-------------------------------------------------------------------------*/
static void
read_access_file (void)
/* Read and parse the ACCESS_FILE, (re)creating all the datastructures.
* After the file has been read, the usage counts will be updated by
* calling comm::refresh_access_data, passing add_access_entry() as
* callback.
*/
{
FILE *infp;
struct access_address *aap, *next_aap, **last;
struct access_class *acp, *next_acp;
char message[MAX_MESSAGE_LENGTH];
int i;
int32 addr, mask;
/* Free the old datastructures */
for (aap = all_access_addresses; aap; aap = next_aap) {
next_aap = aap->next;
pfree((char *)aap);
}
for (acp = all_access_classes; acp; acp = next_acp) {
next_acp = acp->next;
pfree((char *)acp);
}
all_access_classes = NULL;
infp = fopen(ACCESS_FILE, "r");
if (!infp)
perror("driver: Can't read access file '" ACCESS_FILE "'");
FCOUNT_READ(ACCESS_FILE);
last = &all_access_addresses;
/* *last will be set to NULL at the end, so no dangling pointer
* will exist.
*/
/* The parse loop; it will terminate on file end or an error.
* Every rule is parsed in four iterations: the first three read
* the first bytes of the IP address, the fourth and last one
* everything else.
* If an address not specified in exactly 4 bytes, you will get
* nonsense, but no error message.
*/
if (infp) for(addr = mask = 0;;) {
long max_usage, class_id, port;
int first_hour, last_hour, m;
/* Parse the next IP address byte, i.e. everything up to the
* next . or : .
*/
addr <<= 8;
mask <<= 8;
m = fscanf(infp, "%9[^.:\n]%[.:]", message, message+12);
if (m != 2 || *message == '#')
{
do {
i = fgetc(infp);
if (i == EOF)
goto file_end;
} while(i != '\n');
addr = mask = 0;
continue;
}
if (*message != '*') {
int j;
j = atoi(message);
if ((unsigned int)j > 0xff)
break;
addr += j;
mask += 0xff;
}
if (message[12] == '.') /* Then another byte follows */
continue;
/* The ip-number may be followed with a port specification */
i = fscanf(infp, "p%ld:", &port);
if (!i)
port = -1;
/* Parse the time specs next. Start by trying the first (old)
* format.
*/
max_usage = 0;
message[0] = '\0';
i = fscanf(infp, "%ld:%ld:%d:%d:",
&class_id, &max_usage, &first_hour, &last_hour);
if (!i)
break;
aap = pxalloc(sizeof *aap);
if (!aap)
break;
*last = aap;
aap->addr = htonl(addr);
aap->mask = htonl(mask);
aap->port = port;
aap->wday_mask = -1; /* Default: valid on every day */
if (i == 4) { /* Old format */
if (first_hour || last_hour) {
aap->wday_mask = 0x7f;
if (first_hour <= last_hour) {
aap->hour_mask = (2 << last_hour) - (1 << first_hour);
} else {
aap->hour_mask = -(1 << first_hour) + (2 << last_hour) - 1;
}
}
} else if (i == 2) { /* New format */
char c, c2[2];
int32 *maskp;
for (;;) {
c = 'm';
fscanf(infp, "%c %1[=]", &c, c2);
switch(c)
{
case 'w':
maskp = &aap->wday_mask;
goto get_mask;
case 'h':
maskp = &aap->hour_mask;
get_mask:
mask = 0;
do {
int j, k;
*c2 = '\0';
if (!fscanf(infp, "%d %1[-,:] ", &j, c2))
break;
if (*c2 == '-') {
k = 24;
fscanf(infp, "%d %1[,:] ", &k, c2);
if (j <= k) {
mask |= (2 << k) - (1 << j);
} else {
mask |= -(1 << j) + (2 << k) - 1;
}
} else {
mask |= 1 << j;
}
} while (*c2 == ',');
*maskp = mask;
aap->wday_mask &= 0x7f; /* make sure it's not negative */
continue;
default:
ungetc(c, infp);
/* FALLTHROUGH */
case 'm':
break;
} /* switch */
break;
} /* for */
} /* if (i) */
/* The rest of the line is the message to print.
*/
fgets(message, (int)sizeof(message)-1, infp);
message[sizeof(message) - 1] = '\0';
/* Check if this rule creates a new class. If yes, allocate
* a new structure and assign message text and usage to it.
*/
for (acp = all_access_classes; acp; acp = acp->next) {
if (acp->id == class_id)
break;
}
if (!acp) {
size_t len;
len = strlen(message);
if (len && message[len-1] == '\n')
message[--len] = '\0';
acp = pxalloc(sizeof *acp - sizeof acp->message + 1 + len);
if (!acp) {
pfree((char *)aap);
break;
}
acp->id = class_id;
acp->max_usage = max_usage == -1 ? (mp_int)MAXINT : max_usage;
acp->usage = 0;
strcpy(acp->message, message);
acp->next = all_access_classes;
all_access_classes = acp;
}
/* Finishing touches. */
aap->class = acp;
last = &aap->next;
addr = mask = 0;
}
file_end: /* emergency exit from the loop */
/* Terminate the address list properly, then refresh the data */
*last = NULL;
refresh_access_data(add_access_entry);
}
/*-------------------------------------------------------------------------*/
char *
allow_host_access (struct sockaddr_in *full_addr, int login_port, long *idp)
/* Check if the IP address <full_addr> is allowed access at the current
* time. Return NULL if access is granted, else an error message.
*
* If access is allowed, *idp is set to the corresponding class id; this
* information is used later to re-initialize the class structures after
* a re-read of the ACCESS_FILE.
*
* The ACCESS_FILE is read by this function if it has been changed.
*/
{
struct stat statbuf;
struct access_class *acp;
if (ixstat(ACCESS_FILE, &statbuf))
perror("driver: Can't stat access file '" ACCESS_FILE "'");
else if (statbuf.st_mtime > last_read_time) {
last_read_time = statbuf.st_mtime;
read_access_file();
}
acp = find_access_class(full_addr, login_port);
if (acp) {
if (acp->usage >= acp->max_usage)
return acp->message;
acp->usage++;
*idp = acp->id;
return NULL;
}
return "No matching entry";
}
/*-------------------------------------------------------------------------*/
void
release_host_access (long num)
/* One user from the class <num> logged out, update the class structure
* accordingly.
*/
{
struct access_class *acp;
#ifdef DEBUG_ACCESS_CHECK
fprintf(stderr, "release_host_access %ld called.\n", num);
#endif
for (acp = all_access_classes; acp; acp = acp->next) {
if (acp->id != num)
continue;
acp->usage--;
break;
}
}
#endif /* ACCESS_CONTROL */
/***************************************************************************/