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