#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include char *default_port = "2345"; int backlog = 20; #define max_line 1024 #define max_client_fd 1024 #define max_who_len 65536 #define proto_version "1" int ear; fd_set fds_read, fds_write, fds_err; int fd_max; typedef enum { CLOSED=0, HEADER, CHATTING } state_t; typedef enum { LINE=1, FROM=2, TO=4, TIME=8, DATE=16, ACK=32, ECHO=64, SIZE=128 } opts_t; #define default_opts_send LINE /* 1 */ //#define default_opts_recv LINE|FROM|ECHO #define default_opts_recv LINE|FROM|ECHO|TO|TIME|SIZE /* 33 */ #define servertime(t) localtime(t) typedef struct { state_t state; FILE *io; char *nick; char *auth; opts_t opts_recv; opts_t opts_send; } client_t; client_t clients[max_client_fd]; client_t client_null; #define warn(...) do { fprintf(stderr, __VA_ARGS__); fputc('\n', stderr); } while(0) #define error(...) do { warn(__VA_ARGS__); exit(1); } while(0) //#define debug(...) warn(__VA_ARGS__) #define debug(...) #define bit_set(flags, flag, val) ((flags) = (flags) &~(flag) | (val)) void failed(const char *format, ...) { va_list ap; va_start(ap, format); vfprintf(stderr, format, ap); va_end(ap); fprintf(stderr, ": "); perror(""); exit(1); } void nonblock(int fd, int nb) { int flags = fcntl(fd, F_GETFL); flags &= ~O_NONBLOCK; if (nb) flags |= O_NONBLOCK; fcntl(fd, F_SETFL, flags); } void *get_in_addr(struct sockaddr *sa) { if (sa->sa_family == AF_INET) return &(((struct sockaddr_in *) sa)->sin_addr); return &(((struct sockaddr_in6 *) sa)->sin6_addr); } int server(char *host, char *port) { struct addrinfo hints, *servinfo, *p; int ear, rv, yes = 1; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // TODO listen on both ipv4 and ipv6, etc. (multiple listeners) hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; // TODO put this getaddrinfo into a function if ((rv = getaddrinfo(host, port, &hints, &servinfo)) != 0) error("getaddrinfo: %s", gai_strerror(rv)); for (p = servinfo; p != NULL; p = p->ai_next) { ear = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (ear == -1) { perror("server: socket"); continue; } if (setsockopt (ear, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) failed("setsockopt"); if (bind(ear, p->ai_addr, p->ai_addrlen) == -1) { close(ear); perror("server: bind"); continue; } nonblock(ear, 1); break; } if (p == NULL) error("server: failed to bind"); freeaddrinfo(servinfo); if (listen(ear, backlog) == -1) failed("listen"); return ear; } int accept_new_client(void) { struct sockaddr_storage client_addr; socklen_t sin_size; char s[INET6_ADDRSTRLEN]; int i; sin_size = sizeof client_addr; i = accept(ear, (struct sockaddr *) &client_addr, &sin_size); if (i == -1) { if (errno == EAGAIN || errno == EINTR) return 1; perror("accept"); return 1; } nonblock(i, 1); if (i >= max_client_fd) { warn("too many clients"); close(i); return 1; } inet_ntop(client_addr.ss_family, get_in_addr((struct sockaddr *) &client_addr), s, sizeof s); debug("server: got connection %d from %s", i, s); FD_SET(i, &fds_read); FD_SET(i, &fds_err); client_t *c = &clients[i]; c->io = fdopen(i, "r+b"); if (!c->io) failed("fdopen"); if (i >= fd_max) fd_max = i + 1; c->state = HEADER; c->opts_recv = default_opts_recv; c->opts_send = default_opts_send; return 0; } char *date_time_format(int dt_opts) { char *time_format; switch(dt_opts) { case TIME: time_format = "%H:%M:%S"; break; case DATE: time_format = "%Y-%m-%d"; break; case DATE|TIME: time_format = "%Y-%m-%d %H:%M:%S"; break; } return time_format; } void send_msg(client_t *c, char *from, char *msg) { char time_s[64]; time_t t; int dt_opts = c->opts_recv & (DATE|TIME); char *time_format; if (dt_opts) { time_format = date_time_format(dt_opts); t = time(NULL); if (strftime(time_s, sizeof(time_s), time_format, servertime(&t))) { fprintf(c->io, "%s ", time_s); } else { fprintf(c->io, " "); } } if (c->opts_recv & FROM) fprintf(c->io, "%s: ", from); if (c->opts_recv & TO) fprintf(c->io, "%s, ", c->nick); if (c->opts_recv & SIZE) fprintf(c->io, "%zd. ", strlen(msg)); fprintf(c->io, "%s\n", msg); fflush(c->io); } void broadcast(char *from, client_t *from_client, char *msg) { int i; int echo = from_client && (from_client->opts_recv & ECHO); for (i = 0; i < fd_max; ++i) { client_t *c = &clients[i]; if (c->state == CHATTING && (echo || c != from_client)) send_msg(c, from, msg); } } void close_client(client_t *c) { int i = c - clients; fclose(c->io); c->io = NULL; FD_CLR(i, &fds_read); FD_CLR(i, &fds_err); c->state = CLOSED; broadcast("exit", NULL, c->nick); free(c->nick); c->nick = NULL; } void header(client_t *c, char *line) { int i = c - clients; char *key, *value; char *delim = strstr(line, ": "); if (!delim) { debug("bad header from i %d: %s\n", i, line); return; } *delim = '\0'; key = line; value = delim + 2; debug("header from i %d: %s: %s", i, key, value); if (!strcmp(key, "proto")) { if (strcmp(value, proto_version)) { warn("protocol mismatch from i %d: %s", i, value); return; } } else if (!strcmp(key, "nick")) { c->nick = strdup(value); } else if (!strcmp(key, "auth")) { c->auth = strdup(value); } else if (!strcmp(key, "opts")) { char *space = strchr(value, ' '); if (!space) { fprintf(stderr, "bad 'opts' header from i %d: %s\n", i, value); return; } c->opts_recv = atoi(space); c->opts_send = atoi(space + 1); } } char *who(void) { static char who[max_who_len]; int i, len = 0; char *p = who; *p = '\0'; for (i = 0; i < fd_max; ++i) { client_t *c = &clients[i]; if (c->state != CHATTING) continue; int nick_len = strlen(c->nick); if (len + nick_len + 1 >= max_who_len - 5) { strcat(p, "..."); break; } strcat(p, c->nick); p += len; strcat(p, " "); ++p; } return who; } void start_chatting(client_t *c) { int i = c - clients; i=i; if (!c->nick) { close_client(c); return; } debug("i %d is chatting", i); broadcast("enter", NULL, c->nick); c->state = CHATTING; send_msg(c, "who", who()); } int received_msg(client_t *c) { int i = c - clients; i=i; char line[max_line]; int len; debug("can read: %d", i); errno = 0; if (!fgets(line, sizeof(line), c->io)) { if (errno == EAGAIN) return 0; debug("i %d closed connection", i); close_client(c); return 0; } len = strcspn(line, "\n\r"); line[len] = '\0'; if (c->state == HEADER) { if (len) header(c, line); else start_chatting(c); } else broadcast(c->nick, c, line); return 1; } void server_step(void) { fd_set fds_read_r, fds_write_r, fds_err_r; int count; int i; fds_read_r = fds_read; fds_write_r = fds_write; fds_err_r = fds_err; debug("selecting..."); count = select(fd_max, &fds_read_r, &fds_write_r, &fds_err_r, NULL); if (count < 0 && errno != EINTR) failed("select"); if (FD_ISSET(ear, &fds_read_r)) accept_new_client(); for (i = ear + 1; i < fd_max; ++i) { if (FD_ISSET(i, &fds_read_r)) while (received_msg(&clients[i])) { } } } int main(int argc, char **argv) { char *host, *port; int i; if (argc < 1 || argc > 3 || (argc > 1 && argv[1][0] == '-')) { error("usage: chatd [host [port]]"); } host = argc < 2 ? "0.0.0.0" : argv[1]; port = argc < 3 ? default_port : argv[2]; for (i = 0; i < max_client_fd; ++i) { clients[i] = client_null; } FD_ZERO(&fds_read); FD_ZERO(&fds_write); FD_ZERO(&fds_err); ear = server(host, port); FD_SET(ear, &fds_read); FD_SET(ear, &fds_err); fd_max = ear + 1; while (1) server_step(); return 0; }