/*
* This file is part of Cockpit.
*
* Copyright (C) 2014 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 .
*/
/* This gets logged as part of the (more verbose) protocol logging */
#ifdef G_LOG_DOMAIN
#undef G_LOG_DOMAIN
#endif
#define G_LOG_DOMAIN "cockpit-protocol"
#include "config.h"
#include "cockpitwebresponse.h"
#include "cockpitwebfilter.h"
#include "common/cockpitconf.h"
#include "common/cockpiterror.h"
#include "common/cockpitlocale.h"
#include "common/cockpittemplate.h"
#include
#include
#include
/**
* Certain processes may want to have a non-default error page.
*/
const gchar *cockpit_web_failure_resource = NULL;
static const gchar default_failure_template[] =
"@@message@@@@message@@\n";
/**
* CockpitWebResponse:
*
* A response sent back to an HTTP client. You can use the high level one
* shot APIs, like cockpit_web_response_content() and
* cockpit_web_response_error() * or low level builder APIs:
*
* cockpit_web_response_headers() send the headers
* cockpit_web_response_queue() send a block of data.
* cockpit_web_response_complete() finish.
*/
struct _CockpitWebResponse {
GObject parent;
GIOStream *io;
const gchar *logname;
const gchar *path;
gchar *full_path;
gchar *query;
gchar *url_root;
gchar *method;
gchar *origin;
CockpitCacheType cache_type;
/* The output queue */
GPollableOutputStream *out;
GQueue *queue;
gsize partial_offset;
GSource *source;
/* Status flags */
guint count;
gboolean complete;
gboolean failed;
gboolean done;
gboolean chunked;
gboolean keep_alive;
GList *filters;
};
typedef struct {
GObjectClass parent;
} CockpitWebResponseClass;
static guint signal__done;
G_DEFINE_TYPE (CockpitWebResponse, cockpit_web_response, G_TYPE_OBJECT);
static void
cockpit_web_response_init (CockpitWebResponse *self)
{
self->queue = g_queue_new ();
self->cache_type = COCKPIT_WEB_RESPONSE_CACHE_UNSET;
}
static void
cockpit_web_response_done (CockpitWebResponse *self)
{
gboolean reusable = FALSE;
g_object_ref (self);
g_assert (!self->done);
self->done = TRUE;
if (self->source)
{
g_source_destroy (self->source);
g_source_unref (self->source);
self->source = NULL;
}
if (self->complete)
{
reusable = !self->failed && self->keep_alive;
g_object_unref (self);
}
else if (!self->failed)
{
g_critical ("A CockpitWebResponse was freed without being completed properly. "
"This is a programming error.");
}
g_signal_emit (self, signal__done, 0, reusable);
g_object_unref (self->io);
self->io = NULL;
self->out = NULL;
g_object_unref (self);
}
static void
cockpit_web_response_dispose (GObject *object)
{
CockpitWebResponse *self = COCKPIT_WEB_RESPONSE (object);
if (!self->done)
cockpit_web_response_done (self);
g_list_free_full (self->filters, g_object_unref);
self->filters = NULL;
G_OBJECT_CLASS (cockpit_web_response_parent_class)->dispose (object);
}
static void
cockpit_web_response_finalize (GObject *object)
{
CockpitWebResponse *self = COCKPIT_WEB_RESPONSE (object);
g_free (self->full_path);
g_free (self->query);
g_free (self->url_root);
g_free (self->method);
g_free (self->origin);
g_assert (self->io == NULL);
g_assert (self->out == NULL);
g_queue_free_full (self->queue, (GDestroyNotify)g_bytes_unref);
G_OBJECT_CLASS (cockpit_web_response_parent_class)->finalize (object);
}
static void
cockpit_web_response_class_init (CockpitWebResponseClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->dispose = cockpit_web_response_dispose;
gobject_class->finalize = cockpit_web_response_finalize;
signal__done = g_signal_new ("done", COCKPIT_TYPE_WEB_RESPONSE,
G_SIGNAL_RUN_LAST,
0, NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
}
/**
* cockpit_web_response_new:
* @io: the stream to send on
* @path: the path resource or NULL
* @query: the query string or NULL
* @in_headers: input headers or NULL
*
* Create a new web response.
*
* The returned reference belongs to the caller. Additionally
* once cockpit_web_response_complete() is called, an additional
* reference is held until the response is sent and flushed.
*
* Returns: (transfer full): the new response, unref when done with it
*/
CockpitWebResponse *
cockpit_web_response_new (GIOStream *io,
const gchar *original_path,
const gchar *path,
const gchar *query,
GHashTable *in_headers)
{
CockpitWebResponse *self;
GOutputStream *out;
const gchar *connection;
const gchar *protocol = NULL;
const gchar *host = NULL;
gint offset;
/* Trying to be a somewhat performant here, avoiding properties */
self = g_object_new (COCKPIT_TYPE_WEB_RESPONSE, NULL);
self->io = g_object_ref (io);
out = g_io_stream_get_output_stream (io);
if (G_IS_POLLABLE_OUTPUT_STREAM (out))
{
self->out = (GPollableOutputStream *)out;
}
else if (out)
{
g_critical ("Cannot send web response over non-pollable output stream: %s",
G_OBJECT_TYPE_NAME (out));
}
else
{
g_critical ("Cannot send web response: no output stream available");
}
self->url_root = NULL;
self->full_path = g_strdup (path);
self->path = self->full_path;
if (path && original_path)
{
offset = strlen (original_path) - strlen (path);
if (offset > 0 && g_strcmp0 (original_path + offset, path) == 0)
self->url_root = g_strndup (original_path, offset);
}
self->query = g_strdup (query);
if (self->path)
self->logname = self->path;
else
self->logname = "response";
self->keep_alive = TRUE;
if (in_headers)
{
connection = g_hash_table_lookup (in_headers, "Connection");
if (connection)
self->keep_alive = g_str_equal (connection, "keep-alive");
host = g_hash_table_lookup (in_headers, "Host");
}
if (G_IS_SOCKET_CONNECTION (io))
protocol = "http";
else
protocol = "https";
if (protocol && host)
self->origin = g_strdup_printf ("%s://%s", protocol, host);
return self;
}
void
cockpit_web_response_set_method (CockpitWebResponse *response,
const gchar *method)
{
g_return_if_fail (g_strcmp0 (method, "GET") == 0 || g_strcmp0 (method, "HEAD") == 0);
response->method = g_strdup (method);
}
/**
* cockpit_web_response_get_path:
* @self: the response
*
* Returns: the resource path for response
*/
const gchar *
cockpit_web_response_get_path (CockpitWebResponse *self)
{
g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), NULL);
return self->path;
}
/**
* cockpit_web_response_get_url_root:
* @self: the response
*
* Returns: The url root portion of the original path that was removed
*/
const gchar *
cockpit_web_response_get_url_root (CockpitWebResponse *self) {
return self->url_root;
}
/**
* 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_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;
}
#if !GLIB_CHECK_VERSION(2,43,2)
#define G_IO_ERROR_CONNECTION_CLOSED G_IO_ERROR_BROKEN_PIPE
#endif
gboolean
cockpit_web_should_suppress_output_error (const gchar *logname,
GError *error)
{
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_CLOSED) ||
g_error_matches (error, G_IO_ERROR, G_IO_ERROR_BROKEN_PIPE))
{
g_debug ("%s: output error: %s", logname, error->message);
return TRUE;
}
#if !GLIB_CHECK_VERSION(2,43,2)
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_FAILED) &&
strstr (error->message, g_strerror (ECONNRESET)))
{
g_debug ("%s: output error: %s", logname, error->message);
return TRUE;
}
#endif
return FALSE;
}
static void
on_output_flushed (GObject *stream,
GAsyncResult *result,
gpointer user_data)
{
CockpitWebResponse *self = COCKPIT_WEB_RESPONSE (user_data);
GOutputStream *output = G_OUTPUT_STREAM (stream);
GError *error = NULL;
if (g_output_stream_flush_finish (output, result, &error))
{
g_debug ("%s: flushed output", self->logname);
}
else
{
if (!cockpit_web_should_suppress_output_error (self->logname, error))
g_message ("%s: couldn't flush web output: %s", self->logname, error->message);
self->failed = TRUE;
g_error_free (error);
}
cockpit_web_response_done (self);
g_object_unref (self);
}
static gboolean
on_response_output (GObject *pollable,
gpointer user_data)
{
CockpitWebResponse *self = user_data;
GError *error = NULL;
const guint8 *data;
GBytes *block;
gssize count;
gsize len;
block = g_queue_peek_head (self->queue);
if (block)
{
data = g_bytes_get_data (block, &len);
g_assert (len == 0 || self->partial_offset < len);
data += self->partial_offset;
len -= self->partial_offset;
if (len > 0)
{
count = g_pollable_output_stream_write_nonblocking (self->out, data, len,
NULL, &error);
}
else
{
count = 0;
}
if (count < 0)
{
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
{
g_error_free (error);
return TRUE;
}
if (!cockpit_web_should_suppress_output_error (self->logname, error))
g_message ("%s: couldn't write web output: %s", self->logname, error->message);
self->failed = TRUE;
cockpit_web_response_done (self);
g_error_free (error);
return FALSE;
}
if (count == len)
{
g_debug ("%s: sent %d bytes", self->logname, (int)len);
self->partial_offset = 0;
g_queue_pop_head (self->queue);
g_bytes_unref (block);
}
else
{
g_debug ("%s: sent %d partial", self->logname, (int)count);
g_assert (count < len);
self->partial_offset += count;
}
return TRUE;
}
else
{
g_source_destroy (self->source);
g_source_unref (self->source);
self->source = NULL;
if (self->complete)
{
g_debug ("%s: complete flushing output", self->logname);
g_output_stream_flush_async (G_OUTPUT_STREAM (self->out), G_PRIORITY_DEFAULT,
NULL, on_output_flushed, g_object_ref (self));
}
return FALSE;
}
}
static void
queue_bytes (CockpitWebResponse *self,
GBytes *block)
{
g_queue_push_tail (self->queue, g_bytes_ref (block));
self->count++;
if (!self->source)
{
self->source = g_pollable_output_stream_create_source (self->out, NULL);
g_source_set_callback (self->source, (GSourceFunc)on_response_output, self, NULL);
g_source_attach (self->source, NULL);
}
}
static void
queue_block (CockpitWebResponse *self,
GBytes *block)
{
gsize length = g_bytes_get_size (block);
GBytes *bytes;
gchar *data;
/*
* We cannot queue chunks of length zero. Besides being silly, this
* messes with chunked encoding. The 0 length block means end of
* response.
*/
if (length == 0)
return;
g_debug ("%s: queued %d bytes", self->logname, (int)length);
if (!self->chunked)
{
queue_bytes (self, block);
}
else
{
/* Required for chunked transfer encoding. */
data = g_strdup_printf ("%x\r\n", (unsigned int)length);
bytes = g_bytes_new_take (data, strlen (data));
queue_bytes (self, bytes);
g_bytes_unref (bytes);
queue_bytes (self, block);
bytes = g_bytes_new_static ("\r\n", 2);
queue_bytes (self, bytes);
g_bytes_unref (bytes);
}
}
typedef struct {
CockpitWebResponse *response;
GList *filters;
} QueueStep;
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;
}
if (g_strcmp0 (self->method, "HEAD") == 0)
{
g_debug ("%s: ignoring queued block for method HEAD", self->logname);
return TRUE;
}
qn.filters = self->filters;
queue_filter (&qn, block);
return TRUE;
}
/**
* 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_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);
}
/**
* CockpitWebResponding:
* @COCKPIT_WEB_RESPONSE_READY: nothing queued or sent yet
* @COCKPIT_WEB_RESPONSE_QUEUING: started and still queuing data on response
* @COCKPIT_WEB_RESPONSE_COMPLETE: all data is queued or aborted
* @COCKPIT_WEB_RESPONSE_SENT: data is completely sent
*
* Various states of the web response.
*/
/**
* 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;
}
gboolean
cockpit_web_response_is_simple_token (const gchar *string)
{
string += strcspn (string, " \t\r\n\v");
return string[0] == '\0';
}
gboolean
cockpit_web_response_is_header_value (const gchar *string)
{
string += strcspn (string, "\r\n\v");
return string[0] == '\0';
}
enum {
HEADER_CONTENT_TYPE = 1 << 0,
HEADER_CONTENT_ENCODING = 1 << 1,
HEADER_VARY = 1 << 2,
HEADER_CACHE_CONTROL = 1 << 3,
};
static GString *
begin_headers (CockpitWebResponse *response,
guint status,
const gchar *reason)
{
GString *string;
string = g_string_sized_new (1024);
g_string_printf (string, "HTTP/1.1 %d %s\r\n", status, reason);
return string;
}
static guint
append_header (GString *string,
const gchar *name,
const gchar *value)
{
if (value)
{
g_return_val_if_fail (cockpit_web_response_is_simple_token (name), 0);
g_return_val_if_fail (cockpit_web_response_is_header_value (value), 0);
g_string_append_printf (string, "%s: %s\r\n", name, value);
}
if (g_ascii_strcasecmp ("Content-Type", name) == 0)
return HEADER_CONTENT_TYPE;
if (g_ascii_strcasecmp ("Cache-Control", name) == 0)
return HEADER_CACHE_CONTROL;
if (g_ascii_strcasecmp ("Vary", name) == 0)
return HEADER_VARY;
if (g_ascii_strcasecmp ("Content-Encoding", name) == 0)
return HEADER_CONTENT_ENCODING;
else if (g_ascii_strcasecmp ("Content-Length", name) == 0)
g_critical ("Don't set Content-Length manually. This is a programmer error.");
else if (g_ascii_strcasecmp ("Connection", name) == 0)
g_critical ("Don't set Connection header manually. This is a programmer error.");
return 0;
}
static guint
append_table (GString *string,
GHashTable *headers)
{
GHashTableIter iter;
gpointer key;
gpointer value;
guint seen = 0;
if (headers)
{
g_hash_table_iter_init (&iter, headers);
while (g_hash_table_iter_next (&iter, &key, &value))
seen |= append_header (string, key, value);
}
return seen;
}
static guint
append_va (GString *string,
va_list va)
{
const gchar *name;
const gchar *value;
guint seen = 0;
for (;;)
{
name = va_arg (va, const gchar *);
if (!name)
break;
value = va_arg (va, const gchar *);
seen |= append_header (string, name, value);
}
return seen;
}
static GBytes *
finish_headers (CockpitWebResponse *self,
GString *string,
gssize length,
gint status,
guint seen)
{
const gchar *content_type;
/* Automatically figure out content type */
if ((seen & HEADER_CONTENT_TYPE) == 0 &&
self->full_path != NULL && status >= 200 && status <= 299)
{
content_type = cockpit_web_response_content_type (self->full_path);
if (content_type)
g_string_append_printf (string, "Content-Type: %s\r\n", content_type);
}
if (status != 304)
{
if (length >= 0 && !self->filters)
g_string_append_printf (string, "Content-Length: %" G_GSSIZE_FORMAT "\r\n", length);
if (length < 0 || seen & HEADER_CONTENT_ENCODING || self->filters)
{
self->chunked = TRUE;
g_string_append_printf (string, "Transfer-Encoding: chunked\r\n");
}
else
{
self->chunked = FALSE;
}
}
if ((seen & HEADER_CACHE_CONTROL) == 0 && status >= 200 && status <= 299)
{
if (self->cache_type == COCKPIT_WEB_RESPONSE_CACHE_FOREVER)
g_string_append (string, "Cache-Control: max-age=31556926, public\r\n");
else if (self->cache_type == COCKPIT_WEB_RESPONSE_NO_CACHE)
g_string_append (string, "Cache-Control: no-cache, no-store\r\n");
else if (self->cache_type == COCKPIT_WEB_RESPONSE_CACHE_PRIVATE)
g_string_append (string, "Cache-Control: max-age=86400, private\r\n");
}
if ((seen & HEADER_VARY) == 0 && status >= 200 && status <= 299 &&
self->cache_type == COCKPIT_WEB_RESPONSE_CACHE_PRIVATE)
{
g_string_append (string, "Vary: Cookie\r\n");
}
if (!self->keep_alive)
g_string_append (string, "Connection: close\r\n");
g_string_append (string, "\r\n");
return g_string_free_to_bytes (string);
}
/**
* cockpit_web_response_set_cache_type:
* @self: the response
* @cache_type: Ensures the apropriate cache headers are returned for
the given cache type.
*/
void
cockpit_web_response_set_cache_type (CockpitWebResponse *self,
CockpitCacheType cache_type)
{
self->cache_type = cache_type;
}
/**
* 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
*
* 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.
*
* Specify header name/value pairs in the var args, and end with
* a NULL name. If value is NULL, then that header won't be sent.
*
* Don't specify Content-Length or Connection headers.
*
* If @length is zero or greater, then it must represent the
* number of queued blocks to follow.
*/
void
cockpit_web_response_headers (CockpitWebResponse *self,
guint status,
const gchar *reason,
gssize length,
...)
{
GString *string;
GBytes *block;
va_list va;
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);
va_start (va, length);
block = finish_headers (self, string, length, status,
append_va (string, va));
va_end (va);
queue_bytes (self, block);
g_bytes_unref (block);
}
/**
* 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_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);
}
static GBytes *
substitute_message (const gchar *variable,
gpointer user_data)
{
const gchar *message = user_data;
if (g_str_equal (variable, "message"))
return g_bytes_new (message, strlen (message));
return NULL;
}
static GBytes *
substitute_hash_value (const gchar *variable,
gpointer user_data)
{
GHashTable *data = user_data;
gchar *value = g_hash_table_lookup (data, variable);
if (value)
return g_bytes_new (value, strlen (value));
return g_bytes_new ("", 0);
}
/**
* 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;
gchar *escaped = 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));
output = cockpit_template_expand (input, substitute_message,
"@@", "@@", (gpointer)message);
g_bytes_unref (input);
/* If sending arbitrary messages, make sure they're escaped */
if (reason)
{
g_strstrip (reason);
escaped = g_uri_escape_string (reason, " :", FALSE);
message = escaped;
}
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);
}
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);
g_free (escaped);
}
/**
* 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 gboolean
path_has_prefix (const gchar *path,
const gchar *prefix)
{
gsize len;
if (prefix == NULL)
return FALSE;
len = strlen (prefix);
if (len == 0)
return FALSE;
if (!g_str_has_prefix (path, prefix))
return FALSE;
if (prefix[len - 1] == '/' ||
path[len] == '/')
return TRUE;
return FALSE;
}
gchar **
cockpit_web_response_resolve_roots (const gchar **input)
{
GPtrArray *roots;
char *path;
gint i;
roots = g_ptr_array_new ();
for (i = 0; input && input[i]; i++)
{
path = realpath (input[i], NULL);
if (path == NULL)
g_debug ("couldn't resolve document root: %s: %m", input[i]);
else
g_ptr_array_add (roots, path);
}
g_ptr_array_add (roots, NULL);
return (gchar **)g_ptr_array_free (roots, FALSE);
}
static void
web_response_file (CockpitWebResponse *response,
const gchar *escaped,
const gchar **roots,
CockpitTemplateFunc template_func,
gpointer user_data)
{
const gchar *default_policy = "default-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:";
const gchar *headers[5] = { NULL };
GError *error = NULL;
gchar *unescaped = NULL;
gchar *path = NULL;
gchar *alloc = NULL;
GMappedFile *file = NULL;
const gchar *root;
GBytes *body;
GList *output = NULL;
GList *l = NULL;
gint content_length = -1;
gint at = 0;
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);
if (template_func)
{
output = cockpit_template_expand (body, template_func, "${", "}", user_data);
}
else
{
output = g_list_prepend (output, g_bytes_ref (body));
content_length = g_bytes_get_size (body);
}
g_bytes_unref (body);
if (response->origin)
{
headers[at++] = "Access-Control-Allow-Origin";
headers[at++] = response->origin;
}
/*
* The default Content-Security-Policy for .html files allows
* the site to have inline