diff --git a/configs/taler-merchant/README.md b/configs/taler-merchant/README.md new file mode 100644 index 0000000..a5d7caa --- /dev/null +++ b/configs/taler-merchant/README.md @@ -0,0 +1,63 @@ +# taler-merchant (container `taler-hacktivism`) + +Image: `localhost/taler-hacktivism-live:9010` +Publish: **9010/tcp only** +Labels: `org.hacktivism.service=taler-merchant`, `host_port=9010`, `site=taler.hacktivism.ch` + +## Config layout (same pattern as exchange) + +| Path in container | Mirror in this dir | +|-------------------|--------------------| +| `taler-merchant.conf` | includes documented in `taler-merchant.conf.includes.txt` | +| `overrides.conf` | tooling — do not edit (not mirrored) | +| **`merchant-overrides.conf`** | **`merchant-overrides.conf`** | +| `conf.d/merchant.conf` | `conf.d-merchant.conf` | +| nginx `sites-available/taler-merchant` | `nginx-taler-merchant.conf` | + +Secrets (`secrets/*.secret.conf`, SMTP password) are **not** mirrored. + +## Manual start (merchant model) + +| Path | User | Role | +|------|------|------| +| `/root/start_base_services_for_taler.sh` | **root** | logs, SMS env, postgres → shell as `taler-merchant-httpd` | +| `/usr/local/bin/start_merchant.sh` | **taler-merchant-httpd** | start / `--restart` httpd + helpers | +| `/usr/local/bin/check_merchant-health.sh` | any | health (socket + nginx) | + +Also under `/usr/local/bin/`: email/SMS helpers, `stats--merchant-payments.sh`. +Certbot loop: `/root/scripts/certbot_renew.sh` (started from base script). + +Git scripts: `scripts/taler-merchant/`. + +## Site settings (overrides) + +- **Default currency:** CHF (`[taler]` / `[merchant]`) +- **Exchanges (multi-currency):** + - CHF → `exchange.taler-ops.ch` (package default `[merchant-exchange-chf]` in `tops.conf` — do not redeclare as `merchant-exchange-tops`) + - GOA → `exchange.hacktivism.ch` (`[merchant-exchange-goa]` in `merchant-overrides.conf`) +- **SERVE:** unix socket → nginx :9010 SSL → Caddy :9001 +- **Self-provisioning:** YES; **no mandatory TAN** (SMS off) +- **Terms:** `TERMS_ETAG = merchant-tos-dual-v0` — short dual-currency notice (GOA explorational + CHF real). Sources in `terms-src/`; install with `scripts/taler-merchant/install_dual_terms.sh` inside the container, then restart `taler-merchant-httpd`. Public: `https://taler.hacktivism.ch/terms` + +Wire status is **per account × exchange**: an `x-taler-bank` GOA payto shows `ready` for hacktivism and `unsupported` for taler-ops (expected). CHF/IBAN accounts use taler-ops. + +### Container → public exchange (pasta) + +Inside pasta, `exchange.hacktivism.ch` must not resolve to **127.0.0.1** (merchant nginx self-signed). Pin public A in container `/etc/hosts`: + +``` +212.51.151.254 exchange.hacktivism.ch bank.hacktivism.ch taler.hacktivism.ch +``` + +After conf/hosts changes: `taler-merchant-exchangekeyupdate -t` and `taler-merchant-kyccheck -t`.## Ports + +| Port | Role | +|------|------| +| 9010 | nginx TLS in container (podman) | +| unix sock | `taler-merchant-httpd` | + +## Demo instance (2026-07-09) + +Self-provisioned shop **`goa-demo-cp4zqk`**, bank account same username, payto linked, seed **GOA:5000**. + +Details + credential paths: **`demo-instance-goa-demo-cp4zqk.md`**. diff --git a/configs/taler-merchant/conf.d-merchant.conf b/configs/taler-merchant/conf.d-merchant.conf new file mode 100644 index 0000000..e0fd7ef --- /dev/null +++ b/configs/taler-merchant/conf.d-merchant.conf @@ -0,0 +1,7 @@ +# Package drop-in: /etc/taler-merchant/conf.d/merchant.conf +# Read secret sections into configuration, but only +# if we have permission to do so. +@inline-secret@ merchantdb-postgres ../secrets/merchant-db.secret.conf + +[merchant] +SERVE = systemd diff --git a/configs/taler-merchant/merchant-overrides.conf b/configs/taler-merchant/merchant-overrides.conf new file mode 100644 index 0000000..164eeac --- /dev/null +++ b/configs/taler-merchant/merchant-overrides.conf @@ -0,0 +1,71 @@ +# Site overrides for taler.hacktivism.ch (container taler-hacktivism). +# Multi-currency: CHF (taler-ops) + GOA (local exchange). +# Do not edit overrides.conf (tooling). + +[taler] +CURRENCY = CHF + +### Disable package demonstrator KUDOS only +[merchant-exchange-kudos] +DISABLED = YES + +[currency-KUDOS] +ENABLED = NO + +[merchant] +CURRENCY = CHF +DATABASE = postgres +# Dual-currency short notice (GOA explorational + CHF real). +# Install: scripts/taler-merchant/install_dual_terms.sh (inside container). +# Bump etag when text changes so wallets/browsers drop long-lived /terms cache. +TERMS_ETAG = merchant-tos-dual-v2 +TERMS_DIR = ${TALER_DATA_HOME}terms/ +# Swiss FADP privacy (install: scripts/taler-merchant/install_swiss_privacy.sh) +PRIVACY_ETAG = merchant-pp-swiss-v0 +PRIVACY_DIR = ${TALER_DATA_HOME}terms/ + +UNIXPATH = /var/run/taler-merchant/httpd/merchant-http.sock +UNIXPATH_MODE = 660 +SERVE = unix + +DEFAULT_PERSONA = point-of-sale + +# Demo short deadlines (package: pay 1d, refund 15d, wire 1w). +# Wire happens after refund deadline; refund/pay windows also kept short. +DEFAULT_PAY_DELAY = 1 minute +DEFAULT_REFUND_DELAY = 30 s +DEFAULT_WIRE_TRANSFER_DELAY = 30 s + +ENABLE_SELF_PROVISIONING = YES +# No mandatory TAN (SMS auth off) +MANDATORY_TAN_CHANNELS = +HELPER_EMAIL = /usr/local/bin/taler-hacktivism-email-helper.sh +# HELPER_SMS = /usr/local/bin/taler-hacktivism-sms-helper-wrapper.sh + +[merchantdb-postgres] +CONFIG = postgres:///taler-merchant + +### CHF — package default in /usr/share/taler-merchant/config.d/tops.conf +### section [merchant-exchange-chf] → https://exchange.taler-ops.ch/ +### Do NOT redeclare the same EXCHANGE_BASE_URL under another section name +### (exchangekeyupdate: "configured in multiple sections"). +### Ensure package CHF stays enabled (only disable KUDOS above). + +### GOA — local exchange.hacktivism.ch (x-taler-bank / bank.hacktivism.ch) +[merchant-exchange-goa] +EXCHANGE_BASE_URL = https://exchange.hacktivism.ch/ +MASTER_KEY = TW6K5FXF81VYCAH0YWYX0SX98KBBSJ42VX27WAX01FTFH400QG10 +CURRENCY = GOA + +[currency-goa] +ENABLED = YES +name = "GOA exploration currency" +code = GOA +fractional_input_digits = 8 +fractional_normal_digits = 0 +fractional_trailing_zero_digits = 0 +# Keep in sync with exchange-overrides [currency-goa] (wallet compares unit maps). +# Scale keys only in [-8, 24] (exchange rejects outside that range). +alt_unit_names_are_symbols = NO +alt_unit_names = {"24":"Yotta-GOA","21":"Zetta-GOA","18":"Exa-GOA","15":"Peta-GOA","12":"Tera-GOA","9":"Giga-GOA","6":"Mega-GOA","3":"Kilo-GOA","0":"GOA","-1":"Deci-GOA","-2":"Centi-GOA","-3":"Milli-GOA","-6":"Micro-GOA","-7":"Deci-Micro-GOA","-8":"Atomic-GOA"} +common_amounts = "GOA:10 GOA:20 GOA:50 GOA:100 GOA:200 GOA:1000" diff --git a/configs/taler-merchant/nginx-taler-merchant.conf b/configs/taler-merchant/nginx-taler-merchant.conf new file mode 100644 index 0000000..73e6cbc --- /dev/null +++ b/configs/taler-merchant/nginx-taler-merchant.conf @@ -0,0 +1,50 @@ +# Container: /etc/nginx/sites-available/taler-merchant +# Enabled: sites-enabled/taler-merchant → this file +# Host publish: 9010/tcp (podman pasta). Caddy terminates public HTTPS and +# reverse_proxies to https://127.0.0.1:9010 (see configs/caddy/Caddyfile). + +server { + listen 9010 ssl; + listen [::]:9010 ssl; + + server_name taler.hacktivism.ch; + + ssl_certificate /etc/letsencrypt/live/taler.hacktivism.ch/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/taler.hacktivism.ch/privkey.pem; + + access_log /var/log/nginx/merchant.log; + error_log /var/log/nginx/merchant.err; + + # Public landing first (Caddy should also redir / → /intro/; this covers :9010) + location = / { + return 302 /intro/; + } + + # Merchant-httpd only serves /terms and /privacy without trailing slash + location = /terms/ { + return 302 /terms; + } + location = /privacy/ { + return 302 /privacy; + } + + location / { + proxy_pass http://unix:/var/run/taler-merchant/httpd/merchant-http.sock; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host "taler.hacktivism.ch"; + proxy_set_header X-Forwarded-Proto "https"; + } +} + +server { + # ACME HTTP-01 is handled by Caddy :9000; this block kept for reference. + # listen 9010; + # listen [::]:9010; + + server_name taler.hacktivism.ch; + + location /.well-known/acme-challenge/ { + alias /var/www/letsencrypt/.well-known/acme-challenge/; + } +} diff --git a/configs/taler-merchant/taler-merchant.conf.includes.txt b/configs/taler-merchant/taler-merchant.conf.includes.txt new file mode 100644 index 0000000..367b443 --- /dev/null +++ b/configs/taler-merchant/taler-merchant.conf.includes.txt @@ -0,0 +1,5 @@ +# From /etc/taler-merchant/taler-merchant.conf (include order) +@inline-matching@ conf.d/*.conf +@inline@ overrides.conf +# Site overrides last (glob; file is merchant-overrides.conf) +@inline-matching@ merchant-overrides.conf diff --git a/scripts/taler-merchant/README.md b/scripts/taler-merchant/README.md new file mode 100644 index 0000000..339d693 --- /dev/null +++ b/scripts/taler-merchant/README.md @@ -0,0 +1,56 @@ +# taler-merchant scripts + +Mirrored from podman `taler-hacktivism`. + +| File | Location in container | User | +|------|----------------------|------| +| `start_base_services_for_taler.sh` | `/root/` | root | +| `start_merchant.sh` | `/usr/local/bin/` | `taler-merchant-httpd` | +| `ensure_merchant_helpers.sh` | `/usr/local/bin/` | root → `taler-merchant-httpd` | +| `setup_credit_facade.sh` | host root (`/root/koopa-admin-log/…`) | root on koopa | +| `check_merchant-health.sh` | `/usr/local/bin/` | any | +| `certbot_renew.sh` | `/root/scripts/` | root (bg from base) | +| `stats--merchant-payments.sh` | `/usr/local/bin/` | ops | +| `taler-hacktivism-email-helper.sh` | `/usr/local/bin/` | merchant (SMTP password via env in git mirror) | +| `taler-hacktivism-sms-helper-wrapper.sh` | `/usr/local/bin/` | merchant | + +### Settlement (wired / transfers) + +Automatic import needs: + +1. **`credit_facade_url`** = `…/accounts/$BANK_USER/taler-revenue/` (not `taler-wire-gateway/`) +2. **Bearer** bank token in `credit_facade_credentials` (Basic fails on history) +3. Running **`taler-merchant-wirewatch`** + **`taler-merchant-depositcheck`** + +```bash +# on koopa as root +./setup_credit_facade.sh +./ensure_merchant_helpers.sh # inside container or via start_merchant +``` + +SMS backends are symlinks into `/var/taler-src/...` (not copied). + +## Usage + +```bash +# root in container +./start_base_services_for_taler.sh +# then as taler-merchant-httpd in /usr/local/bin: +./start_merchant.sh --restart +# nginx (TLS frontend) if not already up — root: +# /etc/init.d/nginx start +./check_merchant-health.sh +``` + +### `check_merchant-health.sh` + +| Check | Severity | +|-------|----------| +| socket + listener + httpd + nginx | FAIL | +| merchant `/config` (unix sock or `:9010`) | FAIL | +| each enabled `[merchant-exchange-*]`: `GET …/keys` | FAIL | +| `MASTER_KEY` matches `master_public_key` in `/keys` | FAIL | +| helpers (webhook, exchangekeyupdate, depositcheck) | WARN | + +Disabled exchanges (`DISABLED = YES`) are skipped. +If public exchange URL is unreachable from the container, falls back to `http://127.0.0.1:9011/keys` (and pasta host IPs). diff --git a/scripts/taler-merchant/certbot_renew.sh b/scripts/taler-merchant/certbot_renew.sh new file mode 100755 index 0000000..f34007f --- /dev/null +++ b/scripts/taler-merchant/certbot_renew.sh @@ -0,0 +1,5 @@ +#!/bin/sh +while true; do + certbot renew --quiet + sleep 12h +done diff --git a/scripts/taler-merchant/check_merchant-health.sh b/scripts/taler-merchant/check_merchant-health.sh new file mode 100755 index 0000000..18dfc24 --- /dev/null +++ b/scripts/taler-merchant/check_merchant-health.sh @@ -0,0 +1,222 @@ +#!/bin/bash +# Health check for manual taler-merchant (container taler-hacktivism). +# Style: check_exchange-health.sh — [OK] / [FAIL] / [WARN], exit 1 on critical fail. +# +# Keys checks: for each enabled [merchant-exchange-*] with EXCHANGE_BASE_URL, +# GET …/keys and (if set) verify MASTER_KEY appears in the response. + +CONF="${TALER_MERCHANT_CONFIG:-/etc/taler-merchant/taler-merchant.conf}" +OVERRIDES=/etc/taler-merchant/merchant-overrides.conf +SOCK="/var/run/taler-merchant/httpd/merchant-http.sock" +SS_BIN=$(command -v ss) +CURL_BIN=$(command -v curl) + +green() { echo -e "\e[32m$1\e[0m"; } +red() { echo -e "\e[31m$1\e[0m"; } +yellow() { echo -e "\e[33m$1\e[0m"; } + +fail=0 +ok() { green "[OK] $1"; } +bad() { red "[FAIL] $1"; fail=1; } +warn() { yellow "[WARN] $1"; } + +is_url() { + case "$1" in + http://*|https://*) return 0 ;; + *) return 1 ;; + esac +} + +# GET /keys; try primary URL then optional host-local fallbacks for same exchange. +# Writes body to $1 (path). Returns 0 on HTTP success with non-empty body. +fetch_keys() { + local out="$1" + local primary="$2" + shift 2 + local url + for url in "$primary" "$@"; do + [ -z "$url" ] && continue + if $CURL_BIN -skf -m 6 "$url" -o "$out" 2>/dev/null \ + || $CURL_BIN -sf -m 6 "$url" -o "$out" 2>/dev/null; then + if [ -s "$out" ] && grep -qE 'master_public_key|"currency"' "$out" 2>/dev/null; then + echo "$url" + return 0 + fi + fi + done + return 1 +} + +echo "=== Taler Merchant Health Check ===" + +# --- 1–4: local service --- +if [ -S "$SOCK" ]; then + ok "socket exists: $SOCK" +else + bad "socket does NOT exist: $SOCK" +fi + +if [ -n "$SS_BIN" ] && $SS_BIN -xl 2>/dev/null | grep -q "$SOCK"; then + ok "Merchant-HTTPD listening on socket" +elif [ -n "$SS_BIN" ]; then + bad "no listener on socket" +else + warn "ss not available — skip socket listener check" +fi + +if pgrep -f taler-merchant-httpd >/dev/null 2>&1; then + ok "process taler-merchant-httpd" +else + bad "process taler-merchant-httpd is NOT running" +fi + +if pgrep -f "nginx: master" >/dev/null 2>&1; then + ok "nginx master process" +else + bad "nginx master process is NOT running" +fi + +# --- 5: merchant /config --- +if [ -n "$CURL_BIN" ]; then + cfg_ok=0 + if [ -S "$SOCK" ] && $CURL_BIN -sf -m 3 --unix-socket "$SOCK" "http://localhost/config" >/dev/null 2>&1; then + ok "merchant /config via unix socket" + cfg_ok=1 + elif $CURL_BIN -skf -m 3 "https://127.0.0.1:9010/config" >/dev/null 2>&1; then + ok "merchant /config via https://127.0.0.1:9010/config" + cfg_ok=1 + elif $CURL_BIN -sf -m 3 "http://127.0.0.1:9010/config" >/dev/null 2>&1; then + ok "merchant /config via http://127.0.0.1:9010/config" + cfg_ok=1 + fi + [ "$cfg_ok" -eq 0 ] && bad "merchant /config unreachable (socket and :9010)" +else + warn "curl missing — skip /config" +fi + +# --- 6: exchange /keys for configured exchanges --- +echo "--- configured exchanges (/keys) ---" + +# Emit lines: SECTION|DISABLED|BASE|MASTER (from overrides + optional taler-config) +list_exchanges() { + # Prefer site overrides file (authoritative for this host) + if [ -f "$OVERRIDES" ]; then + awk ' + BEGIN { sec=""; dis="NO"; base=""; master="" } + /^\[merchant-exchange-/ { + if (sec != "") printf "%s|%s|%s|%s\n", sec, dis, base, master + sec=$0; gsub(/[\[\] \t\r]/, "", sec) + dis="NO"; base=""; master="" + next + } + /^\[/ { + if (sec != "") printf "%s|%s|%s|%s\n", sec, dis, base, master + sec=""; next + } + sec=="" { next } + /^[ \t]*#/ { next } + { + line=$0 + sub(/[ \t]*#.*$/, "", line) + if (match(line, /^[ \t]*DISABLED[ \t]*=[ \t]*/)) { + dis=substr(line, RSTART+RLENGTH); gsub(/^[ \t]+|[ \t]+$/, "", dis) + } else if (match(line, /^[ \t]*EXCHANGE_BASE_URL[ \t]*=[ \t]*/)) { + base=substr(line, RSTART+RLENGTH); gsub(/^[ \t]+|[ \t]+$/, "", base) + } else if (match(line, /^[ \t]*MASTER_KEY[ \t]*=[ \t]*/)) { + master=substr(line, RSTART+RLENGTH); gsub(/^[ \t]+|[ \t]+$/, "", master) + } + } + END { if (sec != "") printf "%s|%s|%s|%s\n", sec, dis, base, master } + ' "$OVERRIDES" + fi +} + +if [ -z "$CURL_BIN" ]; then + bad "curl missing — cannot check exchange /keys" +else + exch_count=0 + while IFS='|' read -r sec dis base master; do + [ -z "$sec" ] && continue + case "${dis:-NO}" in + YES|yes|true|True|1) + warn "exchange $sec: DISABLED — skip /keys" + continue + ;; + esac + if ! is_url "$base"; then + # package stubs without URL (e.g. kudos only DISABLED) + warn "exchange $sec: no EXCHANGE_BASE_URL — skip" + continue + fi + exch_count=$((exch_count + 1)) + base_slash="${base%/}/" + primary="${base_slash}keys" + keys_tmp=$(mktemp 2>/dev/null || echo "/tmp/m-keys-$$-${exch_count}.json") + + # Fallbacks when public name is not reachable from container: + # host loopback ports published by podman (exchange :9011). + got_url= + if got_url=$(fetch_keys "$keys_tmp" "$primary" \ + "http://127.0.0.1:9011/keys" \ + "http://host.containers.internal:9011/keys" \ + "http://10.0.2.2:9011/keys"); then + ok "exchange $sec: /keys reachable ($got_url)" + if [ -n "$master" ]; then + if grep -qF "$master" "$keys_tmp" 2>/dev/null; then + ok "exchange $sec: MASTER_KEY matches /keys" + else + mpks=$(grep -oE '"master_public_key"[[:space:]]*:[[:space:]]*"[^"]+"' "$keys_tmp" 2>/dev/null | head -2) + bad "exchange $sec: MASTER_KEY does not match /keys (config MASTER_KEY=$master)" + [ -n "$mpks" ] && warn " seen in /keys: $mpks" + fi + else + warn "exchange $sec: no MASTER_KEY in config — skip key match" + fi + else + bad "exchange $sec: no /keys from $primary (and local :9011 fallbacks)" + fi + rm -f "$keys_tmp" 2>/dev/null || true + done </dev/null 2>&1 +} + +# Settlement needs wirewatch + depositcheck; others needed for ops. +for p in \ + taler-merchant-webhook \ + taler-merchant-kyccheck \ + taler-merchant-wirewatch \ + taler-merchant-depositcheck \ + taler-merchant-exchangekeyupdate \ + taler-merchant-reconciliation +do + if live_helper "$p"; then + ok "process $p" + else + bad "process $p not running" + fi +done + +if [ "$fail" -eq 0 ]; then + green "=== ALL CRITICAL CHECKS PASSED ===" + exit 0 +fi +red "=== SOME CHECKS FAILED ===" +exit 1 diff --git a/scripts/taler-merchant/start_base_services_for_taler.sh b/scripts/taler-merchant/start_base_services_for_taler.sh new file mode 100755 index 0000000..ff26c8d --- /dev/null +++ b/scripts/taler-merchant/start_base_services_for_taler.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# Root: base services for manual merchant (no systemd). +# Pattern (all three stacks): root base → shell as service user → start_*.sh +# +# Container: taler-hacktivism +# Then as taler-merchant-httpd: /usr/local/bin/start_merchant.sh [--restart] +# +# Usage: +# /root/start_base_services_for_taler.sh # interactive shell as service user +# /root/start_base_services_for_taler.sh --no-shell # base only (automation) + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo "Run as root" >&2 + exit 1 +fi + +NO_SHELL=0 +for arg in "$@"; do + case "$arg" in + --no-shell|-n) NO_SHELL=1 ;; + --help|-h) + echo "Usage: $0 [--no-shell]" + exit 0 + ;; + esac +done + +PWD_BIN="/usr/local/bin" +MERCHANT_STARTER="start_merchant.sh" +LOG_DIR=/var/log/taler-merchant +RUN_DIR=/var/run/taler-merchant/httpd + +# --- Debian postgresql defaults (pg_createcluster layout) --- +ensure_postgresql() { + echo " Debian perms on /etc/postgresql + data/log/run..." + if [ -d /etc/postgresql ]; then + chown -R root:postgres /etc/postgresql + find /etc/postgresql -type d -exec chmod 755 {} \; + find /etc/postgresql -type f -name '*.conf' -exec chmod 640 {} \; + fi + chown -R postgres:postgres /var/lib/postgresql /var/log/postgresql 2>/dev/null || true + mkdir -p /var/run/postgresql + chown postgres:postgres /var/run/postgresql + # Debian package uses setgid sticky on run dir + chmod 2775 /var/run/postgresql 2>/dev/null || chmod 775 /var/run/postgresql + + if pg_isready -q 2>/dev/null; then + echo " already accepting connections" + pg_isready || true + return 0 + fi + + # Stale socket lock only when not accepting + rm -f /var/run/postgresql/.s.PGSQL.*.lock 2>/dev/null || true + if ! pgrep -u postgres -x postgres >/dev/null 2>&1; then + rm -f /var/lib/postgresql/*/main/postmaster.pid 2>/dev/null || true + fi + + if command -v pg_ctlcluster >/dev/null 2>&1 && command -v pg_lsclusters >/dev/null 2>&1; then + while read -r ver name _rest; do + [ -n "$ver" ] || continue + echo " pg_ctlcluster $ver $name start" + pg_ctlcluster "$ver" "$name" start 2>/dev/null || true + done < <(pg_lsclusters --no-header 2>/dev/null || true) + fi + if ! pg_isready -q 2>/dev/null; then + if [ -x /etc/init.d/postgresql ]; then + /etc/init.d/postgresql start || true + else + service postgresql start || true + fi + fi + sleep 1 + pg_isready || true +} + +echo "Start certbot renewal (background)..." +if [ -x /root/scripts/certbot_renew.sh ]; then + /root/scripts/certbot_renew.sh & +elif [ -x ./scripts/certbot_renew.sh ]; then + ./scripts/certbot_renew.sh & +fi + +echo "Create log + runtime dirs... permissions for taler-merchant-httpd / www-data:" +mkdir -p "$LOG_DIR" /var/run/taler-merchant "$RUN_DIR" +chown taler-merchant-httpd: "$LOG_DIR" +chmod 755 "$LOG_DIR" +chown taler-merchant-httpd:www-data /var/run/taler-merchant "$RUN_DIR" +chmod 755 /var/run/taler-merchant "$RUN_DIR" +# merchant app data (package default home) +if [ -d /var/lib/taler-merchant ]; then + chown -R taler-merchant-httpd:www-data /var/lib/taler-merchant 2>/dev/null || true +fi + +echo "Start base services needed for Taler Merchant." +echo "" + +if [ -f /root/.taler-secrets-env ]; then + echo -n "Read secrets needed for SMS delivery:" + set -a + # shellcheck disable=SC1091 + source /root/.taler-secrets-env && echo " OK" + set +a +else + echo "No /root/.taler-secrets-env (SMS may fail)" +fi + +echo "1. postgresql:" +ensure_postgresql + +echo "2. nginx:" +if [ -x /etc/init.d/nginx ]; then + /etc/init.d/nginx start 2>/dev/null || nginx || true +else + service nginx start 2>/dev/null || nginx || true +fi + +if [ "$NO_SHELL" -eq 1 ]; then + echo "Base services started (--no-shell). Next: runuser -u taler-merchant-httpd -- $PWD_BIN/$MERCHANT_STARTER [--restart]" + exit 0 +fi + +echo "3. Switching now to user taler-merchant-httpd, in $PWD_BIN; find executable $MERCHANT_STARTER there!" +echo "" +cd "$PWD_BIN" +# util-linux: -u and -s/--shell are mutually exclusive +exec runuser -u taler-merchant-httpd -- env \ + CLICKSEND_API_KEY="${CLICKSEND_API_KEY:-}" \ + CLICKSEND_USERNAME="${CLICKSEND_USERNAME:-}" \ + TELESIGN_AUTH_TOKEN="${TELESIGN_AUTH_TOKEN:-}" \ + bash diff --git a/scripts/taler-merchant/start_merchant.sh b/scripts/taler-merchant/start_merchant.sh new file mode 100755 index 0000000..a6d1d90 --- /dev/null +++ b/scripts/taler-merchant/start_merchant.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# Start / restart taler-merchant (manual, no systemd). +# Run as: taler-merchant-httpd +# +# Usage: +# start_merchant.sh +# start_merchant.sh --restart | -r +# start_merchant.sh --help + +set -u + +usage() { + cat <<'EOF' +Usage: start_merchant.sh [--restart|-r] [--help|-h] + + (default) Start merchant helpers + httpd. + --restart Stop live taler-merchant-* daemons, then start cleanly. + Does not touch postgres/nginx. +EOF +} + +DO_RESTART=0 +for arg in "$@"; do + case "$arg" in + --restart|-r) DO_RESTART=1 ;; + --help|-h) usage; exit 0 ;; + *) echo "Unknown option: $arg" >&2; usage >&2; exit 2 ;; + esac +done + +if [ "$(id -un)" != "taler-merchant-httpd" ]; then + echo "This script must be run as user taler-merchant-httpd" >&2 + exit 1 +fi + +list_merchant_pids() { + ps -eo pid=,stat=,args= 2>/dev/null | while read -r pid stat args; do + case "$stat" in Z*) continue ;; esac + case "$args" in + *start_merchant.sh*) continue ;; + esac + case "$args" in + *taler-merchant-httpd\ *|taler-merchant-httpd\ *) + echo "$pid" ;; + *taler-merchant-webhook*|*taler-merchant-kyccheck*|*taler-merchant-wirewatch*) + echo "$pid" ;; + *taler-merchant-depositcheck*|*taler-merchant-exchangekeyupdate*|*taler-merchant-reconciliation*) + echo "$pid" ;; + esac + done | sort -u +} + +kill_taler_merchant() { + local pids + pids=$(list_merchant_pids | tr '\n' ' ') + if [ -z "${pids// }" ]; then + echo "No live taler-merchant processes to stop." + return 0 + fi + echo "Stopping PIDs: $pids" + # shellcheck disable=SC2086 + kill -TERM $pids 2>/dev/null || true + sleep 2 + local left + left=$(list_merchant_pids | tr '\n' ' ') + if [ -n "${left// }" ]; then + echo "SIGKILL remaining: $left" + # shellcheck disable=SC2086 + kill -KILL $left 2>/dev/null || true + sleep 1 + fi + echo "taler-merchant daemons stopped." +} + +TIMESTAMP=$(date +"%Y%m%d_%H%M") +BACKUP_DIR="/var/taler-backups" +LOG_DIR="/var/log/taler-merchant" + +if [ "$DO_RESTART" -eq 1 ]; then + echo "=== restart: kill taler-merchant-* ===" + kill_taler_merchant +fi + +BACKUP_NAME="taler-merchant-$TIMESTAMP.sql" +echo -n "Backup taler-merchant DB: " +mkdir -p "$BACKUP_DIR" 2>/dev/null || true +if pg_dump taler-merchant >"$BACKUP_DIR/$BACKUP_NAME" 2>/dev/null; then + echo "OK" +else + echo "SKIP/FAIL (postgres?)" +fi +echo "Start taler-merchant components:" + +LOG_FILE="$LOG_DIR/taler-merchant-httpd-$(date +%Y-%m-%d).log" +mkdir -p "$LOG_DIR" +touch "$LOG_FILE" + +# Prefer dedicated ensure script (nohup + logs per helper). +if [ -x /usr/local/bin/ensure_merchant_helpers.sh ]; then + /usr/local/bin/ensure_merchant_helpers.sh || true +elif [ -x "$(dirname "$0")/ensure_merchant_helpers.sh" ]; then + "$(dirname "$0")/ensure_merchant_helpers.sh" || true +else + nohup taler-merchant-httpd --log=info >>"$LOG_FILE" 2>&1 /dev/null || true + sleep 2 + nohup taler-merchant-webhook >>"$LOG_DIR/taler-merchant-webhook.log" 2>&1 >"$LOG_DIR/taler-merchant-kyccheck.log" 2>&1 >"$LOG_DIR/taler-merchant-wirewatch.log" 2>&1 >"$LOG_DIR/taler-merchant-depositcheck.log" 2>&1 >"$LOG_DIR/taler-merchant-exchangekeyupdate.log" 2>&1 >"$LOG_DIR/taler-merchant-reconciliation.log" 2>&1 /dev/null || true + sleep 1 +fi + +echo "Live processes:" +ps -eo pid,stat,args 2>/dev/null | grep taler-merchant | grep -v grep | grep -v ' Z ' || true + +if [ -x /usr/local/bin/check_merchant-health.sh ]; then + /usr/local/bin/check_merchant-health.sh || exit 1 +elif [ -x ./check_merchant-health.sh ]; then + ./check_merchant-health.sh || exit 1 +fi +exit 0 diff --git a/scripts/taler-merchant/stats--merchant-payments.sh b/scripts/taler-merchant/stats--merchant-payments.sh new file mode 100755 index 0000000..ac9fcff --- /dev/null +++ b/scripts/taler-merchant/stats--merchant-payments.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Tested for DB scheme v38: + +set -eu + +runuser -u taler-merchant-httpd -- bash -c ' +printf "%-38s | %8s | %12s\n" "merchant_id" "payments" "total_amount" +printf "%-38s-+-%-8s-+-%-12s\n" "--------------------------------------" "--------" "------------" + +for s in $(psql -t -A taler-merchant -c " + SELECT schemaname FROM pg_tables + WHERE schemaname LIKE '\''merchant_instance_%'\'' + AND tablename = '\''merchant_deposits'\'' +"); do + mid=$(psql -t -A taler-merchant -c " + SELECT merchant_id FROM merchant.merchant_instances + WHERE merchant_serial = ${s#merchant_instance_} + " 2>/dev/null || echo "?") + + psql -t -A taler-merchant -c " + SELECT + '\''$mid'\'', + count(*), + round( + (COALESCE(sum((amount_with_fee).val), 0) + + COALESCE(sum((amount_with_fee).frac), 0)::numeric / 1000000000) + , 2) + FROM $s.merchant_deposits + " 2>/dev/null +done | while read line; do + printf "%-38s | %8s | %12s\n" $(echo "$line" | tr "|" " ") +done +' diff --git a/scripts/taler-merchant/taler-hacktivism-email-helper.sh b/scripts/taler-merchant/taler-hacktivism-email-helper.sh new file mode 100755 index 0000000..dada913 --- /dev/null +++ b/scripts/taler-merchant/taler-hacktivism-email-helper.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Usage: echo "body text" | ./taler-hacktivism-email-helper.sh email@example.com +# Needs ``swaks'' to be installed. +TO="$1" +SUBJECT="Taler Merchant Auth Code" +BODY=$(cat) + +# SMTP password: set SMTP_PASSWORD in the environment (not stored in git). +# Live container may still use a literal in-file password — do not re-commit it. +swaks --server mail.cyon.ch \ + --port 587 \ + --auth LOGIN \ + --auth-user taler-merchant@hacktivism.ch \ + --auth-password "${SMTP_PASSWORD:?set SMTP_PASSWORD}" \ + --tls \ + --from taler-merchant@hacktivism.ch \ + --to "$TO" \ + --header "Subject: $SUBJECT" \ + --body "$BODY" diff --git a/scripts/taler-merchant/taler-hacktivism-sms-helper-wrapper.sh b/scripts/taler-merchant/taler-hacktivism-sms-helper-wrapper.sh new file mode 100755 index 0000000..d42eea7 --- /dev/null +++ b/scripts/taler-merchant/taler-hacktivism-sms-helper-wrapper.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +PHONE_NO="$1" +JSON_STRING="{\"CONTACT_PHONE\":\"${PHONE_NO}\"}" +exec /usr/local/bin/taler-hacktivism-sms-helper.sh $JSON_STRING