/*
* This file is part of Cockpit.
*
* Copyright (C) 2015 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see .
*/
#include "config.h"
#include "cockpitchannel.h"
#include "cockpitpeer.h"
#include "cockpitrouter.h"
#include "common/cockpitjson.h"
#include "common/cockpittransport.h"
#include "common/cockpitpipe.h"
#include "common/cockpitpipetransport.h"
#include "common/cockpittemplate.h"
#include
struct _CockpitRouter {
GObjectClass parent;
gchar *init_host;
gulong signal_id;
/* The transport we're talking to */
CockpitTransport *transport;
/* Rules for how to open channels */
GList *rules;
/* All local channels are tracked here, value may be null */
GHashTable *channels;
/* Channel groups */
GHashTable *groups;
GHashTable *fences;
GQueue *fenced;
};
typedef struct _CockpitRouterClass {
GObjectClass parent_class;
} CockpitRouterClass;
G_DEFINE_TYPE (CockpitRouter, cockpit_router, G_TYPE_OBJECT);
enum {
PROP_0,
PROP_TRANSPORT,
};
typedef struct {
gchar **argv;
gchar **environ;
} DynamicKey;
typedef struct {
JsonObject *config;
// Contents owned by config
gchar **env;
gchar **spawn;
GHashTable *peers;
} DynamicPeer;
static guint
strv_hash (gconstpointer v)
{
const gchar * const *strv = v;
guint hash = 0;
gint i;
for (i = 0; strv && strv[i] != NULL; i++)
hash |= g_str_hash (strv[i]);
return hash;
}
static gboolean
strv_equal (gconstpointer v1,
gconstpointer v2)
{
const gchar * const *strv1 = v1;
const gchar * const *strv2 = v2;
gint i;
if (strv1 == strv2)
return TRUE;
if (!strv1 || !strv2)
return FALSE;
for (i = 0; strv1[i] != NULL || strv2[i] != NULL; i++)
{
if (strv1[i] == NULL || strv2[i] == NULL || !g_str_equal (strv1[i], strv2[i]))
return FALSE;
}
return TRUE;
}
static guint
dynamic_key_hash (gconstpointer v)
{
const DynamicKey *key = v;
return strv_hash (key->argv) | strv_hash (key->environ);
}
static gboolean
dynamic_key_equal (gconstpointer v1,
gconstpointer v2)
{
const DynamicKey *key1 = v1;
const DynamicKey *key2 = v2;
return strv_equal (key1->argv, key2->argv) && strv_equal (key1->environ, key2->environ);
}
static void
dynamic_key_free (gpointer v)
{
DynamicKey *key = v;
g_strfreev (key->argv);
g_strfreev (key->environ);
g_free (key);
}
static DynamicPeer *
dynamic_peer_create (JsonObject *config)
{
DynamicPeer *p = g_new0 (DynamicPeer, 1);
p->peers = g_hash_table_new_full (dynamic_key_hash, dynamic_key_equal,
dynamic_key_free, g_object_unref);
p->config = json_object_ref (config);
if (!cockpit_json_get_strv (config, "environ", NULL, &p->env))
p->env = NULL;
if (!cockpit_json_get_strv (config, "spawn", NULL, &p->spawn))
p->spawn = NULL;
return p;
}
static void
dynamic_peer_free (gpointer data)
{
DynamicPeer *p = data;
json_object_unref (p->config);
g_hash_table_unref (p->peers);
g_free (p->spawn);
g_free (p->env);
g_free (p);
}
typedef struct {
gchar *name;
GPatternSpec *glob;
JsonNode *node;
} RouterMatch;
typedef struct {
JsonObject *config;
RouterMatch *matches;
gboolean (* callback) (CockpitRouter *, const gchar *, JsonObject *, GBytes *, gpointer);
gpointer user_data;
GDestroyNotify destroy;
} RouterRule;
static void
router_rule_compile (RouterRule *rule,
JsonObject *object)
{
RouterMatch *match;
GList *names, *l;
JsonNode *node;
gint i;
g_assert (rule->matches == NULL);
names = json_object_get_members (object);
rule->matches = g_new0 (RouterMatch, g_list_length (names) + 1);
for (l = names, i = 0; l != NULL; l = g_list_next (l), i++)
{
match = &rule->matches[i];
match->name = g_strdup (l->data);
node = json_object_get_member (object, l->data);
/* A glob style string pattern */
if (JSON_NODE_HOLDS_VALUE (node) && json_node_get_value_type (node) == G_TYPE_STRING)
match->glob = g_pattern_spec_new (json_node_get_string (node));
/* A null matches anything */
if (!JSON_NODE_HOLDS_NULL (node))
match->node = json_node_copy (node);
}
/* The last match has a null name */
g_list_free (names);
}
static gboolean
router_rule_match (RouterRule *rule,
JsonObject *object)
{
RouterMatch *match;
const gchar *value;
JsonNode *node;
guint i;
for (i = 0; rule->matches && rule->matches[i].name != NULL; i++)
{
match = &rule->matches[i];
if (match->glob)
{
if (!cockpit_json_get_string (object, match->name, NULL, &value) || !value ||
!g_pattern_match (match->glob, strlen (value), value, NULL))
return FALSE;
}
else if (match->node)
{
node = json_object_get_member (object, match->name);
if (!node || !cockpit_json_equal (match->node, node))
return FALSE;
}
else
{
if (!json_object_has_member (object, match->name))
return FALSE;
}
}
return TRUE;
}
static gboolean
router_rule_invoke (RouterRule *rule,
CockpitRouter *self,
const gchar *channel,
JsonObject *options,
GBytes *data)
{
g_assert (rule->callback != NULL);
return (rule->callback) (self, channel, options, data, rule->user_data);
}
static RouterRule *
router_rule_find (GList *rules,
JsonObject *config)
{
for (GList *l = rules; l; l = g_list_next (l))
{
RouterRule *rule = l->data;
if (rule->config && cockpit_json_equal_object (rule->config, config))
return rule;
}
return NULL;
}
static void
router_rule_destroy (RouterRule *rule)
{
gint i;
if (rule->destroy)
(rule->destroy) (rule->user_data);
for (i = 0; rule->matches && rule->matches[i].name != NULL; i++)
{
g_free (rule->matches[i].name);
json_node_free (rule->matches[i].node);
if (rule->matches[i].glob)
g_pattern_spec_free (rule->matches[i].glob);
}
g_free (rule->matches);
if (rule->config)
json_object_unref (rule->config);
g_free (rule);
}
static void
router_rule_dump (RouterRule *rule)
{
RouterMatch *match;
gchar *text;
guint i;
g_print ("rule:\n");
for (i = 0; rule->matches && rule->matches[i].name != NULL; i++)
{
match = &rule->matches[i];
if (match->node)
{
text = cockpit_json_write (match->node, NULL);
g_print (" %s: %s\n", match->name, text);
g_free (text);
}
else if (match->glob)
{
g_print (" %s: glob\n", match->name);
}
else
{
g_print (" %s\n", match->name);
}
}
}
static void
process_init (CockpitRouter *self,
CockpitTransport *transport,
JsonObject *options)
{
const gchar *problem = NULL;
const gchar *host;
gint64 version = -1;
if (self->init_host)
{
g_warning ("caller already sent another 'init' message");
problem = "protocol-error";
}
else if (!cockpit_json_get_int (options, "version", -1, &version))
{
g_warning ("invalid 'version' field in init message");
problem = "protocol-error";
}
else if (version == -1)
{
g_warning ("missing 'version' field in init message");
problem = "protocol-error";
}
else if (!cockpit_json_get_string (options, "host", NULL, &host))
{
g_warning ("invalid 'host' field in init message");
problem = "protocol-error";
}
else if (host == NULL)
{
g_message ("missing 'host' field in init message");
problem = "protocol-error";
}
else if (version != 1)
{
g_message ("unsupported 'version' of cockpit protocol: %" G_GINT64_FORMAT, version);
problem = "not-supported";
}
if (problem)
{
cockpit_transport_close (transport, problem);
}
else
{
g_debug ("received init message");
g_assert (host != NULL);
self->init_host = g_strdup (host);
problem = NULL;
}
}
static void
on_channel_closed (CockpitChannel *local,
const gchar *problem,
gpointer user_data)
{
CockpitRouter *self = COCKPIT_ROUTER (user_data);
const gchar *channel;
GQueue *fenced;
GList *l;
channel = cockpit_channel_get_id (local);
g_hash_table_remove (self->channels, channel);
g_hash_table_remove (self->groups, channel);
/*
* If this was the last channel in the fence group then resume all other channels
* as there's no barrier preventing them from functioning.
*/
if (!g_hash_table_remove (self->fences, channel) || g_hash_table_size (self->fences) != 0)
return;
fenced = self->fenced;
self->fenced = NULL;
if (!fenced)
return;
for (l = fenced->head; l != NULL; l = g_list_next (l))
cockpit_transport_thaw (self->transport, l->data);
g_queue_free_full (fenced, g_free);
}
static void
create_channel (CockpitRouter *self,
const gchar *channel,
JsonObject *options,
GType type)
{
CockpitChannel *local;
local = g_object_new (type,
"transport", self->transport,
"id", channel,
"options", options,
NULL);
/* This owns the local channel */
g_hash_table_replace (self->channels, (gpointer)cockpit_channel_get_id (local), local);
g_signal_connect (local, "closed", G_CALLBACK (on_channel_closed), self);
}
static gboolean
is_empty (const gchar *s)
{
return !s || s[0] == '\0';
}
/*
* For backwards compatibility we need to normalize some host params
* so they can be matched against.
*
* Some sessions shouldn't be shared by multiple channels, such as those that
* explicitly specify a host-key or specific user. This changed over time
* so modify things to make it a simple match.
*
* If the given user is the current user, remove it. Preserves the current
* behavior.
*
*/
static void
cockpit_router_normalize_host_params (JsonObject *options)
{
const gchar *sharable = NULL;
const gchar *user = NULL;
gboolean needs_private = FALSE;
if (!cockpit_json_get_string (options, "session", NULL, &sharable))
sharable = NULL;
if (!cockpit_json_get_string (options, "user", NULL, &user))
user = NULL;
if (!sharable)
{
/* Fallback to older ways of indicating this */
if (user || json_object_has_member (options, "host-key"))
needs_private = TRUE;
if (json_object_has_member (options, "temp-session"))
{
if (needs_private && !cockpit_json_get_bool (options, "temp-session",
TRUE, &needs_private))
needs_private = TRUE;
json_object_remove_member (options, "temp-session");
}
}
if (g_strcmp0 (user, g_get_user_name ()) == 0)
json_object_remove_member (options, "user");
if (needs_private)
json_object_set_string_member (options, "session", "private");
}
static gboolean
cockpit_router_normalize_host (CockpitRouter *self,
JsonObject *options)
{
const gchar *host;
gchar *actual_host = NULL;
gchar *key = NULL;
gchar **parts = NULL;
if (!cockpit_json_get_string (options, "host", self->init_host, &host))
return FALSE;
parts = g_strsplit (host, "+", 3);
if (g_strv_length (parts) == 3 && !is_empty (parts[0]) &&
!is_empty (parts[1]) && !is_empty (parts[2]))
{
key = g_strdup_printf ("host-%s", parts[1]);
if (!json_object_has_member (options, key))
{
json_object_set_string_member (options, key, parts[2]);
actual_host = parts[0];
}
}
if (!actual_host)
actual_host = (gchar *) host;
if (g_strcmp0 (self->init_host, actual_host) == 0)
json_object_remove_member (options, "host");
else if (g_strcmp0 (host, actual_host) != 0)
json_object_set_string_member (options, "host", actual_host);
g_strfreev (parts);
g_free (key);
return TRUE;
}
static gboolean
process_open_channel (CockpitRouter *self,
const gchar *channel,
JsonObject *options,
GBytes *data,
gpointer user_data)
{
GType (* type_function) (void) = user_data;
GType channel_type = 0;
const gchar *group;
if (!cockpit_json_get_string (options, "group", "default", &group))
g_warning ("%s: caller specified invalid 'group' field in open message", channel);
g_assert (type_function != NULL);
channel_type = type_function ();
if (g_str_equal (group, "fence"))
g_hash_table_add (self->fences, g_strdup (channel));
g_hash_table_insert (self->groups, g_strdup (channel), g_strdup (group));
create_channel (self, channel, options, channel_type);
return TRUE;
}
static gboolean
process_open_peer (CockpitRouter *self,
const gchar *channel,
JsonObject *options,
GBytes *data,
gpointer user_data)
{
CockpitPeer *peer = user_data;
return cockpit_peer_handle (peer, channel, options, data);
}
static GBytes *
substitute_json_string (const gchar *variable,
gpointer user_data)
{
const gchar *value;
JsonObject *options = user_data;
if (options && cockpit_json_get_string (options, variable, "", &value))
return g_bytes_new (value, strlen (value));
else if (options)
g_message ("Couldn't get argument for bridge: got invalid value for '%s'", variable);
return g_bytes_new ("", 0);
}
static JsonArray *
strv_to_json_array (gchar **strv)
{
gint i;
JsonArray *array = json_array_new ();
g_assert (strv != NULL);
for (i = 0; strv[i] != NULL; i++)
json_array_add_string_element (array, strv[i]);
return array;
}
static void
add_dynamic_args_to_array (gchar ***key,
gchar **config_args,
JsonObject *options)
{
GPtrArray *arr = NULL;
gint length;
gint i;
g_assert (config_args != NULL);
g_assert (key != NULL);
arr = g_ptr_array_new ();
length = g_strv_length (config_args);
for (i = 0; i < length; i++)
{
GString *s = g_string_new ("");
GBytes *input = g_bytes_new_static (config_args[i], strlen(config_args[i]) + 1);
GList *output = cockpit_template_expand (input, substitute_json_string,
"${", "}", options);
GList *l;
for (l = output; l != NULL; l = g_list_next (l))
{
gsize size;
gconstpointer data = g_bytes_get_data (l->data, &size);
g_string_append_len (s, data, size);
}
g_ptr_array_add (arr, g_string_free (s, FALSE));
g_bytes_unref (input);
g_list_free_full (output, (GDestroyNotify) g_bytes_unref);
}
g_ptr_array_add (arr, NULL);
*key = (gchar **)g_ptr_array_free (arr, FALSE);
}
static gboolean
process_open_dynamic_peer (CockpitRouter *self,
const gchar *channel,
JsonObject *options,
GBytes *data,
gpointer user_data)
{
CockpitPeer *peer = NULL;
DynamicKey key = { NULL, NULL };
DynamicPeer *dp = user_data;
JsonObject *config = NULL;
GList *l, *names = NULL;
if (dp->spawn)
add_dynamic_args_to_array (&key.argv, dp->spawn, options);
if (dp->env)
add_dynamic_args_to_array (&key.environ, dp->env, options);
peer = g_hash_table_lookup (dp->peers, &key);
if (!peer)
{
config = json_object_new ();
names = json_object_get_members (dp->config);
for (l = names; l != NULL; l = g_list_next (l))
{
if (!g_str_equal (l->data, "spawn") && !g_str_equal (l->data, "environ"))
json_object_set_member (config, l->data, json_object_dup_member (dp->config, l->data));
}
if (key.argv)
json_object_set_array_member (config, "spawn", strv_to_json_array (key.argv));
if (key.environ)
json_object_set_array_member (config, "environ", strv_to_json_array (key.environ));
peer = cockpit_peer_new (self->transport, config);
g_hash_table_insert (dp->peers, g_memdup (&key, sizeof (DynamicKey)), peer);
}
else
{
g_strfreev (key.argv);
g_strfreev (key.environ);
}
if (config)
json_object_unref (config);
g_list_free (names);
return cockpit_peer_handle (peer, channel, options, data);
}
static gboolean
process_open_not_supported (CockpitRouter *self,
const gchar *channel,
JsonObject *options,
GBytes *data,
gpointer user_data)
{
const gchar *payload;
if (!cockpit_json_get_string (options, "payload", NULL, &payload))
g_warning ("%s: caller specified invalid 'payload' field in open message", channel);
else if (payload == NULL)
g_warning ("%s: caller didn't provide a 'payload' field in open message", channel);
else
g_debug ("%s: bridge doesn't support channel: %s", channel, payload);
/* This creates a temporary channel that closes with not-supported */
create_channel (self, channel, options, COCKPIT_TYPE_CHANNEL);
return TRUE;
}
static void
process_open (CockpitRouter *self,
CockpitTransport *transport,
const gchar *channel,
JsonObject *options,
GBytes *data)
{
GList *l;
GBytes *new_payload = NULL;
if (!channel)
{
g_warning ("Caller tried to open channel with invalid id");
cockpit_transport_close (transport, "protocol-error");
}
/* Check that this isn't a local channel */
else if (g_hash_table_lookup (self->channels, channel))
{
g_warning ("%s: caller tried to reuse a channel that's already in use", channel);
cockpit_transport_close (self->transport, "protocol-error");
return;
}
/* Request that this channel is frozen, and requeue its open message for later */
else if (g_hash_table_size (self->fences) > 0 && !g_hash_table_lookup (self->fences, channel))
{
if (!self->fenced)
self->fenced = g_queue_new ();
g_queue_push_tail (self->fenced, g_strdup (channel));
cockpit_transport_freeze (self->transport, channel);
cockpit_transport_emit_control (self->transport, "open", channel, options, data);
}
else if (!cockpit_router_normalize_host (self, options))
{
g_warning ("%s: caller specified invalid 'host' field in open message", channel);
process_open_not_supported (self, channel, options, data, NULL);
}
/* Now go throgh the rules */
else
{
cockpit_router_normalize_host_params (options);
new_payload = cockpit_json_write_bytes (options);
for (l = self->rules; l != NULL; l = g_list_next (l))
{
if (router_rule_match (l->data, options) &&
router_rule_invoke (l->data, self, channel, options, new_payload))
{
break;
}
}
}
if (new_payload)
g_bytes_unref (new_payload);
}
static void
process_kill (CockpitRouter *self,
JsonObject *options)
{
GHashTableIter iter;
const gchar *group = NULL;
const gchar *host = NULL;
GList *list, *l;
if (!cockpit_json_get_string (options, "group", NULL, &group))
{
g_warning ("received invalid \"group\" field in kill command");
return;
}
else if (!cockpit_json_get_string (options, "host", NULL, &host))
{
g_warning ("received invalid \"host\" field in kill command");
return;
}
/* Killing on other hosts is handled elsewhere */
if (host && g_strcmp0 (host, self->init_host) != 0)
return;
list = NULL;
if (group)
{
gpointer id, channel_group;
g_hash_table_iter_init (&iter, self->groups);
while (g_hash_table_iter_next (&iter, &id, &channel_group))
{
CockpitChannel *channel;
if (!g_str_equal (group, channel_group))
continue;
channel = g_hash_table_lookup (self->channels, id);
if (channel)
list = g_list_prepend (list, g_object_ref (channel));
}
}
else
{
gpointer id, channel;
g_hash_table_iter_init (&iter, self->channels);
while (g_hash_table_iter_next (&iter, &id, &channel))
list = g_list_prepend (list, g_object_ref (channel));
}
for (l = list; l != NULL; l = g_list_next (l))
{
CockpitChannel *channel = l->data;
g_debug ("killing channel: %s", cockpit_channel_get_id (channel));
cockpit_channel_close (channel, "terminated");
g_object_unref (channel);
}
g_list_free (list);
}
static gboolean
on_transport_control (CockpitTransport *transport,
const char *command,
const gchar *channel_id,
JsonObject *options,
GBytes *message,
gpointer user_data)
{
CockpitRouter *self = user_data;
if (g_str_equal (command, "init"))
{
process_init (self, transport, options);
return TRUE;
}
if (!self->init_host)
{
g_warning ("caller did not send 'init' message first");
cockpit_transport_close (transport, "protocol-error");
return TRUE;
}
if (g_str_equal (command, "open"))
{
process_open (self, transport, channel_id, options, message);
return TRUE;
}
else if (g_str_equal (command, "kill"))
{
process_kill (self, options);
return TRUE;
}
else if (g_str_equal (command, "close"))
{
if (!channel_id)
{
g_warning ("Caller tried to close channel without an id");
cockpit_transport_close (transport, "protocol-error");
}
}
return FALSE;
}
static void
object_unref_if_not_null (gpointer data)
{
if (data)
g_object_unref (data);
}
static void
cockpit_router_init (CockpitRouter *self)
{
RouterRule *rule;
/* Owns the channels */
self->channels = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, object_unref_if_not_null);
self->groups = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
self->fences = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
/* The rules, including a default */
rule = g_new0 (RouterRule, 1);
rule->callback = process_open_not_supported;
self->rules = g_list_prepend (self->rules, rule);
}
static void
cockpit_router_ban_hosts (CockpitRouter *self)
{
RouterRule *rule;
JsonObject *match = json_object_new ();
json_object_set_null_member (match, "host");
rule = g_new0 (RouterRule, 1);
rule->callback = process_open_not_supported;
router_rule_compile (rule, match);
self->rules = g_list_prepend (self->rules, rule);
json_object_unref (match);
}
static void
cockpit_router_dispose (GObject *object)
{
CockpitRouter *self = COCKPIT_ROUTER (object);
if (self->signal_id)
{
g_signal_handler_disconnect (self->transport, self->signal_id);
self->signal_id = 0;
}
g_hash_table_remove_all (self->channels);
g_hash_table_remove_all (self->groups);
g_hash_table_remove_all (self->fences);
g_list_free_full (self->rules, (GDestroyNotify)router_rule_destroy);
self->rules = NULL;
}
static void
cockpit_router_finalize (GObject *object)
{
CockpitRouter *self = COCKPIT_ROUTER (object);
if (self->transport)
g_object_unref (self->transport);
if (self->fenced)
g_queue_free_full (self->fenced, g_free);
g_free (self->init_host);
g_hash_table_destroy (self->channels);
g_hash_table_destroy (self->groups);
g_hash_table_destroy (self->fences);
G_OBJECT_CLASS (cockpit_router_parent_class)->finalize (object);
}
static void
cockpit_router_set_property (GObject *obj,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
CockpitRouter *self = COCKPIT_ROUTER (obj);
switch (prop_id)
{
case PROP_TRANSPORT:
self->transport = g_value_dup_object (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
break;
}
}
static void
cockpit_router_constructed (GObject *object)
{
CockpitRouter *self = COCKPIT_ROUTER (object);
G_OBJECT_CLASS (cockpit_router_parent_class)->constructed (object);
g_return_if_fail (self->transport != NULL);
self->signal_id = g_signal_connect (self->transport, "control",
G_CALLBACK (on_transport_control),
self);
}
static void
cockpit_router_class_init (CockpitRouterClass *class)
{
GObjectClass *object_class;
object_class = G_OBJECT_CLASS (class);
object_class->set_property = cockpit_router_set_property;
object_class->finalize = cockpit_router_finalize;
object_class->dispose = cockpit_router_dispose;
object_class->constructed = cockpit_router_constructed;
g_object_class_install_property (object_class, PROP_TRANSPORT,
g_param_spec_object ("transport", "transport", "transport",
COCKPIT_TYPE_TRANSPORT,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS));
}
/**
* cockpit_router_new:
* @transport: Transport to talk to cockpit-ws with
* @payloads: List of payloads to handle, or NULL
* @bridges: List of peer bridge config, or NULL
*
* Create a new CockpitRouter. The @payloads to handle
* will be added via cockpit_router_add_channel().
*
* The @bridges if specified will be added via
* cockpit_router_add_bridge(). These will be added in
* reverse order so that the first bridge in the list
* would be the first one that matches in the router.
*
* Returns: (transfer full): A new CockpitRouter object.
*/
CockpitRouter *
cockpit_router_new (CockpitTransport *transport,
CockpitPayloadType *payloads,
GList *bridges)
{
CockpitRouter *router;
guint i;
JsonObject *match;
g_return_val_if_fail (transport != NULL, NULL);
router = g_object_new (COCKPIT_TYPE_ROUTER,
"transport", transport,
NULL);
for (i = 0; payloads && payloads[i].name != NULL; i++)
{
match = json_object_new ();
json_object_set_string_member (match, "payload", payloads[i].name);
cockpit_router_add_channel (router, match, payloads[i].function);
json_object_unref (match);
}
/* No hosts are allowed by default */
cockpit_router_ban_hosts (router);
cockpit_router_set_bridges (router, bridges);
// cockpit_router_dump_rules (router);
return router;
}
/**
* cockpit_router_add_channel:
* @self: The router object
* @match: JSON configuration on what to match
* @function: The function to get type from
*
* Add a channel handler to the router. The @match is a
* JSON match object as described in doc/guide/ which matches
* against "open" messages in order to determine whether to
* use this channel.
*
* The @function returns a GType to use for the channel.
*/
void
cockpit_router_add_channel (CockpitRouter *self,
JsonObject *match,
GType (* function) (void))
{
RouterRule *rule;
g_return_if_fail (COCKPIT_IS_ROUTER (self));
g_return_if_fail (match != NULL);
g_return_if_fail (function != NULL);
rule = g_new0 (RouterRule, 1);
rule->callback = process_open_channel;
rule->user_data = function;
router_rule_compile (rule, match);
self->rules = g_list_prepend (self->rules, rule);
}
/**
* cockpit_router_add_peer:
* @self: The router object
* @match: JSON configuration on what to match
* @peer: The CockpitPeer instance to route matches to
*
* Add a peer bridge to the router for handling channels.
* The @match JSON object as described in doc/guide/ and
* matches against "open" messages in order to determine whether
* to send channels to this peer bridge.
*/
void
cockpit_router_add_peer (CockpitRouter *self,
JsonObject *match,
CockpitPeer *peer)
{
RouterRule *rule;
g_return_if_fail (COCKPIT_IS_ROUTER (self));
g_return_if_fail (COCKPIT_IS_PEER (peer));
g_return_if_fail (match != NULL);
rule = g_new0 (RouterRule, 1);
rule->callback = process_open_peer;
rule->user_data = g_object_ref (peer);
rule->destroy = g_object_unref;
router_rule_compile (rule, match);
self->rules = g_list_prepend (self->rules, rule);
}
void
cockpit_router_add_bridge (CockpitRouter *self,
JsonObject *config)
{
RouterRule *rule;
JsonObject *match;
GList *output = NULL;
GBytes *bytes = NULL;
g_return_if_fail (COCKPIT_IS_ROUTER (self));
g_return_if_fail (config != NULL);
/* Actual descriptive warning displayed elsewhere */
if (!cockpit_json_get_object (config, "match", NULL, &match))
match = NULL;
/* See if we have any variables in the JSON */
bytes = cockpit_json_write_bytes (config);
output = cockpit_template_expand (bytes, substitute_json_string,
"${", "}", NULL);
rule = g_new0 (RouterRule, 1);
rule->config = json_object_ref (config);
if (!output->next)
{
rule->callback = process_open_peer;
rule->user_data = cockpit_peer_new (self->transport, config);
rule->destroy = g_object_unref;
}
else
{
rule->callback = process_open_dynamic_peer;
rule->user_data = dynamic_peer_create (config);
rule->destroy = dynamic_peer_free;
}
router_rule_compile (rule, match);
self->rules = g_list_prepend (self->rules, rule);
g_bytes_unref (bytes);
g_list_free_full (output, (GDestroyNotify) g_bytes_unref);
}
/**
* cockpit_router_set_bridges:
* @self: The router object
* @configs: JSON configurations for the bridges
*
* Updates the rules for bridges to match @configs. All rules that
* have been previously added via @cockpit_router_add_bridge and
* @cockpit_router_set_bridges conceptually removed, and all rules
* specified by @configs are added as with @cockpit_add_bridge.
*
* External peers for rules that have not changed will be left
* running. Peers for rules that have disappeared will be terminated.
*/
void cockpit_router_set_bridges (CockpitRouter *self,
GList *bridges)
{
GList *l;
JsonObject *config;
RouterRule *rule;
GList *old_rules;
/* Enumerated in reverse, since the last rule is matched first */
old_rules = self->rules;
self->rules = NULL;
for (l = g_list_last (bridges); l != NULL; l = g_list_previous (l))
{
config = l->data;
rule = router_rule_find (old_rules, config);
if (rule)
{
old_rules = g_list_remove (old_rules, rule);
self->rules = g_list_prepend (self->rules, rule);
}
else
{
cockpit_router_add_bridge (self, config);
}
}
for (l = old_rules; l; l = g_list_next (l))
{
rule = l->data;
if (rule->config)
{
router_rule_destroy (rule);
}
else
{
self->rules = g_list_append (self->rules, rule);
}
}
g_list_free (old_rules);
}
void
cockpit_router_dump_rules (CockpitRouter *self)
{
GList *l;
for (l = self->rules; l != NULL; l = g_list_next (l))
router_rule_dump (l->data);
}