summaryrefslogtreecommitdiff
path: root/event-httpd.c
diff options
context:
space:
mode:
Diffstat (limited to 'event-httpd.c')
-rw-r--r--event-httpd.c302
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();
+}