From 90f74141728cc6bb90df30f9c841f5f1740de0af Mon Sep 17 00:00:00 2001 From: drjones Date: Wed, 9 Sep 2026 19:42:35 -0700 Subject: [PATCH] =?UTF-8?q?Rigel=20SOCKS5=20proxy=20shop=20v1=20=E2=80=94?= =?UTF-8?q?=20no-KYC=20auth,=20BTCPay,=20Nord+IPRoyal=20inventory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 38 ++++++ app.py | 372 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 410 insertions(+) create mode 100644 README.md create mode 100644 app.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec3f9a8 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Rigel — no-KYC SOCKS5 Proxy Shop + +Self-hosted SOCKS5 proxy storefront. Sells access to residential/mobile/datacenter proxies. Bitcoin via BTCPay. No KYC — username + email + password + arithmetic captcha. + +## Stack +- **App**: Flask single-page app, `/opt/rigel/app.py` (SQLite, no-KYC auth) +- **CT**: 158 `rigel` @ `10.30.20.116`, Debian 12, nginx :80 → Flask :5000 +- **Service**: `rigel.service` (systemd) +- **Payments**: BTCPay store `[redacted]` / key `[redacted]` (webhook → `/api/btcpay/webhook`) +- **Public**: `https://rigel.thetempleofdoom.com` (fleet tunnel `1aeb1ac0`, REMOTE-MANAGED config via CF API) + +## Inventory +| Location | Type | Endpoint | +|---|---|---| +| Tokyo | datacenter (Nord exit) | CT680 `10.30.20.154:1080` | +| London | datacenter (Nord exit) | CT681 `10.30.20.71:1080` | +| Sydney | datacenter (Nord exit) | CT682 `10.30.20.189:1080` | +| Residential | rotating/sticky | IPRoyal `geo.iproyal.com:12321` | +| Mobile 4G | rotating | IPRoyal `4g.iproyal.com` | + +## API +- `POST /api/register` — `{username, email, password, captcha_id, captcha}` +- `POST /api/login` — `{username, password}` +- `GET /api/captcha` — arithmetic challenge +- `POST /api/buy` — `{plan_id}` → creates BTCPay invoice + pending subscription +- `GET /api/subscriptions` — user's active proxies +- `POST /api/btcpay/webhook` — activates subscription + issues creds on payment + +## Deploy +``` +tar czf rigel.tar.gz app.py +scp rigel.tar.gz root@10.30.20.85:/tmp/ +ssh root@10.30.20.85 "pct push 158 /tmp/rigel.tar.gz /tmp/rigel.tar.gz && pct exec 158 -- bash -c 'cd /opt/rigel && tar xzf /tmp/rigel.tar.gz && systemctl restart rigel'" +``` + +## Gotchas +- Fleet tunnel is REMOTE-MANAGED — edit ingress via CF API (`/accounts//cfd_tunnel/1aeb1ac0.../configurations`), NOT the local `config-fleet.yml`. Local edits are ignored. +- BTCPay webhook secret wired via `BTCPAY_WEBHOOK_SECRET` env in `rigel.service`. diff --git a/app.py b/app.py new file mode 100644 index 0000000..2a6e7e0 --- /dev/null +++ b/app.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +""" +Rigel — no-KYC SOCKS5 proxy shop. +Sells access to residential/mobile/datacenter proxies. Bitcoin via BTCPay. + +Auth: no-KYC (username + email + password, no ID). Captcha-lite (math challenge). +Inventory: proxied through gost SOCKS5 frontends with per-user auth. +""" +import os, sqlite3, json, hmac, hashlib, time, uuid, secrets +from datetime import datetime, timedelta +from flask import Flask, request, jsonify, g, render_template_string, redirect, session + +app = Flask(__name__) +app.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32)) + +DB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "rigel.db") + +# ── config ────────────────────────────────────────────────────────────── +# Proxy inventory: each location is a gost frontend. We issue per-user creds. +# The actual upstream SOCKS5 endpoints (Nord CTs) are hidden behind a gost +# auth layer so customers never see the raw CT IPs. +LOCATIONS = { + "tokyo": {"name": "Tokyo, JP", "upstream": "10.30.20.154:1080", "type": "datacenter"}, + "london": {"name": "London, UK", "upstream": "10.30.20.71:1080", "type": "datacenter"}, + "sydney": {"name": "Sydney, AU", "upstream": "10.30.20.189:1080", "type": "datacenter"}, + # IPRoyal residential + mobile are added when creds are provisioned + "residential": {"name": "Residential (rotating)", "upstream": "geo.iproyal.com:12321", "type": "residential"}, + "mobile": {"name": "Mobile 4G (sticky)", "upstream": "us.4g.iproyal.com:7001", "type": "mobile"}, +} + +PLANS = { + "day": {"label": "1 Day", "hours": 24, "sats": 8000}, + "week": {"label": "1 Week", "hours": 168, "sats": 45000}, + "month": {"label": "1 Month", "hours": 720, "sats": 150000}, +} + +BTCPAY_URL = os.environ.get("BTCPAY_URL", "https://10.30.20.140") +BTCPAY_STORE = os.environ.get("BTCPAY_STORE_ID", "") +BTCPAY_TOKEN = os.environ.get("BTCPAY_API_KEY", "") + +def db(): + con = getattr(g, "_db", None) + if con is None: + con = sqlite3.connect(DB) + con.row_factory = sqlite3.Row + g._db = con + return con + +@app.teardown_appcontext +def close_db(exc): + con = getattr(g, "_db", None) + if con is not None: + con.close() + +def init_db(): + con = sqlite3.connect(DB) + con.executescript(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + email TEXT, + password_hash TEXT NOT NULL, + balance_sats INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS subscriptions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + location TEXT NOT NULL, + plan TEXT NOT NULL, + sats_paid INTEGER NOT NULL, + starts_at TEXT, + expires_at TEXT, + proxy_user TEXT, + proxy_pass TEXT, + status TEXT DEFAULT 'pending', -- pending|active|expired + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS invoices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + btcpay_invoice_id TEXT, + amount_sats INTEGER, + status TEXT DEFAULT 'new', -- new|paid|expired + created_at TEXT DEFAULT (datetime('now')) + ); + """) + con.commit() + con.close() + +def hash_pw(pw, salt=None): + salt = salt or secrets.token_hex(16) + return salt + ":" + hmac.new(salt.encode(), pw.encode(), hashlib.sha256).hexdigest() + +def check_pw(pw, stored): + salt, digest = stored.split(":", 1) + return hmac.new(salt.encode(), pw.encode(), hashlib.sha256).hexdigest() == digest + +def current_user(): + uid = session.get("uid") + if not uid: + return None + return db().execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone() + +# ── routes ────────────────────────────────────────────────────────────── +@app.route("/") +def index(): + user = current_user() + if not user: + return render_template_string(AUTH_HTML, mode="login", error="") + return render_template_string(DASH_HTML, user=user, locations=LOCATIONS, plans=PLANS) + +@app.route("/health") +def health(): + return jsonify({"status": "ok", "service": "rigel"}) + +@app.route("/api/register", methods=["POST"]) +def register(): + d = request.get_json(force=True) if request.is_json else request.form + username = (d.get("username") or "").strip() + email = (d.get("email") or "").strip() + pw = d.get("password") or "" + captcha = d.get("captcha") or "" + if not username or not pw or len(username) > 25 or len(pw) < 6: + return jsonify({"error": "username + password (6+ chars) required"}), 400 + if not check_captcha(captcha): + return jsonify({"error": "wrong captcha"}), 400 + con = db() + try: + con.execute("INSERT INTO users (username, email, password_hash) VALUES (?,?,?)", + (username, email, hash_pw(pw))) + con.commit() + except sqlite3.IntegrityError: + return jsonify({"error": "username taken"}), 409 + return jsonify({"ok": True}) + +@app.route("/api/login", methods=["POST"]) +def login(): + d = request.get_json(force=True) if request.is_json else request.form + username = (d.get("username") or "").strip() + pw = d.get("password") or "" + captcha = d.get("captcha") or "" + if not check_captcha(captcha): + return jsonify({"error": "wrong captcha"}), 400 + user = db().execute("SELECT * FROM users WHERE username=?", (username,)).fetchone() + if not user or not check_pw(pw, user["password_hash"]): + return jsonify({"error": "bad credentials"}), 401 + session["uid"] = user["id"] + return jsonify({"ok": True}) + +@app.route("/api/logout", methods=["POST"]) +def logout(): + session.pop("uid", None) + return jsonify({"ok": True}) + +# Captcha-lite: math challenge rendered as text (no external captcha service) +@app.route("/api/captcha") +def captcha(): + a, b = secrets.randbelow(9) + 1, secrets.randbelow(9) + 1 + session["captcha"] = str(a + b) + return jsonify({"question": f"What is {a} + {b}?", "id": "c1"}) + +def check_captcha(ans): + return (ans or "").strip() == session.get("captcha", "") + +@app.route("/api/buy", methods=["POST"]) +def buy(): + user = current_user() + if not user: + return jsonify({"error": "login required"}), 401 + d = request.get_json(force=True) if request.is_json else request.form + location = d.get("location", "") + plan = d.get("plan", "") + if location not in LOCATIONS or plan not in PLANS: + return jsonify({"error": "bad location/plan"}), 400 + sats = PLANS[plan]["sats"] + # create BTCPay invoice + invoice_id, checkout = create_btcpay_invoice(user, sats, f"Rigel {LOCATIONS[location]['name']} {PLANS[plan]['label']}") + con = db() + cur = con.execute("INSERT INTO subscriptions (user_id, location, plan, sats_paid, proxy_user) VALUES (?,?,?,?,?)", + (user["id"], location, plan, sats, f"u{user['id']}{secrets.token_hex(4)}")) + con.execute("INSERT INTO invoices (user_id, btcpay_invoice_id, amount_sats) VALUES (?,?,?)", + (user["id"], invoice_id, sats)) + con.commit() + return jsonify({"ok": True, "invoice_id": invoice_id, "checkout": checkout, "sats": sats}) + +def create_btcpay_invoice(user, sats, desc): + if not BTCPAY_STORE or not BTCPAY_TOKEN: + # fallback: no BTCPay configured — return a placeholder + return "local-" + uuid.uuid4().hex[:12], None + import urllib.request, ssl as _ssl + ctx = _ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = _ssl.CERT_NONE + body = json.dumps({"amount": str(sats/1e8), "currency": "BTC", "metadata": {"desc": desc, "user": user["username"]}}) + req = urllib.request.Request(f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices", + data=body.encode(), headers={"Authorization": f"token {BTCPAY_TOKEN}", "Content-Type": "application/json"}, method="POST") + r = urllib.request.urlopen(req, timeout=20, context=ctx) + inv = json.loads(r.read()) + return inv["id"], inv.get("checkoutLink") + +@app.route("/api/subscriptions") +def subscriptions(): + user = current_user() + if not user: + return jsonify({"error": "login required"}), 401 + subs = db().execute("SELECT * FROM subscriptions WHERE user_id=? ORDER BY id DESC", (user["id"],)).fetchall() + return jsonify([dict(s) for s in subs]) + +@app.route("/api/status") +def status(): + user = current_user() + if not user: + return jsonify({"authenticated": False}) + return jsonify({"authenticated": True, "username": user["username"], "balance_sats": user["balance_sats"]}) + +# ── BTCPay webhook: activate subscription on payment ───────────────────── +WEBHOOK_SECRET = os.environ.get("BTCPAY_WEBHOOK_SECRET", "") + +@app.route("/api/btcpay/webhook", methods=["POST"]) +def btcpay_webhook(): + # optional HMAC verification (raw body) + if WEBHOOK_SECRET: + raw = request.get_data() + sig = request.headers.get("BTCPay-Sig", "") + expected = "sha256=" + hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest() + if not hmac.compare_digest(sig, expected): + return jsonify({"error": "bad signature"}), 401 + payload = request.get_json(force=True, silent=True) or {} + etype = payload.get("type", "") + invoice_id = payload.get("invoiceId", "") + status = payload.get("status", "") + + if etype in ("InvoiceSettled", "InvoiceReceivedPayment") and invoice_id: + # find matching invoice -> subscription + con = sqlite3.connect(DB) + con.row_factory = sqlite3.Row + inv = con.execute("SELECT * FROM invoices WHERE btcpay_invoice_id=? AND status!='paid'", (invoice_id,)).fetchone() + if inv: + con.execute("UPDATE invoices SET status='paid' WHERE btcpay_invoice_id=?", (invoice_id,)) + # activate subscription + issue proxy creds + sub = con.execute("SELECT * FROM subscriptions WHERE user_id=? AND status='pending' ORDER BY id DESC LIMIT 1", + (inv["user_id"],)).fetchone() + if sub: + proxy_user = sub["proxy_user"] or f"u{inv['user_id']}{secrets.token_hex(4)}" + proxy_pass = secrets.token_hex(12) + hours = PLANS.get(sub["plan"], {}).get("hours", 24) + now = datetime.utcnow() + expires = now + timedelta(hours=hours) + con.execute("""UPDATE subscriptions SET status='active', proxy_user=?, proxy_pass=?, + starts_at=?, expires_at=? WHERE id=?""", + (proxy_user, proxy_pass, now.isoformat(), expires.isoformat(), sub["id"])) + # provision the proxy auth upstream (gost) — best-effort + provision_proxy(sub["location"], proxy_user, proxy_pass) + con.commit() + con.close() + return jsonify({"ok": True}) + +def provision_proxy(location, user, pw): + """Best-effort: add user auth to the gost frontend for this location. + The gost SOCKS5 frontends need per-user auth; for now we record the creds + and the actual gost auth layer is wired per-location (see rigel-proxy setup).""" + # Placeholder — the real gost auth is applied via the proxy gateway config. + # Creds are persisted on the subscription row so the customer sees them. + app.logger.info(f"provision proxy {location} for {user}") + +# ── templates ─────────────────────────────────────────────────────────── +AUTH_HTML = r""" + +Rigel — SOCKS5 Proxies +
+

✦ Rigel

No-KYC SOCKS5 & residential proxies. Pay in Bitcoin.

+
+
+""" + +DASH_HTML = r""" + +Rigel — Dashboard + +

✦ Rigel — {{user["username"]}}

+
+
+{% for key, loc in locations.items() %} +
+

{{loc["name"]}}

{{loc["type"]}}
+
+ {% for pk, p in plans.items() %} +
{{p["label"]}}{{p["sats"]}} sats
+ {% endfor %} +
+
+{% endfor %} +
+

Your subscriptions

Loading…
+
+ +""" + +if __name__ == "__main__": + init_db() + app.run(host="0.0.0.0", port=5000, debug=False)