docs: new root as prior history lost (orphan + GC); ~215 commits not recoverable
This commit is contained in:
commit
96961f23f5
268 changed files with 24161 additions and 0 deletions
47
scripts/README.md
Normal file
47
scripts/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Scripts
|
||||
|
||||
| Dir | Source on koopa |
|
||||
|-----|-----------------|
|
||||
| `taler-merchant/` | podman `taler-hacktivism`: `/root`, `/usr/local/bin` |
|
||||
| `taler-exchange/` | podman `taler-hacktivism-exchange-ansible`: `/root`, `/usr/local/bin` |
|
||||
| `taler-hacktivism-bank/` | podman `taler-hacktivism-bank`: `/root`, `/usr/local/bin` |
|
||||
| `taler-sanity/` | host root checks (stack, settlement, helpers) |
|
||||
| `taler-monitoring/` | **outside-in** public URL walk (`/config` → keys/terms/integration/webui) |
|
||||
| `monitoring/` | host `/home/hernani/scripts` (tor relay stats) |
|
||||
| `taler-wallet-cli/` | thin wrappers; **benchmarks live in** `../benchmarks/` |
|
||||
| `castopod/` | host `hernani` podman-compose `~/koopa-castopod` — see `castopod/README.md` |
|
||||
|
||||
**Secrets:** never in this tree — sibling **`../koopa-admin-secrets`** (`koopa/host-root/<service>/` ↔ `/root/` on host; `containers/…/secrets/` for in-container).
|
||||
|
||||
## Manual start model (all three)
|
||||
|
||||
1. **root** runs `/root/start_base_services_for_taler_*.sh`
|
||||
→ Debian-style postgres perms + start (`pg_ctlcluster` / `init.d`)
|
||||
→ (+ exchange: secmods/helpers; merchant: nginx)
|
||||
→ interactive shell as service user
|
||||
→ automation: add **`--no-shell`** then run step 2 via `runuser`
|
||||
2. **service user** runs `/usr/local/bin/start_*.sh` [ `--restart` ]
|
||||
→ application process only
|
||||
|
||||
Postgres ownership (Debian defaults, if cluster was root-owned after bad ops):
|
||||
|
||||
```text
|
||||
chown -R root:postgres /etc/postgresql
|
||||
chmod confs 640 / dirs 755
|
||||
chown -R postgres:postgres /var/lib/postgresql /var/log/postgresql /var/run/postgresql
|
||||
# only remove postmaster.pid / socket locks when pg_isready fails and no live postgres
|
||||
pg_ctlcluster 17 main start
|
||||
```
|
||||
|
||||
| Container | Root base | App start | App user |
|
||||
|-----------|-----------|-----------|----------|
|
||||
| `taler-hacktivism` | `start_base_services_for_taler.sh` | `start_merchant.sh` | `taler-merchant-httpd` |
|
||||
| `taler-hacktivism-exchange-ansible` | `start_base_services_for_taler_exchange.sh` | `start_exchange.sh` | `taler-exchange-httpd` |
|
||||
|
||||
Exchange one-shots (root, offline / wire):
|
||||
`taler-exchange/wire-enable-and-upload.sh`, `offline-sign-upload-keys.sh`, `start_wire_helpers.sh`
|
||||
| `taler-hacktivism-bank` | `start_base_services_for_taler_bank.sh` | `start_bank.sh` | `libeufin-bank` |
|
||||
|
||||
`runuser -u USER -- bash` (never `-u` with `-s` on util-linux).
|
||||
|
||||
SMS helper symlinks into `/var/taler-src/...` are not copied (merchant only).
|
||||
13
scripts/bonfire/README.md
Normal file
13
scripts/bonfire/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Bonfire scripts
|
||||
|
||||
Live: `/home/hernani/koopa-bonfire/`.
|
||||
Docs: `configs/bonfire/README.md`, [Bonfire docs](https://docs.bonfirenetworks.org/).
|
||||
|
||||
| Script | Role |
|
||||
|--------|------|
|
||||
| `gitbot-mirror.py` | Poll git.ngi-0.eu, post via GraphQL, republish feeds |
|
||||
| `install-systemd.sh` | Boot + gitbot units |
|
||||
| `apply-branding.sh` | Logo + hacktivism theme (`bonfire remote`) |
|
||||
| `publish-outbox-to-public.sql` | One-off outbox → public feeds |
|
||||
|
||||
No official gitbot or feed-republish guides — custom ops scripts.
|
||||
56
scripts/bonfire/apply-branding.exs
Normal file
56
scripts/bonfire/apply-branding.exs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Apply hacktivism branding to Bonfire instance settings.
|
||||
# Run on koopa: printf '%s\n' "$(cat apply-branding.exs)" | podman exec -i koopa-bonfire /opt/app/bin/bonfire remote
|
||||
#
|
||||
# Instance settings API: https://docs.bonfirenetworks.org/settings_system.html
|
||||
|
||||
palette = %{
|
||||
"color-base-100" => "#1a1410",
|
||||
"color-base-200" => "#221c16",
|
||||
"color-base-300" => "#2a2018",
|
||||
"color-base-content" => "#fff6e8",
|
||||
"color-primary" => "#e8a838",
|
||||
"color-primary-content" => "#1a1410",
|
||||
"color-secondary" => "#3d3128",
|
||||
"color-secondary-content" => "#fff6e8",
|
||||
"color-accent" => "#3ecfbf",
|
||||
"color-accent-content" => "#0e1c1e",
|
||||
"color-neutral" => "#14110e",
|
||||
"color-neutral-content" => "#ebe0d0",
|
||||
"color-info" => "#2563eb",
|
||||
"color-info-content" => "#ffffff",
|
||||
"color-success" => "#16a34a",
|
||||
"color-success-content" => "#ffffff",
|
||||
"color-warning" => "#f0d090",
|
||||
"color-warning-content" => "#1a1410",
|
||||
"color-error" => "#b91c1c",
|
||||
"color-error-content" => "#ffffff",
|
||||
"radius-box" => "0.875rem",
|
||||
"radius-field" => "0.5rem",
|
||||
"radius-selector" => "0.75rem"
|
||||
}
|
||||
|
||||
opts = [skip_boundary_check: true, scope: :instance]
|
||||
|
||||
settings = %{
|
||||
ui: %{
|
||||
theme: %{
|
||||
instance_name: "hacktivism bonfire",
|
||||
instance_tagline: "hacktivism magician - federated FOSS timeline",
|
||||
instance_description:
|
||||
"Bonfire on hacktivism.ch — local FOSS posts, gitbot commits, and federation.",
|
||||
instance_icon: "/images/hacktivism-logo.svg",
|
||||
instance_theme: "dark",
|
||||
instance_theme_light: "light",
|
||||
preferred: :custom,
|
||||
custom_instance: palette
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case Bonfire.Common.Settings.set(settings, opts) do
|
||||
{:ok, _} ->
|
||||
IO.puts("ok branding settings applied")
|
||||
|
||||
other ->
|
||||
IO.inspect(other, label: "branding settings failed")
|
||||
end
|
||||
55
scripts/bonfire/apply-branding.sh
Executable file
55
scripts/bonfire/apply-branding.sh
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env bash
|
||||
# Deploy hacktivism Bonfire branding (hacktivism magician logo + exchange-dark palette).
|
||||
# Live root: ~/koopa-bonfire/
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${KOOPA_BONFIRE_ROOT:-$HOME/koopa-bonfire}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." 2>/dev/null && pwd || true)"
|
||||
BRANDING_DST="$ROOT/data/branding"
|
||||
ENV_FILE="$ROOT/.env"
|
||||
|
||||
install -d -m 755 "$BRANDING_DST"
|
||||
|
||||
if [[ -d "$REPO_ROOT/configs/bonfire/assets/img" ]]; then
|
||||
BRANDING_SRC="$REPO_ROOT/configs/bonfire/assets/img"
|
||||
install -m 644 "$BRANDING_SRC/logo.svg" "$BRANDING_DST/logo.svg"
|
||||
install -m 644 "$BRANDING_SRC/favicon.svg" "$BRANDING_DST/favicon.svg"
|
||||
install -m 644 "$BRANDING_SRC/logo.png" "$BRANDING_DST/logo.png"
|
||||
elif [[ ! -f "$BRANDING_DST/logo.svg" ]]; then
|
||||
echo "missing $BRANDING_DST/logo.svg (copy assets or run from koopa-admin-log checkout)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# APP_NAME drives <title> suffix and og:site_name when instance_name is unset in HTML meta.
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
if grep -q '^APP_NAME=' "$ENV_FILE"; then
|
||||
sed -i 's/^APP_NAME=.*/APP_NAME=hacktivism bonfire/' "$ENV_FILE"
|
||||
else
|
||||
echo 'APP_NAME=hacktivism bonfire' >> "$ENV_FILE"
|
||||
fi
|
||||
if grep -q '^INSTANCE_DESCRIPTION=' "$ENV_FILE"; then
|
||||
sed -i 's/^INSTANCE_DESCRIPTION=.*/INSTANCE_DESCRIPTION=Bonfire on hacktivism.ch - FOSS posts and gitbot commits./' "$ENV_FILE"
|
||||
else
|
||||
echo 'INSTANCE_DESCRIPTION=Bonfire on hacktivism.ch - FOSS posts and gitbot commits.' >> "$ENV_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$SCRIPT_DIR/apply-branding.exs" != "$ROOT/bin/apply-branding.exs" ]]; then
|
||||
install -m 755 "$SCRIPT_DIR/apply-branding.exs" "$ROOT/bin/apply-branding.exs"
|
||||
fi
|
||||
|
||||
echo "Applying instance theme settings via bonfire remote..."
|
||||
sleep 2
|
||||
printf '%s\n' "$(cat "$ROOT/bin/apply-branding.exs")" | podman exec -i koopa-bonfire /opt/app/bin/bonfire remote >/tmp/bonfire-branding.log 2>&1 || {
|
||||
echo "remote apply failed; see /tmp/bonfire-branding.log" >&2
|
||||
tail -30 /tmp/bonfire-branding.log >&2
|
||||
exit 1
|
||||
}
|
||||
grep -q 'ok branding settings applied' /tmp/bonfire-branding.log
|
||||
|
||||
echo "Recreating web container (compose branding volume)..."
|
||||
cd "$ROOT"
|
||||
podman-compose up -d --no-deps --force-recreate web
|
||||
|
||||
echo "Done. Check https://bonfire.hacktivism.ch/ (hard-reload if cached)."
|
||||
240
scripts/bonfire/gitbot-mirror.py
Executable file
240
scripts/bonfire/gitbot-mirror.py
Executable file
|
|
@ -0,0 +1,240 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Poll git.ngi-0.eu and post new commits to Bonfire as gitbot."""
|
||||
from __future__ import annotations
|
||||
import json, os, pathlib, re, subprocess, sys, time, urllib.error, urllib.request
|
||||
|
||||
GQL = os.environ.get("BONFIRE_GQL", "http://127.0.0.1:9021/api/graphql")
|
||||
GIT = os.environ.get("GIT_BASE", "https://git.ngi-0.eu")
|
||||
STATE = pathlib.Path(os.environ.get("GITBOT_STATE", "/home/hernani/koopa-bonfire/gitbot-state.json"))
|
||||
USERS = pathlib.Path(os.environ.get("BONFIRE_USERS_ENV", "/home/hernani/koopa-bonfire/users.env"))
|
||||
INTERVAL = int(os.environ.get("GITBOT_INTERVAL", "300"))
|
||||
MAX_FAILS = int(os.environ.get("GITBOT_MAX_FAILS", "3"))
|
||||
MAX_TIME = 45
|
||||
OUTBOX_FEED = os.environ.get(
|
||||
"GITBOT_OUTBOX_FEED", "019f487a-df20-4ed0-a334-40ec3eac23e7"
|
||||
)
|
||||
FEED_INTERNET = os.environ.get(
|
||||
"GITBOT_FEED_INTERNET", "0aab414c-eb0a-ac1d-8c81-ef0d74ec55da"
|
||||
)
|
||||
FEED_LOCAL = os.environ.get(
|
||||
"GITBOT_FEED_LOCAL", "797632fc-029e-06f0-1031-410d73a5558e"
|
||||
)
|
||||
PUBLISH_CMD = os.environ.get(
|
||||
"GITBOT_PUBLISH_CMD",
|
||||
"podman exec -i koopa-bonfire-db psql -U postgres -d bonfire_db",
|
||||
)
|
||||
|
||||
_BARE_HOST = re.compile(
|
||||
r"(?<!https://)(?<!http://)"
|
||||
r"(?<![\w./@-])"
|
||||
r"((?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+(?:[a-zA-Z]{2,}))"
|
||||
r"(?![\w./:])"
|
||||
)
|
||||
|
||||
MUT = """
|
||||
mutation CreatePost($pc: PostContentInput!) {
|
||||
createPost(postContent: $pc) { id }
|
||||
}
|
||||
"""
|
||||
|
||||
def sanitize_text(text: str) -> str:
|
||||
"""Add https:// to bare hostnames (Bonfire URI.parse breaks without it)."""
|
||||
return _BARE_HOST.sub(r"https://\1", text)
|
||||
|
||||
def load_users():
|
||||
d = {}
|
||||
for line in USERS.read_text().splitlines():
|
||||
if "=" in line and not line.strip().startswith("#"):
|
||||
k, v = line.split("=", 1)
|
||||
d[k.strip()] = v.strip()
|
||||
return d
|
||||
|
||||
def http_json(url, data=None, headers=None, timeout=MAX_TIME):
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=None if data is None else json.dumps(data).encode(),
|
||||
headers=headers or {},
|
||||
method="GET" if data is None else "POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.load(r)
|
||||
|
||||
def gql(token, query, variables=None):
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
body = {"query": query}
|
||||
if variables is not None:
|
||||
body["variables"] = variables
|
||||
return http_json(GQL, body, headers)
|
||||
|
||||
def bonfire_ready() -> bool:
|
||||
base = GQL.rsplit("/api/", 1)[0] + "/"
|
||||
try:
|
||||
with urllib.request.urlopen(base, timeout=10) as r:
|
||||
return 200 <= r.status < 500
|
||||
except (urllib.error.URLError, TimeoutError, OSError):
|
||||
return False
|
||||
|
||||
def login(email_or_user, password):
|
||||
res = gql(
|
||||
None,
|
||||
"mutation($e:String!,$p:String!){ login(emailOrUsername:$e, password:$p){ token currentUsername } }",
|
||||
{"e": email_or_user, "p": password},
|
||||
)
|
||||
tok = (res.get("data") or {}).get("login") or {}
|
||||
if not tok.get("token"):
|
||||
raise RuntimeError(f"login failed: {res}")
|
||||
return tok["token"]
|
||||
|
||||
def select_user(token, username):
|
||||
res = gql(
|
||||
token,
|
||||
"mutation($u:String!){ selectUser(username:$u){ token currentUsername } }",
|
||||
{"u": username},
|
||||
)
|
||||
t = (res.get("data") or {}).get("selectUser") or {}
|
||||
return t.get("token") or token
|
||||
|
||||
def create_post(token, body, summary, name):
|
||||
res = gql(
|
||||
token,
|
||||
MUT,
|
||||
{"pc": {"htmlBody": body, "summary": summary[:140], "name": name}},
|
||||
)
|
||||
post = (res.get("data") or {}).get("createPost")
|
||||
if not post:
|
||||
raise RuntimeError(f"createPost failed: {res}")
|
||||
return post["id"]
|
||||
|
||||
def post_commit(token, full, c):
|
||||
sha = c["sha"]
|
||||
short = sha[:8]
|
||||
raw_msg = c["commit"]["message"].split("\n")[0][:140]
|
||||
msg = sanitize_text(raw_msg)
|
||||
html = c.get("html_url") or f"{GIT}/{full}/commit/{sha}"
|
||||
author = (c.get("commit") or {}).get("author", {}).get("name") or "?"
|
||||
full_body = (
|
||||
f"[gitbot] commit {short} on {full}\n"
|
||||
f"{msg}\nby {author}\n{html}\n#git #ngi0 #foss"
|
||||
)
|
||||
summary = sanitize_text(f"{full}@{short}: {msg}")[:140]
|
||||
try:
|
||||
return create_post(token, full_body, summary, f"commit {short}")
|
||||
except RuntimeError:
|
||||
minimal_body = f"[gitbot] commit {short} on {full}\n{html}\n#git #ngi0 #foss"
|
||||
return create_post(token, minimal_body, f"{full}@{short}", f"commit {short}")
|
||||
|
||||
def load_state():
|
||||
state = {"seen": [], "fails": {}}
|
||||
if STATE.exists():
|
||||
state = json.loads(STATE.read_text())
|
||||
state.setdefault("seen", [])
|
||||
state.setdefault("fails", {})
|
||||
return state
|
||||
|
||||
def save_state(state, seen):
|
||||
state["seen"] = list(seen)[-800:]
|
||||
STATE.write_text(json.dumps(state, indent=2))
|
||||
|
||||
def publish_outbox_to_public() -> bool:
|
||||
"""Copy foss outbox into local/internet feeds (ops workaround; no official republish API)."""
|
||||
sql = f"""
|
||||
INSERT INTO bonfire_data_social_feed_publish (id, feed_id)
|
||||
SELECT fp.id, '{FEED_INTERNET}'::uuid
|
||||
FROM bonfire_data_social_feed_publish fp
|
||||
WHERE fp.feed_id = '{OUTBOX_FEED}'::uuid
|
||||
ON CONFLICT DO NOTHING;
|
||||
INSERT INTO bonfire_data_social_feed_publish (id, feed_id)
|
||||
SELECT fp.id, '{FEED_LOCAL}'::uuid
|
||||
FROM bonfire_data_social_feed_publish fp
|
||||
WHERE fp.feed_id = '{OUTBOX_FEED}'::uuid
|
||||
ON CONFLICT DO NOTHING;
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
PUBLISH_CMD.split(),
|
||||
input=sql,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
print("publish feeds failed:", proc.stderr.strip(), file=sys.stderr)
|
||||
return False
|
||||
print("published outbox -> local/internet feeds", flush=True)
|
||||
return True
|
||||
except (OSError, subprocess.TimeoutExpired) as e:
|
||||
print("publish feeds error:", e, file=sys.stderr)
|
||||
return False
|
||||
|
||||
def once():
|
||||
if not bonfire_ready():
|
||||
print("bonfire not ready, skipping cycle", flush=True)
|
||||
return 0
|
||||
|
||||
u = load_users()
|
||||
tok = login(u.get("FOSS_USER", "foss"), u["FOSS_PW"])
|
||||
try:
|
||||
tok = select_user(tok, u.get("GITBOT_USER", "gitbot"))
|
||||
except Exception as e:
|
||||
print("select gitbot skipped:", e, file=sys.stderr)
|
||||
|
||||
state = load_state()
|
||||
seen = set(state["seen"])
|
||||
fails = state["fails"]
|
||||
skipped = set(state.get("skipped") or [])
|
||||
|
||||
repos = http_json(f"{GIT}/api/v1/repos/search?limit=30&sort=updated")["data"]
|
||||
new_n = 0
|
||||
for repo in repos:
|
||||
full = repo["full_name"]
|
||||
owner, name = full.split("/", 1)
|
||||
try:
|
||||
commits = http_json(f"{GIT}/api/v1/repos/{owner}/{name}/commits?limit=10")
|
||||
except Exception as e:
|
||||
print("repo fail", full, e, file=sys.stderr)
|
||||
continue
|
||||
for c in reversed(commits):
|
||||
sha = c["sha"]
|
||||
if sha in seen or sha in skipped:
|
||||
continue
|
||||
try:
|
||||
pid = post_commit(tok, full, c)
|
||||
print(f"posted {full}@{sha[:8]} -> {pid}", flush=True)
|
||||
seen.add(sha)
|
||||
fails.pop(sha, None)
|
||||
new_n += 1
|
||||
save_state(state, seen)
|
||||
time.sleep(0.4)
|
||||
except Exception as e:
|
||||
n = fails.get(sha, 0) + 1
|
||||
fails[sha] = n
|
||||
state["fails"] = fails
|
||||
save_state(state, seen)
|
||||
print("post fail", full, sha[:8], f"try={n}", e, file=sys.stderr)
|
||||
if n >= MAX_FAILS:
|
||||
skipped.add(sha)
|
||||
state["skipped"] = list(skipped)[-200:]
|
||||
save_state(state, seen)
|
||||
print("skip", full, sha[:8], "after repeated failures", file=sys.stderr)
|
||||
publish_outbox_to_public()
|
||||
print(f"done new={new_n} tracked={len(seen)} skipped={len(skipped)}", flush=True)
|
||||
return new_n
|
||||
|
||||
def main():
|
||||
loop = "--loop" in sys.argv
|
||||
if loop:
|
||||
print(f"gitbot loop interval={INTERVAL}s gql={GQL}", flush=True)
|
||||
while True:
|
||||
try:
|
||||
once()
|
||||
except Exception as e:
|
||||
print("cycle error:", e, file=sys.stderr, flush=True)
|
||||
time.sleep(INTERVAL)
|
||||
else:
|
||||
once()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
29
scripts/bonfire/install-systemd.sh
Executable file
29
scripts/bonfire/install-systemd.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env bash
|
||||
# Install Bonfire + gitbot user systemd units on koopa.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
UNIT_DIR="${HOME}/.config/systemd/user"
|
||||
BIN_DIR="${HOME}/koopa-bonfire/bin"
|
||||
|
||||
mkdir -p "${UNIT_DIR}" "${BIN_DIR}"
|
||||
install -m 755 "${ROOT}/scripts/bonfire/gitbot-mirror.py" "${BIN_DIR}/gitbot-mirror.py"
|
||||
cp "${ROOT}/configs/bonfire/container-koopa-bonfire-db.service" "${UNIT_DIR}/"
|
||||
cp "${ROOT}/configs/bonfire/container-koopa-bonfire.service" "${UNIT_DIR}/"
|
||||
cp "${ROOT}/configs/bonfire/gitbot-mirror.service" "${UNIT_DIR}/"
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable container-koopa-bonfire-db.service container-koopa-bonfire.service gitbot-mirror.service
|
||||
|
||||
if [[ -f "${HOME}/koopa-bonfire/gitbot.pid" ]]; then
|
||||
old_pid="$(cat "${HOME}/koopa-bonfire/gitbot.pid" 2>/dev/null || true)"
|
||||
[[ -n "${old_pid}" ]] && kill "${old_pid}" 2>/dev/null || true
|
||||
rm -f "${HOME}/koopa-bonfire/gitbot.pid"
|
||||
fi
|
||||
|
||||
if ! podman ps --format '{{.Names}}' | grep -qx koopa-bonfire; then
|
||||
(cd "${HOME}/koopa-bonfire" && podman-compose up -d)
|
||||
fi
|
||||
systemctl --user start container-koopa-bonfire-db.service container-koopa-bonfire.service
|
||||
systemctl --user restart gitbot-mirror.service
|
||||
|
||||
systemctl --user --no-pager status container-koopa-bonfire-db.service container-koopa-bonfire.service gitbot-mirror.service
|
||||
28
scripts/bonfire/publish-outbox-to-public.sql
Normal file
28
scripts/bonfire/publish-outbox-to-public.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- Republish outbox activities into guest-visible feeds (ops workaround).
|
||||
-- FeedPublish model: https://docs.bonfirenetworks.org/feed_structure.html
|
||||
-- No official republish procedure. Default outbox = foss.
|
||||
-- Run:
|
||||
-- podman exec -i koopa-bonfire-db psql -U postgres -d bonfire_db < publish-outbox-to-public.sql
|
||||
|
||||
\set OUTBOX_FEED '019f487a-df20-4ed0-a334-40ec3eac23e7'
|
||||
\set FEED_INTERNET '0aab414c-eb0a-ac1d-8c81-ef0d74ec55da'
|
||||
\set FEED_LOCAL_USERS '797632fc-029e-06f0-1031-410d73a5558e'
|
||||
|
||||
INSERT INTO bonfire_data_social_feed_publish (id, feed_id)
|
||||
SELECT fp.id, :'FEED_INTERNET'::uuid
|
||||
FROM bonfire_data_social_feed_publish fp
|
||||
WHERE fp.feed_id = :'OUTBOX_FEED'::uuid
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO bonfire_data_social_feed_publish (id, feed_id)
|
||||
SELECT fp.id, :'FEED_LOCAL_USERS'::uuid
|
||||
FROM bonfire_data_social_feed_publish fp
|
||||
WHERE fp.feed_id = :'OUTBOX_FEED'::uuid
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
SELECT n.name, count(*)
|
||||
FROM bonfire_data_social_feed_publish fp
|
||||
LEFT JOIN bonfire_data_social_named n ON n.id = fp.feed_id
|
||||
GROUP BY 1
|
||||
ORDER BY 2 DESC
|
||||
LIMIT 15;
|
||||
15
scripts/caddy/README.md
Normal file
15
scripts/caddy/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Caddy on koopa
|
||||
|
||||
Live: **`/etc/caddy/Caddyfile`**.
|
||||
Mirror: **`configs/caddy/Caddyfile`** (same as `host/caddy/Caddyfile`).
|
||||
|
||||
## Reload after edit
|
||||
|
||||
```bash
|
||||
sudo caddy validate --config /etc/caddy/Caddyfile
|
||||
sudo systemctl reload caddy
|
||||
```
|
||||
|
||||
## New HTTPS vhost
|
||||
|
||||
Add a site block to the Caddyfile (live + mirror), then validate and reload. Include the host in the shared HTTP site list (ACME + redirect) at the bottom of the file.
|
||||
33
scripts/castopod/README.md
Normal file
33
scripts/castopod/README.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Castopod ops scripts (koopa)
|
||||
|
||||
Live stack: `/home/hernani/koopa-castopod/` (podman-compose).
|
||||
These scripts live in admin-log and should be **copied** to the host when changed.
|
||||
|
||||
|
||||
## Usage (on koopa as `hernani`)
|
||||
|
||||
```bash
|
||||
# copy once
|
||||
mkdir -p ~/koopa-castopod/bin
|
||||
cp /path/to/koopa-admin-log/scripts/castopod/{lib.sh,status.sh,up.sh,install-systemd.sh,apply-branding.sh} \
|
||||
~/koopa-castopod/bin/
|
||||
chmod +x ~/koopa-castopod/bin/*.sh
|
||||
|
||||
~/koopa-castopod/bin/status.sh
|
||||
~/koopa-castopod/bin/up.sh
|
||||
~/koopa-castopod/bin/install-systemd.sh # boot units
|
||||
~/koopa-castopod/bin/apply-branding.sh # logo + theme + optional overlay
|
||||
```
|
||||
|
||||
Env overrides: `CP_MAX_TIME`, `CP_PULL_TIMEOUT`, `CP_HEALTH_TRIES`, `CP_BASEURL`,
|
||||
`CP_SITE_NAME`, `CP_SITE_DESCRIPTION`, `CP_THEME` (default `amber`), `CP_APPLY_OVERLAY` (default `1`).
|
||||
|
||||
Passwords stay in `~/koopa-castopod/.env` and `users.env` (mode 600) — never in admin-log.
|
||||
|
||||
## Official branding docs
|
||||
|
||||
Instance settings (name, description, site icon, six accent themes):
|
||||
|
||||
https://docs.castopod.org/main/en/user-guide/instance/settings/
|
||||
|
||||
See also `configs/castopod/README.md` and `2026/2026-07-13--castopod-boot-branding.md`.
|
||||
137
scripts/castopod/apply-branding.sh
Executable file
137
scripts/castopod/apply-branding.sh
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env bash
|
||||
# Apply hacktivism branding to Castopod (official settings + optional CSS overlay).
|
||||
#
|
||||
# Official docs (instance settings — site name/description/icon + 6 accent themes):
|
||||
# https://docs.castopod.org/main/en/user-guide/instance/settings/
|
||||
# Docker/compose background:
|
||||
# https://docs.castopod.org/main/en/getting-started/docker/
|
||||
#
|
||||
# Logo: same magician mark as git.hacktivism.ch / bonfire.hacktivism.ch
|
||||
# (configs/castopod/assets/img/logo.png — ≥512×512 required by Castopod).
|
||||
# Official accent closest to gold: "amber" (among pine/crimson/lake/amber/jacaranda/onyx).
|
||||
# Optional warm-dark CSS: NOT official — see configs/castopod/assets/css/theme-hacktivism-overlay.css
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${KOOPA_CASTOPOD_ROOT:-$HOME/koopa-castopod}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." 2>/dev/null && pwd || true)"
|
||||
# shellcheck source=lib.sh
|
||||
source "${SCRIPT_DIR}/lib.sh"
|
||||
|
||||
: "${CP_BASEURL:=https://castopod.hacktivism.ch}"
|
||||
: "${CP_ADMIN_GATEWAY:=cp-admin}"
|
||||
: "${CP_SITE_NAME:=hacktivism castopod}"
|
||||
: "${CP_SITE_DESCRIPTION:=Castopod on hacktivism.ch — FOSS Airwaves and free-culture podcasting.}"
|
||||
: "${CP_THEME:=amber}"
|
||||
: "${CP_APPLY_OVERLAY:=1}"
|
||||
|
||||
BRANDING_DST="${ROOT}/branding"
|
||||
ICON_SRC=""
|
||||
CSS_SRC=""
|
||||
|
||||
if [[ -d "${REPO_ROOT}/configs/castopod/assets/img" ]]; then
|
||||
ICON_SRC="${REPO_ROOT}/configs/castopod/assets/img/logo.png"
|
||||
CSS_SRC="${REPO_ROOT}/configs/castopod/assets/css/theme-hacktivism-overlay.css"
|
||||
elif [[ -f "${ROOT}/branding/logo.png" ]]; then
|
||||
ICON_SRC="${ROOT}/branding/logo.png"
|
||||
CSS_SRC="${ROOT}/branding/theme-hacktivism-overlay.css"
|
||||
else
|
||||
echo "missing logo.png (run from koopa-admin-log checkout or copy assets to ${ROOT}/branding/)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -d -m 755 "${BRANDING_DST}"
|
||||
if [[ "$(realpath "${ICON_SRC}" 2>/dev/null || echo "${ICON_SRC}")" != "$(realpath "${BRANDING_DST}/logo.png" 2>/dev/null || echo "${BRANDING_DST}/logo.png")" ]]; then
|
||||
install -m 644 "${ICON_SRC}" "${BRANDING_DST}/logo.png"
|
||||
fi
|
||||
if [[ -n "${CSS_SRC}" && -f "${CSS_SRC}" ]]; then
|
||||
if [[ "$(realpath "${CSS_SRC}" 2>/dev/null || echo "${CSS_SRC}")" != "$(realpath "${BRANDING_DST}/theme-hacktivism-overlay.css" 2>/dev/null || true)" ]]; then
|
||||
install -m 644 "${CSS_SRC}" "${BRANDING_DST}/theme-hacktivism-overlay.css"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Mount overlay into public assets if compose branding volume is present
|
||||
if [[ -d "${ROOT}/branding" ]]; then
|
||||
# Ensure host path for compose bind exists before recreate
|
||||
true
|
||||
fi
|
||||
|
||||
COOKIE=$(mktemp /tmp/cp-brand-cj.XXXXXX)
|
||||
trap 'rm -f "$COOKIE"' EXIT
|
||||
|
||||
echo "== admin login =="
|
||||
cp_admin_login "$COOKIE"
|
||||
|
||||
csrf_from() {
|
||||
local url=$1
|
||||
local html
|
||||
html=$(cp_curl -c "$COOKIE" -b "$COOKIE" "$url")
|
||||
# multiple possible CSRF field names
|
||||
printf '%s' "$html" | sed -n 's/.*name="csrf_test_name" value="\([^"]*\)".*/\1/p' | head -1
|
||||
}
|
||||
|
||||
echo "== official: general settings (site name/description/icon) =="
|
||||
# UI/docs: https://docs.castopod.org/main/en/user-guide/instance/settings/
|
||||
# POST route: /{admin}/settings/instance (SettingsController::attemptInstanceEdit)
|
||||
GEN_GET="${CP_BASEURL}/${CP_ADMIN_GATEWAY}/settings"
|
||||
GEN_POST="${CP_BASEURL}/${CP_ADMIN_GATEWAY}/settings/instance"
|
||||
csrf=$(csrf_from "$GEN_GET")
|
||||
[[ -n "$csrf" ]] || { echo "ERROR: no CSRF on settings general" >&2; exit 1; }
|
||||
|
||||
code=$(cp_curl -c "$COOKIE" -b "$COOKIE" -o /tmp/cp-gen.out -w '%{http_code}' \
|
||||
-X POST "${GEN_POST}" \
|
||||
-F "csrf_test_name=${csrf}" \
|
||||
-F "site_name=${CP_SITE_NAME}" \
|
||||
-F "site_description=${CP_SITE_DESCRIPTION}" \
|
||||
-F "site_icon=@${BRANDING_DST}/logo.png;type=image/png")
|
||||
echo "general POST → HTTP $code"
|
||||
# accept redirect
|
||||
if [[ "$code" != "303" && "$code" != "302" && "$code" != "200" ]]; then
|
||||
echo "WARN: unexpected status; body:" >&2
|
||||
head -c 400 /tmp/cp-gen.out >&2 || true
|
||||
fi
|
||||
|
||||
echo "== official: theme accent (${CP_THEME}) =="
|
||||
THEME_URL="${CP_BASEURL}/${CP_ADMIN_GATEWAY}/settings/theme"
|
||||
csrf=$(csrf_from "$THEME_URL")
|
||||
[[ -n "$csrf" ]] || { echo "ERROR: no CSRF on settings theme" >&2; exit 1; }
|
||||
|
||||
code=$(cp_curl -c "$COOKIE" -b "$COOKIE" -o /tmp/cp-theme.out -w '%{http_code}' \
|
||||
-X POST "${THEME_URL}" \
|
||||
--data-urlencode "csrf_test_name=${csrf}" \
|
||||
--data-urlencode "theme=${CP_THEME}")
|
||||
echo "theme POST → HTTP $code"
|
||||
|
||||
if [[ "${CP_APPLY_OVERLAY}" == "1" && -f "${BRANDING_DST}/theme-hacktivism-overlay.css" ]]; then
|
||||
echo "== optional: warm-dark CSS overlay (not official) =="
|
||||
# Serve from container public path via bind (compose) or copy into media volume
|
||||
podman cp "${BRANDING_DST}/theme-hacktivism-overlay.css" \
|
||||
koopa-castopod:/app/public/assets/hacktivism-overlay.css 2>/dev/null || true
|
||||
|
||||
# Idempotent inject before </head> in public layouts (re-run after image upgrade)
|
||||
# Themes are root-owned in the image — patch as root (re-run after image upgrade).
|
||||
# shellcheck disable=SC2016
|
||||
podman exec -u 0 koopa-castopod sh -c '
|
||||
LINK="<!-- hacktivism-overlay --><link rel=\"stylesheet\" href=\"/assets/hacktivism-overlay.css\">"
|
||||
for f in /app/themes/cp_app/home.php \
|
||||
/app/themes/cp_app/podcast/_layout.php \
|
||||
/app/themes/cp_app/episode/_layout.php \
|
||||
/app/themes/cp_app/pages/_layout.php \
|
||||
/app/themes/cp_app/embed.php; do
|
||||
[ -f "$f" ] || continue
|
||||
if grep -q "hacktivism-overlay" "$f"; then
|
||||
echo "already: $f"
|
||||
continue
|
||||
fi
|
||||
awk -v link="$LINK" "{ if (\$0 ~ /<\\/head>/ && !done) { print link; done=1 } print }" "$f" > "$f.tmp" \
|
||||
&& mv "$f.tmp" "$f" \
|
||||
&& echo "patched: $f"
|
||||
done
|
||||
' || echo "WARN: layout patch skipped (container missing?)"
|
||||
fi
|
||||
|
||||
# Clear page cache so theme/name show up
|
||||
podman exec koopa-castopod sh -c 'rm -rf /app/writable/cache/* 2>/dev/null || true' || true
|
||||
|
||||
echo "Done. Hard-reload https://castopod.hacktivism.ch/"
|
||||
echo "Official UI also: ${CP_BASEURL}/${CP_ADMIN_GATEWAY}/settings (and /settings/theme)"
|
||||
41
scripts/castopod/install-systemd.sh
Executable file
41
scripts/castopod/install-systemd.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env bash
|
||||
# Install Castopod user systemd units on koopa (boot via linger, like Bonfire).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
UNIT_DIR="${HOME}/.config/systemd/user"
|
||||
BIN_DIR="${HOME}/koopa-castopod/bin"
|
||||
|
||||
mkdir -p "${UNIT_DIR}" "${BIN_DIR}"
|
||||
|
||||
for f in lib.sh status.sh up.sh apply-branding.sh; do
|
||||
if [[ -f "${ROOT}/scripts/castopod/${f}" ]]; then
|
||||
install -m 755 "${ROOT}/scripts/castopod/${f}" "${BIN_DIR}/${f}"
|
||||
fi
|
||||
done
|
||||
|
||||
cp "${ROOT}/configs/castopod/container-koopa-castopod-mariadb.service" "${UNIT_DIR}/"
|
||||
cp "${ROOT}/configs/castopod/container-koopa-castopod-redis.service" "${UNIT_DIR}/"
|
||||
cp "${ROOT}/configs/castopod/container-koopa-castopod.service" "${UNIT_DIR}/"
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable \
|
||||
container-koopa-castopod-mariadb.service \
|
||||
container-koopa-castopod-redis.service \
|
||||
container-koopa-castopod.service
|
||||
|
||||
if ! podman ps --format '{{.Names}}' | grep -qx koopa-castopod; then
|
||||
(cd "${HOME}/koopa-castopod" && set -a && source .env && set +a && podman-compose up -d)
|
||||
fi
|
||||
|
||||
systemctl --user start \
|
||||
container-koopa-castopod-mariadb.service \
|
||||
container-koopa-castopod-redis.service \
|
||||
container-koopa-castopod.service
|
||||
|
||||
systemctl --user --no-pager status \
|
||||
container-koopa-castopod-mariadb.service \
|
||||
container-koopa-castopod-redis.service \
|
||||
container-koopa-castopod.service
|
||||
|
||||
echo "OK. Needs: loginctl enable-linger hernani (already set on koopa)."
|
||||
echo "Public: https://castopod.hacktivism.ch/"
|
||||
116
scripts/castopod/lib.sh
Normal file
116
scripts/castopod/lib.sh
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# shellcheck shell=bash
|
||||
# Shared helpers for Castopod ops on koopa — fail fast, never hang forever.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- defaults (override via env) ---
|
||||
: "${CP_BASEURL:=https://castopod.hacktivism.ch}"
|
||||
: "${CP_COMPOSE_DIR:=/home/hernani/koopa-castopod}"
|
||||
: "${CP_CONNECT_TIMEOUT:=10}" # seconds — TCP connect
|
||||
: "${CP_MAX_TIME:=120}" # seconds — whole HTTP transfer
|
||||
: "${CP_PULL_TIMEOUT:=600}" # seconds — image pull
|
||||
: "${CP_HEALTH_TRIES:=30}" # health poll attempts
|
||||
: "${CP_HEALTH_SLEEP:=5}" # seconds between polls
|
||||
|
||||
# curl that cannot hang forever
|
||||
cp_curl() {
|
||||
curl -sS \
|
||||
--connect-timeout "${CP_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CP_MAX_TIME}" \
|
||||
--retry 2 \
|
||||
--retry-delay 2 \
|
||||
--retry-connrefused \
|
||||
"$@"
|
||||
}
|
||||
|
||||
# HTTP status only (no hang)
|
||||
cp_http_code() {
|
||||
local url=$1
|
||||
cp_curl -o /dev/null -w '%{http_code}' "$url" || echo "000"
|
||||
}
|
||||
|
||||
# Run command with hard wall-clock limit (GNU coreutils timeout)
|
||||
cp_timeout() {
|
||||
local secs=$1
|
||||
shift
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout --signal=TERM --kill-after=15s "${secs}" "$@"
|
||||
else
|
||||
# fallback: no timeout binary — still run, but warn
|
||||
echo "WARN: timeout(1) missing; running without wall limit: $*" >&2
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Non-interactive answer stream for spark / CLI that prompts [y,n] or passwords.
|
||||
# Usage: cp_yes | podman exec -i … php spark …
|
||||
# Prefer SQL for activate when possible (spark prompts hang without TTY).
|
||||
cp_yes() {
|
||||
# enough y's for a few prompts; never block waiting for input
|
||||
yes y 2>/dev/null | head -n 20
|
||||
}
|
||||
|
||||
# Poll until URL returns expected code or give up
|
||||
cp_wait_http() {
|
||||
local url=$1
|
||||
local want=${2:-200}
|
||||
local i code
|
||||
for ((i = 1; i <= CP_HEALTH_TRIES; i++)); do
|
||||
code=$(cp_http_code "$url")
|
||||
echo "health try $i/${CP_HEALTH_TRIES}: $url → $code"
|
||||
if [[ "$code" == "$want" || "$code" =~ ^[23] ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep "${CP_HEALTH_SLEEP}"
|
||||
done
|
||||
echo "ERROR: $url still not healthy after ${CP_HEALTH_TRIES} tries (last=$code)" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# MariaDB password from compose .env (no hang if missing)
|
||||
cp_mysql_password() {
|
||||
local envf="${CP_COMPOSE_DIR}/.env"
|
||||
[[ -f "$envf" ]] || { echo "ERROR: missing $envf" >&2; return 1; }
|
||||
# shellcheck disable=SC1090
|
||||
grep -E '^MYSQL_PASSWORD=' "$envf" | head -1 | cut -d= -f2-
|
||||
}
|
||||
|
||||
cp_mysql() {
|
||||
local pw
|
||||
pw=$(cp_mysql_password)
|
||||
podman exec koopa-castopod-mariadb \
|
||||
mariadb -ucastopod -p"$pw" castopod "$@"
|
||||
}
|
||||
|
||||
# Activate shield users without spark interactive prompt
|
||||
cp_activate_users() {
|
||||
cp_mysql -e "UPDATE cp_users SET active=1 WHERE username IN ('admin','ngi');"
|
||||
}
|
||||
|
||||
# Session login for admin UI automation (uses users.env)
|
||||
cp_admin_login() {
|
||||
local cookie=${1:-/tmp/cp-cj}
|
||||
local envf="${CP_COMPOSE_DIR}/users.env"
|
||||
[[ -f "$envf" ]] || { echo "ERROR: missing $envf" >&2; return 1; }
|
||||
# shellcheck disable=SC1090
|
||||
source "$envf"
|
||||
[[ -n "${ADMIN_PW:-}" ]] || { echo "ERROR: ADMIN_PW empty" >&2; return 1; }
|
||||
|
||||
rm -f "$cookie"
|
||||
cp_curl -c "$cookie" -b "$cookie" "${CP_BASEURL}/cp-auth/login" -o /tmp/cp-login.html
|
||||
local csrf
|
||||
csrf=$(sed -n 's/.*name="csrf_test_name" value="\([^"]*\)".*/\1/p' /tmp/cp-login.html | head -1)
|
||||
[[ -n "$csrf" ]] || { echo "ERROR: no CSRF on login page" >&2; return 1; }
|
||||
|
||||
local code
|
||||
code=$(cp_curl -c "$cookie" -b "$cookie" -o /dev/null -w '%{http_code}' \
|
||||
-X POST "${CP_BASEURL}/cp-auth/login" \
|
||||
--data-urlencode "csrf_test_name=${csrf}" \
|
||||
--data-urlencode "email=admin@castopod.hacktivism.ch" \
|
||||
--data-urlencode "password=${ADMIN_PW}")
|
||||
# 303 see other is success
|
||||
if [[ "$code" != "303" && "$code" != "302" && "$code" != "200" ]]; then
|
||||
echo "ERROR: login HTTP $code" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
21
scripts/castopod/status.sh
Normal file
21
scripts/castopod/status.sh
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
# Quick Castopod health — bounded network, no hangs.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=lib.sh
|
||||
source "${ROOT}/lib.sh"
|
||||
|
||||
echo "== podman =="
|
||||
podman ps --filter name=koopa-castopod --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' || true
|
||||
|
||||
echo "== local backend :9020 =="
|
||||
code=$(cp_http_code "http://127.0.0.1:9020/health" || true)
|
||||
echo "local health → ${code:-000} (307 redirect to public is OK)"
|
||||
|
||||
echo "== public =="
|
||||
# Note: do not put bare @url in curl -w; @ means "read file" in some curl contexts.
|
||||
for path in "/" "/@foss" "/@foss/feed.xml" "/@foss/episodes/four-freedoms"; do
|
||||
url="${CP_BASEURL}${path}"
|
||||
code=$(cp_http_code "$url")
|
||||
printf ' %s → %s\n' "$url" "$code"
|
||||
done
|
||||
32
scripts/castopod/up.sh
Normal file
32
scripts/castopod/up.sh
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env bash
|
||||
# Start Castopod stack with hard timeouts on pull/up (no infinite hang).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=lib.sh
|
||||
source "${ROOT}/lib.sh"
|
||||
|
||||
cd "${CP_COMPOSE_DIR}"
|
||||
|
||||
echo "== pull (max ${CP_PULL_TIMEOUT}s) =="
|
||||
cp_timeout "${CP_PULL_TIMEOUT}" podman-compose pull
|
||||
|
||||
echo "== up =="
|
||||
cp_timeout 180 podman-compose up -d
|
||||
|
||||
echo "== wait for app on :9020 =="
|
||||
# Castopod often 307 from /health to https — accept 2xx/3xx
|
||||
ok=0
|
||||
for ((i = 1; i <= CP_HEALTH_TRIES; i++)); do
|
||||
code=$(cp_http_code "http://127.0.0.1:9020/" || true)
|
||||
echo "try $i: local / → $code"
|
||||
if [[ "$code" =~ ^[23] ]]; then
|
||||
ok=1
|
||||
break
|
||||
fi
|
||||
sleep "${CP_HEALTH_SLEEP}"
|
||||
done
|
||||
[[ "$ok" -eq 1 ]] || { echo "ERROR: app not answering on 9020"; podman-compose ps; exit 1; }
|
||||
|
||||
echo "== public via Caddy =="
|
||||
cp_wait_http "${CP_BASEURL}/" || true
|
||||
echo "done."
|
||||
249
scripts/monitoring/countries.txt
Executable file
249
scripts/monitoring/countries.txt
Executable file
|
|
@ -0,0 +1,249 @@
|
|||
AF=Afghanistan
|
||||
AX=Åland Islands
|
||||
AL=Albania
|
||||
DZ=Algeria
|
||||
AS=American Samoa
|
||||
AD=Andorra
|
||||
AO=Angola
|
||||
AI=Anguilla
|
||||
AQ=Antarctica
|
||||
AG=Antigua and Barbuda
|
||||
AR=Argentina
|
||||
AM=Armenia
|
||||
AW=Aruba
|
||||
AU=Australia
|
||||
AT=Austria
|
||||
AZ=Azerbaijan
|
||||
BS=Bahamas
|
||||
BH=Bahrain
|
||||
BD=Bangladesh
|
||||
BB=Barbados
|
||||
BY=Belarus
|
||||
BE=Belgium
|
||||
BZ=Belize
|
||||
BJ=Benin
|
||||
BM=Bermuda
|
||||
BT=Bhutan
|
||||
BO=Bolivia
|
||||
BQ=Bonaire, Sint Eustatius and Saba
|
||||
BA=Bosnia and Herzegovina
|
||||
BW=Botswana
|
||||
BV=Bouvet Island
|
||||
BR=Brazil
|
||||
IO=British Indian Ocean Territory
|
||||
BN=Brunei Darussalam
|
||||
BG=Bulgaria
|
||||
BF=Burkina Faso
|
||||
BI=Burundi
|
||||
KH=Cambodia
|
||||
CM=Cameroon
|
||||
CA=Canada
|
||||
CV=Cabo Verde
|
||||
KY=Cayman Islands
|
||||
CF=Central African Republic
|
||||
TD=Chad
|
||||
CL=Chile
|
||||
CN=China
|
||||
CX=Christmas Island
|
||||
CC=Cocos (Keeling) Islands
|
||||
CO=Colombia
|
||||
KM=Comoros
|
||||
CG=Congo
|
||||
CD=Congo (DRC)
|
||||
CK=Cook Islands
|
||||
CR=Costa Rica
|
||||
CI=Côte d'Ivoire
|
||||
HR=Croatia
|
||||
CU=Cuba
|
||||
CW=Curaçao
|
||||
CY=Cyprus
|
||||
CZ=Czechia
|
||||
DK=Denmark
|
||||
DJ=Djibouti
|
||||
DM=Dominica
|
||||
DO=Dominican Republic
|
||||
EC=Ecuador
|
||||
EG=Egypt
|
||||
SV=El Salvador
|
||||
GQ=Equatorial Guinea
|
||||
ER=Eritrea
|
||||
EE=Estonia
|
||||
SZ=Eswatini
|
||||
ET=Ethiopia
|
||||
FK=Falkland Islands
|
||||
FO=Faroe Islands
|
||||
FJ=Fiji
|
||||
FI=Finland
|
||||
FR=France
|
||||
GF=French Guiana
|
||||
PF=French Polynesia
|
||||
TF=French Southern Territories
|
||||
GA=Gabon
|
||||
GM=Gambia
|
||||
GE=Georgia
|
||||
DE=Germany
|
||||
GH=Ghana
|
||||
GI=Gibraltar
|
||||
GR=Greece
|
||||
GL=Greenland
|
||||
GD=Grenada
|
||||
GP=Guadeloupe
|
||||
GU=Guam
|
||||
GT=Guatemala
|
||||
GG=Guernsey
|
||||
GN=Guinea
|
||||
GW=Guinea-Bissau
|
||||
GY=Guyana
|
||||
HT=Haiti
|
||||
HM=Heard Island and McDonald Islands
|
||||
VA=Holy See
|
||||
HN=Honduras
|
||||
HK=Hong Kong
|
||||
HU=Hungary
|
||||
IS=Iceland
|
||||
IN=India
|
||||
ID=Indonesia
|
||||
IR=Iran
|
||||
IQ=Iraq
|
||||
IE=Ireland
|
||||
IM=Isle of Man
|
||||
IL=Israel
|
||||
IT=Italy
|
||||
JM=Jamaica
|
||||
JP=Japan
|
||||
JE=Jersey
|
||||
JO=Jordan
|
||||
KZ=Kazakhstan
|
||||
KE=Kenya
|
||||
KI=Kiribati
|
||||
KP=North Korea
|
||||
KR=South Korea
|
||||
KW=Kuwait
|
||||
KG=Kyrgyzstan
|
||||
LA=Laos
|
||||
LV=Latvia
|
||||
LB=Lebanon
|
||||
LS=Lesotho
|
||||
LR=Liberia
|
||||
LY=Libya
|
||||
LI=Liechtenstein
|
||||
LT=Lithuania
|
||||
LU=Luxembourg
|
||||
MO=Macao
|
||||
MG=Madagascar
|
||||
MW=Malawi
|
||||
MY=Malaysia
|
||||
MV=Maldives
|
||||
ML=Mali
|
||||
MT=Malta
|
||||
MH=Marshall Islands
|
||||
MQ=Martinique
|
||||
MR=Mauritania
|
||||
MU=Mauritius
|
||||
YT=Mayotte
|
||||
MX=Mexico
|
||||
FM=Micronesia
|
||||
MD=Moldova
|
||||
MC=Monaco
|
||||
MN=Mongolia
|
||||
ME=Montenegro
|
||||
MS=Montserrat
|
||||
MA=Morocco
|
||||
MZ=Mozambique
|
||||
MM=Myanmar
|
||||
NA=Namibia
|
||||
NR=Nauru
|
||||
NP=Nepal
|
||||
NL=Netherlands
|
||||
NC=New Caledonia
|
||||
NZ=New Zealand
|
||||
NI=Nicaragua
|
||||
NE=Niger
|
||||
NG=Nigeria
|
||||
NU=Niue
|
||||
NF=Norfolk Island
|
||||
MK=North Macedonia
|
||||
MP=Northern Mariana Islands
|
||||
NO=Norway
|
||||
OM=Oman
|
||||
PK=Pakistan
|
||||
PW=Palau
|
||||
PS=Palestine
|
||||
PA=Panama
|
||||
PG=Papua New Guinea
|
||||
PY=Paraguay
|
||||
PE=Peru
|
||||
PH=Philippines
|
||||
PN=Pitcairn
|
||||
PL=Poland
|
||||
PT=Portugal
|
||||
PR=Puerto Rico
|
||||
QA=Qatar
|
||||
RE=Réunion
|
||||
RO=Romania
|
||||
RU=Russia
|
||||
RW=Rwanda
|
||||
BL=Saint Barthélemy
|
||||
SH=Saint Helena
|
||||
KN=Saint Kitts and Nevis
|
||||
LC=Saint Lucia
|
||||
MF=Saint Martin
|
||||
PM=Saint Pierre and Miquelon
|
||||
VC=Saint Vincent and the Grenadines
|
||||
WS=Samoa
|
||||
SM=San Marino
|
||||
ST=Sao Tome and Principe
|
||||
SA=Saudi Arabia
|
||||
SN=Senegal
|
||||
RS=Serbia
|
||||
SC=Seychelles
|
||||
SL=Sierra Leone
|
||||
SG=Singapore
|
||||
SX=Sint Maarten
|
||||
SK=Slovakia
|
||||
SI=Slovenia
|
||||
SB=Solomon Islands
|
||||
SO=Somalia
|
||||
ZA=South Africa
|
||||
GS=South Georgia and the South Sandwich Islands
|
||||
SS=South Sudan
|
||||
ES=Spain
|
||||
LK=Sri Lanka
|
||||
SD=Sudan
|
||||
SR=Suriname
|
||||
SJ=Svalbard and Jan Mayen
|
||||
SE=Sweden
|
||||
CH=Switzerland
|
||||
SY=Syria
|
||||
TW=Taiwan
|
||||
TJ=Tajikistan
|
||||
TZ=Tanzania
|
||||
TH=Thailand
|
||||
TL=Timor-Leste
|
||||
TG=Togo
|
||||
TK=Tokelau
|
||||
TO=Tonga
|
||||
TT=Trinidad and Tobago
|
||||
TN=Tunisia
|
||||
TR=Türkiye
|
||||
TM=Turkmenistan
|
||||
TC=Turks and Caicos Islands
|
||||
TV=Tuvalu
|
||||
UG=Uganda
|
||||
UA=Ukraine
|
||||
AE=United Arab Emirates
|
||||
GB=United Kingdom
|
||||
US=United States
|
||||
UM=U.S. Minor Outlying Islands
|
||||
UY=Uruguay
|
||||
UZ=Uzbekistan
|
||||
VU=Vanuatu
|
||||
VE=Venezuela
|
||||
VN=Vietnam
|
||||
VG=Virgin Islands (British)
|
||||
VI=Virgin Islands (U.S.)
|
||||
WF=Wallis and Futuna
|
||||
EH=Western Sahara
|
||||
YE=Yemen
|
||||
ZM=Zambia
|
||||
ZW=Zimbabwe
|
||||
8
scripts/monitoring/tor_inbound_connects.sh
Executable file
8
scripts/monitoring/tor_inbound_connects.sh
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
for ((;;))
|
||||
do
|
||||
date &&
|
||||
echo -n "Tor IPv4 inbound connects: "; ss -s -4 | grep 192.168.100.95:https | wc -l &&
|
||||
echo -n "Tor IPv6 inbound connects: "; ss -s -6 | grep -E '::1]:https' | wc -l &&
|
||||
sleep 3600
|
||||
done
|
||||
2
scripts/monitoring/tor_show_relays--koopa+firecuda.sh
Executable file
2
scripts/monitoring/tor_show_relays--koopa+firecuda.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
for ((;;)); do date && time ./tor_show_relays.sh -5000 | grep '|' | cat -n | grep -Ei 'koopa|firecuda' && sleep 86400; done
|
||||
108
scripts/monitoring/tor_show_relays.sh
Executable file
108
scripts/monitoring/tor_show_relays.sh
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
#!/bin/bash
|
||||
|
||||
CACHE_DIR="cache"
|
||||
COUNTRY_FILE="countries.txt"
|
||||
DEFAULT_TOP=100
|
||||
TOPN=$DEFAULT_TOP
|
||||
|
||||
mkdir -p "$CACHE_DIR"
|
||||
|
||||
# Parse -N flag
|
||||
if [[ "$1" =~ ^-([0-9]+)$ ]]; then
|
||||
TOPN="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
|
||||
# ===========================
|
||||
# LOAD COUNTRY NAMES
|
||||
# ===========================
|
||||
|
||||
declare -A COUNTRY_NAME
|
||||
while IFS='=' read -r ISO NAME; do
|
||||
[[ -z "$ISO" ]] && continue
|
||||
COUNTRY_NAME["$ISO"]="$NAME"
|
||||
done < "$COUNTRY_FILE"
|
||||
|
||||
# ===========================
|
||||
# SMART FETCH (ETag-based)
|
||||
# ===========================
|
||||
|
||||
fetch_if_new() {
|
||||
local url="$1"
|
||||
local outfile="$2"
|
||||
local etagfile="${outfile}.etag"
|
||||
|
||||
echo "→ Checking $outfile"
|
||||
curl -s \
|
||||
--etag-save "$etagfile" \
|
||||
--etag-compare "$etagfile" \
|
||||
-o "$outfile" \
|
||||
"$url"
|
||||
|
||||
echo " Size: $(du -h "$outfile" | cut -f1)"
|
||||
}
|
||||
|
||||
DETAILS_JSON="$CACHE_DIR/details.json"
|
||||
BANDWIDTH_JSON="$CACHE_DIR/bandwidth.json"
|
||||
MERGED="$CACHE_DIR/merged.txt"
|
||||
|
||||
echo "=== STEP 1: Downloading (if new) ==="
|
||||
fetch_if_new \
|
||||
"https://onionoo.torproject.org/details?type=relay&fields=fingerprint,country,nickname" \
|
||||
"$DETAILS_JSON"
|
||||
|
||||
fetch_if_new \
|
||||
"https://onionoo.torproject.org/bandwidth?type=relay&fields=fingerprint,write_history" \
|
||||
"$BANDWIDTH_JSON"
|
||||
|
||||
# ===========================
|
||||
# STEP 2: MERGE EVERYTHING IN ONE jq PASS
|
||||
# ===========================
|
||||
|
||||
echo
|
||||
echo "=== STEP 2: Merging in jq (single pass) ==="
|
||||
|
||||
jq -s -r '
|
||||
# Build index of details by fingerprint
|
||||
(.[0].relays
|
||||
| map({
|
||||
fp: .fingerprint,
|
||||
country: (.country // "??"),
|
||||
nickname: (.nickname // "UnknownRelay")
|
||||
})
|
||||
| INDEX(.fp)
|
||||
) as $d
|
||||
|
||||
# Iterate over bandwidth relays
|
||||
| .[1].relays[]
|
||||
| .fingerprint as $fp
|
||||
| ($d[$fp].country) as $cc
|
||||
| ($d[$fp].nickname) as $nick
|
||||
| (.write_history["1_month"].factor // 0) as $bw
|
||||
|
||||
# Output: bw fp cc nickname
|
||||
| "\($bw) \($fp) \($cc) \($nick)"
|
||||
' "$DETAILS_JSON" "$BANDWIDTH_JSON" > "$MERGED"
|
||||
|
||||
echo " Merged lines: $(wc -l < "$MERGED")"
|
||||
|
||||
# ===========================
|
||||
# STEP 3: SORT + PRINT
|
||||
# ===========================
|
||||
|
||||
echo
|
||||
echo "=== STEP 3: Sorting and printing ==="
|
||||
echo
|
||||
echo "=== Top $TOPN Tor Relays (by 1-month write factor) ==="
|
||||
echo
|
||||
|
||||
sort -nr "$MERGED" | head -n "$TOPN" | while read -r BW FP CC NICK; do
|
||||
CC_UP=$(echo "$CC" | tr '[:lower:]' '[:upper:]')
|
||||
FULL="${COUNTRY_NAME[$CC_UP]}"
|
||||
[[ -z "$FULL" ]] && FULL="$NICK"
|
||||
|
||||
printf "%-40s | %12.2f | %-20s | %2s (%s)\n" "$FP" "$BW" "$NICK" "$CC_UP" "$FULL"
|
||||
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Done."
|
||||
83
scripts/monitoring/tor_stats_per_country.sh
Executable file
83
scripts/monitoring/tor_stats_per_country.sh
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
#!/bin/bash
|
||||
|
||||
PORT=8080
|
||||
DB="/usr/share/GeoIP/GeoLite2-Country.mmdb"
|
||||
LOGFILE="/dev/null"
|
||||
COUNTRY_FILE="countries.txt"
|
||||
|
||||
declare -A COUNTRY_COUNT
|
||||
declare -A SEEN
|
||||
declare -A COUNTRY_NAME
|
||||
|
||||
# --- Parse -N flag (e.g. -10 means show top 10) ---
|
||||
TOPN=0
|
||||
if [[ "$1" =~ ^-([0-9]+)$ ]]; then
|
||||
TOPN="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
|
||||
# --- Load external country list ---
|
||||
while IFS='=' read -r ISO NAME; do
|
||||
[[ -z "$ISO" ]] && continue
|
||||
COUNTRY_NAME["$ISO"]="$NAME"
|
||||
done < "$COUNTRY_FILE"
|
||||
|
||||
echo "Monitoring port $PORT..."
|
||||
echo "Updating table every 5 seconds."
|
||||
[[ $TOPN -gt 0 ]] && echo "Showing only Top $TOPN countries."
|
||||
|
||||
LAST_REFRESH=0
|
||||
|
||||
while true; do
|
||||
# FAST LOOP: collect new connections
|
||||
IPS=$(ss -tn sport = :$PORT | awk 'NR>1 {print $5}' | cut -d: -f1)
|
||||
|
||||
for IP in $IPS; do
|
||||
[[ "$IP" == "127.0.0.1" ]] && continue
|
||||
|
||||
if [[ -z "${SEEN[$IP]}" ]]; then
|
||||
SEEN[$IP]=1
|
||||
|
||||
RAW=$(mmdblookup --file "$DB" --ip "$IP" country iso_code 2>/dev/null)
|
||||
ISO=$(echo "$RAW" | grep -oE '[A-Z]{2}')
|
||||
[[ -z "$ISO" ]] && ISO="UNKNOWN"
|
||||
|
||||
COUNTRY_COUNT["$ISO"]=$(( COUNTRY_COUNT["$ISO"] + 1 ))
|
||||
|
||||
NAME="${COUNTRY_NAME[$ISO]}"
|
||||
[[ -z "$NAME" ]] && NAME="Unknown Country"
|
||||
|
||||
echo "$(date '+%F %T') - $IP - $ISO ($NAME)" >> "$LOGFILE"
|
||||
fi
|
||||
done
|
||||
|
||||
# SLOW LOOP: refresh display every 5 seconds
|
||||
NOW=$(date +%s)
|
||||
if (( NOW - LAST_REFRESH >= 5 )); then
|
||||
LAST_REFRESH=$NOW
|
||||
|
||||
clear
|
||||
echo "=== Live GeoIP Stats (Port $PORT) ==="
|
||||
echo "(Updated: $(date '+%H:%M:%S'))"
|
||||
echo
|
||||
|
||||
# Sort by count (descending)
|
||||
SORTED=$(for ISO in "${!COUNTRY_COUNT[@]}"; do
|
||||
echo "${COUNTRY_COUNT[$ISO]} $ISO"
|
||||
done | sort -rn)
|
||||
|
||||
COUNT=0
|
||||
while read -r LINE; do
|
||||
NUM=$(echo "$LINE" | awk '{print $1}')
|
||||
ISO=$(echo "$LINE" | awk '{print $2}')
|
||||
NAME="${COUNTRY_NAME[$ISO]}"
|
||||
[[ -z "$NAME" ]] && NAME="Unknown Country"
|
||||
|
||||
echo "$ISO ($NAME): $NUM"
|
||||
|
||||
((COUNT++))
|
||||
[[ $TOPN -gt 0 && $COUNT -ge $TOPN ]] && break
|
||||
done <<< "$SORTED"
|
||||
fi
|
||||
|
||||
sleep 0.1
|
||||
done
|
||||
71
scripts/taler-bank/README.md
Normal file
71
scripts/taler-bank/README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# taler-bank scripts
|
||||
|
||||
Container: **`taler-hacktivism-bank`** (libeufin-bank, GOA, **no IBAN**).
|
||||
|
||||
| File | Container path | User |
|
||||
|------|----------------|------|
|
||||
| `start_base_services_for_taler_bank.sh` | `/root/` | **root** |
|
||||
| `start_bank.sh` | `/usr/local/bin/` | **libeufin-bank** |
|
||||
| `check_bank-health.sh` | `/usr/local/bin/` | libeufin-bank / any |
|
||||
| `landing-stats.sh` | `/usr/local/bin/` | root (in container) — writes `/var/www/bank-landing/stats.json` |
|
||||
| `landing-stats-install.sh` | host only | root/podman — copies + runs + optional cron |
|
||||
| `demo-withdraw-api.py` | `/usr/local/bin/` | root — loopback **:19096** |
|
||||
| `install-demo-withdraw-api.sh` | host only | installs API + nginx + auto-confirm |
|
||||
| `auto-confirm-withdrawals.sh` | `/usr/local/bin/` | root — **explorer-only** confirm loop |
|
||||
| `refresh-demo-withdraw.sh` | `/usr/local/bin/` | refresh static `withdraw.uri` |
|
||||
| `credit-account.sh` | host/ops | admin → user credit |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# root in container
|
||||
./start_base_services_for_taler_bank.sh
|
||||
# then as libeufin-bank in /usr/local/bin:
|
||||
./start_bank.sh --restart
|
||||
```
|
||||
|
||||
## Landing stats (inside container)
|
||||
|
||||
```bash
|
||||
# on koopa host — copy + run (writes /var/www/bank-landing/stats.json)
|
||||
./landing-stats-install.sh
|
||||
./landing-stats-install.sh --cron # every minute inside container (* * * * *)
|
||||
./landing-stats-install.sh --run-only
|
||||
```
|
||||
|
||||
Details + JSON schema: `configs/bank-landing/README.md`.
|
||||
|
||||
## Demo withdraw + auto-account API
|
||||
|
||||
`demo-withdraw-api.py` listens on **127.0.0.1:19096** (proxied by nginx on the landing):
|
||||
|
||||
| Path | Behaviour |
|
||||
|------|-----------|
|
||||
| `GET /demo-withdraw.json` | Mint one-shot withdraw from shared **`explorer`** pool; write `withdraw.uri` + watch ids |
|
||||
| `GET /auto-account.json` | Public `POST /accounts` with generated **`goa-account-<random>`** user + password containing **pleasechangeme**; **balance GOA:0**; return credentials once |
|
||||
|
||||
Install / restart:
|
||||
|
||||
```bash
|
||||
./install-demo-withdraw-api.sh
|
||||
# Public checks:
|
||||
curl -sS https://bank.hacktivism.ch/intro/demo-withdraw.json | head
|
||||
curl -sS https://bank.hacktivism.ch/intro/auto-account.json | head # creates a real account
|
||||
```
|
||||
|
||||
Requires **python3** in the bank container. Env: `BANK_URL`, `BANK_USER`/`BANK_PASS`
|
||||
(or `/root/bank-explorer-password.txt`), `AMOUNT` (default `GOA:10` for shared withdraws).
|
||||
|
||||
### Auto-confirm (explorer only)
|
||||
|
||||
```bash
|
||||
# loop inside container — refuses non-explorer unless ALLOW_NON_EXPLORER=1
|
||||
auto-confirm-withdrawals.sh --loop 4
|
||||
```
|
||||
|
||||
Only confirms withdrawals owned by **`explorer`** when status is `selected`
|
||||
(community demo path). Does not confirm arbitrary customer withdraws.
|
||||
|
||||
## Config
|
||||
|
||||
See `configs/taler-hacktivism-bank/` and `configs/bank-landing/`.
|
||||
143
scripts/taler-bank/auto-confirm-withdrawals.sh
Executable file
143
scripts/taler-bank/auto-confirm-withdrawals.sh
Executable file
|
|
@ -0,0 +1,143 @@
|
|||
#!/bin/bash
|
||||
# Auto-confirm bank withdrawals for the community demo pool ONLY.
|
||||
#
|
||||
# Only account: explorer (override only if you really mean another pool user
|
||||
# via BANK_USER, but still confirms with that user's token only — never
|
||||
# confirms other customers' withdrawals).
|
||||
#
|
||||
# Run once: auto-confirm-withdrawals.sh
|
||||
# Loop: auto-confirm-withdrawals.sh --loop [SECS]
|
||||
#
|
||||
# Env:
|
||||
# BANK_URL (default http://127.0.0.1:9012)
|
||||
# BANK_USER (default explorer) — must be the pool account
|
||||
# BANK_PASS or /root/bank-explorer-password.txt
|
||||
# LANDING_DIR (default /var/www/bank-landing)
|
||||
# ALLOW_NON_EXPLORER=1 — allow BANK_USER other than explorer (off by default)
|
||||
set -euo pipefail
|
||||
|
||||
BANK="${BANK_URL:-http://127.0.0.1:9012}"
|
||||
BANK="${BANK%/}"
|
||||
USER="${BANK_USER:-explorer}"
|
||||
LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
|
||||
LOOP=0
|
||||
SLEEP=5
|
||||
if [ "${1:-}" = "--loop" ]; then
|
||||
LOOP=1
|
||||
SLEEP="${2:-5}"
|
||||
fi
|
||||
|
||||
# Safety: only the shared community account unless explicitly overridden
|
||||
if [ "$USER" != "explorer" ] && [ "${ALLOW_NON_EXPLORER:-0}" != "1" ]; then
|
||||
echo "refusing BANK_USER=$USER — auto-confirm is for explorer only (set ALLOW_NON_EXPLORER=1 to override)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASS="${BANK_PASS:-}"
|
||||
if [ -z "$PASS" ]; then
|
||||
for f in "/root/bank-${USER}-password.txt" /root/bank-explorer-password.txt; do
|
||||
if [ -f "$f" ]; then PASS=$(tr -d '\n' <"$f"); break; fi
|
||||
done
|
||||
fi
|
||||
[ -n "$PASS" ] || { echo "no password for $USER" >&2; exit 1; }
|
||||
|
||||
# JSON field extract without python (bank container may lack python3)
|
||||
json_str() {
|
||||
# json_str FIELD < json-text-or-file
|
||||
local field="$1"
|
||||
local data
|
||||
if [ -f "${2:-}" ]; then data=$(cat "$2"); else data=$(cat); fi
|
||||
printf '%s' "$data" | sed -n "s/.*\"${field}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -1
|
||||
}
|
||||
|
||||
token() {
|
||||
curl -sS -m 12 -u "${USER}:${PASS}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"scope":"readwrite","refreshable":true}' \
|
||||
"${BANK}/accounts/${USER}/token"
|
||||
}
|
||||
|
||||
# IDs created for the community pool (landing + watch list only)
|
||||
known_ids() {
|
||||
if [ -f "${LANDING_DIR}/withdraw.uri" ]; then
|
||||
basename "$(tr -d '\n' <"${LANDING_DIR}/withdraw.uri")"
|
||||
fi
|
||||
if [ -f "${LANDING_DIR}/withdraw-watch.ids" ]; then
|
||||
# strip empty / comments
|
||||
grep -E '^[0-9a-fA-F-]{36}$' "${LANDING_DIR}/withdraw-watch.ids" || true
|
||||
fi
|
||||
}
|
||||
|
||||
confirm_one() {
|
||||
local wid="$1"
|
||||
local tok="$2"
|
||||
local info st uname conf
|
||||
|
||||
# Public status — must belong to explorer (pool), not another customer
|
||||
info=$(curl -sS -m 10 "${BANK}/withdrawals/${wid}" 2>/dev/null || true)
|
||||
[ -n "$info" ] || return 0
|
||||
|
||||
st=$(printf '%s' "$info" | sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||
uname=$(printf '%s' "$info" | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||
|
||||
# Only confirm withdrawals owned by the pool account
|
||||
if [ -n "$uname" ] && [ "$uname" != "$USER" ]; then
|
||||
echo "skip $wid owner=$uname (only confirm $USER)"
|
||||
return 0
|
||||
fi
|
||||
# If API omits username, still only confirm via explorer token (cannot confirm others)
|
||||
if [ -z "$uname" ]; then
|
||||
# require sender_wire / payto to mention explorer when present
|
||||
case "$info" in
|
||||
*explorer*) ;;
|
||||
*)
|
||||
# still try only if status selected — confirm endpoint is under explorer account
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ "$st" != "selected" ]; then
|
||||
echo "skip $wid status=${st:-?} owner=${uname:-?}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "confirming $wid as $USER (community pool) ..."
|
||||
conf=$(curl -sS -m 15 -o /tmp/acw-conf.out -w '%{http_code}' \
|
||||
-X POST \
|
||||
-H "Authorization: Bearer ${tok}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{}' \
|
||||
"${BANK}/accounts/${USER}/withdrawals/${wid}/confirm")
|
||||
echo " HTTP $conf $(head -c 200 /tmp/acw-conf.out 2>/dev/null || true)"
|
||||
curl -sS -m 8 "${BANK}/withdrawals/${wid}" 2>/dev/null \
|
||||
| sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/ now status=\1/p' | head -1 || true
|
||||
}
|
||||
|
||||
once() {
|
||||
local tjson tok ids
|
||||
tjson=$(token)
|
||||
tok=$(printf '%s' "$tjson" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||
if [ -z "$tok" ]; then
|
||||
echo "token fail for $USER: $tjson" >&2
|
||||
return 1
|
||||
fi
|
||||
ids=$(known_ids | sort -u)
|
||||
if [ -z "$ids" ]; then
|
||||
echo "no withdrawal ids to watch (landing withdraw.uri / withdraw-watch.ids)"
|
||||
return 0
|
||||
fi
|
||||
while read -r wid; do
|
||||
[ -n "$wid" ] || continue
|
||||
confirm_one "$wid" "$tok"
|
||||
done <<<"$ids"
|
||||
}
|
||||
|
||||
if [ "$LOOP" -eq 1 ]; then
|
||||
echo "auto-confirm loop every ${SLEEP}s for pool user=$USER only"
|
||||
while true; do
|
||||
once || true
|
||||
sleep "$SLEEP"
|
||||
done
|
||||
else
|
||||
once
|
||||
fi
|
||||
48
scripts/taler-bank/check_bank-health.sh
Executable file
48
scripts/taler-bank/check_bank-health.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
#!/bin/bash
|
||||
# Health check for manual libeufin-bank (same style as merchant/exchange health).
|
||||
# Container: taler-hacktivism-bank
|
||||
|
||||
CONF=/etc/libeufin/libeufin-bank.conf
|
||||
PORT=$(grep -E '^\s*PORT\s*=' /etc/libeufin/bank-overrides.conf 2>/dev/null | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}')
|
||||
PORT=${PORT:-9012}
|
||||
|
||||
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; }
|
||||
|
||||
echo "=== Taler Bank (libeufin-bank) Health Check ==="
|
||||
|
||||
if pgrep -f 'libeufin-bank serve|MainKt serve|tech.libeufin.bank.MainKt' >/dev/null 2>&1; then
|
||||
ok "process libeufin-bank serve"
|
||||
else
|
||||
bad "process libeufin-bank serve is NOT running"
|
||||
fi
|
||||
|
||||
if curl -sf -m 3 "http://127.0.0.1:${PORT}/config" >/dev/null 2>&1; then
|
||||
ok "HTTP /config on 127.0.0.1:${PORT}"
|
||||
else
|
||||
bad "no HTTP response on 127.0.0.1:${PORT}/config"
|
||||
fi
|
||||
|
||||
if curl -sf -m 3 "http://127.0.0.1:${PORT}/taler-integration/config" >/dev/null 2>&1; then
|
||||
ok "HTTP /taler-integration/config on 127.0.0.1:${PORT}"
|
||||
else
|
||||
yellow "[WARN] no HTTP response on /taler-integration/config"
|
||||
fi
|
||||
|
||||
if pg_isready >/dev/null 2>&1; then
|
||||
ok "postgresql accepting connections"
|
||||
else
|
||||
yellow "[WARN] postgresql not ready (or pg_isready missing)"
|
||||
fi
|
||||
|
||||
if [ "$fail" -eq 0 ]; then
|
||||
green "=== ALL CRITICAL CHECKS PASSED ==="
|
||||
exit 0
|
||||
fi
|
||||
red "=== SOME CHECKS FAILED ==="
|
||||
exit 1
|
||||
96
scripts/taler-bank/credit-account.sh
Executable file
96
scripts/taler-bank/credit-account.sh
Executable file
|
|
@ -0,0 +1,96 @@
|
|||
#!/bin/bash
|
||||
# Credit a bank user by transferring from admin (creates regional GOA via admin debit).
|
||||
# Run as root on koopa host (bank on 127.0.0.1:9012).
|
||||
#
|
||||
# Usage:
|
||||
# credit-account.sh [USERNAME] [AMOUNT]
|
||||
# credit-account.sh explorer GOA:1000
|
||||
set -euo pipefail
|
||||
|
||||
BANK="${BANK_URL:-http://127.0.0.1:9012}"
|
||||
TO_USER="${1:-explorer}"
|
||||
AMOUNT="${2:-GOA:1000}"
|
||||
ADMIN_PASS="${BANK_ADMIN_PASS:-}"
|
||||
if [ -z "$ADMIN_PASS" ] && [ -f /root/bank-admin-password.txt ]; then
|
||||
ADMIN_PASS=$(tr -d '\n' </root/bank-admin-password.txt)
|
||||
fi
|
||||
[ -n "$ADMIN_PASS" ] || { echo "Need BANK_ADMIN_PASS or /root/bank-admin-password.txt" >&2; exit 1; }
|
||||
|
||||
W=$(mktemp -d)
|
||||
trap 'rm -rf "$W"' EXIT
|
||||
|
||||
echo '{"scope":"readwrite"}' >"$W/tok.json"
|
||||
curl -sS -m 15 -u "admin:${ADMIN_PASS}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @"$W/tok.json" \
|
||||
"${BANK}/accounts/admin/token" >"$W/tok.out"
|
||||
TOKEN=$(python3 -c "import json;print(json.load(open('$W/tok.out')).get('access_token',''))")
|
||||
[ -n "$TOKEN" ] || { echo "admin token failed:"; cat "$W/tok.out"; exit 1; }
|
||||
|
||||
# payto for x-taler-bank regional accounts
|
||||
PAYTO="payto://x-taler-bank/bank.hacktivism.ch/${TO_USER}?receiver-name=${TO_USER}&message=credit-from-admin"
|
||||
|
||||
python3 - "$AMOUNT" "$PAYTO" "$W/tx.json" <<'PY'
|
||||
import json, sys, os
|
||||
amount, payto, out = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
# ShortHashCode: Crockford base32 of 32 random bytes (52 chars)
|
||||
_ALPH = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
||||
def crockford32(data: bytes) -> str:
|
||||
n = int.from_bytes(data, "big")
|
||||
bits = len(data) * 8
|
||||
outc = []
|
||||
while bits > 0:
|
||||
bits -= 5
|
||||
outc.append(_ALPH[(n >> bits) & 31] if bits >= 0 else _ALPH[(n << (-bits)) & 31])
|
||||
if bits <= 0:
|
||||
break
|
||||
# pad to full groups
|
||||
s = "".join(outc)
|
||||
# simpler bit stream
|
||||
return None
|
||||
def crock32(b: bytes) -> str:
|
||||
bits = 0
|
||||
val = 0
|
||||
outc = []
|
||||
for byte in b:
|
||||
val = (val << 8) | byte
|
||||
bits += 8
|
||||
while bits >= 5:
|
||||
bits -= 5
|
||||
outc.append(_ALPH[(val >> bits) & 31])
|
||||
if bits:
|
||||
outc.append(_ALPH[(val << (5 - bits)) & 31])
|
||||
return "".join(outc)
|
||||
uid = crock32(os.urandom(32))
|
||||
json.dump({"payto_uri": payto, "amount": amount, "request_uid": uid}, open(out, "w"))
|
||||
print("request_uid", uid, "len", len(uid))
|
||||
PY
|
||||
|
||||
echo "POST admin -> $TO_USER amount=$AMOUNT"
|
||||
curl -sS -m 15 \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @"$W/tx.json" \
|
||||
"${BANK}/accounts/admin/transactions" | tee "$W/tx.out"
|
||||
echo
|
||||
|
||||
# show balances
|
||||
for u in admin "$TO_USER"; do
|
||||
curl -sS -m 10 -u "admin:${ADMIN_PASS}" \
|
||||
-H 'Content-Type: application/json' -d '{"scope":"readonly"}' \
|
||||
"${BANK}/accounts/admin/token" >"$W/rtok" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# re-token readonly and print target balance
|
||||
echo '{"scope":"readonly"}' >"$W/rtok.json"
|
||||
# admin can GET any account
|
||||
curl -sS -m 10 -H "Authorization: Bearer ${TOKEN}" \
|
||||
"${BANK}/accounts/${TO_USER}" | tee "$W/acc.out"
|
||||
echo
|
||||
python3 -c "
|
||||
import json
|
||||
d=json.load(open('$W/acc.out'))
|
||||
b=d.get('balance') or {}
|
||||
print('RESULT', '${TO_USER}', 'balance=', b.get('amount'), b.get('credit_debit_indicator'))
|
||||
print('debit_threshold=', d.get('debit_threshold'))
|
||||
"
|
||||
316
scripts/taler-bank/demo-withdraw-api.py
Executable file
316
scripts/taler-bank/demo-withdraw-api.py
Executable file
|
|
@ -0,0 +1,316 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTTP helper for bank landing:
|
||||
- GET /demo-withdraw.json — mint shared-pool (explorer) demo withdraw
|
||||
- GET /auto-account.json — create a personal bank account (balance 0)
|
||||
and return one-time credentials for the user to copy
|
||||
|
||||
Listens on 127.0.0.1:19096 (only inside bank container / localhost).
|
||||
Nginx proxies /intro/*.json → this service.
|
||||
|
||||
Env:
|
||||
BANK_URL default http://127.0.0.1:9012
|
||||
BANK_USER default explorer
|
||||
BANK_PASS or /root/bank-explorer-password.txt
|
||||
AMOUNT default GOA:10
|
||||
LANDING_DIR default /var/www/bank-landing
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import ssl
|
||||
import string
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
BANK = os.environ.get("BANK_URL", "http://127.0.0.1:9012").rstrip("/")
|
||||
# Public HTTPS base (Caddy) — absolute webui / login links
|
||||
BANK_PUBLIC = os.environ.get("BANK_PUBLIC", "https://bank.hacktivism.ch").rstrip("/")
|
||||
USER = os.environ.get("BANK_USER", "explorer")
|
||||
AMOUNT = os.environ.get("AMOUNT", "GOA:10")
|
||||
LANDING = Path(os.environ.get("LANDING_DIR", "/var/www/bank-landing"))
|
||||
LISTEN = ("127.0.0.1", int(os.environ.get("DEMO_WITHDRAW_PORT", "19096")))
|
||||
|
||||
|
||||
def public_webui_url() -> str:
|
||||
return f"{BANK_PUBLIC}/webui/"
|
||||
|
||||
|
||||
def load_pass() -> str:
|
||||
p = os.environ.get("BANK_PASS", "").strip()
|
||||
if p:
|
||||
return p
|
||||
for f in (
|
||||
Path(f"/root/bank-{USER}-password.txt"),
|
||||
Path("/root/bank-explorer-password.txt"),
|
||||
):
|
||||
if f.is_file():
|
||||
return f.read_text().strip()
|
||||
raise RuntimeError("no BANK_PASS / explorer password file")
|
||||
|
||||
|
||||
def http_json(method: str, url: str, body=None, headers=None, auth=None):
|
||||
data = None if body is None else json.dumps(body).encode()
|
||||
h = dict(headers or {})
|
||||
if body is not None:
|
||||
h["Content-Type"] = "application/json"
|
||||
if auth:
|
||||
import base64
|
||||
|
||||
token = base64.b64encode(f"{auth[0]}:{auth[1]}".encode()).decode()
|
||||
h["Authorization"] = f"Basic {token}"
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=h)
|
||||
ctx = ssl.create_default_context()
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=20) as r:
|
||||
raw = r.read().decode()
|
||||
return r.status, json.loads(raw) if raw.strip() else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read().decode()
|
||||
try:
|
||||
return e.code, json.loads(raw)
|
||||
except Exception:
|
||||
return e.code, {"raw": raw[:500]}
|
||||
|
||||
|
||||
def mint_withdraw() -> dict:
|
||||
pw = load_pass()
|
||||
code, tok = http_json(
|
||||
"POST",
|
||||
f"{BANK}/accounts/{USER}/token",
|
||||
{"scope": "readwrite", "refreshable": True},
|
||||
auth=(USER, pw),
|
||||
)
|
||||
if code != 200 or not tok.get("access_token"):
|
||||
raise RuntimeError(f"token failed HTTP {code}: {tok}")
|
||||
access = tok["access_token"]
|
||||
code, wd = http_json(
|
||||
"POST",
|
||||
f"{BANK}/accounts/{USER}/withdrawals",
|
||||
{"suggested_amount": AMOUNT},
|
||||
headers={"Authorization": f"Bearer {access}"},
|
||||
)
|
||||
if code not in (200, 201):
|
||||
raise RuntimeError(f"withdrawal create HTTP {code}: {wd}")
|
||||
uri = wd.get("taler_withdraw_uri") or ""
|
||||
wid = wd.get("withdrawal_id") or ""
|
||||
if not uri:
|
||||
raise RuntimeError(f"no taler_withdraw_uri: {wd}")
|
||||
if not wid:
|
||||
wid = uri.rstrip("/").split("/")[-1]
|
||||
# Keep host:port from libeufin (e.g. bank.hacktivism.ch:443). Stripping :443
|
||||
# breaks taler-integration withdraw links / main landing QR on HTTPS banks.
|
||||
uri = str(uri).strip()
|
||||
LANDING.mkdir(parents=True, exist_ok=True)
|
||||
(LANDING / "withdraw.uri").write_text(uri + "\n")
|
||||
(LANDING / "withdraw.amount").write_text(AMOUNT + "\n")
|
||||
(LANDING / "withdraw.created").write_text(
|
||||
time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()) + "\n"
|
||||
)
|
||||
watch_withdrawal(wid)
|
||||
return {
|
||||
"ok": True,
|
||||
"taler_withdraw_uri": uri,
|
||||
"withdrawal_id": wid,
|
||||
"amount": AMOUNT,
|
||||
"pool_account": USER,
|
||||
"taler_integration_base": f"{BANK_PUBLIC}/taler-integration/",
|
||||
"hint": "Open in GNU Taler Wallet (iOS/Android/desktop). No bank registration.",
|
||||
"created": time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()),
|
||||
}
|
||||
|
||||
|
||||
def watch_withdrawal(wid: str) -> None:
|
||||
"""Queue withdrawal_id for auto-confirm loop (shared pool + personal)."""
|
||||
if not wid:
|
||||
return
|
||||
LANDING.mkdir(parents=True, exist_ok=True)
|
||||
watch = LANDING / "withdraw-watch.ids"
|
||||
ids = set()
|
||||
if watch.is_file():
|
||||
ids = {ln.strip() for ln in watch.read_text().splitlines() if ln.strip()}
|
||||
ids.add(str(wid).strip())
|
||||
watch.write_text("\n".join(sorted(ids)) + "\n")
|
||||
|
||||
|
||||
|
||||
# Funny stems for usernames (bank-safe [a-z0-9-]).
|
||||
# Source: hand-curated in this file only — not scraped from the web.
|
||||
# Keep culture-neutral: light space / physics wordplay, no animals, foods,
|
||||
# body parts, religion, politics, or slang that can offend.
|
||||
_FUNNY_STEMS = (
|
||||
"nebula-nudge",
|
||||
"orbit-echo",
|
||||
"voidwave-vibe",
|
||||
"comet-crumb",
|
||||
"quark-pulse",
|
||||
"plasma-spark",
|
||||
"astro-glint",
|
||||
"lunar-loop",
|
||||
"warp-ripple",
|
||||
"photon-bloom",
|
||||
"galaxy-drift",
|
||||
"rocket-ribbon",
|
||||
"satellite-swirl",
|
||||
"meteor-mint",
|
||||
"stardust-swirl",
|
||||
"hyperdrive-hum",
|
||||
"cosmic-coral",
|
||||
"space-spark",
|
||||
"nova-nibble",
|
||||
"aurora-arc",
|
||||
"solar-swish",
|
||||
"pulsar-pop",
|
||||
"comet-cloud",
|
||||
"orbit-opal",
|
||||
"zenith-zip",
|
||||
"eclipse-echo",
|
||||
"horizon-hum",
|
||||
"starlight-step",
|
||||
)
|
||||
|
||||
|
||||
def _rand_username_and_name() -> tuple[str, str]:
|
||||
# Shown as goa-account-<funnypiece>-<tag> (e.g. goa-account-space-potato-k3m9x)
|
||||
stem = secrets.choice(_FUNNY_STEMS)
|
||||
alphabet = string.ascii_lowercase + string.digits
|
||||
tag = "".join(secrets.choice(alphabet) for _ in range(5))
|
||||
username = f"goa-account-{stem}-{tag}"
|
||||
name = username
|
||||
return username, name
|
||||
|
||||
|
||||
def _rand_password() -> str:
|
||||
# Embed "pleasechangeme" with random material before and after.
|
||||
alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
|
||||
prefix = "".join(secrets.choice(alphabet) for _ in range(4))
|
||||
suffix = "".join(secrets.choice(alphabet) for _ in range(6))
|
||||
return prefix + "pleasechangeme" + suffix
|
||||
|
||||
|
||||
def create_personal_account() -> dict:
|
||||
"""Public registration: auto username/password, balance starts at 0."""
|
||||
username, name = _rand_username_and_name()
|
||||
password = _rand_password()
|
||||
code, body = http_json(
|
||||
"POST",
|
||||
f"{BANK}/accounts",
|
||||
{
|
||||
"username": username,
|
||||
"password": password,
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
if code not in (200, 201, 204):
|
||||
# retry once on conflict
|
||||
if code in (409, 400):
|
||||
username, name = _rand_username_and_name()
|
||||
code, body = http_json(
|
||||
"POST",
|
||||
f"{BANK}/accounts",
|
||||
{
|
||||
"username": username,
|
||||
"password": password,
|
||||
"name": name,
|
||||
},
|
||||
)
|
||||
if code not in (200, 201, 204):
|
||||
raise RuntimeError(f"register failed HTTP {code}: {body}")
|
||||
webui = public_webui_url()
|
||||
# Same shared-pool withdraw as step 2 (explorer + auto-confirm)
|
||||
wd = mint_withdraw()
|
||||
withdraw_uri = wd["taler_withdraw_uri"]
|
||||
return {
|
||||
"ok": True,
|
||||
"created_for_you": True,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"name": name,
|
||||
"display_name": name,
|
||||
"balance": "GOA:0",
|
||||
"balance_note": "Starts at zero — not the shared community pool.",
|
||||
"taler_withdraw_uri": withdraw_uri,
|
||||
"withdrawal_id": wd["withdrawal_id"],
|
||||
"withdraw_amount": wd.get("amount") or AMOUNT,
|
||||
"pool_account": wd.get("pool_account") or USER,
|
||||
"qr_payload": withdraw_uri,
|
||||
"webui": webui,
|
||||
"account_url": webui,
|
||||
"login_url": webui,
|
||||
"hint": (
|
||||
f"Login at {webui} with username {username} and the password shown "
|
||||
"(not stored for recovery). Wallet QR is taler://withdraw/… from the "
|
||||
f"shared pool ({USER}), same as step 2."
|
||||
),
|
||||
"created": time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()),
|
||||
"created_human": time.strftime("%Y-%m-%d %H:%M %Z", time.localtime()),
|
||||
}
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt, *args):
|
||||
sys_stderr = __import__("sys").stderr
|
||||
sys_stderr.write("%s - %s\n" % (self.address_string(), fmt % args))
|
||||
|
||||
def _cors(self):
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(204)
|
||||
self._cors()
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
path = self.path.split("?", 1)[0]
|
||||
try:
|
||||
if path in (
|
||||
"/",
|
||||
"/demo-withdraw.json",
|
||||
"/intro/demo-withdraw.json",
|
||||
):
|
||||
body = mint_withdraw()
|
||||
elif path in (
|
||||
"/auto-account.json",
|
||||
"/intro/auto-account.json",
|
||||
):
|
||||
body = create_personal_account()
|
||||
else:
|
||||
self.send_response(404)
|
||||
self._cors()
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"ok":false,"error":"not found"}')
|
||||
return
|
||||
raw = json.dumps(body).encode()
|
||||
self.send_response(200)
|
||||
self._cors()
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
except Exception as e:
|
||||
raw = json.dumps({"ok": False, "error": str(e)}).encode()
|
||||
self.send_response(500)
|
||||
self._cors()
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
|
||||
def main():
|
||||
httpd = ThreadingHTTPServer(LISTEN, Handler)
|
||||
print(f"demo-withdraw-api on http://{LISTEN[0]}:{LISTEN[1]}/", flush=True)
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
80
scripts/taler-bank/install-demo-withdraw-api.sh
Executable file
80
scripts/taler-bank/install-demo-withdraw-api.sh
Executable file
|
|
@ -0,0 +1,80 @@
|
|||
#!/bin/bash
|
||||
# Install + start demo-withdraw API + auto-confirm loop inside bank container.
|
||||
# Host:
|
||||
# ./scripts/taler-bank/install-demo-withdraw-api.sh
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
CTR="${BANK_CONTAINER:-taler-hacktivism-bank}"
|
||||
|
||||
podman cp "$ROOT/demo-withdraw-api.py" "$CTR:/usr/local/bin/demo-withdraw-api.py"
|
||||
podman cp "$ROOT/auto-confirm-withdrawals.sh" "$CTR:/usr/local/bin/auto-confirm-withdrawals.sh"
|
||||
podman cp "$ROOT/refresh-demo-withdraw.sh" "$CTR:/usr/local/bin/refresh-demo-withdraw.sh"
|
||||
podman exec -u root "$CTR" chmod 755 \
|
||||
/usr/local/bin/demo-withdraw-api.py \
|
||||
/usr/local/bin/auto-confirm-withdrawals.sh \
|
||||
/usr/local/bin/refresh-demo-withdraw.sh
|
||||
|
||||
# nginx: proxy demo-withdraw.json
|
||||
NGX=/etc/nginx/sites-available/bank-landing
|
||||
podman exec -u root "$CTR" bash -lc '
|
||||
set -e
|
||||
f=/etc/nginx/sites-available/bank-landing
|
||||
if ! grep -q demo-withdraw.json "$f"; then
|
||||
# insert before location /intro/
|
||||
python3 - <<PY
|
||||
from pathlib import Path
|
||||
p=Path("/etc/nginx/sites-available/bank-landing")
|
||||
t=p.read_text()
|
||||
block=""" location = /intro/demo-withdraw.json {
|
||||
proxy_pass http://127.0.0.1:19096/demo-withdraw.json;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host \$host;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
}
|
||||
"""
|
||||
if "demo-withdraw.json" not in t:
|
||||
t=t.replace(" location /intro/ {", block+" location /intro/ {", 1)
|
||||
p.write_text(t)
|
||||
print("nginx location added")
|
||||
else:
|
||||
print("nginx already has demo-withdraw")
|
||||
if "auto-account.json" not in t:
|
||||
t=p.read_text()
|
||||
block2=""" location = /intro/auto-account.json {
|
||||
proxy_pass http://127.0.0.1:19096/auto-account.json;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host \$host;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
}
|
||||
"""
|
||||
t=t.replace(" location /intro/ {", block2+" location /intro/ {", 1)
|
||||
p.write_text(t)
|
||||
print("nginx auto-account location added")
|
||||
else:
|
||||
print("nginx already has auto-account")
|
||||
PY
|
||||
nginx -t && nginx -s reload || true
|
||||
else
|
||||
echo "nginx already configured"
|
||||
fi
|
||||
'
|
||||
|
||||
# start/restart API
|
||||
podman exec -u root "$CTR" bash -lc '
|
||||
pkill -f "demo-withdraw-api.py" 2>/dev/null || true
|
||||
nohup python3 /usr/local/bin/demo-withdraw-api.py \
|
||||
>>/var/log/demo-withdraw-api.log 2>&1 </dev/null &
|
||||
echo "api pid $!"
|
||||
# auto-confirm loop
|
||||
pkill -f "auto-confirm-withdrawals.sh --loop" 2>/dev/null || true
|
||||
nohup /usr/local/bin/auto-confirm-withdrawals.sh --loop 4 \
|
||||
>>/var/log/auto-confirm-withdrawals.log 2>&1 </dev/null &
|
||||
echo "auto-confirm pid $!"
|
||||
sleep 1
|
||||
curl -sS -m 8 http://127.0.0.1:19096/demo-withdraw.json | head -c 300; echo
|
||||
'
|
||||
|
||||
echo "OK: demo-withdraw API + auto-confirm in $CTR"
|
||||
echo "Public: https://bank.hacktivism.ch/intro/demo-withdraw.json"
|
||||
78
scripts/taler-bank/landing-stats-install.sh
Normal file
78
scripts/taler-bank/landing-stats-install.sh
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/bash
|
||||
# Install + run landing-stats.sh *inside* the bank container, and optionally
|
||||
# install a cron entry there. Documented in configs/bank-landing/README.md.
|
||||
#
|
||||
# Run on koopa host (root or user in podman group):
|
||||
# ./landing-stats-install.sh # copy + one-shot run
|
||||
# ./landing-stats-install.sh --cron # also install *minutely* cron inside container
|
||||
# ./landing-stats-install.sh --run-only # only exec existing script
|
||||
set -euo pipefail
|
||||
|
||||
DO_CRON=0
|
||||
RUN_ONLY=0
|
||||
# cron schedule (default: every minute)
|
||||
: "${LANDING_STATS_CRON:=* * * * *}"
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--cron) DO_CRON=1 ;;
|
||||
--run-only) RUN_ONLY=1 ;;
|
||||
-h|--help)
|
||||
sed -n '2,12p' "$0" | sed 's/^# \?//'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
SRC="$ROOT/landing-stats.sh"
|
||||
[ -f "$SRC" ] || { echo "missing $SRC" >&2; exit 1; }
|
||||
|
||||
# Prefer the known names from ops docs
|
||||
C=""
|
||||
for name in taler-hacktivism-bank taler-bank-hacktivism; do
|
||||
if podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$name"; then
|
||||
C=$name
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$C" ]; then
|
||||
C=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -i bank | head -1 || true)
|
||||
fi
|
||||
[ -n "$C" ] || { echo "no bank container running" >&2; exit 1; }
|
||||
echo "container=$C"
|
||||
|
||||
# Host password → container /root (if not already there)
|
||||
if [ -f /root/bank-explorer-password.txt ]; then
|
||||
podman cp /root/bank-explorer-password.txt "$C:/root/bank-explorer-password.txt" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ "$RUN_ONLY" != "1" ]; then
|
||||
podman exec "$C" mkdir -p /usr/local/bin /var/www/bank-landing
|
||||
podman cp "$SRC" "$C:/usr/local/bin/landing-stats.sh"
|
||||
podman exec "$C" chmod 755 /usr/local/bin/landing-stats.sh
|
||||
echo "installed /usr/local/bin/landing-stats.sh"
|
||||
fi
|
||||
|
||||
# Ensure landing dir exists for nginx intro
|
||||
podman exec "$C" mkdir -p /var/www/bank-landing
|
||||
|
||||
echo "running landing-stats.sh inside $C …"
|
||||
podman exec \
|
||||
-e LANDING_DIR=/var/www/bank-landing \
|
||||
-e BANK_USER=explorer \
|
||||
"$C" /usr/local/bin/landing-stats.sh
|
||||
|
||||
if [ "$DO_CRON" = "1" ]; then
|
||||
# default: every minute (* * * * *); override with LANDING_STATS_CRON='*/5 * * * *'
|
||||
podman exec -e LANDING_STATS_CRON="$LANDING_STATS_CRON" "$C" bash -c '
|
||||
sched="${LANDING_STATS_CRON:-* * * * *}"
|
||||
line="$sched TZ=Europe/Zurich LANDING_DIR=/var/www/bank-landing /usr/local/bin/landing-stats.sh >>/var/log/landing-stats.log 2>&1"
|
||||
(crontab -l 2>/dev/null | grep -v landing-stats.sh; echo "$line") | crontab -
|
||||
echo "cron installed:"; crontab -l | grep landing-stats
|
||||
'
|
||||
fi
|
||||
|
||||
echo "public check: curl -sS https://bank.hacktivism.ch/intro/stats.json | head"
|
||||
# If host maps 9013 → container landing, show local file
|
||||
podman exec "$C" head -c 400 /var/www/bank-landing/stats.json 2>/dev/null || true
|
||||
echo
|
||||
612
scripts/taler-bank/landing-stats.sh
Executable file
612
scripts/taler-bank/landing-stats.sh
Executable file
|
|
@ -0,0 +1,612 @@
|
|||
#!/bin/bash
|
||||
# Generate public landing stats JSON for bank.hacktivism.ch intro page.
|
||||
#
|
||||
# *** Run INSIDE the bank container *** (taler-hacktivism-bank).
|
||||
# Writes: $LANDING_DIR/stats.json → https://bank.hacktivism.ch/intro/stats.json
|
||||
#
|
||||
# Pure bash + curl + awk (no python). Times in Europe/Zurich (CET/CEST).
|
||||
#
|
||||
# Documented: configs/bank-landing/README.md
|
||||
set -euo pipefail
|
||||
|
||||
LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
|
||||
# Demo funding account (for recent withdraw list / flow)
|
||||
BANK_USER="${BANK_USER:-explorer}"
|
||||
# Admin lists all accounts + can read other accounts' txs
|
||||
ADMIN_USER="${ADMIN_USER:-admin}"
|
||||
# Per-account transaction window (libeufin delta). Was -100 → systematically
|
||||
# undercounted credits/withdraws on active accounts (broken public stats).
|
||||
TX_DELTA="${TX_DELTA:--50000}"
|
||||
# Max accounts to list + scan (was 80 → missed later accounts; bank has 100+).
|
||||
MAX_SCAN_ACCOUNTS="${MAX_SCAN_ACCOUNTS:-500}"
|
||||
# Account-list page size for GET /accounts?delta=… (must cover all users)
|
||||
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--500}"
|
||||
# curl timeout per account (deeper history needs more headroom)
|
||||
TX_CURL_TIMEOUT="${TX_CURL_TIMEOUT:-25}"
|
||||
export TZ="${TZ:-Europe/Zurich}"
|
||||
|
||||
PASS="${BANK_PASS:-}"
|
||||
ADMIN_PASS="${BANK_ADMIN_PASS:-}"
|
||||
|
||||
detect_bank() {
|
||||
if [ -n "${BANK_URL:-}" ]; then
|
||||
echo "${BANK_URL%/}"
|
||||
return
|
||||
fi
|
||||
local port="" conf u code
|
||||
for conf in /etc/libeufin/bank-overrides.conf /etc/libeufin/libeufin-bank.conf; do
|
||||
if [ -f "$conf" ]; then
|
||||
port=$(grep -E '^\s*PORT\s*=' "$conf" 2>/dev/null | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}')
|
||||
[ -n "$port" ] && break
|
||||
fi
|
||||
done
|
||||
port="${port:-9012}"
|
||||
for u in "http://127.0.0.1:${port}" "http://127.0.0.1:9012" "http://127.0.0.1:8080"; do
|
||||
code=$(curl -sS -m 2 -o /dev/null -w '%{http_code}' "$u/config" 2>/dev/null || echo 000)
|
||||
if [ "$code" = "200" ]; then
|
||||
echo "$u"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo "http://127.0.0.1:${port}"
|
||||
}
|
||||
|
||||
BANK="$(detect_bank)"
|
||||
BANK="${BANK%/}"
|
||||
|
||||
read_pass_file() {
|
||||
local f
|
||||
for f in "$@"; do
|
||||
if [ -f "$f" ] && [ -r "$f" ]; then
|
||||
tr -d '\n' <"$f"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [ -z "$PASS" ]; then
|
||||
PASS=$(read_pass_file \
|
||||
"/root/bank-${BANK_USER}-password.txt" \
|
||||
/root/bank-explorer-password.txt \
|
||||
"/etc/libeufin/secrets/bank-${BANK_USER}-password.txt" || true)
|
||||
fi
|
||||
if [ -z "$ADMIN_PASS" ]; then
|
||||
ADMIN_PASS=$(read_pass_file \
|
||||
/root/bank-admin-password.txt \
|
||||
/etc/libeufin/secrets/bank-admin-password.txt || true)
|
||||
fi
|
||||
|
||||
WORKDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$WORKDIR"' EXIT
|
||||
OUT="$LANDING_DIR/stats.json"
|
||||
RUN="$LANDING_DIR/stats-run.json"
|
||||
TMP="${OUT}.tmp.$$"
|
||||
mkdir -p "$LANDING_DIR"
|
||||
|
||||
now_iso() { date +%Y-%m-%dT%H:%M%z | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/'; }
|
||||
now_unix() { date +%s; }
|
||||
now_human() { date +"%Y-%m-%d %H:%M %Z"; }
|
||||
iso_from_unix() {
|
||||
local u="$1"
|
||||
date -d "@${u}" +%Y-%m-%dT%H:%M%z 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' \
|
||||
|| date -r "${u}" +%Y-%m-%dT%H:%M%z 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' \
|
||||
|| echo ""
|
||||
}
|
||||
# Human CEST/CET label for display: "2026-07-09 20:49 CEST"
|
||||
human_from_unix() {
|
||||
local u="$1"
|
||||
date -d "@${u}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null \
|
||||
|| date -r "${u}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null \
|
||||
|| echo ""
|
||||
}
|
||||
|
||||
json_str() {
|
||||
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr '\n' ' ')"
|
||||
}
|
||||
|
||||
# Public run status for intro page. Never wipe stats.json on failure.
|
||||
write_run() {
|
||||
local ok_json="$1" msg="${2:-}"
|
||||
cat >"$RUN" <<EOF
|
||||
{
|
||||
"ok": ${ok_json},
|
||||
"at": $(json_str "$(now_iso)"),
|
||||
"at_human": $(json_str "$(now_human)"),
|
||||
"error": $( [ -n "$msg" ] && json_str "$msg" || echo null )
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
write_err() {
|
||||
local msg="$1"
|
||||
write_run false "$msg"
|
||||
rm -f "$TMP" 2>/dev/null || true
|
||||
echo "error: $msg (stats.json left unchanged; stats-run.json updated)" >&2
|
||||
}
|
||||
|
||||
token_for() {
|
||||
local user="$1" pass="$2" out="$3"
|
||||
echo '{"scope":"readonly"}' >"$WORKDIR/tok-req.json"
|
||||
curl -sS -m 12 -u "${user}:${pass}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @"$WORKDIR/tok-req.json" \
|
||||
"${BANK}/accounts/${user}/token" >"$out" || true
|
||||
awk -F'"' '/access_token/ {
|
||||
for (i=1;i<=NF;i++) if ($i=="access_token") { print $(i+2); exit }
|
||||
}' "$out" 2>/dev/null || true
|
||||
}
|
||||
|
||||
extract_field() {
|
||||
# extract "key":"value" or "key": number from a JSON blob (first hit)
|
||||
local key="$1" file="$2"
|
||||
awk -v key="$key" '
|
||||
function between(s, k, p, rest, q2, r) {
|
||||
p = index(s, "\"" k "\"")
|
||||
if (p == 0) return ""
|
||||
rest = substr(s, p + length(k) + 2)
|
||||
while (rest ~ /^[[:space:]:]/) rest = substr(rest, 2)
|
||||
if (substr(rest, 1, 1) == "\"") {
|
||||
rest = substr(rest, 2)
|
||||
q2 = index(rest, "\"")
|
||||
if (q2 == 0) return ""
|
||||
return substr(rest, 1, q2 - 1)
|
||||
}
|
||||
r = ""
|
||||
while (rest ~ /^[0-9]/) {
|
||||
r = r substr(rest, 1, 1)
|
||||
rest = substr(rest, 2)
|
||||
}
|
||||
return r
|
||||
}
|
||||
{ print between($0, key); exit }
|
||||
' "$file" 2>/dev/null
|
||||
}
|
||||
|
||||
# --- tokens ---
|
||||
if [ -z "$PASS" ] && [ -z "$ADMIN_PASS" ]; then
|
||||
write_err "no bank password (explorer or admin) for stats"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ADMIN_TOKEN=""
|
||||
if [ -n "$ADMIN_PASS" ]; then
|
||||
ADMIN_TOKEN=$(token_for "$ADMIN_USER" "$ADMIN_PASS" "$WORKDIR/admin-tok.json")
|
||||
fi
|
||||
USER_TOKEN=""
|
||||
if [ -n "$PASS" ]; then
|
||||
USER_TOKEN=$(token_for "$BANK_USER" "$PASS" "$WORKDIR/user-tok.json")
|
||||
fi
|
||||
# Prefer admin for everything when available
|
||||
AUTH_TOKEN="${ADMIN_TOKEN:-$USER_TOKEN}"
|
||||
if [ -z "$AUTH_TOKEN" ]; then
|
||||
write_err "token failed (admin/explorer)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- account list ---
|
||||
ACCOUNTS_N=0
|
||||
ACCOUNTS_USERS=0
|
||||
: >"$WORKDIR/usernames.txt"
|
||||
if [ -n "$ADMIN_TOKEN" ]; then
|
||||
# Use a large negative delta so we list *all* accounts (delta=-80 truncated
|
||||
# the roster and undercounted bank_accounts + flow).
|
||||
curl -sS -m 30 -H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
"${BANK}/accounts?delta=${ACCOUNTS_DELTA}" >"$WORKDIR/accounts.json" || true
|
||||
# usernames from "username":"..."
|
||||
tr -d '\n' <"$WORKDIR/accounts.json" \
|
||||
| sed 's/},{/}\n{/g' \
|
||||
| while IFS= read -r line; do
|
||||
u=$(printf '%s' "$line" | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
[ -n "$u" ] && echo "$u"
|
||||
done >"$WORKDIR/usernames.txt" || true
|
||||
ACCOUNTS_N=$(grep -cve '^\s*$' "$WORKDIR/usernames.txt" 2>/dev/null || echo 0)
|
||||
ACCOUNTS_USERS=$(grep -Eve '^(admin|exchange)$' "$WORKDIR/usernames.txt" 2>/dev/null | grep -cve '^\s*$' || echo 0)
|
||||
else
|
||||
echo "$BANK_USER" >"$WORKDIR/usernames.txt"
|
||||
ACCOUNTS_N=1
|
||||
ACCOUNTS_USERS=1
|
||||
fi
|
||||
|
||||
# --- scan txs: distinguish incoming (credit) vs withdraw (Taler debit) ---
|
||||
# all-wd.tsv: amount \t t_s \t subject \t username \t reserve
|
||||
# all-in.tsv: amount \t t_s \t subject \t username
|
||||
# flow.tsv: kind \t amount_num kind = incoming | withdraw | other_out
|
||||
: >"$WORKDIR/all-wd.tsv"
|
||||
: >"$WORKDIR/all-in.tsv"
|
||||
: >"$WORKDIR/flow.tsv"
|
||||
SCAN_OK=0
|
||||
SCAN_EMPTY=0
|
||||
SCAN_FAIL=0
|
||||
# Scan customer accounts only (not exchange/admin — exchange credits would double-count)
|
||||
{
|
||||
echo "$BANK_USER"
|
||||
grep -Eve "^(admin|exchange|${BANK_USER})$" "$WORKDIR/usernames.txt" 2>/dev/null || true
|
||||
} | awk 'NF && !seen[$0]++' | head -n "$MAX_SCAN_ACCOUNTS" >"$WORKDIR/scan-users.txt"
|
||||
|
||||
while IFS= read -r uname; do
|
||||
[ -n "$uname" ] || continue
|
||||
case "$uname" in admin|exchange) continue ;; esac
|
||||
# Safe filename (usernames are mostly [A-Za-z0-9_-])
|
||||
safe=$(printf '%s' "$uname" | tr -c 'A-Za-z0-9._-' '_')
|
||||
code=$(curl -sS -m "${TX_CURL_TIMEOUT}" -o "$WORKDIR/tx-${safe}.json" -w '%{http_code}' \
|
||||
-H "Authorization: Bearer ${AUTH_TOKEN}" \
|
||||
"${BANK}/accounts/${uname}/transactions?delta=${TX_DELTA}" 2>/dev/null || echo 000)
|
||||
# 204 / empty body: account with no transactions — normal, not an error
|
||||
if [ "$code" = "204" ] || [ ! -s "$WORKDIR/tx-${safe}.json" ]; then
|
||||
SCAN_EMPTY=$((SCAN_EMPTY + 1))
|
||||
continue
|
||||
fi
|
||||
if [ "$code" != "200" ]; then
|
||||
SCAN_FAIL=$((SCAN_FAIL + 1))
|
||||
continue
|
||||
fi
|
||||
SCAN_OK=$((SCAN_OK + 1))
|
||||
tr -d '\n' <"$WORKDIR/tx-${safe}.json" \
|
||||
| sed 's/},{/}\n{/g' \
|
||||
>"$WORKDIR/tx-lines.txt" 2>/dev/null || continue
|
||||
awk -v uname="$uname" -v flowf="$WORKDIR/flow.tsv" -v inf="$WORKDIR/all-in.tsv" '
|
||||
function between(s, key, p, rest, q2, r) {
|
||||
p = index(s, "\"" key "\"")
|
||||
if (p == 0) return ""
|
||||
rest = substr(s, p + length(key) + 2)
|
||||
while (rest ~ /^[[:space:]:]/) rest = substr(rest, 2)
|
||||
if (substr(rest, 1, 1) == "\"") {
|
||||
rest = substr(rest, 2)
|
||||
q2 = index(rest, "\"")
|
||||
if (q2 == 0) return ""
|
||||
return substr(rest, 1, q2 - 1)
|
||||
}
|
||||
r = ""
|
||||
while (rest ~ /^[0-9]/) {
|
||||
r = r substr(rest, 1, 1)
|
||||
rest = substr(rest, 2)
|
||||
}
|
||||
return r
|
||||
}
|
||||
function amt_n(a, p) {
|
||||
p = index(a, ":")
|
||||
if (p == 0) return 0
|
||||
return substr(a, p + 1) + 0
|
||||
}
|
||||
{
|
||||
line = $0
|
||||
dir = between(line, "direction")
|
||||
amt = between(line, "amount")
|
||||
ts = between(line, "t_s")
|
||||
subj = between(line, "subject")
|
||||
low = tolower(subj)
|
||||
if (amt == "") next
|
||||
n = amt_n(amt)
|
||||
if (dir == "credit") {
|
||||
# Incoming: bank credits into customer accounts (admin top-up, transfers in)
|
||||
print "incoming\t" n >> flowf
|
||||
printf "%s\t%s\t%s\t%s\n", amt, ts, subj, uname >> inf
|
||||
} else if (dir == "debit" && index(low, "withdraw") > 0) {
|
||||
# Withdraw: Taler withdrawal debit → exchange → wallet coins
|
||||
print "withdraw\t" n >> flowf
|
||||
res = subj
|
||||
sub(/^.*[Ww]ithdrawal[ ]+/, "", res)
|
||||
gsub(/[^A-Za-z0-9]/, "", res)
|
||||
printf "%s\t%s\t%s\t%s\t%s\n", amt, ts, subj, uname, res
|
||||
} else if (dir == "debit") {
|
||||
# Other debits (non-withdraw transfers)
|
||||
print "other_out\t" n >> flowf
|
||||
}
|
||||
}
|
||||
' "$WORKDIR/tx-lines.txt" >>"$WORKDIR/all-wd.tsv" 2>/dev/null || true
|
||||
done <"$WORKDIR/scan-users.txt"
|
||||
|
||||
# Sum by kind
|
||||
TOTAL_IN_N=$(awk -F'\t' '$1=="incoming"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
|
||||
TOTAL_WD_FLOW_N=$(awk -F'\t' '$1=="withdraw"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
|
||||
TOTAL_OTHER_OUT_N=$(awk -F'\t' '$1=="other_out"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
|
||||
N_INCOMING=$(awk -F'\t' '$1=="incoming"{c++} END{print c+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
|
||||
# total out = withdraw + other debits
|
||||
TOTAL_OUT_N=$(awk -v a="${TOTAL_WD_FLOW_N:-0}" -v b="${TOTAL_OTHER_OUT_N:-0}" 'BEGIN{printf "%.8f", a+b}')
|
||||
|
||||
# Sort by t_s descending
|
||||
sort -t$'\t' -k2,2nr "$WORKDIR/all-wd.tsv" -o "$WORKDIR/all-wd-sorted.tsv" 2>/dev/null \
|
||||
|| cp "$WORKDIR/all-wd.tsv" "$WORKDIR/all-wd-sorted.tsv"
|
||||
sort -t$'\t' -k2,2nr "$WORKDIR/all-in.tsv" -o "$WORKDIR/all-in-sorted.tsv" 2>/dev/null \
|
||||
|| cp "$WORKDIR/all-in.tsv" "$WORKDIR/all-in-sorted.tsv"
|
||||
|
||||
# Unique reserves = individual wallet withdraws (each wallet reserve_pub)
|
||||
WALLETS_N=$(awk -F'\t' '$5!=""{print $5}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0)
|
||||
# Accounts that funded at least one withdraw
|
||||
ACCOUNTS_WITH_WD=$(awk -F'\t' '$4!=""{print $4}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0)
|
||||
|
||||
# Aggregates
|
||||
NOW=$(now_unix)
|
||||
GEN_ISO=$(now_iso)
|
||||
GEN_HUMAN=$(date +"%Y-%m-%d %H:%M %Z")
|
||||
|
||||
# Build recent withdraws JSON array (up to 10) with CEST times — landing shows all 10
|
||||
RECENT_WD_N="${RECENT_WD_N:-10}"
|
||||
: >"$WORKDIR/recent.jsonl"
|
||||
N_WD=0
|
||||
TOTAL_WD=0
|
||||
W24=0; N24=0; W7=0; N7=0
|
||||
DAY=$((NOW - 86400))
|
||||
WEEK=$((NOW - 7 * 86400))
|
||||
LAST_AMT=""; LAST_TS=""; LAST_SUBJ=""
|
||||
|
||||
while IFS=$'\t' read -r amt ts subj uname res; do
|
||||
[ -n "$amt" ] || continue
|
||||
n=$(printf '%s' "$amt" | awk -F: '{print $2+0}')
|
||||
N_WD=$((N_WD + 1))
|
||||
TOTAL_WD=$(awk -v a="$TOTAL_WD" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
|
||||
ts_n=${ts:-0}
|
||||
if [ "$ts_n" -ge "$DAY" ] 2>/dev/null; then
|
||||
W24=$(awk -v a="$W24" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
|
||||
N24=$((N24 + 1))
|
||||
fi
|
||||
if [ "$ts_n" -ge "$WEEK" ] 2>/dev/null; then
|
||||
W7=$(awk -v a="$W7" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
|
||||
N7=$((N7 + 1))
|
||||
fi
|
||||
if [ -z "$LAST_AMT" ]; then
|
||||
LAST_AMT=$amt
|
||||
LAST_TS=$ts_n
|
||||
LAST_SUBJ=$subj
|
||||
fi
|
||||
if [ "$N_WD" -le "$RECENT_WD_N" ]; then
|
||||
at_h=""; at_iso=""
|
||||
if [ -n "$ts_n" ] && [ "$ts_n" != "0" ]; then
|
||||
at_h=$(human_from_unix "$ts_n")
|
||||
at_iso=$(iso_from_unix "$ts_n")
|
||||
fi
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$amt" "$ts_n" "$at_h" "$at_iso" "$uname" "$res" >>"$WORKDIR/recent.jsonl"
|
||||
fi
|
||||
done <"$WORKDIR/all-wd-sorted.tsv"
|
||||
|
||||
fmt_goa() {
|
||||
awk -v n="$1" 'BEGIN{
|
||||
if (n+0 == int(n+0)) printf "GOA:%d", int(n+0);
|
||||
else printf "GOA:%.8g", n+0;
|
||||
}'
|
||||
}
|
||||
|
||||
TOTAL_AMT=$(fmt_goa "$TOTAL_WD")
|
||||
TOTAL_IN_AMT=$(fmt_goa "${TOTAL_IN_N:-0}")
|
||||
TOTAL_OUT_AMT=$(fmt_goa "${TOTAL_OUT_N:-0}")
|
||||
TOTAL_WD_FLOW_AMT=$(fmt_goa "${TOTAL_WD_FLOW_N:-0}")
|
||||
TOTAL_OTHER_OUT_AMT=$(fmt_goa "${TOTAL_OTHER_OUT_N:-0}")
|
||||
W24_AMT=$(fmt_goa "$W24")
|
||||
W7_AMT=$(fmt_goa "$W7")
|
||||
|
||||
LAST_AT_H=""; LAST_AT_ISO=""
|
||||
if [ -n "${LAST_TS:-}" ] && [ "$LAST_TS" != "0" ]; then
|
||||
LAST_AT_H=$(human_from_unix "$LAST_TS")
|
||||
LAST_AT_ISO=$(iso_from_unix "$LAST_TS")
|
||||
fi
|
||||
|
||||
# recent_withdraws JSON
|
||||
RECENT_JSON="["
|
||||
first=1
|
||||
while IFS=$'\t' read -r amt ts_n at_h at_iso uname res; do
|
||||
[ -n "$amt" ] || continue
|
||||
if [ "$first" = 1 ]; then first=0; else RECENT_JSON="${RECENT_JSON},"; fi
|
||||
RECENT_JSON="${RECENT_JSON}
|
||||
{
|
||||
\"kind\": \"withdraw\",
|
||||
\"amount\": $(json_str "$amt"),
|
||||
\"at\": $(json_str "$at_h"),
|
||||
\"at_iso\": $(json_str "$at_iso"),
|
||||
\"at_unix\": ${ts_n:-null},
|
||||
\"account\": $(json_str "$uname"),
|
||||
\"reserve\": $(json_str "$res")
|
||||
}"
|
||||
done <"$WORKDIR/recent.jsonl"
|
||||
RECENT_JSON="${RECENT_JSON}
|
||||
]"
|
||||
|
||||
# recent incoming (up to 5)
|
||||
: >"$WORKDIR/recent-in.jsonl"
|
||||
N_IN_LIST=0
|
||||
while IFS=$'\t' read -r amt ts subj uname; do
|
||||
[ -n "$amt" ] || continue
|
||||
N_IN_LIST=$((N_IN_LIST + 1))
|
||||
[ "$N_IN_LIST" -le 5 ] || break
|
||||
ts_n=${ts:-0}
|
||||
at_h=""; at_iso=""
|
||||
if [ -n "$ts_n" ] && [ "$ts_n" != "0" ]; then
|
||||
at_h=$(human_from_unix "$ts_n")
|
||||
at_iso=$(iso_from_unix "$ts_n")
|
||||
fi
|
||||
printf '%s\t%s\t%s\t%s\t%s\n' "$amt" "$ts_n" "$at_h" "$at_iso" "$uname" >>"$WORKDIR/recent-in.jsonl"
|
||||
done <"$WORKDIR/all-in-sorted.tsv"
|
||||
|
||||
RECENT_IN_JSON="["
|
||||
first=1
|
||||
while IFS=$'\t' read -r amt ts_n at_h at_iso uname; do
|
||||
[ -n "$amt" ] || continue
|
||||
if [ "$first" = 1 ]; then first=0; else RECENT_IN_JSON="${RECENT_IN_JSON},"; fi
|
||||
RECENT_IN_JSON="${RECENT_IN_JSON}
|
||||
{
|
||||
\"kind\": \"incoming\",
|
||||
\"amount\": $(json_str "$amt"),
|
||||
\"at\": $(json_str "$at_h"),
|
||||
\"at_iso\": $(json_str "$at_iso"),
|
||||
\"at_unix\": ${ts_n:-null},
|
||||
\"account\": $(json_str "$uname")
|
||||
}"
|
||||
done <"$WORKDIR/recent-in.jsonl"
|
||||
RECENT_IN_JSON="${RECENT_IN_JSON}
|
||||
]"
|
||||
|
||||
# Balance of explorer (optional display not required in footer)
|
||||
BALANCE="GOA:0"
|
||||
if [ -n "${USER_TOKEN:-$AUTH_TOKEN}" ]; then
|
||||
curl -sS -m 10 -H "Authorization: Bearer ${USER_TOKEN:-$AUTH_TOKEN}" \
|
||||
"${BANK}/accounts/${BANK_USER}" >"$WORKDIR/acct.json" || true
|
||||
BALANCE=$(awk -F'"' '/"amount"/ {
|
||||
for (i=1;i<=NF;i++) if ($i=="amount") { print $(i+2); exit }
|
||||
}' "$WORKDIR/acct.json" 2>/dev/null || echo "GOA:0")
|
||||
fi
|
||||
|
||||
# Demo block kept for page QR logic only (not shown in footer)
|
||||
DEMO_URI=""; DEMO_AMT=""; DEMO_CREATED=""; DEMO_WID=""; DEMO_STATUS=""
|
||||
[ -f "$LANDING_DIR/withdraw.uri" ] && DEMO_URI=$(tr -d '\n' <"$LANDING_DIR/withdraw.uri")
|
||||
[ -f "$LANDING_DIR/withdraw.amount" ] && DEMO_AMT=$(tr -d '\n' <"$LANDING_DIR/withdraw.amount")
|
||||
[ -f "$LANDING_DIR/withdraw.created" ] && DEMO_CREATED=$(tr -d '\n' <"$LANDING_DIR/withdraw.created")
|
||||
if [ -n "$DEMO_URI" ]; then
|
||||
DEMO_WID=$(basename "$DEMO_URI")
|
||||
curl -sS -m 8 \
|
||||
"${BANK}/taler-integration/withdrawal-operation/${DEMO_WID}" \
|
||||
>"$WORKDIR/demo-wd.json" 2>/dev/null || true
|
||||
DEMO_STATUS=$(awk -F'"' '/"status"/ {
|
||||
for (i=1;i<=NF;i++) if ($i=="status") { print $(i+2); exit }
|
||||
}' "$WORKDIR/demo-wd.json" 2>/dev/null || true)
|
||||
fi
|
||||
DEMO_READY=false
|
||||
case "$DEMO_STATUS" in
|
||||
pending|selected) DEMO_READY=true ;;
|
||||
"") [ -n "$DEMO_URI" ] && DEMO_READY=true ;;
|
||||
esac
|
||||
[ "$DEMO_STATUS" = "confirmed" ] && DEMO_READY=false
|
||||
[ "$DEMO_STATUS" = "aborted" ] && DEMO_READY=false
|
||||
|
||||
# Sanity: never publish an empty-looking success if admin scan should have accounts
|
||||
if [ -n "$ADMIN_TOKEN" ] && [ "${ACCOUNTS_N:-0}" = "0" ]; then
|
||||
write_err "accounts list empty after admin scan"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Live performance probes (same idea as exchange landing-stats)
|
||||
# Milliseconds as integer, rounded (not truncated) so sub-ms loopback does not
|
||||
# always show 0 when we fall back to in-container URLs.
|
||||
measure_ms() {
|
||||
local url="$1" t
|
||||
t=$(curl -sS -o /dev/null -m 8 -w '%{time_total}' "$url" 2>/dev/null || echo "")
|
||||
[ -z "$t" ] && { echo "null"; return; }
|
||||
awk -v t="$t" 'BEGIN{
|
||||
ms = (t+0)*1000
|
||||
if (ms > 0 && ms < 1) ms = 1
|
||||
printf "%d", int(ms + 0.5)
|
||||
}'
|
||||
}
|
||||
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
|
||||
# Prefer public URL for real client latency (Caddy → bank); fall back to loopback.
|
||||
# Integration used to probe only $BANK (127.0.0.1:9012) → ~0–1 ms and not comparable
|
||||
# to /config which already used the public host.
|
||||
BANK_PUBLIC_BASE="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}"
|
||||
CONFIG_MS=$(measure_ms "${BANK_PUBLIC_BASE}/config")
|
||||
CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/config" 2>/dev/null || echo "000")
|
||||
if [ "$CONFIG_HTTP" != "200" ]; then
|
||||
CONFIG_MS=$(measure_ms "${BANK}/config")
|
||||
CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK}/config" 2>/dev/null || echo "000")
|
||||
fi
|
||||
INT_MS=$(measure_ms "${BANK_PUBLIC_BASE}/taler-integration/config")
|
||||
INT_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/taler-integration/config" 2>/dev/null || echo "000")
|
||||
if [ "$INT_HTTP" != "200" ]; then
|
||||
INT_MS=$(measure_ms "${BANK}/taler-integration/config")
|
||||
INT_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK}/taler-integration/config" 2>/dev/null || echo "000")
|
||||
fi
|
||||
WEBUI_MS=$(measure_ms "${BANK_PUBLIC_BASE}/webui/")
|
||||
WEBUI_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/webui/" 2>/dev/null || echo "000")
|
||||
LOADAVG=""
|
||||
[ -r /proc/loadavg ] && LOADAVG=$(awk '{print $1","$2","$3}' /proc/loadavg)
|
||||
|
||||
MEM_JSON='"container_rss_human": "—"'
|
||||
MEM_HELPER="${MEM_HELPER:-/usr/local/lib/landing-mem-snapshot.sh}"
|
||||
if [ -f "$MEM_HELPER" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$MEM_HELPER"
|
||||
mem_snapshot_json || true
|
||||
fi
|
||||
|
||||
cat >"$TMP" <<EOF
|
||||
{
|
||||
"ok": true,
|
||||
"currency": "GOA",
|
||||
"timezone": $(json_str "$TZ"),
|
||||
"generated_at": $(json_str "$GEN_ISO"),
|
||||
"generated_at_human": $(json_str "$GEN_HUMAN"),
|
||||
"generated_at_unix": $NOW,
|
||||
"source": "in-container landing-stats.sh",
|
||||
"bank_url": $(json_str "$BANK"),
|
||||
"scan": {
|
||||
"tx_delta": $(json_str "$TX_DELTA"),
|
||||
"accounts_delta": $(json_str "$ACCOUNTS_DELTA"),
|
||||
"max_scan_accounts": ${MAX_SCAN_ACCOUNTS:-0},
|
||||
"accounts_listed": ${ACCOUNTS_N:-0},
|
||||
"accounts_scanned_ok": ${SCAN_OK:-0},
|
||||
"accounts_empty_tx": ${SCAN_EMPTY:-0},
|
||||
"accounts_scan_fail": ${SCAN_FAIL:-0},
|
||||
"note": "empty_tx includes HTTP 204 (no ledger rows) — normal for new auto-accounts"
|
||||
},
|
||||
"bank_accounts": {
|
||||
"total": ${ACCOUNTS_N:-0},
|
||||
"users": ${ACCOUNTS_USERS:-0},
|
||||
"with_withdraws": ${ACCOUNTS_WITH_WD:-0}
|
||||
},
|
||||
"wallets": {
|
||||
"unique_reserves": ${WALLETS_N:-0},
|
||||
"note": "unique reserve pubs from Taler withdrawals (one per wallet withdraw)"
|
||||
},
|
||||
"balance_explorer": $(json_str "$BALANCE"),
|
||||
"flow": {
|
||||
"incoming": {
|
||||
"label": "Incoming bank credits",
|
||||
"count": ${N_INCOMING:-0},
|
||||
"amount": $(json_str "$TOTAL_IN_AMT"),
|
||||
"value": ${TOTAL_IN_N:-0}
|
||||
},
|
||||
"withdraw": {
|
||||
"label": "Taler withdrawals to wallets",
|
||||
"count": ${N_WD:-0},
|
||||
"amount": $(json_str "$TOTAL_WD_FLOW_AMT"),
|
||||
"value": ${TOTAL_WD_FLOW_N:-0}
|
||||
},
|
||||
"other_out": {
|
||||
"label": "Other debits (non-withdraw)",
|
||||
"amount": $(json_str "$TOTAL_OTHER_OUT_AMT"),
|
||||
"value": ${TOTAL_OTHER_OUT_N:-0}
|
||||
},
|
||||
"total_in": $(json_str "$TOTAL_IN_AMT"),
|
||||
"total_in_value": ${TOTAL_IN_N:-0},
|
||||
"total_out": $(json_str "$TOTAL_OUT_AMT"),
|
||||
"total_out_value": ${TOTAL_OUT_N:-0},
|
||||
"note": "incoming=credits; withdraw=Taler withdrawal debits; excl. admin+exchange accounts"
|
||||
},
|
||||
"withdraws": {
|
||||
"count": ${N_WD:-0},
|
||||
"total_amount": $(json_str "$TOTAL_AMT"),
|
||||
"total_value": ${TOTAL_WD:-0},
|
||||
"last_amount": $( [ -n "$LAST_AMT" ] && json_str "$LAST_AMT" || echo null ),
|
||||
"last_at": $( [ -n "$LAST_AT_H" ] && json_str "$LAST_AT_H" || echo null ),
|
||||
"last_at_iso": $( [ -n "$LAST_AT_ISO" ] && json_str "$LAST_AT_ISO" || echo null ),
|
||||
"last_at_unix": ${LAST_TS:-null},
|
||||
"last_subject": $( [ -n "$LAST_SUBJ" ] && json_str "$LAST_SUBJ" || echo null ),
|
||||
"last_24h": { "count": ${N24:-0}, "amount": $(json_str "$W24_AMT"), "value": ${W24:-0} },
|
||||
"last_7d": { "count": ${N7:-0}, "amount": $(json_str "$W7_AMT"), "value": ${W7:-0} }
|
||||
},
|
||||
"recent_withdraws": $RECENT_JSON,
|
||||
"recent_incoming": $RECENT_IN_JSON,
|
||||
"demo": {
|
||||
"uri": $( [ -n "$DEMO_URI" ] && json_str "$DEMO_URI" || echo null ),
|
||||
"amount": $( [ -n "$DEMO_AMT" ] && json_str "$DEMO_AMT" || echo null ),
|
||||
"created": $( [ -n "$DEMO_CREATED" ] && json_str "$DEMO_CREATED" || echo null ),
|
||||
"withdrawal_id": $( [ -n "$DEMO_WID" ] && json_str "$DEMO_WID" || echo null ),
|
||||
"status": $( [ -n "$DEMO_STATUS" ] && json_str "$DEMO_STATUS" || echo null ),
|
||||
"ready": $DEMO_READY
|
||||
},
|
||||
"performance": {
|
||||
"config_http": $(json_str "$CONFIG_HTTP"),
|
||||
"config_ms": $(num_or_null "$CONFIG_MS"),
|
||||
"integration_http": $(json_str "$INT_HTTP"),
|
||||
"integration_ms": $(num_or_null "$INT_MS"),
|
||||
"webui_http": $(json_str "$WEBUI_HTTP"),
|
||||
"webui_ms": $(num_or_null "$WEBUI_MS"),
|
||||
"loadavg": $(json_str "${LOADAVG:-}"),
|
||||
"memory": {
|
||||
${MEM_JSON}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
grep -q '"ok": true' "$TMP" || { write_err "tmp json missing ok:true"; exit 1; }
|
||||
mv -f "$TMP" "$OUT"
|
||||
write_run true
|
||||
echo "ok accounts=${ACCOUNTS_N} wallets=${WALLETS_N} withdraws=${N_WD} total=${TOTAL_AMT} tz=${TZ} -> $OUT"
|
||||
112
scripts/taler-bank/make-demo-withdraw-qr.sh
Executable file
112
scripts/taler-bank/make-demo-withdraw-qr.sh
Executable file
|
|
@ -0,0 +1,112 @@
|
|||
#!/bin/bash
|
||||
# Create a demo GOA withdrawal for user explorer and write QR assets under LANDING_DIR.
|
||||
# Run on koopa (host) with bank on 127.0.0.1:9012.
|
||||
set -euo pipefail
|
||||
|
||||
BANK="${BANK_URL:-http://127.0.0.1:9012}"
|
||||
USER="${BANK_USER:-explorer}"
|
||||
PASS="${BANK_PASS:-}"
|
||||
AMOUNT="${AMOUNT:-GOA:10}"
|
||||
LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
|
||||
|
||||
if [ -z "$PASS" ]; then
|
||||
if [ -f /root/bank-explorer-password.txt ]; then
|
||||
PASS=$(tr -d '\n' </root/bank-explorer-password.txt)
|
||||
elif [ -f /tmp/bank-caddy-wire.log ]; then
|
||||
PASS=$(grep -E '^explorer=' /tmp/bank-caddy-wire.log | tail -1 | cut -d= -f2-)
|
||||
fi
|
||||
fi
|
||||
if [ -z "$PASS" ]; then
|
||||
echo "Set BANK_PASS or put password in /root/bank-explorer-password.txt" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORKDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$WORKDIR"' EXIT
|
||||
|
||||
echo "{\"scope\":\"readwrite\",\"refreshable\":true}" >"$WORKDIR/tok.json"
|
||||
curl -sS -m 15 -u "${USER}:${PASS}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @"$WORKDIR/tok.json" \
|
||||
"${BANK}/accounts/${USER}/token" >"$WORKDIR/tok.out"
|
||||
|
||||
TOKEN=$(python3 - "$WORKDIR/tok.out" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
print(d.get("access_token",""))
|
||||
PY
|
||||
)
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "token failed:" >&2
|
||||
cat "$WORKDIR/tok.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "{\"suggested_amount\":\"${AMOUNT}\"}" >"$WORKDIR/wd.json"
|
||||
curl -sS -m 15 \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @"$WORKDIR/wd.json" \
|
||||
"${BANK}/accounts/${USER}/withdrawals" >"$WORKDIR/wd.out"
|
||||
|
||||
URI=$(python3 - "$WORKDIR/wd.out" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
print(d.get("taler_withdraw_uri") or "")
|
||||
if not d.get("taler_withdraw_uri"):
|
||||
sys.stderr.write(open(sys.argv[1]).read()+"\n")
|
||||
sys.exit(1)
|
||||
print(d.get("withdrawal_id",""), file=sys.stderr)
|
||||
PY
|
||||
)
|
||||
|
||||
mkdir -p "$LANDING_DIR"
|
||||
printf '%s\n' "$URI" >"$LANDING_DIR/withdraw.uri"
|
||||
printf '%s\n' "$AMOUNT" >"$LANDING_DIR/withdraw.amount"
|
||||
date -u +%Y-%m-%dT%H:%MZ >"$LANDING_DIR/withdraw.created"
|
||||
# track id for auto-confirm-withdrawals.sh
|
||||
WID=$(basename "$URI")
|
||||
echo "$WID" >>"$LANDING_DIR/withdraw-watch.ids"
|
||||
sort -u "$LANDING_DIR/withdraw-watch.ids" -o "$LANDING_DIR/withdraw-watch.ids"
|
||||
echo "watch_id=$WID (auto-confirm when wallet selects)"
|
||||
|
||||
# QR as SVG (no external deps) via python qrcode if present, else pure matrix via segno/qrcode
|
||||
python3 - "$URI" "$LANDING_DIR/withdraw-qr.svg" <<'PY'
|
||||
import sys
|
||||
uri, out = sys.argv[1], sys.argv[2]
|
||||
try:
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
img = qrcode.make(uri, image_factory=qrcode.image.svg.SvgPathImage)
|
||||
img.save(out)
|
||||
print("qrcode lib ok")
|
||||
except Exception as e:
|
||||
# minimal fallback: write HTML with data attribute for client-side QR
|
||||
open(out, "w").write(
|
||||
f'<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" width="8" height="8">'
|
||||
f'<!-- QR_FALLBACK uri={uri} --></svg>\n'
|
||||
)
|
||||
open(out + ".uri", "w").write(uri)
|
||||
print("fallback:", e)
|
||||
PY
|
||||
|
||||
# Also PNG if pillow/qrcode available
|
||||
python3 - "$URI" "$LANDING_DIR/withdraw-qr.png" <<'PY' || true
|
||||
import sys
|
||||
uri, out = sys.argv[1], sys.argv[2]
|
||||
import qrcode
|
||||
img = qrcode.make(uri, box_size=8, border=2)
|
||||
img.save(out)
|
||||
print("png ok", out)
|
||||
PY
|
||||
|
||||
echo "URI=$URI"
|
||||
echo "wrote under $LANDING_DIR"
|
||||
ls -la "$LANDING_DIR"/withdraw* 2>/dev/null || true
|
||||
|
||||
# Refresh public stats.json when landing-stats is available (in-container path)
|
||||
if [ -x /usr/local/bin/landing-stats.sh ]; then
|
||||
LANDING_DIR="$LANDING_DIR" /usr/local/bin/landing-stats.sh || true
|
||||
elif [ -x "$(dirname "$0")/landing-stats.sh" ]; then
|
||||
LANDING_DIR="$LANDING_DIR" "$(dirname "$0")/landing-stats.sh" || true
|
||||
fi
|
||||
54
scripts/taler-bank/refresh-demo-withdraw.sh
Executable file
54
scripts/taler-bank/refresh-demo-withdraw.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
# Create a fresh demo GOA withdraw for landing QR (no python — runs in bank container).
|
||||
# Writes under LANDING_DIR (default /var/www/bank-landing).
|
||||
#
|
||||
# Inside container:
|
||||
# /usr/local/bin/refresh-demo-withdraw.sh
|
||||
# Host:
|
||||
# podman exec taler-hacktivism-bank /usr/local/bin/refresh-demo-withdraw.sh
|
||||
set -euo pipefail
|
||||
|
||||
BANK="${BANK_URL:-http://127.0.0.1:9012}"
|
||||
USER="${BANK_USER:-explorer}"
|
||||
AMOUNT="${AMOUNT:-GOA:10}"
|
||||
LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
|
||||
PASS="${BANK_PASS:-}"
|
||||
|
||||
if [ -z "$PASS" ]; then
|
||||
for f in "/root/bank-${USER}-password.txt" /root/bank-explorer-password.txt; do
|
||||
if [ -f "$f" ]; then PASS=$(tr -d '\n' <"$f"); break; fi
|
||||
done
|
||||
fi
|
||||
[ -n "$PASS" ] || { echo "Set BANK_PASS or /root/bank-explorer-password.txt" >&2; exit 1; }
|
||||
|
||||
BANK="${BANK%/}"
|
||||
TOK=$(curl -sS -m 12 -u "${USER}:${PASS}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"scope":"readwrite","refreshable":true}' \
|
||||
"${BANK}/accounts/${USER}/token")
|
||||
TOKEN=$(printf '%s' "$TOK" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
[ -n "$TOKEN" ] || { echo "token fail: $TOK" >&2; exit 1; }
|
||||
|
||||
WD=$(curl -sS -m 15 \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"suggested_amount\":\"${AMOUNT}\"}" \
|
||||
"${BANK}/accounts/${USER}/withdrawals")
|
||||
URI=$(printf '%s' "$WD" | sed -n 's/.*"taler_withdraw_uri"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
WID=$(printf '%s' "$WD" | sed -n 's/.*"withdrawal_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
[ -n "$URI" ] || { echo "no URI from: $WD" >&2; exit 1; }
|
||||
[ -n "$WID" ] || WID=$(basename "$URI")
|
||||
|
||||
mkdir -p "$LANDING_DIR"
|
||||
printf '%s\n' "$URI" >"$LANDING_DIR/withdraw.uri"
|
||||
printf '%s\n' "$AMOUNT" >"$LANDING_DIR/withdraw.amount"
|
||||
date -u +%Y-%m-%dT%H:%MZ >"$LANDING_DIR/withdraw.created"
|
||||
echo "$WID" >>"$LANDING_DIR/withdraw-watch.ids"
|
||||
sort -u "$LANDING_DIR/withdraw-watch.ids" -o "$LANDING_DIR/withdraw-watch.ids" 2>/dev/null || true
|
||||
|
||||
echo "URI=$URI"
|
||||
echo "WID=$WID"
|
||||
|
||||
if [ -x /usr/local/bin/landing-stats.sh ]; then
|
||||
LANDING_DIR="$LANDING_DIR" /usr/local/bin/landing-stats.sh || true
|
||||
fi
|
||||
109
scripts/taler-bank/start_bank.sh
Executable file
109
scripts/taler-bank/start_bank.sh
Executable file
|
|
@ -0,0 +1,109 @@
|
|||
#!/bin/bash
|
||||
# Start / restart libeufin-bank serve (manual, no systemd).
|
||||
# Run as: libeufin-bank
|
||||
# Same role as start_merchant.sh / start_exchange.sh.
|
||||
#
|
||||
# Container: taler-hacktivism-bank
|
||||
# Prerequisite: /root/start_base_services_for_taler_bank.sh (as root) for postgres.
|
||||
#
|
||||
# Usage:
|
||||
# start_bank.sh
|
||||
# start_bank.sh --restart | -r
|
||||
# start_bank.sh --help
|
||||
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: start_bank.sh [--restart|-r] [--help|-h]
|
||||
|
||||
(default) Start libeufin-bank serve if not already running.
|
||||
--restart Stop live serve process, then start cleanly.
|
||||
Does not touch postgres (use /root/start_base_services_for_taler_bank.sh).
|
||||
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)" != "libeufin-bank" ]; then
|
||||
echo "This script must be run as user libeufin-bank" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONF=/etc/libeufin/libeufin-bank.conf
|
||||
LOG_DIR=/var/log/libeufin-bank
|
||||
PORT=$(grep -E '^\s*PORT\s*=' /etc/libeufin/bank-overrides.conf 2>/dev/null | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}')
|
||||
PORT=${PORT:-9012}
|
||||
|
||||
# Match both wrapper name and the Java MainKt process that actually listens.
|
||||
list_serve_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_bank.sh*) continue ;;
|
||||
*check_bank-health*) continue ;;
|
||||
esac
|
||||
case "$args" in
|
||||
*libeufin-bank\ serve*|/usr/bin/libeufin-bank\ serve*|*MainKt\ serve*|*tech.libeufin.bank.MainKt*)
|
||||
echo "$pid"
|
||||
;;
|
||||
esac
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
kill_serve() {
|
||||
local pids
|
||||
pids=$(list_serve_pids | tr '\n' ' ')
|
||||
if [ -z "${pids// }" ]; then
|
||||
echo "No live libeufin-bank serve 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_serve_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 "libeufin-bank serve stopped."
|
||||
}
|
||||
|
||||
if [ "$DO_RESTART" -eq 1 ]; then
|
||||
echo "=== restart: kill libeufin-bank serve ==="
|
||||
kill_serve
|
||||
fi
|
||||
|
||||
echo "Start libeufin-bank serve (port $PORT):"
|
||||
LOG_FILE="$LOG_DIR/libeufin-bank-$(date +%Y-%m-%d).log"
|
||||
mkdir -p "$LOG_DIR"
|
||||
touch "$LOG_FILE" 2>/dev/null || true
|
||||
|
||||
if [ "$DO_RESTART" -eq 0 ] && [ -n "$(list_serve_pids)" ]; then
|
||||
echo "libeufin-bank serve already running"
|
||||
else
|
||||
nohup libeufin-bank serve -c "$CONF" >>"$LOG_FILE" 2>&1 &
|
||||
disown 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
echo "Live processes:"
|
||||
ps -eo pid,stat,args 2>/dev/null | grep -E 'libeufin-bank serve|MainKt serve' | grep -v grep | grep -v ' Z ' || true
|
||||
|
||||
if [ -x /usr/local/bin/check_bank-health.sh ]; then
|
||||
/usr/local/bin/check_bank-health.sh || exit 1
|
||||
elif [ -x ./check_bank-health.sh ]; then
|
||||
./check_bank-health.sh || exit 1
|
||||
fi
|
||||
exit 0
|
||||
148
scripts/taler-bank/start_base_services_for_taler_bank.sh
Executable file
148
scripts/taler-bank/start_base_services_for_taler_bank.sh
Executable file
|
|
@ -0,0 +1,148 @@
|
|||
#!/bin/bash
|
||||
# Root: base services for manual libeufin-bank (like merchant/exchange start_base).
|
||||
# Then interactive shell as libeufin-bank → run start_bank.sh there.
|
||||
#
|
||||
# Container: taler-hacktivism-bank
|
||||
#
|
||||
# Usage:
|
||||
# /root/start_base_services_for_taler_bank.sh
|
||||
# /root/start_base_services_for_taler_bank.sh --no-shell
|
||||
# /root/start_base_services_for_taler_bank.sh --no-shell --start-bank
|
||||
# /root/start_base_services_for_taler_bank.sh --no-shell --start-bank --restart
|
||||
|
||||
set -e
|
||||
CONF=/etc/libeufin/libeufin-bank.conf
|
||||
LOG_DIR=/var/log/libeufin-bank
|
||||
PWD_BIN=/usr/local/bin
|
||||
BANK_STARTER=start_bank.sh
|
||||
BANK_USER=libeufin-bank
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NO_SHELL=0
|
||||
START_BANK=0
|
||||
BANK_RESTART=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-shell|-n) NO_SHELL=1 ;;
|
||||
--start-bank) START_BANK=1; NO_SHELL=1 ;;
|
||||
--restart|-r) BANK_RESTART=1 ;;
|
||||
--help|-h)
|
||||
cat <<'EOF'
|
||||
Usage: start_base_services_for_taler_bank.sh [options]
|
||||
|
||||
(default) Start postgres + dirs, then interactive shell as libeufin-bank
|
||||
--no-shell Start base services only, do not open a shell
|
||||
--start-bank After base services, run /usr/local/bin/start_bank.sh as libeufin-bank
|
||||
(implies --no-shell)
|
||||
--restart With --start-bank: pass --restart to start_bank.sh
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $arg" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- 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
|
||||
chmod 2775 /var/run/postgresql 2>/dev/null || chmod 775 /var/run/postgresql
|
||||
|
||||
# Snakeoil TLS key: postgres fails if it cannot read the key (common in rootless images).
|
||||
# Prefer fixing perms; if still broken, force ssl=off for local-only DB.
|
||||
if [ -f /etc/ssl/private/ssl-cert-snakeoil.key ]; then
|
||||
chown root:ssl-cert /etc/ssl/private/ssl-cert-snakeoil.key 2>/dev/null || true
|
||||
chmod 640 /etc/ssl/private/ssl-cert-snakeoil.key 2>/dev/null || true
|
||||
fi
|
||||
for pgconf in /etc/postgresql/*/main/postgresql.conf; do
|
||||
[ -f "$pgconf" ] || continue
|
||||
if grep -qE '^\s*ssl\s*=' "$pgconf"; then
|
||||
sed -i 's/^\s*ssl\s*=.*/ssl = off/' "$pgconf"
|
||||
else
|
||||
echo "ssl = off" >>"$pgconf"
|
||||
fi
|
||||
done
|
||||
|
||||
if pg_isready -q 2>/dev/null; then
|
||||
echo " already accepting connections"
|
||||
pg_isready || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
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 "Create log + data dirs... giving permission to ${BANK_USER}:"
|
||||
mkdir -p "$LOG_DIR" /var/lib/libeufin-bank
|
||||
chown "${BANK_USER}:${BANK_USER}" "$LOG_DIR" /var/lib/libeufin-bank
|
||||
chmod 755 "$LOG_DIR" /var/lib/libeufin-bank
|
||||
|
||||
echo "Start base services needed for GOA Exploration Bank."
|
||||
echo ""
|
||||
|
||||
echo "1. postgresql:"
|
||||
ensure_postgresql
|
||||
# ensure role + DB (postgres:///libeufin) — Debian createuser/createdb as postgres
|
||||
su -s /bin/bash postgres -c "psql -tc \"SELECT 1 FROM pg_roles WHERE rolname='libeufin-bank'\" | grep -q 1 || createuser -s libeufin-bank" || true
|
||||
su -s /bin/bash postgres -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='libeufin'\" | grep -q 1 || createdb -O libeufin-bank libeufin" || true
|
||||
pg_isready || true
|
||||
|
||||
if [ "$START_BANK" -eq 1 ]; then
|
||||
echo ""
|
||||
echo "2. start_bank.sh as ${BANK_USER}:"
|
||||
if [ ! -x "$PWD_BIN/$BANK_STARTER" ]; then
|
||||
echo "missing $PWD_BIN/$BANK_STARTER" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$BANK_RESTART" -eq 1 ]; then
|
||||
runuser -u "$BANK_USER" -- "$PWD_BIN/$BANK_STARTER" --restart
|
||||
else
|
||||
runuser -u "$BANK_USER" -- "$PWD_BIN/$BANK_STARTER"
|
||||
fi
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if [ "$NO_SHELL" -eq 1 ]; then
|
||||
echo "Base services started (--no-shell). Next: runuser -u ${BANK_USER} -- $PWD_BIN/$BANK_STARTER [--restart]"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "2. Switching now to user ${BANK_USER}, in $PWD_BIN; find executable $BANK_STARTER there!"
|
||||
echo ""
|
||||
cd "$PWD_BIN"
|
||||
# util-linux: -u and -s are mutually exclusive (same as exchange/merchant)
|
||||
exec runuser -u "$BANK_USER" -- bash
|
||||
48
scripts/taler-dns/README.md
Normal file
48
scripts/taler-dns/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Container DNS pins (hacktivism.ch)
|
||||
|
||||
Pasta networking often has **no usable DNS**. Wirewatch needs:
|
||||
|
||||
```text
|
||||
https://bank.hacktivism.ch/accounts/exchange/taler-wire-gateway/
|
||||
```
|
||||
|
||||
## Bake into images (preferred)
|
||||
|
||||
On koopa:
|
||||
|
||||
```bash
|
||||
# copy bake assets, then:
|
||||
SRC_DIR=/tmp/taler-dns-bake ./bake-hosts-into-images.sh
|
||||
```
|
||||
|
||||
This:
|
||||
|
||||
1. Installs `/usr/local/bin/pin-hacktivism-hosts` **inside** each container
|
||||
2. Hooks `start_base_services_for_taler*.sh` (bank + merchant)
|
||||
3. Enables systemd `pin-hacktivism-hosts.service` (exchange, before wirewatch)
|
||||
4. **Commits** images:
|
||||
- `localhost/taler-hacktivism-banking:live` (+ `:hosts-pinned`)
|
||||
- `localhost/taler-hacktivism-live:landing` (+ `:hosts-pinned`)
|
||||
- `localhost/taler-hacktivism-exchange-ansible:landing` (+ `:hosts-pinned`)
|
||||
|
||||
Pin IP default: `212.51.151.254` (`PIN_IP=` to override).
|
||||
|
||||
## One-shot without bake
|
||||
|
||||
```bash
|
||||
./pin-container-hosts.sh # apply now
|
||||
./pin-container-hosts.sh --check # report resolve + /config
|
||||
```
|
||||
|
||||
## After recreate
|
||||
|
||||
If you start from an **old** image without the bake, run `pin-container-hosts.sh` once.
|
||||
If you use `:hosts-pinned` / updated `:live`/`:landing` images, start_base / systemd re-pin on boot.
|
||||
|
||||
Optional podman flags when recreating (extra safety):
|
||||
|
||||
```bash
|
||||
--add-host=bank.hacktivism.ch:212.51.151.254 \
|
||||
--add-host=exchange.hacktivism.ch:212.51.151.254 \
|
||||
--add-host=taler.hacktivism.ch:212.51.151.254
|
||||
```
|
||||
116
scripts/taler-dns/bake-hosts-into-images.sh
Normal file
116
scripts/taler-dns/bake-hosts-into-images.sh
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/bin/bash
|
||||
# Install in-container pin script (+ systemd on exchange), hook start_base scripts,
|
||||
# commit images so recreate keeps the bake.
|
||||
#
|
||||
# Run on koopa:
|
||||
# ./bake-hosts-into-images.sh
|
||||
set -euo pipefail
|
||||
|
||||
PIN_IP="${PIN_IP:-212.51.151.254}"
|
||||
SRC_DIR="${SRC_DIR:-/tmp/taler-dns-bake}"
|
||||
mkdir -p "$SRC_DIR"
|
||||
|
||||
# expect these next to us or in SRC_DIR
|
||||
SCRIPT_IN="${SCRIPT_IN:-$SRC_DIR/pin-hacktivism-hosts.in-container.sh}"
|
||||
UNIT_IN="${UNIT_IN:-$SRC_DIR/pin-hacktivism-hosts.service}"
|
||||
|
||||
[ -f "$SCRIPT_IN" ] || { echo "missing $SCRIPT_IN" >&2; exit 1; }
|
||||
|
||||
install_into() {
|
||||
local C="$1"
|
||||
if ! podman ps --format '{{.Names}}' | grep -qx "$C"; then
|
||||
echo "SKIP $C (not running)"
|
||||
return 0
|
||||
fi
|
||||
echo "=== install into $C ==="
|
||||
podman cp "$SCRIPT_IN" "$C:/usr/local/bin/pin-hacktivism-hosts"
|
||||
podman exec "$C" chmod 755 /usr/local/bin/pin-hacktivism-hosts
|
||||
# run once now
|
||||
podman exec -e PIN_IP="$PIN_IP" "$C" /usr/local/bin/pin-hacktivism-hosts
|
||||
podman exec "$C" grep hacktivism /etc/hosts || true
|
||||
}
|
||||
|
||||
hook_start_base() {
|
||||
local C="$1" base="$2"
|
||||
podman exec "$C" bash -lc "
|
||||
set -e
|
||||
f='$base'
|
||||
if [ ! -f \"\$f\" ]; then echo \"no \$f\"; exit 0; fi
|
||||
if grep -q pin-hacktivism-hosts \"\$f\"; then
|
||||
echo \"already hooked: \$f\"
|
||||
exit 0
|
||||
fi
|
||||
# insert right after shebang / first line block
|
||||
cp \"\$f\" \"\$f.bak-before-pin\"
|
||||
{
|
||||
head -n 1 \"\$f\"
|
||||
echo ''
|
||||
echo '# Pin public GOA hostnames (pasta DNS). Baked by bake-hosts-into-images.sh'
|
||||
echo 'if [ -x /usr/local/bin/pin-hacktivism-hosts ]; then'
|
||||
echo ' PIN_IP=${PIN_IP} /usr/local/bin/pin-hacktivism-hosts || true'
|
||||
echo 'fi'
|
||||
tail -n +2 \"\$f\"
|
||||
} > \"\$f.new\"
|
||||
mv \"\$f.new\" \"\$f\"
|
||||
chmod +x \"\$f\"
|
||||
echo \"hooked \$f\"
|
||||
"
|
||||
}
|
||||
|
||||
# --- bank ---
|
||||
install_into taler-hacktivism-bank
|
||||
hook_start_base taler-hacktivism-bank /root/start_base_services_for_taler_bank.sh
|
||||
|
||||
# --- merchant ---
|
||||
install_into taler-hacktivism
|
||||
hook_start_base taler-hacktivism /root/start_base_services_for_taler.sh
|
||||
|
||||
# --- exchange (systemd) ---
|
||||
install_into taler-hacktivism-exchange-ansible
|
||||
if [ -f "$UNIT_IN" ]; then
|
||||
podman cp "$UNIT_IN" taler-hacktivism-exchange-ansible:/etc/systemd/system/pin-hacktivism-hosts.service
|
||||
podman exec taler-hacktivism-exchange-ansible bash -lc '
|
||||
sed -i "s/PIN_IP=212.51.151.254/PIN_IP='"${PIN_IP}"'/" /etc/systemd/system/pin-hacktivism-hosts.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable pin-hacktivism-hosts.service
|
||||
systemctl start pin-hacktivism-hosts.service
|
||||
systemctl is-enabled pin-hacktivism-hosts.service
|
||||
# ensure wirewatch starts after pin
|
||||
mkdir -p /etc/systemd/system/taler-exchange-wirewatch.service.d
|
||||
cat > /etc/systemd/system/taler-exchange-wirewatch.service.d/pin-hosts.conf <<EOF
|
||||
[Unit]
|
||||
After=pin-hacktivism-hosts.service
|
||||
Requires=pin-hacktivism-hosts.service
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl restart taler-exchange-wirewatch 2>/dev/null || true
|
||||
'
|
||||
fi
|
||||
|
||||
# --- commit images ---
|
||||
echo "=== commit images ==="
|
||||
podman commit taler-hacktivism-bank \
|
||||
localhost/taler-hacktivism-banking:live
|
||||
podman commit taler-hacktivism-bank \
|
||||
localhost/taler-hacktivism-banking:hosts-pinned
|
||||
|
||||
podman commit taler-hacktivism \
|
||||
localhost/taler-hacktivism-live:landing
|
||||
podman commit taler-hacktivism \
|
||||
localhost/taler-hacktivism-live:hosts-pinned
|
||||
|
||||
podman commit taler-hacktivism-exchange-ansible \
|
||||
localhost/taler-hacktivism-exchange-ansible:landing
|
||||
podman commit taler-hacktivism-exchange-ansible \
|
||||
localhost/taler-hacktivism-exchange-ansible:hosts-pinned
|
||||
|
||||
echo
|
||||
echo "Images updated:"
|
||||
podman images | grep -E 'hacktivism|hosts-pinned' | head -20
|
||||
echo
|
||||
echo "Verify:"
|
||||
for C in taler-hacktivism-bank taler-hacktivism-exchange-ansible taler-hacktivism; do
|
||||
echo -n "$C: "
|
||||
podman exec "$C" getent ahostsv4 bank.hacktivism.ch 2>/dev/null | head -1 || echo FAIL
|
||||
done
|
||||
echo "done."
|
||||
82
scripts/taler-dns/pin-container-hosts.sh
Executable file
82
scripts/taler-dns/pin-container-hosts.sh
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
#!/bin/bash
|
||||
# Pin public GOA hostnames inside Taler containers (pasta often has no useful DNS).
|
||||
# Required so wirewatch can reach https://bank.hacktivism.ch/... wire-gateway.
|
||||
#
|
||||
# Run on koopa (host):
|
||||
# ./pin-container-hosts.sh # apply + show resolve
|
||||
# ./pin-container-hosts.sh --check # only show
|
||||
# PIN_IP=212.51.151.254 ./pin-container-hosts.sh
|
||||
set -euo pipefail
|
||||
|
||||
CHECK_ONLY=0
|
||||
[ "${1:-}" = "--check" ] && CHECK_ONLY=1
|
||||
|
||||
DOMAINS=(bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch)
|
||||
CONTAINERS=(
|
||||
taler-hacktivism-bank
|
||||
taler-hacktivism-exchange-ansible
|
||||
taler-hacktivism
|
||||
)
|
||||
|
||||
PIN_IP="${PIN_IP:-}"
|
||||
if [ -z "$PIN_IP" ]; then
|
||||
PIN_IP=$(getent ahostsv4 bank.hacktivism.ch 2>/dev/null | awk '{print $1; exit}' || true)
|
||||
fi
|
||||
[ -n "$PIN_IP" ] || PIN_IP=212.51.151.254
|
||||
|
||||
echo "PIN_IP=$PIN_IP"
|
||||
echo "domains: ${DOMAINS[*]}"
|
||||
echo
|
||||
|
||||
pin_one() {
|
||||
local C="$1"
|
||||
if ! podman ps --format '{{.Names}}' | grep -qx "$C"; then
|
||||
echo "=== $C === SKIP (not running)"
|
||||
return 0
|
||||
fi
|
||||
if [ "$CHECK_ONLY" != "1" ]; then
|
||||
# Keep localhost lines; drop previous hacktivism pins; append ours
|
||||
podman exec "$C" bash -lc "
|
||||
set -e
|
||||
TMP=\$(mktemp)
|
||||
# keep non-hacktivism lines
|
||||
if [ -f /etc/hosts ]; then
|
||||
grep -vE 'hacktivism\\.ch|[[:space:]]bank\\.hacktivism|[[:space:]]exchange\\.hacktivism|[[:space:]]taler\\.hacktivism' /etc/hosts > \"\$TMP\" || true
|
||||
fi
|
||||
# ensure localhost
|
||||
grep -qE '^127\\.0\\.0\\.1[[:space:]]+localhost' \"\$TMP\" 2>/dev/null || echo '127.0.0.1 localhost' >> \"\$TMP\"
|
||||
grep -qE '^::1[[:space:]]' \"\$TMP\" 2>/dev/null || echo '::1 localhost ip6-localhost ip6-loopback' >> \"\$TMP\"
|
||||
echo '${PIN_IP} bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch' >> \"\$TMP\"
|
||||
# de-dupe consecutive blank lines lightly
|
||||
cat \"\$TMP\" > /etc/hosts
|
||||
rm -f \"\$TMP\"
|
||||
"
|
||||
# wirewatch needs bank DNS after pin
|
||||
if [ "$C" = "taler-hacktivism-exchange-ansible" ]; then
|
||||
podman exec "$C" systemctl try-restart taler-exchange-wirewatch 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== $C ==="
|
||||
podman exec "$C" grep -E 'hacktivism|localhost' /etc/hosts 2>/dev/null || true
|
||||
for h in "${DOMAINS[@]}"; do
|
||||
# ahostsv4 first (matches what wire tools need)
|
||||
ip=$(podman exec "$C" getent ahostsv4 "$h" 2>/dev/null | awk '{print $1; exit}' || true)
|
||||
[ -n "$ip" ] || ip=$(podman exec "$C" getent hosts "$h" 2>/dev/null | awk '{print $1; exit}' || true)
|
||||
code=$(podman exec "$C" curl -skS -m 4 -o /dev/null -w '%{http_code}' "https://${h}/config" 2>/dev/null || echo 000)
|
||||
if [ -n "$ip" ] && [ "$code" = "200" ]; then
|
||||
echo " OK $h → $ip /config=$code"
|
||||
elif [ -n "$ip" ]; then
|
||||
echo " WARN $h → $ip /config=$code"
|
||||
else
|
||||
echo " FAIL $h → (no resolve) /config=$code"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
for C in "${CONTAINERS[@]}"; do
|
||||
pin_one "$C"
|
||||
done
|
||||
|
||||
echo "done (CHECK_ONLY=$CHECK_ONLY)"
|
||||
31
scripts/taler-dns/pin-hacktivism-hosts.in-container.sh
Normal file
31
scripts/taler-dns/pin-hacktivism-hosts.in-container.sh
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
# Runs *inside* a Taler container. Pasta regenerates /etc/hosts on start —
|
||||
# call this from systemd (exchange) or start_base_*.sh (merchant/bank).
|
||||
set -euo pipefail
|
||||
|
||||
PIN_IP="${PIN_IP:-212.51.151.254}"
|
||||
MARKER="# hacktivism-goa-pin"
|
||||
|
||||
TMP=$(mktemp)
|
||||
if [ -f /etc/hosts ]; then
|
||||
# drop any previous pin marker lines and lines that name our domains
|
||||
grep -vE "${MARKER}|hacktivism\\.ch" /etc/hosts >"$TMP" || true
|
||||
else
|
||||
: >"$TMP"
|
||||
fi
|
||||
|
||||
grep -qE '^127\.0\.0\.1[[:space:]]' "$TMP" 2>/dev/null \
|
||||
|| echo '127.0.0.1 localhost' >>"$TMP"
|
||||
grep -qE '^::1[[:space:]]' "$TMP" 2>/dev/null \
|
||||
|| echo '::1 localhost ip6-localhost ip6-loopback' >>"$TMP"
|
||||
|
||||
{
|
||||
cat "$TMP"
|
||||
echo "$MARKER"
|
||||
echo "${PIN_IP} bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch"
|
||||
echo "$MARKER"
|
||||
} > /etc/hosts
|
||||
rm -f "$TMP"
|
||||
|
||||
logger -t pin-hacktivism-hosts "pinned ${PIN_IP} → bank/exchange/taler.hacktivism.ch" 2>/dev/null || true
|
||||
exit 0
|
||||
16
scripts/taler-dns/pin-hacktivism-hosts.service
Normal file
16
scripts/taler-dns/pin-hacktivism-hosts.service
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[Unit]
|
||||
Description=Pin bank/exchange/taler.hacktivism.ch in /etc/hosts (pasta DNS)
|
||||
DefaultDependencies=no
|
||||
After=local-fs.target
|
||||
Before=network-online.target taler-exchange-wirewatch.service taler-exchange-httpd.service
|
||||
Wants=network-pre.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
Environment=PIN_IP=212.51.151.254
|
||||
ExecStart=/usr/local/bin/pin-hacktivism-hosts
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=taler-exchange-wirewatch.service
|
||||
35
scripts/taler-exchange/archive/README.md
Normal file
35
scripts/taler-exchange/archive/README.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Exchange archive / bootstrap
|
||||
|
||||
## `exchange-bootstrap.sh` (greenfield only)
|
||||
|
||||
One-shot bootstrap for a **new** exchange container. **Not** used for daily ops.
|
||||
|
||||
Includes current **GOA exploration currency**:
|
||||
|
||||
- `exchange-overrides.conf` pattern (no `zz-*`)
|
||||
- `PORT = 9011`, `SERVE = tcp`
|
||||
- Unit names: Yotta…Atomic-GOA (scale −8…24)
|
||||
- ATM `common_amounts` 10…1000
|
||||
- Full 1–2–5 **coin** ladder (Micro-GOA … 10 GOA) in `conf.d/exchange-coins.conf`
|
||||
|
||||
After bootstrap:
|
||||
|
||||
1. `/root/start_base_services_for_taler_exchange.sh` (root)
|
||||
2. `./start_exchange.sh` as `taler-exchange-httpd`
|
||||
3. Offline: wire enable + denom/signkey sign for `/keys`
|
||||
Live helpers: `../wire-enable-and-upload.sh`, `../offline-sign-upload-keys.sh`, `../start_wire_helpers.sh`
|
||||
|
||||
## Removed
|
||||
|
||||
| Script | Why |
|
||||
|--------|-----|
|
||||
| `exchange-start-all.sh` | Redundant with base + `start_exchange.sh` |
|
||||
| old zz-hacktivism bootstrap | Wrong ports/currency; superseded |
|
||||
|
||||
## Daily layout (merchant model)
|
||||
|
||||
| Path | User |
|
||||
|------|------|
|
||||
| `/root/start_base_services_for_taler_exchange.sh` | root |
|
||||
| `/usr/local/bin/start_exchange.sh` | `taler-exchange-httpd` |
|
||||
| `/usr/local/bin/check_exchange-health.sh` | httpd / any |
|
||||
451
scripts/taler-exchange/archive/exchange-bootstrap.sh
Executable file
451
scripts/taler-exchange/archive/exchange-bootstrap.sh
Executable file
|
|
@ -0,0 +1,451 @@
|
|||
#!/bin/bash
|
||||
# Greenfield bootstrap for exchange.hacktivism.ch (GOA exploration currency).
|
||||
# Run as root inside container. Not for daily ops — use start_base + start_exchange.
|
||||
#
|
||||
# Writes:
|
||||
# /etc/taler-exchange/exchange-overrides.conf
|
||||
# /etc/taler-exchange/conf.d/exchange-coins.conf
|
||||
# Ensures main conf inlines exchange-overrides.conf (merchant pattern).
|
||||
# Does NOT use conf.d/zz-*.
|
||||
#
|
||||
# After this: offline denom sign + wire still required for /keys withdraw.
|
||||
|
||||
set -euo pipefail
|
||||
CONF=/etc/taler-exchange/taler-exchange.conf
|
||||
OV=/etc/taler-exchange/exchange-overrides.conf
|
||||
COINS=/etc/taler-exchange/conf.d/exchange-coins.conf
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== 1. postgres ==="
|
||||
/etc/init.d/postgresql start
|
||||
sleep 2
|
||||
pg_isready || true
|
||||
|
||||
echo "=== 2. master key (offline user) ==="
|
||||
mkdir -p /var/lib/taler-exchange/offline
|
||||
chown -R taler-exchange-offline:taler-exchange-offline /var/lib/taler-exchange/offline
|
||||
chown taler-exchange-offline:taler-exchange-offline /var/lib/taler-exchange 2>/dev/null || true
|
||||
OUT=$(runuser -u taler-exchange-offline -- taler-exchange-offline -c "$CONF" setup 2>&1) || true
|
||||
echo "$OUT"
|
||||
MASTER_PUB=$(echo "$OUT" | grep -oE '[A-Z0-9]{40,}' | tail -1)
|
||||
if [ -z "${MASTER_PUB:-}" ]; then
|
||||
echo "ERROR: no master public key from offline setup" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "MASTER_PUBLIC_KEY=$MASTER_PUB"
|
||||
ATTR_KEY=$(openssl rand -hex 32)
|
||||
SECRETS_DIR=/etc/taler-exchange/secrets
|
||||
mkdir -p "$SECRETS_DIR"
|
||||
cat >"$SECRETS_DIR/exchange-attribute-encryption.secret.conf" <<SEOF
|
||||
# generated by exchange-bootstrap.sh
|
||||
[exchange]
|
||||
ATTRIBUTE_ENCRYPTION_KEY = ${ATTR_KEY}
|
||||
SEOF
|
||||
chmod 640 "$SECRETS_DIR/exchange-attribute-encryption.secret.conf"
|
||||
chown root:taler-exchange-httpd "$SECRETS_DIR/exchange-attribute-encryption.secret.conf" 2>/dev/null || true
|
||||
|
||||
echo "=== 3. exchange-overrides.conf (site; not zz-*) ==="
|
||||
# Ensure main conf inlines overrides (merchant pattern)
|
||||
if ! grep -q 'exchange-overrides.conf' "$CONF" 2>/dev/null; then
|
||||
printf '\n# Manual site overrides (merchant pattern)\n@inline@ exchange-overrides.conf\n' >>"$CONF"
|
||||
fi
|
||||
# Remove legacy zz drop-ins if present
|
||||
rm -f /etc/taler-exchange/conf.d/zz-hacktivism.conf \
|
||||
/etc/taler-exchange/conf.d/zz-hacktivism-coins.conf 2>/dev/null || true
|
||||
|
||||
cat >"$OV" <<EOF
|
||||
# Manual site overrides for exchange.hacktivism.ch (GOA exploration currency).
|
||||
# Same role as /etc/taler-merchant/merchant-overrides.conf on taler-hacktivism.
|
||||
# Do not edit overrides.conf (tooling). Do not edit package conf.d defaults.
|
||||
# No conf.d/zz-* drop-ins.
|
||||
|
||||
[exchange]
|
||||
CURRENCY = GOA
|
||||
CURRENCY_ROUND_UNIT = GOA:0.00000001
|
||||
TINY_AMOUNT = GOA:0.00000001
|
||||
DEFAULT_P2P_EXPIRATION = 14 days
|
||||
BASE_URL = https://exchange.hacktivism.ch/
|
||||
SERVE = tcp
|
||||
PORT = 9011
|
||||
MASTER_PUBLIC_KEY = ${MASTER_PUB}
|
||||
# ATTRIBUTE_ENCRYPTION_KEY via @inline-secret@ (see secrets/)
|
||||
@inline-secret@ exchange-attribute-encryption ../secrets/exchange-attribute-encryption.secret.conf
|
||||
|
||||
[currency-goa]
|
||||
ENABLED = YES
|
||||
name = "GOA exploration currency"
|
||||
code = GOA
|
||||
# SI display units. taler-exchange rejects scale keys outside [-8, 24] (no Ronna/Quetta).
|
||||
fractional_input_digits = 8
|
||||
fractional_normal_digits = 0
|
||||
fractional_trailing_zero_digits = 0
|
||||
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"
|
||||
|
||||
### Disable package demonstrator currencies (like merchant-overrides)
|
||||
[currency-kudos]
|
||||
ENABLED = NO
|
||||
|
||||
[currency-testkudos]
|
||||
ENABLED = NO
|
||||
EOF
|
||||
echo "Wrote $OV"
|
||||
grep -E '^(name|code|PORT|CURRENCY|alt_unit|fractional_input|common_)' "$OV" || true
|
||||
|
||||
echo "=== 4. GOA coin denominations (1–2–5 ladder) ==="
|
||||
if [ -f "$COINS" ] && ! grep -q 'VALUE = GOA' "$COINS" 2>/dev/null; then
|
||||
mv "$COINS" "${COINS}.package-kudos"
|
||||
echo "Moved package coins -> ${COINS}.package-kudos"
|
||||
fi
|
||||
|
||||
cat >"$COINS" << 'COINS_EOF'
|
||||
# GOA denominations for exchange.hacktivism.ch
|
||||
# Units: 1 GOA = 1000 mGOA = 1_000_000 uGOA
|
||||
# VALUE uses base currency; coin sections cover 1–2–5 ladders at u/m/whole scale.
|
||||
|
||||
# --- uGOA (10^-6 GOA) ---
|
||||
[coin_goa_0_000001]
|
||||
VALUE = GOA:0.000001
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_000002]
|
||||
VALUE = GOA:0.000002
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_000005]
|
||||
VALUE = GOA:0.000005
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_00001]
|
||||
VALUE = GOA:0.00001
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_00002]
|
||||
VALUE = GOA:0.00002
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_00005]
|
||||
VALUE = GOA:0.00005
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_0001]
|
||||
VALUE = GOA:0.0001
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_0002]
|
||||
VALUE = GOA:0.0002
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_0005]
|
||||
VALUE = GOA:0.0005
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
# --- mGOA (10^-3 GOA) ---
|
||||
[coin_goa_0_001]
|
||||
VALUE = GOA:0.001
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_002]
|
||||
VALUE = GOA:0.002
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_005]
|
||||
VALUE = GOA:0.005
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_01]
|
||||
VALUE = GOA:0.01
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_02]
|
||||
VALUE = GOA:0.02
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_05]
|
||||
VALUE = GOA:0.05
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_1]
|
||||
VALUE = GOA:0.1
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_2]
|
||||
VALUE = GOA:0.2
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_0_5]
|
||||
VALUE = GOA:0.5
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
# --- whole GOA ---
|
||||
[coin_goa_1_0]
|
||||
VALUE = GOA:1
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_2_0]
|
||||
VALUE = GOA:2
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_5_0]
|
||||
VALUE = GOA:5
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_10_0]
|
||||
VALUE = GOA:10
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_20_0]
|
||||
VALUE = GOA:20
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_50_0]
|
||||
VALUE = GOA:50
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_100_0]
|
||||
VALUE = GOA:100
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_200_0]
|
||||
VALUE = GOA:200
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
|
||||
[coin_goa_1000_0]
|
||||
VALUE = GOA:1000
|
||||
DURATION_WITHDRAW = 7 days
|
||||
DURATION_SPEND = 2 years
|
||||
DURATION_LEGAL = 3 years
|
||||
FEE_WITHDRAW = GOA:0
|
||||
FEE_DEPOSIT = GOA:0
|
||||
FEE_REFRESH = GOA:0
|
||||
FEE_REFUND = GOA:0
|
||||
RSA_KEYSIZE = 2048
|
||||
CIPHER = RSA
|
||||
COINS_EOF
|
||||
|
||||
echo "Wrote $COINS ($(grep -c '^\[coin_' "$COINS") coin sections)"
|
||||
|
||||
echo "=== 5. database ==="
|
||||
taler-exchange-dbconfig 2>&1 || true
|
||||
runuser -u taler-exchange-httpd -- taler-exchange-dbinit -c "$CONF" 2>&1 || true
|
||||
|
||||
echo "=== 6. next steps (manual) ==="
|
||||
echo " - /root/start_base_services_for_taler_exchange.sh # root: secmods + shell as httpd"
|
||||
echo " - ./start_exchange.sh # as taler-exchange-httpd"
|
||||
echo " - offline: sign denominations + wire accounts for /keys"
|
||||
echo " - MASTER_PUBLIC_KEY=$MASTER_PUB"
|
||||
echo "DONE bootstrap"
|
||||
88
scripts/taler-exchange/check_exchange-health.sh
Executable file
88
scripts/taler-exchange/check_exchange-health.sh
Executable file
|
|
@ -0,0 +1,88 @@
|
|||
#!/bin/bash
|
||||
# Health check for manual taler-exchange (same style as check_merchant-health.sh).
|
||||
# Runnable as taler-exchange-httpd (or any user that can see processes + curl localhost).
|
||||
|
||||
CONF=/etc/taler-exchange/taler-exchange.conf
|
||||
SOCK=/run/taler-exchange/httpd/exchange-http.sock
|
||||
|
||||
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"; }
|
||||
|
||||
echo "=== Taler Exchange Health Check ==="
|
||||
|
||||
# 1. secmods (started by root base script)
|
||||
for p in taler-exchange-secmod-rsa taler-exchange-secmod-cs taler-exchange-secmod-eddsa; do
|
||||
if pgrep -f "$p" >/dev/null 2>&1; then
|
||||
ok "process $p"
|
||||
else
|
||||
bad "process $p not running"
|
||||
fi
|
||||
done
|
||||
|
||||
# 2. httpd
|
||||
if pgrep -f taler-exchange-httpd >/dev/null 2>&1; then
|
||||
ok "process taler-exchange-httpd"
|
||||
else
|
||||
bad "process taler-exchange-httpd is NOT running"
|
||||
fi
|
||||
|
||||
# 3. serving mode
|
||||
SERVE=$(taler-exchange-config -c "$CONF" -s exchange -o SERVE 2>/dev/null || echo unix)
|
||||
PORT=$(taler-exchange-config -c "$CONF" -s exchange -o PORT 2>/dev/null || echo 9011)
|
||||
|
||||
if [ "$SERVE" = "tcp" ]; then
|
||||
if curl -sf -m 3 "http://127.0.0.1:${PORT}/config" >/dev/null 2>&1; then
|
||||
ok "HTTP /config on 127.0.0.1:${PORT}"
|
||||
elif curl -sf -m 3 "http://127.0.0.1:${PORT}/keys" >/dev/null 2>&1; then
|
||||
ok "HTTP /keys on 127.0.0.1:${PORT}"
|
||||
else
|
||||
bad "no HTTP response on 127.0.0.1:${PORT} (/config|/keys)"
|
||||
fi
|
||||
else
|
||||
if [ -S "$SOCK" ]; then
|
||||
ok "socket $SOCK"
|
||||
else
|
||||
bad "socket does NOT exist: $SOCK"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. wire helpers — ensure (no systemd) then require
|
||||
if [ "${SKIP_ENSURE:-0}" != "1" ]; then
|
||||
if [ -x /usr/local/bin/ensure_exchange_helpers.sh ]; then
|
||||
echo "--- ensure_exchange_helpers ---"
|
||||
/usr/local/bin/ensure_exchange_helpers.sh || warn "ensure_exchange_helpers exited non-zero"
|
||||
elif [ -x "$(dirname "$0")/ensure_exchange_helpers.sh" ]; then
|
||||
echo "--- ensure_exchange_helpers ---"
|
||||
"$(dirname "$0")/ensure_exchange_helpers.sh" || warn "ensure_exchange_helpers exited non-zero"
|
||||
elif [ -x /root/ensure_exchange_helpers.sh ]; then
|
||||
echo "--- ensure_exchange_helpers ---"
|
||||
/root/ensure_exchange_helpers.sh || warn "ensure_exchange_helpers exited non-zero"
|
||||
fi
|
||||
fi
|
||||
|
||||
live_helper() {
|
||||
local p="$1"
|
||||
# COMM is 15 chars; use full cmdline match
|
||||
pgrep -f "(^|/)(${p})( |$)" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
for p in taler-exchange-aggregator taler-exchange-wirewatch taler-exchange-transfer taler-exchange-closer; do
|
||||
if live_helper "$p"; then
|
||||
ok "process $p"
|
||||
else
|
||||
bad "process $p not running (settlement needs transfer+aggregator)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$fail" -eq 0 ]; then
|
||||
green "=== ALL CRITICAL CHECKS PASSED ==="
|
||||
exit 0
|
||||
fi
|
||||
red "=== SOME CHECKS FAILED ==="
|
||||
exit 1
|
||||
75
scripts/taler-exchange/ensure_exchange_helpers.sh
Executable file
75
scripts/taler-exchange/ensure_exchange_helpers.sh
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/bin/bash
|
||||
# Ensure exchange wire/db helper processes are running (no systemd).
|
||||
# Root inside taler-exchange container (or host if packages installed there).
|
||||
#
|
||||
# Usage:
|
||||
# ensure_exchange_helpers.sh
|
||||
# ENSURE_ONLY=1 ensure_exchange_helpers.sh # start missing, no health summary
|
||||
set -euo pipefail
|
||||
|
||||
CONF="${TALER_EXCHANGE_CONFIG:-/etc/taler-exchange/taler-exchange.conf}"
|
||||
LOG_DIR="${TALER_EXCHANGE_LOG_DIR:-/var/log/taler-exchange}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "root only" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
chmod 755 "$LOG_DIR" 2>/dev/null || true
|
||||
|
||||
# Live process under user. Note: Linux COMM is 15 chars — never use pgrep -x
|
||||
# for long names like taler-exchange-aggregator / taler-exchange-wirewatch.
|
||||
is_running() {
|
||||
local user="$1"
|
||||
local bin="$2"
|
||||
local base
|
||||
base=$(basename "$bin")
|
||||
# [t] trick avoids matching this grep/pgrep itself
|
||||
ps -u "$user" -o args= 2>/dev/null | grep -qE "(^|/)[${base:0:1}]${base:1}( |$)" \
|
||||
|| pgrep -u "$user" -f "(^|/)(${base})( |$)" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Start with nohup so SIGHUP from parent shell exit does not kill helpers.
|
||||
start_one() {
|
||||
local user="$1"
|
||||
local name="$2"
|
||||
local bin="$3"
|
||||
shift 3
|
||||
if is_running "$user" "$bin"; then
|
||||
echo "already: $name ($user)"
|
||||
return 0
|
||||
fi
|
||||
echo "start: $name as $user"
|
||||
# shellcheck disable=SC2086
|
||||
nohup runuser -u "$user" -- "$bin" "$@" >>"$LOG_DIR/${name}.log" 2>&1 </dev/null &
|
||||
disown 2>/dev/null || true
|
||||
local i
|
||||
for i in 1 2 3 4 5 6; do
|
||||
sleep 0.4
|
||||
if is_running "$user" "$bin"; then
|
||||
echo " ok: $name"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo " FAIL: $name did not stay up (see $LOG_DIR/${name}.log)" >&2
|
||||
tail -15 "$LOG_DIR/${name}.log" 2>/dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
ec=0
|
||||
start_one taler-exchange-aggregator taler-exchange-aggregator \
|
||||
/usr/bin/taler-exchange-aggregator -c "$CONF" -L INFO || ec=1
|
||||
start_one taler-exchange-closer taler-exchange-closer \
|
||||
/usr/bin/taler-exchange-closer -c "$CONF" -L INFO || ec=1
|
||||
start_one taler-exchange-wire taler-exchange-wirewatch \
|
||||
/usr/bin/taler-exchange-wirewatch -c "$CONF" -L INFO || ec=1
|
||||
start_one taler-exchange-wire taler-exchange-transfer \
|
||||
/usr/bin/taler-exchange-transfer -c "$CONF" -L INFO || ec=1
|
||||
|
||||
echo "--- live helpers ---"
|
||||
ps -eo pid,user,stat,etime,args 2>/dev/null \
|
||||
| grep -E 'taler-exchange-(aggregator|closer|wirewatch|transfer)' \
|
||||
| grep -vE 'grep| Z |ensure_exchange' || true
|
||||
|
||||
exit "$ec"
|
||||
263
scripts/taler-exchange/install_no_terms.sh
Executable file
263
scripts/taler-exchange/install_no_terms.sh
Executable file
|
|
@ -0,0 +1,263 @@
|
|||
#!/bin/bash
|
||||
# Install "No Terms Required" ToS + privacy for the exchange (styled like merchant terms).
|
||||
# Run as root inside the exchange container.
|
||||
set -euo pipefail
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
|
||||
CONF="${TALER_EXCHANGE_CONFIG:-/etc/taler-exchange/taler-exchange.conf}"
|
||||
DATA_HOME=$(taler-exchange-config -c "$CONF" -f -s PATHS -o TALER_DATA_HOME 2>/dev/null || true)
|
||||
DATA_HOME="${DATA_HOME:-/var/lib/taler-exchange/}"
|
||||
TERMS_DIR="${DATA_HOME%/}/terms"
|
||||
LANG_DIR="$TERMS_DIR/en"
|
||||
mkdir -p "$LANG_DIR"
|
||||
|
||||
# Bump when HTML/style changes (long Cache-Control on /terms).
|
||||
TOS_ETAG="${TERMS_ETAG:-no-terms-v2}"
|
||||
# Prefer Swiss FADP privacy install (install_swiss_privacy.sh); fallback etag here
|
||||
PP_ETAG="${PRIVACY_ETAG:-exchange-pp-swiss-v0}"
|
||||
|
||||
# Shared palette with merchant-tos-dual / bank terms pages
|
||||
COMMON_CSS='
|
||||
:root { color-scheme: dark light; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 40rem; margin: 2rem auto; padding: 0 1.1rem 3rem;
|
||||
line-height: 1.5; color: #e8e6e3; background: #1a1520;
|
||||
}
|
||||
h1 { font-size: 1.35rem; font-weight: 800; margin: 0 0 1rem; color: #f5f0ea; }
|
||||
h2 { font-size: 1.05rem; margin: 1.4rem 0 0.5rem; color: #e8c878; }
|
||||
p, li { font-size: 0.98rem; }
|
||||
ul { padding-left: 1.2rem; }
|
||||
code, a { color: #5eead4; }
|
||||
a { text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.badge {
|
||||
display: inline-block; font-size: 0.72rem; font-weight: 700;
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
color: #c4b5fd; border: 1px solid rgba(196,181,253,0.35);
|
||||
border-radius: 999px; padding: 0.2rem 0.65rem; margin-bottom: 0.85rem;
|
||||
}
|
||||
.cur {
|
||||
border-radius: 12px; padding: 0.75rem 0.9rem; margin: 0.85rem 0 1rem;
|
||||
border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25);
|
||||
}
|
||||
.cur strong { display: block; font-size: 1.05rem; margin-bottom: 0.25rem; color: #5eead4; }
|
||||
.muted { color: #a39e98; font-size: 0.88rem; }
|
||||
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
|
||||
'
|
||||
|
||||
write_tos() {
|
||||
local base="$1"
|
||||
local title="No Terms Required"
|
||||
local body_txt body_md
|
||||
|
||||
body_txt='No Terms Required.
|
||||
|
||||
This is an experimental / exploration GNU Taler exchange for the currency GOA (hacktivism.ch).
|
||||
|
||||
No formal terms of service are required to use this service.
|
||||
|
||||
By withdrawing or using GOA coins you acknowledge that:
|
||||
- This service is for exploration and testing only.
|
||||
- GOA is not legal tender and has no guaranteed real-world value or redemption.
|
||||
- There is no guaranteed availability, support, or uptime.
|
||||
- Operators may reset balances or change configuration without notice.
|
||||
- Do not use real money you cannot afford to lose.
|
||||
|
||||
If you do not agree, do not use this exchange.
|
||||
|
||||
Related:
|
||||
- Bank: https://bank.hacktivism.ch/intro/
|
||||
- Merchant terms: https://taler.hacktivism.ch/terms
|
||||
- Bank terms: https://bank.hacktivism.ch/terms
|
||||
|
||||
Privacy:
|
||||
Processing under Swiss FADP (revDSG). What data is retained is listed at
|
||||
https://exchange.hacktivism.ch/privacy
|
||||
'
|
||||
|
||||
body_md='# No Terms Required
|
||||
|
||||
This is an **experimental / exploration** GNU Taler exchange for the currency **GOA** (hacktivism.ch).
|
||||
|
||||
**No formal terms of service** are required to use this service.
|
||||
|
||||
By withdrawing or using GOA coins you acknowledge that:
|
||||
|
||||
- This service is for exploration and testing only.
|
||||
- GOA is not legal tender and has no guaranteed real-world value or redemption.
|
||||
- There is no guaranteed availability, support, or uptime.
|
||||
- Operators may reset balances or change configuration without notice.
|
||||
- Do not use real money you cannot afford to lose.
|
||||
|
||||
If you do not agree, do not use this exchange.
|
||||
|
||||
## Related
|
||||
|
||||
- [Bank intro](https://bank.hacktivism.ch/intro/)
|
||||
- [Bank terms](https://bank.hacktivism.ch/terms)
|
||||
- [Merchant terms](https://taler.hacktivism.ch/terms)
|
||||
- [Exchange privacy](https://exchange.hacktivism.ch/privacy)
|
||||
|
||||
## Privacy
|
||||
|
||||
Processing under Swiss FADP (revDSG). What data is retained is listed on
|
||||
[exchange.hacktivism.ch/privacy](https://exchange.hacktivism.ch/privacy).
|
||||
'
|
||||
|
||||
printf '%s\n' "$body_txt" >"$LANG_DIR/${base}.txt"
|
||||
printf '%s\n' "$body_md" >"$LANG_DIR/${base}.md"
|
||||
cat >"$LANG_DIR/${base}.html" <<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
${COMMON_CSS}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="badge">exchange.hacktivism.ch · GOA</div>
|
||||
<h1>${title}</h1>
|
||||
<p>This is an <strong>experimental / exploration</strong> GNU Taler exchange for the
|
||||
currency <strong>GOA</strong> at <code>exchange.hacktivism.ch</code> (hacktivism.ch).</p>
|
||||
<p><strong>No formal terms of service</strong> are required to use this service.</p>
|
||||
|
||||
<div class="cur">
|
||||
<strong>GOA · explorational</strong>
|
||||
Not legal tender. No guaranteed real-world value, redemption, or convertibility.
|
||||
Issued only for exploration and testing on this stack.
|
||||
</div>
|
||||
|
||||
<h2>By withdrawing or using GOA coins you acknowledge</h2>
|
||||
<ul>
|
||||
<li>This service is for exploration and testing only.</li>
|
||||
<li>There is no guaranteed availability, support, or uptime.</li>
|
||||
<li>Operators may reset balances or change configuration without notice.</li>
|
||||
<li>Do not use real money you cannot afford to lose.</li>
|
||||
<li>Software is provided as-is, without warranty.</li>
|
||||
</ul>
|
||||
<p>If you do not agree, do not use this exchange.</p>
|
||||
|
||||
<h2>Related</h2>
|
||||
<ul>
|
||||
<li><a href="https://bank.hacktivism.ch/intro/">Bank intro</a></li>
|
||||
<li><a href="https://bank.hacktivism.ch/terms">Bank terms</a></li>
|
||||
<li><a href="https://taler.hacktivism.ch/terms">Merchant terms</a></li>
|
||||
<li><a href="https://exchange.hacktivism.ch/intro/">Exchange intro</a></li>
|
||||
<li><a href="https://exchange.hacktivism.ch/privacy">Exchange privacy</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Privacy</h2>
|
||||
<p class="muted">Processing under Swiss FADP (revDSG). What data is retained
|
||||
(reserves, wire-in, coins, logs, …) is listed on
|
||||
<a href="https://exchange.hacktivism.ch/privacy">/privacy</a>.</p>
|
||||
<footer class="muted">Version ${base} · hacktivism.ch</footer>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
}
|
||||
|
||||
write_pp() {
|
||||
# Legacy short PP only if not using install_swiss_privacy.sh etag
|
||||
local base="$1"
|
||||
local title="Privacy notice · GOA Exchange"
|
||||
local body_txt body_md
|
||||
|
||||
body_txt='No Privacy Policy Required.
|
||||
|
||||
This is an experimental / exploration GNU Taler exchange for GOA.
|
||||
|
||||
No formal privacy policy is required for this demo service.
|
||||
|
||||
High-level notes:
|
||||
- Wire transfers via the regional bank may identify bank account holders.
|
||||
- The exchange processes withdrawals, deposits, and related protocol operations.
|
||||
- Logs may be kept for operation and debugging.
|
||||
|
||||
Do not use this service if that is unacceptable.
|
||||
|
||||
Related:
|
||||
- Exchange terms: https://exchange.hacktivism.ch/terms
|
||||
'
|
||||
|
||||
body_md='# No Privacy Policy Required
|
||||
|
||||
This is an **experimental / exploration** GNU Taler exchange for **GOA**.
|
||||
|
||||
**No formal privacy policy** is required for this demo service.
|
||||
|
||||
## High-level notes
|
||||
|
||||
- Wire transfers via the regional bank may identify bank account holders.
|
||||
- The exchange processes withdrawals, deposits, and related protocol operations.
|
||||
- Logs may be kept for operation and debugging.
|
||||
|
||||
Do not use this service if that is unacceptable.
|
||||
|
||||
## Related
|
||||
|
||||
- [Exchange terms](https://exchange.hacktivism.ch/terms)
|
||||
'
|
||||
|
||||
printf '%s\n' "$body_txt" >"$LANG_DIR/${base}.txt"
|
||||
printf '%s\n' "$body_md" >"$LANG_DIR/${base}.md"
|
||||
cat >"$LANG_DIR/${base}.html" <<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
${COMMON_CSS}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="badge">exchange.hacktivism.ch · privacy</div>
|
||||
<h1>${title}</h1>
|
||||
<p>This is an <strong>experimental / exploration</strong> GNU Taler exchange for
|
||||
<strong>GOA</strong> at <code>exchange.hacktivism.ch</code>.</p>
|
||||
<p><strong>No formal privacy policy</strong> is required for this demo service.</p>
|
||||
|
||||
<h2>High-level notes</h2>
|
||||
<ul>
|
||||
<li>Wire transfers via the regional bank may identify bank account holders.</li>
|
||||
<li>The exchange processes withdrawals, deposits, and related protocol operations.</li>
|
||||
<li>Logs may be kept for operation and debugging.</li>
|
||||
</ul>
|
||||
<p>Do not use this service if that is unacceptable.</p>
|
||||
|
||||
<h2>Related</h2>
|
||||
<ul>
|
||||
<li><a href="https://exchange.hacktivism.ch/terms">Exchange terms</a></li>
|
||||
<li><a href="https://bank.hacktivism.ch/terms">Bank terms</a></li>
|
||||
<li><a href="https://taler.hacktivism.ch/terms">Merchant terms</a></li>
|
||||
</ul>
|
||||
<footer class="muted">Version ${base}</footer>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
}
|
||||
|
||||
write_tos "$TOS_ETAG"
|
||||
# Prefer Swiss FADP privacy installer when present (precise retention tables)
|
||||
if [ -x /usr/local/bin/install_swiss_privacy.sh ]; then
|
||||
PRIVACY_ETAG="$PP_ETAG" /usr/local/bin/install_swiss_privacy.sh
|
||||
else
|
||||
write_pp "$PP_ETAG"
|
||||
fi
|
||||
|
||||
chmod -R a+rX "$TERMS_DIR"
|
||||
if id taler-exchange-httpd >/dev/null 2>&1; then
|
||||
chown -R taler-exchange-httpd: "$TERMS_DIR" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Installed under $LANG_DIR:"
|
||||
ls -la "$LANG_DIR"/${TOS_ETAG}.* "$LANG_DIR"/${PP_ETAG}.* 2>/dev/null || ls -la "$LANG_DIR"
|
||||
echo "Config should set:"
|
||||
echo " TERMS_ETAG = ${TOS_ETAG}"
|
||||
echo " PRIVACY_ETAG = ${PP_ETAG}"
|
||||
echo " TERMS_DIR / PRIVACY_DIR = \${TALER_DATA_HOME}terms/"
|
||||
168
scripts/taler-exchange/install_swiss_privacy.sh
Normal file
168
scripts/taler-exchange/install_swiss_privacy.sh
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#!/bin/bash
|
||||
# Swiss FADP privacy for exchange.hacktivism.ch — run inside exchange container.
|
||||
set -euo pipefail
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
|
||||
ETAG="${PRIVACY_ETAG:-exchange-pp-swiss-v0}"
|
||||
CONF="${TALER_EXCHANGE_CONFIG:-/etc/taler-exchange/taler-exchange.conf}"
|
||||
DATA_HOME=$(taler-exchange-config -c "$CONF" -f -s PATHS -o TALER_DATA_HOME 2>/dev/null || true)
|
||||
DATA_HOME="${DATA_HOME:-/var/lib/taler-exchange/}"
|
||||
DIR="${PRIVACY_DIR:-${DATA_HOME%/}/terms}/en"
|
||||
mkdir -p "$DIR"
|
||||
|
||||
cat >"$DIR/${ETAG}.txt" <<'EOF'
|
||||
Privacy notice — exchange.hacktivism.ch (Swiss FADP / revDSG)
|
||||
|
||||
Controller: operators of the hacktivism.ch GNU Taler stack.
|
||||
Service: experimental GOA exchange (not a licensed Swiss bank or e-money institution).
|
||||
|
||||
Data retained (precise):
|
||||
1) Reserves — reserve public keys, current remaining amounts, creation/close times: for service life or wipe.
|
||||
2) Wire-in (reserves_in) — credit amounts, timestamps, bank-side account reference as provided by the bank/wire gateway: for service life; links reserve funding to bank accounts.
|
||||
3) Coins — known coin public keys, denomination serials, remaining value, spend/deposit linkage as required by protocol: for service life.
|
||||
4) Withdraw / refresh / recoup / refund / deposit tables — protocol operation records and timestamps: for service life.
|
||||
5) Wire-out — transfers to merchant accounts (amounts, payto/target as stored, times): for service life.
|
||||
6) Keys / denominations — public denomination and signing keys (not personal data by themselves).
|
||||
7) Technical logs — HTTP and process logs (IP, path, status): days–weeks via rotation.
|
||||
8) Terms acceptance markers if stored by clients — as required by wallet software.
|
||||
|
||||
Not retained by the exchange as plaintext identity of wallet holders: GNU Taler is designed so coin spend is unlinkable to customer bank identity at the exchange when used as intended. Funding reserves via the bank may still identify the bank account holder on the bank side.
|
||||
|
||||
Purposes: operate GOA exchange protocol, settle with bank/merchants, security, operations.
|
||||
Legal basis (FADP): provision of requested exchange service; proportionate operation of experimental stack.
|
||||
Recipients: bank.hacktivism.ch (wire); merchants depositing GOA; host operators. No sale of data.
|
||||
Rights: access, rectification, deletion, objection (FADP); complaint to Swiss FDPIC (EDÖB).
|
||||
Security: TLS; experimental stack.
|
||||
|
||||
Related: https://exchange.hacktivism.ch/terms · https://bank.hacktivism.ch/intro/privacy.html · https://taler.hacktivism.ch/privacy
|
||||
EOF
|
||||
|
||||
cat >"$DIR/${ETAG}.md" <<'EOF'
|
||||
# Privacy notice · GOA Exchange · Swiss FADP
|
||||
|
||||
Controller: operators of **hacktivism.ch**. Service: experimental **GOA** exchange at `exchange.hacktivism.ch`.
|
||||
|
||||
Swiss Federal Act on Data Protection (**FADP / revDSG**, since 1 Sep 2023).
|
||||
|
||||
## Data retained
|
||||
|
||||
| Data | Examples | Retention |
|
||||
|------|----------|-----------|
|
||||
| Reserves | Reserve pubs, remaining amount, times | Service life or wipe |
|
||||
| Wire-in | Amounts, timestamps, bank account ref from wire path | Service life |
|
||||
| Coins | Coin pubs, denoms, remaining, deposit links | Service life |
|
||||
| Protocol ops | Withdraw, refresh, deposit, refund, recoup | Service life |
|
||||
| Wire-out | Merchant settlement amounts / targets | Service life |
|
||||
| Technical logs | IP, path, status | Days–weeks (rotation) |
|
||||
|
||||
**Design note:** Coin spend is intended to be unlinkable to the customer at the exchange. Funding a reserve via the bank may identify the **bank** account holder on the bank system.
|
||||
|
||||
## Purposes
|
||||
|
||||
Run the GOA exchange protocol; wire settlement; security and operations.
|
||||
|
||||
## Rights
|
||||
|
||||
FADP access, correction, deletion, objection. Complaint: Swiss **FDPIC / EDÖB**.
|
||||
|
||||
## Related
|
||||
|
||||
- [Exchange terms](https://exchange.hacktivism.ch/terms)
|
||||
- [Bank privacy](https://bank.hacktivism.ch/intro/privacy.html)
|
||||
- [Merchant privacy](https://taler.hacktivism.ch/privacy)
|
||||
EOF
|
||||
|
||||
cat >"$DIR/${ETAG}.html" <<'HTML'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Privacy notice · GOA Exchange · Swiss FADP</title>
|
||||
<style>
|
||||
:root { color-scheme: dark light; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 42rem; margin: 2rem auto; padding: 0 1.1rem 3rem;
|
||||
line-height: 1.5; color: #e8e6e3; background: #1a1520;
|
||||
}
|
||||
h1 { font-size: 1.35rem; font-weight: 800; margin: 0 0 1rem; color: #f5f0ea; }
|
||||
h2 { font-size: 1.05rem; margin: 1.5rem 0 0.5rem; color: #e8c878; }
|
||||
p, li, td, th { font-size: 0.95rem; }
|
||||
ul { padding-left: 1.2rem; }
|
||||
code, a { color: #5eead4; }
|
||||
a { text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.badge {
|
||||
display: inline-block; font-size: 0.72rem; font-weight: 700;
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
color: #c4b5fd; border: 1px solid rgba(196,181,253,0.35);
|
||||
border-radius: 999px; padding: 0.2rem 0.65rem; margin-bottom: 0.85rem;
|
||||
}
|
||||
.note {
|
||||
border-radius: 12px; padding: 0.75rem 0.9rem; margin: 0.85rem 0 1rem;
|
||||
border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25);
|
||||
font-size: 0.9rem; color: #c8c4bf;
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; margin: 0.6rem 0 1rem; font-size: 0.88rem; }
|
||||
th, td { border: 1px solid rgba(255,255,255,0.12); padding: 0.45rem 0.55rem; text-align: left; vertical-align: top; }
|
||||
th { background: rgba(0,0,0,0.35); color: #e8c878; font-weight: 700; }
|
||||
.muted { color: #a39e98; font-size: 0.88rem; }
|
||||
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="badge">exchange.hacktivism.ch · privacy · CH</div>
|
||||
<h1>Privacy notice · GOA Exchange</h1>
|
||||
<p class="note">
|
||||
Swiss Federal Act on Data Protection (<strong>FADP / revDSG</strong>, since 1 Sep 2023).
|
||||
Experimental GOA exchange at <code>exchange.hacktivism.ch</code> — not a licensed bank.
|
||||
</p>
|
||||
|
||||
<h2>1. Controller</h2>
|
||||
<p>Operators of the hacktivism.ch GNU Taler stack. No separate DPO for this experimental service.</p>
|
||||
|
||||
<h2>2. Data retained</h2>
|
||||
<table>
|
||||
<thead><tr><th>Data</th><th>Examples</th><th>Typical retention</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Reserves</td><td>Reserve public keys, remaining amount, times</td><td>Service life or wipe</td></tr>
|
||||
<tr><td>Wire-in</td><td>Amounts, timestamps, bank account reference from wire path</td><td>Service life</td></tr>
|
||||
<tr><td>Coins</td><td>Coin pubs, denominations, remaining value, deposit links</td><td>Service life</td></tr>
|
||||
<tr><td>Protocol operations</td><td>Withdraw, refresh, deposit, refund, recoup</td><td>Service life</td></tr>
|
||||
<tr><td>Wire-out</td><td>Merchant settlement amounts / targets</td><td>Service life</td></tr>
|
||||
<tr><td>Technical logs</td><td>IP, path, status</td><td>Days–weeks (rotation)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Design note:</strong> Coin spend is intended to be unlinkable to the customer
|
||||
<em>at the exchange</em>. Funding a reserve via the bank may identify the bank account
|
||||
holder on the <strong>bank</strong> system.</p>
|
||||
|
||||
<h2>3. Purposes</h2>
|
||||
<ul>
|
||||
<li>Operate the GOA GNU Taler exchange protocol</li>
|
||||
<li>Wire settlement with bank and merchants</li>
|
||||
<li>Security, debugging, capacity monitoring</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Your rights</h2>
|
||||
<p>Access, rectification, deletion, objection under the FADP. Complaint:
|
||||
Swiss <strong>FDPIC / EDÖB</strong>.</p>
|
||||
|
||||
<h2>Related</h2>
|
||||
<ul>
|
||||
<li><a href="https://exchange.hacktivism.ch/terms">Exchange terms</a></li>
|
||||
<li><a href="https://bank.hacktivism.ch/intro/privacy.html">Bank privacy</a></li>
|
||||
<li><a href="https://taler.hacktivism.ch/privacy">Merchant privacy</a></li>
|
||||
</ul>
|
||||
<footer class="muted">exchange-pp-swiss-v0 · Swiss FADP (revDSG)</footer>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
chmod -R a+rX "${DATA_HOME%/}/terms"
|
||||
if id taler-exchange-httpd >/dev/null 2>&1; then
|
||||
chown -R taler-exchange-httpd: "${DATA_HOME%/}/terms" 2>/dev/null || true
|
||||
fi
|
||||
echo "ok exchange privacy $ETAG -> $DIR"
|
||||
ls -la "$DIR"/${ETAG}.*
|
||||
300
scripts/taler-exchange/landing-stats-exchange.sh
Normal file
300
scripts/taler-exchange/landing-stats-exchange.sh
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
#!/bin/bash
|
||||
# Run INSIDE taler-hacktivism-exchange-ansible.
|
||||
# Writes /var/www/exchange-landing/stats.json
|
||||
#
|
||||
# Data lives in Postgres DB taler-exchange, schema exchange.*
|
||||
# (reserves, reserves_in, known_coins, withdraw, denominations, …)
|
||||
#
|
||||
# IMPORTANT: never use psql -F$'\t' -v ON_ERROR_STOP=1
|
||||
# If the tab arg is lost, -F eats -v and ON_ERROR_STOP=1 becomes the
|
||||
# *username* → peer auth fails → silent empty counts (all zeros).
|
||||
# Never overwrite stats.json on failure — write stats-run.json instead.
|
||||
set -euo pipefail
|
||||
export TZ="${TZ:-Europe/Zurich}"
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
LANDING_DIR="${LANDING_DIR:-/var/www/exchange-landing}"
|
||||
OUT="$LANDING_DIR/stats.json"
|
||||
RUN="$LANDING_DIR/stats-run.json"
|
||||
TMP="${OUT}.tmp.$$"
|
||||
DB="${EXCHANGE_DB:-taler-exchange}"
|
||||
BASE_URL="${EXCHANGE_BASE_URL:-https://exchange.hacktivism.ch}"
|
||||
ERRLOG="${LANDING_STATS_ERRLOG:-/var/log/landing-stats-exchange.err}"
|
||||
mkdir -p "$LANDING_DIR"
|
||||
|
||||
now_iso() { date +%Y-%m-%dT%H:%M%z | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/'; }
|
||||
now_human() { date +"%Y-%m-%d %H:%M %Z"; }
|
||||
json_str() {
|
||||
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n\r\t' ' ')"
|
||||
}
|
||||
|
||||
write_run() {
|
||||
local ok_json="$1" msg="${2:-}"
|
||||
cat >"$RUN" <<EOF
|
||||
{
|
||||
"ok": ${ok_json},
|
||||
"at": $(json_str "$(now_iso)"),
|
||||
"at_human": $(json_str "$(now_human)"),
|
||||
"error": $( [ -n "$msg" ] && json_str "$msg" || echo null )
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
abort() {
|
||||
write_run false "$*"
|
||||
printf '%s\n' "abort exchange-stats: $*" | tee -a "$ERRLOG" >&2
|
||||
rm -f "$TMP" 2>/dev/null || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Prefer runuser; fall back to su. Always absolute-ish via PATH.
|
||||
as_postgres() {
|
||||
if command -v runuser >/dev/null 2>&1; then
|
||||
runuser -u postgres -- "$@"
|
||||
elif command -v su >/dev/null 2>&1; then
|
||||
su -s /bin/bash postgres -c "$*"
|
||||
else
|
||||
return 127
|
||||
fi
|
||||
}
|
||||
|
||||
# Single value or one row with '|' separators (no -F, no -v flags).
|
||||
psqlq() {
|
||||
local sql="$1" out ec
|
||||
set +e
|
||||
out=$(as_postgres psql -d "$DB" -At -c "$sql" 2>>"$ERRLOG")
|
||||
ec=$?
|
||||
set -e
|
||||
if [ "$ec" -ne 0 ]; then
|
||||
abort "psql failed (ec=$ec): ${sql:0:120}"
|
||||
fi
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
fmt_goa() {
|
||||
local v="${1:-0}" f="${2:-0}"
|
||||
v=${v//[^0-9-]/}; f=${f//[^0-9-]/}; v=${v:-0}; f=${f:-0}
|
||||
if [ "$f" = "0" ] || [ -z "$f" ]; then
|
||||
printf 'GOA:%s' "$v"
|
||||
return
|
||||
fi
|
||||
awk -v v="$v" -v f="$f" 'BEGIN{
|
||||
frac = sprintf("%08d", f+0); sub(/0+$/, "", frac)
|
||||
if (frac == "") printf "GOA:%d", v+0
|
||||
else printf "GOA:%d.%s", v+0, frac
|
||||
}'
|
||||
}
|
||||
|
||||
q_count() {
|
||||
local n
|
||||
n=$(psqlq "SELECT count(*)::text FROM $1;")
|
||||
# strip whitespace/newlines
|
||||
n=$(printf '%s' "$n" | tr -d '[:space:]')
|
||||
[[ "$n" =~ ^[0-9]+$ ]] || abort "bad count for $1: '$n'"
|
||||
printf '%s' "$n"
|
||||
}
|
||||
|
||||
# --- preflight ---
|
||||
command -v psql >/dev/null 2>&1 || abort "psql not in PATH ($PATH)"
|
||||
command -v runuser >/dev/null 2>&1 || command -v su >/dev/null 2>&1 || abort "neither runuser nor su in PATH"
|
||||
PROBE=$(psqlq "SELECT 1;")
|
||||
[ "$PROBE" = "1" ] || abort "postgres not reachable (SELECT 1 → '$PROBE')"
|
||||
|
||||
# --- coin lifecycle ---
|
||||
KNOWN_COINS=$(q_count "exchange.known_coins")
|
||||
COINS_LIVE=$(psqlq "SELECT count(*)::text FROM exchange.known_coins WHERE (remaining).val > 0 OR (remaining).frac > 0;")
|
||||
COINS_LIVE=$(printf '%s' "${COINS_LIVE:-0}" | tr -d '[:space:]')
|
||||
COINS_LIVE=${COINS_LIVE:-0}
|
||||
COINS_SPENT=$(( KNOWN_COINS - COINS_LIVE ))
|
||||
|
||||
REM_ROW=$(psqlq "SELECT coalesce(sum((remaining).val),0)::text || '|' || coalesce(sum((remaining).frac),0)::text FROM exchange.known_coins;")
|
||||
IFS='|' read -r REM_VAL REM_FRAC <<<"${REM_ROW:-0|0}"
|
||||
REMAINING_AMT=$(fmt_goa "${REM_VAL:-0}" "${REM_FRAC:-0}")
|
||||
|
||||
DENOMS_TOTAL=$(q_count "exchange.denominations")
|
||||
DENOM_VALUES=$(psqlq "SELECT count(DISTINCT ((coin).val, (coin).frac))::text FROM exchange.denominations;")
|
||||
DENOM_VALUES=$(printf '%s' "${DENOM_VALUES:-0}" | tr -d '[:space:]')
|
||||
|
||||
NOW_US=$(date +%s)000000
|
||||
DENOMS_WITHDRAWABLE=$(psqlq "SELECT count(*)::text FROM exchange.denominations WHERE valid_from <= ${NOW_US} AND expire_withdraw > ${NOW_US};")
|
||||
DENOMS_WITHDRAWABLE=$(printf '%s' "${DENOMS_WITHDRAWABLE:-0}" | tr -d '[:space:]')
|
||||
|
||||
RESERVES=$(q_count "exchange.reserves")
|
||||
RESERVES_IN=$(q_count "exchange.reserves_in")
|
||||
WIN_ROW=$(psqlq "SELECT coalesce(sum((credit).val),0)::text || '|' || coalesce(sum((credit).frac),0)::text FROM exchange.reserves_in;")
|
||||
IFS='|' read -r WIN_VAL WIN_FRAC <<<"${WIN_ROW:-0|0}"
|
||||
WIRE_IN_AMT=$(fmt_goa "${WIN_VAL:-0}" "${WIN_FRAC:-0}")
|
||||
|
||||
WITHDRAW_OPS=$(q_count "exchange.withdraw")
|
||||
WOUT_ROW=$(psqlq "SELECT coalesce(sum((amount_with_fee).val),0)::text || '|' || coalesce(sum((amount_with_fee).frac),0)::text FROM exchange.withdraw;")
|
||||
IFS='|' read -r WO_VAL WO_FRAC <<<"${WOUT_ROW:-0|0}"
|
||||
WITHDRAW_AMT=$(fmt_goa "${WO_VAL:-0}" "${WO_FRAC:-0}")
|
||||
|
||||
REFRESH_OPS=$(q_count "exchange.refresh")
|
||||
RECOUP=$(q_count "exchange.recoup")
|
||||
REFUNDS=$(q_count "exchange.refunds")
|
||||
COIN_DEPOSITS=$(q_count "exchange.coin_deposits")
|
||||
BATCH_DEPOSITS=$(q_count "exchange.batch_deposits")
|
||||
WIRE_OUT=$(q_count "exchange.wire_out")
|
||||
COIN_HISTORY=$(q_count "exchange.coin_history")
|
||||
WIRE_ACCTS=$(q_count "exchange.wire_accounts")
|
||||
|
||||
# known coins by denom value (pipe-separated)
|
||||
BY_DENOM_TSV=$(psqlq "
|
||||
SELECT (d.coin).val::text || '|' || (d.coin).frac::text || '|' || count(*)::text || '|' ||
|
||||
count(*) FILTER (WHERE (k.remaining).val > 0 OR (k.remaining).frac > 0)::text
|
||||
FROM exchange.known_coins k
|
||||
JOIN exchange.denominations d ON d.denominations_serial = k.denominations_serial
|
||||
GROUP BY (d.coin).val, (d.coin).frac
|
||||
ORDER BY (d.coin).val, (d.coin).frac;
|
||||
")
|
||||
|
||||
BY_DENOM_JSON="["
|
||||
bf=1
|
||||
while IFS='|' read -r dv df cnt live; do
|
||||
[ -z "${dv:-}" ] && continue
|
||||
amt=$(fmt_goa "$dv" "$df")
|
||||
if [ "$bf" = 1 ]; then bf=0; else BY_DENOM_JSON="${BY_DENOM_JSON},"; fi
|
||||
BY_DENOM_JSON="${BY_DENOM_JSON}
|
||||
{\"value\": $(json_str "$amt"), \"coins\": ${cnt:-0}, \"live\": ${live:-0}}"
|
||||
done <<<"$BY_DENOM_TSV"
|
||||
BY_DENOM_JSON="${BY_DENOM_JSON}
|
||||
]"
|
||||
|
||||
LADDER_TSV=$(psqlq "
|
||||
SELECT (coin).val::text || '|' || (coin).frac::text || '|' || count(*)::text
|
||||
FROM exchange.denominations
|
||||
GROUP BY (coin).val, (coin).frac
|
||||
ORDER BY (coin).val, (coin).frac;
|
||||
")
|
||||
LADDER_JSON="["
|
||||
lf=1
|
||||
while IFS='|' read -r dv df nkeys; do
|
||||
[ -z "${dv:-}" ] && continue
|
||||
amt=$(fmt_goa "$dv" "$df")
|
||||
if [ "$lf" = 1 ]; then lf=0; else LADDER_JSON="${LADDER_JSON},"; fi
|
||||
LADDER_JSON="${LADDER_JSON}
|
||||
{\"value\": $(json_str "$amt"), \"keys\": ${nkeys:-0}}"
|
||||
done <<<"$LADDER_TSV"
|
||||
LADDER_JSON="${LADDER_JSON}
|
||||
]"
|
||||
|
||||
# recent wire-in / withdraw activity (no personal names — reserve_pub hex truncated)
|
||||
RECENT_JSON="["
|
||||
rf=1
|
||||
while IFS='|' read -r ts val frac; do
|
||||
[ -z "${ts:-}" ] && continue
|
||||
sec=$(awk -v t="$ts" 'BEGIN{printf "%d", int(t/1000000)}')
|
||||
human=$(date -d "@${sec}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null || echo "$sec")
|
||||
amt=$(fmt_goa "${val:-0}" "${frac:-0}")
|
||||
if [ "$rf" = 1 ]; then rf=0; else RECENT_JSON="${RECENT_JSON},"; fi
|
||||
RECENT_JSON="${RECENT_JSON}
|
||||
{\"kind\": \"wire_in\", \"amount\": $(json_str "$amt"), \"ts_human\": $(json_str "$human"), \"ts_us\": ${ts:-0}}"
|
||||
done < <(psqlq "
|
||||
SELECT execution_date::text || '|' || (credit).val::text || '|' || (credit).frac::text
|
||||
FROM exchange.reserves_in
|
||||
ORDER BY execution_date DESC
|
||||
LIMIT 8;
|
||||
")
|
||||
while IFS='|' read -r ts val frac; do
|
||||
[ -z "${ts:-}" ] && continue
|
||||
sec=$(awk -v t="$ts" 'BEGIN{printf "%d", int(t/1000000)}')
|
||||
human=$(date -d "@${sec}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null || echo "$sec")
|
||||
amt=$(fmt_goa "${val:-0}" "${frac:-0}")
|
||||
if [ "$rf" = 1 ]; then rf=0; else RECENT_JSON="${RECENT_JSON},"; fi
|
||||
RECENT_JSON="${RECENT_JSON}
|
||||
{\"kind\": \"withdraw\", \"amount\": $(json_str "$amt"), \"ts_human\": $(json_str "$human"), \"ts_us\": ${ts:-0}}"
|
||||
done < <(psqlq "
|
||||
SELECT execution_date::text || '|' || (amount_with_fee).val::text || '|' || (amount_with_fee).frac::text
|
||||
FROM exchange.withdraw
|
||||
ORDER BY execution_date DESC
|
||||
LIMIT 6;
|
||||
")
|
||||
RECENT_JSON="${RECENT_JSON}
|
||||
]"
|
||||
|
||||
# live performance
|
||||
measure_ms() {
|
||||
local url="$1" t
|
||||
t=$(curl -sS -o /dev/null -m 8 -w '%{time_total}' "$url" 2>/dev/null || echo "")
|
||||
[ -z "$t" ] && { echo "null"; return; }
|
||||
awk -v t="$t" 'BEGIN{printf "%d", (t+0)*1000}'
|
||||
}
|
||||
KEYS_MS=$(measure_ms "${BASE_URL}/keys")
|
||||
CONFIG_MS=$(measure_ms "${BASE_URL}/config")
|
||||
KEYS_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BASE_URL}/keys" 2>/dev/null || echo "000")
|
||||
CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BASE_URL}/config" 2>/dev/null || echo "000")
|
||||
LOADAVG=""
|
||||
[ -r /proc/loadavg ] && LOADAVG=$(awk '{print $1","$2","$3}' /proc/loadavg)
|
||||
|
||||
WW="false"
|
||||
pgrep -f taler-exchange-wirewatch >/dev/null 2>&1 && WW="true"
|
||||
|
||||
STAT_EVENTS=$(q_count "exchange.exchange_statistic_counter_event")
|
||||
|
||||
# Fail closed: zero denoms usually means broken query path (table always has keys)
|
||||
if [ "${DENOMS_TOTAL:-0}" = "0" ]; then
|
||||
abort "denominations count is 0 — refusing to publish (check peer auth / PATH)"
|
||||
fi
|
||||
|
||||
GEN=$(now_iso)
|
||||
HUMAN=$(now_human)
|
||||
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
|
||||
|
||||
MEM_JSON='"container_rss_human": "—"'
|
||||
MEM_HELPER="${MEM_HELPER:-/usr/local/lib/landing-mem-snapshot.sh}"
|
||||
if [ -f "$MEM_HELPER" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$MEM_HELPER"
|
||||
mem_snapshot_json || true
|
||||
fi
|
||||
|
||||
cat >"$TMP" <<EOF
|
||||
{
|
||||
"ok": true,
|
||||
"source": "exchange-db",
|
||||
"schema": "exchange.* @ taler-exchange",
|
||||
"focus": "coins",
|
||||
"timezone": $(json_str "$TZ"),
|
||||
"generated_at": $(json_str "$GEN"),
|
||||
"generated_at_human": $(json_str "$HUMAN"),
|
||||
"reserves": ${RESERVES:-0},
|
||||
"wire_in_count": ${RESERVES_IN:-0},
|
||||
"wire_in_amount": $(json_str "$WIRE_IN_AMT"),
|
||||
"wire_out": ${WIRE_OUT:-0},
|
||||
"wire_accounts": ${WIRE_ACCTS:-0},
|
||||
"wirewatch_running": $WW,
|
||||
"known_coins": ${KNOWN_COINS:-0},
|
||||
"coins_live": ${COINS_LIVE:-0},
|
||||
"coins_spent": ${COINS_SPENT:-0},
|
||||
"coins_remaining_amount": $(json_str "$REMAINING_AMT"),
|
||||
"withdraw_ops": ${WITHDRAW_OPS:-0},
|
||||
"withdraw_amount": $(json_str "$WITHDRAW_AMT"),
|
||||
"refresh_ops": ${REFRESH_OPS:-0},
|
||||
"recoup": ${RECOUP:-0},
|
||||
"refunds": ${REFUNDS:-0},
|
||||
"coin_deposits": ${COIN_DEPOSITS:-0},
|
||||
"batch_deposits": ${BATCH_DEPOSITS:-0},
|
||||
"coin_history_events": ${COIN_HISTORY:-0},
|
||||
"denominations": ${DENOMS_TOTAL:-0},
|
||||
"denom_values": ${DENOM_VALUES:-0},
|
||||
"denoms_withdrawable": ${DENOMS_WITHDRAWABLE:-0},
|
||||
"by_denom": $BY_DENOM_JSON,
|
||||
"denom_ladder": $LADDER_JSON,
|
||||
"recent_activity": $RECENT_JSON,
|
||||
"performance": {
|
||||
"keys_http": $(json_str "$KEYS_HTTP"),
|
||||
"keys_ms": $(num_or_null "$KEYS_MS"),
|
||||
"config_http": $(json_str "$CONFIG_HTTP"),
|
||||
"config_ms": $(num_or_null "$CONFIG_MS"),
|
||||
"loadavg": $(json_str "${LOADAVG:-}"),
|
||||
"statistic_counter_events": ${STAT_EVENTS:-0},
|
||||
"memory": {
|
||||
${MEM_JSON}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
grep -q '"ok": true' "$TMP" || abort "tmp json missing ok:true"
|
||||
mv -f "$TMP" "$OUT"
|
||||
write_run true
|
||||
echo "ok exchange reserves=$RESERVES wire_in=$WIRE_IN_AMT coins=$KNOWN_COINS withdraw=$WITHDRAW_AMT denoms=$DENOMS_TOTAL keys_ms=$KEYS_MS -> $OUT"
|
||||
62
scripts/taler-exchange/offline-sign-upload-keys.sh
Executable file
62
scripts/taler-exchange/offline-sign-upload-keys.sh
Executable file
|
|
@ -0,0 +1,62 @@
|
|||
#!/bin/bash
|
||||
# Offline: download future keys from local exchange, sign, upload. Restore public BASE_URL.
|
||||
# Run as root inside taler-hacktivism-exchange-ansible.
|
||||
set -euo pipefail
|
||||
CONF=/etc/taler-exchange/taler-exchange.conf
|
||||
OV=/etc/taler-exchange/exchange-overrides.conf
|
||||
PUBLIC='https://exchange.hacktivism.ch/'
|
||||
LOCAL='http://127.0.0.1:9011/'
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || { echo "root only"; exit 1; }
|
||||
|
||||
sed -i "s|^BASE_URL = .*|BASE_URL = ${LOCAL}|" "$OV"
|
||||
echo "BASE_URL=$(grep '^BASE_URL' "$OV")"
|
||||
|
||||
echo "=== download ==="
|
||||
if ! runuser -u taler-exchange-offline -- \
|
||||
taler-exchange-offline -c "$CONF" -L INFO download > /tmp/future-keys.json 2>/tmp/dl.err; then
|
||||
echo "download failed:"; cat /tmp/dl.err
|
||||
# still show size
|
||||
fi
|
||||
echo "dl_err:"; cat /tmp/dl.err | tail -30
|
||||
echo "dl_size=$(wc -c </tmp/future-keys.json 2>/dev/null || echo 0)"
|
||||
head -c 400 /tmp/future-keys.json 2>/dev/null; echo
|
||||
|
||||
if [ ! -s /tmp/future-keys.json ]; then
|
||||
echo "empty download — check secmod connectivity / management API"
|
||||
sed -i "s|^BASE_URL = .*|BASE_URL = ${PUBLIC}|" "$OV"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== sign ==="
|
||||
runuser -u taler-exchange-offline -- \
|
||||
taler-exchange-offline -c "$CONF" -L INFO sign < /tmp/future-keys.json > /tmp/signed-keys.json 2>/tmp/sg.err
|
||||
echo "sg_err:"; cat /tmp/sg.err | tail -20
|
||||
echo "sg_size=$(wc -c </tmp/signed-keys.json)"
|
||||
|
||||
echo "=== upload ==="
|
||||
runuser -u taler-exchange-offline -- \
|
||||
taler-exchange-offline -c "$CONF" -L INFO upload < /tmp/signed-keys.json 2>/tmp/up.err
|
||||
echo "up_err:"; cat /tmp/up.err | tail -20
|
||||
|
||||
sed -i "s|^BASE_URL = .*|BASE_URL = ${PUBLIC}|" "$OV"
|
||||
echo "BASE_URL restored=$(grep '^BASE_URL' "$OV")"
|
||||
|
||||
echo "=== /keys probe ==="
|
||||
sleep 1
|
||||
# wake suspended handlers with a tiny delay
|
||||
for i in 1 2 3 4 5 6; do
|
||||
code=$(curl -sS -m 12 -o /tmp/keys.json -w '%{http_code}' http://127.0.0.1:9011/keys || true)
|
||||
sz=$(wc -c </tmp/keys.json 2>/dev/null || echo 0)
|
||||
echo "try $i code=$code size=$sz"
|
||||
if [ "$code" = "200" ] && [ "$sz" -gt 200 ]; then
|
||||
echo KEYS_OK
|
||||
head -c 300 /tmp/keys.json; echo
|
||||
grep -oE '"master_public_key"[[:space:]]*:[[:space:]]*"[^"]+"' /tmp/keys.json | head -1 || true
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo KEYS_FAIL
|
||||
tail -25 /var/log/taler-exchange/taler-exchange-httpd-*.log 2>/dev/null | tail -25
|
||||
exit 1
|
||||
148
scripts/taler-exchange/start_base_services_for_taler_exchange.sh
Executable file
148
scripts/taler-exchange/start_base_services_for_taler_exchange.sh
Executable file
|
|
@ -0,0 +1,148 @@
|
|||
#!/bin/bash
|
||||
# Root: base services for manual exchange (like merchant start_base_services_for_taler.sh).
|
||||
# Then interactive shell as taler-exchange-httpd → run start_exchange.sh there.
|
||||
#
|
||||
# Secmods run as dedicated users (not httpd) — started here as root.
|
||||
# httpd is started by /usr/local/bin/start_exchange.sh as taler-exchange-httpd.
|
||||
#
|
||||
# Usage:
|
||||
# /root/start_base_services_for_taler_exchange.sh
|
||||
# /root/start_base_services_for_taler_exchange.sh --no-shell
|
||||
|
||||
set -e
|
||||
CONF=/etc/taler-exchange/taler-exchange.conf
|
||||
LOG_DIR=/var/log/taler-exchange
|
||||
PWD_BIN=/usr/local/bin
|
||||
EXCHANGE_STARTER=start_exchange.sh
|
||||
|
||||
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
|
||||
|
||||
# --- 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
|
||||
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
|
||||
|
||||
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 "Create log + runtime dirs... giving permission to taler-exchange users:"
|
||||
mkdir -p "$LOG_DIR"
|
||||
mkdir -p /run/taler-exchange/secmod-rsa /run/taler-exchange/secmod-cs \
|
||||
/run/taler-exchange/secmod-eddsa /run/taler-exchange/httpd
|
||||
chown root:root /run/taler-exchange
|
||||
chmod 755 /run/taler-exchange
|
||||
chown taler-exchange-secmod-rsa:taler-exchange-secmod /run/taler-exchange/secmod-rsa
|
||||
chown taler-exchange-secmod-cs:taler-exchange-secmod /run/taler-exchange/secmod-cs
|
||||
chown taler-exchange-secmod-eddsa:taler-exchange-secmod /run/taler-exchange/secmod-eddsa
|
||||
chown taler-exchange-httpd:www-data /run/taler-exchange/httpd
|
||||
chmod 755 /run/taler-exchange/secmod-rsa /run/taler-exchange/secmod-cs /run/taler-exchange/secmod-eddsa
|
||||
chmod 750 /run/taler-exchange/httpd
|
||||
chown taler-exchange-httpd: "$LOG_DIR"
|
||||
chmod 755 "$LOG_DIR"
|
||||
|
||||
# Package default: each secmod user owns its tree (keys/ must not be root-owned).
|
||||
mkdir -p /var/lib/taler-exchange/secmod-rsa /var/lib/taler-exchange/secmod-cs \
|
||||
/var/lib/taler-exchange/secmod-eddsa
|
||||
chown -R taler-exchange-secmod-rsa:taler-exchange-secmod /var/lib/taler-exchange/secmod-rsa
|
||||
chown -R taler-exchange-secmod-cs:taler-exchange-secmod /var/lib/taler-exchange/secmod-cs
|
||||
chown -R taler-exchange-secmod-eddsa:taler-exchange-secmod /var/lib/taler-exchange/secmod-eddsa
|
||||
chmod 700 /var/lib/taler-exchange/secmod-rsa /var/lib/taler-exchange/secmod-cs \
|
||||
/var/lib/taler-exchange/secmod-eddsa
|
||||
|
||||
echo "Start base services needed for Taler Exchange."
|
||||
echo ""
|
||||
|
||||
echo "1. postgresql:"
|
||||
ensure_postgresql
|
||||
|
||||
# Linux COMM is 15 chars — never pgrep -x for long names; match full path in args.
|
||||
start_bg() {
|
||||
local user="$1"; shift
|
||||
local name="$1"; shift
|
||||
local bin="$1"
|
||||
if ps -eo args= 2>/dev/null | grep -F "$bin" | grep -v grep >/dev/null 2>&1; then
|
||||
echo " already running: $name"
|
||||
return 0
|
||||
fi
|
||||
echo " start $name as $user"
|
||||
nohup runuser -u "$user" -- "$@" >>"$LOG_DIR/${name}.log" 2>&1 </dev/null &
|
||||
disown 2>/dev/null || true
|
||||
sleep 0.3
|
||||
}
|
||||
|
||||
echo "2. crypto secmods:"
|
||||
start_bg taler-exchange-secmod-rsa taler-exchange-secmod-rsa \
|
||||
/usr/bin/taler-exchange-secmod-rsa -c "$CONF" -L INFO
|
||||
start_bg taler-exchange-secmod-cs taler-exchange-secmod-cs \
|
||||
/usr/bin/taler-exchange-secmod-cs -c "$CONF" -L INFO
|
||||
start_bg taler-exchange-secmod-eddsa taler-exchange-secmod-eddsa \
|
||||
/usr/bin/taler-exchange-secmod-eddsa -c "$CONF" -L INFO
|
||||
|
||||
echo "3. wire/db helpers (need wire config to stay up):"
|
||||
start_bg taler-exchange-aggregator taler-exchange-aggregator \
|
||||
/usr/bin/taler-exchange-aggregator -c "$CONF" -L INFO || true
|
||||
start_bg taler-exchange-closer taler-exchange-closer \
|
||||
/usr/bin/taler-exchange-closer -c "$CONF" -L INFO || true
|
||||
start_bg taler-exchange-wire taler-exchange-wirewatch \
|
||||
/usr/bin/taler-exchange-wirewatch -c "$CONF" -L INFO || true
|
||||
start_bg taler-exchange-wire taler-exchange-transfer \
|
||||
/usr/bin/taler-exchange-transfer -c "$CONF" -L INFO || true
|
||||
|
||||
if [ "$NO_SHELL" -eq 1 ]; then
|
||||
echo "Base services started (--no-shell). Next: runuser -u taler-exchange-httpd -- $PWD_BIN/$EXCHANGE_STARTER [--restart]"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "4. Switching now to user taler-exchange-httpd, in $PWD_BIN; find executable $EXCHANGE_STARTER there!"
|
||||
echo ""
|
||||
cd "$PWD_BIN"
|
||||
# same pattern as merchant: -u and -s/--shell are mutually exclusive on util-linux runuser
|
||||
exec runuser -u taler-exchange-httpd -- bash
|
||||
104
scripts/taler-exchange/start_exchange.sh
Executable file
104
scripts/taler-exchange/start_exchange.sh
Executable file
|
|
@ -0,0 +1,104 @@
|
|||
#!/bin/bash
|
||||
# Start / restart taler-exchange-httpd (manual, no systemd).
|
||||
# Run as: taler-exchange-httpd
|
||||
# Same role as start_merchant.sh for the merchant.
|
||||
#
|
||||
# Usage:
|
||||
# start_exchange.sh
|
||||
# start_exchange.sh --restart | -r
|
||||
# start_exchange.sh --help
|
||||
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: start_exchange.sh [--restart|-r] [--help|-h]
|
||||
|
||||
(default) Start taler-exchange-httpd if not already running.
|
||||
--restart Stop live taler-exchange-httpd, then start cleanly.
|
||||
Does not touch postgres/secmods/wire helpers
|
||||
(those come from /root/start_base_services_for_taler_exchange.sh).
|
||||
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-exchange-httpd" ]; then
|
||||
echo "This script must be run as user taler-exchange-httpd" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONF=/etc/taler-exchange/taler-exchange.conf
|
||||
LOG_DIR=/var/log/taler-exchange
|
||||
PORT=$(taler-exchange-config -c "$CONF" -s exchange -o PORT 2>/dev/null || echo 9011)
|
||||
|
||||
list_httpd_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_exchange.sh*) continue ;;
|
||||
esac
|
||||
case "$args" in
|
||||
*taler-exchange-httpd\ *|taler-exchange-httpd\ *|/usr/bin/taler-exchange-httpd\ *)
|
||||
echo "$pid"
|
||||
;;
|
||||
esac
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
kill_httpd() {
|
||||
local pids
|
||||
pids=$(list_httpd_pids | tr '\n' ' ')
|
||||
if [ -z "${pids// }" ]; then
|
||||
echo "No live taler-exchange-httpd 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_httpd_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-exchange-httpd stopped."
|
||||
}
|
||||
|
||||
if [ "$DO_RESTART" -eq 1 ]; then
|
||||
echo "=== restart: kill taler-exchange-httpd ==="
|
||||
kill_httpd
|
||||
fi
|
||||
|
||||
echo "Start taler-exchange-httpd:"
|
||||
LOG_FILE="$LOG_DIR/taler-exchange-httpd-$(date +%Y-%m-%d).log"
|
||||
mkdir -p "$LOG_DIR"
|
||||
touch "$LOG_FILE" 2>/dev/null || true
|
||||
|
||||
if [ "$DO_RESTART" -eq 0 ] && [ -n "$(list_httpd_pids)" ]; then
|
||||
echo "taler-exchange-httpd already running"
|
||||
else
|
||||
nohup taler-exchange-httpd -c "$CONF" -L INFO >>"$LOG_FILE" 2>&1 &
|
||||
disown 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
echo "Live processes:"
|
||||
ps -eo pid,stat,args 2>/dev/null | grep taler-exchange-httpd | grep -v grep | grep -v ' Z ' || true
|
||||
|
||||
if [ -x /usr/local/bin/check_exchange-health.sh ]; then
|
||||
/usr/local/bin/check_exchange-health.sh || exit 1
|
||||
elif [ -x ./check_exchange-health.sh ]; then
|
||||
./check_exchange-health.sh || exit 1
|
||||
fi
|
||||
exit 0
|
||||
43
scripts/taler-exchange/start_wire_helpers.sh
Executable file
43
scripts/taler-exchange/start_wire_helpers.sh
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
#!/bin/bash
|
||||
# Start wire/db helpers only (root, no interactive shell, no systemd).
|
||||
# Uses nohup so helpers survive the launching shell.
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
if [ -x "$ROOT/ensure_exchange_helpers.sh" ]; then
|
||||
exec "$ROOT/ensure_exchange_helpers.sh"
|
||||
fi
|
||||
if [ -x /usr/local/bin/ensure_exchange_helpers.sh ]; then
|
||||
exec /usr/local/bin/ensure_exchange_helpers.sh
|
||||
fi
|
||||
# Fallback if ensure not installed yet
|
||||
CONF=/etc/taler-exchange/taler-exchange.conf
|
||||
LOG_DIR=/var/log/taler-exchange
|
||||
[ "$(id -u)" -eq 0 ] || { echo "root only"; exit 1; }
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
start_bg() {
|
||||
local user="$1"; shift
|
||||
local name="$1"; shift
|
||||
local bin="$1"
|
||||
if pgrep -u "$user" -x "$(basename "$bin")" >/dev/null 2>&1; then
|
||||
echo "already: $name"
|
||||
return 0
|
||||
fi
|
||||
echo "start: $name as $user"
|
||||
nohup runuser -u "$user" -- "$@" >>"$LOG_DIR/${name}.log" 2>&1 </dev/null &
|
||||
disown 2>/dev/null || true
|
||||
sleep 0.4
|
||||
}
|
||||
|
||||
start_bg taler-exchange-aggregator taler-exchange-aggregator \
|
||||
/usr/bin/taler-exchange-aggregator -c "$CONF" -L INFO
|
||||
start_bg taler-exchange-closer taler-exchange-closer \
|
||||
/usr/bin/taler-exchange-closer -c "$CONF" -L INFO
|
||||
start_bg taler-exchange-wire taler-exchange-wirewatch \
|
||||
/usr/bin/taler-exchange-wirewatch -c "$CONF" -L INFO
|
||||
start_bg taler-exchange-wire taler-exchange-transfer \
|
||||
/usr/bin/taler-exchange-transfer -c "$CONF" -L INFO
|
||||
|
||||
sleep 1
|
||||
pgrep -af 'taler-exchange-(aggregator|closer|wirewatch|transfer|httpd|secmod)' || true
|
||||
[ -x /usr/local/bin/check_exchange-health.sh ] && /usr/local/bin/check_exchange-health.sh || true
|
||||
91
scripts/taler-exchange/wire-enable-and-upload.sh
Executable file
91
scripts/taler-exchange/wire-enable-and-upload.sh
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
#!/bin/bash
|
||||
# One-shot: offline enable wire account + fees, upload to local exchange, verify /keys.
|
||||
# Run as root inside container taler-hacktivism-exchange-ansible.
|
||||
set -euo pipefail
|
||||
|
||||
CONF=/etc/taler-exchange/taler-exchange.conf
|
||||
OV=/etc/taler-exchange/exchange-overrides.conf
|
||||
PAYTO='payto://x-taler-bank/bank.hacktivism.ch/exchange?receiver-name=GOA%20Exchange'
|
||||
LOCAL_BASE='http://127.0.0.1:9011/'
|
||||
OPS=/tmp/offline-wire-ops-$$.json
|
||||
OFFLINE_USER=taler-exchange-offline
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run as root in exchange container" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== 1. Ensure wire account stanza in exchange-overrides ==="
|
||||
if ! grep -q '\[exchange-account-1\]' "$OV" 2>/dev/null; then
|
||||
cat >>"$OV" <<'EOF'
|
||||
|
||||
### Regional bank (libeufin, x-taler-bank) — no IBAN
|
||||
[exchange-account-1]
|
||||
PAYTO_URI = payto://x-taler-bank/bank.hacktivism.ch/exchange?receiver-name=GOA%20Exchange
|
||||
ENABLE_CREDIT = YES
|
||||
ENABLE_DEBIT = YES
|
||||
@inline-secret@ exchange-accountcredentials-1 ../secrets/exchange-accountcredentials-1.secret.conf
|
||||
EOF
|
||||
echo "appended exchange-account-1"
|
||||
else
|
||||
echo "exchange-account-1 already present"
|
||||
fi
|
||||
|
||||
# credentials must be readable by wire helpers + httpd
|
||||
if [ -f /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf ]; then
|
||||
chown root:taler-exchange-wire /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf 2>/dev/null \
|
||||
|| chown root:root /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf
|
||||
chmod 640 /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf
|
||||
fi
|
||||
|
||||
echo "=== 2. Point BASE_URL at local httpd for offline upload ==="
|
||||
# save public URL
|
||||
PUBLIC_BASE=$(taler-config -c "$CONF" -s exchange -o BASE_URL 2>/dev/null || echo 'https://exchange.hacktivism.ch/')
|
||||
# temporary override file (highest priority if inlined last — patch OV)
|
||||
if grep -q '^BASE_URL' "$OV"; then
|
||||
sed -i "s|^BASE_URL = .*|BASE_URL = ${LOCAL_BASE}|" "$OV"
|
||||
else
|
||||
echo "BASE_URL = ${LOCAL_BASE}" >>"$OV"
|
||||
fi
|
||||
echo "BASE_URL now: $(taler-config -c "$CONF" -s exchange -o BASE_URL)"
|
||||
|
||||
echo "=== 3. Sign enable-account + wire-fee + global-fee (as offline user) ==="
|
||||
# global-fee: year, history, account, purse, purse_timeout, history_expiration, max_free_purses
|
||||
runuser -u "$OFFLINE_USER" -- taler-exchange-offline -c "$CONF" \
|
||||
enable-account "$PAYTO" \
|
||||
wire-fee now x-taler-bank GOA:0 GOA:0 \
|
||||
global-fee now GOA:0 GOA:0 GOA:0 '1 day' '1 year' 5 \
|
||||
>"$OPS"
|
||||
echo "ops bytes: $(wc -c <"$OPS")"
|
||||
head -c 200 "$OPS"; echo
|
||||
|
||||
echo "=== 4. Upload ops to exchange ==="
|
||||
runuser -u "$OFFLINE_USER" -- taler-exchange-offline -c "$CONF" upload <"$OPS"
|
||||
echo "upload exit: $?"
|
||||
|
||||
echo "=== 5. Restore public BASE_URL ==="
|
||||
sed -i "s|^BASE_URL = .*|BASE_URL = ${PUBLIC_BASE}|" "$OV"
|
||||
# ensure trailing slash style
|
||||
if ! grep -q "BASE_URL = https://exchange.hacktivism.ch" "$OV"; then
|
||||
sed -i "s|^BASE_URL = .*|BASE_URL = https://exchange.hacktivism.ch/|" "$OV"
|
||||
fi
|
||||
echo "BASE_URL restored: $(taler-config -c "$CONF" -s exchange -o BASE_URL)"
|
||||
|
||||
echo "=== 6. Probe /keys (may need httpd already running) ==="
|
||||
for i in 1 2 3 4 5; do
|
||||
code=$(curl -sS -m 8 -o /tmp/keys-out.json -w '%{http_code}' http://127.0.0.1:9011/keys || echo fail)
|
||||
sz=$(wc -c </tmp/keys-out.json 2>/dev/null || echo 0)
|
||||
echo "try $i: http=$code size=$sz"
|
||||
if [ "$code" = "200" ] && [ "${sz:-0}" -gt 100 ]; then
|
||||
echo "KEYS_OK"
|
||||
head -c 250 /tmp/keys-out.json; echo
|
||||
# show accounts if present
|
||||
grep -oE '"payto_uri"[^,}]+|"master_public_key"[^,}]+' /tmp/keys-out.json | head -10 || true
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "KEYS_NOT_YET — check logs"
|
||||
tail -30 /var/log/taler-exchange/taler-exchange-httpd-*.log 2>/dev/null | tail -30
|
||||
exit 1
|
||||
72
scripts/taler-landing/deploy-landings.sh
Normal file
72
scripts/taler-landing/deploy-landings.sh
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#!/bin/bash
|
||||
# Install exchange + merchant landing pages inside containers and ensure nginx
|
||||
# listens on 9014 (exchange) / 9015 (merchant).
|
||||
#
|
||||
# Port map (wire these in Caddy / podman -p):
|
||||
# 9013 bank landing (already on taler-hacktivism-bank)
|
||||
# 9014 exchange landing (taler-hacktivism-exchange-ansible)
|
||||
# 9015 merchant landing (taler-hacktivism)
|
||||
#
|
||||
# Run on koopa (host) with podman access.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
# allow override when files already on host /tmp
|
||||
SRC_EX="${SRC_EX:-$ROOT/configs/exchange-landing}"
|
||||
SRC_MER="${SRC_MER:-$ROOT/configs/merchant-landing}"
|
||||
SRC_QR="${SRC_QR:-$ROOT/configs/bank-landing/qrcode.min.js}"
|
||||
|
||||
C_EX=taler-hacktivism-exchange-ansible
|
||||
C_MER=taler-hacktivism
|
||||
|
||||
echo "=== exchange landing → $C_EX :9014 ==="
|
||||
# nginx may be missing on exchange image
|
||||
if ! podman exec "$C_EX" command -v nginx >/dev/null 2>&1; then
|
||||
echo "installing nginx in $C_EX …"
|
||||
podman exec "$C_EX" bash -c 'export DEBIAN_FRONTEND=noninteractive; apt-get update -qq && apt-get install -y -qq nginx'
|
||||
fi
|
||||
podman exec "$C_EX" mkdir -p /var/www/exchange-landing /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||
podman cp "$SRC_EX/index.html" "$C_EX:/var/www/exchange-landing/index.html"
|
||||
if [ -f "$SRC_QR" ]; then
|
||||
podman cp "$SRC_QR" "$C_EX:/var/www/exchange-landing/qrcode.min.js"
|
||||
fi
|
||||
podman cp "$SRC_EX/nginx-landing.conf" "$C_EX:/etc/nginx/sites-available/exchange-landing"
|
||||
podman exec "$C_EX" bash -c '
|
||||
ln -sfn /etc/nginx/sites-available/exchange-landing /etc/nginx/sites-enabled/exchange-landing
|
||||
# ensure sites-enabled is included
|
||||
if ! grep -q sites-enabled /etc/nginx/nginx.conf 2>/dev/null; then
|
||||
sed -i "/http {/a\\ include /etc/nginx/sites-enabled/*;" /etc/nginx/nginx.conf 2>/dev/null || true
|
||||
fi
|
||||
nginx -t
|
||||
if command -v systemctl >/dev/null && systemctl is-system-running >/dev/null 2>&1; then
|
||||
systemctl enable nginx 2>/dev/null || true
|
||||
systemctl restart nginx || nginx
|
||||
else
|
||||
nginx -s reload 2>/dev/null || nginx
|
||||
fi
|
||||
ss -lntp | grep 9014 || netstat -lntp 2>/dev/null | grep 9014 || true
|
||||
'
|
||||
|
||||
echo "=== merchant landing → $C_MER :9015 ==="
|
||||
podman exec "$C_MER" mkdir -p /var/www/merchant-landing
|
||||
podman cp "$SRC_MER/index.html" "$C_MER:/var/www/merchant-landing/index.html"
|
||||
podman cp "$SRC_MER/nginx-landing.conf" "$C_MER:/etc/nginx/sites-available/merchant-landing"
|
||||
podman exec "$C_MER" bash -c '
|
||||
ln -sfn /etc/nginx/sites-available/merchant-landing /etc/nginx/sites-enabled/merchant-landing
|
||||
nginx -t
|
||||
nginx -s reload 2>/dev/null || systemctl reload nginx 2>/dev/null || true
|
||||
ss -lntp | grep 9015 || true
|
||||
'
|
||||
|
||||
echo
|
||||
echo "=== inside-container checks ==="
|
||||
podman exec "$C_EX" curl -sS -m 3 -o /dev/null -w "exchange_landing=%{http_code}\n" http://127.0.0.1:9014/intro/ || true
|
||||
podman exec "$C_MER" curl -sS -m 3 -o /dev/null -w "merchant_landing=%{http_code}\n" http://127.0.0.1:9015/intro/ || true
|
||||
|
||||
echo
|
||||
echo "PORTS TO WIRE (host → Caddy / firewall / podman -p):"
|
||||
echo " 9013 bank landing (taler-hacktivism-bank) already published"
|
||||
echo " 9014 exchange landing (taler-hacktivism-exchange-ansible) NEEDS -p 9014:9014"
|
||||
echo " 9015 merchant landing (taler-hacktivism) NEEDS -p 9015:9015"
|
||||
echo
|
||||
echo "If host curl to :9014/:9015 fails, republish pasta ports (commit+replace) — see README."
|
||||
56
scripts/taler-merchant/README.md
Normal file
56
scripts/taler-merchant/README.md
Normal 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).
|
||||
5
scripts/taler-merchant/certbot_renew.sh
Executable file
5
scripts/taler-merchant/certbot_renew.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
while true; do
|
||||
certbot renew --quiet
|
||||
sleep 12h
|
||||
done
|
||||
222
scripts/taler-merchant/check_merchant-health.sh
Executable file
222
scripts/taler-merchant/check_merchant-health.sh
Executable 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 ==="
|
||||
|
||||
# --- 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 <<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
|
||||
100
scripts/taler-merchant/ensure_merchant_helpers.sh
Executable file
100
scripts/taler-merchant/ensure_merchant_helpers.sh
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
#!/bin/bash
|
||||
# Ensure merchant helper processes are running (no systemd).
|
||||
# Prefer run as taler-merchant-httpd; root may use runuser.
|
||||
#
|
||||
# Usage:
|
||||
# ensure_merchant_helpers.sh
|
||||
# (as root) ensure_merchant_helpers.sh # re-exec as taler-merchant-httpd
|
||||
set -euo pipefail
|
||||
|
||||
CONF="${TALER_MERCHANT_CONFIG:-/etc/taler-merchant/taler-merchant.conf}"
|
||||
LOG_DIR="${TALER_MERCHANT_LOG_DIR:-/var/log/taler-merchant}"
|
||||
|
||||
# As root: start wirewatch supervisor (needs root for runuser), then re-exec as httpd.
|
||||
if [ "$(id -un)" = "root" ]; then
|
||||
mkdir -p "$LOG_DIR"
|
||||
if [ -x /usr/local/bin/taler-merchant-wirewatch-supervise.sh ]; then
|
||||
if ! ps -eo args= 2>/dev/null | grep -q 'taler-merchant-wirewatch-supervise\.sh'; then
|
||||
echo "start: taler-merchant-wirewatch-supervise"
|
||||
nohup /usr/local/bin/taler-merchant-wirewatch-supervise.sh \
|
||||
>>"$LOG_DIR/wirewatch-supervise.nohup" 2>&1 </dev/null &
|
||||
disown 2>/dev/null || true
|
||||
else
|
||||
echo "already: taler-merchant-wirewatch-supervise"
|
||||
fi
|
||||
fi
|
||||
exec runuser -u taler-merchant-httpd -- "$0" "$@"
|
||||
fi
|
||||
|
||||
if [ "$(id -un)" != "taler-merchant-httpd" ]; then
|
||||
echo "run as taler-merchant-httpd or root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
chmod 755 "$LOG_DIR" 2>/dev/null || true
|
||||
|
||||
# Linux COMM is 15 chars — do not use pgrep -x for taler-merchant-wirewatch etc.
|
||||
is_running() {
|
||||
local bin="$1"
|
||||
local base
|
||||
base=$(basename "$bin")
|
||||
ps -u "$(id -un)" -o args= 2>/dev/null | grep -qE "(^|/)[${base:0:1}]${base:1}( |$)" \
|
||||
|| pgrep -u "$(id -un)" -f "(^|/)(${base})( |$)" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
start_one() {
|
||||
local name="$1"
|
||||
local bin="$2"
|
||||
shift 2
|
||||
if is_running "$bin"; then
|
||||
echo "already: $name"
|
||||
return 0
|
||||
fi
|
||||
echo "start: $name"
|
||||
nohup "$bin" "$@" >>"$LOG_DIR/${name}.log" 2>&1 </dev/null &
|
||||
disown 2>/dev/null || true
|
||||
local i
|
||||
for i in 1 2 3 4 5 6; do
|
||||
sleep 0.4
|
||||
if is_running "$bin"; then
|
||||
echo " ok: $name"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo " FAIL: $name did not stay up (see $LOG_DIR/${name}.log)" >&2
|
||||
tail -20 "$LOG_DIR/${name}.log" 2>/dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
ec=0
|
||||
# Helpers needed for settlement / ops (not only httpd).
|
||||
start_one taler-merchant-webhook /usr/bin/taler-merchant-webhook || ec=1
|
||||
start_one taler-merchant-kyccheck /usr/bin/taler-merchant-kyccheck || ec=1
|
||||
# Prefer already-running supervisor (started as root above); else bare wirewatch.
|
||||
if ps -eo args= 2>/dev/null | grep -q 'taler-merchant-wirewatch-supervise\.sh'; then
|
||||
echo "already: taler-merchant-wirewatch (via supervise)"
|
||||
elif is_running /usr/bin/taler-merchant-wirewatch; then
|
||||
echo "already: taler-merchant-wirewatch"
|
||||
else
|
||||
start_one taler-merchant-wirewatch /usr/bin/taler-merchant-wirewatch \
|
||||
-c "$CONF" -L INFO || ec=1
|
||||
fi
|
||||
start_one taler-merchant-depositcheck /usr/bin/taler-merchant-depositcheck || ec=1
|
||||
start_one taler-merchant-exchangekeyupdate /usr/bin/taler-merchant-exchangekeyupdate || ec=1
|
||||
start_one taler-merchant-reconciliation /usr/bin/taler-merchant-reconciliation || ec=1
|
||||
|
||||
if ! is_running taler-merchant-httpd; then
|
||||
echo "start: taler-merchant-httpd"
|
||||
nohup /usr/bin/taler-merchant-httpd --log=info \
|
||||
>>"$LOG_DIR/taler-merchant-httpd-$(date +%Y-%m-%d).log" 2>&1 </dev/null &
|
||||
disown 2>/dev/null || true
|
||||
sleep 1
|
||||
is_running taler-merchant-httpd || ec=1
|
||||
fi
|
||||
|
||||
echo "--- live merchant ---"
|
||||
ps -eo pid,user,stat,etime,args 2>/dev/null \
|
||||
| grep taler-merchant | grep -vE 'grep| Z |ensure_merchant' || true
|
||||
|
||||
exit "$ec"
|
||||
232
scripts/taler-merchant/install_dual_terms.sh
Normal file
232
scripts/taler-merchant/install_dual_terms.sh
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
#!/bin/bash
|
||||
# Install dual-currency "No Formal Terms" ToS for the merchant backend.
|
||||
# Run as root inside the merchant container (taler-hacktivism).
|
||||
#
|
||||
# Sets files under TERMS_DIR/en/ for TERMS_ETAG = merchant-tos-dual-v0
|
||||
# (see configs/taler-hacktivism/merchant-overrides.conf).
|
||||
set -euo pipefail
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
|
||||
ETAG="${TERMS_ETAG:-merchant-tos-dual-v2}"
|
||||
CONF="${TALER_MERCHANT_CONFIG:-/etc/taler-merchant/taler-merchant.conf}"
|
||||
|
||||
DATA_HOME=""
|
||||
if command -v taler-config >/dev/null 2>&1; then
|
||||
DATA_HOME=$(taler-config -c "$CONF" -f -s PATHS -o TALER_DATA_HOME 2>/dev/null || true)
|
||||
fi
|
||||
# common fallbacks
|
||||
for d in \
|
||||
"$DATA_HOME" \
|
||||
/var/lib/taler-merchant/ \
|
||||
/var/lib/taler-merchant
|
||||
do
|
||||
[ -n "${d:-}" ] || continue
|
||||
d="${d%/}/"
|
||||
if [ -d "$d" ] || mkdir -p "$d" 2>/dev/null; then
|
||||
DATA_HOME="$d"
|
||||
break
|
||||
fi
|
||||
done
|
||||
DATA_HOME="${DATA_HOME:-/var/lib/taler-merchant/}"
|
||||
TERMS_DIR="${TERMS_DIR:-${DATA_HOME%/}/terms}"
|
||||
LANG_DIR="$TERMS_DIR/en"
|
||||
mkdir -p "$LANG_DIR"
|
||||
|
||||
TITLE="No Formal Terms · Dual Currency Notice"
|
||||
|
||||
BODY_MD='# No Formal Terms · Dual Currency Notice
|
||||
|
||||
This is a **self-hosted GNU Taler merchant backend** at `taler.hacktivism.ch` (hacktivism.ch).
|
||||
|
||||
**No formal terms of service** from Taler Operations AG (or any other third-party portal operator) apply to this instance. This short notice is the site policy for using the backend.
|
||||
|
||||
## Dual currency
|
||||
|
||||
This backend is configured for **two currencies at once**:
|
||||
|
||||
- **GOA** — explorational / experimental currency of the local stack (`exchange.hacktivism.ch`, `bank.hacktivism.ch`). GOA is **not** legal tender. It has **no guaranteed real-world value**, redemption, or convertibility.
|
||||
- **CHF** — Swiss francs, a **real** currency. CHF amounts are real money settled via the CHF exchange configured on this host (taler-ops / TOPS infrastructure as deployed). Treat CHF with the seriousness of ordinary payments.
|
||||
|
||||
By creating a merchant instance, accepting payments, or otherwise using this service you acknowledge that:
|
||||
|
||||
- GOA is for exploration and testing only.
|
||||
- CHF involves real money — only use funds you control and can afford to risk on a self-hosted experimental stack.
|
||||
- There is no guaranteed availability, support, or uptime.
|
||||
- Operators may reset GOA state, change configuration, delete instance data, or interrupt service without notice.
|
||||
- Software is provided as-is, without warranty.
|
||||
|
||||
If you do not agree, do not use this merchant backend.
|
||||
|
||||
## Related
|
||||
|
||||
- Exchange (GOA): https://exchange.hacktivism.ch/terms
|
||||
- Bank intro: https://bank.hacktivism.ch/intro/
|
||||
- Merchant intro: https://taler.hacktivism.ch/intro/
|
||||
'
|
||||
|
||||
BODY_TXT='No Formal Terms · Dual Currency Notice
|
||||
|
||||
This is a self-hosted GNU Taler merchant backend at taler.hacktivism.ch (hacktivism.ch).
|
||||
|
||||
No formal terms of service from Taler Operations AG (or any other third-party portal operator) apply to this instance. This short notice is the site policy for using the backend.
|
||||
|
||||
Dual currency
|
||||
-------------
|
||||
This backend is configured for two currencies at once:
|
||||
|
||||
- GOA — explorational / experimental currency of the local stack
|
||||
(exchange.hacktivism.ch, bank.hacktivism.ch). GOA is not legal tender.
|
||||
It has no guaranteed real-world value, redemption, or convertibility.
|
||||
- CHF — Swiss francs, a real currency. CHF amounts are real money
|
||||
settled via the CHF exchange configured on this host (taler-ops / TOPS
|
||||
infrastructure as deployed). Treat CHF with the seriousness of
|
||||
ordinary payments.
|
||||
|
||||
By creating a merchant instance, accepting payments, or otherwise using
|
||||
this service you acknowledge that:
|
||||
|
||||
- GOA is for exploration and testing only.
|
||||
- CHF involves real money — only use funds you control and can afford
|
||||
to risk on a self-hosted experimental stack.
|
||||
- There is no guaranteed availability, support, or uptime.
|
||||
- Operators may reset GOA state, change configuration, delete instance
|
||||
data, or interrupt service without notice.
|
||||
- Software is provided as-is, without warranty.
|
||||
|
||||
If you do not agree, do not use this merchant backend.
|
||||
|
||||
Related
|
||||
-------
|
||||
- Exchange (GOA): https://exchange.hacktivism.ch/terms
|
||||
- Bank intro: https://bank.hacktivism.ch/intro/
|
||||
- Merchant intro: https://taler.hacktivism.ch/intro/
|
||||
'
|
||||
|
||||
printf '%s\n' "$BODY_TXT" >"$LANG_DIR/${ETAG}.txt"
|
||||
printf '%s\n' "$BODY_MD" >"$LANG_DIR/${ETAG}.md"
|
||||
|
||||
# HTML (browser-friendly; style close to exchange short terms)
|
||||
cat >"$LANG_DIR/${ETAG}.html" <<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>${TITLE}</title>
|
||||
<style>
|
||||
:root { color-scheme: dark light; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 40rem; margin: 2rem auto; padding: 0 1.1rem 3rem;
|
||||
line-height: 1.5; color: #e8e6e3; background: #1a1520;
|
||||
}
|
||||
h1 { font-size: 1.35rem; font-weight: 800; margin: 0 0 1rem; color: #f5f0ea; }
|
||||
h2 { font-size: 1.05rem; margin: 1.4rem 0 0.5rem; color: #e8c878; }
|
||||
p, li { font-size: 0.98rem; }
|
||||
ul { padding-left: 1.2rem; }
|
||||
code, a { color: #5eead4; }
|
||||
a { text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.badge {
|
||||
display: inline-block; font-size: 0.72rem; font-weight: 700;
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
color: #c4b5fd; border: 1px solid rgba(196,181,253,0.35);
|
||||
border-radius: 999px; padding: 0.2rem 0.65rem; margin-bottom: 0.85rem;
|
||||
}
|
||||
.pair {
|
||||
display: grid; gap: 0.65rem; margin: 0.85rem 0 1rem;
|
||||
}
|
||||
@media (min-width: 520px) { .pair { grid-template-columns: 1fr 1fr; } }
|
||||
.cur {
|
||||
border-radius: 12px; padding: 0.75rem 0.9rem;
|
||||
border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25);
|
||||
}
|
||||
.cur strong { display: block; font-size: 1.05rem; margin-bottom: 0.25rem; }
|
||||
.cur.go a, .cur.go strong { color: #5eead4; }
|
||||
.cur.chf strong { color: #e8c878; }
|
||||
.muted { color: #a39e98; font-size: 0.88rem; }
|
||||
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="badge">taler.hacktivism.ch · merchant</div>
|
||||
<h1>${TITLE}</h1>
|
||||
<p>This is a <strong>self-hosted GNU Taler merchant backend</strong> at
|
||||
<code>taler.hacktivism.ch</code> (hacktivism.ch).</p>
|
||||
<p><strong>No formal terms of service</strong> from Taler Operations AG
|
||||
(or any other third-party portal operator) apply to this instance.
|
||||
This short notice is the site policy for using the backend.</p>
|
||||
|
||||
<h2>Dual currency</h2>
|
||||
<p>This backend is configured for <strong>two currencies at once</strong>:</p>
|
||||
<div class="pair">
|
||||
<div class="cur go">
|
||||
<strong>GOA · explorational</strong>
|
||||
Local stack currency
|
||||
(<a href="https://exchange.hacktivism.ch/">exchange</a>,
|
||||
<a href="https://bank.hacktivism.ch/intro/">bank</a>).
|
||||
Not legal tender. No guaranteed real-world value, redemption, or convertibility.
|
||||
</div>
|
||||
<div class="cur chf">
|
||||
<strong>CHF · real</strong>
|
||||
Swiss francs. Real money, settled via the CHF exchange configured
|
||||
on this host (taler-ops / TOPS infrastructure as deployed).
|
||||
Treat CHF with the seriousness of ordinary payments.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>By using this service you acknowledge</h2>
|
||||
<ul>
|
||||
<li>GOA is for exploration and testing only.</li>
|
||||
<li>CHF involves real money — only use funds you control and can afford to risk on a self-hosted experimental stack.</li>
|
||||
<li>There is no guaranteed availability, support, or uptime.</li>
|
||||
<li>Operators may reset GOA state, change configuration, delete instance data, or interrupt service without notice.</li>
|
||||
<li>Software is provided as-is, without warranty.</li>
|
||||
</ul>
|
||||
<p>If you do not agree, do not use this merchant backend.</p>
|
||||
|
||||
<h2>Related</h2>
|
||||
<ul>
|
||||
<li><a href="https://exchange.hacktivism.ch/terms">Exchange terms (GOA)</a></li>
|
||||
<li><a href="https://bank.hacktivism.ch/intro/">Bank intro</a></li>
|
||||
<li><a href="https://taler.hacktivism.ch/intro/">Merchant intro</a></li>
|
||||
<li><a href="https://taler.hacktivism.ch/privacy">Merchant privacy</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Privacy</h2>
|
||||
<p class="muted">Processing under Swiss FADP (revDSG). What data is retained
|
||||
(instances, orders, deposits, logs, …) is listed on
|
||||
<a href="https://taler.hacktivism.ch/privacy">/privacy</a>.</p>
|
||||
<footer class="muted">Version ${ETAG} · hacktivism.ch</footer>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
# Optional PDF so Accept: */* / wallets preferring PDF still get content
|
||||
PDF="$LANG_DIR/${ETAG}.pdf"
|
||||
if command -v pandoc >/dev/null 2>&1; then
|
||||
if pandoc -f markdown -t pdf -o "$PDF" "$LANG_DIR/${ETAG}.md" 2>/dev/null; then
|
||||
echo "pdf via pandoc: $PDF"
|
||||
elif command -v wkhtmltopdf >/dev/null 2>&1; then
|
||||
wkhtmltopdf "$LANG_DIR/${ETAG}.html" "$PDF" 2>/dev/null && echo "pdf via wkhtmltopdf" || true
|
||||
fi
|
||||
elif command -v wkhtmltopdf >/dev/null 2>&1; then
|
||||
wkhtmltopdf "$LANG_DIR/${ETAG}.html" "$PDF" 2>/dev/null && echo "pdf via wkhtmltopdf" || true
|
||||
fi
|
||||
# If no PDF toolchain: leave absent — httpd will serve md/html/txt by Accept
|
||||
|
||||
chmod -R a+rX "$TERMS_DIR"
|
||||
if id taler-merchant-httpd >/dev/null 2>&1; then
|
||||
chown -R taler-merchant-httpd: "$TERMS_DIR" 2>/dev/null \
|
||||
|| chown -R taler-merchant-httpd:www-data "$TERMS_DIR" 2>/dev/null \
|
||||
|| true
|
||||
fi
|
||||
|
||||
echo "Installed under $LANG_DIR:"
|
||||
ls -la "$LANG_DIR"/${ETAG}.* 2>/dev/null || ls -la "$LANG_DIR"
|
||||
echo
|
||||
echo "Config should set:"
|
||||
echo " [merchant]"
|
||||
echo " TERMS_ETAG = ${ETAG}"
|
||||
echo " TERMS_DIR = \${TALER_DATA_HOME}terms/"
|
||||
echo "Then restart taler-merchant-httpd."
|
||||
195
scripts/taler-merchant/install_swiss_privacy.sh
Normal file
195
scripts/taler-merchant/install_swiss_privacy.sh
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
#!/bin/bash
|
||||
# Install Swiss FADP privacy policy for merchant backend (taler.hacktivism.ch).
|
||||
# Run as root inside taler-hacktivism. Sets files for PRIVACY_ETAG=merchant-pp-swiss-v0
|
||||
set -euo pipefail
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
|
||||
ETAG="${PRIVACY_ETAG:-merchant-pp-swiss-v0}"
|
||||
CONF="${TALER_MERCHANT_CONFIG:-/etc/taler-merchant/taler-merchant.conf}"
|
||||
DATA_HOME=""
|
||||
if command -v taler-config >/dev/null 2>&1; then
|
||||
DATA_HOME=$(taler-config -c "$CONF" -f -s PATHS -o TALER_DATA_HOME 2>/dev/null || true)
|
||||
fi
|
||||
DATA_HOME="${DATA_HOME:-/var/lib/taler-merchant/}"
|
||||
PRIVACY_DIR="${PRIVACY_DIR:-${DATA_HOME%/}/terms}"
|
||||
LANG_DIR="$PRIVACY_DIR/en"
|
||||
mkdir -p "$LANG_DIR"
|
||||
|
||||
TITLE="Privacy notice · GOA/CHF Merchant · Swiss FADP"
|
||||
BODY_TXT='Privacy notice — taler.hacktivism.ch merchant backend (Swiss FADP / revDSG)
|
||||
|
||||
Controller: operators of the hacktivism.ch GNU Taler stack.
|
||||
This dual-currency merchant backend accepts GOA (explorational) and CHF (via taler-ops exchange configuration).
|
||||
|
||||
Data retained (precise):
|
||||
1) Merchant instances — merchant_id, serial, creation metadata, config flags: lifetime of instance + up to 12 months after delete or stack wipe.
|
||||
2) Authentication — API tokens / login tokens (hashed or bearer secrets as stored by software): until expiry, revoke, or instance delete.
|
||||
3) Contract terms / orders — order_id, amount (currency:amount), summary (free text), paid/wired flags, creation time: for service lifetime; public landing stats may show anonymised aggregates and recent amounts/summaries.
|
||||
4) Deposits & refunds — deposit proofs, refund amounts/reasons, timestamps: for service lifetime or until operational wipe.
|
||||
5) Settlement accounts — payto URIs / account labels configured on instances: while configured.
|
||||
6) Exchange interaction metadata — which exchange (GOA/CHF) was used for deposits: with related order records.
|
||||
7) Technical logs — HTTP/access and application logs (IP, path, status): typically days–weeks via rotation.
|
||||
8) Public stats.json — aggregate counts and recent activity without full account credentials: overwritten minutely.
|
||||
|
||||
Not retained by this merchant backend: wallet private keys; full card PANs; unsolicited government ID documents unless an operator enables separate KYC tooling.
|
||||
|
||||
Purposes: accept GNU Taler payments, refunds, settlement, abuse prevention, operations.
|
||||
Legal basis (FADP): performance of service requested by merchant operators/customers; proportionate operation of a public experimental stack.
|
||||
Recipients: configured exchanges (exchange.hacktivism.ch for GOA; exchange.taler-ops.ch for CHF as configured); host operators. No sale of data.
|
||||
Rights: access, rectification, deletion, objection under FADP; complaint to Swiss FDPIC (EDÖB).
|
||||
Security: TLS; experimental — no certified ISMS. Do not put sensitive personal data in order summaries.
|
||||
|
||||
Related: https://taler.hacktivism.ch/terms · https://exchange.hacktivism.ch/privacy · https://bank.hacktivism.ch/intro/privacy.html
|
||||
'
|
||||
|
||||
BODY_MD='# Privacy notice · Merchant · Swiss FADP
|
||||
|
||||
Controller: operators of the **hacktivism.ch** GNU Taler stack (`taler.hacktivism.ch`).
|
||||
Dual currency: **GOA** (explorational) and **CHF** (taler-ops exchange path).
|
||||
|
||||
Processing under the Swiss Federal Act on Data Protection (**FADP / revDSG**, since 1 Sep 2023).
|
||||
|
||||
## Data retained
|
||||
|
||||
| Data | Examples | Retention |
|
||||
|------|----------|-----------|
|
||||
| Instances | merchant_id, serial, config | Instance life + up to 12 months after delete/wipe |
|
||||
| Auth | API / login tokens | Until expiry, revoke, or instance delete |
|
||||
| Orders | order_id, amount, summary, paid/wired, time | Service life or wipe; public stats may show recent aggregates |
|
||||
| Deposits / refunds | proofs, amounts, reasons | Service life or wipe |
|
||||
| Settlement | payto / account labels | While configured |
|
||||
| Exchange metadata | GOA/CHF exchange used | With related order records |
|
||||
| Technical logs | IP, path, status | Days–weeks (rotation) |
|
||||
| Public stats.json | Aggregates, recent activity | Overwritten continuously |
|
||||
|
||||
**Not retained:** wallet private keys; card PANs; government ID unless separate KYC is enabled.
|
||||
|
||||
## Purposes
|
||||
|
||||
Accept payments, refunds, settlement; security and operations of this experimental backend.
|
||||
|
||||
## Rights
|
||||
|
||||
Access, correction, deletion, objection (FADP). Complaint: Swiss **FDPIC / EDÖB**.
|
||||
|
||||
## Related
|
||||
|
||||
- [Merchant terms](https://taler.hacktivism.ch/terms)
|
||||
- [Exchange privacy](https://exchange.hacktivism.ch/privacy)
|
||||
- [Bank privacy](https://bank.hacktivism.ch/intro/privacy.html)
|
||||
'
|
||||
|
||||
cat >"$LANG_DIR/${ETAG}.txt" <<EOF
|
||||
$BODY_TXT
|
||||
EOF
|
||||
cat >"$LANG_DIR/${ETAG}.md" <<EOF
|
||||
$BODY_MD
|
||||
EOF
|
||||
|
||||
cat >"$LANG_DIR/${ETAG}.html" <<'HTML'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Privacy notice · GOA/CHF Merchant · Swiss FADP</title>
|
||||
<style>
|
||||
:root { color-scheme: dark light; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 42rem; margin: 2rem auto; padding: 0 1.1rem 3rem;
|
||||
line-height: 1.5; color: #e8e6e3; background: #1a1520;
|
||||
}
|
||||
h1 { font-size: 1.35rem; font-weight: 800; margin: 0 0 1rem; color: #f5f0ea; }
|
||||
h2 { font-size: 1.05rem; margin: 1.5rem 0 0.5rem; color: #e8c878; }
|
||||
p, li, td, th { font-size: 0.95rem; }
|
||||
ul { padding-left: 1.2rem; }
|
||||
code, a { color: #5eead4; }
|
||||
a { text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.badge {
|
||||
display: inline-block; font-size: 0.72rem; font-weight: 700;
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
color: #c4b5fd; border: 1px solid rgba(196,181,253,0.35);
|
||||
border-radius: 999px; padding: 0.2rem 0.65rem; margin-bottom: 0.85rem;
|
||||
}
|
||||
.note {
|
||||
border-radius: 12px; padding: 0.75rem 0.9rem; margin: 0.85rem 0 1rem;
|
||||
border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25);
|
||||
font-size: 0.9rem; color: #c8c4bf;
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; margin: 0.6rem 0 1rem; font-size: 0.88rem; }
|
||||
th, td { border: 1px solid rgba(255,255,255,0.12); padding: 0.45rem 0.55rem; text-align: left; vertical-align: top; }
|
||||
th { background: rgba(0,0,0,0.35); color: #e8c878; font-weight: 700; }
|
||||
.muted { color: #a39e98; font-size: 0.88rem; }
|
||||
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="badge">taler.hacktivism.ch · privacy · CH</div>
|
||||
<h1>Privacy notice · GOA/CHF Merchant</h1>
|
||||
<p class="note">
|
||||
Swiss Federal Act on Data Protection (<strong>FADP / revDSG</strong>, since 1 Sep 2023).
|
||||
Dual-currency merchant backend at <code>taler.hacktivism.ch</code>:
|
||||
<strong>GOA</strong> (explorational) and <strong>CHF</strong> (taler-ops path).
|
||||
</p>
|
||||
|
||||
<h2>1. Controller</h2>
|
||||
<p>Operators of the hacktivism.ch GNU Taler stack. Experimental public deployment;
|
||||
no separate DPO appointed.</p>
|
||||
|
||||
<h2>2. Data retained</h2>
|
||||
<table>
|
||||
<thead><tr><th>Data</th><th>Examples</th><th>Typical retention</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Instances</td><td>merchant_id, serial, config</td><td>Instance life + up to 12 months after delete/wipe</td></tr>
|
||||
<tr><td>Authentication</td><td>API / login tokens</td><td>Until expiry, revoke, or instance delete</td></tr>
|
||||
<tr><td>Orders</td><td>order_id, amount, summary, paid/wired, time</td><td>Service life or wipe; public stats may list recent amounts/summaries</td></tr>
|
||||
<tr><td>Deposits / refunds</td><td>proofs, amounts, reasons</td><td>Service life or wipe</td></tr>
|
||||
<tr><td>Settlement accounts</td><td>payto / account labels</td><td>While configured</td></tr>
|
||||
<tr><td>Exchange metadata</td><td>GOA/CHF exchange used</td><td>With related order records</td></tr>
|
||||
<tr><td>Technical logs</td><td>IP, path, status</td><td>Days–weeks (rotation)</td></tr>
|
||||
<tr><td>Public stats.json</td><td>Aggregates, recent activity</td><td>Overwritten continuously</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Not retained:</strong> wallet private keys; payment card PANs; government ID documents
|
||||
unless a separate KYC feature is enabled by operators.</p>
|
||||
|
||||
<h2>3. Purposes</h2>
|
||||
<ul>
|
||||
<li>Accept GNU Taler payments (GOA and/or CHF), refunds, and settlement</li>
|
||||
<li>Authenticate instance operators</li>
|
||||
<li>Security, abuse prevention, debugging</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Recipients</h2>
|
||||
<ul>
|
||||
<li>Local GOA exchange (<code>exchange.hacktivism.ch</code>)</li>
|
||||
<li>CHF exchange as configured (e.g. <code>exchange.taler-ops.ch</code>)</li>
|
||||
<li>Host/infrastructure operators under this deployment</li>
|
||||
</ul>
|
||||
<p>No sale of personal data.</p>
|
||||
|
||||
<h2>5. Your rights</h2>
|
||||
<p>Access, rectification, deletion, and objection under the FADP (within legal limits).
|
||||
Complaint: Swiss Federal Data Protection and Information Commissioner
|
||||
(<strong>FDPIC / EDÖB</strong>).</p>
|
||||
|
||||
<h2>Related</h2>
|
||||
<ul>
|
||||
<li><a href="https://taler.hacktivism.ch/terms">Merchant terms</a></li>
|
||||
<li><a href="https://exchange.hacktivism.ch/privacy">Exchange privacy</a></li>
|
||||
<li><a href="https://bank.hacktivism.ch/intro/privacy.html">Bank privacy</a></li>
|
||||
</ul>
|
||||
<footer class="muted">merchant-pp-swiss-v0 · Swiss FADP (revDSG)</footer>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
chmod -R a+rX "$PRIVACY_DIR"
|
||||
if id taler-merchant-httpd >/dev/null 2>&1; then
|
||||
chown -R taler-merchant-httpd: "$PRIVACY_DIR" 2>/dev/null \
|
||||
|| chown -R taler-merchant-httpd:www-data "$PRIVACY_DIR" 2>/dev/null || true
|
||||
fi
|
||||
echo "ok privacy $ETAG -> $LANG_DIR"
|
||||
ls -la "$LANG_DIR"/${ETAG}.*
|
||||
439
scripts/taler-merchant/landing-stats-merchant.sh
Normal file
439
scripts/taler-merchant/landing-stats-merchant.sh
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
#!/bin/bash
|
||||
# Run INSIDE taler-hacktivism. Writes /var/www/merchant-landing/stats.json
|
||||
#
|
||||
# Current taler-merchant schema:
|
||||
# merchant.merchant_instances → merchant_serial, merchant_id
|
||||
# merchant_instance_<serial>.* → per-instance tables
|
||||
#
|
||||
# IMPORTANT:
|
||||
# - cron uses a minimal PATH. Without /usr/sbin, runuser is missing and
|
||||
# every query used to fail silently → all zeros (and overwrote good data).
|
||||
# - Never write stats.json on failure: leave the previous good file in place.
|
||||
# - Avoid: psql -F$'\t' -v ON_ERROR_STOP=… (if -F loses the tab arg, -v is
|
||||
# eaten as fieldsep and ON_ERROR_STOP becomes the *username*).
|
||||
set -euo pipefail
|
||||
export TZ="${TZ:-Europe/Zurich}"
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
LANDING_DIR="${LANDING_DIR:-/var/www/merchant-landing}"
|
||||
OUT="$LANDING_DIR/stats.json"
|
||||
RUN="$LANDING_DIR/stats-run.json"
|
||||
TMP="${OUT}.tmp.$$"
|
||||
DB="${MERCHANT_DB:-taler-merchant}"
|
||||
ACTIVITY_LIMIT="${ACTIVITY_LIMIT:-12}"
|
||||
ERRLOG="${LANDING_STATS_ERRLOG:-/var/log/landing-stats-merchant.err}"
|
||||
mkdir -p "$LANDING_DIR"
|
||||
|
||||
now_iso() { date +%Y-%m-%dT%H:%M%z | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/'; }
|
||||
now_human() { date +"%Y-%m-%d %H:%M %Z"; }
|
||||
json_str() {
|
||||
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n\r\t' ' ' | sed 's/ */ /g')"
|
||||
}
|
||||
|
||||
# Public run status for the intro page (never wipe stats.json on failure).
|
||||
write_run() {
|
||||
local ok_json="$1" msg="${2:-}"
|
||||
cat >"$RUN" <<EOF
|
||||
{
|
||||
"ok": ${ok_json},
|
||||
"at": $(json_str "$(now_iso)"),
|
||||
"at_human": $(json_str "$(now_human)"),
|
||||
"error": $( [ -n "$msg" ] && json_str "$msg" || echo null )
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
abort() {
|
||||
# Do NOT touch $OUT — previous good stats stay live; site shows run note.
|
||||
write_run false "$*"
|
||||
printf '%s\n' "abort merchant-stats: $*" | tee -a "$ERRLOG" >&2
|
||||
rm -f "$TMP" /tmp/merch_cur_$$.tsv /tmp/merch_act_$$.tsv 2>/dev/null || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
as_postgres() {
|
||||
if command -v runuser >/dev/null 2>&1; then
|
||||
runuser -u postgres -- "$@"
|
||||
elif command -v su >/dev/null 2>&1; then
|
||||
su -s /bin/bash postgres -c "$*"
|
||||
else
|
||||
return 127
|
||||
fi
|
||||
}
|
||||
|
||||
# pipe SQL on stdin — never -f on root-owned temps (postgres cannot read them)
|
||||
psql_pipe() {
|
||||
local fs=$'\t' ec
|
||||
set +e
|
||||
as_postgres psql -d "$DB" -At -F"$fs" 2>>"$ERRLOG"
|
||||
ec=$?
|
||||
set -e
|
||||
return "$ec"
|
||||
}
|
||||
|
||||
# Required single-value query. Empty/fail → abort (no site write).
|
||||
psqlq() {
|
||||
local sql="$1" out ec
|
||||
set +e
|
||||
out=$(as_postgres psql -d "$DB" -At -c "$sql" 2>>"$ERRLOG")
|
||||
ec=$?
|
||||
set -e
|
||||
if [ "$ec" -ne 0 ]; then
|
||||
abort "psql failed (ec=$ec): ${sql:0:120}"
|
||||
fi
|
||||
# strip trailing newlines only
|
||||
printf '%s' "$out" | tr -d '\r'
|
||||
}
|
||||
|
||||
# "64.03" or "64.03000001" → clean CUR:value using decimal string (no binary float)
|
||||
fmt_cur_num() {
|
||||
local c="$1" n="$2"
|
||||
awk -v c="$c" -v n="$n" 'BEGIN{
|
||||
gsub(/ /,"",n)
|
||||
if (n == "" || n+0 == 0 && n !~ /[1-9]/) { printf "%s:0", c; exit }
|
||||
if (n ~ /[eE]/) {
|
||||
x = n+0
|
||||
s = sprintf("%.8f", x)
|
||||
} else {
|
||||
s = n
|
||||
}
|
||||
if (s ~ /\./) {
|
||||
split(s, a, ".")
|
||||
whole = a[1]; frac = substr(a[2] "00000000", 1, 8)
|
||||
extra = substr(a[2], 9, 1)
|
||||
if (extra != "" && extra+0 >= 5) {
|
||||
f = frac+0 + 1
|
||||
if (f >= 100000000) { whole = whole+1; f = f - 100000000 }
|
||||
frac = sprintf("%08d", f)
|
||||
}
|
||||
sub(/0+$/, "", frac)
|
||||
if (frac == "") printf "%s:%s", c, whole
|
||||
else printf "%s:%s.%s", c, whole, frac
|
||||
} else {
|
||||
printf "%s:%s", c, s
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
# --- preflight: tools + DB reachability (fail closed) ---
|
||||
command -v psql >/dev/null 2>&1 || abort "psql not in PATH ($PATH)"
|
||||
command -v runuser >/dev/null 2>&1 || command -v su >/dev/null 2>&1 || abort "neither runuser nor su in PATH"
|
||||
|
||||
PROBE=$(psqlq "SELECT 1;")
|
||||
[ "$PROBE" = "1" ] || abort "postgres not reachable (SELECT 1 → '$PROBE')"
|
||||
|
||||
SERIALS=$(psqlq "SELECT merchant_serial::text FROM merchant.merchant_instances ORDER BY merchant_serial;")
|
||||
# If table exists but we got nothing, still ok (empty install). If table missing, psqlq aborted.
|
||||
|
||||
# Build list of existing instance schemas
|
||||
SCHEMAS=""
|
||||
INSTANCES=0
|
||||
while read -r serial; do
|
||||
[ -z "$serial" ] && continue
|
||||
# only pure integers
|
||||
[[ "$serial" =~ ^[0-9]+$ ]] || abort "bad merchant_serial='$serial'"
|
||||
sch="merchant_instance_${serial}"
|
||||
exists=$(psqlq "SELECT 1 FROM pg_namespace WHERE nspname = '${sch}';")
|
||||
if [ "$exists" != "1" ]; then
|
||||
# schema lag — skip, do not abort (instance row without schema yet)
|
||||
continue
|
||||
fi
|
||||
INSTANCES=$((INSTANCES + 1))
|
||||
SCHEMAS="${SCHEMAS}${SCHEMAS:+ }$sch:$serial"
|
||||
done <<<"$SERIALS"
|
||||
|
||||
# Sanity: if instance schemas exist in PG but SERIALS empty → query path broken
|
||||
NS_COUNT=$(psqlq "SELECT count(*)::text FROM pg_namespace WHERE nspname LIKE 'merchant_instance_%';")
|
||||
NS_COUNT=$(printf '%s' "$NS_COUNT" | tr -d '[:space:]')
|
||||
if [ "${NS_COUNT:-0}" -gt 0 ] && [ "$INSTANCES" -eq 0 ]; then
|
||||
abort "pg has ${NS_COUNT} merchant_instance_* schemas but resolved INSTANCES=0 (query/PATH bug)"
|
||||
fi
|
||||
|
||||
# --- dual-currency aggregation entirely in PostgreSQL (numeric) ---
|
||||
{
|
||||
echo "SELECT"
|
||||
echo " coalesce(nullif(split_part(c.contract_terms->>'amount', ':', 1), ''), '?') AS currency,"
|
||||
echo " count(*)::bigint,"
|
||||
echo " count(*) FILTER (WHERE c.paid)::bigint,"
|
||||
echo " count(*) FILTER (WHERE NOT c.paid)::bigint,"
|
||||
echo " count(*) FILTER (WHERE c.wired)::bigint,"
|
||||
echo " round(coalesce(sum(NULLIF(split_part(c.contract_terms->>'amount', ':', 2), '')::numeric), 0), 8)::text,"
|
||||
echo " round(coalesce(sum(NULLIF(split_part(c.contract_terms->>'amount', ':', 2), '')::numeric) FILTER (WHERE c.paid), 0), 8)::text"
|
||||
echo "FROM ("
|
||||
first=1
|
||||
for item in $SCHEMAS; do
|
||||
sch=${item%%:*}
|
||||
if [ "$first" = 1 ]; then first=0; else echo " UNION ALL "; fi
|
||||
echo "SELECT contract_terms, paid, wired FROM ${sch}.merchant_contract_terms"
|
||||
done
|
||||
if [ "$first" = 1 ]; then
|
||||
echo "SELECT NULL::jsonb AS contract_terms, false AS paid, false AS wired WHERE false"
|
||||
fi
|
||||
echo ") c"
|
||||
echo "WHERE c.contract_terms ? 'amount'"
|
||||
echo "GROUP BY 1 ORDER BY 1;"
|
||||
} | psql_pipe >"/tmp/merch_cur_$$.tsv" || abort "currency aggregate query failed"
|
||||
CUR_TSV=$(cat "/tmp/merch_cur_$$.tsv" 2>/dev/null || true)
|
||||
rm -f "/tmp/merch_cur_$$.tsv"
|
||||
|
||||
TOTAL_CONTRACTS=0; TOTAL_PAID=0; TOTAL_WIRED=0; TOTAL_UNPAID=0
|
||||
declare -A C_CONTRACTS C_PAID C_UNPAID C_WIRED C_AMT C_AMT_PAID
|
||||
|
||||
while IFS=$'\t' read -r cur contracts paid unpaid wired amt amt_paid; do
|
||||
[ -z "${cur:-}" ] && continue
|
||||
TOTAL_CONTRACTS=$((TOTAL_CONTRACTS + contracts))
|
||||
TOTAL_PAID=$((TOTAL_PAID + paid))
|
||||
TOTAL_UNPAID=$((TOTAL_UNPAID + unpaid))
|
||||
TOTAL_WIRED=$((TOTAL_WIRED + wired))
|
||||
C_CONTRACTS[$cur]=$contracts
|
||||
C_PAID[$cur]=$paid
|
||||
C_UNPAID[$cur]=$unpaid
|
||||
C_WIRED[$cur]=$wired
|
||||
C_AMT[$cur]=$amt
|
||||
C_AMT_PAID[$cur]=$amt_paid
|
||||
done <<<"$CUR_TSV"
|
||||
|
||||
# deposits / refunds
|
||||
DEPOSITS=0; REFUNDS=0
|
||||
for item in $SCHEMAS; do
|
||||
sch=${item%%:*}
|
||||
d=$(psqlq "SELECT count(*)::text FROM ${sch}.merchant_deposits;")
|
||||
r=$(psqlq "SELECT count(*)::text FROM ${sch}.merchant_refunds;")
|
||||
d=$(printf '%s' "$d" | tr -d '[:space:]')
|
||||
r=$(printf '%s' "$r" | tr -d '[:space:]')
|
||||
[[ "$d" =~ ^[0-9]+$ ]] || abort "bad deposits count for $sch: '$d'"
|
||||
[[ "$r" =~ ^[0-9]+$ ]] || abort "bad refunds count for $sch: '$r'"
|
||||
DEPOSITS=$((DEPOSITS + d))
|
||||
REFUNDS=$((REFUNDS + r))
|
||||
done
|
||||
|
||||
# --- recent activity: 5 latest events per currency (GOA + CHF), payments + refunds ---
|
||||
ACT_PER_CURRENCY="${ACT_PER_CURRENCY:-5}"
|
||||
|
||||
# Query latest ACT_PER_CURRENCY events for one currency code (payments ∪ refunds).
|
||||
# Writes TSV rows: ts kind order_id amount summary status
|
||||
query_activity_for_currency() {
|
||||
local cur="$1"
|
||||
local out="$2"
|
||||
{
|
||||
echo "SELECT * FROM ("
|
||||
echo "SELECT * FROM ("
|
||||
first=1
|
||||
for item in $SCHEMAS; do
|
||||
sch=${item%%:*}
|
||||
if [ "$first" = 1 ]; then first=0; else echo " UNION ALL "; fi
|
||||
cat <<SQL
|
||||
SELECT creation_time AS ts,
|
||||
'payment'::text AS kind,
|
||||
order_id,
|
||||
coalesce(contract_terms->>'amount','') AS amount,
|
||||
left(translate(coalesce(contract_terms->>'summary',''), E'\t\n\r', ' '), 64) AS summary,
|
||||
CASE WHEN wired THEN 'wired' ELSE 'paid' END AS status
|
||||
FROM ${sch}.merchant_contract_terms
|
||||
WHERE paid
|
||||
AND upper(split_part(coalesce(contract_terms->>'amount',''), ':', 1)) = upper('${cur}')
|
||||
SQL
|
||||
done
|
||||
if [ "$first" = 1 ]; then
|
||||
echo "SELECT 0::bigint AS ts, ''::text AS kind, ''::text AS order_id, ''::text AS amount, ''::text AS summary, ''::text AS status WHERE false"
|
||||
fi
|
||||
echo " UNION ALL "
|
||||
first=1
|
||||
for item in $SCHEMAS; do
|
||||
sch=${item%%:*}
|
||||
if [ "$first" = 1 ]; then first=0; else echo " UNION ALL "; fi
|
||||
cat <<SQL
|
||||
SELECT r.refund_timestamp AS ts,
|
||||
'refund'::text AS kind,
|
||||
ct.order_id,
|
||||
(r.refund_amount).curr || ':' ||
|
||||
CASE WHEN (r.refund_amount).frac = 0
|
||||
THEN (r.refund_amount).val::text
|
||||
ELSE trim(trailing '0' FROM trim(trailing '.' FROM (
|
||||
((r.refund_amount).val + (r.refund_amount).frac::numeric / 100000000)::numeric(32,8)
|
||||
)::text))
|
||||
END AS amount,
|
||||
left(translate(coalesce(r.reason,''), E'\t\n\r', ' '), 64) AS summary,
|
||||
'refunded'::text AS status
|
||||
FROM ${sch}.merchant_refunds r
|
||||
JOIN ${sch}.merchant_contract_terms ct ON ct.order_serial = r.order_serial
|
||||
WHERE upper((r.refund_amount).curr) = upper('${cur}')
|
||||
SQL
|
||||
done
|
||||
if [ "$first" = 1 ]; then
|
||||
echo "SELECT 0::bigint AS ts, ''::text AS kind, ''::text AS order_id, ''::text AS amount, ''::text AS summary, ''::text AS status WHERE false"
|
||||
fi
|
||||
echo ") u ORDER BY ts DESC LIMIT ${ACT_PER_CURRENCY}"
|
||||
echo ") act;"
|
||||
} | psql_pipe >"$out" || abort "activity query failed for $cur"
|
||||
}
|
||||
|
||||
# Build JSON array of activity items from a TSV file
|
||||
activity_tsv_to_json() {
|
||||
local tsv_file="$1"
|
||||
local json="["
|
||||
local af=1
|
||||
local ts kind oid amt sum st sec human_fmt iso
|
||||
while IFS=$'\t' read -r ts kind oid amt sum st; do
|
||||
[ -z "${ts:-}" ] && continue
|
||||
[ "$ts" = "0" ] && [ -z "$kind" ] && continue
|
||||
sec=$(awk -v t="$ts" 'BEGIN{printf "%d", int(t/1000000)}')
|
||||
human_fmt=$(date -d "@${sec}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null || echo "$sec")
|
||||
iso=$(date -d "@${sec}" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' || true)
|
||||
if [ "$af" = 1 ]; then af=0; else json="${json},"; fi
|
||||
json="${json}
|
||||
{
|
||||
\"ts_us\": ${ts:-0},
|
||||
\"ts\": $(json_str "$iso"),
|
||||
\"ts_human\": $(json_str "$human_fmt"),
|
||||
\"kind\": $(json_str "$kind"),
|
||||
\"order_id\": $(json_str "$oid"),
|
||||
\"amount\": $(json_str "$amt"),
|
||||
\"summary\": $(json_str "$sum"),
|
||||
\"status\": $(json_str "$st")
|
||||
}"
|
||||
done <"$tsv_file"
|
||||
json="${json}
|
||||
]"
|
||||
printf '%s' "$json"
|
||||
}
|
||||
|
||||
query_activity_for_currency GOA "/tmp/merch_act_goa_$$.tsv"
|
||||
query_activity_for_currency CHF "/tmp/merch_act_chf_$$.tsv"
|
||||
ACT_GOA_JSON=$(activity_tsv_to_json "/tmp/merch_act_goa_$$.tsv")
|
||||
ACT_CHF_JSON=$(activity_tsv_to_json "/tmp/merch_act_chf_$$.tsv")
|
||||
rm -f "/tmp/merch_act_goa_$$.tsv" "/tmp/merch_act_chf_$$.tsv"
|
||||
|
||||
# Flat recent_activity: GOA then CHF (compat; landing prefers by_currency)
|
||||
ACT_JSON="["
|
||||
af=1
|
||||
for block in "$ACT_GOA_JSON" "$ACT_CHF_JSON"; do
|
||||
# strip outer [ ] and inject if non-empty
|
||||
inner=$(printf '%s' "$block" | sed '1s/^\s*\[//; $s/\]\s*$//')
|
||||
# empty array → skip
|
||||
if printf '%s' "$inner" | grep -q '"kind"'; then
|
||||
if [ "$af" = 1 ]; then af=0; else ACT_JSON="${ACT_JSON},"; fi
|
||||
ACT_JSON="${ACT_JSON}${inner}"
|
||||
fi
|
||||
done
|
||||
ACT_JSON="${ACT_JSON}
|
||||
]"
|
||||
|
||||
ACT_BY_CUR_JSON="[
|
||||
{
|
||||
\"currency\": \"GOA\",
|
||||
\"limit\": ${ACT_PER_CURRENCY},
|
||||
\"items\": ${ACT_GOA_JSON}
|
||||
},
|
||||
{
|
||||
\"currency\": \"CHF\",
|
||||
\"limit\": ${ACT_PER_CURRENCY},
|
||||
\"items\": ${ACT_CHF_JSON}
|
||||
}
|
||||
]"
|
||||
|
||||
# by_currency JSON — GOA then CHF then rest
|
||||
CUR_JSON="["
|
||||
cf=1
|
||||
emit_cur() {
|
||||
local cur="$1"
|
||||
[ -n "${C_CONTRACTS[$cur]+x}" ] || return 1
|
||||
local amt_fmt paid_fmt
|
||||
amt_fmt=$(fmt_cur_num "$cur" "${C_AMT[$cur]}")
|
||||
paid_fmt=$(fmt_cur_num "$cur" "${C_AMT_PAID[$cur]}")
|
||||
if [ "$cf" = 1 ]; then cf=0; else CUR_JSON="${CUR_JSON},"; fi
|
||||
CUR_JSON="${CUR_JSON}
|
||||
{
|
||||
\"currency\": $(json_str "$cur"),
|
||||
\"contracts\": ${C_CONTRACTS[$cur]:-0},
|
||||
\"paid\": ${C_PAID[$cur]:-0},
|
||||
\"unpaid\": ${C_UNPAID[$cur]:-0},
|
||||
\"wired\": ${C_WIRED[$cur]:-0},
|
||||
\"amount_sum\": $(json_str "$amt_fmt"),
|
||||
\"amount_paid_sum\": $(json_str "$paid_fmt")
|
||||
}"
|
||||
}
|
||||
emit_cur GOA || true
|
||||
emit_cur CHF || true
|
||||
for cur in "${!C_CONTRACTS[@]}"; do
|
||||
case "$cur" in GOA|CHF) continue ;; esac
|
||||
emit_cur "$cur" || true
|
||||
done
|
||||
CUR_JSON="${CUR_JSON}
|
||||
]"
|
||||
|
||||
GEN=$(now_iso)
|
||||
HUMAN=$(now_human)
|
||||
|
||||
# Live performance probes (exchange-style)
|
||||
measure_ms() {
|
||||
local url="$1" t
|
||||
t=$(curl -skS -o /dev/null -m 8 -w '%{time_total}' "$url" 2>/dev/null || echo "")
|
||||
[ -z "$t" ] && { echo "null"; return; }
|
||||
awk -v t="$t" 'BEGIN{printf "%d", (t+0)*1000}'
|
||||
}
|
||||
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
|
||||
MERCHANT_PUBLIC_BASE="${MERCHANT_PUBLIC_URL:-https://taler.hacktivism.ch}"
|
||||
MERCHANT_LOCAL="${MERCHANT_LOCAL_URL:-https://127.0.0.1:9010}"
|
||||
CONFIG_MS=$(measure_ms "${MERCHANT_PUBLIC_BASE}/config")
|
||||
CONFIG_HTTP=$(curl -skS -o /dev/null -m 8 -w '%{http_code}' "${MERCHANT_PUBLIC_BASE}/config" 2>/dev/null || echo "000")
|
||||
if [ "$CONFIG_HTTP" != "200" ]; then
|
||||
CONFIG_MS=$(measure_ms "${MERCHANT_LOCAL}/config")
|
||||
CONFIG_HTTP=$(curl -skS -o /dev/null -m 8 -w '%{http_code}' "${MERCHANT_LOCAL}/config" 2>/dev/null || echo "000")
|
||||
fi
|
||||
TERMS_MS=$(measure_ms "${MERCHANT_PUBLIC_BASE}/terms")
|
||||
TERMS_HTTP=$(curl -skS -o /dev/null -m 8 -w '%{http_code}' -H "Accept: text/html" "${MERCHANT_PUBLIC_BASE}/terms" 2>/dev/null || echo "000")
|
||||
WEBUI_MS=$(measure_ms "${MERCHANT_PUBLIC_BASE}/webui/")
|
||||
WEBUI_HTTP=$(curl -skS -o /dev/null -m 8 -w '%{http_code}' "${MERCHANT_PUBLIC_BASE}/webui/" 2>/dev/null || echo "000")
|
||||
LOADAVG=""
|
||||
[ -r /proc/loadavg ] && LOADAVG=$(awk '{print $1","$2","$3}' /proc/loadavg)
|
||||
|
||||
MEM_JSON='"container_rss_human": "—"'
|
||||
MEM_HELPER="${MEM_HELPER:-/usr/local/lib/landing-mem-snapshot.sh}"
|
||||
if [ -f "$MEM_HELPER" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$MEM_HELPER"
|
||||
mem_snapshot_json || true
|
||||
fi
|
||||
|
||||
# Atomic write only after full success
|
||||
cat >"$TMP" <<EOF
|
||||
{
|
||||
"ok": true,
|
||||
"source": "merchant-db",
|
||||
"schema": "merchant.merchant_instances + merchant_instance_<serial>",
|
||||
"dual_currency": true,
|
||||
"currencies_note": "CHF (taler-ops) + GOA (hacktivism)",
|
||||
"timezone": $(json_str "$TZ"),
|
||||
"generated_at": $(json_str "$GEN"),
|
||||
"generated_at_human": $(json_str "$HUMAN"),
|
||||
"instances": ${INSTANCES:-0},
|
||||
"orders": ${TOTAL_CONTRACTS:-0},
|
||||
"paid": ${TOTAL_PAID:-0},
|
||||
"unpaid": ${TOTAL_UNPAID:-0},
|
||||
"wired": ${TOTAL_WIRED:-0},
|
||||
"deposits": ${DEPOSITS:-0},
|
||||
"refunds": ${REFUNDS:-0},
|
||||
"by_currency": $CUR_JSON,
|
||||
"recent_activity_limit_per_currency": ${ACT_PER_CURRENCY},
|
||||
"recent_activity_by_currency": $ACT_BY_CUR_JSON,
|
||||
"recent_activity": $ACT_JSON,
|
||||
"performance": {
|
||||
"config_http": $(json_str "$CONFIG_HTTP"),
|
||||
"config_ms": $(num_or_null "$CONFIG_MS"),
|
||||
"terms_http": $(json_str "$TERMS_HTTP"),
|
||||
"terms_ms": $(num_or_null "$TERMS_MS"),
|
||||
"webui_http": $(json_str "$WEBUI_HTTP"),
|
||||
"webui_ms": $(num_or_null "$WEBUI_MS"),
|
||||
"loadavg": $(json_str "${LOADAVG:-}"),
|
||||
"memory": {
|
||||
${MEM_JSON}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# basic JSON sanity before publish
|
||||
grep -q '"ok": true' "$TMP" || abort "tmp json missing ok:true"
|
||||
mv -f "$TMP" "$OUT"
|
||||
write_run true
|
||||
echo "ok merchant instances=$INSTANCES orders=$TOTAL_CONTRACTS paid=$TOTAL_PAID refunds=$REFUNDS -> $OUT"
|
||||
91
scripts/taler-merchant/setup_credit_facade.sh
Executable file
91
scripts/taler-merchant/setup_credit_facade.sh
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
#!/bin/bash
|
||||
# Configure merchant bank account credit facade for automatic settlement import.
|
||||
#
|
||||
# Root cause (2026-07-09): merchant wirewatch must call the **Taler Revenue API**
|
||||
# (`…/taler-revenue/`), not the exchange wire-gateway (`…/taler-wire-gateway/`).
|
||||
# On this bank, history endpoints accept **Bearer** tokens only (Basic → 401).
|
||||
#
|
||||
# Usage (as root on koopa host):
|
||||
# setup_credit_facade.sh
|
||||
# MERCHANT_INSTANCE=goa-demo-cp4zqk setup_credit_facade.sh
|
||||
# BANK_USER=… BANK_PW_FILE=… MERCHANT_PW_FILE=… setup_credit_facade.sh
|
||||
#
|
||||
# Effects:
|
||||
# - PATCH /instances/$INST/private/accounts/$H_WIRE with credit_facade_*
|
||||
# - long-lived refreshable bank token (1y)
|
||||
# - restarts taler-merchant-wirewatch (once, after facade is set)
|
||||
set -euo pipefail
|
||||
|
||||
INST="${MERCHANT_INSTANCE:-goa-demo-cp4zqk}"
|
||||
BANK_USER="${BANK_USER:-$INST}"
|
||||
MERCHANT_PW_FILE="${MERCHANT_PW_FILE:-/root/merchant-${INST}-password.txt}"
|
||||
BANK_PW_FILE="${BANK_PW_FILE:-/root/bank-${BANK_USER}-password.txt}"
|
||||
MER_URL="${MERCHANT_URL:-https://127.0.0.1:9010}"
|
||||
BANK_URL="${BANK_URL:-http://127.0.0.1:9012}"
|
||||
PUBLIC_BANK="${PUBLIC_BANK_BASE:-https://bank.hacktivism.ch}"
|
||||
FACADE_URL="${CREDIT_FACADE_URL:-${PUBLIC_BANK}/accounts/${BANK_USER}/taler-revenue/}"
|
||||
|
||||
if [ "$(id -un)" != "root" ]; then
|
||||
echo "run as root on koopa host" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -r "$MERCHANT_PW_FILE" ] || [ ! -r "$BANK_PW_FILE" ]; then
|
||||
echo "need readable $MERCHANT_PW_FILE and $BANK_PW_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MPW=$(tr -d '\n' <"$MERCHANT_PW_FILE")
|
||||
BPW=$(tr -d '\n' <"$BANK_PW_FILE")
|
||||
AUTH="Authorization: Bearer secret-token:${MPW}"
|
||||
|
||||
echo "=== bank token for $BANK_USER ==="
|
||||
TOK=$(curl -sS -u "${BANK_USER}:${BPW}" -H 'Content-Type: application/json' \
|
||||
-d '{"scope":"readwrite","refreshable":true,"duration":{"d_us":31536000000000}}' \
|
||||
"${BANK_URL}/accounts/${BANK_USER}/token" \
|
||||
| python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
|
||||
echo "tok_len=${#TOK}"
|
||||
|
||||
echo "=== verify revenue history ==="
|
||||
code=$(curl -sS -m 15 -o /tmp/rev-check.json -w "%{http_code}" \
|
||||
-H "Authorization: Bearer ${TOK}" \
|
||||
"${BANK_URL}/accounts/${BANK_USER}/taler-revenue/history?delta=-3")
|
||||
echo "revenue_history_http=$code"
|
||||
python3 -c 'import json;d=json.load(open("/tmp/rev-check.json"));print("incoming",len(d.get("incoming_transactions")or[]))'
|
||||
[ "$code" = "200" ] || { echo "FAIL revenue history"; exit 1; }
|
||||
|
||||
echo "=== PATCH credit_facade ($FACADE_URL) ==="
|
||||
H_WIRE=$(curl -sk -H "$AUTH" "${MER_URL}/instances/${INST}/private/accounts" \
|
||||
| python3 -c 'import sys,json;print(json.load(sys.stdin)["accounts"][0]["h_wire"])')
|
||||
export FACADE_URL TOK
|
||||
BODY=$(python3 - <<'PY'
|
||||
import json, os
|
||||
print(json.dumps({
|
||||
"credit_facade_url": os.environ["FACADE_URL"],
|
||||
"credit_facade_credentials": {"type": "bearer", "token": os.environ["TOK"]},
|
||||
}))
|
||||
PY
|
||||
)
|
||||
curl -sk -X PATCH -H "$AUTH" -H 'Content-Type: application/json' -d "$BODY" \
|
||||
"${MER_URL}/instances/${INST}/private/accounts/${H_WIRE}" \
|
||||
-w "PATCH_HTTP=%{http_code}\n" -o /dev/null
|
||||
|
||||
echo "=== restart wirewatch in merchant container ==="
|
||||
su - hernani -c 'podman exec taler-hacktivism bash -c "
|
||||
set +e
|
||||
for p in \$(ps -eo pid=,args= | awk \"\\\$0 ~ /\\/usr\\/bin\\/taler-merchant-wirewatch/ {print \\\$1}\"); do
|
||||
kill \$p 2>/dev/null || true
|
||||
done
|
||||
sleep 1
|
||||
runuser -u taler-merchant-httpd -- nohup /usr/bin/taler-merchant-wirewatch \
|
||||
-c /etc/taler-merchant/taler-merchant.conf -L INFO \
|
||||
>>/var/log/taler-merchant/wirewatch.log 2>&1 &
|
||||
sleep 2
|
||||
ps -eo pid,etime,args | grep -E \"[t]aler-merchant-wirewatch\" || echo FAIL_no_wirewatch
|
||||
tail -8 /var/log/taler-merchant/wirewatch.log
|
||||
"'
|
||||
|
||||
echo "=== private/transfers (sample) ==="
|
||||
curl -sk -H "$AUTH" "${MER_URL}/instances/${INST}/private/transfers" \
|
||||
| python3 -c 'import sys,json;d=json.load(sys.stdin);t=d.get("transfers")or[];print("transfers",len(t))'
|
||||
|
||||
echo OK_credit_facade
|
||||
133
scripts/taler-merchant/start_base_services_for_taler.sh
Executable file
133
scripts/taler-merchant/start_base_services_for_taler.sh
Executable 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
|
||||
126
scripts/taler-merchant/start_merchant.sh
Executable file
126
scripts/taler-merchant/start_merchant.sh
Executable 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
|
||||
33
scripts/taler-merchant/stats--merchant-payments.sh
Executable file
33
scripts/taler-merchant/stats--merchant-payments.sh
Executable 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
|
||||
'
|
||||
19
scripts/taler-merchant/taler-hacktivism-email-helper.sh
Executable file
19
scripts/taler-merchant/taler-hacktivism-email-helper.sh
Executable 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"
|
||||
5
scripts/taler-merchant/taler-hacktivism-sms-helper-wrapper.sh
Executable file
5
scripts/taler-merchant/taler-hacktivism-sms-helper-wrapper.sh
Executable 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
|
||||
26
scripts/taler-merchant/taler-merchant-wirewatch-supervise.sh
Executable file
26
scripts/taler-merchant/taler-merchant-wirewatch-supervise.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
#!/bin/bash
|
||||
# Run inside merchant container (as root).
|
||||
# Restarts taler-merchant-wirewatch when it exits.
|
||||
# Without systemd, wirewatch exits on PG config NOTIFY and would stay down.
|
||||
set +e
|
||||
LOG=/var/log/taler-merchant/wirewatch-supervise.log
|
||||
WW_LOG=/var/log/taler-merchant/wirewatch.log
|
||||
CONF="${TALER_MERCHANT_CONFIG:-/etc/taler-merchant/taler-merchant.conf}"
|
||||
mkdir -p /var/log/taler-merchant
|
||||
echo "$(date -u +%FT%TZ) supervise start" >>"$LOG"
|
||||
for p in $(ps -eo pid=,args= | awk '$2=="/usr/bin/taler-merchant-wirewatch"{print $1}'); do
|
||||
kill "$p" 2>/dev/null || true
|
||||
done
|
||||
sleep 1
|
||||
while true; do
|
||||
echo "$(date -u +%FT%TZ) start wirewatch" >>"$LOG"
|
||||
if [ "$(id -un)" = "root" ]; then
|
||||
runuser -u taler-merchant-httpd -- /usr/bin/taler-merchant-wirewatch -c "$CONF" -L INFO >>"$WW_LOG" 2>&1
|
||||
ec=$?
|
||||
else
|
||||
/usr/bin/taler-merchant-wirewatch -c "$CONF" -L INFO >>"$WW_LOG" 2>&1
|
||||
ec=$?
|
||||
fi
|
||||
echo "$(date -u +%FT%TZ) wirewatch exit=$ec; sleep 2" >>"$LOG"
|
||||
sleep 2
|
||||
done
|
||||
100
scripts/taler-monitoring/README.md
Normal file
100
scripts/taler-monitoring/README.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# taler-monitoring
|
||||
|
||||
Report for the **GOA** stack with clear severity tags and **per-area test IDs**:
|
||||
|
||||
| Tag | Meaning |
|
||||
|-----|---------|
|
||||
| `[OK]` | check passed |
|
||||
| `[INFO]` | inside status (container, ports, log noise) |
|
||||
| `[WARN]` | degraded but maybe not fatal |
|
||||
| `[ERROR]` | component problem |
|
||||
| `[BLOCKER]` | **withdraw/pay path cannot complete** because of this |
|
||||
|
||||
IDs: **`www-001`**, **`inside-001`**, **`versions-001`**, **`sanity-001`**, **`server-001`**, **`e2e-001`** …
|
||||
Catalog: **[TESTS.md](./TESTS.md)**.
|
||||
|
||||
```text
|
||||
[OK] www-001 exchange /config https://exchange…/config
|
||||
[BLOCKER] e2e-015 prereq: merchant HTTP 502
|
||||
```
|
||||
|
||||
End of each phase: totals + list of **BLOCKERS** and **ERRORS**.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Local GOA stack (may use SSH for inside/e2e)
|
||||
./taler-monitoring.sh # urls + inside + versions + e2e
|
||||
./taler-monitoring.sh inside # containers on koopa
|
||||
./taler-monitoring.sh versions # deb.taler.net + package versions vs trixie
|
||||
./taler-monitoring.sh sanity
|
||||
./taler-monitoring.sh e2e
|
||||
|
||||
# Other domains — public HTTPS only, never SSH
|
||||
./taler-monitoring.sh -d taler.net
|
||||
./taler-monitoring.sh --domain taler-ops.ch
|
||||
./taler-monitoring.sh -d demo.taler.net urls
|
||||
./taler-monitoring.sh taler.net # bare domain = same as -d
|
||||
```
|
||||
|
||||
| Domain | Bank | Exchange | Merchant | Currency |
|
||||
|--------|------|----------|----------|----------|
|
||||
| `hacktivism.ch` (default) | bank.hacktivism.ch | exchange.hacktivism.ch | taler.hacktivism.ch | GOA |
|
||||
| `taler.net` / `demo.taler.net` | bank.demo.taler.net | exchange.demo.taler.net | backend.demo.taler.net | KUDOS |
|
||||
| `taler-ops.ch` | bank.* (probe) | exchange.taler-ops.ch | backend.* (probe) | CHF |
|
||||
| other | bank.DOMAIN | exchange.DOMAIN | backend/taler/merchant (probe) | any |
|
||||
|
||||
**SSH only for koopa** (`hacktivism.ch` / `-d koopa`).
|
||||
|
||||
Other domains: never SSH. Optional **e2e** aborts cleanly on login/KYC.
|
||||
|
||||
### E2E amounts (variable)
|
||||
|
||||
| | Local (koopa) | Remote |
|
||||
|--|---------------|--------|
|
||||
| **ATM withdraw** | 20 · 50 · 100 · 200 | 10 · 20 · 50 |
|
||||
| **Pay ladder** | 0.01 … 10 | 0.01 … 1 |
|
||||
|
||||
```bash
|
||||
./taler-monitoring.sh e2e
|
||||
./taler-monitoring.sh -d taler.net urls e2e
|
||||
# customize:
|
||||
E2E_WITHDRAW_VALUES="20 50 100" E2E_PAY_VALUES="0.05 1 5" ./taler-monitoring.sh e2e
|
||||
E2E_VARIABLE=0 WITHDRAW_AMT=GOA:50 PAY_AMT=GOA:1 ./taler-monitoring.sh e2e # single fixed
|
||||
# GOA shop catalog (local hacktivism): full list in E2E_SHOP_PRODUCTS; each run
|
||||
# shuffles and pays E2E_SHOP_PICK_N products (default 2).
|
||||
# E2E_SHOP_PRODUCTS lines: id|Product name|GOA:amount
|
||||
# E2E_SHOP_PICK_N=2
|
||||
# (landing QR = taler://pay-template/…/{id}; popup = live taler://pay after POST templates/{id})
|
||||
# remote secrets:
|
||||
# E2E_BANK_ADMIN_PASS=… E2E_MERCHANT_TOKEN=…
|
||||
```
|
||||
|
||||
## Phases
|
||||
|
||||
| Phase | What |
|
||||
|-------|------|
|
||||
| **inside** | SSH koopa: processes, postgres, local /config,/keys, wirewatch, recent log ERRORs |
|
||||
| **versions** | `deb.taler.net` reachable (InRelease/Packages/pool `.deb`); containers can reach it + have apt source; installed Taler packages vs **trixie** |
|
||||
| **sanity** | bank · exchange · merchant sections (public + server) |
|
||||
| **e2e** | account → credit → withdraw → wallet → confirm → order → pay |
|
||||
|
||||
```bash
|
||||
# package suite (default trixie on deb.taler.net)
|
||||
TALER_APT_SUITE=trixie ./taler-monitoring.sh versions
|
||||
TALER_PKG_BEHIND=error ./taler-monitoring.sh versions # any behind = ERROR
|
||||
```
|
||||
|
||||
E2E maps failures to blockers, e.g.:
|
||||
|
||||
- `bank-confirm HTTP 409` → wallet did not select exchange
|
||||
- `no GOA balance` → wirewatch / transfer
|
||||
- `create order failed` → merchant auth/instance
|
||||
- `Alarm clock` during pay → usually missing `handle-uri --yes` or wrong pay URI (must be `…/instances/{inst}/{oid}/?c={token}` from merchant `taler_pay_uri`)
|
||||
- `insufficient balance` → withdraw incomplete
|
||||
|
||||
## Needs
|
||||
|
||||
- SSH `koopa` (inside/sanity server bits)
|
||||
- secrets under `koopa-admin-secrets/...` for e2e
|
||||
- `taler-wallet-cli` for e2e
|
||||
165
scripts/taler-monitoring/TESTS.md
Normal file
165
scripts/taler-monitoring/TESTS.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# taler-monitoring — test IDs by area
|
||||
|
||||
Every check line is numbered **per area** as `AREA-NNN` (zero-padded):
|
||||
|
||||
| Area | Phase script | Meaning |
|
||||
|------|--------------|---------|
|
||||
| **www** | `check_urls.sh` | public HTTPS (outside-in) |
|
||||
| **inside** | `check_inside.sh` | containers / processes on koopa |
|
||||
| **versions** | `check_versions.sh` | deb.taler.net available + packages vs trixie |
|
||||
| **sanity** | `check_sanity.sh` | public + server per component |
|
||||
| **server** | `check_server.sh` | SSH host ports / processes |
|
||||
| **e2e** | `check_e2e.sh` | withdraw + pay cycle |
|
||||
|
||||
Format in output:
|
||||
|
||||
```text
|
||||
[OK] www-001 exchange /config https://exchange…/config
|
||||
[ERROR] e2e-012 bank-auth: admin token failed
|
||||
[BLOCKER] e2e-015 prereq: merchant HTTP 502
|
||||
```
|
||||
|
||||
IDs are assigned **in run order** within the area (`set_area` resets the counter). Optional soft checks still consume a number when they WARN.
|
||||
|
||||
---
|
||||
|
||||
## www — public URLs (`./taler-monitoring.sh urls`)
|
||||
|
||||
| ID | Check |
|
||||
|----|--------|
|
||||
| www-… | exchange `/config`, currency, **alt_unit_names** |
|
||||
| www-… | exchange `/keys` (+ alt_unit_names soft) |
|
||||
| www-… | exchange `/intro/`, `/` (302→intro) |
|
||||
| www-… | **exchange `/terms`** body (not empty / not API error) |
|
||||
| www-… | **exchange `/privacy`** body |
|
||||
| www-… | exchange `/terms/` (200 or redirect) |
|
||||
| www-… | bank `/config`, currency, **alt_unit_names** |
|
||||
| www-… | bank integration / webui / intro / `/` |
|
||||
| www-… | **bank `/terms`** body |
|
||||
| www-… | **bank `/privacy`** (or `/intro/privacy.html` fallback) |
|
||||
| www-… | merchant `/config`, currency, currencies alt_unit_names |
|
||||
| www-… | each merchant `exchanges[]` `/config` alt_unit_names |
|
||||
| www-… | merchant `/intro/`, `/webui/`, `/` |
|
||||
| www-… | **merchant `/terms`** body (dual-currency notice) |
|
||||
| www-… | **merchant `/privacy`** body (must not be `not configured`) |
|
||||
| www-… | merchant `/terms/` redirect |
|
||||
| www-… | **landing exposed links** (bank / merchant / exchange): parse each `/intro/` HTML, probe every own-stack `https://` + root-relative `href`/`src`/`content`, soft-check external stores/docs |
|
||||
| www-… | landing static: `qrcode.min.js`, `og-goa-shop.png`, `qr-logo.png`, shop-pay.js/css |
|
||||
| www-… | cross-links between bank ↔ merchant ↔ exchange intros (local stack) |
|
||||
| www-… | **bank `/intro/demo-withdraw.json`** → `taler://withdraw/HOST:PORT/taler-integration/…` + integration op HTTP 200 |
|
||||
| www-… | bank `/intro/auto-account.json` (earlier) → same withdraw shape, **no payto_uri**, login at `/webui/` |
|
||||
| www-… | **performance** (outside-in): public HTTPS RTT for bank `/config`, `/taler-integration/config`, `/webui/`, `/intro/`, `stats.json`; exchange `/config`, `/keys`, `/intro/`; merchant `/config`, `/webui/`, `/intro/` — report ms; WARN ≥ `PERF_WARN_MS` (default 8000); **ERROR ≥ `PERF_FAIL_MS` (default 20000)** |
|
||||
|
||||
**Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. On local stack, optional content needle (terms/privacy/FADP/GOA…).
|
||||
|
||||
**Performance rule:** Measured from the **monitoring runner** (public URLs via Caddy), not container loopback. HTTP must match expect (usually 200); latency is reported on the OK line. Slow ≥ `PERF_WARN_MS` → WARN only (no ERROR on slowness alone).
|
||||
|
||||
**Landing links rule:** Own-stack (bank/exchange/taler.\* + page host) must be HTTP 200 (or redirect→200). External (App Store, Play, F-Droid, wallet.taler.net, docs/git.taler.net, …) soft WARN if down. Auto-account wallet link must be `taler://withdraw/…:port/taler-integration/…`, never payto.
|
||||
|
||||
**alt_unit_names rule:** wallet codec requires a non-empty map including scale key `"0"`. For multi-currency merchant, also follow every entry in `exchanges[]` and check that exchange’s public `/config`.
|
||||
|
||||
(IDs after a failed early check may shift if later soft checks are skipped when body missing — numbering follows **executed** checks.)
|
||||
|
||||
---
|
||||
|
||||
## inside — koopa SSH (`./taler-monitoring.sh inside`)
|
||||
|
||||
| ID | Check (typical order) |
|
||||
|----|------------------------|
|
||||
| inside-001 | ssh koopa |
|
||||
| inside-002+ | per-component emit: container, ports, libeufin/httpd, postgres, local `/config`/`/keys`, wirewatch, DNS pin, caddy |
|
||||
|
||||
Remote lines `E|comp|LEVEL|key|detail` each become one numbered result.
|
||||
|
||||
---
|
||||
|
||||
## sanity — bank · exchange · merchant (`./taler-monitoring.sh sanity`)
|
||||
|
||||
| ID | Section |
|
||||
|----|---------|
|
||||
| sanity-001… | bank public + server |
|
||||
| sanity-… | exchange public + server |
|
||||
| sanity-… | merchant public + server |
|
||||
|
||||
Sequential through the whole script (one `set_area sanity`).
|
||||
|
||||
---
|
||||
|
||||
## versions — packages vs deb.taler.net (`./taler-monitoring.sh versions`)
|
||||
|
||||
### Outside (runner / public network — no SSH)
|
||||
|
||||
| ID (order) | Check |
|
||||
|------------|--------|
|
||||
| versions-… | DNS `deb.taler.net` |
|
||||
| versions-… | HTTPS portal + apt base URL |
|
||||
| versions-… | suite `InRelease` / `Release` |
|
||||
| versions-… | suite `Packages` + `Packages.gz` |
|
||||
| versions-… | sample pool `.deb` fetchable (Range 200/206) |
|
||||
| versions-… | suite offers `taler-exchange`, `taler-merchant`, `libeufin-bank` |
|
||||
| versions-… | optional `trixie-testing` Packages |
|
||||
| versions-… | TLS verify (soft) |
|
||||
|
||||
### Inside (SSH koopa containers)
|
||||
|
||||
| ID | Check |
|
||||
|----|--------|
|
||||
| versions-… | ssh koopa |
|
||||
| versions-… | each container → `InRelease` (pasta can reach apt repo) |
|
||||
| versions-… | each container lists `deb.taler.net` in apt sources |
|
||||
| versions-… | each installed `taler*` / `libeufin*` / `libtaler*` / `libdonau*` vs suite version |
|
||||
| versions-… | core packages installed (`taler-exchange`, `libeufin-bank`, `taler-merchant`) |
|
||||
|
||||
Outside always runs. Inside skipped with `SKIP_SSH=1` (still reports outside results).
|
||||
|
||||
Compare rules:
|
||||
|
||||
- **match** suite → OK
|
||||
- **ahead** of suite (often testing/dev) → INFO
|
||||
- **behind** suite → ERROR for core packages, WARN otherwise (`TALER_PKG_BEHIND=error` forces ERROR)
|
||||
|
||||
Default suite: **trixie** (`TALER_APT_SUITE`, `TALER_APT_BASE=https://deb.taler.net/apt/debian`).
|
||||
|
||||
Without SSH (`SKIP_SSH=1` or remote domain): still runs outside-in repo checks; skips container install compare.
|
||||
|
||||
---
|
||||
|
||||
## server — SSH ports (`./taler-monitoring.sh server`)
|
||||
|
||||
| ID | Check |
|
||||
|----|--------|
|
||||
| server-001 | ssh |
|
||||
| server-002+ | containers, local pasta ports, processes, caddy |
|
||||
|
||||
---
|
||||
|
||||
## e2e — payment path (`./taler-monitoring.sh e2e`)
|
||||
|
||||
| ID | Step (approx.) |
|
||||
|----|----------------|
|
||||
| e2e-001 | budget info |
|
||||
| e2e-002 | wallet-cli present |
|
||||
| e2e-003 | mode / currency info |
|
||||
| e2e-004… | secrets, reachability gates |
|
||||
| e2e-… | account, credit, withdraw, confirm, coins, order, pay ladder |
|
||||
| e2e-… | **GOA shop products** — full catalog list; **random pick of 2** (override `E2E_SHOP_PICK_N`) |
|
||||
| e2e-… | balances, dig on failure |
|
||||
|
||||
Shop product pays use instance `goa-shop` (default) and catalog `E2E_SHOP_PRODUCTS`
|
||||
(`id|Product name|amount` lines). Each e2e run **shuffles** the catalog and pays
|
||||
only `E2E_SHOP_PICK_N` products (**default 2**). Flow matches the landing popup
|
||||
(public POST `/templates/{id}`, not private orders). Report labels use product name.
|
||||
|
||||
Blockers keep the same ID prefix: `[BLOCKER] e2e-0NN step: message`.
|
||||
|
||||
---
|
||||
|
||||
## Run one area
|
||||
|
||||
```bash
|
||||
./taler-monitoring.sh urls # www only
|
||||
./taler-monitoring.sh inside # inside only
|
||||
./taler-monitoring.sh versions # deb.taler.net + package drift
|
||||
./taler-monitoring.sh e2e # e2e only
|
||||
./taler-monitoring.sh -d taler.net urls
|
||||
```
|
||||
1220
scripts/taler-monitoring/check_e2e.sh
Executable file
1220
scripts/taler-monitoring/check_e2e.sh
Executable file
File diff suppressed because it is too large
Load diff
141
scripts/taler-monitoring/check_inside.sh
Executable file
141
scripts/taler-monitoring/check_inside.sh
Executable file
|
|
@ -0,0 +1,141 @@
|
|||
#!/usr/bin/env bash
|
||||
# Inside status for bank / exchange / merchant. Hard-capped SSH — never hang forever.
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
# Area inside-### — container / process state on koopa (SSH)
|
||||
set_area inside
|
||||
section "inside · collect from koopa"
|
||||
|
||||
if [ "${SKIP_SSH}" = "1" ]; then
|
||||
warn "ssh" "SKIP_SSH=1 — skipped"
|
||||
summary
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! koopa_ssh_ok; then
|
||||
err "ssh" "cannot reach ${KOOPA_SSH} in ${SSH_CONNECT_TIMEOUT}s — set SKIP_SSH=1 to skip inside"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
ok "ssh ${KOOPA_SSH}"
|
||||
|
||||
# One short remote script (≤ SSH_CMD_TIMEOUT). Every slow step is local curl -m 3 or quick pgrep.
|
||||
RAW=$(
|
||||
koopa_ssh_bash "${SSH_CMD_TIMEOUT}" <<'REMOTE' || true
|
||||
set +e
|
||||
emit() { printf 'E|%s|%s|%s|%s\n' "$1" "$2" "$3" "$(printf '%s' "${4:-}" | tr '\n\r' ' ' | head -c 200)"; }
|
||||
# quick curl
|
||||
hc() { curl -skS -m 3 -o /tmp/mb -w '%{http_code}' "$1" 2>/dev/null || echo 000; }
|
||||
# quick process check inside container (pgrep only)
|
||||
hasp() { podman exec "$1" pgrep -f "$2" >/dev/null 2>&1; }
|
||||
|
||||
BANK=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'hacktivism-bank|taler-bank' | head -1)
|
||||
[ -z "$BANK" ] && BANK=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -i bank | head -1)
|
||||
EX=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -i exchange | head -1)
|
||||
MER=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -E '^taler-hacktivism$' | head -1)
|
||||
[ -z "$MER" ] && MER=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'merchant|hacktivism' | grep -viE 'bank|exchange' | head -1)
|
||||
|
||||
# Domain resolve inside container (wirewatch needs bank.hacktivism.ch → real IP)
|
||||
# emit: comp LEVEL dns "host → ip" or fail
|
||||
check_dns() {
|
||||
local comp="$1" ctr="$2" host="$3"
|
||||
local line ip code
|
||||
# Prefer IPv4 (wirewatch/libcurl often happier; avoid dead AAAA)
|
||||
line=$(podman exec "$ctr" getent ahostsv4 "$host" 2>/dev/null | head -1)
|
||||
[ -z "$line" ] && line=$(podman exec "$ctr" getent hosts "$host" 2>/dev/null | head -1)
|
||||
ip=$(echo "$line" | awk '{print $1}')
|
||||
if [ -z "$ip" ]; then
|
||||
emit "$comp" ERROR "dns $host" "no resolve — run pin-container-hosts.sh"
|
||||
return 1
|
||||
fi
|
||||
# 127.0.0.1 is almost always wrong for public bank/exchange from inside pasta
|
||||
if [ "$ip" = "127.0.0.1" ] || [ "$ip" = "::1" ]; then
|
||||
emit "$comp" ERROR "dns $host" "$ip (loopback — wirewatch will fail)"
|
||||
return 1
|
||||
fi
|
||||
code=$(podman exec "$ctr" curl -skS -m 3 -o /dev/null -w '%{http_code}' "https://${host}/config" 2>/dev/null || echo 000)
|
||||
if [ "$code" = "200" ]; then
|
||||
emit "$comp" OK "dns $host" "→ $ip /config=$code"
|
||||
else
|
||||
emit "$comp" WARN "dns $host" "→ $ip /config=$code"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -z "$BANK" ]; then emit bank ERROR container "not running"
|
||||
else
|
||||
emit bank INFO container "$(podman ps --filter name=$BANK --format '{{.Names}} {{.Status}}' | head -1)"
|
||||
emit bank INFO ports "$(podman ps --filter name=$BANK --format '{{.Ports}}' | head -1)"
|
||||
hasp "$BANK" 'MainKt serve|libeufin-bank serve' && emit bank OK libeufin "running" || emit bank ERROR libeufin "not running — API/withdraw dead"
|
||||
podman exec "$BANK" pg_isready -q 2>/dev/null && emit bank OK postgres "ready" || emit bank ERROR postgres "not ready"
|
||||
c=$(hc http://127.0.0.1:9012/config)
|
||||
[ "$c" = "200" ] && emit bank OK "local /config" "HTTP $c" || emit bank ERROR "local /config" "HTTP $c"
|
||||
c=$(hc http://127.0.0.1:9012/taler-integration/config)
|
||||
[ "$c" = "200" ] && emit bank OK "local integration" "HTTP $c" || emit bank ERROR "local integration" "HTTP $c"
|
||||
hasp "$BANK" 'nginx' && emit bank OK nginx ":9013" || emit bank WARN nginx "not running"
|
||||
check_dns bank "$BANK" bank.hacktivism.ch || true
|
||||
check_dns bank "$BANK" exchange.hacktivism.ch || true
|
||||
fi
|
||||
|
||||
if [ -z "$EX" ]; then emit exchange ERROR container "not running"
|
||||
else
|
||||
emit exchange INFO container "$(podman ps --filter name=$EX --format '{{.Names}} {{.Status}}' | head -1)"
|
||||
c=$(hc http://127.0.0.1:9011/config)
|
||||
[ "$c" = "200" ] && emit exchange OK "local /config" "HTTP $c" || emit exchange ERROR "local /config" "HTTP $c"
|
||||
c=$(curl -sS -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:9011/keys 2>/dev/null || echo 000)
|
||||
[ "$c" = "200" ] && emit exchange OK "local /keys" "HTTP $c" || emit exchange ERROR "local /keys" "HTTP $c"
|
||||
hasp "$EX" 'taler-exchange-httpd' && emit exchange OK httpd "running" || emit exchange ERROR httpd "not running"
|
||||
hasp "$EX" 'taler-exchange-wirewatch' && emit exchange OK wirewatch "running" || emit exchange ERROR wirewatch "not running — withdraw stuck after bank confirm"
|
||||
hasp "$EX" 'taler-exchange-aggregator' && emit exchange OK aggregator "running" || emit exchange WARN aggregator "not running"
|
||||
hasp "$EX" 'taler-exchange-transfer' && emit exchange OK transfer "running" || emit exchange WARN transfer "not running"
|
||||
# critical for wire gateway
|
||||
check_dns exchange "$EX" bank.hacktivism.ch || true
|
||||
check_dns exchange "$EX" exchange.hacktivism.ch || true
|
||||
check_dns exchange "$EX" taler.hacktivism.ch || true
|
||||
fi
|
||||
|
||||
if [ -z "$MER" ]; then emit merchant ERROR container "not running"
|
||||
else
|
||||
emit merchant INFO container "$(podman ps --filter name=$MER --format '{{.Names}} {{.Status}}' | head -1)"
|
||||
c=$(hc https://127.0.0.1:9010/config)
|
||||
[ "$c" = "200" ] && emit merchant OK "local /config" "HTTP $c" || emit merchant ERROR "local /config" "HTTP $c"
|
||||
hasp "$MER" 'taler-merchant-httpd' && emit merchant OK httpd "running" || emit merchant ERROR httpd "not running"
|
||||
hasp "$MER" 'taler-merchant-wirewatch' && emit merchant OK wirewatch "running" || emit merchant WARN wirewatch "not running"
|
||||
hasp "$MER" 'taler-merchant-depositcheck' && emit merchant OK depositcheck "running" || emit merchant WARN depositcheck "not running"
|
||||
check_dns merchant "$MER" bank.hacktivism.ch || true
|
||||
check_dns merchant "$MER" exchange.hacktivism.ch || true
|
||||
check_dns merchant "$MER" taler.hacktivism.ch || true
|
||||
fi
|
||||
|
||||
if systemctl is-active caddy >/dev/null 2>&1 || pgrep -x caddy >/dev/null 2>&1; then
|
||||
emit caddy OK process "active"
|
||||
else
|
||||
emit caddy ERROR process "not active"
|
||||
fi
|
||||
echo DONE
|
||||
REMOTE
|
||||
)
|
||||
|
||||
if [ -z "$RAW" ] || ! echo "$RAW" | grep -q '^E|'; then
|
||||
err "ssh" "remote timed out or empty (cap ${SSH_CMD_TIMEOUT}s)"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
E\|*)
|
||||
IFS='|' read -r _ comp level key detail <<<"$line"
|
||||
case "$level" in
|
||||
OK) ok "[$comp] $key${detail:+ ($detail)}" ;;
|
||||
ERROR) err "$comp" "$key" "$detail" ;;
|
||||
WARN) warn "[$comp] $key" "$detail" ;;
|
||||
INFO) info "[$comp] $key" "$detail" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done <<<"$RAW"
|
||||
|
||||
summary
|
||||
281
scripts/taler-monitoring/check_sanity.sh
Executable file
281
scripts/taler-monitoring/check_sanity.sh
Executable file
|
|
@ -0,0 +1,281 @@
|
|||
#!/usr/bin/env bash
|
||||
# Sanity checks for bank · exchange · merchant (public + server-side).
|
||||
# Sections are independent; continues after failures.
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
expect_code() {
|
||||
local label="$1" want="$2" url="$3"
|
||||
local code
|
||||
code=$(http_code "$url")
|
||||
case ",$want," in
|
||||
*",$code,"*) ok "$label" ;;
|
||||
*) fail "$label" "HTTP $code (want $want) $url" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
json_currency() {
|
||||
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("currency") or "")' "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Area sanity-### — public + optional server-side per component
|
||||
set_area sanity
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "sanity · bank"
|
||||
# ---------------------------------------------------------------------------
|
||||
expect_code "bank public /config" 200 "$BANK_PUBLIC/config"
|
||||
expect_code "bank public /taler-integration/config" 200 "$BANK_PUBLIC/taler-integration/config"
|
||||
expect_code "bank public /webui/" 200 "$BANK_PUBLIC/webui/"
|
||||
|
||||
code=$(http_body "$BANK_PUBLIC/config" "$tmp/bank-config.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
cur=$(json_currency "$tmp/bank-config.json")
|
||||
[ "$cur" = "GOA" ] && ok "bank currency GOA" || fail "bank currency" "got ${cur:-?}"
|
||||
# wire type / name if present
|
||||
python3 - "$tmp/bank-config.json" <<'PY' 2>/dev/null && ok "bank config JSON object" || fail "bank config JSON"
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
sys.exit(0 if isinstance(d, dict) and d.get("currency") else 1)
|
||||
PY
|
||||
if json_has_alt_unit_names "$tmp/bank-config.json" >/tmp/alt-bank-s.$$ 2>&1; then
|
||||
ok "bank /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-bank-s.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "bank /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-bank-s.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-bank-s.$$
|
||||
fi
|
||||
|
||||
# server-side bank
|
||||
if koopa_ssh_ok; then
|
||||
BOUT=$(koopa_ssh_bash 40 <<'REMOTE' || true
|
||||
set +e
|
||||
BANK=$(podman ps --format '{{.Names}}' | grep -iE 'hacktivism-bank|taler-bank' | head -1)
|
||||
[ -z "$BANK" ] && BANK=$(podman ps --format '{{.Names}}' | grep -i bank | head -1)
|
||||
echo "CTR=$BANK"
|
||||
if [ -z "$BANK" ]; then echo "NOCTR"; exit 0; fi
|
||||
code=$(curl -sS -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:9012/config 2>/dev/null || echo 000)
|
||||
echo "LOCAL_CONFIG=$code"
|
||||
code2=$(curl -sS -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:9012/taler-integration/config 2>/dev/null || echo 000)
|
||||
echo "LOCAL_INT=$code2"
|
||||
if podman exec "$BANK" bash -c 'pgrep -f "MainKt serve|libeufin-bank serve" >/dev/null' 2>/dev/null; then
|
||||
echo "LIBEUFIN=1"
|
||||
else
|
||||
echo "LIBEUFIN=0"
|
||||
fi
|
||||
if podman exec "$BANK" bash -c 'pg_isready -q' 2>/dev/null; then
|
||||
echo "PG=1"
|
||||
else
|
||||
echo "PG=0"
|
||||
fi
|
||||
# in-container health if present
|
||||
if podman exec "$BANK" test -x /usr/local/bin/check_bank-health.sh 2>/dev/null; then
|
||||
podman exec "$BANK" /usr/local/bin/check_bank-health.sh 2>&1 | sed 's/^/HEALTH /' | tail -20
|
||||
echo "HEALTH_EC=${PIPESTATUS[0]}"
|
||||
fi
|
||||
REMOTE
|
||||
)
|
||||
echo "$BOUT" | grep -q '^CTR=.\+' && ok "bank container $(echo "$BOUT" | sed -n 's/^CTR=//p' | head -1)" || fail "bank container" "not found"
|
||||
echo "$BOUT" | grep -q 'LOCAL_CONFIG=200' && ok "bank local :9012/config" || fail "bank local :9012/config"
|
||||
echo "$BOUT" | grep -q 'LOCAL_INT=200' && ok "bank local :9012/taler-integration/config" || fail "bank local integration"
|
||||
echo "$BOUT" | grep -q 'LIBEUFIN=1' && ok "bank libeufin-bank process" || fail "bank libeufin-bank process"
|
||||
echo "$BOUT" | grep -q 'PG=1' && ok "bank postgres ready" || warn "bank postgres" "pg_isready failed"
|
||||
if echo "$BOUT" | grep -q 'HEALTH '; then
|
||||
if echo "$BOUT" | grep -qE 'HEALTH_EC=0|ALL CRITICAL CHECKS PASSED'; then
|
||||
ok "bank check_bank-health.sh"
|
||||
else
|
||||
# health script may false-fail process grep; warn not fail if local config ok
|
||||
warn "bank check_bank-health.sh" "non-zero or incomplete"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
warn "bank server-side" "ssh ${KOOPA_SSH} unavailable"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "sanity · exchange"
|
||||
# ---------------------------------------------------------------------------
|
||||
expect_code "exchange public /config" 200 "$EXCHANGE_PUBLIC/config"
|
||||
expect_code "exchange public /keys" 200 "$EXCHANGE_PUBLIC/keys"
|
||||
expect_code "exchange public /terms" 200 "$EXCHANGE_PUBLIC/terms"
|
||||
|
||||
code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ex-config.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
cur=$(json_currency "$tmp/ex-config.json")
|
||||
[ "$cur" = "GOA" ] && ok "exchange currency GOA" || fail "exchange currency" "got ${cur:-?}"
|
||||
if json_has_alt_unit_names "$tmp/ex-config.json" "GOA" >/tmp/alt-ex-s.$$ 2>&1; then
|
||||
ok "exchange /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-ex-s.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "exchange /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-ex-s.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-ex-s.$$
|
||||
fi
|
||||
|
||||
code=$(http_body "$EXCHANGE_PUBLIC/keys" "$tmp/ex-keys.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
python3 - "$tmp/ex-keys.json" <<'PY'
|
||||
import json,sys,time
|
||||
d=json.load(open(sys.argv[1]))
|
||||
sk=d.get("signkeys") or []
|
||||
acc=d.get("accounts") or []
|
||||
den=d.get("denominations") or []
|
||||
# count denoms roughly
|
||||
n=0
|
||||
for g in den:
|
||||
if isinstance(g, dict):
|
||||
n += len(g.get("denoms") or [])
|
||||
print(f"signkeys={len(sk)} accounts={len(acc)} denom_groups={len(den)} denoms~={n}")
|
||||
sys.exit(0 if sk and (acc or n) else 1)
|
||||
PY
|
||||
ec=$?
|
||||
detail=$(python3 - "$tmp/ex-keys.json" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
sk=d.get("signkeys") or []
|
||||
acc=d.get("accounts") or []
|
||||
den=d.get("denominations") or []
|
||||
n=sum(len(g.get("denoms") or []) for g in den if isinstance(g, dict))
|
||||
print(f"signkeys={len(sk)} accounts={len(acc)} denoms~={n}")
|
||||
PY
|
||||
)
|
||||
[ "$ec" -eq 0 ] && ok "exchange /keys usable ($detail)" || fail "exchange /keys usable" "$detail"
|
||||
fi
|
||||
|
||||
# /wire if exposed
|
||||
wcode=$(http_code "$EXCHANGE_PUBLIC/wire")
|
||||
if [ "$wcode" = "200" ]; then
|
||||
ok "exchange public /wire"
|
||||
else
|
||||
warn "exchange public /wire" "HTTP $wcode (accounts may only be in /keys)"
|
||||
fi
|
||||
|
||||
if koopa_ssh_ok; then
|
||||
EOUT=$(koopa_ssh_bash 40 <<'REMOTE' || true
|
||||
set +e
|
||||
EX=$(podman ps --format '{{.Names}}' | grep -i exchange | head -1)
|
||||
echo "CTR=$EX"
|
||||
code=$(curl -sS -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:9011/config 2>/dev/null || echo 000)
|
||||
echo "LOCAL_CONFIG=$code"
|
||||
codek=$(curl -sS -m 8 -o /dev/null -w '%{http_code}' http://127.0.0.1:9011/keys 2>/dev/null || echo 000)
|
||||
echo "LOCAL_KEYS=$codek"
|
||||
if [ -n "$EX" ]; then
|
||||
if podman exec "$EX" bash -c 'pgrep -f taler-exchange-httpd >/dev/null' 2>/dev/null; then
|
||||
echo "HTTPD=1"
|
||||
else
|
||||
echo "HTTPD=0"
|
||||
fi
|
||||
for p in taler-exchange-secmod-rsa taler-exchange-secmod-eddsa taler-exchange-wirewatch taler-exchange-aggregator; do
|
||||
if podman exec "$EX" bash -c "pgrep -f $p >/dev/null" 2>/dev/null; then
|
||||
echo "PROC_$p=1"
|
||||
else
|
||||
echo "PROC_$p=0"
|
||||
fi
|
||||
done
|
||||
if podman exec "$EX" test -x /usr/local/bin/check_exchange-health.sh 2>/dev/null; then
|
||||
SKIP_ENSURE=1 podman exec -e SKIP_ENSURE=1 "$EX" /usr/local/bin/check_exchange-health.sh 2>&1 | sed 's/^/HEALTH /' | tail -25
|
||||
fi
|
||||
fi
|
||||
REMOTE
|
||||
)
|
||||
echo "$EOUT" | grep -q '^CTR=.\+' && ok "exchange container $(echo "$EOUT" | sed -n 's/^CTR=//p' | head -1)" || fail "exchange container"
|
||||
echo "$EOUT" | grep -q 'LOCAL_CONFIG=200' && ok "exchange local :9011/config" || fail "exchange local :9011/config"
|
||||
echo "$EOUT" | grep -q 'LOCAL_KEYS=200' && ok "exchange local :9011/keys" || fail "exchange local :9011/keys"
|
||||
echo "$EOUT" | grep -q 'HTTPD=1' && ok "exchange-httpd process" || warn "exchange-httpd process" "not detected"
|
||||
echo "$EOUT" | grep -q 'PROC_taler-exchange-wirewatch=1' && ok "exchange wirewatch" || warn "exchange wirewatch" "not running"
|
||||
echo "$EOUT" | grep -q 'PROC_taler-exchange-aggregator=1' && ok "exchange aggregator" || warn "exchange aggregator" "not running"
|
||||
else
|
||||
warn "exchange server-side" "ssh unavailable"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "sanity · merchant"
|
||||
# ---------------------------------------------------------------------------
|
||||
expect_code "merchant public /config" 200 "$MERCHANT_PUBLIC/config"
|
||||
expect_code "merchant public /webui/" 200 "$MERCHANT_PUBLIC/webui/"
|
||||
|
||||
code=$(http_body "$MERCHANT_PUBLIC/config" "$tmp/mer-config.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
if python3 - "$tmp/mer-config.json" "$EXCHANGE_PUBLIC" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
want=sys.argv[2].rstrip("/")
|
||||
curs=list((d.get("currencies") or {}).keys())
|
||||
ex=d.get("exchanges") or []
|
||||
urls=[]
|
||||
for e in ex:
|
||||
if isinstance(e, dict):
|
||||
u=e.get("base_url") or e.get("url") or e.get("exchange_base_url") or ""
|
||||
if u: urls.append(u.rstrip("/"))
|
||||
elif isinstance(e, str):
|
||||
urls.append(e.rstrip("/"))
|
||||
ok_goa = "GOA" in curs or any((e.get("currency") if isinstance(e, dict) else None)=="GOA" for e in ex)
|
||||
ok_ex = any(want in u or "exchange.hacktivism.ch" in u for u in urls)
|
||||
print("currencies", curs)
|
||||
print("exchanges", urls[:5])
|
||||
sys.exit(0 if ok_goa and ok_ex else 1)
|
||||
PY
|
||||
then
|
||||
ok "merchant config GOA + exchange.hacktivism.ch"
|
||||
else
|
||||
fail "merchant config GOA + exchange" "see currencies/exchanges"
|
||||
fi
|
||||
if json_has_alt_unit_names "$tmp/mer-config.json" >/tmp/alt-mer-s.$$ 2>&1; then
|
||||
ok "merchant currencies alt_unit_names" "$(tr '\n' '; ' </tmp/alt-mer-s.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "merchant currencies alt_unit_names" "$(tr '\n' '; ' </tmp/alt-mer-s.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-mer-s.$$
|
||||
# Each exchange listed on merchant /config must publish alt_unit_names on its own /config
|
||||
check_merchant_listed_exchanges_alt_units "$tmp/mer-config.json"
|
||||
fi
|
||||
|
||||
# demo instance reachable?
|
||||
INST="${MERCHANT_INSTANCE}"
|
||||
icode=$(http_code "$MERCHANT_PUBLIC/instances/${INST}/config")
|
||||
if [ "$icode" = "200" ]; then
|
||||
ok "merchant instance ${INST} /config"
|
||||
else
|
||||
# some deployments use private only
|
||||
warn "merchant instance ${INST} /config" "HTTP $icode"
|
||||
fi
|
||||
|
||||
if koopa_ssh_ok; then
|
||||
MOUT=$(koopa_ssh_bash 40 <<'REMOTE' || true
|
||||
set +e
|
||||
MER=$(podman ps --format '{{.Names}}' | grep -E '^taler-hacktivism$' | head -1)
|
||||
[ -z "$MER" ] && MER=$(podman ps --format '{{.Names}}' | grep -iE 'merchant|hacktivism' | grep -viE 'bank|exchange' | head -1)
|
||||
echo "CTR=$MER"
|
||||
code=$(curl -skS -m 5 -o /dev/null -w '%{http_code}' https://127.0.0.1:9010/config 2>/dev/null || echo 000)
|
||||
echo "LOCAL_CONFIG=$code"
|
||||
if [ -n "$MER" ]; then
|
||||
if podman exec "$MER" bash -c 'pgrep -f taler-merchant-httpd >/dev/null' 2>/dev/null; then
|
||||
echo "HTTPD=1"
|
||||
else
|
||||
echo "HTTPD=0"
|
||||
fi
|
||||
if podman exec "$MER" test -x /usr/local/bin/check_merchant-health.sh 2>/dev/null; then
|
||||
SKIP_ENSURE=1 podman exec -e SKIP_ENSURE=1 "$MER" /usr/local/bin/check_merchant-health.sh 2>&1 | sed 's/^/HEALTH /' | tail -30
|
||||
fi
|
||||
fi
|
||||
REMOTE
|
||||
)
|
||||
echo "$MOUT" | grep -q '^CTR=.\+' && ok "merchant container $(echo "$MOUT" | sed -n 's/^CTR=//p' | head -1)" || fail "merchant container"
|
||||
echo "$MOUT" | grep -q 'LOCAL_CONFIG=200' && ok "merchant local :9010/config" || fail "merchant local :9010/config"
|
||||
echo "$MOUT" | grep -q 'HTTPD=1' && ok "merchant-httpd process" || warn "merchant-httpd process" "not detected"
|
||||
if echo "$MOUT" | grep -q 'HEALTH '; then
|
||||
if echo "$MOUT" | grep -qiE 'ALL CRITICAL|HEALTH_EC=0|\[OK\]'; then
|
||||
ok "merchant check_merchant-health.sh (sample OK)"
|
||||
else
|
||||
warn "merchant check_merchant-health.sh" "see remote output"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
warn "merchant server-side" "ssh unavailable"
|
||||
fi
|
||||
|
||||
summary
|
||||
128
scripts/taler-monitoring/check_server.sh
Executable file
128
scripts/taler-monitoring/check_server.sh
Executable file
|
|
@ -0,0 +1,128 @@
|
|||
#!/usr/bin/env bash
|
||||
# Server-side component checks on koopa (via SSH).
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
# Area server-### — host/container ports via SSH
|
||||
set_area server
|
||||
section "server · ssh ${KOOPA_SSH}"
|
||||
|
||||
if ! koopa_ssh_ok; then
|
||||
fail "ssh ${KOOPA_SSH}" "unreachable within ${SSH_CONNECT_TIMEOUT}s (SKIP_SSH=1 to skip)"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
ok "ssh ${KOOPA_SSH}"
|
||||
|
||||
# Run a remote script; collect structured lines
|
||||
REMOTE=$(koopa_ssh_bash "${SSH_CMD_TIMEOUT}" <<'REMOTE'
|
||||
set +e
|
||||
report() { printf 'R|%s|%s|%s\n' "$1" "$2" "$3"; }
|
||||
|
||||
# containers
|
||||
for name in taler-hacktivism-bank taler-hacktivism-exchange-ansible taler-hacktivism; do
|
||||
if podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$name"; then
|
||||
st=$(podman ps --filter "name=^${name}$" --format '{{.Status}}' | head -1)
|
||||
report OK "container $name" "$st"
|
||||
else
|
||||
# soft match
|
||||
hit=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -E "$name|bank|exchange|hacktivism" | head -3 | tr '\n' ' ')
|
||||
if podman ps --format '{{.Names}}' 2>/dev/null | grep -q bank && [ "$name" = taler-hacktivism-bank ]; then
|
||||
bn=$(podman ps --format '{{.Names}}' | grep -i bank | head -1)
|
||||
report OK "container bank ($bn)" "$(podman ps --filter name="$bn" --format '{{.Status}}' | head -1)"
|
||||
elif podman ps --format '{{.Names}}' 2>/dev/null | grep -qi exchange && echo "$name" | grep -qi exchange; then
|
||||
en=$(podman ps --format '{{.Names}}' | grep -i exchange | head -1)
|
||||
report OK "container exchange ($en)" "$(podman ps --filter name="$en" --format '{{.Status}}' | head -1)"
|
||||
elif podman ps --format '{{.Names}}' 2>/dev/null | grep -qx taler-hacktivism && [ "$name" = taler-hacktivism ]; then
|
||||
report OK "container taler-hacktivism" "$(podman inspect -f '{{.State.Status}}' taler-hacktivism 2>/dev/null)"
|
||||
else
|
||||
report FAIL "container $name" "not running (seen: $hit)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# local HTTP on pasta ports
|
||||
for spec in \
|
||||
"bank-api|http://127.0.0.1:9012/config|200" \
|
||||
"bank-integration|http://127.0.0.1:9012/taler-integration/config|200" \
|
||||
"landing|http://127.0.0.1:9013/intro/|200" \
|
||||
"exchange|http://127.0.0.1:9011/config|200" \
|
||||
"merchant|https://127.0.0.1:9010/config|200"
|
||||
do
|
||||
IFS='|' read -r id url want <<<"$spec"
|
||||
code=$(curl -skS -m 5 -o /tmp/mon-s.out -w '%{http_code}' "$url" 2>/dev/null || echo 000)
|
||||
if [ "$code" = "$want" ]; then
|
||||
report OK "local $id : ${url#*//}" "HTTP $code"
|
||||
else
|
||||
report FAIL "local $id" "HTTP $code want $want"
|
||||
fi
|
||||
done
|
||||
|
||||
# processes inside bank
|
||||
BANK=$(podman ps --format '{{.Names}}' | grep -iE 'bank|hacktivism-bank' | head -1)
|
||||
if [ -n "$BANK" ]; then
|
||||
if podman exec "$BANK" bash -c 'pgrep -f "MainKt serve|libeufin-bank serve" >/dev/null' 2>/dev/null; then
|
||||
report OK "libeufin-bank process" "in $BANK"
|
||||
else
|
||||
report FAIL "libeufin-bank process" "not running in $BANK"
|
||||
fi
|
||||
if podman exec "$BANK" bash -c 'pg_isready -q' 2>/dev/null; then
|
||||
report OK "postgres (bank)" "ready"
|
||||
else
|
||||
report WARN "postgres (bank)" "pg_isready failed"
|
||||
fi
|
||||
if podman exec "$BANK" bash -c 'pgrep -x nginx >/dev/null' 2>/dev/null; then
|
||||
report OK "nginx landing" "in $BANK"
|
||||
else
|
||||
report WARN "nginx landing" "not seen in $BANK"
|
||||
fi
|
||||
else
|
||||
report FAIL "bank container" "none"
|
||||
fi
|
||||
|
||||
# exchange process / systemd if any
|
||||
EX=$(podman ps --format '{{.Names}}' | grep -i exchange | head -1)
|
||||
if [ -n "$EX" ]; then
|
||||
if podman exec "$EX" bash -c 'pgrep -f taler-exchange-httpd >/dev/null || systemctl is-active taler-exchange-httpd 2>/dev/null | grep -q active' 2>/dev/null; then
|
||||
report OK "exchange-httpd" "in $EX"
|
||||
else
|
||||
# config answering is enough
|
||||
report WARN "exchange-httpd process" "not detected; port check above"
|
||||
fi
|
||||
fi
|
||||
|
||||
MER=$(podman ps --format '{{.Names}}' | grep -E '^taler-hacktivism$' | head -1)
|
||||
[ -z "$MER" ] && MER=$(podman ps --format '{{.Names}}' | grep -i merchant | head -1)
|
||||
if [ -n "$MER" ]; then
|
||||
if podman exec "$MER" bash -c 'pgrep -f taler-merchant-httpd >/dev/null || true; curl -sk -m 3 -o /dev/null -w %{http_code} https://127.0.0.1:9010/config' 2>/dev/null | grep -q 200; then
|
||||
report OK "merchant-httpd" "responds in $MER"
|
||||
else
|
||||
report WARN "merchant-httpd" "check manually in $MER"
|
||||
fi
|
||||
fi
|
||||
|
||||
# caddy on host
|
||||
if systemctl is-active caddy >/dev/null 2>&1 || pgrep -x caddy >/dev/null 2>&1; then
|
||||
report OK "caddy" "active"
|
||||
else
|
||||
report WARN "caddy" "not detected as active"
|
||||
fi
|
||||
REMOTE
|
||||
)
|
||||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
R\|*)
|
||||
IFS='|' read -r _ st label detail <<<"$line"
|
||||
case "$st" in
|
||||
OK) ok "$label${detail:+ ($detail)}" ;;
|
||||
FAIL) fail "$label" "$detail" ;;
|
||||
WARN) warn "$label" "$detail" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done <<<"$REMOTE"
|
||||
|
||||
summary
|
||||
636
scripts/taler-monitoring/check_urls.sh
Executable file
636
scripts/taler-monitoring/check_urls.sh
Executable file
|
|
@ -0,0 +1,636 @@
|
|||
#!/usr/bin/env bash
|
||||
# Outside-in public HTTPS checks (no SSH).
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
# Area www-### — public HTTPS (outside-in)
|
||||
set_area www
|
||||
section "www · public URLs · ${TALER_DOMAIN:-?} (outside-in, no SSH)"
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
check_url() {
|
||||
local label="$1" expect="$2" url="$3"
|
||||
local code
|
||||
code=$(http_code "$url")
|
||||
case ",$expect," in
|
||||
*",$code,"*) ok "$label $url" ;;
|
||||
*) fail "$label $url" "got $code want $expect" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Soft check: OK on expect, WARN on foreign stack if down, ERROR on local stack
|
||||
check_url_soft() {
|
||||
local label="$1" expect="$2" url="$3"
|
||||
local code
|
||||
code=$(http_code "$url")
|
||||
case ",$expect," in
|
||||
*",$code,"*) ok "$label $url" ;;
|
||||
*)
|
||||
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
||||
warn "$label $url" "got $code (optional on remote domain)"
|
||||
else
|
||||
fail "$label $url" "got $code want $expect"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
expect_currency() {
|
||||
local label="$1" file="$2" want="${EXPECT_CURRENCY:-}"
|
||||
local cur
|
||||
cur=$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("currency",""))' "$file" 2>/dev/null || true)
|
||||
if [ -z "$want" ]; then
|
||||
info "$label currency" "${cur:-?}"
|
||||
return
|
||||
fi
|
||||
if [ "$cur" = "$want" ]; then
|
||||
ok "$label currency=$want"
|
||||
else
|
||||
fail "$label currency" "got ${cur:-?} want $want"
|
||||
fi
|
||||
}
|
||||
|
||||
# Legal docs: /terms and /privacy must be HTTP 200 with a real document body
|
||||
# (not empty, not "not configured", not JSON API error).
|
||||
# $1=label $2=url $3=optional needle regex (case-insensitive) for local stack
|
||||
check_legal_doc() {
|
||||
local label="$1" url="$2" needle="${3:-}"
|
||||
local f code soft
|
||||
soft=0
|
||||
[ "${LOCAL_STACK:-1}" = "0" ] && soft=1
|
||||
f=$(mktemp)
|
||||
code=$(curl -skS --max-redirs 5 -L -m "${TIMEOUT}" \
|
||||
-H "Accept: text/html,text/markdown,text/plain,*/*" \
|
||||
-o "$f" -w '%{http_code}' "$url" 2>/dev/null || echo 000)
|
||||
if [ "$code" != "200" ]; then
|
||||
rm -f "$f"
|
||||
if [ "$soft" = "1" ]; then
|
||||
warn "$label" "HTTP $code — $url"
|
||||
else
|
||||
fail "$label" "HTTP $code — $url"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
if [ ! -s "$f" ]; then
|
||||
rm -f "$f"
|
||||
fail "$label" "empty body — $url"
|
||||
return
|
||||
fi
|
||||
# merchant returns plain "not configured" when PRIVACY_ETAG missing
|
||||
if grep -qiE '^(not configured)\s*$' "$f" 2>/dev/null \
|
||||
|| grep -qiE '"code"\s*:\s*21' "$f" 2>/dev/null; then
|
||||
rm -f "$f"
|
||||
fail "$label" "not configured / API error — $url"
|
||||
return
|
||||
fi
|
||||
if [ -n "$needle" ] && [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
if ! grep -qiE "$needle" "$f" 2>/dev/null; then
|
||||
warn "$label content" "missing /$needle/ — $url"
|
||||
rm -f "$f"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
ok "$label" "HTTP 200 · $(wc -c <"$f" | tr -d ' ') bytes"
|
||||
rm -f "$f"
|
||||
}
|
||||
|
||||
# --- exchange (core; always required) --- www-001 …
|
||||
check_url "exchange /config" 200 "$EXCHANGE_PUBLIC/config"
|
||||
code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ec.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
expect_currency "exchange" "$tmp/ec.json"
|
||||
# currency_specification.alt_unit_names (wallet codec)
|
||||
if json_has_alt_unit_names "$tmp/ec.json" "${EXPECT_CURRENCY:-}" >/tmp/alt-ex.$$ 2>&1; then
|
||||
ok "exchange /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-ex.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "exchange /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-ex.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-ex.$$
|
||||
fi
|
||||
check_url "exchange /keys" 200 "$EXCHANGE_PUBLIC/keys"
|
||||
# /keys should also expose currency_specification.alt_unit_names when present
|
||||
code=$(http_body "$EXCHANGE_PUBLIC/keys" "$tmp/ek.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
if python3 - "$tmp/ek.json" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
cs=d.get("currency_specification") or {}
|
||||
au=cs.get("alt_unit_names") if isinstance(cs,dict) else None
|
||||
if not isinstance(au, dict) or "0" not in au:
|
||||
# older keys without embedded spec: not a hard fail if /config is good
|
||||
sys.exit(2)
|
||||
sys.exit(0)
|
||||
PY
|
||||
then
|
||||
ok "exchange /keys alt_unit_names"
|
||||
else
|
||||
ec=$?
|
||||
if [ "$ec" = "2" ]; then
|
||||
warn "exchange /keys alt_unit_names" "no currency_specification in /keys (ok if /config has it)"
|
||||
else
|
||||
fail "exchange /keys alt_unit_names"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
check_url_soft "exchange /intro/" 200 "$EXCHANGE_PUBLIC/intro/"
|
||||
# Root should land on intro (302/301 then 200 on follow is checked separately)
|
||||
check_url_soft "exchange /" 302,301,200 "$EXCHANGE_PUBLIC/"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Performance — outside-in public HTTPS latency (this runner, not loopback)
|
||||
# Same spirit as bank landing-stats public probes; measured here from outside.
|
||||
# ---------------------------------------------------------------------------
|
||||
section "www · performance · public HTTPS latency (outside-in)"
|
||||
|
||||
# Latency thresholds (ms), outside-in from this runner.
|
||||
# ≥ PERF_WARN_MS → WARN. ≥ PERF_FAIL_MS → ERROR (suite fails).
|
||||
PERF_WARN_MS="${PERF_WARN_MS:-8000}"
|
||||
PERF_FAIL_MS="${PERF_FAIL_MS:-20000}"
|
||||
|
||||
# Measure one URL: require HTTP expect (default 200), report time_total in ms.
|
||||
# $1=label $2=url $3=optional expected codes (default 200)
|
||||
check_perf() {
|
||||
local label="$1" url="$2" expect="${3:-200}"
|
||||
local out code t_s ms
|
||||
out=$(curl -skS --max-redirs 3 -L -m "${PERF_CURL_TIMEOUT:-25}" \
|
||||
-o /dev/null -w '%{http_code} %{time_total}' "$url" 2>/dev/null || echo "000 0")
|
||||
code=$(printf '%s' "$out" | awk '{print $1}')
|
||||
t_s=$(printf '%s' "$out" | awk '{print $2}')
|
||||
ms=$(awk -v t="${t_s:-0}" 'BEGIN{
|
||||
ms=(t+0)*1000
|
||||
if (ms>0 && ms<1) ms=1
|
||||
printf "%d", int(ms+0.5)
|
||||
}')
|
||||
case ",$expect," in
|
||||
*",$code,"*)
|
||||
if [ "$ms" -ge "${PERF_FAIL_MS}" ] 2>/dev/null; then
|
||||
fail "$label" "HTTP $code · ${ms} ms ≥ fail ${PERF_FAIL_MS} ms · $url"
|
||||
elif [ "$ms" -ge "${PERF_WARN_MS}" ] 2>/dev/null; then
|
||||
warn "$label" "HTTP $code · ${ms} ms ≥ warn ${PERF_WARN_MS} ms · $url"
|
||||
else
|
||||
ok "$label" "HTTP $code · ${ms} ms · $url"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
fail "$label" "HTTP $code want $expect · ${ms} ms · $url"
|
||||
else
|
||||
warn "$label" "HTTP $code want $expect · ${ms} ms · $url"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Bank first (wallet-critical paths before UI chrome)
|
||||
check_perf "perf bank /taler-integration/config" "$BANK_PUBLIC/taler-integration/config"
|
||||
check_perf "perf bank /config" "$BANK_PUBLIC/config"
|
||||
check_perf "perf bank /intro/" "$BANK_PUBLIC/intro/"
|
||||
check_perf "perf bank /intro/stats.json" "$BANK_PUBLIC/intro/stats.json" 200
|
||||
check_perf "perf bank /webui/" "$BANK_PUBLIC/webui/" 200,301,302
|
||||
|
||||
# Exchange
|
||||
check_perf "perf exchange /config" "$EXCHANGE_PUBLIC/config"
|
||||
check_perf "perf exchange /keys" "$EXCHANGE_PUBLIC/keys"
|
||||
check_perf "perf exchange /intro/" "$EXCHANGE_PUBLIC/intro/"
|
||||
|
||||
# Merchant
|
||||
check_perf "perf merchant /config" "$MERCHANT_PUBLIC/config"
|
||||
check_perf "perf merchant /webui/" "$MERCHANT_PUBLIC/webui/" 200,301,302
|
||||
check_perf "perf merchant /intro/" "$MERCHANT_PUBLIC/intro/"
|
||||
|
||||
info "perf note" "measured from this host (outside-in); not container loopback"
|
||||
|
||||
|
||||
# Terms + privacy (legal docs)
|
||||
check_legal_doc "exchange /terms" "$EXCHANGE_PUBLIC/terms" "terms|GOA|exploration|FADP|revDSG|privacy"
|
||||
check_legal_doc "exchange /privacy" "$EXCHANGE_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|exploration"
|
||||
# trailing slash: 200 or redirect to bare path
|
||||
code=$(http_code "$EXCHANGE_PUBLIC/terms/")
|
||||
case "$code" in
|
||||
200|301|302) ok "exchange /terms/" "HTTP $code" ;;
|
||||
*)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then warn "exchange /terms/" "HTTP $code"
|
||||
else warn "exchange /terms/" "HTTP $code"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- bank ---
|
||||
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
||||
check_url_soft "bank /config" 200 "$BANK_PUBLIC/config"
|
||||
else
|
||||
check_url "bank /config" 200 "$BANK_PUBLIC/config"
|
||||
fi
|
||||
code=$(http_body "$BANK_PUBLIC/config" "$tmp/bc.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
expect_currency "bank" "$tmp/bc.json"
|
||||
if json_has_alt_unit_names "$tmp/bc.json" >/tmp/alt-bank.$$ 2>&1; then
|
||||
ok "bank /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-bank.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "bank /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-bank.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-bank.$$
|
||||
# Wallet probes this; GET must be 200 (POST→405 is normal)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
check_url "bank /taler-integration/config" 200 "$BANK_PUBLIC/taler-integration/config"
|
||||
else
|
||||
check_url_soft "bank /taler-integration/config" 200 "$BANK_PUBLIC/taler-integration/config"
|
||||
fi
|
||||
check_url_soft "bank /webui/" 200 "$BANK_PUBLIC/webui/"
|
||||
check_url_soft "bank /intro/" 200 "$BANK_PUBLIC/intro/"
|
||||
check_url_soft "bank /" 302,301,200 "$BANK_PUBLIC/"
|
||||
# Auto-account: credentials + shared-pool taler://withdraw (like step 2)
|
||||
aa_code=$(http_body "$BANK_PUBLIC/intro/auto-account.json" "$tmp/aa.json")
|
||||
case "$aa_code" in
|
||||
200)
|
||||
if python3 - "$tmp/aa.json" <<'PY'
|
||||
import json, re, sys
|
||||
from urllib.parse import urlparse
|
||||
d = json.load(open(sys.argv[1]))
|
||||
if not d.get("ok"):
|
||||
print("ok!=true"); sys.exit(1)
|
||||
if "payto_uri" in d and d.get("payto_uri"):
|
||||
print("payto_uri must not be present"); sys.exit(1)
|
||||
wuri = d.get("taler_withdraw_uri") or d.get("qr_payload") or ""
|
||||
wm = re.match(r"^taler://withdraw/([^/]+)/taler-integration/([0-9a-fA-F-]+)$", wuri)
|
||||
if not wm:
|
||||
print("need taler://withdraw/HOST:PORT/taler-integration/ID:", wuri[:120]); sys.exit(1)
|
||||
if ":" not in wm.group(1):
|
||||
print("withdraw missing port:", wm.group(1)); sys.exit(1)
|
||||
webui = d.get("login_url") or d.get("webui") or d.get("account_url") or ""
|
||||
u = urlparse(webui)
|
||||
if u.scheme not in ("http", "https") or "webui" not in (u.path or ""):
|
||||
print("login webui missing:", webui[:80]); sys.exit(1)
|
||||
print("user=%s withdraw=%s login=%s" % (d.get("username"), wm.group(1), webui))
|
||||
sys.exit(0)
|
||||
PY
|
||||
then
|
||||
ok "bank /intro/auto-account.json" "$(python3 -c 'import json;d=json.load(open("'"$tmp/aa.json"'"));print(d.get("username",""),"·",(d.get("taler_withdraw_uri") or "")[:72])' 2>/dev/null || true)"
|
||||
else
|
||||
fail "bank /intro/auto-account.json" "invalid withdraw/login (HTTP body bad)"
|
||||
fi
|
||||
;;
|
||||
405|501|404|502|503|000)
|
||||
fail "bank /intro/auto-account.json" "HTTP $aa_code (want 200; 405/501 = broken)"
|
||||
;;
|
||||
*)
|
||||
fail "bank /intro/auto-account.json" "HTTP $aa_code want 200"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Bank legal docs (landing nginx via Caddy /terms* /privacy* or /intro/*)
|
||||
check_legal_doc "bank /terms" "$BANK_PUBLIC/terms" "terms|GOA|exploration|bank|FADP|revDSG"
|
||||
# Prefer /privacy; fall back to /intro/privacy.html for older deploys
|
||||
code=$(http_code "$BANK_PUBLIC/privacy")
|
||||
if [ "$code" = "200" ]; then
|
||||
check_legal_doc "bank /privacy" "$BANK_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|bank"
|
||||
else
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
check_legal_doc "bank /privacy (or /intro/privacy.html)" \
|
||||
"$BANK_PUBLIC/intro/privacy.html" "privacy|FADP|revDSG|data|GOA|bank"
|
||||
# still report bare /privacy failure for local
|
||||
warn "bank /privacy" "HTTP $code — prefer Caddy handle /privacy* → landing"
|
||||
else
|
||||
check_url_soft "bank /privacy" 200 "$BANK_PUBLIC/privacy"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- merchant ---
|
||||
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
||||
check_url_soft "merchant /config" 200 "$MERCHANT_PUBLIC/config"
|
||||
else
|
||||
check_url "merchant /config" 200 "$MERCHANT_PUBLIC/config"
|
||||
fi
|
||||
code=$(http_body "$MERCHANT_PUBLIC/config" "$tmp/mc.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
want="${EXPECT_CURRENCY:-}"
|
||||
if python3 - "$tmp/mc.json" "$want" <<'PY'
|
||||
import json,sys
|
||||
try:
|
||||
d=json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
sys.exit(2)
|
||||
want=sys.argv[2]
|
||||
if not want:
|
||||
sys.exit(0)
|
||||
curs=list((d.get("currencies") or {}).keys())
|
||||
ex=d.get("exchanges") or []
|
||||
ok = want in curs or any((e.get("currency") if isinstance(e,dict) else None)==want for e in ex)
|
||||
if not ok and isinstance(d.get("currency"), str):
|
||||
ok = d["currency"]==want
|
||||
sys.exit(0 if ok else 1)
|
||||
PY
|
||||
then
|
||||
ok "merchant /config (${want:-currency} ok)"
|
||||
else
|
||||
ec=$?
|
||||
if [ "$ec" = "2" ]; then
|
||||
warn "merchant /config" "non-JSON body"
|
||||
elif [ -n "$want" ]; then
|
||||
fail "merchant /config currency" "want $want"
|
||||
else
|
||||
info "merchant /config" "ok"
|
||||
fi
|
||||
fi
|
||||
# merchant-local currency maps (GOA + CHF, …)
|
||||
if json_has_alt_unit_names "$tmp/mc.json" >/tmp/alt-mer.$$ 2>&1; then
|
||||
ok "merchant /config currencies alt_unit_names" "$(tr '\n' '; ' </tmp/alt-mer.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "merchant /config currencies alt_unit_names" "$(tr '\n' '; ' </tmp/alt-mer.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-mer.$$
|
||||
# Follow each exchange listed in merchant /config and require its /config alt_unit_names
|
||||
check_merchant_listed_exchanges_alt_units "$tmp/mc.json"
|
||||
check_url_soft "merchant /intro/" 200 "$MERCHANT_PUBLIC/intro/"
|
||||
check_url_soft "merchant /webui/" 200 "$MERCHANT_PUBLIC/webui/"
|
||||
check_url_soft "merchant /" 302,301,200 "$MERCHANT_PUBLIC/"
|
||||
fi
|
||||
|
||||
# Merchant legal docs
|
||||
check_legal_doc "merchant /terms" "$MERCHANT_PUBLIC/terms" "terms|dual|GOA|CHF|explorational|merchant"
|
||||
check_legal_doc "merchant /privacy" "$MERCHANT_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|CHF|merchant"
|
||||
code=$(http_code "$MERCHANT_PUBLIC/terms/")
|
||||
case "$code" in
|
||||
200|301|302) ok "merchant /terms/" "HTTP $code" ;;
|
||||
*) warn "merchant /terms/" "HTTP $code (expect 302 → /terms)" ;;
|
||||
esac
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Landing pages: every HTTPS link exposed on bank / merchant / exchange intros
|
||||
# + required static assets + bank withdraw mint (taler://withdraw only)
|
||||
# ---------------------------------------------------------------------------
|
||||
section "www · landing exposed links · bank / merchant / exchange"
|
||||
|
||||
# Known-good landing static paths (relative to each host base)
|
||||
# Checked even if HTML parse misses them.
|
||||
check_landing_asset() {
|
||||
local label="$1" url="$2" soft="${3:-0}"
|
||||
local code
|
||||
code=$(http_code "$url")
|
||||
case "$code" in
|
||||
200) ok "$label" "HTTP 200 · $url" ;;
|
||||
301|302|303|307|308)
|
||||
# follow once for assets that redirect
|
||||
code=$(curl -skS --max-redirs 3 -L -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || echo 000)
|
||||
if [ "$code" = "200" ]; then
|
||||
ok "$label" "HTTP redirect→200 · $url"
|
||||
elif [ "$soft" = "1" ]; then
|
||||
warn "$label" "HTTP $code — $url"
|
||||
else
|
||||
fail "$label" "HTTP $code after redirect — $url"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ "$soft" = "1" ]; then
|
||||
warn "$label" "HTTP $code — $url"
|
||||
else
|
||||
fail "$label" "HTTP $code — $url"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Soft external (app stores / upstream docs): WARN if down, never ERROR
|
||||
check_external_soft() {
|
||||
local label="$1" url="$2"
|
||||
local code
|
||||
code=$(curl -skS --max-redirs 5 -L -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || echo 000)
|
||||
case "$code" in
|
||||
200|204|301|302|303|307|308) ok "$label" "HTTP $code · $url" ;;
|
||||
*) warn "$label" "HTTP $code (external soft) · $url" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Parse one landing HTML: collect absolute https + root-relative href/src;
|
||||
# resolve against base; classify own-stack vs external.
|
||||
# Writes lists: $1.own $1.ext (one URL per line)
|
||||
extract_landing_urls() {
|
||||
local base="$1" html="$2" out_prefix="$3"
|
||||
python3 - "$base" "$html" "$out_prefix" <<'PY'
|
||||
import re, sys
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
base, html_path, out = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
html = open(html_path, encoding="utf-8", errors="replace").read()
|
||||
base = base.rstrip("/") + "/"
|
||||
parsed_base = urlparse(base)
|
||||
# Hard-check only the three public Taler hosts for this stack (not git.* etc.)
|
||||
own_hosts = {
|
||||
(parsed_base.hostname or "").lower(),
|
||||
"bank.hacktivism.ch",
|
||||
"exchange.hacktivism.ch",
|
||||
"taler.hacktivism.ch",
|
||||
}
|
||||
# include configured public hosts when domain differs (demo / ops)
|
||||
for envu in (
|
||||
__import__("os").environ.get("BANK_PUBLIC", ""),
|
||||
__import__("os").environ.get("EXCHANGE_PUBLIC", ""),
|
||||
__import__("os").environ.get("MERCHANT_PUBLIC", ""),
|
||||
):
|
||||
h = urlparse(envu).hostname if envu else None
|
||||
if h:
|
||||
own_hosts.add(h.lower())
|
||||
|
||||
raw = set()
|
||||
for m in re.finditer(
|
||||
r'''(?:href|src|content)=["']([^"'#]+)["']''', html, re.I
|
||||
):
|
||||
raw.add(m.group(1).strip())
|
||||
# bare absolute URLs in scripts (fetch, template strings)
|
||||
for m in re.finditer(r'''https://[^\s"'<>\\]+''', html):
|
||||
u = m.group(0).rstrip("\\).,;'\"")
|
||||
# strip trailing punctuation leftovers
|
||||
while u and u[-1] in ".,);]}\"'":
|
||||
u = u[:-1]
|
||||
if u.startswith("https://"):
|
||||
raw.add(u)
|
||||
|
||||
own, ext = set(), set()
|
||||
skip_prefix = ("data:", "javascript:", "mailto:", "taler://", "blob:")
|
||||
skip_exact = {"website", "summary_large_image", "image/png", "en_US"}
|
||||
for r in raw:
|
||||
if not r or r in skip_exact:
|
||||
continue
|
||||
if r.startswith(skip_prefix):
|
||||
continue
|
||||
# meta content noise
|
||||
if re.fullmatch(r"\d+", r) or r.startswith("width="):
|
||||
continue
|
||||
if " " in r and not r.startswith("http"):
|
||||
continue
|
||||
if r.startswith("//"):
|
||||
absu = "https:" + r
|
||||
elif r.startswith("http://") or r.startswith("https://"):
|
||||
absu = r
|
||||
elif r.startswith("/"):
|
||||
absu = urljoin(base, r)
|
||||
else:
|
||||
# relative asset
|
||||
if "/" in r or r.endswith((".js", ".css", ".png", ".svg", ".html", ".json", ".uri")):
|
||||
absu = urljoin(base + "intro/", r)
|
||||
else:
|
||||
continue
|
||||
# drop query-only noise / anchors already stripped
|
||||
p = urlparse(absu)
|
||||
if p.scheme not in ("http", "https"):
|
||||
continue
|
||||
# normalize: drop fragment
|
||||
absu = absu.split("#", 1)[0]
|
||||
host = (p.hostname or "").lower()
|
||||
# og image query ok
|
||||
if host in own_hosts:
|
||||
own.add(absu)
|
||||
else:
|
||||
ext.add(absu)
|
||||
|
||||
open(out + ".own", "w").write("\n".join(sorted(own)) + ("\n" if own else ""))
|
||||
open(out + ".ext", "w").write("\n".join(sorted(ext)) + ("\n" if ext else ""))
|
||||
print(f"own={len(own)} ext={len(ext)}")
|
||||
PY
|
||||
}
|
||||
|
||||
check_one_landing() {
|
||||
local name="$1" base="$2"
|
||||
local html="$tmp/landing-${name}.html"
|
||||
local pref="$tmp/urls-${name}"
|
||||
local code n own_n ext_n
|
||||
code=$(http_body "${base}/intro/" "$html")
|
||||
if [ "$code" != "200" ]; then
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
fail "landing ${name} /intro/" "HTTP $code"
|
||||
else
|
||||
warn "landing ${name} /intro/" "HTTP $code"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
ok "landing ${name} /intro/" "HTTP 200 · $(wc -c <"$html" | tr -d ' ') bytes"
|
||||
|
||||
# Required static assets (hard on local)
|
||||
check_landing_asset "landing ${name} qrcode.min.js" "${base}/intro/qrcode.min.js"
|
||||
check_landing_asset "landing ${name} og-goa-shop.png" "${base}/intro/og-goa-shop.png"
|
||||
check_landing_asset "landing ${name} qr-logo.png" "${base}/intro/qr-logo.png" 1
|
||||
|
||||
n=$(extract_landing_urls "$base" "$html" "$pref" 2>/dev/null || echo "own=0 ext=0")
|
||||
info "landing ${name} link extract" "$n"
|
||||
own_n=0
|
||||
ext_n=0
|
||||
[ -f "${pref}.own" ] && own_n=$(grep -c . "${pref}.own" 2>/dev/null || echo 0)
|
||||
[ -f "${pref}.ext" ] && ext_n=$(grep -c . "${pref}.ext" 2>/dev/null || echo 0)
|
||||
if [ "${own_n:-0}" -lt 1 ]; then
|
||||
fail "landing ${name} own-stack links" "none extracted from HTML"
|
||||
else
|
||||
ok "landing ${name} own-stack links" "${own_n} URLs to probe"
|
||||
fi
|
||||
|
||||
# Probe every own-stack URL from the page
|
||||
if [ -f "${pref}.own" ]; then
|
||||
while IFS= read -r u; do
|
||||
[ -n "$u" ] || continue
|
||||
# skip mint endpoints that create resources on GET if any (auto-account creates accounts)
|
||||
case "$u" in
|
||||
*/intro/auto-account.json)
|
||||
# shape checked separately; still require 200 GET
|
||||
;;
|
||||
esac
|
||||
code=$(http_code "$u")
|
||||
case "$code" in
|
||||
200) ok "landing ${name} link" "HTTP 200 · $u" ;;
|
||||
301|302|303|307|308)
|
||||
code=$(curl -skS --max-redirs 5 -L -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$u" 2>/dev/null || echo 000)
|
||||
if [ "$code" = "200" ]; then
|
||||
ok "landing ${name} link" "redirect→200 · $u"
|
||||
else
|
||||
fail "landing ${name} link" "HTTP $code after redirect · $u"
|
||||
fi
|
||||
;;
|
||||
405|501)
|
||||
# some APIs reject wrong method — try GET already failed; soft note
|
||||
fail "landing ${name} link" "HTTP $code · $u"
|
||||
;;
|
||||
*)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
fail "landing ${name} link" "HTTP $code · $u"
|
||||
else
|
||||
warn "landing ${name} link" "HTTP $code · $u"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done < "${pref}.own"
|
||||
fi
|
||||
|
||||
# External store / docs: soft
|
||||
if [ -f "${pref}.ext" ]; then
|
||||
while IFS= read -r u; do
|
||||
[ -n "$u" ] || continue
|
||||
check_external_soft "landing ${name} external" "$u"
|
||||
done < "${pref}.ext"
|
||||
fi
|
||||
}
|
||||
|
||||
check_one_landing "bank" "$BANK_PUBLIC"
|
||||
check_one_landing "merchant" "$MERCHANT_PUBLIC"
|
||||
check_one_landing "exchange" "$EXCHANGE_PUBLIC"
|
||||
|
||||
# Cross-links between the three landings (always on local stack)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
check_landing_asset "cross bank→merchant intro" "$MERCHANT_PUBLIC/intro/"
|
||||
check_landing_asset "cross bank→exchange intro" "$EXCHANGE_PUBLIC/intro/"
|
||||
check_landing_asset "cross merchant→bank intro" "$BANK_PUBLIC/intro/"
|
||||
check_landing_asset "cross exchange→bank intro" "$BANK_PUBLIC/intro/"
|
||||
fi
|
||||
|
||||
# Bank-only: shared-pool withdraw mint + static withdraw files + shop assets
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ] || [ -n "${BANK_PUBLIC:-}" ]; then
|
||||
check_landing_asset "bank shop-pay.js" "$BANK_PUBLIC/intro/shop-pay.js" 1
|
||||
check_landing_asset "bank shop-pay.css" "$BANK_PUBLIC/intro/shop-pay.css" 1
|
||||
dw_code=$(http_body "$BANK_PUBLIC/intro/demo-withdraw.json" "$tmp/dw.json")
|
||||
case "$dw_code" in
|
||||
200)
|
||||
if python3 - "$tmp/dw.json" <<'PY'
|
||||
import json, re, sys
|
||||
d = json.load(open(sys.argv[1]))
|
||||
if not d.get("ok", True) and "taler_withdraw_uri" not in d:
|
||||
print("not ok"); sys.exit(1)
|
||||
u = d.get("taler_withdraw_uri") or ""
|
||||
m = re.match(r"^taler://withdraw/([^/]+)/taler-integration/([0-9a-fA-F-]+)$", u)
|
||||
if not m:
|
||||
print("bad uri:", u[:120]); sys.exit(1)
|
||||
if ":" not in m.group(1):
|
||||
print("missing port:", m.group(1)); sys.exit(1)
|
||||
print(u[:88])
|
||||
sys.exit(0)
|
||||
PY
|
||||
then
|
||||
ok "bank /intro/demo-withdraw.json" "$(python3 -c 'import json;print(json.load(open("'"$tmp/dw.json"'")).get("taler_withdraw_uri","")[:80])' 2>/dev/null || true)"
|
||||
wid=$(python3 -c 'import json;print(json.load(open("'"$tmp/dw.json"'")).get("withdrawal_id",""))' 2>/dev/null || true)
|
||||
if [ -n "$wid" ]; then
|
||||
check_landing_asset "bank taler-integration withdraw op" \
|
||||
"$BANK_PUBLIC/taler-integration/withdrawal-operation/${wid}"
|
||||
fi
|
||||
else
|
||||
fail "bank /intro/demo-withdraw.json" "invalid taler://withdraw shape"
|
||||
fi
|
||||
;;
|
||||
405|501|404|502|503|000)
|
||||
fail "bank /intro/demo-withdraw.json" "HTTP $dw_code (want 200)"
|
||||
;;
|
||||
*)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
fail "bank /intro/demo-withdraw.json" "HTTP $dw_code want 200"
|
||||
else
|
||||
warn "bank /intro/demo-withdraw.json" "HTTP $dw_code"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Merchant landing shop assets
|
||||
check_landing_asset "merchant shop-pay.js" "$MERCHANT_PUBLIC/intro/shop-pay.js" 1
|
||||
check_landing_asset "merchant shop-pay.css" "$MERCHANT_PUBLIC/intro/shop-pay.css" 1
|
||||
|
||||
summary
|
||||
469
scripts/taler-monitoring/check_versions.sh
Executable file
469
scripts/taler-monitoring/check_versions.sh
Executable file
|
|
@ -0,0 +1,469 @@
|
|||
#!/usr/bin/env bash
|
||||
# Area versions-### — Taler packages vs deb.taler.net (trixie) + repo availability.
|
||||
#
|
||||
# Checks:
|
||||
# 1) deb.taler.net apt endpoints reachable (InRelease / Packages / sample .deb)
|
||||
# 2) containers can reach deb.taler.net (install path from inside)
|
||||
# 3) installed taler*/libeufin*/libtaler*/libdonau* versions vs suite index
|
||||
#
|
||||
# Env:
|
||||
# TALER_APT_SUITE=trixie
|
||||
# TALER_APT_BASE=https://deb.taler.net/apt/debian
|
||||
# TALER_APT_INDEX=…/dists/trixie/main/binary-amd64/Packages
|
||||
# TALER_APT_TESTING_INDEX=…/dists/trixie-testing/…
|
||||
# TALER_PKG_BEHIND=warn|error (default: core packages ERROR if behind, else warn)
|
||||
# SKIP_SSH=1 skip container install checks
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
set_area versions
|
||||
|
||||
SUITE="${TALER_APT_SUITE:-trixie}"
|
||||
APT_BASE="${TALER_APT_BASE:-https://deb.taler.net/apt/debian}"
|
||||
APT_BASE="${APT_BASE%/}"
|
||||
INDEX="${TALER_APT_INDEX:-${APT_BASE}/dists/${SUITE}/main/binary-amd64/Packages}"
|
||||
INRELEASE="${TALER_APT_INRELEASE:-${APT_BASE}/dists/${SUITE}/InRelease}"
|
||||
TESTING_INDEX="${TALER_APT_TESTING_INDEX:-${APT_BASE}/dists/${SUITE}-testing/main/binary-amd64/Packages}"
|
||||
BEHIND_MODE="${TALER_PKG_BEHIND:-warn}"
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
http_get() {
|
||||
# usage: http_get URL [outfile] → prints http_code; body to outfile or /dev/null
|
||||
local url="$1" out="${2:-/dev/null}"
|
||||
curl -sS --max-time 30 -L -o "$out" -w '%{http_code}' "$url" 2>/dev/null || echo 000
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1) OUTSIDE — deb.taler.net install source (runner / laptop, no SSH)
|
||||
# ---------------------------------------------------------------------------
|
||||
section "versions · outside · deb.taler.net (${SUITE})"
|
||||
|
||||
# DNS
|
||||
HOST_APT="${APT_BASE#https://}"; HOST_APT="${HOST_APT#http://}"; HOST_APT="${HOST_APT%%/*}"
|
||||
if getent hosts "$HOST_APT" >/dev/null 2>&1 \
|
||||
|| python3 -c "import socket; socket.getaddrinfo('${HOST_APT}', 443)" >/dev/null 2>&1; then
|
||||
ip=$(python3 -c "import socket; print(socket.getaddrinfo('${HOST_APT}',443)[0][4][0])" 2>/dev/null || true)
|
||||
ok "outside DNS ${HOST_APT}" "${ip:-resolved}"
|
||||
else
|
||||
fail "outside DNS ${HOST_APT}" "unresolvable — cannot use as apt source"
|
||||
fi
|
||||
|
||||
# HTTPS portal / apt root
|
||||
code=$(http_get "https://${HOST_APT}/")
|
||||
[ "$code" = "200" ] && ok "outside https://${HOST_APT}/" "HTTP $code" \
|
||||
|| warn "outside https://${HOST_APT}/" "HTTP $code"
|
||||
|
||||
code=$(http_get "${APT_BASE}/")
|
||||
case "$code" in
|
||||
200|301|302) ok "outside apt base" "${APT_BASE}/ HTTP $code" ;;
|
||||
*) fail "outside apt base" "${APT_BASE}/ HTTP $code" ;;
|
||||
esac
|
||||
|
||||
# Suite metadata (what apt update needs)
|
||||
code=$(http_get "$INRELEASE" "$tmp/InRelease")
|
||||
if [ "$code" = "200" ] && [ -s "$tmp/InRelease" ]; then
|
||||
ok "outside InRelease" "${SUITE} HTTP 200 · $(wc -c <"$tmp/InRelease" | tr -d ' ') bytes"
|
||||
else
|
||||
fail "outside InRelease" "HTTP $code — $INRELEASE (apt update will fail)"
|
||||
fi
|
||||
|
||||
RELEASE_URL="${APT_BASE}/dists/${SUITE}/Release"
|
||||
code=$(http_get "$RELEASE_URL" "$tmp/Release")
|
||||
[ "$code" = "200" ] && ok "outside Release" "${SUITE} HTTP 200" \
|
||||
|| warn "outside Release" "HTTP $code — $RELEASE_URL"
|
||||
|
||||
# Packages (plain) — required for version compare
|
||||
code=$(http_get "$INDEX" "$tmp/Packages")
|
||||
if [ "$code" = "200" ] && [ -s "$tmp/Packages" ] && grep -q '^Package: ' "$tmp/Packages"; then
|
||||
n_pkg=$(grep -c '^Package: ' "$tmp/Packages" || true)
|
||||
ok "outside Packages" "${SUITE} HTTP 200 · ${n_pkg} packages"
|
||||
else
|
||||
fail "outside Packages" "HTTP $code — $INDEX"
|
||||
# still try more probes, but cannot compare versions without index
|
||||
fi
|
||||
|
||||
# Packages.gz (apt often prefers this)
|
||||
PKGZ_URL="${APT_BASE}/dists/${SUITE}/main/binary-amd64/Packages.gz"
|
||||
code=$(http_get "$PKGZ_URL" "$tmp/Packages.gz")
|
||||
if [ "$code" = "200" ] && [ -s "$tmp/Packages.gz" ]; then
|
||||
ok "outside Packages.gz" "${SUITE} HTTP 200 · $(wc -c <"$tmp/Packages.gz" | tr -d ' ') bytes"
|
||||
else
|
||||
warn "outside Packages.gz" "HTTP $code — apt may still use plain Packages"
|
||||
fi
|
||||
|
||||
# Sample pool .deb downloadable (proves packages are installable, not just listed)
|
||||
if [ -s "$tmp/Packages" ]; then
|
||||
DEB_PATH=$(awk '
|
||||
/^Package: taler-exchange$/ { p=1 }
|
||||
p && /^Filename: / { sub(/^Filename: /,""); print; exit }
|
||||
p && /^$/ { p=0 }
|
||||
' "$tmp/Packages")
|
||||
[ -z "$DEB_PATH" ] && DEB_PATH=$(awk '
|
||||
/^Package: taler-merchant$/ { p=1 }
|
||||
p && /^Filename: / { sub(/^Filename: /,""); print; exit }
|
||||
p && /^$/ { p=0 }
|
||||
' "$tmp/Packages")
|
||||
if [ -n "$DEB_PATH" ]; then
|
||||
DEB_URL="${APT_BASE}/${DEB_PATH}"
|
||||
code=$(curl -sS --max-time 30 -o /dev/null -w '%{http_code}' -r 0-128 "$DEB_URL" 2>/dev/null || echo 000)
|
||||
case "$code" in
|
||||
200|206)
|
||||
ok "outside pool .deb" "$(basename "$DEB_PATH") HTTP $code"
|
||||
;;
|
||||
*)
|
||||
fail "outside pool .deb" "HTTP $code — $DEB_URL (index ok but debs not fetchable)"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
warn "outside pool .deb" "no Filename for taler-exchange/merchant in index"
|
||||
fi
|
||||
|
||||
for pkg in taler-exchange taler-merchant libeufin-bank; do
|
||||
ver=$(awk -v p="$pkg" '
|
||||
$0=="Package: "p { hit=1; next }
|
||||
hit && /^Version: / { sub(/^Version: /,""); print; exit }
|
||||
hit && /^$/ { hit=0 }
|
||||
' "$tmp/Packages")
|
||||
if [ -n "$ver" ]; then
|
||||
ok "outside suite offers ${pkg}" "$ver"
|
||||
else
|
||||
fail "outside suite offers ${pkg}" "missing from ${SUITE} Packages"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
HAVE_TESTING=0
|
||||
code=$(http_get "$TESTING_INDEX" "$tmp/Packages-testing")
|
||||
if [ "$code" = "200" ] && [ -s "$tmp/Packages-testing" ] && grep -q '^Package: ' "$tmp/Packages-testing"; then
|
||||
HAVE_TESTING=1
|
||||
ok "outside ${SUITE}-testing Packages" "HTTP 200 (optional compare)"
|
||||
else
|
||||
info "outside ${SUITE}-testing Packages" "HTTP ${code:-000} (optional)"
|
||||
fi
|
||||
|
||||
# TLS: certificate verify (curl default) already used above; explicit openssl probe if available
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
if echo | openssl s_client -servername "$HOST_APT" -connect "${HOST_APT}:443" 2>/dev/null \
|
||||
| grep -q 'Verify return code: 0'; then
|
||||
ok "outside TLS ${HOST_APT}" "verify ok"
|
||||
else
|
||||
# curl succeeded with default CA — soft warn only
|
||||
warn "outside TLS ${HOST_APT}" "openssl verify not clean (curl may still work)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Need Packages index for later compare
|
||||
if [ ! -s "$tmp/Packages" ] || ! grep -q '^Package: ' "$tmp/Packages"; then
|
||||
fail "outside Packages usable" "cannot continue version compare without suite index"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2) INSIDE — containers can reach deb.taler.net (pasta / install path)
|
||||
# ---------------------------------------------------------------------------
|
||||
section "versions · inside · containers → deb.taler.net"
|
||||
|
||||
if [ "${SKIP_SSH}" = "1" ]; then
|
||||
warn "ssh" "SKIP_SSH=1 — skip container reachability + installed package compare"
|
||||
info "outside-only" "deb.taler.net public checks completed above"
|
||||
summary
|
||||
exit 0
|
||||
fi
|
||||
if ! koopa_ssh_ok; then
|
||||
err "ssh" "cannot reach ${KOOPA_SSH} — set SKIP_SSH=1 to skip container checks"
|
||||
info "outside-only" "deb.taler.net public checks completed above"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
ok "ssh ${KOOPA_SSH}"
|
||||
|
||||
INRELEASE_URL="${APT_BASE}/dists/${SUITE}/InRelease"
|
||||
# Write remote script to a file (avoids bash 3.2 parse bugs with case/;; inside $(…)<<heredoc).
|
||||
{
|
||||
printf 'INRELEASE_URL=%q\n' "$INRELEASE_URL"
|
||||
cat <<'REMOTE'
|
||||
set +e
|
||||
resolve_ctr() {
|
||||
local want="$1" c
|
||||
c=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -Fx "$want" | head -1)
|
||||
if [ -n "$c" ]; then echo "$c"; return; fi
|
||||
case "$want" in
|
||||
*exchange*) podman ps --format '{{.Names}}' 2>/dev/null | grep -i exchange | head -1 ;;
|
||||
*bank*) podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'hacktivism-bank|taler-bank' | head -1 ;;
|
||||
*)
|
||||
c=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -E '^taler-hacktivism$' | head -1)
|
||||
if [ -n "$c" ]; then echo "$c"; return; fi
|
||||
podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'merchant|hacktivism' | grep -viE 'bank|exchange' | head -1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
for want in taler-hacktivism-exchange-ansible taler-hacktivism-bank taler-hacktivism; do
|
||||
c=$(resolve_ctr "$want")
|
||||
[ -n "$c" ] || continue
|
||||
role=other
|
||||
case "$c" in
|
||||
*exchange*) role=exchange ;;
|
||||
*bank*) role=bank ;;
|
||||
*) role=merchant ;;
|
||||
esac
|
||||
code=$(podman exec "$c" curl -sS -m 12 -o /dev/null -w '%{http_code}' "$INRELEASE_URL" 2>/dev/null || echo 000)
|
||||
echo "R|${role}|${c}|${code}"
|
||||
if podman exec "$c" bash -lc 'grep -Rqs deb.taler.net /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null'; then
|
||||
echo "S|${role}|${c}|yes"
|
||||
else
|
||||
echo "S|${role}|${c}|no"
|
||||
fi
|
||||
podman exec "$c" dpkg-query -W -f '${Package}\t${Version}\n' 2>/dev/null \
|
||||
| awk -v role="$role" -v ctr="$c" '
|
||||
$1 ~ /^(taler-|libeufin-|libtaler|libdonau)/ {
|
||||
printf "P|%s|%s|%s|%s\n", role, ctr, $1, $2
|
||||
}'
|
||||
done
|
||||
REMOTE
|
||||
} >"$tmp/remote-versions.sh"
|
||||
|
||||
REMOTE_OUT=$(koopa_ssh_bash 60 <"$tmp/remote-versions.sh" || true)
|
||||
|
||||
printf '%s\n' "$REMOTE_OUT" | grep -E '^R\|' >"$tmp/reach.tsv" || true
|
||||
printf '%s\n' "$REMOTE_OUT" | grep -E '^S\|' >"$tmp/sources.tsv" || true
|
||||
printf '%s\n' "$REMOTE_OUT" | grep -E '^P\|' >"$tmp/installed.tsv" || true
|
||||
|
||||
if [ -s "$tmp/reach.tsv" ]; then
|
||||
while IFS='|' read -r _ role ctr code; do
|
||||
if [ "$code" = "200" ]; then
|
||||
ok "container ${role} → deb.taler.net" "${ctr} InRelease HTTP $code"
|
||||
else
|
||||
fail "container ${role} → deb.taler.net" "${ctr} InRelease HTTP $code — apt install/update will fail"
|
||||
fi
|
||||
done <"$tmp/reach.tsv"
|
||||
else
|
||||
warn "container → deb.taler.net" "no reachability rows"
|
||||
fi
|
||||
|
||||
if [ -s "$tmp/sources.tsv" ]; then
|
||||
while IFS='|' read -r _ role ctr has; do
|
||||
if [ "$has" = "yes" ]; then
|
||||
ok "container ${role} apt source" "${ctr} lists deb.taler.net"
|
||||
else
|
||||
warn "container ${role} apt source" "${ctr} no deb.taler.net in sources.list*"
|
||||
fi
|
||||
done <"$tmp/sources.tsv"
|
||||
fi
|
||||
|
||||
if [ ! -s "$tmp/installed.tsv" ]; then
|
||||
fail "installed packages" "none found in taler containers"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
n_inst=$(wc -l <"$tmp/installed.tsv" | tr -d ' ')
|
||||
ok "collected installed packages" "${n_inst} rows"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3) compare installed vs trixie (and note testing)
|
||||
# ---------------------------------------------------------------------------
|
||||
section "versions · compare installed vs ${SUITE}"
|
||||
|
||||
export BEHIND_MODE HAVE_TESTING
|
||||
export TALER_APT_SUITE="$SUITE"
|
||||
CORE_PKGS="taler-exchange taler-exchange-database taler-merchant taler-merchant-webui libeufin-bank libeufin-common libtalerexchange libtalermerchant taler-terms-generator"
|
||||
export CORE_PKGS
|
||||
|
||||
python3 - "$tmp/Packages" "$tmp/Packages-testing" "$tmp/installed.tsv" <<'PY' >"$tmp/py.out"
|
||||
import sys, os
|
||||
from collections import defaultdict
|
||||
|
||||
main_idx, test_idx, inst_path = sys.argv[1:4]
|
||||
have_testing = os.environ.get("HAVE_TESTING", "0") == "1"
|
||||
core = set(os.environ.get("CORE_PKGS", "").split())
|
||||
suite = os.environ.get("TALER_APT_SUITE", "trixie")
|
||||
|
||||
def _ord_ch(c):
|
||||
if c == "~":
|
||||
return -1
|
||||
if c.isdigit():
|
||||
return 0
|
||||
if c.isalpha():
|
||||
return ord(c)
|
||||
return ord(c) + 256
|
||||
|
||||
def _split_ver(v):
|
||||
epoch = 0
|
||||
if ":" in v:
|
||||
e, v = v.split(":", 1)
|
||||
try:
|
||||
epoch = int(e)
|
||||
except ValueError:
|
||||
epoch = 0
|
||||
if "-" in v:
|
||||
upstream, deb = v.rsplit("-", 1)
|
||||
else:
|
||||
upstream, deb = v, ""
|
||||
return epoch, upstream, deb
|
||||
|
||||
def _cmp_part(a, b):
|
||||
ia = ib = 0
|
||||
while ia < len(a) or ib < len(b):
|
||||
while True:
|
||||
ca = a[ia] if ia < len(a) and not a[ia].isdigit() else ""
|
||||
cb = b[ib] if ib < len(b) and not b[ib].isdigit() else ""
|
||||
if ca == "" and cb == "":
|
||||
break
|
||||
if ca == "" and cb:
|
||||
return -1 if _ord_ch(cb) > 0 else (1 if cb == "~" else -1)
|
||||
if cb == "" and ca:
|
||||
return 1 if _ord_ch(ca) > 0 else (-1 if ca == "~" else 1)
|
||||
oa, ob = _ord_ch(ca), _ord_ch(cb)
|
||||
ia += 1
|
||||
ib += 1
|
||||
if oa != ob:
|
||||
return (oa > ob) - (oa < ob)
|
||||
sa = sb = ""
|
||||
while ia < len(a) and a[ia].isdigit():
|
||||
sa += a[ia]; ia += 1
|
||||
while ib < len(b) and b[ib].isdigit():
|
||||
sb += b[ib]; ib += 1
|
||||
na = int(sa) if sa else 0
|
||||
nb = int(sb) if sb else 0
|
||||
if na != nb:
|
||||
return (na > nb) - (na < nb)
|
||||
return 0
|
||||
|
||||
def deb_cmp(a, b):
|
||||
if a == b:
|
||||
return 0
|
||||
ea, ua, da = _split_ver(a)
|
||||
eb, ub, db = _split_ver(b)
|
||||
if ea != eb:
|
||||
return (ea > eb) - (ea < eb)
|
||||
c = _cmp_part(ua, ub)
|
||||
if c:
|
||||
return c
|
||||
return _cmp_part(da, db)
|
||||
|
||||
def parse_packages(path):
|
||||
pkgs = {}
|
||||
if not path or not os.path.isfile(path) or os.path.getsize(path) == 0:
|
||||
return pkgs
|
||||
cur = ver = None
|
||||
with open(path, encoding="utf-8", errors="replace") as f:
|
||||
for line in f:
|
||||
line = line.rstrip("\n")
|
||||
if line.startswith("Package: "):
|
||||
if cur and ver and (cur not in pkgs or deb_cmp(ver, pkgs[cur]) > 0):
|
||||
pkgs[cur] = ver
|
||||
cur = line[9:].strip()
|
||||
ver = None
|
||||
elif line.startswith("Version: ") and cur:
|
||||
ver = line[9:].strip()
|
||||
elif line == "" and cur:
|
||||
if ver and (cur not in pkgs or deb_cmp(ver, pkgs[cur]) > 0):
|
||||
pkgs[cur] = ver
|
||||
cur = ver = None
|
||||
if cur and ver and (cur not in pkgs or deb_cmp(ver, pkgs[cur]) > 0):
|
||||
pkgs[cur] = ver
|
||||
return pkgs
|
||||
|
||||
main = parse_packages(main_idx)
|
||||
testing = parse_packages(test_idx) if have_testing else {}
|
||||
|
||||
by_pkg = defaultdict(list)
|
||||
with open(inst_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line.startswith("P|"):
|
||||
continue
|
||||
parts = line.split("|")
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
_, role, ctr, pkg, ver = parts[:5]
|
||||
by_pkg[pkg].append((role, ctr, ver))
|
||||
|
||||
for pkg in sorted(by_pkg.keys()):
|
||||
versions = sorted({v for _, _, v in by_pkg[pkg]}, key=lambda v: v)
|
||||
# pick "highest" via deb_cmp
|
||||
inst = versions[0]
|
||||
for v in versions[1:]:
|
||||
if deb_cmp(v, inst) > 0:
|
||||
inst = v
|
||||
roles = ",".join(sorted({r for r, _, _ in by_pkg[pkg]}))
|
||||
multi = len(set(versions)) > 1
|
||||
trixie = main.get(pkg)
|
||||
testv = testing.get(pkg)
|
||||
status = "ok"
|
||||
detail = ""
|
||||
if multi:
|
||||
status = "warn"
|
||||
detail = "multiple installed: " + ", ".join(sorted(set(versions)))
|
||||
if trixie is None:
|
||||
if status == "ok":
|
||||
status = "info"
|
||||
detail = (detail + "; " if detail else "") + f"not in {suite} main index"
|
||||
else:
|
||||
c = deb_cmp(inst, trixie)
|
||||
if c == 0:
|
||||
detail = (detail + "; " if detail else "") + f"= {suite} {trixie}"
|
||||
elif c < 0:
|
||||
status = "behind"
|
||||
detail = f"installed {inst} < {suite} {trixie}"
|
||||
else:
|
||||
status = "ahead"
|
||||
detail = f"installed {inst} > {suite} {trixie}"
|
||||
if testv and deb_cmp(inst, testv) == 0:
|
||||
detail += f" (={suite}-testing)"
|
||||
elif testv:
|
||||
detail += f" (testing has {testv})"
|
||||
core_flag = "1" if pkg in core else "0"
|
||||
print(f"{status}|{pkg}|{inst}|{trixie or '-'}|{roles}|{core_flag}|{detail}")
|
||||
PY
|
||||
|
||||
ok_n=0; ahead_n=0; behind_n=0
|
||||
while IFS='|' read -r status pkg inst trixie roles core detail; do
|
||||
[ -n "${status:-}" ] || continue
|
||||
label="pkg ${pkg} (${roles})"
|
||||
case "$status" in
|
||||
ok)
|
||||
ok "$label" "${inst}"
|
||||
ok_n=$((ok_n + 1))
|
||||
;;
|
||||
ahead)
|
||||
info "$label" "$detail"
|
||||
ahead_n=$((ahead_n + 1))
|
||||
;;
|
||||
behind)
|
||||
behind_n=$((behind_n + 1))
|
||||
if [ "$core" = "1" ] || [ "$BEHIND_MODE" = "error" ]; then
|
||||
fail "$label" "$detail"
|
||||
else
|
||||
warn "$label" "$detail"
|
||||
fi
|
||||
;;
|
||||
warn)
|
||||
warn "$label" "$detail"
|
||||
;;
|
||||
info)
|
||||
info "$label" "installed ${inst}${detail:+ — $detail}"
|
||||
;;
|
||||
*)
|
||||
info "$label" "$status $detail"
|
||||
;;
|
||||
esac
|
||||
done <"$tmp/py.out"
|
||||
|
||||
# Core packages must exist somewhere
|
||||
for need in taler-exchange libeufin-bank taler-merchant; do
|
||||
if grep -qE "^P\|[^|]+\|[^|]+\|${need}\|" "$tmp/installed.tsv"; then
|
||||
ok "core installed ${need}"
|
||||
else
|
||||
fail "core installed ${need}" "not present in any taler container"
|
||||
fi
|
||||
done
|
||||
|
||||
info "suite" "deb.taler.net ${SUITE} (testing_index=${HAVE_TESTING})"
|
||||
info "tally" "match=${ok_n} ahead=${ahead_n} behind=${behind_n}"
|
||||
|
||||
summary
|
||||
7
scripts/taler-monitoring/harness-live.txt
Normal file
7
scripts/taler-monitoring/harness-live.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Optional: taler-harness against *our* exchange (needs a harness version
|
||||
# that accepts current /config JSON). Older installs often FAIL codec checks
|
||||
# even when the exchange is fine — trust `./taler-monitoring.sh urls` first.
|
||||
#
|
||||
# id<TAB>command
|
||||
|
||||
lint-exchange-goa taler-harness deployment lint-exchange-url https://exchange.hacktivism.ch/
|
||||
41
scripts/taler-monitoring/harness-tests.txt
Normal file
41
scripts/taler-monitoring/harness-tests.txt
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Real-life–relevant taler-harness integration tests
|
||||
# (from: taler-harness list-integrationtests)
|
||||
#
|
||||
# Criteria: everyday user/merchant/bank paths — withdraw, pay, deposit, refund,
|
||||
# libeufin bank, templates/paywall. Excludes KYC edge cases, experimental,
|
||||
# timetravel, perf, backup, mailbox, most fault-injection.
|
||||
#
|
||||
# These spin up a local TESTKUDOS stack (same client code paths as production).
|
||||
# For tests against *public* demos, see harness-live.txt / `./taler-monitoring.sh live`.
|
||||
|
||||
# --- Core path: get money + spend it (the GOA story) ---
|
||||
simple-payment
|
||||
withdrawal-bank-integrated
|
||||
payment
|
||||
|
||||
# --- Withdraw variants users actually hit ---
|
||||
withdrawal-manual
|
||||
withdrawal-external
|
||||
withdrawal-idempotent
|
||||
|
||||
# --- Pay variants shops use ---
|
||||
payment-template
|
||||
paywall-flow
|
||||
payment-idempotency
|
||||
payment-zero
|
||||
payment-abort
|
||||
otp
|
||||
|
||||
# --- After pay: coins / merchant ops ---
|
||||
deposit
|
||||
refund
|
||||
refund-auto
|
||||
wallet-refresh
|
||||
|
||||
# --- Bank stack we run (libeufin / regional) ---
|
||||
libeufin-bank
|
||||
bank-api
|
||||
|
||||
# --- Client hygiene ---
|
||||
wallet-config
|
||||
term-of-service-format
|
||||
536
scripts/taler-monitoring/lib.sh
Executable file
536
scripts/taler-monitoring/lib.sh
Executable file
|
|
@ -0,0 +1,536 @@
|
|||
# shellcheck shell=bash
|
||||
# Shared helpers for taler-monitoring (laptop or koopa).
|
||||
|
||||
# Default stack = GOA / hacktivism (overridden by TALER_DOMAIN / --domain)
|
||||
: "${TALER_DOMAIN:=hacktivism.ch}"
|
||||
: "${BANK_PUBLIC:=https://bank.hacktivism.ch}"
|
||||
: "${EXCHANGE_PUBLIC:=https://exchange.hacktivism.ch}"
|
||||
: "${MERCHANT_PUBLIC:=https://taler.hacktivism.ch}"
|
||||
: "${BANK_LOCAL:=http://127.0.0.1:9012}"
|
||||
: "${EXCHANGE_LOCAL:=http://127.0.0.1:9011}"
|
||||
: "${MERCHANT_LOCAL:=https://127.0.0.1:9010}"
|
||||
: "${LANDING_LOCAL:=http://127.0.0.1:9013}"
|
||||
: "${KOOPA_SSH:=koopa}"
|
||||
: "${MERCHANT_INSTANCE:=goa-demo-cp4zqk}"
|
||||
: "${WITHDRAW_AMT:=GOA:20}" # single-shot fallback; e2e ladder uses ATM notes
|
||||
: "${PAY_AMT:=GOA:0.01}"
|
||||
: "${CREDIT_AMT:=GOA:400}" # covers ATM ladder 20+50+100+200
|
||||
: "${TIMEOUT:=12}"
|
||||
: "${E2E_TIMEOUT:=55}" # whole e2e budget; skip rest when exceeded
|
||||
: "${E2E_PAY_SECS:=22}" # dedicated seconds for pay handle-uri (avoid Alarm clock)
|
||||
# Devtest: inject reserve credit via wire-gateway admin/add-incoming (optional).
|
||||
# Default off once wirewatch DNS works; set E2E_FAKE_INCOMING=1 to force.
|
||||
: "${E2E_FAKE_INCOMING:=0}"
|
||||
# SSH must never hang the monitoring run
|
||||
: "${SSH_CONNECT_TIMEOUT:=3}"
|
||||
: "${SSH_CMD_TIMEOUT:=12}" # hard cap for whole remote script (seconds)
|
||||
: "${SKIP_SSH:=0}"
|
||||
# Expected currency for public /config checks (empty = report only, don't fail)
|
||||
: "${EXPECT_CURRENCY:=GOA}"
|
||||
# 1 = this is the local koopa/hacktivism stack (inside/e2e/SSH make sense)
|
||||
: "${LOCAL_STACK:=1}"
|
||||
# Probe merchant host candidates when applying a generic domain (0=off)
|
||||
: "${TALER_DOMAIN_PROBE:=1}"
|
||||
|
||||
BANK_PUBLIC=${BANK_PUBLIC%/}
|
||||
EXCHANGE_PUBLIC=${EXCHANGE_PUBLIC%/}
|
||||
MERCHANT_PUBLIC=${MERCHANT_PUBLIC%/}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain presets → public bank / exchange / merchant base URLs
|
||||
#
|
||||
# TALER_DOMAIN=hacktivism.ch (default, GOA, local stack)
|
||||
# TALER_DOMAIN=taler.net → demo.taler.net (KUDOS)
|
||||
# TALER_DOMAIN=demo.taler.net
|
||||
# TALER_DOMAIN=taler-ops.ch → exchange.taler-ops.ch (CHF; bank/merchant if up)
|
||||
# TALER_DOMAIN=example.org → bank/exchange/backend|taler|merchant.example.org
|
||||
#
|
||||
# Explicit BANK_PUBLIC / EXCHANGE_PUBLIC / MERCHANT_PUBLIC still win if set
|
||||
# *after* apply_taler_domain, or pass full URLs via --bank/--exchange/--merchant.
|
||||
# ---------------------------------------------------------------------------
|
||||
_normalize_domain() {
|
||||
local d="$1"
|
||||
d="${d#https://}"
|
||||
d="${d#http://}"
|
||||
d="${d%%/*}"
|
||||
d="${d%%:*}"
|
||||
# Strip service host prefix only if a real domain remains (has a dot).
|
||||
# e.g. bank.demo.taler.net → demo.taler.net, taler.hacktivism.ch → hacktivism.ch
|
||||
# but NOT taler.net → net
|
||||
case "$d" in
|
||||
bank.*|exchange.*|taler.*|backend.*|merchant.*|shop.*|libeufin.*)
|
||||
rest="${d#*.}"
|
||||
if [[ "$rest" == *.* ]]; then
|
||||
d="$rest"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
printf '%s' "$d"
|
||||
}
|
||||
|
||||
_probe_https_config() {
|
||||
# 0 if https://$1/config returns 200
|
||||
local host="$1" code
|
||||
code=$(curl -skS --max-redirs 0 -m 4 -o /dev/null -w '%{http_code}' "https://${host}/config" 2>/dev/null || echo 000)
|
||||
[ "$code" = "200" ]
|
||||
}
|
||||
|
||||
apply_taler_domain() {
|
||||
local raw="${1:-}"
|
||||
local d
|
||||
[ -n "$raw" ] || return 0
|
||||
d=$(_normalize_domain "$raw")
|
||||
TALER_DOMAIN="$d"
|
||||
|
||||
case "$d" in
|
||||
# Local koopa stack only — SSH / inside / e2e allowed
|
||||
koopa|hacktivism.ch|hacktivism)
|
||||
BANK_PUBLIC="https://bank.hacktivism.ch"
|
||||
EXCHANGE_PUBLIC="https://exchange.hacktivism.ch"
|
||||
MERCHANT_PUBLIC="https://taler.hacktivism.ch"
|
||||
EXPECT_CURRENCY="GOA"
|
||||
LOCAL_STACK=1
|
||||
SKIP_SSH=0
|
||||
TALER_DOMAIN="hacktivism.ch"
|
||||
: "${WITHDRAW_AMT:=GOA:20}"
|
||||
: "${PAY_AMT:=GOA:0.01}"
|
||||
: "${CREDIT_AMT:=GOA:400}"
|
||||
;;
|
||||
taler.net|demo.taler.net)
|
||||
# Official public demo (KUDOS) — public only, never SSH; tiny e2e amounts
|
||||
BANK_PUBLIC="https://bank.demo.taler.net"
|
||||
EXCHANGE_PUBLIC="https://exchange.demo.taler.net"
|
||||
MERCHANT_PUBLIC="https://backend.demo.taler.net"
|
||||
EXPECT_CURRENCY="KUDOS"
|
||||
LOCAL_STACK=0
|
||||
SKIP_SSH=1
|
||||
MERCHANT_INSTANCE="${MERCHANT_INSTANCE:-sandbox}"
|
||||
WITHDRAW_AMT="${WITHDRAW_AMT:-KUDOS:20}"
|
||||
PAY_AMT="${PAY_AMT:-KUDOS:0.01}"
|
||||
CREDIT_AMT="${CREDIT_AMT:-KUDOS:100}"
|
||||
TALER_DOMAIN="demo.taler.net"
|
||||
;;
|
||||
taler-ops.ch)
|
||||
# Public CHF exchange; bank/merchant hosts vary — probe common names
|
||||
EXCHANGE_PUBLIC="https://exchange.taler-ops.ch"
|
||||
BANK_PUBLIC="https://bank.taler-ops.ch"
|
||||
MERCHANT_PUBLIC="https://backend.taler-ops.ch"
|
||||
EXPECT_CURRENCY="CHF"
|
||||
LOCAL_STACK=0
|
||||
SKIP_SSH=1
|
||||
WITHDRAW_AMT="${WITHDRAW_AMT:-CHF:20}"
|
||||
PAY_AMT="${PAY_AMT:-CHF:0.01}"
|
||||
CREDIT_AMT="${CREDIT_AMT:-CHF:100}"
|
||||
if [ "${TALER_DOMAIN_PROBE}" = "1" ]; then
|
||||
local h
|
||||
for h in bank.taler-ops.ch bank.demo.taler-ops.ch; do
|
||||
_probe_https_config "$h" && { BANK_PUBLIC="https://$h"; break; }
|
||||
done
|
||||
for h in backend.taler-ops.ch merchant.taler-ops.ch taler.taler-ops.ch shop.taler-ops.ch; do
|
||||
_probe_https_config "$h" && { MERCHANT_PUBLIC="https://$h"; break; }
|
||||
done
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Any other domain — public HTTPS only, never SSH to koopa
|
||||
BANK_PUBLIC="https://bank.${d}"
|
||||
EXCHANGE_PUBLIC="https://exchange.${d}"
|
||||
MERCHANT_PUBLIC="https://backend.${d}"
|
||||
EXPECT_CURRENCY="${EXPECT_CURRENCY:-}" # unknown — don't hard-fail currency
|
||||
LOCAL_STACK=0
|
||||
SKIP_SSH=1
|
||||
if [ "${TALER_DOMAIN_PROBE}" = "1" ]; then
|
||||
local h
|
||||
for h in "backend.${d}" "taler.${d}" "merchant.${d}" "shop.${d}"; do
|
||||
_probe_https_config "$h" && { MERCHANT_PUBLIC="https://$h"; break; }
|
||||
done
|
||||
for h in "bank.${d}" "libeufin.${d}"; do
|
||||
_probe_https_config "$h" && { BANK_PUBLIC="https://$h"; break; }
|
||||
done
|
||||
_probe_https_config "exchange.${d}" || true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
BANK_PUBLIC=${BANK_PUBLIC%/}
|
||||
EXCHANGE_PUBLIC=${EXCHANGE_PUBLIC%/}
|
||||
MERCHANT_PUBLIC=${MERCHANT_PUBLIC%/}
|
||||
|
||||
# Hard rule: only the local koopa/hacktivism stack may use SSH
|
||||
if [ "${LOCAL_STACK}" != "1" ]; then
|
||||
SKIP_SSH=1
|
||||
fi
|
||||
}
|
||||
|
||||
# Apply TALER_DOMAIN from env once (CLI exports TALER_DOMAIN_APPLIED=1 after overrides).
|
||||
if [ "${TALER_DOMAIN_APPLIED:-0}" != "1" ] \
|
||||
&& [ -n "${TALER_DOMAIN:-}" ] && [ "${TALER_DOMAIN}" != "hacktivism.ch" ]; then
|
||||
apply_taler_domain "$TALER_DOMAIN"
|
||||
TALER_DOMAIN_APPLIED=1
|
||||
fi
|
||||
|
||||
# Safe SSH: publickey only, short connect, overall alarm so we never block forever.
|
||||
SSH_BASE_OPTS=(
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="${SSH_CONNECT_TIMEOUT}"
|
||||
-o ConnectionAttempts=1
|
||||
-o ServerAliveInterval=3
|
||||
-o ServerAliveCountMax=2
|
||||
-o StrictHostKeyChecking=accept-new
|
||||
-o PreferredAuthentications=publickey
|
||||
-o PasswordAuthentication=no
|
||||
-o KbdInteractiveAuthentication=no
|
||||
-o GSSAPIAuthentication=no
|
||||
-o NumberOfPasswordPrompts=0
|
||||
)
|
||||
|
||||
# Hard wall-clock timeout so ssh/curl never block the monitoring run forever.
|
||||
with_timeout() {
|
||||
local secs="$1"; shift
|
||||
if command -v gtimeout >/dev/null 2>&1; then
|
||||
gtimeout --kill-after=2 "$secs" "$@"
|
||||
return $?
|
||||
fi
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout -k 2 "$secs" "$@" 2>/dev/null || timeout --kill-after=2 "$secs" "$@"
|
||||
return $?
|
||||
fi
|
||||
# Portable: perl alarm + process group kill
|
||||
perl -e '
|
||||
use strict; use warnings;
|
||||
my $secs = shift @ARGV;
|
||||
my $pid = fork();
|
||||
die "fork: $!" unless defined $pid;
|
||||
if ($pid == 0) {
|
||||
setpgrp(0, 0);
|
||||
exec @ARGV;
|
||||
exit 127;
|
||||
}
|
||||
$SIG{ALRM} = sub {
|
||||
kill "TERM", -$pid;
|
||||
select(undef, undef, undef, 1.0);
|
||||
kill "KILL", -$pid;
|
||||
exit 124;
|
||||
};
|
||||
alarm $secs;
|
||||
waitpid($pid, 0);
|
||||
my $code = $? >> 8;
|
||||
alarm 0;
|
||||
exit $code;
|
||||
' "$secs" "$@"
|
||||
}
|
||||
|
||||
# Probe: 0 if koopa SSH works quickly
|
||||
koopa_ssh_ok() {
|
||||
[ "${SKIP_SSH}" = "1" ] && return 1
|
||||
with_timeout $((SSH_CONNECT_TIMEOUT + 3)) \
|
||||
ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" 'echo ok' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Run remote bash -s with optional stdin script; hard-capped
|
||||
# usage: koopa_ssh_bash [timeout_secs] <<'EOF' ... EOF
|
||||
# or: koopa_ssh_run timeout_secs 'remote command'
|
||||
koopa_ssh_run() {
|
||||
local t="${1:-$SSH_CMD_TIMEOUT}"
|
||||
shift
|
||||
with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" "$@"
|
||||
}
|
||||
|
||||
koopa_ssh_bash() {
|
||||
local t="${1:-$SSH_CMD_TIMEOUT}"
|
||||
with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" 'bash -s'
|
||||
}
|
||||
|
||||
if [ "${NO_COLOR:-0}" = "1" ] || [ ! -t 1 ]; then
|
||||
G= R= Y= C= N= B=
|
||||
else
|
||||
G=$'\e[32m'; R=$'\e[31m'; Y=$'\e[33m'; C=$'\e[36m'; B=$'\e[1m'; N=$'\e[0m'
|
||||
fi
|
||||
|
||||
PASS_N=0
|
||||
FAIL_N=0
|
||||
WARN_N=0
|
||||
INFO_N=0
|
||||
BLOCKERS=() # human-readable payment/withdraw blockers
|
||||
ERRORS=() # all ERROR lines (component scope)
|
||||
|
||||
# Test IDs by area: www-001, e2e-001, inside-001, …
|
||||
# Usage: set_area www then each ok/fail/warn/info/blocker/err auto-numbers.
|
||||
TEST_AREA=""
|
||||
TEST_N=0
|
||||
# Last issued id (www-001); set by _take_tid — not via $(…) so TEST_N persists.
|
||||
LAST_TID=""
|
||||
set_area() {
|
||||
TEST_AREA="$1"
|
||||
TEST_N=0
|
||||
LAST_TID=""
|
||||
}
|
||||
# Assign next id into LAST_TID (must not run in a subshell).
|
||||
_take_tid() {
|
||||
LAST_TID=""
|
||||
if [ -z "${TEST_AREA:-}" ]; then
|
||||
return
|
||||
fi
|
||||
TEST_N=$((TEST_N + 1))
|
||||
LAST_TID=$(printf '%s-%03d' "$TEST_AREA" "$TEST_N")
|
||||
}
|
||||
_fmt_tid() {
|
||||
# prefix "www-001 " or empty
|
||||
if [ -n "${LAST_TID:-}" ]; then
|
||||
printf '%s ' "$LAST_TID"
|
||||
fi
|
||||
}
|
||||
|
||||
ok() {
|
||||
local label="$1"
|
||||
_take_tid
|
||||
printf '%s[OK]%s %s%s\n' "$G" "$N" "$(_fmt_tid)" "$label"
|
||||
PASS_N=$((PASS_N + 1))
|
||||
}
|
||||
# component-scoped error: err bank "libeufin down" "detail"
|
||||
err() {
|
||||
local comp="$1" msg="$2" detail="${3:-}"
|
||||
_take_tid
|
||||
printf '%s[ERROR]%s %s%s: %s%s\n' "$R" "$N" "$(_fmt_tid)" "$comp" "$msg" "${detail:+ — $detail}"
|
||||
FAIL_N=$((FAIL_N + 1))
|
||||
ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ — $detail}")
|
||||
}
|
||||
# legacy fail label ...
|
||||
fail() {
|
||||
local label="$1" detail="${2:-}"
|
||||
_take_tid
|
||||
printf '%s[ERROR]%s %s%s%s\n' "$R" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}"
|
||||
FAIL_N=$((FAIL_N + 1))
|
||||
ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ — $detail}")
|
||||
}
|
||||
warn() {
|
||||
local label="$1" detail="${2:-}"
|
||||
_take_tid
|
||||
printf '%s[WARN]%s %s%s%s\n' "$Y" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}"
|
||||
WARN_N=$((WARN_N + 1))
|
||||
}
|
||||
info() {
|
||||
local label="$1" detail="${2:-}"
|
||||
_take_tid
|
||||
if [ -n "$detail" ]; then
|
||||
printf '%s[INFO]%s %s%s — %s\n' "$C" "$N" "$(_fmt_tid)" "$label" "$detail"
|
||||
else
|
||||
printf '%s[INFO]%s %s%s\n' "$C" "$N" "$(_fmt_tid)" "$label"
|
||||
fi
|
||||
INFO_N=$((INFO_N + 1))
|
||||
}
|
||||
blocker() {
|
||||
# Payment/withdraw path cannot proceed because of this
|
||||
local step="$1" msg="$2"
|
||||
_take_tid
|
||||
printf '%s[BLOCKER]%s %s%s: %s\n' "$R$B" "$N" "$(_fmt_tid)" "$step" "$msg"
|
||||
BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg")
|
||||
FAIL_N=$((FAIL_N + 1))
|
||||
ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg")
|
||||
}
|
||||
section() { printf '\n%s== %s ==%s\n' "$B" "$*" "$N"; }
|
||||
|
||||
summary() {
|
||||
echo ""
|
||||
if [ "${#BLOCKERS[@]}" -gt 0 ]; then
|
||||
printf '%s--- BLOCKERS (fix/withdraw path) ---%s\n' "$R$B" "$N"
|
||||
local b
|
||||
for b in "${BLOCKERS[@]}"; do
|
||||
printf '%s • %s%s\n' "$R" "$b" "$N"
|
||||
done
|
||||
fi
|
||||
if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then
|
||||
printf '%s--- ERRORS ---%s\n' "$R" "$N"
|
||||
local e
|
||||
for e in "${ERRORS[@]}"; do
|
||||
case "$e" in BLOCKER*) continue ;; esac
|
||||
printf '%s • %s%s\n' "$R" "$e" "$N"
|
||||
done
|
||||
fi
|
||||
printf 'totals: %s%d OK%s' "$G" "$PASS_N" "$N"
|
||||
[ "$FAIL_N" -gt 0 ] && printf ', %s%d ERROR%s' "$R" "$FAIL_N" "$N"
|
||||
[ "$WARN_N" -gt 0 ] && printf ', %s%d WARN%s' "$Y" "$WARN_N" "$N"
|
||||
[ "$INFO_N" -gt 0 ] && printf ', %d INFO' "$INFO_N"
|
||||
[ "${#BLOCKERS[@]}" -gt 0 ] && printf ', %s%d BLOCKER%s' "$R" "${#BLOCKERS[@]}" "$N"
|
||||
printf '\n'
|
||||
[ "$FAIL_N" -eq 0 ]
|
||||
}
|
||||
|
||||
http_code() {
|
||||
local url="$1"; shift
|
||||
curl -skS --max-redirs 0 -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$@" "$url" 2>/dev/null || echo 000
|
||||
}
|
||||
|
||||
http_body() {
|
||||
local url="$1" out="$2"; shift 2
|
||||
curl -skS --max-redirs 0 -m "${TIMEOUT}" -o "$out" -w '%{http_code}' "$@" "$url" 2>/dev/null || echo 000
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Currency unit map checks (wallet codec: alt_unit_names must include "0")
|
||||
# ---------------------------------------------------------------------------
|
||||
# Returns 0 if JSON body at $1 has usable alt_unit_names.
|
||||
# Supports:
|
||||
# - exchange/bank: currency_specification.alt_unit_names
|
||||
# - merchant: currencies.<CODE>.alt_unit_names for each code
|
||||
# Optional $2 = required currency code for currency_specification.currency
|
||||
json_has_alt_unit_names() {
|
||||
local file="$1" want_cur="${2:-}"
|
||||
python3 - "$file" "$want_cur" <<'PY'
|
||||
import json, sys
|
||||
path, want = sys.argv[1], sys.argv[2]
|
||||
try:
|
||||
d = json.load(open(path))
|
||||
except Exception as e:
|
||||
print(f"json-error: {e}")
|
||||
sys.exit(2)
|
||||
|
||||
def check_au(au, label):
|
||||
if not isinstance(au, dict) or not au:
|
||||
print(f"{label}: missing/empty alt_unit_names")
|
||||
return False
|
||||
if "0" not in au or not str(au.get("0") or "").strip():
|
||||
print(f"{label}: alt_unit_names missing non-empty key \"0\" (have {sorted(au.keys())})")
|
||||
return False
|
||||
print(f"{label}: alt_unit_names ok (0={au.get('0')!r}, n={len(au)})")
|
||||
return True
|
||||
|
||||
ok = True
|
||||
cs = d.get("currency_specification")
|
||||
if isinstance(cs, dict):
|
||||
if want and cs.get("currency") and cs.get("currency") != want:
|
||||
print(f"currency_specification.currency={cs.get('currency')!r} want {want!r}")
|
||||
ok = False
|
||||
if not check_au(cs.get("alt_unit_names"), "currency_specification"):
|
||||
ok = False
|
||||
elif "currency_specification" in d:
|
||||
print("currency_specification: not an object")
|
||||
ok = False
|
||||
|
||||
curs = d.get("currencies")
|
||||
if isinstance(curs, dict) and curs:
|
||||
for code, spec in curs.items():
|
||||
if not isinstance(spec, dict):
|
||||
print(f"currencies.{code}: not an object")
|
||||
ok = False
|
||||
continue
|
||||
if not check_au(spec.get("alt_unit_names"), f"currencies.{code}"):
|
||||
ok = False
|
||||
|
||||
if not isinstance(cs, dict) and not (isinstance(curs, dict) and curs):
|
||||
# neither shape — fail
|
||||
print("no currency_specification or currencies map")
|
||||
ok = False
|
||||
|
||||
sys.exit(0 if ok else 1)
|
||||
PY
|
||||
}
|
||||
|
||||
# Check one exchange base URL's /config for alt_unit_names.
|
||||
# $1=label $2=base_url $3=expected currency (optional) $4=strict(1) or soft(0)
|
||||
check_exchange_alt_units() {
|
||||
local label="$1" base="$2" want_cur="${3:-}" strict="${4:-1}"
|
||||
local f code
|
||||
base="${base%/}"
|
||||
f=$(mktemp)
|
||||
code=$(http_body "${base}/config" "$f")
|
||||
if [ "$code" != "200" ]; then
|
||||
rm -f "$f"
|
||||
if [ "$strict" = "1" ]; then
|
||||
fail "$label /config" "HTTP $code ($base)"
|
||||
else
|
||||
warn "$label /config" "HTTP $code ($base)"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
local out ec
|
||||
set +e
|
||||
out=$(json_has_alt_unit_names "$f" "$want_cur" 2>&1)
|
||||
ec=$?
|
||||
set -e
|
||||
rm -f "$f"
|
||||
if [ "$ec" -eq 0 ]; then
|
||||
ok "$label alt_unit_names" "$(echo "$out" | tr '\n' '; ' | sed 's/; $//')"
|
||||
else
|
||||
if [ "$strict" = "1" ]; then
|
||||
fail "$label alt_unit_names" "$(echo "$out" | tr '\n' '; ' | sed 's/; $//')"
|
||||
else
|
||||
warn "$label alt_unit_names" "$(echo "$out" | tr '\n' '; ' | sed 's/; $//')"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# From merchant /config JSON file: walk exchanges[] and check each base_url/config.
|
||||
# Local stack hosts (hacktivism.ch or $EXCHANGE_PUBLIC host) are strict; others soft.
|
||||
check_merchant_listed_exchanges_alt_units() {
|
||||
local mer_json="$1"
|
||||
local list
|
||||
list=$(python3 - "$mer_json" <<'PY'
|
||||
import json, sys
|
||||
d = json.load(open(sys.argv[1]))
|
||||
for e in d.get("exchanges") or []:
|
||||
if not isinstance(e, dict):
|
||||
continue
|
||||
u = (e.get("base_url") or e.get("url") or "").rstrip("/")
|
||||
c = e.get("currency") or ""
|
||||
if u:
|
||||
print(f"{c}\t{u}")
|
||||
PY
|
||||
)
|
||||
if [ -z "$list" ]; then
|
||||
fail "merchant exchanges[]" "empty — no exchanges to check for alt_unit_names"
|
||||
return
|
||||
fi
|
||||
local line cur url strict host
|
||||
while IFS=$'\t' read -r cur url; do
|
||||
[ -n "$url" ] || continue
|
||||
host="${url#https://}"; host="${host#http://}"; host="${host%%/*}"
|
||||
strict=1
|
||||
case "$host" in
|
||||
*hacktivism.ch) strict=1 ;;
|
||||
*)
|
||||
# foreign exchange (e.g. taler-ops) — soft unless it is our configured EXCHANGE_PUBLIC
|
||||
if [ "$url" = "${EXCHANGE_PUBLIC}" ] || [ "$url" = "${EXCHANGE_PUBLIC}/" ]; then
|
||||
strict=1
|
||||
else
|
||||
strict=0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
check_exchange_alt_units "exchange ${cur:-?} ${host}" "$url" "$cur" "$strict"
|
||||
done <<<"$list"
|
||||
}
|
||||
|
||||
SECRETS_ROOT="${SECRETS_ROOT:-}"
|
||||
if [ -z "$SECRETS_ROOT" ]; then
|
||||
for d in \
|
||||
"$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)/koopa-admin-secrets/koopa/host-root" \
|
||||
"/Users/newkamek/src/koopa/koopa-admin-secrets/koopa/host-root" \
|
||||
"$HOME/src/koopa/koopa-admin-secrets/koopa/host-root"
|
||||
do
|
||||
if [ -d "$d/taler-bank" ]; then SECRETS_ROOT=$d; break; fi
|
||||
done
|
||||
fi
|
||||
|
||||
read_secret() {
|
||||
local rel="$1"
|
||||
if [ -n "${SECRETS_ROOT:-}" ] && [ -f "${SECRETS_ROOT}/${rel}" ]; then
|
||||
tr -d '\n' <"${SECRETS_ROOT}/${rel}"
|
||||
return 0
|
||||
fi
|
||||
koopa_ssh_ok || return 1
|
||||
koopa_ssh_run 10 "tr -d '\\n' </root/$(basename "$rel") 2>/dev/null || true" 2>/dev/null
|
||||
}
|
||||
|
||||
find_wallet_cli() {
|
||||
if [ -n "${WALLET_CLI:-}" ] && [ -f "$WALLET_CLI" ]; then
|
||||
echo "$WALLET_CLI"; return 0
|
||||
fi
|
||||
for c in \
|
||||
/Users/newkamek/src/taler/taler-typescript-core/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs \
|
||||
"$(command -v taler-wallet-cli 2>/dev/null || true)"
|
||||
do
|
||||
[ -n "$c" ] && [ -f "$c" ] && { echo "$c"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
198
scripts/taler-monitoring/taler-monitoring.sh
Executable file
198
scripts/taler-monitoring/taler-monitoring.sh
Executable file
|
|
@ -0,0 +1,198 @@
|
|||
#!/usr/bin/env bash
|
||||
# taler-monitoring — public URL / stack checks for a Taler domain
|
||||
#
|
||||
# ./taler-monitoring.sh # local GOA (urls + inside + e2e)
|
||||
# ./taler-monitoring.sh -d taler.net urls # public demo, no SSH
|
||||
# ./taler-monitoring.sh --domain taler-ops.ch # public ops, no SSH
|
||||
# TALER_DOMAIN=demo.taler.net ./taler-monitoring.sh urls
|
||||
#
|
||||
# Tags: [OK] [INFO] [WARN] [ERROR] [BLOCKER]
|
||||
# Exit 0 only if every selected phase exits 0.
|
||||
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
taler-monitoring — bank / exchange / merchant checks
|
||||
|
||||
Usage:
|
||||
./taler-monitoring.sh [options] [phases...]
|
||||
|
||||
Phases:
|
||||
urls public HTTPS (no SSH) ← default with --domain
|
||||
inside container status via SSH (local stack only)
|
||||
versions taler packages vs deb.taler.net trixie + repo availability
|
||||
sanity public + optional server
|
||||
server server-side only (SSH)
|
||||
e2e withdraw + pay (small amounts; remote aborts on login/KYC)
|
||||
all urls + inside + versions + sanity + e2e (SSH phases only on koopa)
|
||||
|
||||
Options:
|
||||
-d, --domain DOMAIN target domain
|
||||
koopa / hacktivism.ch → local stack, SSH ok
|
||||
everything else → no SSH; e2e optional (tiny amounts)
|
||||
presets: koopa | hacktivism.ch | taler.net | taler-ops.ch
|
||||
generic: bank/exchange/backend.<domain>
|
||||
--bank URL override bank base (https://…)
|
||||
--exchange URL override exchange base
|
||||
--merchant URL override merchant base
|
||||
--currency CODE expected currency (GOA, KUDOS, CHF, …); empty = report only
|
||||
--no-probe do not probe alternate merchant/bank hostnames
|
||||
-h, --help
|
||||
|
||||
Examples:
|
||||
./taler-monitoring.sh -d taler.net
|
||||
./taler-monitoring.sh -d taler-ops.ch urls
|
||||
./taler-monitoring.sh -d demo.taler.net --currency KUDOS
|
||||
./taler-monitoring.sh --exchange https://exchange.taler-ops.ch urls
|
||||
|
||||
Env (same meaning):
|
||||
TALER_DOMAIN BANK_PUBLIC EXCHANGE_PUBLIC MERCHANT_PUBLIC EXPECT_CURRENCY
|
||||
SKIP_SSH=1 NO_COLOR=1
|
||||
EOF
|
||||
}
|
||||
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
PHASES=()
|
||||
DOMAIN_SET=0
|
||||
BANK_OVERRIDE=""
|
||||
EXCHANGE_OVERRIDE=""
|
||||
MERCHANT_OVERRIDE=""
|
||||
CURRENCY_OVERRIDE=""
|
||||
NO_PROBE=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-d|--domain)
|
||||
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
|
||||
apply_taler_domain "$2"
|
||||
DOMAIN_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--bank)
|
||||
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
|
||||
BANK_OVERRIDE="${2%/}"; shift 2
|
||||
;;
|
||||
--exchange)
|
||||
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
|
||||
EXCHANGE_OVERRIDE="${2%/}"; shift 2
|
||||
;;
|
||||
--merchant)
|
||||
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
|
||||
MERCHANT_OVERRIDE="${2%/}"; shift 2
|
||||
;;
|
||||
--currency)
|
||||
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
|
||||
CURRENCY_OVERRIDE="$2"; shift 2
|
||||
;;
|
||||
--no-probe) NO_PROBE=1; shift ;;
|
||||
urls|inside|versions|sanity|server|e2e|all) PHASES+=("$1"); shift ;;
|
||||
*)
|
||||
# bare domain shorthand: ./taler-monitoring.sh taler.net
|
||||
if [[ "$1" == *.* && "$1" != *://* && "$1" != -* ]]; then
|
||||
apply_taler_domain "$1"
|
||||
DOMAIN_SET=1
|
||||
shift
|
||||
else
|
||||
echo "Unknown: $1" >&2; usage >&2; exit 2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$NO_PROBE" = "1" ]; then
|
||||
TALER_DOMAIN_PROBE=0
|
||||
fi
|
||||
if [ -n "$CURRENCY_OVERRIDE" ]; then
|
||||
EXPECT_CURRENCY="$CURRENCY_OVERRIDE"
|
||||
fi
|
||||
if [ -n "$BANK_OVERRIDE" ]; then BANK_PUBLIC="$BANK_OVERRIDE"; fi
|
||||
if [ -n "$EXCHANGE_OVERRIDE" ]; then EXCHANGE_PUBLIC="$EXCHANGE_OVERRIDE"; fi
|
||||
if [ -n "$MERCHANT_OVERRIDE" ]; then MERCHANT_PUBLIC="$MERCHANT_OVERRIDE"; fi
|
||||
|
||||
# Only koopa may use SSH. Remote domains: public + optional e2e (no SSH).
|
||||
if [ "${LOCAL_STACK:-1}" != "1" ]; then
|
||||
SKIP_SSH=1
|
||||
fi
|
||||
|
||||
# Remote: no SSH; ATM withdraw ladder set in check_e2e (smaller notes)
|
||||
if [ "${LOCAL_STACK}" != "1" ]; then
|
||||
E2E_FAKE_INCOMING=0
|
||||
E2E_REMOTE=1
|
||||
: "${E2E_WITHDRAW_VALUES:=10 20 50}"
|
||||
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}"
|
||||
fi
|
||||
|
||||
# Export so check_*.sh (re-source lib) see the same targets via env
|
||||
export TALER_DOMAIN BANK_PUBLIC EXCHANGE_PUBLIC MERCHANT_PUBLIC
|
||||
export EXPECT_CURRENCY SKIP_SSH LOCAL_STACK TALER_DOMAIN_PROBE
|
||||
export WITHDRAW_AMT PAY_AMT CREDIT_AMT MERCHANT_INSTANCE
|
||||
export E2E_FAKE_INCOMING E2E_REMOTE E2E_VARIABLE E2E_ATM_MAX
|
||||
export E2E_WITHDRAW_VALUES E2E_PAY_VALUES
|
||||
export TALER_DOMAIN_APPLIED=1
|
||||
|
||||
# Default phases
|
||||
if [ "${#PHASES[@]}" -eq 0 ]; then
|
||||
if [ "${LOCAL_STACK}" = "1" ]; then
|
||||
PHASES=(urls inside versions e2e) # koopa: SSH ok
|
||||
else
|
||||
PHASES=(urls) # remote default: public only; pass "e2e" to try
|
||||
fi
|
||||
fi
|
||||
|
||||
OUT_PHASES=()
|
||||
for p in "${PHASES[@]}"; do
|
||||
if [ "$p" = "all" ]; then
|
||||
if [ "${LOCAL_STACK}" = "1" ]; then
|
||||
OUT_PHASES+=(urls inside versions sanity e2e)
|
||||
else
|
||||
# remote: repo index check only (no installed pkgs without SSH)
|
||||
OUT_PHASES+=(urls versions e2e)
|
||||
fi
|
||||
else
|
||||
OUT_PHASES+=("$p")
|
||||
fi
|
||||
done
|
||||
PHASES=()
|
||||
seen=" "
|
||||
for p in "${OUT_PHASES[@]}"; do
|
||||
# Drop SSH-only phases for non-koopa (e2e is allowed — public path only)
|
||||
# versions still runs: outside-in deb.taler.net checks; container parts soft-skip if no SSH
|
||||
if [ "${LOCAL_STACK}" != "1" ] || [ "${SKIP_SSH}" = "1" ]; then
|
||||
case "$p" in inside|server)
|
||||
echo "[INFO] skip phase '$p' (only koopa uses SSH)" >&2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
case "$seen" in *" $p "*) ;; *) PHASES+=("$p"); seen="$seen$p " ;; esac
|
||||
done
|
||||
|
||||
if [ "${#PHASES[@]}" -eq 0 ]; then
|
||||
PHASES=(urls)
|
||||
fi
|
||||
|
||||
printf 'target domain=%s\n' "${TALER_DOMAIN}"
|
||||
printf ' bank %s\n' "$BANK_PUBLIC"
|
||||
printf ' exchange %s\n' "$EXCHANGE_PUBLIC"
|
||||
printf ' merchant %s\n' "$MERCHANT_PUBLIC"
|
||||
printf ' currency expect=%s skip_ssh=%s\n' "${EXPECT_CURRENCY:-any}" "$SKIP_SSH"
|
||||
|
||||
chmod +x "$ROOT"/check_*.sh 2>/dev/null || true
|
||||
|
||||
ec=0
|
||||
for p in "${PHASES[@]}"; do
|
||||
case "$p" in
|
||||
urls) "$ROOT/check_urls.sh" || ec=1 ;;
|
||||
inside) "$ROOT/check_inside.sh" || ec=1 ;;
|
||||
versions) "$ROOT/check_versions.sh" || ec=1 ;;
|
||||
sanity) "$ROOT/check_sanity.sh" || ec=1 ;;
|
||||
server) "$ROOT/check_server.sh" || ec=1 ;;
|
||||
e2e) "$ROOT/check_e2e.sh" || ec=1 ;;
|
||||
esac
|
||||
done
|
||||
exit "$ec"
|
||||
31
scripts/taler-sanity/README.md
Normal file
31
scripts/taler-sanity/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Taler sanity checks (koopa)
|
||||
|
||||
Run **on koopa as root** (reads `/root/*-password.txt`, may use `podman` as `hernani`).
|
||||
|
||||
| Script | What it checks |
|
||||
|--------|----------------|
|
||||
| `check_helpers-running.sh` | **No systemd:** starts missing exchange/merchant helpers (`ensure_*`), then requires transfer/aggregator/wirewatch/… |
|
||||
| `check_stack-health.sh` | Public + local `/config` and exchange `/keys` HTTP 200; merchant has CHF+GOA |
|
||||
| `check_merchant-delays.sh` | Global + instance delays are short (pay/refund/wire ≤ 120s) |
|
||||
| `check_exchange-wirewatch.sh` | Wire gateway reachable with **bearer**; auth method; hosts pin |
|
||||
| `check_settlement.sh` | Paid orders: `wired`, deposit_total, bank balances/transfers |
|
||||
| `run_all.sh` | Runs all of the above |
|
||||
|
||||
Helper ensure scripts (in containers, no systemd):
|
||||
|
||||
| Script | Role |
|
||||
|--------|------|
|
||||
| `../taler-exchange/ensure_exchange_helpers.sh` | `nohup` aggregator, closer, wirewatch, **transfer** |
|
||||
| `../taler-merchant/ensure_merchant_helpers.sh` | `nohup` wirewatch, depositcheck, kyccheck, webhook, … |
|
||||
| Health: `check_*-health.sh` | Calls ensure (unless `SKIP_ENSURE=1`), then **FAIL** if still missing |
|
||||
|
||||
```bash
|
||||
# on koopa
|
||||
cd /path/to/koopa-admin-log/scripts/taler-sanity
|
||||
./run_all.sh
|
||||
./check_settlement.sh 2026.190-03V36SPAZ8CK6
|
||||
```
|
||||
|
||||
Env overrides: `BANK_URL`, `MERCHANT_URL`, `EXCHANGE_PUBLIC`, `BANK_PUBLIC`, `MERCHANT_PUBLIC`, `MERCHANT_INSTANCE`.
|
||||
|
||||
See also: `../taler-wallet-cli/` for end-to-end withdraw → pay → settlement.
|
||||
92
scripts/taler-sanity/check_exchange-wirewatch.sh
Normal file
92
scripts/taler-sanity/check_exchange-wirewatch.sh
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#!/bin/bash
|
||||
# Sanity: exchange can read bank wire gateway (bearer + public URL).
|
||||
# Run on koopa as root (needs /root/bank-exchange-password.txt + podman).
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
need_root
|
||||
|
||||
echo "=== Exchange wirewatch / wire gateway ==="
|
||||
|
||||
if ! read_pw EPW /root/bank-exchange-password.txt; then
|
||||
fail "missing /root/bank-exchange-password.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ET=$(bank_token exchange "$EPW")
|
||||
if [ -z "$ET" ]; then
|
||||
fail "could not get bank token for exchange user"
|
||||
exit 1
|
||||
fi
|
||||
pass "bank token for exchange user"
|
||||
|
||||
# History must work with Bearer (Basic is rejected by libeufin on history)
|
||||
tmp=$(mktemp)
|
||||
code=$(curl -sk -m 15 -o "$tmp" -w '%{http_code}' \
|
||||
-H "Authorization: Bearer ${ET}" \
|
||||
"${BANK_PUBLIC}/accounts/exchange/taler-wire-gateway/history/incoming?delta=-10")
|
||||
if [ "$code" = "200" ]; then
|
||||
pass "wire gateway history via ${BANK_PUBLIC} (HTTP $code)"
|
||||
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(" incoming_count", len(d.get("incoming_transactions") or []))' "$tmp"
|
||||
else
|
||||
# fallback local
|
||||
code2=$(curl -sS -m 15 -o "$tmp" -w '%{http_code}' \
|
||||
-H "Authorization: Bearer ${ET}" \
|
||||
"${BANK_URL}/accounts/exchange/taler-wire-gateway/history/incoming?delta=-10")
|
||||
if [ "$code2" = "200" ]; then
|
||||
warn "public wire gateway HTTP $code; local BANK_URL OK ($code2) — check DNS/hosts in exchange container"
|
||||
else
|
||||
fail "wire gateway history public=$code local=$code2"
|
||||
fi
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
|
||||
# Basic must fail on history (documents expected auth mode)
|
||||
code_b=$(curl -sk -m 10 -o /dev/null -w '%{http_code}' \
|
||||
-u "exchange:${EPW}" \
|
||||
"${BANK_URL}/accounts/exchange/taler-wire-gateway/history/incoming?delta=-5")
|
||||
if [ "$code_b" = "401" ]; then
|
||||
pass "Basic auth correctly rejected on history ($code_b) — use bearer TOKEN"
|
||||
else
|
||||
warn "Basic on history returned $code_b (expected 401)"
|
||||
fi
|
||||
|
||||
# Container credential + wirewatch process
|
||||
if su - hernani -c 'podman exec taler-hacktivism-exchange-ansible true' 2>/dev/null; then
|
||||
su - hernani -c 'podman exec taler-hacktivism-exchange-ansible bash -c "
|
||||
set +e
|
||||
echo \"--- secret (redacted) ---\"
|
||||
if [ -r /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf ]; then
|
||||
sed \"s/secret-token:.*/secret-token:***/; s/^TOKEN = .*/TOKEN = ***/; s/^PASSWORD = .*/PASSWORD = ***/\" \
|
||||
/etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf
|
||||
else
|
||||
# root-only file: cat as root via outer exec
|
||||
true
|
||||
fi
|
||||
echo \"--- hosts bank ---\"
|
||||
grep bank.hacktivism.ch /etc/hosts || echo \"(no hosts pin)\"
|
||||
echo \"--- wirewatch ---\"
|
||||
ps -eo pid,stat,args | grep \"[w]irewatch\" || echo \"(no wirewatch process)\"
|
||||
"' 2>&1 || warn "podman exec exchange failed"
|
||||
|
||||
AUTH_METHOD=$(su - hernani -c 'podman exec -u root taler-hacktivism-exchange-ansible \
|
||||
grep -E "^WIRE_GATEWAY_AUTH_METHOD" /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf' 2>/dev/null | awk '{print $3}')
|
||||
GW_URL=$(su - hernani -c 'podman exec -u root taler-hacktivism-exchange-ansible \
|
||||
grep -E "^WIRE_GATEWAY_URL" /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf' 2>/dev/null | awk '{print $3}')
|
||||
info "WIRE_GATEWAY_URL=$GW_URL"
|
||||
info "WIRE_GATEWAY_AUTH_METHOD=$AUTH_METHOD"
|
||||
if [ "$AUTH_METHOD" = "bearer" ]; then pass "auth method bearer"
|
||||
else fail "auth method is '$AUTH_METHOD' (want bearer + TOKEN=)"
|
||||
fi
|
||||
case "$GW_URL" in
|
||||
https://bank.hacktivism.ch/*) pass "gateway uses public bank URL" ;;
|
||||
http://127.*|http://host.containers*) warn "gateway uses local URL: $GW_URL" ;;
|
||||
*) warn "unexpected gateway URL: $GW_URL" ;;
|
||||
esac
|
||||
else
|
||||
warn "exchange container not reachable via podman"
|
||||
fi
|
||||
|
||||
echo "=== summary fails=$FAILS ==="
|
||||
exit "$FAILS"
|
||||
81
scripts/taler-sanity/check_helpers-running.sh
Executable file
81
scripts/taler-sanity/check_helpers-running.sh
Executable file
|
|
@ -0,0 +1,81 @@
|
|||
#!/bin/bash
|
||||
# Sanity: ensure exchange + merchant helpers are up (no systemd).
|
||||
# Runs on koopa as root; uses podman into both containers.
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
need_root
|
||||
|
||||
fail=0
|
||||
pass() { echo "OK $1"; }
|
||||
warn() { echo "WARN $1"; }
|
||||
bad() { echo "FAIL $1"; fail=$((fail + 1)); }
|
||||
|
||||
EXC_CT="${EXCHANGE_CONTAINER:-taler-hacktivism-exchange-ansible}"
|
||||
MER_CT="${MERCHANT_CONTAINER:-taler-hacktivism}"
|
||||
|
||||
echo "=== ensure + check helpers (no systemd) ==="
|
||||
|
||||
# Deploy/ensure scripts if present on host under admin-log mount or /tmp
|
||||
deploy_into() {
|
||||
local ct="$1"
|
||||
local src="$2"
|
||||
local dst="$3"
|
||||
if [ -f "$src" ]; then
|
||||
su - hernani -c "podman cp $(printf %q "$src") ${ct}:${dst}" 2>/dev/null \
|
||||
|| podman cp "$src" "${ct}:${dst}" 2>/dev/null || true
|
||||
su - hernani -c "podman exec ${ct} chmod 755 ${dst}" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
ADMIN="${KOOPA_ADMIN_LOG:-/home/hernani/koopa-admin-log}"
|
||||
# Prefer workspace copy if synced; else scripts next to this file
|
||||
EX_SCRIPTS="$ROOT/../taler-exchange"
|
||||
MER_SCRIPTS="$ROOT/../taler-merchant"
|
||||
[ -d "$ADMIN/scripts/taler-exchange" ] && EX_SCRIPTS="$ADMIN/scripts/taler-exchange"
|
||||
[ -d "$ADMIN/scripts/taler-merchant" ] && MER_SCRIPTS="$ADMIN/scripts/taler-merchant"
|
||||
|
||||
deploy_into "$EXC_CT" "$EX_SCRIPTS/ensure_exchange_helpers.sh" /usr/local/bin/ensure_exchange_helpers.sh
|
||||
deploy_into "$EXC_CT" "$EX_SCRIPTS/check_exchange-health.sh" /usr/local/bin/check_exchange-health.sh
|
||||
deploy_into "$MER_CT" "$MER_SCRIPTS/ensure_merchant_helpers.sh" /usr/local/bin/ensure_merchant_helpers.sh
|
||||
deploy_into "$MER_CT" "$MER_SCRIPTS/check_merchant-health.sh" /usr/local/bin/check_merchant-health.sh
|
||||
|
||||
echo "--- exchange container: ensure ---"
|
||||
if su - hernani -c "podman exec $EXC_CT /usr/local/bin/ensure_exchange_helpers.sh"; then
|
||||
pass "exchange helpers ensure"
|
||||
else
|
||||
bad "exchange helpers ensure failed"
|
||||
fi
|
||||
|
||||
echo "--- merchant container: ensure ---"
|
||||
if su - hernani -c "podman exec $MER_CT /usr/local/bin/ensure_merchant_helpers.sh"; then
|
||||
pass "merchant helpers ensure"
|
||||
else
|
||||
bad "merchant helpers ensure failed"
|
||||
fi
|
||||
|
||||
echo "--- process presence ---"
|
||||
check_ct_proc() {
|
||||
local ct="$1"
|
||||
local p="$2"
|
||||
# COMM is 15 chars — match full cmdline
|
||||
if su - hernani -c "podman exec $ct pgrep -f '(^|/)${p}( |$)'" >/dev/null 2>&1; then
|
||||
pass "$ct: $p"
|
||||
else
|
||||
bad "$ct: $p missing"
|
||||
fi
|
||||
}
|
||||
|
||||
for p in taler-exchange-httpd taler-exchange-aggregator taler-exchange-transfer \
|
||||
taler-exchange-wirewatch taler-exchange-closer; do
|
||||
check_ct_proc "$EXC_CT" "$p"
|
||||
done
|
||||
for p in taler-merchant-httpd taler-merchant-wirewatch taler-merchant-depositcheck \
|
||||
taler-merchant-webhook taler-merchant-kyccheck \
|
||||
taler-merchant-exchangekeyupdate taler-merchant-reconciliation; do
|
||||
check_ct_proc "$MER_CT" "$p"
|
||||
done
|
||||
|
||||
echo "=== summary fails=$fail ==="
|
||||
exit "$fail"
|
||||
72
scripts/taler-sanity/check_merchant-delays.sh
Normal file
72
scripts/taler-sanity/check_merchant-delays.sh
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#!/bin/bash
|
||||
# Sanity: merchant global + demo instance delays are demo-short (pay/refund/wire).
|
||||
# Run on koopa (network to :9010 / public merchant).
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
# Expected upper bounds (seconds) — fail if longer (package defaults 1d/15d/1w)
|
||||
MAX_PAY_S=${MAX_PAY_S:-120}
|
||||
MAX_REFUND_S=${MAX_REFUND_S:-120}
|
||||
MAX_WIRE_S=${MAX_WIRE_S:-120}
|
||||
|
||||
echo "=== Merchant delay defaults (expect short demo values) ==="
|
||||
|
||||
tmp=$(mktemp)
|
||||
curl -sk -m 12 -o "$tmp" "${MERCHANT_PUBLIC}/config" || curl -sk -m 12 -o "$tmp" "${MERCHANT_URL}/config"
|
||||
python3 - "$tmp" "$MAX_PAY_S" "$MAX_REFUND_S" "$MAX_WIRE_S" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
max_pay, max_ref, max_wire = map(float, sys.argv[2:5])
|
||||
fails=0
|
||||
for key, mx in (
|
||||
("default_pay_delay", max_pay),
|
||||
("default_refund_delay", max_ref),
|
||||
("default_wire_transfer_delay", max_wire),
|
||||
):
|
||||
us=d.get(key,{}).get("d_us")
|
||||
s=(us or 0)/1e6
|
||||
ok = us is not None and s <= mx
|
||||
print(f"{'OK' if ok else 'FAIL'} {key}: {s:.0f}s (max {mx:.0f}s)")
|
||||
if not ok: fails+=1
|
||||
print("currency", d.get("currency"))
|
||||
sys.exit(fails)
|
||||
PY
|
||||
ec=$?
|
||||
rm -f "$tmp"
|
||||
FAILS=$((FAILS + ec))
|
||||
|
||||
# Instance goa-demo if password present
|
||||
if [ -f /root/merchant-goa-demo-cp4zqk-password.txt ]; then
|
||||
MPW=$(tr -d '\n' </root/merchant-goa-demo-cp4zqk-password.txt)
|
||||
tmp=$(mktemp)
|
||||
curl -sk -m 12 -H "Authorization: Bearer secret-token:${MPW}" \
|
||||
-o "$tmp" "${MERCHANT_URL}/instances/goa-demo-cp4zqk/private/" || true
|
||||
if [ -s "$tmp" ]; then
|
||||
python3 - "$tmp" "$MAX_PAY_S" "$MAX_REFUND_S" "$MAX_WIRE_S" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
max_pay, max_ref, max_wire = map(float, sys.argv[2:5])
|
||||
fails=0
|
||||
for key, mx in (
|
||||
("default_pay_delay", max_pay),
|
||||
("default_refund_delay", max_ref),
|
||||
("default_wire_transfer_delay", max_wire),
|
||||
):
|
||||
us=d.get(key,{}).get("d_us")
|
||||
s=(us or 0)/1e6
|
||||
ok = us is not None and s <= mx
|
||||
print(f"{'OK' if ok else 'FAIL'} instance {key}: {s:.0f}s")
|
||||
if not ok: fails+=1
|
||||
sys.exit(fails)
|
||||
PY
|
||||
FAILS=$((FAILS + $?))
|
||||
else
|
||||
warn "could not load instance goa-demo-cp4zqk"
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
|
||||
echo "=== summary fails=$FAILS ==="
|
||||
exit "$FAILS"
|
||||
119
scripts/taler-sanity/check_settlement.sh
Normal file
119
scripts/taler-sanity/check_settlement.sh
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
#!/bin/bash
|
||||
# Sanity / ops: check paid orders wired? bank transfers? balances?
|
||||
# Run on koopa as root.
|
||||
#
|
||||
# Usage:
|
||||
# check_settlement.sh # all paid orders for goa-demo
|
||||
# check_settlement.sh ORDER_ID # one order
|
||||
# MERCHANT_INSTANCE=foo check_settlement.sh
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
need_root
|
||||
|
||||
INST="${MERCHANT_INSTANCE:-goa-demo-cp4zqk}"
|
||||
ORDER_ID="${1:-}"
|
||||
|
||||
if ! read_pw MPW "/root/merchant-${INST}-password.txt"; then
|
||||
read_pw MPW /root/merchant-goa-demo-cp4zqk-password.txt || true
|
||||
fi
|
||||
if [ -z "${MPW:-}" ]; then
|
||||
fail "no merchant password for $INST"
|
||||
exit 1
|
||||
fi
|
||||
AUTH=$(merchant_auth_header "$MPW")
|
||||
|
||||
echo "=== Settlement check instance=$INST ==="
|
||||
|
||||
tmp=$(mktemp)
|
||||
curl -sk -m 15 -H "$AUTH" -o "$tmp" \
|
||||
"${MERCHANT_URL}/instances/${INST}/private/orders?paid=YES"
|
||||
|
||||
python3 - "$tmp" "$ORDER_ID" <<'PY'
|
||||
import json,sys,time
|
||||
d=json.load(open(sys.argv[1]))
|
||||
want=sys.argv[2] or None
|
||||
orders=d.get("orders") or []
|
||||
if want:
|
||||
orders=[o for o in orders if o.get("order_id")==want]
|
||||
if not orders:
|
||||
print("FAIL no paid orders" + (f" matching {want}" if want else ""))
|
||||
sys.exit(1)
|
||||
print(f"INFO paid_orders={len(orders)}")
|
||||
for o in orders:
|
||||
print(f" - {o.get('order_id')} amount={o.get('amount')} summary={o.get('summary')!r} paid={o.get('paid')}")
|
||||
sys.exit(0)
|
||||
PY
|
||||
ec=$?
|
||||
[ "$ec" -eq 0 ] || FAILS=$((FAILS + 1))
|
||||
|
||||
# Detail each order
|
||||
mapfile -t OIDS < <(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); want=sys.argv[2] or None
|
||||
oids=[o["order_id"] for o in d.get("orders",[])]
|
||||
print("\n".join([x for x in oids if not want or x==want]))' "$tmp" "$ORDER_ID")
|
||||
|
||||
for oid in "${OIDS[@]}"; do
|
||||
echo "--- order $oid ---"
|
||||
ot=$(mktemp)
|
||||
curl -sk -m 15 -H "$AUTH" -o "$ot" \
|
||||
"${MERCHANT_URL}/instances/${INST}/private/orders/${oid}"
|
||||
python3 - "$ot" <<'PY'
|
||||
import json,sys,time
|
||||
d=json.load(open(sys.argv[1]))
|
||||
ct=d.get("contract_terms") or {}
|
||||
ts=(ct.get("timestamp") or {}).get("t_s")
|
||||
rd=(ct.get("refund_deadline") or {}).get("t_s")
|
||||
wd=(ct.get("wire_transfer_deadline") or {}).get("t_s")
|
||||
now=int(time.time())
|
||||
print(" status", d.get("order_status"), "wired", d.get("wired"), "deposit_total", d.get("deposit_total"))
|
||||
print(" amount", ct.get("amount"), "summary", ct.get("summary"))
|
||||
if ts and rd and wd:
|
||||
print(f" age_s={now-ts} refund_in_s={rd-now} wire_in_s={wd-now}")
|
||||
print(f" windows: refund={rd-ts}s wire_from_pay={wd-ts}s (wire_after_refund={wd-rd}s)")
|
||||
if now >= wd and not d.get("wired"):
|
||||
print(" FAIL wire deadline passed but wired=false")
|
||||
sys.exit(2)
|
||||
if now < wd:
|
||||
print(" INFO still before wire deadline — settlement not due yet")
|
||||
if d.get("wired"):
|
||||
print(" OK wired=true")
|
||||
sys.exit(0)
|
||||
PY
|
||||
ec=$?
|
||||
[ "$ec" -eq 2 ] && FAILS=$((FAILS + 1))
|
||||
rm -f "$ot"
|
||||
done
|
||||
|
||||
echo "=== private/transfers ==="
|
||||
tr=$(mktemp)
|
||||
curl -sk -m 15 -H "$AUTH" -o "$tr" \
|
||||
"${MERCHANT_URL}/instances/${INST}/private/transfers"
|
||||
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); t=d.get("transfers") or []; print(f" transfers={len(t)}");
|
||||
[print(" ", x) for x in t[:10]]' "$tr"
|
||||
rm -f "$tr" "$tmp"
|
||||
|
||||
# Bank balances
|
||||
BANK_USER="${BANK_USER:-$INST}"
|
||||
if read_pw BPW "/root/bank-${BANK_USER}-password.txt" || read_pw BPW /root/bank-goa-demo-cp4zqk-password.txt; then
|
||||
BT=$(bank_token "$BANK_USER" "$BPW")
|
||||
if [ -n "$BT" ]; then
|
||||
curl -sS -m 12 -H "Authorization: Bearer ${BT}" \
|
||||
"${BANK_URL}/accounts/${BANK_USER}" | python3 -c 'import sys,json; d=json.load(sys.stdin); print("merchant_bank_balance", d.get("balance"))'
|
||||
curl -sS -m 12 -H "Authorization: Bearer ${BT}" \
|
||||
"${BANK_URL}/accounts/${BANK_USER}/transactions?delta=-10" \
|
||||
| python3 -c 'import sys,json; d=json.load(sys.stdin); txs=d.get("transactions") or []; print(f"merchant_bank_tx={len(txs)}");
|
||||
[print(f" {t.get(\"direction\")} {t.get(\"amount\")} {t.get(\"subject\",\"\")[:60]}") for t in txs[:8]]'
|
||||
fi
|
||||
fi
|
||||
|
||||
if read_pw EPW /root/bank-exchange-password.txt; then
|
||||
ET=$(bank_token exchange "$EPW")
|
||||
if [ -n "$ET" ]; then
|
||||
curl -sS -m 12 -H "Authorization: Bearer ${ET}" \
|
||||
"${BANK_URL}/accounts/exchange" | python3 -c 'import sys,json; d=json.load(sys.stdin); print("exchange_bank_balance", d.get("balance"))'
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== summary fails=$FAILS ==="
|
||||
exit "$FAILS"
|
||||
73
scripts/taler-sanity/check_stack-health.sh
Normal file
73
scripts/taler-sanity/check_stack-health.sh
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#!/bin/bash
|
||||
# Sanity: public + local Taler endpoints respond (GOA stack).
|
||||
# Run on koopa as root (or any user with network to services).
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
echo "=== Taler stack health ==="
|
||||
|
||||
# Public HTTPS (via Caddy / VeciGate)
|
||||
for url in \
|
||||
"${EXCHANGE_PUBLIC}/config" \
|
||||
"${EXCHANGE_PUBLIC}/keys" \
|
||||
"${BANK_PUBLIC}/config" \
|
||||
"${MERCHANT_PUBLIC}/config"
|
||||
do
|
||||
code=$(http_code "$url")
|
||||
if [ "$code" = "200" ]; then pass "$url -> $code"
|
||||
else fail "$url -> $code"
|
||||
fi
|
||||
done
|
||||
|
||||
# Local loopback ports (containers via pasta)
|
||||
for spec in \
|
||||
"bank ${BANK_URL}/config" \
|
||||
"merchant ${MERCHANT_URL}/config" \
|
||||
"exchange-via-public ${EXCHANGE_PUBLIC}/config"
|
||||
do
|
||||
name=${spec%% *}
|
||||
url=${spec#* }
|
||||
code=$(http_code "$url")
|
||||
if [ "$code" = "200" ]; then pass "local $name -> $code"
|
||||
else fail "local $name -> $code"
|
||||
fi
|
||||
done
|
||||
|
||||
# Merchant multi-currency
|
||||
tmp=$(mktemp)
|
||||
curl -sk -m 12 -o "$tmp" "${MERCHANT_PUBLIC}/config" || true
|
||||
if python3 - "$tmp" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
ex=[e.get("currency") for e in d.get("exchanges") or []]
|
||||
cur=list((d.get("currencies") or {}).keys())
|
||||
ok = "GOA" in ex and "CHF" in ex and "GOA" in cur
|
||||
print("currency_default", d.get("currency"))
|
||||
print("exchanges", ex)
|
||||
print("currencies", cur)
|
||||
sys.exit(0 if ok else 1)
|
||||
PY
|
||||
then pass "merchant has CHF + GOA exchanges"
|
||||
else fail "merchant missing CHF/GOA in /config"
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
|
||||
# Exchange currency GOA
|
||||
tmp=$(mktemp)
|
||||
curl -sk -m 12 -o "$tmp" "${EXCHANGE_PUBLIC}/config" || true
|
||||
if python3 - "$tmp" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
c=(d.get("currency") or d.get("currency_specification",{}).get("currency"))
|
||||
print("exchange_currency", c)
|
||||
sys.exit(0 if c=="GOA" else 1)
|
||||
PY
|
||||
then pass "exchange currency GOA"
|
||||
else fail "exchange currency not GOA"
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
|
||||
echo "=== summary fails=$FAILS ==="
|
||||
exit "$FAILS"
|
||||
60
scripts/taler-sanity/lib.sh
Normal file
60
scripts/taler-sanity/lib.sh
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# shellcheck shell=bash
|
||||
# Common helpers for Taler sanity checks (run on koopa host as root).
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
: "${BANK_URL:=http://127.0.0.1:9012}"
|
||||
: "${MERCHANT_URL:=https://127.0.0.1:9010}"
|
||||
: "${EXCHANGE_PUBLIC:=https://exchange.hacktivism.ch}"
|
||||
: "${BANK_PUBLIC:=https://bank.hacktivism.ch}"
|
||||
: "${MERCHANT_PUBLIC:=https://taler.hacktivism.ch}"
|
||||
|
||||
pass() { echo "OK $*"; }
|
||||
fail() { echo "FAIL $*"; FAILS=$((FAILS + 1)); }
|
||||
warn() { echo "WARN $*"; }
|
||||
info() { echo "INFO $*"; }
|
||||
|
||||
FAILS=0
|
||||
|
||||
need_root() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This check expects root on koopa (password files under /root)." >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
read_pw() {
|
||||
# read_pw VAR path
|
||||
local _v="$1" _p="$2"
|
||||
if [ ! -f "$_p" ]; then
|
||||
eval "$_v="
|
||||
return 1
|
||||
fi
|
||||
eval "$_v=\$(tr -d '\\n' <\"$_p\")"
|
||||
}
|
||||
|
||||
bank_token() {
|
||||
# bank_token USER PASS -> prints access_token
|
||||
local user="$1" pass="$2"
|
||||
curl -sS -m 15 -u "${user}:${pass}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"scope":"readwrite","refreshable":true}' \
|
||||
"${BANK_URL}/accounts/${user}/token" \
|
||||
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("access_token",""))'
|
||||
}
|
||||
|
||||
merchant_auth_header() {
|
||||
# merchant_auth_header PASSWORD
|
||||
printf 'Authorization: Bearer secret-token:%s' "$1"
|
||||
}
|
||||
|
||||
http_code() {
|
||||
# http_code URL [curl args...]
|
||||
local url="$1"
|
||||
shift
|
||||
curl -sk -m 12 -o /dev/null -w '%{http_code}' "$@" "$url" 2>/dev/null || echo "000"
|
||||
}
|
||||
|
||||
json_get() {
|
||||
# json_get FILE python-expr-on-d
|
||||
python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print($2)" "$1"
|
||||
}
|
||||
22
scripts/taler-sanity/run_all.sh
Normal file
22
scripts/taler-sanity/run_all.sh
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/bash
|
||||
# Run all sanity checks; exit non-zero if any failed.
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
ec=0
|
||||
for s in \
|
||||
check_helpers-running.sh \
|
||||
check_stack-health.sh \
|
||||
check_merchant-delays.sh \
|
||||
check_exchange-wirewatch.sh \
|
||||
check_settlement.sh
|
||||
do
|
||||
echo ""
|
||||
echo "########## $s ##########"
|
||||
if bash "$ROOT/$s"; then
|
||||
echo "PASS $s"
|
||||
else
|
||||
echo "FAIL $s (exit $?)"
|
||||
ec=1
|
||||
fi
|
||||
done
|
||||
exit $ec
|
||||
74
scripts/taler-shared/install_privacy_docs.sh
Normal file
74
scripts/taler-shared/install_privacy_docs.sh
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#!/bin/bash
|
||||
# Write privacy policy files (txt/md/html) into a TERMS/PRIVACY dir.
|
||||
# Used by exchange + merchant installers. Style matches dual-currency terms.
|
||||
#
|
||||
# Usage (sourced or): install_privacy_docs DIR ETAG TITLE BADGE
|
||||
# Environment: BODY_MD, BODY_TXT, BODY_HTML_MAIN (inner body HTML after h1)
|
||||
set -euo pipefail
|
||||
|
||||
install_privacy_docs() {
|
||||
local dir="$1" etag="$2" title="$3" badge="$4"
|
||||
local lang="${5:-en}"
|
||||
local out="$dir/$lang"
|
||||
mkdir -p "$out"
|
||||
|
||||
printf '%s\n' "${BODY_TXT:?}" >"$out/${etag}.txt"
|
||||
printf '%s\n' "${BODY_MD:?}" >"$out/${etag}.md"
|
||||
|
||||
cat >"$out/${etag}.html" <<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
:root { color-scheme: dark light; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 42rem; margin: 2rem auto; padding: 0 1.1rem 3rem;
|
||||
line-height: 1.5; color: #e8e6e3; background: #1a1520;
|
||||
}
|
||||
h1 { font-size: 1.35rem; font-weight: 800; margin: 0 0 1rem; color: #f5f0ea; }
|
||||
h2 { font-size: 1.05rem; margin: 1.5rem 0 0.5rem; color: #e8c878; }
|
||||
h3 { font-size: 0.95rem; margin: 1rem 0 0.4rem; color: #c4b5fd; }
|
||||
p, li, td, th { font-size: 0.95rem; }
|
||||
ul, ol { padding-left: 1.2rem; }
|
||||
code, a { color: #5eead4; }
|
||||
a { text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.badge {
|
||||
display: inline-block; font-size: 0.72rem; font-weight: 700;
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
color: #c4b5fd; border: 1px solid rgba(196,181,253,0.35);
|
||||
border-radius: 999px; padding: 0.2rem 0.65rem; margin-bottom: 0.85rem;
|
||||
}
|
||||
.note {
|
||||
border-radius: 12px; padding: 0.75rem 0.9rem; margin: 0.85rem 0 1rem;
|
||||
border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25);
|
||||
font-size: 0.9rem; color: #c8c4bf;
|
||||
}
|
||||
table {
|
||||
width: 100%; border-collapse: collapse; margin: 0.6rem 0 1rem;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid rgba(255,255,255,0.12); padding: 0.45rem 0.55rem;
|
||||
text-align: left; vertical-align: top;
|
||||
}
|
||||
th { background: rgba(0,0,0,0.35); color: #e8c878; font-weight: 700; }
|
||||
.muted { color: #a39e98; font-size: 0.88rem; }
|
||||
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="badge">${badge}</div>
|
||||
<h1>${title}</h1>
|
||||
${BODY_HTML_MAIN}
|
||||
<footer class="muted">Version ${etag} · Swiss FADP (revDSG) · hacktivism.ch</footer>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
chmod -R a+rX "$dir"
|
||||
echo "privacy docs: $out/${etag}.{txt,md,html}"
|
||||
}
|
||||
130
scripts/taler-shared/mem-snapshot.sh
Normal file
130
scripts/taler-shared/mem-snapshot.sh
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
#!/bin/bash
|
||||
# Memory snapshot for landing-stats (source after json_str is defined).
|
||||
# Sets MEM_JSON (fields to embed inside performance.memory).
|
||||
# IMPORTANT: no pipelines around the /proc loop (bash subshell loses counters).
|
||||
|
||||
mem_fmt_bytes() {
|
||||
awk -v b="${1:-0}" 'BEGIN{
|
||||
b = b + 0
|
||||
if (b < 1024) printf "%d B", b
|
||||
else if (b < 1048576) printf "%.1f KiB", b / 1024
|
||||
else if (b < 1073741824) printf "%.1f MiB", b / 1048576
|
||||
else printf "%.2f GiB", b / 1073741824
|
||||
}'
|
||||
}
|
||||
|
||||
mem_snapshot_json() {
|
||||
local rss_kb rss_b pid comm cmd short
|
||||
local sum_b=0 cgroup_b="" limit_b=""
|
||||
local postgres_b=0 taler_b=0 nginx_b=0 java_b=0 redis_b=0 other_b=0
|
||||
local n_pg=0 n_taler=0 n_nginx=0 n_java=0 n_redis=0 n_other=0
|
||||
local allf topf top_json top_n=0 lim_json cg_json hum cjs sjs hjs
|
||||
|
||||
if [ -r /sys/fs/cgroup/memory.current ]; then
|
||||
cgroup_b=$(tr -d ' \n' </sys/fs/cgroup/memory.current 2>/dev/null || true)
|
||||
if [ -r /sys/fs/cgroup/memory.max ]; then
|
||||
limit_b=$(tr -d ' \n' </sys/fs/cgroup/memory.max 2>/dev/null || true)
|
||||
[ "$limit_b" = "max" ] && limit_b=""
|
||||
fi
|
||||
elif [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then
|
||||
cgroup_b=$(tr -d ' \n' </sys/fs/cgroup/memory/memory.usage_in_bytes 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
allf=$(mktemp)
|
||||
topf=$(mktemp)
|
||||
|
||||
for st in /proc/[0-9]*/status; do
|
||||
[ -f "$st" ] || continue
|
||||
pid=${st#/proc/}
|
||||
pid=${pid%/status}
|
||||
rss_kb=$(awk '/^VmRSS:/{print $2; exit}' "$st" 2>/dev/null || true)
|
||||
[[ "$rss_kb" =~ ^[0-9]+$ ]] || continue
|
||||
rss_b=$((rss_kb * 1024))
|
||||
sum_b=$((sum_b + rss_b))
|
||||
comm=$(awk '/^Name:/{print $2; exit}' "$st" 2>/dev/null || echo "?")
|
||||
cmd=""
|
||||
if [ -r "/proc/$pid/cmdline" ]; then
|
||||
cmd=$(tr '\0' ' ' <"/proc/$pid/cmdline" 2>/dev/null | sed 's/[[:space:]]*$//')
|
||||
fi
|
||||
[ -n "$cmd" ] || cmd=$comm
|
||||
short=$(printf '%s' "$cmd" | cut -c1-100)
|
||||
|
||||
case "$comm $cmd" in
|
||||
*postgres*|*postmaster*)
|
||||
postgres_b=$((postgres_b + rss_b)); n_pg=$((n_pg + 1)) ;;
|
||||
*taler-exchange*|*taler-merchant*|*taler-auditor*|*taler-helper*|*taler_*)
|
||||
taler_b=$((taler_b + rss_b)); n_taler=$((n_taler + 1)) ;;
|
||||
*nginx*)
|
||||
nginx_b=$((nginx_b + rss_b)); n_nginx=$((n_nginx + 1)) ;;
|
||||
*java*|*libeufin*|*MainKt*)
|
||||
java_b=$((java_b + rss_b)); n_java=$((n_java + 1)) ;;
|
||||
*redis-server*|*redis*)
|
||||
redis_b=$((redis_b + rss_b)); n_redis=$((n_redis + 1)) ;;
|
||||
*)
|
||||
other_b=$((other_b + rss_b)); n_other=$((n_other + 1)) ;;
|
||||
esac
|
||||
printf '%s\t%s\t%s\n' "$rss_b" "$comm" "$short" >>"$allf"
|
||||
done
|
||||
|
||||
sort -t$'\t' -nr -k1,1 "$allf" 2>/dev/null | head -10 >"$topf"
|
||||
rm -f "$allf"
|
||||
|
||||
local total_b=$sum_b
|
||||
if [[ "$cgroup_b" =~ ^[0-9]+$ ]] && [ "$cgroup_b" -gt 0 ]; then
|
||||
total_b=$cgroup_b
|
||||
fi
|
||||
|
||||
top_json="["
|
||||
top_n=0
|
||||
while IFS=$'\t' read -r rss_b comm short; do
|
||||
[ -z "${rss_b:-}" ] && continue
|
||||
[ "$top_n" -gt 0 ] && top_json="${top_json},"
|
||||
top_n=$((top_n + 1))
|
||||
hum=$(mem_fmt_bytes "$rss_b")
|
||||
cjs=$(json_str "$comm")
|
||||
sjs=$(json_str "$short")
|
||||
hjs=$(json_str "$hum")
|
||||
top_json="${top_json}
|
||||
{\"rss_bytes\": ${rss_b}, \"rss_human\": ${hjs}, \"comm\": ${cjs}, \"cmd\": ${sjs}}"
|
||||
done <"$topf"
|
||||
top_json="${top_json}
|
||||
]"
|
||||
rm -f "$topf"
|
||||
|
||||
lim_json="null"
|
||||
if [[ "$limit_b" =~ ^[0-9]+$ ]] && [ "$limit_b" -gt 0 ]; then
|
||||
lim_json=$limit_b
|
||||
fi
|
||||
cg_json="null"
|
||||
if [[ "$cgroup_b" =~ ^[0-9]+$ ]]; then
|
||||
cg_json=$cgroup_b
|
||||
fi
|
||||
|
||||
MEM_JSON="
|
||||
\"container_rss_bytes\": ${total_b},
|
||||
\"container_rss_human\": $(json_str "$(mem_fmt_bytes "$total_b")"),
|
||||
\"proc_sum_rss_bytes\": ${sum_b},
|
||||
\"proc_sum_rss_human\": $(json_str "$(mem_fmt_bytes "$sum_b")"),
|
||||
\"cgroup_bytes\": ${cg_json},
|
||||
\"cgroup_limit_bytes\": ${lim_json},
|
||||
\"postgres_rss_bytes\": ${postgres_b},
|
||||
\"postgres_rss_human\": $(json_str "$(mem_fmt_bytes "$postgres_b")"),
|
||||
\"postgres_n\": ${n_pg},
|
||||
\"taler_rss_bytes\": ${taler_b},
|
||||
\"taler_rss_human\": $(json_str "$(mem_fmt_bytes "$taler_b")"),
|
||||
\"taler_n\": ${n_taler},
|
||||
\"nginx_rss_bytes\": ${nginx_b},
|
||||
\"nginx_rss_human\": $(json_str "$(mem_fmt_bytes "$nginx_b")"),
|
||||
\"nginx_n\": ${n_nginx},
|
||||
\"java_rss_bytes\": ${java_b},
|
||||
\"java_rss_human\": $(json_str "$(mem_fmt_bytes "$java_b")"),
|
||||
\"java_n\": ${n_java},
|
||||
\"redis_rss_bytes\": ${redis_b},
|
||||
\"redis_rss_human\": $(json_str "$(mem_fmt_bytes "$redis_b")"),
|
||||
\"redis_n\": ${n_redis},
|
||||
\"other_rss_bytes\": ${other_b},
|
||||
\"other_rss_human\": $(json_str "$(mem_fmt_bytes "$other_b")"),
|
||||
\"other_n\": ${n_other},
|
||||
\"top\": ${top_json}
|
||||
"
|
||||
}
|
||||
37
scripts/taler-shared/terms-style.css.fragment
Normal file
37
scripts/taler-shared/terms-style.css.fragment
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* Shared dark palette for hacktivism.ch Taler terms & privacy pages */
|
||||
:root { color-scheme: dark light; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 42rem; margin: 2rem auto; padding: 0 1.1rem 3rem;
|
||||
line-height: 1.5; color: #e8e6e3; background: #1a1520;
|
||||
}
|
||||
h1 { font-size: 1.35rem; font-weight: 800; margin: 0 0 1rem; color: #f5f0ea; }
|
||||
h2 { font-size: 1.05rem; margin: 1.5rem 0 0.5rem; color: #e8c878; }
|
||||
h3 { font-size: 0.95rem; margin: 1rem 0 0.4rem; color: #c4b5fd; }
|
||||
p, li, td, th { font-size: 0.95rem; }
|
||||
ul, ol { padding-left: 1.2rem; }
|
||||
code, a { color: #5eead4; }
|
||||
a { text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.badge {
|
||||
display: inline-block; font-size: 0.72rem; font-weight: 700;
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
color: #c4b5fd; border: 1px solid rgba(196,181,253,0.35);
|
||||
border-radius: 999px; padding: 0.2rem 0.65rem; margin-bottom: 0.85rem;
|
||||
}
|
||||
.note {
|
||||
border-radius: 12px; padding: 0.75rem 0.9rem; margin: 0.85rem 0 1rem;
|
||||
border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25);
|
||||
font-size: 0.9rem; color: #c8c4bf;
|
||||
}
|
||||
table {
|
||||
width: 100%; border-collapse: collapse; margin: 0.6rem 0 1rem;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid rgba(255,255,255,0.12); padding: 0.45rem 0.55rem;
|
||||
text-align: left; vertical-align: top;
|
||||
}
|
||||
th { background: rgba(0,0,0,0.35); color: #e8c878; font-weight: 700; }
|
||||
.muted { color: #a39e98; font-size: 0.88rem; }
|
||||
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
|
||||
32
scripts/taler-wallet-cli/goa-deb-withdraw-pay.sh
Executable file
32
scripts/taler-wallet-cli/goa-deb-withdraw-pay.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/sh
|
||||
# Debian/Ubuntu · same as bank landing deb pane (install + run)
|
||||
set -eu
|
||||
|
||||
# install (deb.taler.net · trixie): keyring, apt source, packages
|
||||
if ! command -v taler-wallet-cli >/dev/null 2>&1; then
|
||||
sudo mkdir -p /etc/apt/keyrings && \
|
||||
sudo wget -q -O /etc/apt/keyrings/taler-systems.gpg https://taler.net/taler-systems.gpg && \
|
||||
echo 'deb [signed-by=/etc/apt/keyrings/taler-systems.gpg] https://deb.taler.net/apt/debian trixie main' | sudo tee /etc/apt/sources.list.d/taler.list && \
|
||||
sudo apt-get update && sudo apt-get install -y taler-wallet-cli wget
|
||||
fi
|
||||
command -v wget >/dev/null 2>&1 || sudo apt-get install -y wget
|
||||
|
||||
# private wallet DB under /tmp (not the default home DB)
|
||||
tw() {
|
||||
taler-wallet-cli \
|
||||
--wallet-db="${TMPDIR:-/tmp}/taler-wallet-goa.sqlite3" \
|
||||
--no-throttle "$@"
|
||||
}
|
||||
|
||||
# mint from bank, register exchange, withdraw GOA:10
|
||||
wget -q -O demo-withdraw.json https://bank.hacktivism.ch/intro/demo-withdraw.json && \
|
||||
tw exchanges add https://exchange.hacktivism.ch/ && \
|
||||
tw exchanges accept-tos https://exchange.hacktivism.ch/ && \
|
||||
tw withdraw accept-uri --exchange https://exchange.hacktivism.ch/ \
|
||||
"$(grep -o 'taler://[^"]*' demo-withdraw.json)" && \
|
||||
tw run-until-done && \
|
||||
|
||||
# pay orbit-sticker (GOA:2); balance ends at GOA:8
|
||||
tw handle-uri --yes \
|
||||
taler://pay-template/taler.hacktivism.ch/instances/goa-shop/orbit-sticker && \
|
||||
tw run-until-done && tw balance
|
||||
41
scripts/taler-wallet-cli/goa-posix-withdraw-pay.sh
Executable file
41
scripts/taler-wallet-cli/goa-posix-withdraw-pay.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/bin/sh
|
||||
# Unix/macOS · monorepo CLI (same flow as bank landing unix pane)
|
||||
#
|
||||
# install (once):
|
||||
# git + node (LTS) + monorepo build deps
|
||||
# git clone https://git.taler.net/taler-typescript-core.git
|
||||
# cd taler-typescript-core && ./bootstrap && ./configure && make
|
||||
#
|
||||
# run from taler-typescript-core after make (needs: node, wget|curl, grep)
|
||||
set -eu
|
||||
|
||||
command -v node >/dev/null 2>&1 || { echo "install: node (LTS)" >&2; exit 1; }
|
||||
[ -f packages/taler-wallet-cli/bin/taler-wallet-cli.mjs ] || {
|
||||
echo "install/build: run from taler-typescript-core after make" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v wget >/dev/null 2>&1 || command -v curl >/dev/null 2>&1 || {
|
||||
echo "install: wget or curl" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# monorepo CLI + private wallet DB (not system package / default home DB)
|
||||
tw() {
|
||||
node packages/taler-wallet-cli/bin/taler-wallet-cli.mjs \
|
||||
--wallet-db="${TMPDIR:-/tmp}/taler-wallet-goa.sqlite3" \
|
||||
--no-throttle "$@"
|
||||
}
|
||||
|
||||
# mint from bank, register exchange, withdraw GOA:10
|
||||
(wget -q -O demo-withdraw.json https://bank.hacktivism.ch/intro/demo-withdraw.json || \
|
||||
curl -fsS -o demo-withdraw.json https://bank.hacktivism.ch/intro/demo-withdraw.json) && \
|
||||
tw exchanges add https://exchange.hacktivism.ch/ && \
|
||||
tw exchanges accept-tos https://exchange.hacktivism.ch/ && \
|
||||
tw withdraw accept-uri --exchange https://exchange.hacktivism.ch/ \
|
||||
"$(grep -o 'taler://[^"]*' demo-withdraw.json)" && \
|
||||
tw run-until-done && \
|
||||
|
||||
# pay orbit-sticker (GOA:2); balance ends at GOA:8
|
||||
tw handle-uri --yes \
|
||||
taler://pay-template/taler.hacktivism.ch/instances/goa-shop/orbit-sticker && \
|
||||
tw run-until-done && tw balance
|
||||
3
scripts/taler-wallet-cli/run-amount-ladder-bench.sh
Executable file
3
scripts/taler-wallet-cli/run-amount-ladder-bench.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
# Compatibility wrapper — canonical benchmark lives under benchmarks/amount-ladder/
|
||||
exec "$(cd "$(dirname "$0")/../.." && pwd)/benchmarks/amount-ladder/run.sh" "$@"
|
||||
350
scripts/taler-wallet-cli/run-extreme-amount-tests.sh
Executable file
350
scripts/taler-wallet-cli/run-extreme-amount-tests.sh
Executable file
|
|
@ -0,0 +1,350 @@
|
|||
#!/bin/bash
|
||||
# Extreme amount tests for GOA: largest practical price vs atomic/tiny coin.
|
||||
#
|
||||
# Live denoms (exchange.hacktivism.ch): GOA:0.000001 … GOA:10
|
||||
# Currency TINY / round unit: GOA:0.00000001 (Atomic-GOA)
|
||||
#
|
||||
# Run on the local machine (wallet) with ssh BatchMode to koopa for bank/merchant ops.
|
||||
# Requires: monorepo taler-wallet-cli, passwords on koopa /root/*-password.txt
|
||||
#
|
||||
# Usage:
|
||||
# ./run-extreme-amount-tests.sh
|
||||
# HIGH_AMOUNT=GOA:500 TINY_AMOUNT=GOA:0.000001 ./run-extreme-amount-tests.sh
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
SCRATCH="${ROOT}/.tmp/extreme-$(date -u +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$SCRATCH"
|
||||
chmod 700 "$SCRATCH"
|
||||
|
||||
MONO="${MONO:-/Users/newkamek/src/taler/taler-typescript-core/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs}"
|
||||
WDB="${WDB:-$SCRATCH/wallet.sqlite3}"
|
||||
EX="${EXCHANGE_URL:-https://exchange.hacktivism.ch/}"
|
||||
KOOPA="${KOOPA_HOST:-koopa}"
|
||||
INST="${MERCHANT_INSTANCE:-goa-demo-cp4zqk}"
|
||||
BANK_PUBLIC="${BANK_PUBLIC:-https://bank.hacktivism.ch}"
|
||||
MER_PUBLIC="${MER_PUBLIC:-https://taler.hacktivism.ch}"
|
||||
|
||||
# High price: many max-denom (GOA:10) coins. Tiny: smallest issued coin.
|
||||
HIGH_AMOUNT="${HIGH_AMOUNT:-GOA:1000}"
|
||||
TINY_AMOUNT="${TINY_AMOUNT:-GOA:0.000001}"
|
||||
# True atomic (round unit) — may fail if no coin below 1e-6
|
||||
ATOMIC_AMOUNT="${ATOMIC_AMOUNT:-GOA:0.00000001}"
|
||||
# Fat bank balance for withdraw buffer
|
||||
FAT_CREDIT="${FAT_CREDIT:-GOA:50000}"
|
||||
FAT_USER="${FAT_USER:-extreme-fat}"
|
||||
# withdraw a bit more than HIGH
|
||||
WITHDRAW_AMOUNT="${WITHDRAW_AMOUNT:-GOA:1500}"
|
||||
|
||||
wcli() { node "$MONO" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"; }
|
||||
|
||||
log() { printf '%s %s\n' "$(date -u +%H:%M:%S)" "$*"; }
|
||||
die() { echo "FAIL: $*" >&2; exit 1; }
|
||||
|
||||
command -v node >/dev/null || die "node missing"
|
||||
[ -f "$MONO" ] || die "wallet missing: $MONO"
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "$KOOPA" 'echo ok' >/dev/null || die "ssh $KOOPA failed"
|
||||
|
||||
log "scratch=$SCRATCH"
|
||||
log "HIGH=$HIGH_AMOUNT TINY=$TINY_AMOUNT ATOMIC=$ATOMIC_AMOUNT WITHDRAW=$WITHDRAW_AMOUNT FAT=$FAT_USER+$FAT_CREDIT"
|
||||
|
||||
# --- 1) koopa: bank account + credit + templates + orders ---
|
||||
log "=== koopa setup (admin credit, templates, orders) ==="
|
||||
# Generate bank password locally (not committed)
|
||||
FAT_PW=$(python3 -c 'import secrets; print(secrets.token_urlsafe(12))')
|
||||
echo "$FAT_PW" >"$SCRATCH/fat-bank.password"
|
||||
chmod 600 "$SCRATCH/fat-bank.password"
|
||||
|
||||
cat >"$SCRATCH/koopa-setup.sh" <<EOS
|
||||
#!/bin/bash
|
||||
set +e
|
||||
exec > /tmp/extreme-setup.out 2>&1
|
||||
set -x
|
||||
INST=$INST
|
||||
FAT_USER=$FAT_USER
|
||||
FAT_PW='$(printf %s "$FAT_PW" | sed "s/'/'\\\\''/g")'
|
||||
FAT_CREDIT=$FAT_CREDIT
|
||||
HIGH_AMOUNT=$HIGH_AMOUNT
|
||||
TINY_AMOUNT=$TINY_AMOUNT
|
||||
ATOMIC_AMOUNT=$ATOMIC_AMOUNT
|
||||
BANK=http://127.0.0.1:9012
|
||||
MER=https://127.0.0.1:9010
|
||||
|
||||
ADMIN_PASS=\$(tr -d '\\n' </root/bank-admin-password.txt)
|
||||
MPW=\$(tr -d '\\n' </root/merchant-\${INST}-password.txt)
|
||||
AUTH="Authorization: Bearer secret-token:\${MPW}"
|
||||
|
||||
# admin token
|
||||
AT=\$(curl -sS -u "admin:\${ADMIN_PASS}" -H 'Content-Type: application/json' \\
|
||||
-d '{"scope":"readwrite"}' "\${BANK}/accounts/admin/token" \\
|
||||
| python3 -c 'import sys,json;print(json.load(sys.stdin).get("access_token",""))')
|
||||
[ -n "\$AT" ] || { echo FAIL_admin_token; exit 1; }
|
||||
|
||||
# create or reuse bank account
|
||||
code=\$(curl -sS -o /tmp/acc-create.json -w '%{http_code}' -X POST \\
|
||||
-H "Authorization: Bearer \${AT}" -H 'Content-Type: application/json' \\
|
||||
-d "{\\"username\\":\\"\${FAT_USER}\\",\\"password\\":\\"\${FAT_PW}\\",\\"name\\":\\"Extreme Fat Wallet\\",\\"is_public\\":false,\\"is_taler_exchange\\":false,\\"debit_threshold\\":\\"GOA:1000000\\"}" \\
|
||||
"\${BANK}/accounts")
|
||||
echo create_account_http=\$code
|
||||
cat /tmp/acc-create.json; echo
|
||||
# if exists (409), reset password via admin if possible
|
||||
if [ "\$code" != "200" ] && [ "\$code" != "201" ] && [ "\$code" != "204" ]; then
|
||||
echo "account may exist; try credit anyway"
|
||||
fi
|
||||
|
||||
# credit from admin (idempotent enough — we just need lots of GOA)
|
||||
python3 - <<'PY'
|
||||
import json, os, urllib.request, base64
|
||||
bank=os.environ.get("BANK","http://127.0.0.1:9012")
|
||||
PY
|
||||
# use host credit-account if present
|
||||
if [ -x /root/koopa-admin-log/scripts/taler-bank/credit-account.sh ]; then
|
||||
/root/koopa-admin-log/scripts/taler-bank/credit-account.sh "\$FAT_USER" "\$FAT_CREDIT" || true
|
||||
elif [ -x /Users/newkamek/src/koopa/koopa-admin-log/scripts/taler-bank/credit-account.sh ]; then
|
||||
true
|
||||
else
|
||||
# inline credit
|
||||
PAYTO="payto://x-taler-bank/bank.hacktivism.ch/\${FAT_USER}?receiver-name=\${FAT_USER}&message=extreme-credit"
|
||||
UID=\$(python3 -c 'import os; a="0123456789ABCDEFGHJKMNPQRSTVWXYZ"; b=os.urandom(32); bits=0;v=0;o=[]
|
||||
for byte in b:
|
||||
v=(v<<8)|byte; bits+=8
|
||||
while bits>=5:
|
||||
bits-=5; o.append(a[(v>>bits)&31])
|
||||
if bits: o.append(a[(v<<(5-bits))&31])
|
||||
print("".join(o))')
|
||||
curl -sS -H "Authorization: Bearer \${AT}" -H 'Content-Type: application/json' \\
|
||||
-d "{\\"payto_uri\\":\\"\${PAYTO}\\",\\"amount\\":\\"\${FAT_CREDIT}\\",\\"request_uid\\":\\"\${UID}\\"}" \\
|
||||
"\${BANK}/accounts/admin/transactions" | tee /tmp/credit.out
|
||||
echo
|
||||
fi
|
||||
|
||||
# balance
|
||||
curl -sS -H "Authorization: Bearer \${AT}" "\${BANK}/accounts/\${FAT_USER}" | tee /tmp/fat-bal.json
|
||||
echo
|
||||
python3 -c 'import json;d=json.load(open("/tmp/fat-bal.json"));print("FAT_BALANCE", (d.get("balance") or {}).get("amount"), (d.get("balance") or {}).get("credit_debit_indicator"))'
|
||||
|
||||
# store bank pw for host-side withdraw
|
||||
printf '%s' "\$FAT_PW" > /root/bank-\${FAT_USER}-password.txt
|
||||
chmod 600 /root/bank-\${FAT_USER}-password.txt
|
||||
|
||||
# templates
|
||||
for id_summary_amount in \\
|
||||
"extreme-high|extreme high price|\${HIGH_AMOUNT}" \\
|
||||
"extreme-tiny|extreme tiny coin|\${TINY_AMOUNT}" \\
|
||||
"extreme-atomic|true atomic round unit|\${ATOMIC_AMOUNT}"
|
||||
do
|
||||
IFS='|' read -r TID SUM AMT <<<"\$id_summary_amount"
|
||||
body=\$(python3 -c "import json; print(json.dumps({
|
||||
'template_id': '\$TID',
|
||||
'template_description': '\$SUM',
|
||||
'editable_defaults': False,
|
||||
'template_contract': {
|
||||
'summary': '\$SUM',
|
||||
'amount': '\$AMT',
|
||||
'minimum_age': 0,
|
||||
},
|
||||
}))")
|
||||
# try POST then PATCH
|
||||
code=\$(curl -sk -o /tmp/tpl.json -w '%{http_code}' -X POST -H "\$AUTH" -H 'Content-Type: application/json' \\
|
||||
-d "\$body" "\${MER}/instances/\${INST}/private/templates")
|
||||
echo "template_\$TID post=\$code \$(head -c 120 /tmp/tpl.json)"
|
||||
if [ "\$code" = "409" ] || [ "\$code" = "400" ]; then
|
||||
curl -sk -X PATCH -H "\$AUTH" -H 'Content-Type: application/json' \\
|
||||
-d "\$body" "\${MER}/instances/\${INST}/private/templates/\${TID}" -w " patch=%{http_code}\\n"
|
||||
fi
|
||||
done
|
||||
|
||||
# also list templates
|
||||
curl -sk -H "\$AUTH" "\${MER}/instances/\${INST}/private/templates" | python3 -m json.tool | head -80
|
||||
|
||||
# create two firm orders (high + tiny) for deterministic pay URIs
|
||||
create_order() {
|
||||
local sum="\$1" amt="\$2" tag="\$3"
|
||||
curl -sk -X POST -H "\$AUTH" -H 'Content-Type: application/json' \\
|
||||
-d "{\\"order\\":{\\"summary\\":\\"\${sum}\\",\\"amount\\":\\"\${amt}\\",\\"fulfillment_message\\":\\"extreme-ok\\"},\\"create_token\\":true}" \\
|
||||
"\${MER}/instances/\${INST}/private/orders" > "/tmp/ord-\${tag}.json"
|
||||
python3 - "\$tag" <<'PY'
|
||||
import json,sys
|
||||
tag=sys.argv[1]
|
||||
d=json.load(open(f"/tmp/ord-{tag}.json"))
|
||||
print(f"ORDER_{tag.upper()}", d.get("order_id"), "token", (d.get("token") or "")[:12])
|
||||
open(f"/tmp/oid-{tag}.txt","w").write(d.get("order_id") or "")
|
||||
open(f"/tmp/otok-{tag}.txt","w").write(d.get("token") or "")
|
||||
PY
|
||||
}
|
||||
create_order "extreme-high-order" "\$HIGH_AMOUNT" high
|
||||
create_order "extreme-tiny-order" "\$TINY_AMOUNT" tiny
|
||||
create_order "extreme-atomic-order" "\$ATOMIC_AMOUNT" atomic
|
||||
|
||||
# create bank integrated withdrawal for fat user
|
||||
BT=\$(curl -sS -u "\${FAT_USER}:\${FAT_PW}" -H 'Content-Type: application/json' \\
|
||||
-d '{"scope":"readwrite"}' "\${BANK}/accounts/\${FAT_USER}/token" \\
|
||||
| python3 -c 'import sys,json;print(json.load(sys.stdin).get("access_token",""))')
|
||||
curl -sS -H "Authorization: Bearer \${BT}" -H 'Content-Type: application/json' \\
|
||||
-d "{\\"amount\\":\\"\$WITHDRAW_AMOUNT\\",\\"exchange_url\\":\\"https://exchange.hacktivism.ch/\\"}" \\
|
||||
"\${BANK}/accounts/\${FAT_USER}/withdrawals" | tee /tmp/wd-create.json
|
||||
echo
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
d=json.load(open("/tmp/wd-create.json"))
|
||||
wid=d.get("withdrawal_id") or d.get("id")
|
||||
uri=d.get("taler_withdraw_uri")
|
||||
print("WID", wid)
|
||||
print("WURI", uri)
|
||||
open("/tmp/wid.txt","w").write(wid or "")
|
||||
open("/tmp/wuri.txt","w").write(uri or "")
|
||||
PY
|
||||
|
||||
# publish artifacts for hernani-readable pull
|
||||
cp /tmp/oid-*.txt /tmp/otok-*.txt /tmp/wuri.txt /tmp/wid.txt /tmp/fat-bal.json /tmp/extreme-setup.out /home/hernani/ 2>/dev/null || true
|
||||
chmod 644 /home/hernani/oid-*.txt /home/hernani/otok-*.txt /home/hernani/wuri.txt /home/hernani/wid.txt 2>/dev/null || true
|
||||
|
||||
echo SETUP_EXTREME_OK
|
||||
EOS
|
||||
|
||||
# inject WITHDRAW_AMOUNT into script (heredoc already expanded partially - fix)
|
||||
# The setup script uses $WITHDRAW_AMOUNT as shell var on koopa - need to export into script
|
||||
sed -i.bak "s|\$WITHDRAW_AMOUNT|$WITHDRAW_AMOUNT|g" "$SCRATCH/koopa-setup.sh" 2>/dev/null || \
|
||||
sed -i '' "s|\$WITHDRAW_AMOUNT|$WITHDRAW_AMOUNT|g" "$SCRATCH/koopa-setup.sh"
|
||||
|
||||
scp -o BatchMode=yes "$SCRATCH/koopa-setup.sh" "${KOOPA}:/tmp/extreme-setup.sh"
|
||||
# also scp credit-account for reliable credit
|
||||
scp -o BatchMode=yes "$ROOT/scripts/taler-bank/credit-account.sh" "${KOOPA}:/tmp/credit-account.sh" || true
|
||||
|
||||
# run via screen window 0 (root@koopa) — screen-run cannot source local-machine paths
|
||||
SESSION=$(screen -ls 2>/dev/null | awk 'match($0, /[0-9]+\.[A-Za-z0-9._-]+/) { print substr($0, RSTART, RLENGTH); exit }')
|
||||
[ -n "$SESSION" ] || die "no screen session"
|
||||
screen -S "$SESSION" -p 0 -X stuff $'bash /tmp/extreme-setup.sh; chmod 644 /tmp/extreme-setup.out /home/hernani/*.txt 2>/dev/null\n'
|
||||
|
||||
for i in $(seq 1 90); do
|
||||
if ssh -o BatchMode=yes "$KOOPA" 'grep -q SETUP_EXTREME_OK /tmp/extreme-setup.out 2>/dev/null'; then
|
||||
log "setup ready @${i}s"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
[ "$i" -eq 90 ] && { ssh -o BatchMode=yes "$KOOPA" 'tail -80 /tmp/extreme-setup.out' || true; die "setup timeout"; }
|
||||
done
|
||||
ssh -o BatchMode=yes "$KOOPA" 'cat /tmp/extreme-setup.out' >"$SCRATCH/koopa-setup.out" || true
|
||||
# pull artifacts (hernani-readable)
|
||||
scp -o BatchMode=yes "${KOOPA}:/home/hernani/wuri.txt" "${KOOPA}:/home/hernani/wid.txt" \
|
||||
"${KOOPA}:/home/hernani/oid-high.txt" "${KOOPA}:/home/hernani/oid-tiny.txt" \
|
||||
"${KOOPA}:/home/hernani/oid-atomic.txt" \
|
||||
"${KOOPA}:/home/hernani/otok-high.txt" "${KOOPA}:/home/hernani/otok-tiny.txt" \
|
||||
"${KOOPA}:/home/hernani/otok-atomic.txt" \
|
||||
"$SCRATCH/" 2>/dev/null || true
|
||||
|
||||
WURI=$(tr -d '\n' <"$SCRATCH/wuri.txt" 2>/dev/null || true)
|
||||
WID=$(tr -d '\n' <"$SCRATCH/wid.txt" 2>/dev/null || true)
|
||||
OID_HIGH=$(tr -d '\n' <"$SCRATCH/oid-high.txt" 2>/dev/null || true)
|
||||
OID_TINY=$(tr -d '\n' <"$SCRATCH/oid-tiny.txt" 2>/dev/null || true)
|
||||
OID_ATOMIC=$(tr -d '\n' <"$SCRATCH/oid-atomic.txt" 2>/dev/null || true)
|
||||
TOK_HIGH=$(tr -d '\n' <"$SCRATCH/otok-high.txt" 2>/dev/null || true)
|
||||
TOK_TINY=$(tr -d '\n' <"$SCRATCH/otok-tiny.txt" 2>/dev/null || true)
|
||||
TOK_ATOMIC=$(tr -d '\n' <"$SCRATCH/otok-atomic.txt" 2>/dev/null || true)
|
||||
|
||||
log "WURI=$WURI"
|
||||
log "orders high=$OID_HIGH tiny=$OID_TINY atomic=$OID_ATOMIC"
|
||||
[ -n "$WURI" ] || die "no withdraw URI"
|
||||
[ -n "$OID_HIGH" ] && [ -n "$OID_TINY" ] || die "orders missing"
|
||||
|
||||
# ensure auto-confirm watches this withdrawal
|
||||
ssh -o BatchMode=yes "$KOOPA" "echo $WID >> /var/www/bank-landing/withdraw-watch.ids 2>/dev/null; sort -u /var/www/bank-landing/withdraw-watch.ids -o /var/www/bank-landing/withdraw-watch.ids 2>/dev/null; true"
|
||||
# root confirm via screen
|
||||
screen -S "$SESSION" -p 0 -X stuff $'BPW=$(tr -d "\\n" </root/bank-'"$FAT_USER"'-password.txt); BT=$(curl -sS -u "'"$FAT_USER"':${BPW}" -H "Content-Type: application/json" -d "{\"scope\":\"readwrite\"}" http://127.0.0.1:9012/accounts/'"$FAT_USER"'/token | python3 -c "import sys,json;print(json.load(sys.stdin).get(\"access_token\",\"\"))"); curl -sS -X POST -H "Authorization: Bearer ${BT}" http://127.0.0.1:9012/accounts/'"$FAT_USER"'/withdrawals/'"$WID"'/confirm; echo CONF_DONE\n'
|
||||
|
||||
# --- 2) wallet withdraw ---
|
||||
log "=== wallet: add exchange + ToS ==="
|
||||
wcli exchanges add "$EX" 2>&1 | tee "$SCRATCH/ex-add.out" || true
|
||||
wcli exchanges update "$EX" 2>&1 | tee "$SCRATCH/ex-upd.out" || true
|
||||
wcli exchanges tos "$EX" 2>&1 | tee "$SCRATCH/ex-tos.out" || true
|
||||
wcli exchanges accept-tos "$EX" 2>&1 | tee "$SCRATCH/ex-accept.out" || true
|
||||
|
||||
log "=== withdraw accept-uri ==="
|
||||
wcli withdraw accept-uri --exchange "$EX" "$WURI" 2>&1 | tee "$SCRATCH/accept-uri.out" || true
|
||||
|
||||
for round in $(seq 1 20); do
|
||||
log "run-until-done round $round"
|
||||
wcli run-until-done 2>&1 | tee -a "$SCRATCH/rud.out" || true
|
||||
wcli balance 2>&1 | tee "$SCRATCH/bal-$round.out"
|
||||
if grep -qE "GOA:" "$SCRATCH/bal-$round.out"; then
|
||||
# check non-zero available
|
||||
if python3 -c 'import re,sys; t=open(sys.argv[1]).read();
|
||||
m=re.search(r"available[^0-9A-Z]*GOA:([0-9.]+)", t)
|
||||
print(m.group(1) if m else ""); sys.exit(0 if m and float(m.group(1))>0 else 1)' "$SCRATCH/bal-$round.out" 2>/dev/null; then
|
||||
log "coins available"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
# re-confirm withdraw on bank
|
||||
screen -S "$SESSION" -p 0 -X stuff $'BPW=$(tr -d "\\n" </root/bank-'"$FAT_USER"'-password.txt); BT=$(curl -sS -u "'"$FAT_USER"':${BPW}" -H "Content-Type: application/json" -d "{\"scope\":\"readwrite\"}" http://127.0.0.1:9012/accounts/'"$FAT_USER"'/token | python3 -c "import sys,json;print(json.load(sys.stdin).get(\"access_token\",\"\"))"); curl -sS -X POST -H "Authorization: Bearer ${BT}" http://127.0.0.1:9012/accounts/'"$FAT_USER"'/withdrawals/'"$WID"'/confirm 2>/dev/null; echo\n'
|
||||
sleep 3
|
||||
done
|
||||
|
||||
wcli balance 2>&1 | tee "$SCRATCH/balance-after-withdraw.out"
|
||||
wcli transactions 2>&1 | tee "$SCRATCH/tx-after-withdraw.out" | head -100
|
||||
|
||||
# --- 3) pay high + tiny (+ try atomic) ---
|
||||
pay_order() {
|
||||
local tag="$1" oid="$2" tok="$3"
|
||||
local uri="taler://pay/taler.hacktivism.ch/instances/${INST}/${oid}/?c=${tok}"
|
||||
echo "$uri" >"$SCRATCH/pay-${tag}.uri"
|
||||
log "=== pay $tag order=$oid ==="
|
||||
log "uri=$uri"
|
||||
wcli handle-uri "$uri" 2>&1 | tee "$SCRATCH/pay-${tag}-handle.out" || true
|
||||
# some wallets use payments prepare/confirm
|
||||
wcli run-until-done 2>&1 | tee -a "$SCRATCH/pay-${tag}-rud.out" || true
|
||||
# check merchant
|
||||
ssh -o BatchMode=yes "$KOOPA" "MPW=\$(tr -d '\\n' </root/merchant-${INST}-password.txt); curl -sk -H \"Authorization: Bearer secret-token:\${MPW}\" https://127.0.0.1:9010/instances/${INST}/private/orders/${oid}" \
|
||||
>"$SCRATCH/order-${tag}-status.json" 2>/dev/null || true
|
||||
python3 - "$SCRATCH/order-${tag}-status.json" "$tag" <<'PY' || true
|
||||
import json,sys
|
||||
p,tag=sys.argv[1],sys.argv[2]
|
||||
try:
|
||||
d=json.load(open(p))
|
||||
except Exception as e:
|
||||
print(f"STATUS_{tag} unreadable", e); raise SystemExit
|
||||
print(f"STATUS_{tag}", "order_status=", d.get("order_status"), "paid=", d.get("paid"),
|
||||
"wired=", d.get("wired"), "deposit_total=", d.get("deposit_total"),
|
||||
"amount=", (d.get("contract_terms") or {}).get("amount"))
|
||||
PY
|
||||
}
|
||||
|
||||
pay_order high "$OID_HIGH" "$TOK_HIGH"
|
||||
wcli balance 2>&1 | tee "$SCRATCH/balance-after-high.out"
|
||||
|
||||
pay_order tiny "$OID_TINY" "$TOK_TINY"
|
||||
wcli balance 2>&1 | tee "$SCRATCH/balance-after-tiny.out"
|
||||
|
||||
if [ -n "$OID_ATOMIC" ] && [ -n "$TOK_ATOMIC" ]; then
|
||||
pay_order atomic "$OID_ATOMIC" "$TOK_ATOMIC" || true
|
||||
wcli balance 2>&1 | tee "$SCRATCH/balance-after-atomic.out" || true
|
||||
fi
|
||||
|
||||
# template URIs for docs
|
||||
{
|
||||
echo "taler://pay-template/taler.hacktivism.ch/instances/${INST}/extreme-high/"
|
||||
echo "taler://pay-template/taler.hacktivism.ch/instances/${INST}/extreme-tiny/"
|
||||
echo "taler://pay-template/taler.hacktivism.ch/instances/${INST}/extreme-atomic/"
|
||||
} | tee "$SCRATCH/template-uris.txt"
|
||||
|
||||
log "=== summary ==="
|
||||
python3 - <<PY
|
||||
import json,glob,os
|
||||
scratch="$SCRATCH"
|
||||
print("scratch", scratch)
|
||||
for tag in ("high","tiny","atomic"):
|
||||
p=f"{scratch}/order-{tag}-status.json"
|
||||
if not os.path.isfile(p):
|
||||
print(tag, "no status file"); continue
|
||||
try:
|
||||
d=json.load(open(p))
|
||||
print(tag, "paid=", d.get("paid"), "status=", d.get("order_status"),
|
||||
"amount=", (d.get("contract_terms") or {}).get("amount"),
|
||||
"deposit_total=", d.get("deposit_total"))
|
||||
except Exception as e:
|
||||
print(tag, "err", e)
|
||||
print("template_uris:")
|
||||
print(open(f"{scratch}/template-uris.txt").read())
|
||||
PY
|
||||
|
||||
log "done — details in $SCRATCH"
|
||||
54
scripts/taler-wallet-cli/withdraw-qr1-goa.sh
Executable file
54
scripts/taler-wallet-cli/withdraw-qr1-goa.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
# Local machine: withdraw GOA via bank-integrated QR1 URI (explorer demo account).
|
||||
# Requires: monorepo wallet, ssh koopa, auto-confirm on host.
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
SCRATCH="${ROOT}/.tmp/session-withdraw-$(date -u +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$SCRATCH"
|
||||
MONO="${MONO:-/Users/newkamek/src/taler/taler-typescript-core/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs}"
|
||||
WDB="${WDB:-$SCRATCH/wallet.sqlite3}"
|
||||
EX="${EXCHANGE_URL:-https://exchange.hacktivism.ch/}"
|
||||
URI="${1:-}"
|
||||
|
||||
wcli() { node "$MONO" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"; }
|
||||
|
||||
echo "scratch=$SCRATCH"
|
||||
echo "=== add + accept ToS for $EX ==="
|
||||
wcli exchanges add "$EX" 2>&1 | tee "$SCRATCH/ex-add.out" || true
|
||||
wcli exchanges update "$EX" 2>&1 | tee "$SCRATCH/ex-upd.out" || true
|
||||
wcli exchanges tos "$EX" 2>&1 | tee "$SCRATCH/ex-tos.out" || true
|
||||
wcli exchanges accept-tos "$EX" 2>&1 | tee "$SCRATCH/ex-accept-tos.out"
|
||||
|
||||
if [ -z "$URI" ]; then
|
||||
URI=$(ssh -o BatchMode=yes koopa 'tr -d "\n" </var/www/bank-landing/withdraw.uri')
|
||||
fi
|
||||
echo "URI=$URI"
|
||||
echo "$URI" >"$SCRATCH/withdraw.uri"
|
||||
|
||||
echo "=== withdraw accept-uri ==="
|
||||
wcli withdraw accept-uri --exchange "$EX" "$URI" 2>&1 | tee "$SCRATCH/accept-uri.out"
|
||||
|
||||
WID=$(basename "$URI")
|
||||
echo "=== ensure watch + auto-confirm on koopa ==="
|
||||
ssh -o BatchMode=yes koopa "echo $WID >> /var/www/bank-landing/withdraw-watch.ids; sort -u /var/www/bank-landing/withdraw-watch.ids -o /var/www/bank-landing/withdraw-watch.ids"
|
||||
# one-shot confirm as root via ssh if hernani can sudo? usually not — use screen later
|
||||
# public status
|
||||
curl -sS "https://bank.hacktivism.ch/taler-integration/withdrawals/${WID}" 2>/dev/null | head -c 500 || true
|
||||
echo
|
||||
|
||||
for round in 1 2 3 4 5 6 7 8; do
|
||||
echo "=== run-until-done round $round ==="
|
||||
wcli run-until-done &
|
||||
PID=$!
|
||||
( sleep 20; kill $PID 2>/dev/null ) &
|
||||
wait $PID 2>/dev/null || true
|
||||
wcli balance 2>&1 | tee "$SCRATCH/bal-$round.out"
|
||||
wcli transactions 2>&1 | tee "$SCRATCH/tx-$round.out" | head -80
|
||||
if grep -qE '"available": "GOA:[1-9]' "$SCRATCH/bal-$round.out"; then
|
||||
echo "OK coins received"
|
||||
exit 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
echo "FAIL no balance" >&2
|
||||
exit 1
|
||||
Loading…
Add table
Add a link
Reference in a new issue