/* * 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 "netcosm.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; void out_raw(const unsigned char *buf, size_t len) { 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[128]; memset(buf, 0, sizeof(buf)); va_list ap; va_start(ap, fmt); int len = vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); out_raw((unsigned char*)buf, len); } static volatile sig_atomic_t request_complete; 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; sigset_t block, old; sigemptyset(&block); sigaddset(&block, SIGRTMIN); sigprocmask(SIG_BLOCK, &block, &old); pid_t our_pid = getpid(); size_t total_len = (data?sz:0) + 1; if(!data) sz = 0; /* pack it all into one write so it's atomic */ char *req = malloc(1 + sizeof(pid_t) + sizeof(size_t) + sz); memcpy(req, &our_pid, sizeof(pid_t)); memcpy(req + sizeof(pid_t), &total_len, sizeof(size_t)); memcpy(req + sizeof(pid_t) + sizeof(size_t), &cmd, 1); memcpy(req + sizeof(pid_t) + sizeof(size_t) + 1, data, sz); write(to_parent, req, 1 + sizeof(pid_t) + sizeof(size_t) + sz); sigsuspend(&old); sigprocmask(SIG_SETMASK, &old, NULL); while(!request_complete) usleep(1); free(req); } #define BUFSZ 128 char *client_read(void) { char *buf; tryagain: buf = malloc(BUFSZ); memset(buf, 0, BUFSZ); if(read(client_fd, buf, BUFSZ - 1) < 0) error("lost connection"); buf[BUFSZ - 1] = '\0'; if(buf[0] & 0x80) { int ret = telnet_handle_command((unsigned char*)buf); free(buf); if(ret == TELNET_EXIT) exit(0); goto tryagain; } 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; } static void print_all(int fd) { unsigned char buf[MSG_MAX + 1]; do { ssize_t len = read(fd, &buf, MSG_MAX); if(len <= 0) break; buf[MSG_MAX] = '\0'; out_raw(buf, len); } while(1); } enum reqdata_typespec reqdata_type = TYPE_NONE; union reqdata_t returned_reqdata; void read_string_max(int fd, char *buf, size_t max) { size_t len; if(read(fd, &len, sizeof(len)) != sizeof(len)) error("read_string_max"); if(len > max - 1) error("read_string_max"); if(read(fd, buf, len) != (int)len) error("unexpected EOF"); buf[max - 1] = '\0'; } void sig_rt_0_handler(int s, siginfo_t *info, void *v) { (void) s; (void) v; /* we only listen to requests from our parent */ if(info->si_pid != getppid()) { sig_debugf("Unknown PID sent SIGRTMIN+1\n"); return; } reqdata_type = TYPE_NONE; unsigned char cmd; read(from_parent, &cmd, 1); switch(cmd) { case REQ_BCASTMSG: { print_all(from_parent); break; } case REQ_KICK: { print_all(from_parent); union sigval junk; /* the master still expects an ACK */ sigqueue(getppid(), SIGRTMIN+1, junk); exit(EXIT_SUCCESS); } case REQ_MOVE: { bool status; read(from_parent, &status, sizeof(status)); reqdata_type = TYPE_BOOLEAN; returned_reqdata.boolean = status; if(!status) out("Cannot go that way.\n"); } case REQ_GETUSERDATA: { sig_debugf("got user data\n"); bool success; read(from_parent, &success, sizeof(success)); if(success) reqdata_type = TYPE_USERDATA; else { sig_debugf("failure\n"); break; } struct userdata_t *user = &returned_reqdata.userdata; if(read(from_parent, user, sizeof(*user)) != sizeof(*user)) error("user data too short"); break; } case REQ_DELUSERDATA: { reqdata_type = TYPE_BOOLEAN; if(read(from_parent, &returned_reqdata.boolean, sizeof(bool)) != sizeof(bool)) error("error reading bool"); break; } case REQ_ADDUSERDATA: { reqdata_type = TYPE_BOOLEAN; if(read(from_parent, &returned_reqdata.boolean, sizeof(bool)) != sizeof(bool)) error("error reading bool"); break; } case REQ_NOP: break; default: sig_debugf("WARNING: client process received unknown code %d\n", cmd); break; } sig_debugf("Client finishes handling request.\n"); request_complete = 1; /* signal the master that we're done */ union sigval junk; sigqueue(getppid(), SIGRTMIN+1, junk); } static void sigpipe_handler(int s) { (void) s; union sigval junk; /* * necessary in case we get SIGPIPE in our SIGRTMIN+1 handler, * the master expects a response from us */ sigqueue(getppid(), SIGRTMIN+1, junk); _exit(0); } 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 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 }, }; static void *map = NULL; if(!map) { map = hash_init(ARRAYLEN(dirs), hash_djb, compare_strings); hash_insert_pairs(map, (struct hash_pair*)dirs, sizeof(struct dir_pair), ARRAYLEN(dirs)); } struct dir_pair *pair = hash_lookup(map, dir); if(pair) { send_master(REQ_MOVE, &pair->val, sizeof(pair->val)); } else out("Unknown direction.\n"); } void client_look(void) { send_master(REQ_GETROOMNAME, NULL, 0); out("\n"); send_master(REQ_GETROOMDESC, NULL, 0); } #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; struct sigaction sa; sigemptyset(&sa.sa_mask); sa.sa_handler = sigpipe_handler; sa.sa_flags = SA_RESTART; if(sigaction(SIGPIPE, &sa, NULL) < 0) error("sigaction"); telnet_init(); char *ip = inet_ntoa(addr->sin_addr); debugf("New client %s\n", ip); debugf("Total clients: %d\n", total); 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; char *current_user; 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("Access Granted.\n\n"); authlevel = current_data->priv; break; } else { client_change_state(STATE_FAILED); free(current_user); out("Access Denied.\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("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); if(pid_s) { /* weird pointer voodoo */ /* TODO: simplify */ char pidbuf[MAX(sizeof(pid_t), MSG_MAX)]; pid_t pid = strtol(pid_s, NULL, 0); if(pid == getpid()) { out("You cannot kick yourself. Use EXIT instead.\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); //} } 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")) { client_look(); } 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); client_move(dir); client_look(); } else out("Expected direction after GO.\n"); } next_cmd: free(cmd); } done: free(current_user); }