commit 9d084373dca656bc9d7a84808380b1762d9525fd Author: drjones Date: Sat Sep 19 18:19:46 2026 -0700 Pleiades proxy store: SOCKS5 gateway + Flask storefront + BTCPay checkout 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 %}