LCOV - code coverage report
Current view: top level - libmalcontent-web - filter-updater.c (source / functions) Coverage Total Hit
Test: 2 coverage DB files Lines: 8.7 % 515 45
Test Date: 2025-09-15 13:55:46 Functions: 23.5 % 34 8
Branches: 3.7 % 298 11

             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                 :             : 
        

Generated by: LCOV version 2.0-1