From 5bf23439057947b9eabb8e185d9a55fba0a9d94e Mon Sep 17 00:00:00 2001 From: John Scott Date: Sat, 10 Jun 2023 09:42:53 -0400 Subject: [PATCH] Initial commit --- daytimed.c | 282 ++++++++++++++++++++++++++++++++++++++++++++++++++++ meson.build | 56 +++++++++++ 2 files changed, 338 insertions(+) create mode 100644 daytimed.c create mode 100644 meson.build diff --git a/daytimed.c b/daytimed.c new file mode 100644 index 0000000..cf2e2bf --- /dev/null +++ b/daytimed.c @@ -0,0 +1,282 @@ +/* SPDX-FileCopyrightText: 2023 John Scott + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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); +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..08c4d6f --- /dev/null +++ b/meson.build @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: 2023 John Scott +# 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 ')) + atomic_bool_lock_free_str = cc.get_define('ATOMIC_BOOL_LOCK_FREE', prefix: '#include ') + 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 +#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 ')) + assert(cc.has_function('iconv', args: ['-D_POSIX_C_SOURCE=200809L'], prefix: '#include ')) + assert(cc.has_function('iconv_close', args: ['-D_POSIX_C_SOURCE=200809L'], prefix: '#include ')) + assert(cc.has_type('iconv_t', args: ['-D_POSIX_C_SOURCE=200809L'], prefix: '#include ')) + # iconv() is notorious for having varied prototypes on some older systems. + assert(cc.compiles(''' +#include +#include +#include +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 ')) + 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) -- 2.45.2