diff options
-rw-r--r-- | .gitignore | 8 | ||||
-rw-r--r-- | Makefile | 21 | ||||
-rw-r--r-- | README | 36 | ||||
-rw-r--r-- | plugin-api.h | 81 | ||||
-rw-r--r-- | plugin-http.c | 24 | ||||
-rw-r--r-- | plugin-irc.c | 24 | ||||
-rw-r--r-- | ponymap.c | 875 | ||||
-rw-r--r-- | siphash.c | 87 | ||||
-rw-r--r-- | siphash.h | 10 | ||||
-rw-r--r-- | utils.c | 1998 |
10 files changed, 3164 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4eee501 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Build files +/ponymap + +# Qt Creator files +/ponymap.config +/ponymap.files +/ponymap.creator* +/ponymap.includes diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..87a593e --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +SHELL = /bin/sh +CC = clang +# -Wunused-function is pretty annoying here, as everything is static +CFLAGS = -std=c99 -Wall -Wextra -Wno-unused-function -ggdb +# -lpthread is only there for debugging (gdb & errno) +# -lrt is only for glibc < 2.17 +LDFLAGS = `pkg-config --libs libssl` -lpthread -lrt -ldl + +.PHONY: all clean +.SUFFIXES: + +targets = ponymap + +all: $(targets) + +clean: + rm -f $(targets) + +ponymap: ponymap.c utils.c siphash.c + $(CC) ponymap.c siphash.c -o $@ $(CFLAGS) $(LDFLAGS) + @@ -0,0 +1,36 @@ +ponymap +======= + +`ponymap' is an experimental network scanner, not even in the alpha stage yet. + +The ultimate purpose of this scanner is bruteforcing hosts and ports in search +of running services. Replacing nmap is not the goal. + +Building and Running +-------------------- +Build dependencies: openssl, clang, pkg-config, GNU make, awk, sh + +If you don't have Clang, you can edit the Makefile to use GCC or TCC, they work +just as good. But there's no CMake support yet, so I force it in the Makefile. + + $ git clone https://github.com/pjanouch/ponymap.git + $ make + +That is all, no installation is required, or supported for that matter. + +First you might want to generate a configuration file: + $ ./ponymap --write-default-config + +After making any necessary edits to the file (there are comments to aid you in +doing that), simply run the appropriate program with no arguments to retrieve +a usage text. + +License +------- +`ponymap' is written by Přemysl Janouch <p.janouch@gmail.com>. + +You may use the software under the terms of the ISC license, the text of which +is included within the package, or, at your option, you may relicense the work +under the MIT or the Modified BSD License, as listed at the following site: + +http://www.gnu.org/licenses/license-list.html diff --git a/plugin-api.h b/plugin-api.h new file mode 100644 index 0000000..6324821 --- /dev/null +++ b/plugin-api.h @@ -0,0 +1,81 @@ +/* + * plugin-api.h: plugin API for ponymap + * + * Copyright (c) 2014, Přemysl Janouch <p.janouch@gmail.com> + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +// This API is meant to be as simplistic as is realistically possible. + +/// The version of the API, and by extension, of all the following structs +#define API_VERSION 1 + +///< Opaque object with data relating to a service scan +struct unit; + +enum +{ + SERVICE_SUPPORTS_TLS = (1 << 0) ///< Plain TLS can be used +}; + +struct service +{ + const char *name; ///< Name of the service + int flags; ///< Service flags + + /// Initialize a scan, returning a handle to it + void *(*scan_init) (struct unit *u); + + /// We have received some data from the peer + void (*on_data) (void *handle, struct unit *u, struct str *data); + + /// Server has closed the connection + void (*on_eof) (void *handle, struct unit *u); + + /// Network or other error has occured + void (*on_error) (void *handle, struct unit *u); + + /// The scan has been cancelled + void (*on_cancelled) (void *handle, struct unit *u); +}; + +struct plugin_api +{ + /// Register the plugin for a service + void (*register_service) (void *ctx, struct service *info); + + /// Send some data to the peer + ssize_t (*unit_write) (struct unit *u, const void *buf, size_t len); + + /// Mark the scan as un/successful + void (*unit_set_success) (struct unit *u, bool success); + + /// Add some information resulting from the scan + void (*unit_add_info) (struct unit *u, const char *result); + + /// Abort the scan, close the connection + void (*unit_abort) (struct unit *u); +}; + +struct plugin_info +{ + /// Version of the API used by this plugin + int32_t api_version; + + /// Let the plugin initialize itself and register any services. + /// The context needs to be passed to the relevant API functions. + bool (*initialize) (void *ctx, struct plugin_api *api); +}; diff --git a/plugin-http.c b/plugin-http.c new file mode 100644 index 0000000..647814e --- /dev/null +++ b/plugin-http.c @@ -0,0 +1,24 @@ +/* + * plugin-http.c: HTTP service detection plugin + * + * Copyright (c) 2014, Přemysl Janouch <p.janouch@gmail.com> + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#include "utils.c" +#include "plugin-api.h" + +// TODO diff --git a/plugin-irc.c b/plugin-irc.c new file mode 100644 index 0000000..0dfc528 --- /dev/null +++ b/plugin-irc.c @@ -0,0 +1,24 @@ +/* + * plugin-http.c: IRC service detection plugin + * + * Copyright (c) 2014, Přemysl Janouch <p.janouch@gmail.com> + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#include "utils.c" +#include "plugin-api.h" + +// TODO diff --git a/ponymap.c b/ponymap.c new file mode 100644 index 0000000..f9c8cc0 --- /dev/null +++ b/ponymap.c @@ -0,0 +1,875 @@ +/* + * ponymap.c: the experimental network scanner + * + * Copyright (c) 2014, Přemysl Janouch <p.janouch@gmail.com> + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#define PROGRAM_NAME "ponymap" +#define PROGRAM_VERSION "alpha" + +#include "utils.c" +#include "plugin-api.h" +#include <dirent.h> +#include <dlfcn.h> + +// --- Configuration (application-specific) ------------------------------------ + +static struct config_item g_config_table[] = +{ + { "plugin_dir", NULL, "Where to search for plugins" }, + { NULL, NULL, NULL } +}; + +// --- Application data -------------------------------------------------------- + +// The scan is a cartesian product of: [IP ranges] -> [ports] -> [services] + +struct port_range +{ + LIST_HEADER (port_range) + uint16_t start; ///< The beginning of the range + uint16_t end; ///< The end of the range +}; + +static void +port_range_delete (struct port_range *self) +{ + free (self); +} + +struct ip_range +{ + LIST_HEADER (ip_range) + uint32_t start; ///< The beginning of the range + uint32_t end; ///< The end of the range + + char *original_name; ///< The name the user typed in + uint32_t original_address; ///< The address of `original_name' +}; + +static void +ip_range_delete (struct ip_range *self) +{ + free (self->original_name); + free (self); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct target +{ + LIST_HEADER (target) + size_t ref_count; ///< Reference count + + uint32_t ip; ///< IP address + char *hostname; ///< Hostname + + // TODO: some fields with results + // XXX: what is the relation to `struct unit'? +}; + +// XXX: how is this supposed to be working? +struct transport +{ + LIST_HEADER (transport) + + const char *name; ///< Name of the transport + + ssize_t (*read) (struct unit *u, void *buf, size_t len); + ssize_t (*write) (struct unit *u, const void *buf, size_t len); +}; + +struct unit +{ + struct target *target; ///< Target context + struct transport *transport; ///< Transport methods + + int socket_fd; ///< The TCP socket + struct str read_buffer; ///< Unprocessed input + struct str write_buffer; ///< Output yet to be sent out + + SSL *ssl; ///< SSL connection + bool ssl_rx_want_tx; ///< SSL_read() wants to write + bool ssl_tx_want_rx; ///< SSL_write() wants to read + + bool success; ///< Service has been found + struct str_vector info; ///< Info resulting from the scan +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct job_generator +{ + struct ip_range *ip_range_iter; ///< Current IP range + uint32_t ip_iter; ///< IP iterator within the range + struct target *current_target; ///< Current target + + struct port_range *port_range_iter; ///< Current port range + uint16_t port_iter; ///< Port iterator within the range + + struct str_map_iter svc_iter; ///< Service iterator + struct service *svc; ///< Current service iterator value + + struct transport *transport_iter; ///< Transport iterator +}; + +struct app_context +{ + struct str_map config; ///< User configuration + + struct str_map svc_list; ///< List of services to scan for + struct port_range *port_list; ///< List of ports to scan on + struct ip_range *ip_list; ///< List of IP's to scan + + struct str_map services; ///< All registered services + struct transport *transports; ///< All available transports + struct job_generator generator; ///< Job generator +#if 0 + struct target *running_list; ///< List of currently scanned targets +#endif + struct poller poller; ///< Manages polled descriptors + bool quitting; ///< User requested quitting + bool polling; ///< The event loop is running +}; + +static void +app_context_init (struct app_context *self) +{ + str_map_init (&self->config); + self->config.free = free; + load_config_defaults (&self->config, g_config_table); + + str_map_init (&self->svc_list); + self->port_list = NULL; + self->ip_list = NULL; + + str_map_init (&self->services); + self->transports = NULL; + // Ignoring the generator so far + + poller_init (&self->poller); + self->quitting = false; + self->polling = false; +} + +static void +app_context_free (struct app_context *self) +{ + str_map_free (&self->config); + str_map_free (&self->svc_list); + poller_free (&self->poller); + + for (struct ip_range *iter = self->ip_list; iter; ) + { + struct ip_range *next = iter->next; + ip_range_delete (iter); + iter = next; + } + + for (struct port_range *iter = self->port_list; iter; ) + { + struct port_range *next = iter->next; + port_range_delete (iter); + iter = next; + } +} + +static void +try_finish_quit (struct app_context *ctx) +{ + if (ctx->quitting) + ctx->polling = false; +} + +static void +initiate_quit (struct app_context *ctx) +{ + ctx->quitting = true; + try_finish_quit (ctx); +} + +// --- Signals ----------------------------------------------------------------- + +static int g_signal_pipe[2]; ///< A pipe used to signal... signals + +/// Program termination has been requested by a signal +static volatile sig_atomic_t g_termination_requested; + +static void +sigterm_handler (int signum) +{ + (void) signum; + + g_termination_requested = true; + + int original_errno = errno; + if (write (g_signal_pipe[1], "t", 1) == -1) + soft_assert (errno == EAGAIN); + errno = original_errno; +} + +static void +setup_signal_handlers (void) +{ + if (pipe (g_signal_pipe) == -1) + exit_fatal ("%s: %s", "pipe", strerror (errno)); + + set_cloexec (g_signal_pipe[0]); + set_cloexec (g_signal_pipe[1]); + + // So that the pipe cannot overflow; it would make write() block within + // the signal handler, which is something we really don't want to happen. + // The same holds true for read(). + set_blocking (g_signal_pipe[0], false); + set_blocking (g_signal_pipe[1], false); + + signal (SIGPIPE, SIG_IGN); + + struct sigaction sa; + sa.sa_flags = SA_RESTART; + sigemptyset (&sa.sa_mask); + sa.sa_handler = sigterm_handler; + if (sigaction (SIGINT, &sa, NULL) == -1 + || sigaction (SIGTERM, &sa, NULL) == -1) + exit_fatal ("sigaction: %s", strerror (errno)); +} + +// --- Plugins ----------------------------------------------------------------- + +static void +plugin_api_register_service (void *app_context, struct service *info) +{ + struct app_context *ctx = app_context; + if (str_map_find (&ctx->services, info->name)) + print_error ("attempt to re-register duplicate service `%s'", + info->name); + else + str_map_set (&ctx->services, info->name, info); +} + +static ssize_t +plugin_api_unit_write (struct unit *u, const void *buf, size_t len) +{ + // TODO +} + +static void +plugin_api_unit_set_success (struct unit *u, bool success) +{ + u->success = success; +} + +static void +plugin_api_unit_add_info (struct unit *u, const char *result) +{ + str_vector_add (&u->info, result); +} + +static void +plugin_api_unit_abort (struct unit *u) +{ + // TODO +} + +static struct plugin_api g_plugin_vtable = +{ + .register_service = plugin_api_register_service, + .unit_write = plugin_api_unit_write, + .unit_set_success = plugin_api_unit_set_success, + .unit_add_info = plugin_api_unit_add_info, + .unit_abort = plugin_api_unit_abort +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static bool +load_one_plugin (struct app_context *ctx, const char *name, const char *path) +{ + void *table = dlopen (path, RTLD_LAZY | RTLD_LOCAL); + if (!table) + { + print_error ("could not load `%s': %s", name, dlerror ()); + return false; + } + + struct plugin_info *info = dlsym (table, "ponymap_plugin_info"); + if (!info) + print_error ("could not load `%s': %s", + name, "cannot find plugin info"); + else if (info->api_version != API_VERSION) + print_error ("could not load `%s': %s", + name, "cannot find plugin info"); + else if (!info->initialize (ctx, &g_plugin_vtable)) + print_error ("could not load `%s': %s", + name, "plugin initialization failed"); + else + return true; + + dlclose (table); + return false; +} + +static bool +load_plugins (struct app_context *ctx) +{ + const char *plugin_dir = str_map_find (&ctx->config, "plugin_dir"); + if (!plugin_dir) + { + print_fatal ("no plugin directory defined"); + return false; + } + + DIR *dir = opendir (plugin_dir); + if (!dir) + { + print_fatal ("%s: %s", + "cannot open plugin directory", strerror (errno)); + return false; + } + + bool success = false; + struct dirent buf, *iter; + while (true) + { + if (readdir_r (dir, &buf, &iter)) + { + print_fatal ("%s: %s", "readdir_r", strerror (errno)); + break; + } + if (!iter) + { + success = true; + break; + } + + char *dot = strrchr (iter->d_name, '.'); + if (dot && !strcmp (dot, ".so")) + continue; + + char *path = xstrdup_printf ("%s/%s", plugin_dir, iter->d_name); + (void) load_one_plugin (ctx, iter->d_name, path); + free (path); + } + closedir (dir); + return success; +} + +// --- Transports -------------------------------------------------------------- + +static ssize_t +transport_plain_read (struct unit *c, void *buf, size_t len) +{ +} + +static ssize_t +transport_plain_write (struct unit *c, const void *buf, size_t len) +{ +} + +static struct transport g_transport_plain = +{ + .read = transport_plain_read, + .write = transport_plain_write +}; + +static ssize_t +transport_tls_read (struct unit *c, void *buf, size_t len) +{ +} + +static ssize_t +transport_tls_write (struct unit *c, const void *buf, size_t len) +{ +} + +static struct transport g_transport_tls = +{ + .read = transport_tls_read, + .write = transport_tls_write +}; + +// --- Scanning ---------------------------------------------------------------- + +static void +target_unref (struct target *self) +{ + if (!self || --self->ref_count) + return; + + free (self->hostname); + free (self); +} + +static void +job_generator_new_target (struct app_context *ctx) +{ + struct job_generator *g = &ctx->generator; + + struct target *target = xcalloc (1, sizeof *target); + target_unref (g->current_target); + g->current_target = target; + + target->ref_count = 1; + target->ip = g->ip_iter; + if (g->ip_iter == g->ip_range_iter->original_address) + target->hostname = xstrdup (g->ip_range_iter->original_name); +} + +static void +job_generator_init (struct app_context *ctx) +{ + struct job_generator *g = &ctx->generator; + + g->ip_range_iter = ctx->ip_list; + g->ip_iter = g->ip_range_iter->start; + g->current_target = NULL; + job_generator_new_target (ctx); + + g->port_range_iter = ctx->port_list; + g->port_iter = g->port_range_iter->start; + + str_map_iter_init (&g->svc_iter, &ctx->svc_list); + g->svc = str_map_iter_next (&g->svc_iter); + + g->transport_iter = ctx->transports; +} + +static bool +job_generator_run (struct app_context *ctx, + uint32_t ip, uint16_t port, struct service *svc, struct transport *trans) +{ + struct job_generator *g = &ctx->generator; + + // TODO: create a socket + // TODO: set it non-blocking + // TODO: connect() + // TODO: set a timer +} + +static bool +job_generator_step (struct app_context *ctx) +{ + struct job_generator *g = &ctx->generator; + + // XXX: we're probably going to need a way to distinguish + // between "try again" and "stop trying". + if (!g->ip_range_iter) + return false; + if (!job_generator_run (ctx, + g->ip_iter, g->port_iter, g->svc, g->transport_iter)) + return false; + + // Try to find the next available transport + while (true) + { + if (!(g->transport_iter = g->transport_iter->next)) + break; + if (g->transport_iter == &g_transport_tls + && !(g->svc->flags & SERVICE_SUPPORTS_TLS)) + continue; + return true; + } + g->transport_iter = ctx->transports; + + // Try to find the next service to scan for + if ((g->svc = str_map_iter_next (&g->svc_iter))) + return true; + str_map_iter_init (&g->svc_iter, &ctx->svc_list); + g->svc = str_map_iter_next (&g->svc_iter); + + // Try to find the next port to scan + if (g->port_iter != UINT16_MAX && g->port_iter < g->port_range_iter->end) + { + g->port_iter++; + return true; + } + g->port_range_iter = g->port_range_iter->next; + if (g->port_range_iter) + { + g->port_iter = g->port_range_iter->start; + return true; + } + g->port_range_iter = ctx->port_list; + g->port_iter = g->port_range_iter->start; + + // Try to find the next IP to scan + if (g->ip_iter != UINT32_MAX && g->ip_iter < g->ip_range_iter->end) + { + g->ip_iter++; + return true; + } + g->ip_range_iter = g->ip_range_iter->next; + if (g->ip_range_iter) + { + g->ip_iter = g->ip_range_iter->start; + return true; + } + + // No more jobs to be created + return false; +} + +// --- Main program ------------------------------------------------------------ + +typedef bool (*list_foreach_fn) (void *, const char *); + +static bool +list_foreach (const char *list, list_foreach_fn callback, void *user_data) +{ + struct str_vector items; + str_vector_init (&items); + + bool success = false; + split_str_ignore_empty (list, ',', &items); + for (size_t i = 0; i < items.len; i++) + if (!callback (user_data, strip_str_in_place (items.vector[i], " "))) + goto fail; + + success = true; +fail: + str_vector_free (&items); + return success; +} + +static bool +parse_port (const char *port, uint16_t *out) +{ + unsigned long x; + struct servent *service; + + if ((service = getservbyname (port, "tcp"))) + *out = ntohs (service->s_port); + else if (xstrtoul (&x, port, 10) && x <= UINT16_MAX) + *out = x; + else + return false; + return true; +} + +static bool +add_port_range (struct app_context *ctx, const char *range) +{ + uint16_t start_port, end_port; + const char *hyphen = strchr (range, '-'); + if (hyphen) + { + char start[hyphen - range + 1]; + memcpy (start, range, sizeof range - 1); + start[sizeof start - 1] = '\0'; + + const char *end = hyphen + 1; + + if (!parse_port (start, &start_port) + || !parse_port (end, &end_port)) + goto fail; + } + else if (!parse_port (range, &start_port)) + goto fail; + else + end_port = start_port; + + if (start_port > end_port) + goto fail; + + struct port_range *pr = xcalloc (1, sizeof *pr); + pr->start = start_port; + pr->end = end_port; + LIST_PREPEND (ctx->port_list, pr); + return true; + +fail: + print_error ("%s: `%s'", "invalid port range", range); + return false; +} + +static bool +add_service (struct app_context *ctx, const char *name) +{ + // To be resolved later + str_map_set (&ctx->svc_list, name, (void *) name); + return true; +} + +static bool +add_target (struct app_context *ctx, const char *target) +{ + char host[strlen (target) + 1]; + strcpy (host, target); + + unsigned long mask = 32; + char *slash = strchr (host, '/'); + if (slash) + { + *slash++ = '\0'; + if (!xstrtoul (&mask, slash, 10) || mask > 32) + { + print_error ("invalid network mask in `%s'", target); + return false; + } + } + + struct addrinfo hints = { .ai_family = AF_INET }; + struct addrinfo *result; + int err = getaddrinfo (target, NULL, &hints, &result); + if (err) + { + print_error ("cannot resolve `%s': %s", host, gai_strerror (err)); + return false; + } + + struct ip_range *range = xcalloc (1, sizeof *range); + uint32_t bitmask = ~(((uint64_t) 1 << (32 - mask)) - 1); + + hard_assert (result->ai_family == AF_INET); + hard_assert (result->ai_addr->sa_family == AF_INET); + uint32_t addr = ntohl (((struct sockaddr_in *) + result->ai_addr)->sin_addr.s_addr); + range->start = addr & bitmask; + range->end = addr | bitmask; + freeaddrinfo (result); + + range->original_name = xstrdup (host); + range->original_address = addr; + + LIST_PREPEND (ctx->ip_list, range); + return true; +} + +static void +merge_port_ranges (struct app_context *ctx) +{ + // Make sure that no port is scanned twice + struct port_range *i1, *i2, *i2_next; + for (i1 = ctx->port_list; i1; i1 = i1->next) + for (i2 = ctx->port_list; i2; i2 = i2_next) + { + i2_next = i2->next; + if (i1 == i2 || i1->end < i2->start || i2->end < i1->start) + continue; + + i1->start = MIN (i1->start, i2->start); + i1->end = MAX (i1->end, i2->end); + LIST_UNLINK (ctx->port_list, i2); + free (i2); + } +} + +static void +merge_ip_ranges (struct app_context *ctx) +{ + // Make sure that no IP is scanned twice + struct ip_range *i1, *i2, *i2_next; + for (i1 = ctx->ip_list; i1; i1 = i1->next) + for (i2 = ctx->ip_list; i2; i2 = i2_next) + { + i2_next = i2->next; + if (i1 == i2 || i1->end < i2->start || i2->end < i1->start) + continue; + + i1->start = MIN (i1->start, i2->start); + i1->end = MAX (i1->end, i2->end); + LIST_UNLINK (ctx->ip_list, i2); + free (i2); + } +} + +static bool +resolve_service_names (struct app_context *ctx) +{ + struct str_map_iter iter; + str_map_iter_init (&iter, &ctx->svc_list); + const char *name; + bool success = true; + while ((name = str_map_iter_next (&iter))) + { + struct service *service; + if ((service = str_map_find (&ctx->services, name))) + { + str_map_set (&ctx->svc_list, name, service); + continue; + } + print_error ("unknown service `%s'", name); + success = false; + } + return success; +} + +static void +on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx) +{ + char *dummy; + (void) read (fd->fd, &dummy, 1); + + if (g_termination_requested && !ctx->quitting) + { + initiate_quit (ctx); + } +} + +static void +print_usage (const char *program_name) +{ + fprintf (stderr, + "Usage: %s [OPTION]... { ADDRESS [/MASK] }...\n" + "Experimental network scanner.\n" + "\n" + " -d, --debug run in debug mode\n" + " -h, --help display this help and exit\n" + " -V, --version output version information and exit\n" + " -p, --port PORTS\n" + " ports/port ranges, separated by commas\n" + " -s, --service SERVICES\n" + " services to scan for\n" + " --write-default-cfg [FILENAME]\n" + " write a default configuration file and exit\n", + program_name); +} + +int +main (int argc, char *argv[]) +{ + const char *invocation_name = argv[0]; + + struct app_context ctx; + app_context_init (&ctx); + + // TODO: timeout for connect() + // TODO: timeout for fingerprint/whatever + static struct option opts[] = + { + { "debug", no_argument, NULL, 'd' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "port", required_argument, NULL, 'p' }, + { "service", required_argument, NULL, 's' }, + { "write-default-cfg", optional_argument, NULL, 'w' }, + { NULL, 0, NULL, 0 } + }; + + while (1) + { + int c, opt_index; + + c = getopt_long (argc, argv, "dhVp:s:", opts, &opt_index); + if (c == -1) + break; + + switch (c) + { + case 'd': + g_debug_mode = true; + break; + case 'h': + print_usage (invocation_name); + exit (EXIT_SUCCESS); + case 'V': + printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); + exit (EXIT_SUCCESS); + case 'p': + if (!list_foreach (optarg, + (list_foreach_fn) add_port_range, &ctx)) + exit (EXIT_FAILURE); + break; + case 's': + if (!list_foreach (optarg, + (list_foreach_fn) add_service, &ctx)) + exit (EXIT_FAILURE); + break; + case 'w': + call_write_default_config (optarg, g_config_table); + exit (EXIT_SUCCESS); + default: + print_error ("wrong options"); + exit (EXIT_FAILURE); + } + } + + argc -= optind; + argv += optind; + + if (!argc) + { + print_usage (invocation_name); + exit (EXIT_FAILURE); + } + + // Resolve all the scan targets + for (int i = 0; i < argc; i++) + if (!add_target (&ctx, argv[i])) + exit (EXIT_FAILURE); + + setup_signal_handlers (); + + SSL_library_init (); + atexit (EVP_cleanup); + SSL_load_error_strings (); + // XXX: ERR_load_BIO_strings()? Anything else? + atexit (ERR_free_strings); + + struct error *e = NULL; + if (!read_config_file (&ctx.config, &e)) + { + print_error ("error loading configuration: %s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + poller_set (&ctx.poller, g_signal_pipe[0], POLLIN, + (poller_dispatcher_func) on_signal_pipe_readable, &ctx); + + if (!load_plugins (&ctx)) + exit (EXIT_FAILURE); + + LIST_PREPEND (ctx.transports, g_transport_plain); + LIST_PREPEND (ctx.transports, g_transport_tls); + + if (!ctx.port_list) + { + struct port_range *range = xcalloc (1, sizeof *range); + range->start = 1; // port 0 is reserved, not bothering with it + range->end = 65535; + ctx.port_list = range; + } + + if (!ctx.svc_list.len) + { + struct str_map_iter iter; + str_map_iter_init (&iter, &ctx.services); + struct service *service; + while ((service = str_map_iter_next (&iter))) + str_map_set (&ctx.svc_list, service->name, service); + } + else + { + // So far we've only stored service _names_ to the hash map; + // let's try to resolve them to actual services. + if (!resolve_service_names (&ctx)) + exit (EXIT_FAILURE); + } + + merge_port_ranges (&ctx); + merge_ip_ranges (&ctx); + + // TODO: initate the scan -> generate as many jobs as possible + + ctx.polling = true; + while (ctx.polling) + poller_run (&ctx.poller); + + app_context_free (&ctx); + return EXIT_SUCCESS; +} diff --git a/siphash.c b/siphash.c new file mode 100644 index 0000000..7a5c8d4 --- /dev/null +++ b/siphash.c @@ -0,0 +1,87 @@ +// Code taken from https://github.com/floodyberry/siphash with some edits. +// +// To the extent possible under law, the author(s) have dedicated all copyright +// and related and neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along +// with this software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>. + +#include "siphash.h" + +inline static uint64_t +u8to64_le (const unsigned char *p) +{ + return + (uint64_t) p[0] | + (uint64_t) p[1] << 8 | + (uint64_t) p[2] << 16 | + (uint64_t) p[3] << 24 | + (uint64_t) p[4] << 32 | + (uint64_t) p[5] << 40 | + (uint64_t) p[6] << 48 | + (uint64_t) p[7] << 56 ; +} + +uint64_t +siphash (const unsigned char key[16], const unsigned char *m, size_t len) +{ + uint64_t v0, v1, v2, v3; + uint64_t mi, k0, k1; + uint64_t last7; + size_t i, blocks; + + k0 = u8to64_le (key + 0); + k1 = u8to64_le (key + 8); + v0 = k0 ^ 0x736f6d6570736575ull; + v1 = k1 ^ 0x646f72616e646f6dull; + v2 = k0 ^ 0x6c7967656e657261ull; + v3 = k1 ^ 0x7465646279746573ull; + + last7 = (uint64_t) (len & 0xff) << 56; + +#define ROTL64(a,b) (((a)<<(b))|((a)>>(64-b))) + +#define COMPRESS \ + v0 += v1; v2 += v3; \ + v1 = ROTL64 (v1,13); v3 = ROTL64 (v3,16); \ + v1 ^= v0; v3 ^= v2; \ + v0 = ROTL64 (v0,32); \ + v2 += v1; v0 += v3; \ + v1 = ROTL64 (v1,17); v3 = ROTL64 (v3,21); \ + v1 ^= v2; v3 ^= v0; \ + v2 = ROTL64(v2,32); + + for (i = 0, blocks = (len & ~(size_t) 7); i < blocks; i += 8) + { + mi = u8to64_le (m + i); + v3 ^= mi; + COMPRESS + COMPRESS + v0 ^= mi; + } + + switch (len - blocks) + { + case 7: last7 |= (uint64_t) m[i + 6] << 48; + case 6: last7 |= (uint64_t) m[i + 5] << 40; + case 5: last7 |= (uint64_t) m[i + 4] << 32; + case 4: last7 |= (uint64_t) m[i + 3] << 24; + case 3: last7 |= (uint64_t) m[i + 2] << 16; + case 2: last7 |= (uint64_t) m[i + 1] << 8; + case 1: last7 |= (uint64_t) m[i + 0] ; + default:; + }; + v3 ^= last7; + COMPRESS + COMPRESS + v0 ^= last7; + v2 ^= 0xff; + COMPRESS + COMPRESS + COMPRESS + COMPRESS + + return v0 ^ v1 ^ v2 ^ v3; +} + diff --git a/siphash.h b/siphash.h new file mode 100644 index 0000000..dca7c42 --- /dev/null +++ b/siphash.h @@ -0,0 +1,10 @@ +#ifndef SIPHASH_H +#define SIPHASH_H + +#include <stdint.h> +#include <stdlib.h> + +uint64_t siphash (const unsigned char key[16], + const unsigned char *m, size_t len); + +#endif // SIPHASH_H @@ -0,0 +1,1998 @@ +/* + * utils.c: utilities + * + * Copyright (c) 2014, Přemysl Janouch <p.janouch@gmail.com> + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#define _POSIX_C_SOURCE 199309L +#define _XOPEN_SOURCE 600 + +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdbool.h> +#include <ctype.h> +#include <time.h> +#include <limits.h> + +#include <unistd.h> +#include <sys/wait.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <fcntl.h> +#include <poll.h> +#include <signal.h> +#include <strings.h> +#include <regex.h> +#include <libgen.h> +#include <syslog.h> +#include <fnmatch.h> + +#include <sys/socket.h> +#include <sys/types.h> +#include <netinet/in.h> +#include <netdb.h> + +#ifndef NI_MAXHOST +#define NI_MAXHOST 1025 +#endif // ! NI_MAXHOST + +#ifndef NI_MAXSERV +#define NI_MAXSERV 32 +#endif // ! NI_MAXSERV + +#include <getopt.h> +#include <openssl/ssl.h> +#include <openssl/err.h> +#include "siphash.h" + +extern char **environ; + +#ifdef _POSIX_MONOTONIC_CLOCK +#define CLOCK_BEST CLOCK_MONOTONIC +#else // ! _POSIX_MONOTIC_CLOCK +#define CLOCK_BEST CLOCK_REALTIME +#endif // ! _POSIX_MONOTONIC_CLOCK + +#if defined __GNUC__ +#define ATTRIBUTE_PRINTF(x, y) __attribute__ ((format (printf, x, y))) +#else // ! __GNUC__ +#define ATTRIBUTE_PRINTF(x, y) +#endif // ! __GNUC__ + +#if defined __GNUC__ && __GNUC__ >= 4 +#define ATTRIBUTE_SENTINEL __attribute__ ((sentinel)) +#else // ! __GNUC__ || __GNUC__ < 4 +#define ATTRIBUTE_SENTINEL +#endif // ! __GNUC__ || __GNUC__ < 4 + +#define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0])) + +#define BLOCK_START do { +#define BLOCK_END } while (0) + +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +// --- Logging ----------------------------------------------------------------- + +static void +log_message_syslog (int prio, const char *quote, const char *fmt, va_list ap) +{ + va_list va; + va_copy (va, ap); + int size = vsnprintf (NULL, 0, fmt, va); + va_end (va); + if (size < 0) + return; + + char buf[size + 1]; + if (vsnprintf (buf, sizeof buf, fmt, ap) >= 0) + syslog (prio, "%s%s", quote, buf); +} + +static void +log_message_stdio (int prio, const char *quote, const char *fmt, va_list ap) +{ + (void) prio; + FILE *stream = stderr; + + fputs (quote, stream); + vfprintf (stream, fmt, ap); + fputs ("\n", stream); +} + +static void (*g_log_message_real) (int, const char *, const char *, va_list) + = log_message_stdio; + +static void +log_message (int priority, const char *quote, const char *fmt, ...) + ATTRIBUTE_PRINTF (3, 4); + +static void +log_message (int priority, const char *quote, const char *fmt, ...) +{ + va_list ap; + va_start (ap, fmt); + g_log_message_real (priority, quote, fmt, ap); + va_end (ap); +} + +// `fatal' is reserved for unexpected failures that would harm further operation + +#define print_fatal(...) log_message (LOG_ERR, "fatal: ", __VA_ARGS__) +#define print_error(...) log_message (LOG_ERR, "error: ", __VA_ARGS__) +#define print_warning(...) log_message (LOG_WARNING, "warning: ", __VA_ARGS__) +#define print_status(...) log_message (LOG_INFO, "-- ", __VA_ARGS__) + +#define exit_fatal(...) \ + BLOCK_START \ + print_fatal (__VA_ARGS__); \ + exit (EXIT_FAILURE); \ + BLOCK_END + +// --- Debugging and assertions ------------------------------------------------ + +// We should check everything that may possibly fail with at least a soft +// assertion, so that any causes for problems don't slip us by silently. +// +// `g_soft_asserts_are_deadly' may be useful while running inside a debugger. + +static bool g_debug_mode; ///< Debug messages are printed +static bool g_soft_asserts_are_deadly; ///< soft_assert() aborts as well + +#define print_debug(...) \ + BLOCK_START \ + if (g_debug_mode) \ + log_message (LOG_DEBUG, "debug: ", __VA_ARGS__); \ + BLOCK_END + +static void +assertion_failure_handler (bool is_fatal, const char *file, int line, + const char *function, const char *condition) +{ + if (is_fatal) + { + print_fatal ("assertion failed [%s:%d in function %s]: %s", + file, line, function, condition); + abort (); + } + else + print_debug ("assertion failed [%s:%d in function %s]: %s", + file, line, function, condition); +} + +#define soft_assert(condition) \ + ((condition) ? true : \ + (assertion_failure_handler (g_soft_asserts_are_deadly, \ + __FILE__, __LINE__, __func__, #condition), false)) + +#define hard_assert(condition) \ + ((condition) ? (void) 0 : \ + assertion_failure_handler (true, \ + __FILE__, __LINE__, __func__, #condition)) + +// --- Safe memory management -------------------------------------------------- + +// When a memory allocation fails and we need the memory, we're usually pretty +// much fucked. Use the non-prefixed versions when there's a legitimate +// worry that an unrealistic amount of memory may be requested for allocation. + +// XXX: it's not a good idea to use print_message() as it may want to allocate +// further memory for printf() and the output streams. That may fail. + +static void * +xmalloc (size_t n) +{ + void *p = malloc (n); + if (!p) + exit_fatal ("malloc: %s", strerror (errno)); + return p; +} + +static void * +xcalloc (size_t n, size_t m) +{ + void *p = calloc (n, m); + if (!p && n && m) + exit_fatal ("calloc: %s", strerror (errno)); + return p; +} + +static void * +xrealloc (void *o, size_t n) +{ + void *p = realloc (o, n); + if (!p && n) + exit_fatal ("realloc: %s", strerror (errno)); + return p; +} + +static void * +xreallocarray (void *o, size_t n, size_t m) +{ + if (m && n > SIZE_MAX / m) + { + errno = ENOMEM; + exit_fatal ("reallocarray: %s", strerror (errno)); + } + return xrealloc (o, n * m); +} + +static char * +xstrdup (const char *s) +{ + return strcpy (xmalloc (strlen (s) + 1), s); +} + +static char * +xstrndup (const char *s, size_t n) +{ + size_t size = strlen (s); + if (n > size) + n = size; + + char *copy = xmalloc (n + 1); + memcpy (copy, s, n); + copy[n] = '\0'; + return copy; +} + +// --- Double-linked list helpers ---------------------------------------------- + +#define LIST_HEADER(type) \ + struct type *next; \ + struct type *prev; + +#define LIST_PREPEND(head, link) \ + BLOCK_START \ + (link)->prev = NULL; \ + (link)->next = (head); \ + if ((link)->next) \ + (link)->next->prev = (link); \ + (head) = (link); \ + BLOCK_END + +#define LIST_UNLINK(head, link) \ + BLOCK_START \ + if ((link)->prev) \ + (link)->prev->next = (link)->next; \ + else \ + (head) = (link)->next; \ + if ((link)->next) \ + (link)->next->prev = (link)->prev; \ + BLOCK_END + +// --- Dynamically allocated string array -------------------------------------- + +struct str_vector +{ + char **vector; + size_t len; + size_t alloc; +}; + +static void +str_vector_init (struct str_vector *self) +{ + self->alloc = 4; + self->len = 0; + self->vector = xcalloc (sizeof *self->vector, self->alloc); +} + +static void +str_vector_free (struct str_vector *self) +{ + unsigned i; + for (i = 0; i < self->len; i++) + free (self->vector[i]); + + free (self->vector); + self->vector = NULL; +} + +static void +str_vector_reset (struct str_vector *self) +{ + str_vector_free (self); + str_vector_init (self); +} + +static void +str_vector_add_owned (struct str_vector *self, char *s) +{ + self->vector[self->len] = s; + if (++self->len >= self->alloc) + self->vector = xreallocarray (self->vector, + sizeof *self->vector, (self->alloc <<= 1)); + self->vector[self->len] = NULL; +} + +static void +str_vector_add (struct str_vector *self, const char *s) +{ + str_vector_add_owned (self, xstrdup (s)); +} + +static void +str_vector_add_args (struct str_vector *self, const char *s, ...) + ATTRIBUTE_SENTINEL; + +static void +str_vector_add_args (struct str_vector *self, const char *s, ...) +{ + va_list ap; + + va_start (ap, s); + while (s) + { + str_vector_add (self, s); + s = va_arg (ap, const char *); + } + va_end (ap); +} + +static void +str_vector_add_vector (struct str_vector *self, char **vector) +{ + while (*vector) + str_vector_add (self, *vector++); +} + +static void +str_vector_remove (struct str_vector *self, size_t i) +{ + hard_assert (i < self->len); + free (self->vector[i]); + memmove (self->vector + i, self->vector + i + 1, + (self->len-- - i) * sizeof *self->vector); +} + +// --- Dynamically allocated strings ------------------------------------------- + +// Basically a string builder to abstract away manual memory management. + +struct str +{ + char *str; ///< String data, null terminated + size_t alloc; ///< How many bytes are allocated + size_t len; ///< How long the string actually is +}; + +/// We don't care about allocations that are way too large for the content, as +/// long as the allocation is below the given threshold. (Trivial heuristics.) +#define STR_SHRINK_THRESHOLD (1 << 20) + +static void +str_init (struct str *self) +{ + self->alloc = 16; + self->len = 0; + self->str = strcpy (xmalloc (self->alloc), ""); +} + +static void +str_free (struct str *self) +{ + free (self->str); + self->str = NULL; + self->alloc = 0; + self->len = 0; +} + +static void +str_reset (struct str *self) +{ + str_free (self); + str_init (self); +} + +static char * +str_steal (struct str *self) +{ + char *str = self->str; + self->str = NULL; + str_free (self); + return str; +} + +static void +str_ensure_space (struct str *self, size_t n) +{ + // We allocate at least one more byte for the terminating null character + size_t new_alloc = self->alloc; + while (new_alloc <= self->len + n) + new_alloc <<= 1; + if (new_alloc != self->alloc) + self->str = xrealloc (self->str, (self->alloc = new_alloc)); +} + +static void +str_append_data (struct str *self, const char *data, size_t n) +{ + str_ensure_space (self, n); + memcpy (self->str + self->len, data, n); + self->len += n; + self->str[self->len] = '\0'; +} + +static void +str_append_c (struct str *self, char c) +{ + str_append_data (self, &c, 1); +} + +static void +str_append (struct str *self, const char *s) +{ + str_append_data (self, s, strlen (s)); +} + +static void +str_append_str (struct str *self, const struct str *another) +{ + str_append_data (self, another->str, another->len); +} + +static int +str_append_vprintf (struct str *self, const char *fmt, va_list va) +{ + va_list ap; + int size; + + va_copy (ap, va); + size = vsnprintf (NULL, 0, fmt, ap); + va_end (ap); + + if (size < 0) + return -1; + + va_copy (ap, va); + str_ensure_space (self, size); + size = vsnprintf (self->str + self->len, self->alloc - self->len, fmt, ap); + va_end (ap); + + if (size > 0) + self->len += size; + + return size; +} + +static int +str_append_printf (struct str *self, const char *fmt, ...) + ATTRIBUTE_PRINTF (2, 3); + +static int +str_append_printf (struct str *self, const char *fmt, ...) +{ + va_list ap; + + va_start (ap, fmt); + int size = str_append_vprintf (self, fmt, ap); + va_end (ap); + return size; +} + +static void +str_remove_slice (struct str *self, size_t start, size_t length) +{ + size_t end = start + length; + hard_assert (end <= self->len); + memmove (self->str + start, self->str + end, self->len - end); + self->str[self->len -= length] = '\0'; + + // Shrink the string if the allocation becomes way too large + if (self->alloc >= STR_SHRINK_THRESHOLD && self->len < (self->alloc >> 2)) + self->str = xrealloc (self->str, self->alloc >>= 2); +} + +// --- Errors ------------------------------------------------------------------ + +// Error reporting utilities. Inspired by GError, only much simpler. + +struct error +{ + char *message; ///< Textual description of the event +}; + +static void +error_set (struct error **e, const char *message, ...) ATTRIBUTE_PRINTF (2, 3); + +static void +error_set (struct error **e, const char *message, ...) +{ + if (!e) + return; + + va_list ap; + va_start (ap, message); + int size = vsnprintf (NULL, 0, message, ap); + va_end (ap); + + hard_assert (size >= 0); + + struct error *tmp = xmalloc (sizeof *tmp); + tmp->message = xmalloc (size + 1); + + va_start (ap, message); + size = vsnprintf (tmp->message, size + 1, message, ap); + va_end (ap); + + hard_assert (size >= 0); + + soft_assert (*e == NULL); + *e = tmp; +} + +static void +error_free (struct error *e) +{ + free (e->message); + free (e); +} + +static void +error_propagate (struct error **destination, struct error *source) +{ + if (!destination) + { + error_free (source); + return; + } + + soft_assert (*destination == NULL); + *destination = source; +} + +// --- String hash map --------------------------------------------------------- + +// The most basic <string, managed pointer> map (or associative array). + +struct str_map_link +{ + LIST_HEADER (str_map_link) + + void *data; ///< Payload + size_t key_length; ///< Length of the key without '\0' + char key[]; ///< The key for this link +}; + +struct str_map +{ + struct str_map_link **map; ///< The hash table data itself + size_t alloc; ///< Number of allocated entries + size_t len; ///< Number of entries in the table + void (*free) (void *); ///< Callback to destruct the payload + + /// Callback that transforms all key values for storage and comparison; + /// has to behave exactly like strxfrm(). + size_t (*key_xfrm) (char *dest, const char *src, size_t n); +}; + +// As long as you don't remove the current entry, you can modify the map. +// Use `link' directly to access the data. + +struct str_map_iter +{ + struct str_map *map; ///< The map we're iterating + size_t next_index; ///< Next table index to search + struct str_map_link *link; ///< Current link +}; + +#define STR_MAP_MIN_ALLOC 16 + +typedef void (*str_map_free_func) (void *); + +static void +str_map_init (struct str_map *self) +{ + self->alloc = STR_MAP_MIN_ALLOC; + self->len = 0; + self->free = NULL; + self->key_xfrm = NULL; + self->map = xcalloc (self->alloc, sizeof *self->map); +} + +static void +str_map_free (struct str_map *self) +{ + struct str_map_link **iter, **end = self->map + self->alloc; + struct str_map_link *link, *tmp; + + for (iter = self->map; iter < end; iter++) + for (link = *iter; link; link = tmp) + { + tmp = link->next; + if (self->free) + self->free (link->data); + free (link); + } + + free (self->map); + self->map = NULL; +} + +static void +str_map_iter_init (struct str_map_iter *self, struct str_map *map) +{ + self->map = map; + self->next_index = 0; + self->link = NULL; +} + +static void * +str_map_iter_next (struct str_map_iter *self) +{ + struct str_map *map = self->map; + if (self->link) + self->link = self->link->next; + while (!self->link) + { + if (self->next_index >= map->alloc) + return NULL; + self->link = map->map[self->next_index++]; + } + return self->link->data; +} + +static uint64_t +str_map_hash (const char *s, size_t len) +{ + static unsigned char key[16] = "SipHash 2-4 key!"; + return siphash (key, (const void *) s, len); +} + +static uint64_t +str_map_pos (struct str_map *self, const char *s) +{ + size_t mask = self->alloc - 1; + return str_map_hash (s, strlen (s)) & mask; +} + +static uint64_t +str_map_link_hash (struct str_map_link *self) +{ + return str_map_hash (self->key, self->key_length); +} + +static void +str_map_resize (struct str_map *self, size_t new_size) +{ + struct str_map_link **old_map = self->map; + size_t i, old_size = self->alloc; + + // Only powers of two, so that we don't need to compute the modulo + hard_assert ((new_size & (new_size - 1)) == 0); + size_t mask = new_size - 1; + + self->alloc = new_size; + self->map = xcalloc (self->alloc, sizeof *self->map); + for (i = 0; i < old_size; i++) + { + struct str_map_link *iter = old_map[i], *next_iter; + while (iter) + { + next_iter = iter->next; + uint64_t pos = str_map_link_hash (iter) & mask; + LIST_PREPEND (self->map[pos], iter); + iter = next_iter; + } + } + + free (old_map); +} + +static void +str_map_set_real (struct str_map *self, const char *key, void *value) +{ + uint64_t pos = str_map_pos (self, key); + struct str_map_link *iter = self->map[pos]; + for (; iter; iter = iter->next) + { + if (strcmp (key, iter->key)) + continue; + + // Storing the same data doesn't destroy it + if (self->free && value != iter->data) + self->free (iter->data); + + if (value) + { + iter->data = value; + return; + } + + LIST_UNLINK (self->map[pos], iter); + free (iter); + self->len--; + + // The array should be at least 1/4 full + if (self->alloc >= (STR_MAP_MIN_ALLOC << 2) + && self->len < (self->alloc >> 2)) + str_map_resize (self, self->alloc >> 2); + return; + } + + if (!value) + return; + + if (self->len >= self->alloc) + { + str_map_resize (self, self->alloc << 1); + pos = str_map_pos (self, key); + } + + // Link in a new element for the given <key, value> pair + size_t key_length = strlen (key); + struct str_map_link *link = xmalloc (sizeof *link + key_length + 1); + link->data = value; + link->key_length = key_length; + memcpy (link->key, key, key_length + 1); + + LIST_PREPEND (self->map[pos], link); + self->len++; +} + +static void +str_map_set (struct str_map *self, const char *key, void *value) +{ + if (!self->key_xfrm) + { + str_map_set_real (self, key, value); + return; + } + char tmp[self->key_xfrm (NULL, key, 0) + 1]; + self->key_xfrm (tmp, key, sizeof tmp); + str_map_set_real (self, tmp, value); +} + +static void * +str_map_find_real (struct str_map *self, const char *key) +{ + struct str_map_link *iter = self->map[str_map_pos (self, key)]; + for (; iter; iter = iter->next) + if (!strcmp (key, (const char *) iter + sizeof *iter)) + return iter->data; + return NULL; +} + +static void * +str_map_find (struct str_map *self, const char *key) +{ + if (!self->key_xfrm) + return str_map_find_real (self, key); + + char tmp[self->key_xfrm (NULL, key, 0) + 1]; + self->key_xfrm (tmp, key, sizeof tmp); + return str_map_find_real (self, tmp); +} + +// --- File descriptor utilities ----------------------------------------------- + +static void +set_cloexec (int fd) +{ + soft_assert (fcntl (fd, F_SETFD, fcntl (fd, F_GETFD) | FD_CLOEXEC) != -1); +} + +static bool +set_blocking (int fd, bool blocking) +{ + int flags = fcntl (fd, F_GETFL); + hard_assert (flags != -1); + + bool prev = !(flags & O_NONBLOCK); + if (blocking) + flags &= ~O_NONBLOCK; + else + flags |= O_NONBLOCK; + + hard_assert (fcntl (fd, F_SETFL, flags) != -1); + return prev; +} + +static void +xclose (int fd) +{ + while (close (fd) == -1) + if (!soft_assert (errno == EINTR)) + break; +} + +// --- Polling ----------------------------------------------------------------- + +// Basically the poor man's GMainLoop/libev/libuv. It might make some sense +// to instead use those tested and proven libraries but we don't need much +// and it's interesting to implement. + +// At the moment the FD's are stored in an unsorted array. This is not ideal +// complexity-wise but I don't think I have much of a choice with poll(), +// and neither with epoll for that matter. +// +// unsorted array sorted array +// search O(n) O(log n) [O(log log n)] +// insert by fd O(n) O(n) +// delete by fd O(n) O(n) +// +// Insertion in the unsorted array can be reduced to O(1) if I maintain a +// bitmap of present FD's but that's still not a huge win. +// +// I don't expect this to be much of an issue, as there are typically not going +// to be that many FD's to watch, and the linear approach is cache-friendly. + +typedef void (*poller_dispatcher_func) (const struct pollfd *, void *); +typedef void (*poller_timer_func) (void *); + +#define POLLER_MIN_ALLOC 16 + +struct poller_timer_info +{ + int64_t when; ///< When is the timer to expire + poller_timer_func dispatcher; ///< Event dispatcher + void *user_data; ///< User data +}; + +struct poller_timers +{ + struct poller_timer_info *info; ///< Min-heap of timers + size_t len; ///< Number of scheduled timers + size_t alloc; ///< Number of timers allocated +}; + +static void +poller_timers_init (struct poller_timers *self) +{ + self->alloc = POLLER_MIN_ALLOC; + self->len = 0; + self->info = xmalloc (self->alloc * sizeof *self->info); +} + +static void +poller_timers_free (struct poller_timers *self) +{ + free (self->info); +} + +static int64_t +poller_timers_get_current_time (void) +{ +#ifdef _POSIX_TIMERS + struct timespec tp; + hard_assert (clock_gettime (CLOCK_BEST, &tp) != -1); + return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000; +#else + struct timeval tp; + gettimeofday (&tp, NULL); + return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_usec / 1000; +#endif +} + +static void +poller_timers_heapify_down (struct poller_timers *self, size_t index) +{ + typedef struct poller_timer_info info_t; + info_t *end = self->info + self->len; + + while (true) + { + info_t *parent = self->info + index; + info_t *left = self->info + 2 * index + 1; + info_t *right = self->info + 2 * index + 2; + + info_t *largest = parent; + if (left < end && left->when > largest->when) + largest = left; + if (right < end && right->when > largest->when) + largest = right; + if (parent == largest) + break; + + info_t tmp = *parent; + *parent = *largest; + *largest = tmp; + + index = largest - self->info; + } +} + +static void +poller_timers_remove_at_index (struct poller_timers *self, size_t index) +{ + hard_assert (index < self->len); + if (index == --self->len) + return; + + self->info[index] = self->info[self->len]; + poller_timers_heapify_down (self, index); +} + +static void +poller_timers_dispatch (struct poller_timers *self) +{ + int64_t now = poller_timers_get_current_time (); + while (self->len && self->info->when <= now) + { + struct poller_timer_info info = *self->info; + poller_timers_remove_at_index (self, 0); + info.dispatcher (info.user_data); + } +} + +static void +poller_timers_heapify_up (struct poller_timers *self, size_t index) +{ + while (index != 0) + { + size_t parent = (index - 1) / 2; + if (self->info[parent].when <= self->info[index].when) + break; + + struct poller_timer_info tmp = self->info[parent]; + self->info[parent] = self->info[index]; + self->info[index] = tmp; + + index = parent; + } +} + +static ssize_t +poller_timers_find (struct poller_timers *self, + poller_timer_func dispatcher, void *data) +{ + // NOTE: there may be duplicates. + for (size_t i = 0; i < self->len; i++) + if (self->info[i].dispatcher == dispatcher + && self->info[i].user_data == data) + return i; + return -1; +} + +static ssize_t +poller_timers_find_by_data (struct poller_timers *self, void *data) +{ + for (size_t i = 0; i < self->len; i++) + if (self->info[i].user_data == data) + return i; + return -1; +} + +static void +poller_timers_add (struct poller_timers *self, + poller_timer_func dispatcher, void *data, int timeout_ms) +{ + if (self->len == self->alloc) + self->info = xreallocarray (self->info, + self->alloc <<= 1, sizeof *self->info); + + self->info[self->len] = (struct poller_timer_info) { + .when = poller_timers_get_current_time () + timeout_ms, + .dispatcher = dispatcher, .user_data = data }; + poller_timers_heapify_up (self, self->len++); +} + +static int +poller_timers_get_poll_timeout (struct poller_timers *self) +{ + if (!self->len) + return -1; + + int64_t timeout = self->info->when - poller_timers_get_current_time (); + if (timeout <= 0) + return 0; + if (timeout > INT_MAX) + return INT_MAX; + return timeout; +} + +#ifdef __linux__ + +// I don't really need this, I've basically implemented this just because I can. + +#include <sys/epoll.h> + +struct poller_info +{ + int fd; ///< Our file descriptor + short events; ///< The poll() events we registered for + poller_dispatcher_func dispatcher; ///< Event dispatcher + void *user_data; ///< User data +}; + +struct poller +{ + int epoll_fd; ///< The epoll FD + struct poller_info **info; ///< Information associated with each FD + struct epoll_event *revents; ///< Output array for epoll_wait() + size_t len; ///< Number of polled descriptors + size_t alloc; ///< Number of entries allocated + + struct poller_timers timers; ///< Timeouts + + /// Index of the element in `revents' that's about to be dispatched next. + int dispatch_next; + + /// The total number of entries stored in `revents' by epoll_wait(). + int dispatch_total; +}; + +static void +poller_init (struct poller *self) +{ + self->epoll_fd = epoll_create (POLLER_MIN_ALLOC); + hard_assert (self->epoll_fd != -1); + set_cloexec (self->epoll_fd); + + self->len = 0; + self->alloc = POLLER_MIN_ALLOC; + self->info = xcalloc (self->alloc, sizeof *self->info); + self->revents = xcalloc (self->alloc, sizeof *self->revents); + + poller_timers_init (&self->timers); + + self->dispatch_next = 0; + self->dispatch_total = 0; +} + +static void +poller_free (struct poller *self) +{ + for (size_t i = 0; i < self->len; i++) + { + struct poller_info *info = self->info[i]; + hard_assert (epoll_ctl (self->epoll_fd, + EPOLL_CTL_DEL, info->fd, (void *) "") != -1); + free (info); + } + + poller_timers_free (&self->timers); + + xclose (self->epoll_fd); + free (self->info); + free (self->revents); +} + +static ssize_t +poller_find_by_fd (struct poller *self, int fd) +{ + for (size_t i = 0; i < self->len; i++) + if (self->info[i]->fd == fd) + return i; + return -1; +} + +static void +poller_ensure_space (struct poller *self) +{ + if (self->len < self->alloc) + return; + + self->alloc <<= 1; + hard_assert (self->alloc != 0); + + self->revents = xreallocarray + (self->revents, sizeof *self->revents, self->alloc); + self->info = xreallocarray + (self->info, sizeof *self->info, self->alloc); +} + +static short +poller_epoll_to_poll_events (uint32_t events) +{ + short result = 0; + if (events & EPOLLIN) result |= POLLIN; + if (events & EPOLLOUT) result |= POLLOUT; + if (events & EPOLLERR) result |= POLLERR; + if (events & EPOLLHUP) result |= POLLHUP; + if (events & EPOLLPRI) result |= POLLPRI; + return result; +} + +static uint32_t +poller_poll_to_epoll_events (short events) +{ + uint32_t result = 0; + if (events & POLLIN) result |= EPOLLIN; + if (events & POLLOUT) result |= EPOLLOUT; + if (events & POLLERR) result |= EPOLLERR; + if (events & POLLHUP) result |= EPOLLHUP; + if (events & POLLPRI) result |= EPOLLPRI; + return result; +} + +static void +poller_set (struct poller *self, int fd, short events, + poller_dispatcher_func dispatcher, void *data) +{ + ssize_t index = poller_find_by_fd (self, fd); + bool modifying = true; + if (index == -1) + { + poller_ensure_space (self); + self->info[index = self->len++] = xcalloc (1, sizeof **self->info); + modifying = false; + } + + struct poller_info *info = self->info[index]; + info->fd = fd; + info->events = events; + info->dispatcher = dispatcher; + info->user_data = data; + + struct epoll_event event; + event.events = poller_poll_to_epoll_events (events); + event.data.ptr = info; + hard_assert (epoll_ctl (self->epoll_fd, + modifying ? EPOLL_CTL_MOD : EPOLL_CTL_ADD, fd, &event) != -1); +} + +static void +poller_remove_from_dispatch (struct poller *self, + const struct poller_info *info) +{ + if (!self->dispatch_total) + return; + + int i; + for (i = self->dispatch_next; i < self->dispatch_total; i++) + if (self->revents[i].data.ptr == info) + break; + if (i == self->dispatch_total) + return; + + if (i != --self->dispatch_total) + self->revents[i] = self->revents[self->dispatch_total]; +} + +static void +poller_remove_at_index (struct poller *self, size_t index) +{ + hard_assert (index < self->len); + struct poller_info *info = self->info[index]; + + poller_remove_from_dispatch (self, info); + hard_assert (epoll_ctl (self->epoll_fd, + EPOLL_CTL_DEL, info->fd, (void *) "") != -1); + + free (info); + if (index != --self->len) + self->info[index] = self->info[self->len]; +} + +static void +poller_run (struct poller *self) +{ + // Not reentrant + hard_assert (!self->dispatch_total); + + int n_fds; + do + n_fds = epoll_wait (self->epoll_fd, self->revents, self->len, + poller_timers_get_poll_timeout (&self->timers)); + while (n_fds == -1 && errno == EINTR); + + if (n_fds == -1) + exit_fatal ("%s: %s", "epoll", strerror (errno)); + + poller_timers_dispatch (&self->timers); + + self->dispatch_next = 0; + self->dispatch_total = n_fds; + + while (self->dispatch_next < self->dispatch_total) + { + struct epoll_event *revents = self->revents + self->dispatch_next; + struct poller_info *info = revents->data.ptr; + + struct pollfd pfd; + pfd.fd = info->fd; + pfd.revents = poller_epoll_to_poll_events (revents->events); + pfd.events = info->events; + + self->dispatch_next++; + info->dispatcher (&pfd, info->user_data); + } + + self->dispatch_next = 0; + self->dispatch_total = 0; +} + +#else // !__linux__ + +struct poller_info +{ + poller_dispatcher_func dispatcher; ///< Event dispatcher + void *user_data; ///< User data +}; + +struct poller +{ + struct pollfd *fds; ///< Polled descriptors + struct poller_info *fds_info; ///< Additional information for each FD + size_t len; ///< Number of polled descriptors + size_t alloc; ///< Number of entries allocated + + struct poller_timers timers; ///< Timers + int dispatch_next; ///< The next dispatched FD or -1 +}; + +static void +poller_init (struct poller *self) +{ + self->alloc = POLLER_MIN_ALLOC; + self->len = 0; + self->fds = xcalloc (self->alloc, sizeof *self->fds); + self->fds_info = xcalloc (self->alloc, sizeof *self->fds_info); + poller_timers_init (&self->timers); + self->dispatch_next = -1; +} + +static void +poller_free (struct poller *self) +{ + free (self->fds); + free (self->fds_info); + poller_timers_free (&self->timers); +} + +static ssize_t +poller_find_by_fd (struct poller *self, int fd) +{ + for (size_t i = 0; i < self->len; i++) + if (self->fds[i].fd == fd) + return i; + return -1; +} + +static void +poller_ensure_space (struct poller *self) +{ + if (self->len < self->alloc) + return; + + self->alloc <<= 1; + self->fds = xreallocarray (self->fds, sizeof *self->fds, self->alloc); + self->fds_info = xreallocarray + (self->fds_info, sizeof *self->fds_info, self->alloc); +} + +static void +poller_set (struct poller *self, int fd, short events, + poller_dispatcher_func dispatcher, void *data) +{ + ssize_t index = poller_find_by_fd (self, fd); + if (index == -1) + { + poller_ensure_space (self); + index = self->len++; + } + + struct pollfd *new_entry = self->fds + index; + memset (new_entry, 0, sizeof *new_entry); + new_entry->fd = fd; + new_entry->events = events; + + self->fds_info[index] = (struct poller_info) { dispatcher, data }; +} + +static void +poller_remove_at_index (struct poller *self, size_t index) +{ + hard_assert (index < self->len); + if (index == --self->len) + return; + + // Make sure that we don't disrupt the dispatch loop; kind of crude + if ((int) index < self->dispatch_next) + { + memmove (self->fds + index, self->fds + index + 1, + (self->len - index) * sizeof *self->fds); + memmove (self->fds_info + index, self->fds_info + index + 1, + (self->len - index) * sizeof *self->fds_info); + self->dispatch_next--; + } + else + { + self->fds[index] = self->fds[self->len]; + self->fds_info[index] = self->fds_info[self->len]; + } +} + +static void +poller_run (struct poller *self) +{ + // Not reentrant + hard_assert (self->dispatch_next == -1); + + int result; + do + result = poll (self->fds, self->len, + poller_timers_get_poll_timeout (&self->timers)); + while (result == -1 && errno == EINTR); + + if (result == -1) + exit_fatal ("%s: %s", "poll", strerror (errno)); + + poller_timers_dispatch (&self->timers); + + for (int i = 0; i < (int) self->len; ) + { + struct pollfd pfd = self->fds[i]; + struct poller_info *info = self->fds_info + i; + self->dispatch_next = ++i; + if (pfd.revents) + info->dispatcher (&pfd, info->user_data); + i = self->dispatch_next; + } + + self->dispatch_next = -1; +} + +#endif // !__linux__ + +// --- Utilities --------------------------------------------------------------- + +static void +split_str_ignore_empty (const char *s, char delimiter, struct str_vector *out) +{ + const char *begin = s, *end; + + while ((end = strchr (begin, delimiter))) + { + if (begin != end) + str_vector_add_owned (out, xstrndup (begin, end - begin)); + begin = ++end; + } + + if (*begin) + str_vector_add (out, begin); +} + +static char * +strip_str_in_place (char *s, const char *stripped_chars) +{ + char *end = s + strlen (s); + while (end > s && strchr (stripped_chars, end[-1])) + *--end = '\0'; + + char *start = s + strspn (s, stripped_chars); + if (start > s) + memmove (s, start, end - start + 1); + return s; +} + +static char * +join_str_vector (const struct str_vector *v, char delimiter) +{ + if (!v->len) + return xstrdup (""); + + struct str result; + str_init (&result); + str_append (&result, v->vector[0]); + for (size_t i = 1; i < v->len; i++) + str_append_printf (&result, "%c%s", delimiter, v->vector[i]); + return str_steal (&result); +} + +static char *xstrdup_printf (const char *, ...) ATTRIBUTE_PRINTF (1, 2); + +static char * +xstrdup_printf (const char *format, ...) +{ + va_list ap; + struct str tmp; + str_init (&tmp); + va_start (ap, format); + str_append_vprintf (&tmp, format, ap); + va_end (ap); + return str_steal (&tmp); +} + +static bool +str_append_env_path (struct str *output, const char *var, bool only_absolute) +{ + const char *value = getenv (var); + + if (!value || (only_absolute && *value != '/')) + return false; + + str_append (output, value); + return true; +} + +static void +get_xdg_home_dir (struct str *output, const char *var, const char *def) +{ + str_reset (output); + if (!str_append_env_path (output, var, true)) + { + str_append_env_path (output, "HOME", false); + str_append_c (output, '/'); + str_append (output, def); + } +} + +static void +get_xdg_config_dirs (struct str_vector *out) +{ + struct str config_home; + str_init (&config_home); + get_xdg_home_dir (&config_home, "XDG_CONFIG_HOME", ".config"); + str_vector_add (out, config_home.str); + str_free (&config_home); + + const char *xdg_config_dirs; + if ((xdg_config_dirs = getenv ("XDG_CONFIG_DIRS"))) + split_str_ignore_empty (xdg_config_dirs, ':', out); +} + +static char * +resolve_config_filename (const char *filename) +{ + // Absolute path is absolute + if (*filename == '/') + return xstrdup (filename); + + struct str_vector paths; + str_vector_init (&paths); + get_xdg_config_dirs (&paths); + + struct str file; + str_init (&file); + + char *result = NULL; + for (unsigned i = 0; i < paths.len; i++) + { + // As per spec, relative paths are ignored + if (*paths.vector[i] != '/') + continue; + + str_reset (&file); + str_append_printf (&file, "%s/" PROGRAM_NAME "/%s", + paths.vector[i], filename); + + struct stat st; + if (!stat (file.str, &st)) + { + result = str_steal (&file); + break; + } + } + + str_vector_free (&paths); + str_free (&file); + return result; +} + +static bool +ensure_directory_existence (const char *path, struct error **e) +{ + struct stat st; + + if (stat (path, &st)) + { + if (mkdir (path, S_IRWXU | S_IRWXG | S_IRWXO)) + { + error_set (e, "cannot create directory `%s': %s", + path, strerror (errno)); + return false; + } + } + else if (!S_ISDIR (st.st_mode)) + { + error_set (e, "cannot create directory `%s': %s", + path, "file exists but is not a directory"); + return false; + } + return true; +} + +static bool +mkdir_with_parents (char *path, struct error **e) +{ + char *p = path; + + // XXX: This is prone to the TOCTTOU problem. The solution would be to + // rewrite the function using the {mkdir,fstat}at() functions from + // POSIX.1-2008, ideally returning a file descriptor to the open + // directory, with the current code as a fallback. Or to use chdir(). + while ((p = strchr (p + 1, '/'))) + { + *p = '\0'; + bool success = ensure_directory_existence (path, e); + *p = '/'; + + if (!success) + return false; + } + + return ensure_directory_existence (path, e); +} + +static bool +set_boolean_if_valid (bool *out, const char *s) +{ + if (!strcasecmp (s, "yes")) *out = true; + else if (!strcasecmp (s, "no")) *out = false; + else if (!strcasecmp (s, "on")) *out = true; + else if (!strcasecmp (s, "off")) *out = false; + else if (!strcasecmp (s, "true")) *out = true; + else if (!strcasecmp (s, "false")) *out = false; + else return false; + + return true; +} + +static bool +xstrtoul (unsigned long *out, const char *s, int base) +{ + char *end; + errno = 0; + *out = strtoul (s, &end, base); + return errno == 0 && !*end && end != s; +} + +static bool +read_line (FILE *fp, struct str *s) +{ + int c; + bool at_end = true; + + str_reset (s); + while ((c = fgetc (fp)) != EOF) + { + at_end = false; + if (c == '\r') + continue; + if (c == '\n') + break; + str_append_c (s, c); + } + + return !at_end; +} + +#define XSSL_ERROR_TRY_AGAIN INT_MAX + +/// A small wrapper around SSL_get_error() to simplify further handling +static int +xssl_get_error (SSL *ssl, int result, const char **error_info) +{ + int error = SSL_get_error (ssl, result); + switch (error) + { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + return error; + case SSL_ERROR_SYSCALL: + if ((error = ERR_get_error ())) + *error_info = ERR_error_string (error, NULL); + else if (result == 0) + // An EOF that's not according to the protocol is still an EOF + return SSL_ERROR_ZERO_RETURN; + else + { + if (errno == EINTR) + return XSSL_ERROR_TRY_AGAIN; + *error_info = strerror (errno); + } + return SSL_ERROR_SSL; + default: + if ((error = ERR_get_error ())) + *error_info = ERR_error_string (error, NULL); + else + *error_info = "Unknown error"; + return SSL_ERROR_SSL; + } +} + +static char * +format_host_port_pair (const char *host, const char *port) +{ + // IPv6 addresses mess with the "colon notation"; let's go with RFC 2732 + if (strchr (host, ':')) + return xstrdup_printf ("[%s]:%s", host, port); + return xstrdup_printf ("%s:%s", host, port); +} + +// --- Regular expressions ----------------------------------------------------- + +static regex_t * +regex_compile (const char *regex, int flags, struct error **e) +{ + regex_t *re = xmalloc (sizeof *re); + int err = regcomp (re, regex, flags); + if (!err) + return re; + + char buf[regerror (err, re, NULL, 0)]; + regerror (err, re, buf, sizeof buf); + + free (re); + error_set (e, "%s: %s", "failed to compile regular expression", buf); + return NULL; +} + +static void +regex_free (void *regex) +{ + regfree (regex); + free (regex); +} + +// The cost of hashing a string is likely to be significantly smaller than that +// of compiling the whole regular expression anew, so here is a simple cache. +// Adding basic support for subgroups is easy: check `re_nsub' and output into +// a `struct str_vector' (if all we want is the substrings). + +static void +regex_cache_init (struct str_map *cache) +{ + str_map_init (cache); + cache->free = regex_free; +} + +static bool +regex_cache_match (struct str_map *cache, const char *regex, int flags, + const char *s, struct error **e) +{ + regex_t *re = str_map_find (cache, regex); + if (!re) + { + re = regex_compile (regex, flags, e); + if (!re) + return false; + str_map_set (cache, regex, re); + } + return regexec (re, s, 0, NULL, 0) != REG_NOMATCH; +} + +// --- IRC utilities ----------------------------------------------------------- + +struct irc_message +{ + struct str_map tags; ///< IRC 3.2 message tags + char *prefix; ///< Message prefix + char *command; ///< IRC command + struct str_vector params; ///< Command parameters +}; + +static void +irc_parse_message_tags (const char *tags, struct str_map *out) +{ + struct str_vector v; + str_vector_init (&v); + split_str_ignore_empty (tags, ';', &v); + + for (size_t i = 0; i < v.len; i++) + { + char *key = v.vector[i], *equal_sign = strchr (key, '='); + if (equal_sign) + { + *equal_sign = '\0'; + str_map_set (out, key, xstrdup (equal_sign + 1)); + } + else + str_map_set (out, key, xstrdup ("")); + } + + str_vector_free (&v); +} + +static void +irc_parse_message (struct irc_message *msg, const char *line) +{ + str_map_init (&msg->tags); + msg->tags.free = free; + + msg->prefix = NULL; + msg->command = NULL; + str_vector_init (&msg->params); + + // IRC 3.2 message tags + if (*line == '@') + { + size_t tags_len = strcspn (++line, " "); + char *tags = xstrndup (line, tags_len); + irc_parse_message_tags (tags, &msg->tags); + free (tags); + + line += tags_len; + while (*line == ' ') + line++; + } + + // Prefix + if (*line == ':') + { + size_t prefix_len = strcspn (++line, " "); + msg->prefix = xstrndup (line, prefix_len); + line += prefix_len; + } + + // Command name + { + while (*line == ' ') + line++; + + size_t cmd_len = strcspn (line, " "); + msg->command = xstrndup (line, cmd_len); + line += cmd_len; + } + + // Arguments + while (true) + { + while (*line == ' ') + line++; + + if (*line == ':') + { + str_vector_add (&msg->params, ++line); + break; + } + + size_t param_len = strcspn (line, " "); + if (!param_len) + break; + + str_vector_add_owned (&msg->params, xstrndup (line, param_len)); + line += param_len; + } +} + +static void +irc_free_message (struct irc_message *msg) +{ + str_map_free (&msg->tags); + free (msg->prefix); + free (msg->command); + str_vector_free (&msg->params); +} + +static void +irc_process_buffer (struct str *buf, + void (*callback)(const struct irc_message *, const char *, void *), + void *user_data) +{ + char *start = buf->str, *end = start + buf->len; + for (char *p = start; p + 1 < end; p++) + { + // Split the input on newlines + if (p[0] != '\r' || p[1] != '\n') + continue; + + *p = 0; + + struct irc_message msg; + irc_parse_message (&msg, start); + callback (&msg, start, user_data); + irc_free_message (&msg); + + start = p + 2; + } + + // XXX: we might want to just advance some kind of an offset to avoid + // moving memory around unnecessarily. + str_remove_slice (buf, 0, start - buf->str); +} + +static int +irc_tolower (int c) +{ + if (c == '[') return '{'; + if (c == ']') return '}'; + if (c == '\\') return '|'; + if (c == '~') return '^'; + return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; +} + +static size_t +irc_strxfrm (char *dest, const char *src, size_t n) +{ + size_t len = strlen (src); + while (n-- && (*dest++ = irc_tolower (*src++))) + ; + return len; +} + +static int +irc_strcmp (const char *a, const char *b) +{ + int x; + while (*a || *b) + if ((x = irc_tolower (*a++) - irc_tolower (*b++))) + return x; + return 0; +} + +static int +irc_fnmatch (const char *pattern, const char *string) +{ + size_t pattern_size = strlen (pattern) + 1; + size_t string_size = strlen (string) + 1; + char x_pattern[pattern_size], x_string[string_size]; + irc_strxfrm (x_pattern, pattern, pattern_size); + irc_strxfrm (x_string, string, string_size); + return fnmatch (x_pattern, x_string, 0); +} + +// --- Configuration ----------------------------------------------------------- + +// The keys are stripped of surrounding whitespace, the values are not. + +struct config_item +{ + const char *key; + const char *default_value; + const char *description; +}; + +static void +load_config_defaults (struct str_map *config, const struct config_item *table) +{ + for (; table->key != NULL; table++) + if (table->default_value) + str_map_set (config, table->key, xstrdup (table->default_value)); + else + str_map_set (config, table->key, NULL); +} + +static bool +read_config_file (struct str_map *config, struct error **e) +{ + char *filename = resolve_config_filename (PROGRAM_NAME ".conf"); + if (!filename) + return true; + + FILE *fp = fopen (filename, "r"); + if (!fp) + { + error_set (e, "could not open `%s' for reading: %s", + filename, strerror (errno)); + return false; + } + + struct str line; + str_init (&line); + + bool errors = false; + for (unsigned line_no = 1; read_line (fp, &line); line_no++) + { + char *start = line.str; + if (*start == '#') + continue; + + while (isspace (*start)) + start++; + + char *end = strchr (start, '='); + if (end) + { + char *value = end + 1; + do + *end = '\0'; + while (isspace (*--end)); + + str_map_set (config, start, xstrdup (value)); + } + else if (*start) + { + error_set (e, "line %u in config: %s", line_no, "malformed input"); + errors = true; + break; + } + } + + str_free (&line); + fclose (fp); + return !errors; +} + +static char * +write_default_config (const char *filename, const char *prolog, + const struct config_item *table, struct error **e) +{ + struct str path, base; + + str_init (&path); + str_init (&base); + + if (filename) + { + char *tmp = xstrdup (filename); + str_append (&path, dirname (tmp)); + strcpy (tmp, filename); + str_append (&base, basename (tmp)); + free (tmp); + } + else + { + get_xdg_home_dir (&path, "XDG_CONFIG_HOME", ".config"); + str_append (&path, "/" PROGRAM_NAME); + str_append (&base, PROGRAM_NAME ".conf"); + } + + if (!mkdir_with_parents (path.str, e)) + goto error; + + str_append_c (&path, '/'); + str_append_str (&path, &base); + + FILE *fp = fopen (path.str, "w"); + if (!fp) + { + error_set (e, "could not open `%s' for writing: %s", + path.str, strerror (errno)); + goto error; + } + + if (prolog) + fputs (prolog, fp); + + errno = 0; + for (; table->key != NULL; table++) + { + fprintf (fp, "# %s\n", table->description); + if (table->default_value) + fprintf (fp, "%s=%s\n", table->key, table->default_value); + else + fprintf (fp, "#%s=\n", table->key); + } + fclose (fp); + if (errno) + { + error_set (e, "writing to `%s' failed: %s", path.str, strerror (errno)); + goto error; + } + + str_free (&base); + return str_steal (&path); + +error: + str_free (&base); + str_free (&path); + return NULL; + +} + +static void +call_write_default_config (const char *hint, const struct config_item *table) +{ + static const char *prolog = + "# " PROGRAM_NAME " " PROGRAM_VERSION " configuration file\n" + "#\n" + "# Relative paths are searched for in ${XDG_CONFIG_HOME:-~/.config}\n" + "# /" PROGRAM_NAME " as well as in $XDG_CONFIG_DIRS/" PROGRAM_NAME "\n" + "\n"; + + struct error *e = NULL; + char *filename = write_default_config (hint, prolog, table, &e); + if (!filename) + { + print_error ("%s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + print_status ("configuration written to `%s'", filename); + free (filename); +} |