sockchat.c
00a2dd39
 // 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`.
c8078bd5
 // 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?
00a2dd39
 // 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.
c8078bd5
 
 /// 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
00a2dd39
 #include "arg.h"
c8078bd5
 #include "report.h"
 
 
00a2dd39
 /// Constants
 
 //// Program
c8078bd5
 
 #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 \
00a2dd39
     PROGNAME " 1.0\n"
c8078bd5
 #define USAGE \
     "Usage:\n" \
     "  " PROGNAME " (server|client) [options]\n" \
     "  " PROGNAME " -h|--help\n" \
     "  " PROGNAME " --version\n"
00a2dd39
 #define DESCRIPTION \
     "Chat with unlimited number of peers through a variety of sockets.\n"
c8078bd5
 #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
00a2dd39
 #define BUFLEN_MAX _POSIX_MAX_CANON
c8078bd5
 
 
 /// Forward declarations
 
 //// Implementation
00a2dd39
 int initsockfd(
     char const * init_name,
     int (*init)(int sockfd, struct sockaddr const * addr, socklen_t addrlen),
c8078bd5
     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
00a2dd39
 #define ROLE(ROLE, INIT, FLAGS) { #ROLE, #INIT, INIT, FLAGS, run_##ROLE }
c8078bd5
 struct role {
     char const * name;
00a2dd39
     char const * init_name;
     int (*init)(int sockfd, struct sockaddr const * addr, socklen_t addrlen);
c8078bd5
     int flags;
     void (*run)(int sockfd);
 };
 static struct role roles[] = {
     ROLE(server, bind, AI_PASSIVE),
     ROLE(client, connect, 0),
 };
00a2dd39
 
 #define CONST_FAMILY(FAMILY) { #FAMILY, AF_##FAMILY }
 static struct arg_const families[] = {
     CONST_FAMILY(UNSPEC),
     CONST_FAMILY(INET),
     CONST_FAMILY(INET6),
     CONST_FAMILY(UNIX),
c8078bd5
 };
00a2dd39
 
 #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),
c8078bd5
 };
00a2dd39
 
 #define CONST_PROTOCOL(PROTOCOL) { #PROTOCOL, IPPROTO_##PROTOCOL }
 static struct arg_const protocols[] = {
c8078bd5
     { "0", 0 },
00a2dd39
     // CONST_PROTOCOL(IP),
     // CONST_PROTOCOL(IPV6),
     // CONST_PROTOCOL(ICMP),
     // CONST_PROTOCOL(TCP),
     // CONST_PROTOCOL(UDP),
     // CONST_PROTOCOL(RAW),
c8078bd5
 };
 
 
 /// Command line interface
00a2dd39
 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);
c8078bd5
 }
 void help(FILE * stream) {
00a2dd39
     version    (stream); fprintf(stream, "\n");
     usage      (stream); fprintf(stream, "\n");
     description(stream); fprintf(stream, "\n");
     options    (stream); fprintf(stream, "\n");
     args       (stream);
c8078bd5
 }
 
 
00a2dd39
 /// Main
 
c8078bd5
 int main(int argc, char * argv[]) {
     // Command line interface
00a2dd39
     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()
c8078bd5
 
     // Implementation
00a2dd39
     report_info(0, "Using buffer length %d", BUFLEN_MAX);
     role->run(initsockfd(
         role->init_name,
         role->init,
c8078bd5
         role->flags,
00a2dd39
         family,
         socktype,
         protocol,
c8078bd5
         node,
         service
     ));
 }
 
 
 /// Implementation
 
00a2dd39
 //// 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),
c8078bd5
     int flags,
     int family,
     int socktype,
     int protocol,
     char const * node,
     char const * service
 ) {
     int sockfd;
00a2dd39
     // Initialize socket.
c8078bd5
     {
         struct addrinfo * addrinfos;
         // Get addresses.
         {
00a2dd39
             // The assumption of these flags are non-standard.
             flags |= AI_V4MAPPED | AI_ADDRCONFIG;
             struct addrinfo hints = {
                 flags, family, socktype, protocol, 0, 0, 0, 0
             };
c8078bd5
             if (0 != (gai_errno = getaddrinfo(
                 node, service, &hints, &addrinfos
             )))
00a2dd39
                 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)
c8078bd5
                 );
         }
00a2dd39
         // Try init on addresses until one works.
c8078bd5
         struct addrinfo * addrinfo;
         for (
             addrinfo = addrinfos;
             addrinfo != NULL;
             addrinfo = addrinfo->ai_next
         ) {
00a2dd39
             // 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
             );
c8078bd5
             // Create socket.
             if (-1 == (sockfd = socket(
                 addrinfo->ai_family,
                 addrinfo->ai_socktype,
                 addrinfo->ai_protocol
             ))) {
00a2dd39
                 report_error(errno, "Failed to create %s socket", init_name);
c8078bd5
                 continue;
             }
00a2dd39
             // Perform init.
             if (-1 == init(
c8078bd5
                 sockfd,
                 addrinfo->ai_addr,
                 addrinfo->ai_addrlen
             )) {
00a2dd39
                 report_error(errno, "Failed to %s to '%s:%s'",
                     init_name, gai_host, gai_serv
                 );
c8078bd5
                 close(sockfd);
                 continue;
             }
             // Succeeded.
00a2dd39
             report_info(0, "Succeeded to %s to '%s:%s'",
                 init_name, gai_host, gai_serv
             );
c8078bd5
             break;
         }
00a2dd39
         // Fail.
c8078bd5
         if (NULL == addrinfo)
00a2dd39
             report_fatal(0, "Failed to %s", init_name);
c8078bd5
         // Clean up.
         freeaddrinfo(addrinfos);
     }
     return sockfd;
 }
 
 
00a2dd39
 //// Server
c8078bd5
 
00a2dd39
 // TODO: Also store `addlen` in `client` (and use in `memcmp`)?
 void run_server(int sockfd) {
c8078bd5
     // Client data.
     struct client_data {
00a2dd39
         char * user_name;
c8078bd5
     };
 
     // Client accounting.
     struct client {
         struct client * next;
         struct client * prev;
         struct sockaddr_storage addr;
00a2dd39
         void * data;
c8078bd5
     };
     struct client * clients = NULL;
 
00a2dd39
     // 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
         );
c8078bd5
 
     // Talk to clients.
     while (1) {
00a2dd39
         // Get client and action.
         // `NI_MAXHOST` and `NI_MAXSERV` are non-standard.
c8078bd5
         struct sockaddr_storage addr;
         struct client * client;
00a2dd39
         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?
c8078bd5
                 (struct sockaddr *)&addr,
00a2dd39
                 &addrlen
c8078bd5
             )))
00a2dd39
                 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)
c8078bd5
                 );
00a2dd39
             // Get client address name.
c8078bd5
             if (0 != (gai_errno = getnameinfo(
                 (struct sockaddr *)&addr,
                 addrlen,
00a2dd39
                 gai_host,
                 sizeof(gai_host),
                 gai_serv,
                 sizeof(gai_serv),
                 NI_NUMERICHOST | NI_NUMERICSERV
c8078bd5
             )))
00a2dd39
                 report_fatal(
                     gai_errno == EAI_SYSTEM ? errno : 0,
                     "Failed to get client address name: %s",
c8078bd5
                     gai_strerror(gai_errno)
                 );
00a2dd39
             // 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.
c8078bd5
             continue;
         }
         // Client sent message.
00a2dd39
         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);
c8078bd5
         }
     }
 }
 
 
00a2dd39
 //// 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);
 }
 
c8078bd5
 void run_client(int sockfd) {
     // Allocate socket buffer.
00a2dd39
     ssize_t sockbuflen;
     char * sockbuf = malloc(BUFLEN_MAX + 1);
c8078bd5
     if (!sockbuf)
00a2dd39
         report_fatal(errno, "Failed to allocate %s of size %d",
             "input buffer", BUFLEN_MAX + 1
c8078bd5
         );
 
     // Allocate input buffer.
00a2dd39
     ssize_t inputbuflen;
     char * inputbuf = malloc(BUFLEN_MAX + 1);
c8078bd5
     if (!inputbuf)
00a2dd39
         report_fatal(errno, "Failed to allocate %s of size %d",
             "socket buffer", BUFLEN_MAX + 1
c8078bd5
         );
 
     // Join.
     {
00a2dd39
         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);
a03fa824
         // if (-1 == recv(sockfd, sockbuf, BUFLEN_MAX, MSG_PEEK))
         //     report_fatal(errno, "Failed to receive %s", action.name);
c8078bd5
     }
 
00a2dd39
     // Setup terminal.
     termios_setup();
 
c8078bd5
     // 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.
00a2dd39
         printf("\r%*s\r", (int)inputbuflen, "");
c8078bd5
         // Socket data available.
         if (FD_ISSET(sockfd, &rfds)) {
             // Receive.
00a2dd39
             if (-1 == (sockbuflen = recv(sockfd, sockbuf, BUFLEN_MAX, 0)))
c8078bd5
                 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))
00a2dd39
                 report_fatal(errno, "Failed to read");
c8078bd5
             // Printable with non-full buffer.
00a2dd39
             if (isprint(c) && inputbuflen < BUFLEN_MAX) {
c8078bd5
                 // 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.
00a2dd39
                 inputbuf[inputbuflen] = '\0';
c8078bd5
                 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.
00a2dd39
         // TODO: Have we not "finalized" the buffer here?
         printf("%.*s", (int)inputbuflen, inputbuf);
c8078bd5
         fflush(stdout);
     }
 }