From 9d084373dca656bc9d7a84808380b1762d9525fd Mon Sep 17 00:00:00 2001 From: drjones Date: Sat, 19 Sep 2026 18:19:46 -0700 Subject: [PATCH] Pleiades proxy store: SOCKS5 gateway + Flask storefront + BTCPay checkout --- .gitignore | 6 + admin.py | 82 ++++++++++ app.py | 312 +++++++++++++++++++++++++++++++++++++++ app.service | 13 ++ config.example.json | 27 ++++ db.py | 120 +++++++++++++++ gateway.py | 243 ++++++++++++++++++++++++++++++ gateway.service | 13 ++ nginx.conf | 19 +++ static/style.css | 145 ++++++++++++++++++ templates/base.html | 42 ++++++ templates/dashboard.html | 58 ++++++++ templates/index.html | 41 +++++ templates/login.html | 14 ++ templates/order.html | 15 ++ templates/plans.html | 29 ++++ templates/signup.html | 15 ++ 17 files changed, 1194 insertions(+) create mode 100644 .gitignore create mode 100644 admin.py create mode 100644 app.py create mode 100644 app.service create mode 100644 config.example.json create mode 100644 db.py create mode 100644 gateway.py create mode 100644 gateway.service create mode 100644 nginx.conf create mode 100644 static/style.css create mode 100644 templates/base.html create mode 100644 templates/dashboard.html create mode 100644 templates/index.html create mode 100644 templates/login.html create mode 100644 templates/order.html create mode 100644 templates/plans.html create mode 100644 templates/signup.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c97ae4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +config.json +*.db +venv/ +__pycache__/ +*.pyc +.env diff --git a/admin.py b/admin.py new file mode 100644 index 0000000..9ce6d17 --- /dev/null +++ b/admin.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Pleiades admin CLI — manage users, plans, balances.""" +import sys +import time + +import db + + +def create_user(username, web_pass, proxy_pass, country="", city="", region="", + sticky=1, balance_gb=0, active=1): + d = db.get_db() + try: + d.execute( + "INSERT INTO users (username,password_hash,proxy_password_hash," + "proxy_password_plain,active,region,country,city,sticky,balance_gb,created_at) " + "VALUES (?,?,?,?,?,?,?,?,?,?,?)", + (username, db.scrypt_hash(web_pass), db.scrypt_hash(proxy_pass), proxy_pass, + int(active), region, country, city, int(sticky), float(balance_gb), int(time.time()))) + d.commit() + print(f"OK created user '{username}'") + except Exception as e: + print(f"ERROR: {e}") + finally: + d.close() + + +def set_country(username, country): + d = db.get_db() + d.execute("UPDATE users SET country=? WHERE username=?", (country, username)) + d.commit() + d.close() + print(f"OK {username} country={country}") + + +def set_field(username, field, value): + d = db.get_db() + d.execute(f"UPDATE users SET {field}=? WHERE username=?", (value, username)) + d.commit() + d.close() + print(f"OK {username} {field}={value}") + + +def add_balance(username, gb): + d = db.get_db() + d.execute("UPDATE users SET balance_gb=balance_gb+? WHERE username=?", (float(gb), username)) + d.commit() + d.close() + print(f"OK {username} +{gb} GB") + + +def list_users(): + d = db.get_db() + for r in d.execute("SELECT id,username,active,country,city,sticky,balance_gb FROM users"): + print(f"#{r['id']} {r['username']} active={r['active']} geo={r['country'] or '-'}/{r['city'] or '-'} " + f"sticky={r['sticky']} bal={r['balance_gb']}GB") + d.close() + + +def main(): + db.init_db() + if len(sys.argv) < 2: + print(__doc__) + return + cmd = sys.argv[1] + a = sys.argv[2:] + if cmd == "create-user" and len(a) >= 3: + create_user(a[0], a[1], a[2], *a[3:]) + elif cmd == "set-country" and len(a) == 2: + set_country(a[0], a[1]) + elif cmd == "set" and len(a) == 3: + set_field(a[0], a[1], a[2]) + elif cmd == "add-balance" and len(a) == 2: + add_balance(a[0], a[1]) + elif cmd == "list": + list_users() + else: + print("commands: create-user [country city region sticky bal active] | " + "set-country | set | add-balance | list") + + +if __name__ == "__main__": + main() diff --git a/app.py b/app.py new file mode 100644 index 0000000..69b6c5b --- /dev/null +++ b/app.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +"""Pleiades storefront — Flask app: signup/login, plans, BTCPay checkout, dashboard.""" +import base64 +import hashlib +import hmac +import json +import time +from functools import wraps + +import requests +from flask import (Flask, abort, flash, jsonify, redirect, render_template, + request, session, url_for) + +import db + +CFG = db.CFG +app = Flask(__name__) +app.secret_key = CFG["secret_key"] + +BTCPAY = CFG["btcpay"] +PROXY = CFG["proxy_public"] +SITE_HOST = CFG.get("site_host", "clean-proxys.thetempleofdoom.com") + +PRICE_USD_PER_GB = CFG.get("price_usd_per_gb", 8.0) # display only + + +# ---------- auth ---------- +def current_user(): + uid = session.get("uid") + if not uid: + return None + d = db.get_db() + row = d.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone() + d.close() + return dict(row) if row else None + + +def login_required(f): + @wraps(f) + def wrapper(*a, **kw): + if not current_user(): + flash("Log in first.", "warn") + return redirect(url_for("login")) + return f(*a, **kw) + return wrapper + + +# ---------- BTCPay ---------- +def create_invoice(amount_sats, order_id): + data = { + "amount": amount_sats / 1e8, + "currency": "BTC", + "checkout": { + "redirectURL": f"https://{SITE_HOST}/order/{order_id}", + "redirectAutomatically": True, + }, + "metadata": {"orderId": order_id}, + } + r = requests.post( + f"{BTCPAY['url']}/api/v1/stores/{BTCPAY['store_id']}/invoices", + headers={"Authorization": f"token {BTCPAY['api_key']}"}, + json=data, timeout=20, verify=False, + ) + r.raise_for_status() + return r.json() + + +def verify_webhook(): + sig = request.headers.get("BTCPay-Sig", "") + if not sig.startswith("sha256="): + return False + expected = sig.split("=", 1)[1] + calc = hmac.new(BTCPAY["webhook_secret"].encode(), request.get_data(), + hashlib.sha256).hexdigest() + return hmac.compare_digest(calc, expected) + + +# ---------- routes ---------- +@app.route("/") +def index(): + return render_template("index.html", user=current_user()) + + +@app.route("/signup", methods=["GET", "POST"]) +def signup(): + if request.method == "POST": + username = request.form.get("username", "").strip() + password = request.form.get("password", "") + if len(username) < 3 or len(password) < 6: + flash("Username 3+ chars, password 6+ chars.", "warn") + return render_template("signup.html") + d = db.get_db() + exists = d.execute("SELECT 1 FROM users WHERE username=?", (username,)).fetchone() + if exists: + d.close() + flash("Username taken.", "warn") + return render_template("signup.html") + proxy_pass = db.random_token(18) + d.execute( + "INSERT INTO users (username,password_hash,proxy_password_hash,proxy_password_plain," + "active,region,country,city,sticky,balance_gb,created_at) VALUES (?,?,?,?,0,'','US','',1,0,?)", + (username, db.scrypt_hash(password), db.scrypt_hash(proxy_pass), proxy_pass, + int(time.time()))) + d.commit() + uid = d.execute("SELECT id FROM users WHERE username=?", (username,)).fetchone()["id"] + d.close() + session["uid"] = uid + flash(f"Account created. Proxy password: {proxy_pass} (save it)", "ok") + return redirect(url_for("dashboard")) + return render_template("signup.html") + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if request.method == "POST": + username = request.form.get("username", "").strip() + password = request.form.get("password", "") + d = db.get_db() + row = d.execute("SELECT * FROM users WHERE username=?", (username,)).fetchone() + d.close() + if row and db.verify_hash(password, row["password_hash"]): + session["uid"] = row["id"] + return redirect(url_for("dashboard")) + flash("Bad credentials.", "warn") + return render_template("login.html") + + +@app.route("/logout") +def logout(): + session.clear() + return redirect(url_for("index")) + + +@app.route("/plans") +def plans(): + d = db.get_db() + ps = [dict(r) for r in d.execute("SELECT * FROM plans WHERE active=1 ORDER BY gb")] + d.close() + return render_template("plans.html", plans=ps, user=current_user()) + + +@app.route("/buy/", methods=["POST"]) +@login_required +def buy(plan_id): + d = db.get_db() + plan = d.execute("SELECT * FROM plans WHERE id=?", (plan_id,)).fetchone() + d.close() + if not plan: + abort(404) + order_id = db.random_token(12) + d = db.get_db() + d.execute("INSERT INTO orders (user_id,plan_id,invoice_id,amount_sats,status,created_at) " + "VALUES (?,?,?,?,'pending',?)", + (current_user()["id"], plan_id, order_id, plan["price_sats"], int(time.time()))) + d.commit() + d.close() + inv = create_invoice(plan["price_sats"], order_id) + return redirect(inv["checkoutLink"]) + + +@app.route("/order/") +@login_required +def order(order_id): + d = db.get_db() + o = d.execute("SELECT * FROM orders WHERE invoice_id=? AND user_id=?", + (order_id, current_user()["id"])).fetchone() + d.close() + if not o: + abort(404) + return render_template("order.html", order=dict(o), user=current_user()) + + +@app.route("/webhook/btcpay", methods=["POST"]) +def webhook_btcpay(): + if not verify_webhook(): + abort(401) + payload = request.get_json(force=True) + if payload.get("type") not in ("InvoiceSettled", "InvoiceProcessing"): + return "ok" + order_id = (payload.get("metadata") or {}).get("orderId") + if not order_id: + return "ok" + d = db.get_db() + o = d.execute("SELECT * FROM orders WHERE invoice_id=?", (order_id,)).fetchone() + if o and o["status"] != "paid": + plan = d.execute("SELECT * FROM plans WHERE id=?", (o["plan_id"],)).fetchone() + d.execute("UPDATE orders SET status='paid', paid_at=? WHERE id=?", (int(time.time()), o["id"])) + d.execute("UPDATE users SET balance_gb=balance_gb+?, active=1 WHERE id=?", + (plan["gb"], o["user_id"])) + d.commit() + d.close() + return "ok" + + +@app.route("/dashboard") +@login_required +def dashboard(): + u = current_user() + d = db.get_db() + used = d.execute("SELECT COALESCE(SUM(bytes),0) s FROM usage_log WHERE user_id=?", (u["id"],)).fetchone()["s"] + d.close() + socks = f"socks5://{u['username']}:{u['proxy_password_plain']}@{PROXY['host']}:{PROXY['socks_port']}" + http = f"http://{u['username']}:{u['proxy_password_plain']}@{PROXY['host']}:{PROXY['http_port']}" + return render_template("dashboard.html", user=u, used_gb=used / 1e9, + socks=socks, http=http) + + +@app.route("/dashboard/location", methods=["POST"]) +@login_required +def set_location(): + u = current_user() + country = request.form.get("country", "").strip().upper() + city = request.form.get("city", "").strip() + region = request.form.get("region", "").strip() + sticky = 1 if request.form.get("sticky") else 0 + d = db.get_db() + d.execute("UPDATE users SET country=?, city=?, region=?, sticky=? WHERE id=?", + (country, city, region, sticky, u["id"])) + d.commit() + d.close() + flash("Location updated.", "ok") + return redirect(url_for("dashboard")) + + +@app.route("/dashboard/rotate", methods=["POST"]) +@login_required +def rotate(): + u = current_user() + newpass = db.random_token(18) + d = db.get_db() + d.execute("UPDATE users SET proxy_password_hash=?, proxy_password_plain=? WHERE id=?", + (db.scrypt_hash(newpass), newpass, u["id"])) + d.commit() + d.close() + flash(f"New proxy password: {newpass}", "ok") + return redirect(url_for("dashboard")) + + +# ---------- agent discovery ---------- +@app.route("/llms.txt") +def llms_txt(): + body = f"""# {SITE_HOST} + +> Clean residential SOCKS5/HTTP proxies. No KYC. Crypto checkout. Encrypted backend. + +## What this service does +Sells residential SOCKS5 and HTTP CONNECT proxies with global geo-targeting +(country/city/region). Buy with Bitcoin (no KYC). Instant activation. + +## Key facts +- Protocol: SOCKS5 (port {PROXY['socks_port']}), HTTP CONNECT (port {PROXY['http_port']}) +- Auth: per-user username + password +- Geo: country, city, region targeting +- Sessions: sticky or rotating +- Billing: pay-per-GB, Bitcoin (BTCPay) +- Signup: username + password only (no email, no KYC) + +## API +- GET /api/v1/info -> service info (JSON) +- GET /api/v1/plans -> available plans (JSON) +- Human signup: {SITE_HOST}/signup + +## Connect (after signup + top-up) +socks5://USER:PASS@{PROXY['host']}:{PROXY['socks_port']} +""" + return app.response_class(body, mimetype="text/plain") + + +@app.route("/robots.txt") +def robots(): + return app.response_class( + "User-agent: *\nAllow: /\nSitemap: https://%s/sitemap.xml\n" % SITE_HOST, + mimetype="text/plain") + + +@app.route("/api/v1/info") +def api_info(): + return jsonify({ + "name": "Clean Proxys", + "host": SITE_HOST, + "protocols": ["socks5", "http"], + "socks_port": PROXY["socks_port"], + "http_port": PROXY["http_port"], + "auth": "username:password", + "billing": "bitcoin (btcpay), pay-per-GB", + "kyc": False, + "signup": f"https://{SITE_HOST}/signup", + "docs": f"https://{SITE_HOST}/llms.txt", + }) + + +@app.route("/api/v1/plans") +def api_plans(): + d = db.get_db() + ps = [dict(r) for r in d.execute("SELECT id,name,gb,price_sats FROM plans WHERE active=1 ORDER BY gb")] + d.close() + return jsonify({"plans": ps}) + + +@app.route("/sitemap.xml") +def sitemap(): + urls = [f"https://{SITE_HOST}/{p}" for p in ["", "plans", "signup", "login", "llms.txt"]] + body = '\n\n' + for u in urls: + body += f" {u}\n" + body += "" + return app.response_class(body, mimetype="application/xml") + + +if __name__ == "__main__": + db.init_db() + app.run(host="127.0.0.1", port=5000, debug=False) diff --git a/app.service b/app.service new file mode 100644 index 0000000..e511592 --- /dev/null +++ b/app.service @@ -0,0 +1,13 @@ +[Unit] +Description=Pleiades storefront (Flask via gunicorn) +After=network.target + +[Service] +Type=simple +WorkingDirectory=/opt/pleiades +ExecStart=/opt/pleiades/venv/bin/gunicorn -w 2 -b 127.0.0.1:5000 app:app +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..6fab180 --- /dev/null +++ b/config.example.json @@ -0,0 +1,27 @@ +{ + "upstream": { + "host": "geo.iproyal.com", + "port": 12321, + "username": "YOUR_IPROYAL_USERNAME", + "password": "YOUR_IPROYAL_PASSWORD" + }, + "gateway": { + "listen": "0.0.0.0", + "socks_port": 1080, + "http_port": 8080 + }, + "db_path": "/opt/pleiades/pleiades.db", + "secret_key": "RANDOM_32_BYTES_HEX", + "site_host": "clean-proxys.thetempleofdoom.com", + "proxy_public": { + "host": "YOUR_WAN_IP", + "socks_port": 1080, + "http_port": 8080 + }, + "btcpay": { + "url": "https://10.30.20.140", + "api_key": "BTCPAY_SCOPED_KEY", + "store_id": "BTCPAY_STORE_ID", + "webhook_secret": "WEBHOOK_SECRET" + } +} diff --git a/db.py b/db.py new file mode 100644 index 0000000..2551795 --- /dev/null +++ b/db.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Pleiades shared DB + auth helpers. Used by both gateway.py and app.py.""" +import base64 +import hashlib +import json +import os +import sqlite3 +import time + +CFG_PATH = os.environ.get("PLEIADES_CONFIG", "/opt/pleiades/config.json") +CFG = json.load(open(CFG_PATH)) +DB_PATH = CFG["db_path"] + +SCHEMA = """ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, -- web login (scrypt) + proxy_password_hash TEXT NOT NULL, -- proxy auth (scrypt) + proxy_password_plain TEXT, -- shown to customer (once / dashboard) + active INTEGER DEFAULT 0, -- 0 until first payment + region TEXT DEFAULT '', + country TEXT DEFAULT '', + city TEXT DEFAULT '', + sticky INTEGER DEFAULT 1, + balance_gb REAL DEFAULT 0, + created_at INTEGER +); +CREATE TABLE IF NOT EXISTS plans ( + id INTEGER PRIMARY KEY, + name TEXT, + gb REAL, + price_sats INTEGER, + active INTEGER DEFAULT 1 +); +CREATE TABLE IF NOT EXISTS orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + plan_id INTEGER, + invoice_id TEXT, + amount_sats INTEGER, + status TEXT DEFAULT 'pending', + created_at INTEGER, + paid_at INTEGER +); +CREATE TABLE IF NOT EXISTS usage_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + bytes INTEGER, + ts INTEGER +); +""" + + +def get_db(): + db = sqlite3.connect(DB_PATH, check_same_thread=False) + db.row_factory = sqlite3.Row + return db + + +def init_db(): + db = get_db() + db.executescript(SCHEMA) + # seed default plans if empty + cur = db.execute("SELECT COUNT(*) c FROM plans") + if cur.fetchone()["c"] == 0: + for p in [ + ("Starter", 5, 25000), # 5 GB ~ 25k sats + ("Standard", 25, 90000), # 25 GB ~ 90k sats + ("Pro", 100, 250000), # 100 GB ~ 250k sats + ]: + db.execute("INSERT INTO plans (name, gb, price_sats) VALUES (?,?,?)", p) + db.commit() + db.close() + + +def scrypt_hash(pw, salt=None): + salt = salt or os.urandom(16) + h = hashlib.scrypt(pw.encode(), salt=salt, n=16384, r=8, p=1) + return base64.b64encode(salt).decode() + ":" + base64.b64encode(h).decode() + + +def verify_hash(pw, stored): + try: + salt_b64, hash_b64 = stored.split(":") + salt = base64.b64decode(salt_b64) + h = base64.b64encode(hashlib.scrypt(pw.encode(), salt=salt, n=16384, r=8, p=1)).decode() + return h == hash_b64 + except Exception: + return False + + +def lookup_user_by_proxy(username, proxy_password): + """Validate proxy auth. Returns user dict or None.""" + db = get_db() + row = db.execute( + "SELECT * FROM users WHERE username=? AND active=1", (username,) + ).fetchone() + db.close() + if not row: + return None + if not verify_hash(proxy_password, row["proxy_password_hash"]): + return None + if row["balance_gb"] <= 0: + return None + return dict(row) + + +def add_usage(user_id, nbytes): + db = get_db() + db.execute("INSERT INTO usage_log (user_id, bytes, ts) VALUES (?,?,?)", + (user_id, nbytes, int(time.time()))) + gb = nbytes / (1024 * 1024 * 1024) + db.execute("UPDATE users SET balance_gb = MAX(0, balance_gb - ?) WHERE id=?", (gb, user_id)) + db.commit() + db.close() + + +def random_token(n=24): + return base64.urlsafe_b64encode(os.urandom(n)).decode().rstrip("=") diff --git a/gateway.py b/gateway.py new file mode 100644 index 0000000..bc5b3af --- /dev/null +++ b/gateway.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +"""Pleiades gateway — SOCKS5 + HTTP CONNECT proxy. + +Accepts customer user:pass, looks up their geo prefs, rewrites the upstream +IPRoyal password with geo/sticky suffixes, and relays. Meters bytes per user +for GB billing. Does NOT log traffic content — only connection metadata. + +Billing note: bytes are metered (required for GB plans). Target hostnames and +payload are never persisted. +""" +import asyncio +import base64 +import logging +import signal +import struct + +import db + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger("pleiades-gateway") + +UP = db.CFG["upstream"] +GW = db.CFG["gateway"] +UP_AUTH = base64.b64encode(f"{UP['username']}:{UP['password']}".encode()).decode() + + +def _sticky_sid(user): + """Stable 8-char alphanumeric session id per user (sticky IP for lifetime window).""" + import hashlib + return hashlib.md5(("u%d" % user["id"]).encode()).hexdigest()[:8] + + +def build_upstream_password(user): + """Append geo/sticky suffixes to the base upstream password.""" + p = UP["password"] + if user.get("region"): + p += f"_region-{user['region']}" + if user.get("country"): + p += f"_country-{user['country']}" + if user.get("city"): + p += f"_city-{user['city']}" + if user.get("sticky"): + p += f"_session-{_sticky_sid(user)}_lifetime-30m" + return p + + +async def connect_upstream(host, port, user): + """Open TCP to IPRoyal, issue HTTP CONNECT, return (reader, writer, leftover).""" + up_pass = build_upstream_password(user) + auth = base64.b64encode(f"{UP['username']}:{up_pass}".encode()).decode() + reader, writer = await asyncio.open_connection(UP["host"], UP["port"]) + req = (f"CONNECT {host}:{port} HTTP/1.1\r\n" + f"Host: {host}:{port}\r\n" + f"Proxy-Authorization: Basic {auth}\r\n\r\n") + writer.write(req.encode()) + await writer.drain() + data = b"" + while b"\r\n\r\n" not in data: + chunk = await reader.read(4096) + if not chunk: + break + data += chunk + if len(data) > 65536: + break + head, _, leftover = data.partition(b"\r\n\r\n") + status = head.decode(errors="replace").split("\r\n")[0] + if "200" not in status: + raise ConnectionError(f"upstream refused: {status}") + return reader, writer, leftover + + +async def pipe(reader, writer, counter): + try: + while True: + data = await reader.read(65536) + if not data: + break + counter[0] += len(data) + writer.write(data) + await writer.drain() + except Exception: + pass + finally: + try: + writer.close() + except Exception: + pass + + +def parse_target(reader, data): + """Parse SOCKS5 address from accumulated bytes. Returns (host, port, consumed).""" + atyp = data[3] + if atyp == 0x01: # IPv4 + host = ".".join(str(b) for b in data[4:8]) + port = struct.unpack(">H", data[8:10])[0] + return host, port, 10 + if atyp == 0x03: # domain + ln = data[4] + host = data[5:5 + ln].decode(errors="replace") + port = struct.unpack(">H", data[5 + ln:7 + ln])[0] + return host, port, 7 + ln + if atyp == 0x04: # IPv6 + import ipaddress + host = str(ipaddress.IPv6Address(data[4:20])) + port = struct.unpack(">H", data[20:22])[0] + return host, port, 22 + raise ValueError("bad atyp") + + +async def handle_socks(reader, writer): + peername = writer.get_extra_info("peername") + user = None + counter = [0] + try: + # greeting + head = await reader.readexactly(2) + ver, nmethods = head[0], head[1] + methods = await reader.readexactly(nmethods) + if ver != 5 or 0x02 not in methods: + writer.write(b"\x05\xff") + await writer.drain() + return + writer.write(b"\x05\x02") # choose user/pass auth + await writer.drain() + # auth (RFC 1929) + await reader.readexactly(1) # version 0x01 + ulen = (await reader.readexactly(1))[0] + username = (await reader.readexactly(ulen)).decode() + plen = (await reader.readexactly(1))[0] + password = (await reader.readexactly(plen)).decode() + user = db.lookup_user_by_proxy(username, password) + if user is None: + writer.write(b"\x01\x01") + await writer.drain() + return + writer.write(b"\x01\x00") + await writer.drain() + # request header: ver, cmd, rsv, atyp + hdr = await reader.readexactly(4) + cmd = hdr[1] + atyp = hdr[3] + if cmd != 0x01: # only CONNECT supported + writer.write(b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00") + await writer.drain() + return + if atyp == 0x01: # IPv4 + host = ".".join(str(b) for b in (await reader.readexactly(4))) + elif atyp == 0x03: # domain + ln = (await reader.readexactly(1))[0] + host = (await reader.readexactly(ln)).decode(errors="replace") + elif atyp == 0x04: # IPv6 + import ipaddress + host = str(ipaddress.IPv6Address(await reader.readexactly(16))) + else: + raise ValueError("bad atyp") + port = struct.unpack(">H", await reader.readexactly(2))[0] + # connect upstream + up_reader, up_writer, leftover = await connect_upstream(host, port, user) + # success reply + writer.write(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00") + await writer.drain() + # forward any leftover bytes from upstream to client + if leftover: + counter[0] += len(leftover) + writer.write(leftover) + await writer.drain() + # relay both directions + await asyncio.gather( + pipe(reader, up_writer, counter), + pipe(up_reader, writer, counter), + ) + except (asyncio.IncompleteReadError, ConnectionError, ValueError, struct.error) as e: + log.debug("socks %s closed: %s", peername, e) + except Exception as e: + log.warning("socks %s error: %s", peername, e) + finally: + if user: + db.add_usage(user["id"], counter[0]) + + +async def handle_http(reader, writer): + peername = writer.get_extra_info("peername") + user = None + counter = [0] + try: + first = await reader.readuntil(b"\r\n") + parts = first.decode(errors="replace").split() + if len(parts) < 2 or parts[0].upper() != "CONNECT": + writer.write(b"HTTP/1.1 405 Method Not Allowed\r\n\r\n") + await writer.drain() + return + target = parts[1] + host, _, port = target.rpartition(":") + # read headers to find proxy auth + auth = None + while True: + line = await reader.readuntil(b"\r\n") + if line == b"\r\n": + break + if line.lower().startswith(b"proxy-authorization:"): + auth = line.decode().split(":", 1)[1].strip() + if auth and auth.lower().startswith("basic "): + raw = base64.b64decode(auth[6:]).decode() + username, _, password = raw.partition(":") + user = db.lookup_user_by_proxy(username, password) + if user is None: + writer.write(b"HTTP/1.1 407 Proxy Authentication Required\r\n\r\n") + await writer.drain() + return + up_reader, up_writer, leftover = await connect_upstream(host, int(port), user) + writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n") + await writer.drain() + if leftover: + counter[0] += len(leftover) + writer.write(leftover) + await writer.drain() + await asyncio.gather( + pipe(reader, up_writer, counter), + pipe(up_reader, writer, counter), + ) + except (asyncio.IncompleteReadError, ConnectionError, ValueError) as e: + log.debug("http %s closed: %s", peername, e) + except Exception as e: + log.warning("http %s error: %s", peername, e) + finally: + if user: + db.add_usage(user["id"], counter[0]) + + +async def main(): + db.init_db() + socks = await asyncio.start_server(handle_socks, GW["listen"], GW["socks_port"]) + http = await asyncio.start_server(handle_http, GW["listen"], GW["http_port"]) + log.info("SOCKS5 on :%d HTTP on :%d", GW["socks_port"], GW["http_port"]) + async with socks, http: + await asyncio.Future() + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + pass diff --git a/gateway.service b/gateway.service new file mode 100644 index 0000000..010c07e --- /dev/null +++ b/gateway.service @@ -0,0 +1,13 @@ +[Unit] +Description=Pleiades proxy gateway (SOCKS5 + HTTP CONNECT) +After=network.target + +[Service] +Type=simple +WorkingDirectory=/opt/pleiades +ExecStart=/opt/pleiades/venv/bin/python /opt/pleiades/gateway.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..a4a6ccc --- /dev/null +++ b/nginx.conf @@ -0,0 +1,19 @@ +server { + listen 80 default_server; + server_name clean-proxys.thetempleofdoom.com _; + + client_max_body_size 5m; + + location /static/ { + alias /opt/pleiades/static/; + expires 7d; + } + + location / { + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..a84db52 --- /dev/null +++ b/static/style.css @@ -0,0 +1,145 @@ +:root { + --bg: #05070d; + --bg2: #080c14; + --surface: #0c1119; + --surface2: #111a27; + --border: #1c2837; + --border-glow: #2a3d52; + --text: #e6edf3; + --muted: #8296ab; + --neon: #00ff9c; + --neon2: #00e5ff; + --neon3: #b26bff; + --danger: #ff4d6d; + --ok: #00ff9c; + --warn: #ffb74d; + --mono: "JetBrains Mono", "SF Mono", "Fira Code", ui-monospace, Menlo, monospace; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + background: + radial-gradient(1200px 600px at 80% -10%, rgba(0,229,255,.08), transparent 60%), + radial-gradient(900px 500px at 10% 110%, rgba(0,255,156,.06), transparent 60%), + var(--bg); + color: var(--text); + font-family: var(--mono); + line-height: 1.6; + min-height: 100vh; +} + +a { color: var(--neon2); text-decoration: none; } +a:hover { text-shadow: 0 0 12px var(--neon2); } + +.wrap { max-width: 1080px; margin: 0 auto; padding: 0 22px; } + +/* nav */ +nav { + border-bottom: 1px solid var(--border); + background: rgba(5,7,13,.85); + backdrop-filter: blur(8px); + position: sticky; top: 0; z-index: 50; +} +nav .inner { display: flex; align-items: center; justify-content: space-between; height: 62px; } +.logo { font-weight: 700; font-size: 19px; letter-spacing: .5px; color: var(--text); } +.logo .dot { color: var(--neon); text-shadow: 0 0 10px var(--neon); } +.nav-links a { margin-left: 22px; color: var(--muted); font-size: 14px; } +.nav-links a:hover { color: var(--neon); text-shadow: 0 0 10px var(--neon); } +.nav-links a.cta { color: var(--bg); background: var(--neon); padding: 7px 14px; border-radius: 6px; font-weight: 700; } +.nav-links a.cta:hover { text-shadow: none; box-shadow: 0 0 18px var(--neon); } + +/* hero */ +.hero { padding: 72px 0 52px; text-align: center; } +.hero h1 { font-size: 46px; margin: 0 0 14px; letter-spacing: -.5px; line-height: 1.1; } +.hero h1 .glow { color: var(--neon); text-shadow: 0 0 24px var(--neon); } +.hero p.lede { font-size: 18px; color: var(--muted); max-width: 640px; margin: 0 auto 30px; } +.hero .cta-row { display: flex; gap: 14px; justify-content: center; } + +.btn { + display: inline-block; padding: 12px 26px; border-radius: 8px; font-weight: 700; + font-size: 15px; cursor: pointer; border: 1px solid transparent; transition: .15s; +} +.btn-primary { background: var(--neon); color: #04140c; box-shadow: 0 0 22px rgba(0,255,156,.35); } +.btn-primary:hover { box-shadow: 0 0 34px var(--neon); } +.btn-ghost { background: transparent; color: var(--neon2); border-color: var(--border-glow); } +.btn-ghost:hover { border-color: var(--neon2); box-shadow: 0 0 16px rgba(0,229,255,.3); } + +/* sections */ +section { padding: 48px 0; } +h2 { font-size: 28px; margin: 0 0 10px; } +h2 .accent { color: var(--neon2); text-shadow: 0 0 16px var(--neon2); } +.sub { color: var(--muted); margin-bottom: 28px; } + +.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 18px; } +.card { + background: var(--surface); border: 1px solid var(--border); border-radius: 12px; + padding: 22px; transition: .15s; +} +.card:hover { border-color: var(--neon); box-shadow: 0 0 22px rgba(0,255,156,.12); transform: translateY(-2px); } +.card h3 { margin: 0 0 8px; color: var(--neon); font-size: 16px; } +.card p { margin: 0; color: var(--muted); font-size: 13.5px; } +.card .ic { font-size: 26px; display: block; margin-bottom: 10px; } + +/* code block */ +pre.code { + background: #03060b; border: 1px solid var(--border); border-radius: 10px; + padding: 18px; overflow-x: auto; font-size: 13px; color: #9fe8c8; +} +pre.code .k { color: var(--neon2); } + +/* forms */ +.form-card { max-width: 420px; margin: 40px auto; } +label { display: block; font-size: 13px; color: var(--muted); margin: 14px 0 6px; } +input, select { + width: 100%; background: var(--bg2); border: 1px solid var(--border); color: var(--text); + border-radius: 8px; padding: 11px 13px; font-family: var(--mono); font-size: 14px; +} +input:focus, select:focus { outline: none; border-color: var(--neon); box-shadow: 0 0 0 3px rgba(0,255,156,.15); } +input[type=submit] { margin-top: 20px; background: var(--neon); color: #04140c; font-weight: 700; border: none; cursor: pointer; } +input[type=submit]:hover { box-shadow: 0 0 24px var(--neon); } + +/* flash */ +.flash { padding: 11px 16px; border-radius: 8px; margin: 16px 0; font-size: 14px; border: 1px solid; } +.flash.ok { color: var(--ok); border-color: rgba(0,255,156,.4); background: rgba(0,255,156,.06); } +.flash.warn { color: var(--warn); border-color: rgba(255,183,77,.4); background: rgba(255,183,77,.06); } + +/* plans */ +.plan { position: relative; } +.plan .price { font-size: 34px; font-weight: 700; color: var(--neon); text-shadow: 0 0 18px var(--neon); } +.plan .per { color: var(--muted); font-size: 13px; } +.plan .tag { position: absolute; top: 18px; right: 18px; font-size: 11px; color: var(--neon3); border: 1px solid var(--neon3); padding: 2px 8px; border-radius: 20px; } +.plan ul { list-style: none; padding: 0; margin: 16px 0; color: var(--muted); font-size: 13.5px; } +.plan ul li { padding: 4px 0; } +.plan ul li::before { content: "▸ "; color: var(--neon); } +.plan form { margin-top: 6px; } + +/* dashboard */ +.dash-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 18px; } +.stat { display: flex; flex-direction: column; } +.stat .val { font-size: 30px; font-weight: 700; color: var(--neon2); text-shadow: 0 0 16px var(--neon2); } +.stat .lbl { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 1px; } +.stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 18px; } + +.copy { display: flex; gap: 8px; align-items: center; } +.copy input { flex: 1; } +.copy button { background: var(--surface2); color: var(--neon2); border: 1px solid var(--border); padding: 10px 16px; border-radius: 8px; cursor: pointer; font-family: var(--mono); } + +.switch { display: flex; align-items: center; gap: 10px; font-size: 14px; color: var(--muted); } + +footer { + border-top: 1px solid var(--border); padding: 26px 0; margin-top: 60px; text-align: center; + color: var(--muted); font-size: 13px; +} +footer a { color: var(--neon3); } + +.muted { color: var(--muted); } +.center { text-align: center; } +.mono { font-family: var(--mono); } + +@media (max-width: 720px) { + .hero h1 { font-size: 32px; } + .dash-grid { grid-template-columns: 1fr; } + .stats-row { grid-template-columns: 1fr; } +} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..b5eb430 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,42 @@ + + + + + + {% block title %}Clean Proxys{% endblock %} + + + + + + + + + + +
+ {% with msgs = get_flashed_messages(with_categories=true) %} + {% for cat, m in msgs %}
{{ m }}
{% endfor %} + {% endwith %} + {% block body %}{% endblock %} +
+ + + + diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..dad32fb --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% block body %} +

Dashboard

+
+
Balance{{ '%.2f'|format(user.balance_gb) }} GB
+
Used{{ '%.3f'|format(used_gb) }} GB
+
Status + {{ 'ACTIVE' if user.active else 'INACTIVE' }} +
+
+ +{% if not user.active %} +
Account inactive — top up to activate your proxies.
+{% endif %} + +
+
+
+

Connection strings

+ +
+ + +
+ +
+ + +
+

Proxy password is unique to you. Use it for proxy auth, not your web password.

+
+ +
+
+
+ +
+

Location & session

+
+ + + + + + +
+ + +
+ +
+
+
+ + +{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..9570824 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block body %} +
+

The cleanest residential proxies on the web.

+

Global geo-targeted SOCKS5 & HTTP proxies with an encrypted backend. No KYC. No email required. Pay with Bitcoin and connect in seconds.

+ +
$ curl -x socks5h://user:pass@host:1080 https://ipinfo.io/ip
+# 81.5.104.39  ← your clean, geo-targeted egress
+
+ +
+

Built different.

+

Everything a serious operator needs, nothing that gets in the way.

+
+
🌍

Global geo-targeting

Country, city and region targeting across a real residential pool — not a VPN masquerade.

+
🔌

SOCKS5 & HTTP

Native SOCKS5 and HTTP CONNECT. Drop-in for any tool, bot, or agent.

+
🔐

Encrypted backend

Your traffic rides an encrypted transport from end to end. Per-user credentials, always.

+
⚡

Instant activation

Pay with Bitcoin, top up in seconds. No verification queue, no paperwork.

+
🕶️

No KYC

Just a username and password. That's it.

+
🤖

Agent-ready

REST API, llms.txt, and a schema agents can parse. Programmatic access is a first-class feature.

+
+
+ +
+

Sticky or rotating.

+

Choose a stable session IP or a fresh egress on every request — switch anytime from your dashboard.

+
+

Sticky sessions

Hold the same IP for 30 minutes. Ideal for account sessions and stateful tasks.

+

Rotating

A new IP on every connection. Ideal for scraping, crawling, and high-volume jobs.

+
+
+ +
+

Ready when you are.

+

Sign up, top up, connect. The whole thing takes under a minute.

+ Create an account +
+{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..c8dfd9f --- /dev/null +++ b/templates/login.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block body %} +
+

Log in

+
+ + + + + +
+

No account? Sign up

+
+{% endblock %} diff --git a/templates/order.html b/templates/order.html new file mode 100644 index 0000000..0bab31e --- /dev/null +++ b/templates/order.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block body %} +
+

Order placed

+

Invoice {{ order.invoice_id }}

+

Status: {{ order.status }}

+ {% if order.status != 'paid' %} +

Complete payment in the BTCPay checkout, then return here. Your balance credits automatically.

+ Check dashboard + {% else %} +

Paid. Balance credited.

+ Go to dashboard + {% endif %} +
+{% endblock %} diff --git a/templates/plans.html b/templates/plans.html new file mode 100644 index 0000000..36d3a16 --- /dev/null +++ b/templates/plans.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block body %} +
+

Simple pricing.

+

Pay-per-GB residential proxies. Bitcoin only. No subscriptions, no expiry traps.

+
+
+ {% for p in plans %} +
+ {% if p.gb >= 100 %}BEST VALUE{% endif %} +

{{ p.name }}

+
{{ p.price_sats }} sats
+
{{ p.gb }} GB residential traffic
+
    +
  • {{ p.gb }} GB residential traffic
  • +
  • All countries & cities
  • +
  • SOCKS5 + HTTP
  • +
  • Sticky or rotating
  • +
  • No KYC
  • +
+ {% if user %} +
+ {% else %} + Sign up to buy + {% endif %} +
+ {% endfor %} +
+{% endblock %} diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..e856f9a --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block body %} +
+

Create account

+

No email. No KYC. Just a username and password.

+
+ + + + + +
+

Already have one? Log in

+
+{% endblock %}