Pleiades proxy store: SOCKS5 gateway + Flask storefront + BTCPay checkout
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
config.json
|
||||
*.db
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
82
admin.py
Normal file
82
admin.py
Normal 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
312
app.py
Normal 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
13
app.service
Normal 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
27
config.example.json
Normal 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
120
db.py
Normal 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
243
gateway.py
Normal 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
13
gateway.service
Normal 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
19
nginx.conf
Normal 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
145
static/style.css
Normal 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
42
templates/base.html
Normal 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 & HTTP proxies. ·
|
||||
<a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener">Support the project ☕</a>
|
||||
· <a href="/llms.txt">llms.txt</a>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
58
templates/dashboard.html
Normal file
58
templates/dashboard.html
Normal 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 & 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
41
templates/index.html
Normal 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 & 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 & 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
14
templates/login.html
Normal 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
15
templates/order.html
Normal 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
29
templates/plans.html
Normal 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 & 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
15
templates/signup.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user