/* * common.c: common functionality * * Copyright (c) 2014 - 2015, Přemysl Janouch <p.janouch@gmail.com> * * 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 LIBERTY_WANT_SSL #define LIBERTY_WANT_POLLER #define LIBERTY_WANT_PROTO_IRC #ifdef WANT_SYSLOG_LOGGING #define print_fatal_data ((void *) LOG_ERR) #define print_error_data ((void *) LOG_ERR) #define print_warning_data ((void *) LOG_WARNING) #define print_status_data ((void *) LOG_INFO) #define print_debug_data ((void *) LOG_DEBUG) #endif // WANT_SYSLOG_LOGGING #include "liberty/liberty.c" #include <setjmp.h> #include <inttypes.h> #include <arpa/inet.h> /// Shorthand to set an error and return failure from the function #define FAIL(...) \ BLOCK_START \ error_set (e, __VA_ARGS__); \ return false; \ BLOCK_END // A few other debugging shorthands #define LOG_FUNC_FAILURE(name, desc) \ print_debug ("%s: %s: %s", __func__, (name), (desc)) #define LOG_LIBC_FAILURE(name) \ print_debug ("%s: %s: %s", __func__, (name), strerror (errno)) // --- To be moved to liberty -------------------------------------------------- static void split_str (const char *s, char delimiter, struct str_vector *out) { const char *begin = s, *end; while ((end = strchr (begin, delimiter))) { str_vector_add_owned (out, xstrndup (begin, end - begin)); begin = ++end; } str_vector_add (out, begin); } static ssize_t str_vector_find (const struct str_vector *v, const char *s) { for (size_t i = 0; i < v->len; i++) if (!strcmp (v->vector[i], s)) return i; return -1; } // --- Logging ----------------------------------------------------------------- static void log_message_syslog (void *user_data, const char *quote, const char *fmt, va_list ap) { int prio = (int) (intptr_t) user_data; 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); } // --- Connector --------------------------------------------------------------- // This is a helper that tries to establish a connection with any address on // a given list. Sadly it also introduces a bit of a callback hell. struct connector_target { LIST_HEADER (struct connector_target) char *hostname; ///< Target hostname or address char *service; ///< Target service name or port struct addrinfo *results; ///< Resolved target struct addrinfo *iter; ///< Current endpoint }; static struct connector_target * connector_target_new (void) { struct connector_target *self = xmalloc (sizeof *self); return self; } static void connector_target_destroy (struct connector_target *self) { free (self->hostname); free (self->service); freeaddrinfo (self->results); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct connector { int socket; ///< Socket FD for the connection struct poller_fd connected_event; ///< We've connected or failed struct connector_target *targets; ///< Targets struct connector_target *targets_t; ///< Tail of targets void *user_data; ///< User data for callbacks // You may destroy the connector object in these two main callbacks: /// Connection has been successfully established void (*on_connected) (void *user_data, int socket); /// Failed to establish a connection to either target void (*on_failure) (void *user_data); // Optional: /// Connecting to a new address void (*on_connecting) (void *user_data, const char *address); /// Connecting to the last address has failed void (*on_error) (void *user_data, const char *error); }; static void connector_notify_connecting (struct connector *self, struct connector_target *target, struct addrinfo *gai_iter) { if (!self->on_connecting) return; const char *real_host = target->hostname; // We don't really need this, so we can let it quietly fail char buf[NI_MAXHOST]; int err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen, buf, sizeof buf, NULL, 0, NI_NUMERICHOST); if (err) LOG_FUNC_FAILURE ("getnameinfo", gai_strerror (err)); else real_host = buf; char *address = format_host_port_pair (real_host, target->service); self->on_connecting (self->user_data, address); free (address); } static void connector_notify_error (struct connector *self, const char *error) { if (self->on_error) self->on_error (self->user_data, error); } static void connector_prepare_next (struct connector *self) { struct connector_target *target = self->targets; if (!(target->iter = target->iter->ai_next)) { LIST_UNLINK_WITH_TAIL (self->targets, self->targets_t, target); connector_target_destroy (target); } } static void connector_step (struct connector *self) { struct connector_target *target = self->targets; if (!target) { // Total failure, none of the targets has succeeded self->on_failure (self->user_data); return; } struct addrinfo *gai_iter = target->iter; hard_assert (gai_iter != NULL); connector_notify_connecting (self, target, gai_iter); int fd = self->socket = socket (gai_iter->ai_family, gai_iter->ai_socktype, gai_iter->ai_protocol); if (fd == -1) { connector_notify_error (self, strerror (errno)); connector_prepare_next (self); connector_step (self); return; } set_cloexec (fd); set_blocking (fd, false); int yes = 1; soft_assert (setsockopt (fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof yes) != -1); if (!connect (fd, gai_iter->ai_addr, gai_iter->ai_addrlen)) { set_blocking (fd, true); self->on_connected (self->user_data, fd); return; } else if (errno != EINPROGRESS) { connector_notify_error (self, strerror (errno)); xclose (fd); connector_prepare_next (self); connector_step (self); return; } self->connected_event.fd = self->socket = fd; poller_fd_set (&self->connected_event, POLLOUT); connector_prepare_next (self); } static void connector_on_ready (const struct pollfd *pfd, struct connector *self) { // See http://cr.yp.to/docs/connect.html if this doesn't work. // The second connect() method doesn't work with DragonflyBSD. int error = 0; socklen_t error_len = sizeof error; hard_assert (!getsockopt (pfd->fd, SOL_SOCKET, SO_ERROR, &error, &error_len)); if (error) { connector_notify_error (self, strerror (error)); poller_fd_reset (&self->connected_event); xclose (self->socket); self->socket = -1; connector_step (self); } else { poller_fd_reset (&self->connected_event); self->socket = -1; set_blocking (pfd->fd, true); self->on_connected (self->user_data, pfd->fd); } } static void connector_init (struct connector *self, struct poller *poller) { memset (self, 0, sizeof *self); self->socket = -1; poller_fd_init (&self->connected_event, poller, self->socket); self->connected_event.user_data = self; self->connected_event.dispatcher = (poller_fd_fn) connector_on_ready; } static void connector_free (struct connector *self) { poller_fd_reset (&self->connected_event); if (self->socket != -1) xclose (self->socket); LIST_FOR_EACH (struct connector_target, iter, self->targets) connector_target_destroy (iter); } static bool connector_add_target (struct connector *self, const char *hostname, const char *service, struct error **e) { struct addrinfo hints, *results; memset (&hints, 0, sizeof hints); hints.ai_socktype = SOCK_STREAM; // TODO: even this should be done asynchronously, most likely in // a thread pool, similarly to how libuv does it int err = getaddrinfo (hostname, service, &hints, &results); if (err) { error_set (e, "%s: %s", "getaddrinfo", gai_strerror (err)); return false; } struct connector_target *target = connector_target_new (); target->hostname = xstrdup (hostname); target->service = xstrdup (service); target->results = results; target->iter = target->results; LIST_APPEND_WITH_TAIL (self->targets, self->targets_t, target); return true; } // --- SOCKS 5/4a (blocking implementation) ------------------------------------ // These are awkward protocols. Note that the `username' is used differently // in SOCKS 4a and 5. In the former version, it is the username that you can // get ident'ed against. In the latter version, it forms a pair with the // password field and doesn't need to be an actual user on your machine. // TODO: make a non-blocking poller-based version of this; // either use c-ares or (even better) start another thread to do resolution struct socks_addr { enum socks_addr_type { SOCKS_IPV4 = 1, ///< IPv4 address SOCKS_DOMAIN = 3, ///< Domain name to be resolved SOCKS_IPV6 = 4 ///< IPv6 address } type; ///< The type of this address union { uint8_t ipv4[4]; ///< IPv4 address, network octet order const char *domain; ///< Domain name uint8_t ipv6[16]; ///< IPv6 address, network octet order } data; ///< The address itself }; struct socks_data { struct socks_addr address; ///< Target address uint16_t port; ///< Target port const char *username; ///< Authentication username const char *password; ///< Authentication password struct socks_addr bound_address; ///< Bound address at the server uint16_t bound_port; ///< Bound port at the server }; static bool socks_get_socket (struct addrinfo *addresses, int *fd, struct error **e) { int sockfd; for (; addresses; addresses = addresses->ai_next) { sockfd = socket (addresses->ai_family, addresses->ai_socktype, addresses->ai_protocol); if (sockfd == -1) continue; set_cloexec (sockfd); int yes = 1; soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof yes) != -1); if (!connect (sockfd, addresses->ai_addr, addresses->ai_addrlen)) break; xclose (sockfd); } if (!addresses) { error_set (e, "couldn't connect to the SOCKS server"); return false; } *fd = sockfd; return true; } #define SOCKS_FAIL(...) \ BLOCK_START \ error_set (e, __VA_ARGS__); \ goto fail; \ BLOCK_END #define SOCKS_RECV(buf, len) \ BLOCK_START \ if ((n = recv (sockfd, (buf), (len), 0)) == -1) \ SOCKS_FAIL ("%s: %s", "recv", strerror (errno)); \ if (n != (len)) \ SOCKS_FAIL ("%s: %s", "protocol error", "unexpected EOF"); \ BLOCK_END static bool socks_4a_connect (struct addrinfo *addresses, struct socks_data *data, int *fd, struct error **e) { int sockfd; if (!socks_get_socket (addresses, &sockfd, e)) return false; const void *dest_ipv4 = "\x00\x00\x00\x01"; const char *dest_domain = NULL; char buf[INET6_ADDRSTRLEN]; switch (data->address.type) { case SOCKS_IPV4: dest_ipv4 = data->address.data.ipv4; break; case SOCKS_IPV6: // About the best thing we can do, not sure if it works anywhere at all if (!inet_ntop (AF_INET6, &data->address.data.ipv6, buf, sizeof buf)) SOCKS_FAIL ("%s: %s", "inet_ntop", strerror (errno)); dest_domain = buf; break; case SOCKS_DOMAIN: dest_domain = data->address.data.domain; } struct str req; str_init (&req); str_append_c (&req, 4); // version str_append_c (&req, 1); // connect str_append_c (&req, data->port >> 8); // higher bits of port str_append_c (&req, data->port); // lower bits of port str_append_data (&req, dest_ipv4, 4); // destination address if (data->username) str_append (&req, data->username); str_append_c (&req, '\0'); if (dest_domain) { str_append (&req, dest_domain); str_append_c (&req, '\0'); } ssize_t n = send (sockfd, req.str, req.len, 0); str_free (&req); if (n == -1) SOCKS_FAIL ("%s: %s", "send", strerror (errno)); uint8_t resp[8]; SOCKS_RECV (resp, sizeof resp); if (resp[0] != 0) SOCKS_FAIL ("protocol error"); switch (resp[1]) { case 90: break; case 91: SOCKS_FAIL ("request rejected or failed"); case 92: SOCKS_FAIL ("%s: %s", "request rejected", "SOCKS server cannot connect to identd on the client"); case 93: SOCKS_FAIL ("%s: %s", "request rejected", "identd reports different user-id"); default: SOCKS_FAIL ("protocol error"); } *fd = sockfd; return true; fail: xclose (sockfd); return false; } #undef SOCKS_FAIL #define SOCKS_FAIL(...) \ BLOCK_START \ error_set (e, __VA_ARGS__); \ return false; \ BLOCK_END static bool socks_5_userpass_auth (int sockfd, struct socks_data *data, struct error **e) { size_t ulen = strlen (data->username); if (ulen > 255) ulen = 255; size_t plen = strlen (data->password); if (plen > 255) plen = 255; uint8_t req[3 + ulen + plen], *p = req; *p++ = 0x01; // version *p++ = ulen; // username length memcpy (p, data->username, ulen); p += ulen; *p++ = plen; // password length memcpy (p, data->password, plen); p += plen; ssize_t n = send (sockfd, req, p - req, 0); if (n == -1) SOCKS_FAIL ("%s: %s", "send", strerror (errno)); uint8_t resp[2]; SOCKS_RECV (resp, sizeof resp); if (resp[0] != 0x01) SOCKS_FAIL ("protocol error"); if (resp[1] != 0x00) SOCKS_FAIL ("authentication failure"); return true; } static bool socks_5_auth (int sockfd, struct socks_data *data, struct error **e) { bool can_auth = data->username && data->password; uint8_t hello[4]; hello[0] = 0x05; // version hello[1] = 1 + can_auth; // number of authentication methods hello[2] = 0x00; // no authentication required hello[3] = 0x02; // username/password ssize_t n = send (sockfd, hello, 3 + can_auth, 0); if (n == -1) SOCKS_FAIL ("%s: %s", "send", strerror (errno)); uint8_t resp[2]; SOCKS_RECV (resp, sizeof resp); if (resp[0] != 0x05) SOCKS_FAIL ("protocol error"); switch (resp[1]) { case 0x02: if (!can_auth) SOCKS_FAIL ("protocol error"); if (!socks_5_userpass_auth (sockfd, data, e)) return false; case 0x00: break; case 0xFF: SOCKS_FAIL ("no acceptable authentication methods"); default: SOCKS_FAIL ("protocol error"); } return true; } static bool socks_5_send_req (int sockfd, struct socks_data *data, struct error **e) { uint8_t req[4 + 256 + 2], *p = req; *p++ = 0x05; // version *p++ = 0x01; // connect *p++ = 0x00; // reserved *p++ = data->address.type; switch (data->address.type) { case SOCKS_IPV4: memcpy (p, data->address.data.ipv4, sizeof data->address.data.ipv4); p += sizeof data->address.data.ipv4; break; case SOCKS_DOMAIN: { size_t dlen = strlen (data->address.data.domain); if (dlen > 255) dlen = 255; *p++ = dlen; memcpy (p, data->address.data.domain, dlen); p += dlen; break; } case SOCKS_IPV6: memcpy (p, data->address.data.ipv6, sizeof data->address.data.ipv6); p += sizeof data->address.data.ipv6; break; } *p++ = data->port >> 8; *p++ = data->port; if (send (sockfd, req, p - req, 0) == -1) SOCKS_FAIL ("%s: %s", "send", strerror (errno)); return true; } static bool socks_5_process_resp (int sockfd, struct socks_data *data, struct error **e) { uint8_t resp_header[4]; ssize_t n; SOCKS_RECV (resp_header, sizeof resp_header); if (resp_header[0] != 0x05) SOCKS_FAIL ("protocol error"); switch (resp_header[1]) { case 0x00: break; case 0x01: SOCKS_FAIL ("general SOCKS server failure"); case 0x02: SOCKS_FAIL ("connection not allowed by ruleset"); case 0x03: SOCKS_FAIL ("network unreachable"); case 0x04: SOCKS_FAIL ("host unreachable"); case 0x05: SOCKS_FAIL ("connection refused"); case 0x06: SOCKS_FAIL ("TTL expired"); case 0x07: SOCKS_FAIL ("command not supported"); case 0x08: SOCKS_FAIL ("address type not supported"); default: SOCKS_FAIL ("protocol error"); } switch ((data->bound_address.type = resp_header[3])) { case SOCKS_IPV4: SOCKS_RECV (data->bound_address.data.ipv4, sizeof data->bound_address.data.ipv4); break; case SOCKS_IPV6: SOCKS_RECV (data->bound_address.data.ipv6, sizeof data->bound_address.data.ipv6); break; case SOCKS_DOMAIN: { uint8_t len; SOCKS_RECV (&len, sizeof len); char domain[len + 1]; SOCKS_RECV (domain, len); domain[len] = '\0'; data->bound_address.data.domain = xstrdup (domain); break; } default: SOCKS_FAIL ("protocol error"); } uint16_t port; SOCKS_RECV (&port, sizeof port); data->bound_port = ntohs (port); return true; } #undef SOCKS_FAIL #undef SOCKS_RECV static bool socks_5_connect (struct addrinfo *addresses, struct socks_data *data, int *fd, struct error **e) { int sockfd; if (!socks_get_socket (addresses, &sockfd, e)) return false; if (!socks_5_auth (sockfd, data, e) || !socks_5_send_req (sockfd, data, e) || !socks_5_process_resp (sockfd, data, e)) { xclose (sockfd); return false; } *fd = sockfd; return true; } static int socks_connect (const char *socks_host, const char *socks_port, const char *host, const char *port, const char *username, const char *password, struct error **e) { int result = -1; struct addrinfo gai_hints, *gai_result; memset (&gai_hints, 0, sizeof gai_hints); gai_hints.ai_socktype = SOCK_STREAM; unsigned long port_no; const struct servent *serv; if ((serv = getservbyname (port, "tcp"))) port_no = (uint16_t) ntohs (serv->s_port); else if (!xstrtoul (&port_no, port, 10) || !port_no || port_no > UINT16_MAX) { error_set (e, "invalid port number"); goto fail; } int err = getaddrinfo (socks_host, socks_port, &gai_hints, &gai_result); if (err) { error_set (e, "%s: %s", "getaddrinfo", gai_strerror (err)); goto fail; } struct socks_data data = { .username = username, .password = password, .port = port_no }; if (inet_pton (AF_INET, host, &data.address.data.ipv4) == 1) data.address.type = SOCKS_IPV4; else if (inet_pton (AF_INET6, host, &data.address.data.ipv6) == 1) data.address.type = SOCKS_IPV6; else { data.address.type = SOCKS_DOMAIN; data.address.data.domain = host; } if (!socks_5_connect (gai_result, &data, &result, NULL)) socks_4a_connect (gai_result, &data, &result, e); if (data.bound_address.type == SOCKS_DOMAIN) free ((char *) data.bound_address.data.domain); freeaddrinfo (gai_result); fail: return result; } // --- CTCP decoding ----------------------------------------------------------- #define CTCP_M_QUOTE '\020' #define CTCP_X_DELIM '\001' #define CTCP_X_QUOTE '\\' struct ctcp_chunk { LIST_HEADER (struct ctcp_chunk) bool is_extended; ///< Is this a tagged extended message? struct str tag; ///< The tag, if any struct str text; ///< Message contents }; static struct ctcp_chunk * ctcp_chunk_new (void) { struct ctcp_chunk *self = xcalloc (1, sizeof *self); str_init (&self->tag); str_init (&self->text); return self; } static void ctcp_chunk_destroy (struct ctcp_chunk *self) { str_free (&self->tag); str_free (&self->text); free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void ctcp_low_level_decode (const char *message, struct str *output) { bool escape = false; for (const char *p = message; *p; p++) { if (escape) { switch (*p) { case '0': str_append_c (output, '\0'); break; case 'r': str_append_c (output, '\r'); break; case 'n': str_append_c (output, '\n'); break; default: str_append_c (output, *p); } escape = false; } else if (*p == CTCP_M_QUOTE) escape = true; else str_append_c (output, *p); } } static void ctcp_intra_decode (const char *chunk, size_t len, struct str *output) { bool escape = false; for (size_t i = 0; i < len; i++) { char c = chunk[i]; if (escape) { if (c == 'a') str_append_c (output, CTCP_X_DELIM); else str_append_c (output, c); escape = false; } else if (c == CTCP_X_QUOTE) escape = true; else str_append_c (output, c); } } static void ctcp_parse_tagged (const char *chunk, size_t len, struct ctcp_chunk *output) { // We may search for the space before doing the higher level decoding, // as it doesn't concern space characters at all size_t tag_end = len; for (size_t i = 0; i < len; i++) if (chunk[i] == ' ') { tag_end = i; break; } output->is_extended = true; ctcp_intra_decode (chunk, tag_end, &output->tag); if (tag_end++ != len) ctcp_intra_decode (chunk + tag_end, len - tag_end, &output->text); } static struct ctcp_chunk * ctcp_parse (const char *message) { struct str m; str_init (&m); ctcp_low_level_decode (message, &m); struct ctcp_chunk *result = NULL, *result_tail = NULL; size_t start = 0; bool in_ctcp = false; for (size_t i = 0; i < m.len; i++) { char c = m.str[i]; if (c != CTCP_X_DELIM) continue; // Remember the current state size_t my_start = start; bool my_is_ctcp = in_ctcp; start = i + 1; in_ctcp = !in_ctcp; // Skip empty chunks if (my_start == i) continue; struct ctcp_chunk *chunk = ctcp_chunk_new (); if (my_is_ctcp) ctcp_parse_tagged (m.str + my_start, i - my_start, chunk); else str_append_data (&chunk->text, m.str + my_start, i - my_start); LIST_APPEND_WITH_TAIL (result, result_tail, chunk); } // Finish the last text part. We ignore unended tagged chunks. // TODO: don't ignore them, e.g. a /me may get cut off if (!in_ctcp && start != m.len) { struct ctcp_chunk *chunk = ctcp_chunk_new (); // According to the original CTCP specification we should use // ctcp_intra_decode() but no one seems to use that and it breaks // normal text with backslashes str_append_data (&chunk->text, m.str + start, m.len - start); LIST_APPEND_WITH_TAIL (result, result_tail, chunk); } str_free (&m); return result; } static void ctcp_destroy (struct ctcp_chunk *list) { LIST_FOR_EACH (struct ctcp_chunk, iter, list) ctcp_chunk_destroy (iter); } // --- Advanced configuration -------------------------------------------------- // This is a new configuration format, superseding the one currently present // in liberty. It's just a lot more complicated and allows key-value maps. // We need it in degesch to provide non-sucking user experience. enum config_item_type { CONFIG_ITEM_NULL, ///< No value CONFIG_ITEM_OBJECT, ///< Key-value map CONFIG_ITEM_BOOLEAN, ///< Truth value CONFIG_ITEM_INTEGER, ///< Integer CONFIG_ITEM_STRING, ///< Arbitrary string of characters CONFIG_ITEM_STRING_ARRAY ///< Comma-separated list of strings }; struct config_item_ { enum config_item_type type; ///< Type of the item union { struct str_map object; ///< Key-value data bool boolean; ///< Boolean data int64_t integer; ///< Integer data struct str string; ///< String data } value; ///< The value of this item struct config_schema *schema; ///< Schema describing this value void *user_data; ///< User value attached by schema owner }; struct config_schema { const char *name; ///< Name of the item const char *comment; ///< User-readable description enum config_item_type type; ///< Required type const char *default_; ///< Default as a configuration snippet /// Check if the new value can be accepted. /// In addition to this, "type" and having a default is considered. bool (*validate) (const struct config_item_ *, struct error **e); /// The value has changed void (*on_change) (struct config_item_ *); }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static const char * config_item_type_name (enum config_item_type type) { switch (type) { case CONFIG_ITEM_NULL: return "null"; case CONFIG_ITEM_BOOLEAN: return "boolean"; case CONFIG_ITEM_INTEGER: return "integer"; case CONFIG_ITEM_STRING: return "string"; case CONFIG_ITEM_STRING_ARRAY: return "string array"; default: hard_assert (!"invalid config item type value"); return NULL; } } static bool config_item_type_is_string (enum config_item_type type) { return type == CONFIG_ITEM_STRING || type == CONFIG_ITEM_STRING_ARRAY; } static void config_item_free (struct config_item_ *self) { switch (self->type) { case CONFIG_ITEM_STRING: case CONFIG_ITEM_STRING_ARRAY: str_free (&self->value.string); break; case CONFIG_ITEM_OBJECT: str_map_free (&self->value.object); default: break; } } static void config_item_destroy (struct config_item_ *self) { config_item_free (self); free (self); } /// Doesn't do any validations or handle schemas, just moves source data /// to the target item and destroys the source item static void config_item_move (struct config_item_ *self, struct config_item_ *source) { // Not quite sure how to handle that hard_assert (!source->schema); config_item_free (self); self->type = source->type; memcpy (&self->value, &source->value, sizeof source->value); free (source); } static struct config_item_ * config_item_new (enum config_item_type type) { struct config_item_ *self = xcalloc (1, sizeof *self); self->type = type; return self; } static struct config_item_ * config_item_null (void) { return config_item_new (CONFIG_ITEM_NULL); } static struct config_item_ * config_item_boolean (bool b) { struct config_item_ *self = config_item_new (CONFIG_ITEM_BOOLEAN); self->value.boolean = b; return self; } static struct config_item_ * config_item_integer (int64_t i) { struct config_item_ *self = config_item_new (CONFIG_ITEM_INTEGER); self->value.integer = i; return self; } static struct config_item_ * config_item_string (const struct str *s) { struct config_item_ *self = config_item_new (CONFIG_ITEM_STRING); str_init (&self->value.string); hard_assert (utf8_validate (self->value.string.str, self->value.string.len)); if (s) str_append_str (&self->value.string, s); return self; } static struct config_item_ * config_item_string_from_cstr (const char *s) { struct str tmp; str_init (&tmp); str_append (&tmp, s); struct config_item_ *self = config_item_string (&tmp); str_free (&tmp); return self; } static struct config_item_ * config_item_string_array (const struct str *s) { struct config_item_ *self = config_item_string (s); self->type = CONFIG_ITEM_STRING_ARRAY; return self; } static struct config_item_ * config_item_object (void) { struct config_item_ *self = config_item_new (CONFIG_ITEM_OBJECT); str_map_init (&self->value.object); self->value.object.free = (void (*)(void *)) config_item_destroy; return self; } static bool config_schema_accepts_type (struct config_schema *self, enum config_item_type type) { if (self->type == type) return true; // This is a bit messy but it has its purpose if (config_item_type_is_string (self->type) && config_item_type_is_string (type)) return true; return !self->default_ && type == CONFIG_ITEM_NULL; } static bool config_item_validate_by_schema (struct config_item_ *self, struct config_schema *schema, struct error **e) { struct error *error = NULL; if (!config_schema_accepts_type (schema, self->type)) error_set (e, "invalid type of value, expected: %s%s", config_item_type_name (schema->type), !schema->default_ ? " (or null)" : ""); else if (schema->validate && !schema->validate (self, &error)) { error_set (e, "%s: %s", "invalid value", error->message); error_free (error); } else return true; return false; } static bool config_item_set_from (struct config_item_ *self, struct config_item_ *source, struct error **e) { struct config_schema *schema = self->schema; if (!schema) { // Easy, we don't know what this item is config_item_move (self, source); return true; } if (!config_item_validate_by_schema (source, schema, e)) return false; // Make sure the string subtype fits the schema if (config_item_type_is_string (source->type) && config_item_type_is_string (schema->type)) source->type = schema->type; config_item_move (self, source); // Notify owner about the change so that they can apply it if (schema->on_change) schema->on_change (self); return true; } static struct config_item_ * config_item_get (struct config_item_ *self, const char *path, struct error **e) { hard_assert (self->type == CONFIG_ITEM_OBJECT); struct str_vector v; str_vector_init (&v); split_str (path, '.', &v); struct config_item_ *result = NULL; size_t i = 0; while (true) { const char *key = v.vector[i]; if (!*key) error_set (e, "empty path element"); else if (!(self = str_map_find (&self->value.object, key))) error_set (e, "`%s' not found in object", key); else if (++i == v.len) result = self; else if (self->type != CONFIG_ITEM_OBJECT) error_set (e, "`%s' is not an object", key); else continue; break; } str_vector_free (&v); return result; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct config_writer { struct str *output; unsigned indent; }; static void config_item_write_object_innards (struct config_writer *self, struct config_item_ *object); static void config_item_write_string (struct str *output, const struct str *s) { str_append_c (output, '"'); for (size_t i = 0; i < s->len; i++) { unsigned char c = s->str[i]; if (c == '\n') str_append (output, "\\n"); else if (c == '\r') str_append (output, "\\r"); else if (c == '\t') str_append (output, "\\t"); else if (c == '\\') str_append (output, "\\\\"); else if (c == '"') str_append (output, "\\\""); else if (c < 32) str_append_printf (output, "\\x%02x", c); else str_append_c (output, c); } str_append_c (output, '"'); } static void config_item_write_object (struct config_writer *self, struct config_item_ *value) { char indent[self->indent + 1]; memset (indent, '\t', self->indent); indent[self->indent] = 0; str_append_c (self->output, '{'); if (value->value.object.len) { self->indent++; str_append_c (self->output, '\n'); config_item_write_object_innards (self, value); self->indent--; str_append (self->output, indent); } str_append_c (self->output, '}'); } static void config_item_write_value (struct config_writer *self, struct config_item_ *value) { switch (value->type) { case CONFIG_ITEM_NULL: str_append (self->output, "null"); break; case CONFIG_ITEM_BOOLEAN: str_append (self->output, value->value.boolean ? "on" : "off"); break; case CONFIG_ITEM_INTEGER: str_append_printf (self->output, "%" PRIi64, value->value.integer); break; case CONFIG_ITEM_STRING: case CONFIG_ITEM_STRING_ARRAY: config_item_write_string (self->output, &value->value.string); break; case CONFIG_ITEM_OBJECT: config_item_write_object (self, value); break; default: hard_assert (!"invalid item type"); } } static void config_item_write_kv_pair (struct config_writer *self, const char *key, struct config_item_ *value) { char indent[self->indent + 1]; memset (indent, '\t', self->indent); indent[self->indent] = 0; if (value->schema && value->schema->comment) str_append_printf (self->output, "%s# %s\n", indent, value->schema->comment); str_append_printf (self->output, "%s%s = ", indent, key); config_item_write_value (self, value); str_append_c (self->output, '\n'); } static void config_item_write_object_innards (struct config_writer *self, struct config_item_ *object) { hard_assert (object->type == CONFIG_ITEM_OBJECT); struct str_map_iter iter; str_map_iter_init (&iter, &object->value.object); struct config_item_ *value; while ((value = str_map_iter_next (&iter))) config_item_write_kv_pair (self, iter.link->key, value); } static void config_item_write (struct config_item_ *value, bool object_innards, struct str *output) { struct config_writer writer = { .output = output, .indent = 0 }; if (object_innards) config_item_write_object_innards (&writer, value); else config_item_write_value (&writer, value); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum config_token { CONFIG_T_ABORT, ///< EOF or error CONFIG_T_WORD, ///< [a-zA-Z0-9_]+ CONFIG_T_EQUALS, ///< Equal sign CONFIG_T_LBRACE, ///< Left curly bracket CONFIG_T_RBRACE, ///< Right curly bracket CONFIG_T_NEWLINE, ///< New line CONFIG_T_NULL, ///< CONFIG_ITEM_NULL CONFIG_T_BOOLEAN, ///< CONFIG_ITEM_BOOLEAN CONFIG_T_INTEGER, ///< CONFIG_ITEM_INTEGER CONFIG_T_STRING ///< CONFIG_ITEM_STRING{,_LIST} }; static const char * config_token_name (enum config_token token) { switch (token) { case CONFIG_T_ABORT: return "end of input"; case CONFIG_T_WORD: return "word"; case CONFIG_T_EQUALS: return "equal sign"; case CONFIG_T_LBRACE: return "left brace"; case CONFIG_T_RBRACE: return "right brace"; case CONFIG_T_NEWLINE: return "newline"; case CONFIG_T_NULL: return "null value"; case CONFIG_T_BOOLEAN: return "boolean"; case CONFIG_T_INTEGER: return "integer"; case CONFIG_T_STRING: return "string"; default: hard_assert (!"invalid token value"); return NULL; } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct config_tokenizer { const char *p; ///< Current position in input size_t len; ///< How many bytes of input are left bool report_line; ///< Whether to count lines at all unsigned line; ///< Current line unsigned column; ///< Current column int64_t integer; ///< Parsed boolean or integer value struct str string; ///< Parsed string value }; /// Input has to be null-terminated anyway static void config_tokenizer_init (struct config_tokenizer *self, const char *p, size_t len) { memset (self, 0, sizeof *self); self->p = p; self->len = len; self->report_line = true; str_init (&self->string); } static void config_tokenizer_free (struct config_tokenizer *self) { str_free (&self->string); } static bool config_tokenizer_is_word_char (int c) { return isalnum_ascii (c) || c == '_'; } static int config_tokenizer_advance (struct config_tokenizer *self) { int c = *self->p++; if (c == '\n' && self->report_line) { self->column = 0; self->line++; } else self->column++; self->len--; return c; } static void config_tokenizer_error (struct config_tokenizer *self, struct error **e, const char *format, ...) ATTRIBUTE_PRINTF (3, 4); static void config_tokenizer_error (struct config_tokenizer *self, struct error **e, const char *format, ...) { struct str description; str_init (&description); va_list ap; va_start (ap, format); str_append_vprintf (&description, format, ap); va_end (ap); if (self->report_line) error_set (e, "near line %u, column %u: %s", self->line + 1, self->column + 1, description.str); else if (self->len) error_set (e, "near character %u: %s", self->column + 1, description.str); else error_set (e, "near end: %s", description.str); str_free (&description); } static bool config_tokenizer_hexa_escape (struct config_tokenizer *self, struct str *output) { int i; unsigned char code = 0; for (i = 0; self->len && i < 2; i++) { unsigned char c = tolower_ascii (*self->p); if (c >= '0' && c <= '9') code = (code << 4) | (c - '0'); else if (c >= 'a' && c <= 'f') code = (code << 4) | (c - 'a' + 10); else break; config_tokenizer_advance (self); } if (!i) return false; str_append_c (output, code); return true; } static bool config_tokenizer_octal_escape (struct config_tokenizer *self, struct str *output) { int i; unsigned char code = 0; for (i = 0; self->len && i < 3; i++) { unsigned char c = *self->p; if (c >= '0' && c <= '7') code = (code << 3) | (c - '0'); else break; config_tokenizer_advance (self); } if (!i) return false; str_append_c (output, code); return true; } static bool config_tokenizer_escape_sequence (struct config_tokenizer *self, struct str *output, struct error **e) { if (!self->len) { config_tokenizer_error (self, e, "premature end of escape sequence"); return false; } unsigned char c; switch ((c = *self->p)) { case '"': break; case '\\': break; case 'a': c = '\a'; break; case 'b': c = '\b'; break; case 'f': c = '\f'; break; case 'n': c = '\n'; break; case 'r': c = '\r'; break; case 't': c = '\t'; break; case 'v': c = '\v'; break; case 'x': case 'X': config_tokenizer_advance (self); if (config_tokenizer_hexa_escape (self, output)) return true; config_tokenizer_error (self, e, "invalid hexadecimal escape"); return false; default: if (config_tokenizer_octal_escape (self, output)) return true; config_tokenizer_error (self, e, "unknown escape sequence"); return false; } str_append_c (output, c); config_tokenizer_advance (self); return true; } static bool config_tokenizer_string (struct config_tokenizer *self, struct str *output, struct error **e) { unsigned char c; while (self->len) { if ((c = config_tokenizer_advance (self)) == '"') return true; if (c != '\\') str_append_c (output, c); else if (!config_tokenizer_escape_sequence (self, output, e)) return false; } config_tokenizer_error (self, e, "premature end of string"); return false; } static enum config_token config_tokenizer_next (struct config_tokenizer *self, struct error **e) { // Skip over any whitespace between tokens while (self->len && isspace_ascii (*self->p) && *self->p != '\n') config_tokenizer_advance (self); if (!self->len) return CONFIG_T_ABORT; switch (*self->p) { case '\n': config_tokenizer_advance (self); return CONFIG_T_NEWLINE; case '=': config_tokenizer_advance (self); return CONFIG_T_EQUALS; case '{': config_tokenizer_advance (self); return CONFIG_T_LBRACE; case '}': config_tokenizer_advance (self); return CONFIG_T_RBRACE; case '#': // Comments go until newline while (self->len) if (config_tokenizer_advance (self) == '\n') return CONFIG_T_NEWLINE; return CONFIG_T_ABORT; case '"': config_tokenizer_advance (self); str_reset (&self->string); if (!config_tokenizer_string (self, &self->string, e)) return CONFIG_T_ABORT; if (!utf8_validate (self->string.str, self->string.len)) { config_tokenizer_error (self, e, "not a valid UTF-8 string"); return CONFIG_T_ABORT; } return CONFIG_T_STRING; } char *end; errno = 0; self->integer = strtoll (self->p, &end, 10); if (errno == ERANGE) { config_tokenizer_error (self, e, "integer out of range"); return CONFIG_T_ABORT; } if (end != self->p) { self->len -= end - self->p; self->p = end; return CONFIG_T_INTEGER; } if (!config_tokenizer_is_word_char (*self->p)) { config_tokenizer_error (self, e, "invalid input"); return CONFIG_T_ABORT; } str_reset (&self->string); do str_append_c (&self->string, config_tokenizer_advance (self)); while (config_tokenizer_is_word_char (*self->p)); if (!strcmp (self->string.str, "null")) return CONFIG_T_NULL; bool boolean; if (!set_boolean_if_valid (&boolean, self->string.str)) return CONFIG_T_WORD; self->integer = boolean; return CONFIG_T_BOOLEAN; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct config_parser { struct config_tokenizer tokenizer; ///< Tokenizer struct error *error; ///< Tokenizer error enum config_token token; ///< Current token in the tokenizer bool replace_token; ///< Replace the token }; static void config_parser_init (struct config_parser *self, const char *script, size_t len) { memset (self, 0, sizeof *self); config_tokenizer_init (&self->tokenizer, script, len); // As reading in tokens may cause exceptions, we wait for the first peek() // to replace the initial CONFIG_T_ABORT. self->replace_token = true; } static void config_parser_free (struct config_parser *self) { config_tokenizer_free (&self->tokenizer); if (self->error) error_free (self->error); } static enum config_token config_parser_peek (struct config_parser *self, jmp_buf out) { if (self->replace_token) { self->token = config_tokenizer_next (&self->tokenizer, &self->error); if (self->error) longjmp (out, 1); self->replace_token = false; } return self->token; } static bool config_parser_accept (struct config_parser *self, enum config_token token, jmp_buf out) { return self->replace_token = (config_parser_peek (self, out) == token); } static void config_parser_expect (struct config_parser *self, enum config_token token, jmp_buf out) { if (config_parser_accept (self, token, out)) return; config_tokenizer_error (&self->tokenizer, &self->error, "unexpected `%s', expected `%s'", config_token_name (self->token), config_token_name (token)); longjmp (out, 1); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // We don't need no generator, but a few macros will come in handy. // From time to time C just doesn't have the right features. #define PEEK() config_parser_peek (self, err) #define ACCEPT(token) config_parser_accept (self, token, err) #define EXPECT(token) config_parser_expect (self, token, err) #define SKIP_NL() do {} while (ACCEPT (CONFIG_T_NEWLINE)) static struct config_item_ *config_parser_parse_object (struct config_parser *self, jmp_buf out); static struct config_item_ * config_parser_parse_value (struct config_parser *self, jmp_buf out) { struct config_item_ *volatile result = NULL; jmp_buf err; if (setjmp (err)) { if (result) config_item_destroy (result); longjmp (out, 1); } if (ACCEPT (CONFIG_T_LBRACE)) { result = config_parser_parse_object (self, out); SKIP_NL (); EXPECT (CONFIG_T_RBRACE); return result; } if (ACCEPT (CONFIG_T_NULL)) return config_item_null (); if (ACCEPT (CONFIG_T_BOOLEAN)) return config_item_boolean (self->tokenizer.integer); if (ACCEPT (CONFIG_T_INTEGER)) return config_item_integer (self->tokenizer.integer); if (ACCEPT (CONFIG_T_STRING)) return config_item_string (&self->tokenizer.string); config_tokenizer_error (&self->tokenizer, &self->error, "unexpected `%s', expected a value", config_token_name (self->token)); longjmp (out, 1); } /// Parse a single "key = value" assignment into @a object static bool config_parser_parse_kv_pair (struct config_parser *self, struct config_item_ *object, jmp_buf out) { char *volatile key = NULL; jmp_buf err; if (setjmp (err)) { free (key); longjmp (out, 1); } SKIP_NL (); // Either this object's closing right brace if called recursively, // or end of file when called on a whole configuration file if (PEEK () == CONFIG_T_RBRACE || PEEK () == CONFIG_T_ABORT) return false; EXPECT (CONFIG_T_WORD); key = xstrdup (self->tokenizer.string.str); SKIP_NL (); EXPECT (CONFIG_T_EQUALS); SKIP_NL (); str_map_set (&object->value.object, key, config_parser_parse_value (self, err)); free (key); key = NULL; if (PEEK () == CONFIG_T_RBRACE || PEEK () == CONFIG_T_ABORT) return false; EXPECT (CONFIG_T_NEWLINE); return true; } /// Parse the inside of an object definition static struct config_item_ * config_parser_parse_object (struct config_parser *self, jmp_buf out) { struct config_item_ *volatile object = config_item_object (); jmp_buf err; if (setjmp (err)) { config_item_destroy (object); longjmp (out, 1); } while (config_parser_parse_kv_pair (self, object, err)) ; return object; } #undef PEEK #undef ACCEPT #undef EXPECT #undef SKIP_NL // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Parse a configuration snippet either as an object or a bare value. /// If it's the latter (@a single_value_only), no newlines may follow. static struct config_item_ * config_item_parse (const char *script, size_t len, bool single_value_only, struct error **e) { struct config_parser parser; config_parser_init (&parser, script, len); struct config_item_ *volatile object = NULL; jmp_buf err; if (setjmp (err)) { if (object) { config_item_destroy (object); object = NULL; } error_propagate (e, parser.error); parser.error = NULL; goto end; } if (single_value_only) { // This is really only intended for in-program configuration // and telling the line number would look awkward parser.tokenizer.report_line = false; object = config_parser_parse_value (&parser, err); } else object = config_parser_parse_object (&parser, err); config_parser_expect (&parser, CONFIG_T_ABORT, err); end: config_parser_free (&parser); return object; } /// Clone an item. Schema assignments aren't retained. struct config_item_ * config_item_clone (struct config_item_ *self) { // Oh well, it saves code struct str tmp; str_init (&tmp); config_item_write (self, false, &tmp); struct config_item_ *result = config_item_parse (tmp.str, tmp.len, true, NULL); str_free (&tmp); return result; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void config_schema_initialize_item (struct config_schema *schema, struct config_item_ *parent, void *user_data) { struct config_item_ *item = str_map_find (&parent->value.object, schema->name); bool replace = true; if (item) { // FIXME: either do this silently or tell about it via a callback // or just store it in an output vector; don't print it directly struct error *e = NULL; replace = !config_item_validate_by_schema (item, schema, &e); if (e) { print_error ("resetting configuration item " "`%s' to default: %s", schema->name, e->message); error_free (e); } } if (replace) { struct error *e = NULL; if (schema->default_) item = config_item_parse (schema->default_, strlen (schema->default_), true, &e); else item = config_item_null (); if (e || !config_item_validate_by_schema (item, schema, &e)) exit_fatal ("invalid default for `%s': %s", schema->name, e->message); // This will free the old item if there was any str_map_set (&parent->value.object, schema->name, item); } // Make sure the string subtype fits the schema if (config_item_type_is_string (item->type) && config_item_type_is_string (schema->type)) item->type = schema->type; item->schema = schema; item->user_data = user_data; } static void config_schema_apply_to_object (struct config_schema *schema_array, struct config_item_ *object, void *user_data) { hard_assert (object->type == CONFIG_ITEM_OBJECT); while (schema_array->name) config_schema_initialize_item (schema_array++, object, user_data); } static void config_schema_call_changed (struct config_item_ *item) { if (item->type == CONFIG_ITEM_OBJECT) { struct str_map_iter iter; str_map_iter_init (&iter, &item->value.object); struct config_item_ *child; while ((child = str_map_iter_next (&iter))) config_schema_call_changed (child); } else if (item->schema && item->schema->on_change) item->schema->on_change (item); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // XXX: this doesn't necessarily have to be well designed at all typedef void (*config_module_load_fn) (struct config_item_ *subtree, void *user_data); struct config_module { char *name; ///< Name of the subtree config_module_load_fn loader; ///< Module config subtree loader void *user_data; ///< User data }; static struct config_module * config_module_new () { struct config_module *self = xcalloc (1, sizeof *self); return self; } static void config_module_destroy (struct config_module *self) { free (self->name); free (self); } struct config { struct str_map modules; ///< Toplevel modules struct config_item_ *root; ///< CONFIG_ITEM_OBJECT }; static void config_init (struct config *self) { memset (self, 0, sizeof *self); str_map_init (&self->modules); self->modules.free = (void (*) (void *)) config_module_destroy; } static void config_free (struct config *self) { str_map_free (&self->modules); if (self->root) config_item_destroy (self->root); } static void config_register_module (struct config *self, const char *name, config_module_load_fn loader, void *user_data) { struct config_module *module = config_module_new (); module->name = xstrdup (name); module->loader = loader; module->user_data = user_data; str_map_set (&self->modules, name, module); } static void config_load (struct config *self, struct config_item_ *root) { hard_assert (root->type == CONFIG_ITEM_OBJECT); self->root = root; struct str_map_iter iter; str_map_iter_init (&iter, &self->modules); struct config_module *module; while ((module = str_map_iter_next (&iter))) { struct config_item_ *subtree = str_map_find (&root->value.object, module->name); // Silently fix inputs that only a lunatic user could create if (!subtree || subtree->type != CONFIG_ITEM_OBJECT) { subtree = config_item_object (); str_map_set (&root->value.object, module->name, subtree); } if (module->loader) module->loader (subtree, module->user_data); } }