/*
* This file is part of Cockpit.
*
* Copyright (C) 2014 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 "common/cockpitjson.h"
#include "common/cockpitloopback.h"
#include "common/cockpitunicode.h"
#include
#include
#include
#include
#include
#include
const gchar * cockpit_bridge_local_address = NULL;
/**
* CockpitChannel:
*
* A base class for the server (ie: bridge) side of a channel. Derived
* classes implement the actual payload contents, opening the channel
* etc...
*
* The channel queues messages received until unfrozen. The caller can
* start off a channel as frozen, and then the implementation later
* indicates that it's open and ready to receive messages.
*
* A channel sends messages over a #CockpitTransport. If the transport
* closes then the channel closes, but the channel can also close
* individually either for failure reasons, or with an orderly shutdown.
*
* See doc/protocol.md for information about channels.
*/
struct _CockpitChannelPrivate {
gulong recv_sig;
gulong close_sig;
gulong control_sig;
/* Construct arguments */
CockpitTransport *transport;
gchar *id;
JsonObject *open_options;
gchar **capabilities;
/* Queued messages before channel is ready */
gboolean prepared;
guint prepare_tag;
/* Whether we've sent a closed message */
gboolean sent_close;
/* Whether we called the close vfunc */
gboolean emitted_close;
/* Whether the transport closed (before we did) */
gboolean transport_closed;
/* EOF flags */
gboolean sent_done;
gboolean received_done;
/* Binary options */
gboolean binary_ok;
/* Other state */
JsonObject *close_options;
};
enum {
PROP_0,
PROP_TRANSPORT,
PROP_ID,
PROP_OPTIONS,
PROP_CAPABILITIES,
};
static guint cockpit_channel_sig_closed;
G_DEFINE_TYPE (CockpitChannel, cockpit_channel, G_TYPE_OBJECT);
static gboolean
on_idle_prepare (gpointer data)
{
CockpitChannel *self = COCKPIT_CHANNEL (data);
g_object_ref (self);
self->priv->prepare_tag = 0;
cockpit_channel_prepare (self);
g_object_unref (self);
return FALSE;
}
static void
cockpit_channel_init (CockpitChannel *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, COCKPIT_TYPE_CHANNEL,
CockpitChannelPrivate);
}
static void
process_recv (CockpitChannel *self,
GBytes *payload)
{
CockpitChannelClass *klass;
if (self->priv->received_done)
{
cockpit_channel_fail (self, "protocol-error", "channel received message after done");
}
else
{
klass = COCKPIT_CHANNEL_GET_CLASS (self);
g_assert (klass->recv);
(klass->recv) (self, payload);
}
}
static gboolean
on_transport_recv (CockpitTransport *transport,
const gchar *channel_id,
GBytes *data,
gpointer user_data)
{
CockpitChannel *self = user_data;
if (g_strcmp0 (channel_id, self->priv->id) != 0)
return FALSE;
process_recv (self, data);
return TRUE;
}
static void
process_control (CockpitChannel *self,
const gchar *command,
JsonObject *options)
{
CockpitChannelClass *klass;
const gchar *problem;
if (g_str_equal (command, "close"))
{
g_debug ("close channel %s", self->priv->id);
if (!cockpit_json_get_string (options, "problem", NULL, &problem))
problem = NULL;
cockpit_channel_close (self, problem);
return;
}
if (g_str_equal (command, "done"))
{
if (self->priv->received_done)
cockpit_channel_fail (self, "protocol-error", "channel received second done");
else
self->priv->received_done = TRUE;
}
klass = COCKPIT_CHANNEL_GET_CLASS (self);
if (klass->control)
(klass->control) (self, command, options);
}
static gboolean
on_transport_control (CockpitTransport *transport,
const char *command,
const gchar *channel_id,
JsonObject *options,
GBytes *payload,
gpointer user_data)
{
CockpitChannel *self = user_data;
if (g_strcmp0 (channel_id, self->priv->id) != 0)
return FALSE;
process_control (self, command, options);
return TRUE;
}
static void
on_transport_closed (CockpitTransport *transport,
const gchar *problem,
gpointer user_data)
{
CockpitChannel *self = COCKPIT_CHANNEL (user_data);
self->priv->transport_closed = TRUE;
if (problem == NULL)
problem = "disconnected";
if (!self->priv->emitted_close)
cockpit_channel_close (self, problem);
}
static void
cockpit_channel_constructed (GObject *object)
{
CockpitChannel *self = COCKPIT_CHANNEL (object);
G_OBJECT_CLASS (cockpit_channel_parent_class)->constructed (object);
g_return_if_fail (self->priv->id != NULL);
self->priv->capabilities = NULL;
self->priv->recv_sig = g_signal_connect (self->priv->transport, "recv",
G_CALLBACK (on_transport_recv), self);
self->priv->control_sig = g_signal_connect (self->priv->transport, "control",
G_CALLBACK (on_transport_control), self);
self->priv->close_sig = g_signal_connect (self->priv->transport, "closed",
G_CALLBACK (on_transport_closed), self);
/* Freeze this channel's messages until ready */
cockpit_transport_freeze (self->priv->transport, self->priv->id);
self->priv->prepare_tag = g_idle_add_full (G_PRIORITY_HIGH, on_idle_prepare, self, NULL);
}
static gboolean
strv_contains (gchar **strv,
gchar *str)
{
g_return_val_if_fail (strv != NULL, FALSE);
g_return_val_if_fail (str != NULL, FALSE);
for (; *strv != NULL; strv++)
{
if (g_str_equal (str, *strv))
return TRUE;
}
return FALSE;
}
static gboolean
cockpit_channel_ensure_capable (CockpitChannel *channel,
JsonObject *options)
{
gchar **capabilities = NULL;
JsonObject *close_options = NULL; // owned by channel
gboolean missing = FALSE;
gboolean ret = FALSE;
gint len;
gint i;
if (!cockpit_json_get_strv (options, "capabilities", NULL, &capabilities))
{
cockpit_channel_fail (channel, "protocol-error", "got invalid capabilities field in open message");
goto out;
}
if (!capabilities)
{
ret = TRUE;
goto out;
}
len = g_strv_length (capabilities);
for (i = 0; i < len; i++)
{
if (channel->priv->capabilities == NULL ||
!strv_contains(channel->priv->capabilities, capabilities[i]))
{
g_message ("unsupported capability required: %s", capabilities[i]);
missing = TRUE;
}
}
if (missing)
{
JsonArray *arr = json_array_new (); // owned by closed options
if (channel->priv->capabilities != NULL)
{
len = g_strv_length (channel->priv->capabilities);
for (i = 0; i < len; i++)
json_array_add_string_element (arr, channel->priv->capabilities[i]);
}
close_options = cockpit_channel_close_options (channel);
json_object_set_array_member (close_options, "capabilities", arr);
cockpit_channel_close (channel, "not-supported");
}
ret = !missing;
out:
g_free (capabilities);
return ret;
}
static void
cockpit_channel_real_prepare (CockpitChannel *channel)
{
CockpitChannel *self = COCKPIT_CHANNEL (channel);
JsonObject *options;
const gchar *binary;
options = cockpit_channel_get_options (self);
if (!cockpit_channel_ensure_capable (self, options))
return;
if (G_OBJECT_TYPE (channel) == COCKPIT_TYPE_CHANNEL)
{
cockpit_channel_close (channel, "not-supported");
return;
}
if (!cockpit_json_get_string (options, "binary", NULL, &binary))
{
cockpit_channel_fail (self, "protocol-error", "channel has invalid \"binary\" option");
}
else if (binary != NULL)
{
self->priv->binary_ok = TRUE;
if (!g_str_equal (binary, "raw"))
{
cockpit_channel_fail (self, "protocol-error",
"channel has invalid \"binary\" option: %s", binary);
}
}
}
static void
cockpit_channel_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
CockpitChannel *self = COCKPIT_CHANNEL (object);
switch (prop_id)
{
case PROP_TRANSPORT:
g_value_set_object (value, self->priv->transport);
break;
case PROP_ID:
g_value_set_string (value, self->priv->id);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
cockpit_channel_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
CockpitChannel *self = COCKPIT_CHANNEL (object);
switch (prop_id)
{
case PROP_TRANSPORT:
self->priv->transport = g_value_dup_object (value);
break;
case PROP_ID:
self->priv->id = g_value_dup_string (value);
break;
case PROP_OPTIONS:
self->priv->open_options = g_value_dup_boxed (value);
break;
case PROP_CAPABILITIES:
g_return_if_fail (self->priv->capabilities == NULL);
self->priv->capabilities = g_value_dup_boxed (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
cockpit_channel_dispose (GObject *object)
{
CockpitChannel *self = COCKPIT_CHANNEL (object);
/*
* This object was destroyed before going to the main loop
* no need to wait until later before we fire various signals.
*/
if (self->priv->prepare_tag)
{
g_source_remove (self->priv->prepare_tag);
self->priv->prepare_tag = 0;
}
if (self->priv->recv_sig)
g_signal_handler_disconnect (self->priv->transport, self->priv->recv_sig);
self->priv->recv_sig = 0;
if (self->priv->control_sig)
g_signal_handler_disconnect (self->priv->transport, self->priv->control_sig);
self->priv->control_sig = 0;
if (self->priv->close_sig)
g_signal_handler_disconnect (self->priv->transport, self->priv->close_sig);
self->priv->close_sig = 0;
if (!self->priv->emitted_close)
cockpit_channel_close (self, "terminated");
G_OBJECT_CLASS (cockpit_channel_parent_class)->dispose (object);
}
static void
cockpit_channel_finalize (GObject *object)
{
CockpitChannel *self = COCKPIT_CHANNEL (object);
g_object_unref (self->priv->transport);
if (self->priv->open_options)
json_object_unref (self->priv->open_options);
if (self->priv->close_options)
json_object_unref (self->priv->close_options);
g_strfreev (self->priv->capabilities);
g_free (self->priv->id);
G_OBJECT_CLASS (cockpit_channel_parent_class)->finalize (object);
}
static void
cockpit_channel_real_close (CockpitChannel *self,
const gchar *problem)
{
JsonObject *object;
GBytes *message;
if (self->priv->sent_close)
return;
self->priv->sent_close = TRUE;
if (!self->priv->transport_closed)
{
if (self->priv->close_options)
{
object = self->priv->close_options;
self->priv->close_options = NULL;
}
else
{
object = json_object_new ();
}
json_object_set_string_member (object, "command", "close");
json_object_set_string_member (object, "channel", self->priv->id);
if (problem)
json_object_set_string_member (object, "problem", problem);
message = cockpit_json_write_bytes (object);
json_object_unref (object);
cockpit_transport_send (self->priv->transport, NULL, message);
g_bytes_unref (message);
}
g_signal_emit (self, cockpit_channel_sig_closed, 0, problem);
}
static void
cockpit_channel_class_init (CockpitChannelClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
GSocketAddress *address;
GInetAddress *inet;
const gchar *port;
gobject_class->constructed = cockpit_channel_constructed;
gobject_class->get_property = cockpit_channel_get_property;
gobject_class->set_property = cockpit_channel_set_property;
gobject_class->dispose = cockpit_channel_dispose;
gobject_class->finalize = cockpit_channel_finalize;
klass->prepare = cockpit_channel_real_prepare;
klass->close = cockpit_channel_real_close;
/**
* CockpitChannel:transport:
*
* The transport to send and receive messages over.
*/
g_object_class_install_property (gobject_class, PROP_TRANSPORT,
g_param_spec_object ("transport", "transport", "transport", COCKPIT_TYPE_TRANSPORT,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
/**
* CockpitChannel:channel:
*
* The numeric channel to receive and send messages on.
*/
g_object_class_install_property (gobject_class, PROP_ID,
g_param_spec_string ("id", "id", "id", NULL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
/**
* CockpitChannel:options:
*
* The JSON options used to open this channel. The exact contents are
* dependent on the derived channel class ... but this must at the
* very least contain a 'payload' field describing what kind of channel
* this should be.
*/
g_object_class_install_property (gobject_class, PROP_OPTIONS,
g_param_spec_boxed ("options", "options", "options", JSON_TYPE_OBJECT,
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
/**
* CockpitChannel:capabilities:
*
* The capabilties that this channel supports.
*/
g_object_class_install_property (gobject_class, PROP_CAPABILITIES,
g_param_spec_boxed ("capabilities",
"Capabilities",
"Channel Capabilities",
G_TYPE_STRV,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
/**
* CockpitChannel::closed:
*
* Emitted when the channel closes. This is similar to CockpitTransport::closed
* but only applies to the individual channel.
*
* The channel will also be closed when the transport closes.
*/
cockpit_channel_sig_closed = g_signal_new ("closed", COCKPIT_TYPE_CHANNEL, G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (CockpitChannelClass, closed),
NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
g_type_class_add_private (klass, sizeof (CockpitChannelPrivate));
/*
* If we're running under a test server, register that server's HTTP address
* as an internal address, available for use in cockpit channels.
*/
port = g_getenv ("COCKPIT_TEST_SERVER_PORT");
if (port)
{
inet = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV4);
address = g_inet_socket_address_new (inet, atoi (port));
cockpit_channel_internal_address ("/test-server", address);
g_object_unref (address);
g_object_unref (inet);
}
}
/**
* cockpit_channel_close:
* @self: a channel
* @problem: the problem or NULL
*
* Close the channel. This can be called mulitple times.
*
* It may be that the channel doesn't close immediately.
* The channel will emit the CockpitChannel::closed signal when the
* channel actually closes.
*
* If this is called immediately after or during construction then
* the closing will happen after the main loop so that handlers
* can connect appropriately.
*
* A @problem of NULL represents an orderly close.
*/
void
cockpit_channel_close (CockpitChannel *self,
const gchar *problem)
{
CockpitChannelClass *klass;
g_return_if_fail (COCKPIT_IS_CHANNEL (self));
/* No further messages should be received */
if (self->priv->recv_sig)
g_signal_handler_disconnect (self->priv->transport, self->priv->recv_sig);
self->priv->recv_sig = 0;
if (self->priv->control_sig)
g_signal_handler_disconnect (self->priv->transport, self->priv->control_sig);
self->priv->control_sig = 0;
if (self->priv->close_sig)
g_signal_handler_disconnect (self->priv->transport, self->priv->close_sig);
self->priv->close_sig = 0;
klass = COCKPIT_CHANNEL_GET_CLASS (self);
g_assert (klass->close != NULL);
self->priv->emitted_close = TRUE;
(klass->close) (self, problem);
}
/*
* cockpit_channel_fail:
* @self: a channel
* @problem: the problem
*
* Close the channel with a @problem. In addition a "message" field
* will be set on the channel, using the @format argument to bulid
* the message. The message will also be logged.
*
* See cockpit_channel_close() for further info.
*/
void
cockpit_channel_fail (CockpitChannel *self,
const gchar *problem,
const gchar *format,
...)
{
JsonObject *options;
gchar *message;
va_list va;
g_return_if_fail (problem != NULL);
g_return_if_fail (COCKPIT_IS_CHANNEL (self));
va_start (va, format);
message = g_strdup_vprintf (format, va);
va_end (va);
options = cockpit_channel_close_options (self);
if (!json_object_has_member (options, "message"))
json_object_set_string_member (options, "message", message);
g_message ("%s: %s", self->priv->id, message);
g_free (message);
cockpit_channel_close (self, problem);
}
/* Used by implementations */
/**
* cockpit_channel_ready:
* @self: a pipe
* @message: an optional control message, or NULL
*
* Called by channel implementations to signal when they're
* ready. Any messages received before the channel was ready
* will be delivered to the channel's recv() vfunc in the order
* that they were received.
*
* If this is called immediately after or during construction then
* the closing will happen after the main loop so that handlers
* can connect appropriately.
*/
void
cockpit_channel_ready (CockpitChannel *self,
JsonObject *message)
{
g_object_ref (self);
cockpit_transport_thaw (self->priv->transport, self->priv->id);
cockpit_channel_control (self, "ready", message);
g_object_unref (self);
}
/**
* cockpit_channel_send:
* @self: a pipe
* @payload: the message payload to send
* @trust_is_utf8: set to true if sure data is UTF8
*
* Called by implementations to send a message over the transport
* on the right channel.
*
* This message is queued, and sent once the transport can.
*/
void
cockpit_channel_send (CockpitChannel *self,
GBytes *payload,
gboolean trust_is_utf8)
{
GBytes *validated = NULL;
if (!trust_is_utf8)
{
if (!self->priv->binary_ok)
payload = validated = cockpit_unicode_force_utf8 (payload);
}
cockpit_transport_send (self->priv->transport, self->priv->id, payload);
if (validated)
g_bytes_unref (validated);
}
/**
* cockpit_channel_get_option:
* @self: a channel
*
* Called by implementations to get the channel's open options.
*
* Returns: (transfer none): the open options, should not be NULL
*/
JsonObject *
cockpit_channel_get_options (CockpitChannel *self)
{
g_return_val_if_fail (COCKPIT_IS_CHANNEL (self), NULL);
return self->priv->open_options;
}
/**
* cockpit_channel_close_options
* @self: a channel
*
* Called by implementations to get the channel's close options.
*
* Returns: (transfer none): the close options, should not be NULL
*/
JsonObject *
cockpit_channel_close_options (CockpitChannel *self)
{
g_return_val_if_fail (COCKPIT_IS_CHANNEL (self), NULL);
if (!self->priv->close_options)
self->priv->close_options = json_object_new ();
return self->priv->close_options;
}
/**
* cockpit_channel_get_id:
* @self a channel
*
* Get the identifier for this channel.
*
* Returns: (transfer none): the identifier
*/
const gchar *
cockpit_channel_get_id (CockpitChannel *self)
{
g_return_val_if_fail (COCKPIT_IS_CHANNEL (self), NULL);
return self->priv->id;
}
/**
* cockpit_channel_prepare:
* @self: the channel
*
* Usually this is automatically called after the channel is
* created and control returns to the mainloop. However you
* can preempt that by calling this function. In the case of
* a frozen channel, this method needs to be called to set
* things in motion.
*/
void
cockpit_channel_prepare (CockpitChannel *self)
{
CockpitChannelClass *klass;
g_return_if_fail (COCKPIT_IS_CHANNEL (self));
if (self->priv->prepared)
return;
if (self->priv->prepare_tag)
{
g_source_remove (self->priv->prepare_tag);
self->priv->prepare_tag = 0;
}
self->priv->prepared = TRUE;
if (!self->priv->emitted_close)
{
klass = COCKPIT_CHANNEL_GET_CLASS (self);
g_assert (klass->prepare);
(klass->prepare) (self);
}
}
/**
* cockpit_channel_control:
* @self: the channel
* @command: the control command
* @options: optional control message or NULL
*
* Send a control message to the other side.
*
* If @options is not NULL, then it may be modified by this code.
*
* With @command of "done" will send an EOF to the other side. This
* should only be called once. Whether an EOF should be sent or not
* depends on the payload type.
*/
void
cockpit_channel_control (CockpitChannel *self,
const gchar *command,
JsonObject *options)
{
JsonObject *object;
GBytes *message;
const gchar *problem;
gchar *problem_copy = NULL;
g_return_if_fail (COCKPIT_IS_CHANNEL (self));
g_return_if_fail (command != NULL);
if (g_str_equal (command, "done"))
{
g_return_if_fail (self->priv->sent_done == FALSE);
self->priv->sent_done = TRUE;
}
/* If closing save the close options
* and let close send the message */
else if (g_str_equal (command, "close"))
{
if (!self->priv->close_options)
{
/* Ref for close_options, freed in parent */
self->priv->close_options = json_object_ref (options);
}
if (!cockpit_json_get_string (options, "problem", NULL, &problem))
problem = NULL;
/* Use a problem copy so it out lasts the value in close_options */
problem_copy = g_strdup (problem);
cockpit_channel_close (self, problem_copy);
goto out;
}
if (options)
object = json_object_ref (options);
else
object = json_object_new ();
json_object_set_string_member (object, "command", command);
json_object_set_string_member (object, "channel", self->priv->id);
message = cockpit_json_write_bytes (object);
json_object_unref (object);
cockpit_transport_send (self->priv->transport, NULL, message);
g_bytes_unref (message);
out:
g_free (problem_copy);
}
static GHashTable *internal_addresses;
static void
safe_unref (gpointer data)
{
GObject *object = data;
if (object != NULL)
g_object_unref (object);
}
static gboolean
lookup_internal (const gchar *name,
GSocketConnectable **connectable)
{
const gchar *env;
gboolean ret = FALSE;
GSocketAddress *address;
g_assert (name != NULL);
g_assert (connectable != NULL);
if (internal_addresses)
{
ret = g_hash_table_lookup_extended (internal_addresses, name, NULL,
(gpointer *)connectable);
}
if (!ret && g_str_equal (name, "ssh-agent"))
{
*connectable = NULL;
env = g_getenv ("SSH_AUTH_SOCK");
if (env != NULL && env[0] != '\0')
{
address = g_unix_socket_address_new (env);
*connectable = G_SOCKET_CONNECTABLE (address);
cockpit_channel_internal_address ("ssh-agent", address);
}
ret = TRUE;
}
return ret;
}
void
cockpit_channel_internal_address (const gchar *name,
GSocketAddress *address)
{
if (!internal_addresses)
{
internal_addresses = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, safe_unref);
}
if (address)
address = g_object_ref (address);
g_hash_table_replace (internal_addresses, g_strdup (name), address);
}
gboolean
cockpit_channel_remove_internal_address (const gchar *name)
{
gboolean ret = FALSE;
if (internal_addresses)
ret = g_hash_table_remove (internal_addresses, name);
return ret;
}
static GSocketConnectable *
parse_address (CockpitChannel *self,
gchar **possible_name,
gboolean *local_address)
{
GSocketConnectable *connectable = NULL;
const gchar *unix_path;
const gchar *internal;
const gchar *address;
JsonObject *options;
gboolean local = FALSE;
GError *error = NULL;
const gchar *host;
gint64 port;
gchar *name = NULL;
gboolean open = FALSE;
options = self->priv->open_options;
if (!cockpit_json_get_string (options, "unix", NULL, &unix_path))
{
cockpit_channel_fail (self, "protocol-error", "invalid \"unix\" option in channel");
goto out;
}
if (!cockpit_json_get_int (options, "port", G_MAXINT64, &port))
{
cockpit_channel_fail (self, "protocol-error", "invalid \"port\" option in channel");
goto out;
}
if (!cockpit_json_get_string (options, "internal", NULL, &internal))
{
cockpit_channel_fail (self, "protocol-error", "invalid \"internal\" option in channel");
goto out;
}
if (!cockpit_json_get_string (options, "address", NULL, &address))
{
cockpit_channel_fail (self, "protocol-error", "invalid \"address\" option in channel");
goto out;
}
if (port != G_MAXINT64 && unix_path)
{
cockpit_channel_fail (self, "protocol-error", "cannot specify both \"port\" and \"unix\" options");
goto out;
}
else if (port != G_MAXINT64)
{
if (port <= 0 || port > 65535)
{
cockpit_channel_fail (self, "protocol-error", "received invalid \"port\" option");
goto out;
}
if (address)
{
connectable = g_network_address_new (address, port);
host = address;
/* This isn't perfect, but matches the use case. Specify address => non-local */
local = FALSE;
}
else if (cockpit_bridge_local_address)
{
connectable = g_network_address_parse (cockpit_bridge_local_address, port, &error);
host = cockpit_bridge_local_address;
local = TRUE;
}
else
{
connectable = cockpit_loopback_new (port);
host = "localhost";
local = TRUE;
}
if (error != NULL)
{
cockpit_channel_fail (self, "internal-error",
"couldn't parse local address: %s: %s", host, error->message);
goto out;
}
else
{
name = g_strdup_printf ("%s:%d", host, (gint)port);
}
}
else if (unix_path)
{
name = g_strdup (unix_path);
connectable = G_SOCKET_CONNECTABLE (g_unix_socket_address_new (unix_path));
local = FALSE;
}
else if (internal)
{
gboolean reg = lookup_internal (internal, &connectable);
if (!connectable)
{
if (reg)
cockpit_channel_close (self, "not-found");
else
cockpit_channel_fail (self, "not-found", "couldn't find internal address: %s", internal);
goto out;
}
name = g_strdup (internal);
connectable = g_object_ref (connectable);
local = FALSE;
}
else
{
cockpit_channel_fail (self, "protocol-error",
"no \"port\" or \"unix\" or other address option for channel");
goto out;
}
open = TRUE;
out:
g_clear_error (&error);
if (open)
{
if (possible_name)
*possible_name = g_strdup (name);
if (local_address)
*local_address = local;
}
else
{
if (connectable)
g_object_unref (connectable);
connectable = NULL;
}
g_free (name);
return connectable;
}
GSocketAddress *
cockpit_channel_parse_address (CockpitChannel *self,
gchar **possible_name)
{
GSocketConnectable *connectable;
GSocketAddressEnumerator *enumerator;
GSocketAddress *address;
GError *error = NULL;
gchar *name = NULL;
connectable = parse_address (self, &name, NULL);
if (!connectable)
return NULL;
/* This is sync, but realistically, it doesn't matter for current use cases */
enumerator = g_socket_connectable_enumerate (connectable);
g_object_unref (connectable);
address = g_socket_address_enumerator_next (enumerator, NULL, &error);
g_object_unref (enumerator);
if (error != NULL)
{
cockpit_channel_fail (self, "not-found", "couldn't find address: %s: %s", name, error->message);
g_error_free (error);
g_free (name);
return NULL;
}
if (possible_name)
*possible_name = name;
else
g_free (name);
return address;
}
static gboolean
parse_option_file_or_data (CockpitChannel *self,
JsonObject *options,
const gchar *option,
const gchar **file,
const gchar **data)
{
JsonObject *object;
JsonNode *node;
g_assert (file != NULL);
g_assert (data != NULL);
node = json_object_get_member (options, option);
if (!node)
{
*file = NULL;
*data = NULL;
return TRUE;
}
if (!JSON_NODE_HOLDS_OBJECT (node))
{
cockpit_channel_fail (self, "protocol-error", "invalid \"%s\" tls option for channel", option);
return FALSE;
}
object = json_node_get_object (node);
if (!cockpit_json_get_string (object, "file", NULL, file))
{
cockpit_channel_fail (self, "protocol-error", "invalid \"file\" %s option for channel", option);
}
else if (!cockpit_json_get_string (object, "data", NULL, data))
{
cockpit_channel_fail (self, "protocol-error", "invalid \"data\" %s option for channel", option);
}
else if (!*file && !*data)
{
cockpit_channel_fail (self, "not-supported", "missing or unsupported \"%s\" option for channel", option);
}
else if (*file && *data)
{
cockpit_channel_fail (self, "protocol-error", "cannot specify both \"file\" and \"data\" in \"%s\" option for channel", option);
}
else
{
return TRUE;
}
return FALSE;
}
static gboolean
load_pem_contents (CockpitChannel *self,
const gchar *filename,
const gchar *option,
GString *pem)
{
GError *error = NULL;
gchar *contents = NULL;
gsize len;
if (!g_file_get_contents (filename, &contents, &len, &error))
{
cockpit_channel_fail (self, "internal-error",
"couldn't load \"%s\" file: %s: %s", option, filename, error->message);
g_clear_error (&error);
return FALSE;
}
else
{
g_string_append_len (pem, contents, len);
g_string_append_c (pem, '\n');
g_free (contents);
return TRUE;
}
}
static gchar *
expand_filename (const gchar *filename)
{
if (!g_path_is_absolute (filename))
return g_build_filename (g_get_home_dir (), filename, NULL);
else
return g_strdup (filename);
}
static gboolean
parse_cert_option_as_pem (CockpitChannel *self,
JsonObject *options,
const gchar *option,
GString *pem)
{
gboolean ret = TRUE;
const gchar *file;
const gchar *data;
gchar *path;
if (!parse_option_file_or_data (self, options, option, &file, &data))
return FALSE;
if (file)
{
path = expand_filename (file);
/* For now we assume file contents are PEM */
ret = load_pem_contents (self, path, option, pem);
g_free (path);
}
else if (data)
{
/* Format this as PEM of the given type */
g_string_append (pem, data);
g_string_append_c (pem, '\n');
}
return ret;
}
static gboolean
parse_cert_option_as_database (CockpitChannel *self,
JsonObject *options,
const gchar *option,
GTlsDatabase **database)
{
gboolean temporary = FALSE;
GError *error = NULL;
gboolean ret = TRUE;
const gchar *file;
const gchar *data;
gchar *path;
gint fd;
if (!parse_option_file_or_data (self, options, option, &file, &data))
return FALSE;
if (file)
{
path = expand_filename (file);
ret = TRUE;
}
else if (data)
{
temporary = TRUE;
path = g_build_filename (g_get_user_runtime_dir (), "cockpit-bridge-cert-authority.XXXXXX", NULL);
fd = g_mkstemp (path);
if (fd < 0)
{
ret = FALSE;
cockpit_channel_fail (self, "internal-error",
"couldn't create temporary directory: %s: %s", path, g_strerror (errno));
}
else
{
close (fd);
if (!g_file_set_contents (path, data, -1, &error))
{
cockpit_channel_fail (self, "internal-error",
"couldn't write temporary data to: %s: %s", path, error->message);
g_clear_error (&error);
ret = FALSE;
}
}
}
else
{
/* Not specified */
*database = NULL;
return TRUE;
}
if (ret)
{
*database = g_tls_file_database_new (path, &error);
if (error)
{
cockpit_channel_fail (self, "internal-error",
"couldn't load certificate data: %s: %s", path, error->message);
g_clear_error (&error);
ret = FALSE;
}
}
/* Leave around when problem, for debugging */
if (temporary && ret == TRUE)
g_unlink (path);
g_free (path);
return ret;
}
static gboolean
parse_stream_options (CockpitChannel *self,
CockpitConnectable *connectable)
{
gboolean ret = FALSE;
GTlsCertificate *cert = NULL;
GTlsDatabase *database = NULL;
gboolean use_tls = FALSE;
GError *error = NULL;
GString *pem = NULL;
JsonObject *options;
JsonNode *node;
/* No validation for local servers by default */
gboolean validate = !connectable->local;
node = json_object_get_member (self->priv->open_options, "tls");
if (node && !JSON_NODE_HOLDS_OBJECT (node))
{
cockpit_channel_fail (self, "protocol-error", "invalid \"tls\" option for channel");
goto out;
}
else if (node)
{
options = json_node_get_object (node);
use_tls = TRUE;
/*
* The only function in GLib to parse private keys takes
* them in PEM concatenated form. This is a limitation of GLib,
* rather than concatenated form being a decent standard for
* certificates and keys. So build a combined PEM as expected by
* GLib here.
*/
pem = g_string_sized_new (8192);
if (!parse_cert_option_as_pem (self, options, "certificate", pem))
goto out;
if (pem->len)
{
if (!parse_cert_option_as_pem (self, options, "key", pem))
goto out;
cert = g_tls_certificate_new_from_pem (pem->str, pem->len, &error);
if (error != NULL)
{
cockpit_channel_fail (self, "internal-error",
"invalid \"certificate\" or \"key\" content: %s", error->message);
g_error_free (error);
goto out;
}
}
if (!parse_cert_option_as_database (self, options, "authority", &database))
goto out;
if (!cockpit_json_get_bool (options, "validate", validate, &validate))
{
cockpit_channel_fail (self, "protocol-error", "invalid \"validate\" option");
goto out;
}
}
ret = TRUE;
out:
if (ret)
{
connectable->tls = use_tls;
connectable->tls_cert = cert;
cert = NULL;
if (database)
{
connectable->tls_database = database;
connectable->tls_flags = G_TLS_CERTIFICATE_VALIDATE_ALL;
if (!validate)
connectable->tls_flags &= ~(G_TLS_CERTIFICATE_INSECURE | G_TLS_CERTIFICATE_BAD_IDENTITY);
database = NULL;
}
else
{
if (validate)
connectable->tls_flags = G_TLS_CERTIFICATE_VALIDATE_ALL;
else
connectable->tls_flags = G_TLS_CERTIFICATE_GENERIC_ERROR;
}
}
if (pem)
g_string_free (pem, TRUE);
if (cert)
g_object_unref (cert);
if (database)
g_object_unref (database);
return ret;
}
CockpitConnectable *
cockpit_channel_parse_stream (CockpitChannel *self)
{
CockpitConnectable *connectable;
GSocketConnectable *address;
gboolean local = FALSE;
gchar *name = NULL;
address = parse_address (self, &name, &local);
if (!address)
return NULL;
connectable = g_new0 (CockpitConnectable, 1);
connectable->address = address;
connectable->name = name;
connectable->refs = 1;
connectable->local = local;
if (!parse_stream_options (self, connectable))
{
cockpit_connectable_unref (connectable);
connectable = NULL;
}
return connectable;
}