diff options
Diffstat (limited to 'event-httpd.c')
-rw-r--r-- | event-httpd.c | 302 |
1 files changed, 302 insertions, 0 deletions
diff --git a/event-httpd.c b/event-httpd.c new file mode 100644 index 0000000..3ec2790 --- /dev/null +++ b/event-httpd.c @@ -0,0 +1,302 @@ +// mini-httpd: this is a single threaded, event driven very basic http +// server designed for event streams. +// +// Copyright (C) 2021 Russell King. +// Licensed under GPL version 2. See COPYING. +// +// This is designed *not* to be a publically accessible HTTP server, +// but is designed to be used behind e.g. an Apache reverse proxy that +// sends X-Forwarded-* headers. The presence of these headers prevents +// the served data being updated maliciously - updates must be done +// directly to this server. + +#include <gio/gio.h> +#include <stdlib.h> +#include <string.h> + +#include "event-httpd.h" +#include "resource.h" + +static GHashTable *resource_hash; + +void close_client(struct client *c) +{ + if (c->request) + g_free(c->request); + g_object_unref(c->data); + g_object_unref(c->conn); + g_free(c); +} + +void respond_header(struct client *c, int error_code, const char *reason, + const char *headers) +{ + g_output_stream_printf(c->out, NULL, NULL, NULL, + "HTTP/1.1 %d %s\r\n%s\r\n", + error_code, reason, headers); +} + +void respond_chunk(struct client *c, GString *s) +{ + if (c->can_chunk) + g_output_stream_printf(c->out, NULL, NULL, NULL, + "%x\r\n%s\r\n", s->len, s->str); + else + g_output_stream_write(c->out, s->str, s->len, NULL, NULL); +} + +static void respond_error(struct client *c, int error_code, const char *reason) +{ + GString *headers, *body = NULL; + + headers = g_string_new("Cache-Control: no-cache\r\n" + "Connection: close\r\n"); + + if (error_code != 204) { + g_string_append(headers, + "Content-type: text/html; charset=UTF-8\r\n"); + if (c->can_chunk) + g_string_append(headers, + "Transfer-Encoding: chunked\r\n"); + + body = g_string_sized_new(1024); + g_string_printf(body, + "<html><head><title>%d %s</title></head>" + "<body>%s</body></html>", + error_code, reason, reason); + } + + respond_header(c, error_code, reason, headers->str); + if (body) + respond_chunk(c, body); + g_string_free(body, TRUE); + g_string_free(headers, TRUE); + + close_client(c); +} + +static void finish(GObject *source, GAsyncResult *res, gpointer user_data) +{ + struct client *c = user_data; + GError *error = NULL; + gsize len; + + g_data_input_stream_read_line_finish(c->data, res, &len, &error); + + if (c->resource->close) { + c->resource->close(c, c->resource); + c->resource = NULL; + } + + close_client(c); +} + +static void update(GObject *source, GAsyncResult *res, gpointer user_data) +{ + struct client *c = user_data; + GError *error = NULL; + char *line; + gsize len; + + line = g_data_input_stream_read_line_finish(c->data, res, &len, &error); + if (error || !line) { + if (error) + g_free(error); + close_client(c); + return; + } + + c->resource->update(c, c->resource, line); + g_data_input_stream_read_line_async(c->data, 0, NULL, update, c); +} + +enum method { + GET, + UPDATE, +}; + +static void receive(GObject *source, GAsyncResult *res, gpointer user_data) +{ + struct client *c = user_data; + struct resource *resource; + enum method method; + GError *error = NULL; + gsize len; + char *line, *uri, *query, *unescaped, *version; + + line = g_data_input_stream_read_line_finish(c->data, res, &len, &error); + if (error || !line) { + if (error) + g_free(error); + close_client(c); + return; + } + + // In the interest of robustness, servers SHOULD ignore any empty + // line(s) received where a Request-Line is expected. + if (!c->request) { + if (line[0]) + c->request = g_strdup(line); + + g_data_input_stream_read_line_async(c->data, 0, NULL, + receive, c); + return; + } + + // Continue reading the request headers (we discard them) + if (line[0]) { + // Detect any X-Forwarded-* header + // FIXME: should this be case-insensitive? + if (g_str_has_prefix(line, "X-Forwarded-")) + c->forwarded = TRUE; + + g_data_input_stream_read_line_async(c->data, 0, NULL, + receive, c); + return; + } + + // End of request. Parse it. + // Find the URI + uri = strchr(c->request, ' '); + if (!uri) { + respond_error(c, 400, "Invalid request"); + return; + } + + *uri++ = '\0'; + + // Find the version + version = strchr(uri, ' '); + if (!version) { + respond_error(c, 400, "Invalid request"); + return; + } + + *version++ = '\0'; + + // Check that the version is HTTP/1.x. We probably ought to + // parse the major version better, as leading zeros should be + // accepted. + if (!g_str_has_prefix(version, "HTTP/1.")) { + respond_error(c, 505, "HTTP Version Not Supported"); + return; + } + + // Split the query string + query = strchr(uri, '?'); + if (query) + *query++ = '\0'; + + c->uri = uri; + c->query = query; + c->version = version; + + // HTTP/1.1 and later can use chunked mode. Note that http 1.1 + // allows leading zeros. + c->can_chunk = atoi(version + 7) != '0'; + + // Check the method + if (!strcmp(c->request, "GET")) { + method = GET; + } else if (!strcmp(c->request, "UPDATE") && !c->forwarded) { + // Update is only permitted if not forwarded through a + // proxy. NOTE: this is the only way we control access. + method = UPDATE; + } else { + respond_error(c, 501, "Not Implemented"); + return; + } + + // Unescape the URI + unescaped = g_uri_unescape_string(uri, NULL); + + // Lookup the resource handler + resource = g_hash_table_lookup(resource_hash, unescaped); + if (!resource) { + respond_error(c, 404, "Not Found"); + return; + } + + c->resource = resource; + + // We have a valid resource, start the response + switch (method) { + case GET: + if (!resource->get) { + respond_error(c, 204, "No Content"); + close_client(c); + return; + } + + if (resource->get(c, resource)) { + g_data_input_stream_read_line_async(c->data, 0, NULL, + finish, c); + } else { + close_client(c); + } + break; + + case UPDATE: + if (!resource->update) { + respond_error(c, 204, "No Content"); + close_client(c); + return; + } + + g_data_input_stream_read_line_async(c->data, 0, NULL, + update, c); + break; + } +} + +static gboolean incoming(GSocketService *service, GSocketConnection *connection, + GObject *source_object, gpointer user_data) +{ + struct client *c; + + c = g_new0(struct client, 1); + c->conn = g_object_ref(connection); + c->out = g_io_stream_get_output_stream(G_IO_STREAM(connection)); + c->in = g_io_stream_get_input_stream(G_IO_STREAM(connection)); + c->data = g_data_input_stream_new(c->in); + + g_tcp_connection_set_graceful_disconnect(G_TCP_CONNECTION(connection), + TRUE); + + /* Be tolerant of input */ + g_data_input_stream_set_newline_type(c->data, + G_DATA_STREAM_NEWLINE_TYPE_ANY); + + g_data_input_stream_read_line_async(c->data, 0, NULL, receive, c); + + return TRUE; +} + +int mini_httpd_init(int port, const char *progname) +{ + GSocketService *service; + GError *error = NULL; + + service = g_socket_service_new(); + if (!g_socket_listener_add_inet_port(G_SOCKET_LISTENER(service), port, + NULL, &error)) { + g_printerr("%s: %s\n", progname, error->message); + return 0; + } + + g_signal_connect(service, "incoming", G_CALLBACK(incoming), NULL); + + return 1; +} + +int main(int argc, char *argv[]) +{ + resource_hash = g_hash_table_new(g_str_hash, g_str_equal); + resource_init(resource_hash); + + if (!mini_httpd_init(1180, argv[0])) + return 1; + + g_main_loop_run(g_main_loop_new(NULL, FALSE)); + g_assert_not_reached(); +} |