diff --git a/app.py b/app.py index 89230ec..c2bb30f 100644 --- a/app.py +++ b/app.py @@ -14,12 +14,14 @@ from flask import (Flask, abort, flash, jsonify, redirect, render_template, import db import iproyal import hydraproxy +import quality CFG = db.CFG app = Flask(__name__) app.secret_key = CFG["secret_key"] app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # mitigate CSRF on modern browsers app.config["SESSION_COOKIE_HTTPONLY"] = True # block JS session theft (XSS) +db.init_db() # ensure schema + migrations on gunicorn start # simple SQLite-backed rate limiter (shared across gunicorn workers) def _client_ip(): @@ -196,20 +198,24 @@ def plans(): @app.route("/buy/", methods=["POST"]) @login_required def buy(plan_id): + u = current_user() 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) + price = plan["price_sats"] + if db.get_balance_sats(u["id"]) < price: + flash("Insufficient wallet balance. Deposit first.", "warn") + return redirect(url_for("wallet")) + db.add_transaction(u["id"], -price, "gb", f"plan_{plan_id}") 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.execute("UPDATE users SET balance_gb = balance_gb + ?, active=1 WHERE id=?", + (plan["gb"], u["id"])) d.commit() d.close() - inv = create_invoice(plan["price_sats"], order_id) - return redirect(inv["checkoutLink"]) + flash(f"Added {plan['gb']} GB. Happy scraping.", "ok") + return redirect(url_for("dashboard")) @app.route("/order/") @@ -235,6 +241,20 @@ def webhook_btcpay(): kind = (payload.get("metadata") or {}).get("kind") if not order_id: return "ok" + if kind == "deposit": + uid = (payload.get("metadata") or {}).get("user_id") + amt = (payload.get("metadata") or {}).get("amount_sats") + d = db.get_db() + already = d.execute("SELECT 1 FROM transactions WHERE ref=? AND type='deposit'", + (order_id,)).fetchone() + if not already and uid and amt: + d.execute("UPDATE users SET balance_sats = balance_sats + ? WHERE id=?", + (int(amt), uid)) + d.execute("INSERT INTO transactions (user_id, amount_sats, type, ref, ts) " + "VALUES (?,?,?,?,?)", (uid, int(amt), "deposit", order_id, int(time.time()))) + d.commit() + d.close() + return "ok" d = db.get_db() if kind == "dedicated": row = d.execute("SELECT * FROM dedicated_ips WHERE invoice_id=? AND status='pending_payment'", @@ -265,6 +285,31 @@ def webhook_btcpay(): return "ok" +@app.route("/wallet") +@login_required +def wallet(): + u = current_user() + bal = db.get_balance_sats(u["id"]) + txs = db.get_transactions(u["id"]) + return render_template("wallet.html", user=u, balance=bal, txs=txs) + + +@app.route("/wallet/deposit", methods=["POST"]) +@login_required +def wallet_deposit(): + u = current_user() + try: + sats = int(request.form.get("sats", 0)) + except ValueError: + sats = 0 + if sats < 500: + flash("Minimum deposit is 500 sats.", "warn") + return redirect(url_for("wallet")) + ref = db.random_token(12) + inv = create_invoice(sats, ref, meta={"kind": "deposit", "user_id": u["id"], "amount_sats": sats}) + return redirect(inv["checkoutLink"]) + + @app.route("/dashboard") @login_required def dashboard(): @@ -277,11 +322,15 @@ def dashboard(): "WHERE o.user_id=? ORDER BY o.id DESC LIMIT 20", (u["id"],)).fetchall()] sessions = [dict(r) for r in d.execute( "SELECT * FROM usage_log WHERE user_id=? ORDER BY id DESC LIMIT 10", (u["id"],)).fetchall()] + hydra = [dict(r) for r in d.execute( + "SELECT * FROM hydraproxy_orders WHERE user_id=? ORDER BY id DESC", (u["id"],)).fetchall()] d.close() + bal = db.get_balance_sats(u["id"]) 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, dedicated=deds, orders=orders, sessions=sessions) + socks=socks, http=http, dedicated=deds, orders=orders, sessions=sessions, + hydra=hydra, balance=bal) @app.route("/dashboard/test") @@ -410,16 +459,39 @@ def dedicated_buy(): flash("Pick a location.", "warn") return redirect(url_for("dedicated")) _, price = DEDICATED_PRODUCTS[pid]["plans"][days] + u = current_user() + if db.get_balance_sats(u["id"]) < price: + flash("Insufficient wallet balance. Deposit first.", "warn") + return redirect(url_for("wallet")) oid = db.random_token(12) d = db.get_db() d.execute("INSERT INTO dedicated_ips (user_id,product_id,product_name,plan_days,location_id," "location_name,invoice_id,price_sats,status,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)", - (current_user()["id"], pid, DEDICATED_PRODUCTS[pid]["name"], days, loc_id, loc_name, + (u["id"], pid, DEDICATED_PRODUCTS[pid]["name"], days, loc_id, loc_name, oid, price, "pending_payment", int(time.time()))) d.commit() d.close() - inv = create_invoice(price, oid, meta={"kind": "dedicated"}) - return redirect(inv["checkoutLink"]) + db.add_transaction(u["id"], -price, "dedicated", oid) + # place the IPRoyal order directly (no webhook needed in wallet model) + try: + plan_id = DEDICATED_PRODUCTS[pid]["plans"][days][0] + o = iproyal.create_order(pid, plan_id, loc_id, 1) + oid2 = o.get("id") or o.get("order_id") or (o.get("data") or {}).get("id") + d = db.get_db() + d.execute("UPDATE dedicated_ips SET status='provisioning', iproyal_order_id=? WHERE invoice_id=?", + (oid2, oid)) + d.commit() + d.close() + flash("Dedicated IP ordered — provisioning now.", "ok") + except Exception as e: + app.logger.error("dedicated order failed: %s", e) + d = db.get_db() + d.execute("UPDATE dedicated_ips SET status='failed' WHERE invoice_id=?", (oid,)) + d.commit() + d.close() + db.add_transaction(u["id"], price, "dedicated_refund", oid) + flash("Dedicated order failed — wallet refunded.", "warn") + return redirect(url_for("dashboard")) @app.route("/dedicated/poll", methods=["POST"]) @@ -504,6 +576,53 @@ def api_hydraproxy_info(): return jsonify({"error": str(e)}), 502 +@app.route("/hydraproxy/order", methods=["POST"]) +@login_required +def hydraproxy_order(): + u = current_user() + product = request.form.get("product", "").strip() + location = request.form.get("location", "").strip() + if product not in HYDRA_PRODUCTS: + flash("Bad product.", "warn") + return redirect(url_for("hydraproxy_page")) + price = HYDRA_PRODUCTS[product]["price_sats"] + if db.get_balance_sats(u["id"]) < price: + flash("Insufficient wallet balance. Deposit first.", "warn") + return redirect(url_for("wallet")) + d = db.get_db() + d.execute("INSERT INTO hydraproxy_orders (user_id, product, location, price_sats, status, created_at) " + "VALUES (?,?,?,?,'pending_fulfillment',?)", + (u["id"], product, location, price, int(time.time()))) + d.commit() + d.close() + db.add_transaction(u["id"], -price, "hydraproxy", product) + flash("Order placed — our broker fulfills it shortly and delivers your proxy to the dashboard.", "ok") + return redirect(url_for("dashboard")) + + +@app.route("/quality") +@login_required +def quality_page(): + return render_template("quality.html", user=current_user(), + price=CFG.get("quality_price_sats", 220)) + + +@app.route("/quality/run", methods=["POST"]) +@login_required +def quality_run(): + u = current_user() + price = CFG.get("quality_price_sats", 220) + if db.get_balance_sats(u["id"]) < price: + return jsonify({"error": "Insufficient balance. Deposit first."}), 402 + proxy = request.form.get("proxy", "").strip() + if not proxy: + proxy = f"socks5h://{u['username']}:{u['proxy_password_plain']}@127.0.0.1:{PROXY['socks_port']}" + db.add_transaction(u["id"], -price, "quality", "ip_check") + result = quality.score_proxy(proxy) + result["charged_sats"] = price + return jsonify(result) + + # ---------- admin ---------- ADMIN_PASSWORD = CFG.get("admin_password", "") @@ -530,6 +649,7 @@ def admin(): "FROM users ORDER BY id DESC LIMIT 50")] orders = [dict(r) for r in d.execute("SELECT * FROM orders ORDER BY id DESC LIMIT 20")] deds = [dict(r) for r in d.execute("SELECT * FROM dedicated_ips ORDER BY id DESC LIMIT 30")] + hydra_orders = [dict(r) for r in d.execute("SELECT * FROM hydraproxy_orders ORDER BY id DESC LIMIT 30")] d.close() try: ipr_balance = iproyal.get_balance() @@ -544,7 +664,7 @@ def admin(): ipr_balance < CFG.get("alerts", {}).get("low_balance_threshold", 10.0) return render_template("admin.html", stats=stats, users=users, orders=orders, dedicated=deds, ipr_balance=ipr_balance, low_balance=low_balance, - hydra_balance=hydra_balance) + hydra_balance=hydra_balance, hydra_orders=hydra_orders) @app.route("/admin/credit", methods=["POST"]) @@ -564,6 +684,37 @@ def admin_credit(): return redirect(url_for("admin")) +@app.route("/admin/hydraproxy/fulfill", methods=["POST"]) +def admin_hydraproxy_fulfill(): + if not session.get("admin"): + abort(403) + oid = request.form.get("order_id", "").strip() + hydra_order_id = request.form.get("hydra_order_id", "").strip() + if not oid or not hydra_order_id: + flash("Need our order id and the HydraProxy order id.", "warn") + return redirect(url_for("admin")) + try: + details = hydraproxy.proxy_details(hydra_order_id) + pinfo = details.get("proxy_info", {}) + proxy = details.get("proxy", {}) + host = proxy.get("hostname") or proxy.get("server_ip") + port = proxy.get("port") + if isinstance(port, list): + port = port[0] + user = pinfo.get("username", "") + pw = pinfo.get("password", "") + d = db.get_db() + d.execute("UPDATE hydraproxy_orders SET status='active', hydra_order_id=?, proxy_host=?, " + "proxy_port=?, proxy_user=?, proxy_pass=?, fulfilled_at=? WHERE id=?", + (hydra_order_id, host, str(port), user, pw, int(time.time()), oid)) + d.commit() + d.close() + flash(f"Fulfilled order {oid} with HydraProxy order {hydra_order_id}.", "ok") + except Exception as e: + flash(f"Fulfill failed: {e}", "warn") + return redirect(url_for("admin")) + + # ---------- agent discovery ---------- @app.route("/llms.txt") def llms_txt(): diff --git a/db.py b/db.py index 6b450ec..4e41cba 100644 --- a/db.py +++ b/db.py @@ -73,6 +73,29 @@ CREATE TABLE IF NOT EXISTS rate_limits ( ip TEXT, ts REAL ); +CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + amount_sats INTEGER, -- positive = deposit/credit, negative = spend + type TEXT, -- deposit / gb / dedicated / hydraproxy / quality / admin_credit + ref TEXT, + ts INTEGER +); +CREATE TABLE IF NOT EXISTS hydraproxy_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + product TEXT, -- residential / mobile / static + location TEXT, + price_sats INTEGER, + status TEXT DEFAULT 'pending_fulfillment', -- pending_fulfillment/active/failed/refunded + hydra_order_id TEXT, + proxy_host TEXT, + proxy_port TEXT, + proxy_user TEXT, + proxy_pass TEXT, + created_at INTEGER, + fulfilled_at INTEGER +); """ @@ -85,6 +108,10 @@ def get_db(): def init_db(): db = get_db() db.executescript(SCHEMA) + # migrate: add wallet column if missing (older DBs) + cols = [r[1] for r in db.execute("PRAGMA table_info(users)")] + if "balance_sats" not in cols: + db.execute("ALTER TABLE users ADD COLUMN balance_sats INTEGER DEFAULT 0") # seed default plans if empty cur = db.execute("SELECT COUNT(*) c FROM plans") if cur.fetchone()["c"] == 0: @@ -142,3 +169,31 @@ def add_usage(user_id, nbytes): def random_token(n=24): return base64.urlsafe_b64encode(os.urandom(n)).decode().rstrip("=") + + +# ---------- wallet ---------- +def add_transaction(user_id, amount_sats, type_, ref=""): + """Record a ledger entry AND atomically adjust the user's sats balance.""" + db = get_db() + db.execute("INSERT INTO transactions (user_id, amount_sats, type, ref, ts) " + "VALUES (?,?,?,?,?)", (user_id, amount_sats, type_, ref, int(time.time()))) + db.execute("UPDATE users SET balance_sats = balance_sats + ? WHERE id=?", + (amount_sats, user_id)) + db.commit() + db.close() + + +def get_balance_sats(user_id): + db = get_db() + r = db.execute("SELECT balance_sats FROM users WHERE id=?", (user_id,)).fetchone() + db.close() + return r["balance_sats"] if r else 0 + + +def get_transactions(user_id, limit=50): + db = get_db() + rows = [dict(r) for r in db.execute( + "SELECT * FROM transactions WHERE user_id=? ORDER BY id DESC LIMIT ?", + (user_id, limit))] + db.close() + return rows diff --git a/quality.py b/quality.py new file mode 100644 index 0000000..54956be --- /dev/null +++ b/quality.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Proxy/IP quality scorer — battery of checks via free APIs + DNSBL. + +Scores 0-100: connectivity, residential/ISP type, proxy-flag, blacklists, +latency, and header-leak anonymity. No API keys required. +""" +import json +import socket +import subprocess +import time + + +def _curl(proxy, url, timeout=20): + cmd = ["curl", "-s", "--max-time", str(timeout)] + if proxy: + cmd += ["-x", proxy] + cmd.append(url) + t0 = time.time() + try: + r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 5) + out = (r.stdout or "").strip() + except subprocess.TimeoutExpired: + out = "" + return out, (time.time() - t0) * 1000 + + +def _dnsbl(ip): + rev = ".".join(reversed(ip.split("."))) + hits = [] + for bl in ("zen.spamhaus.org", "bl.spamcop.net", "b.barracudacentral.org"): + try: + socket.gethostbyname(f"{rev}.{bl}") + hits.append(bl) + except socket.gaierror: + pass + return hits + + +def score_proxy(proxy=None): + checks = [] + score = 0 + + out, latency = _curl(proxy, "http://ip-api.com/json" + "?fields=status,query,country,city,regionName,isp,org,as,hosting,proxy,mobile") + try: + d = json.loads(out) + except Exception: + d = {} + + if d.get("status") != "success": + checks.append(("Connectivity", False, "proxy failed to connect")) + return {"score": 0, "checks": checks, "breakdown": {}, "charged": True} + + ip = d.get("query") + score += 30 + checks.append(("Connectivity", True, f"egress {ip}")) + + hosting = bool(d.get("hosting")) + if not hosting: + score += 25 + checks.append(("Residential / ISP", True, f"{d.get('isp')} ({d.get('org')})")) + else: + checks.append(("Residential / ISP", False, "datacenter / hosting IP")) + + flagged = bool(d.get("proxy")) + if not flagged: + score += 20 + checks.append(("Not proxy-flagged", True, "clean")) + else: + checks.append(("Not proxy-flagged", False, "IP flagged as proxy/VPN")) + + bl = _dnsbl(ip) + if not bl: + score += 15 + checks.append(("Blacklists", True, "no hits")) + else: + checks.append(("Blacklists", False, ", ".join(bl))) + + if latency < 2000: + score += 10 + checks.append(("Latency", True, f"{latency:.0f}ms")) + elif latency < 5000: + score += 5 + checks.append(("Latency", True, f"{latency:.0f}ms (slow)")) + else: + checks.append(("Latency", False, f"{latency:.0f}ms")) + + leak = [] + try: + hout, _ = _curl(proxy, "https://httpbin.org/headers", timeout=15) + hd = json.loads(hout).get("headers", {}) + for k in ("X-Forwarded-For", "Via", "X-Real-Ip"): + if k in hd: + leak.append(k) + except Exception: + pass + + if leak: + checks.append(("Anonymity", False, f"leaks: {', '.join(leak)}")) + else: + checks.append(("Anonymity", True, "no header leaks")) + + breakdown = { + "egress_ip": ip, + "geo": f"{d.get('city')}, {d.get('regionName')}, {d.get('country')}", + "isp": d.get("isp"), + "asn": d.get("as"), + "hosting": hosting, + "proxy_flagged": flagged, + "mobile": bool(d.get("mobile")), + "blacklists": bl, + "latency_ms": round(latency), + "leak_headers": leak, + } + + return {"score": score, "checks": checks, "breakdown": breakdown, "charged": True} diff --git a/templates/admin.html b/templates/admin.html index 8971a24..5f646ba 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -27,6 +27,31 @@ +
+

HydraProxy fulfillment queue

+ {% if hydra_orders %} + + + {% for h in hydra_orders %} + + + + + + {% endfor %} +
IDUserProductLocationStatus
{{ h.id }}{{ h.user_id }}{{ h.product }}{{ h.location or '—' }}{{ h.status }}
+ {% else %} +

No HydraProxy orders yet.

+ {% endif %} +

Fulfill an order

+
+
+
+ +
+

Buy the proxy at HydraProxy's dashboard, paste its order ID here — creds auto-pull via proxy-details and land on the customer's dashboard.

+
+

Recent orders

diff --git a/templates/base.html b/templates/base.html index 3dedd4a..ca8758b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -40,7 +40,7 @@ Residential Dedicated IPs HydraProxy - {% if user %}DashboardLogout + {% if user %}DashboardWalletQualityLogout {% else %}LoginGet proxies{% endif %}
diff --git a/templates/dashboard.html b/templates/dashboard.html index 951f294..2710848 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -3,6 +3,7 @@

Dashboard

Balance{{ '%.2f'|format(user.balance_gb) }} GB
+
Wallet{{ balance }} sats
Used{{ '%.3f'|format(used_gb) }} GB
Status {{ 'ACTIVE' if user.active else 'INACTIVE' }} @@ -93,6 +94,29 @@

No dedicated IPs yet — order one.

{% endif %} +{% if hydra %} +
+

HydraProxy orders (broker-fulfilled)

+ {% for h in hydra %} +
+
+ {{ h.product }}{% if h.location %} · {{ h.location }}{% endif %} + {{ h.status }} +
+ {% if h.status == 'active' %} + +
+ + +
+ {% elif h.status == 'pending_fulfillment' %} +

Broker is fulfilling your order — your proxy appears here when ready.

+ {% endif %} +
+ {% endfor %} +
+{% endif %} + {% if orders %}

Order history

diff --git a/templates/quality.html b/templates/quality.html new file mode 100644 index 0000000..2a8354e --- /dev/null +++ b/templates/quality.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% block title %}IP Quality Tester — Clean Proxys{% endblock %} +{% block body %} +

IP Quality Tester

+

Check how clean a proxy is before you trust it. {{ price }} sats (~$0.15) per check, billed to your wallet.

+ +
+

Run a check

+

Leave blank to test your own Clean Proxys proxy, or paste any socks5:// or http:// proxy.

+
+ + + +
+
+ +
+ + +{% endblock %} diff --git a/templates/wallet.html b/templates/wallet.html new file mode 100644 index 0000000..0bfae36 --- /dev/null +++ b/templates/wallet.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% block title %}Wallet — Clean Proxys{% endblock %} +{% block body %} +

Wallet

+

Deposit Bitcoin, spend sats on proxies and tools. No KYC, no email.

+ +
+
{{ balance }}
Balance (sats)
+
{{ (balance / 1e8) | round(8, 'floor') }}
BTC
+
+ +
+

Deposit

+

Pick an amount — funds credit instantly on payment confirmation.

+
+
+ {% for amt in [10000, 50000, 100000, 250000] %} + + {% endfor %} +
+ + + +
+
+ +
+

Transactions

+ {% if txs %} + + + {% for t in txs %} + + + + + + + {% endfor %} +
WhenTypeAmountRef
{{ t.ts }}{{ t.type }}{{ '+' if t.amount_sats >= 0 else '' }}{{ t.amount_sats }} sats{{ t.ref }}
+ {% else %} +

No transactions yet — make your first deposit.

+ {% endif %} +
+{% endblock %}