Bug #685090 - GOA EWS module doesn't save all required values
[platform/upstream/evolution-data-server.git] / modules / gnome-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 has_suffix_icmp (const gchar *text,
119                  const gchar *suffix)
120 {
121         gint ii, tlen, slen;
122
123         g_return_val_if_fail (text != NULL, FALSE);
124         g_return_val_if_fail (suffix != NULL, FALSE);
125
126         tlen = strlen (text);
127         slen = strlen (suffix);
128
129         if (!*text || !*suffix || tlen < slen)
130                 return FALSE;
131
132         for (ii = 0; ii < slen; ii++) {
133                 if (g_ascii_tolower (text[tlen - ii - 1]) != 
134                     g_ascii_tolower (suffix[slen - ii - 1]))
135                         break;
136         }
137
138         return ii == slen;
139 }
140
141 static gboolean
142 ews_autodiscover_parse_protocol (xmlNode *node,
143                                  AutodiscoverData *data)
144 {
145         gboolean got_as_url = FALSE;
146         gboolean got_oab_url = FALSE;
147
148         for (node = node->children; node; node = node->next) {
149                 xmlChar *content;
150
151                 if (ews_check_node (node, "ASUrl")) {
152                         content = xmlNodeGetContent (node);
153                         data->as_url = g_strdup ((gchar *) content);
154                         xmlFree (content);
155                         got_as_url = TRUE;
156
157                 } else if (ews_check_node (node, "OABUrl")) {
158                         const gchar *oab_url;
159
160                         content = xmlNodeGetContent (node);
161                         oab_url = (const char *) content;
162
163                         if (!has_suffix_icmp (oab_url, "oab.xml")) {
164                                 gchar *tmp;
165
166                                 if (g_str_has_suffix (oab_url, "/"))
167                                         tmp = g_strconcat (oab_url, "oab.xml", NULL);
168                                 else
169                                         tmp = g_strconcat (oab_url, "/", "oab.xml", NULL);
170
171                                 data->oab_url = tmp; /* takes ownership */
172                         } else {
173                                 data->oab_url = g_strdup (oab_url);
174                         }
175                         xmlFree (content);
176                         got_oab_url = TRUE;
177                 }
178
179                 if (got_as_url && got_oab_url)
180                         break;
181         }
182
183         return (got_as_url && got_oab_url);
184 }
185
186 static void
187 ews_autodiscover_response_cb (SoupSession *session,
188                               SoupMessage *msg,
189                               gpointer user_data)
190 {
191         GSimpleAsyncResult *simple;
192         AutodiscoverData *data;
193         gboolean success = FALSE;
194         guint status;
195         gint idx;
196         gsize size;
197         xmlDoc *doc;
198         xmlNode *node;
199         GError *error = NULL;
200
201         simple = G_SIMPLE_ASYNC_RESULT (user_data);
202         data = g_simple_async_result_get_op_res_gpointer (simple);
203
204         status = msg->status_code;
205         if (status == SOUP_STATUS_CANCELLED)
206                 return;
207
208         size = sizeof (data->msgs) / sizeof (data->msgs[0]);
209
210         for (idx = 0; idx < size; idx++) {
211                 if (data->msgs[idx] == msg)
212                         break;
213         }
214         if (idx == size)
215                 return;
216
217         data->msgs[idx] = NULL;
218
219         if (status != SOUP_STATUS_OK) {
220                 g_set_error (
221                         &error, GOA_ERROR,
222                         GOA_ERROR_FAILED, /* TODO: more specific */
223                         _("Code: %u - Unexpected response from server"),
224                         status);
225                 goto out;
226         }
227
228         soup_buffer_free (
229                 soup_message_body_flatten (
230                 SOUP_MESSAGE (msg)->response_body));
231
232         g_debug ("The response headers");
233         g_debug ("===================");
234         g_debug ("%s", SOUP_MESSAGE (msg)->response_body->data);
235
236         doc = xmlReadMemory (
237                 msg->response_body->data,
238                 msg->response_body->length,
239                 "autodiscover.xml", NULL, 0);
240         if (doc == NULL) {
241                 g_set_error (
242                         &error, GOA_ERROR,
243                         GOA_ERROR_FAILED, /* TODO: more specific */
244                         _("Failed to parse autodiscover response XML"));
245                 goto out;
246         }
247
248         node = xmlDocGetRootElement (doc);
249         if (g_strcmp0 ((gchar *) node->name, "Autodiscover") != 0) {
250                 g_set_error (
251                         &error, GOA_ERROR,
252                         GOA_ERROR_FAILED, /* TODO: more specific */
253                         _("Failed to find Autodiscover element"));
254                 goto out;
255         }
256
257         for (node = node->children; node; node = node->next) {
258                 if (ews_check_node (node, "Response"))
259                         break;
260         }
261         if (node == NULL) {
262                 g_set_error (
263                         &error, GOA_ERROR,
264                         GOA_ERROR_FAILED, /* TODO: more specific */
265                         _("Failed to find Response element"));
266                 goto out;
267         }
268
269         for (node = node->children; node; node = node->next) {
270                 if (ews_check_node (node, "Account"))
271                         break;
272         }
273         if (node == NULL) {
274                 g_set_error (
275                         &error, GOA_ERROR,
276                         GOA_ERROR_FAILED, /* TODO: more specific */
277                         _("Failed to find Account element"));
278                 goto out;
279         }
280
281         for (node = node->children; node; node = node->next) {
282                 if (ews_check_node (node, "Protocol")) {
283                         success = ews_autodiscover_parse_protocol (node, data);
284                         break;
285                 }
286         }
287         if (!success) {
288                 g_set_error (
289                         &error, GOA_ERROR,
290                         GOA_ERROR_FAILED, /* TODO: more specific */
291                         _("Failed to find ASUrl and OABUrl in autodiscover response"));
292                         goto out;
293         }
294
295         for (idx = 0; idx < size; idx++) {
296                 if (data->msgs[idx] != NULL) {
297                         /* Since we are cancelling from the same thread
298                          * that we queued the message, the callback (ie.
299                          * this function) will be invoked before
300                          * soup_session_cancel_message returns. */
301                         soup_session_cancel_message (
302                                 data->session, data->msgs[idx],
303                                 SOUP_STATUS_CANCELLED);
304                         data->msgs[idx] = NULL;
305                 }
306         }
307
308 out:
309         if (error != NULL) {
310                 for (idx = 0; idx < size; idx++) {
311                         if (data->msgs[idx] != NULL) {
312                                 /* There's another request outstanding.
313                                  * Hope that it has better luck. */
314                                 g_clear_error (&error);
315                                 return;
316                         }
317                 }
318                 g_simple_async_result_take_error (simple, error);
319         }
320
321         g_simple_async_result_complete_in_idle (simple);
322         g_object_unref (simple);
323 }
324
325 static xmlDoc *
326 ews_create_autodiscover_xml (const gchar *email)
327 {
328         xmlDoc *doc;
329         xmlNode *node;
330         xmlNs *ns;
331
332         doc = xmlNewDoc ((xmlChar *) "1.0");
333
334         node = xmlNewDocNode (doc, NULL, (xmlChar *) "Autodiscover", NULL);
335         xmlDocSetRootElement (doc, node);
336         ns = xmlNewNs (
337                 node,
338                 (xmlChar *) "http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006",
339                 NULL);
340
341         node = xmlNewChild (node, ns, (xmlChar *) "Request", NULL);
342         xmlNewChild (node, ns, (xmlChar *) "EMailAddress", (xmlChar *) email);
343         xmlNewChild (
344                 node, ns,
345                 (xmlChar *) "AcceptableResponseSchema",
346                 (xmlChar *) "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a");
347
348         return doc;
349 }
350
351 static gconstpointer
352 compat_libxml_output_buffer_get_content (xmlOutputBufferPtr buf,
353                                          gsize *out_len)
354 {
355 #ifdef LIBXML2_NEW_BUFFER
356         *out_len = xmlOutputBufferGetSize (buf);
357         return xmlOutputBufferGetContent (buf);
358 #else
359         *out_len = buf->buffer->use;
360         return buf->buffer->content;
361 #endif
362 }
363
364 static void
365 ews_post_restarted_cb (SoupMessage *msg,
366                        gpointer data)
367 {
368         xmlOutputBuffer *buf = data;
369         gconstpointer buf_content;
370         gsize buf_size;
371
372         /* In violation of RFC2616, libsoup will change a
373          * POST request to a GET on receiving a 302 redirect. */
374         g_debug ("Working around libsoup bug with redirect");
375         g_object_set (msg, SOUP_MESSAGE_METHOD, "POST", NULL);
376
377         buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
378         soup_message_set_request (
379                 msg, "text/xml; charset=utf-8",
380                 SOUP_MEMORY_COPY,
381                 buf_content, buf_size);
382 }
383
384 static SoupMessage *
385 ews_create_msg_for_url (const gchar *url,
386                         xmlOutputBuffer *buf)
387 {
388         SoupMessage *msg;
389         gconstpointer buf_content;
390         gsize buf_size;
391
392         msg = soup_message_new (buf != NULL ? "POST" : "GET", url);
393         soup_message_headers_append (
394                 msg->request_headers, "User-Agent", "libews/0.1");
395
396         if (buf != NULL) {
397                 buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
398                 soup_message_set_request (
399                         msg, "text/xml; charset=utf-8",
400                         SOUP_MEMORY_COPY,
401                         buf_content, buf_size);
402                 g_signal_connect (
403                         msg, "restarted",
404                         G_CALLBACK (ews_post_restarted_cb), buf);
405         }
406
407         soup_buffer_free (
408                 soup_message_body_flatten (
409                 SOUP_MESSAGE (msg)->request_body));
410
411         g_debug ("The request headers");
412         g_debug ("===================");
413         g_debug ("%s", SOUP_MESSAGE (msg)->request_body->data);
414
415         return msg;
416 }
417 #endif /* HAVE_GOA_PASSWORD_BASED */
418
419 void
420 goa_ews_autodiscover (GoaObject *goa_object,
421                       GCancellable *cancellable,
422                       GAsyncReadyCallback callback,
423                       gpointer user_data)
424 {
425         /* XXX This function is only called if HAVE_GOA_PASSWORD_BASED
426          *     is defined, so don't worry about a fallback behavior. */
427 #ifdef HAVE_GOA_PASSWORD_BASED
428         GoaAccount *goa_account;
429         GoaExchange *goa_exchange;
430         GoaPasswordBased *goa_password;
431         GSimpleAsyncResult *simple;
432         AutodiscoverData *data;
433         AutodiscoverAuthData *auth;
434         gchar *url1;
435         gchar *url2;
436         xmlDoc *doc;
437         xmlOutputBuffer *buf;
438         gchar *email;
439         gchar *host;
440         gchar *password = NULL;
441         GError *error = NULL;
442
443         g_return_if_fail (GOA_IS_OBJECT (goa_object));
444
445         goa_account = goa_object_get_account (goa_object);
446         goa_exchange = goa_object_get_exchange (goa_object);
447         goa_password = goa_object_get_password_based (goa_object);
448
449         email = goa_account_dup_presentation_identity (goa_account);
450         host = goa_exchange_dup_host (goa_exchange);
451
452         doc = ews_create_autodiscover_xml (email);
453         buf = xmlAllocOutputBuffer (NULL);
454         xmlNodeDumpOutput (buf, doc, xmlDocGetRootElement (doc), 0, 1, NULL);
455         xmlOutputBufferFlush (buf);
456
457         url1 = g_strdup_printf (
458                 "https://%s/autodiscover/autodiscover.xml", host);
459         url2 = g_strdup_printf (
460                 "https://autodiscover.%s/autodiscover/autodiscover.xml", host);
461
462         g_free (host);
463         g_free (email);
464
465         /* http://msdn.microsoft.com/en-us/library/ee332364.aspx says we are
466         * supposed to try $domain and then autodiscover.$domain. But some
467         * people have broken firewalls on the former which drop packets
468         * instead of rejecting connections, and make the request take ages
469         * to time out. So run both queries in parallel and let the fastest
470         * (successful) one win. */
471         data = g_slice_new0 (AutodiscoverData);
472         data->buf = buf;
473         data->msgs[0] = ews_create_msg_for_url (url1, buf);
474         data->msgs[1] = ews_create_msg_for_url (url2, buf);
475         data->session = soup_session_async_new_with_options (
476                 SOUP_SESSION_USE_NTLM, TRUE,
477                 SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
478                 SOUP_SESSION_TIMEOUT, 90,
479                 NULL);
480         if (G_IS_CANCELLABLE (cancellable)) {
481                 data->cancellable = g_object_ref (cancellable);
482                 data->cancellable_id = g_cancellable_connect (
483                         data->cancellable,
484                         G_CALLBACK (ews_autodiscover_cancelled_cb),
485                         data, NULL);
486         }
487
488         simple = g_simple_async_result_new (
489                 G_OBJECT (goa_object), callback,
490                 user_data, goa_ews_autodiscover);
491
492         g_simple_async_result_set_check_cancellable (simple, cancellable);
493
494         g_simple_async_result_set_op_res_gpointer (
495                 simple, data, (GDestroyNotify) ews_autodiscover_data_free);
496
497         goa_password_based_call_get_password_sync (
498                 goa_password, "", &password, cancellable, &error);
499
500         /* Sanity check */
501         g_return_if_fail (
502                 ((password != NULL) && (error == NULL)) ||
503                 ((password == NULL) && (error != NULL)));
504
505         if (error == NULL) {
506                 gchar *username;
507
508                 username = goa_account_dup_identity (goa_account);
509
510                 auth = g_slice_new0 (AutodiscoverAuthData);
511                 auth->username = username;  /* takes ownership */
512                 auth->password = password;  /* takes ownership */
513
514                 g_signal_connect_data (
515                         data->session, "authenticate",
516                         G_CALLBACK (ews_authenticate), auth,
517                         ews_autodiscover_auth_data_free, 0);
518
519                 soup_session_queue_message (
520                         data->session, data->msgs[0],
521                         ews_autodiscover_response_cb, simple);
522                 soup_session_queue_message (
523                         data->session, data->msgs[1],
524                         ews_autodiscover_response_cb, simple);
525         } else {
526                 g_simple_async_result_take_error (simple, error);
527                 g_simple_async_result_complete_in_idle (simple);
528                 g_object_unref (simple);
529         }
530
531         g_free (url2);
532         g_free (url1);
533         xmlFreeDoc (doc);
534
535         g_object_unref (goa_account);
536         g_object_unref (goa_exchange);
537         g_object_unref (goa_password);
538 #endif /* HAVE_GOA_PASSWORD_BASED */
539 }
540
541 gboolean
542 goa_ews_autodiscover_finish (GoaObject *goa_object,
543                              GAsyncResult *result,
544                              gchar **out_as_url,
545                              gchar **out_oab_url,
546                              GError **error)
547 {
548         GSimpleAsyncResult *simple;
549         AutodiscoverData *data;
550
551         g_return_val_if_fail (
552                 g_simple_async_result_is_valid (
553                 result, G_OBJECT (goa_object),
554                 goa_ews_autodiscover), FALSE);
555
556         simple = G_SIMPLE_ASYNC_RESULT (result);
557         data = g_simple_async_result_get_op_res_gpointer (simple);
558
559         if (g_simple_async_result_propagate_error (simple, error))
560                 return FALSE;
561
562         if (out_as_url != NULL) {
563                 *out_as_url = data->as_url;
564                 data->as_url = NULL;
565         }
566
567         if (out_oab_url != NULL) {
568                 *out_oab_url = data->oab_url;
569                 data->oab_url = NULL;
570         }
571
572         return TRUE;
573 }
574
575 gboolean
576 goa_ews_autodiscover_sync (GoaObject *goa_object,
577                            gchar **out_as_url,
578                            gchar **out_oab_url,
579                            GCancellable *cancellable,
580                            GError **error)
581 {
582         EAsyncClosure *closure;
583         GAsyncResult *result;
584         gboolean success;
585
586         g_return_val_if_fail (GOA_IS_OBJECT (goa_object), FALSE);
587
588         closure = e_async_closure_new ();
589
590         goa_ews_autodiscover (
591                 goa_object, cancellable,
592                 e_async_closure_callback, closure);
593
594         result = e_async_closure_wait (closure);
595
596         success = goa_ews_autodiscover_finish (
597                 goa_object, result, out_as_url, out_oab_url, error);
598
599         e_async_closure_free (closure);
600
601         return success;
602 }
603