/* * WarGames - a WOPR emulator written in C * Copyright (C) 2014 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 . * * Contact the author at contact@fwei.tk */ #include "joshua.h" #include "telnet.h" #include "util.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #define DEFAULT_PORT 23 #define LOG_LOCATION "/var/log/wopr" int server_socket; uint16_t port; int pipes[FD_SETSIZE][2]; struct connection_data_t connection_data[FD_SETSIZE]; int debugf(const char* fmt, ...) { va_list l; va_start(l, fmt); int ret = vprintf(fmt, l); va_end(l); return ret; } int make_server_socket(uint16_t port) { int sock; struct sockaddr_in name; sock = socket(AF_INET, SOCK_STREAM, 0); if(sock < 0) { debugf("FATAL: Error opening socket.\n"); return -1; } const int opt_val = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof(opt_val)); setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &opt_val, sizeof(opt_val)); name.sin_family = AF_INET; name.sin_port = htons(port); name.sin_addr.s_addr = htonl(INADDR_ANY); int ret = bind(sock, (struct sockaddr*) &name, sizeof(name)); if(ret < 0) { debugf("FATAL: Error binding to port %d\n", port); return -1; } return sock; } char pending_buffer[1024]; void handle_command(unsigned char* buf, int buflen, int connection) { unsigned char cmd, opt; if(buflen < 2) { debugf("Invalid command.\n"); return; } cmd = buf[1]; /* handle two-byte commands */ switch(cmd) { case AYT: { unsigned char iac_nop[] = {IAC, NOP}; write(connection, iac_nop, sizeof(iac_nop)); return; } } if(buflen < 3) { debugf("Invalid command.\n"); return; } opt = buf[2]; switch(cmd) { case SB: { switch(opt) { case NAWS: { printf("NAWS command recieved.\n"); /* format of NAWS data: IAC SB NAWS W W H H IAC SE */ uint16_t height, width; uint8_t height_hi, height_lo, width_hi, width_lo; if(buflen < 9) { debugf("IAC SB NAWS command too short!\n"); return; } width_hi = buf[3]; width_lo = buf[4]; height_hi = buf[5]; height_lo = buf[6]; height = (height_hi << 8) | height_lo; width = (width_hi << 8) | width_lo; connection_data[connection].know_termsize = (height == 0 || width == 0) ? 0 : 1; connection_data[connection].term_height = height; connection_data[connection].term_width = width; return; } } break; } } if((opt == LINEMODE && cmd == WILL) || (opt == NAWS)) { /* avoid an endless negiociation loop */ return; } /* unimplemented command, just deny it */ unsigned char deny_cmd[3] = {IAC, WONT, opt}; write(connection, deny_cmd, sizeof(deny_cmd)); fsync(connection); return; } int process_data(int fd) { unsigned char buf[1024]; memset(buf, 0, sizeof(buf)); int ret = read(fd, buf, sizeof(buf)); debugf("Client %d sends: %s\n", fd, buf); debugf("Byte dump of data: "); for(int i = 0; buf[i]; ++i) { debugf("%02x ", buf[i]); } debugf("\n"); char ctrl_c[] = {0xff, 0xf4, 0xff, 0xfd, 0x06, 0x00}; if(strcmp(ctrl_c, buf) == 0) { debugf("Got CTRL-C from client %d.\n", fd); return -1; } if(ret < 0) /* error */ { debugf("Error in read()\n"); return -1; } if(ret == 0) { debugf("EOF from client %d.\n", fd); return -1; } else { int buflen = strlen(buf); if(buflen > 0) { if(buf[0] == 0xff) { handle_command(buf, buflen, fd); } else if(strlen(buf) > 0) { write(pipes[fd][1], buf, strlen(buf)); } } } return 0; } void serv_cleanup() { debugf("\nPreparing to exit...\n"); fflush(stdout); shutdown(server_socket, SHUT_RDWR); } void setup_new_connection(int fd) { unsigned char will_naws[] = {IAC, DO, NAWS}; write(fd, will_naws, sizeof(will_naws)); will_naws[1] = WILL; write(fd, will_naws, sizeof(will_naws)); unsigned char dont_echo[] = {IAC, DONT, ECHO}; write(fd, dont_echo, sizeof(dont_echo)); dont_echo[1] = WONT; write(fd, dont_echo, sizeof(dont_echo)); unsigned char dont_sga[] = {IAC, WONT, SGA}; write(fd, dont_sga, sizeof(dont_sga)); unsigned char will_linemode[] = {IAC, WILL, LINEMODE}; write(fd, will_linemode, sizeof(will_linemode)); memset(&connection_data[fd], 0, sizeof(struct connection_data_t)); debugf("New connection set up.\n"); } int main(int argc, char* argv[]) { if(argc != 2) { debugf("Listening on default port: %d\n", DEFAULT_PORT); port = DEFAULT_PORT; } else { int port2 = atoi(argv[1]); if(port2 < 0 || port2 > 65535) { debugf("FATAL: Port out of range.\n"); return 2; } port = atoi(argv[1]); } debugf("Initializing server on port %u...\n", port); signal(SIGINT, &serv_cleanup); int sock = make_server_socket(port); server_socket = sock; fd_set active_fd_set, read_fd_set; struct sockaddr_in client; if(listen(sock, 1) < 0) { debugf("FATAL: Error opening socket.\n"); return 1; } FD_ZERO(&active_fd_set); FD_SET(sock, &active_fd_set); debugf("Server ready, listening on port %d.\n", port); debugf("Maximum clients: %d\n", FD_SETSIZE); while(1) { read_fd_set = active_fd_set; int ret = select(FD_SETSIZE, &read_fd_set, 0, 0, 0); if(ret < 0) { debugf("Error in select().\n"); return 1; } for(int i = 0; i < FD_SETSIZE; ++i) { if(FD_ISSET(i, &read_fd_set)) { if(i == sock) { /* new connection */ int new, size; size = sizeof(client); new = accept(sock, (struct sockaddr*) &client, &size); if(new < 0) { debugf("Error accepting new connection.\n"); continue; } debugf("New connection, number %d.\n", new); FD_SET(new, &active_fd_set); int ret = pipe(pipes[new]); if(ret < 0) { debugf("Pipe error.\n"); continue; } pid_t pid = fork(); if(pid < 0) { debugf("Fork error.\n"); continue; } if(pid == 0) /* child */ { /* set up the connection */ setup_new_connection(new); be_joshua(new); debugf("Client exits\n"); shutdown(new, SHUT_RDWR); FD_CLR(new, &active_fd_set); exit(0); } } else { /* data from existing connection */ if(process_data(i) < 0) { shutdown(i, SHUT_RDWR); FD_CLR(i, &active_fd_set); /* should kill the child associated with this connection, too */ } } } } } }