// TODO: Add `listen` and `accept` for 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? /// 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 "args.h" #include "report.h" /// Parameters //// Application #define PROGNAME "sockchat" #define VERSION_STR "1.0" #define DESCRIPTION \ "Chat with unlimited number of peers through a variety of sockets" //// 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 " " VERSION_STR " - " DESCRIPTION "\n" #define USAGE \ "Usage:\n" \ " " PROGNAME " (server|client) [options]\n" \ " " PROGNAME " -h|--help\n" \ " " PROGNAME " --version\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 BUF_SIZE _POSIX_MAX_CANON /// Forward declarations //// Implementation int getsockfd( char const * action_name, int (*action)(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 struct role { char const * name; char const * action_name; int (*action)(int sockfd, struct sockaddr const * addr, socklen_t addrlen); int flags; void (*run)(int sockfd); }; struct option { char const * name; int value; }; #define ROLE(NAME, ACTION, FLAGS) { #NAME, #ACTION, ACTION, FLAGS, run_##NAME } static struct role roles[] = { ROLE(server, bind, AI_PASSIVE), ROLE(client, connect, 0), }; #define OPTION_FAMILY(FAMILY) { #FAMILY, AF_##FAMILY } static struct option families[] = { OPTION_FAMILY(UNSPEC), OPTION_FAMILY(UNIX), OPTION_FAMILY(LOCAL), OPTION_FAMILY(INET), OPTION_FAMILY(INET6), OPTION_FAMILY(PACKET), }; #define OPTION_SOCKTYPE(SOCKTYPE) { #SOCKTYPE, SOCK_##SOCKTYPE } static struct option socktypes[] = { { "0", 0 }, OPTION_SOCKTYPE(STREAM), OPTION_SOCKTYPE(DGRAM), OPTION_SOCKTYPE(SEQPACKET), OPTION_SOCKTYPE(RAW), OPTION_SOCKTYPE(RDM), OPTION_SOCKTYPE(PACKET), }; #define OPTION_PROTOCOL(PROTOCOL) { #PROTOCOL, IPPROTO_##PROTOCOL } static struct option protocols[] = { { "0", 0 }, OPTION_PROTOCOL(IP), OPTION_PROTOCOL(TCP), OPTION_PROTOCOL(UDP), OPTION_PROTOCOL(UDPLITE), OPTION_PROTOCOL(SCTP), OPTION_PROTOCOL(ICMP), }; /// Command line interface void version(FILE * stream) { fprintf(stream, "%s", VERSION); } void usage (FILE * stream) { fprintf(stream, "%s", USAGE); } void options(FILE * stream) { fprintf(stream, "%s", OPTIONS); } void args (FILE * stream) { ARGS_PRINT(stream, family, families ); fprintf(stream, "\n"); ARGS_PRINT(stream, socktype, socktypes); fprintf(stream, "\n"); ARGS_PRINT(stream, protocol, protocols); } void help(FILE * stream) { version(stream); fprintf(stream, "\n"); usage (stream); fprintf(stream, "\n"); options(stream); fprintf(stream, "\n"); args (stream); } /// main int main(int argc, char * argv[]) { // Command line interface ARGS_SPECIALS() ARGS_DECLARE(role, NULL) ARGS_DECLARE(family, DEFAULT_FAMILY) ARGS_DECLARE(socktype, DEFAULT_SOCKTYPE) ARGS_DECLARE(protocol, DEFAULT_PROTOCOL) ARGS_DECLARE(node, DEFAULT_NODE) ARGS_DECLARE(service, DEFAULT_SERVICE) ARGS_POSITIONAL(role) ARGS_OPTIONS_BEGIN("f:t:p:n:s:") ARGS_OPTION_WITH_ARGUMENT('f', family) ARGS_OPTION_WITH_ARGUMENT('t', socktype) ARGS_OPTION_WITH_ARGUMENT('p', protocol) ARGS_OPTION_WITH_ARGUMENT('n', node) ARGS_OPTION_WITH_ARGUMENT('s', service) ARGS_OPTIONS_END() ARGS_CONVERT_FIND(role, role, roles) ARGS_CONVERT_FIND(option, family, families) ARGS_CONVERT_FIND(option, socktype, socktypes) ARGS_CONVERT_FIND(option, protocol, protocols) ARGS_CONVERT_NULL(node) ARGS_CONVERT_NULL(service) ARGS_CATCH() // Implementation report_info(0, "Using buffer size %d", BUF_SIZE); role->run(getsockfd( role->action_name, role->action, role->flags, family->value, socktype->value, protocol->value, node, service )); } /// Implementation //// getsockfd int getsockfd( char const * action_name, int (*action)(int sockfd, struct sockaddr const * addr, socklen_t addrlen), int flags, int family, int socktype, int protocol, char const * node, char const * service ) { int gai_errno; int sockfd; { struct addrinfo * addrinfos; // Get addresses. { // Populate hints. // The extra flags are assumed by the GNU C library if no hints are // given (in contradiction to POSIX), and are a good idea. struct addrinfo hints; memset(&hints, 0, sizeof(hints)); hints.ai_flags = flags | AI_V4MAPPED | AI_ADDRCONFIG; hints.ai_family = family; hints.ai_socktype = socktype; hints.ai_protocol = protocol; // Query. if (0 != (gai_errno = getaddrinfo( node, service, &hints, &addrinfos ))) report_fatal(gai_errno == EAI_SYSTEM ? errno : 0, "Failed to get addresses for '%s:%s': %s", node, service, gai_strerror(gai_errno) ); } // Try action on addresses until one works. report_info(0, "Trying to %s...", action_name); struct addrinfo * addrinfo; for ( addrinfo = addrinfos; addrinfo != NULL; addrinfo = addrinfo->ai_next ) { // Create socket. if (-1 == (sockfd = socket( addrinfo->ai_family, addrinfo->ai_socktype, addrinfo->ai_protocol ))) { report_info(errno, "> Failed to create socket"); continue; } // Perform action. if (-1 == action( sockfd, addrinfo->ai_addr, addrinfo->ai_addrlen )) { close(sockfd); report_info(errno, "> Failed to %s", action_name); continue; } // Succeeded. report_info(0, "Succeeded to %s", action_name); break; } if (NULL == addrinfo) report_fatal(0, "Failed to get socket"); // Clean up. freeaddrinfo(addrinfos); } return sockfd; } //// run_server void run_server(int sockfd) { int gai_errno; // Client data. struct client_data { char * name; }; // Client accounting. struct client { struct client * next; struct client * prev; struct sockaddr_storage addr; struct client_data data; }; struct client * clients = NULL; // Allocate buffer. int buflen; char * buf = malloc(BUF_SIZE); if (!buf) report_fatal(errno, "Failed to allocate buffer of size %d", BUF_SIZE); // Talk to clients. while (1) { // Peek receive length and address. int recvlen; struct sockaddr_storage addr; socklen_t addrlen = sizeof(addr); if (-1 == (recvlen = recvfrom( sockfd, buf, BUF_SIZE - 1, MSG_PEEK, (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, can only hold %d", addrlen, sizeof(addr) ); // Look up client. struct client * client; for ( client = clients; client != NULL; client = client->next ) if (0 == memcmp(&client->addr, &addr, sizeof(addr))) break; // Client left. if (!recvlen) { if (-1 == recv(sockfd, NULL, 0, 0)) report_fatal(errno, "Failed to receive leave"); if (client) { // Data. report_info(0, "Client '%s' left", client->data.name); free(client->data.name); // Accounting. remque(client); free(client); } continue; } // Client joined. if (!client) { // Accounting. if (NULL == (client = calloc(1, sizeof(*client)))) report_fatal(errno, "Failed allocate client of size %d", sizeof(*client) ); insque(client, &clients); client->addr = addr; // Data. buflen = 0; // Add open delimiter. buflen += snprintf(&buf[buflen], BUF_SIZE - buflen - 1, "["); // Get client address node name. if (0 != (gai_errno = getnameinfo( (struct sockaddr *)&addr, addrlen, &buf[buflen], BUF_SIZE - buflen - 1, NULL, 0, 0 ))) report_error(gai_errno == EAI_SYSTEM ? errno : 0, "Failed to get client address node name: %s", gai_strerror(gai_errno) ); buflen += strlen(&buf[buflen]); // Add separator. buflen += snprintf(&buf[buflen], BUF_SIZE - buflen - 1, ":"); // Get client address service name. if (0 != (gai_errno = getnameinfo( (struct sockaddr *)&addr, addrlen, NULL, 0, &buf[buflen], BUF_SIZE - buflen - 1, 0 ))) report_error(gai_errno == EAI_SYSTEM ? errno : 0, "Failed to get client address service name: %s", gai_strerror(gai_errno) ); buflen += strlen(&buf[buflen]); // Add close delimiter. buflen += snprintf(&buf[buflen], BUF_SIZE - buflen - 1, "] "); // Get client user name. if (-1 == (recvlen = recv( sockfd, &buf[buflen], BUF_SIZE - buflen - 1, 0 ))) report_fatal(errno, "Failed to receive client user name"); buflen += recvlen; buflen -= buflen && buf[buflen - 1] == '\0'; // Store. buf[buflen++] = '\0'; client->data.name = strdup(buf); report_info(0, "Client '%s' joined", client->data.name); continue; } // Client sent message. report_info(0, "Client '%s' sent %d byte%s", client->data.name, recvlen, recvlen == 1 ? "" : "s" ); buflen = 0; // Add client user name and separator. buflen += snprintf( &buf[buflen], BUF_SIZE - buflen - 1, "%s: ", client->data.name ); // Add message. if (-1 == (recvlen = recv( sockfd, &buf[buflen], BUF_SIZE - buflen - 1, 0 ))) report_fatal(errno, "Failed to receive"); buflen += recvlen; buflen -= buflen && buf[buflen - 1] == '\0'; // Send message to all clients. buf[buflen++] = '\0'; for ( client = clients; client != NULL; client = client->next ) { if (-1 == sendto( sockfd, buf, buflen, 0, (struct sockaddr *)&client->addr, sizeof(client->addr) )) report_fatal(errno, "Failed to send"); } } } //// run_client void run_client(int sockfd) { // Allocate socket buffer. int sockbuflen; char * sockbuf = malloc(BUF_SIZE); if (!sockbuf) report_fatal(errno, "Failed to allocate socket buffer of size %d", BUF_SIZE ); // Allocate input buffer. int inputbuflen; char * inputbuf = malloc(BUF_SIZE); if (!inputbuf) report_fatal(errno, "Failed to allocate input buffer of size %d", BUF_SIZE ); // Join. // Get user name. printf("User name: "); fflush(stdout); if (-1 == (inputbuflen = read(STDIN_FILENO, inputbuf, BUF_SIZE - 1))) 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 join"); // Setup terminal. struct termios termios_stdin; 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"); } // 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", inputbuflen, ""); // Socket data available. if (FD_ISSET(sockfd, &rfds)) { // Receive. if (-1 == (sockbuflen = recv(sockfd, sockbuf, BUF_SIZE - 1, 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 input"); // Printable with non-full buffer. if (isprint(c) && inputbuflen < BUF_SIZE - 1) { // 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. if (inputbuflen) 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. printf("%.*s", inputbuflen, inputbuf); fflush(stdout); } // Restore terminal. if (-1 == tcsetattr(STDIN_FILENO, TCSANOW, &termios_stdin)) report_fatal(errno, "Failed to restore terminal settings"); }