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