/** * cockpit_web_response_complete: * @self: the response * * See cockpit_web_response_content() for easy to use stuff. * * Tell the response that all the data has been queued. * The response will hold a reference to itself until the * data is actually sent, so you can unref it. */ void cockpit_web_response_complete (CockpitWebResponse *self) { GBytes *bytes; g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self)); g_return_if_fail (self->complete == FALSE); if (self->failed) return; /* Hold a reference until cockpit_web_response_done() */ g_object_ref (self); self->complete = TRUE; if (self->chunked) { bytes = g_bytes_new_static ("0\r\n\r\n", 5); queue_bytes (self, bytes); g_bytes_unref (bytes); } if (self->source) { g_debug ("%s: queueing complete", self->logname); } else { g_debug ("%s: complete closing io", self->logname); g_output_stream_flush_async (G_OUTPUT_STREAM (self->out), G_PRIORITY_DEFAULT, NULL, on_output_flushed, g_object_ref (self)); } }
/** * cockpit_web_response_headers: * @self: the response * @status: the HTTP status code * @reason: the HTTP reason * @length: the combined length of data blocks to follow, or -1 * @headers: headers to include or NULL * * See cockpit_web_response_content() for an easy to use function. * * Queue the headers of the response. No data blocks must yet be * queued on the response. * * Don't put Content-Length or Connection in @headers. * * If @length is zero or greater, then it must represent the * number of queued blocks to follow. */ void cockpit_web_response_headers_full (CockpitWebResponse *self, guint status, const gchar *reason, gssize length, GHashTable *headers) { GString *string; GBytes *block; g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self)); if (self->count > 0) { g_critical ("Headers should be sent first. This is a programmer error."); return; } string = begin_headers (self, status, reason); block = finish_headers (self, string, length, status, append_table (string, headers)); queue_bytes (self, block); g_bytes_unref (block); }
/** * 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); }
static void queue_filter (gpointer data, GBytes *bytes) { QueueStep *qs = data; QueueStep qn = { .response = qs->response }; g_return_if_fail (bytes != NULL); if (qs->filters) { qn.filters = qs->filters->next; cockpit_web_filter_push (qs->filters->data, bytes, queue_filter, &qn); } else { queue_block (qs->response, bytes); } } /** * cockpit_web_response_queue: * @self: the response * @block: the block of data to queue * * Queue a single block of data on the response. Will be sent * during the main loop. * * See cockpit_web_response_content() for a simple way to * avoid queueing individual blocks. * * If this function returns %FALSE, then the response has failed * or has been completed elsewhere. The block was ignored and * queuing more blocks doesn't makes sense. * * After done queuing all your blocks call * cockpit_web_response_complete(). * * Returns: Whether queuing more blocks makes sense */ gboolean cockpit_web_response_queue (CockpitWebResponse *self, GBytes *block) { QueueStep qn = { .response = self }; g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), FALSE); g_return_val_if_fail (block != NULL, FALSE); g_return_val_if_fail (self->complete == FALSE, FALSE); if (self->failed) { g_debug ("%s: ignoring queued block after failure", self->logname); return FALSE; } qn.filters = self->filters; queue_filter (&qn, block); return TRUE; }
void cockpit_web_response_add_filter (CockpitWebResponse *self, CockpitWebFilter *filter) { g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self)); g_return_if_fail (COCKPIT_IS_WEB_FILTER (filter)); g_return_if_fail (self->count == 0); self->filters = g_list_append (self->filters, g_object_ref (filter)); }
/** * cockpit_web_response_get_state: * @self: the web response * * Return the state of the web response. */ CockpitWebResponding cockpit_web_response_get_state (CockpitWebResponse *self) { g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), 0); if (self->done) return COCKPIT_WEB_RESPONSE_SENT; else if (self->complete) return COCKPIT_WEB_RESPONSE_COMPLETE; else if (self->count == 0) return COCKPIT_WEB_RESPONSE_READY; else return COCKPIT_WEB_RESPONSE_QUEUING; }
/** * cockpit_web_response_abort: * @self: the response * * This function is used when streaming content, and at * some point we can't provide the remainder of the content * * This completes the response and terminates the connection. */ void cockpit_web_response_abort (CockpitWebResponse *self) { g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self)); g_return_if_fail (self->complete == FALSE); if (self->failed) return; /* Hold a reference until cockpit_web_response_done() */ g_object_ref (self); self->complete = TRUE; self->failed = TRUE; g_debug ("%s: aborted", self->logname); cockpit_web_response_done (self); }
static gboolean response_next_path (CockpitWebResponse *self, gchar **component) { const gchar *beg = NULL; const gchar *path; g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), FALSE); path = self->path; if (path && path[0] == '/') { beg = path + 1; path = strchr (beg, '/'); } else { path = NULL; } if (!beg || path == beg) return FALSE; self->path = path; if (self->path) { if (component) *component = g_strndup (beg, path - beg); } else if (beg && beg[0]) { if (component) *component = g_strdup (beg); } else { return FALSE; } return TRUE; }
/** * cockpit_web_response_content: * @self: the response * @headers: headers to include or NULL * @block: first block to send * * This is a simple way to send an HTTP response as a single * call. The response will be complete after this call, and will * send in the main-loop. * * The var args are additional GBytes* blocks to send, followed by * a trailing NULL. * * Don't include Content-Length or Connection in @headers. * * This calls cockpit_web_response_headers_full(), * cockpit_web_response_queue() and cockpit_web_response_complete() * internally. */ void cockpit_web_response_content (CockpitWebResponse *self, GHashTable *headers, GBytes *block, ...) { GBytes *first; gsize length = 0; va_list va; va_list va2; g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self)); first = block; va_start (va, block); va_copy (va2, va); while (block) { length += g_bytes_get_size (block); block = va_arg (va, GBytes *); } va_end (va); cockpit_web_response_headers_full (self, 200, "OK", length, headers); block = first; for (;;) { if (!block) { cockpit_web_response_complete (self); break; } if (!cockpit_web_response_queue (self, block)) break; block = va_arg (va2, GBytes *); } va_end (va2); }
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_error: * @self: the response * @status: the HTTP status code * @headers: headers to include or NULL * @format: printf format of error message * * Send an error message with a basic HTML page containing * the error. */ void cockpit_web_response_error (CockpitWebResponse *self, guint code, GHashTable *headers, const gchar *format, ...) { va_list var_args; gchar *reason = NULL; const gchar *message; GBytes *input = NULL; GList *output, *l; GError *error = NULL; g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self)); if (format) { va_start (var_args, format); reason = g_strdup_vprintf (format, var_args); va_end (var_args); message = reason; } else { switch (code) { case 400: message = "Bad request"; break; case 401: message = "Not Authorized"; break; case 403: message = "Forbidden"; break; case 404: message = "Not Found"; break; case 405: message = "Method Not Allowed"; break; case 413: message = "Request Entity Too Large"; break; case 502: message = "Remote Page is Unavailable"; break; case 500: message = "Internal Server Error"; break; default: if (code < 100) reason = g_strdup_printf ("%u Continue", code); else if (code < 200) reason = g_strdup_printf ("%u OK", code); else if (code < 300) reason = g_strdup_printf ("%u Moved", code); else reason = g_strdup_printf ("%u Failed", code); message = reason; break; } } g_debug ("%s: returning error: %u %s", self->logname, code, message); if (cockpit_web_failure_resource) { input = g_resources_lookup_data (cockpit_web_failure_resource, G_RESOURCE_LOOKUP_FLAGS_NONE, &error); if (input == NULL) { g_critical ("couldn't load: %s: %s", cockpit_web_failure_resource, error->message); g_error_free (error); } } if (!input) input = g_bytes_new_static (default_failure_template, strlen (default_failure_template)); if (headers) { if (!g_hash_table_lookup (headers, "Content-Type")) g_hash_table_replace (headers, g_strdup ("Content-Type"), g_strdup ("text/html; charset=utf8")); cockpit_web_response_headers_full (self, code, message, -1, headers); } else { cockpit_web_response_headers (self, code, message, -1, "Content-Type", "text/html; charset=utf8", NULL); } output = cockpit_template_expand (input, substitute_message, (gpointer)message); g_bytes_unref (input); for (l = output; l != NULL; l = g_list_next (l)) { if (!cockpit_web_response_queue (self, l->data)) break; } if (l == NULL) cockpit_web_response_complete (self); g_list_free_full (output, (GDestroyNotify)g_bytes_unref); g_free (reason); }
/** * cockpit_web_response_get_stream: * @self: the response * * Returns: the stream we're sending on */ GIOStream * cockpit_web_response_get_stream (CockpitWebResponse *self) { g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), NULL); return self->io; }
/** * cockpit_web_response_get_query: * @self: the response * * Returns: the resource path for response */ const gchar * cockpit_web_response_get_query (CockpitWebResponse *self) { g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), NULL); return self->query; }
/** * 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); }
/** * 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); }
/** * 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); }