paivana

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

README (27902B)


      1 Paivana
      2 =======
      3 
      4 Paivana is an HTTP reverse proxy that gates access to a target website
      5 behind a GNU Taler payment.  Unpaid visitors receive a paywall page
      6 where they can pay using a GNU Taler wallet; once payment is confirmed
      7 the request is forwarded transparently to the configured upstream
      8 server.
      9 
     10 The sole binary is `paivana-httpd`.
     11 
     12 
     13 How it works
     14 ------------
     15 
     16 0. `paivana-httpd` learns prices from the Paivana templates configured
     17    in the taler-merchant-backend.  Paivana templates include a regular
     18    expression which determines the set of pages the template applies to.
     19    That expression (POSIX extended) is matched against the *entire*
     20    URL and is anchored at both ends, so `/premium/` applies to nothing
     21    while `.*/premium/.*` applies to every URL containing it.
     22 1. An HTTP client accesses a page at `paivana-httpd`.
     23 2. If the paywall is enabled for the respective URL and no valid access
     24    cookie is present, `paivana-httpd` redirects the browser to
     25    a static paywall HTML page (customizable Mustache template)
     26    referencing the payment template.
     27    The page includes a "Paivana" HTTP header to also facilitate agentic
     28    payments.
     29 3. The browser computes a unique payment identifier and
     30    renders a dynamic payment request (taler:// QR code
     31    or link) and long-polls the taler-merchant-backend awaiting
     32    completion of the payment.
     33 4. The user instructs their Taler wallet to complete the payment.
     34 5. The browser notices that the payment is complete and calls back to
     35    `POST /.well-known/paivana` (provided by `paivana-httpd`)
     36    with a reference to the unique payment identifier.
     37 6. `paivana-httpd` verifies the payment with the merchant, sets
     38    an access cookie, and redirects the browser to the original URL.
     39    The access ends at the `expiration` the browser named in step 3 and
     40    repeated in step 5 — it is hashed into the payment identifier, so the
     41    two must agree — bounded above by the contract's `max_pickup_time`,
     42    which is what the merchant's `max_pickup_delay` on the template sets.
     43 7. Requests with a valid cookie are forwarded to the upstream server
     44    via libcurl.  Both directions are streamed: bytes are passed on as
     45    they arrive, so the size of a proxied body is not bounded by memory
     46    (see "Architecture notes").
     47 
     48 The cookie carries a keyed hash over `(expiration time, website, client
     49 address)` keyed by a `paivana_secret` derived from the configured
     50 `SECRET`.  The construction is `GNUNET_CRYPTO_hkdf_gnunet()`, which is
     51 HMAC-based but is not itself an HMAC.  `SECRET` is required whenever the
     52 paywall is on: it is the only input to that hash the client does not
     53 supply, so a key invented afresh at each start would invalidate every
     54 access anyone had already paid for.  Under `-n` no cookie is ever minted
     55 and none is needed.
     56 
     57 Keep the same `SECRET` on every `paivana-httpd` serving one site — a
     58 cookie issued by one has to verify on the next — and treat it as
     59 equivalent to the content itself: whoever holds it can mint access for
     60 any visitor, URL and expiry.
     61 
     62 
     63 Dependencies
     64 ------------
     65 
     66 - GNUnet (libgnunetutil, libgnunetjson, libgnunetcurl)
     67 - libmicrohttpd
     68 - libcurl >= 7.62.0
     69 - libjansson
     70 - libgcrypt
     71 - GNU Taler: libtalerutil, libtalerjson, libtalermerchant,
     72              libtalermhd, libtalertemplating
     73 
     74 That is what the binary links against; `readelf -d` on it is the
     75 authority.  Paivana does not use libtalerexchange, and compresses
     76 nothing itself — that happens inside libtalermhd, which brings zlib
     77 with it.
     78 
     79 
     80 Build
     81 -----
     82 
     83 The project uses Meson but supports a GNU build process.
     84 
     85     ./bootstrap
     86     ./configure --prefix=$TARGET
     87     make
     88     sudo make install
     89 
     90 
     91 Configuration
     92 -------------
     93 
     94 Paivana reads an INI-style `.conf` file.  The only section used is
     95 `[paivana]`.  A minimal working configuration:
     96 
     97     [paivana]
     98     DESTINATION_BASE_URL = https://example.com/
     99     MERCHANT_BACKEND_URL  = https://backend.demo.taler.net/instances/sandbox/
    100     # Optional server-side route; useful with MERCHANT_BACKEND_UNIX_PATH.
    101     # MERCHANT_BACKEND_INTERNAL_URL = http://backend.internal/instances/sandbox/
    102     MERCHANT_ACCESS_TOKEN = secret-token:sandbox
    103     BASE_URL = http://localhost:9967/
    104     SERVE = tcp
    105     PORT  = 9967
    106 
    107 ### Required keys
    108 
    109   Key                     Description
    110   ----------------------  -----------------------------------------------------
    111   DESTINATION_BASE_URL    Upstream server to proxy to once payment is confirmed.
    112   MERCHANT_BACKEND_URL    Public base URL of the Taler merchant backend.
    113                           It is advertised to browsers and wallets as well
    114                           as used for backend requests unless an internal
    115                           URL is configured below.
    116   MERCHANT_ACCESS_TOKEN   Bearer token for all calls to the merchant backend.
    117   BASE_URL                Public base URL of Paivana.  Required unless `-f`
    118                           is given, in which case it is derived from the
    119                           forwarding headers (see below).
    120   SECRET                  Key for the access-cookie MAC.  Required unless
    121                           `-n`; see above for why there is no sensible
    122                           default.  It is *not* an input to the Paivana ID,
    123                           which the browser has to be able to recompute on
    124                           its own.  The Debian package generates one into
    125                           /etc/paivana/secrets/paivana.secret.conf, which
    126                           is not world-readable.
    127   SERVE                   `tcp`, `unix` (Unix-domain socket) or `systemd`
    128                           (socket activation).  There is no default: an
    129                           instance without it exits at startup, and it does
    130                           so only after the templates have been fetched.
    131   PORT                    TCP port.  Required when SERVE = tcp.
    132   UNIXPATH                Path to bind to.  Required when SERVE = unix.
    133   UNIXPATH_MODE           Access mode of that socket, octal.  Required
    134                           when SERVE = unix — it is what governs who may
    135                           reach Paivana, since a Unix-domain peer is
    136                           trusted to report the client address.
    137 
    138 ### Optional keys
    139 
    140   Key       Description
    141   --------  ---------------------------------------------------------------
    142   WHITELIST POSIX extended regular expression; matching request paths
    143             are forwarded without payment.  Matched against the
    144             *entire* path and anchored at both ends, so `/free/`
    145             whitelists nothing while `/free/.*` whitelists that
    146             subtree.
    147   TRUSTED_PROXIES
    148             IPv4 networks whose members are reverse proxies trusted to
    149             report the client address in `Forwarded` or
    150             `X-Forwarded-For`.  Only consulted with `-f`, and only
    151             needed when more than one proxy is in front.  See "Trusted
    152             proxies" below.
    153   TRUSTED_PROXIES6
    154             IPv6 counterpart of TRUSTED_PROXIES.
    155   CONNECTION_LIMIT
    156             Total number of concurrent client connections to accept,
    157             default 384.  Divided evenly over the listen sockets that
    158             come up, so the process-wide total is what you set --
    159             with no BIND_TO there are two (IPv4 and IPv6).  The current
    160             event loop uses select() and can represent only 1024 file
    161             descriptors.  Each active request may hold a client and an
    162             outbound socket, so the maximum is (1024 - 256 reserve) / 2
    163             = 384.  The reserve covers listeners, curl pools, resolver,
    164             scheduler and transient overlap.  Paivana refuses unsafe
    165             values or an RLIMIT_NOFILE below the calculated requirement.
    166   PAYMENT_CONNECTION_LIMIT
    167             Slots reserved within CONNECTION_LIMIT for payment-redemption
    168             POSTs, default 32.  This leaves 352 ordinary slots and also
    169             bounds the merchant lookups that unauthenticated clients can
    170             hold for their five-second long poll.  Must be at least 1 and
    171             smaller than CONNECTION_LIMIT.
    172   PER_IP_CONNECTION_LIMIT
    173             Concurrent connections accepted from any one client
    174             address, default 32; 0 disables the check.  Set it to 0
    175             wherever the peer address is not the client's -- under
    176             SERVE = unix or systemd every client shares one peer
    177             address, and behind a reverse proxy or a NAT many clients
    178             do, so a limit there throttles everyone at once.
    179   RELAY_MEMORY_LIMIT
    180             Aggregate accounting ceiling for ordinary-request streaming
    181             rings, default 268435456 bytes (256 MiB).  Startup requires
    182             (REQUEST_BUFFER_MAX + RESPONSE_BUFFER_MAX) multiplied by
    183             (CONNECTION_LIMIT - PAYMENT_CONNECTION_LIMIT) to fit.  The
    184             defaults account for 176 MiB and retain 80 MiB of this budget
    185             for configuration growth; templates, curl and allocator memory
    186             remain outside the accounting, which is why the margin matters.
    187   SHUTDOWN_GRACE_PERIOD
    188             Time accepted requests may finish after SIGTERM, default 60 s;
    189             0 means immediate shutdown.  Listeners are quiesced first, so
    190             socket activation queues new connections for the replacement.
    191             Keep the service manager's stop timeout above this value; the
    192             package uses 75 s to leave 15 s for cancellation and cleanup.
    193   BIND_TO   IP address to bind to; dual-stack wildcard if absent.
    194   DESTINATION_UNIXPATH
    195             Unix-domain socket to reach the upstream on instead of
    196             connecting to the authority in `DESTINATION_BASE_URL`.  That
    197             URL is still what the request line and `Host` are built
    198             from, so it remains required.
    199   MERCHANT_BACKEND_UNIX_PATH
    200             Unix-domain socket to reach the merchant backend.  The URL
    201             scheme of `MERCHANT_BACKEND_INTERNAL_URL` (or, when absent,
    202             `MERCHANT_BACKEND_URL`) is still spoken over that socket:
    203             use `http` for a cleartext socket and `https` only for a
    204             TLS-speaking socket.  An unusable path is a warning and the
    205             setting is then ignored, not a startup failure.
    206   MERCHANT_BACKEND_INTERNAL_URL
    207             Complete merchant-instance base URL used only for Paivana's
    208             server-side template and order requests.  It must be an HTTP(S)
    209             URL ending in `/` and include the instance path.  Defaults to
    210             `MERCHANT_BACKEND_URL`.  Set this to an `http` URL together with
    211             `MERCHANT_BACKEND_UNIX_PATH` when a public HTTPS merchant is
    212             exposed locally through a cleartext socket; browser-facing
    213             pay-template URIs, JavaScript and CSP continue to use the public
    214             `MERCHANT_BACKEND_URL`.
    215 
    216 
    217 Running
    218 -------
    219 
    220 $ paivana-httpd -c /etc/paivana/paivana.conf
    221 
    222 Besides the options GNUnet gives every program (`-c` / `--config`,
    223 `-L` / `--log`, `-l` / `--logfile`, `-h`, `-v`), Paivana takes four:
    224 
    225   -n, --no-payment    Bypass the paywall entirely — a pure reverse proxy,
    226                       which is what the test suite runs.  No cookie is
    227                       ever minted and `SECRET` is not required.
    228   -g, --global-payment
    229                       One payment grants access to the whole site rather
    230                       than to the URL it was made for.  The website goes
    231                       into the cookie's keyed hash as the empty string,
    232                       and the cookie is scoped to `/` rather than to the
    233                       page, so cookies minted under one setting do not
    234                       verify under the other: flipping it invalidates
    235                       whatever access is outstanding.
    236   -f, --respect-forwarded-headers
    237                       Take the client address from the forwarding
    238                       headers.  Only safe behind a proxy that overwrites
    239                       them; see "Deployment behind a reverse proxy".
    240   -u, --max-upload BYTES
    241                       Bytes of a request body held in memory at once
    242                       while relaying it upstream, default 262144.  A
    243                       throughput knob, not a limit: the largest body
    244                       accepted is MAX_REQUEST_SIZE.  For configurations
    245                       written when these were one number, setting this
    246                       and not MAX_REQUEST_SIZE still sets both.
    247 
    248 The daemon does not serve requests until it has fetched paywall templates
    249 from the merchant backend.  If template loading fails, startup is aborted:
    250 a template that could not be loaded would otherwise leave everything it
    251 covers unpaywalled.  Transient backend outages are the service manager's
    252 job to ride out -- the shipped `paivana-httpd.service` restarts with an
    253 increasing back-off, and does not restart on a configuration error.
    254 
    255 An order-status request that fails before receiving any HTTP response is
    256 retried once inside the original five-second deadline.  If that retry also
    257 fails early, Paivana's sampled warning is followed by one diagnostic GET for
    258 the same order with a forced fresh connection.  Its log record reports whether
    259 an HTTP status was completed or merely observed before a transfer error, the
    260 new-connection count, local and remote addresses, OS errno, HTTP version and
    261 DNS/TCP/TLS/first-byte/total timings.  The diagnostic uses the existing
    262 authenticated merchant context but never logs its bearer token or response
    263 body, and its result does not change the error returned to the client.  A real
    264 HTTP status from the forced-fresh request, after both shared-context attempts
    265 failed, is strong evidence for a connection-pool/reuse problem rather than a
    266 merchant application outage.
    267 
    268 An instance that offers no template at all is refused for the same
    269 reason: with nothing to sell, no URL ever matches a paywall and the
    270 entire site would be served for free without a word of warning.  Serving
    271 a site without a paywall is what `-n` is for, and it has to be asked for.
    272 
    273 
    274 Deployment behind a reverse proxy
    275 ----------------------------------
    276 
    277 The recommended production setup runs Paivana over a Unix socket and
    278 places nginx or Apache in front for TLS termination.
    279 
    280 In that setup Paivana **must** be started with `-f` /
    281 `--respect-forwarded-headers`.  A Unix-domain peer has no address of
    282 its own, so without `-f` there is no client address at all: the access
    283 cookie cannot be bound to a client, and `POST /.well-known/paivana`
    284 fails.  `-f` makes Paivana take the client address from the forwarding
    285 headers instead, and forward the chain it was given to the upstream
    286 rather than replacing it.
    287 
    288 Paivana reads both the RFC 7239 `Forwarded` header and the de-facto
    289 `X-Forwarded-*` ones, preferring `Forwarded` where both are present,
    290 and emits both upstream — the standardized one for origins that speak
    291 it, the de-facto ones for the many that do not.  Of the `X-Forwarded-*`
    292 family it emits `-For`, `-Proto`, `-Host` and `-Port`; the port is
    293 taken from the front end's `X-Forwarded-Port` under `-f`, and otherwise
    294 from the authority in `Host` when that names one.
    295 
    296 What the origin sees as `Host` is **not** what the client sent: it is
    297 the authority of `DESTINATION_BASE_URL`, because that is the name
    298 Paivana connects to.  The client's own value survives as
    299 `X-Forwarded-Host` (and as the `host` parameter of `Forwarded`).  An
    300 origin doing virtual hosting must therefore be configured for the
    301 `DESTINATION_BASE_URL` authority, and an origin that generates absolute
    302 URLs should be told to build them from `X-Forwarded-Host` /
    303 `X-Forwarded-Proto` / `X-Forwarded-Port`.  If it builds them from
    304 `Host` instead, its `Location` values will name Paivana's view of the
    305 origin — an internal host and port, which Paivana relays unchanged.
    306 That leaks the internal name, and points the client straight at the
    307 origin wherever the client can route to it, bypassing the paywall.
    308 
    309 `-f` is only safe if the server in front **writes** the forwarding
    310 headers itself, whether by overwriting them or by appending its own
    311 element.  What it must not do is pass the client's copies through
    312 untouched: Paivana believes the rightmost element (see "Trusted
    313 proxies"), so a header no hop of yours has written is a header the
    314 client filled in, and the client then chooses the identity its access
    315 cookie is bound to.  The configurations below get this right; if you
    316 write your own, note that nginx sets `X-Forwarded-For` only when told
    317 to and forwards a client-supplied `Forwarded` verbatim — and
    318 `Forwarded` is the one Paivana prefers.
    319 
    320 Conversely, do not pass `-f` to a Paivana that clients can reach
    321 directly — there it is the client, not a proxy, that is setting those
    322 headers.
    323 
    324 nginx (`/etc/nginx/sites-available/paivana`):
    325 
    326 An optional per-client concurrency limit belongs here rather than in
    327 Paivana when a Unix socket is used.  The shipped example contains commented
    328 `limit_conn_zone` / `limit_conn` directives.  Its value is a site policy,
    329 not part of Paivana's descriptor calculation: a low value also combines all
    330 legitimate users behind the same NAT, and nginx behind another proxy must
    331 first be configured to trust and recover the real client address.
    332 
    333     server {
    334         listen 443 ssl;
    335         server_name example.com;
    336 
    337         location / {
    338             proxy_pass http://unix:/run/paivana/httpd/paivana-http.sock;
    339             proxy_set_header Host $host;
    340 
    341             # $remote_addr, not $proxy_add_x_forwarded_for: this is
    342             # the outermost hop, so these overwrite rather than
    343             # extend what the client claimed.
    344             proxy_set_header X-Forwarded-For   $remote_addr;
    345             proxy_set_header X-Forwarded-Proto $scheme;
    346             proxy_set_header X-Forwarded-Host  $host;
    347             proxy_set_header X-Forwarded-Port  $server_port;
    348 
    349             # RFC 7239; preferred by Paivana over the above.  The
    350             # element is built by a `map` — see the shipped config.
    351             proxy_set_header Forwarded \
    352                 "$paivana_forwarded_elem;proto=$scheme;host=$host";
    353         }
    354     }
    355 
    356 Apache (requires mod_proxy, mod_proxy_http and mod_headers):
    357 
    358     <Location "/">
    359         # mod_proxy appends the real client to any X-Forwarded-For the
    360         # client itself sent, so drop the client's copies first.
    361         RequestHeader unset X-Forwarded-For
    362         RequestHeader unset X-Forwarded-Proto
    363         RequestHeader unset X-Forwarded-Host
    364         RequestHeader unset X-Forwarded-Port
    365         RequestHeader unset Forwarded
    366 
    367         # RFC 7239; Apache emits none of its own.  It has to be an
    368         # expr= value: %{...}e reads the CGI environment, which is not
    369         # populated when mod_headers runs, so the %{REMOTE_ADDR}e form
    370         # yields the literal string "(null)".
    371         RequestHeader set Forwarded \
    372             "expr=for=%{REMOTE_ADDR};proto=%{REQUEST_SCHEME};host=%{HTTP_HOST}"
    373 
    374         ProxyPass "unix:/run/paivana/httpd/paivana-http.sock|http://example.com/"
    375     </Location>
    376 
    377 Ready-made versions of both are shipped in `debian/examples/`, and
    378 installed by the Debian package into
    379 `/usr/share/doc/paivana-httpd/examples/`.  They are examples rather
    380 than drop-ins on purpose: the package only *recommends* a web server,
    381 so it must not create `/etc/nginx/` or `/etc/apache2/` on a system
    382 that has neither (Debian Policy 9.1.1).  Copy the one you want into
    383 place and enable it yourself.
    384 
    385 
    386 Trusted proxies
    387 ---------------
    388 
    389 `-f` on its own extends trust exactly one hop.  The chain is walked
    390 from the right and the walk stops at once, so the client is the
    391 *rightmost* element — the one the peer we accepted the connection from
    392 wrote.  Entries a client prepends to its own header sit to the left of
    393 that and cannot be promoted.
    394 
    395 What `-f` alone therefore rests on is that the server in front sets or
    396 appends those headers itself.  Appending is safe here, because the real
    397 peer ends up rightmost; passing the client's own headers through
    398 unchanged is not, and that is the failure to watch for, since nginx
    399 forwards a client-supplied `Forwarded` verbatim and Paivana prefers
    400 `Forwarded`.
    401 
    402 `TRUSTED_PROXIES` and `TRUSTED_PROXIES6` are what let the walk step
    403 further left, through hops you have listed, when there is more than one
    404 proxy in front:
    405 
    406     [paivana]
    407     TRUSTED_PROXIES  = 10.0.0.0/8;192.168.0.0/16;
    408     TRUSTED_PROXIES6 = 2001:db8::/32;
    409 
    410 The walk then steps over each listed proxy in turn; the first element
    411 that is not one of them is the client.  Each step leftwards is
    412 permitted only by the node being stepped over, so an element written by
    413 someone you did not list is as far back as the chain can be believed.
    414 
    415 Two things this does *not* do.  The address that connected is never
    416 matched against these lists — `-f` is what says the peer may speak for
    417 a client, and the lists only govern how far past it the walk may go.
    418 And an element that names no address (RFC 7239 `unknown`, an obfuscated
    419 identifier, a host name) is not skipped either: the walk stops there
    420 and Paivana falls back to the socket peer, which under `SERVE = unix`
    421 means no client address at all and a `POST /.well-known/paivana` that
    422 fails.  A front server that emits a `Forwarded` element it cannot fill
    423 in is therefore not a degraded paywall but a broken one.
    424 
    425 `X-Forwarded-Proto`, `-Host` and `-Port` are outside all of this: they
    426 are read as the leftmost value of the first such field line and the
    427 walk never vets them.  Under `-f` with no `BASE_URL` they are what
    428 Paivana rebuilds its own scheme and authority from, which is the
    429 residual reason to care about the front server's configuration even
    430 with `TRUSTED_PROXIES` set.
    431 
    432 Syntax notes, inherited from GNUnet's network-policy parser:
    433 
    434   - entries are separated *and terminated* by `;` — a missing trailing
    435     semicolon means nothing is parsed;
    436   - `TRUSTED_PROXIES6` does not tolerate spaces between entries
    437     (`TRUSTED_PROXIES` does);
    438   - `0.0.0.0/0` and `::/0` cannot be expressed: they are
    439     indistinguishable from the end of the list.
    440 
    441 Anything that parses to an empty list is refused at startup rather
    442 than silently trusting nobody.
    443 
    444 Put IPv4 proxies in `TRUSTED_PROXIES`, not in `TRUSTED_PROXIES6` as
    445 `::ffff:a.b.c.d`: addresses are folded to their IPv4 form before
    446 matching, so a mapped entry would never be hit.
    447 
    448 Set `BASE_URL` in the configuration file to the public HTTPS URL so
    449 that redirects and cookie domains are correct.  It may be omitted only
    450 when `-f` is given: the flag asserts that a reverse proxy in front of
    451 Paivana has already enforced a correct `Host`, which is what makes it
    452 safe to reconstruct our own URL from the request.  Without `-f` the
    453 client is assumed to have connected directly, `Host` is whatever it
    454 chose to send, and `BASE_URL` is therefore mandatory.
    455 
    456 
    457 Source layout
    458 -------------
    459 
    460     src/backend/          Main binary and all subsystems
    461       paivana-httpd.c     Entry point, scheduler, global state, shutdown
    462       paivana-httpd_reverse.c   Request-proxying state machine (core)
    463       paivana-httpd_pay.c       POST /.well-known/paivana handler
    464       paivana-httpd_cookie.c    Access-cookie keyed hash, Paivana ID
    465       paivana-httpd_templates.c Paywall template loading and rendering
    466       paivana-httpd_helper.c    Client IP / base URL helpers
    467       paivana-httpd_daemon.c    MHD daemon startup
    468       paivana_pd.c              GNUnet project-data descriptor
    469     src/frontend/         The paywall page served to unpaid visitors
    470       paywall.en.must.j2  Mustache template source (Jinja2)
    471       paywall.js          Payment identifier, QR code, long poll
    472       generate-paywall.py Renders the Jinja2 source at build time
    473     src/include/platform.h  GNUnet-style platform header (include first)
    474     src/tests/              Automated reverse-proxy and unit tests
    475     doc/prebuilt/           Git submodule: taler-docs (man pages)
    476 
    477 
    478 Architecture notes
    479 ------------------
    480 
    481 Single-threaded event loop: GNUnet scheduler drives both inbound HTTP
    482 (libmicrohttpd) and outbound requests (libgnunetcurl / libcurl multi).
    483 Running multiple `paivana-httpd` processes on the same port is
    484 supported as the main way to scale-up the system.
    485 
    486 Requests and responses are streamed in both directions: each is moved
    487 through a fixed-size ring buffer (REQUEST_BUFFER_MAX and
    488 RESPONSE_BUFFER_MAX, 256 KiB each by default) rather than assembled
    489 whole, so the size of a proxied body is bounded by nothing in Paivana.
    490 When the client cannot keep up, Paivana stops reading from the origin;
    491 when the origin cannot keep up, it stops reading from the client.  The
    492 memory an ordinary in-flight request costs is therefore the two buffers.
    493 Startup checks their product with the ordinary capacity
    494 (`CONNECTION_LIMIT - PAYMENT_CONNECTION_LIMIT`) against
    495 `RELAY_MEMORY_LIMIT`; with the defaults this is 352 * 512 KiB = 176 MiB.
    496 
    497 The process currently uses native `fd_set`s in both the MHD and curl
    498 scheduler integration.  `LimitNOFILE` therefore cannot safely raise
    499 concurrency beyond descriptor 1023.  The default 384-connection budget
    500 charges two descriptors per request and reserves 256 for non-request and
    501 transient use.  Moving to a poll/epoll integration is required before
    502 raising that ceiling.
    503 
    504 An upload is still bounded, by MAX_REQUEST_SIZE (1 MiB by default),
    505 because accepting one is a policy decision rather than a memory
    506 constraint.  A response is not bounded at all: an operator who wants to
    507 bound what their origin serves can do it at the origin.
    508 
    509 The MHD daemon is not started until paywall templates have been fetched
    510 from the merchant backend asynchronously.
    511 
    512 A 1xx interim response is not forwarded — RFC 9110 §15.2 asks a proxy
    513 to forward them, and Paivana instead drops them, because the response
    514 MHD is handed is a single final one.  `103 Early Hints`
    515 therefore does not reach clients through Paivana.  Its header fields
    516 are dropped with it rather than being merged into the final response,
    517 which is the part that would be actively harmful.  Trailer fields are
    518 dropped for the same reason (RFC 9110 §6.5.1 forbids merging them into
    519 the header section).
    520 
    521 An origin that accepts the connection but does not produce response
    522 *headers* within `UPSTREAM_TIMEOUT` (60 s) yields `504 Gateway
    523 Timeout`; one that cannot be reached at all yields `502 Bad Gateway`.
    524 The distinction matters because caches and monitoring retry the former
    525 and not the latter.  That clock is cancelled once the header section
    526 ends, and it is the only one that can still produce a status code:
    527 after it, the status is already on the wire.
    528 
    529 There is deliberately no ceiling on how long a request may take — a
    530 large download legitimately runs for as long as it runs.  What is
    531 bounded instead is a *stall*: `UPSTREAM_STALL_TIMEOUT` (60 s) is how
    532 long the origin may move no bytes in either direction.  The clock does
    533 not run while Paivana is itself holding the origin back because the
    534 client has not drained what has already arrived, so a client on a slow
    535 link is never mistaken for a slow origin.
    536 
    537 The MHD connection timeout does not cover any of this: a connection
    538 waiting on the origin is suspended, and MHD drops suspended connections
    539 from its timeout lists.
    540 
    541 Once the response headers have gone out the status cannot be retracted,
    542 so an origin that fails mid-body can only be reported as a framing
    543 error: a declared `Content-Length` that is not met, or a chunked
    544 response closed without its terminating chunk.  Both are required to be
    545 treated as failures by RFC 9112 §8.1.2.  The exception is an HTTP/1.0
    546 client receiving a response of unknown length, where the close *is* the
    547 framing and truncation is indistinguishable from success.
    548 
    549 `OPTIONS` carrying `Max-Forwards: 0` is answered by Paivana itself with
    550 an `Allow` list, as RFC 9110 §7.6.2 requires of an intermediary; any
    551 larger value is decremented before the request is passed on.
    552 
    553 
    554 License
    555 -------
    556 
    557 GNU Affero General Public License version 3 or later.
    558 See COPYING for the full text.
    559 
    560 
    561 Bug reports
    562 -----------
    563 
    564 Please report bugs at https://bugs.taler.net/.