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