Pleiades proxy store: SOCKS5 gateway + Flask storefront + BTCPay checkout

This commit is contained in:
drjones
2026-09-19 18:19:46 -07:00
commit 9d084373dc
17 changed files with 1194 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
config.json
*.db
venv/
__pycache__/
*.pyc
.env

82
admin.py Normal file
View File

@@ -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 <u> <webpass> <proxypass> [country city region sticky bal active] | "
"set-country <u> <CC> | set <u> <field> <val> | add-balance <u> <gb> | list")
if __name__ == "__main__":
main()

312
app.py Normal file
View File

@@ -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/<int:plan_id>", 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/<order_id>")
@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 = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
for u in urls:
body += f" <url><loc>{u}</loc></url>\n"
body += "</urlset>"
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)

13
app.service Normal file
View File

@@ -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

27
config.example.json Normal file
View File

@@ -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"
}
}

120
db.py Normal file
View File

@@ -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("=")

243
gateway.py Normal file
View File

@@ -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

13
gateway.service Normal file
View File

@@ -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

19
nginx.conf Normal file
View File

@@ -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;
}
}

145
static/style.css Normal file
View File

@@ -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; }
}

42
templates/base.html Normal file
View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Clean Proxys{% endblock %}</title>
<meta name="description" content="Clean residential SOCKS5 & HTTP proxies. Global geo-targeting. No KYC. Bitcoin checkout. Encrypted backend. Agent-ready.">
<meta name="robots" content="index,follow">
<meta property="og:title" content="Clean Proxys — residential SOCKS5 proxies">
<meta property="og:description" content="Clean, fast, geo-targeted residential proxies. No KYC. Pay with Bitcoin. Built for agents and humans alike.">
<meta property="og:type" content="website">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<nav>
<div class="wrap inner">
<a class="logo" href="/">CLEAN<span class="dot">.</span>PROXYS</a>
<div class="nav-links">
<a href="/">Home</a>
<a href="/plans">Plans</a>
{% if user %}<a href="/dashboard">Dashboard</a><a href="/logout">Logout</a>
{% else %}<a href="/login">Login</a><a class="cta" href="/signup">Sign up</a>{% endif %}
</div>
</div>
</nav>
<div class="wrap">
{% with msgs = get_flashed_messages(with_categories=true) %}
{% for cat, m in msgs %}<div class="flash {{ cat }}">{{ m }}</div>{% endfor %}
{% endwith %}
{% block body %}{% endblock %}
</div>
<footer>
<div class="wrap">
Clean Proxys — residential SOCKS5 &amp; HTTP proxies. &nbsp;·&nbsp;
<a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener">Support the project ☕</a>
&nbsp;·&nbsp; <a href="/llms.txt">llms.txt</a>
</div>
</footer>
</body>
</html>

58
templates/dashboard.html Normal file
View File

@@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block body %}
<h2>Dashboard</h2>
<div class="stats-row">
<div class="card stat"><span class="lbl">Balance</span><span class="val">{{ '%.2f'|format(user.balance_gb) }} GB</span></div>
<div class="card stat"><span class="lbl">Used</span><span class="val">{{ '%.3f'|format(used_gb) }} GB</span></div>
<div class="card stat"><span class="lbl">Status</span>
<span class="val" style="color:{{ 'var(--ok)' if user.active else 'var(--danger)' }}">{{ 'ACTIVE' if user.active else 'INACTIVE' }}</span>
</div>
</div>
{% if not user.active %}
<div class="flash warn">Account inactive — <a href="/plans">top up</a> to activate your proxies.</div>
{% endif %}
<div class="dash-grid">
<div>
<div class="card">
<h3>Connection strings</h3>
<label>SOCKS5</label>
<div class="copy">
<input type="text" readonly value="{{ socks }}" onclick="this.select()">
<button onclick="copy(this.previousElementSibling)">Copy</button>
</div>
<label>HTTP (HTTPS tunneling)</label>
<div class="copy">
<input type="text" readonly value="{{ http }}" onclick="this.select()">
<button onclick="copy(this.previousElementSibling)">Copy</button>
</div>
<p class="muted" style="font-size:12px;margin-bottom:0">Proxy password is unique to you. Use it for proxy auth, not your web password.</p>
<form method="post" action="/dashboard/rotate" style="margin-top:14px">
<input type="submit" value="Rotate proxy password">
</form>
</div>
</div>
<div class="card">
<h3>Location &amp; session</h3>
<form method="post" action="/dashboard/location">
<label>Country (ISO code, e.g. US, GB, JP, DE)</label>
<input type="text" name="country" value="{{ user.country }}" placeholder="US">
<label>City (optional)</label>
<input type="text" name="city" value="{{ user.city }}" placeholder="New York">
<label>Region (optional, e.g. europe)</label>
<input type="text" name="region" value="{{ user.region }}" placeholder="">
<div class="switch" style="margin-top:14px">
<input type="checkbox" name="sticky" id="sticky" {% if user.sticky %}checked{% endif %} style="width:auto">
<label for="sticky" style="margin:0">Sticky session (hold IP 30 min)</label>
</div>
<input type="submit" value="Save location">
</form>
</div>
</div>
<script>
function copy(el){ el.select(); document.execCommand('copy'); }
</script>
{% endblock %}

41
templates/index.html Normal file
View File

@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block body %}
<div class="hero">
<h1>The <span class="glow">cleanest</span> residential proxies on the web.</h1>
<p class="lede">Global geo-targeted SOCKS5 &amp; HTTP proxies with an encrypted backend. No KYC. No email required. Pay with Bitcoin and connect in seconds.</p>
<div class="cta-row">
<a class="btn btn-primary" href="/signup">Get proxies →</a>
<a class="btn btn-ghost" href="/plans">View plans</a>
</div>
<pre class="code" style="max-width:640px;margin:34px auto 0;text-align:left;">$ curl -x <span class="k">socks5h://user:pass@host:1080</span> https://ipinfo.io/ip
# 81.5.104.39 ← your clean, geo-targeted egress</pre>
</div>
<section>
<h2>Built <span class="accent">different</span>.</h2>
<p class="sub">Everything a serious operator needs, nothing that gets in the way.</p>
<div class="grid">
<div class="card"><span class="ic">🌍</span><h3>Global geo-targeting</h3><p>Country, city and region targeting across a real residential pool — not a VPN masquerade.</p></div>
<div class="card"><span class="ic">🔌</span><h3>SOCKS5 &amp; HTTP</h3><p>Native SOCKS5 and HTTP CONNECT. Drop-in for any tool, bot, or agent.</p></div>
<div class="card"><span class="ic">🔐</span><h3>Encrypted backend</h3><p>Your traffic rides an encrypted transport from end to end. Per-user credentials, always.</p></div>
<div class="card"><span class="ic">⚡</span><h3>Instant activation</h3><p>Pay with Bitcoin, top up in seconds. No verification queue, no paperwork.</p></div>
<div class="card"><span class="ic">🕶️</span><h3>No KYC</h3><p>Just a username and password. That's it.</p></div>
<div class="card"><span class="ic">🤖</span><h3>Agent-ready</h3><p>REST API, llms.txt, and a schema agents can parse. Programmatic access is a first-class feature.</p></div>
</div>
</section>
<section>
<h2>Sticky or <span class="accent">rotating</span>.</h2>
<p class="sub">Choose a stable session IP or a fresh egress on every request — switch anytime from your dashboard.</p>
<div class="grid">
<div class="card"><h3>Sticky sessions</h3><p>Hold the same IP for 30 minutes. Ideal for account sessions and stateful tasks.</p></div>
<div class="card"><h3>Rotating</h3><p>A new IP on every connection. Ideal for scraping, crawling, and high-volume jobs.</p></div>
</div>
</section>
<section class="center">
<h2>Ready when you are.</h2>
<p class="sub">Sign up, top up, connect. The whole thing takes under a minute.</p>
<a class="btn btn-primary" href="/signup">Create an account</a>
</section>
{% endblock %}

14
templates/login.html Normal file
View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block body %}
<div class="form-card card">
<h2>Log in</h2>
<form method="post">
<label>Username</label>
<input type="text" name="username" required autocomplete="username">
<label>Password</label>
<input type="password" name="password" required autocomplete="current-password">
<input type="submit" value="Log in">
</form>
<p class="muted center" style="margin-top:16px">No account? <a href="/signup">Sign up</a></p>
</div>
{% endblock %}

15
templates/order.html Normal file
View File

@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block body %}
<div class="form-card card center">
<h2>Order placed</h2>
<p class="muted">Invoice <span class="mono">{{ order.invoice_id }}</span></p>
<p class="muted">Status: <strong>{{ order.status }}</strong></p>
{% if order.status != 'paid' %}
<p class="muted">Complete payment in the BTCPay checkout, then return here. Your balance credits automatically.</p>
<a class="btn btn-primary" href="/dashboard">Check dashboard</a>
{% else %}
<p style="color:var(--ok)">Paid. Balance credited.</p>
<a class="btn btn-primary" href="/dashboard">Go to dashboard</a>
{% endif %}
</div>
{% endblock %}

29
templates/plans.html Normal file
View File

@@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block body %}
<div class="hero" style="padding-bottom:20px">
<h1>Simple <span class="glow">pricing</span>.</h1>
<p class="lede">Pay-per-GB residential proxies. Bitcoin only. No subscriptions, no expiry traps.</p>
</div>
<div class="grid">
{% for p in plans %}
<div class="card plan">
{% if p.gb >= 100 %}<span class="tag">BEST VALUE</span>{% endif %}
<h3>{{ p.name }}</h3>
<div class="price">{{ p.price_sats }} <span style="font-size:16px;color:var(--muted)">sats</span></div>
<div class="per">{{ p.gb }} GB residential traffic</div>
<ul>
<li>{{ p.gb }} GB residential traffic</li>
<li>All countries &amp; cities</li>
<li>SOCKS5 + HTTP</li>
<li>Sticky or rotating</li>
<li>No KYC</li>
</ul>
{% if user %}
<form method="post" action="/buy/{{ p.id }}"><input type="submit" value="Buy {{ p.gb }} GB"></form>
{% else %}
<a class="btn btn-ghost" href="/signup">Sign up to buy</a>
{% endif %}
</div>
{% endfor %}
</div>
{% endblock %}

15
templates/signup.html Normal file
View File

@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block body %}
<div class="form-card card">
<h2>Create account</h2>
<p class="muted" style="margin-top:-4px">No email. No KYC. Just a username and password.</p>
<form method="post">
<label>Username</label>
<input type="text" name="username" required minlength="3" autocomplete="username">
<label>Password</label>
<input type="password" name="password" required minlength="6" autocomplete="new-password">
<input type="submit" value="Create account">
</form>
<p class="muted center" style="margin-top:16px">Already have one? <a href="/login">Log in</a></p>
</div>
{% endblock %}