I followed the Beej's Guide to Network Programming. The program acts like a multi-user chat server. One connects and sends a message to the server, which is forwarded to all that are connected. There's a simple makefile below; also Valgrind output.
Review Goals:
I have specific questions:
- Is my code POSIX compliant?
- Do you see any outdated practices or functions?
- Does any part of my code invoke undefined behaviour?
- Is it portable (across UNIX-like systems)?
- How much more information can I glean about a client from a given file descriptor?
- Is there an alternative to
errno
/perror
style of error checking? - Currently, I'm only logging the client's host and service name. What other sort of information should I log? Should I look into syslog?
- Do you see any potential buffer overflows?
- How can I improve the code?
Code:
log.h:
#ifndef LOG_H
#define LOG_H
#define LOG_TIME 0x01 /* 0b00000001 */
#define LOG_DATE 0x02 /* 0b00000010 */
#define LOG_USER 0x04 /* 0b00000100 */
#define LOG_COUNT 0x08 /* 0b00001000 */
#define LOG_ALL 0xFF /* 0xFFFFFFFF */
#define LOG_FULLTIME 0x03 /* 0x00000011 */
#define TS_BUF_LENGHT 50
void log_msg (FILE * fp, const char *msg, uint8_t options);
#endif
log.c:
void log_msg (FILE * fp, const char *msg, uint8_t options)
{
time_t time_val;
static long long log_count = 0;
char time_stamp[TS_BUF_LENGHT];
char date_stamp[TS_BUF_LENGHT];
struct tm *tm_info;
time_val = time (0);
tm_info = localtime (&time_val);
(void) strftime (date_stamp, TS_BUF_LENGHT, "%F (%a)", tm_info);
(void) strftime (time_stamp, TS_BUF_LENGHT, "%H:%M:%S", tm_info);
if (options & LOG_COUNT) {
fprintf (fp, "%lld\n, ", ++log_count);
}
if (options & LOG_DATE) {
fprintf (fp, "%s, ", date_stamp);
}
if (options & LOG_TIME) {
fprintf (fp, "%s, ", time_stamp);
}
fprintf (fp, "%s\n", msg);
}
selectserver.h:
#ifndef SELECTSERVER_H
#define SELECTSERVER_H
#define _POSIX_C_SOURCE 200809
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#define PROGRAM_NAME "selectserver"
#define PORT "9808" /* Port we are listening on */
#define MAX_LOG_TEXT 2048 /* Max text lenght for logging */
#ifdef BUFSIZ /* Max text lenght */
#define BUFSIZE BUFSIZ
#else
#define BUFSIZE 4096
#endif
#define ARRAY_CARDINALITY(x) (sizeof(x) / sizeof (x[0]))
#define NI_MAXHOST 1024
#define NI_MAXSERV 35
#define FFLUSH(fp) if (fflush (fp) == EOF) perror ("fflush()")
#define FSYNC(fd) if (fsync (fd) == -1) perror ("fsync()")
#endif
selectserver.c:
*/
* selectserver.c -- a multiperson chat server
*/
#include "selectserver.h"
#include "log.h"
FILE *log_fp = 0;
int log_fd = 0;
static void exit_cleanup (void)
{
if (log_fp) {
(void) fclose (log_fp);
}
}
static void sigint_handler (int sig)
{
(void) close (log_fd);
(void) signal (sig, SIG_DFL);
(void) raise (sig);
}
static int init_addr (struct addrinfo **servinfo)
{
struct addrinfo hints;
memset (&hints, 0x00, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
int status = 0;
errno = 0;
if ((status = getaddrinfo (0, PORT, &hints, servinfo)) != 0) {
perror ("getaddrinfo()");
return -1;
}
return 0;
}
static int init_sock (struct addrinfo **servinfo)
{
int master_fd = 0;
struct addrinfo *p = 0;
int yes = 1;
socklen_t size = sizeof (yes);
errno = 0;
for (p = *servinfo; p; p = p->ai_next) {
master_fd = socket (p->ai_family, p->ai_socktype, p->ai_protocol);
if (master_fd == -1) {
perror ("socket()");
continue;
}
errno = 0;
if (setsockopt
(master_fd, SOL_SOCKET, SO_REUSEADDR, (void *) &yes,
size) == -1) {
perror ("setsockopt()");
continue;
}
errno = 0;
if (bind (master_fd, p->ai_addr, p->ai_addrlen) == -1) {
close (master_fd);
perror ("bind");
continue;
}
break;
}
if (!p) {
(void) fprintf (stderr, "%s: Failed to setup a socket.\n",
PROGRAM_NAME);
return -1;
}
errno = 0;
if (listen (master_fd, SOMAXCONN) == -1) {
perror ("listen()");
return -1;
}
return master_fd;
}
/* Synopsis: Writes the host and service names to stderr and log file.
*/
static void write_slave_info (int slave_fd, struct sockaddr slave_addr,
socklen_t addr_len)
{
char log_txt[MAX_LOG_TEXT] = { 0 };
char host[NI_MAXHOST] = { 0 };
char service[NI_MAXSERV] = { 0 };
int ret_val = 0;
errno = 0;
if ((ret_val =
getnameinfo (&slave_addr, addr_len, host, sizeof host, service,
sizeof service, NI_NUMERICHOST)) != 0) {
(void) fprintf (stderr, "%s: getnameinfo: %s\n", PROGRAM_NAME,
gai_strerror (ret_val));
}
(void) snprintf (log_txt, sizeof log_txt,
"INFO: New connection from HOST:%s, SERVICE:%s, on socket %d.",
host, service, slave_fd);
log_msg (stderr, log_txt, LOG_FULLTIME);
log_msg (log_fp, log_txt, LOG_FULLTIME);
errno = 0;
FFLUSH (log_fp);
errno = 0;
FSYNC (log_fd);
}
/* Returns: The slave file descriptor on success, -1 otherwise.
*/
static int accept_new_connection (int master_fd)
{
int slave_fd = 0;
struct sockaddr slave_addr = { 0 };
socklen_t addr_len = sizeof slave_addr;
int yes = 1;
socklen_t size_yes = sizeof yes;
int flag = 5;
socklen_t size_flag = sizeof flag;
errno = 0;
do {
errno = 0;
slave_fd = accept (master_fd, &slave_addr, &addr_len);
} while ((slave_fd == -1) && (errno == EINTR));
if (slave_fd == -1) {
perror ("accept()");
return -1;
}
errno = 0;
if (setsockopt
(slave_fd, SOL_SOCKET, SO_KEEPALIVE, (void *) &yes,
size_yes) == -1) {
perror ("setsockopt()");
return -1;
}
errno = 0;
if (setsockopt
(slave_fd, IPPROTO_TCP, TCP_KEEPCNT, (void *) &flag,
size_flag) == -1) {
perror ("setsockopt()");
return -1;
}
errno = 0;
if (setsockopt
(slave_fd, IPPROTO_TCP, TCP_KEEPINTVL, (void *) &flag,
size_flag) == -1) {
perror ("setsockopt()");
return -1;
}
write_slave_info (slave_fd, slave_addr, addr_len);
return slave_fd;
}
static void write_farewell (int slave_fd)
{
char log_txt[MAX_LOG_TEXT] = { 0 };
(void) snprintf (log_txt, sizeof log_txt,
"INFO: Socket %d hung up.\n", slave_fd);
log_msg (stderr, log_txt, LOG_FULLTIME);
log_msg (log_fp, log_txt, LOG_FULLTIME);
FFLUSH (log_fp);
FSYNC (log_fd);
}
/* Synopsis: Get the number of bytes that are immediately available for
* reading.
* Returns: -1 on failure, number of bytes available elsewise.
*/
static int get_bytes (int slave_fd)
{
int flag = 0;
errno = 0;
if (ioctl (slave_fd, FIONREAD, &flag) == -1) {
perror ("ioctl()");
return -1;
}
return flag;
}
/* Synopsis: Calls recv() in a loop to read as much as available.
*
* Returns: 0 on allocation failure, storing 0 in status, a pointer
* to the line elsewise.
* Stores -1 in status in case of a closed connection
* or a recv() error.
*
* Remarks: The caller is responsible for freeing the returned
* memory (in case of success), else we risk exhaustion.
*/
static char *read_line (size_t *nbytes, int slave_fd, int *status)
{
static const size_t page_size = BUFSIZE;
char *buf = realloc (0, page_size);
if (!buf) {
errno = ENOMEM;
perror ("realloc()");
free (buf);
status = 0;
return 0;
}
int flag = 0;
ssize_t ret_val = 0;
size_t total = 0;
do {
ret_val = recv (slave_fd, buf + ret_val, page_size - 1, 0);
if (ret_val > 0) {
total += (size_t) ret_val;
buf[ret_val] = '\0';
flag = get_bytes (slave_fd);
if (flag == -1) {
free (buf);
*status = -1;
return 0;
}
if (flag > 0) {
if (flag > ((page_size - 1) - (size_t) ret_val)) {
char *new = realloc (buf, page_size + page_size);
if (!new) {
errno = ENOMEM;
perror ("realloc()");
free (buf);
status = 0;
return 0;
}
}
}
} else {
flag = 0;
}
} while (flag > 0);
/* Slave closed the connection
*/
if (ret_val == 0) {
write_farewell (slave_fd);
free (buf);
*status = -1;
return 0;
}
/* We have an error, sire!
*/
if (ret_val == -1) {
perror ("recv()");
free (buf);
*status = -1;
}
*nbytes = total;
return buf;
}
/* Synopsis: Calls send() in a loop to ensure that all data is sent.
Stores the number of bytes sent in len.
* Returns: 0 on success, -1 otherwise.
*/
static int send_all (int slave_fd, const char *line, size_t *len)
{
size_t total = 0;
size_t bytes_left = *len;
ssize_t status = 0;
while (total < *len) {
errno = 0;
status = send (slave_fd, line + total, bytes_left, 0);
if (status == -1) { /* A send() error */
if (errno == EINTR) {
continue;
}
perror ("send()");
break;
}
total += (size_t) status;
bytes_left -= (size_t) status;
}
*len = total;
return status == -1 ? -1 : 0;
}
static void send_msg (size_t nbytes, const char *line, int sender_fd,
int master_fd, fd_set master, int fd_max)
{
for (int i = 0; i <= fd_max; i++) {
/* Send it to everyone */
if (FD_ISSET (i, &master)) {
/* Excluding the master and sender */
if (i != master_fd && i != sender_fd) {
size_t len = nbytes;
int status = send_all (i, line, &len);
if (status == -1) {
perror ("send()");
} else if (len != nbytes) {
(void) fprintf (stderr,
"%s: We only sent %ld bytes because of a send() error.\n",
PROGRAM_NAME, len);
}
}
}
}
}
/* Synopsis: Calls select and handles new connections.
* Returns: -1 on failure to init select or ENOMEM, 0 otherwise.
*/
static int handle_connections (int master_fd)
{
fd_set master; /* Master file descriptor list */
fd_set read_fds; /* Temp file descriptor list for select() */
int fd_max = 0; /* Max descriptor seen so far */
FD_ZERO (&master);
FD_ZERO (&read_fds);
FD_SET (master_fd, &master);
fd_max = master_fd;
for (;;) {
errno = 0;
/* Because select is destructive */
read_fds = master;
if (select (fd_max + 1, &read_fds, 0, 0, 0) == -1) {
if (errno == EINTR) {
continue;
}
perror ("select()");
return -1;
}
/* Iterate through the existing connections looking for data to read */
for (int i = 0; i <= fd_max; i++) {
/* We have a connection */
if (FD_ISSET (i, &read_fds)) {
/* It's the master */
if (i == master_fd) {
int slave_fd = 0;
if ((slave_fd =
accept_new_connection (master_fd)) != -1) {
FD_SET (slave_fd, &master);
}
if (slave_fd > fd_max) {
fd_max = slave_fd;
}
} /* We have data to read */
else {
size_t nbytes = 0;
int status = 0;
char *line = read_line (&nbytes, i, &status);
/* A read error, or the slave closed connection */
if (!line && !status) {
return -1; /* ENOMEM */
} else if (status == -1) {
FD_CLR (i, &master);
close (i);
} else {
send_msg (nbytes, line, i, master_fd, master,
fd_max);
free (line);
}
}
}
}
}
return 0;
}
#define ARRAY_CARDINALITY(x) (sizeof(x) / sizeof (x[0]))
int main (void)
{
static sigset_t caught_signals;
sigemptyset (&caught_signals);
static int const sig[] = {
SIGALRM, SIGHUP, SIGINT,
SIGPIPE, SIGQUIT, SIGTERM,
};
const size_t nsigs = ARRAY_CARDINALITY (sig);
struct sigaction act;
for (size_t i = 0; i < nsigs; i++) {
if (sigaction (sig[i], 0, &act) == -1) {
perror ("sigaction()");
return EXIT_FAILURE;
}
if (act.sa_handler != SIG_IGN) {
sigaddset (&caught_signals, sig[i]);
}
}
act.sa_handler = sigint_handler;
act.sa_mask = caught_signals;
act.sa_flags = 0;
for (size_t i = 0; i < nsigs; i++) {
if (sigismember (&caught_signals, sig[i])) {
if (sigaction (sig[i], &act, 0) == -1) {
perror ("sigaction()");
return EXIT_FAILURE;
}
}
}
atexit (exit_cleanup);
errno = 0;
log_fp = fopen ("server.log", "a");
if (!log_fp) {
perror ("fopen()");
return EXIT_FAILURE;
}
errno = 0;
log_fd = fileno (log_fp);
if (log_fd == -1) {
perror ("fileno()");
return EXIT_FAILURE;
}
int master_fd = 0; /* One descriptor to rule them all */
struct addrinfo *servinfo;
/* Set up the server address */
if (init_addr (&servinfo) == -1) {
return EXIT_FAILURE;
}
/* Open a socket and set it to listening */
if ((master_fd = init_sock (&servinfo)) == -1) {
freeaddrinfo (servinfo);
return EXIT_FAILURE;
}
freeaddrinfo (servinfo);
/* Wait for and eventually handle a new connection */
(void) printf ("\nListening for connections on port %s.\n", PORT);
if (handle_connections (master_fd) == -1) {
close (master_fd);
return EXIT_FAILURE;
}
close (master_fd);
return EXIT_SUCCESS;
}
Formatting:
indent -kr -cs -pcs -saf -sai -saw --no-tabs --case-indentation 4 selectserver.c selectserver.h log.h log.c
Makefile:
# the compiler:
CC = gcc-10
# compiler flags:
CFLAGS = -O2 -pedantic-errors -Wall -Wfloat-equal -Wundef \
-Wshadow -Wpointer-arith -Wcast-align -Wstrict-prototypes \
-Wstrict-overflow=5 -Wconversion -Wformat-overflow=2 \
-Wcast-qual -Wformat-signedness -Wformat-truncation=1\
-Wunreachable-code -fanalyzer -Wformat=2 -fno-builtin \
-std=c17 -Winit-self -Wmaybe-uninitialized \
-Wuninitialized -Werror=implicit-function-declaration \
-Wmisleading-indentation -Wmissing-braces \
-Wstrict-prototypes -Wmissing-prototypes -Wredundant-decls \
-Wundef -fno-omit-frame-pointer -fno-common \
-fsanitize=address -fsanitize=undefined
TARGET = selectserver1.1
OBJS = selectserver.o log.o
DEPS = log.h selectserver.h
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(CFLAGS)
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
clean:
rm -rf *.o
Dynamic analysis:
/* Valgrind's dump after I sent 100 simultaneous connections with a message to the server.
* I exited with SIGINT (or Ctrl-C).
*
* Flags passed: valgrind --leak-check=full --show-leak-kinds=all --show-error-list=yes -s
* --leak-check-heuristics=all --log-file=dynamic_analysis ./selectserver1.1
*/
==1466== Memcheck, a memory error detector
==1466== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1466== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==1466== Command: ./selectserver1.1
==1466== Parent PID: 8
==1466==
==1466== error calling PR_SET_PTRACER, vgdb might block
==1466==
==1466== Process terminating with default action of signal 2 (SIGINT)
==1466== at 0x497FF7A: select (select.c:41)
==1466== by 0x10A0A4: handle_connections (selectserver.c:326)
==1466== by 0x10A565: main (selectserver.c:440)
==1466==
==1466== HEAP SUMMARY:
==1466== in use at exit: 472 bytes in 1 blocks
==1466== total heap usage: 575 allocs, 574 frees, 1,352,203 bytes allocated
==1466==
==1466== 472 bytes in 1 blocks are still reachable in loss record 1 of 1
==1466== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==1466== by 0x48ED92D: __fopen_internal (iofopen.c:65)
==1466== by 0x48ED92D: fopen@@GLIBC_2.2.5 (iofopen.c:86)
==1466== by 0x10A4DC: main (selectserver.c:412)
==1466==
==1466== LEAK SUMMARY:
==1466== definitely lost: 0 bytes in 0 blocks
==1466== indirectly lost: 0 bytes in 0 blocks
==1466== possibly lost: 0 bytes in 0 blocks
==1466== still reachable: 472 bytes in 1 blocks
==1466== suppressed: 0 bytes in 0 blocks
==1466==
==1466== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)