/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ /* * Copyright © 2012 – 2017 Red Hat, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General * Public License along with this library; if not, see . */ /* Based on code by the Evolution team. * * This was originally written as a part of evolution-ews: * evolution-ews/src/server/e-ews-connection.c */ #include "config.h" #include #include #include #include "goaewsclient.h" #include "goautils.h" struct _GoaEwsClient { GObject parent_instance; }; G_DEFINE_TYPE (GoaEwsClient, goa_ews_client, G_TYPE_OBJECT); /* ---------------------------------------------------------------------------------------------------- */ static void goa_ews_client_init (GoaEwsClient *self) { } static void goa_ews_client_class_init (GoaEwsClientClass *self) { } /* ---------------------------------------------------------------------------------------------------- */ GoaEwsClient * goa_ews_client_new (void) { return GOA_EWS_CLIENT (g_object_new (GOA_TYPE_EWS_CLIENT, NULL)); } /* ---------------------------------------------------------------------------------------------------- */ typedef struct { GCancellable *cancellable; GSimpleAsyncResult *res; SoupMessage *msgs[2]; SoupSession *session; gboolean accept_ssl_errors; guint pending; gulong cancellable_id; xmlOutputBuffer *buf; } AutodiscoverData; typedef struct { gchar *password; gchar *username; } AutodiscoverAuthData; static gboolean ews_client_autodiscover_data_free (gpointer user_data) { AutodiscoverData *data = user_data; g_simple_async_result_complete_in_idle (data->res); if (data->cancellable_id > 0) { g_cancellable_disconnect (data->cancellable, data->cancellable_id); g_object_unref (data->cancellable); } /* soup_session_queue_message stole the references to data->msgs */ xmlOutputBufferClose (data->buf); g_object_unref (data->res); g_object_unref (data->session); g_slice_free (AutodiscoverData, data); return G_SOURCE_REMOVE; } static void ews_client_autodiscover_auth_data_free (gpointer data, GClosure *closure) { AutodiscoverAuthData *auth = data; g_free (auth->password); g_free (auth->username); g_slice_free (AutodiscoverAuthData, auth); } static gboolean ews_client_check_node (const xmlNode *node, const gchar *name) { g_return_val_if_fail (node != NULL, FALSE); return node->type == XML_ELEMENT_NODE && !g_strcmp0 ((gchar *) node->name, name); } static void ews_client_authenticate (SoupSession *session, SoupMessage *msg, SoupAuth *auth, gboolean retrying, gpointer user_data) { AutodiscoverAuthData *data = user_data; if (retrying) return; soup_auth_authenticate (auth, data->username, data->password); } static void ews_client_request_started (SoupSession *session, SoupMessage *msg, SoupSocket *socket, gpointer user_data) { AutodiscoverData *data = user_data; GError *error; GTlsCertificateFlags cert_flags; error = NULL; if (!data->accept_ssl_errors && soup_message_get_https_status (msg, NULL, &cert_flags) && cert_flags != 0) { goa_utils_set_error_ssl (&error, cert_flags); g_simple_async_result_take_error (data->res, error); soup_session_abort (data->session); } } static void ews_client_autodiscover_cancelled_cb (GCancellable *cancellable, gpointer user_data) { AutodiscoverData *data = user_data; soup_session_abort (data->session); } static gboolean ews_client_autodiscover_parse_protocol (xmlNode *node) { gboolean as_url = FALSE; gboolean oab_url = FALSE; for (node = node->children; node; node = node->next) { if (ews_client_check_node (node, "ASUrl")) as_url = TRUE; else if (ews_client_check_node (node, "OABUrl")) oab_url = TRUE; if (as_url && oab_url) break; } return as_url && oab_url; } static void ews_client_autodiscover_response_cb (SoupSession *session, SoupMessage *msg, gpointer user_data) { GError *error = NULL; AutodiscoverData *data = user_data; gboolean op_res = FALSE; guint idx; guint status; gsize size; xmlDoc *doc; xmlNode *node; size = sizeof (data->msgs) / sizeof (data->msgs[0]); for (idx = 0; idx < size; idx++) { if (data->msgs[idx] == msg) break; } if (idx == size || data->pending == 0) return; data->msgs[idx] = NULL; status = msg->status_code; /* status == SOUP_STATUS_CANCELLED, if we are being aborted by the * GCancellable, an SSL error or another message that was * successful. */ if (status == SOUP_STATUS_CANCELLED) { /* If a previous autodiscover attempt for the same GAsyncResult * was successful then no additional attempts are required and * we should use the result from the earlier attempt. */ op_res = g_simple_async_result_get_op_res_gboolean (data->res); goto out; } else if (status != SOUP_STATUS_OK) { g_warning ("goa_ews_client_autodiscover() failed: %u — %s", msg->status_code, msg->reason_phrase); goa_utils_set_error_soup (&error, msg); goto out; } soup_buffer_free (soup_message_body_flatten (SOUP_MESSAGE (msg)->response_body)); g_debug ("The response headers"); g_debug ("==================="); g_debug ("%s", SOUP_MESSAGE (msg)->response_body->data); doc = xmlReadMemory (msg->response_body->data, msg->response_body->length, "autodiscover.xml", NULL, 0); if (doc == NULL) { g_set_error (&error, GOA_ERROR, GOA_ERROR_FAILED, /* TODO: more specific */ _("Failed to parse autodiscover response XML")); goto out; } node = xmlDocGetRootElement (doc); if (g_strcmp0 ((gchar *) node->name, "Autodiscover")) { g_set_error (&error, GOA_ERROR, GOA_ERROR_FAILED, /* TODO: more specific */ /* Translators: the parameter is an XML element name. */ _("Failed to find “%s” element"), "Autodiscover"); goto out; } for (node = node->children; node; node = node->next) { if (ews_client_check_node (node, "Response")) break; } if (node == NULL) { g_set_error (&error, GOA_ERROR, GOA_ERROR_FAILED, /* TODO: more specific */ /* Translators: the parameter is an XML element name. */ _("Failed to find “%s” element"), "Response"); goto out; } for (node = node->children; node; node = node->next) { if (ews_client_check_node (node, "Account")) break; } if (node == NULL) { g_set_error (&error, GOA_ERROR, GOA_ERROR_FAILED, /* TODO: more specific */ /* Translators: the parameter is an XML element name. */ _("Failed to find “%s” element"), "Account"); goto out; } for (node = node->children; node; node = node->next) { if (ews_client_check_node (node, "Protocol")) { op_res = ews_client_autodiscover_parse_protocol (node); /* Since the server may send back multiple nodes * don't break unless we found the one we want. */ if (op_res) break; } } if (!op_res) { g_set_error (&error, GOA_ERROR, GOA_ERROR_FAILED, /* TODO: more specific*/ _("Failed to find ASUrl and OABUrl in autodiscover response")); goto out; } /* This autodiscover attempt was successful. Save the result now so * that it won't get lost when we hear from another autodiscover * attempt for the same GAsyncResult. */ g_simple_async_result_set_op_res_gboolean (data->res, op_res); for (idx = 0; idx < size; idx++) { if (data->msgs[idx] != NULL) { /* The callback (ie. this function) will be invoked after we * have returned to the main loop. */ soup_session_cancel_message (data->session, data->msgs[idx], SOUP_STATUS_CANCELLED); } } out: /* error == NULL, if we are being aborted by the GCancellable, an * SSL error or another message that was successful. */ if (!op_res) { /* There's another request outstanding. * Hope that it has better luck. */ if (data->pending > 1) g_clear_error (&error); if (error != NULL) g_simple_async_result_take_error (data->res, error); } data->pending--; if (data->pending == 0) { GMainContext *context; GSource *source; /* The result of the GAsyncResult should already be set when we * get here. If it wasn't explicitly set to TRUE then * autodiscovery has failed and the default value of the * GAsyncResult (which is FALSE) should be returned to the * original caller. */ source = g_idle_source_new (); g_source_set_priority (source, G_PRIORITY_DEFAULT_IDLE); g_source_set_callback (source, ews_client_autodiscover_data_free, data, NULL); g_source_set_name (source, "[goa] ews_client_autodiscover_data_free"); context = g_main_context_get_thread_default (); g_source_attach (source, context); g_source_unref (source); } } static xmlDoc * ews_client_create_autodiscover_xml (const gchar *email) { xmlDoc *doc; xmlNode *node; xmlNs *ns; doc = xmlNewDoc ((xmlChar *) "1.0"); node = xmlNewDocNode (doc, NULL, (xmlChar *) "Autodiscover", NULL); xmlDocSetRootElement (doc, node); ns = xmlNewNs (node, (xmlChar *) "http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006", NULL); node = xmlNewChild (node, ns, (xmlChar *) "Request", NULL); xmlNewChild (node, ns, (xmlChar *) "EMailAddress", (xmlChar *) email); xmlNewChild (node, ns, (xmlChar *) "AcceptableResponseSchema", (xmlChar *) "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"); return doc; } static void ews_client_post_restarted_cb (SoupMessage *msg, gpointer data) { xmlOutputBuffer *buf = data; /* In violation of RFC2616, libsoup will change a POST request to * a GET on receiving a 302 redirect. */ g_debug ("Working around libsoup bug with redirect"); g_object_set (msg, SOUP_MESSAGE_METHOD, "POST", NULL); soup_message_set_request(msg, "text/xml; charset=utf-8", SOUP_MEMORY_COPY, #ifdef LIBXML2_NEW_BUFFER (gchar *) xmlOutputBufferGetContent(buf), xmlOutputBufferGetSize(buf)); #else (gchar *) buf->buffer->content, buf->buffer->use); #endif } static SoupMessage * ews_client_create_msg_for_url (const gchar *url, xmlOutputBuffer *buf) { SoupMessage *msg; msg = soup_message_new (buf != NULL ? "POST" : "GET", url); soup_message_headers_append (msg->request_headers, "User-Agent", "libews/0.1"); if (buf != NULL) { soup_message_set_request (msg, "text/xml; charset=utf-8", SOUP_MEMORY_COPY, #ifdef LIBXML2_NEW_BUFFER (gchar *) xmlOutputBufferGetContent(buf), xmlOutputBufferGetSize(buf)); #else (gchar *) buf->buffer->content, buf->buffer->use); #endif g_signal_connect (msg, "restarted", G_CALLBACK (ews_client_post_restarted_cb), buf); } soup_buffer_free (soup_message_body_flatten (SOUP_MESSAGE (msg)->request_body)); g_debug ("The request headers"); g_debug ("==================="); g_debug ("%s", SOUP_MESSAGE (msg)->request_body->data); return msg; } void goa_ews_client_autodiscover (GoaEwsClient *self, const gchar *email, const gchar *password, const gchar *username, const gchar *server, gboolean accept_ssl_errors, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { AutodiscoverData *data; AutodiscoverAuthData *auth; gchar *url1; gchar *url2; xmlDoc *doc; xmlOutputBuffer *buf; g_return_if_fail (GOA_IS_EWS_CLIENT (self)); g_return_if_fail (email != NULL && email[0] != '\0'); g_return_if_fail (password != NULL && password[0] != '\0'); g_return_if_fail (username != NULL && username[0] != '\0'); g_return_if_fail (server != NULL && server[0] != '\0'); g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); doc = ews_client_create_autodiscover_xml (email); buf = xmlAllocOutputBuffer (NULL); xmlNodeDumpOutput (buf, doc, xmlDocGetRootElement (doc), 0, 1, NULL); xmlOutputBufferFlush (buf); url1 = g_strdup_printf ("https://%s/autodiscover/autodiscover.xml", server); url2 = g_strdup_printf ("https://autodiscover.%s/autodiscover/autodiscover.xml", server); /* http://msdn.microsoft.com/en-us/library/ee332364.aspx says we are * supposed to try $domain and then autodiscover.$domain. But some * people have broken firewalls on the former which drop packets * instead of rejecting connections, and make the request take ages * to time out. So run both queries in parallel and let the fastest * (successful) one win. */ data = g_slice_new0 (AutodiscoverData); data->buf = buf; data->res = g_simple_async_result_new (G_OBJECT (self), callback, user_data, goa_ews_client_autodiscover); data->msgs[0] = ews_client_create_msg_for_url (url1, buf); data->msgs[1] = ews_client_create_msg_for_url (url2, buf); data->pending = sizeof (data->msgs) / sizeof (data->msgs[0]); data->session = soup_session_new_with_options (SOUP_SESSION_SSL_STRICT, FALSE, NULL); soup_session_add_feature_by_type (data->session, SOUP_TYPE_AUTH_NTLM); data->accept_ssl_errors = accept_ssl_errors; if (cancellable != NULL) { data->cancellable = g_object_ref (cancellable); data->cancellable_id = g_cancellable_connect (data->cancellable, G_CALLBACK (ews_client_autodiscover_cancelled_cb), data, NULL); g_simple_async_result_set_check_cancellable (data->res, data->cancellable); } auth = g_slice_new0 (AutodiscoverAuthData); auth->username = g_strdup (username); auth->password = g_strdup (password); g_signal_connect_data (data->session, "authenticate", G_CALLBACK (ews_client_authenticate), auth, ews_client_autodiscover_auth_data_free, 0); g_signal_connect (data->session, "request-started", G_CALLBACK (ews_client_request_started), data); soup_session_queue_message (data->session, data->msgs[0], ews_client_autodiscover_response_cb, data); soup_session_queue_message (data->session, data->msgs[1], ews_client_autodiscover_response_cb, data); g_free (url2); g_free (url1); xmlFreeDoc (doc); } gboolean goa_ews_client_autodiscover_finish (GoaEwsClient *self, GAsyncResult *res, GError **error) { GSimpleAsyncResult *simple; g_return_val_if_fail (g_simple_async_result_is_valid (res, G_OBJECT (self), goa_ews_client_autodiscover), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); simple = G_SIMPLE_ASYNC_RESULT (res); if (g_simple_async_result_propagate_error (simple, error)) return FALSE; return g_simple_async_result_get_op_res_gboolean (simple); } /* ---------------------------------------------------------------------------------------------------- */ typedef struct { GError **error; GMainLoop *loop; gboolean op_res; } AutodiscoverSyncData; static void ews_client_autodiscover_sync_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) { AutodiscoverSyncData *data = user_data; data->op_res = goa_ews_client_autodiscover_finish (GOA_EWS_CLIENT (source_object), res, data->error); g_main_loop_quit (data->loop); } gboolean goa_ews_client_autodiscover_sync (GoaEwsClient *self, const gchar *email, const gchar *password, const gchar *username, const gchar *server, gboolean accept_ssl_errors, GCancellable *cancellable, GError **error) { AutodiscoverSyncData data; GMainContext *context = NULL; data.error = error; context = g_main_context_new (); g_main_context_push_thread_default (context); data.loop = g_main_loop_new (context, FALSE); goa_ews_client_autodiscover (self, email, password, username, server, accept_ssl_errors, cancellable, ews_client_autodiscover_sync_cb, &data); g_main_loop_run (data.loop); g_main_loop_unref (data.loop); g_main_context_pop_thread_default (context); g_main_context_unref (context); return data.op_res; }