~jscott/daytimed

5bf23439057947b9eabb8e185d9a55fba0a9d94e — John Scott 10 months ago master
Initial commit
2 files changed, 338 insertions(+), 0 deletions(-)

A daytimed.c
A meson.build
A  => daytimed.c +282 -0
@@ 1,282 @@
/* SPDX-FileCopyrightText: 2023 John Scott <jscott@posteo.net>
 * SPDX-License-Identifier: GPL-3.0-or-later
 * Other license terms may be negotiated by contacting the author.
 *
 * This is a TCP daytime server that runs on the conventional port.
 * The output is dependent on, and formatted in accordance with, the locale.
 * It's extremely simple and does not use multithreading or multiprocessing. */

#define _POSIX_C_SOURCE 200809L
#include <assert.h>
#include <errno.h>
#include <iconv.h>
#include <langinfo.h>
#include <locale.h>
#include <netdb.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>

static atomic_bool interrupted = false;
static void interrupt(int signo) {
	interrupted = true;
}

int main(void) {
	if(!setlocale(LC_ALL, "")) {
		fputs("Failed to enable default locale\n", stderr);
		exit(EXIT_FAILURE);
	}

	if(!atomic_is_lock_free(&interrupted)) {
		/* This means we cannot safely access interrupted from the signal handler. */
		fputs("Atomic booleans are not lock-free\n", stderr);
		exit(EXIT_FAILURE);
	}

	struct addrinfo *res;
	const struct addrinfo hints = {
		.ai_family = AF_UNSPEC,
		.ai_protocol = IPPROTO_TCP,
		.ai_flags = AI_PASSIVE
	};
	int s = getaddrinfo(NULL, "daytime", &hints, &res);
	if(s) {
		fprintf(stderr, "Failed to get address information: %s\n", (s == EAI_SYSTEM) ? strerror(errno) : gai_strerror(s));
		exit(EXIT_FAILURE);
	}

	int sock;
	/* Because getaddrinfo() was successful, we should have at least one parameter combo to try. */
	assert(res);
	for(const struct addrinfo *info = res; info; info = info->ai_next) {
		sock = socket(info->ai_family, info->ai_socktype, info->ai_protocol);
		if(sock == -1) {
			perror("Failed to open socket");
			continue;
		}

		if(bind(sock, info->ai_addr, info->ai_addrlen) == -1) {
			perror("Failed to bind to an address");
			if(close(sock) == -1) {
				perror("Failed to close socket");
			}
			sock = -1;
			continue;
		}

		if(setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &(int){0}, sizeof(int)) == -1) {
			perror("Failed to disable receiving");
			if(close(sock) == -1) {
				perror("Failed to close socket");
			}
			sock = -1;
			continue;
		}

		if(listen(sock, SOMAXCONN) == -1) {
			perror("Failed to listen");
			if(close(sock) == -1) {
				perror("Failed to close socket");
			}
			sock = -1;
		} else {
			break;
		}
	}
	freeaddrinfo(res);

	if(sock == -1) {
		exit(EXIT_FAILURE);
	}

	/* Allow the process to exit gracefully when termination is requested. */
	struct sigaction sigact = {
		.sa_handler = interrupt
	};
	if(sigemptyset(&sigact.sa_mask) == -1) {
		perror("Failed to empty signal set");
		if(close(sock) == -1) {
			perror("Failed to close socket");
		}
		exit(EXIT_FAILURE);
	}
	if(sigaction(SIGINT, &sigact, NULL) == -1 || sigaction(SIGTERM, &sigact, NULL) == -1 || signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
		perror("Failed to register signal handler");
		if(close(sock) == -1) {
			perror("Failed to close socket");
		}
		exit(EXIT_FAILURE);
	}

	/* We will use this same conversion descriptor over and over, never closing it until termination time.
	 * Note that if nl_langinfo() fails it returns the string "", which is actually desirable behavior for us. */
	const iconv_t conv = iconv_open("US-ASCII", nl_langinfo(CODESET));
	if(conv == (iconv_t)-1) {
		perror("Failed to get codeset conversion descriptor");
		if(close(sock) == -1) {
			perror("Failed to close socket");
		}
		exit(EXIT_FAILURE);
	}

	/* This is the buffer we'll use over and over again to store the time expression in the locale's encoding.
	 * Note that BUFSIZ is an arbitrary initial size for the buffer. */
	size_t timelen = BUFSIZ;
	char *timebuf = malloc(timelen);
	if(!timebuf) {
		perror("Failed to allocate memory");
		if(iconv_close(conv) == -1) {
			perror("Failed to close codeset conversion descriptor");
		}
		if(close(sock) == -1) {
			perror("Failed to close socket");
		}
		exit(EXIT_FAILURE);
	}

	/* In a similar regard, here's a buffer we recycle for getting the peer's hostname. */
	socklen_t peerhostnamelen = BUFSIZ;
	char *peerhostname = malloc(peerhostnamelen);
	if(!peerhostname) {
		perror("Failed to allocate memory");
		free(timebuf);
		if(iconv_close(conv) == -1) {
			perror("Failed to close codeset conversion descriptor");
		}
		if(close(sock) == -1) {
			perror("Failed to close socket");
		}
		exit(EXIT_FAILURE);
	}

	/* Now we are ready. */
nextconn:
	while(!interrupted) {
		union {
			struct sockaddr_storage ss_addr;
			struct sockaddr sa_addr;
		} peeraddr = {0};
		socklen_t peeraddrlen = sizeof(peeraddr);

		const int connection = accept(sock, &peeraddr.sa_addr, &peeraddrlen);
		if(connection == -1) {
			perror("Failed to accept connection");
			continue;
		}
		assert(peeraddrlen <= sizeof(peeraddr));

		const time_t now = time(NULL);
		if(now == (time_t)-1) {
			perror("Failed to obtain current time");
			if(close(connection) == -1) {
				perror("Failed to close connection");
			}
			continue;
		}

		const struct tm *broke_down = gmtime(&now);
		if(!broke_down) {
			fputs("Failed to obtain broken-down time structure\n", stderr);
			if(close(connection) == -1) {
				perror("Failed to close connection");
			}
			continue;
		}

		size_t z;
		while(!(z = strftime(timebuf, timelen, "%c\n", broke_down))) {
			void *tp = realloc(timebuf, 2 * timelen);
			if(!tp) {
				perror("Failed to allocate memory");
				if(close(connection) == -1) {
					perror("Failed to close connection");
				}
				goto nextconn;
			}
			timebuf = tp;
			timelen *= 2;
		}

		if(iconv(conv, NULL, &(size_t){0}, NULL, &(size_t){0}) == (size_t)-1) {
			perror("Failed to reset codeset conversion descriptor to the initial conversion state");
			if(close(connection) == -1) {
				perror("Failed to close connection");
			}
			continue;
		}

		const size_t asciibuflen = mbsrtowcs(NULL, &(const char *){timebuf}, z, &(mbstate_t){0});
		if(asciibuflen == (size_t)-1) {
			perror("Failed to count characters");
			if(close(connection) == -1) {
				perror("Failed to close connection");
			}
			continue;
		}

		assert(asciibuflen);
		char ascii[asciibuflen];
		char *asciipos = ascii;
		if(iconv(conv, &(char*){timebuf}, &(size_t){z}, &asciipos, &(size_t){sizeof(ascii)}) == (size_t)-1) {
			perror("Failed to convert between codesets");
			if(close(connection) == -1) {
				perror("Failed to close connection");
			}
			continue;
		}

		const size_t bytes_to_write = asciipos - ascii;
		size_t bytes_written = 0;
		while(bytes_written < bytes_to_write) {
			ssize_t k = write(connection, ascii + bytes_written, bytes_to_write - bytes_written);
			if(k == -1) {
				perror("Failed to write");
				if(close(connection) == -1) {
					perror("Failed to close connection");
				}
				goto nextconn;
			}
			bytes_written += k;
		}

		if(close(connection) == -1) {
			perror("Failed to close connection");
		}

		/* Let's print who connected.
		 * We do this way down here for simplicity. */
		while((s = getnameinfo(&peeraddr.sa_addr, peeraddrlen, peerhostname, peerhostnamelen, NULL, 0, 0)) == EAI_OVERFLOW) {
			void *tp = realloc(peerhostname, 2 * peerhostnamelen);
			if(!tp) {
				perror("Failed to allocate memory");
				goto nextconn;
			}
			peerhostname = tp;
			peerhostnamelen *= 2;
		}
		if(s) {
			fprintf(stderr, "Failed to get client name information: %s\n", (s == EAI_SYSTEM) ? strerror(errno) : gai_strerror(s));
		} else if(printf("%s %s", peerhostname, timebuf) < 0) {
			perror("Failed to print client name information");
		}
	}

	if(iconv_close(conv) == -1) {
		perror("Failed to close codeset conversion descriptor");
	}
	free(peerhostname);
	free(timebuf);
	if(close(sock) == -1) {
		perror("Failed to close socket");
	}
	exit(EXIT_FAILURE);
}

A  => meson.build +56 -0
@@ 1,56 @@
# SPDX-FileCopyrightText: 2023 John Scott <jscott@posteo.net>
# SPDX-License-Identifier: GPL-3.0-or-later
project('daytimed', 'c', license: 'GPL-3.0-or-later')

cc = meson.get_compiler('c')

if not cc.compiles('''
#if __STDC_VERSION__ < 201112L
#error
#endif
''', name: 'check for C11 conformance')
	warning('A C11-conformant system is required')
elif cc.get_define('__STDC_NO_ATOMICS__') != ''
	warning('Support for C11 atomics is required')
else
	assert(cc.has_header('stdatomic.h'))
	assert(cc.has_header_symbol('stdatomic.h', 'atomic_is_lock_free'))
	assert(cc.has_type('atomic_bool', prefix: '#include <stdatomic.h>'))
	atomic_bool_lock_free_str = cc.get_define('ATOMIC_BOOL_LOCK_FREE', prefix: '#include <stdatomic.h>')
	assert(atomic_bool_lock_free_str != '')
	if atomic_bool_lock_free_str.to_int() == 0
		error('Atomic booleans are never lock-free')
	endif
endif

if not cc.compiles('''
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#if _POSIX_VERSION < _POSIX_C_SOURCE
#error
#endif
''', name: 'check for POSIX Issue 7 conformance')
	warning('A POSIX-conformant system is required')
else
	# These things were newly added to the base standard in Issue 7, so let's check for them.

	assert(cc.has_header('iconv.h', args: ['-D_POSIX_C_SOURCE=200809L']))
	assert(cc.has_function('iconv_open', args: ['-D_POSIX_C_SOURCE=200809L'], prefix: '#include <iconv.h>'))
	assert(cc.has_function('iconv', args: ['-D_POSIX_C_SOURCE=200809L'], prefix: '#include <iconv.h>'))
	assert(cc.has_function('iconv_close', args: ['-D_POSIX_C_SOURCE=200809L'], prefix: '#include <iconv.h>'))
	assert(cc.has_type('iconv_t', args: ['-D_POSIX_C_SOURCE=200809L'], prefix: '#include <iconv.h>'))
	# iconv() is notorious for having varied prototypes on some older systems.
	assert(cc.compiles('''
#include <assert.h>
#include <iconv.h>
#include <stdbool.h>
static_assert(_Generic(iconv, size_t (*)(iconv_t, char **, size_t *, char **, size_t *): true, default: false), "improper iconv() prototype");
''', name: 'check that iconv() has the expected prototype', args: ['-D_POSIX_C_SOURCE=200809L']))

	assert(cc.has_header('langinfo.h', args: ['-D_POSIX_C_SOURCE=200809L']))
	assert(cc.has_function('nl_langinfo', args: ['-D_POSIX_C_SOURCE=200809L'], prefix: '#include <langinfo.h>'))
	assert(cc.has_header_symbol('langinfo.h', 'CODESET', args: ['-D_POSIX_C_SOURCE=200809L']))
endif

xnet = cc.find_library('xnet', required: false)
executable('daytimed', 'daytimed.c', dependencies: xnet, install: true)