commit f246df40dad142eb7cb750f1bebfaf03ee4b6905
parent 02c5af222492b8e4545b9373559e57822767bfcd
Author: Florian Dold <dold@taler.net>
Date: Mon, 27 Jul 2026 18:52:01 +0200
add paivana paywall
Diffstat:
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