/* Copyright (C) Doom 2D: Forever Developers
*
* 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, version 3 of the License ONLY.
*
* 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, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MS_VERSION "0.3"
#define MS_MAX_SERVERS 128
#define MS_MAX_CLIENTS (MS_MAX_SERVERS + 1)
#define MS_URGENT_FILE "urgent.txt"
#define MS_MOTD_FILE "motd.txt"
#define MS_BAN_FILE "master_bans.txt"
#define MS_PIPE_FILE "d2df_master.pipe"
#define DEFAULT_SPAM_CAP 10
#define DEFAULT_MAX_SERVERS MS_MAX_SERVERS
#define DEFAULT_MAX_PER_HOST 4
#define DEFAULT_SERVER_TIMEOUT 100
#define DEFAULT_CLIENT_TIMEOUT 3
#define DEFAULT_SPAM_TIMEOUT 1
#define DEFAULT_PORT 25665
#define NET_BUFSIZE 65536
#define NET_FULLMASK 0xFFFFFFFF
#define SV_PROTO_MIN 140
#define SV_PROTO_MAX 210
#define SV_NAME_MAX 64
#define SV_MAP_MAX 64
#define SV_MAX_PLAYERS 24
#define SV_MAX_GAMEMODE 5
#define SV_NEW_SERVER_INTERVAL 3
#define MAX_STRLEN 0xFF
enum log_severity_e {
LOG_NOTE,
LOG_WARN,
LOG_ERROR
};
enum net_ch_e {
NET_CH_MAIN,
NET_CH_UPD,
NET_CH_COUNT
};
enum net_msg_e {
NET_MSG_ADD = 200,
NET_MSG_RM = 201,
NET_MSG_LIST = 202
};
enum sv_flags_e {
SV_FL_PASSWORD = 1 << 0,
SV_FL_VERIFIED = 1 << 1,
SV_FL_MAX = SV_FL_PASSWORD | SV_FL_VERIFIED,
};
typedef struct enet_buf_s {
enet_uint8 *data;
size_t size;
size_t pos;
int overflow;
} enet_buf_t;
typedef struct ban_record_s {
enet_uint32 host;
enet_uint32 mask;
int ban_count;
time_t cur_ban;
struct ban_record_s *next;
struct ban_record_s *prev;
} ban_record_t;
typedef struct server_s {
enet_uint32 host; // BE; 0 means this slot is unused
enet_uint16 port; // LE, which is what the game and enet both expect
enet_uint8 flags;
enet_uint8 proto;
enet_uint8 gamemode;
enet_uint8 players;
enet_uint8 maxplayers;
char name[MAX_STRLEN + 2];
char map[MAX_STRLEN + 2];
time_t death_time;
time_t timestamp;
ENetPeer *peer; // who sent this server in
} server_t;
// real servers
static server_t servers[MS_MAX_SERVERS];
static int max_servers = DEFAULT_MAX_SERVERS;
static int max_servers_per_host = DEFAULT_MAX_PER_HOST;
static int num_servers = 0;
// fake servers to show on old versions of the game
static const server_t fake_servers[] = {
{
.name = "! \xc2\xc0\xd8\xc0 \xca\xce\xcf\xc8\xdf \xc8\xc3\xd0\xdb "
"\xd3\xd1\xd2\xc0\xd0\xc5\xcb\xc0! "
"\xd1\xca\xc0\xd7\xc0\xc9\xd2\xc5 \xcd\xce\xc2\xd3\xde C "
"doom2d.org !",
.map = "! Your game is outdated. "
"Get latest version at doom2d.org !",
.proto = 255,
},
{
.name = "! \xcf\xd0\xce\xc1\xd0\xce\xd1\xdcTE \xcf\xce\xd0\xd2\xdb "
"25666 \xc8 57133 HA CEPBEPE \xcf\xc5\xd0\xc5\xc4 \xc8\xc3\xd0\xce\xc9 !",
.map = "! Forward ports 25666 and 57133 before hosting !",
.proto = 255,
},
};
static const int num_fake_servers = sizeof(fake_servers) / sizeof(*fake_servers);
// ban list
static ban_record_t *banlist;
// settings
static int ms_port = DEFAULT_PORT;
static int ms_sv_timeout = DEFAULT_SERVER_TIMEOUT;
static int ms_cl_timeout = DEFAULT_CLIENT_TIMEOUT;
static int ms_spam_timeout = DEFAULT_SPAM_TIMEOUT;
static int ms_spam_cap = DEFAULT_SPAM_CAP;
static char ms_motd[MAX_STRLEN + 1] = "";
static char ms_urgent[MAX_STRLEN + 1] = "";
static ENetHost *ms_host;
// network buffers
static enet_uint8 buf_send_data[NET_BUFSIZE];
static enet_buf_t buf_send = { .data = buf_send_data, .size = sizeof(buf_send_data) };
static enet_buf_t buf_recv; // rx data supplied by enet packets
// stupid client spam filter
static enet_uint32 cl_last_addr;
static time_t cl_last_time;
static int cl_spam_cnt;
/* common utility functions */
static char *u_strstrip(char *p) {
if (!p) return p;
while (isspace(*p)) ++p;
const size_t len = strlen(p);
if (len) {
for (size_t i = len - 1; i && isspace(p[i]); --i)
p[i] = '\0';
}
return p;
}
static char *u_vabuf(void) {
static char vabuf[4][MAX_STRLEN];
static int idx = 0;
char *ret = vabuf[idx++];
if (idx >= 4) idx = 0;
return ret;
}
static const char *u_strtime(const time_t t) {
char *buf = u_vabuf();
struct tm *ptm = localtime(&t);
strftime(buf, MAX_STRLEN - 1, "%d/%m/%y %H:%M:%S", ptm);
return buf;
}
static inline const char *u_logprefix(const enum log_severity_e s) {
switch (s) {
case LOG_WARN: return "WARNING: ";
case LOG_ERROR: return "ERROR: ";
default: return "";
}
}
static void u_log(const enum log_severity_e severity, const char *fmt, ...) {
printf("[%s] %s", u_strtime(time(NULL)), u_logprefix(severity));
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
printf("\n");
}
static void __attribute__((noreturn)) u_fatal(const char *fmt, ...) {
fprintf(stderr, "[%s] FATAL ERROR:\n", u_strtime(time(NULL)));
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fprintf(stderr, "\n");
fflush(stderr);
exit(1);
}
static bool u_strisprint(const char *str) {
if (!str || !*str)
return false;
for (const char *p = str; *p; ++p) {
// only stuff before space, DEL, NBSP and SHY are considered garbage since we're on 1251
if (*p < 0x20 || *p == 0x7F || *p == 0xA0 || *p == 0xAD)
return false;
}
return true;
}
static bool u_strisver(const char *str) {
if (!str || !*str)
return false;
for (const char *p = str; *p; ++p) {
// version strings consist of 0-9 . and space
if (!isdigit(*p) && *p != '.' && *p != ' ')
return false;
}
return true;
}
static const char *u_iptostr(const enet_uint32 host) {
ENetAddress addr = { .host = host, .port = 0 };
char *buf = u_vabuf();
enet_address_get_host_ip(&addr, buf, MAX_STRLEN - 1);
return buf;
}
static bool u_readtextfile(const char *fname, char *buf, size_t max) {
FILE *f = fopen(fname, "r");
char *const end = buf + max - 1;
char *p = buf;
if (f) {
char ln[max];
char *const lend = ln + max - 1;
while (p < end && fgets(ln, max, f)) {
for (char *n = ln; n < lend && *n && *n != '\r' && *n != '\n'; ++n) {
*(p++) = *n;
if (p == end) break;
}
}
*p = '\0';
fclose(f);
return true;
}
return false;
}
static inline enet_uint32 u_prefixtomask(const enet_uint32 prefix) {
return ENET_HOST_TO_NET_32((0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF);
}
static inline enet_uint32 u_masktoprefix(const enet_uint32 mask) {
return (32 - __builtin_ctz(ENET_NET_TO_HOST_32(mask)));
}
static inline void u_printsv(const server_t *sv) {
printf("* addr: %s:%d\n", u_iptostr(sv->host), sv->port);
printf("* name: %s\n", sv->name);
printf("* map: %s (mode %d)\n", sv->map, sv->gamemode);
printf("* plrs: %d/%d\n", sv->players, sv->maxplayers);
printf("* flag: %04x\n", sv->flags);
}
/* buffer utility functions */
static inline int b_enough_left(enet_buf_t *buf, size_t size) {
if (buf->pos + size > buf->size) {
buf->overflow = 1;
return 0;
}
return 1;
}
static enet_uint8 b_read_uint8(enet_buf_t *buf) {
if (b_enough_left(buf, 1))
return buf->data[buf->pos++];
return 0;
}
static enet_uint16 b_read_uint16(enet_buf_t *buf) {
enet_uint16 ret = 0;
if (b_enough_left(buf, sizeof(ret))) {
ret = *(enet_uint16*)(buf->data + buf->pos);
buf->pos += sizeof(ret);
}
return ret;
}
static char *b_read_dstring(enet_buf_t *buf) {
char *ret = NULL;
if (b_enough_left(buf, 1)) {
const size_t len = b_read_uint8(buf);
if (b_enough_left(buf, len)) {
ret = malloc(len + 1);
memmove(ret, (char*)(buf->data + buf->pos), len);
buf->pos += len;
ret[len] = '\0';
}
}
return ret;
}
static char *b_read_dstring_to(enet_buf_t *buf, char *out, size_t out_size) {
if (b_enough_left(buf, 1)) {
const size_t len = b_read_uint8(buf);
if (b_enough_left(buf, len)) {
if (len < out_size) {
memmove(out, (char*)(buf->data + buf->pos), len);
out[len] = '\0';
} else if (out_size) {
out[0] = '\0';
}
buf->pos += len;
return out;
}
}
return NULL;
}
static void b_write_uint8(enet_buf_t *buf, enet_uint8 val) {
buf->data[buf->pos++] = val;
}
static void b_write_uint16(enet_buf_t *buf, enet_uint16 val) {
*(enet_uint16*)(buf->data + buf->pos) = val;
buf->pos += sizeof(val);
}
static void b_write_dstring(enet_buf_t *buf, const char* val) {
enet_uint8 len = strlen(val);
b_write_uint8(buf, len);
memmove((char*)(buf->data + buf->pos), val, len);
buf->pos += len;
}
void b_write_server(enet_buf_t *buf, const server_t *s) {
b_write_dstring(buf, u_iptostr(s->host));
b_write_uint16 (buf, s->port);
b_write_dstring(buf, s->name);
b_write_dstring(buf, s->map);
b_write_uint8 (buf, s->gamemode);
b_write_uint8 (buf, s->players);
b_write_uint8 (buf, s->maxplayers);
b_write_uint8 (buf, s->proto);
b_write_uint8 (buf, (s->flags & SV_FL_PASSWORD));
}
/* server functions */
static inline void sv_remove(server_t *sv) {
if (sv->host) {
// drop the associated peer, if any
if (sv->peer && sv->peer->state == ENET_PEER_STATE_CONNECTED && sv->peer->data == sv) {
sv->peer->data = NULL;
enet_peer_reset(sv->peer);
}
sv->host = 0;
sv->port = 0;
sv->peer = NULL;
--num_servers;
}
}
static void sv_remove_by_addr(const enet_uint32 host, const enet_uint16 port) {
for (int i = 0; i < max_servers; ++i) {
if (servers[i].host == host && servers[i].port == port)
sv_remove(servers + i);
}
}
static void sv_remove_by_host(enet_uint32 host, enet_uint32 mask) {
host &= mask;
for (int i = 0; i < max_servers; ++i) {
if (servers[i].host && (servers[i].host & mask) == host)
sv_remove(servers + i);
}
}
static int sv_count_by_host(enet_uint32 host, enet_uint32 mask) {
host &= mask;
int count = 0;
for (int i = 0; i < max_servers; ++i) {
if (servers[i].host && (servers[i].host & mask) == host)
++count;
}
return count;
}
static time_t sv_last_timestamp_for_host(enet_uint32 host, enet_uint32 mask) {
host &= mask;
time_t last = 0;
for (int i = 0; i < max_servers; ++i) {
if (servers[i].host && (servers[i].host & mask) == host) {
if (servers[i].timestamp > last)
last = servers[i].timestamp;
}
}
return last;
}
static inline server_t *sv_find_or_add(const enet_uint32 host, const enet_uint32 port) {
server_t *empty = NULL;
for (int i = 0; i < max_servers; ++i) {
server_t *s = servers + i;
if (s->host == host && s->port == port)
return s; // this server already exists
if (!s->host && !empty)
empty = s; // remember the first empty slot in case it's needed later
}
return empty;
}
static inline void sv_clear_peer(ENetPeer *peer) {
server_t *sv = peer->data;
if (sv) {
sv->peer = NULL;
peer->data = NULL;
}
}
/* ban list functions */
static inline time_t ban_get_time(const int cnt) {
static const time_t times[] = {
1 * 5 * 60,
1 * 30 * 60,
1 * 60 * 60,
24 * 60 * 60,
72 * 60 * 60,
720 * 60 * 60,
8760 * 60 * 60,
};
static const size_t numtimes = sizeof(times) / sizeof(*times);
if (cnt >= numtimes || cnt < 0)
return times[numtimes - 1];
return times[cnt];
}
static ban_record_t *ban_check(const enet_uint32 host) {
const time_t now = time(NULL);
for (ban_record_t *b = banlist; b; b = b->next) {
if ((b->host & b->mask) == (host & b->mask)) {
if (b->cur_ban > now)
return b;
}
}
return NULL;
}
static inline ban_record_t *ban_record_check(const enet_uint32 host) {
for (ban_record_t *b = banlist; b; b = b->next) {
if ((b->host & b->mask) == (host & b->mask))
return b;
}
return NULL;
}
static ban_record_t *ban_record_add_addr(const enet_uint32 host, const enet_uint32 mask, const int cnt, const time_t cur) {
ban_record_t *rec = ban_record_check(host);
if (rec) return rec;
rec = calloc(1, sizeof(*rec));
if (!rec) return NULL;
rec->host = host & mask;
rec->mask = mask;
if (rec->mask == 0) rec->mask = NET_FULLMASK;
rec->ban_count = cnt;
rec->cur_ban = cur;
if (banlist) banlist->prev = rec;
rec->next = banlist;
banlist = rec;
return rec;
}
static enet_uint32 ban_parse_ip_mask(const char *ip, enet_uint32 *out_mask) {
enet_uint32 prefix = 32;
// find and get the prefix length, if any
char ip_copy[24] = { 0 };
strncpy(ip_copy, ip, sizeof(ip_copy) - 1);
char *slash = strrchr(ip_copy, '/');
if (slash) {
*slash++ = '\0'; // strip the prefix length off
if (*slash) prefix = atoi(slash);
}
ENetAddress addr = { 0 };
if (enet_address_set_host_ip(&addr, ip_copy) != 0)
return 0;
// transform prefix length into mask
*out_mask = u_prefixtomask(prefix);
return addr.host;
}
static ban_record_t *ban_record_add_ip(const char *ip, const int cnt, const time_t cur) {
enet_uint32 mask = 0;
const enet_uint32 host = ban_parse_ip_mask(ip, &mask);
if (!host) {
u_log(LOG_ERROR, "banlist: `%s` is not a valid address", ip);
return NULL;
}
return ban_record_add_addr(host, mask, cnt, cur);
}
static void ban_free_list(void) {
ban_record_t *rec = banlist;
while (rec) {
ban_record_t *next = rec->next;
free(rec);
rec = next;
}
banlist = NULL;
}
static bool ban_record_remove_addr(const enet_uint32 host, const enet_uint32 mask) {
for (ban_record_t *b = banlist; b; b = b->next) {
if ((b->host == host) && (b->mask == mask)) {
if (b == banlist)
banlist = b->next;
if (b->next) b->next->prev = b->prev;
if (b->prev) b->prev->next = b->next;
free(b);
return true;
}
}
return false;
}
static bool ban_record_remove_ip(const char *ip) {
enet_uint32 mask = 0;
const enet_uint32 host = ban_parse_ip_mask(ip, &mask);
if (!host) {
u_log(LOG_ERROR, "unban: `%s` is not a valid address", ip);
return NULL;
}
return ban_record_remove_addr(host, mask);
}
static void ban_load_list(const char *fname) {
FILE *f = fopen(fname, "r");
if (!f) {
u_log(LOG_WARN, "banlist: could not open %s for reading", fname);
return;
}
char ln[MAX_STRLEN] = { 0 };
while (fgets(ln, sizeof(ln), f)) {
for (int i = sizeof(ln) - 1; i >= 0; --i)
if (ln[i] == '\n' || ln[i] == '\r')
ln[i] = 0;
if (ln[0] == 0)
continue;
char ip[21] = { 0 }; // optionally includes the "/nn" prefix length at the end
int expd = 0;
int count = 0;
if (sscanf(ln, "%20s %d %d", ip, &expd, &count) < 3) {
u_log(LOG_ERROR, "banlist: malformed line: `%s`", ln);
continue;
}
const time_t exp = (time_t)expd; // shut up gcc
if (ban_record_add_ip(ip, count, exp))
u_log(LOG_NOTE, "banlist: banned %s until %s (ban level %d)", ip, u_strtime(exp), count);
}
fclose(f);
}
static void ban_save_list(const char *fname) {
FILE *f = fopen(fname, "w");
if (!f) {
u_log(LOG_ERROR, "banlist: could not open %s for writing", fname);
return;
}
for (ban_record_t *rec = banlist; rec; rec = rec->next) {
if (rec->ban_count)
fprintf(f, "%s/%u %d %d\n", u_iptostr(rec->host), u_masktoprefix(rec->mask), (int)rec->cur_ban, rec->ban_count);
}
fclose(f);
}
static bool ban_sanity_check(const server_t *srv) {
// can't have more than 24 maxplayers; can't have more than max
if (srv->players > srv->maxplayers || srv->maxplayers > SV_MAX_PLAYERS || srv->maxplayers == 0)
return false;
// name and map have to be non-garbage
if (!u_strisprint(srv->map) || !u_strisprint(srv->name))
return false;
// these protocols don't exist
if (srv->proto < SV_PROTO_MIN || srv->proto > SV_PROTO_MAX)
return false;
// the game doesn't allow server names longer than 64 chars
if (strlen(srv->name) > SV_NAME_MAX)
return false;
// game mode has to actually exist
if (srv->gamemode > SV_MAX_GAMEMODE)
return false;
// flags field can't be higher than the sum of all the flags
if (srv->flags > SV_FL_MAX)
return false;
return true;
}
static void ban_add_mask(const enet_uint32 host, const enet_uint32 mask, const char *reason) {
const time_t now = time(NULL);
ban_record_t *rec = ban_record_add_addr(host, mask, 0, 0);
if (!rec) u_fatal("OOM trying to ban %s", u_iptostr(host));
rec->cur_ban = now + ban_get_time(rec->ban_count);
rec->ban_count++;
u_log(LOG_NOTE, "banned %s until %s, reason: %s, ban level: %d", u_iptostr(rec->host), u_strtime(rec->cur_ban), reason, rec->ban_count);
ban_save_list(MS_BAN_FILE);
sv_remove_by_host(host, mask);
if (host == cl_last_addr)
cl_last_addr = 0;
}
static void ban_add(const enet_uint32 host, const char *reason) {
const time_t now = time(NULL);
ban_record_t *rec = ban_record_add_addr(host, NET_FULLMASK, 0, 0);
if (!rec) u_fatal("OOM trying to ban %s", u_iptostr(host));
rec->cur_ban = now + ban_get_time(rec->ban_count);
rec->ban_count++;
u_log(LOG_NOTE, "banned %s until %s, reason: %s, ban level: %d", u_iptostr(rec->host), u_strtime(rec->cur_ban), reason, rec->ban_count);
ban_save_list(MS_BAN_FILE);
sv_remove_by_host(host, NET_FULLMASK);
if (host == cl_last_addr)
cl_last_addr = 0;
}
static void ban_remove_mask(const enet_uint32 host, const enet_uint32 mask) {
if (!ban_record_remove_addr(host, mask)) {
u_log(LOG_ERROR, "could not find %s in ban list", u_iptostr(host));
return;
}
u_log(LOG_NOTE, "unbanned %s", u_iptostr(host));
ban_save_list(MS_BAN_FILE);
}
static inline void ban_peer(ENetPeer *peer, const char *reason) {
if (peer) {
ban_add(peer->address.host, reason);
sv_clear_peer(peer);
enet_peer_reset(peer);
}
}
/* main */
#if ENABLE_PIPE
static int io_fd = -1;
static bool io_install_pipe(void) {
const int rc = mkfifo(MS_PIPE_FILE, 0664);
if (rc < 0 && errno != EEXIST) {
u_log(LOG_ERROR, "io_install_pipe(): mkfifo(): %s", strerror(errno));
return false;
}
io_fd = open(MS_PIPE_FILE, O_RDONLY | O_NONBLOCK);
if (io_fd < 0) {
u_log(LOG_ERROR, "io_install_pipe(): open(): %s", strerror(errno));
remove(MS_PIPE_FILE);
return false;
}
return true;
}
static void io_uninstall_pipe(void) {
if (io_fd >= 0) {
close(io_fd);
io_fd = -1;
}
remove(MS_PIPE_FILE);
}
static void io_read_commands(void) {
if (io_fd < 0)
return;
char cmd[128];
const int cmd_len = read(io_fd, cmd, sizeof(cmd) - 1);
if (cmd_len < 1)
return;
cmd[cmd_len] = '\0';
if (!strncmp(cmd, "ban ", 4)) {
const char *ip = u_strstrip(cmd + 4); // skip "ban "
enet_uint32 mask = 0;
enet_uint32 host = ban_parse_ip_mask(ip, &mask);
if (!host) {
u_log(LOG_ERROR, "ban: `%s` is not a valid address", ip);
return;
}
ban_add_mask(host, mask, "banned by console");
} else if (!strncmp(cmd, "unban ", 6)) {
const char *ip = u_strstrip(cmd + 6); // skip "unban "
enet_uint32 mask = 0;
enet_uint32 host = ban_parse_ip_mask(ip, &mask);
if (!host) {
u_log(LOG_ERROR, "ban: `%s` is not a valid address", ip);
return;
}
ban_remove_mask(host, mask);
} else if (!strncmp(cmd, "reload", 6)) {
u_log(LOG_WARN, "reloading banlist");
ban_free_list();
ban_load_list(MS_BAN_FILE);
} else if (!strncmp(cmd, "die", 3)) {
u_log(LOG_WARN, "shutting down");
exit(0);
}
}
#endif
static void deinit(void) {
// ban_save_list(MS_BAN_FILE);
ban_free_list();
if (ms_host) {
enet_host_destroy(ms_host);
ms_host = NULL;
}
enet_deinitialize();
#ifdef ENABLE_PIPE
io_uninstall_pipe();
#endif
}
static bool handle_msg(const enet_uint8 msgid, ENetPeer *peer) {
server_t *sv = NULL;
server_t tmpsv = { 0 };
char clientver[MAX_STRLEN] = { 0 };
const time_t now = time(NULL);
switch (msgid) {
case NET_MSG_ADD:
tmpsv.port = b_read_uint16(&buf_recv);
b_read_dstring_to(&buf_recv, tmpsv.name, sizeof(tmpsv.name));
b_read_dstring_to(&buf_recv, tmpsv.map, sizeof(tmpsv.map));
tmpsv.gamemode = b_read_uint8(&buf_recv);
tmpsv.players = b_read_uint8(&buf_recv);
tmpsv.maxplayers = b_read_uint8(&buf_recv);
tmpsv.proto = b_read_uint8(&buf_recv);
tmpsv.flags = b_read_uint8(&buf_recv);
if (buf_recv.overflow) {
ban_peer(peer, "malformed MSG_ADD");
return true;
}
sv = sv_find_or_add(peer->address.host, tmpsv.port);
if (!sv) {
u_log(LOG_ERROR, "ran out of server slots trying to add %s:%d", u_iptostr(peer->address.host), tmpsv.port);
return true;
}
if (sv->host == peer->address.host) {
// old server; update it
memcpy(sv->map, tmpsv.map, sizeof(sv->map));
memcpy(sv->name, tmpsv.name, sizeof(sv->name));
sv->players = tmpsv.players;
sv->maxplayers = tmpsv.maxplayers;
sv->flags = tmpsv.flags;
sv->gamemode = tmpsv.gamemode;
// first check if the new values are garbage
if (!ban_sanity_check(sv)) {
ban_peer(peer, "tripped sanity check");
return true;
}
// only then update the times
sv->death_time = now + ms_sv_timeout;
sv->timestamp = now;
// check if we're updating from a new peer
if (sv->peer != peer) {
// if there was an old one, kill it
if (sv->peer) {
sv->peer->data = NULL;
enet_peer_reset(sv->peer);
}
sv->peer = peer;
peer->data = sv;
}
u_log(LOG_NOTE, "updated server #%d:", sv - servers);
u_printsv(sv);
} else {
// new server; first check if this host is creating too many servers in the list
if (max_servers_per_host) {
const int count = sv_count_by_host(peer->address.host, NET_FULLMASK);
if (count >= max_servers_per_host) {
ban_peer(peer, "too many servers in list");
return true;
}
/*
// FIXME: commented out as this might trip when the master restarts
if (count > 0) {
// check if this is too soon to create a new server
const time_t delta = now - sv_last_timestamp_for_host(peer->address.host, NET_FULLMASK);
if (delta < count * SV_NEW_SERVER_INTERVAL) {
ban_peer(peer, "creating servers too fast");
return true;
}
}
*/
}
// then add that shit
*sv = tmpsv;
sv->host = peer->address.host;
sv->death_time = now + ms_sv_timeout;
sv->timestamp = now;
if (!ban_sanity_check(sv)) {
sv->host = 0;
sv->port = 0;
ban_peer(peer, "tripped sanity check");
return true;
}
sv->peer = peer;
peer->data = sv;
++num_servers;
u_log(LOG_NOTE, "added new server #%d:", sv - servers);
u_printsv(sv);
}
return true;
case NET_MSG_RM:
tmpsv.port = b_read_uint16(&buf_recv);
if (buf_recv.overflow) {
ban_peer(peer, "malformed MSG_RM");
return true;
}
sv_remove_by_addr(peer->address.host, tmpsv.port);
// this peer can be disconnected pretty much immediately since he has no servers left, tell him to fuck off
sv_clear_peer(peer);
enet_peer_disconnect_later(peer, 0);
return true;
case NET_MSG_LIST:
buf_send.pos = 0;
buf_send.overflow = 0;
b_write_uint8(&buf_send, NET_MSG_LIST);
clientver[0] = 0;
if (buf_recv.size > 2) {
// holy shit a fresh client
b_read_dstring_to(&buf_recv, clientver, sizeof(clientver));
b_write_uint8(&buf_send, num_servers);
} else {
// old client; feed him fake servers first
b_write_uint8(&buf_send, num_servers + num_fake_servers);
for (int i = 0; i < num_fake_servers; ++i)
b_write_server(&buf_send, &fake_servers[i]);
}
if (buf_recv.overflow) {
ban_peer(peer, "malformed MSG_LIST");
return true;
}
if (clientver[0] && !u_strisver(clientver)) {
ban_peer(peer, "malformed MSG_LIST clientver");
return true;
}
for (int i = 0; i < max_servers; ++i) {
if (servers[i].host)
b_write_server(&buf_send, servers + i);
}
if (clientver[0]) {
// TODO: check if this client is outdated (?) and send back new verstring
// for now just write the same shit back
b_write_dstring(&buf_send, clientver);
// write the motd and urgent message
b_write_dstring(&buf_send, ms_motd);
b_write_dstring(&buf_send, ms_urgent);
}
ENetPacket *p = enet_packet_create(buf_send.data, buf_send.pos, ENET_PACKET_FLAG_RELIABLE);
enet_peer_send(peer, NET_CH_MAIN, p);
// enet_host_flush(ms_host);
// this peer can be disconnected pretty much immediately after receiving the server list, tell him to fuck off
sv_clear_peer(peer);
enet_peer_disconnect_later(peer, 0);
u_log(LOG_NOTE, "sent server list to %s:%d (ver %s)", u_iptostr(peer->address.host), peer->address.port, clientver[0] ? clientver : "");
return true;
default:
break;
}
return false;
}
static void print_usage(void) {
printf("Usage: d2df_master [OPTIONS...]\n");
printf("Available options:\n");
printf("-h show this message and exit\n");
printf("-p N listen on port N (default: %d)\n", DEFAULT_PORT);
printf("-t N seconds before server is removed from list (default: %d)\n", DEFAULT_SERVER_TIMEOUT);
printf("-c N how long a client is allowed to hold the connection active (default: %d)\n", DEFAULT_CLIENT_TIMEOUT);
printf("-s N max number of servers in server list, 1-%d (default: %d)\n", MS_MAX_SERVERS, DEFAULT_MAX_SERVERS);
printf("-d N if N > 0, disallow more than N servers on the same IP (default: %d)\n", DEFAULT_MAX_PER_HOST);
printf("-f N crappy spam filter: ban clients after they send N requests in a row too fast (default: %d)\n", DEFAULT_SPAM_CAP);
printf("-w N how often does a client have to send packets for the filter to kick in, i.e. once every N sec (default: %d)\n", DEFAULT_SPAM_TIMEOUT);
fflush(stdout);
}
static inline bool parse_int_arg(int argc, char **argv, const int i, const char *name, int vmin, int vmax, int *outval) {
if (strcmp(name, argv[i]))
return false;
if (i >= argc - 1) {
fprintf(stderr, "expected integer value after %s\n", name);
return false;
}
const int v = atoi(argv[i + 1]);
if (v < vmin || v > vmax) {
fprintf(stderr, "expected integer value in range %d - %d\n", vmin, vmax);
return false;
}
*outval = v;
return true;
}
static bool parse_args(int argc, char **argv) {
if (argc < 2)
return true;
if (!strcmp(argv[1], "-h")) {
print_usage();
return false;
}
for (int i = 1; i < argc; ++i) {
const bool success =
parse_int_arg(argc, argv, i, "-p", 1, 0xFFFF, &ms_port)
|| parse_int_arg(argc, argv, i, "-t", 1, 0x7FFFFFFF, &ms_sv_timeout)
|| parse_int_arg(argc, argv, i, "-c", 1, 0x7FFFFFFF, &ms_cl_timeout)
|| parse_int_arg(argc, argv, i, "-s", 1, MS_MAX_SERVERS, &max_servers)
|| parse_int_arg(argc, argv, i, "-d", 0, MS_MAX_SERVERS, &max_servers_per_host)
|| parse_int_arg(argc, argv, i, "-f", 0, 0xFFFF, &ms_spam_cap)
|| parse_int_arg(argc, argv, i, "-w", 1, 0x7FFFFFFF, &ms_spam_timeout);
if (success) {
++i;
} else {
fprintf(stderr, "unknown or invalid argument: %s\n", argv[i]);
return false;
}
}
return true;
}
// a stupid thing to filter sustained spam from a single IP
static bool spam_filter(ENetPeer *peer, const time_t now) {
if (peer->address.host == cl_last_addr) {
// spam === sending shit faster than once a second
if (now - cl_last_time < ms_spam_timeout) {
if (cl_spam_cnt > 1)
u_log(LOG_WARN, "address %s is sending packets too fast", u_iptostr(peer->address.host));
if (++cl_spam_cnt >= ms_spam_cap) {
ban_peer(peer, "spam");
cl_last_addr = 0;
return true;
}
} else {
cl_spam_cnt = 0;
}
} else {
cl_last_addr = peer->address.host;
cl_spam_cnt = 0;
}
cl_last_time = now;
return false;
}
// filter incoming UDP packets before the protocol kicks in
static int packet_filter(ENetHost *host, ENetEvent *event) {
return !!ban_check(host->receivedAddress.host);
}
int main(int argc, char **argv) {
if (enet_initialize() != 0)
u_fatal("could not init enet");
if (!parse_args(argc, argv))
return 1; // early exit
u_log(LOG_NOTE, "d2df master server starting on port %d", ms_port);
if (!u_readtextfile(MS_MOTD_FILE, ms_motd, sizeof(ms_motd)))
u_log(LOG_NOTE, "couldn't read motd from %s", MS_MOTD_FILE);
else
u_log(LOG_NOTE, "motd: %s", ms_motd);
if (!u_readtextfile(MS_URGENT_FILE, ms_urgent, sizeof(ms_urgent)))
u_log(LOG_NOTE, "couldn't read urgentmsg from %s", MS_URGENT_FILE);
else
u_log(LOG_NOTE, "urgentmsg: %s", ms_urgent);
ban_load_list(MS_BAN_FILE);
atexit(deinit);
#ifdef ENABLE_PIPE
io_install_pipe();
#endif
ENetAddress addr;
addr.host = 0;
addr.port = ms_port;
ms_host = enet_host_create(&addr, MS_MAX_CLIENTS, NET_CH_COUNT + 1, 0, 0);
if (!ms_host)
u_fatal("could not create enet host on port %d", ms_port);
ms_host->intercept = packet_filter;
bool running = true;
enet_uint8 msgid = 0;
ENetEvent event;
while (running) {
while (enet_host_service(ms_host, &event, 10) > 0) {
const time_t now = time(NULL);
const bool filtered = !event.peer || (ms_spam_cap && spam_filter(event.peer, now));
if (!filtered) {
switch (event.type) {
case ENET_EVENT_TYPE_CONNECT:
u_log(LOG_NOTE, "%s:%d connected", u_iptostr(event.peer->address.host), event.peer->address.port);
if (event.peer->channelCount != NET_CH_COUNT)
ban_peer(event.peer, "what is this");
else
enet_peer_timeout(event.peer, 0, 0, ms_cl_timeout * 1000);
break;
case ENET_EVENT_TYPE_RECEIVE:
if (!event.packet || event.packet->dataLength == 0) {
ban_peer(event.peer, "empty packet");
break;
}
// set up receive buffer
buf_recv.pos = 0;
buf_recv.overflow = 0;
buf_recv.data = event.packet->data;
buf_recv.size = event.packet->dataLength;
// read message id and handle the message
msgid = b_read_uint8(&buf_recv);
if (!handle_msg(msgid, event.peer)) {
// cheeky cunt sending invalid messages
ban_peer(event.peer, "unknown message");
}
break;
case ENET_EVENT_TYPE_DISCONNECT:
// u_log(LOG_NOTE, "%s:%d disconnected", u_iptostr(event.peer->address.host), event.peer->address.port);
break;
default:
break;
}
} else if (event.peer) {
// u_log(LOG_WARN, "filtered event %d from %s", event.type, u_iptostr(event.peer->address.host));
sv_clear_peer(event.peer);
enet_peer_reset(event.peer);
}
if (event.packet) {
buf_recv.data = NULL;
enet_packet_destroy(event.packet);
}
}
const time_t now = time(NULL);
// time out servers
for (int i = 0; i < max_servers; ++i) {
if (servers[i].host && servers[i].death_time <= now) {
u_log(LOG_NOTE, "server #%d %s:%d timed out", i, u_iptostr(servers[i].host), servers[i].port);
sv_remove(servers + i);
}
}
#ifdef ENABLE_PIPE
// read commands from pipe
io_read_commands();
#endif
}
}