2093 lines
54 KiB
C
2093 lines
54 KiB
C
/*
|
|
* 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
|
|
|
|
// --- To be moved to liberty --------------------------------------------------
|
|
|
|
static void
|
|
split_str (const char *s, const char *delimiters, struct str_vector *out)
|
|
{
|
|
const char *begin = s, *end;
|
|
while ((end = strpbrk (begin, delimiters)))
|
|
{
|
|
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);
|
|
}
|
|
|
|
// --- SOCKS 5/4a --------------------------------------------------------------
|
|
|
|
// Asynchronous SOCKS connector. Adds more stuff on top of the regular one.
|
|
|
|
// 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.
|
|
|
|
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
|
|
char *domain; ///< Domain name
|
|
uint8_t ipv6[16]; ///< IPv6 address, network octet order
|
|
}
|
|
data; ///< The address itself
|
|
};
|
|
|
|
static void
|
|
socks_addr_free (struct socks_addr *self)
|
|
{
|
|
if (self->type == SOCKS_DOMAIN)
|
|
free (self->data.domain);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
struct socks_target
|
|
{
|
|
LIST_HEADER (struct socks_target)
|
|
|
|
char *address_str; ///< Target address as a string
|
|
struct socks_addr address; ///< Target address
|
|
uint16_t port; ///< Target service port
|
|
};
|
|
|
|
enum socks_protocol
|
|
{
|
|
SOCKS_5, ///< SOCKS5
|
|
SOCKS_4A, ///< SOCKS4A
|
|
SOCKS_MAX ///< End of protocol
|
|
};
|
|
|
|
static inline const char *
|
|
socks_protocol_to_string (enum socks_protocol self)
|
|
{
|
|
switch (self)
|
|
{
|
|
case SOCKS_5: return "SOCKS5";
|
|
case SOCKS_4A: return "SOCKS4A";
|
|
default: return NULL;
|
|
}
|
|
}
|
|
|
|
struct socks_connector
|
|
{
|
|
struct connector *connector; ///< Proxy server iterator (effectively)
|
|
enum socks_protocol protocol_iter; ///< Protocol iterator
|
|
struct socks_target *targets_iter; ///< Targets iterator
|
|
|
|
// Negotiation:
|
|
|
|
struct poller_timer timeout; ///< Timeout timer
|
|
|
|
int socket_fd; ///< Current socket file descriptor
|
|
struct poller_fd socket_event; ///< Socket can be read from/written to
|
|
struct str read_buffer; ///< Read buffer
|
|
struct str write_buffer; ///< Write buffer
|
|
|
|
bool done; ///< Tunnel succesfully established
|
|
uint8_t bound_address_len; ///< Length of domain name
|
|
size_t data_needed; ///< How much data "on_data" needs
|
|
|
|
/// Process incoming data if there's enough of it available
|
|
bool (*on_data) (struct socks_connector *, struct msg_unpacker *);
|
|
|
|
// Configuration:
|
|
|
|
char *hostname; ///< SOCKS server hostname
|
|
char *service; ///< SOCKS server service name or port
|
|
|
|
char *username; ///< Username for authentication
|
|
char *password; ///< Password for authentication
|
|
|
|
struct socks_target *targets; ///< Targets
|
|
struct socks_target *targets_tail; ///< Tail of targets
|
|
|
|
void *user_data; ///< User data for callbacks
|
|
|
|
// Additional results:
|
|
|
|
struct socks_addr bound_address; ///< Bound address at the server
|
|
uint16_t bound_port; ///< Bound port at the server
|
|
|
|
// 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, const char *via, const char *version);
|
|
/// Connecting to the last address has failed
|
|
void (*on_error) (void *user_data, const char *error);
|
|
};
|
|
|
|
// I've tried to make the actual protocol handlers as simple as possible
|
|
|
|
#define SOCKS_FAIL(...) \
|
|
BLOCK_START \
|
|
char *error = xstrdup_printf (__VA_ARGS__); \
|
|
if (self->on_error) \
|
|
self->on_error (self->user_data, error); \
|
|
free (error); \
|
|
return false; \
|
|
BLOCK_END
|
|
|
|
#define SOCKS_DATA_CB(name) static bool name \
|
|
(struct socks_connector *self, struct msg_unpacker *unpacker)
|
|
|
|
#define SOCKS_GO(name, data_needed_) \
|
|
self->on_data = name; \
|
|
self->data_needed = data_needed_; \
|
|
return true
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
SOCKS_DATA_CB (socks_4a_finish)
|
|
{
|
|
uint8_t null, status;
|
|
hard_assert (msg_unpacker_u8 (unpacker, &null));
|
|
hard_assert (msg_unpacker_u8 (unpacker, &status));
|
|
|
|
if (null != 0)
|
|
SOCKS_FAIL ("protocol error");
|
|
|
|
switch (status)
|
|
{
|
|
case 90:
|
|
self->done = true;
|
|
return false;
|
|
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");
|
|
}
|
|
}
|
|
|
|
static bool
|
|
socks_4a_start (struct socks_connector *self)
|
|
{
|
|
struct socks_target *target = self->targets_iter;
|
|
const void *dest_ipv4 = "\x00\x00\x00\x01";
|
|
const char *dest_domain = NULL;
|
|
|
|
char buf[INET6_ADDRSTRLEN];
|
|
switch (target->address.type)
|
|
{
|
|
case SOCKS_IPV4:
|
|
dest_ipv4 = target->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, &target->address.data.ipv6, buf, sizeof buf))
|
|
SOCKS_FAIL ("%s: %s", "inet_ntop", strerror (errno));
|
|
dest_domain = buf;
|
|
break;
|
|
case SOCKS_DOMAIN:
|
|
dest_domain = target->address.data.domain;
|
|
}
|
|
|
|
struct str *wb = &self->write_buffer;
|
|
str_init (wb);
|
|
str_pack_u8 (wb, 4); // version
|
|
str_pack_u8 (wb, 1); // connect
|
|
|
|
str_pack_u16 (wb, target->port); // port
|
|
str_append_data (wb, dest_ipv4, 4); // destination address
|
|
|
|
if (self->username)
|
|
str_append (wb, self->username);
|
|
str_append_c (wb, '\0');
|
|
|
|
if (dest_domain)
|
|
{
|
|
str_append (wb, dest_domain);
|
|
str_append_c (wb, '\0');
|
|
}
|
|
|
|
SOCKS_GO (socks_4a_finish, 8);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
SOCKS_DATA_CB (socks_5_request_port)
|
|
{
|
|
hard_assert (msg_unpacker_u16 (unpacker, &self->bound_port));
|
|
self->done = true;
|
|
return false;
|
|
}
|
|
|
|
SOCKS_DATA_CB (socks_5_request_ipv4)
|
|
{
|
|
memcpy (self->bound_address.data.ipv4, unpacker->data, unpacker->len);
|
|
SOCKS_GO (socks_5_request_port, 2);
|
|
}
|
|
|
|
SOCKS_DATA_CB (socks_5_request_ipv6)
|
|
{
|
|
memcpy (self->bound_address.data.ipv6, unpacker->data, unpacker->len);
|
|
SOCKS_GO (socks_5_request_port, 2);
|
|
}
|
|
|
|
SOCKS_DATA_CB (socks_5_request_domain_data)
|
|
{
|
|
self->bound_address.data.domain = xstrndup (unpacker->data, unpacker->len);
|
|
SOCKS_GO (socks_5_request_port, 2);
|
|
}
|
|
|
|
SOCKS_DATA_CB (socks_5_request_domain)
|
|
{
|
|
hard_assert (msg_unpacker_u8 (unpacker, &self->bound_address_len));
|
|
SOCKS_GO (socks_5_request_domain_data, self->bound_address_len);
|
|
}
|
|
|
|
SOCKS_DATA_CB (socks_5_request_finish)
|
|
{
|
|
uint8_t version, status, reserved, type;
|
|
hard_assert (msg_unpacker_u8 (unpacker, &version));
|
|
hard_assert (msg_unpacker_u8 (unpacker, &status));
|
|
hard_assert (msg_unpacker_u8 (unpacker, &reserved));
|
|
hard_assert (msg_unpacker_u8 (unpacker, &type));
|
|
|
|
if (version != 0x05)
|
|
SOCKS_FAIL ("protocol error");
|
|
|
|
switch (status)
|
|
{
|
|
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 ((self->bound_address.type = type))
|
|
{
|
|
case SOCKS_IPV4:
|
|
SOCKS_GO (socks_5_request_ipv4, sizeof self->bound_address.data.ipv4);
|
|
case SOCKS_IPV6:
|
|
SOCKS_GO (socks_5_request_ipv6, sizeof self->bound_address.data.ipv6);
|
|
case SOCKS_DOMAIN:
|
|
SOCKS_GO (socks_5_request_domain, 1);
|
|
default:
|
|
SOCKS_FAIL ("protocol error");
|
|
}
|
|
}
|
|
|
|
static bool
|
|
socks_5_request_start (struct socks_connector *self)
|
|
{
|
|
struct socks_target *target = self->targets_iter;
|
|
struct str *wb = &self->write_buffer;
|
|
str_pack_u8 (wb, 0x05); // version
|
|
str_pack_u8 (wb, 0x01); // connect
|
|
str_pack_u8 (wb, 0x00); // reserved
|
|
str_pack_u8 (wb, target->address.type);
|
|
|
|
switch (target->address.type)
|
|
{
|
|
case SOCKS_IPV4:
|
|
str_append_data (wb,
|
|
target->address.data.ipv4, sizeof target->address.data.ipv4);
|
|
break;
|
|
case SOCKS_DOMAIN:
|
|
{
|
|
size_t dlen = strlen (target->address.data.domain);
|
|
if (dlen > 255)
|
|
dlen = 255;
|
|
|
|
str_pack_u8 (wb, dlen);
|
|
str_append_data (wb, target->address.data.domain, dlen);
|
|
break;
|
|
}
|
|
case SOCKS_IPV6:
|
|
str_append_data (wb,
|
|
target->address.data.ipv6, sizeof target->address.data.ipv6);
|
|
break;
|
|
}
|
|
str_pack_u16 (wb, target->port);
|
|
|
|
SOCKS_GO (socks_5_request_finish, 4);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
SOCKS_DATA_CB (socks_5_userpass_finish)
|
|
{
|
|
uint8_t version, status;
|
|
hard_assert (msg_unpacker_u8 (unpacker, &version));
|
|
hard_assert (msg_unpacker_u8 (unpacker, &status));
|
|
|
|
if (version != 0x01)
|
|
SOCKS_FAIL ("protocol error");
|
|
if (status != 0x00)
|
|
SOCKS_FAIL ("authentication failure");
|
|
|
|
return socks_5_request_start (self);
|
|
}
|
|
|
|
static bool
|
|
socks_5_userpass_start (struct socks_connector *self)
|
|
{
|
|
size_t ulen = strlen (self->username);
|
|
if (ulen > 255)
|
|
ulen = 255;
|
|
|
|
size_t plen = strlen (self->password);
|
|
if (plen > 255)
|
|
plen = 255;
|
|
|
|
struct str *wb = &self->write_buffer;
|
|
str_pack_u8 (wb, 0x01); // version
|
|
str_pack_u8 (wb, ulen); // username length
|
|
str_append_data (wb, self->username, ulen);
|
|
str_pack_u8 (wb, plen); // password length
|
|
str_append_data (wb, self->password, plen);
|
|
|
|
SOCKS_GO (socks_5_userpass_finish, 2);
|
|
}
|
|
|
|
SOCKS_DATA_CB (socks_5_auth_finish)
|
|
{
|
|
uint8_t version, method;
|
|
hard_assert (msg_unpacker_u8 (unpacker, &version));
|
|
hard_assert (msg_unpacker_u8 (unpacker, &method));
|
|
|
|
if (version != 0x05)
|
|
SOCKS_FAIL ("protocol error");
|
|
|
|
bool can_auth = self->username && self->password;
|
|
|
|
switch (method)
|
|
{
|
|
case 0x02:
|
|
if (!can_auth)
|
|
SOCKS_FAIL ("protocol error");
|
|
|
|
return socks_5_userpass_start (self);
|
|
case 0x00:
|
|
return socks_5_request_start (self);
|
|
case 0xFF:
|
|
SOCKS_FAIL ("no acceptable authentication methods");
|
|
default:
|
|
SOCKS_FAIL ("protocol error");
|
|
}
|
|
}
|
|
|
|
static bool
|
|
socks_5_auth_start (struct socks_connector *self)
|
|
{
|
|
bool can_auth = self->username && self->password;
|
|
|
|
struct str *wb = &self->write_buffer;
|
|
str_pack_u8 (wb, 0x05); // version
|
|
str_pack_u8 (wb, 1 + can_auth); // number of authentication methods
|
|
str_pack_u8 (wb, 0x00); // no authentication required
|
|
if (can_auth)
|
|
str_pack_u8 (wb, 0x02); // username/password
|
|
|
|
SOCKS_GO (socks_5_auth_finish, 2);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void socks_connector_start (struct socks_connector *self);
|
|
|
|
static void
|
|
socks_connector_destroy_connector (struct socks_connector *self)
|
|
{
|
|
if (self->connector)
|
|
{
|
|
connector_free (self->connector);
|
|
free (self->connector);
|
|
self->connector = NULL;
|
|
}
|
|
}
|
|
|
|
static void
|
|
socks_connector_cancel_events (struct socks_connector *self)
|
|
{
|
|
// Before calling the final callbacks, we should cancel events that
|
|
// could potentially fire; caller should destroy us immediately, though
|
|
poller_fd_reset (&self->socket_event);
|
|
poller_timer_reset (&self->timeout);
|
|
}
|
|
|
|
static void
|
|
socks_connector_fail (struct socks_connector *self)
|
|
{
|
|
socks_connector_cancel_events (self);
|
|
self->on_failure (self->user_data);
|
|
}
|
|
|
|
static bool
|
|
socks_connector_step_iterators (struct socks_connector *self)
|
|
{
|
|
// At the lowest level we iterate over all addresses for the SOCKS server
|
|
// and just try to connect; this is done automatically by the connector
|
|
|
|
// Then we iterate over available protocols
|
|
if (++self->protocol_iter != SOCKS_MAX)
|
|
return true;
|
|
|
|
// At the highest level we iterate over possible targets
|
|
self->protocol_iter = 0;
|
|
if (self->targets_iter && (self->targets_iter = self->targets_iter->next))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static void
|
|
socks_connector_step (struct socks_connector *self)
|
|
{
|
|
if (self->socket_fd != -1)
|
|
{
|
|
poller_fd_reset (&self->socket_event);
|
|
xclose (self->socket_fd);
|
|
self->socket_fd = -1;
|
|
}
|
|
|
|
socks_connector_destroy_connector (self);
|
|
if (socks_connector_step_iterators (self))
|
|
socks_connector_start (self);
|
|
else
|
|
socks_connector_fail (self);
|
|
}
|
|
|
|
static void
|
|
socks_connector_on_timeout (struct socks_connector *self)
|
|
{
|
|
if (self->on_error)
|
|
self->on_error (self->user_data, "timeout");
|
|
|
|
socks_connector_destroy_connector (self);
|
|
socks_connector_fail (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
socks_connector_on_connected (void *user_data, int socket_fd)
|
|
{
|
|
set_blocking (socket_fd, false);
|
|
|
|
struct socks_connector *self = user_data;
|
|
self->socket_fd = socket_fd;
|
|
self->socket_event.fd = socket_fd;
|
|
poller_fd_set (&self->socket_event, POLLIN | POLLOUT);
|
|
str_reset (&self->read_buffer);
|
|
str_reset (&self->write_buffer);
|
|
|
|
if (!(self->protocol_iter == SOCKS_5 && socks_5_auth_start (self))
|
|
&& !(self->protocol_iter == SOCKS_4A && socks_4a_start (self)))
|
|
socks_connector_fail (self);
|
|
}
|
|
|
|
static void
|
|
socks_connector_on_failure (void *user_data)
|
|
{
|
|
struct socks_connector *self = user_data;
|
|
// TODO: skip SOCKS server on connection failure
|
|
socks_connector_step (self);
|
|
}
|
|
|
|
static void
|
|
socks_connector_on_connecting (void *user_data, const char *via)
|
|
{
|
|
struct socks_connector *self = user_data;
|
|
if (!self->on_connecting)
|
|
return;
|
|
|
|
struct socks_target *target = self->targets_iter;
|
|
char *port = xstrdup_printf ("%u", target->port);
|
|
char *address = format_host_port_pair (target->address_str, port);
|
|
free (port);
|
|
self->on_connecting (self->user_data, address, via,
|
|
socks_protocol_to_string (self->protocol_iter));
|
|
free (address);
|
|
}
|
|
|
|
static void
|
|
socks_connector_on_error (void *user_data, const char *error)
|
|
{
|
|
struct socks_connector *self = user_data;
|
|
// TODO: skip protocol on protocol failure
|
|
if (self->on_error)
|
|
self->on_error (self->user_data, error);
|
|
}
|
|
|
|
static void
|
|
socks_connector_start (struct socks_connector *self)
|
|
{
|
|
hard_assert (!self->connector);
|
|
|
|
struct connector *connector =
|
|
self->connector = xcalloc (1, sizeof *connector);
|
|
connector_init (connector, self->socket_event.poller);
|
|
|
|
connector->user_data = self;
|
|
connector->on_connected = socks_connector_on_connected;
|
|
connector->on_connecting = socks_connector_on_connecting;
|
|
connector->on_error = socks_connector_on_error;
|
|
connector->on_failure = socks_connector_on_failure;
|
|
|
|
struct error *e = NULL;
|
|
if (!connector_add_target (connector, self->hostname, self->service, &e))
|
|
{
|
|
if (self->on_error)
|
|
self->on_error (self->user_data, e->message);
|
|
error_free (e);
|
|
|
|
socks_connector_destroy_connector (self);
|
|
socks_connector_fail (self);
|
|
return;
|
|
}
|
|
|
|
poller_timer_set (&self->timeout, 60 * 1000);
|
|
connector_step (connector);
|
|
self->done = false;
|
|
|
|
self->bound_port = 0;
|
|
socks_addr_free (&self->bound_address);
|
|
memset (&self->bound_address, 0, sizeof self->bound_address);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static bool
|
|
socks_try_fill_read_buffer (struct socks_connector *self, size_t n)
|
|
{
|
|
ssize_t remains = (ssize_t) n - (ssize_t) self->read_buffer.len;
|
|
if (remains <= 0)
|
|
return true;
|
|
|
|
ssize_t received;
|
|
str_ensure_space (&self->read_buffer, remains);
|
|
do
|
|
received = recv (self->socket_fd,
|
|
self->read_buffer.str + self->read_buffer.len, remains, 0);
|
|
while ((received == -1) && errno == EINTR);
|
|
|
|
if (received == 0)
|
|
SOCKS_FAIL ("%s: %s", "protocol error", "unexpected EOF");
|
|
if (received == -1 && errno != EAGAIN)
|
|
SOCKS_FAIL ("%s: %s", "recv", strerror (errno));
|
|
if (received > 0)
|
|
self->read_buffer.len += received;
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
socks_call_on_data (struct socks_connector *self)
|
|
{
|
|
size_t to_consume = self->data_needed;
|
|
if (!socks_try_fill_read_buffer (self, to_consume))
|
|
return false;
|
|
if (self->read_buffer.len < to_consume)
|
|
return true;
|
|
|
|
struct msg_unpacker unpacker;
|
|
msg_unpacker_init (&unpacker, self->read_buffer.str, self->read_buffer.len);
|
|
bool result = self->on_data (self, &unpacker);
|
|
str_remove_slice (&self->read_buffer, 0, to_consume);
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
socks_try_flush_write_buffer (struct socks_connector *self)
|
|
{
|
|
struct str *wb = &self->write_buffer;
|
|
ssize_t n_written;
|
|
|
|
while (wb->len)
|
|
{
|
|
n_written = send (self->socket_fd, wb->str, wb->len, 0);
|
|
if (n_written >= 0)
|
|
{
|
|
str_remove_slice (wb, 0, n_written);
|
|
continue;
|
|
}
|
|
|
|
if (errno == EAGAIN)
|
|
break;
|
|
if (errno == EINTR)
|
|
continue;
|
|
|
|
SOCKS_FAIL ("%s: %s", "send", strerror (errno));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
socks_connector_on_ready
|
|
(const struct pollfd *pfd, struct socks_connector *self)
|
|
{
|
|
(void) pfd;
|
|
|
|
if (socks_call_on_data (self) && socks_try_flush_write_buffer (self))
|
|
{
|
|
poller_fd_set (&self->socket_event,
|
|
self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN);
|
|
}
|
|
else if (self->done)
|
|
{
|
|
socks_connector_cancel_events (self);
|
|
|
|
int fd = self->socket_fd;
|
|
self->socket_fd = -1;
|
|
set_blocking (fd, true);
|
|
self->on_connected (self->user_data, fd);
|
|
}
|
|
else
|
|
// We've failed this target, let's try to move on
|
|
socks_connector_step (self);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
socks_connector_init (struct socks_connector *self, struct poller *poller)
|
|
{
|
|
memset (self, 0, sizeof *self);
|
|
|
|
poller_fd_init (&self->socket_event, poller, (self->socket_fd = -1));
|
|
self->socket_event.dispatcher = (poller_fd_fn) socks_connector_on_ready;
|
|
self->socket_event.user_data = self;
|
|
|
|
poller_timer_init (&self->timeout, poller);
|
|
self->timeout.dispatcher = (poller_timer_fn) socks_connector_on_timeout;
|
|
self->timeout.user_data = self;
|
|
|
|
str_init (&self->read_buffer);
|
|
str_init (&self->write_buffer);
|
|
}
|
|
|
|
static void
|
|
socks_connector_free (struct socks_connector *self)
|
|
{
|
|
socks_connector_destroy_connector (self);
|
|
socks_connector_cancel_events (self);
|
|
|
|
if (self->socket_fd != -1)
|
|
xclose (self->socket_fd);
|
|
|
|
str_free (&self->read_buffer);
|
|
str_free (&self->write_buffer);
|
|
|
|
free (self->hostname);
|
|
free (self->service);
|
|
free (self->username);
|
|
free (self->password);
|
|
|
|
LIST_FOR_EACH (struct socks_target, iter, self->targets)
|
|
{
|
|
socks_addr_free (&iter->address);
|
|
free (iter->address_str);
|
|
free (iter);
|
|
}
|
|
|
|
socks_addr_free (&self->bound_address);
|
|
}
|
|
|
|
static bool
|
|
socks_connector_add_target (struct socks_connector *self,
|
|
const char *host, const char *service, struct error **e)
|
|
{
|
|
unsigned long port;
|
|
const struct servent *serv;
|
|
if ((serv = getservbyname (service, "tcp")))
|
|
port = (uint16_t) ntohs (serv->s_port);
|
|
else if (!xstrtoul (&port, service, 10) || !port || port > UINT16_MAX)
|
|
{
|
|
error_set (e, "invalid port number");
|
|
return false;
|
|
}
|
|
|
|
struct socks_target *target = xcalloc (1, sizeof *target);
|
|
if (inet_pton (AF_INET, host, &target->address.data.ipv4) == 1)
|
|
target->address.type = SOCKS_IPV4;
|
|
else if (inet_pton (AF_INET6, host, &target->address.data.ipv6) == 1)
|
|
target->address.type = SOCKS_IPV6;
|
|
else
|
|
{
|
|
target->address.type = SOCKS_DOMAIN;
|
|
target->address.data.domain = xstrdup (host);
|
|
}
|
|
|
|
target->port = port;
|
|
target->address_str = xstrdup (host);
|
|
LIST_APPEND_WITH_TAIL (self->targets, self->targets_tail, target);
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
socks_connector_run (struct socks_connector *self,
|
|
const char *host, const char *service,
|
|
const char *username, const char *password)
|
|
{
|
|
hard_assert (self->targets);
|
|
hard_assert (host && service);
|
|
|
|
self->hostname = xstrdup (host);
|
|
self->service = xstrdup (service);
|
|
|
|
if (username) self->username = xstrdup (username);
|
|
if (password) self->password = xstrdup (password);
|
|
|
|
self->targets_iter = self->targets;
|
|
self->protocol_iter = 0;
|
|
// XXX: this can fail immediately from an error creating the connector
|
|
socks_connector_start (self);
|
|
}
|
|
|
|
// --- 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?
|
|
bool is_partial; ///< Unterminated 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;
|
|
|
|
// According to the original CTCP specification we should use
|
|
// ctcp_intra_decode() on all parts, however no one seems to
|
|
// use that and it breaks normal text with backslashes
|
|
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 part. Unended tagged chunks are marked as such.
|
|
if (start != m.len)
|
|
{
|
|
struct ctcp_chunk *chunk = ctcp_chunk_new ();
|
|
if (in_ctcp)
|
|
{
|
|
ctcp_parse_tagged (m.str + start, m.len - start, chunk);
|
|
chunk->is_partial = true;
|
|
}
|
|
else
|
|
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);
|
|
}
|
|
}
|