merchant: start/health + certbot_renew (container nginx TLS :8081, ACME :8082)

This commit is contained in:
Hernâni Marques 2026-06-29 16:11:55 +02:00
parent ad85a60c62
commit 4823c8f602
No known key found for this signature in database
13 changed files with 795 additions and 0 deletions

View file

@ -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).

View file

@ -0,0 +1,5 @@
#!/bin/sh
while true; do
certbot renew --quiet
sleep 12h
done

View file

@ -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 ==="
# --- 14: 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 <<EOF
$(list_exchanges)
EOF
[ "$exch_count" -eq 0 ] && warn "no enabled exchanges with EXCHANGE_BASE_URL"
fi
# --- 7: helpers — ensure (no systemd) then require ---
if [ "${SKIP_ENSURE:-0}" != "1" ]; then
if [ -x /usr/local/bin/ensure_merchant_helpers.sh ]; then
echo "--- ensure_merchant_helpers ---"
/usr/local/bin/ensure_merchant_helpers.sh || warn "ensure_merchant_helpers exited non-zero"
elif [ -x "$(dirname "$0")/ensure_merchant_helpers.sh" ]; then
echo "--- ensure_merchant_helpers ---"
"$(dirname "$0")/ensure_merchant_helpers.sh" || warn "ensure_merchant_helpers exited non-zero"
fi
fi
live_helper() {
local p="$1"
pgrep -f "(^|/)(${p})( |$)" >/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

View file

@ -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

View file

@ -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 &
disown 2>/dev/null || true
sleep 2
nohup taler-merchant-webhook >>"$LOG_DIR/taler-merchant-webhook.log" 2>&1 </dev/null &
nohup taler-merchant-kyccheck >>"$LOG_DIR/taler-merchant-kyccheck.log" 2>&1 </dev/null &
nohup taler-merchant-wirewatch -c /etc/taler-merchant/taler-merchant.conf -L INFO \
>>"$LOG_DIR/taler-merchant-wirewatch.log" 2>&1 </dev/null &
nohup taler-merchant-depositcheck >>"$LOG_DIR/taler-merchant-depositcheck.log" 2>&1 </dev/null &
nohup taler-merchant-exchangekeyupdate >>"$LOG_DIR/taler-merchant-exchangekeyupdate.log" 2>&1 </dev/null &
nohup taler-merchant-reconciliation >>"$LOG_DIR/taler-merchant-reconciliation.log" 2>&1 </dev/null &
disown 2>/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

View file

@ -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
'

View file

@ -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"

View file

@ -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