/* * 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 "cockpithttpstream.h" #include "cockpitconnect.h" #include "cockpitstream.h" #include "common/cockpitjson.h" #include "common/cockpitpipe.h" #include "common/cockpitwebresponse.h" #include "websocket/websocket.h" #include #include /** * CockpitHttpClient * * Information about a certain set of HTTP connections that * have been given a connection name, grouping them together as * a client. In this mode we cache connections and reuse them * as well as share options and address info. */ typedef struct { gint refs; gchar *name; CockpitConnectable *connectable; CockpitStream *stream; gulong sig_close; guint timeout; } CockpitHttpClient; static GHashTable *clients; static void cockpit_http_client_reset (CockpitHttpClient *client) { if (client->timeout) g_source_remove (client->timeout); if (client->stream) { g_signal_handler_disconnect (client->stream, client->sig_close); g_object_unref (client->stream); } client->sig_close = 0; client->stream = NULL; client->timeout = 0; } static void cockpit_http_client_unref (gpointer data) { CockpitHttpClient *client = data; if (--client->refs == 0) { cockpit_http_client_reset (client); if (client->connectable) cockpit_connectable_unref (client->connectable); g_free (client->name); g_slice_free (CockpitHttpClient, client); } } static CockpitHttpClient * cockpit_http_client_ref (CockpitHttpClient *client) { client->refs++; return client; } static void on_client_close (CockpitStream *stream, const gchar *problem, gpointer data) { CockpitHttpClient *client = data; g_debug ("%s: connection closed", client->name); cockpit_http_client_reset (client); } static gboolean on_client_timeout (gpointer data) { CockpitHttpClient *client = data; g_debug ("%s: connection timed out", client->name); cockpit_http_client_reset (client); return FALSE; } static CockpitHttpClient * cockpit_http_client_ensure (const gchar *name) { CockpitHttpClient *client = NULL; if (name) { if (!clients) clients = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, cockpit_http_client_unref); client = g_hash_table_lookup (clients, name); } if (!client) { client = g_slice_new0 (CockpitHttpClient); client->name = g_strdup (name); if (clients && name) { g_debug ("%s: registering client", name); g_hash_table_replace (clients, client->name, cockpit_http_client_ref (client)); } } else if (name) { g_debug ("%s: using client", name); } cockpit_http_client_ref (client); return client; } static void cockpit_http_client_checkin (CockpitHttpClient *client, CockpitStream *stream) { cockpit_http_client_reset (client); client->stream = g_object_ref (stream); client->sig_close = g_signal_connect (stream, "close", G_CALLBACK (on_client_close), client); client->timeout = g_timeout_add_seconds (10, on_client_timeout, client); } static CockpitStream * cockpit_http_client_checkout (CockpitHttpClient *client) { CockpitStream *stream = NULL; if (client->stream) { g_debug ("%s: reusing connection", client->name); stream = g_object_ref (client->stream); cockpit_http_client_reset (client); } return stream; } /** * CockpitHttpStream: * * A #CockpitChannel that represents a HTTP request/response. * * The payload type for this channel is 'http-stream2'. */ /* * Some things we should add later without breaking the payload: * * - Specifying the HTTP version of the request. * - Chunked messages in the request when request is HTTP/1.1 * - Trans coding for non-UTF8 charsets. */ #define COCKPIT_HTTP_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_HTTP_STREAM, CockpitHttpStream)) enum { BUFFER_REQUEST, RELAY_REQUEST, RELAY_DATA, FINISHED }; typedef struct _CockpitHttpStream { CockpitChannel parent; /* The nickname for debugging and logging */ gchar *name; CockpitHttpClient *client; /* The connection */ CockpitStream *stream; gulong sig_open; gulong sig_read; gulong sig_rejected_cert; gulong sig_close; gint state; gboolean failed; gboolean binary; gboolean keep_alive; gboolean headers_inline; /* The request */ GList *request; /* From parsing the response */ gboolean response_chunked; gssize response_length; } CockpitHttpStream; typedef struct { CockpitChannelClass parent_class; } CockpitHttpStreamClass; G_DEFINE_TYPE (CockpitHttpStream, cockpit_http_stream, COCKPIT_TYPE_CHANNEL); static gboolean parse_content_length (CockpitHttpStream *self, CockpitChannel *channel, guint status, GHashTable *headers) { const gchar *header; guint64 value; gchar *end; if (status == 204) { self->response_length = 0; return TRUE; } header = g_hash_table_lookup (headers, "Content-Length"); if (header == NULL) { self->response_length = -1; return TRUE; } value = g_ascii_strtoull (header, &end, 10); if (end[0] != '\0') { cockpit_channel_fail (channel, "protocol-error", "%s: received invalid Content-Length in HTTP stream response", self->name); return FALSE; } else if (value > G_MAXSSIZE) { cockpit_channel_fail (channel, "protocol-error", "%s: received Content-Length that was too big", self->name); return FALSE; } self->response_length = value; g_debug ("%s: content length is %" G_GSSIZE_FORMAT, self->name, self->response_length); return TRUE; } static gboolean parse_transfer_encoding (CockpitHttpStream *self, CockpitChannel *channel, GHashTable *headers) { const gchar *header; header = g_hash_table_lookup (headers, "Transfer-Encoding"); if (header == NULL) { self->response_chunked = FALSE; return TRUE; } if (!g_str_equal (header, "chunked")) { cockpit_channel_fail (channel, "protocol-error", "%s: received unsupported Transfer-Encoding in HTTP response: %s", self->name, header); return FALSE; } self->response_chunked = TRUE; g_debug ("%s: chunked encoding", self->name); return TRUE; } gboolean cockpit_http_stream_parse_keep_alive (const gchar *version, GHashTable *headers) { const gchar *header; header = g_hash_table_lookup (headers, "Connection"); if (!header) { g_debug ("got no \"Connection\" header on %s response", version); if (version && g_ascii_strcasecmp (version, "HTTP/1.1") == 0) header = "keep-alive"; } else { g_debug ("got \"Connection\" header of %s on %s response", header, version); } /* * This is pretty conservative. If a Connection header is present * and it *doesn't* have the non-standard "keep-alive" value in * it, then assume we can't keep alive. Either the connection is * meant to close, or we have no idea what the server is trying * to tell us. */ return (header && strstr (header, "keep-alive") != NULL); } static gboolean parse_keep_alive (CockpitHttpStream *self, CockpitChannel *channel, const gchar *version, GHashTable *headers) { self->keep_alive = cockpit_http_stream_parse_keep_alive (version, headers); return TRUE; } static gboolean relay_headers (CockpitHttpStream *self, CockpitChannel *channel, GByteArray *buffer) { GHashTable *headers = NULL; gchar *version = NULL; gchar *reason = NULL; JsonObject *object; const gchar *data; JsonObject *heads; GHashTableIter iter; GBytes *message; gpointer key; gpointer value; guint status; gsize length; gssize offset; gssize offset2; data = (const gchar *)buffer->data; length = buffer->len; offset = web_socket_util_parse_status_line (data, length, &version, &status, &reason); if (offset == 0) return FALSE; /* want more data */ if (offset < 0) { cockpit_channel_fail (channel, "protocol-error", "%s: received response with bad HTTP status line", self->name); goto out; } offset2 = web_socket_util_parse_headers (data + offset, length - offset, &headers); if (offset2 == 0) return FALSE; /* want more data */ if (offset2 < 0) { cockpit_channel_fail (channel, "protocol-error", "%s: received response with bad HTTP headers", self->name); goto out; } g_debug ("%s: response: %u %s", self->name, status, reason); g_hash_table_iter_init (&iter, headers); while (g_hash_table_iter_next (&iter, &key, &value)) g_debug ("%s: header: %s %s", self->name, (gchar *)key, (gchar *)value); if (!parse_transfer_encoding (self, channel, headers) || !parse_content_length (self, channel, status, headers) || !parse_keep_alive (self, channel, version, headers)) goto out; cockpit_pipe_skip (buffer, offset + offset2); if (!self->binary) { g_hash_table_remove (headers, "Content-Length"); g_hash_table_remove (headers, "Range"); } g_hash_table_remove (headers, "Connection"); g_hash_table_remove (headers, "Transfer-Encoding"); /* Now serialize all the rest of this into JSON */ object = json_object_new (); json_object_set_int_member (object, "status", status); json_object_set_string_member (object, "reason", reason); heads = json_object_new(); g_hash_table_iter_init (&iter, headers); while (g_hash_table_iter_next (&iter, &key, &value)) json_object_set_string_member (heads, key, value); json_object_set_object_member (object, "headers", heads); if (self->headers_inline) { message = cockpit_json_write_bytes (object); cockpit_channel_send (channel, message, TRUE); g_bytes_unref (message); } else { cockpit_channel_control (channel, "response", object); } json_object_unref (object); out: if (headers) g_hash_table_unref (headers); g_free (version); g_free (reason); return TRUE; } static void relay_data (CockpitChannel *channel, GBytes *data) { GBytes *block; gsize size; gsize offset; gsize length; size = g_bytes_get_size (data); if (size < 8192) { cockpit_channel_send (channel, data, FALSE); } else { for (offset = 0; offset < size; offset += 4096) { length = MIN (4096, size - offset); block = g_bytes_new_from_bytes (data, offset, length); cockpit_channel_send (channel, block, FALSE); g_bytes_unref (block); } } } static gboolean relay_chunked (CockpitHttpStream *self, CockpitChannel *channel, GByteArray *buffer) { GBytes *message; const gchar *data; const gchar *pos; guint64 size; gsize length; gsize beg; gchar *end; data = (const gchar *)buffer->data; length = buffer->len; pos = memchr (data, '\r', length); if (pos == NULL) return FALSE; /* want more data */ beg = (pos + 2) - data; if (length < beg) { /* have to have a least the ending chars */ return FALSE; /* want more data */ } size = g_ascii_strtoull (data, &end, 16); if (pos[1] != '\n' || end != pos) { cockpit_channel_fail (channel, "protocol-error", "%s: received invalid HTTP chunk", self->name); } else if (size > G_MAXSSIZE) { cockpit_channel_fail (channel, "protocol-error", "%s: received extremely large HTTP chunk", self->name); } else if (length < beg + size + 2) { return FALSE; /* want more data */ } else if (data[beg + size] != '\r' || data[beg + size + 1] != '\n') { cockpit_channel_fail (channel, "protocol-error", "%s: received invalid HTTP chunk data", self->name); } else if (size == 0) { /* All done, yay */ g_debug ("%s: received last chunk", self->name); cockpit_pipe_skip (buffer, beg + 2); cockpit_channel_close (channel, NULL); g_assert (self->state == FINISHED); } else { message = cockpit_pipe_consume (buffer, beg, size, 2); relay_data (channel, message); g_bytes_unref (message); return TRUE; } return TRUE; } static gboolean relay_length (CockpitHttpStream *self, CockpitChannel *channel, GByteArray *buffer) { GBytes *message = NULL; gsize block; g_assert (self->response_length >= 0); if (self->response_length == 0) { /* All done, yay */ g_debug ("%s: received enough bytes", self->name); cockpit_channel_close (channel, NULL); g_assert (self->state == FINISHED); } else if (buffer->len == 0) { /* Not enough data? */ return FALSE; } else { block = MIN (buffer->len, self->response_length); self->response_length -= block; message = cockpit_pipe_consume (buffer, 0, block, 0); relay_data (channel, message); g_bytes_unref (message); } return TRUE; } static gboolean relay_all (CockpitHttpStream *self, CockpitChannel *channel, GByteArray *buffer) { GBytes *message; if (buffer->len == 0) { /* Not enough data? */ return FALSE; } message = cockpit_pipe_consume (buffer, 0, buffer->len, 0); relay_data (channel, message); g_bytes_unref (message); return TRUE; } static void on_stream_open (CockpitStream *stream, gpointer user_data) { CockpitChannel *channel = user_data; cockpit_channel_ready (channel, NULL); } static void on_stream_read (CockpitStream *stream, GByteArray *buffer, gboolean end_of_data, gpointer user_data) { CockpitHttpStream *self = user_data; CockpitChannel *channel = user_data; gboolean ret; g_object_ref (self); if (self->state < RELAY_REQUEST) { if (buffer->len != 0) { cockpit_channel_fail (channel, "protocol-error", "%s: received data before HTTP request was sent", self->name); } } else if (self->state < RELAY_DATA) { /* Parse headers */ if (relay_headers (self, channel, buffer)) { self->state = RELAY_DATA; } else if (end_of_data) { cockpit_channel_fail (channel, "protocol-error", "%s: received truncated HTTP response", self->name); } } while (self->state == RELAY_DATA) { if (self->response_chunked) ret = relay_chunked (self, channel, buffer); else if (self->response_length >= 0) ret = relay_length (self, channel, buffer); else ret = relay_all (self, channel, buffer); if (!ret) break; } g_object_unref (self); } static void on_rejected_cert (CockpitStream *stream, const gchar *pem_data, gpointer user_data) { CockpitHttpStream *self = user_data; CockpitChannel *channel = user_data; JsonObject *close_options = NULL; // owned by channel if (self->state != FINISHED) { close_options = cockpit_channel_close_options (channel); json_object_set_string_member (close_options, "rejected-certificate", pem_data); } } static void on_stream_close (CockpitStream *stream, const gchar *problem, gpointer user_data) { CockpitHttpStream *self = user_data; CockpitChannel *channel = user_data; self->keep_alive = FALSE; if (self->state != FINISHED) { if (problem) { cockpit_channel_close (channel, problem); } else if (self->state == RELAY_DATA && !self->response_chunked && self->response_length <= 0) { g_debug ("%s: end of stream is end of data", self->name); cockpit_channel_close (channel, NULL); } else { cockpit_channel_fail (channel, "protocol-error", "%s: received truncated HTTP response", self->name); } } } static gboolean disallowed_header (const gchar *name, const gchar *value, gboolean binary) { static const gchar *bad_headers[] = { "Content-Length", "Content-MD5", "TE", "Trailer", "Transfer-Encoding", "Upgrade", NULL }; static const gchar *bad_text[] = { "Accept-Encoding", "Content-Encoding", "Accept-Charset", "Accept-Ranges", "Content-Range", "Range", NULL }; gint i; for (i = 0; bad_headers[i] != NULL; i++) { if (g_ascii_strcasecmp (bad_headers[i], name) == 0) return TRUE; } if (!binary) { for (i = 0; bad_text[i] != NULL; i++) { if (g_ascii_strcasecmp (bad_text[i], name) == 0) return TRUE; } } /* Only allow the caller to specify Connection: close */ if (g_ascii_strcasecmp ("Connection", name) == 0 && !g_strcmp0 (value, "close")) { return TRUE; } return FALSE; } static void send_http_request (CockpitHttpStream *self) { CockpitChannel *channel = COCKPIT_CHANNEL (self); JsonObject *options; gboolean had_host; gboolean had_encoding; const gchar *method; const gchar *path; GString *string = NULL; JsonNode *node; JsonObject *headers; const gchar *header; const gchar *value; GList *request = NULL; GList *names = NULL; GBytes *bytes; GList *l; gsize total; options = cockpit_channel_get_options (channel); /* * The checks we do here for token validity are just enough to be able * to format an HTTP response, without leaking across lines etc. */ if (!cockpit_json_get_string (options, "path", NULL, &path)) { cockpit_channel_fail (channel, "protocol-error", "%s: bad \"path\" field in HTTP stream request", self->name); goto out; } else if (path == NULL) { cockpit_channel_fail (channel, "protocol-error", "%s: missing \"path\" field in HTTP stream request", self->name); goto out; } else if (!cockpit_web_response_is_simple_token (path)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"path\" field in HTTP stream request", self->name); goto out; } if (!cockpit_json_get_string (options, "method", NULL, &method)) { cockpit_channel_fail (channel, "protocol-error", "%s: bad \"method\" field in HTTP stream request", self->name); goto out; } else if (method == NULL) { cockpit_channel_fail (channel, "protocol-error", "%s: missing \"method\" field in HTTP stream request", self->name); goto out; } else if (!cockpit_web_response_is_simple_token (method)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"method\" field in HTTP stream request", self->name); goto out; } g_debug ("%s: sending %s request", self->name, method); string = g_string_sized_new (128); g_string_printf (string, "%s %s HTTP/1.1\r\n", method, path); had_encoding = had_host = FALSE; node = json_object_get_member (options, "headers"); if (node) { if (!JSON_NODE_HOLDS_OBJECT (node)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"headers\" field in HTTP stream request", self->name); goto out; } headers = json_node_get_object (node); names = json_object_get_members (headers); for (l = names; l != NULL; l = g_list_next (l)) { header = l->data; if (!cockpit_web_response_is_simple_token (header)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid header in HTTP stream request: %s", self->name, header); goto out; } node = json_object_get_member (headers, header); if (!node || !JSON_NODE_HOLDS_VALUE (node) || json_node_get_value_type (node) != G_TYPE_STRING) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid header value in HTTP stream request: %s", self->name, header); goto out; } value = json_node_get_string (node); if (disallowed_header (header, value, self->binary)) { cockpit_channel_fail (channel, "protocol-error", "%s: disallowed header in HTTP stream request: %s", self->name, header); goto out; } if (!cockpit_web_response_is_header_value (value)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid header value in HTTP stream request: %s", self->name, header); goto out; } g_string_append_printf (string, "%s: %s\r\n", (gchar *)l->data, value); g_debug ("%s: sending header: %s %s", self->name, (gchar *)l->data, value); if (g_ascii_strcasecmp (l->data, "Host") == 0) had_host = TRUE; if (g_ascii_strcasecmp (l->data, "Accept-Encoding") == 0) had_encoding = TRUE; } } if (!had_host) { g_string_append (string, "Host: "); g_string_append_uri_escaped (string, self->client->connectable->name, "[]!%$&()*+,-.:;=\\_~", FALSE); g_string_append (string, "\r\n"); } if (!had_encoding) g_string_append (string, "Accept-Encoding: identity\r\n"); if (!self->binary) g_string_append (string, "Accept-Charset: UTF-8\r\n"); request = g_list_reverse (self->request); self->request = NULL; /* Calculate how much data we have to send */ total = 0; for (l = request; l != NULL; l = g_list_next (l)) total += g_bytes_get_size (l->data); if (request || g_ascii_strcasecmp (method, "POST") == 0) g_string_append_printf (string, "Content-Length: %" G_GSIZE_FORMAT "\r\n", total); g_string_append (string, "\r\n"); bytes = g_string_free_to_bytes (string); string = NULL; cockpit_stream_write (self->stream, bytes); g_bytes_unref (bytes); /* Now send all the data */ for (l = request; l != NULL; l = g_list_next (l)) cockpit_stream_write (self->stream, l->data); out: g_list_free (names); g_list_free_full (request, (GDestroyNotify)g_bytes_unref); if (string) g_string_free (string, TRUE); } static void cockpit_http_stream_recv (CockpitChannel *channel, GBytes *message) { CockpitHttpStream *self = (CockpitHttpStream *)channel; self->request = g_list_prepend (self->request, g_bytes_ref (message)); } static gboolean cockpit_http_stream_control (CockpitChannel *channel, const gchar *command, JsonObject *options) { CockpitHttpStream *self = COCKPIT_HTTP_STREAM (channel); if (g_str_equal (command, "done")) { g_return_val_if_fail (self->state == BUFFER_REQUEST, FALSE); self->state = RELAY_REQUEST; send_http_request (self); return TRUE; } return FALSE; } static void cockpit_http_stream_close (CockpitChannel *channel, const gchar *problem) { CockpitHttpStream *self = COCKPIT_HTTP_STREAM (channel); if (problem) { self->failed = TRUE; self->state = FINISHED; COCKPIT_CHANNEL_CLASS (cockpit_http_stream_parent_class)->close (channel, problem); } else if (self->state == RELAY_DATA) { g_debug ("%s: relayed response", self->name); self->state = FINISHED; cockpit_channel_control (channel, "done", NULL); /* Save this for another round? */ if (self->keep_alive) { if (self->sig_open) g_signal_handler_disconnect (self->stream, self->sig_open); g_signal_handler_disconnect (self->stream, self->sig_read); g_signal_handler_disconnect (self->stream, self->sig_close); g_signal_handler_disconnect (self->stream, self->sig_rejected_cert); cockpit_http_client_checkin (self->client, self->stream); g_object_unref (self->stream); self->stream = NULL; } COCKPIT_CHANNEL_CLASS (cockpit_http_stream_parent_class)->close (channel, NULL); } else if (self->state != FINISHED) { g_warn_if_reached (); self->failed = TRUE; self->state = FINISHED; COCKPIT_CHANNEL_CLASS (cockpit_http_stream_parent_class)->close (channel, "internal-error"); } } static void cockpit_http_stream_init (CockpitHttpStream *self) { self->response_length = -1; self->keep_alive = FALSE; self->state = BUFFER_REQUEST; } static void cockpit_http_stream_prepare (CockpitChannel *channel) { CockpitHttpStream *self = COCKPIT_HTTP_STREAM (channel); CockpitConnectable *connectable = NULL; const gchar *payload; const gchar *connection; JsonObject *options; const gchar *path; COCKPIT_CHANNEL_CLASS (cockpit_http_stream_parent_class)->prepare (channel); if (self->failed) goto out; options = cockpit_channel_get_options (channel); if (!cockpit_json_get_string (options, "connection", NULL, &connection)) { cockpit_channel_fail (channel, "protocol-error", "bad \"connection\" field in HTTP stream request"); goto out; } if (!cockpit_json_get_string (options, "path", "/", &path)) { cockpit_channel_fail (channel, "protocol-error", "bad \"path\" field in HTTP stream request"); goto out; } /* * In http-stream1 the headers are sent as first message. * In http-stream2 the headers are in a control message. */ if (cockpit_json_get_string (options, "payload", NULL, &payload) && payload && g_str_equal (payload, "http-stream1")) { self->headers_inline = TRUE; } self->client = cockpit_http_client_ensure (connection); if (!self->client->connectable || json_object_has_member (options, "unix") || json_object_has_member (options, "port") || json_object_has_member (options, "internal") || json_object_has_member (options, "tls") || json_object_has_member (options, "address")) { connectable = cockpit_channel_parse_stream (channel); if (!connectable) goto out; if (self->client->connectable) cockpit_connectable_unref (self->client->connectable); self->client->connectable = cockpit_connectable_ref (connectable); } self->name = g_strdup_printf ("%s://%s%s", self->client->connectable->tls ? "https" : "http", self->client->connectable->name, path); self->stream = cockpit_http_client_checkout (self->client); if (!self->stream) { self->stream = cockpit_stream_connect (self->name, self->client->connectable); self->sig_open = g_signal_connect (self->stream, "open", G_CALLBACK (on_stream_open), self); } /* Parsed elsewhere */ self->binary = json_object_has_member (options, "binary"); self->sig_read = g_signal_connect (self->stream, "read", G_CALLBACK (on_stream_read), self); self->sig_close = g_signal_connect (self->stream, "close", G_CALLBACK (on_stream_close), self); self->sig_rejected_cert = g_signal_connect (self->stream, "rejected-cert", G_CALLBACK (on_rejected_cert), self); /* If not waiting for open */ if (!self->sig_open) cockpit_channel_ready (channel, NULL); out: if (connectable) cockpit_connectable_unref (connectable); } static void cockpit_http_stream_dispose (GObject *object) { CockpitHttpStream *self = COCKPIT_HTTP_STREAM (object); if (self->stream) { if (self->sig_open) g_signal_handler_disconnect (self->stream, self->sig_open); g_signal_handler_disconnect (self->stream, self->sig_read); g_signal_handler_disconnect (self->stream, self->sig_close); g_signal_handler_disconnect (self->stream, self->sig_rejected_cert); cockpit_stream_close (self->stream, NULL); g_object_unref (self->stream); } g_list_free_full (self->request, (GDestroyNotify)g_bytes_unref); self->request = NULL; G_OBJECT_CLASS (cockpit_http_stream_parent_class)->dispose (object); } static void cockpit_http_stream_finalize (GObject *object) { CockpitHttpStream *self = COCKPIT_HTTP_STREAM (object); g_free (self->name); if (self->client) cockpit_http_client_unref (self->client); G_OBJECT_CLASS (cockpit_http_stream_parent_class)->finalize (object); } static void cockpit_http_stream_constructed (GObject *object) { const gchar *caps[] = { "tls-certificates", "address", NULL }; G_OBJECT_CLASS (cockpit_http_stream_parent_class)->constructed (object); g_object_set (object, "capabilities", &caps, NULL); } static void cockpit_http_stream_class_init (CockpitHttpStreamClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass); gobject_class->dispose = cockpit_http_stream_dispose; gobject_class->finalize = cockpit_http_stream_finalize; gobject_class->constructed = cockpit_http_stream_constructed; channel_class->prepare = cockpit_http_stream_prepare; channel_class->control = cockpit_http_stream_control; channel_class->recv = cockpit_http_stream_recv; channel_class->close = cockpit_http_stream_close; }