/* * 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; }