/*
* NetCosm - a MUD server
* Copyright (C) 2016 Franklin Wei
*
* 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 3 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, see .
*/
#include "globals.h"
#include "auth.h"
#include "client.h"
#include "hash.h"
#include "server.h"
#include "room.h"
#include "telnet.h"
#include "util.h"
static bool admin = false;
static int client_fd, to_parent, from_parent;
static room_id current_room = 0;
static volatile sig_atomic_t output_locked = 0;
char *current_user = NULL;
bool poll_requests(void);
void out_raw(const void *buf, size_t len)
{
if(!len)
return;
try_again:
while(output_locked);
/* something weird happened and the value changed between the loop and here */
if(!output_locked)
{
output_locked = 1;
write(client_fd, buf, len);
output_locked = 0;
}
else
goto try_again;
}
void __attribute__((format(printf,1,2))) out(const char *fmt, ...)
{
char buf[1024];
memset(buf, 0, sizeof(buf));
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
/* do some line wrapping */
static int pos = 0;
int last_space = 0;
char *ptr = buf;
uint16_t line_width = telnet_get_width() + 1;
char *line_buf = malloc(line_width + 2);
size_t line_idx = 0;
while(ptr[pos])
{
bool is_newline = (ptr[pos] == '\n');
if(is_newline || pos >= line_width)
{
if(is_newline || !last_space)
last_space = pos;
while(*ptr && last_space-- > 0)
{
line_buf[line_idx++] = *ptr++;
}
line_buf[line_idx++] = '\r';
line_buf[line_idx++] = '\n';
out_raw(line_buf, line_idx);
line_idx = 0;
if(is_newline)
++ptr; /* skip the newline */
while(*ptr == ' ')
++ptr;
last_space = 0;
pos = 0;
}
else
{
if(ptr[pos] == ' ')
last_space = pos;
++pos;
}
}
out_raw(ptr, strlen(ptr));
free(line_buf);
}
static volatile sig_atomic_t request_complete;
/* for rate-limiting */
static int reqs_since_ts;
static time_t ts = 0;
void send_master(unsigned char cmd, const void *data, size_t sz)
{
if(!admin)
{
time_t t = time(NULL);
if(ts != t)
{
ts = t;
reqs_since_ts = 0;
}
if(reqs_since_ts++ > 10)
{
out("Rate limit exceeded.\n");
return;
}
}
request_complete = 0;
pid_t our_pid = getpid();
if(!data)
sz = 0;
/* format of child->parent packets:
* | PID | CMD | DATA |
*/
/* pack it all into one write so it's atomic */
char *req = malloc(sizeof(pid_t) + 1 + sz);
memcpy(req, &our_pid, sizeof(pid_t));
memcpy(req + sizeof(pid_t), &cmd, 1);
if(data)
memcpy(req + sizeof(pid_t) + 1, data, sz);
assert(1 + sizeof(pid_t) + sz <= MSG_MAX);
write(to_parent, req, 1 + sizeof(pid_t) + sz);
/* poll till we get data */
struct pollfd pfd;
pfd.fd = from_parent;
pfd.events = POLLIN;
poll(&pfd, 1, -1);
while(!request_complete) poll_requests();
free(req);
}
#define BUFSZ 128
char *client_read(void)
{
char *buf;
size_t bufidx;
tryagain:
buf = malloc(BUFSZ);
bufidx = 0;
memset(buf, 0, BUFSZ);
/* set of the client fd and the pipe from our parent */
struct pollfd fds[2];
/* order matters here: we first fulfill parent requests, then
* handle client data */
fds[0].fd = from_parent;
fds[0].events = POLLIN;
fds[1].fd = client_fd;
fds[1].events = POLLIN;
while(1)
{
poll(fds, ARRAYLEN(fds), -1);
for(int i = 0; i < 2; ++i)
{
if(fds[i].revents & POLLIN)
{
if(fds[i].fd == from_parent)
{
poll_requests();
}
else if(fds[i].fd == client_fd)
{
ssize_t len = read(client_fd, buf + bufidx, BUFSZ - bufidx - 1);
if(len <= 0)
error("lost connection (%d)", fds[i].revents);
buf[BUFSZ - 1] = '\0';
enum telnet_status ret = telnet_parse_data((unsigned char*)buf + bufidx, len);
switch(ret)
{
case TELNET_EXIT:
case TELNET_FOUNDCMD:
free(buf);
if(ret == TELNET_EXIT)
exit(0);
goto tryagain;
case TELNET_DATA:
bufidx += len;
continue;
case TELNET_LINEOVER:
break;
}
remove_cruft(buf);
return buf;
}
}
}
}
}
/* still not encrypted, but a bit more secure than echoing the password! */
char *client_read_password(void)
{
telnet_echo_off();
char *ret = client_read();
telnet_echo_on();
out("\n");
return ret;
}
enum reqdata_typespec reqdata_type = TYPE_NONE;
union reqdata_t returned_reqdata;
bool poll_requests(void)
{
if(!are_child)
return false;
bool got_cmd = false;
while(1)
{
unsigned char packet[MSG_MAX + 1];
memset(packet, 0, sizeof(packet));
ssize_t packetlen = read(from_parent, packet, MSG_MAX);
unsigned char *data = packet + 1;
size_t datalen = packetlen - 1;
packet[MSG_MAX] = '\0';
if(packetlen <= 0)
goto fail;
got_cmd = true;
unsigned char cmd = packet[0];
switch(cmd)
{
case REQ_BCASTMSG:
{
out((char*)data, datalen);
break;
}
case REQ_KICK:
{
out((char*)data, datalen);
exit(EXIT_SUCCESS);
}
case REQ_MOVE:
{
int status = *((int*)data);
reqdata_type = TYPE_BOOLEAN;
returned_reqdata.boolean = status;
if(!status)
out("Cannot go that way.\n");
break;
}
case REQ_GETUSERDATA:
{
if(datalen == sizeof(struct userdata_t))
reqdata_type = TYPE_USERDATA;
else
break;
struct userdata_t *user = &returned_reqdata.userdata;
*user = *((struct userdata_t*)data);
break;
}
case REQ_DELUSERDATA:
{
reqdata_type = TYPE_BOOLEAN;
returned_reqdata.boolean = *((bool*)data);
break;
}
case REQ_ADDUSERDATA:
{
reqdata_type = TYPE_BOOLEAN;
returned_reqdata.boolean = *((bool*)data);
break;
}
case REQ_NOP:
break;
case REQ_PRINTNEWLINE:
{
out("\n");
break;
}
case REQ_ALLDONE:
request_complete = 1;
return true;
default:
debugf("WARNING: client process received unknown code %d\n", cmd);
break;
}
}
fail:
return got_cmd;
}
void client_change_state(int state)
{
send_master(REQ_CHANGESTATE, &state, sizeof(state));
}
void client_change_user(const char *user)
{
send_master(REQ_CHANGEUSER, user, strlen(user) + 1);
}
void client_change_room(room_id id)
{
send_master(REQ_SETROOM, &id, sizeof(id));
}
void *dir_map = NULL;
bool client_move(const char *dir)
{
const struct dir_pair {
const char *text;
enum direction_t val;
} dirs[] = {
{ "N", DIR_N },
{ "NORTH", DIR_N },
{ "NE", DIR_NE },
{ "NORTHEAST", DIR_N },
{ "E", DIR_E },
{ "EAST", DIR_E },
{ "SE", DIR_SE },
{ "SOUTHEAST", DIR_SE },
{ "S", DIR_S },
{ "SOUTH", DIR_S },
{ "SW", DIR_SW },
{ "SOUTHWEST", DIR_SW },
{ "W", DIR_W },
{ "WEST", DIR_W },
{ "NW", DIR_NW },
{ "NORTHWEST", DIR_NW },
{ "U", DIR_UP },
{ "UP", DIR_UP },
{ "D", DIR_DN },
{ "DOWN", DIR_DN },
{ "IN", DIR_IN },
{ "OUT", DIR_OT },
};
if(!dir_map)
{
dir_map = hash_init(ARRAYLEN(dirs), hash_djb, compare_strings);
hash_insert_pairs(dir_map, (struct hash_pair*)dirs, sizeof(struct dir_pair), ARRAYLEN(dirs));
}
struct dir_pair *pair = hash_lookup(dir_map, dir);
if(pair)
{
send_master(REQ_MOVE, &pair->val, sizeof(pair->val));
if(reqdata_type == TYPE_BOOLEAN && returned_reqdata.boolean)
return true;
else
return false;
}
else
{
out("Unknown direction.\n");
return false;
}
}
void client_look(void)
{
send_master(REQ_GETROOMNAME, NULL, 0);
out("\n");
send_master(REQ_GETROOMDESC, NULL, 0);
}
void client_look_at(char *obj)
{
all_lower(obj);
send_master(REQ_LOOKAT, obj, strlen(obj) + 1);
}
void client_take(char *obj)
{
all_lower(obj);
send_master(REQ_TAKE, obj, strlen(obj) + 1);
}
void client_inventory(void)
{
send_master(REQ_PRINTINVENTORY, NULL, 0);
}
void client_drop(char *what)
{
send_master(REQ_DROP, what, strlen(what) + 1);
}
#define WSPACE " \t\r\n"
void client_main(int fd, struct sockaddr_in *addr, int total, int to, int from)
{
client_fd = fd;
to_parent = to;
from_parent = from;
output_locked = 0;
telnet_init();
char *ip = inet_ntoa(addr->sin_addr);
debugf("New client %s\n", ip);
debugf("Total clients: %d\n", total);
debugf("client is running with uid %d\n", getuid());
auth:
out("NetCosm " NETCOSM_VERSION "\n");
if(total > 1)
out("%d clients connected.\n", total);
else
out("%d client connected.\n", total);
out("\nPlease authenticate to continue.\n\n");
int failures = 0;
int authlevel;
struct userdata_t *current_data = NULL;
client_change_state(STATE_AUTH);
/* auth loop */
while(1)
{
out("login: ");
current_user = client_read();
remove_cruft(current_user);
out("Password: ");
char *pass = client_read_password();
client_change_state(STATE_CHECKING);
current_data = auth_check(current_user, pass);
memset(pass, 0, strlen(pass));
free(pass);
if(current_data)
{
out("Last login: %s", ctime(¤t_data->last_login));
current_data->last_login = time(0);
authlevel = current_data->priv;
userdb_request_add(current_data);
break;
}
else
{
client_change_state(STATE_FAILED);
free(current_user);
current_user = NULL;
out("Login incorrect\n\n");
if(++failures >= MAX_FAILURES)
return;
}
}
/* something has gone wrong, but we are here for some reason */
if(authlevel == PRIV_NONE)
return;
admin = (authlevel == PRIV_ADMIN);
if(admin)
client_change_state(STATE_ADMIN);
else
client_change_state(STATE_LOGGEDIN);
/* authenticated */
debugf("client: Authenticated as %s\n", current_user);
client_change_user(current_user);
current_room = 0;
client_change_room(current_room);
client_look();
while(1)
{
out(">> ");
char *cmd = client_read();
char *save = NULL;
char *tok = strtok_r(cmd, WSPACE, &save);
if(!tok)
goto next_cmd;
all_upper(tok);
if(admin)
{
if(!strcmp(tok, "USER"))
{
char *what = strtok_r(NULL, WSPACE, &save);
if(!what)
goto next_cmd;
all_upper(what);
if(!strcmp(what, "DEL"))
{
char *user = strtok_r(NULL, WSPACE, &save);
if(user)
{
if(strcmp(user, current_user) && auth_user_del(user))
out("Success.\n");
else
out("Failure.\n");
}
else
{
out("Usage: USER DEL \n");
}
}
else if(!strcmp(what, "ADD") || !strcmp(what, "MODIFY"))
{
char *user = strtok_r(NULL, WSPACE, &save);
if(user)
{
if(!strcmp(user, current_user))
{
out("Do not modify your own password using USER. User CHPASS instead.\n");
goto next_cmd;
}
out("Editing user '%s'\n", user);
out("New Password (_DO_NOT_USE_A_VALUABLE_PASSWORD_): ");
/* BAD BAD BAD BAD BAD BAD BAD CLEARTEXT PASSWORDS!!! */
char *pass = client_read_password();
out("Verify Password: ");
char *pass2 = client_read_password();
if(strcmp(pass, pass2))
{
memset(pass, 0, strlen(pass));
memset(pass2, 0, strlen(pass2));
free(pass);
free(pass2);
out("Failure.\n");
goto next_cmd;
}
out("Admin privileges [y/N]? ");
char *allow_admin = client_read();
int priv = PRIV_USER;
if(toupper(allow_admin[0]) == 'Y')
priv = PRIV_ADMIN;
free(allow_admin);
if(auth_user_add(user, pass, priv))
out("Success.\n");
else
out("Failure.\n");
memset(pass, 0, strlen(pass));
free(pass);
}
else
out("Usage: USER \n");
}
else if(!strcmp(what, "LIST"))
{
auth_user_list();
}
else
{
out("Usage: USER \n");
}
}
else if(!strcmp(tok, "CLIENT"))
{
char *what = strtok_r(NULL, WSPACE, &save);
if(!what)
{
out("Usage: CLIENT \n");
goto next_cmd;
}
all_upper(what);
if(!strcmp(what, "LIST"))
{
send_master(REQ_LISTCLIENTS, NULL, 0);
}
else if(!strcmp(what, "KICK"))
{
char *pid_s = strtok_r(NULL, WSPACE, &save);
all_upper(pid_s);
if(pid_s)
{
if(!strcmp(pid_s, "ALL"))
{
const char *msg = "Kicking everyone...\n";
send_master(REQ_KICKALL, msg, strlen(msg));
goto next_cmd;
}
/* weird pointer voodoo */
/* TODO: simplify */
char pidbuf[MAX(sizeof(pid_t), MSG_MAX)];
char *end;
pid_t pid = strtol(pid_s, &end, 0);
if(pid == getpid())
{
out("You cannot kick yourself. Use EXIT instead.\n");
goto next_cmd;
}
else if(*end != '\0')
{
out("Expected a child PID after KICK.\n");
goto next_cmd;
}
memcpy(pidbuf, &pid, sizeof(pid));
int len = sizeof(pid_t) + snprintf(pidbuf + sizeof(pid_t),
sizeof(pidbuf) - sizeof(pid_t),
"You were kicked.\n");
send_master(REQ_KICK, pidbuf, len);
debugf("Success.\n");
}
else
out("Usage: CLIENT KICK \n");
}
}
//else if(!strcmp(tok, "HANG"))
//{
// send_master(REQ_HANG);
//}
}
/* unprivileged commands */
if(!strcmp(tok, "QUIT") || !strcmp(tok, "EXIT"))
{
free(cmd);
goto done;
}
else if(!strcmp(tok, "SAY"))
{
char buf[MSG_MAX];
char *what = strtok_r(NULL, "", &save);
int len = snprintf(buf, sizeof(buf), "%s says %s\n", current_user, what);
send_master(REQ_BCASTMSG, buf, len);
}
else if(!strcmp(tok, "DATE"))
{
time_t t = time(NULL);
out("%s", ctime(&t));
}
else if(!strcmp(tok, "LOGOUT"))
{
out("Logged out.\n");
goto auth;
}
else if(!strcmp(tok, "LOOK"))
{
char *what = strtok_r(NULL, " ", &save);
if(!what)
client_look();
else
{
client_look_at(what);
}
}
else if(!strcmp(tok, "INVENTORY"))
{
client_inventory();
}
else if(!strcmp(tok, "TAKE"))
{
char *what = strtok_r(NULL, " ", &save);
client_take(what);
}
else if(!strcmp(tok, "WAIT"))
{
send_master(REQ_WAIT, NULL, 0);
}
else if(!strcmp(tok, "GO"))
{
char *dir = strtok_r(NULL, WSPACE, &save);
if(dir)
{
all_upper(dir);
if(client_move(dir))
client_look();
}
else
out("Expected direction after GO.\n");
}
else if(!strcmp(tok, "DROP"))
{
char *what = strtok_r(NULL, " ", &save);
client_drop(what);
}
next_cmd:
free(cmd);
}
done:
free(current_user);
current_user = NULL;
}