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/.