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