84374b921851d33bc1acc8224c26dc873d9ac4ba
[platform/upstream/evolution-data-server.git] / modules / online-accounts / goaewsclient.c
1 /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
2 /*
3  * Copyright (C) 2012 Red Hat, Inc.
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General
16  * Public License along with this library; if not, write to the
17  * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
18  * Boston, MA 02111-1307, USA.
19  *
20  * Author: Debarshi Ray <debarshir@gnome.org>
21  */
22
23 /* Based on code by the Evolution team.
24  *
25  * This was originally written as a part of evolution-ews:
26  * evolution-ews/src/server/e-ews-connection.c
27  */
28
29 #include <config.h>
30 #include <glib/gi18n-lib.h>
31
32 #include <libsoup/soup.h>
33 #include <libxml/xmlIO.h>
34
35 #include <libedataserver/libedataserver.h>
36
37 #include "goaewsclient.h"
38
39 typedef struct {
40         GCancellable *cancellable;
41         SoupMessage *msgs[2];
42         SoupSession *session;
43         gulong cancellable_id;
44         xmlOutputBuffer *buf;
45
46         /* results */
47         gchar *as_url;
48         gchar *oab_url;
49 } AutodiscoverData;
50
51 typedef struct {
52         gchar *password;
53         gchar *username;
54 } AutodiscoverAuthData;
55
56 #ifdef HAVE_GOA_PASSWORD_BASED
57 static void
58 ews_autodiscover_data_free (AutodiscoverData *data)
59 {
60         if (data->cancellable_id > 0) {
61                 g_cancellable_disconnect (
62                         data->cancellable, data->cancellable_id);
63                 g_object_unref (data->cancellable);
64         }
65
66         /* soup_session_queue_message stole the references to data->msgs */
67         xmlOutputBufferClose (data->buf);
68         g_object_unref (data->session);
69
70         g_free (data->as_url);
71         g_free (data->oab_url);
72
73         g_slice_free (AutodiscoverData, data);
74 }
75
76 static void
77 ews_autodiscover_auth_data_free (gpointer data,
78                                  GClosure *closure)
79 {
80         AutodiscoverAuthData *auth = data;
81
82         g_free (auth->password);
83         g_free (auth->username);
84         g_slice_free (AutodiscoverAuthData, auth);
85 }
86
87 static gboolean
88 ews_check_node (const xmlNode *node,
89                 const gchar *name)
90 {
91         g_return_val_if_fail (node != NULL, FALSE);
92
93         return (node->type == XML_ELEMENT_NODE) &&
94                 (g_strcmp0 ((gchar *) node->name, name) == 0);
95 }
96
97 static void
98 ews_authenticate (SoupSession *session,
99                   SoupMessage *msg,
100                   SoupAuth *auth,
101                   gboolean retrying,
102                   AutodiscoverAuthData *data)
103 {
104         if (retrying)
105                 return;
106
107         soup_auth_authenticate (auth, data->username, data->password);
108 }
109
110 static void
111 ews_autodiscover_cancelled_cb (GCancellable *cancellable,
112                                AutodiscoverData *data)
113 {
114         soup_session_abort (data->session);
115 }
116
117 static gboolean
118 ews_autodiscover_parse_protocol (xmlNode *node,
119                                  AutodiscoverData *data)
120 {
121         gboolean got_as_url = FALSE;
122         gboolean got_oab_url = FALSE;
123
124         for (node = node->children; node; node = node->next) {
125                 xmlChar *content;
126
127                 if (ews_check_node (node, "ASUrl")) {
128                         content = xmlNodeGetContent (node);
129                         data->as_url = g_strdup ((gchar *) content);
130                         xmlFree (content);
131                         got_as_url = TRUE;
132
133                 } else if (ews_check_node (node, "OABUrl")) {
134                         content = xmlNodeGetContent (node);
135                         data->oab_url = g_strdup ((gchar *) content);
136                         xmlFree (content);
137                         got_oab_url = TRUE;
138                 }
139
140                 if (got_as_url && got_oab_url)
141                         break;
142         }
143
144         return (got_as_url && got_oab_url);
145 }
146
147 static void
148 ews_autodiscover_response_cb (SoupSession *session,
149                               SoupMessage *msg,
150                               gpointer user_data)
151 {
152         GSimpleAsyncResult *simple;
153         AutodiscoverData *data;
154         gboolean success = FALSE;
155         guint status;
156         gint idx;
157         gsize size;
158         xmlDoc *doc;
159         xmlNode *node;
160         GError *error = NULL;
161
162         simple = G_SIMPLE_ASYNC_RESULT (user_data);
163         data = g_simple_async_result_get_op_res_gpointer (simple);
164
165         status = msg->status_code;
166         if (status == SOUP_STATUS_CANCELLED)
167                 return;
168
169         size = sizeof (data->msgs) / sizeof (data->msgs[0]);
170
171         for (idx = 0; idx < size; idx++) {
172                 if (data->msgs[idx] == msg)
173                         break;
174         }
175         if (idx == size)
176                 return;
177
178         data->msgs[idx] = NULL;
179
180         if (status != SOUP_STATUS_OK) {
181                 g_set_error (
182                         &error, GOA_ERROR,
183                         GOA_ERROR_FAILED, /* TODO: more specific */
184                         _("Code: %u - Unexpected response from server"),
185                         status);
186                 goto out;
187         }
188
189         soup_buffer_free (
190                 soup_message_body_flatten (
191                 SOUP_MESSAGE (msg)->response_body));
192
193         g_debug ("The response headers");
194         g_debug ("===================");
195         g_debug ("%s", SOUP_MESSAGE (msg)->response_body->data);
196
197         doc = xmlReadMemory (
198                 msg->response_body->data,
199                 msg->response_body->length,
200                 "autodiscover.xml", NULL, 0);
201         if (doc == NULL) {
202                 g_set_error (
203                         &error, GOA_ERROR,
204                         GOA_ERROR_FAILED, /* TODO: more specific */
205                         _("Failed to parse autodiscover response XML"));
206                 goto out;
207         }
208
209         node = xmlDocGetRootElement (doc);
210         if (g_strcmp0 ((gchar *) node->name, "Autodiscover") != 0) {
211                 g_set_error (
212                         &error, GOA_ERROR,
213                         GOA_ERROR_FAILED, /* TODO: more specific */
214                         _("Failed to find Autodiscover element"));
215                 goto out;
216         }
217
218         for (node = node->children; node; node = node->next) {
219                 if (ews_check_node (node, "Response"))
220                         break;
221         }
222         if (node == NULL) {
223                 g_set_error (
224                         &error, GOA_ERROR,
225                         GOA_ERROR_FAILED, /* TODO: more specific */
226                         _("Failed to find Response element"));
227                 goto out;
228         }
229
230         for (node = node->children; node; node = node->next) {
231                 if (ews_check_node (node, "Account"))
232                         break;
233         }
234         if (node == NULL) {
235                 g_set_error (
236                         &error, GOA_ERROR,
237                         GOA_ERROR_FAILED, /* TODO: more specific */
238                         _("Failed to find Account element"));
239                 goto out;
240         }
241
242         for (node = node->children; node; node = node->next) {
243                 if (ews_check_node (node, "Protocol")) {
244                         success = ews_autodiscover_parse_protocol (node, data);
245                         break;
246                 }
247         }
248         if (!success) {
249                 g_set_error (
250                         &error, GOA_ERROR,
251                         GOA_ERROR_FAILED, /* TODO: more specific */
252                         _("Failed to find ASUrl and OABUrl in autodiscover response"));
253                         goto out;
254         }
255
256         for (idx = 0; idx < size; idx++) {
257                 if (data->msgs[idx] != NULL) {
258                         /* Since we are cancelling from the same thread
259                          * that we queued the message, the callback (ie.
260                          * this function) will be invoked before
261                          * soup_session_cancel_message returns. */
262                         soup_session_cancel_message (
263                                 data->session, data->msgs[idx],
264                                 SOUP_STATUS_CANCELLED);
265                         data->msgs[idx] = NULL;
266                 }
267         }
268
269 out:
270         if (error != NULL) {
271                 for (idx = 0; idx < size; idx++) {
272                         if (data->msgs[idx] != NULL) {
273                                 /* There's another request outstanding.
274                                  * Hope that it has better luck. */
275                                 g_clear_error (&error);
276                                 return;
277                         }
278                 }
279                 g_simple_async_result_take_error (simple, error);
280         }
281
282         g_simple_async_result_complete_in_idle (simple);
283         g_object_unref (simple);
284 }
285
286 static xmlDoc *
287 ews_create_autodiscover_xml (const gchar *email)
288 {
289         xmlDoc *doc;
290         xmlNode *node;
291         xmlNs *ns;
292
293         doc = xmlNewDoc ((xmlChar *) "1.0");
294
295         node = xmlNewDocNode (doc, NULL, (xmlChar *) "Autodiscover", NULL);
296         xmlDocSetRootElement (doc, node);
297         ns = xmlNewNs (
298                 node,
299                 (xmlChar *) "http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006",
300                 NULL);
301
302         node = xmlNewChild (node, ns, (xmlChar *) "Request", NULL);
303         xmlNewChild (node, ns, (xmlChar *) "EMailAddress", (xmlChar *) email);
304         xmlNewChild (
305                 node, ns,
306                 (xmlChar *) "AcceptableResponseSchema",
307                 (xmlChar *) "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a");
308
309         return doc;
310 }
311
312 static void
313 ews_post_restarted_cb (SoupMessage *msg,
314                        gpointer data)
315 {
316         xmlOutputBuffer *buf = data;
317
318         /* In violation of RFC2616, libsoup will change a
319          * POST request to a GET on receiving a 302 redirect. */
320         g_debug ("Working around libsoup bug with redirect");
321         g_object_set (msg, SOUP_MESSAGE_METHOD, "POST", NULL);
322
323         soup_message_set_request (
324                 msg, "text/xml; charset=utf-8",
325                 SOUP_MEMORY_COPY,
326                 (gchar *) buf->buffer->content,
327                 buf->buffer->use);
328 }
329
330 static SoupMessage *
331 ews_create_msg_for_url (const gchar *url,
332                         xmlOutputBuffer *buf)
333 {
334         SoupMessage *msg;
335
336         msg = soup_message_new (buf != NULL ? "POST" : "GET", url);
337         soup_message_headers_append (
338                 msg->request_headers, "User-Agent", "libews/0.1");
339
340         if (buf != NULL) {
341                 soup_message_set_request (
342                         msg, "text/xml; charset=utf-8",
343                         SOUP_MEMORY_COPY,
344                         (gchar *) buf->buffer->content,
345                         buf->buffer->use);
346                 g_signal_connect (
347                         msg, "restarted",
348                         G_CALLBACK (ews_post_restarted_cb), buf);
349         }
350
351         soup_buffer_free (
352                 soup_message_body_flatten (
353                 SOUP_MESSAGE (msg)->request_body));
354
355         g_debug ("The request headers");
356         g_debug ("===================");
357         g_debug ("%s", SOUP_MESSAGE (msg)->request_body->data);
358
359         return msg;
360 }
361 #endif /* HAVE_GOA_PASSWORD_BASED */
362
363 void
364 goa_ews_autodiscover (GoaObject *goa_object,
365                       GCancellable *cancellable,
366                       GAsyncReadyCallback callback,
367                       gpointer user_data)
368 {
369         /* XXX This function is only called if HAVE_GOA_PASSWORD_BASED
370          *     is defined, so don't worry about a fallback behavior. */
371 #ifdef HAVE_GOA_PASSWORD_BASED
372         GoaAccount *goa_account;
373         GoaExchange *goa_exchange;
374         GoaPasswordBased *goa_password;
375         GSimpleAsyncResult *simple;
376         AutodiscoverData *data;
377         AutodiscoverAuthData *auth;
378         gchar *url1;
379         gchar *url2;
380         xmlDoc *doc;
381         xmlOutputBuffer *buf;
382         gchar *email;
383         gchar *host;
384         gchar *password = NULL;
385         GError *error = NULL;
386
387         g_return_if_fail (GOA_IS_OBJECT (goa_object));
388
389         goa_account = goa_object_get_account (goa_object);
390         goa_exchange = goa_object_get_exchange (goa_object);
391         goa_password = goa_object_get_password_based (goa_object);
392
393         email = goa_account_dup_presentation_identity (goa_account);
394         host = goa_exchange_dup_host (goa_exchange);
395
396         doc = ews_create_autodiscover_xml (email);
397         buf = xmlAllocOutputBuffer (NULL);
398         xmlNodeDumpOutput (buf, doc, xmlDocGetRootElement (doc), 0, 1, NULL);
399         xmlOutputBufferFlush (buf);
400
401         url1 = g_strdup_printf (
402                 "https://%s/autodiscover/autodiscover.xml", host);
403         url2 = g_strdup_printf (
404                 "https://autodiscover.%s/autodiscover/autodiscover.xml", host);
405
406         g_free (host);
407         g_free (email);
408
409         /* http://msdn.microsoft.com/en-us/library/ee332364.aspx says we are
410         * supposed to try $domain and then autodiscover.$domain. But some
411         * people have broken firewalls on the former which drop packets
412         * instead of rejecting connections, and make the request take ages
413         * to time out. So run both queries in parallel and let the fastest
414         * (successful) one win. */
415         data = g_slice_new0 (AutodiscoverData);
416         data->buf = buf;
417         data->msgs[0] = ews_create_msg_for_url (url1, buf);
418         data->msgs[1] = ews_create_msg_for_url (url2, buf);
419         data->session = soup_session_async_new_with_options (
420                 SOUP_SESSION_USE_NTLM, TRUE,
421                 SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
422                 SOUP_SESSION_TIMEOUT, 90,
423                 NULL);
424         if (G_IS_CANCELLABLE (cancellable)) {
425                 data->cancellable = g_object_ref (cancellable);
426                 data->cancellable_id = g_cancellable_connect (
427                         data->cancellable,
428                         G_CALLBACK (ews_autodiscover_cancelled_cb),
429                         data, NULL);
430         }
431
432         simple = g_simple_async_result_new (
433                 G_OBJECT (goa_object), callback,
434                 user_data, goa_ews_autodiscover);
435
436         g_simple_async_result_set_check_cancellable (simple, cancellable);
437
438         g_simple_async_result_set_op_res_gpointer (
439                 simple, data, (GDestroyNotify) ews_autodiscover_data_free);
440
441         goa_password_based_call_get_password_sync (
442                 goa_password, "", &password, cancellable, &error);
443
444         /* Sanity check */
445         g_return_if_fail (
446                 ((password != NULL) && (error == NULL)) ||
447                 ((password == NULL) && (error != NULL)));
448
449         if (error == NULL) {
450                 gchar *username;
451
452                 username = goa_account_dup_identity (goa_account);
453
454                 auth = g_slice_new0 (AutodiscoverAuthData);
455                 auth->username = username;  /* takes ownership */
456                 auth->password = password;  /* takes ownership */
457
458                 g_signal_connect_data (
459                         data->session, "authenticate",
460                         G_CALLBACK (ews_authenticate), auth,
461                         ews_autodiscover_auth_data_free, 0);
462
463                 soup_session_queue_message (
464                         data->session, data->msgs[0],
465                         ews_autodiscover_response_cb, simple);
466                 soup_session_queue_message (
467                         data->session, data->msgs[1],
468                         ews_autodiscover_response_cb, simple);
469         } else {
470                 g_simple_async_result_take_error (simple, error);
471                 g_simple_async_result_complete_in_idle (simple);
472                 g_object_unref (simple);
473         }
474
475         g_free (url2);
476         g_free (url1);
477         xmlFreeDoc (doc);
478
479         g_object_unref (goa_account);
480         g_object_unref (goa_exchange);
481         g_object_unref (goa_password);
482 #endif /* HAVE_GOA_PASSWORD_BASED */
483 }
484
485 gboolean
486 goa_ews_autodiscover_finish (GoaObject *goa_object,
487                              GAsyncResult *result,
488                              gchar **out_as_url,
489                              gchar **out_oab_url,
490                              GError **error)
491 {
492         GSimpleAsyncResult *simple;
493         AutodiscoverData *data;
494
495         g_return_val_if_fail (
496                 g_simple_async_result_is_valid (
497                 result, G_OBJECT (goa_object),
498                 goa_ews_autodiscover), FALSE);
499
500         simple = G_SIMPLE_ASYNC_RESULT (result);
501         data = g_simple_async_result_get_op_res_gpointer (simple);
502
503         if (g_simple_async_result_propagate_error (simple, error))
504                 return FALSE;
505
506         if (out_as_url != NULL) {
507                 *out_as_url = data->as_url;
508                 data->as_url = NULL;
509         }
510
511         if (out_oab_url != NULL) {
512                 *out_oab_url = data->oab_url;
513                 data->oab_url = NULL;
514         }
515
516         return TRUE;
517 }
518
519 gboolean
520 goa_ews_autodiscover_sync (GoaObject *goa_object,
521                            gchar **out_as_url,
522                            gchar **out_oab_url,
523                            GCancellable *cancellable,
524                            GError **error)
525 {
526         EAsyncClosure *closure;
527         GAsyncResult *result;
528         gboolean success;
529
530         g_return_val_if_fail (GOA_IS_OBJECT (goa_object), FALSE);
531
532         closure = e_async_closure_new ();
533
534         goa_ews_autodiscover (
535                 goa_object, cancellable,
536                 e_async_closure_callback, closure);
537
538         result = e_async_closure_wait (closure);
539
540         success = goa_ews_autodiscover_finish (
541                 goa_object, result, out_as_url, out_oab_url, error);
542
543         e_async_closure_free (closure);
544
545         return success;
546 }
547