1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3 * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
4 *
5 * This library is free software: you can redistribute it and/or modify it
6 * under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation.
8 *
9 * This library is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12 * for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this library. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 /**
19 * SECTION: e-soup-session
20 * @include: libedataserver/libedataserver.h
21 * @short_description: A SoupSession descendant
22 *
23 * The #ESoupSession is a #SoupSession descendant, which hides common
24 * tasks related to the way evolution-data-server works.
25 **/
26
27 #include "evolution-data-server-config.h"
28
29 #include <stdio.h>
30 #include <glib/gi18n-lib.h>
31
32 #include "e-oauth2-services.h"
33 #include "e-soup-auth-bearer.h"
34 #include "e-soup-logger.h"
35 #include "e-soup-ssl-trust.h"
36 #include "e-source-authentication.h"
37 #include "e-source-webdav.h"
38
39 #include "e-soup-session.h"
40
41 #define BUFFER_SIZE 16384
42
43 struct _ESoupSessionPrivate {
44 GMutex property_lock;
45 ESource *source;
46 ENamedParameters *credentials;
47
48 gboolean ssl_info_set;
49 gchar *ssl_certificate_pem;
50 GTlsCertificateFlags ssl_certificate_errors;
51
52 SoupLoggerLogLevel log_level;
53
54 GError *bearer_auth_error;
55 ESoupAuthBearer *using_bearer_auth;
56
57 gboolean auth_prefilled; /* When TRUE, the first 'retrying' is ignored in the "authenticate" handler */
58 };
59
60 enum {
61 PROP_0,
62 PROP_SOURCE,
63 PROP_CREDENTIALS
64 };
65
G_DEFINE_TYPE_WITH_PRIVATE(ESoupSession,e_soup_session,SOUP_TYPE_SESSION)66 G_DEFINE_TYPE_WITH_PRIVATE (ESoupSession, e_soup_session, SOUP_TYPE_SESSION)
67
68 static void
69 e_soup_session_ensure_auth_usage (ESoupSession *session,
70 SoupURI *in_soup_uri,
71 SoupMessage *message,
72 SoupAuth *soup_auth)
73 {
74 SoupAuthManager *auth_manager;
75 SoupSessionFeature *feature;
76 SoupURI *soup_uri;
77 GType auth_type;
78
79 g_return_if_fail (E_IS_SOUP_SESSION (session));
80 g_return_if_fail (SOUP_IS_AUTH (soup_auth));
81
82 feature = soup_session_get_feature (SOUP_SESSION (session), SOUP_TYPE_AUTH_MANAGER);
83
84 auth_type = G_OBJECT_TYPE (soup_auth);
85
86 if (!soup_session_feature_has_feature (feature, auth_type)) {
87 /* Add the SoupAuth type to support it. */
88 soup_session_feature_add_feature (feature, auth_type);
89 }
90
91 if (in_soup_uri) {
92 soup_uri = in_soup_uri;
93 } else {
94 soup_uri = message ? soup_message_get_uri (message) : NULL;
95 if (soup_uri && soup_uri->host && *soup_uri->host) {
96 soup_uri = soup_uri_copy_host (soup_uri);
97 } else {
98 soup_uri = NULL;
99 }
100
101 if (!soup_uri) {
102 ESourceWebdav *extension;
103 ESource *source;
104
105 source = e_soup_session_get_source (session);
106 extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
107 soup_uri = e_source_webdav_dup_soup_uri (extension);
108 }
109 }
110
111 auth_manager = SOUP_AUTH_MANAGER (feature);
112
113 /* This will make sure the 'soup_auth' is used regardless of the current 'auth_manager' state.
114 See https://gitlab.gnome.org/GNOME/libsoup/-/issues/196 for more information. */
115 soup_auth_manager_clear_cached_credentials (auth_manager);
116 soup_auth_manager_use_auth (auth_manager, soup_uri, soup_auth);
117
118 if (!in_soup_uri)
119 soup_uri_free (soup_uri);
120 }
121
122 static gboolean
e_soup_session_setup_bearer_auth(ESoupSession * session,SoupMessage * message,gboolean is_in_authenticate_handler,ESoupAuthBearer * bearer,GCancellable * cancellable,GError ** error)123 e_soup_session_setup_bearer_auth (ESoupSession *session,
124 SoupMessage *message,
125 gboolean is_in_authenticate_handler,
126 ESoupAuthBearer *bearer,
127 GCancellable *cancellable,
128 GError **error)
129 {
130 ESource *source;
131 gchar *access_token = NULL;
132 gint expires_in_seconds = -1;
133 gboolean success = FALSE;
134
135 g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
136 g_return_val_if_fail (E_IS_SOUP_AUTH_BEARER (bearer), FALSE);
137
138 source = e_soup_session_get_source (session);
139
140 success = e_source_get_oauth2_access_token_sync (source, cancellable,
141 &access_token, &expires_in_seconds, error);
142
143 if (success) {
144 e_soup_auth_bearer_set_access_token (bearer, access_token, expires_in_seconds);
145
146 /* Preload the SoupAuthManager with a valid "Bearer" token
147 * when using OAuth 2.0. This avoids an extra unauthorized
148 * HTTP round-trip, which apparently Google doesn't like. */
149 if (!is_in_authenticate_handler)
150 e_soup_session_ensure_auth_usage (session, NULL, message, SOUP_AUTH (bearer));
151 }
152
153 g_free (access_token);
154
155 return success;
156 }
157
158 static gboolean
e_soup_session_maybe_prepare_bearer_auth(ESoupSession * session,SoupURI * soup_uri,SoupMessage * message,GCancellable * cancellable,GError ** error)159 e_soup_session_maybe_prepare_bearer_auth (ESoupSession *session,
160 SoupURI *soup_uri,
161 SoupMessage *message,
162 GCancellable *cancellable,
163 GError **error)
164 {
165 gboolean success;
166
167 g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
168 g_return_val_if_fail (soup_uri != NULL, FALSE);
169
170 g_mutex_lock (&session->priv->property_lock);
171 if (session->priv->using_bearer_auth) {
172 ESoupAuthBearer *using_bearer_auth = g_object_ref (session->priv->using_bearer_auth);
173
174 g_mutex_unlock (&session->priv->property_lock);
175
176 success = e_soup_session_setup_bearer_auth (session, message, FALSE, using_bearer_auth, cancellable, error);
177
178 g_clear_object (&using_bearer_auth);
179 } else {
180 SoupAuth *soup_auth;
181
182 g_mutex_unlock (&session->priv->property_lock);
183
184 soup_auth = g_object_new (
185 E_TYPE_SOUP_AUTH_BEARER,
186 SOUP_AUTH_HOST, soup_uri->host, NULL);
187
188 success = e_soup_session_setup_bearer_auth (session, message, FALSE, E_SOUP_AUTH_BEARER (soup_auth), cancellable, error);
189 if (success) {
190 g_mutex_lock (&session->priv->property_lock);
191 g_clear_object (&session->priv->using_bearer_auth);
192 session->priv->using_bearer_auth = g_object_ref (soup_auth);
193 g_mutex_unlock (&session->priv->property_lock);
194 }
195
196 g_object_unref (soup_auth);
197 }
198
199 return success;
200 }
201
202 static gboolean
e_soup_session_maybe_prepare_basic_auth(ESoupSession * session,SoupURI * soup_uri,SoupMessage * message,const gchar * in_username,const ENamedParameters * credentials,GCancellable * cancellable,GError ** error)203 e_soup_session_maybe_prepare_basic_auth (ESoupSession *session,
204 SoupURI *soup_uri,
205 SoupMessage *message,
206 const gchar *in_username,
207 const ENamedParameters *credentials,
208 GCancellable *cancellable,
209 GError **error)
210 {
211 SoupAuth *soup_auth;
212 const gchar *username;
213
214 g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
215 g_return_val_if_fail (soup_uri != NULL, FALSE);
216
217 if (!credentials || !e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_PASSWORD)) {
218 /* This error message won't get into the UI */
219 g_set_error_literal (error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED, soup_status_get_phrase (SOUP_STATUS_UNAUTHORIZED));
220
221 if (message)
222 soup_message_set_status (message, SOUP_STATUS_UNAUTHORIZED);
223
224 return FALSE;
225 }
226
227 username = e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME);
228 if (!username || !*username)
229 username = in_username;
230
231 soup_auth = soup_auth_new (SOUP_TYPE_AUTH_BASIC, message, "Basic");
232
233 soup_auth_authenticate (soup_auth, username, e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD));
234
235 g_mutex_lock (&session->priv->property_lock);
236 session->priv->auth_prefilled = TRUE;
237 g_mutex_unlock (&session->priv->property_lock);
238
239 e_soup_session_ensure_auth_usage (session, soup_uri, message, soup_auth);
240
241 g_clear_object (&soup_auth);
242
243 return TRUE;
244 }
245
246 static gboolean
e_soup_session_maybe_prepare_auth(ESoupSession * session,SoupRequestHTTP * request,GCancellable * cancellable,GError ** error)247 e_soup_session_maybe_prepare_auth (ESoupSession *session,
248 SoupRequestHTTP *request,
249 GCancellable *cancellable,
250 GError **error)
251 {
252 ESource *source;
253 ENamedParameters *credentials;
254 SoupMessage *message;
255 SoupURI *soup_uri;
256 gchar *auth_method = NULL, *user = NULL;
257 gboolean success = TRUE;
258
259 g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
260
261 source = e_soup_session_get_source (session);
262
263 if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
264 ESourceAuthentication *extension;
265
266 extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
267 auth_method = e_source_authentication_dup_method (extension);
268 user = e_source_authentication_dup_user (extension);
269 } else {
270 return TRUE;
271 }
272
273 credentials = e_soup_session_dup_credentials (session);
274 message = soup_request_http_get_message (request);
275 soup_uri = message ? soup_message_get_uri (message) : NULL;
276 if (soup_uri && soup_uri->host && *soup_uri->host) {
277 soup_uri = soup_uri_copy_host (soup_uri);
278 } else {
279 soup_uri = NULL;
280 }
281
282 if (!soup_uri) {
283 ESourceWebdav *extension;
284
285 extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
286 soup_uri = e_source_webdav_dup_soup_uri (extension);
287 }
288
289 g_mutex_lock (&session->priv->property_lock);
290 session->priv->auth_prefilled = FALSE;
291 g_mutex_unlock (&session->priv->property_lock);
292
293 /* Provide credentials beforehand only on secure connections */
294 if (soup_uri_get_scheme (soup_uri) == SOUP_URI_SCHEME_HTTPS) {
295 if (g_strcmp0 (auth_method, "OAuth2") == 0 ||
296 e_oauth2_services_is_oauth2_alias_static (auth_method)) {
297 success = e_soup_session_maybe_prepare_bearer_auth (session, soup_uri, message, cancellable, error);
298 } else if (g_strcmp0 (auth_method, "GSSAPI") == 0 && soup_auth_negotiate_supported ()) {
299 SoupSession *soup_session = SOUP_SESSION (session);
300
301 soup_session_add_feature_by_type (soup_session, SOUP_TYPE_AUTH_NEGOTIATE);
302 soup_session_remove_feature_by_type (soup_session, SOUP_TYPE_AUTH_BASIC);
303 } else if (user && *user) {
304 /* Default to Basic authentication when user is filled */
305 success = e_soup_session_maybe_prepare_basic_auth (session, soup_uri, message, user, credentials, cancellable, error);
306 }
307 }
308
309 e_named_parameters_free (credentials);
310 g_clear_object (&message);
311 soup_uri_free (soup_uri);
312 g_free (auth_method);
313 g_free (user);
314
315 return success;
316 }
317
318 static void
e_soup_session_authenticate_cb(SoupSession * soup_session,SoupMessage * message,SoupAuth * auth,gboolean retrying,gpointer user_data)319 e_soup_session_authenticate_cb (SoupSession *soup_session,
320 SoupMessage *message,
321 SoupAuth *auth,
322 gboolean retrying,
323 gpointer user_data)
324 {
325 ESoupSession *session;
326 const gchar *username;
327 ENamedParameters *credentials;
328 gchar *auth_user = NULL;
329
330 g_return_if_fail (E_IS_SOUP_SESSION (soup_session));
331
332 session = E_SOUP_SESSION (soup_session);
333
334 if (E_IS_SOUP_AUTH_BEARER (auth)) {
335 g_object_ref (auth);
336 g_warn_if_fail ((gpointer) session->priv->using_bearer_auth == (gpointer) auth);
337 g_clear_object (&session->priv->using_bearer_auth);
338 session->priv->using_bearer_auth = E_SOUP_AUTH_BEARER (auth);
339 }
340
341 g_mutex_lock (&session->priv->property_lock);
342 if (retrying && !session->priv->auth_prefilled) {
343 g_mutex_unlock (&session->priv->property_lock);
344 return;
345 }
346 session->priv->auth_prefilled = FALSE;
347 g_mutex_unlock (&session->priv->property_lock);
348
349 if (session->priv->using_bearer_auth) {
350 GError *local_error = NULL;
351
352 e_soup_session_setup_bearer_auth (session, message, TRUE, E_SOUP_AUTH_BEARER (auth), NULL, &local_error);
353
354 if (local_error) {
355 g_mutex_lock (&session->priv->property_lock);
356
357 /* Warn about an unclaimed error before we clear it.
358 * This is just to verify the errors we set here are
359 * actually making it back to the user. */
360 g_warn_if_fail (session->priv->bearer_auth_error == NULL);
361 g_clear_error (&session->priv->bearer_auth_error);
362
363 g_propagate_error (&session->priv->bearer_auth_error, local_error);
364
365 g_mutex_unlock (&session->priv->property_lock);
366 }
367
368 return;
369 }
370
371 credentials = e_soup_session_dup_credentials (session);
372
373 username = credentials ? e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME) : NULL;
374 if ((!username || !*username) &&
375 e_source_has_extension (session->priv->source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
376 ESourceAuthentication *auth_extension;
377
378 auth_extension = e_source_get_extension (session->priv->source, E_SOURCE_EXTENSION_AUTHENTICATION);
379 auth_user = e_source_authentication_dup_user (auth_extension);
380
381 username = auth_user;
382 }
383
384 if (!username || !*username || !credentials ||
385 !e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
386 soup_message_set_status (message, SOUP_STATUS_UNAUTHORIZED);
387 else
388 soup_auth_authenticate (auth, username, e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD));
389
390 e_named_parameters_free (credentials);
391 g_free (auth_user);
392 }
393
394 static void
e_soup_session_set_source(ESoupSession * session,ESource * source)395 e_soup_session_set_source (ESoupSession *session,
396 ESource *source)
397 {
398 g_return_if_fail (E_IS_SOUP_SESSION (session));
399 g_return_if_fail (E_IS_SOURCE (source));
400 g_return_if_fail (!session->priv->source);
401
402 session->priv->source = g_object_ref (source);
403 }
404
405 static void
e_soup_session_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)406 e_soup_session_set_property (GObject *object,
407 guint property_id,
408 const GValue *value,
409 GParamSpec *pspec)
410 {
411 switch (property_id) {
412 case PROP_SOURCE:
413 e_soup_session_set_source (
414 E_SOUP_SESSION (object),
415 g_value_get_object (value));
416 return;
417
418 case PROP_CREDENTIALS:
419 e_soup_session_set_credentials (
420 E_SOUP_SESSION (object),
421 g_value_get_boxed (value));
422 return;
423 }
424
425 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
426 }
427
428 static void
e_soup_session_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)429 e_soup_session_get_property (GObject *object,
430 guint property_id,
431 GValue *value,
432 GParamSpec *pspec)
433 {
434 switch (property_id) {
435 case PROP_SOURCE:
436 g_value_set_object (
437 value,
438 e_soup_session_get_source (
439 E_SOUP_SESSION (object)));
440 return;
441
442 case PROP_CREDENTIALS:
443 g_value_take_boxed (
444 value,
445 e_soup_session_dup_credentials (
446 E_SOUP_SESSION (object)));
447 return;
448 }
449
450 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
451 }
452
453 static void
e_soup_session_finalize(GObject * object)454 e_soup_session_finalize (GObject *object)
455 {
456 ESoupSession *session = E_SOUP_SESSION (object);
457
458 g_clear_error (&session->priv->bearer_auth_error);
459 g_clear_object (&session->priv->source);
460 g_clear_object (&session->priv->using_bearer_auth);
461 g_clear_pointer (&session->priv->credentials, e_named_parameters_free);
462 g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
463
464 g_mutex_clear (&session->priv->property_lock);
465
466 /* Chain up to parent's method. */
467 G_OBJECT_CLASS (e_soup_session_parent_class)->finalize (object);
468 }
469
470 static void
e_soup_session_class_init(ESoupSessionClass * klass)471 e_soup_session_class_init (ESoupSessionClass *klass)
472 {
473 GObjectClass *object_class;
474
475 object_class = G_OBJECT_CLASS (klass);
476 object_class->set_property = e_soup_session_set_property;
477 object_class->get_property = e_soup_session_get_property;
478 object_class->finalize = e_soup_session_finalize;
479
480 /**
481 * ESoupSession:source:
482 *
483 * The #ESource being used for this soup session.
484 *
485 * Since: 3.26
486 **/
487 g_object_class_install_property (
488 object_class,
489 PROP_SOURCE,
490 g_param_spec_object (
491 "source",
492 "Source",
493 NULL,
494 E_TYPE_SOURCE,
495 G_PARAM_READWRITE |
496 G_PARAM_CONSTRUCT_ONLY |
497 G_PARAM_STATIC_STRINGS));
498
499 /**
500 * ESoupSession:credentials:
501 *
502 * The #ENamedParameters containing login credentials.
503 *
504 * Since: 3.26
505 **/
506 g_object_class_install_property (
507 object_class,
508 PROP_CREDENTIALS,
509 g_param_spec_boxed (
510 "credentials",
511 "Credentials",
512 NULL,
513 E_TYPE_NAMED_PARAMETERS,
514 G_PARAM_READWRITE |
515 G_PARAM_EXPLICIT_NOTIFY |
516 G_PARAM_STATIC_STRINGS));
517 }
518
519 static void
e_soup_session_init(ESoupSession * session)520 e_soup_session_init (ESoupSession *session)
521 {
522 session->priv = e_soup_session_get_instance_private (session);
523 session->priv->ssl_info_set = FALSE;
524 session->priv->log_level = SOUP_LOGGER_LOG_NONE;
525 session->priv->auth_prefilled = FALSE;
526
527 g_mutex_init (&session->priv->property_lock);
528
529 g_object_set (
530 G_OBJECT (session),
531 SOUP_SESSION_TIMEOUT, 90,
532 SOUP_SESSION_SSL_STRICT, TRUE,
533 SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
534 SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
535 NULL);
536
537 soup_session_add_feature_by_type (SOUP_SESSION (session), SOUP_TYPE_CONTENT_DECODER);
538
539 g_signal_connect (session, "authenticate",
540 G_CALLBACK (e_soup_session_authenticate_cb), NULL);
541 }
542
543 /**
544 * e_soup_session_new:
545 * @source: an #ESource
546 *
547 * Creates a new #ESoupSession associated with given @source.
548 * The @source can be used to store and read SSL trust settings, but only if
549 * it already contains an #ESourceWebdav extension. Otherwise the SSL trust
550 * settings are ignored.
551 *
552 * Returns: (transfer full): a new #ESoupSession; free it with g_object_unref(),
553 * when no longer needed.
554 *
555 * Since: 3.26
556 **/
557 ESoupSession *
e_soup_session_new(ESource * source)558 e_soup_session_new (ESource *source)
559 {
560 g_return_val_if_fail (E_IS_SOURCE (source), NULL);
561
562 return g_object_new (E_TYPE_SOUP_SESSION,
563 "source", source,
564 NULL);
565 }
566
567 /**
568 * e_soup_session_setup_logging:
569 * @session: an #ESoupSession
570 * @logging_level: (nullable): logging level to setup, or %NULL
571 *
572 * Setups logging for the @session. The @logging_level can be one of:
573 * "all" - log whole raw communication;
574 * "body" - the same as "all";
575 * "headers" - log the headers only;
576 * "min" - minimal logging;
577 * "1" - the same as "all".
578 * Any other value, including %NULL, disables logging.
579 *
580 * Use e_soup_session_get_log_level() to get current log level.
581 *
582 * Since: 3.26
583 **/
584 void
e_soup_session_setup_logging(ESoupSession * session,const gchar * logging_level)585 e_soup_session_setup_logging (ESoupSession *session,
586 const gchar *logging_level)
587 {
588 SoupLogger *logger;
589
590 g_return_if_fail (E_IS_SOUP_SESSION (session));
591
592 soup_session_remove_feature_by_type (SOUP_SESSION (session), SOUP_TYPE_LOGGER);
593 session->priv->log_level = SOUP_LOGGER_LOG_NONE;
594
595 if (!logging_level)
596 return;
597
598 if (g_ascii_strcasecmp (logging_level, "all") == 0 ||
599 g_ascii_strcasecmp (logging_level, "body") == 0 ||
600 g_ascii_strcasecmp (logging_level, "1") == 0)
601 session->priv->log_level = SOUP_LOGGER_LOG_BODY;
602 else if (g_ascii_strcasecmp (logging_level, "headers") == 0)
603 session->priv->log_level = SOUP_LOGGER_LOG_HEADERS;
604 else if (g_ascii_strcasecmp (logging_level, "min") == 0)
605 session->priv->log_level = SOUP_LOGGER_LOG_MINIMAL;
606 else
607 return;
608
609 logger = soup_logger_new (session->priv->log_level, -1);
610 soup_session_add_feature (SOUP_SESSION (session), SOUP_SESSION_FEATURE (logger));
611 g_object_unref (logger);
612 }
613
614 /**
615 * e_soup_session_get_log_level:
616 * @session: an #ESoupSession
617 *
618 * Returns: Current log level, as #SoupLoggerLogLevel
619 *
620 * Since: 3.26
621 **/
622 SoupLoggerLogLevel
e_soup_session_get_log_level(ESoupSession * session)623 e_soup_session_get_log_level (ESoupSession *session)
624 {
625 g_return_val_if_fail (E_IS_SOUP_SESSION (session), SOUP_LOGGER_LOG_NONE);
626
627 return session->priv->log_level;
628 }
629
630 /**
631 * e_soup_session_get_source:
632 * @session: an #ESoupSession
633 *
634 * Returns: (transfer none): Associated #ESource with the @session.
635 *
636 * Since: 3.26
637 **/
638 ESource *
e_soup_session_get_source(ESoupSession * session)639 e_soup_session_get_source (ESoupSession *session)
640 {
641 g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
642
643 return session->priv->source;
644 }
645
646 /**
647 * e_soup_session_set_credentials:
648 * @session: an #ESoupSession
649 * @credentials: (nullable): an #ENamedParameters with credentials to use, or %NULL
650 *
651 * Sets credentials to use for connection. Using %NULL for @credentials
652 * unsets previous value.
653 *
654 * Since: 3.26
655 **/
656 void
e_soup_session_set_credentials(ESoupSession * session,const ENamedParameters * credentials)657 e_soup_session_set_credentials (ESoupSession *session,
658 const ENamedParameters *credentials)
659 {
660 g_return_if_fail (E_IS_SOUP_SESSION (session));
661
662 g_mutex_lock (&session->priv->property_lock);
663
664 if (credentials == session->priv->credentials) {
665 g_mutex_unlock (&session->priv->property_lock);
666 return;
667 }
668
669 e_named_parameters_free (session->priv->credentials);
670 if (credentials)
671 session->priv->credentials = e_named_parameters_new_clone (credentials);
672 else
673 session->priv->credentials = NULL;
674
675 g_mutex_unlock (&session->priv->property_lock);
676
677 g_object_notify (G_OBJECT (session), "credentials");
678 }
679
680 /**
681 * e_soup_session_dup_credentials:
682 * @session: an #ESoupSession
683 *
684 * Returns: (nullable) (transfer full): A copy of the credentials being
685 * previously set with e_soup_session_set_credentials(), or %NULL when
686 * none are set. Free the returned pointer with e_named_parameters_free(),
687 * when no longer needed.
688 *
689 * Since: 3.26
690 **/
691 ENamedParameters *
e_soup_session_dup_credentials(ESoupSession * session)692 e_soup_session_dup_credentials (ESoupSession *session)
693 {
694 ENamedParameters *credentials;
695
696 g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
697
698 g_mutex_lock (&session->priv->property_lock);
699
700 if (session->priv->credentials)
701 credentials = e_named_parameters_new_clone (session->priv->credentials);
702 else
703 credentials = NULL;
704
705 g_mutex_unlock (&session->priv->property_lock);
706
707 return credentials;
708 }
709
710 /**
711 * e_soup_session_get_authentication_requires_credentials:
712 * @session: an #ESoupSession
713 *
714 * Returns: Whether the last connection attempt required any credentials.
715 * Authentications like OAuth2 do not want extra credentials to work.
716 *
717 * Since: 3.28
718 **/
719 gboolean
e_soup_session_get_authentication_requires_credentials(ESoupSession * session)720 e_soup_session_get_authentication_requires_credentials (ESoupSession *session)
721 {
722 g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
723
724 return !session->priv->using_bearer_auth;
725 }
726
727 /**
728 * e_soup_session_get_ssl_error_details:
729 * @session: an #ESoupSession
730 * @out_certificate_pem: (out): return location for a server TLS/SSL certificate
731 * in PEM format, when the last operation failed with a TLS/SSL error
732 * @out_certificate_errors: (out): return location for a #GTlsCertificateFlags,
733 * with certificate error flags when the operation failed with a TLS/SSL error
734 *
735 * Populates @out_certificate_pem and @out_certificate_errors with the last values
736 * returned on #SOUP_STATUS_SSL_FAILED error.
737 *
738 * Returns: Whether the information was available and set to the out parameters.
739 *
740 * Since: 3.26
741 **/
742 gboolean
e_soup_session_get_ssl_error_details(ESoupSession * session,gchar ** out_certificate_pem,GTlsCertificateFlags * out_certificate_errors)743 e_soup_session_get_ssl_error_details (ESoupSession *session,
744 gchar **out_certificate_pem,
745 GTlsCertificateFlags *out_certificate_errors)
746 {
747 g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
748 g_return_val_if_fail (out_certificate_pem != NULL, FALSE);
749 g_return_val_if_fail (out_certificate_errors != NULL, FALSE);
750
751 g_mutex_lock (&session->priv->property_lock);
752 if (!session->priv->ssl_info_set) {
753 g_mutex_unlock (&session->priv->property_lock);
754 return FALSE;
755 }
756
757 *out_certificate_pem = g_strdup (session->priv->ssl_certificate_pem);
758 *out_certificate_errors = session->priv->ssl_certificate_errors;
759
760 g_mutex_unlock (&session->priv->property_lock);
761
762 return TRUE;
763 }
764
765 static void
e_soup_session_preset_request(SoupRequestHTTP * request)766 e_soup_session_preset_request (SoupRequestHTTP *request)
767 {
768 SoupMessage *message;
769
770 if (!request)
771 return;
772
773 message = soup_request_http_get_message (request);
774 if (message) {
775 e_soup_session_util_normalize_uri_path (soup_message_get_uri (message));
776
777 soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" VERSION);
778 soup_message_headers_append (message->request_headers, "Connection", "close");
779
780 /* Disable caching for proxies (RFC 4918, section 10.4.5) */
781 soup_message_headers_append (message->request_headers, "Cache-Control", "no-cache");
782 soup_message_headers_append (message->request_headers, "Pragma", "no-cache");
783
784 g_clear_object (&message);
785 }
786 }
787
788 /**
789 * e_soup_session_new_request:
790 * @session: an #ESoupSession
791 * @method: an HTTP method
792 * @uri_string: a URI string to use for the request
793 * @error: return location for a #GError, or %NULL
794 *
795 * Creates a new #SoupRequestHTTP, similar to soup_session_request_http(),
796 * but also presets request headers with "User-Agent" to be "Evolution/version"
797 * and with "Connection" to be "close".
798 *
799 * See also e_soup_session_new_request_uri().
800 *
801 * Returns: (transfer full): a new #SoupRequestHTTP, or %NULL on error
802 *
803 * Since: 3.26
804 **/
805 SoupRequestHTTP *
e_soup_session_new_request(ESoupSession * session,const gchar * method,const gchar * uri_string,GError ** error)806 e_soup_session_new_request (ESoupSession *session,
807 const gchar *method,
808 const gchar *uri_string,
809 GError **error)
810 {
811 SoupRequestHTTP *request;
812
813 g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
814
815 request = soup_session_request_http (SOUP_SESSION (session), method, uri_string, error);
816 if (!request)
817 return NULL;
818
819 e_soup_session_preset_request (request);
820
821 return request;
822 }
823
824 /**
825 * e_soup_session_new_request_uri:
826 * @session: an #ESoupSession
827 * @method: an HTTP method
828 * @uri: a #SoupURI to use for the request
829 * @error: return location for a #GError, or %NULL
830 *
831 * Creates a new #SoupRequestHTTP, similar to soup_session_request_http_uri(),
832 * but also presets request headers with "User-Agent" to be "Evolution/version"
833 * and with "Connection" to be "close".
834 *
835 * See also e_soup_session_new_request().
836 *
837 * Returns: (transfer full): a new #SoupRequestHTTP, or %NULL on error
838 *
839 * Since: 3.26
840 **/
841 SoupRequestHTTP *
e_soup_session_new_request_uri(ESoupSession * session,const gchar * method,SoupURI * uri,GError ** error)842 e_soup_session_new_request_uri (ESoupSession *session,
843 const gchar *method,
844 SoupURI *uri,
845 GError **error)
846 {
847 SoupRequestHTTP *request;
848
849 g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
850
851 request = soup_session_request_http_uri (SOUP_SESSION (session), method, uri, error);
852 if (!request)
853 return NULL;
854
855 e_soup_session_preset_request (request);
856
857 return request;
858 }
859
860 static void
e_soup_session_extract_ssl_data(ESoupSession * session,SoupMessage * message)861 e_soup_session_extract_ssl_data (ESoupSession *session,
862 SoupMessage *message)
863 {
864 GTlsCertificate *certificate = NULL;
865
866 g_return_if_fail (E_IS_SOUP_SESSION (session));
867 g_return_if_fail (SOUP_IS_MESSAGE (message));
868
869 g_mutex_lock (&session->priv->property_lock);
870
871 g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
872 session->priv->ssl_info_set = FALSE;
873
874 g_object_get (G_OBJECT (message),
875 "tls-certificate", &certificate,
876 "tls-errors", &session->priv->ssl_certificate_errors,
877 NULL);
878
879 if (certificate) {
880 g_object_get (certificate, "certificate-pem", &session->priv->ssl_certificate_pem, NULL);
881 session->priv->ssl_info_set = TRUE;
882
883 g_object_unref (certificate);
884 }
885
886 g_mutex_unlock (&session->priv->property_lock);
887 }
888
889 static gboolean
e_soup_session_extract_google_daily_limit_error(SoupMessage * message,GError ** error)890 e_soup_session_extract_google_daily_limit_error (SoupMessage *message,
891 GError **error)
892 {
893 gchar *body;
894 gboolean contains_daily_limit = FALSE;
895
896 if (!message || !message->response_body ||
897 !message->response_body->data || !message->response_body->length)
898 return FALSE;
899
900 body = g_strndup (message->response_body->data, message->response_body->length);
901
902 /* Do not localize this string, it is returned by the server. */
903 if (body && (e_util_strstrcase (body, "Daily Limit") ||
904 e_util_strstrcase (body, "https://console.developers.google.com/"))) {
905 /* Special-case this condition and provide this error up to the UI. */
906 g_set_error_literal (error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN, body);
907 contains_daily_limit = TRUE;
908 }
909
910 g_free (body);
911
912 return contains_daily_limit;
913 }
914
915 /**
916 * e_soup_session_check_result:
917 * @session: an #ESoupSession
918 * @request: a #SoupRequestHTTP
919 * @read_bytes: (nullable): optional bytes which had been read from the stream, or %NULL
920 * @bytes_length: how many bytes had been read; ignored when @read_bytes is %NULL
921 * @error: return location for a #GError, or %NULL
922 *
923 * Checks result of the @request and sets the @error if it failed.
924 * When it failed and the @read_bytes is provided, then these are
925 * set to @request's message response_body, thus it can be used
926 * later.
927 *
928 * Returns: Whether succeeded, aka %TRUE, when no error recognized
929 * and %FALSE otherwise.
930 *
931 * Since: 3.26
932 **/
933 gboolean
e_soup_session_check_result(ESoupSession * session,SoupRequestHTTP * request,gconstpointer read_bytes,gsize bytes_length,GError ** error)934 e_soup_session_check_result (ESoupSession *session,
935 SoupRequestHTTP *request,
936 gconstpointer read_bytes,
937 gsize bytes_length,
938 GError **error)
939 {
940 SoupMessage *message;
941 gboolean success;
942
943 g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
944 g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), FALSE);
945
946 message = soup_request_http_get_message (request);
947 g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
948
949 success = SOUP_STATUS_IS_SUCCESSFUL (message->status_code);
950 if (!success) {
951 if (read_bytes && bytes_length > 0) {
952 SoupBuffer *buffer;
953
954 soup_message_body_append (message->response_body, SOUP_MEMORY_COPY, read_bytes, bytes_length);
955
956 /* This writes data to message->response_body->data */
957 buffer = soup_message_body_flatten (message->response_body);
958 if (buffer)
959 soup_buffer_free (buffer);
960 }
961
962 if (message->status_code == SOUP_STATUS_CANCELLED) {
963 g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CANCELLED, _("Operation was cancelled"));
964 } else if (message->status_code == SOUP_STATUS_FORBIDDEN &&
965 e_soup_session_extract_google_daily_limit_error (message, error)) {
966 /* Nothing to do */
967 } else {
968 g_set_error (error, SOUP_HTTP_ERROR, message->status_code,
969 _("Failed with HTTP error %d: %s"), message->status_code,
970 e_soup_session_util_status_to_string (message->status_code, message->reason_phrase));
971 }
972
973 if (message->status_code == SOUP_STATUS_SSL_FAILED)
974 e_soup_session_extract_ssl_data (session, message);
975 }
976
977 g_object_unref (message);
978
979 return success;
980 }
981
982 /**
983 * e_soup_session_send_request_sync:
984 * @session: an #ESoupSession
985 * @request: a #SoupRequestHTTP to send
986 * @cancellable: optional #GCancellable object, or %NULL
987 * @error: return location for a #GError, or %NULL
988 *
989 * Synchronously sends prepared request and returns #GInputStream
990 * that can be used to read its contents.
991 *
992 * This calls soup_request_send() internally, but it also setups
993 * the request according to #ESoupSession:source authentication
994 * settings. It also extracts information about used certificate,
995 * in case of SOUP_STATUS_SSL_FAILED error and keeps it for later use
996 * by e_soup_session_get_ssl_error_details().
997 *
998 * Use e_soup_session_send_request_simple_sync() to read whole
999 * content into a #GByteArray.
1000 *
1001 * Note that SoupSession doesn't log content read from GInputStream,
1002 * thus the caller may print the read content on its own when needed.
1003 *
1004 * Note the @request is fully filled only after there is anything
1005 * read from the resulting #GInputStream, thus use
1006 * e_soup_session_check_result() to verify that the receive had
1007 * been finished properly.
1008 *
1009 * Returns: (transfer full): A newly allocated #GInputStream,
1010 * that can be used to read from the URI pointed to by @request.
1011 * Free it with g_object_unref(), when no longer needed.
1012 *
1013 * Since: 3.26
1014 **/
1015 GInputStream *
e_soup_session_send_request_sync(ESoupSession * session,SoupRequestHTTP * request,GCancellable * cancellable,GError ** error)1016 e_soup_session_send_request_sync (ESoupSession *session,
1017 SoupRequestHTTP *request,
1018 GCancellable *cancellable,
1019 GError **error)
1020 {
1021 GInputStream *input_stream;
1022 SoupMessage *message;
1023 gboolean redirected;
1024 gint resend_count = 0;
1025 GError *local_error = NULL;
1026
1027 g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
1028 g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), NULL);
1029
1030 if (!e_soup_session_maybe_prepare_auth (session, request, cancellable, error))
1031 return NULL;
1032
1033 g_mutex_lock (&session->priv->property_lock);
1034 g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
1035 session->priv->ssl_certificate_errors = 0;
1036 session->priv->ssl_info_set = FALSE;
1037 g_mutex_unlock (&session->priv->property_lock);
1038
1039 if (session->priv->source &&
1040 e_source_has_extension (session->priv->source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
1041 message = soup_request_http_get_message (request);
1042
1043 e_soup_ssl_trust_connect (message, session->priv->source);
1044
1045 g_clear_object (&message);
1046 }
1047
1048 redirected = TRUE;
1049 while (redirected) {
1050 ESoupAuthBearer *using_bearer_auth = NULL;
1051
1052 redirected = FALSE;
1053
1054 g_mutex_lock (&session->priv->property_lock);
1055 if (session->priv->using_bearer_auth)
1056 using_bearer_auth = g_object_ref (session->priv->using_bearer_auth);
1057 g_mutex_unlock (&session->priv->property_lock);
1058
1059 if (using_bearer_auth &&
1060 e_soup_auth_bearer_is_expired (using_bearer_auth)) {
1061 message = soup_request_http_get_message (request);
1062
1063 if (!e_soup_session_setup_bearer_auth (session, message, FALSE, using_bearer_auth, cancellable, &local_error)) {
1064 if (local_error) {
1065 soup_message_set_status_full (message, SOUP_STATUS_BAD_REQUEST, local_error->message);
1066 g_propagate_error (error, local_error);
1067 } else {
1068 soup_message_set_status (message, SOUP_STATUS_BAD_REQUEST);
1069 }
1070
1071 g_object_unref (using_bearer_auth);
1072 g_clear_object (&message);
1073
1074 return NULL;
1075 }
1076
1077 g_clear_object (&message);
1078 }
1079
1080 g_clear_object (&using_bearer_auth);
1081
1082 input_stream = soup_request_send (SOUP_REQUEST (request), cancellable, &local_error);
1083 if (input_stream) {
1084 message = soup_request_http_get_message (request);
1085
1086 if (message && e_soup_session_get_log_level (session) == SOUP_LOGGER_LOG_BODY)
1087 input_stream = e_soup_logger_attach (message, input_stream);
1088
1089 if (message && SOUP_STATUS_IS_REDIRECTION (message->status_code)) {
1090 /* libsoup uses 20, but the constant is not in any public header */
1091 if (resend_count >= 30) {
1092 soup_message_set_status (message, SOUP_STATUS_TOO_MANY_REDIRECTS);
1093 } else {
1094 const gchar *new_location;
1095
1096 new_location = soup_message_headers_get_list (message->response_headers, "Location");
1097 if (new_location) {
1098 SoupURI *new_uri;
1099
1100 new_uri = soup_uri_new_with_base (soup_message_get_uri (message), new_location);
1101
1102 soup_message_set_uri (message, new_uri);
1103
1104 g_clear_object (&input_stream);
1105 soup_uri_free (new_uri);
1106
1107 g_signal_emit_by_name (message, "restarted");
1108
1109 resend_count++;
1110 redirected = TRUE;
1111 }
1112 }
1113 }
1114
1115 g_clear_object (&message);
1116 }
1117 }
1118
1119 if (input_stream)
1120 return input_stream;
1121
1122 if (g_error_matches (local_error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE)) {
1123 local_error->domain = SOUP_HTTP_ERROR;
1124 local_error->code = SOUP_STATUS_SSL_FAILED;
1125 }
1126
1127 if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
1128 message = soup_request_http_get_message (request);
1129
1130 e_soup_session_extract_ssl_data (session, message);
1131
1132 g_clear_object (&message);
1133 } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN)) {
1134 message = soup_request_http_get_message (request);
1135
1136 if (e_soup_session_extract_google_daily_limit_error (message, error))
1137 g_clear_error (&local_error);
1138
1139 g_clear_object (&message);
1140 }
1141
1142 if (local_error)
1143 g_propagate_error (error, local_error);
1144
1145 return NULL;
1146 }
1147
1148 /**
1149 * e_soup_session_send_request_simple_sync:
1150 * @session: an #ESoupSession
1151 * @request: a #SoupRequestHTTP to send
1152 * @cancellable: optional #GCancellable object, or %NULL
1153 * @error: return location for a #GError, or %NULL
1154 *
1155 * Similar to e_soup_session_send_request_sync(), except it reads
1156 * whole response content into memory and returns it as a #GByteArray.
1157 * Use e_soup_session_send_request_sync() when you want to have
1158 * more control on the content read.
1159 *
1160 * The function prints read content to stdout when
1161 * e_soup_session_get_log_level() returns #SOUP_LOGGER_LOG_BODY.
1162 *
1163 * Returns: (transfer full): A newly allocated #GByteArray,
1164 * which contains whole content from the URI pointed to by @request.
1165 *
1166 * Since: 3.26
1167 **/
1168 GByteArray *
e_soup_session_send_request_simple_sync(ESoupSession * session,SoupRequestHTTP * request,GCancellable * cancellable,GError ** error)1169 e_soup_session_send_request_simple_sync (ESoupSession *session,
1170 SoupRequestHTTP *request,
1171 GCancellable *cancellable,
1172 GError **error)
1173 {
1174 GInputStream *input_stream;
1175 GByteArray *bytes;
1176 gint expected_length;
1177 gpointer buffer;
1178 gsize nread = 0;
1179 gboolean success = FALSE;
1180
1181 g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
1182 g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), NULL);
1183
1184 input_stream = e_soup_session_send_request_sync (session, request, cancellable, error);
1185 if (!input_stream)
1186 return NULL;
1187
1188 expected_length = soup_request_get_content_length (SOUP_REQUEST (request));
1189 if (expected_length > 0)
1190 bytes = g_byte_array_sized_new (expected_length);
1191 else
1192 bytes = g_byte_array_new ();
1193
1194 buffer = g_malloc (BUFFER_SIZE);
1195
1196 while (success = g_input_stream_read_all (input_stream, buffer, BUFFER_SIZE, &nread, cancellable, error),
1197 success && nread > 0) {
1198 g_byte_array_append (bytes, buffer, nread);
1199 }
1200
1201 g_free (buffer);
1202 g_object_unref (input_stream);
1203
1204 if (success)
1205 success = e_soup_session_check_result (session, request, bytes->data, bytes->len, error);
1206
1207 if (!success) {
1208 g_byte_array_free (bytes, TRUE);
1209 bytes = NULL;
1210 }
1211
1212 return bytes;
1213 }
1214
1215 /**
1216 * e_soup_session_util_status_to_string:
1217 * @status_code: an HTTP status code
1218 * @reason_phrase: (nullable): preferred string to use for the message, or %NULL
1219 *
1220 * Returns the @reason_phrase, if it's non-%NULL and non-empty, a static string
1221 * corresponding to @status_code. In case neither that can be found a localized
1222 * "Unknown error" message is returned.
1223 *
1224 * Returns: (transfer none): Error text based on given arguments. The returned
1225 * value is valid as long as @reason_phrase is not freed.
1226 *
1227 * Since: 3.26
1228 **/
1229 const gchar *
e_soup_session_util_status_to_string(guint status_code,const gchar * reason_phrase)1230 e_soup_session_util_status_to_string (guint status_code,
1231 const gchar *reason_phrase)
1232 {
1233 if (!reason_phrase || !*reason_phrase)
1234 reason_phrase = soup_status_get_phrase (status_code);
1235
1236 if (reason_phrase && *reason_phrase)
1237 return reason_phrase;
1238
1239 return _("Unknown error");
1240 }
1241
1242 static gboolean
part_needs_encoding(const gchar * part)1243 part_needs_encoding (const gchar *part)
1244 {
1245 const gchar *pp;
1246
1247 if (!part || !*part)
1248 return FALSE;
1249
1250 for (pp = part; *pp; pp++) {
1251 if (!strchr ("/!()+-*~';,.$&_", *pp) &&
1252 !g_ascii_isalnum (*pp) &&
1253 (*pp != '%' || pp[1] != '4' || pp[2] != '0') && /* cover '%40', aka '@', as a common case, to avoid unnecessary allocations */
1254 (*pp != '%' || pp[1] != '2' || pp[2] != '0')) { /* '%20', aka ' ' */
1255 break;
1256 }
1257 }
1258
1259 return *pp;
1260 }
1261
1262 /**
1263 * e_soup_session_util_normalize_uri_path:
1264 * @suri: a #SoupURI to normalize the path for
1265 *
1266 * Normalizes the path of the @suri, aka encodes characters, which should
1267 * be encoded, if needed. Returns, whether any change had been made to the path.
1268 * It doesn't touch other parts of the @suri.
1269 *
1270 * Returns: whether made any changes
1271 *
1272 * Since: 3.38
1273 **/
1274 gboolean
e_soup_session_util_normalize_uri_path(SoupURI * suri)1275 e_soup_session_util_normalize_uri_path (SoupURI *suri)
1276 {
1277 const gchar *path;
1278 gchar **parts, *tmp;
1279 gboolean did_change = FALSE;
1280 gint ii;
1281
1282 if (!suri)
1283 return FALSE;
1284
1285 path = soup_uri_get_path (suri);
1286
1287 if (!path || !*path || g_strcmp0 (path, "/") == 0)
1288 return FALSE;
1289
1290 if (!part_needs_encoding (path))
1291 return FALSE;
1292
1293 parts = g_strsplit (path, "/", -1);
1294
1295 if (!parts)
1296 return FALSE;
1297
1298 for (ii = 0; parts[ii]; ii++) {
1299 gchar *part = parts[ii];
1300
1301 if (part_needs_encoding (part)) {
1302 if (strchr (part, '%')) {
1303 tmp = soup_uri_decode (part);
1304 g_free (part);
1305 part = tmp;
1306 }
1307
1308 tmp = soup_uri_encode (part, NULL);
1309 g_free (part);
1310 parts[ii] = tmp;
1311 }
1312 }
1313
1314 tmp = g_strjoinv ("/", parts);
1315 if (g_strcmp0 (path, tmp) != 0) {
1316 soup_uri_set_path (suri, tmp);
1317 did_change = TRUE;
1318 }
1319
1320 g_free (tmp);
1321 g_strfreev (parts);
1322
1323 return did_change;
1324 }
1325