sandcastle-ng

Scripts for the deployment of Sandcastle (GNU Taler)
Log | Files | Refs | README

commit f246df40dad142eb7cb750f1bebfaf03ee4b6905
parent 02c5af222492b8e4545b9373559e57822767bfcd
Author: Florian Dold <dold@taler.net>
Date:   Mon, 27 Jul 2026 18:52:01 +0200

add paivana paywall

Diffstat:
MDockerfile | 6++++++
MREADME.md | 7+++++++
Abuildconfig/paivana.giturl | 1+
Abuildconfig/paivana.tag | 1+
Mhost/container-taler-sandcastle-demo.service | 1+
Mhost/container-taler-sandcastle-head.service | 1+
Mhost/container-taler-sandcastle-kyctest.service | 1+
Mhost/container-taler-sandcastle-liebe.service | 1+
Mhost/container-taler-sandcastle-test.service | 1+
Msandcastle-run | 3+++
Mscripts/demo/setup-sandcastle.sh | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 206 insertions(+), 0 deletions(-)

diff --git a/Dockerfile b/Dockerfile @@ -113,6 +113,11 @@ FROM donau as taler-merchant COPY buildconfig/taler-merchant.* /buildconfig/ RUN sandcastle-build-generic taler-merchant +# Paivana (needs libtalermerchant from the merchant) +FROM taler-merchant as paivana +COPY buildconfig/paivana.* /buildconfig/ +RUN sandcastle-build-generic paivana + # Challenger FROM base-system as challenger-webui COPY buildconfig/challenger-webui.* /buildconfig/ @@ -195,6 +200,7 @@ COPY --from=taler-merchant-demos /packages/taler-merchant-demos/* /packages/ COPY --from=challenger-webui /packages/challenger-webui/* /packages/ COPY --from=challenger /packages/challenger/* /packages/ COPY --from=donau /packages/donau/* /packages/ +COPY --from=paivana /packages/paivana/* /packages/ COPY --from=turnstile /opt/turnstile /opt/turnstile RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get -y upgrade && \ apt-get install --no-install-recommends -y /packages/*.deb diff --git a/README.md b/README.md @@ -25,6 +25,7 @@ so it is recommended that you set up the following domain names: * `landing.$MYDOMAIN` for the demo landing page * `shop.$MYDOMAIN` for the demo blog shop page * `donations.$MYDOMAIN` for the demo donations page +* `paivana.$MYDOMAIN` for the demo Paivana paywall Your host system should reverse-proxy HTTP(s) traffic to the respective service port for each of the services. @@ -110,6 +111,10 @@ The following variables are typically adjusted in an override file: * ``USE_INSECURE_SANDBOX_PASSWORDS``: If set to `1`, use password `sandbox` for all services instead of random ones. WARNING! Use only when listening to localhost! +* ``PAIVANA_DESTINATION``: Upstream website that the Paivana paywall + protects. Defaults to `https://www.taler.net/`. Note that this is an + *external* server that the container proxies to, it is not served by the + sandcastle itself. # Running the Deployment @@ -144,6 +149,8 @@ the services will be available as - http://exchange.taler.localhost:4321 - http://auditor.taler.localhost:4321 - http://challenger.taler.localhost:4321 + - http://paivana.taler.localhost:4321/en/ - www.taler.net behind a Taler paywall + (use the ``/en/`` entry point, as www.taler.net redirects ``/`` to its own domain) This gives a nice environment for integration testing. diff --git a/buildconfig/paivana.giturl b/buildconfig/paivana.giturl @@ -0,0 +1 @@ +git://git.taler.net/paivana diff --git a/buildconfig/paivana.tag b/buildconfig/paivana.tag @@ -0,0 +1 @@ +v0.0.1 diff --git a/host/container-taler-sandcastle-demo.service b/host/container-taler-sandcastle-demo.service @@ -21,6 +21,7 @@ Environment=SANDCASTLE_PORT_CHALLENGER=127.0.0.1:15003 Environment=SANDCASTLE_PORT_AUDITOR=127.0.0.1:15004 Environment=SANDCASTLE_PORT_DONAU=127.0.0.1:15005 Environment=SANDCASTLE_PORT_DRUPAL=127.0.0.1:15006 +Environment=SANDCASTLE_PORT_PAIVANA=127.0.0.1:15007 Restart=on-failure TimeoutStopSec=70 ExecStart=%h/sandcastle-ng/sandcastle-run diff --git a/host/container-taler-sandcastle-head.service b/host/container-taler-sandcastle-head.service @@ -21,6 +21,7 @@ Environment=SANDCASTLE_PORT_CHALLENGER=127.0.0.1:17010 Environment=SANDCASTLE_PORT_AUDITOR=127.0.0.1:17011 Environment=SANDCASTLE_PORT_DONAU=127.0.0.1:17012 Environment=SANDCASTLE_PORT_DRUPAL=127.0.0.1:17013 +Environment=SANDCASTLE_PORT_PAIVANA=127.0.0.1:17014 Restart=on-failure TimeoutStopSec=70 ExecStart=%h/sandcastle-ng/sandcastle-run diff --git a/host/container-taler-sandcastle-kyctest.service b/host/container-taler-sandcastle-kyctest.service @@ -19,6 +19,7 @@ Environment=SANDCASTLE_PORT_LIBEUFIN_BANK=127.0.0.1:16407 Environment=SANDCASTLE_PORT_BANK_SPA=127.0.0.1:16409 Environment=SANDCASTLE_PORT_CHALLENGER=127.0.0.1:16410 Environment=SANDCASTLE_PORT_AUDITOR=127.0.0.1:16411 +Environment=SANDCASTLE_PORT_PAIVANA=127.0.0.1:16414 Restart=on-failure TimeoutStopSec=70 ExecStart=%h/sandcastle-ng/sandcastle-run diff --git a/host/container-taler-sandcastle-liebe.service b/host/container-taler-sandcastle-liebe.service @@ -17,6 +17,7 @@ Environment=SANDCASTLE_PORT_PROVISION=127.0.0.1:26004 Environment=SANDCASTLE_PORT_LANDING=127.0.0.1:26005 Environment=SANDCASTLE_PORT_LIBEUFIN_BANK=127.0.0.1:26007 Environment=SANDCASTLE_PORT_BANK_SPA=127.0.0.1:26009 +Environment=SANDCASTLE_PORT_PAIVANA=127.0.0.1:26014 Restart=on-failure TimeoutStopSec=70 ExecStart=%h/sandcastle-ng/sandcastle-run diff --git a/host/container-taler-sandcastle-test.service b/host/container-taler-sandcastle-test.service @@ -21,6 +21,7 @@ Environment=SANDCASTLE_PORT_CHALLENGER=127.0.0.1:16010 Environment=SANDCASTLE_PORT_AUDITOR=127.0.0.1:16011 Environment=SANDCASTLE_PORT_DONAU=127.0.0.1:16012 Environment=SANDCASTLE_PORT_DRUPAL=127.0.0.1:16013 +Environment=SANDCASTLE_PORT_PAIVANA=127.0.0.1:16014 Restart=on-failure TimeoutStopSec=70 ExecStart=%h/sandcastle-ng/sandcastle-run diff --git a/sandcastle-run b/sandcastle-run @@ -17,6 +17,7 @@ SANDCASTLE_PORT_CHALLENGER=${SANDCASTLE_PORT_CHALLENGER:-127.0.0.1:16010} SANDCASTLE_PORT_AUDITOR=${SANDCASTLE_PORT_AUDITOR:-127.0.0.1:16011} SANDCASTLE_PORT_DONAU=${SANDCASTLE_PORT_DONAU:-127.0.0.1:16012} SANDCASTLE_PORT_DRUPAL=${SANDCASTLE_PORT_DRUPAL:-127.0.0.1:16013} +SANDCASTLE_PORT_PAIVANA=${SANDCASTLE_PORT_PAIVANA:-127.0.0.1:16014} # Container-internal ports, should by synced with scripts/setup-sandcastle.sh PORT_INTERNAL_EXCHANGE=8201 @@ -31,6 +32,7 @@ PORT_INTERNAL_CHALLENGER=8506 PORT_INTERNAL_AUDITOR=8507 PORT_INTERNAL_DONAU=8508 PORT_INTERNAL_DRUPAL=8509 +PORT_INTERNAL_PAIVANA=8510 SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) cd $SCRIPT_DIR @@ -92,6 +94,7 @@ exec podman run \ -p=$SANDCASTLE_PORT_AUDITOR:$PORT_INTERNAL_AUDITOR \ -p=$SANDCASTLE_PORT_DONAU:$PORT_INTERNAL_DONAU \ -p=$SANDCASTLE_PORT_DRUPAL:$PORT_INTERNAL_DRUPAL \ + -p=$SANDCASTLE_PORT_PAIVANA:$PORT_INTERNAL_PAIVANA \ --privileged \ --name taler-sandcastle \ --systemd=always \ diff --git a/scripts/demo/setup-sandcastle.sh b/scripts/demo/setup-sandcastle.sh @@ -77,6 +77,7 @@ if [[ $WIRE_METHOD = iban ]]; then MERCHANT_IBAN_TOR=DE2648777 MERCHANT_IBAN_SANDBOX=DE949115029592 MERCHANT_IBAN_UMAMI=DE358841382499 + MERCHANT_IBAN_PAIVANA=DE5183926 MERCHANT_IBAN_TESTING=DE38936850270207298566 MERCHANT_PAYTO_ADMIN="payto://iban/$MERCHANT_IBAN_ADMIN?receiver-name=Default+Merchant" @@ -87,6 +88,7 @@ if [[ $WIRE_METHOD = iban ]]; then MERCHANT_PAYTO_TALER="payto://iban/$MERCHANT_IBAN_TALER?receiver-name=Taler+Merchant" MERCHANT_PAYTO_TOR="payto://iban/$MERCHANT_IBAN_TOR?receiver-name=Tor+Merchant" MERCHANT_PAYTO_UMAMI="payto://iban/$MERCHANT_IBAN_UMAMI?receiver-name=Umami" + MERCHANT_PAYTO_PAIVANA="payto://iban/$MERCHANT_IBAN_PAIVANA?receiver-name=Paivana+Merchant" MERCHANT_PAYTO_SANDBOX="payto://iban/$MERCHANT_IBAN_SANDBOX?receiver-name=Sandbox+Merchant" elif [[ $WIRE_METHOD = x-taler-bank ]]; then XTBHOST=sandcastle @@ -99,6 +101,7 @@ elif [[ $WIRE_METHOD = x-taler-bank ]]; then MERCHANT_PAYTO_TALER="payto://x-taler-bank/$XTBHOST/merchant-taler?receiver-name=Taler+Merchant" MERCHANT_PAYTO_TOR="payto://x-taler-bank/$XTBHOST/merchant-tor?receiver-name=Tor+Merchant" MERCHANT_PAYTO_UMAMI="payto://x-taler-bank/$XTBHOST/merchant-umami?receiver-name=Umami" + MERCHANT_PAYTO_PAIVANA="payto://x-taler-bank/$XTBHOST/merchant-paivana?receiver-name=Paivana+Merchant" MERCHANT_PAYTO_SANDBOX="payto://x-taler-bank/$XTBHOST/merchant-sandbox?receiver-name=Sandbox+Merchant" else echo "wire method $WIRE_METHOD not supported" @@ -116,6 +119,12 @@ DONATIONS_DOMAIN=donations.$MYDOMAIN DRUPAL_DOMAIN=drupal.$MYDOMAIN CHALLENGER_DOMAIN=challenger.$MYDOMAIN AUDITOR_DOMAIN=auditor.$MYDOMAIN +PAIVANA_DOMAIN=paivana.$MYDOMAIN + +# Website that is put behind the Paivana paywall. +# Note that this is an *upstream* server, it is not +# served by the sandcastle itself. +PAIVANA_DESTINATION=${PAIVANA_DESTINATION:-https://www.taler.net/} # Ports of the services running inside the container. # Should be synchronized with the sandcastle-run script. @@ -131,6 +140,7 @@ PORT_INTERNAL_CHALLENGER=8506 PORT_INTERNAL_AUDITOR=8507 PORT_INTERNAL_DONAU=8508 PORT_INTERNAL_DRUPAL=8509 +PORT_INTERNAL_PAIVANA=8510 ENABLE_AUDITOR=0 @@ -147,6 +157,8 @@ systemctl stop taler-demo-blog.service systemctl stop taler-demo-donations.service systemctl stop libeufin-bank.service systemctl stop donau-httpd.service +systemctl stop paivana-httpd.service +systemctl stop paivana-httpd.socket # libeufin-nexus is not used systemctl stop libeufin-nexus-ebics-fetch.service @@ -250,6 +262,54 @@ AUTH_COMMAND = /data/sandcastle-challenger-auth ADDRESS_RESTRICTIONS = {"email":{"hint":"not an e-mail address","regex":"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$"}} EOF +mkdir -p /etc/paivana +cat <<EOF >/etc/paivana/paivana.conf +[paivana] +# Serve on the socket provided by paivana-httpd.socket. +SERVE = systemd + +# Public base URL of the paywall. +BASE_URL = $PROTO://$PAIVANA_DOMAIN$PORT_SUFFIX/ + +# Upstream site that is protected by the paywall. +DESTINATION_BASE_URL = $PAIVANA_DESTINATION + +# Merchant instance that sells access to the site. +MERCHANT_BACKEND_URL = $PROTO://$MERCHANT_DOMAIN$PORT_SUFFIX/instances/paivana/ +MERCHANT_ACCESS_TOKEN = secret-token:$(get_credential_pw merchant/paivana) + +# Stable key for the access cookie MAC, so that paid clients +# survive a restart of paivana-httpd. +SECRET = $(get_credential_pw paivana/secret) + +# Static resources are served without payment, as the +# protected site is otherwise unusable after paying. +WHITELIST = ^/(favicon\.ico|robots\.txt|.*\.(css|js|png|jpe?g|gif|svg|ico|webp|woff2?|ttf|eot))\$ +EOF + +# The config contains the merchant access token and the cookie secret. +chown paivana-httpd:root /etc/paivana/paivana.conf +chmod 640 /etc/paivana/paivana.conf + +# The paywall runs behind the caddy reverse proxy, so we need +# '-f' to get the client address from X-Forwarded-For. With '-g' +# a single payment unlocks the entire site instead of just the +# URL that was paid for. +mkdir -p /etc/systemd/system/paivana-httpd.service.d +cat <<EOF >/etc/systemd/system/paivana-httpd.service.d/sandcastle.conf +[Unit] +# Paywall templates are fetched from the merchant backend on +# startup, so keep retrying until the merchant is up. +After=taler-merchant-httpd.service +StartLimitIntervalSec=0 + +[Service] +ExecStart= +ExecStart=/usr/bin/paivana-httpd -c /etc/paivana/paivana.conf -f -g -L INFO +EOF + +systemctl daemon-reload + cat <<EOF >/etc/libeufin/libeufin-bank.conf [libeufin-bank] BASE_URL = $PROTO://$BANK_DOMAIN$PORT_SUFFIX/ @@ -635,6 +695,10 @@ cat <<EOF >/etc/caddy/Caddyfile reverse_proxy unix//run/taler-auditor/httpd/auditor-http.sock } +:$PORT_INTERNAL_PAIVANA { + reverse_proxy unix//run/paivana/httpd/paivana-http.sock +} + :$PORT_INTERNAL_CHALLENGER { handle { reverse_proxy unix//run/challenger/httpd/challenger.http { @@ -701,6 +765,11 @@ https://$CHALLENGER_DOMAIN { reverse_proxy unix//run/challenger/httpd/challenger.http } +https://$PAIVANA_DOMAIN { + tls internal + reverse_proxy unix//run/paivana/httpd/paivana-http.sock +} + EOF else @@ -752,6 +821,10 @@ http://$DRUPAL_DOMAIN$PORT_SUFFIX { reverse_proxy :$PORT_INTERNAL_DRUPAL } +http://$PAIVANA_DOMAIN$PORT_SUFFIX { + reverse_proxy unix//run/paivana/httpd/paivana-http.sock +} + EOF fi @@ -766,6 +839,7 @@ cat <<EOF >>/etc/hosts 127.0.0.1 $DONATIONS_DOMAIN 127.0.0.1 $CHALLENGER_DOMAIN 127.0.0.1 $DRUPAL_DOMAIN +127.0.0.1 $PAIVANA_DOMAIN # End of Taler Sandcastle Domains EOF @@ -856,6 +930,13 @@ taler-harness deployment provision-bank-account "${BANK_BASEURL}" \ --name "Umami Merchant" \ --password $(get_credential_pw bank/merchant-umami) +sudo -u libeufin-bank libeufin-bank passwd merchant-paivana $(get_credential_pw bank/merchant-paivana) || true +taler-harness deployment provision-bank-account "${BANK_BASEURL}" \ + --login merchant-paivana --public \ + --payto "$MERCHANT_PAYTO_PAIVANA" \ + --name "Paivana Merchant" \ + --password $(get_credential_pw bank/merchant-paivana) + # Special bank account without a secure password sudo -u libeufin-bank libeufin-bank passwd merchant-sandbox sandbox || true taler-harness deployment provision-bank-account "${BANK_BASEURL}" \ @@ -1059,6 +1140,18 @@ if [[ $instance_missing = yes ]]; then --payto "$MERCHANT_PAYTO_UMAMI" fi +instance_missing=no +reset_merchant_pw paivana || instance_missing=yes +if [[ $instance_missing = yes ]]; then + taler-harness deployment provision-merchant-instance \ + ${MERCHANT_BASEURL} \ + --management-token $ADMIN_TOK \ + --instance-password $(get_credential_pw merchant/paivana) \ + --name "Paivana Merchant" \ + --id paivana \ + --payto "$MERCHANT_PAYTO_PAIVANA" +fi + # Special instance with fixed "sandbox" password sudo -u taler-merchant-httpd taler-merchant-passwd --instance sandbox sandbox || true taler-harness deployment provision-merchant-instance \ @@ -1099,6 +1192,96 @@ for lang in "${langs[@]}"; do done +# Set up the Paivana paywall. +# The paywall itself is configured entirely via merchant templates, +# see /etc/paivana/paivana.conf for the daemon configuration. + +PAIVANA_INSTANCE_URL=${MERCHANT_BASEURL}instances/paivana/ +PAIVANA_TOKEN=secret-token:$(get_credential_pw merchant/paivana) + +# Subscription offered as an alternative to paying per visit. +# Reuses the validity settings of the blog subscriptions above. +curl -X POST "${PAIVANA_INSTANCE_URL}private/tokenfamilies" \ + -H "Authorization: Bearer $PAIVANA_TOKEN" \ + -H "Content-Type: application/json" \ + --data-raw "{ + \"kind\": \"subscription\", + \"slug\": \"paivana_abo\", + \"name\": \"One month of access\", + \"description\": \"One month of access to $PAIVANA_DESTINATION\", + \"valid_before\": { \"t_s\": ${valid_before_ts} }, + \"duration\": { \"d_us\": ${duration_us} }, + \"validity_granularity\": { \"d_us\": ${validity_granularity_us} } +}" + +# paivana-httpd learns which URLs cost how much from the templates +# of its merchant instance. A single template covering the whole +# site is enough here, as paivana-httpd runs with '-g'. +paivana_template=$(cat <<EOF +{ + "template_id": "paivana", + "template_description": "Paywall for $PAIVANA_DESTINATION", + "template_contract": { + "template_type": "paivana", + "summary": "Access to $PAIVANA_DESTINATION", + "website_regex": ".*", + "max_pickup_duration": { "d_us": $((24 * 60 * 60 * 1000000)) }, + "choices": [ + { + "amount": "$CURRENCY:1", + "description": "Pay for one day of access" + }, + { + "amount": "$CURRENCY:10", + "description": "Buy a one month subscription", + "outputs": [ { "type": "token", "token_family_slug": "paivana_abo" } ] + }, + { + "amount": "$CURRENCY:0", + "description": "Use my subscription", + "inputs": [ { "type": "token", "token_family_slug": "paivana_abo" } ], + "outputs": [ { "type": "token", "token_family_slug": "paivana_abo" } ] + } + ] + } +} +EOF +) + +paivana_response=$(mktemp) +paivana_status=$(curl -X POST "${PAIVANA_INSTANCE_URL}private/templates" \ + -H "Authorization: Bearer $PAIVANA_TOKEN" \ + -H "Content-Type: application/json" \ + --data-raw "$paivana_template" \ + -s -o "$paivana_response" -w '%{http_code}') + +case "$paivana_status" in + 204) + ;; + 409) + # Template exists already, update it so that changes take effect. + paivana_status=$(curl -X PATCH "${PAIVANA_INSTANCE_URL}private/templates/paivana" \ + -H "Authorization: Bearer $PAIVANA_TOKEN" \ + -H "Content-Type: application/json" \ + --data-raw "$(jq 'del(.template_id)' <<<"$paivana_template")" \ + -s -o "$paivana_response" -w '%{http_code}') + if [[ $paivana_status != 204 ]]; then + echo "Failed to update Paivana template (HTTP $paivana_status)" >&2 + cat "$paivana_response" >&2 + exit 1 + fi + ;; + *) + echo "Failed to create Paivana template (HTTP $paivana_status)" >&2 + cat "$paivana_response" >&2 + exit 1 + ;; +esac +rm -f "$paivana_response" + +systemctl enable --now paivana-httpd.socket +systemctl enable --now paivana-httpd.service + # Set up Donau