Bug 681321 - Support both old and new-buf libxml2 APIs
[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 gconstpointer
313 compat_libxml_output_buffer_get_content (xmlOutputBufferPtr buf,
314                                          gsize *out_len)
315 {
316 #ifdef LIBXML2_NEW_BUFFER
317         *out_len = xmlOutputBufferGetSize (buf);
318         return xmlOutputBufferGetContent (buf);
319 #else
320         *out_len = buf->buffer->use;
321         return buf->buffer->content;
322 #endif
323 }
324
325 static void
326 ews_post_restarted_cb (SoupMessage *msg,
327                        gpointer data)
328 {
329         xmlOutputBuffer *buf = data;
330         gconstpointer buf_content;
331         gsize buf_size;
332
333         /* In violation of RFC2616, libsoup will change a
334          * POST request to a GET on receiving a 302 redirect. */
335         g_debug ("Working around libsoup bug with redirect");
336         g_object_set (msg, SOUP_MESSAGE_METHOD, "POST", NULL);
337
338         buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
339         soup_message_set_request (
340                 msg, "text/xml; charset=utf-8",
341                 SOUP_MEMORY_COPY,
342                 buf_content, buf_size);
343 }
344
345 static SoupMessage *
346 ews_create_msg_for_url (const gchar *url,
347                         xmlOutputBuffer *buf)
348 {
349         SoupMessage *msg;
350         gconstpointer buf_content;
351         gsize buf_size;
352
353         msg = soup_message_new (buf != NULL ? "POST" : "GET", url);
354         soup_message_headers_append (
355                 msg->request_headers, "User-Agent", "libews/0.1");
356
357         if (buf != NULL) {
358                 buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
359                 soup_message_set_request (
360                         msg, "text/xml; charset=utf-8",
361                         SOUP_MEMORY_COPY,
362                         buf_content, buf_size);
363                 g_signal_connect (
364                         msg, "restarted",
365                         G_CALLBACK (ews_post_restarted_cb), buf);
366         }
367
368         soup_buffer_free (
369                 soup_message_body_flatten (
370                 SOUP_MESSAGE (msg)->request_body));
371
372         g_debug ("The request headers");
373         g_debug ("===================");
374         g_debug ("%s", SOUP_MESSAGE (msg)->request_body->data);
375
376         return msg;
377 }
378 #endif /* HAVE_GOA_PASSWORD_BASED */
379
380 void
381 goa_ews_autodiscover (GoaObject *goa_object,
382                       GCancellable *cancellable,
383                       GAsyncReadyCallback callback,
384                       gpointer user_data)
385 {
386         /* XXX This function is only called if HAVE_GOA_PASSWORD_BASED
387          *     is defined, so don't worry about a fallback behavior. */
388 #ifdef HAVE_GOA_PASSWORD_BASED
389         GoaAccount *goa_account;
390         GoaExchange *goa_exchange;
391         GoaPasswordBased *goa_password;
392         GSimpleAsyncResult *simple;
393         AutodiscoverData *data;
394         AutodiscoverAuthData *auth;
395         gchar *url1;
396         gchar *url2;
397         xmlDoc *doc;
398         xmlOutputBuffer *buf;
399         gchar *email;
400         gchar *host;
401         gchar *password = NULL;
402         GError *error = NULL;
403
404         g_return_if_fail (GOA_IS_OBJECT (goa_object));
405
406         goa_account = goa_object_get_account (goa_object);
407         goa_exchange = goa_object_get_exchange (goa_object);
408         goa_password = goa_object_get_password_based (goa_object);
409
410         email = goa_account_dup_presentation_identity (goa_account);
411         host = goa_exchange_dup_host (goa_exchange);
412
413         doc = ews_create_autodiscover_xml (email);
414         buf = xmlAllocOutputBuffer (NULL);
415         xmlNodeDumpOutput (buf, doc, xmlDocGetRootElement (doc), 0, 1, NULL);
416         xmlOutputBufferFlush (buf);
417
418         url1 = g_strdup_printf (
419                 "https://%s/autodiscover/autodiscover.xml", host);
420         url2 = g_strdup_printf (
421                 "https://autodiscover.%s/autodiscover/autodiscover.xml", host);
422
423         g_free (host);
424         g_free (email);
425
426         /* http://msdn.microsoft.com/en-us/library/ee332364.aspx says we are
427         * supposed to try $domain and then autodiscover.$domain. But some
428         * people have broken firewalls on the former which drop packets
429         * instead of rejecting connections, and make the request take ages
430         * to time out. So run both queries in parallel and let the fastest
431         * (successful) one win. */
432         data = g_slice_new0 (AutodiscoverData);
433         data->buf = buf;
434         data->msgs[0] = ews_create_msg_for_url (url1, buf);
435         data->msgs[1] = ews_create_msg_for_url (url2, buf);
436         data->session = soup_session_async_new_with_options (
437                 SOUP_SESSION_USE_NTLM, TRUE,
438                 SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
439                 SOUP_SESSION_TIMEOUT, 90,
440                 NULL);
441         if (G_IS_CANCELLABLE (cancellable)) {
442                 data->cancellable = g_object_ref (cancellable);
443                 data->cancellable_id = g_cancellable_connect (
444                         data->cancellable,
445                         G_CALLBACK (ews_autodiscover_cancelled_cb),
446                         data, NULL);
447         }
448
449         simple = g_simple_async_result_new (
450                 G_OBJECT (goa_object), callback,
451                 user_data, goa_ews_autodiscover);
452
453         g_simple_async_result_set_check_cancellable (simple, cancellable);
454
455         g_simple_async_result_set_op_res_gpointer (
456                 simple, data, (GDestroyNotify) ews_autodiscover_data_free);
457
458         goa_password_based_call_get_password_sync (
459                 goa_password, "", &password, cancellable, &error);
460
461         /* Sanity check */
462         g_return_if_fail (
463                 ((password != NULL) && (error == NULL)) ||
464                 ((password == NULL) && (error != NULL)));
465
466         if (error == NULL) {
467                 gchar *username;
468
469                 username = goa_account_dup_identity (goa_account);
470
471                 auth = g_slice_new0 (AutodiscoverAuthData);
472                 auth->username = username;  /* takes ownership */
473                 auth->password = password;  /* takes ownership */
474
475                 g_signal_connect_data (
476                         data->session, "authenticate",
477                         G_CALLBACK (ews_authenticate), auth,
478                         ews_autodiscover_auth_data_free, 0);
479
480                 soup_session_queue_message (
481                         data->session, data->msgs[0],
482                         ews_autodiscover_response_cb, simple);
483                 soup_session_queue_message (
484                         data->session, data->msgs[1],
485                         ews_autodiscover_response_cb, simple);
486         } else {
487                 g_simple_async_result_take_error (simple, error);
488                 g_simple_async_result_complete_in_idle (simple);
489                 g_object_unref (simple);
490         }
491
492         g_free (url2);
493         g_free (url1);
494         xmlFreeDoc (doc);
495
496         g_object_unref (goa_account);
497         g_object_unref (goa_exchange);
498         g_object_unref (goa_password);
499 #endif /* HAVE_GOA_PASSWORD_BASED */
500 }
501
502 gboolean
503 goa_ews_autodiscover_finish (GoaObject *goa_object,
504                              GAsyncResult *result,
505                              gchar **out_as_url,
506                              gchar **out_oab_url,
507                              GError **error)
508 {
509         GSimpleAsyncResult *simple;
510         AutodiscoverData *data;
511
512         g_return_val_if_fail (
513                 g_simple_async_result_is_valid (
514                 result, G_OBJECT (goa_object),
515                 goa_ews_autodiscover), FALSE);
516
517         simple = G_SIMPLE_ASYNC_RESULT (result);
518         data = g_simple_async_result_get_op_res_gpointer (simple);
519
520         if (g_simple_async_result_propagate_error (simple, error))
521                 return FALSE;
522
523         if (out_as_url != NULL) {
524                 *out_as_url = data->as_url;
525                 data->as_url = NULL;
526         }
527
528         if (out_oab_url != NULL) {
529                 *out_oab_url = data->oab_url;
530                 data->oab_url = NULL;
531         }
532
533         return TRUE;
534 }
535
536 gboolean
537 goa_ews_autodiscover_sync (GoaObject *goa_object,
538                            gchar **out_as_url,
539                            gchar **out_oab_url,
540                            GCancellable *cancellable,
541                            GError **error)
542 {
543         EAsyncClosure *closure;
544         GAsyncResult *result;
545         gboolean success;
546
547         g_return_val_if_fail (GOA_IS_OBJECT (goa_object), FALSE);
548
549         closure = e_async_closure_new ();
550
551         goa_ews_autodiscover (
552                 goa_object, cancellable,
553                 e_async_closure_callback, closure);
554
555         result = e_async_closure_wait (closure);
556
557         success = goa_ews_autodiscover_finish (
558                 goa_object, result, out_as_url, out_oab_url, error);
559
560         e_async_closure_free (closure);
561
562         return success;
563 }
564