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