// 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 #include #include #include #include "event-httpd.h" #include "resource.h" static GHashTable *resource_hash; void close_client(struct client *c) { g_free(c->method); g_free(c->uri); g_free(c->query); g_free(c->version); 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, const 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, "%d %s" "%s", 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->ops->close) { c->resource->ops->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; int r; line = g_data_input_stream_read_line_finish(c->data, res, &len, &error); if (error || !line) { if (c->resource->ops->update_close) c->resource->ops->update_close(c, c->resource); if (error) g_free(error); close_client(c); return; } r = c->resource->ops->update(c, c->resource, line); g_free(line); // If the update function returns an error, close this connection. // This could be because we have a "newer" updater, or an error. if (r == -1) close_client(c); g_data_input_stream_read_line_async(c->data, 0, NULL, update, c); } static void parse_request_line(struct client *c, const char *line) { char **parts, **uri; // In the interest of robustness, servers SHOULD ignore any empty // line(s) received where a Request-Line is expected. if (!line[0]) return; // Split the request line: Method SP Request-URI SP HTTP-Version // We do not support early HTTP. parts = g_strsplit(line, " ", 3); if (!parts || !parts[0] || !parts[1] || !parts[2]) { c->error = 400; c->errstr = "Bad Request"; g_strfreev(parts); return; } c->method = g_strdup(parts[0]); c->version = g_strdup(parts[2]); // Split the URI and query string uri = g_strsplit(parts[1], "?", 2); g_strfreev(parts); if (!uri || !uri[0]) { c->error = 400; c->errstr = "Bad Request"; g_strfreev(uri); return; } // Unescape the URI c->uri = g_uri_unescape_string(uri[0], NULL); if (uri[1]) c->query = g_strdup(uri[1]); g_strfreev(uri); } static bool parse_request(struct client *c, const char *line) { // Parse the request line if (!c->method && !c->error) { parse_request_line(c, line); return true; } // 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; return true; } return false; } 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; bool more; 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; } more = parse_request(c, line); g_free(line); // If there is more header to parse, read another line. if (more) { g_data_input_stream_read_line_async(c->data, 0, NULL, receive, c); return; } // Did we encounter an error? if (c->error) { respond_error(c, c->error, c->errstr); return; } // 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(c->version, "HTTP/1.")) { respond_error(c, 505, "HTTP Version Not Supported"); return; } // HTTP/1.1 and later can use chunked mode. Note that http 1.1 // allows leading zeros. c->can_chunk = atoi(c->version + 7) != '0'; // Check the method if (!strcmp(c->method, "GET")) { method = GET; } else if (!strcmp(c->method, "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; } // Lookup the resource handler resource = g_hash_table_lookup(resource_hash, c->uri); if (!resource) { respond_error(c, 404, "Not Found"); return; } c->resource = resource; if (!resource->ops) { respond_error(c, 204, "No Content"); return; } // We have a valid resource, start the response switch (method) { case GET: if (!resource->ops->get) { respond_error(c, 204, "No Content"); return; } if (resource->ops->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->ops->update) { respond_error(c, 204, "No Content"); return; } if (resource->ops->update_open) resource->ops->update_open(c, resource); 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(); }