// 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");
}