diff --git a/degesch.c b/degesch.c index 27886f0..1235b5c 100644 --- a/degesch.c +++ b/degesch.c @@ -67,41 +67,6 @@ enum #include #include -// --- Configuration (application-specific) ------------------------------------ - -// TODO: reject all junk present in the configuration; there can be newlines - -static struct config_item g_config_table[] = -{ - { "nickname", NULL, "IRC nickname" }, - { "username", NULL, "IRC user name" }, - { "realname", NULL, "IRC real name/e-mail" }, - - { "irc_host", NULL, "Address of the IRC server" }, - { "irc_port", "6667", "Port of the IRC server" }, - { "ssl", "off", "Whether to use SSL" }, - { "ssl_cert", NULL, "Client SSL certificate (PEM)" }, - { "ssl_verify", "on", "Whether to verify certificates" }, - { "ssl_ca_file", NULL, "OpenSSL CA bundle file" }, - { "ssl_ca_path", NULL, "OpenSSL CA bundle path" }, - { "autojoin", NULL, "Channels to join on start" }, - { "reconnect", "on", "Whether to reconnect on error" }, - { "reconnect_delay", "5", "Time between reconnecting" }, - - { "socks_host", NULL, "Address of a SOCKS 4a/5 proxy" }, - { "socks_port", "1080", "SOCKS port number" }, - { "socks_username", NULL, "SOCKS auth. username" }, - { "socks_password", NULL, "SOCKS auth. password" }, - - { "isolate_buffers", "off", "Isolate global/server buffers" }, - -#define XX(x, y, z) { "attr_" y, NULL, z }, - ATTR_TABLE (XX) -#undef XX - - { NULL, NULL, NULL } -}; - // --- Application data -------------------------------------------------------- // All text stored in our data structures is encoded in UTF-8. @@ -492,7 +457,7 @@ struct app_context { // Configuration: - struct str_map config; ///< User configuration + struct config config; ///< Program configuration char *attrs[ATTR_COUNT]; ///< Terminal attributes bool no_colors; ///< Colour output mode bool reconnect; ///< Whether to reconnect on conn. fail. @@ -546,10 +511,7 @@ app_context_init (struct app_context *self) { memset (self, 0, sizeof *self); - str_map_init (&self->config); - self->config.free = free; - load_config_defaults (&self->config, g_config_table); - + config_init (&self->config); poller_init (&self->poller); server_init (&self->server, &self->poller); @@ -582,7 +544,7 @@ app_context_init (struct app_context *self) static void app_context_free (struct app_context *self) { - str_map_free (&self->config); + config_free (&self->config); for (size_t i = 0; i < ATTR_COUNT; i++) free (self->attrs[i]); @@ -605,6 +567,224 @@ static void refresh_prompt (struct app_context *ctx); static char *irc_cut_nickname (const char *prefix); static const char *irc_find_userhost (const char *prefix); +// --- Configuration ----------------------------------------------------------- + +// TODO: eventually add "on_change" callbacks + +static bool +config_validate_nonjunk_string + (const struct config_item_ *item, struct error **e) +{ + if (item->type == CONFIG_ITEM_NULL) + return true; + + hard_assert (config_item_type_is_string (item->type)); + for (size_t i = 0; i < item->value.string.len; i++) + { + // Not even a tabulator + unsigned char c = item->value.string.str[i]; + if (c < 32) + { + error_set (e, "control characters are not allowed"); + return false; + } + } + return true; +} + +static bool +config_validate_nonnegative + (const struct config_item_ *item, struct error **e) +{ + if (item->type == CONFIG_ITEM_NULL) + return true; + + hard_assert (item->type == CONFIG_ITEM_INTEGER); + if (item->value.integer >= 0) + return true; + + error_set (e, "must be non-negative"); + return false; +} + +struct config_schema g_config_server[] = +{ + { .name = "nickname", + .comment = "IRC nickname", + .type = CONFIG_ITEM_STRING, + .validate = config_validate_nonjunk_string }, + { .name = "username", + .comment = "IRC user name", + .type = CONFIG_ITEM_STRING, + .validate = config_validate_nonjunk_string }, + { .name = "realname", + .comment = "IRC real name/e-mail", + .type = CONFIG_ITEM_STRING, + .validate = config_validate_nonjunk_string }, + + { .name = "irc_host", + .comment = "Address of the IRC server", + .type = CONFIG_ITEM_STRING, + .validate = config_validate_nonjunk_string }, + { .name = "irc_port", + .comment = "Port of the IRC server", + .type = CONFIG_ITEM_INTEGER, + .validate = config_validate_nonnegative, + .default_ = "6667" }, + + { .name = "ssl", + .comment = "Whether to use SSL/TLS", + .type = CONFIG_ITEM_BOOLEAN, + .default_ = "off" }, + { .name = "ssl_cert", + .comment = "Client SSL certificate (PEM)", + .type = CONFIG_ITEM_STRING }, + { .name = "ssl_verify", + .comment = "Whether to verify certificates", + .type = CONFIG_ITEM_BOOLEAN, + .default_ = "on" }, + { .name = "ssl_ca_file", + .comment = "OpenSSL CA bundle file", + .type = CONFIG_ITEM_STRING }, + { .name = "ssl_ca_path", + .comment = "OpenSSL CA bundle path", + .type = CONFIG_ITEM_STRING }, + + { .name = "autojoin", + .comment = "Channels to join on start", + .type = CONFIG_ITEM_STRING_ARRAY, + .validate = config_validate_nonjunk_string }, + { .name = "reconnect", + .comment = "Whether to reconnect on error", + .type = CONFIG_ITEM_BOOLEAN, + .default_ = "on" }, + { .name = "reconnect_delay", + .comment = "Time between reconnecting", + .type = CONFIG_ITEM_INTEGER, + .default_ = "5" }, + + { .name = "socks_host", + .comment = "Address of a SOCKS 4a/5 proxy", + .type = CONFIG_ITEM_STRING, + .validate = config_validate_nonjunk_string }, + { .name = "socks_port", + .comment = "SOCKS port number", + .type = CONFIG_ITEM_INTEGER, + .validate = config_validate_nonnegative, + .default_ = "1080" }, + { .name = "socks_username", + .comment = "SOCKS auth. username", + .type = CONFIG_ITEM_STRING }, + { .name = "socks_password", + .comment = "SOCKS auth. password", + .type = CONFIG_ITEM_STRING }, + {} +}; + +struct config_schema g_config_behaviour[] = +{ + { .name = "isolate_buffers", + .comment = "Don't leak messages from the server and global buffers", + .type = CONFIG_ITEM_BOOLEAN, + .default_ = "off" }, + {} +}; + +struct config_schema g_config_attributes[] = +{ +#define XX(x, y, z) { .name = y, .comment = z, .type = CONFIG_ITEM_STRING }, + ATTR_TABLE (XX) +#undef XX + {} +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +load_config_server (struct config_item_ *subtree, void *user_data) +{ + (void) user_data; + // This will eventually iterate over the object and create servers + config_schema_apply_to_object (g_config_server, subtree); +} + +static void +load_config_behaviour (struct config_item_ *subtree, void *user_data) +{ + (void) user_data; + config_schema_apply_to_object (g_config_behaviour, subtree); +} + +static void +load_config_attributes (struct config_item_ *subtree, void *user_data) +{ + (void) user_data; + config_schema_apply_to_object (g_config_attributes, subtree); +} + +static void +register_config_modules (struct app_context *ctx) +{ + struct config *config = &ctx->config; + config_register_module (config, + "server", load_config_server, ctx); + config_register_module (config, + "behaviour", load_config_behaviour, ctx); + config_register_module (config, + "attributes", load_config_attributes, ctx); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static const char * +get_config_string (struct app_context *ctx, const char *key) +{ + struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); + hard_assert (item); + if (item->type == CONFIG_ITEM_NULL) + return NULL; + hard_assert (config_item_type_is_string (item->type)); + return item->value.string.str; +} + +static bool +set_config_string (struct app_context *ctx, const char *key, const char *value) +{ + struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); + hard_assert (item); + + struct str s; + str_init (&s); + str_append (&s, value); + struct config_item_ *new_ = config_item_string (&s); + str_free (&s); + + struct error *e = NULL; + if (config_item_set_from (item, new_, &e)) + return true; + + config_item_destroy (new_); + print_error ("couldn't set `%s' in configuration: %s", key, e->message); + error_free (e); + return false; +} + +static int64_t +get_config_integer (struct app_context *ctx, const char *key) +{ + struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); + hard_assert (item && item->type == CONFIG_ITEM_INTEGER); + return item->value.integer; +} + +static bool +get_config_boolean (struct app_context *ctx, const char *key) +{ + struct config_item_ *item = config_item_get (ctx->config.root, key, NULL); + hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN); + return item->value.boolean; +} + // --- Attributed output ------------------------------------------------------- static struct @@ -775,12 +955,12 @@ init_attribute (struct app_context *ctx, int id, const char *default_) { static const char *table[ATTR_COUNT] = { -#define XX(x, y, z) [ATTR_ ## x] = "attr_" y, +#define XX(x, y, z) [ATTR_ ## x] = "attributes." y, ATTR_TABLE (XX) #undef XX }; - const char *user = str_map_find (&ctx->config, table[id]); + const char *user = get_config_string (ctx, table[id]); if (user) ctx->attrs[id] = xstrdup (user); else @@ -1633,7 +1813,7 @@ init_buffers (struct app_context *ctx) global->name = xstrdup (PROGRAM_NAME); server->type = BUFFER_SERVER; - server->name = xstrdup (str_map_find (&ctx->config, "irc_host")); + server->name = xstrdup (get_config_string (ctx, "server.irc_host")); server->server = &ctx->server; LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, global); @@ -1893,34 +2073,17 @@ irc_send (struct server *s, const char *format, ...) return result; } -static bool -irc_get_boolean_from_config - (struct app_context *ctx, const char *name, bool *value, struct error **e) -{ - const char *str = str_map_find (&ctx->config, name); - hard_assert (str != NULL); - - if (set_boolean_if_valid (value, str)) - return true; - - error_set (e, "invalid configuration value for `%s'", name); - return false; -} - static bool irc_initialize_ssl_ctx (struct server *s, struct error **e) { // XXX: maybe we should call SSL_CTX_set_options() for some workarounds - bool verify; - if (!irc_get_boolean_from_config (s->ctx, "ssl_verify", &verify, e)) - return false; - + bool verify = get_config_boolean (s->ctx, "server.ssl_verify"); if (!verify) SSL_CTX_set_verify (s->ssl_ctx, SSL_VERIFY_NONE, NULL); - const char *ca_file = str_map_find (&s->ctx->config, "ca_file"); - const char *ca_path = str_map_find (&s->ctx->config, "ca_path"); + const char *ca_file = get_config_string (s->ctx, "server.ca_file"); + const char *ca_path = get_config_string (s->ctx, "server.ca_path"); struct error *error = NULL; if (ca_file || ca_path) @@ -1970,7 +2133,7 @@ irc_initialize_ssl (struct server *s, struct error **e) if (!s->ssl) goto error_ssl_2; - const char *ssl_cert = str_map_find (&s->ctx->config, "ssl_cert"); + const char *ssl_cert = get_config_string (s->ctx, "server.ssl_cert"); if (ssl_cert) { char *path = resolve_config_filename (ssl_cert); @@ -3147,7 +3310,7 @@ irc_process_message (const struct irc_message *msg, // we can also use WHOIS if it's not supported (optional by RFC 2812) irc_send (s, "USERHOST %s", s->irc_user->nickname); - const char *autojoin = str_map_find (&s->ctx->config, "autojoin"); + const char *autojoin = get_config_string (s->ctx, "server.autojoin"); if (autojoin) irc_send (s, "JOIN :%s", autojoin); } @@ -4076,8 +4239,7 @@ on_irc_timeout (void *user_data) { // Provoke a response from the server struct server *s = user_data; - irc_send (s, "PING :%s", - (char *) str_map_find (&s->ctx->config, "nickname")); + irc_send (s, "PING :%s", get_config_string (s->ctx, "server.nickname")); } static void @@ -4146,30 +4308,29 @@ irc_connect (struct server *s, struct error **e) { struct app_context *ctx = s->ctx; - const char *irc_host = str_map_find (&ctx->config, "irc_host"); - const char *irc_port = str_map_find (&ctx->config, "irc_port"); + const char *irc_host = get_config_string (ctx, "server.irc_host"); + int64_t irc_port_int = get_config_integer (ctx, "server.irc_port"); - const char *socks_host = str_map_find (&ctx->config, "socks_host"); - const char *socks_port = str_map_find (&ctx->config, "socks_port"); - const char *socks_username = str_map_find (&ctx->config, "socks_username"); - const char *socks_password = str_map_find (&ctx->config, "socks_password"); + const char *socks_host = get_config_string (ctx, "server.socks_host"); + int64_t socks_port_int = get_config_integer (ctx, "server.socks_port"); + const char *socks_username = + get_config_string (ctx, "server.socks_username"); + const char *socks_password = + get_config_string (ctx, "server.socks_password"); - const char *nickname = str_map_find (&ctx->config, "nickname"); - const char *username = str_map_find (&ctx->config, "username"); - const char *realname = str_map_find (&ctx->config, "realname"); + // FIXME: use it as a number everywhere, there's no need for named services + // FIXME: memory leak + char *irc_port = xstrdup_printf ("%" PRIi64, irc_port_int); + char *socks_port = xstrdup_printf ("%" PRIi64, socks_port_int); - // We have a default value for these - hard_assert (irc_port && socks_port); + const char *nickname = get_config_string (ctx, "server.nickname"); + const char *username = get_config_string (ctx, "server.username"); + const char *realname = get_config_string (ctx, "server.realname"); // These are filled automatically if needed hard_assert (nickname && username && realname); - // TODO: again, get rid of `struct error' in here. The question is: how - // do we tell our caller that he should not try to reconnect? - bool use_ssl; - if (!irc_get_boolean_from_config (ctx, "ssl", &use_ssl, e)) - return false; - + bool use_ssl = get_config_boolean (ctx, "server.ssl"); if (socks_host) { char *address = format_host_port_pair (irc_host, irc_port); @@ -4288,115 +4449,12 @@ on_readline_input (char *line) // --- Configuration loading --------------------------------------------------- -static bool -read_hexa_escape (const char **cursor, struct str *output) -{ - int i; - char c, code = 0; - - for (i = 0; i < 2; i++) - { - c = tolower (*(*cursor)); - if (c >= '0' && c <= '9') - code = (code << 4) | (c - '0'); - else if (c >= 'a' && c <= 'f') - code = (code << 4) | (c - 'a' + 10); - else - break; - - (*cursor)++; - } - - if (!i) - return false; - - str_append_c (output, code); - return true; -} - -static bool -read_octal_escape (const char **cursor, struct str *output) -{ - int i; - char c, code = 0; - - for (i = 0; i < 3; i++) - { - c = *(*cursor); - if (c < '0' || c > '7') - break; - - code = (code << 3) | (c - '0'); - (*cursor)++; - } - - if (!i) - return false; - - str_append_c (output, code); - return true; -} - -static bool -read_string_escape_sequence (const char **cursor, - struct str *output, struct error **e) -{ - int c; - switch ((c = *(*cursor)++)) - { - case '?': str_append_c (output, '?'); break; - case '"': str_append_c (output, '"'); break; - case '\\': str_append_c (output, '\\'); break; - case 'a': str_append_c (output, '\a'); break; - case 'b': str_append_c (output, '\b'); break; - case 'f': str_append_c (output, '\f'); break; - case 'n': str_append_c (output, '\n'); break; - case 'r': str_append_c (output, '\r'); break; - case 't': str_append_c (output, '\t'); break; - case 'v': str_append_c (output, '\v'); break; - - case 'e': - case 'E': - str_append_c (output, '\x1b'); - break; - - case 'x': - case 'X': - if (!read_hexa_escape (cursor, output)) - FAIL ("invalid hexadecimal escape"); - break; - - case '\0': - FAIL ("premature end of escape sequence"); - - default: - (*cursor)--; - if (!read_octal_escape (cursor, output)) - FAIL ("unknown escape sequence"); - } - return true; -} - -static bool -unescape_string (const char *s, struct str *output, struct error **e) -{ - int c; - while ((c = *s++)) - { - if (c != '\\') - str_append_c (output, c); - else if (!read_string_escape_sequence (&s, output, e)) - return false; - } - return true; -} - static bool autofill_user_info (struct app_context *ctx, struct error **e) { - const char *nickname = str_map_find (&ctx->config, "nickname"); - const char *username = str_map_find (&ctx->config, "username"); - const char *realname = str_map_find (&ctx->config, "realname"); + const char *nickname = get_config_string (ctx, "server.nickname"); + const char *username = get_config_string (ctx, "server.username"); + const char *realname = get_config_string (ctx, "server.realname"); if (nickname && username && realname) return true; @@ -4407,9 +4465,9 @@ autofill_user_info (struct app_context *ctx, struct error **e) FAIL ("cannot retrieve user information: %s", strerror (errno)); if (!nickname) - str_map_set (&ctx->config, "nickname", xstrdup (pwd->pw_name)); + set_config_string (ctx, "server.nickname", pwd->pw_name); if (!username) - str_map_set (&ctx->config, "username", xstrdup (pwd->pw_name)); + set_config_string (ctx, "server.username", pwd->pw_name); // Not all systems have the GECOS field but the vast majority does if (!realname) @@ -4421,76 +4479,174 @@ autofill_user_info (struct app_context *ctx, struct error **e) if (comma) *comma = '\0'; - str_map_set (&ctx->config, "realname", xstrdup (gecos)); + set_config_string (ctx, "server.realname", gecos); } return true; } static bool -unescape_config (struct str_map *input, struct str_map *output, struct error **e) +read_file (const char *filename, struct str *output, struct error **e) { - struct error *error = NULL; - struct str_map_iter iter; - str_map_iter_init (&iter, input); - while (str_map_iter_next (&iter)) + FILE *fp = fopen (filename, "rb"); + if (!fp) { - struct str value; - str_init (&value); - if (!unescape_string (iter.link->data, &value, &error)) - { - error_set (e, "error reading configuration: %s: %s", - iter.link->key, error->message); - error_free (error); - return false; - } - - str_map_set (output, iter.link->key, str_steal (&value)); + error_set (e, "could not open `%s' for reading: %s", + filename, strerror (errno)); + return false; } - return true; + + char buf[BUFSIZ]; + size_t len; + + while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf) + str_append_data (output, buf, len); + str_append_data (output, buf, len); + + bool success = !ferror (fp); + fclose (fp); + + if (success) + return true; + + error_set (e, "error while reading `%s': %s", + filename, strerror (errno)); + return false; } static bool -load_config (struct app_context *ctx, struct error **e) +load_configuration (struct app_context *ctx, struct error **e) { - // TODO: employ a better configuration file format, so that we don't have - // to do this convoluted post-processing anymore. + char *filename = resolve_config_filename (PROGRAM_NAME ".conf"); + if (!filename) + { + error_set (e, "cannot find configuration"); + return false; + } - struct str_map map; - str_map_init (&map); - map.free = free; - - bool success = read_config_file (&map, e) && - unescape_config (&map, &ctx->config, e) && - autofill_user_info (ctx, e); - str_map_free (&map); + struct str data; + str_init (&data); + bool success = read_file (filename, &data, e); + free (filename); if (!success) + { + str_free (&data); + return false; + } + + struct error *error = NULL; + struct config_item_ *root = + config_item_parse (data.str, data.len, false, &error); + str_free (&data); + if (!root) + { + error_set (e, "configuration parse error: %s", error->message); + error_free (error); + return false; + } + + config_load (&ctx->config, root); + + if (!autofill_user_info (ctx, e)) return false; - const char *irc_host = str_map_find (&ctx->config, "irc_host"); - if (!irc_host) + if (!get_config_string (ctx, "server.irc_host")) { error_set (e, "no hostname specified in configuration"); return false; } - if (!irc_get_boolean_from_config (ctx, - "reconnect", &ctx->reconnect, e) - || !irc_get_boolean_from_config (ctx, - "isolate_buffers", &ctx->isolate_buffers, e)) - return false; - - const char *delay_str = str_map_find (&ctx->config, "reconnect_delay"); - hard_assert (delay_str != NULL); // We have a default value for this - if (!xstrtoul (&ctx->reconnect_delay, delay_str, 10)) - { - error_set (e, "invalid configuration value for `%s'", - "reconnect_delay"); - return false; - } + ctx->reconnect = + get_config_boolean (ctx, "server.reconnect"); + ctx->isolate_buffers = + get_config_boolean (ctx, "behaviour.isolate_buffers"); + ctx->reconnect_delay = + get_config_integer (ctx, "server.reconnect_delay"); return true; } +static char * +write_configuration_file (const struct str *data, struct error **e) +{ + struct str path; + str_init (&path); + get_xdg_home_dir (&path, "XDG_CONFIG_HOME", ".config"); + str_append (&path, "/" PROGRAM_NAME); + + if (!mkdir_with_parents (path.str, e)) + goto error; + + str_append (&path, "/" PROGRAM_NAME ".conf"); + FILE *fp = fopen (path.str, "w"); + if (!fp) + { + error_set (e, "could not open `%s' for writing: %s", + path.str, strerror (errno)); + goto error; + } + + errno = 0; + fwrite (data->str, data->len, 1, fp); + fclose (fp); + + if (errno) + { + error_set (e, "writing to `%s' failed: %s", path.str, strerror (errno)); + goto error; + } + return str_steal (&path); + +error: + str_free (&path); + return NULL; +} + +static void +serialize_configuration (struct app_context *ctx, struct str *output) +{ + str_append (output, + "# " 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" + "# Everything is in UTF-8. Any custom comments will be overwritten.\n" + "\n"); + + config_item_write (ctx->config.root, true, output); +} + +static void +write_default_configuration () +{ + // XXX: this is a hack before we remove this awkward functionality + // altogether; the user will want to do this from the user interface + + struct app_context ctx = {}; + config_init (&ctx.config); + register_config_modules (&ctx); + config_load (&ctx.config, config_item_object ()); + + struct str data; + str_init (&data); + serialize_configuration (&ctx, &data); + + struct error *e = NULL; + char *filename = write_configuration_file (&data, &e); + str_free (&data); + config_free (&ctx.config); + + if (!filename) + { + print_error ("%s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + print_status ("configuration written to `%s'", filename); + free (filename); +} + + // --- Main program ------------------------------------------------------------ static void @@ -4519,8 +4675,7 @@ main (int argc, char *argv[]) { 'd', "debug", NULL, 0, "run in debug mode" }, { 'h', "help", NULL, 0, "display this help and exit" }, { 'V', "version", NULL, 0, "output version information and exit" }, - { 'w', "write-default-cfg", "FILENAME", - OPT_OPTIONAL_ARG | OPT_LONG_ONLY, + { 'w', "write-default-cfg", NULL, OPT_LONG_ONLY, "write a default configuration file and exit" }, { 0, NULL, NULL, 0, NULL } }; @@ -4542,7 +4697,7 @@ main (int argc, char *argv[]) printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); exit (EXIT_SUCCESS); case 'w': - call_write_default_config (optarg, g_config_table); + write_default_configuration (); exit (EXIT_SUCCESS); default: print_error ("wrong options"); @@ -4571,9 +4726,10 @@ main (int argc, char *argv[]) stifle_history (HISTORY_LIMIT); setup_signal_handlers (); + register_config_modules (&ctx); struct error *e = NULL; - if (!load_config (&ctx, &e)) + if (!load_configuration (&ctx, &e)) { print_error ("%s", e->message); error_free (e);