338 lines
12 KiB
Python
338 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Finish ntfy auth, Umami sites, tracker injection, rebuilds."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import secrets
|
|
import string
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import paramiko
|
|
|
|
HOST = "107.173.30.245"
|
|
PASS = os.environ.get("NOMADRO_SSH_PASS", "Xiao4669805")
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
CONF = "/etc/nomadro"
|
|
NTFY_DIR = "/opt/ntfy"
|
|
|
|
SITES = {
|
|
"nomadro.com": "Nomadro (blog + app)",
|
|
"portal.nomadro.com": "Nomadro portal",
|
|
"dylive.nomadro.com": "DouyinWEB / dylive",
|
|
"nomadro.live": "TikTok Promo / nomadro.live",
|
|
"wx.nomadro.com": "HackRobot / wx",
|
|
"x.nomadro.com": "XAuto / x",
|
|
}
|
|
|
|
NEXT_LAYOUTS = [
|
|
("/var/www/nomadro.com/app/layout.tsx", "nomadro.com"),
|
|
("/opt/douyinWEB/frontend/src/app/layout.tsx", "dylive.nomadro.com"),
|
|
("/opt/tiktokWEB/frontend/src/app/layout.tsx", "nomadro.live"),
|
|
("/opt/wauto/web/app/layout.tsx", "wx.nomadro.com"),
|
|
]
|
|
|
|
|
|
def gen(n=20):
|
|
return "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(n))
|
|
|
|
|
|
def run(c, cmd, timeout=300):
|
|
print("$", cmd[:140])
|
|
_i, o, e = c.exec_command(cmd, timeout=timeout)
|
|
# channel timeout
|
|
chan = _i.channel if hasattr(_i, "channel") else o.channel
|
|
chan.settimeout(timeout)
|
|
out = (o.read() + e.read()).decode("utf-8", errors="replace")
|
|
if out.strip():
|
|
print(out[-3000:] if len(out) > 3000 else out)
|
|
return out
|
|
|
|
|
|
def main():
|
|
c = paramiko.SSHClient()
|
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
c.connect(HOST, username="root", password=PASS, timeout=30, allow_agent=False, look_for_keys=False)
|
|
sftp = c.open_sftp()
|
|
|
|
# Load or create umami admin password
|
|
try:
|
|
with sftp.file(f"{CONF}/umami.env", "r") as f:
|
|
umami_env = f.read().decode()
|
|
except OSError:
|
|
umami_env = ""
|
|
umami_pass = None
|
|
for line in umami_env.splitlines():
|
|
if line.startswith("UMAMI_PASSWORD="):
|
|
umami_pass = line.split("=", 1)[1].strip()
|
|
if not umami_pass:
|
|
umami_pass = gen()
|
|
with sftp.file(f"{CONF}/umami.env", "w") as f:
|
|
f.write(
|
|
f"UMAMI_URL=https://umami.nomadro.com\nUMAMI_USER=admin\nUMAMI_PASSWORD={umami_pass}\n"
|
|
)
|
|
|
|
ntfy_pass = gen()
|
|
|
|
# ntfy user + token — use expect-like via script file to avoid hang
|
|
run(
|
|
c,
|
|
f"""
|
|
set -e
|
|
cd {NTFY_DIR}
|
|
# Remove incomplete user if any
|
|
docker compose exec -T ntfy ntfy user del admin 2>/dev/null || true
|
|
|
|
# Non-interactive add: ntfy reads password from NTFY_PASSWORD env in recent versions
|
|
docker compose exec -T -e NTFY_PASSWORD='{ntfy_pass}' ntfy ntfy user add --role=admin admin 2>/tmp/ntfy_user_err || true
|
|
if ! docker compose exec -T ntfy ntfy user list 2>/dev/null | grep -q admin; then
|
|
# fallback: hash + config — use `ntfy user hash` then add with password hash if available
|
|
HASH=$(docker compose exec -T ntfy ntfy hash '{ntfy_pass}' 2>/dev/null | tr -d '\\r' || true)
|
|
echo "hash=$HASH"
|
|
# try password flag variants
|
|
docker compose exec -T ntfy ntfy user add admin --role=admin --password '{ntfy_pass}' 2>/tmp/ntfy_err2 || true
|
|
fi
|
|
docker compose exec -T ntfy ntfy user list || true
|
|
docker compose exec -T ntfy ntfy access admin server-alerts rw || docker compose exec -T ntfy ntfy access admin server-alerts read-write || true
|
|
docker compose exec -T ntfy ntfy access admin deploy rw || docker compose exec -T ntfy ntfy access admin deploy read-write || true
|
|
|
|
# Create token
|
|
TOKEN_OUT=$(docker compose exec -T ntfy ntfy token add admin 2>&1 | tr -d '\\r')
|
|
echo "TOKEN_OUT=$TOKEN_OUT"
|
|
TOKEN=$(echo "$TOKEN_OUT" | grep -Eo 'tk_[A-Za-z0-9]+' | tail -1)
|
|
if [ -z "$TOKEN" ]; then
|
|
TOKEN=$(echo "$TOKEN_OUT" | awk 'NF{{print $NF}}' | tail -1)
|
|
fi
|
|
echo "TOKEN=$TOKEN"
|
|
cat > {CONF}/ntfy.env <<EOF
|
|
NTFY_BASE_URL=https://ntfy.nomadro.com
|
|
NTFY_USER=admin
|
|
NTFY_PASSWORD={ntfy_pass}
|
|
NTFY_TOKEN=$TOKEN
|
|
NTFY_TOPIC=server-alerts
|
|
EOF
|
|
chmod 600 {CONF}/ntfy.env {CONF}/umami.env
|
|
cat {CONF}/ntfy.env
|
|
""",
|
|
timeout=90,
|
|
)
|
|
|
|
# Install helpers
|
|
run(
|
|
c,
|
|
f"""
|
|
cp {CONF}/observability-src/notify-ntfy.sh /usr/local/bin/notify-ntfy
|
|
cp {CONF}/observability-src/healthcheck.sh /usr/local/bin/nomadro-healthcheck
|
|
chmod +x /usr/local/bin/notify-ntfy /usr/local/bin/nomadro-healthcheck
|
|
cat >/etc/cron.d/nomadro-health <<'EOF'
|
|
*/5 * * * * root /usr/local/bin/nomadro-healthcheck >/dev/null 2>&1
|
|
EOF
|
|
""",
|
|
)
|
|
|
|
# Wait public umami
|
|
for _ in range(40):
|
|
out = run(c, "curl -fsS -o /dev/null -w '%{http_code}' https://umami.nomadro.com/api/heartbeat || echo x")
|
|
if "200" in out:
|
|
break
|
|
time.sleep(3)
|
|
|
|
sites_json = json.dumps(SITES)
|
|
out = run(
|
|
c,
|
|
f"""
|
|
python3 - <<'PY'
|
|
import json, urllib.request, urllib.error
|
|
admin_pass = {umami_pass!r}
|
|
base = "https://umami.nomadro.com"
|
|
sites = json.loads({sites_json!r})
|
|
|
|
def req(url, method="GET", data=None, token=None):
|
|
body = None if data is None else json.dumps(data).encode()
|
|
headers = {{"Content-Type":"application/json","Accept":"application/json"}}
|
|
if token:
|
|
headers["Authorization"] = f"Bearer {{token}}"
|
|
r = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
try:
|
|
with urllib.request.urlopen(r, timeout=45) as resp:
|
|
raw = resp.read().decode()
|
|
return resp.status, json.loads(raw) if raw else {{}}
|
|
except urllib.error.HTTPError as e:
|
|
raw = e.read().decode(errors="replace")
|
|
try:
|
|
return e.code, json.loads(raw) if raw else {{}}
|
|
except Exception:
|
|
return e.code, {{"raw": raw}}
|
|
|
|
code, login = req(base+"/api/auth/login","POST",{{"username":"admin","password":admin_pass}})
|
|
if code != 200:
|
|
code, login = req(base+"/api/auth/login","POST",{{"username":"admin","password":"umami"}})
|
|
if code != 200:
|
|
raise SystemExit(f"login failed {{code}} {{login}}")
|
|
token = login["token"]; uid = login["user"]["id"]
|
|
code2, upd = req(base+f"/api/users/{{uid}}","POST",{{"password":admin_pass}}, token=token)
|
|
print("password update", code2, upd)
|
|
code, login = req(base+"/api/auth/login","POST",{{"username":"admin","password":admin_pass}})
|
|
if code != 200:
|
|
raise SystemExit(f"re-login failed {{code}} {{login}}")
|
|
token = login["token"]
|
|
code, listing = req(base+"/api/websites", token=token)
|
|
items = listing if isinstance(listing, list) else (listing.get("data") or listing.get("websites") or [])
|
|
existing = {{w["domain"]: w["id"] for w in items if isinstance(w, dict) and w.get("domain")}}
|
|
result = {{}}
|
|
for domain, name in sites.items():
|
|
if domain in existing:
|
|
result[domain] = existing[domain]
|
|
continue
|
|
code, created = req(base+"/api/websites","POST",{{"name":name,"domain":domain}}, token=token)
|
|
print("create", domain, code, created)
|
|
if code not in (200,201) or "id" not in created:
|
|
raise SystemExit(f"create failed {{domain}}")
|
|
result[domain] = created["id"]
|
|
with open("{CONF}/umami-sites.env","w") as f:
|
|
for d, wid in result.items():
|
|
key = d.replace(".","_").replace("-","_").upper()
|
|
f.write(f"UMAMI_ID_{{key}}={{wid}}\\n")
|
|
f.write("UMAMI_SCRIPT=https://umami.nomadro.com/script.js\\n")
|
|
print("SITE_IDS="+json.dumps(result))
|
|
PY
|
|
""",
|
|
timeout=120,
|
|
)
|
|
|
|
env_map = {}
|
|
for line in run(c, f"cat {CONF}/umami-sites.env").splitlines():
|
|
if line.startswith("UMAMI_ID_") and "=" in line:
|
|
k, v = line.split("=", 1)
|
|
env_map[k] = v.strip()
|
|
|
|
def id_for(domain: str) -> str:
|
|
key = "UMAMI_ID_" + domain.replace(".", "_").replace("-", "_").upper()
|
|
return env_map[key]
|
|
|
|
wid_nomadro = id_for("nomadro.com")
|
|
mu = (REPO / "deploy/wordpress/mu-plugins/nomadro-adsense.php").read_text(encoding="utf-8")
|
|
# Only replace the const assignment so comparison/placeholder logic stays intact
|
|
mu = mu.replace(
|
|
"const NOMADRO_UMAMI_WEBSITE_ID = 'REPLACE_UMAMI_WEBSITE_ID';",
|
|
f"const NOMADRO_UMAMI_WEBSITE_ID = '{wid_nomadro}';",
|
|
1,
|
|
)
|
|
with sftp.file("/tmp/nomadro-adsense.php", "w") as f:
|
|
f.write(mu)
|
|
run(
|
|
c,
|
|
"""
|
|
docker compose -f /var/www/nomadro-wp/docker-compose.yml cp /tmp/nomadro-adsense.php wordpress:/var/www/html/wp-content/mu-plugins/nomadro-adsense.php
|
|
docker compose -f /var/www/nomadro-wp/docker-compose.yml exec -T wordpress chown www-data:www-data /var/www/html/wp-content/mu-plugins/nomadro-adsense.php
|
|
mkdir -p /var/www/nomadro-wp/mu-plugins && cp /tmp/nomadro-adsense.php /var/www/nomadro-wp/mu-plugins/nomadro-adsense.php
|
|
""",
|
|
)
|
|
|
|
run(
|
|
c,
|
|
f"""
|
|
touch /var/www/nomadro.com/.env.local
|
|
grep -q '^NEXT_PUBLIC_UMAMI_WEBSITE_ID=' /var/www/nomadro.com/.env.local \\
|
|
&& sed -i 's/^NEXT_PUBLIC_UMAMI_WEBSITE_ID=.*/NEXT_PUBLIC_UMAMI_WEBSITE_ID={wid_nomadro}/' /var/www/nomadro.com/.env.local \\
|
|
|| echo 'NEXT_PUBLIC_UMAMI_WEBSITE_ID={wid_nomadro}' >> /var/www/nomadro.com/.env.local
|
|
""",
|
|
)
|
|
sftp.put(str(REPO / "components/umami-analytics.tsx"), "/var/www/nomadro.com/components/umami-analytics.tsx")
|
|
sftp.put(str(REPO / "app/layout.tsx"), "/var/www/nomadro.com/app/layout.tsx")
|
|
|
|
for layout, domain in NEXT_LAYOUTS:
|
|
wid = id_for(domain)
|
|
run(
|
|
c,
|
|
f"""
|
|
python3 - <<'PY'
|
|
from pathlib import Path
|
|
import re
|
|
path = Path({layout!r})
|
|
wid = {wid!r}
|
|
text = path.read_text(encoding='utf-8')
|
|
line = f'<script defer src="https://umami.nomadro.com/script.js" data-website-id="{{wid}}"></script>'
|
|
if "umami.nomadro.com/script.js" in text:
|
|
text = re.sub(r'<script defer src="https://umami\\.nomadro\\.com/script\\.js" data-website-id="[^"]*"></script>', line, text)
|
|
else:
|
|
if "<html" in text and "<head" not in text:
|
|
text = re.sub(r'(<html[^>]*>)', r'\\1\\n <head>\\n '+line+r'\\n </head>', text, count=1)
|
|
elif "<head>" in text:
|
|
text = text.replace("<head>", "<head>\\n "+line, 1)
|
|
elif "<body" in text:
|
|
text = text.replace("<body", " "+line+"\\n <body", 1)
|
|
else:
|
|
text += "\\n"+line+"\\n"
|
|
path.write_text(text, encoding='utf-8')
|
|
print('patched', path)
|
|
PY
|
|
""",
|
|
)
|
|
|
|
# Also write env files for other next apps
|
|
for path, domain in [
|
|
("/opt/douyinWEB/frontend/.env.local", "dylive.nomadro.com"),
|
|
("/opt/tiktokWEB/frontend/.env.local", "nomadro.live"),
|
|
("/opt/wauto/web/.env.local", "wx.nomadro.com"),
|
|
]:
|
|
wid = id_for(domain)
|
|
run(
|
|
c,
|
|
f"""
|
|
touch {path}
|
|
grep -q '^NEXT_PUBLIC_UMAMI_WEBSITE_ID=' {path} \\
|
|
&& sed -i 's/^NEXT_PUBLIC_UMAMI_WEBSITE_ID=.*/NEXT_PUBLIC_UMAMI_WEBSITE_ID={wid}/' {path} \\
|
|
|| echo 'NEXT_PUBLIC_UMAMI_WEBSITE_ID={wid}' >> {path}
|
|
""",
|
|
)
|
|
|
|
rebuilds = [
|
|
("cd /var/www/nomadro.com && pnpm build && systemctl restart nomadro", 900),
|
|
("cd /opt/douyinWEB/frontend && npm run build && systemctl restart douyinweb-web", 900),
|
|
(
|
|
"cd /opt/tiktokWEB/frontend && npm run build && "
|
|
"mkdir -p .next/standalone/.next && cp -a .next/static .next/standalone/.next/static 2>/dev/null || true; "
|
|
"systemctl restart tiktok-promo-frontend",
|
|
900,
|
|
),
|
|
(
|
|
"cd /opt/wauto/web && npm run build && "
|
|
"mkdir -p .next/standalone/.next && cp -a public .next/standalone/ 2>/dev/null || true; "
|
|
"cp -a .next/static .next/standalone/.next/static 2>/dev/null || true; "
|
|
"systemctl restart hackrobot-web",
|
|
900,
|
|
),
|
|
]
|
|
for cmd, t in rebuilds:
|
|
try:
|
|
run(c, cmd, timeout=t)
|
|
except Exception as ex:
|
|
print("rebuild warn", ex)
|
|
|
|
# Test notify (may fail if token empty)
|
|
run(c, "/usr/local/bin/notify-ntfy 'Observability online' 'Umami + ntfy ready' high 'tada' || true")
|
|
|
|
run(
|
|
c,
|
|
"""
|
|
echo '=== verify ==='
|
|
curl -fsS https://umami.nomadro.com/api/heartbeat; echo
|
|
curl -fsS https://ntfy.nomadro.com/v1/health; echo
|
|
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'umami|ntfy|NAMES'
|
|
echo '--- umami sites ---'; cat /etc/nomadro/umami-sites.env
|
|
echo '--- sample homepage umami ---'
|
|
curl -fsS -A 'Mozilla/5.0' https://nomadro.com/ | grep -o 'umami.nomadro.com/script.js' | head -3
|
|
curl -fsS -A 'Googlebot' https://nomadro.com/ | grep -o 'data-website-id="[^"]*"' | head -3
|
|
""",
|
|
)
|
|
|
|
sftp.close()
|
|
c.close()
|
|
print("DONE")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|