/* gcal-simple-server.c * * Copyright 2019 Georges Basile Stavracas Neto * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "GcalSimpleServer" #include "gcal-simple-server.h" struct _GcalSimpleServer { GObject parent; /* Only valid while running */ GThread *thread; GMainLoop *thread_mainloop; SoupServer *server; gchar *uri; GMutex running_mutex; GCond running_cond; gboolean running; }; G_DEFINE_TYPE (GcalSimpleServer, gcal_simple_server, G_TYPE_OBJECT) /* * Auxiliary methods */ static void process_get (SoupMessage *message, const gchar *prefix, const gchar *path) { g_autofree gchar *calendar_path = g_strdup_printf ("%s/calendar", prefix); g_autofree gchar *calendar_file = g_strdup_printf ("%s/calendar.ics", prefix); if (g_strcmp0 (path, calendar_path) == 0) { g_debug ("Serving empty calendar"); soup_message_set_response (message, "text/calendar", SOUP_MEMORY_STATIC, GCAL_TEST_SERVER_EMPTY_CALENDAR, strlen (GCAL_TEST_SERVER_EMPTY_CALENDAR)); } if (g_strcmp0 (path, calendar_file) == 0) { g_autoptr (SoupBuffer) buffer = NULL; GMappedFile *mapping; g_debug ("Serving calendar.ics"); mapping = g_mapped_file_new (path, FALSE, NULL); if (!mapping) { soup_message_set_status (message, SOUP_STATUS_INTERNAL_SERVER_ERROR); return; } buffer = soup_buffer_new_with_owner (g_mapped_file_get_contents (mapping), g_mapped_file_get_length (mapping), mapping, (GDestroyNotify)g_mapped_file_unref); soup_message_body_append_buffer (message->response_body, buffer); } } static void process_caldav (SoupMessage *message, const gchar *path) { g_debug ("Processing CalDAV request"); } /* * Callbacks */ static gboolean idle_quit_server_cb (gpointer user_data) { GcalSimpleServer *self = user_data; g_main_loop_quit (self->thread_mainloop); return G_SOURCE_REMOVE; } static void no_auth_handler_cb (SoupServer *server, SoupMessage *message, const gchar *path, GHashTable *query, SoupClientContext *client, gpointer user_data) { g_debug ("No authenticaton needed"); if (message->method == SOUP_METHOD_GET) process_get (message, "/public", path); else if (message->method == SOUP_METHOD_PROPFIND) process_caldav (message, path); else soup_message_set_status (message, SOUP_STATUS_NOT_IMPLEMENTED); } static void auth_handler_cb (SoupServer *server, SoupMessage *message, const gchar *path, GHashTable *query, SoupClientContext *client, gpointer user_data) { g_debug ("Needs authenticaton"); if (message->method == SOUP_METHOD_GET) process_get (message, "/secret-area", path); else if (message->method == SOUP_METHOD_PROPFIND) process_caldav (message, path); else soup_message_set_status (message, SOUP_STATUS_NOT_IMPLEMENTED); } static gboolean authorize_cb (SoupAuthDomain *domain, SoupMessage *message, const char *username, const char *password, gpointer user_data) { const struct { const gchar *username; const gchar *password; } valid_credentials[] = { { "unicorn", "iamnotahorse" }, { "feaneron", "idonotmaintainanything" }, { "purism", "FINISHTHEPHONE!" }, }; guint i; g_debug ("Authorizing username: '%s', password: '%s'", username, password); if (!username && !password) return FALSE; for (i = 0; i < G_N_ELEMENTS (valid_credentials); i++) { if (g_strcmp0 (valid_credentials[i].username, username) == 0 && g_strcmp0 (valid_credentials[i].password, password) == 0) { return TRUE; } } return FALSE; } static gpointer run_server_in_thread (gpointer data) { g_autoptr (SoupAuthDomain) domain = NULL; g_autoptr (GMainContext) context = NULL; g_autoptr (SoupServer) server = NULL; g_autoptr (GMainLoop) mainloop = NULL; g_autoslist (SoupURI) uris = NULL; g_autoptr (GError) error = NULL; GcalSimpleServer *self = data; g_autofree gchar *uri = NULL; context = g_main_context_new (); g_main_context_push_thread_default (context); mainloop = g_main_loop_new (context, FALSE); self->thread_mainloop = mainloop; server = soup_server_new (SOUP_SERVER_SERVER_HEADER, "gcal-simple-server", NULL); /* Anything under /secret-area and /private requires authentication */ domain = soup_auth_domain_basic_new (SOUP_AUTH_DOMAIN_REALM, "GcalSimpleServer", SOUP_AUTH_DOMAIN_BASIC_AUTH_CALLBACK, authorize_cb, SOUP_AUTH_DOMAIN_BASIC_AUTH_DATA, self, SOUP_AUTH_DOMAIN_ADD_PATH, "/secret-area", NULL); soup_server_add_auth_domain (server, domain); soup_server_listen_local (server, 0, SOUP_SERVER_LISTEN_IPV4_ONLY, &error); if (error) { g_critical ("Error starting up server: %s", error->message); return NULL; } soup_server_add_handler (server, NULL, no_auth_handler_cb, NULL, NULL); soup_server_add_handler (server, "/public", no_auth_handler_cb, NULL, NULL); soup_server_add_handler (server, "/secret-area", auth_handler_cb, NULL, NULL); uris = soup_server_get_uris (server); g_assert_cmpint (g_slist_length (uris), ==, 1); uri = soup_uri_to_string (uris->data, FALSE); self->uri = g_strdup (uri); g_debug ("Listening on %s", uri); g_mutex_lock (&self->running_mutex); self->running = TRUE; g_cond_signal (&self->running_cond); g_mutex_unlock (&self->running_mutex); g_main_loop_run (mainloop); g_debug ("Stopping"); soup_server_disconnect (server); g_main_context_pop_thread_default (context); return NULL; } /* * GObject overrides */ static void gcal_simple_server_finalize (GObject *object) { GcalSimpleServer *self = (GcalSimpleServer *)object; if (self->thread) gcal_simple_server_stop (self); g_clear_pointer (&self->uri, g_free); G_OBJECT_CLASS (gcal_simple_server_parent_class)->finalize (object); } static void gcal_simple_server_class_init (GcalSimpleServerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = gcal_simple_server_finalize; } static void gcal_simple_server_init (GcalSimpleServer *self) { } GcalSimpleServer * gcal_simple_server_new (void) { return g_object_new (GCAL_TYPE_SIMPLE_SERVER, NULL); } void gcal_simple_server_start (GcalSimpleServer *self) { g_return_if_fail (GCAL_IS_SIMPLE_SERVER (self)); if (self->thread) { g_warning ("Server already started"); return; } self->thread = g_thread_new ("gcal_simple_server_thread", run_server_in_thread, self); g_mutex_lock (&self->running_mutex); while (!self->running) g_cond_wait (&self->running_cond, &self->running_mutex); g_mutex_unlock (&self->running_mutex); } void gcal_simple_server_stop (GcalSimpleServer *self) { GMainContext *context; g_return_if_fail (GCAL_IS_SIMPLE_SERVER (self)); if (!self->thread) { g_warning ("Server not running"); return; } context = g_main_loop_get_context (self->thread_mainloop); soup_add_completion (context, idle_quit_server_cb, self); g_mutex_lock (&self->running_mutex); self->running = FALSE; g_cond_signal (&self->running_cond); g_mutex_unlock (&self->running_mutex); g_thread_join (self->thread); self->thread = NULL; } SoupURI* gcal_simple_server_get_uri (GcalSimpleServer *self) { g_return_val_if_fail (GCAL_IS_SIMPLE_SERVER (self), NULL); g_return_val_if_fail (self->thread != NULL, NULL); return soup_uri_new (self->uri); }