diff --git a/src/meson.build b/src/meson.build index 90938097..91a06dd3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -124,6 +124,7 @@ sources = files( 'search/gcal-search-hit-event.c', 'search/gcal-search-model.c', 'utils/gcal-date-time-utils.c', + 'utils/gcal-source-discoverer.c', 'utils/gcal-thread-utils.c', 'utils/gcal-utils.c', 'views/gcal-month-cell.c', diff --git a/src/utils/gcal-source-discoverer.c b/src/utils/gcal-source-discoverer.c new file mode 100644 index 00000000..6c61cdab --- /dev/null +++ b/src/utils/gcal-source-discoverer.c @@ -0,0 +1,405 @@ +/* gcal-source-discoverer.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 "GcalSourceDiscoverer" + +#include +#include + +#include "gcal-debug.h" +#include "gcal-source-discoverer.h" +#include "gcal-utils.h" + +G_DEFINE_QUARK (GcalSourceDiscoverer, gcal_source_discoverer_error); + +typedef struct +{ + gchar *uri; + gchar *username; + gchar *password; +} DiscovererData; + + +/* + * Auxiliary methods + */ + +static void +discoverer_data_free (gpointer data) +{ + DiscovererData *discoverer_data = data; + + if (!discoverer_data) + return; + + g_free (discoverer_data->uri); + g_free (discoverer_data->username); + g_free (discoverer_data->password); + g_free (discoverer_data); +} + +static ESource* +create_source_for_uri (DiscovererData *data) +{ + ESourceAuthentication *auth; + g_autoptr (SoupURI) soup_uri = NULL; + g_autofree gchar *display_name = NULL; + g_autofree gchar *basename = NULL; + ESourceExtension *ext; + ESourceWebdav *webdav; + ESource *source; + const gchar *host; + const gchar *path; + + soup_uri = soup_uri_new (data->uri); + if (!soup_uri) + GCAL_RETURN (NULL); + + host = soup_uri_get_host (soup_uri); + + /* Create the new source and add the needed extensions */ + source = e_source_new (NULL, NULL, NULL); + e_source_set_parent (source, "webcal-stub"); + + /* Display name */ + path = soup_uri_get_path (soup_uri); + basename = g_path_get_basename (path); + display_name = gcal_utils_format_filename_for_display (basename); + e_source_set_display_name (source, display_name); + + ext = e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR); + e_source_backend_set_backend_name (E_SOURCE_BACKEND (ext), "webcal"); + + /* Authentication */ + auth = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION); + e_source_authentication_set_host (auth, host); + + /* Webdav */ + webdav = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND); + e_source_webdav_set_soup_uri (webdav, soup_uri); + + return source; +} + +static ESource* +create_discovered_source (ESource *source, + GSList *user_addresses, + EWebDAVDiscoveredSource *discovered_source) +{ + g_autoptr (SoupURI) soup_uri = NULL; + ESourceSelectable *selectable; + ESourceExtension *ext; + ESourceWebdav *webdav; + ESource *new_source; + const gchar *resource_path; + + soup_uri = soup_uri_new (discovered_source->href); + resource_path = soup_uri_get_path (soup_uri); + + new_source = e_source_new (NULL, NULL, NULL); + e_source_set_parent (new_source, "local"); + + ext = e_source_get_extension (new_source, E_SOURCE_EXTENSION_CALENDAR); + e_source_backend_set_backend_name (E_SOURCE_BACKEND (ext), "local"); + + /* Copy Authentication data */ + if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) + { + ESourceAuthentication *new_auth, *parent_auth; + + parent_auth = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION); + new_auth = e_source_get_extension (new_source, E_SOURCE_EXTENSION_AUTHENTICATION); + + e_source_authentication_set_host (new_auth, e_source_authentication_get_host (parent_auth)); + e_source_authentication_set_method (new_auth, e_source_authentication_get_method (parent_auth)); + e_source_authentication_set_port (new_auth, e_source_authentication_get_port (parent_auth)); + e_source_authentication_set_user (new_auth, e_source_authentication_get_user (parent_auth)); + e_source_authentication_set_proxy_uid (new_auth, e_source_authentication_get_proxy_uid (parent_auth)); + } + + /* Copy Webdav data */ + if (e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) + { + ESourceWebdav *new_webdav, *parent_webdav; + + parent_webdav = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND); + new_webdav = e_source_get_extension (new_source, E_SOURCE_EXTENSION_WEBDAV_BACKEND); + + e_source_webdav_set_display_name (new_webdav, e_source_webdav_get_display_name (parent_webdav)); + e_source_webdav_set_resource_path (new_webdav, e_source_webdav_get_resource_path (parent_webdav)); + e_source_webdav_set_resource_query (new_webdav, e_source_webdav_get_resource_query (parent_webdav)); + e_source_webdav_set_email_address (new_webdav, e_source_webdav_get_email_address (parent_webdav)); + e_source_webdav_set_ssl_trust (new_webdav, e_source_webdav_get_ssl_trust (parent_webdav)); + + e_source_set_parent (new_source, "webcal-stub"); + e_source_backend_set_backend_name (E_SOURCE_BACKEND (ext), "webcal"); + } + + /* build up the new source */ + e_source_set_display_name (new_source, discovered_source->display_name); + + webdav = e_source_get_extension (new_source, E_SOURCE_EXTENSION_WEBDAV_BACKEND); + e_source_webdav_set_resource_path (webdav, resource_path); + e_source_webdav_set_display_name (webdav, discovered_source->display_name); + + if (user_addresses) + e_source_webdav_set_email_address (webdav, user_addresses->data); + + /* Setup the color */ + selectable = e_source_get_extension (new_source, E_SOURCE_EXTENSION_CALENDAR); + e_source_selectable_set_color (selectable, discovered_source->color); + + return new_source; +} + +static gboolean +is_authentication_error (gint code) +{ + switch (code) + { + case SOUP_STATUS_UNAUTHORIZED: + case SOUP_STATUS_FORBIDDEN: + case SOUP_STATUS_METHOD_NOT_ALLOWED: + return TRUE; + } + + return FALSE; +} + + +/* + * Callbacks + */ + +static void +on_soup_session_authenticate_cb (SoupSession *session, + SoupMessage *message, + SoupAuth *auth, + gboolean retrying, + gpointer user_data) +{ + DiscovererData *data = user_data; + + if (data->username && data->password) + soup_auth_authenticate (auth, data->username, data->password); + else + soup_message_set_status (message, SOUP_STATUS_UNAUTHORIZED); +} + +typedef GPtrArray* (*DiscoverFunc) (DiscovererData *data, + GCancellable *cancellable, + GError **error); + +static GPtrArray* +discover_file_in_thread (DiscovererData *data, + GCancellable *cancellable, + GError **error) +{ + g_autoptr (GInputStream) input_stream = NULL; + g_autoptr (SoupMessage) message = NULL; + g_autoptr (SoupSession) session = NULL; + g_autoptr (GPtrArray) source = NULL; + const gchar *content_type; + + GCAL_ENTRY; + + GCAL_TRACE_MSG ("Creating request for %s", data->uri); + + session = soup_session_new_with_options (SOUP_SESSION_TIMEOUT, 10, NULL); + g_signal_connect (session, "authenticate", G_CALLBACK (on_soup_session_authenticate_cb), data); + + message = soup_message_new ("GET", data->uri); + input_stream = soup_session_send (session, message, cancellable, error); + + if (!input_stream) + GCAL_RETURN (NULL); + + g_input_stream_close (input_stream, cancellable, error); + + content_type = soup_message_headers_get_content_type (message->response_headers, NULL); + GCAL_TRACE_MSG ("Message retrieved, content type: %s, status code: %u", content_type, message->status_code); + + if (is_authentication_error (message->status_code)) + { + g_set_error (error, + GCAL_SOURCE_DISCOVERER_ERROR, + GCAL_SOURCE_DISCOVERER_ERROR_UNAUTHORIZED, + "%s", + soup_status_get_phrase (message->status_code)); + + GCAL_RETURN (NULL); + } + + if (content_type && g_strstr_len (content_type, -1, "text/calendar") != NULL) + { + source = g_ptr_array_new_full (1, g_object_unref); + g_ptr_array_add (source, create_source_for_uri (data)); + } + + GCAL_RETURN (g_steal_pointer (&source)); +} + +static GPtrArray* +discover_webdav_in_thread (DiscovererData *data, + GCancellable *cancellable, + GError **error) +{ + g_autoptr (ENamedParameters) credentials = NULL; + g_autoptr (GPtrArray) sources = NULL; + g_autoptr (ESource) source = NULL; + g_autoptr (GError) local_error = NULL; + g_autofree gchar *certificate_pem = NULL; + GTlsCertificateFlags flags; + GSList *discovered_sources = NULL; + GSList *user_addresses = NULL; + GSList *l; + + GCAL_ENTRY; + + credentials = e_named_parameters_new (); + e_named_parameters_set (credentials, E_SOURCE_CREDENTIAL_USERNAME, data->username); + e_named_parameters_set (credentials, E_SOURCE_CREDENTIAL_PASSWORD, data->password); + + source = create_source_for_uri (data); + + e_webdav_discover_sources_sync (source, + data->uri, + E_WEBDAV_DISCOVER_SUPPORTS_EVENTS, + credentials, + &certificate_pem, + &flags, + &discovered_sources, + &user_addresses, + cancellable, + &local_error); + + if (local_error) + { + if (is_authentication_error (local_error->code)) + { + g_set_error (error, + GCAL_SOURCE_DISCOVERER_ERROR, + GCAL_SOURCE_DISCOVERER_ERROR_UNAUTHORIZED, + "%s", + soup_status_get_phrase (local_error->code)); + } + else + { + g_propagate_error (error, g_steal_pointer (&local_error)); + } + + GCAL_RETURN (NULL); + } + + sources = g_ptr_array_new_full (g_slist_length (discovered_sources), g_object_unref); + for (l = discovered_sources; l; l = l->next) + { + EWebDAVDiscoveredSource *discovered_source; + ESource *new_source; + + discovered_source = l->data; + new_source = create_discovered_source (source, user_addresses, discovered_source); + + if (!new_source) + continue; + + g_ptr_array_add (sources, new_source); + } + + g_clear_pointer (&discovered_sources, e_webdav_discover_free_discovered_sources); + g_slist_free_full (user_addresses, g_free); + + GCAL_RETURN (g_steal_pointer (&sources)); +} + +static void +discover_sources_in_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr (GError) error = NULL; + DiscovererData *data = task_data; + guint i; + + const DiscoverFunc discover_funcs[] = { + discover_file_in_thread, + discover_webdav_in_thread, + }; + + for (i = 0; i < G_N_ELEMENTS (discover_funcs); i++) + { + g_autoptr (GPtrArray) sources = NULL; + g_autoptr (GError) local_error = NULL; + + sources = discover_funcs[i] (data, cancellable, &local_error); + + if (sources) + { + g_task_return_pointer (task, g_steal_pointer (&sources), NULL); + return; + } + else if (local_error) + { + g_clear_error (&error); + g_propagate_error (&error, g_steal_pointer (&local_error)); + } + } + + if (error) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_UNKNOWN, "Unknown error"); +} + +void +gcal_discover_sources_from_uri (const gchar *uri, + const gchar *username, + const gchar *password, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + DiscovererData *data; + + g_assert (uri != NULL); + + data = g_new0 (DiscovererData, 1); + data->uri = g_strdup (uri); + data->username = g_strdup (username); + data->password = g_strdup (password); + + task = g_task_new (NULL, cancellable, callback, user_data); + g_task_set_source_tag (task, gcal_discover_sources_from_uri); + g_task_set_task_data (task, data, discoverer_data_free); + + g_task_run_in_thread (task, discover_sources_in_thread_cb); +} + +GPtrArray* +gcal_discover_sources_from_uri_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, NULL), NULL); + g_return_val_if_fail (!error || !*error, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} diff --git a/src/utils/gcal-source-discoverer.h b/src/utils/gcal-source-discoverer.h new file mode 100644 index 00000000..6e95bd2b --- /dev/null +++ b/src/utils/gcal-source-discoverer.h @@ -0,0 +1,52 @@ +/* gcal-source-discoverer.h + * + * 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 + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +/** + * GcalDiscovererError: + * @GCAL_SOURCE_DISCOVERER_ERROR_UNAUTHORIZED: indicated an invalid start date + * + * Errors that can generate. + */ +typedef enum +{ + GCAL_SOURCE_DISCOVERER_ERROR_UNAUTHORIZED, +} GcalSourceDiscovererError; + +#define GCAL_SOURCE_DISCOVERER_ERROR (gcal_source_discoverer_error_quark ()) + +GQuark gcal_source_discoverer_error_quark (void); + +void gcal_discover_sources_from_uri (const gchar *uri, + const gchar *username, + const gchar *password, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +GPtrArray* gcal_discover_sources_from_uri_finish (GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/tests/gcal-simple-server.c b/tests/gcal-simple-server.c index 21ad7db8..15b3f010 100644 --- a/tests/gcal-simple-server.c +++ b/tests/gcal-simple-server.c @@ -88,7 +88,7 @@ static void process_caldav (SoupMessage *message, const gchar *path) { - g_message ("Processing CalDAV"); + g_debug ("Processing CalDAV request"); } diff --git a/tests/meson.build b/tests/meson.build index a45a00ef..ec704dfd 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -33,6 +33,7 @@ libgcal_test_dep = declare_dependency( tests = [ 'server', + 'discoverer', 'event', 'manager', ] diff --git a/tests/test-discoverer.c b/tests/test-discoverer.c new file mode 100644 index 00000000..73ce1016 --- /dev/null +++ b/tests/test-discoverer.c @@ -0,0 +1,190 @@ +/* test-discoverer.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 + */ + + +#include +#include + +#include "gcal-simple-server.h" +#include "gcal-source-discoverer.h" + +static GcalSimpleServer* +init_server (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + + server = gcal_simple_server_new (); + gcal_simple_server_start (server); + + return g_steal_pointer (&server); +} + +/*********************************************************************************************************************/ + +static void +discovered_file_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GPtrArray) sources = NULL; + g_autoptr (GError) error = NULL; + GMainLoop *mainloop = user_data; + + sources = gcal_discover_sources_from_uri_finish (result, &error); + g_assert_no_error (error); + g_assert_cmpuint (sources->len, ==, 1); + + g_main_loop_quit (mainloop); +} + +static void +discoverer_file (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + g_autoptr (GMainLoop) mainloop = NULL; + g_autoptr (SoupURI) uri = NULL; + g_autofree gchar *uri_str = NULL; + + server = init_server (); + uri = gcal_simple_server_get_uri (server); + soup_uri_set_path (uri, "/public/calendar"); + + uri_str = soup_uri_to_string (uri, FALSE); + + mainloop = g_main_loop_new (NULL, FALSE); + + gcal_discover_sources_from_uri (uri_str, + NULL, + NULL, + NULL, + discovered_file_cb, + mainloop); + + g_main_loop_run (mainloop); +} + +/*********************************************************************************************************************/ + +static void +discovered_webdav_no_auth_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GPtrArray) sources = NULL; + g_autoptr (GError) error = NULL; + GMainLoop *mainloop = user_data; + + sources = gcal_discover_sources_from_uri_finish (result, &error); + g_assert_error (error, GCAL_SOURCE_DISCOVERER_ERROR, GCAL_SOURCE_DISCOVERER_ERROR_UNAUTHORIZED); + + g_main_loop_quit (mainloop); +} + +static void +discoverer_webdav_no_auth (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + g_autoptr (GMainLoop) mainloop = NULL; + g_autoptr (SoupURI) uri = NULL; + g_autofree gchar *uri_str = NULL; + + server = init_server (); + uri = gcal_simple_server_get_uri (server); + soup_uri_set_path (uri, "/secret-area/calendar"); + + uri_str = soup_uri_to_string (uri, FALSE); + + mainloop = g_main_loop_new (NULL, FALSE); + + gcal_discover_sources_from_uri (uri_str, + NULL, + NULL, + NULL, + discovered_webdav_no_auth_cb, + mainloop); + + g_main_loop_run (mainloop); +} + +/*********************************************************************************************************************/ + +#if 0 + +// TODO: Implement raw CalDAV server in GcalSimpleServer + +static void +discoverer_webdav_auth_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GPtrArray) sources = NULL; + g_autoptr (GError) error = NULL; + GMainLoop *mainloop = user_data; + + sources = gcal_discover_sources_from_uri_finish (result, &error); + g_assert_no_error (error); + + g_main_loop_quit (mainloop); +} + +static void +discoverer_webdav_auth (void) +{ + g_autoptr (GcalSimpleServer) server = NULL; + g_autoptr (GMainLoop) mainloop = NULL; + g_autoptr (SoupURI) uri = NULL; + g_autofree gchar *uri_str = NULL; + + server = init_server (); + uri = gcal_simple_server_get_uri (server); + soup_uri_set_path (uri, "/secret-area/dav"); + + uri_str = soup_uri_to_string (uri, FALSE); + + mainloop = g_main_loop_new (NULL, FALSE); + + gcal_discover_sources_from_uri (uri_str, + "feaneron", + "idonotmaintainanything", + NULL, + discoverer_webdav_auth_cb, + mainloop); + + g_main_loop_run (mainloop); +} + +#endif + +/*********************************************************************************************************************/ + +gint +main (gint argc, + gchar *argv[]) +{ + g_setenv ("TZ", "UTC", TRUE); + + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/discoverer/file", discoverer_file); + g_test_add_func ("/discoverer/webdav/no-auth", discoverer_webdav_no_auth); + + return g_test_run (); +} +