static void send_login_html (CockpitWebResponse *response, CockpitHandlerData *ws) { static const gchar *marker = "<head>"; CockpitWebFilter *filter; GBytes *environment; GError *error = NULL; GBytes *bytes; GBytes *url_bytes = NULL; CockpitWebFilter *filter2 = NULL; const gchar *url_root = NULL; gchar *base; 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 ("<base href=\"%s/\">", url_root); else base = g_strdup ("<base href=\"/\">"); 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); 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 <script> and <style> tags. */ cockpit_web_response_headers (response, 200, "OK", -1, "Content-Security-Policy", "default-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:", NULL); if (cockpit_web_response_queue (response, bytes)) cockpit_web_response_complete (response); g_bytes_unref (bytes); } }
static void handle_resource (CockpitHandlerData *data, CockpitWebService *service, const gchar *path, GHashTable *headers, CockpitWebResponse *response) { gchar *where; where = cockpit_web_response_pop_path (response); if (where && (where[0] == '@' || where[0] == '$') && where[1] != '\0') { if (service) { cockpit_channel_response_serve (service, headers, response, where, cockpit_web_response_get_path (response)); } else if (g_str_has_suffix (path, ".html")) { send_login_html (response, data); } else { cockpit_web_response_error (response, 401, NULL, NULL); } } else { cockpit_web_response_error (response, 404, NULL, NULL); } g_free (where); }
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_disposition; /* Parse the external */ if (!cockpit_web_service_parse_external (open, &content_type, &content_disposition, NULL)) { cockpit_web_response_error (response, 400, NULL, "Bad channel request"); return; } transport = cockpit_web_service_ensure_transport (service, open); 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)); /* 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); }
/** * cockpit_web_response_error: * @self: the response * @headers: headers to include or NULL * @error: the error * * Send an error message with a basic HTML page containing * the error. */ void cockpit_web_response_gerror (CockpitWebResponse *self, GHashTable *headers, GError *error) { int code; g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self)); if (g_error_matches (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED)) code = 401; else if (g_error_matches (error, COCKPIT_ERROR, COCKPIT_ERROR_PERMISSION_DENIED)) code = 403; else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA)) code = 400; else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NO_SPACE)) code = 413; else code = 500; cockpit_web_response_error (self, code, headers, "%s", error->message); }
gboolean cockpit_handler_deauthorize (CockpitWebServer *server, CockpitWebServerRequestType reqtype, const gchar *path, GHashTable *headers, GBytes *input, CockpitWebResponse *response, CockpitHandlerData *ws) { CockpitWebService *service; CockpitCreds *creds; const gchar *body; GBytes *bytes; service = cockpit_auth_check_cookie (ws->auth, headers); if (!service) { cockpit_web_response_error (response, 401, NULL, "Unauthorized"); return TRUE; } /* Poison the creds, so they no longer work for new reauthorization */ creds = cockpit_web_service_get_creds (service); cockpit_creds_poison (creds); g_object_unref (service); body ="<html><head><title>Deauthorized</title></head>" "<body>Deauthorized</body></html>"; bytes = g_bytes_new_static (body, strlen (body)); cockpit_web_response_content (response, NULL, bytes, NULL); g_bytes_unref (bytes); return TRUE; }
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); }
gboolean cockpit_handler_resource (CockpitWebServer *server, const gchar *path, GHashTable *headers, CockpitWebResponse *response, CockpitHandlerData *ws) { CockpitWebService *service; if (g_str_has_prefix (path, "/cockpit/static/")) { cockpit_web_response_file (response, path + 16, TRUE, ws->static_roots); return TRUE; } service = cockpit_auth_check_cookie (ws->auth, headers); if (service) { cockpit_web_service_resource (service, headers, response); g_object_unref (service); } else if (g_str_equal (path, "/") || g_str_has_suffix (path, ".html")) { send_login_html (response, ws); } else { cockpit_web_response_error (response, 401, NULL, NULL); } return TRUE; }
static void handle_shell (CockpitHandlerData *data, CockpitWebService *service, const gchar *path, GHashTable *headers, CockpitWebResponse *response) { gboolean valid; const gchar *shell_path; /* Check if a valid path for a shell to be served at */ valid = g_str_equal (path, "/") || g_str_has_prefix (path, "/@") || strspn (path + 1, COCKPIT_RESOURCE_PACKAGE_VALID) == strcspn (path + 1, "/"); if (g_str_has_prefix (path, "/@/") || g_str_has_prefix (path, "//")) valid = FALSE; if (!valid) { cockpit_web_response_error (response, 404, NULL, NULL); } else if (service) { shell_path = cockpit_conf_string ("WebService", "Shell"); cockpit_channel_response_serve (service, headers, response, NULL, shell_path ? shell_path : cockpit_ws_shell_component); } else { send_login_html (response, data); } }
gboolean cockpit_handler_default (CockpitWebServer *server, const gchar *path, GHashTable *headers, CockpitWebResponse *response, CockpitHandlerData *data) { CockpitWebService *service; const gchar *remainder = NULL; gboolean resource; path = cockpit_web_response_get_path (response); g_return_val_if_fail (path != NULL, FALSE); resource = g_str_has_prefix (path, "/cockpit/") || g_str_has_prefix (path, "/cockpit+") || g_str_equal (path, "/cockpit"); // Check for auth service = cockpit_auth_check_cookie (data->auth, path, headers); /* Stuff in /cockpit or /cockpit+xxx */ if (resource) { cockpit_web_response_skip_path (response); remainder = cockpit_web_response_get_path (response); if (!remainder) { cockpit_web_response_error (response, 404, NULL, NULL); return TRUE; } else if (g_str_has_prefix (remainder, "/static/")) { cockpit_branding_serve (service, response, path, remainder + 8, data->os_release, data->branding_roots); return TRUE; } } if (resource) { if (g_str_equal (remainder, "/login")) { handle_login (data, service, path, headers, response); } else { handle_resource (data, service, path, headers, response); } } else { handle_shell (data, service, path, headers, response); } 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 cockpit_web_server_default_handle_resource (CockpitWebServer *self, const gchar *path, GHashTable *headers, CockpitWebResponse *response) { if (self->document_roots) cockpit_web_response_file (response, path, FALSE, (const gchar **)self->document_roots); else cockpit_web_response_error (response, 404, NULL, NULL); return TRUE; }
static void send_login_html (CockpitWebResponse *response, CockpitHandlerData *ws) { GHashTable *headers = NULL; GList *l, *output = NULL; gchar *login_html; GMappedFile *file; GError *error = NULL; GBytes *body = NULL; gsize length; login_html = g_build_filename (ws->static_roots[0], "login.html", NULL); file = g_mapped_file_new (login_html, FALSE, &error); if (file == NULL) { g_warning ("%s: %s", login_html, error->message); cockpit_web_response_error (response, 500, NULL, NULL); g_clear_error (&error); goto out; } body = g_mapped_file_get_bytes (file); output = cockpit_template_expand (body, substitute_environment, ws->os_release); length = 0; for (l = output; l != NULL; l = g_list_next (l)) length += g_bytes_get_size (l->data); headers = cockpit_web_server_new_table (); g_hash_table_insert (headers, g_strdup ("Content-Type"), g_strdup ("text/html; charset=utf8")); cockpit_web_response_headers_full (response, 200, "OK", length, headers); for (l = output; l != NULL; l = g_list_next (l)) { if (!cockpit_web_response_queue (response, l->data)) break; } if (l == NULL) cockpit_web_response_complete (response); out: g_list_free_full (output, (GDestroyNotify)g_bytes_unref); if (headers) g_hash_table_unref (headers); g_free (login_html); if (body) g_bytes_unref (body); if (file) g_mapped_file_unref (file); }
static void respond_with_error (const gchar *original_path, const gchar *path, GIOStream *io_stream, GHashTable *headers, guint status, const gchar *message) { CockpitWebResponse *response; response = cockpit_web_response_new (io_stream, original_path, path, NULL, headers); cockpit_web_response_error (response, status, NULL, "%s", message); g_object_unref (response); }
static void respond_with_error (const gchar *original_path, const gchar *path, GIOStream *io_stream, gboolean for_tls_proxy, GHashTable *headers, guint status, const gchar *message) { CockpitWebResponse *response; response = cockpit_web_response_new (io_stream, original_path, path, NULL, headers, for_tls_proxy ? COCKPIT_WEB_RESPONSE_FOR_TLS_PROXY : COCKPIT_WEB_RESPONSE_NONE); cockpit_web_response_error (response, status, NULL, "%s", message); g_object_unref (response); }
gboolean cockpit_handler_login (CockpitWebServer *server, CockpitWebServerRequestType reqtype, const gchar *path, GHashTable *headers, GBytes *input, CockpitWebResponse *response, CockpitHandlerData *ws) { CockpitWebService *service; gchar *remote_peer = NULL; GIOStream *io_stream; LoginResponse *lr; lr = g_new0 (LoginResponse, 1); lr->response = g_object_ref (response); lr->headers = cockpit_web_server_new_table (); if (reqtype == COCKPIT_WEB_SERVER_REQUEST_GET) { service = cockpit_auth_check_cookie (ws->auth, headers); if (service == NULL) { cockpit_web_response_error (response, 401, NULL, NULL); login_response_free (lr); } else { cockpit_web_service_modules (service, "localhost", on_login_modules, lr); g_object_unref (service); /* no response yet */ } } else if (reqtype == COCKPIT_WEB_SERVER_REQUEST_POST) { io_stream = cockpit_web_response_get_stream (response); remote_peer = get_remote_address (io_stream); cockpit_auth_login_async (ws->auth, headers, input, remote_peer, on_login_complete, lr); g_free (remote_peer); /* no response yet */ } return TRUE; }
static void process_delayed_reply (CockpitRequest *request, const gchar *path, GHashTable *headers) { CockpitWebResponse *response; const gchar *host; const gchar *body; GBytes *bytes; gsize length; gchar *url; g_assert (request->delayed_reply > 299); response = cockpit_web_response_new (request->io, NULL, NULL, headers); g_signal_connect_data (response, "done", G_CALLBACK (on_web_response_done), g_object_ref (request->web_server), (GClosureNotify)g_object_unref, 0); if (request->delayed_reply == 301) { body = "<html><head><title>Moved</title></head>" "<body>Please use TLS</body></html>"; host = g_hash_table_lookup (headers, "Host"); url = g_strdup_printf ("https://%s%s", host != NULL ? host : "", path); length = strlen (body); cockpit_web_response_headers (response, 301, "Moved Permanently", length, "Content-Type", "text/html", "Location", url, NULL); g_free (url); bytes = g_bytes_new_static (body, length); if (cockpit_web_response_queue (response, bytes)) cockpit_web_response_complete (response); g_bytes_unref (bytes); } else { cockpit_web_response_error (response, request->delayed_reply, NULL, NULL); } g_object_unref (response); }
static gboolean mock_http_qs (CockpitWebResponse *response) { const gchar *qs; GBytes *bytes; qs = cockpit_web_response_get_query (response); if (!qs) { cockpit_web_response_error (response, 400, NULL, "No query string"); } else { bytes = g_bytes_new (qs, strlen (qs)); cockpit_web_response_content (response, NULL, bytes, NULL); g_bytes_unref (bytes); } return TRUE; }
gboolean cockpit_handler_resource (CockpitWebService *server, CockpitWebServerRequestType reqtype, const gchar *path, GHashTable *headers, GBytes *input, CockpitWebResponse *response, CockpitHandlerData *ws) { CockpitWebService *service; service = cockpit_auth_check_cookie (ws->auth, headers); if (service) { cockpit_web_service_resource (service, response); g_object_unref (service); } else { cockpit_web_response_error (response, 401, NULL, NULL); } return TRUE; }
/** * cockpit_web_response_file: * @response: the response * @path: escaped path, or NULL to get from response * @roots: directories to look for file in * * Serve a file from disk as an HTTP response. */ void cockpit_web_response_file (CockpitWebResponse *response, const gchar *escaped, gboolean cache_forever, const gchar **roots) { const gchar *cache_control; GError *error = NULL; gchar *unescaped = NULL; gchar *path = NULL; GMappedFile *file = NULL; const gchar *root; GBytes *body; g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (response)); if (!escaped) escaped = cockpit_web_response_get_path (response); g_return_if_fail (escaped != NULL); /* Someone is trying to escape the root directory, or access hidden files? */ unescaped = g_uri_unescape_string (escaped, NULL); if (strstr (unescaped, "/.") || strstr (unescaped, "../") || strstr (unescaped, "//")) { g_debug ("%s: invalid path request", escaped); cockpit_web_response_error (response, 404, NULL, "Not Found"); goto out; } again: root = *(roots++); if (root == NULL) { cockpit_web_response_error (response, 404, NULL, "Not Found"); goto out; } g_free (path); path = g_build_filename (root, unescaped, NULL); if (g_file_test (path, G_FILE_TEST_IS_DIR)) { cockpit_web_response_error (response, 403, NULL, "Directory Listing Denied"); goto out; } /* As a double check of above behavior */ g_assert (path_has_prefix (path, root)); g_clear_error (&error); file = g_mapped_file_new (path, FALSE, &error); if (file == NULL) { if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT) || g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NAMETOOLONG)) { g_debug ("%s: file not found in root: %s", escaped, root); goto again; } else if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_PERM) || g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_ACCES) || g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_ISDIR)) { cockpit_web_response_error (response, 403, NULL, "Access denied"); goto out; } else { g_warning ("%s: %s", path, error->message); cockpit_web_response_error (response, 500, NULL, "Internal server error"); goto out; } } body = g_mapped_file_get_bytes (file); cache_control = cache_forever ? "max-age=31556926, public" : NULL; cockpit_web_response_headers (response, 200, "OK", g_bytes_get_size (body), "Cache-Control", cache_control, NULL); if (cockpit_web_response_queue (response, body)) cockpit_web_response_complete (response); g_bytes_unref (body); out: g_free (unescaped); g_clear_error (&error); g_free (path); if (file) g_mapped_file_unref (file); }
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; const gchar *checksum; JsonObject *object = NULL; JsonObject *heads; gchar *channel = NULL; gchar *language = 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); if (where == NULL) { host = "localhost"; } else if (where[0] == '@') { host = where + 1; } else if (where[0] == '$') { quoted_etag = g_strdup_printf ("\"%s\"", where); 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"), where) == 0 || 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; } transport = cockpit_web_service_find_transport (service, where + 1); if (!transport) goto out; host = cockpit_web_service_get_host (service, transport); if (!host) { g_warn_if_reached (); goto out; } } else { 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); if (!transport) { transport = cockpit_web_service_ensure_transport (service, object); if (!transport) goto out; } if (where) { /* * Maybe send back a redirect to the checksum url. We only do this if actually * accessing a file, and not a some sort of data like '/checksum', or a root path * like '/' */ if (where[0] == '@' && strchr (path, '.')) { checksum = cockpit_web_service_get_checksum (service, transport); if (checksum) { handled = redirect_to_checksum_path (service, response, checksum, path); 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, "Host") == 0 || 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) continue; json_object_set_string_member (heads, key, value); g_free (val); } /* Parse the language out of the CockpitLang cookie */ language = cockpit_web_server_parse_cookie (in_headers, "CockpitLang"); if (language) json_object_set_string_member (heads, "Accept-Language", language); json_object_set_string_member (heads, "Host", 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); if (!where) chesp->inject = cockpit_channel_inject_new (service, path); handled = TRUE; out: g_free (language); 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); }
/** * cockpit_web_response_file: * @response: the response * @path: escaped path, or NULL to get from response * @roots: directories to look for file in * * Serve a file from disk as an HTTP response. */ void cockpit_web_response_file (CockpitWebResponse *response, const gchar *escaped, gboolean cache_forever, const gchar **roots) { const gchar *cache_control; GError *error = NULL; gchar *unescaped; char *path = NULL; gchar *built = NULL; GMappedFile *file = NULL; const gchar *root; GBytes *body; g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (response)); if (!escaped) escaped = cockpit_web_response_get_path (response); g_return_if_fail (escaped != NULL); again: root = *(roots++); if (root == NULL) { cockpit_web_response_error (response, 404, NULL, "Not Found"); goto out; } unescaped = g_uri_unescape_string (escaped, NULL); built = g_build_filename (root, unescaped, NULL); g_free (unescaped); path = realpath (built, NULL); g_free (built); if (path == NULL) { if (errno == ENOENT || errno == ENOTDIR || errno == ELOOP || errno == ENAMETOOLONG) { g_debug ("%s: file not found in root: %s", escaped, root); goto again; } else if (errno == EACCES) { cockpit_web_response_error (response, 403, NULL, "Access Denied"); goto out; } else { g_warning ("%s: resolving path failed: %m", escaped); cockpit_web_response_error (response, 500, NULL, "Internal Server Error"); goto out; } } /* Double check that realpath() did the right thing */ g_return_if_fail (strstr (path, "../") == NULL); g_return_if_fail (!g_str_has_suffix (path, "/..")); /* Someone is trying to escape the root directory */ if (!path_has_prefix (path, root) && !path_has_prefix (path, cockpit_web_exception_escape_root)) { g_debug ("%s: request tried to escape the root directory: %s: %s", escaped, root, path); cockpit_web_response_error (response, 404, NULL, "Not Found"); goto out; } if (g_file_test (path, G_FILE_TEST_IS_DIR)) { cockpit_web_response_error (response, 403, NULL, "Directory Listing Denied"); goto out; } file = g_mapped_file_new (path, FALSE, &error); if (file == NULL) { if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_PERM) || g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_ACCES) || g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_ISDIR)) { cockpit_web_response_error (response, 403, NULL, "Access denied"); g_clear_error (&error); goto out; } else { g_warning ("%s: %s", path, error->message); cockpit_web_response_error (response, 500, NULL, "Internal server error"); g_clear_error (&error); goto out; } } body = g_mapped_file_get_bytes (file); cache_control = cache_forever ? "max-age=31556926, public" : NULL; cockpit_web_response_headers (response, 200, "OK", g_bytes_get_size (body), "Cache-Control", cache_control, NULL); if (cockpit_web_response_queue (response, body)) cockpit_web_response_complete (response); g_bytes_unref (body); out: free (path); if (file) g_mapped_file_unref (file); }
static void send_login_html (CockpitWebResponse *response, CockpitHandlerData *ws, GHashTable *headers) { static const gchar *marker = "<meta insert_dynamic_content_here>"; 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 ("<base href=\"%s/\">", url_root); else base = g_strdup ("<base href=\"/\">"); 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 <script> and <style> tags. */ cockpit_web_response_headers (response, 200, "OK", -1, "Content-Type", "text/html", "Content-Security-Policy", "default-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:", NULL); if (cockpit_web_response_queue (response, bytes)) cockpit_web_response_complete (response); g_bytes_unref (bytes); } g_strfreev (languages); }
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_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_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 void send_index_response (CockpitWebResponse *response, CockpitWebService *service, JsonObject *modules) { GHashTable *out_headers; GError *error = NULL; GMappedFile *file = NULL; GBytes *body = NULL; GBytes *prefix = NULL; GBytes *environ = NULL; GBytes *suffix = NULL; gchar *index_html; const gchar *needle; const gchar *data; const gchar *pos; gsize needle_len; gsize length; gsize offset; /* * Since the index file cannot be properly cached, it can change on * each request, so we include full environment information directly * rather than making the client do another round trip later. * * If the caller is already logged in, then this is included in the * environment. */ index_html = g_build_filename (cockpit_ws_static_directory, "index.html", NULL); file = g_mapped_file_new (index_html, FALSE, &error); if (file == NULL) { g_warning ("%s: %s", index_html, error->message); cockpit_web_response_error (response, 500, NULL, NULL); g_clear_error (&error); goto out; } body = g_mapped_file_get_bytes (file); data = g_bytes_get_data (body, &length); needle = "cockpit_environment_info"; pos = g_strstr_len (data, length, needle); if (!pos) { g_warning ("couldn't find 'cockpit_environment_info' string in index.html"); cockpit_web_response_error (response, 500, NULL, NULL); goto out; } environ = build_environment (service, modules); offset = (pos - data); prefix = g_bytes_new_from_bytes (body, 0, offset); needle_len = strlen (needle); suffix = g_bytes_new_from_bytes (body, offset + needle_len, length - (offset + needle_len)); out_headers = cockpit_web_server_new_table (); g_hash_table_insert (out_headers, g_strdup ("Content-Type"), g_strdup ("text/html; charset=utf8")); cockpit_web_response_content (response, out_headers, prefix, environ, suffix, NULL); g_hash_table_unref (out_headers); out: g_free (index_html); if (prefix) g_bytes_unref (prefix); if (body) g_bytes_unref (body); if (environ) g_bytes_unref (environ); if (suffix) g_bytes_unref (suffix); if (file) g_mapped_file_unref (file); }
gboolean cockpit_handler_external (CockpitWebServer *server, const gchar *original_path, const gchar *path, 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_channel_response_open (service, headers, response, open); g_object_unref (response); } json_object_unref (open); } g_object_unref (service); return TRUE; }
/** * cockpit_web_response_file: * @response: the response * @path: escaped path, or NULL to get from response * @roots: directories to look for file in * * Serve a file from disk as an HTTP response. */ void cockpit_web_response_file (CockpitWebResponse *response, const gchar *escaped, const gchar **roots) { const gchar *csp_header; GError *error = NULL; gchar *unescaped = NULL; gchar *path = NULL; GMappedFile *file = NULL; const gchar *root; GBytes *body; g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (response)); if (!escaped) escaped = cockpit_web_response_get_path (response); g_return_if_fail (escaped != NULL); /* Someone is trying to escape the root directory, or access hidden files? */ unescaped = g_uri_unescape_string (escaped, NULL); if (strstr (unescaped, "/.") || strstr (unescaped, "../") || strstr (unescaped, "//")) { g_debug ("%s: invalid path request", escaped); cockpit_web_response_error (response, 404, NULL, "Not Found"); goto out; } again: root = *(roots++); if (root == NULL) { cockpit_web_response_error (response, 404, NULL, "Not Found"); goto out; } g_free (path); path = g_build_filename (root, unescaped, NULL); if (g_file_test (path, G_FILE_TEST_IS_DIR)) { cockpit_web_response_error (response, 403, NULL, "Directory Listing Denied"); goto out; } /* As a double check of above behavior */ g_assert (path_has_prefix (path, root)); g_clear_error (&error); file = g_mapped_file_new (path, FALSE, &error); if (file == NULL) { if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT) || g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NAMETOOLONG)) { g_debug ("%s: file not found in root: %s", escaped, root); goto again; } else if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_PERM) || g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_ACCES) || g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_ISDIR)) { cockpit_web_response_error (response, 403, NULL, "Access denied"); goto out; } else { g_warning ("%s: %s", path, error->message); cockpit_web_response_error (response, 500, NULL, "Internal server error"); goto out; } } body = g_mapped_file_get_bytes (file); /* * The default Content-Security-Policy for .html files allows * the site to have inline <script> and <style> tags. This code * is not used when serving resources once logged in, only for * static resources when we don't yet have a session. */ csp_header = NULL; if (g_str_has_suffix (unescaped, ".html")) csp_header = "Content-Security-Policy"; cockpit_web_response_headers (response, 200, "OK", g_bytes_get_size (body), csp_header, "default-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:", NULL); if (cockpit_web_response_queue (response, body)) cockpit_web_response_complete (response); g_bytes_unref (body); out: g_free (unescaped); g_clear_error (&error); g_free (path); if (file) g_mapped_file_unref (file); }