/*
* 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"
bool are_admin = false;
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(!are_child)
error("out() called from master");
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 - 1)
{
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);
}
#define CLIENT_READ_SZ 128
char *client_read(void)
{
char *buf;
size_t bufidx;
tryagain:
buf = calloc(1, CLIENT_READ_SZ);
bufidx = 0;
/* 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, CLIENT_READ_SZ - bufidx - 1);
if(len <= 0)
error("lost connection (%d)", fds[i].revents);
buf[CLIENT_READ_SZ - 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;
}
#define WSPACE " \t\r\n"
#define CMD_OK 0
#define CMD_LOGOUT 1
#define CMD_QUIT 2
/*** callbacks ***/
int user_cb(char **save)
{
char *what = strtok_r(NULL, WSPACE, save);
if(!what)
return CMD_OK;
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");
return CMD_OK;
}
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");
return CMD_OK;
}
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"))
{
client_user_list();
}
else
{
out("Usage: USER \n");
}
return CMD_OK;
}
int client_cb(char **save)
{
char *what = strtok_r(NULL, WSPACE, save);
if(!what)
{
out("Usage: CLIENT \n");
return CMD_OK;
}
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));
return CMD_OK;
}
/* 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");
return CMD_OK;
}
else if(*end != '\0')
{
out("Expected a child PID after KICK.\n");
return CMD_OK;
}
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");
}
return CMD_OK;
}
int quit_cb(char **save)
{
(void) save;
return CMD_QUIT;
}
int say_cb(char **save)
{
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);
return CMD_OK;
}
int date_cb(char **save)
{
(void) save;
time_t t = time(NULL);
out("%s", ctime(&t));
return CMD_OK;
}
int logout_cb(char **save)
{
(void) save;
out("Logged out.\n");
telnet_clear_screen();
return CMD_LOGOUT;
}
int look_cb(char **save)
{
char *what = strtok_r(NULL, "", save);
if(!what)
client_look();
else
{
client_look_at(what);
}
return CMD_OK;
}
int inventory_cb(char **save)
{
(void) save;
client_inventory();
return CMD_OK;
}
int take_cb(char **save)
{
char *what = strtok_r(NULL, "", save);
client_take(what);
return CMD_OK;
}
int wait_cb(char **save)
{
(void) save;
send_master(REQ_WAIT, NULL, 0);
return CMD_OK;
}
int go_cb(char **save)
{
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");
return CMD_OK;
}
int drop_cb(char **save)
{
char *what = strtok_r(NULL, "", save);
client_drop(what);
return CMD_OK;
}
static const struct client_cmd {
const char *cmd;
int (*cb)(char **saveptr);
bool admin_only;
} cmds[] = {
{ "USER", user_cb, true },
{ "CLIENT", client_cb, true },
{ "EXIT", quit_cb, false },
{ "QUIT", quit_cb, false },
{ "SAY", say_cb, false },
{ "DATE", date_cb, false },
{ "LOGOUT", logout_cb, false },
{ "LOOK", look_cb, false },
{ "INVENTORY", inventory_cb, false },
{ "TAKE", take_cb, false },
{ "WAIT", wait_cb, false },
{ "GO", go_cb, false },
{ "DROP", drop_cb, false },
};
static void *cmd_map = NULL;
void client_init(void)
{
cmd_map = hash_init(ARRAYLEN(cmds), hash_djb, compare_strings);
hash_insert_pairs(cmd_map, (const struct hash_pair*)cmds, sizeof(cmds[0]), ARRAYLEN(cmds));
}
void client_shutdown(void)
{
hash_free(cmd_map);
cmd_map = NULL;
}
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;
are_admin = (authlevel == PRIV_ADMIN);
if(are_admin)
client_change_state(STATE_ADMIN);
else
client_change_state(STATE_LOGGEDIN);
/* authenticated, begin main command loop */
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 *line = client_read();
char *orig = strdup(line);
char *save = NULL;
char *tok = strtok_r(line, WSPACE, &save);
if(!tok)
goto next_cmd;
all_upper(tok);
const struct client_cmd *cmd = hash_lookup(cmd_map, tok);
if(cmd && cmd->cb && (!cmd->admin_only || (cmd->admin_only && are_admin)))
{
int ret = cmd->cb(&save);
switch(ret)
{
case CMD_OK:
goto next_cmd;
case CMD_LOGOUT:
free(line);
free(orig);
goto auth;
case CMD_QUIT:
free(line);
free(orig);
goto done;
default:
error("client: bad callback return value");
}
}
else if(cmd && cmd->admin_only && !are_admin)
{
out("You are not allowed to do that.\n");
goto next_cmd;
}
/* if we can't handle it, let the master process try */
send_master(REQ_EXECVERB, orig, strlen(orig) + 1);
next_cmd:
free(line);
free(orig);
}
done:
free(current_user);
current_user = NULL;
}