degesch: use the new configuration

This is a simple, almost 1:1 conversion.  Needs further unfucking.
This commit is contained in:
Přemysl Eric Janouch 2015-05-02 23:00:34 +02:00
parent 3b8e8cc97f
commit c23898166c
1 changed files with 398 additions and 242 deletions

640
degesch.c
View File

@ -67,41 +67,6 @@ enum
#include <readline/readline.h>
#include <readline/history.h>
// --- 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);