Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
2 : : *
3 : : * Copyright 2025 GNOME Foundation, Inc.
4 : : *
5 : : * SPDX-License-Identifier: LGPL-2.1-or-later
6 : : *
7 : : * This library is free software; you can redistribute it and/or
8 : : * modify it under the terms of the GNU Lesser General Public
9 : : * License as published by the Free Software Foundation; either
10 : : * version 2.1 of the License, or (at your option) any later version.
11 : : *
12 : : * This library is distributed in the hope that it will be useful,
13 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 : : * Lesser General Public License for more details.
16 : : *
17 : : * You should have received a copy of the GNU Lesser General Public
18 : : * License along with this library; if not, write to the Free Software
19 : : * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 : : *
21 : : * Authors:
22 : : * - Philip Withnall <pwithnall@gnome.org>
23 : : */
24 : :
25 : : #include "config.h"
26 : :
27 : : #include <cdb.h>
28 : : #include <glib.h>
29 : : #include <glib-object.h>
30 : : #include <glib-unix.h>
31 : : #include <glib/gi18n-lib.h>
32 : : #include <gio/gio.h>
33 : : #include <libmalcontent/malcontent.h>
34 : : #include <libsoup/soup.h>
35 : : #include <pwd.h>
36 : : #include <sys/file.h>
37 : :
38 : : #include "filter-list.h"
39 : : #include "filter-updater.h"
40 : :
41 : :
42 : : static void mct_filter_updater_dispose (GObject *object);
43 : : static void mct_filter_updater_get_property (GObject *object,
44 : : guint property_id,
45 : : GValue *value,
46 : : GParamSpec *pspec);
47 : : static void mct_filter_updater_set_property (GObject *object,
48 : : guint property_id,
49 : : const GValue *value,
50 : : GParamSpec *pspec);
51 : :
52 : : /* https://datatracker.ietf.org/doc/html/rfc2181#section-11 not including trailing nul */
53 : : #define HOSTNAME_MAX 255 /* bytes */
54 : :
55 [ + + ]: 6 : G_DEFINE_QUARK (MctFilterUpdaterError, mct_filter_updater_error)
56 : :
57 : : /**
58 : : * MctFilterUpdater:
59 : : *
60 : : * Processing class which updates the compiled filter for one or more users.
61 : : *
62 : : * Updating the compiled filters involves:
63 : : *
64 : : * 1. Downloading all the filter lists for the users being updated. Users may
65 : : * re-use the same filter lists, in which case they are only downloaded
66 : : * once. Downloaded filter lists are cached.
67 : : * 2. For each user, the filter rules from all their filter lists are combined
68 : : * into a unified list, which is then compiled into a tinycdb database for
69 : : * that user.
70 : : * 3. The compiled tinycdb database is atomically replaced over the user’s
71 : : * current database. If the user is currently logged in, any programs which
72 : : * do name resolution will reload the database via a file change
73 : : * notification, and will start using the new database immediately.
74 : : * 4. If the filter lists are being updated for *all* users (if `filter_uid` is
75 : : * zero in the call to
76 : : * [method@Malcontent.FilterUpdater.update_filters_async]) then old cached
77 : : * filter lists are cleared from the download cache.
78 : : *
79 : : * The tinycdb database is what’s read by the NSS module which implements DNS
80 : : * filtering.
81 : : *
82 : : * In terms of file storage and permissions, two directories are used:
83 : : * - `$CACHE_DIRECTORY` (typically `/var/cache/malcontent-webd`), used to store
84 : : * downloaded filter lists. Only accessible to the `malcontent-webd` user.
85 : : * - `$STATE_DIRECTORY` (typically `/var/cache/malcontent-webd`, with a symlink
86 : : * to it from `/var/cache/malcontent-nss` for the NSS module to read), used
87 : : * to store the compiled filter lists in the `filter-lists` subdirectory.
88 : : * Writable by the `malcontent-webd` user, and world readable.
89 : : *
90 : : * These are [provided by systemd](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RuntimeDirectory=).
91 : : *
92 : : * Ideally we’d have a file permissions setup where each
93 : : * `/var/cache/malcontent-webd/filter-lists/$user` file is readable by
94 : : * `malcontent-webd` and `$user` (and no other users), but not modifiable by any
95 : : * user except `malcontent-webd`. And `/var/cache/malcontent-webd/filter-lists`
96 : : * is not listable by any user except `malcontent-webd`. Unfortunately that’s
97 : : * not possible, so all files are owned by `malcontent-webd` and are world
98 : : * readable.
99 : : *
100 : : * Since: 0.14.0
101 : : */
102 : : struct _MctFilterUpdater
103 : : {
104 : : GObject parent;
105 : :
106 : : MctManager *policy_manager; /* (owned) (not nullable) */
107 : : MctUserManager *user_manager; /* (owned) (not nullable) */
108 : : GFile *state_directory; /* (owned) (not nullable) */
109 : : GFile *cache_directory; /* (owned) (not nullable) */
110 : :
111 : : gboolean update_in_progress;
112 : : };
113 : :
114 : : typedef enum
115 : : {
116 : : PROP_POLICY_MANAGER = 1,
117 : : PROP_USER_MANAGER,
118 : : PROP_STATE_DIRECTORY,
119 : : PROP_CACHE_DIRECTORY,
120 : : } MctFilterUpdaterProperty;
121 : :
122 : : static GParamSpec *props[PROP_CACHE_DIRECTORY + 1] = { NULL, };
123 : :
124 [ + + + - : 10 : G_DEFINE_TYPE (MctFilterUpdater, mct_filter_updater, G_TYPE_OBJECT)
+ + ]
125 : :
126 : : static void
127 : 1 : mct_filter_updater_class_init (MctFilterUpdaterClass *klass)
128 : : {
129 : 1 : GObjectClass *object_class = (GObjectClass *) klass;
130 : :
131 : 1 : object_class->dispose = mct_filter_updater_dispose;
132 : 1 : object_class->get_property = mct_filter_updater_get_property;
133 : 1 : object_class->set_property = mct_filter_updater_set_property;
134 : :
135 : : /**
136 : : * MctFilterUpdater:policy-manager: (not nullable)
137 : : *
138 : : * Parental controls policy manager.
139 : : *
140 : : * This provides access to the users’ web filtering settings.
141 : : *
142 : : * Since: 0.14.0
143 : : */
144 : 1 : props[PROP_POLICY_MANAGER] =
145 : 1 : g_param_spec_object ("policy-manager", NULL, NULL,
146 : : MCT_TYPE_MANAGER,
147 : : G_PARAM_READWRITE |
148 : : G_PARAM_CONSTRUCT_ONLY |
149 : : G_PARAM_STATIC_STRINGS);
150 : :
151 : : /**
152 : : * MctFilterUpdater:user-manager: (not nullable)
153 : : *
154 : : * User manager providing access to the system’s user database.
155 : : *
156 : : * This must have already been asynchronously loaded using
157 : : * [method@Malcontent.UserManager.load_async] before the filter updater is
158 : : * run.
159 : : *
160 : : * Since: 0.14.0
161 : : */
162 : 1 : props[PROP_USER_MANAGER] =
163 : 1 : g_param_spec_object ("user-manager", NULL, NULL,
164 : : MCT_TYPE_USER_MANAGER,
165 : : G_PARAM_READWRITE |
166 : : G_PARAM_CONSTRUCT_ONLY |
167 : : G_PARAM_STATIC_STRINGS);
168 : :
169 : : /**
170 : : * MctFilterUpdater:state-directory: (not nullable)
171 : : *
172 : : * Directory to store persistent state in.
173 : : *
174 : : * The compiled tinycdb databases are stored beneath this directory.
175 : : *
176 : : * Since: 0.14.0
177 : : */
178 : 1 : props[PROP_STATE_DIRECTORY] =
179 : 1 : g_param_spec_object ("state-directory", NULL, NULL,
180 : : G_TYPE_FILE,
181 : : G_PARAM_READWRITE |
182 : : G_PARAM_CONSTRUCT_ONLY |
183 : : G_PARAM_STATIC_STRINGS);
184 : :
185 : : /**
186 : : * MctFilterUpdater:cache-directory: (not nullable)
187 : : *
188 : : * Directory to store cached data in.
189 : : *
190 : : * The downloaded filter list URI files are cached beneath this directory.
191 : : *
192 : : * Since: 0.14.0
193 : : */
194 : 1 : props[PROP_CACHE_DIRECTORY] =
195 : 1 : g_param_spec_object ("cache-directory", NULL, NULL,
196 : : G_TYPE_FILE,
197 : : G_PARAM_READWRITE |
198 : : G_PARAM_CONSTRUCT_ONLY |
199 : : G_PARAM_STATIC_STRINGS);
200 : :
201 : 1 : g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
202 : 1 : }
203 : :
204 : : static void
205 : 1 : mct_filter_updater_init (MctFilterUpdater *self)
206 : : {
207 : 1 : }
208 : :
209 : : static void
210 : 0 : mct_filter_updater_dispose (GObject *object)
211 : : {
212 : 0 : MctFilterUpdater *self = MCT_FILTER_UPDATER (object);
213 : :
214 : 0 : g_assert (!self->update_in_progress);
215 : :
216 [ # # ]: 0 : g_clear_object (&self->policy_manager);
217 [ # # ]: 0 : g_clear_object (&self->user_manager);
218 [ # # ]: 0 : g_clear_object (&self->state_directory);
219 [ # # ]: 0 : g_clear_object (&self->cache_directory);
220 : :
221 : : /* Chain up to the parent class */
222 : 0 : G_OBJECT_CLASS (mct_filter_updater_parent_class)->dispose (object);
223 : 0 : }
224 : :
225 : : static void
226 : 0 : mct_filter_updater_get_property (GObject *object,
227 : : guint property_id,
228 : : GValue *value,
229 : : GParamSpec *pspec)
230 : : {
231 : 0 : MctFilterUpdater *self = MCT_FILTER_UPDATER (object);
232 : :
233 [ # # # # : 0 : switch ((MctFilterUpdaterProperty) property_id)
# ]
234 : : {
235 : 0 : case PROP_POLICY_MANAGER:
236 : 0 : g_value_set_object (value, self->policy_manager);
237 : 0 : break;
238 : 0 : case PROP_USER_MANAGER:
239 : 0 : g_value_set_object (value, self->user_manager);
240 : 0 : break;
241 : 0 : case PROP_STATE_DIRECTORY:
242 : 0 : g_value_set_object (value, self->state_directory);
243 : 0 : break;
244 : 0 : case PROP_CACHE_DIRECTORY:
245 : 0 : g_value_set_object (value, self->cache_directory);
246 : 0 : break;
247 : 0 : default:
248 : : g_assert_not_reached ();
249 : : }
250 : 0 : }
251 : :
252 : : static void
253 : 4 : mct_filter_updater_set_property (GObject *object,
254 : : guint property_id,
255 : : const GValue *value,
256 : : GParamSpec *pspec)
257 : : {
258 : 4 : MctFilterUpdater *self = MCT_FILTER_UPDATER (object);
259 : :
260 [ + + + + : 4 : switch ((MctFilterUpdaterProperty) property_id)
- ]
261 : : {
262 : 1 : case PROP_POLICY_MANAGER:
263 : : /* Construct only. */
264 : 1 : g_assert (self->policy_manager == NULL);
265 : 1 : self->policy_manager = g_value_dup_object (value);
266 : 1 : break;
267 : 1 : case PROP_USER_MANAGER:
268 : : /* Construct only */
269 : 1 : g_assert (self->user_manager == NULL);
270 : 1 : self->user_manager = g_value_dup_object (value);
271 : 1 : break;
272 : 1 : case PROP_STATE_DIRECTORY:
273 : : /* Construct only. */
274 : 1 : g_assert (self->state_directory == NULL);
275 : 1 : self->state_directory = g_value_dup_object (value);
276 : 1 : break;
277 : 1 : case PROP_CACHE_DIRECTORY:
278 : : /* Construct only. */
279 : 1 : g_assert (self->cache_directory == NULL);
280 : 1 : self->cache_directory = g_value_dup_object (value);
281 : 1 : break;
282 : 0 : default:
283 : : g_assert_not_reached ();
284 : : }
285 : 4 : }
286 : :
287 : : typedef struct
288 : : {
289 : : uid_t uid;
290 : : char *username; /* (not nullable) (owned) */
291 : : MctWebFilter *web_filter; /* (nullable) (owned) */
292 : : } UserData;
293 : :
294 : : static void
295 : 0 : user_data_clear (UserData *data)
296 : : {
297 [ # # ]: 0 : g_clear_pointer (&data->username, g_free);
298 [ # # ]: 0 : g_clear_pointer (&data->web_filter, mct_web_filter_unref);
299 : 0 : }
300 : :
301 : : typedef struct
302 : : {
303 : : uid_t filter_uid;
304 : : GArray *user_datas; /* (nullable) (owned) (element-type UserData) */
305 : : GPtrArray *filter_uris; /* (nullable) (owned) (element-type utf8) */
306 : : GFile *filter_lists_dir; /* (owned) */
307 : : int filter_lists_dir_fd; /* (owned) */
308 : : } UpdateFiltersData;
309 : :
310 : : static void
311 : 0 : update_filters_data_free (UpdateFiltersData *data)
312 : : {
313 [ # # ]: 0 : g_clear_pointer (&data->user_datas, g_array_unref);
314 [ # # ]: 0 : g_clear_pointer (&data->filter_uris, g_ptr_array_unref);
315 [ # # ]: 0 : g_clear_object (&data->filter_lists_dir);
316 : 0 : g_clear_fd (&data->filter_lists_dir_fd, NULL);
317 : 0 : g_free (data);
318 : 0 : }
319 : :
320 [ # # ]: 0 : G_DEFINE_AUTOPTR_CLEANUP_FUNC (UpdateFiltersData, update_filters_data_free)
321 : :
322 : : static void calculate_users_cb (GObject *object,
323 : : GAsyncResult *result,
324 : : void *user_data);
325 : :
326 : : static void
327 : 0 : calculate_users_async (MctFilterUpdater *self,
328 : : uid_t filter_uid,
329 : : GCancellable *cancellable,
330 : : GAsyncReadyCallback callback,
331 : : void *user_data)
332 : : {
333 : 0 : g_autoptr(GTask) task = NULL;
334 : :
335 : 0 : task = g_task_new (self, cancellable, callback, user_data);
336 [ # # ]: 0 : g_task_set_source_tag (task, calculate_users_async);
337 : 0 : g_task_set_task_data (task, GUINT_TO_POINTER (filter_uid), NULL);
338 : :
339 : : /* Enumerate which users might have web filtering enabled on their account,
340 : : * and filter by @uid (zero means no filtering).
341 : : * Don’t bother to actually check whether each user has web filtering enabled;
342 : : * that will be done by the caller anyway. */
343 : 0 : g_assert (mct_user_manager_get_is_loaded (self->user_manager));
344 : :
345 : : /* Now the users have been loaded, list and filter them. */
346 : 0 : mct_user_manager_get_all_users_async (self->user_manager, cancellable,
347 : 0 : calculate_users_cb, g_steal_pointer (&task));
348 : 0 : }
349 : :
350 : : static void
351 : 0 : calculate_users_cb (GObject *object,
352 : : GAsyncResult *result,
353 : : void *user_data)
354 : : {
355 : 0 : MctUserManager *user_manager = MCT_USER_MANAGER (object);
356 [ # # ]: 0 : g_autoptr(GTask) task = G_TASK (g_steal_pointer (&user_data));
357 : 0 : uid_t filter_uid = GPOINTER_TO_UINT (g_task_get_task_data (task));
358 [ # # ]: 0 : g_autoptr(GArray) user_datas = NULL;
359 [ # # ]: 0 : g_autoptr(MctUserArray) users = NULL;
360 : 0 : size_t n_users = 0;
361 [ # # ]: 0 : g_autoptr(GError) local_error = NULL;
362 : :
363 : 0 : users = mct_user_manager_get_all_users_finish (user_manager, result, &n_users, &local_error);
364 [ # # ]: 0 : if (users == NULL)
365 : : {
366 : 0 : g_task_return_error (task, g_steal_pointer (&local_error));
367 : 0 : return;
368 : : }
369 : :
370 : 0 : user_datas = g_array_sized_new (FALSE, FALSE, sizeof (UserData), n_users);
371 : 0 : g_array_set_clear_func (user_datas, (GDestroyNotify) user_data_clear);
372 : :
373 [ # # ]: 0 : for (size_t i = 0; i < n_users; i++)
374 : : {
375 : 0 : MctUser *user = users[i];
376 : :
377 : : /* Always ignore root. */
378 [ # # ]: 0 : if (mct_user_get_uid (user) == 0)
379 : 0 : continue;
380 : :
381 [ # # # # ]: 0 : if (filter_uid == 0 || mct_user_get_uid (user) == filter_uid)
382 : : {
383 : 0 : const uid_t uid = mct_user_get_uid (user);
384 : 0 : const char *username = mct_user_get_username (user);
385 : :
386 : 0 : g_array_append_val (user_datas, ((const UserData) {
387 : : .uid = uid,
388 : : .username = g_strdup (username),
389 : : .web_filter = NULL, /* set later */
390 : : }));
391 : : }
392 : : }
393 : :
394 : 0 : g_task_return_pointer (task, g_steal_pointer (&user_datas), (GDestroyNotify) g_array_unref);
395 : : }
396 : :
397 : : static GArray *
398 : 0 : calculate_users_finish (MctFilterUpdater *self,
399 : : GAsyncResult *result,
400 : : GError **error)
401 : : {
402 : 0 : return g_task_propagate_pointer (G_TASK (result), error);
403 : : }
404 : :
405 : : static void
406 : 0 : update_filters_task_weak_notify_cb (void *user_data,
407 : : GObject *where_the_task_was)
408 : : {
409 : 0 : MctFilterUpdater *self = MCT_FILTER_UPDATER (user_data);
410 : :
411 : 0 : g_assert (self->update_in_progress);
412 : 0 : self->update_in_progress = FALSE;
413 : 0 : }
414 : :
415 : : /* We use on-disk cache files (rather than, for example, `O_TMPFILE` files) so
416 : : * that we can use HTTP caching headers to reduce re-downloads of the filter
417 : : * files if they haven’t changed since we last checked them. */
418 : : static GFile *
419 : 0 : cache_file_for_uri (MctFilterUpdater *self,
420 : : const char *uri)
421 : : {
422 : : /* Use the D-Bus escaping functions because they result in a string which is
423 : : * alphanumeric with underscores, i.e. no slashes. */
424 : 0 : g_autofree char *escaped_uri = g_dbus_escape_object_path (uri);
425 : 0 : return g_file_get_child (self->cache_directory, escaped_uri);
426 : : }
427 : :
428 : : static GFile *
429 : 0 : get_filter_lists_dir (MctFilterUpdater *self)
430 : : {
431 : 0 : return g_file_get_child (self->state_directory, "filter-lists");
432 : : }
433 : :
434 : : static GFile *
435 : 0 : tmp_file_for_user (MctFilterUpdater *self,
436 : : const UserData *user)
437 : : {
438 : 0 : g_autofree char *tmp_basename = NULL;
439 : 0 : g_autoptr(GFile) filter_lists_dir = get_filter_lists_dir (self);
440 : :
441 : 0 : g_assert (user->username != NULL);
442 : 0 : tmp_basename = g_strconcat (user->username, "~", NULL);
443 : :
444 : 0 : return g_file_get_child (filter_lists_dir, tmp_basename);
445 : : }
446 : :
447 : : static GFile *
448 : 0 : user_file_for_user (MctFilterUpdater *self,
449 : : const UserData *user)
450 : : {
451 : 0 : g_autoptr(GFile) filter_lists_dir = get_filter_lists_dir (self);
452 : 0 : g_assert (user->username != NULL);
453 : 0 : return g_file_get_child (filter_lists_dir, user->username);
454 : : }
455 : :
456 : : static gboolean
457 : 0 : add_hostname_to_cdbm (const char *hostname,
458 : : size_t hostname_len,
459 : : struct cdb_make *cdbm,
460 : : char prefix_char)
461 : : {
462 : 0 : char prefixed_hostname[HOSTNAME_MAX] = { '\0', };
463 : : size_t prefixed_hostname_len;
464 : :
465 [ # # # # ]: 0 : if (!mct_web_filter_validate_domain_name_len (hostname, hostname_len) ||
466 : : hostname_len >= sizeof (prefixed_hostname) - 1)
467 : 0 : return FALSE;
468 : :
469 : : /* We require all entries in filter lists to be valid domain names. Ideally
470 : : * we’d also require them all to be valid hostnames (since they’re only ever
471 : : * going to be requested through a web browser). However, some filter lists on
472 : : * the internet include non-hostname domain names, so we have to accept them. */
473 [ # # ]: 0 : if (!mct_web_filter_validate_hostname_len (hostname, hostname_len))
474 : 0 : g_debug ("Domain name ‘%.*s’ in filter list is not a valid hostname",
475 : : (int) hostname_len, hostname);
476 : :
477 [ # # ]: 0 : if (prefix_char != '\0')
478 : : {
479 : 0 : prefixed_hostname[0] = prefix_char;
480 : 0 : strncpy (prefixed_hostname + 1, hostname, hostname_len);
481 : 0 : prefixed_hostname_len = hostname_len + 1;
482 : : }
483 : : else
484 : : {
485 : 0 : strncpy (prefixed_hostname, hostname, hostname_len);
486 : 0 : prefixed_hostname_len = hostname_len;
487 : : }
488 : :
489 : : /* Negative return values indicate an error. Positive return values indicate
490 : : * the record already existed. Success is zero. */
491 [ # # ]: 0 : if (cdb_make_put (cdbm, prefixed_hostname, prefixed_hostname_len, NULL, 0, CDB_PUT_INSERT) < 0)
492 : 0 : return FALSE;
493 : :
494 : 0 : return TRUE;
495 : : }
496 : :
497 : : typedef struct
498 : : {
499 : : const char *filter_uri;
500 : : struct cdb_make *cdbm;
501 : : char prefix_char;
502 : : } AddFilterListsData;
503 : :
504 : : static gboolean
505 : 0 : parsed_hostname_cb (const char *hostname,
506 : : size_t hostname_len,
507 : : void *user_data,
508 : : GError **error)
509 : : {
510 : 0 : AddFilterListsData *data = user_data;
511 : :
512 [ # # ]: 0 : if (!add_hostname_to_cdbm (hostname, hostname_len, data->cdbm, data->prefix_char))
513 : : {
514 : 0 : g_set_error (error, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_INVALID_FILTER_FORMAT,
515 : : _("Invalid hostname ‘%.*s’ in filter list from ‘%s’"),
516 : : (int) hostname_len, hostname, data->filter_uri);
517 : 0 : return FALSE;
518 : : }
519 : :
520 : 0 : return TRUE;
521 : : }
522 : :
523 : : static gboolean
524 : 0 : add_filter_lists_to_cdbm (MctFilterUpdater *self,
525 : : GHashTable *filter_lists,
526 : : const char * const *custom_filter_list,
527 : : size_t custom_filter_list_len,
528 : : struct cdb_make *cdbm,
529 : : char prefix_char,
530 : : GError **error)
531 : : {
532 : : GHashTableIter iter;
533 : : void *value;
534 : 0 : g_autoptr(GError) local_error = NULL;
535 : :
536 : 0 : g_hash_table_iter_init (&iter, filter_lists);
537 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, NULL, &value))
538 : : {
539 [ # # ]: 0 : g_autoptr(GFile) cache_file = NULL;
540 [ # # ]: 0 : g_autoptr(GMappedFile) mapped_cache_file = NULL;
541 : 0 : const char *filter_uri = value;
542 : : const char *filter_contents;
543 : : size_t filter_contents_len;
544 : 0 : AddFilterListsData data = {
545 : : .filter_uri = filter_uri,
546 : : .cdbm = cdbm,
547 : : .prefix_char = prefix_char,
548 : : };
549 : :
550 : 0 : cache_file = cache_file_for_uri (self, filter_uri);
551 [ # # ]: 0 : g_debug ("Adding filter rules from cached file %s%s",
552 : : g_file_peek_path (cache_file),
553 : : (prefix_char != '\0') ? " (with prefix)" : "");
554 : :
555 : 0 : mapped_cache_file = g_mapped_file_new (g_file_peek_path (cache_file), FALSE, &local_error);
556 [ # # ]: 0 : if (mapped_cache_file == NULL)
557 : : {
558 : 0 : g_set_error (error, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
559 : : _("Error loading cache file ‘%s’: %s"), g_file_peek_path (cache_file),
560 : 0 : local_error->message);
561 : 0 : return FALSE;
562 : : }
563 : :
564 : 0 : filter_contents = g_mapped_file_get_contents (mapped_cache_file);
565 : 0 : filter_contents_len = g_mapped_file_get_length (mapped_cache_file);
566 : :
567 [ # # ]: 0 : if (!mct_filter_list_parse_from_data (filter_contents, filter_contents_len,
568 : : parsed_hostname_cb, &data, error))
569 : 0 : return FALSE;
570 : : }
571 : :
572 [ # # ]: 0 : g_debug ("Adding %zu custom filter rules%s",
573 : : custom_filter_list_len,
574 : : (prefix_char != '\0') ? " (with prefix)" : "");
575 : :
576 [ # # ]: 0 : for (size_t i = 0; i < custom_filter_list_len; i++)
577 : : {
578 [ # # ]: 0 : if (!add_hostname_to_cdbm (custom_filter_list[i], strlen (custom_filter_list[i]), cdbm, prefix_char))
579 : : {
580 : 0 : g_set_error (error, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_INVALID_FILTER_FORMAT,
581 : : _("Invalid hostname ‘%s’ in custom filter list"),
582 : 0 : custom_filter_list[i]);
583 : 0 : return FALSE;
584 : : }
585 : : }
586 : :
587 : 0 : return TRUE;
588 : : }
589 : :
590 : : static void *
591 : 0 : identity_copy (const void *src,
592 : : void *user_data)
593 : : {
594 : 0 : return (void *) src;
595 : : }
596 : :
597 : : static void *
598 : 0 : str_copy (const void *str,
599 : : void *user_data)
600 : : {
601 : 0 : return g_strdup (str);
602 : : }
603 : :
604 : : static void
605 : 0 : ptr_array_extend_with_hash_table_values (GPtrArray *array,
606 : : GHashTable *hash_table,
607 : : GCopyFunc value_copy_func,
608 : : void *user_data)
609 : : {
610 : : GHashTableIter iter;
611 : : void *value;
612 : :
613 [ # # ]: 0 : if (value_copy_func == NULL)
614 : 0 : value_copy_func = identity_copy;
615 : :
616 : 0 : g_hash_table_iter_init (&iter, hash_table);
617 : :
618 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, NULL, &value))
619 : 0 : g_ptr_array_add (array, value_copy_func (value, user_data));
620 : 0 : }
621 : :
622 : : static void
623 : 0 : ptr_array_uniqueify (GPtrArray *array,
624 : : GCompareFunc compare_func)
625 : : {
626 : 0 : g_ptr_array_sort_values (array, compare_func);
627 : :
628 [ # # ]: 0 : for (size_t i = 1; i < array->len; i++)
629 : : {
630 : 0 : const void *a = g_ptr_array_index (array, i - 1);
631 : 0 : const void *b = g_ptr_array_index (array, i);
632 : :
633 [ # # ]: 0 : if (compare_func (a, b) == 0)
634 : : {
635 : 0 : g_ptr_array_remove_index (array, i);
636 : 0 : i--;
637 : : }
638 : : }
639 : 0 : }
640 : :
641 : : /* See https://httpwg.org/specs/rfc7231.html#http.date
642 : : * For example: Sun, 06 Nov 1994 08:49:37 GMT */
643 : : static gchar *
644 : 0 : date_time_to_rfc7231 (GDateTime *date_time)
645 : : {
646 : 0 : return soup_date_time_to_string (date_time, SOUP_DATE_HTTP);
647 : : }
648 : :
649 : : #define METADATA_ETAG_ATTRIBUTE "xattr::malcontent::etag"
650 : :
651 : : /* Get an ETag from @file, from the GIO metadata, so it can be used to allow
652 : : * HTTP caching when querying to update the file in future. Also return the
653 : : * file’s last modified date (even if an ETag isn’t set) so that can be used as
654 : : * a fallback for caching. */
655 : : static gchar *
656 : 0 : get_file_etag (GFile *file,
657 : : GDateTime **last_modified_date_out,
658 : : GCancellable *cancellable)
659 : : {
660 : 0 : g_autoptr(GFileInfo) info = NULL;
661 : : const char *attributes;
662 : 0 : g_autoptr(GError) local_error = NULL;
663 : :
664 : 0 : g_return_val_if_fail (G_IS_FILE (file), NULL);
665 : 0 : g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
666 : :
667 [ # # ]: 0 : if (last_modified_date_out == NULL)
668 : 0 : attributes = METADATA_ETAG_ATTRIBUTE;
669 : : else
670 : 0 : attributes = METADATA_ETAG_ATTRIBUTE "," G_FILE_ATTRIBUTE_TIME_MODIFIED;
671 : :
672 : 0 : info = g_file_query_info (file, attributes, G_FILE_QUERY_INFO_NONE, cancellable, &local_error);
673 : :
674 [ # # ]: 0 : if (info == NULL)
675 : : {
676 : 0 : g_debug ("Error getting attribute ‘%s’ for file ‘%s’: %s",
677 : : METADATA_ETAG_ATTRIBUTE, g_file_peek_path (file), local_error->message);
678 : :
679 [ # # ]: 0 : if (last_modified_date_out != NULL)
680 : 0 : *last_modified_date_out = NULL;
681 : :
682 : 0 : return NULL;
683 : : }
684 : :
685 [ # # ]: 0 : if (last_modified_date_out != NULL)
686 : 0 : *last_modified_date_out = g_file_info_get_modification_date_time (info);
687 : :
688 : 0 : return g_strdup (g_file_info_get_attribute_string (info, METADATA_ETAG_ATTRIBUTE));
689 : : }
690 : :
691 : : /* Store an ETag on @file, in the GIO metadata, so it can be used to allow HTTP
692 : : * caching when querying to update the file in future. */
693 : : static gboolean
694 : 0 : set_file_etag (GFile *file,
695 : : const char *etag,
696 : : GCancellable *cancellable)
697 : : {
698 : 0 : g_autoptr(GError) local_error = NULL;
699 : :
700 : 0 : g_return_val_if_fail (G_IS_FILE (file), FALSE);
701 : 0 : g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE);
702 : :
703 [ # # # # ]: 0 : if (etag == NULL || *etag == '\0')
704 : : {
705 [ # # ]: 0 : if (!g_file_set_attribute (file, METADATA_ETAG_ATTRIBUTE, G_FILE_ATTRIBUTE_TYPE_INVALID,
706 : : NULL, G_FILE_QUERY_INFO_NONE, cancellable, &local_error))
707 : : {
708 : 0 : g_debug ("Error clearing attribute ‘%s’ on file ‘%s’: %s",
709 : : METADATA_ETAG_ATTRIBUTE, g_file_peek_path (file), local_error->message);
710 : 0 : return FALSE;
711 : : }
712 : :
713 : 0 : return TRUE;
714 : : }
715 : :
716 [ # # ]: 0 : if (!g_file_set_attribute_string (file, METADATA_ETAG_ATTRIBUTE, etag, G_FILE_QUERY_INFO_NONE, cancellable, &local_error))
717 : : {
718 : 0 : g_debug ("Error setting attribute ‘%s’ to ‘%s’ on file ‘%s’: %s",
719 : : METADATA_ETAG_ATTRIBUTE, etag, g_file_peek_path (file), local_error->message);
720 : 0 : return FALSE;
721 : : }
722 : :
723 : 0 : return TRUE;
724 : : }
725 : :
726 : : static void update_filters_calculate_users_cb (GObject *object,
727 : : GAsyncResult *result,
728 : : void *user_data);
729 : :
730 : : /**
731 : : * mct_filter_updater_update_filters_async:
732 : : * @self: a filter updater
733 : : * @filter_uid: UID to update the filters for, or zero to update for all users
734 : : * @cancellable: a cancellable, or `NULL`
735 : : * @callback: callback for the asynchronous function
736 : : * @user_data: data to pass to @callback
737 : : *
738 : : * Asynchronously updates the compiled web filters for one or more users.
739 : : *
740 : : * If @filter_uid is non-zero, the filters for that user will be updated. If
741 : : * @filter_uid is zero, the filters for all users will be updated.
742 : : *
743 : : * See the documentation for [class@Malcontent.FilterUpdater] for details on how
744 : : * filters are updated.
745 : : *
746 : : * If an update is already in progress,
747 : : * [error@Malcontent.FilterUpdaterError.BUSY] will be raised. All other errors
748 : : * from [error@Malcontent.FilterUpdaterError] are also possible.
749 : : */
750 : : void
751 : 0 : mct_filter_updater_update_filters_async (MctFilterUpdater *self,
752 : : uid_t filter_uid,
753 : : GCancellable *cancellable,
754 : : GAsyncReadyCallback callback,
755 : : void *user_data)
756 : : {
757 [ # # ]: 0 : g_autoptr(GTask) task = NULL;
758 [ # # ]: 0 : g_autoptr(UpdateFiltersData) data_owned = NULL;
759 : : UpdateFiltersData *data;
760 : :
761 : 0 : g_return_if_fail (MCT_IS_FILTER_UPDATER (self));
762 : 0 : g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
763 : :
764 : 0 : task = g_task_new (self, cancellable, callback, user_data);
765 [ # # ]: 0 : g_task_set_source_tag (task, mct_filter_updater_update_filters_async);
766 : :
767 : 0 : data = data_owned = g_new0 (UpdateFiltersData, 1);
768 : 0 : data->filter_uid = filter_uid;
769 : 0 : data->filter_lists_dir_fd = -1;
770 : 0 : g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) update_filters_data_free);
771 : :
772 [ # # ]: 0 : if (self->update_in_progress)
773 : : {
774 : 0 : g_task_return_new_error_literal (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_BUSY,
775 : 0 : _("Web filter update already in progress"));
776 : 0 : return;
777 : : }
778 : :
779 : : /* Prevent duplicate updates racing within this class, and also take a file
780 : : * lock just in case another process is running. */
781 : 0 : self->update_in_progress = TRUE;
782 : 0 : g_object_weak_ref (G_OBJECT (task), update_filters_task_weak_notify_cb, self);
783 : :
784 : : /* Open the user filter directory. */
785 : 0 : data->filter_lists_dir = get_filter_lists_dir (self);
786 : :
787 [ # # ]: 0 : while ((data->filter_lists_dir_fd = open (g_file_peek_path (data->filter_lists_dir), O_DIRECTORY | O_CLOEXEC)) < 0)
788 : : {
789 : 0 : int errsv = errno;
790 : :
791 [ # # ]: 0 : if (errsv == EINTR)
792 : : {
793 : 0 : continue;
794 : : }
795 [ # # ]: 0 : else if (errsv == ENOENT)
796 : : {
797 [ # # ]: 0 : if (mkdir (g_file_peek_path (data->filter_lists_dir), 0755) == 0)
798 : : {
799 : 0 : continue;
800 : : }
801 : : else
802 : : {
803 : 0 : int mkdir_errsv = errno;
804 : 0 : g_task_return_new_error (task,
805 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
806 : 0 : _("Error creating user filter directory: %s"),
807 : : g_strerror (mkdir_errsv));
808 : 0 : return;
809 : : }
810 : : }
811 : : else
812 : : {
813 : 0 : g_task_return_new_error (task,
814 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
815 : 0 : _("Error opening user filter directory: %s"),
816 : : g_strerror (errsv));
817 : 0 : return;
818 : : }
819 : : }
820 : :
821 : : /* And take a lock on it. This will be implicitly dropped when we close the FD. */
822 [ # # ]: 0 : while (flock (data->filter_lists_dir_fd, LOCK_EX | LOCK_NB) < 0)
823 : : {
824 : 0 : int errsv = errno;
825 : :
826 [ # # ]: 0 : if (errsv == EINTR)
827 : : {
828 : 0 : continue;
829 : : }
830 [ # # ]: 0 : else if (errsv == EWOULDBLOCK)
831 : : {
832 : 0 : g_task_return_new_error_literal (task,
833 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_BUSY,
834 : 0 : _("Web filter update already in progress"));
835 : 0 : return;
836 : : }
837 : : else
838 : : {
839 : 0 : g_task_return_new_error (task,
840 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
841 : 0 : _("Error opening user filter directory: %s"),
842 : : g_strerror (errsv));
843 : 0 : return;
844 : : }
845 : : }
846 : :
847 : 0 : calculate_users_async (self, filter_uid, cancellable,
848 : 0 : update_filters_calculate_users_cb, g_steal_pointer (&task));
849 : : }
850 : :
851 : : static void
852 : 0 : update_filters_calculate_users_cb (GObject *object,
853 : : GAsyncResult *result,
854 : : void *user_data)
855 : : {
856 : 0 : MctFilterUpdater *self = MCT_FILTER_UPDATER (object);
857 [ # # ]: 0 : g_autoptr(GTask) task = G_TASK (g_steal_pointer (&user_data));
858 : 0 : GCancellable *cancellable = g_task_get_cancellable (task);
859 : 0 : UpdateFiltersData *data = g_task_get_task_data (task);
860 [ # # ]: 0 : g_autoptr(SoupSession) session = NULL;
861 [ # # ]: 0 : g_autoptr(GError) local_error = NULL;
862 : :
863 : 0 : data->user_datas = calculate_users_finish (self, result, &local_error);
864 [ # # ]: 0 : if (data->user_datas == NULL)
865 : : {
866 : 0 : g_task_return_new_error (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_QUERYING_POLICY,
867 : 0 : _("Error loading user’s web filtering policy: %s"), local_error->message);
868 : 0 : return;
869 : : }
870 [ # # # # ]: 0 : else if (data->user_datas->len == 0 && data->filter_uid != 0)
871 : : {
872 : 0 : g_task_return_new_error (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_DISABLED,
873 : 0 : _("Web filtering is disabled for user %u"), data->filter_uid);
874 : 0 : return;
875 : : }
876 : :
877 : : /* Load the configurations for all the users from AccountsService and build a
878 : : * list of all the filter files to download. */
879 : 0 : data->filter_uris = g_ptr_array_new_with_free_func (g_free);
880 : :
881 [ # # ]: 0 : for (size_t i = 0; i < data->user_datas->len; i++)
882 : : {
883 : 0 : UserData *user = &g_array_index (data->user_datas, UserData, i);
884 [ # # ]: 0 : g_autoptr(MctWebFilter) web_filter_owned = NULL;
885 : : MctWebFilter *web_filter;
886 : : GHashTable *block_lists, *allow_lists;
887 : :
888 : 0 : g_debug ("Loading web filters for user %u", user->uid);
889 : :
890 : : /* FIXME Make this all async */
891 : 0 : web_filter = web_filter_owned = mct_manager_get_web_filter (self->policy_manager, user->uid,
892 : : MCT_MANAGER_GET_VALUE_FLAGS_NONE,
893 : : cancellable, &local_error);
894 [ # # # # ]: 0 : if (local_error != NULL &&
895 : 0 : g_error_matches (local_error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_DISABLED))
896 : : {
897 : 0 : g_task_return_new_error (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_DISABLED,
898 : 0 : _("Web filtering is disabled for user %u"), user->uid);
899 : 0 : return;
900 : : }
901 [ # # ]: 0 : else if (local_error != NULL)
902 : : {
903 : : /* In particular, we could hit this path for:
904 : : * - MCT_MANAGER_ERROR_INVALID_DATA
905 : : * - MCT_MANAGER_ERROR_INVALID_USER
906 : : * - MCT_MANAGER_ERROR_PERMISSION_DENIED
907 : : * as well as misc `GDBusError` errors. */
908 : 0 : g_task_return_new_error (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_QUERYING_POLICY,
909 : 0 : _("Error loading user’s web filtering policy: %s"),
910 : 0 : local_error->message);
911 : 0 : return;
912 : : }
913 : :
914 : 0 : user->web_filter = g_steal_pointer (&web_filter_owned);
915 : :
916 : : /* List the filter files to download. */
917 : 0 : block_lists = mct_web_filter_get_block_lists (web_filter);
918 : 0 : ptr_array_extend_with_hash_table_values (data->filter_uris, block_lists, str_copy, NULL);
919 : :
920 : 0 : allow_lists = mct_web_filter_get_allow_lists (web_filter);
921 : 0 : ptr_array_extend_with_hash_table_values (data->filter_uris, allow_lists, str_copy, NULL);
922 : : }
923 : :
924 : : /* Delete expired old filters from cache, if we’re updating the compiled
925 : : * filters for all users. If only updating one user, don’t delete old filters
926 : : * in case they’re cached for use by other users. */
927 [ # # ]: 0 : if (data->filter_uid == 0)
928 : : {
929 [ # # ]: 0 : g_autoptr(GFileEnumerator) enumerator = NULL;
930 [ # # ]: 0 : g_autoptr(GHashTable) wanted_cache_files = NULL;
931 : :
932 : 0 : wanted_cache_files = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, g_object_unref, NULL);
933 [ # # ]: 0 : for (size_t i = 0; i < data->filter_uris->len; i++)
934 : 0 : g_hash_table_add (wanted_cache_files, cache_file_for_uri (self, data->filter_uris->pdata[i]));
935 : :
936 : 0 : enumerator = g_file_enumerate_children (self->cache_directory,
937 : : G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
938 : : G_FILE_QUERY_INFO_NONE,
939 : : cancellable,
940 : : &local_error);
941 [ # # # # ]: 0 : if (enumerator == NULL &&
942 : 0 : !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
943 : : {
944 : 0 : g_task_return_new_error (task,
945 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
946 : 0 : _("Error clearing old items from download cache: %s"),
947 : 0 : local_error->message);
948 : 0 : return;
949 : : }
950 : :
951 : 0 : g_clear_error (&local_error);
952 : :
953 : : while (TRUE)
954 : 0 : {
955 : : GFileInfo *info;
956 : : GFile *child;
957 : :
958 [ # # ]: 0 : if (!g_file_enumerator_iterate (enumerator, &info, &child, cancellable, &local_error))
959 : : {
960 : 0 : g_task_return_new_error (task,
961 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
962 : 0 : _("Error clearing old items from download cache: %s"),
963 : 0 : local_error->message);
964 : 0 : return;
965 : : }
966 : :
967 [ # # ]: 0 : if (info == NULL)
968 : 0 : break;
969 : :
970 : : /* Ignore non-files. */
971 [ # # ]: 0 : if (g_file_info_get_file_type (info) != G_FILE_TYPE_REGULAR)
972 : 0 : continue;
973 : :
974 : : /* Delete the cached file if it’s not listed in data->filter_uris.
975 : : * Ignore failure because it’s only a cache file. */
976 [ # # ]: 0 : if (!g_hash_table_contains (wanted_cache_files, child))
977 : : {
978 : 0 : g_debug ("Deleting old cache file for %s which is no longer in merged filter list",
979 : : g_file_peek_path (child));
980 : 0 : g_file_delete (child, cancellable, NULL);
981 : : }
982 : : }
983 : : }
984 : :
985 : : /* Download the files. */
986 : 0 : ptr_array_uniqueify (data->filter_uris, (GCompareFunc) g_strcmp0);
987 : :
988 : 0 : session = soup_session_new ();
989 : :
990 [ # # ]: 0 : for (size_t i = 0; i < data->filter_uris->len; i++)
991 : : {
992 : 0 : const char *uri = data->filter_uris->pdata[i];
993 [ # # # ]: 0 : g_autoptr(SoupMessage) message = NULL;
994 [ # # # ]: 0 : g_autoptr(GInputStream) input_stream = NULL;
995 : : unsigned int status_code;
996 [ # # # ]: 0 : g_autoptr(GFileOutputStream) output_stream = NULL;
997 [ # # # ]: 0 : g_autoptr(GFile) cache_file = NULL;
998 [ # # # ]: 0 : g_autofree char *last_etag = NULL;
999 [ # # # ]: 0 : g_autoptr(GDateTime) last_modified_date = NULL;
1000 : : const char *new_etag;
1001 [ # # # ]: 0 : g_autofree char *last_modified_date_str = NULL;
1002 : :
1003 : : /* Require HTTPS, since we’re not in the 90s any more.
1004 : : * This is stricter than implementing HSTS. */
1005 [ # # ]: 0 : if (g_strcmp0 (g_uri_peek_scheme (uri), "https") != 0)
1006 : : {
1007 : 0 : g_task_return_new_error (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_INVALID_FILTER_FORMAT,
1008 : 0 : _("Invalid filter list ‘%s’: %s"),
1009 : 0 : uri, _("Filter lists must be provided via HTTPS"));
1010 : 0 : return;
1011 : : }
1012 : :
1013 : : /* Create a HTTP message. */
1014 [ # # ]: 0 : message = soup_message_new (SOUP_METHOD_GET, uri);
1015 [ # # ]: 0 : if (message == NULL)
1016 : : {
1017 : 0 : g_task_return_new_error (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_INVALID_FILTER_FORMAT,
1018 : 0 : _("Invalid filter list ‘%s’: %s"),
1019 : 0 : uri, _("Invalid URI"));
1020 : 0 : return;
1021 : : }
1022 : :
1023 : : /* Caching support. Prefer ETags to modification dates, as the latter
1024 : : * have problems with rapid updates and clock drift. */
1025 : 0 : cache_file = cache_file_for_uri (self, uri);
1026 : :
1027 : 0 : last_etag = get_file_etag (cache_file, &last_modified_date, cancellable);
1028 [ # # # # ]: 0 : if (last_etag != NULL && *last_etag == '\0')
1029 : 0 : last_etag = NULL;
1030 : :
1031 [ # # ]: 0 : last_modified_date_str = (last_modified_date != NULL) ? date_time_to_rfc7231 (last_modified_date) : NULL;
1032 : :
1033 [ # # ]: 0 : if (last_etag != NULL)
1034 : 0 : soup_message_headers_append (soup_message_get_request_headers (message), "If-None-Match", last_etag);
1035 [ # # ]: 0 : else if (last_modified_date != NULL)
1036 : 0 : soup_message_headers_append (soup_message_get_request_headers (message), "If-Modified-Since", last_modified_date_str);
1037 : :
1038 : 0 : g_debug ("Downloading filter list from %s (If-None-Match: %s, If-Modified-Since: %s) into cache file %s",
1039 : : uri, last_etag, last_modified_date_str, g_file_peek_path (cache_file));
1040 : :
1041 : 0 : input_stream = soup_session_send (session, message, cancellable, &local_error);
1042 : :
1043 : 0 : status_code = soup_message_get_status (message);
1044 [ # # ]: 0 : if (input_stream == NULL)
1045 : : {
1046 : 0 : g_task_return_new_error (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_DOWNLOADING,
1047 : 0 : _("Failed to download URI ‘%s’: %s"),
1048 : 0 : uri, local_error->message);
1049 : 0 : return;
1050 : : }
1051 [ # # ]: 0 : else if (status_code == SOUP_STATUS_NOT_MODIFIED)
1052 : : {
1053 : : /* If the file has not been modified from the ETag or
1054 : : * Last-Modified date we have, finish the download
1055 : : * early.
1056 : : *
1057 : : * Preserve the existing ETag. */
1058 : 0 : g_debug ("Skipped downloading ‘%s’: %s",
1059 : : uri, soup_status_get_phrase (status_code));
1060 : 0 : continue;
1061 : : }
1062 [ # # ]: 0 : else if (status_code != SOUP_STATUS_OK)
1063 : : {
1064 : 0 : g_task_return_new_error (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_DOWNLOADING,
1065 : 0 : _("Failed to download URI ‘%s’: %s"),
1066 : : uri, soup_status_get_phrase (status_code));
1067 : 0 : return;
1068 : : }
1069 : :
1070 : 0 : output_stream = g_file_replace (cache_file,
1071 : : NULL /* ETag (different from the HTTP one!) */,
1072 : : FALSE /* make_backup */,
1073 : : G_FILE_CREATE_PRIVATE,
1074 : : cancellable,
1075 : : &local_error);
1076 [ # # ]: 0 : if (output_stream == NULL)
1077 : : {
1078 : 0 : g_task_return_new_error (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
1079 : 0 : _("Failed to download URI ‘%s’: %s"),
1080 : 0 : uri, local_error->message);
1081 : 0 : return;
1082 : : }
1083 : :
1084 [ # # ]: 0 : if (g_output_stream_splice (G_OUTPUT_STREAM (output_stream), input_stream,
1085 : : G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE |
1086 : : G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
1087 : : cancellable, &local_error) < 0)
1088 : : {
1089 : 0 : g_task_return_new_error (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
1090 : 0 : _("Failed to download URI ‘%s’: %s"),
1091 : 0 : uri, local_error->message);
1092 : 0 : return;
1093 : : }
1094 : :
1095 : : /* Store the new ETag for later use. */
1096 : 0 : new_etag = soup_message_headers_get_one (soup_message_get_response_headers (message), "ETag");
1097 [ # # # # ]: 0 : if (new_etag != NULL && *new_etag == '\0')
1098 : 0 : new_etag = NULL;
1099 : :
1100 : 0 : set_file_etag (cache_file, new_etag, cancellable);
1101 : : }
1102 : :
1103 : : /* For each user, compile their filters into a single big filter. This also
1104 : : * needs to pay attention to their other `MctWebFilter` options, such as
1105 : : * custom filters and whether to block by default. */
1106 [ # # ]: 0 : for (size_t i = 0; i < data->user_datas->len; i++)
1107 : : {
1108 : 0 : const UserData *user = &g_array_index (data->user_datas, UserData, i);
1109 : 0 : MctWebFilter *web_filter = user->web_filter;
1110 [ # # # ]: 0 : g_autoptr(GFile) tmpfile = NULL;
1111 [ # # # ]: 0 : g_autofree char *tmpfile_basename = NULL;
1112 [ # # # ]: 0 : g_autoptr(GFile) user_file = NULL;
1113 [ # # # ]: 0 : g_autofree char *user_file_basename = NULL;
1114 : : struct cdb_make cdbm;
1115 [ # # # ]: 0 : g_autofd int tmpfile_fd = -1;
1116 : : GHashTable *allow_lists;
1117 : : const char * const *custom_allow_list;
1118 : : size_t custom_allow_list_len;
1119 : :
1120 : 0 : user_file = user_file_for_user (self, user);
1121 : :
1122 [ # # ]: 0 : if (mct_web_filter_get_filter_type (web_filter) == MCT_WEB_FILTER_TYPE_NONE)
1123 : : {
1124 : : /* Delete the old compiled filter, if it exists. */
1125 : 0 : g_debug ("Deleting compiled filter file %s for user %u as they have web filtering disabled",
1126 : : g_file_peek_path (user_file), user->uid);
1127 : :
1128 : 0 : g_file_delete (user_file, cancellable, &local_error);
1129 [ # # # # ]: 0 : if (local_error != NULL &&
1130 : 0 : !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
1131 : : {
1132 : 0 : g_task_return_new_error (task, MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
1133 : 0 : _("Error deleting old user web filter ‘%s’: %s"),
1134 : 0 : g_file_peek_path (user_file), local_error->message);
1135 : 0 : return;
1136 : : }
1137 : :
1138 : 0 : g_clear_error (&local_error);
1139 : :
1140 : 0 : continue;
1141 : : }
1142 : :
1143 : : /* Open a temporary file to contain the compiled filter for this user. */
1144 : : /* FIXME: Would be better for this to use openat() on filter_lists_dir_fd,
1145 : : * but I can’t get that to work */
1146 [ # # ]: 0 : while ((tmpfile_fd = open (g_file_peek_path (data->filter_lists_dir), O_RDWR | O_TMPFILE | O_CLOEXEC, 0600)) < 0)
1147 : : {
1148 : 0 : int errsv = errno;
1149 : :
1150 [ # # ]: 0 : if (errsv == EINTR)
1151 : 0 : continue;
1152 : :
1153 : 0 : g_task_return_new_error (task,
1154 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
1155 : 0 : _("Error opening user web filter temporary file: %s"),
1156 : : g_strerror (errsv));
1157 : 0 : return;
1158 : : }
1159 : :
1160 : 0 : g_debug ("Building tinycdb database for user %u in temporary file", user->uid);
1161 : :
1162 : 0 : cdb_make_start (&cdbm, tmpfile_fd);
1163 : :
1164 : : /* If blocking everything by default, add a special key to indicate this,
1165 : : * and don’t bother adding all the filter list files. */
1166 [ # # ]: 0 : if (mct_web_filter_get_filter_type (web_filter) == MCT_WEB_FILTER_TYPE_ALLOWLIST)
1167 : : {
1168 : 0 : g_debug ("Adding wildcard filter rule");
1169 [ # # ]: 0 : if (cdb_make_put (&cdbm, "*", strlen ("*"), NULL, 0, CDB_PUT_REPLACE) != 0)
1170 : : {
1171 : 0 : int errsv = errno;
1172 : 0 : g_task_return_new_error (task,
1173 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
1174 : 0 : _("Error adding to user filter: %s"), g_strerror (errsv));
1175 : 0 : return;
1176 : : }
1177 : : }
1178 : : else
1179 : : {
1180 : : GHashTable *block_lists;
1181 : : const char * const *custom_block_list;
1182 : : size_t custom_block_list_len;
1183 : :
1184 : 0 : block_lists = mct_web_filter_get_block_lists (web_filter);
1185 : 0 : custom_block_list = mct_web_filter_get_custom_block_list (web_filter, &custom_block_list_len);
1186 : :
1187 [ # # ]: 0 : if (!add_filter_lists_to_cdbm (self, block_lists,
1188 : : custom_block_list, custom_block_list_len,
1189 : : &cdbm, 0, &local_error))
1190 : : {
1191 : 0 : g_assert (local_error->domain == MCT_FILTER_UPDATER_ERROR);
1192 : 0 : g_task_return_error (task, g_steal_pointer (&local_error));
1193 : 0 : return;
1194 : : }
1195 : : }
1196 : :
1197 : : /* Add the allow lists. */
1198 : 0 : allow_lists = mct_web_filter_get_allow_lists (web_filter);
1199 : 0 : custom_allow_list = mct_web_filter_get_custom_allow_list (web_filter, &custom_allow_list_len);
1200 : :
1201 [ # # ]: 0 : if (!add_filter_lists_to_cdbm (self, allow_lists,
1202 : : custom_allow_list, custom_allow_list_len,
1203 : : &cdbm, '~', &local_error))
1204 : : {
1205 : 0 : g_assert (local_error->domain == MCT_FILTER_UPDATER_ERROR);
1206 : 0 : g_task_return_error (task, g_steal_pointer (&local_error));
1207 : 0 : return;
1208 : : }
1209 : :
1210 : : /* Finally, add the redirections to force safe search to be enabled. */
1211 [ # # ]: 0 : if (mct_web_filter_get_force_safe_search (web_filter))
1212 : : {
1213 : : const struct
1214 : : {
1215 : : const char *hostname;
1216 : : const char *replacement;
1217 : : }
1218 : 0 : safe_search_substitutions[] =
1219 : : {
1220 : : { "duckduckgo.com", "safe.duckduckgo.com" },
1221 : : { "www.duckduckgo.com", "safe.duckduckgo.com" },
1222 : : { "start.duckduckgo.com", "safe.duckduckgo.com" },
1223 : :
1224 : : { "www.bing.com", "strict.bing.com" },
1225 : :
1226 : : { "www.google.ad", "forcesafesearch.google.com" },
1227 : : { "www.google.ae", "forcesafesearch.google.com" },
1228 : : { "www.google.al", "forcesafesearch.google.com" },
1229 : : { "www.google.am", "forcesafesearch.google.com" },
1230 : : { "www.google.as", "forcesafesearch.google.com" },
1231 : : { "www.google.at", "forcesafesearch.google.com" },
1232 : : { "www.google.az", "forcesafesearch.google.com" },
1233 : : { "www.google.ba", "forcesafesearch.google.com" },
1234 : : { "www.google.be", "forcesafesearch.google.com" },
1235 : : { "www.google.bf", "forcesafesearch.google.com" },
1236 : : { "www.google.bg", "forcesafesearch.google.com" },
1237 : : { "www.google.bi", "forcesafesearch.google.com" },
1238 : : { "www.google.bj", "forcesafesearch.google.com" },
1239 : : { "www.google.bs", "forcesafesearch.google.com" },
1240 : : { "www.google.bt", "forcesafesearch.google.com" },
1241 : : { "www.google.by", "forcesafesearch.google.com" },
1242 : : { "www.google.ca", "forcesafesearch.google.com" },
1243 : : { "www.google.cd", "forcesafesearch.google.com" },
1244 : : { "www.google.cf", "forcesafesearch.google.com" },
1245 : : { "www.google.cg", "forcesafesearch.google.com" },
1246 : : { "www.google.ch", "forcesafesearch.google.com" },
1247 : : { "www.google.ci", "forcesafesearch.google.com" },
1248 : : { "www.google.cl", "forcesafesearch.google.com" },
1249 : : { "www.google.cm", "forcesafesearch.google.com" },
1250 : : { "www.google.cn", "forcesafesearch.google.com" },
1251 : : { "www.google.co.ao", "forcesafesearch.google.com" },
1252 : : { "www.google.co.bw", "forcesafesearch.google.com" },
1253 : : { "www.google.co.ck", "forcesafesearch.google.com" },
1254 : : { "www.google.co.cr", "forcesafesearch.google.com" },
1255 : : { "www.google.co.id", "forcesafesearch.google.com" },
1256 : : { "www.google.co.il", "forcesafesearch.google.com" },
1257 : : { "www.google.co.in", "forcesafesearch.google.com" },
1258 : : { "www.google.co.jp", "forcesafesearch.google.com" },
1259 : : { "www.google.co.ke", "forcesafesearch.google.com" },
1260 : : { "www.google.co.kr", "forcesafesearch.google.com" },
1261 : : { "www.google.co.ls", "forcesafesearch.google.com" },
1262 : : { "www.google.co.ma", "forcesafesearch.google.com" },
1263 : : { "www.google.co.mz", "forcesafesearch.google.com" },
1264 : : { "www.google.co.nz", "forcesafesearch.google.com" },
1265 : : { "www.google.co.th", "forcesafesearch.google.com" },
1266 : : { "www.google.co.tz", "forcesafesearch.google.com" },
1267 : : { "www.google.co.ug", "forcesafesearch.google.com" },
1268 : : { "www.google.co.uk", "forcesafesearch.google.com" },
1269 : : { "www.google.co.uz", "forcesafesearch.google.com" },
1270 : : { "www.google.co.ve", "forcesafesearch.google.com" },
1271 : : { "www.google.co.vi", "forcesafesearch.google.com" },
1272 : : { "www.google.com", "forcesafesearch.google.com" },
1273 : : { "www.google.com.af", "forcesafesearch.google.com" },
1274 : : { "www.google.com.ag", "forcesafesearch.google.com" },
1275 : : { "www.google.com.ai", "forcesafesearch.google.com" },
1276 : : { "www.google.com.ar", "forcesafesearch.google.com" },
1277 : : { "www.google.com.au", "forcesafesearch.google.com" },
1278 : : { "www.google.com.bd", "forcesafesearch.google.com" },
1279 : : { "www.google.com.bh", "forcesafesearch.google.com" },
1280 : : { "www.google.com.bn", "forcesafesearch.google.com" },
1281 : : { "www.google.com.bo", "forcesafesearch.google.com" },
1282 : : { "www.google.com.br", "forcesafesearch.google.com" },
1283 : : { "www.google.com.bz", "forcesafesearch.google.com" },
1284 : : { "www.google.com.co", "forcesafesearch.google.com" },
1285 : : { "www.google.com.cu", "forcesafesearch.google.com" },
1286 : : { "www.google.com.cy", "forcesafesearch.google.com" },
1287 : : { "www.google.com.do", "forcesafesearch.google.com" },
1288 : : { "www.google.com.ec", "forcesafesearch.google.com" },
1289 : : { "www.google.com.eg", "forcesafesearch.google.com" },
1290 : : { "www.google.com.et", "forcesafesearch.google.com" },
1291 : : { "www.google.com.fj", "forcesafesearch.google.com" },
1292 : : { "www.google.com.gh", "forcesafesearch.google.com" },
1293 : : { "www.google.com.gi", "forcesafesearch.google.com" },
1294 : : { "www.google.com.gt", "forcesafesearch.google.com" },
1295 : : { "www.google.com.hk", "forcesafesearch.google.com" },
1296 : : { "www.google.com.jm", "forcesafesearch.google.com" },
1297 : : { "www.google.com.kh", "forcesafesearch.google.com" },
1298 : : { "www.google.com.kw", "forcesafesearch.google.com" },
1299 : : { "www.google.com.lb", "forcesafesearch.google.com" },
1300 : : { "www.google.com.ly", "forcesafesearch.google.com" },
1301 : : { "www.google.com.mm", "forcesafesearch.google.com" },
1302 : : { "www.google.com.mt", "forcesafesearch.google.com" },
1303 : : { "www.google.com.mx", "forcesafesearch.google.com" },
1304 : : { "www.google.com.my", "forcesafesearch.google.com" },
1305 : : { "www.google.com.na", "forcesafesearch.google.com" },
1306 : : { "www.google.com.nf", "forcesafesearch.google.com" },
1307 : : { "www.google.com.ng", "forcesafesearch.google.com" },
1308 : : { "www.google.com.ni", "forcesafesearch.google.com" },
1309 : : { "www.google.com.np", "forcesafesearch.google.com" },
1310 : : { "www.google.com.om", "forcesafesearch.google.com" },
1311 : : { "www.google.com.pa", "forcesafesearch.google.com" },
1312 : : { "www.google.com.pe", "forcesafesearch.google.com" },
1313 : : { "www.google.com.pg", "forcesafesearch.google.com" },
1314 : : { "www.google.com.ph", "forcesafesearch.google.com" },
1315 : : { "www.google.com.pk", "forcesafesearch.google.com" },
1316 : : { "www.google.com.pr", "forcesafesearch.google.com" },
1317 : : { "www.google.com.py", "forcesafesearch.google.com" },
1318 : : { "www.google.com.qa", "forcesafesearch.google.com" },
1319 : : { "www.google.com.sa", "forcesafesearch.google.com" },
1320 : : { "www.google.com.sb", "forcesafesearch.google.com" },
1321 : : { "www.google.com.sg", "forcesafesearch.google.com" },
1322 : : { "www.google.com.sl", "forcesafesearch.google.com" },
1323 : : { "www.google.com.sv", "forcesafesearch.google.com" },
1324 : : { "www.google.com.tj", "forcesafesearch.google.com" },
1325 : : { "www.google.com.tr", "forcesafesearch.google.com" },
1326 : : { "www.google.com.tw", "forcesafesearch.google.com" },
1327 : : { "www.google.com.ua", "forcesafesearch.google.com" },
1328 : : { "www.google.com.uy", "forcesafesearch.google.com" },
1329 : : { "www.google.com.vc", "forcesafesearch.google.com" },
1330 : : { "www.google.com.vn", "forcesafesearch.google.com" },
1331 : : { "www.google.cv", "forcesafesearch.google.com" },
1332 : : { "www.google.cz", "forcesafesearch.google.com" },
1333 : : { "www.google.de", "forcesafesearch.google.com" },
1334 : : { "www.google.dj", "forcesafesearch.google.com" },
1335 : : { "www.google.dk", "forcesafesearch.google.com" },
1336 : : { "www.google.dm", "forcesafesearch.google.com" },
1337 : : { "www.google.dz", "forcesafesearch.google.com" },
1338 : : { "www.google.ee", "forcesafesearch.google.com" },
1339 : : { "www.google.es", "forcesafesearch.google.com" },
1340 : : { "www.google.fi", "forcesafesearch.google.com" },
1341 : : { "www.google.fm", "forcesafesearch.google.com" },
1342 : : { "www.google.fr", "forcesafesearch.google.com" },
1343 : : { "www.google.ga", "forcesafesearch.google.com" },
1344 : : { "www.google.ge", "forcesafesearch.google.com" },
1345 : : { "www.google.gg", "forcesafesearch.google.com" },
1346 : : { "www.google.gl", "forcesafesearch.google.com" },
1347 : : { "www.google.gm", "forcesafesearch.google.com" },
1348 : : { "www.google.gp", "forcesafesearch.google.com" },
1349 : : { "www.google.gr", "forcesafesearch.google.com" },
1350 : : { "www.google.gy", "forcesafesearch.google.com" },
1351 : : { "www.google.hn", "forcesafesearch.google.com" },
1352 : : { "www.google.hr", "forcesafesearch.google.com" },
1353 : : { "www.google.ht", "forcesafesearch.google.com" },
1354 : : { "www.google.hu", "forcesafesearch.google.com" },
1355 : : { "www.google.ie", "forcesafesearch.google.com" },
1356 : : { "www.google.im", "forcesafesearch.google.com" },
1357 : : { "www.google.iq", "forcesafesearch.google.com" },
1358 : : { "www.google.is", "forcesafesearch.google.com" },
1359 : : { "www.google.it", "forcesafesearch.google.com" },
1360 : : { "www.google.je", "forcesafesearch.google.com" },
1361 : : { "www.google.jo", "forcesafesearch.google.com" },
1362 : : { "www.google.kg", "forcesafesearch.google.com" },
1363 : : { "www.google.ki", "forcesafesearch.google.com" },
1364 : : { "www.google.kz", "forcesafesearch.google.com" },
1365 : : { "www.google.la", "forcesafesearch.google.com" },
1366 : : { "www.google.li", "forcesafesearch.google.com" },
1367 : : { "www.google.lk", "forcesafesearch.google.com" },
1368 : : { "www.google.lt", "forcesafesearch.google.com" },
1369 : : { "www.google.lu", "forcesafesearch.google.com" },
1370 : : { "www.google.lv", "forcesafesearch.google.com" },
1371 : : { "www.google.md", "forcesafesearch.google.com" },
1372 : : { "www.google.me", "forcesafesearch.google.com" },
1373 : : { "www.google.mg", "forcesafesearch.google.com" },
1374 : : { "www.google.mk", "forcesafesearch.google.com" },
1375 : : { "www.google.ml", "forcesafesearch.google.com" },
1376 : : { "www.google.mn", "forcesafesearch.google.com" },
1377 : : { "www.google.ms", "forcesafesearch.google.com" },
1378 : : { "www.google.mu", "forcesafesearch.google.com" },
1379 : : { "www.google.mv", "forcesafesearch.google.com" },
1380 : : { "www.google.mw", "forcesafesearch.google.com" },
1381 : : { "www.google.ne", "forcesafesearch.google.com" },
1382 : : { "www.google.nl", "forcesafesearch.google.com" },
1383 : : { "www.google.no", "forcesafesearch.google.com" },
1384 : : { "www.google.nr", "forcesafesearch.google.com" },
1385 : : { "www.google.nu", "forcesafesearch.google.com" },
1386 : : { "www.google.pl", "forcesafesearch.google.com" },
1387 : : { "www.google.pn", "forcesafesearch.google.com" },
1388 : : { "www.google.ps", "forcesafesearch.google.com" },
1389 : : { "www.google.pt", "forcesafesearch.google.com" },
1390 : : { "www.google.ro", "forcesafesearch.google.com" },
1391 : : { "www.google.rs", "forcesafesearch.google.com" },
1392 : : { "www.google.ru", "forcesafesearch.google.com" },
1393 : : { "www.google.rw", "forcesafesearch.google.com" },
1394 : : { "www.google.sc", "forcesafesearch.google.com" },
1395 : : { "www.google.se", "forcesafesearch.google.com" },
1396 : : { "www.google.sh", "forcesafesearch.google.com" },
1397 : : { "www.google.si", "forcesafesearch.google.com" },
1398 : : { "www.google.sk", "forcesafesearch.google.com" },
1399 : : { "www.google.sm", "forcesafesearch.google.com" },
1400 : : { "www.google.sn", "forcesafesearch.google.com" },
1401 : : { "www.google.so", "forcesafesearch.google.com" },
1402 : : { "www.google.sr", "forcesafesearch.google.com" },
1403 : : { "www.google.st", "forcesafesearch.google.com" },
1404 : : { "www.google.td", "forcesafesearch.google.com" },
1405 : : { "www.google.tg", "forcesafesearch.google.com" },
1406 : : { "www.google.tk", "forcesafesearch.google.com" },
1407 : : { "www.google.tl", "forcesafesearch.google.com" },
1408 : : { "www.google.tm", "forcesafesearch.google.com" },
1409 : : { "www.google.tn", "forcesafesearch.google.com" },
1410 : : { "www.google.to", "forcesafesearch.google.com" },
1411 : : { "www.google.tt", "forcesafesearch.google.com" },
1412 : : { "www.google.vg", "forcesafesearch.google.com" },
1413 : : { "www.google.vu", "forcesafesearch.google.com" },
1414 : : { "www.google.ws", "forcesafesearch.google.com" },
1415 : :
1416 : : { "m.youtube.com", "restrict.youtube.com" },
1417 : : { "www.youtube-nocookie.com", "restrict.youtube.com" },
1418 : : { "www.youtube.com", "restrict.youtube.com" },
1419 : : { "youtube.googleapis.com", "restrict.youtube.com" },
1420 : : { "youtubei.googleapis.com", "restrict.youtube.com" },
1421 : :
1422 : : { "pixabay.com", "safesearch.pixabay.com" },
1423 : : };
1424 : :
1425 : 0 : g_debug ("Adding safe search substitutions");
1426 : :
1427 : : /* FIXME: Perhaps also block search engines which don’t support safe
1428 : : * search: https://github.com/hagezi/dns-blocklists?tab=readme-ov-file#safesearch */
1429 [ # # ]: 0 : for (size_t j = 0; j < G_N_ELEMENTS (safe_search_substitutions); j++)
1430 : : {
1431 [ # # ]: 0 : if (cdb_make_put (&cdbm, safe_search_substitutions[j].hostname, strlen (safe_search_substitutions[j].hostname),
1432 : 0 : safe_search_substitutions[j].replacement, strlen (safe_search_substitutions[j].replacement),
1433 : : CDB_PUT_REPLACE) != 0)
1434 : : {
1435 : 0 : int errsv = errno;
1436 : 0 : g_task_return_new_error (task,
1437 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
1438 : 0 : _("Error adding to user filter: %s"), g_strerror (errsv));
1439 : 0 : return;
1440 : : }
1441 : : }
1442 : : }
1443 : :
1444 : : /* Write the indexes. */
1445 [ # # ]: 0 : if (cdb_make_finish (&cdbm) != 0)
1446 : : {
1447 : 0 : int errsv = errno;
1448 : 0 : g_task_return_new_error (task,
1449 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
1450 : 0 : _("Error adding to user filter: %s"), g_strerror (errsv));
1451 : 0 : return;
1452 : : }
1453 : :
1454 : : /* Atomically replace the compiled filter file with the new version. This
1455 : : * will cause the libnss module to reload it in user processes.
1456 : : * It would be nice if we could do an atomic rename with the linkat()
1457 : : * call, but that’s not currently possible, so this has to be two steps.
1458 : : * At least it means we can set file permissions in the middle. */
1459 : 0 : tmpfile = tmp_file_for_user (self, user);
1460 : 0 : tmpfile_basename = g_file_get_basename (tmpfile);
1461 : 0 : user_file_basename = g_file_get_basename (user_file);
1462 : :
1463 : 0 : g_debug ("Committing tinycdb database for user %u to file %s",
1464 : : user->uid, g_file_peek_path (user_file));
1465 : :
1466 [ # # ]: 0 : while (linkat (tmpfile_fd, "", data->filter_lists_dir_fd, tmpfile_basename, AT_EMPTY_PATH) < 0)
1467 : : {
1468 : 0 : int errsv = errno;
1469 : :
1470 [ # # ]: 0 : if (errsv == EINTR)
1471 : 0 : continue;
1472 : :
1473 : 0 : g_task_return_new_error (task,
1474 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
1475 : 0 : _("Error committing user filter: %s"),
1476 : : g_strerror (errsv));
1477 : 0 : return;
1478 : : }
1479 : :
1480 [ # # ]: 0 : while (fchmod (tmpfile_fd, 0644) < 0)
1481 : : {
1482 : 0 : int errsv = errno;
1483 : :
1484 [ # # ]: 0 : if (errsv == EINTR)
1485 : 0 : continue;
1486 : :
1487 : 0 : g_task_return_new_error (task,
1488 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
1489 : 0 : _("Error committing user filter: %s"),
1490 : : g_strerror (errsv));
1491 : 0 : return;
1492 : : }
1493 : :
1494 [ # # ]: 0 : while (renameat (data->filter_lists_dir_fd, tmpfile_basename, data->filter_lists_dir_fd, user_file_basename) < 0)
1495 : : {
1496 : 0 : int errsv = errno;
1497 : :
1498 [ # # ]: 0 : if (errsv == EINTR)
1499 : 0 : continue;
1500 : :
1501 : : /* Clean up. */
1502 : 0 : unlinkat (data->filter_lists_dir_fd, tmpfile_basename, 0 /* flags */);
1503 : :
1504 : 0 : g_task_return_new_error (task,
1505 : : MCT_FILTER_UPDATER_ERROR, MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM,
1506 : 0 : _("Error committing user filter: %s"),
1507 : : g_strerror (errsv));
1508 : 0 : return;
1509 : : }
1510 : : }
1511 : :
1512 : : /* Success! */
1513 : 0 : g_task_return_boolean (task, TRUE);
1514 : : }
1515 : :
1516 : : /**
1517 : : * mct_filter_updater_update_filters_finish:
1518 : : * @self: a filter updater
1519 : : * @result: result of the asynchronous operation
1520 : : * @error: return location for a [type@GLib.Error], or `NULL`
1521 : : *
1522 : : * Finish an asynchronous operation to update the filters.
1523 : : *
1524 : : * Returns: true on success, false otherwise
1525 : : * Since: 0.14.0
1526 : : */
1527 : : gboolean
1528 : 0 : mct_filter_updater_update_filters_finish (MctFilterUpdater *self,
1529 : : GAsyncResult *result,
1530 : : GError **error)
1531 : : {
1532 : 0 : g_return_val_if_fail (MCT_IS_FILTER_UPDATER (self), FALSE);
1533 : 0 : g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
1534 : 0 : g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
1535 : :
1536 : 0 : return g_task_propagate_boolean (G_TASK (result), error);
1537 : : }
1538 : :
1539 : : /**
1540 : : * mct_filter_updater_new:
1541 : : * @policy_manager: (transfer none): parental controls policy manager
1542 : : * @user_manager: (transfer none): user manager
1543 : : * @state_directory: (transfer none): directory to store state in
1544 : : * @cache_directory: (transfer none): directory to store cached data in
1545 : : *
1546 : : * Create a new [class@Malcontent.FilterUpdater].
1547 : : *
1548 : : * Returns: (transfer full): a new [class@Malcontent.FilterUpdater]
1549 : : * Since: 0.14.0
1550 : : */
1551 : : MctFilterUpdater *
1552 : 1 : mct_filter_updater_new (MctManager *policy_manager,
1553 : : MctUserManager *user_manager,
1554 : : GFile *state_directory,
1555 : : GFile *cache_directory)
1556 : : {
1557 : 1 : g_return_val_if_fail (MCT_IS_MANAGER (policy_manager), NULL);
1558 : 1 : g_return_val_if_fail (MCT_IS_USER_MANAGER (user_manager), NULL);
1559 : 1 : g_return_val_if_fail (G_IS_FILE (state_directory), NULL);
1560 : 1 : g_return_val_if_fail (G_IS_FILE (cache_directory), NULL);
1561 : :
1562 : 1 : return g_object_new (MCT_TYPE_FILTER_UPDATER,
1563 : : "policy-manager", policy_manager,
1564 : : "user-manager", user_manager,
1565 : : "state-directory", state_directory,
1566 : : "cache-directory", cache_directory,
1567 : : NULL);
1568 : : }
1569 : :
|