/* * This file is part of Cockpit. * * Copyright (C) 2013 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 "cockpithandlers.h" #include "cockpitbranding.h" #include "cockpitchannelresponse.h" #include "cockpitchannelsocket.h" #include "cockpitwebservice.h" #include "cockpitws.h" #include "common/cockpitconf.h" #include "common/cockpitjson.h" #include "common/cockpitwebinject.h" #include "websocket/websocket.h" #include #include #include #include /* For overriding during tests */ const gchar *cockpit_ws_shell_component = "/shell/index.html"; static void on_web_socket_noauth (WebSocketConnection *connection, gpointer data) { GBytes *payload; GBytes *prefix; g_debug ("closing unauthenticated web socket"); payload = cockpit_transport_build_control ("command", "init", "problem", "no-session", NULL); prefix = g_bytes_new_static ("\n", 1); web_socket_connection_send (connection, WEB_SOCKET_DATA_TEXT, prefix, payload); web_socket_connection_close (connection, WEB_SOCKET_CLOSE_GOING_AWAY, "no-session"); g_bytes_unref (prefix); g_bytes_unref (payload); } static void handle_noauth_socket (GIOStream *io_stream, const gchar *path, GHashTable *headers, GByteArray *input_buffer) { WebSocketConnection *connection; connection = cockpit_web_service_create_socket (NULL, path, io_stream, headers, input_buffer); g_signal_connect (connection, "open", G_CALLBACK (on_web_socket_noauth), NULL); /* Unreferences connection when it closes */ g_signal_connect (connection, "close", G_CALLBACK (g_object_unref), NULL); } /* Called by @server when handling HTTP requests to /cockpit/socket */ gboolean cockpit_handler_socket (CockpitWebServer *server, const gchar *original_path, const gchar *path, const gchar *method, GIOStream *io_stream, GHashTable *headers, GByteArray *input, CockpitHandlerData *ws) { CockpitWebService *service = NULL; const gchar *segment = NULL; /* * Socket requests should come in on /cockpit/socket or /cockpit+app/socket. * However older javascript may connect on /socket, so we continue to support that. */ if (path && path[0]) segment = strchr (path + 1, '/'); if (!segment) segment = path; if (!segment || !g_str_equal (segment, "/socket")) return FALSE; /* don't support HEAD on a socket, it makes little sense */ if (g_strcmp0 (method, "GET") != 0) return FALSE; if (headers) service = cockpit_auth_check_cookie (ws->auth, path, headers); if (service) { cockpit_web_service_socket (service, path, io_stream, headers, input); g_object_unref (service); } else { handle_noauth_socket (io_stream, path, headers, input); } return TRUE; } gboolean cockpit_handler_external (CockpitWebServer *server, const gchar *original_path, const gchar *path, const gchar *method, GIOStream *io_stream, GHashTable *headers, GByteArray *input, CockpitHandlerData *ws) { CockpitWebResponse *response = NULL; CockpitWebService *service = NULL; const gchar *segment = NULL; JsonObject *open = NULL; const gchar *query = NULL; CockpitCreds *creds; const gchar *expected; const gchar *upgrade; guchar *decoded; GBytes *bytes; gsize length; gsize seglen; /* The path must start with /cockpit+xxx/channel/csrftoken? or similar */ if (path && path[0]) segment = strchr (path + 1, '/'); if (!segment) return FALSE; if (!g_str_has_prefix (segment, "/channel/")) return FALSE; segment += 9; /* Make sure we are authenticated, otherwise 404 */ service = cockpit_auth_check_cookie (ws->auth, path, headers); if (!service) return FALSE; creds = cockpit_web_service_get_creds (service); g_return_val_if_fail (creds != NULL, FALSE); expected = cockpit_creds_get_csrf_token (creds); g_return_val_if_fail (expected != NULL, FALSE); /* The end of the token */ query = strchr (segment, '?'); if (query) { seglen = query - segment; query += 1; } else { seglen = strlen (segment); query = ""; } /* No such path is valid */ if (strlen (expected) != seglen || memcmp (expected, segment, seglen) != 0) { g_message ("invalid csrf token"); return FALSE; } decoded = g_base64_decode (query, &length); if (decoded) { bytes = g_bytes_new_take (decoded, length); if (!cockpit_transport_parse_command (bytes, NULL, NULL, &open)) { open = NULL; g_message ("invalid external channel query"); } g_bytes_unref (bytes); } if (!open) { response = cockpit_web_response_new (io_stream, original_path, path, NULL, headers); cockpit_web_response_error (response, 400, NULL, NULL); g_object_unref (response); } else { upgrade = g_hash_table_lookup (headers, "Upgrade"); if (upgrade && g_ascii_strcasecmp (upgrade, "websocket") == 0) { cockpit_channel_socket_open (service, open, original_path, path, io_stream, headers, input); } else { response = cockpit_web_response_new (io_stream, original_path, path, NULL, headers); cockpit_web_response_set_method (response, method); cockpit_channel_response_open (service, headers, response, open); g_object_unref (response); } json_object_unref (open); } g_object_unref (service); return TRUE; } static void add_oauth_to_environment (JsonObject *environment) { static const gchar *url; JsonObject *object; url = cockpit_conf_string ("OAuth", "URL"); if (url) { object = json_object_new (); json_object_set_string_member (object, "URL", url); json_object_set_string_member (object, "ErrorParam", cockpit_conf_string ("oauth", "ErrorParam")); json_object_set_string_member (object, "TokenParam", cockpit_conf_string ("oauth", "TokenParam")); json_object_set_object_member (environment, "OAuth", object); } } static void add_page_to_environment (JsonObject *object) { static gint page_login_to = -1; JsonObject *page; const gchar *value; page = json_object_new (); value = cockpit_conf_string ("WebService", "LoginTitle"); if (value) json_object_set_string_member (page, "title", value); if (page_login_to < 0) { page_login_to = cockpit_conf_bool ("WebService", "LoginTo", g_file_test (cockpit_ws_ssh_program, G_FILE_TEST_IS_EXECUTABLE)); } json_object_set_boolean_member (page, "connect", page_login_to); json_object_set_object_member (object, "page", page); } static GBytes * build_environment (GHashTable *os_release) { /* * We don't include entirety of os-release into the * environment for the login.html page. There could * be unexpected things in here. * * However since we are displaying branding based on * the OS name variant flavor and version, including * the corresponding information is not a leak. */ static const gchar *release_fields[] = { "NAME", "ID", "PRETTY_NAME", "VARIANT", "VARIANT_ID", "CPE_NAME", }; static const gchar *prefix = "\n "; GByteArray *buffer; GBytes *bytes; JsonObject *object; const gchar *value; gchar *hostname; JsonObject *osr; gint i; object = json_object_new (); add_page_to_environment (object); hostname = g_malloc0 (HOST_NAME_MAX + 1); gethostname (hostname, HOST_NAME_MAX); hostname[HOST_NAME_MAX] = '\0'; json_object_set_string_member (object, "hostname", hostname); g_free (hostname); if (os_release) { osr = json_object_new (); for (i = 0; i < G_N_ELEMENTS (release_fields); i++) { value = g_hash_table_lookup (os_release, release_fields[i]); if (value) json_object_set_string_member (osr, release_fields[i], value); } json_object_set_object_member (object, "os-release", osr); } add_oauth_to_environment (object); bytes = cockpit_json_write_bytes (object); json_object_unref (object); buffer = g_bytes_unref_to_array (bytes); g_byte_array_prepend (buffer, (const guint8 *)prefix, strlen (prefix)); g_byte_array_append (buffer, (const guint8 *)suffix, strlen (suffix)); return g_byte_array_free_to_bytes (buffer); } static void send_login_html (CockpitWebResponse *response, CockpitHandlerData *ws, GHashTable *headers) { static const gchar *marker = ""; CockpitWebFilter *filter; GBytes *environment; GError *error = NULL; GBytes *bytes; GBytes *url_bytes = NULL; CockpitWebFilter *filter2 = NULL; const gchar *url_root = NULL; gchar *base; gchar *language = NULL; gchar **languages = NULL; GBytes *po_bytes; CockpitWebFilter *filter3 = NULL; environment = build_environment (ws->os_release); filter = cockpit_web_inject_new (marker, environment, 1); g_bytes_unref (environment); cockpit_web_response_add_filter (response, filter); g_object_unref (filter); url_root = cockpit_web_response_get_url_root (response); if (url_root) base = g_strdup_printf ("", url_root); else base = g_strdup (""); url_bytes = g_bytes_new_take (base, strlen(base)); filter2 = cockpit_web_inject_new (marker, url_bytes, 1); g_bytes_unref (url_bytes); cockpit_web_response_add_filter (response, filter2); g_object_unref (filter2); cockpit_web_response_set_cache_type (response, COCKPIT_WEB_RESPONSE_NO_CACHE); if (ws->login_po_html) { language = cockpit_web_server_parse_cookie (headers, "CockpitLang"); if (!language) { languages = cockpit_web_server_parse_languages (headers, NULL); language = languages[0]; } po_bytes = cockpit_web_response_negotiation (ws->login_po_html, NULL, language, NULL, &error); if (error) { g_message ("%s", error->message); g_clear_error (&error); } else { filter3 = cockpit_web_inject_new (marker, po_bytes, 1); g_bytes_unref (po_bytes); cockpit_web_response_add_filter (response, filter3); g_object_unref (filter3); } } bytes = cockpit_web_response_negotiation (ws->login_html, NULL, NULL, NULL, &error); if (error) { g_message ("%s", error->message); cockpit_web_response_error (response, 500, NULL, NULL); g_error_free (error); } else if (!bytes) { cockpit_web_response_error (response, 404, NULL, NULL); } else { /* The login Content-Security-Policy allows the page to have inline