/* * This file is part of Cockpit. * * Copyright (C) 2015 Red Hat, Inc. * * Cockpit is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * Cockpit is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Cockpit; If not, see . */ #include "config.h" #include "cockpitchannelresponse.h" #include "common/cockpitwebinject.h" #include "common/cockpitwebserver.h" #include "common/cockpitwebresponse.h" #include typedef struct { CockpitWebService *service; gchar *base_path; gchar *host; } CockpitChannelInject; static void cockpit_channel_inject_free (gpointer data) { CockpitChannelInject *inject = data; if (inject) { if (inject->service) g_object_remove_weak_pointer (G_OBJECT (inject->service), (gpointer *)&inject->service); g_free (inject->base_path); g_free (inject->host); g_free (inject); } } static CockpitChannelInject * cockpit_channel_inject_new (CockpitWebService *service, const gchar *path, const gchar *host) { CockpitChannelInject *inject = g_new (CockpitChannelInject, 1); inject->service = service; g_object_add_weak_pointer (G_OBJECT (inject->service), (gpointer *)&inject->service); inject->base_path = g_strdup (path); inject->host = g_strdup (host); return inject; } static void cockpit_channel_inject_update_checksum (CockpitChannelInject *inject, GHashTable *headers) { const gchar *checksum = g_hash_table_lookup (headers, COCKPIT_CHECKSUM_HEADER); if (checksum) cockpit_web_service_set_host_checksum (inject->service, inject->host, checksum); /* No need to send our custom header outside of cockpit */ g_hash_table_remove (headers, COCKPIT_CHECKSUM_HEADER); } static void cockpit_channel_inject_perform (CockpitChannelInject *inject, CockpitWebResponse *response, CockpitTransport *transport) { static const gchar *marker = ""; CockpitWebFilter *filter; CockpitCreds *creds; gchar *prefixed_application = NULL; const gchar *checksum; GString *str; GBytes *base; if (!inject->base_path) return; str = g_string_new (""); creds = cockpit_web_service_get_creds (inject->service); if (cockpit_web_response_get_url_root (response)) { prefixed_application = g_strdup_printf ("%s/%s", cockpit_web_response_get_url_root (response), cockpit_creds_get_application (creds)); } else { prefixed_application = g_strdup_printf ("/%s", cockpit_creds_get_application (creds)); } checksum = cockpit_web_service_get_checksum (inject->service, inject->host); if (checksum) { g_string_printf (str, "\n ", prefixed_application, checksum, inject->base_path); } else { g_string_printf (str, "\n ", prefixed_application, inject->host, inject->base_path); } base = g_string_free_to_bytes (str); filter = cockpit_web_inject_new (marker, base, 1); g_bytes_unref (base); cockpit_web_response_add_filter (response, filter); g_object_unref (filter); g_free (prefixed_application); } typedef struct { const gchar *logname; gchar *channel; JsonObject *open; CockpitWebResponse *response; GHashTable *headers; CockpitTransport *transport; gulong transport_recv; gulong transport_control; gulong transport_closed; /* Set when injecting data into response */ CockpitChannelInject *inject; } CockpitChannelResponse; static gboolean ensure_headers (CockpitChannelResponse *chesp, guint status, const gchar *reason) { if (cockpit_web_response_get_state (chesp->response) == COCKPIT_WEB_RESPONSE_READY) { if (chesp->inject && chesp->inject->service) { cockpit_channel_inject_update_checksum (chesp->inject, chesp->headers); cockpit_channel_inject_perform (chesp->inject, chesp->response, chesp->transport); } cockpit_web_response_headers_full (chesp->response, status, reason, -1, chesp->headers); return TRUE; } return FALSE; } static void cockpit_channel_response_close (CockpitChannelResponse *chesp, const gchar *problem) { CockpitWebResponding state; /* Ensure no more signals arrive about our response */ g_signal_handler_disconnect (chesp->transport, chesp->transport_recv); g_signal_handler_disconnect (chesp->transport, chesp->transport_control); g_signal_handler_disconnect (chesp->transport, chesp->transport_closed); /* The web response should not yet be complete */ state = cockpit_web_response_get_state (chesp->response); if (problem == NULL) { /* Closed without any data */ if (state == COCKPIT_WEB_RESPONSE_READY) { ensure_headers (chesp, 204, "OK"); cockpit_web_response_complete (chesp->response); g_debug ("%s: no content in external channel", chesp->logname); } else if (state < COCKPIT_WEB_RESPONSE_COMPLETE) { g_message ("%s: truncated data in external channel", chesp->logname); cockpit_web_response_abort (chesp->response); } else { g_debug ("%s: completed serving external channel", chesp->logname); } } else if (state == COCKPIT_WEB_RESPONSE_READY) { if (g_str_equal (problem, "not-found")) { g_debug ("%s: not found", chesp->logname); cockpit_web_response_error (chesp->response, 404, NULL, NULL); } else if (g_str_equal (problem, "no-host") || g_str_equal (problem, "no-cockpit") || g_str_equal (problem, "unknown-hostkey") || g_str_equal (problem, "authentication-failed") || g_str_equal (problem, "disconnected")) { g_debug ("%s: remote server unavailable: %s", chesp->logname, problem); cockpit_web_response_error (chesp->response, 502, NULL, NULL); } else { g_message ("%s: external channel failed: %s", chesp->logname, problem); cockpit_web_response_error (chesp->response, 500, NULL, NULL); } } else { if (g_str_equal (problem, "disconnected") || g_str_equal (problem, "terminated")) g_debug ("%s: failure while serving external channel: %s", chesp->logname, problem); else g_message ("%s: failure while serving external channel: %s", chesp->logname, problem); cockpit_web_response_abort (chesp->response); } g_object_unref (chesp->response); g_object_unref (chesp->transport); g_hash_table_unref (chesp->headers); cockpit_channel_inject_free (chesp->inject); json_object_unref (chesp->open); g_free (chesp->channel); g_free (chesp); } static gboolean on_transport_recv (CockpitTransport *transport, const gchar *channel, GBytes *payload, CockpitChannelResponse *chesp) { if (channel && g_str_equal (channel, chesp->channel)) { ensure_headers (chesp, 200, "OK"); cockpit_web_response_queue (chesp->response, payload); return TRUE; } return FALSE; } static void object_to_headers (JsonObject *object, const gchar *header, JsonNode *node, gpointer user_data) { GHashTable *headers = user_data; const gchar *value = json_node_get_string (node); g_return_if_fail (value != NULL); if (g_ascii_strcasecmp (header, "Content-Length") == 0 || g_ascii_strcasecmp (header, "Connection") == 0) return; g_hash_table_insert (headers, g_strdup (header), g_strdup (value)); } static gboolean parse_httpstream_response (CockpitChannelResponse *chesp, JsonObject *object, gint64 *status, const gchar **reason) { JsonNode *node; if (!cockpit_json_get_int (object, "status", 200, status) || !cockpit_json_get_string (object, "reason", NULL, reason)) { g_warning ("%s: received invalid httpstream response", chesp->logname); return FALSE; } node = json_object_get_member (object, "headers"); if (node) { if (!JSON_NODE_HOLDS_OBJECT (node)) { g_warning ("%s: received invalid httpstream headers", chesp->logname); return FALSE; } json_object_foreach_member (json_node_get_object (node), object_to_headers, chesp->headers); } return TRUE; } static gboolean on_httpstream_recv (CockpitTransport *transport, const gchar *channel, GBytes *payload, CockpitChannelResponse *chesp) { GError *error = NULL; JsonObject *object; gint64 status; const gchar *reason; if (!channel || !g_str_equal (channel, chesp->channel)) return FALSE; g_return_val_if_fail (cockpit_web_response_get_state (chesp->response) == COCKPIT_WEB_RESPONSE_READY, FALSE); /* First response payload message is meta data, then switch to actual data */ g_signal_handler_disconnect (chesp->transport, chesp->transport_recv); chesp->transport_recv = g_signal_connect (chesp->transport, "recv", G_CALLBACK (on_transport_recv), chesp); object = cockpit_json_parse_bytes (payload, &error); if (error) { g_warning ("%s: couldn't parse http-stream1 header payload: %s", chesp->logname, error->message); cockpit_web_response_error (chesp->response, 500, NULL, NULL); g_error_free (error); return TRUE; } if (parse_httpstream_response (chesp, object, &status, &reason)) { if (!ensure_headers (chesp, status, reason)) g_return_val_if_reached (FALSE); } else { cockpit_web_response_error (chesp->response, 500, NULL, NULL); } json_object_unref (object); return TRUE; } static gboolean on_transport_control (CockpitTransport *transport, const gchar *command, const gchar *channel, JsonObject *options, GBytes *message, CockpitChannelResponse *chesp) { const gchar *problem = NULL; if (!channel || !g_str_equal (channel, chesp->channel)) return FALSE; /* not handled */ if (g_str_equal (command, "done")) { ensure_headers (chesp, 200, "OK"); cockpit_web_response_complete (chesp->response); return TRUE; } else if (g_str_equal (command, "close")) { if (!cockpit_json_get_string (options, "problem", NULL, &problem)) { g_message ("%s: received close command with invalid problem", chesp->logname); problem = "disconnected"; } cockpit_channel_response_close (chesp, problem); } else { /* Ignore other control messages */ } return TRUE; /* handled */ } static gboolean on_httpstream_control (CockpitTransport *transport, const gchar *command, const gchar *channel, JsonObject *options, GBytes *message, CockpitChannelResponse *chesp) { gint64 status; const gchar *reason; if (!channel || !g_str_equal (channel, chesp->channel)) return FALSE; /* not handled */ if (g_str_equal (command, "response")) { if (parse_httpstream_response (chesp, options, &status, &reason)) { if (!ensure_headers (chesp, status, reason)) g_return_val_if_reached (FALSE); } else { cockpit_web_response_error (chesp->response, 500, NULL, NULL); } return TRUE; } return on_transport_control (transport, command, channel, options, message, chesp); } static void on_transport_closed (CockpitTransport *transport, const gchar *problem, CockpitChannelResponse *chesp) { cockpit_channel_response_close (chesp, problem ? problem : "disconnected"); } static CockpitChannelResponse * cockpit_channel_response_create (CockpitWebService *service, CockpitWebResponse *response, CockpitTransport *transport, const gchar *logname, GHashTable *headers, JsonObject *open) { CockpitChannelResponse *chesp; const gchar *payload; JsonObject *done; GBytes *bytes; payload = json_object_get_string_member (open, "payload"); chesp = g_new0 (CockpitChannelResponse, 1); chesp->response = g_object_ref (response); chesp->transport = g_object_ref (transport); chesp->headers = g_hash_table_ref (headers); chesp->channel = cockpit_web_service_unique_channel (service); chesp->open = json_object_ref (open); if (!cockpit_json_get_string (open, "path", chesp->channel, &chesp->logname)) chesp->logname = chesp->channel; json_object_set_string_member (open, "command", "open"); json_object_set_string_member (open, "channel", chesp->channel); /* Special handling for http-stream1, splice in headers, handle injection */ if (g_strcmp0 (payload, "http-stream1") == 0) chesp->transport_recv = g_signal_connect (transport, "recv", G_CALLBACK (on_httpstream_recv), chesp); else chesp->transport_recv = g_signal_connect (transport, "recv", G_CALLBACK (on_transport_recv), chesp); /* Special handling for http-stream2, splice in headers, handle injection */ if (g_strcmp0 (payload, "http-stream2") == 0) chesp->transport_control = g_signal_connect (transport, "control", G_CALLBACK (on_httpstream_control), chesp); else chesp->transport_control = g_signal_connect (transport, "control", G_CALLBACK (on_transport_control), chesp); chesp->transport_closed = g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), chesp); bytes = cockpit_json_write_bytes (chesp->open); cockpit_transport_send (transport, NULL, bytes); g_bytes_unref (bytes); done = cockpit_transport_build_json ("command", "done", "channel", chesp->channel, NULL); bytes = cockpit_json_write_bytes (done); json_object_unref (done); cockpit_transport_send (transport, NULL, bytes); g_bytes_unref (bytes); return chesp; } static gboolean is_resource_a_package_file (const gchar *path) { return path && path[0] && strchr (path + 1, '/') != NULL; } static gboolean parse_host_and_etag (CockpitWebService *service, GHashTable *headers, const gchar *where, const gchar *path, const gchar **host, gchar **etag) { gchar **languages = NULL; gboolean translatable; gchar *language; /* Parse the language out of the CockpitLang cookie and set Accept-Language */ language = cockpit_web_server_parse_cookie (headers, "CockpitLang"); if (language) g_hash_table_replace (headers, g_strdup ("Accept-Language"), language); if (!where) { *host = "localhost"; *etag = NULL; return TRUE; } if (where[0] == '@') { *host = where + 1; *etag = NULL; return TRUE; } if (!where || where[0] != '$') return FALSE; *host = cockpit_web_service_get_host (service, where + 1); if (!*host) return FALSE; /* Top level resources (like the /manifests) are not translatable */ translatable = is_resource_a_package_file (path); /* The ETag contains the language setting */ if (translatable) { languages = cockpit_web_server_parse_languages (headers, "C"); *etag = g_strdup_printf ("\"%s-%s\"", where, languages[0]); g_strfreev (languages); } else { *etag = g_strdup_printf ("\"%s\"", where); } return TRUE; } void cockpit_channel_response_serve (CockpitWebService *service, GHashTable *in_headers, CockpitWebResponse *response, const gchar *where, const gchar *path) { CockpitChannelResponse *chesp = NULL; CockpitTransport *transport = NULL; CockpitCacheType cache_type = COCKPIT_WEB_RESPONSE_CACHE_PRIVATE; const gchar *host = NULL; const gchar *pragma; gchar *quoted_etag = NULL; GHashTable *out_headers = NULL; gchar *val = NULL; gboolean handled = FALSE; GHashTableIter iter; JsonObject *object = NULL; JsonObject *heads; GIOStream *connection; const gchar *protocol; const gchar *http_host = "localhost"; gchar *channel = NULL; gpointer key; gpointer value; g_return_if_fail (COCKPIT_IS_WEB_SERVICE (service)); g_return_if_fail (in_headers != NULL); g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (response)); g_return_if_fail (path != NULL); /* Where might be NULL, but that's still valid */ if (!parse_host_and_etag (service, in_headers, where, path, &host, "ed_etag)) { /* Did not recognize the where */ goto out; } if (quoted_etag) { cache_type = COCKPIT_WEB_RESPONSE_CACHE_FOREVER; pragma = g_hash_table_lookup (in_headers, "Pragma"); if ((!pragma || !strstr (pragma, "no-cache")) && g_strcmp0 (g_hash_table_lookup (in_headers, "If-None-Match"), quoted_etag) == 0) { cockpit_web_response_headers (response, 304, "Not Modified", 0, "ETag", quoted_etag, NULL); cockpit_web_response_complete (response); handled = TRUE; goto out; } } cockpit_web_response_set_cache_type (response, cache_type); object = cockpit_transport_build_json ("command", "open", "payload", "http-stream1", "internal", "packages", "method", "GET", "host", host, "path", path, "binary", "raw", NULL); transport = cockpit_web_service_get_transport (service); if (!transport) goto out; out_headers = cockpit_web_server_new_table (); channel = cockpit_web_service_unique_channel (service); json_object_set_string_member (object, "channel", channel); if (quoted_etag) { /* * If we have a checksum, then use it as an ETag. It is intentional that * a cockpit-bridge version could (in the future) override this. */ g_hash_table_insert (out_headers, g_strdup ("ETag"), quoted_etag); quoted_etag = NULL; } heads = json_object_new (); g_hash_table_iter_init (&iter, in_headers); while (g_hash_table_iter_next (&iter, &key, &value)) { val = NULL; if (g_ascii_strcasecmp (key, "Cookie") == 0 || g_ascii_strcasecmp (key, "Referer") == 0 || g_ascii_strcasecmp (key, "Connection") == 0 || g_ascii_strcasecmp (key, "Pragma") == 0 || g_ascii_strcasecmp (key, "Cache-Control") == 0 || g_ascii_strcasecmp (key, "User-Agent") == 0 || g_ascii_strcasecmp (key, "Accept-Charset") == 0 || g_ascii_strcasecmp (key, "Accept-Ranges") == 0 || g_ascii_strcasecmp (key, "Content-Length") == 0 || g_ascii_strcasecmp (key, "Content-MD5") == 0 || g_ascii_strcasecmp (key, "Content-Range") == 0 || g_ascii_strcasecmp (key, "Range") == 0 || g_ascii_strcasecmp (key, "TE") == 0 || g_ascii_strcasecmp (key, "Trailer") == 0 || g_ascii_strcasecmp (key, "Upgrade") == 0 || g_ascii_strcasecmp (key, "Transfer-Encoding") == 0 || g_ascii_strcasecmp (key, "X-Forwarded-For") == 0 || g_ascii_strcasecmp (key, "X-Forwarded-Host") == 0 || g_ascii_strcasecmp (key, "X-Forwarded-Protocol") == 0) continue; if (g_ascii_strcasecmp (key, "Host") == 0) http_host = (gchar *) value; else json_object_set_string_member (heads, key, value); g_free (val); } /* Send along the HTTP scheme the package should assume is accessing things */ connection = cockpit_web_response_get_stream (response); protocol = cockpit_web_response_get_protocol (connection, in_headers); json_object_set_string_member (heads, "Host", host); json_object_set_string_member (heads, "X-Forwarded-Proto", protocol); json_object_set_string_member (heads, "X-Forwarded-Host", http_host); json_object_set_object_member (object, "headers", heads); chesp = cockpit_channel_response_create (service, response, transport, cockpit_web_response_get_path (response), out_headers, object); chesp->inject = cockpit_channel_inject_new (service, where ? NULL : path, host); handled = TRUE; out: if (object) json_object_unref (object); g_free (quoted_etag); if (out_headers) g_hash_table_unref (out_headers); g_free (channel); if (!handled) cockpit_web_response_error (response, 404, NULL, NULL); } void cockpit_channel_response_open (CockpitWebService *service, GHashTable *in_headers, CockpitWebResponse *response, JsonObject *open) { CockpitTransport *transport; WebSocketDataType data_type; GHashTable *headers; const gchar *content_type; const gchar *content_encoding; const gchar *content_disposition; /* Parse the external */ if (!cockpit_web_service_parse_external (open, &content_type, &content_encoding, &content_disposition, NULL)) { cockpit_web_response_error (response, 400, NULL, "Bad channel request"); return; } transport = cockpit_web_service_get_transport (service); if (!transport) { cockpit_web_response_error (response, 502, NULL, "Failed to open channel transport"); return; } headers = cockpit_web_server_new_table (); if (content_disposition) g_hash_table_insert (headers, g_strdup ("Content-Disposition"), g_strdup (content_disposition)); if (!json_object_has_member (open, "binary")) json_object_set_string_member (open, "binary", "raw"); if (!content_type) { if (!cockpit_web_service_parse_binary (open, &data_type)) g_return_if_reached (); if (data_type == WEB_SOCKET_DATA_TEXT) content_type = "text/plain"; else content_type = "application/octet-stream"; } g_hash_table_insert (headers, g_strdup ("Content-Type"), g_strdup (content_type)); if (content_encoding) g_hash_table_insert (headers, g_strdup ("Content-Encoding"), g_strdup (content_encoding)); /* We shouldn't need to send this part further */ json_object_remove_member (open, "external"); cockpit_channel_response_create (service, response, transport, NULL, headers, open); g_hash_table_unref (headers); }