GCC Code Coverage Report


Directory: src/
File: msg-service.c
Date: 2025-02-08 02:01:04
Exec Total Coverage
Lines: 132 149 88.6%
Functions: 23 24 95.8%
Branches: 46 64 71.9%

Line Branch Exec Source
1 /* Copyright 2022-2024 Jan-Michael Brummer <jan-michael.brummer1@volkswagen.de>
2 *
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU Lesser General Public License as published by
5 * the Free Software Foundation, either version 3 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16
17 #include "config.h"
18 #include "msg-service.h"
19
20 #include "msg-error.h"
21 #include "msg-json-utils.h"
22 #include "msg-private.h"
23
24 typedef struct _MsgServicePrivate MsgServicePrivate;
25 struct _MsgServicePrivate {
26 MsgAuthorizer *authorizer;
27 SoupSession *session;
28 };
29
30
5/6
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 315 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 315 times.
639 G_DEFINE_TYPE_WITH_PRIVATE (MsgService, msg_service, G_TYPE_OBJECT);
31
32 #define MSG_SERVICE_GET_PRIVATE(o) ((MsgServicePrivate *)msg_service_get_instance_private ((o)))
33
34 enum {
35 PROP_0,
36 PROP_AUTHORIZER,
37 PROP_COUNT
38 };
39
40 static GParamSpec *properties[PROP_COUNT] = { NULL, };
41
42 gboolean
43 98 msg_service_refresh_authorization (MsgService *self,
44 GCancellable *cancellable,
45 GError **error)
46 {
47 98 MsgServicePrivate *priv = MSG_SERVICE_GET_PRIVATE (self);
48
49
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 97 times.
98 if (!priv->authorizer) {
50 1 g_set_error (error, MSG_ERROR, MSG_ERROR_FAILED, "Authorizer is NULL");
51 1 return FALSE;
52 }
53
54 97 return msg_authorizer_refresh_authorization (priv->authorizer, cancellable, error);
55 }
56
57 guint
58 103 msg_service_get_https_port (void)
59 {
60 const char *port_string;
61
62 /* Allow changing the HTTPS port just for testing. */
63 103 port_string = g_getenv ("MSG_HTTPS_PORT");
64
2/2
✓ Branch 0 taken 102 times.
✓ Branch 1 taken 1 times.
103 if (port_string != NULL) {
65 102 guint64 port = g_ascii_strtoull (port_string, NULL, 10);
66
67
2/2
✓ Branch 0 taken 98 times.
✓ Branch 1 taken 4 times.
102 if (port != 0) {
68 98 g_debug ("Overriding message port to %" G_GUINT64_FORMAT ".", port);
69 98 return port;
70 }
71 }
72
73 /* Return the default. */
74 5 return 443;
75 }
76
77 gboolean
78 55 msg_service_accept_certificate_cb (__attribute__ ((unused)) SoupMessage *msg,
79 __attribute__ ((unused)) GTlsCertificate *tls_cert,
80 __attribute__ ((unused)) GTlsCertificateFlags tls_errors,
81 gpointer session)
82 {
83 55 return !!g_object_get_data (session, "msg-lax-ssl");
84 }
85
86
87 SoupMessage *
88 100 msg_service_new_message_from_uri (MsgService *self,
89 const char *method,
90 GUri *uri)
91 {
92 100 MsgServicePrivate *priv = MSG_SERVICE_GET_PRIVATE (self);
93 SoupMessage *ret;
94
95 100 ret = soup_message_new_from_uri (method, uri);
96
97 100 g_signal_connect (ret, "accept-certificate", G_CALLBACK (msg_service_accept_certificate_cb), priv->session);
98
99 100 return ret;
100 }
101
102
103 /**
104 * msg_service_build_message:
105 * @method: transfer method
106 * @uri: uri to access
107 * @etag: an optional etag
108 * @etag_if_match: use etag if
109 *
110 * Construct and checks a #SoupMessage for transfer
111 *
112 * Returns: (transfer full): a #SoupMessage or NULL on error.
113 */
114 SoupMessage *
115 101 msg_service_build_message (MsgService *self,
116 const char *method,
117 const char *uri,
118 const char *etag,
119 gboolean etag_if_match)
120 {
121 SoupMessage *message;
122 101 g_autoptr (GUri) _uri = NULL;
123 101 g_autoptr (GUri) _uri_parsed = NULL;
124
125 101 _uri_parsed = g_uri_parse (uri, SOUP_HTTP_URI_FLAGS, NULL);
126 202 _uri = g_uri_build_with_user (g_uri_get_flags (_uri_parsed),
127 g_uri_get_scheme (_uri_parsed),
128 g_uri_get_user (_uri_parsed),
129 g_uri_get_password (_uri_parsed),
130 g_uri_get_auth_params (_uri_parsed),
131 g_uri_get_host (_uri_parsed),
132 101 msg_service_get_https_port (),
133 g_uri_get_path (_uri_parsed),
134 g_uri_get_query (_uri_parsed),
135 g_uri_get_fragment (_uri_parsed));
136
137
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 100 times.
101 if (g_strcmp0 (g_uri_get_scheme (_uri), "https") != 0)
138 1 return NULL;
139
140 100 message = msg_service_new_message_from_uri (self, method, _uri);
141
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 98 times.
100 if (etag)
142
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 soup_message_headers_append (soup_message_get_request_headers (message), (etag_if_match == TRUE) ? "If-Match" : "If-None-Match", etag);
143
144 100 return message;
145 }
146
147 /**
148 * msg_service_send:
149 * @self: a msg service
150 * @message: a #SoupMessage
151 * @cancellable: a #GCancellable
152 * @error: a #Gerror
153 *
154 * Adds authorizer information to `message` and send it.
155 *
156 * Returns: (transfer full): a #GInputStream
157 */
158 GInputStream *
159 1 msg_service_send (MsgService *self,
160 SoupMessage *message,
161 GCancellable *cancellable,
162 GError **error)
163 {
164 1 MsgServicePrivate *priv = MSG_SERVICE_GET_PRIVATE (self);
165 GInputStream *stream;
166
167 1 msg_authorizer_process_request (priv->authorizer, message);
168
169 1 retry:
170 1 stream = soup_session_send (priv->session, message, cancellable, error);
171
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (msg_service_handle_rate_limiting (message))
172 goto retry;
173
174 1 return stream;
175 }
176
177 /**
178 * msg_service_send_and_read:
179 * @self: a msg service
180 * @message: a #SoupMessage
181 * @cancellable: a #GCancellable
182 * @error: a #GError
183 *
184 * Adds authorizer information to `message` and send it.
185 *
186 * Returns: (transfer full): a #GBytes or %NULL on error.
187 */
188 GBytes *
189 17 msg_service_send_and_read (MsgService *self,
190 SoupMessage *message,
191 GCancellable *cancellable,
192 GError **error)
193 {
194 17 MsgServicePrivate *priv = MSG_SERVICE_GET_PRIVATE (self);
195 GBytes *bytes;
196
197 17 msg_authorizer_process_request (priv->authorizer, message);
198
199 17 retry:
200 17 bytes = soup_session_send_and_read (priv->session, message, cancellable, error);
201
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (msg_service_handle_rate_limiting (message))
202 goto retry;
203
204 17 return bytes;
205 }
206
207 /**
208 * msg_service_send_and_parse_response:
209 * @self: a msg service
210 * @message: a #SoupMessage
211 * @cancellable: a #GCancellable
212 * @error: a #GError
213 *
214 * A combination of `msg_service_send_and_read` and `msg_service_parse_response`
215 *
216 * Returns: (transfer full): a #JsonParser or %NULL on error
217 */
218 JsonParser *
219 78 msg_service_send_and_parse_response (MsgService *self,
220 SoupMessage *message,
221 JsonObject **object,
222 GCancellable *cancellable,
223 GError **error)
224 {
225 78 MsgServicePrivate *priv = MSG_SERVICE_GET_PRIVATE (self);
226 156 g_autoptr (GBytes) response = NULL;
227
228 78 retry:
229 78 msg_authorizer_process_request (priv->authorizer, message);
230
231 78 response = soup_session_send_and_read (priv->session, message, cancellable, error);
232
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 78 times.
78 if (msg_service_handle_rate_limiting (message))
233 goto retry;
234
235
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 78 times.
78 if (!response)
236 return NULL;
237
238 78 return msg_service_parse_response (response, object, error);
239 }
240
241
242 static void
243 4 msg_service_set_authorizer (MsgService *self,
244 MsgAuthorizer *authorizer)
245 {
246 4 MsgServicePrivate *priv = MSG_SERVICE_GET_PRIVATE (self);
247
248
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 g_return_if_fail (MSG_IS_SERVICE (self));
249
6/10
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 3 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 3 times.
4 g_return_if_fail (authorizer == NULL || MSG_IS_AUTHORIZER (authorizer));
250
251
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 g_clear_object (&priv->authorizer);
252 4 priv->authorizer = authorizer;
253
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
4 if (priv->authorizer)
254 3 g_object_ref (priv->authorizer);
255
256 4 g_object_notify (G_OBJECT (self), "authorizer");
257 }
258
259 static void
260 4 msg_service_set_property (GObject *object,
261 guint property_id,
262 const GValue *value,
263 GParamSpec *pspec)
264 {
265 4 MsgService *self = MSG_SERVICE (object);
266
267
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 switch (property_id) {
268 4 case PROP_AUTHORIZER:
269 4 msg_service_set_authorizer (self, MSG_AUTHORIZER (g_value_get_object (value)));
270 4 break;
271 default:
272 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
273 break;
274 }
275 4 }
276
277 static void
278 msg_service_get_property (GObject *object,
279 guint property_id,
280 __attribute__ ((unused)) GValue *value,
281 GParamSpec *pspec)
282 {
283 switch (property_id) {
284 default:
285 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
286 break;
287 }
288 }
289
290 static void
291 3087 soup_log_printer (__attribute__ ((unused)) SoupLogger *logger,
292 __attribute__ ((unused)) SoupLoggerLogLevel level,
293 char direction,
294 const char *data,
295 __attribute__ ((unused)) gpointer user_data)
296 {
297 3087 g_debug ("%c %s", direction, data);
298 3087 }
299
300 SoupLoggerLogLevel
301 8 msg_service_get_log_level (void)
302 {
303 static int level = -1;
304
305
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
8 if (level < 0) {
306 4 const char *env = g_getenv ("MSG_DEBUG");
307
308
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (env != NULL) {
309 4 level = atoi (env);
310 } else {
311 level = 0;
312 }
313 }
314
315 8 return level;
316 }
317
318 static void
319 4 msg_service_init (MsgService *self)
320 {
321 4 MsgServicePrivate *priv = MSG_SERVICE_GET_PRIVATE (self);
322
323 4 priv->session = soup_session_new ();
324
325 /* Iff MSG_LAX_SSL_CERTIFICATES=1, relax SSL certificate validation to allow using invalid/unsigned certificates for testing. */
326
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (g_strcmp0 (g_getenv ("MSG_LAX_SSL_CERTIFICATES"), "1") == 0) {
327 4 g_object_set_data (G_OBJECT (priv->session), "msg-lax-ssl", (gpointer)TRUE);
328 }
329
330
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (msg_service_get_log_level () > SOUP_LOGGER_LOG_NONE) {
331 4 g_autoptr (SoupLogger) logger = NULL;
332
333 4 logger = soup_logger_new (msg_service_get_log_level ());
334 4 soup_logger_set_printer (logger, (SoupLoggerPrinter)soup_log_printer, NULL, NULL);
335 4 soup_session_add_feature (priv->session, SOUP_SESSION_FEATURE (logger));
336 }
337 4 }
338
339 static void
340 5 msg_service_class_init (MsgServiceClass *class)
341 {
342 5 GObjectClass *object_class = G_OBJECT_CLASS (class);
343
344 5 object_class->set_property = msg_service_set_property;
345 5 object_class->get_property = msg_service_get_property;
346
347 5 properties [PROP_AUTHORIZER] = g_param_spec_object ("authorizer",
348 "Authorizer",
349 "The authorizer for this service",
350 MSG_TYPE_AUTHORIZER,
351 G_PARAM_STATIC_STRINGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
352
353 5 g_object_class_install_properties (object_class, PROP_COUNT, properties);
354 5 }
355
356 /**
357 * msg_service_parse_response:
358 * @bytes: input bytes containing response buffer
359 * @object: a pointer to the returning root object
360 * @error: a #GError
361 *
362 * Parse response data and check for errors. In case
363 * no errors are found, return json root object.
364 *
365 * Returns: (transfer full): a #JsonParser
366 */
367 JsonParser *
368 81 msg_service_parse_response (GBytes *bytes,
369 JsonObject **object,
370 GError **error)
371 {
372 81 g_autoptr (JsonParser) parser = NULL;
373 81 JsonObject *root_object = NULL;
374 81 JsonNode *root = NULL;
375 const char *content;
376 gsize len;
377
378 81 content = g_bytes_get_data (bytes, &len);
379
380 81 parser = json_parser_new ();
381
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 80 times.
81 if (!json_parser_load_from_data (parser, content, len, error))
382 1 return NULL;
383
384 80 root = json_parser_get_root (parser);
385
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 79 times.
80 if (!JSON_NODE_HOLDS_OBJECT (root)) {
386 1 g_set_error (error,
387 msg_error_quark (),
388 MSG_ERROR_FAILED,
389 "Invalid response, didn't get JSON object");
390 1 return NULL;
391 }
392
393 79 root_object = json_node_get_object (root);
394 /* g_print (json_to_string (root, TRUE)); */
395
396
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 77 times.
79 if (json_object_has_member (root_object, "error")) {
397 2 JsonObject *error_object = json_object_get_object_member (root_object, "error");
398 2 const char *message = json_object_get_string_member (error_object, "message");
399
400 2 g_set_error_literal (error,
401 MSG_ERROR,
402 MSG_ERROR_FAILED,
403 message);
404 2 return NULL;
405 }
406
407
2/2
✓ Branch 0 taken 76 times.
✓ Branch 1 taken 1 times.
77 if (object)
408 76 *object = root_object;
409
410 77 return g_steal_pointer (&parser);
411 }
412
413 /**
414 * msg_service_get_session:
415 * @self: a #MsgService
416 *
417 * Get related soup session
418 *
419 * Returns: (transfer none): a #SoupSession
420 */
421 SoupSession *
422 5 msg_service_get_session (MsgService *self)
423 {
424 5 MsgServicePrivate *priv = MSG_SERVICE_GET_PRIVATE (self);
425 5 return priv->session;
426 }
427
428 /**
429 * msg_service_get_authorizer:
430 * @self: a #MsgService
431 *
432 * Get related authorizer.
433 *
434 * Returns: (transfer none): a #MsgAuthorizer
435 */
436 MsgAuthorizer *
437 2 msg_service_get_authorizer (MsgService *self)
438 {
439 2 MsgServicePrivate *priv = MSG_SERVICE_GET_PRIVATE (self);
440 2 return priv->authorizer;
441 }
442
443 /**
444 * msg_service_get_next_link:
445 * @object: a #JsonObject
446 *
447 * Get next link
448 *
449 * Returns: (transfer full): next link or %NULL if not available
450 */
451 char *
452 17 msg_service_get_next_link (JsonObject *object)
453 {
454 34 return g_strdup (msg_json_object_get_string (object, "@odata.nextLink"));
455 }
456
457 gboolean
458 97 msg_service_handle_rate_limiting (SoupMessage *msg)
459 {
460
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 97 times.
97 if (soup_message_get_status (msg) == 429) {
461 const char *retry_after = soup_message_headers_get_one (soup_message_get_response_headers (msg), "retry-after");
462
463 if (retry_after) {
464 int seconds = atoi (retry_after);
465 g_usleep (seconds * G_USEC_PER_SEC);
466 return TRUE;
467 }
468 }
469
470 97 return FALSE;
471 }
472