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