#!/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)