paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

paivana-httpd.c (36002B)


      1 /*
      2   This file is part of GNU Taler
      3   Copyright (C) 2012-2014 GNUnet e.V.
      4   Copyright (C) 2018, 2025, 2026 Taler Systems SA
      5 
      6   GNU Taler is free software; you can redistribute it and/or
      7   modify it under the terms of the GNU Affero General Public License
      8   as published by the Free Software Foundation; either version
      9   3, or (at your option) any later version.
     10 
     11   GNU Taler is distributed in the hope that it will be useful, but
     12   WITHOUT ANY WARRANTY; without even the implied warranty of
     13   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     14   GNU Affero General Public License for more details.
     15 
     16   You should have received a copy of the GNU Affero General Public
     17   License along with GNU Taler; see the file COPYING.  If not,
     18   write to the Free Software Foundation, Inc., 51 Franklin
     19   Street, Fifth Floor, Boston, MA 02110-1301, USA.
     20 */
     21 
     22 /**
     23  * @author Martin Schanzenbach
     24  * @author Christian Grothoff
     25  * @author Marcello Stanisci
     26  * @file src/backend/paivana-httpd.c
     27  * @brief HTTP proxy that acts as a GNU Taler paywall
     28  */
     29 #include "platform.h"
     30 #include <curl/curl.h>
     31 #include <gnunet/gnunet_util_lib.h>
     32 #include <gnunet/gnunet_curl_lib.h>
     33 #include <taler/taler_mhd_lib.h>
     34 #include <taler/taler_templating_lib.h>
     35 #include <taler/merchant/common.h>
     36 #include "paivana-httpd.h"
     37 #include "paivana-httpd_cookie.h"
     38 #include "paivana-httpd_daemon.h"
     39 #include "paivana-httpd_helper.h"
     40 #include "paivana-httpd_pay.h"
     41 #include "paivana-httpd_reverse.h"
     42 #include "paivana-httpd_templates.h"
     43 #include "paivana_pd.h"
     44 
     45 
     46 char *PH_target_server_base_url;
     47 
     48 char *PH_target_server_unixpath;
     49 
     50 char *PH_merchant_base_url;
     51 
     52 char *PH_base_url;
     53 
     54 struct GNUNET_CURL_Context *PH_merchant_ctx;
     55 
     56 struct GNUNET_CURL_Context *PH_proxy_ctx;
     57 
     58 int PH_no_check;
     59 
     60 int PH_respect_forwarded_headers;
     61 
     62 struct GNUNET_STRINGS_IPv4NetworkPolicy *PH_trusted_proxies4;
     63 
     64 struct GNUNET_STRINGS_IPv6NetworkPolicy *PH_trusted_proxies6;
     65 
     66 bool PH_have_trusted_proxies;
     67 
     68 /**
     69  * Compiled-in default for #PH_request_buffer_max.  Named because
     70  * `run()` has to be able to tell it apart from a value the operator
     71  * wrote, which is what decides whether #PH_max_request_size inherits
     72  * it.
     73  */
     74 #define PH_DEFAULT_REQUEST_BUFFER_MAX (256 * 1024)
     75 
     76 unsigned long long PH_request_buffer_max = PH_DEFAULT_REQUEST_BUFFER_MAX;
     77 
     78 unsigned long long PH_response_buffer_max = 256 * 1024;
     79 
     80 unsigned long long PH_max_request_size = 1024 * 1024;
     81 
     82 struct GNUNET_TIME_Relative PH_upstream_timeout;
     83 
     84 struct GNUNET_TIME_Relative PH_upstream_stall_timeout;
     85 
     86 unsigned int PH_connection_limit = 512;
     87 
     88 unsigned int PH_per_ip_connection_limit = 32;
     89 
     90 int PH_global_ret;
     91 
     92 int PH_global_cookie;
     93 
     94 regex_t PH_whitelist_ex;
     95 
     96 bool PH_have_whitelist_ex;
     97 
     98 /**
     99  * Our configuration.
    100  */
    101 const struct GNUNET_CONFIGURATION_Handle *PH_cfg;
    102 
    103 
    104 /**
    105  * Closure for #GNUNET_CURL_gnunet_scheduler_reschedule() of
    106  * #PH_merchant_ctx.
    107  */
    108 static struct GNUNET_CURL_RescheduleContext *merchant_ctx_rc;
    109 
    110 /**
    111  * Closure for #GNUNET_CURL_gnunet_scheduler_reschedule() of
    112  * #PH_proxy_ctx.
    113  */
    114 static struct GNUNET_CURL_RescheduleContext *proxy_ctx_rc;
    115 
    116 
    117 /* *************** General / main code *************** */
    118 
    119 
    120 /**
    121  * Load one of the `TRUSTED_PROXIES` options.
    122  *
    123  * The GNUnet policy parsers are lenient in ways that matter here, so
    124  * a non-NULL return is not on its own evidence that anything was
    125  * understood:
    126  *
    127  * - the list is terminated by an all-zero entry, so a network of
    128  *   0.0.0.0/0 or ::/0 *is* the terminator and silently truncates
    129  *   everything after it.  "Trust everyone" is therefore inexpressible
    130  *   — and would be a strange thing to write anyway;
    131  * - the v4 parser accepts a value whose last entry is not terminated
    132  *   by ';' -- the operator wrote the separators but not the
    133  *   terminator -- and drops that entry without a word, along with
    134  *   anything else after the final ';'.  (Its v6 counterpart refuses
    135  *   such a value outright, so this one is the v4 side's alone.)
    136  *
    137  * A genuinely malformed entry, including a v6 network handed to the
    138  * v4 parser, does come back as NULL rather than as a shorter list; it
    139  * is the only one of these that announces itself.
    140  *
    141  * The first comes out as "parsed, but nothing usable", which we reject
    142  * along with NULL: quietly trusting nobody would send every visitor to
    143  * the socket address, and the operator would have no hint why.  The
    144  * second is worse, because it succeeds: a single missing ';' would
    145  * leave the clients behind an unlisted proxy sharing that proxy's
    146  * address as their identity, i.e. sharing one paid cookie.  So the
    147  * entries that came back are counted against the ';' that went in, and
    148  * the value has to end in one.
    149  *
    150  * Not checked for here: an entry may also carry a port policy
    151  * ("10.0.0.0/8:80;"), which parses into a perfectly usable entry whose
    152  * port range nothing in paivana ever consults -- it reads as a
    153  * restriction on the trust and is not one.
    154  *
    155  * @param c configuration to read from
    156  * @param option name of the option
    157  * @param[out] count set to the number of usable entries
    158  * @return false if the option is present but unusable
    159  */
    160 static bool
    161 load_trusted_proxies (const struct GNUNET_CONFIGURATION_Handle *c,
    162                       const char *option,
    163                       unsigned int *count)
    164 {
    165   char *opt;
    166   bool v6 = (0 != strcmp (option,
    167                           "TRUSTED_PROXIES"));
    168   unsigned int want = 0;
    169   size_t len;
    170 
    171   *count = 0;
    172   if (GNUNET_OK !=
    173       GNUNET_CONFIGURATION_get_value_string (c,
    174                                              "paivana",
    175                                              option,
    176                                              &opt))
    177     return true; /* not configured at all: fine */
    178   len = strlen (opt);
    179   while ( (len > 0) &&
    180           ( (' ' == opt[len - 1]) ||
    181             ('\t' == opt[len - 1]) ) )
    182     opt[--len] = '\0';
    183   for (const char *p = strchr (opt, ';'); NULL != p; p = strchr (p + 1, ';'))
    184     want++;
    185   if (v6)
    186   {
    187     PH_trusted_proxies6 = GNUNET_STRINGS_parse_ipv6_policy (opt);
    188     if (NULL != PH_trusted_proxies6)
    189       while (! GNUNET_is_zero (&PH_trusted_proxies6[*count].network))
    190         (*count)++;
    191   }
    192   else
    193   {
    194     PH_trusted_proxies4 = GNUNET_STRINGS_parse_ipv4_policy (opt);
    195     if (NULL != PH_trusted_proxies4)
    196       while (0 != PH_trusted_proxies4[*count].network.s_addr)
    197         (*count)++;
    198   }
    199   if ( (0 == *count) ||
    200        (*count != want) ||
    201        (0 == len) ||
    202        (';' != opt[len - 1]) )
    203   {
    204     if ( (0 != *count) &&
    205          (*count != want) )
    206       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    207                   "Only %u of the %u entries of `%s' were understood;"
    208                   " refusing to trust a prefix of the list\n",
    209                   *count,
    210                   want,
    211                   option);
    212     else if (0 != *count)
    213       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    214                   "`%s' does not end in the ';' that terminates the last"
    215                   " entry; anything after the final one is dropped\n",
    216                   option);
    217     GNUNET_log_config_invalid (
    218       GNUNET_ERROR_TYPE_ERROR,
    219       "paivana",
    220       option,
    221       v6
    222       ? "not a usable IPv6 network list; entries are separated *and*"
    223       " terminated by ';' and must not contain spaces, e.g."
    224       " \"2001:db8::/32;fe80::/10;\" (note that ::/0 is indistinguishable"
    225       " from the end of the list and cannot be used)"
    226       : "not a usable IPv4 network list; entries are separated *and*"
    227       " terminated by ';', e.g. \"10.0.0.0/8;192.168.0.0/16;\""
    228       " (note that 0.0.0.0/0 is indistinguishable from the end of the"
    229       " list and cannot be used)");
    230     GNUNET_free (opt);
    231     return false;
    232   }
    233   GNUNET_free (opt);
    234   PH_have_trusted_proxies = true;
    235   return true;
    236 }
    237 
    238 
    239 /**
    240  * Task run on shutdown
    241  *
    242  * @param cls closure
    243  */
    244 static void
    245 do_shutdown (void *cls)
    246 {
    247   (void) cls;
    248   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    249               "Shutting down...\n");
    250   TALER_MHD_daemons_halt ();
    251   PAIVANA_HTTPD_payment_shutdown ();
    252   PAIVANA_HTTPD_reverse_shutdown ();
    253   TALER_MHD_daemons_destroy ();
    254   PAIVANA_HTTPD_unload_templates ();
    255   TALER_TEMPLATING_done ();
    256   GNUNET_free (PH_target_server_base_url);
    257   GNUNET_free (PH_target_server_unixpath);
    258   GNUNET_free (PH_trusted_proxies4);
    259   GNUNET_free (PH_trusted_proxies6);
    260   GNUNET_free (PH_merchant_base_url);
    261   GNUNET_free (PH_base_url);
    262   if (PH_have_whitelist_ex)
    263   {
    264     regfree (&PH_whitelist_ex);
    265     PH_have_whitelist_ex = false;
    266   }
    267   if (NULL != PH_merchant_ctx)
    268   {
    269     GNUNET_CURL_fini (PH_merchant_ctx);
    270     PH_merchant_ctx = NULL;
    271   }
    272   if (NULL != merchant_ctx_rc)
    273   {
    274     GNUNET_CURL_gnunet_rc_destroy (merchant_ctx_rc);
    275     merchant_ctx_rc = NULL;
    276   }
    277   if (NULL != PH_proxy_ctx)
    278   {
    279     GNUNET_CURL_fini (PH_proxy_ctx);
    280     PH_proxy_ctx = NULL;
    281   }
    282   if (NULL != proxy_ctx_rc)
    283   {
    284     GNUNET_CURL_gnunet_rc_destroy (proxy_ctx_rc);
    285     proxy_ctx_rc = NULL;
    286   }
    287 }
    288 
    289 
    290 /**
    291  * Remove trailing slashes from the web URL @a url, in place.
    292  *
    293  * Our configuration syntax prefers base URLs to be written with a
    294  * trailing '/', while everything we append to one -- a request path,
    295  * a "/.well-known/..." endpoint -- brings a leading '/' of its own.
    296  * Dropping them here is what keeps the concatenation from yielding
    297  * "//", which would otherwise reach the upstream verbatim and, for
    298  * BASE_URL, end up in the string the access cookie is keyed on and in
    299  * the URL the templates' regular expressions are matched against.
    300  *
    301  * Never strips below "scheme://h", so that a URL consisting of
    302  * nothing but a scheme and a host keeps its host.
    303  *
    304  * Refuses a URL carrying a query or a fragment instead of mangling it:
    305  * the trailing '/' would then be part of the query ("http://h/?p=a/")
    306  * and stripping it changes what the query says.  Neither has a sensible
    307  * reading in a base URL we concatenate a request path onto anyway --
    308  * "http://h/?p=a" + "/x" is not a request for /x -- so the operator is
    309  * better told than quietly corrected.
    310  *
    311  * @param[in,out] url URL to canonicalize; must have passed
    312  *        TALER_is_web_url()
    313  * @return false if @a url carries a query or a fragment, in which case
    314  *        it is left untouched
    315  */
    316 static bool
    317 strip_trailing_slashes (char *url)
    318 {
    319   size_t len = strlen (url);
    320   const char *sep;
    321   size_t min_len;
    322 
    323   if ( (NULL != strchr (url,
    324                         '?')) ||
    325        (NULL != strchr (url,
    326                         '#')) )
    327     return false;
    328   sep = strstr (url,
    329                 "://");
    330   GNUNET_assert (NULL != sep); /* was a web URL after all! */
    331   min_len = (size_t) (sep - url) + strlen ("://") + 1;
    332   while ( (len > min_len) &&
    333           ('/' == url[len - 1]) )
    334     url[--len] = '\0';
    335   return true;
    336 }
    337 
    338 
    339 /**
    340  * Main function that will be run.  Main tasks are (1) init. the
    341  * curl infrastructure (curl_global_init() / curl_multi_init()),
    342  * then fetch the HTTP port where its Web service should listen at,
    343  * and finally start MHD on that port.
    344  *
    345  * @param cls closure
    346  * @param args remaining command-line arguments
    347  * @param cfgfile name of the configuration file used (for saving, can be NULL!)
    348  * @param c configuration
    349  */
    350 static void
    351 run (void *cls,
    352      char *const *args,
    353      const char *cfgfile,
    354      const struct GNUNET_CONFIGURATION_Handle *c)
    355 {
    356   char *secret;
    357   bool buffer_max_explicit;
    358 
    359   (void) cls;
    360   (void) args;
    361   (void) cfgfile;
    362   PH_cfg = c;
    363   PH_upstream_timeout
    364     = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS,
    365                                      60);
    366   PH_upstream_stall_timeout
    367     = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS,
    368                                      60);
    369   GNUNET_SCHEDULER_add_shutdown (&do_shutdown,
    370                                  NULL);
    371   if ( (0 == PH_request_buffer_max) ||
    372        (PH_request_buffer_max > GNUNET_MAX_MALLOC_CHECKED) )
    373   {
    374     /* 0 would leave no room to move a body through at all; above the
    375        hard allocation cap the buffer simply cannot be made.  A negative
    376        argument ends up here too: GNUnet parses the option with
    377        sscanf("%llu"), which reads "-1" as ULLONG_MAX rather than
    378        complaining. */
    379     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    380                 "-u/--max-upload must be between 1 and %llu:"
    381                 " it sizes the buffer a request body is relayed through\n",
    382                 (unsigned long long) GNUNET_MAX_MALLOC_CHECKED);
    383     PH_global_ret = EXIT_INVALIDARGUMENT;
    384     GNUNET_SCHEDULER_shutdown ();
    385     return;
    386   }
    387   /* `-u' is applied by GNUNET_PROGRAM_run() before we are called, so a
    388      value differing from the compiled-in default is one the operator
    389      wrote.  See PH_max_request_size for why that has to be visible. */
    390   buffer_max_explicit = (PH_DEFAULT_REQUEST_BUFFER_MAX
    391                          != PH_request_buffer_max);
    392   if (! PH_no_check)
    393   {
    394     if (GNUNET_OK !=
    395         TALER_TEMPLATING_init (PAIVANA_project_data ()))
    396     {
    397       /* Almost always a missing or unreadable $PREFIX/share/paivana/
    398          templates/ -- an installation problem, not a bug, so say so
    399          instead of adding a second "Assertion failed" to the one
    400          TALER_TEMPLATING_init() already logged. */
    401       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    402                   "Failed to load the paywall templates; is paivana"
    403                   " installed, and does PAIVANA_PREFIX point at the"
    404                   " installation?\n");
    405       PH_global_ret = EXIT_NOTINSTALLED;
    406       GNUNET_SCHEDULER_shutdown ();
    407       return;
    408     }
    409   }
    410   if (! PAIVANA_HTTPD_reverse_init ())
    411   {
    412     GNUNET_break (0);
    413     PH_global_ret = EXIT_FAILURE;
    414     GNUNET_SCHEDULER_shutdown ();
    415     return;
    416   }
    417 
    418   if (GNUNET_OK !=
    419       GNUNET_CONFIGURATION_get_value_string (
    420         c,
    421         "paivana",
    422         "DESTINATION_BASE_URL",
    423         &PH_target_server_base_url))
    424   {
    425     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    426                                "paivana",
    427                                "DESTINATION_BASE_URL");
    428     PH_global_ret = EXIT_NOTCONFIGURED;
    429     GNUNET_SCHEDULER_shutdown ();
    430     return;
    431   }
    432   if (! TALER_is_web_url (PH_target_server_base_url))
    433   {
    434     GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    435                                "paivana",
    436                                "DESTINATION_BASE_URL",
    437                                "not a web url");
    438     PH_global_ret = EXIT_NOTCONFIGURED;
    439     GNUNET_SCHEDULER_shutdown ();
    440     return;
    441   }
    442   {
    443     unsigned long long v;
    444 
    445     if (GNUNET_OK ==
    446         GNUNET_CONFIGURATION_get_value_number (c,
    447                                                "paivana",
    448                                                "CONNECTION_LIMIT",
    449                                                &v))
    450     {
    451       if ( (0 == v) ||
    452            (v > UINT_MAX) )
    453       {
    454         GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    455                                    "paivana",
    456                                    "CONNECTION_LIMIT",
    457                                    "must be between 1 and UINT_MAX");
    458         PH_global_ret = EXIT_NOTCONFIGURED;
    459         GNUNET_SCHEDULER_shutdown ();
    460         return;
    461       }
    462       PH_connection_limit = (unsigned int) v;
    463     }
    464     if (GNUNET_OK ==
    465         GNUNET_CONFIGURATION_get_value_number (c,
    466                                                "paivana",
    467                                                "PER_IP_CONNECTION_LIMIT",
    468                                                &v))
    469     {
    470       if (v > UINT_MAX)
    471       {
    472         GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    473                                    "paivana",
    474                                    "PER_IP_CONNECTION_LIMIT",
    475                                    "must not exceed UINT_MAX");
    476         PH_global_ret = EXIT_NOTCONFIGURED;
    477         GNUNET_SCHEDULER_shutdown ();
    478         return;
    479       }
    480       PH_per_ip_connection_limit = (unsigned int) v;
    481     }
    482     if (GNUNET_OK ==
    483         GNUNET_CONFIGURATION_get_value_number (c,
    484                                                "paivana",
    485                                                "REQUEST_BUFFER_MAX",
    486                                                &v))
    487     {
    488       if ( (0 == v) ||
    489            (v > GNUNET_MAX_MALLOC_CHECKED) )
    490       {
    491         GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    492                                    "paivana",
    493                                    "REQUEST_BUFFER_MAX",
    494                                    "must be between 1 and 40 MiB");
    495         PH_global_ret = EXIT_NOTCONFIGURED;
    496         GNUNET_SCHEDULER_shutdown ();
    497         return;
    498       }
    499       PH_request_buffer_max = v;
    500       buffer_max_explicit = true;
    501     }
    502     if (GNUNET_OK ==
    503         GNUNET_CONFIGURATION_get_value_number (c,
    504                                                "paivana",
    505                                                "RESPONSE_BUFFER_MAX",
    506                                                &v))
    507     {
    508       if ( (0 == v) ||
    509            (v > GNUNET_MAX_MALLOC_CHECKED) )
    510       {
    511         GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    512                                    "paivana",
    513                                    "RESPONSE_BUFFER_MAX",
    514                                    "must be between 1 and 40 MiB");
    515         PH_global_ret = EXIT_NOTCONFIGURED;
    516         GNUNET_SCHEDULER_shutdown ();
    517         return;
    518       }
    519       PH_response_buffer_max = v;
    520     }
    521     if (GNUNET_OK ==
    522         GNUNET_CONFIGURATION_get_value_number (c,
    523                                                "paivana",
    524                                                "MAX_REQUEST_SIZE",
    525                                                &v))
    526     {
    527       PH_max_request_size = v;
    528     }
    529     else if (buffer_max_explicit)
    530     {
    531       /* Before streaming these were one number: the buffer a body was
    532          assembled in *was* the largest body we would accept.  An
    533          operator who raised it to permit large uploads meant the limit,
    534          so honour that reading rather than silently tightening their
    535          configuration back down to the 1 MiB default. */
    536       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    537                   "Taking MAX_REQUEST_SIZE from the configured request"
    538                   " buffer size (%llu bytes); set it explicitly to say"
    539                   " otherwise\n",
    540                   PH_request_buffer_max);
    541       PH_max_request_size = PH_request_buffer_max;
    542     }
    543   }
    544   {
    545     struct GNUNET_TIME_Relative st;
    546 
    547     if (GNUNET_OK ==
    548         GNUNET_CONFIGURATION_get_value_time (c,
    549                                              "paivana",
    550                                              "UPSTREAM_TIMEOUT",
    551                                              &st))
    552     {
    553       if (0 == st.rel_value_us)
    554       {
    555         GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    556                                    "paivana",
    557                                    "UPSTREAM_TIMEOUT",
    558                                    "must not be zero");
    559         PH_global_ret = EXIT_NOTCONFIGURED;
    560         GNUNET_SCHEDULER_shutdown ();
    561         return;
    562       }
    563       PH_upstream_timeout = st;
    564     }
    565     if (GNUNET_OK ==
    566         GNUNET_CONFIGURATION_get_value_time (c,
    567                                              "paivana",
    568                                              "UPSTREAM_STALL_TIMEOUT",
    569                                              &st))
    570     {
    571       if (0 == st.rel_value_us)
    572       {
    573         GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    574                                    "paivana",
    575                                    "UPSTREAM_STALL_TIMEOUT",
    576                                    "must not be zero");
    577         PH_global_ret = EXIT_NOTCONFIGURED;
    578         GNUNET_SCHEDULER_shutdown ();
    579         return;
    580       }
    581       PH_upstream_stall_timeout = st;
    582     }
    583   }
    584   {
    585     unsigned int n4;
    586     unsigned int n6;
    587 
    588     if ( (! load_trusted_proxies (c,
    589                                   "TRUSTED_PROXIES",
    590                                   &n4)) ||
    591          (! load_trusted_proxies (c,
    592                                   "TRUSTED_PROXIES6",
    593                                   &n6)) )
    594     {
    595       PH_global_ret = EXIT_NOTCONFIGURED;
    596       GNUNET_SCHEDULER_shutdown ();
    597       return;
    598     }
    599     if (PH_have_trusted_proxies)
    600     {
    601       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    602                   "Trusting %u IPv4 and %u IPv6 network(s) as reverse proxies\n",
    603                   n4,
    604                   n6);
    605       if (! PH_respect_forwarded_headers)
    606       {
    607         /* The policy says which proxies may speak for a client; it is
    608            the -f flag that says we listen at all.  Configuring one
    609            without the other is a mistake in either direction, but only
    610            this one leaves the policy inert. */
    611         GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    612                     "TRUSTED_PROXIES configured but -f/--respect-forwarded-headers"
    613                     " is not set; forwarded headers are ignored entirely\n");
    614       }
    615     }
    616     else if (PH_respect_forwarded_headers)
    617     {
    618       /* Not the same mistake in reverse: a single proxy in front, one
    619          that writes the forwarding headers itself, needs no policy at
    620          all.  PAIVANA_HTTPD_resolve_forwarding() walks the chain from
    621          the right and stops at the first hop it does not trust, so
    622          with no policy it stops on its first step -- at the element
    623          our peer wrote, which nothing the client prepends can displace.
    624          What the policy adds is the ability to keep stepping leftwards
    625          through hops that are named there, which is what a chain of
    626          more than one proxy needs.  The hazard worth warning about is
    627          narrower, and it is upstream of us. */
    628       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    629                   "-f/--respect-forwarded-headers is set without TRUSTED_PROXIES:"
    630                   " the client address is taken from the rightmost element of the"
    631                   " forwarding chain, which is the client's own address only if"
    632                   " the server in front sets or appends these headers itself."
    633                   "  One that passes the client's `%s' through unchanged -- as"
    634                   " nginx does, and `%s' is the header we prefer over `%s' --"
    635                   " leaves the client choosing the element we believe."
    636                   "  TRUSTED_PROXIES is needed only for a chain of more than one"
    637                   " proxy\n",
    638                   MHD_HTTP_HEADER_FORWARDED,
    639                   MHD_HTTP_HEADER_FORWARDED,
    640                   PH_HEADER_X_FORWARDED_FOR);
    641     }
    642   }
    643   /* No need to check the return value.  If given, we take it,
    644    * otherwise it stays NULL.  */
    645   GNUNET_CONFIGURATION_get_value_filename (
    646     c,
    647     "paivana",
    648     "DESTINATION_UNIXPATH",
    649     &PH_target_server_unixpath);
    650   if (! strip_trailing_slashes (PH_target_server_base_url))
    651   {
    652     GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    653                                "paivana",
    654                                "DESTINATION_BASE_URL",
    655                                "must not carry a query or a fragment");
    656     PH_global_ret = EXIT_NOTCONFIGURED;
    657     GNUNET_SCHEDULER_shutdown ();
    658     return;
    659   }
    660   if (! PH_no_check)
    661   {
    662     if (GNUNET_OK !=
    663         GNUNET_CONFIGURATION_get_value_string (
    664           c,
    665           "paivana",
    666           "MERCHANT_BACKEND_URL",
    667           &PH_merchant_base_url))
    668     {
    669       GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    670                                  "paivana",
    671                                  "MERCHANT_BACKEND_URL");
    672       PH_global_ret = EXIT_NOTCONFIGURED;
    673       GNUNET_SCHEDULER_shutdown ();
    674       return;
    675     }
    676     if (! TALER_is_web_url (PH_merchant_base_url))
    677     {
    678       GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    679                                  "paivana",
    680                                  "MERCHANT_BACKEND_URL",
    681                                  "not a web url");
    682       PH_global_ret = EXIT_NOTCONFIGURED;
    683       GNUNET_SCHEDULER_shutdown ();
    684       return;
    685     }
    686     /* Deliberately *not* run through strip_trailing_slashes(): a Taler
    687        merchant API base URL is joined with relative paths, so it wants
    688        the '/' the other base URLs above shed.  TALER_url_join() insists
    689        on it and returns NULL without it, which surfaces much later as
    690        an assertion failure on the handle built from the joined URL --
    691        so check for it here, where we can still name the option that is
    692        wrong. */
    693     if ('/' != PH_merchant_base_url[strlen (PH_merchant_base_url) - 1])
    694     {
    695       GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    696                                  "paivana",
    697                                  "MERCHANT_BACKEND_URL",
    698                                  "must end with a '/'");
    699       PH_global_ret = EXIT_NOTCONFIGURED;
    700       GNUNET_SCHEDULER_shutdown ();
    701       return;
    702     }
    703   }
    704   {
    705     char *merchant_unix_path;
    706 
    707     if (GNUNET_OK ==
    708         GNUNET_CONFIGURATION_get_value_string (
    709           c,
    710           "paivana",
    711           "MERCHANT_BACKEND_UNIX_PATH",
    712           &merchant_unix_path))
    713     {
    714       if (! TALER_MERCHANT_global_set_unixpath (merchant_unix_path))
    715       {
    716         GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING,
    717                                    "paivana",
    718                                    "MERCHANT_BACKEND_UNIX_PATH",
    719                                    "invalid path; ignoring the setting");
    720       }
    721       GNUNET_free (merchant_unix_path);
    722     }
    723   }
    724   {
    725     char *whitelist;
    726 
    727     if (GNUNET_OK ==
    728         GNUNET_CONFIGURATION_get_value_string (
    729           c,
    730           "paivana",
    731           "WHITELIST",
    732           &whitelist))
    733     {
    734       regex_t bare;
    735       int rc;
    736 
    737       /* Compile the expression exactly as written first, and refuse
    738          the configuration if that fails.  The wrapping below is a
    739          textual splice, and a value like "a)|(b" splices into
    740          "^(a)|(b)$" -- which compiles, but means "^a" OR "b$", each
    741          anchored on one side only.  Getting out of the group that way
    742          needs parentheses that do not balance on their own, and that
    743          is precisely what a bare regcomp() rejects; an expression that
    744          is valid by itself is unaffected and keeps matching what it
    745          always did.  (The merchant backend validates the website_regex
    746          of a template the same way.) */
    747       rc = regcomp (&bare,
    748                     whitelist,
    749                     REG_NOSUB | REG_EXTENDED);
    750       if (0 == rc)
    751       {
    752         char *anchored;
    753 
    754         regfree (&bare);
    755         /* Anchor the expression: regexec(3) is unanchored, so a
    756            WHITELIST of "/free/" would otherwise waive payment for every
    757            URL merely *containing* it -- including one an attacker
    758            appends to a path they want for free.  Wrapping in a group
    759            keeps alternations ("a|b") from binding the anchors to only
    760            the first and last branch.  An expression that already
    761            anchors itself is unaffected, as ^ and $ inside still match
    762            at string start/end. */
    763         GNUNET_asprintf (&anchored,
    764                          "^(%s)$",
    765                          whitelist);
    766         rc = regcomp (&PH_whitelist_ex,
    767                       anchored,
    768                       REG_NOSUB | REG_EXTENDED);
    769         GNUNET_free (anchored);
    770       }
    771       if (0 != rc)
    772       {
    773         GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    774                                    "paivana",
    775                                    "WHITELIST",
    776                                    "Invalid regular expression");
    777         GNUNET_free (whitelist);
    778         PH_global_ret = EXIT_NOTCONFIGURED;
    779         GNUNET_SCHEDULER_shutdown ();
    780         return;
    781       }
    782       PH_have_whitelist_ex = true;
    783       GNUNET_free (whitelist);
    784     }
    785   }
    786 
    787   if (GNUNET_OK !=
    788       GNUNET_CONFIGURATION_get_value_string (
    789         c,
    790         "paivana",
    791         "BASE_URL",
    792         &PH_base_url))
    793   {
    794     /* Without BASE_URL we reconstruct our own URL from the request, and
    795        the only thing that makes that safe is a reverse proxy in front
    796        of us that enforced a correct Host: -- which is exactly what -f
    797        asserts.  Talking to clients directly, Host: is whatever the
    798        client typed, and it decides both the string the access cookie is
    799        keyed on and the string the templates' website_regex is matched
    800        against: a client sending "Host: anything.invalid" would match no
    801        template and be served for free.  The scheme has the same
    802        problem in reverse: direct connections have no X-Forwarded-Proto
    803        to consult, so a site reached over https would generate http://
    804        URLs and again match no template. */
    805     if ( (! PH_respect_forwarded_headers) &&
    806          (! PH_no_check) )
    807     {
    808       /* -n has no paywall, hence no access decision that the site's
    809          own identity could be got wrong for; the base URL is then only
    810          cosmetic and the "pure reverse proxy" mode stays configurable
    811          with nothing but DESTINATION_BASE_URL. */
    812       GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    813                                  "paivana",
    814                                  "BASE_URL");
    815       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    816                   "BASE_URL is required unless -f is given: without a"
    817                   " reverse proxy vouching for it, the client's Host"
    818                   " header cannot be used to identify this site\n");
    819       PH_global_ret = EXIT_NOTCONFIGURED;
    820       GNUNET_SCHEDULER_shutdown ();
    821       return;
    822     }
    823     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_INFO,
    824                                "paivana",
    825                                "BASE_URL");
    826   }
    827   if (NULL != PH_base_url)
    828   {
    829     if (! TALER_is_web_url (PH_base_url))
    830     {
    831       GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    832                                  "paivana",
    833                                  "BASE_URL",
    834                                  "not a web url");
    835       PH_global_ret = EXIT_NOTCONFIGURED;
    836       GNUNET_SCHEDULER_shutdown ();
    837       return;
    838     }
    839     if (! strip_trailing_slashes (PH_base_url))
    840     {
    841       GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    842                                  "paivana",
    843                                  "BASE_URL",
    844                                  "must not carry a query or a fragment");
    845       PH_global_ret = EXIT_NOTCONFIGURED;
    846       GNUNET_SCHEDULER_shutdown ();
    847       return;
    848     }
    849   }
    850 
    851   if (GNUNET_OK !=
    852       GNUNET_CONFIGURATION_get_value_string (
    853         c,
    854         "paivana",
    855         "SECRET",
    856         &secret))
    857   {
    858     if (! PH_no_check)
    859     {
    860       /* The key is the only input to the access-cookie MAC that the
    861          client does not supply, so a fresh random one invalidates
    862          every access anyone has paid for.  That used to be a warning
    863          and a random key, which is survivable only if restarts are
    864          rare -- and the shipped unit restarts hourly (RuntimeMaxSec),
    865          so a customer paying at 10:59 was shown the paywall again at
    866          11:01.  Refuse to start instead: the packaging generates one
    867          at install time, and an operator configuring by hand needs to
    868          be told rather than silently sold a paywall that forgets.  `-n'
    869          mints no cookies at all and so needs no key. */
    870       GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    871                                  "paivana",
    872                                  "SECRET");
    873       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    874                   "Refusing to start without `SECRET': every restart"
    875                   " would invalidate all access already paid for."
    876                   "  Generate one with `gpg --gen-random 0 32 | base64'"
    877                   " (or pass -n to serve without a paywall).\n");
    878       PH_global_ret = EXIT_NOTCONFIGURED;
    879       GNUNET_SCHEDULER_shutdown ();
    880       return;
    881     }
    882     GNUNET_CRYPTO_random_block (&paivana_secret,
    883                                 sizeof (paivana_secret));
    884   }
    885   else
    886   {
    887     GNUNET_CRYPTO_hash (secret,
    888                         strlen (secret),
    889                         &paivana_secret);
    890     GNUNET_free (secret);
    891   }
    892   PH_proxy_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
    893                                    &proxy_ctx_rc);
    894   GNUNET_assert (NULL != PH_proxy_ctx);
    895   proxy_ctx_rc = GNUNET_CURL_gnunet_rc_create (PH_proxy_ctx);
    896   if (! PH_no_check)
    897   {
    898     char *merchant_access_token;
    899     char *auth_header;
    900 
    901     if (GNUNET_OK !=
    902         GNUNET_CONFIGURATION_get_value_string (
    903           c,
    904           "paivana",
    905           "MERCHANT_ACCESS_TOKEN",
    906           &merchant_access_token))
    907     {
    908       GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    909                                  "paivana",
    910                                  "MERCHANT_ACCESS_TOKEN");
    911       PH_global_ret = EXIT_NOTCONFIGURED;
    912       GNUNET_SCHEDULER_shutdown ();
    913       return;
    914     }
    915     if ('\0' == merchant_access_token[0])
    916     {
    917       /* An empty value builds "Authorization: Bearer " and the backend
    918          answers 401 to every request we make with it, i.e. no template
    919          ever loads and no payment is ever confirmed -- and nothing
    920          says why.  The option being present but blank is the shape a
    921          half-finished install has. */
    922       GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
    923                                  "paivana",
    924                                  "MERCHANT_ACCESS_TOKEN",
    925                                  "must not be empty");
    926       GNUNET_free (merchant_access_token);
    927       PH_global_ret = EXIT_NOTCONFIGURED;
    928       GNUNET_SCHEDULER_shutdown ();
    929       return;
    930     }
    931     /* A second context, because the credential below is appended to
    932        *every* request the context makes: on a shared context we would
    933        hand our merchant bearer token to the origin server (and to
    934        whoever it redirects to) on each forwarded request. */
    935     PH_merchant_ctx
    936       = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
    937                           &merchant_ctx_rc);
    938     GNUNET_assert (NULL != PH_merchant_ctx);
    939     merchant_ctx_rc = GNUNET_CURL_gnunet_rc_create (PH_merchant_ctx);
    940     GNUNET_asprintf (&auth_header,
    941                      "%s: Bearer %s",
    942                      MHD_HTTP_HEADER_AUTHORIZATION,
    943                      merchant_access_token);
    944     GNUNET_free (merchant_access_token);
    945     GNUNET_assert (GNUNET_OK ==
    946                    GNUNET_CURL_append_header (PH_merchant_ctx,
    947                                               auth_header));
    948     GNUNET_free (auth_header);
    949   }
    950   /* Once templates are done loading, this will
    951      start the daemon as well.  In -n (no-payment) mode we skip
    952      the merchant round-trip entirely. */
    953   if (PH_no_check)
    954   {
    955     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    956                 "Paywall disabled (-n), skipping template load\n");
    957     PAIVANA_HTTPD_serve_requests ();
    958     return;
    959   }
    960   PAIVANA_HTTPD_load_templates ();
    961 }
    962 
    963 
    964 /**
    965  * Main function.
    966  */
    967 int
    968 main (int argc,
    969       char *const *argv)
    970 {
    971   struct GNUNET_GETOPT_CommandLineOption options[] = {
    972     GNUNET_GETOPT_option_flag (
    973       'f',
    974       "respect-forwarded-headers",
    975       gettext_noop (
    976         "trust X-Forwarded-For for the client address (only safe behind a trusted reverse proxy)"),
    977       &PH_respect_forwarded_headers),
    978     GNUNET_GETOPT_option_flag (
    979       'g',
    980       "global-payment",
    981       gettext_noop (
    982         "disables per-page payment, useful if a single payment should grant access to the entire site"),
    983       &PH_global_cookie),
    984     GNUNET_GETOPT_option_flag (
    985       'n',
    986       "no-payment",
    987       gettext_noop (
    988         "disables payment, useful for testing reverse-proxy only"),
    989       &PH_no_check),
    990     GNUNET_GETOPT_option_ulong (
    991       'u',
    992       "max-upload",
    993       "BYTES",
    994       gettext_noop (
    995         "bytes of a request body to hold in memory while relaying it upstream (default: 262144); the largest body accepted is MAX_REQUEST_SIZE"),
    996       &PH_request_buffer_max),
    997     GNUNET_GETOPT_OPTION_END
    998   };
    999   enum GNUNET_GenericReturnValue ret;
   1000 
   1001   ret = GNUNET_PROGRAM_run (
   1002     PAIVANA_project_data (),
   1003     argc,
   1004     argv,
   1005     "paivana-httpd",
   1006     "reverse proxy requesting Taler payment",
   1007     options,
   1008     &run, NULL);
   1009   if (GNUNET_SYSERR == ret)
   1010     return EXIT_INVALIDARGUMENT;
   1011   if (GNUNET_NO == ret)
   1012     return EXIT_SUCCESS;
   1013   return PH_global_ret;
   1014 }
   1015 
   1016 
   1017 /* end of paivana-httpd.c */