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

Generated by: LCOV version 2.0-1