// TODO: Break out `run_*` into `*.{c,h}`? // TODO: Harmonize all reporting strings and comments. // TODO: Harmonize all conditional and loop statements. // TODO: Add `listen` and `accept` if the socket has a `STREAM` `socktype`. // TODO: Compare to the example in `getaddrinfo(3)`. // TODO: Mention `getaddrinfo` and the RFC 3484 sorting order in readme. // TODO: Change client store from queue to binary tree? // TODO: Create graph of parameter flow. // TODO: Use Kaj and Börje in readme example. // TODO: Document idioms: // - Trim: `buflen -= buflen && buf[buflen - 1] == char;` // - Format: `buflen += snprintf(&buf[buflen], BUFLEN_MAX - buflen, format, args...);` // TODO: Document "size" for raw bytes and "len" for "logical" data (e.g. // excluding string terminating null). // TODO: Give each client an `id`. // TODO: Also make the server send a timestamp base on join? // TODO: `#include <stdbool.h>` and change any relevant `int`s to `bool`s. // TODO: Change to `run_*(int * sockfd, int sockfdlen, bool nonblock)`. // TODO: Do different things on `initsockfd` depending on `family`. // TODO: We can combine IDs and actions: an ID of 0 implies a JOIN action, // otherwise a MESSAGE action. /// Headers //// POSIX 2004 #define _XOPEN_SOURCE 600 //// C standard library #include <stdlib.h> #include <stdio.h> #include <string.h> #include <ctype.h> #include <limits.h> //// POSIX #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/select.h> #include <netdb.h> #include <search.h> #include <termios.h> // Other #include "arg.h" #include "report.h" /// Constants //// Program #define PROGNAME "sockchat" //// Arguments // TODO // #define DEFAULT_FAMILY "UNSPEC" // #define DEFAULT_SOCKTYPE "0" #define DEFAULT_FAMILY "INET" #define DEFAULT_SOCKTYPE "DGRAM" #define DEFAULT_PROTOCOL "0" #define DEFAULT_NODE "" #define DEFAULT_SERVICE "3200" //// Command line interface #define VERSION \ PROGNAME " 1.0\n" #define USAGE \ "Usage:\n" \ " " PROGNAME " (server|client) [options]\n" \ " " PROGNAME " -h|--help\n" \ " " PROGNAME " --version\n" #define DESCRIPTION \ "Chat with unlimited number of peers through a variety of sockets.\n" #define OPTIONS \ "Options:\n" \ " -f <family> [default: " DEFAULT_FAMILY "]\n" \ " -t <socktype> [default: " DEFAULT_SOCKTYPE "]\n" \ " -p <protocol> [default: " DEFAULT_PROTOCOL "]\n" \ " -n <node> [default: " DEFAULT_NODE "]\n" \ " -s <service> [default: " DEFAULT_SERVICE "]\n" //// Implementation #define BUFLEN_MAX _POSIX_MAX_CANON /// Forward declarations //// Implementation int initsockfd( char const * init_name, int (*init)(int sockfd, struct sockaddr const * addr, socklen_t addrlen), int flags, int family, int socktype, int protocol, char const * node, char const * service ); void run_server(int sockfd); void run_client(int sockfd); /// Data //// Arguments #define ROLE(ROLE, INIT, FLAGS) { #ROLE, #INIT, INIT, FLAGS, run_##ROLE } struct role { char const * name; char const * init_name; int (*init)(int sockfd, struct sockaddr const * addr, socklen_t addrlen); int flags; void (*run)(int sockfd); }; static struct role roles[] = { ROLE(server, bind, AI_PASSIVE), ROLE(client, connect, 0), }; #define CONST_FAMILY(FAMILY) { #FAMILY, AF_##FAMILY } static struct arg_const families[] = { CONST_FAMILY(UNSPEC), CONST_FAMILY(INET), CONST_FAMILY(INET6), CONST_FAMILY(UNIX), }; #define CONST_SOCKTYPE(SOCKTYPE) { #SOCKTYPE, SOCK_##SOCKTYPE } static struct arg_const socktypes[] = { // { "0", 0 }, CONST_SOCKTYPE(DGRAM), // CONST_SOCKTYPE(SEQPACKET), // CONST_SOCKTYPE(STREAM), // CONST_SOCKTYPE(RAW), }; #define CONST_PROTOCOL(PROTOCOL) { #PROTOCOL, IPPROTO_##PROTOCOL } static struct arg_const protocols[] = { { "0", 0 }, // CONST_PROTOCOL(IP), // CONST_PROTOCOL(IPV6), // CONST_PROTOCOL(ICMP), // CONST_PROTOCOL(TCP), // CONST_PROTOCOL(UDP), // CONST_PROTOCOL(RAW), }; /// Command line interface void version (FILE * stream) { fprintf(stream, "%s", VERSION); } void usage (FILE * stream) { fprintf(stream, "%s", USAGE); } void description(FILE * stream) { fprintf(stream, "%s", DESCRIPTION); } void options (FILE * stream) { fprintf(stream, "%s", OPTIONS); } void args (FILE * stream) { ARG_PRINT(stream, family, families ); fprintf(stream, "\n"); ARG_PRINT(stream, socktype, socktypes); fprintf(stream, "\n"); ARG_PRINT(stream, protocol, protocols); } void help(FILE * stream) { version (stream); fprintf(stream, "\n"); usage (stream); fprintf(stream, "\n"); description(stream); fprintf(stream, "\n"); options (stream); fprintf(stream, "\n"); args (stream); } /// Main int main(int argc, char * argv[]) { // Command line interface ARG_SPECIALS() ARG_DECLARE(role, NULL) ARG_DECLARE(family, DEFAULT_FAMILY) ARG_DECLARE(socktype, DEFAULT_SOCKTYPE) ARG_DECLARE(protocol, DEFAULT_PROTOCOL) ARG_DECLARE(node, DEFAULT_NODE) ARG_DECLARE(service, DEFAULT_SERVICE) ARG_POSITIONAL(role) ARG_GETOPT_BEGIN("f:t:p:n:s:") ARG_GETOPT_WITH_ARGUMENT('f', family) ARG_GETOPT_WITH_ARGUMENT('t', socktype) ARG_GETOPT_WITH_ARGUMENT('p', protocol) ARG_GETOPT_WITH_ARGUMENT('n', node) ARG_GETOPT_WITH_ARGUMENT('s', service) ARG_GETOPT_END() ARG_CONVERT_FIND(role, role, roles) ARG_CONVERT_FIND_CONST(family, families) ARG_CONVERT_FIND_CONST(socktype, socktypes) ARG_CONVERT_FIND_CONST(protocol, protocols) ARG_CONVERT_NULL(node) ARG_CONVERT_NULL(service) ARG_CATCH() // Implementation report_info(0, "Using buffer length %d", BUFLEN_MAX); role->run(initsockfd( role->init_name, role->init, role->flags, family, socktype, protocol, node, service )); } /// Implementation //// Common // `NI_MAXHOST` and `NI_MAXSERV` are non-standard. static char gai_host[1025]; static char gai_serv[32]; static int gai_errno; #define ACTION_NONE 0 #define ACTION_LEAVE 1 #define ACTION_JOIN 2 #define ACTION_MESSAGE 3 struct action { char const * name; int value; }; #define ACTION(ACTION) { #ACTION, ACTION_##ACTION } static struct action actions[] = { ACTION(NONE), ACTION(LEAVE), ACTION(JOIN), ACTION(MESSAGE), }; // TODO: Call and print results of `getsockname` and `getpeername`? int initsockfd( char const * init_name, int (*init)(int sockfd, struct sockaddr const * addr, socklen_t addrlen), int flags, int family, int socktype, int protocol, char const * node, char const * service ) { int sockfd; // Initialize socket. { struct addrinfo * addrinfos; // Get addresses. { // The assumption of these flags are non-standard. flags |= AI_V4MAPPED | AI_ADDRCONFIG; struct addrinfo hints = { flags, family, socktype, protocol, 0, 0, 0, 0 }; if (0 != (gai_errno = getaddrinfo( node, service, &hints, &addrinfos ))) report_fatal( gai_errno == EAI_SYSTEM ? errno : 0, "Failed to get %s addresses for '%s:%s': %s", init_name, node ? node : "", service ? service : "", gai_strerror(gai_errno) ); } // Try init on addresses until one works. struct addrinfo * addrinfo; for ( addrinfo = addrinfos; addrinfo != NULL; addrinfo = addrinfo->ai_next ) { // Get address name. if (0 != (gai_errno = getnameinfo( addrinfo->ai_addr, addrinfo->ai_addrlen, gai_host, sizeof(gai_host), gai_serv, sizeof(gai_serv), NI_NUMERICHOST | NI_NUMERICSERV ))) { report_error( gai_errno == EAI_SYSTEM ? errno : 0, "Failed to get %s address name: %s", init_name, gai_strerror(gai_errno) ); continue; } report_info(0, "Trying to %s to '%s:%s'...", init_name, gai_host, gai_serv ); // Create socket. if (-1 == (sockfd = socket( addrinfo->ai_family, addrinfo->ai_socktype, addrinfo->ai_protocol ))) { report_error(errno, "Failed to create %s socket", init_name); continue; } // Perform init. if (-1 == init( sockfd, addrinfo->ai_addr, addrinfo->ai_addrlen )) { report_error(errno, "Failed to %s to '%s:%s'", init_name, gai_host, gai_serv ); close(sockfd); continue; } // Succeeded. report_info(0, "Succeeded to %s to '%s:%s'", init_name, gai_host, gai_serv ); break; } // Fail. if (NULL == addrinfo) report_fatal(0, "Failed to %s", init_name); // Clean up. freeaddrinfo(addrinfos); } return sockfd; } //// Server // TODO: Also store `addlen` in `client` (and use in `memcmp`)? void run_server(int sockfd) { // Client data. struct client_data { char * user_name; }; // Client accounting. struct client { struct client * next; struct client * prev; struct sockaddr_storage addr; void * data; }; struct client * clients = NULL; // Allocate socket buffer. ssize_t sockbuflen; char * sockbuf = malloc(BUFLEN_MAX + 1); if (!sockbuf) report_fatal(errno, "Failed to allocate %s of size %d", "input buffer", BUFLEN_MAX + 1 ); // Talk to clients. while (1) { // Get client and action. // `NI_MAXHOST` and `NI_MAXSERV` are non-standard. struct sockaddr_storage addr; struct client * client; struct action action; { // Peek receive. ssize_t recvlen; socklen_t addrlen = sizeof(addr); if (-1 == (recvlen = recvfrom( sockfd, // TODO sockbuf, BUFLEN_MAX, // NULL, // 0, MSG_PEEK, // TODO: Also specify `MSG_TRUNC` so that we don't have to give the buffer? (struct sockaddr *)&addr, &addrlen ))) report_fatal(errno, "Failed to peek receive"); // Check for too large addresses (guaranteed not to happen). if (addrlen > sizeof(addr)) report_fatal(0, "Failed to store address of size %d, max size is %d", addrlen, sizeof(addr) ); // Get client address name. if (0 != (gai_errno = getnameinfo( (struct sockaddr *)&addr, addrlen, gai_host, sizeof(gai_host), gai_serv, sizeof(gai_serv), NI_NUMERICHOST | NI_NUMERICSERV ))) report_fatal( gai_errno == EAI_SYSTEM ? errno : 0, "Failed to get client address name: %s", gai_strerror(gai_errno) ); // Look up client. for ( client = clients; client != NULL; client = client->next ) if (0 == memcmp(&client->addr, &addr, sizeof(addr))) break; // Look up action. action = actions[!!recvlen<<1 | !!client<<0]; // Report. report_info(0, "Received %d byte%s from '%s:%s' %s", recvlen, recvlen == 1 ? "" : "s", gai_host, gai_serv, action.name ); } // No action. if (action.value == ACTION_NONE) { // Receive none. if (-1 == recv(sockfd, NULL, 0, 0)) report_fatal(errno, "Failed to receive %s", action.name); continue; } // Client left. else if (action.value == ACTION_LEAVE) { // Receive leave. if (-1 == recv(sockfd, NULL, 0, 0)) report_fatal(errno, "Failed to receive %s", action.name); // Client data. struct client_data * client_data = client->data; free(client_data->user_name); free(client_data); // Client accounting. remque(client); free(client); // Done. continue; } // Client joined. else if (action.value == ACTION_JOIN) { sockbuflen = 0; // Receive. { ssize_t recvlen; if (-1 == (recvlen = recv( sockfd, sockbuf, BUFLEN_MAX - sockbuflen, 0 ))) report_fatal(errno, "Failed to receive %s", action.name); sockbuflen += recvlen; } // Validate. if (!sockbuflen || sockbuf[sockbuflen - 1] != '\0') { report_error(0, "Malformed %s", action.name); } // Client data. struct client_data * client_data; if (NULL == (client_data = malloc(sizeof(*client_data)))) report_fatal(errno, "Failed to allocate %s of size %d", "client data", sizeof(*client_data) ); if (NULL == (client_data->user_name = strdup(sockbuf))) report_fatal(errno, "Failed to allocate %s", "client user name" ); // Client accounting. if (NULL == (client = calloc(1, sizeof(*client)))) report_fatal(errno, "Failed allocate %s of size %d", "client", sizeof(*client) ); insque(client, &clients); client->addr = addr; client->data = client_data; // Done. continue; } // Client sent message. else if (action.value == ACTION_MESSAGE) { sockbuflen = 0; // Client data. struct client_data * client_data = client->data; // TODO: `snprintf` returns how many bytes *would* have been // written! Use `strlen`? sockbuflen += snprintf( &sockbuf[sockbuflen], BUFLEN_MAX - sockbuflen, "[%s:%s] %s: ", gai_host, gai_serv, client_data->user_name ); // Receive message. { ssize_t recvlen; if (-1 == (recvlen = recv( sockfd, &sockbuf[sockbuflen], BUFLEN_MAX - sockbuflen, 0 ))) report_fatal(errno, "Failed to receive %s", action.name); sockbuflen += recvlen; } sockbuflen -= sockbuflen && sockbuf[sockbuflen - 1] == '\0'; // Finalize buffer. sockbuf[sockbuflen++] = '\0'; // Send message. for ( client = clients; client != NULL; client = client->next ) { if (-1 == sendto( sockfd, sockbuf, sockbuflen, 0, (struct sockaddr *)&client->addr, sizeof(client->addr) )) report_fatal(errno, "Failed to send %s", action.name); } } else { report_fatal(0, "Unexpected action %d", action.value); } } } //// Client static struct termios termios_stdin; static void termios_restore() { if (-1 == tcsetattr(STDIN_FILENO, TCSANOW, &termios_stdin)) report_fatal(errno, "Failed to restore terminal settings"); } static void termios_setup() { if (-1 == tcgetattr(STDIN_FILENO, &termios_stdin)) report_fatal(errno, "Failed to get terminal settings"); { struct termios termios = termios_stdin; termios.c_lflag &= ~(ICANON | ECHO); if (-1 == tcsetattr(STDIN_FILENO, TCSANOW, &termios)) report_fatal(errno, "Failed to set terminal settings"); } atexit(termios_restore); } void run_client(int sockfd) { // Allocate socket buffer. ssize_t sockbuflen; char * sockbuf = malloc(BUFLEN_MAX + 1); if (!sockbuf) report_fatal(errno, "Failed to allocate %s of size %d", "input buffer", BUFLEN_MAX + 1 ); // Allocate input buffer. ssize_t inputbuflen; char * inputbuf = malloc(BUFLEN_MAX + 1); if (!inputbuf) report_fatal(errno, "Failed to allocate %s of size %d", "socket buffer", BUFLEN_MAX + 1 ); // Join. { struct action action = actions[ACTION_JOIN]; // Get user name. printf("User name: "); fflush(stdout); if (-1 == (inputbuflen = read(STDIN_FILENO, inputbuf, BUFLEN_MAX))) report_fatal(errno, "Failed to read user name"); inputbuflen -= inputbuflen && inputbuf[inputbuflen - 1] == '\n'; // Send join. inputbuf[inputbuflen++] = '\0'; if (-1 == send(sockfd, inputbuf, inputbuflen, 0)) report_fatal(errno, "Failed to send %s", action.name); // if (-1 == recv(sockfd, sockbuf, BUFLEN_MAX, MSG_PEEK)) // report_fatal(errno, "Failed to receive %s", action.name); } // Setup terminal. termios_setup(); // Talk to user and server. inputbuflen = 0; while (1) { // Wait for data to be available from input or socket. fd_set rfds; FD_ZERO(&rfds); FD_SET(STDIN_FILENO, &rfds); FD_SET(sockfd, &rfds); if (-1 == select(sockfd+1, &rfds, NULL, NULL, NULL)) { printf("\n"); report_fatal(errno, "Failed to wait for data"); } // Clear input line. printf("\r%*s\r", (int)inputbuflen, ""); // Socket data available. if (FD_ISSET(sockfd, &rfds)) { // Receive. if (-1 == (sockbuflen = recv(sockfd, sockbuf, BUFLEN_MAX, 0))) report_fatal(errno, "Failed to receive"); sockbuf[sockbuflen++] = '\0'; // Print socket line. printf("%s\n", sockbuf); } // Input data available. if (FD_ISSET(STDIN_FILENO, &rfds)) { // Read. char c; if (-1 == read(STDIN_FILENO, &c, 1)) report_fatal(errno, "Failed to read"); // Printable with non-full buffer. if (isprint(c) && inputbuflen < BUFLEN_MAX) { // Add. inputbuf[inputbuflen++] = c; // Backspace. } else if (c == '\b' || c == 127) { // Remove last character from buffer. if (inputbuflen) --inputbuflen; // Enter. } else if (c == '\n' || c == '\r') { // Send. inputbuf[inputbuflen] = '\0'; if (-1 == send(sockfd, inputbuf, inputbuflen, 0)) report_fatal(errno, "Failed to send"); // Quit if input line empty. if (!inputbuflen) break; // Reset input line. inputbuflen = 0; } } // Print input line. // TODO: Have we not "finalized" the buffer here? printf("%.*s", (int)inputbuflen, inputbuf); fflush(stdout); } }