Add HydraProxy provider (aggregator): wrapper + /hydraproxy storefront + admin balance + API + MCP tool (management-only, broker-fulfilled)
This commit is contained in:
36
app.py
36
app.py
@@ -13,6 +13,7 @@ from flask import (Flask, abort, flash, jsonify, redirect, render_template,
|
||||
|
||||
import db
|
||||
import iproyal
|
||||
import hydraproxy
|
||||
|
||||
CFG = db.CFG
|
||||
app = Flask(__name__)
|
||||
@@ -476,6 +477,33 @@ def poll_provisioning():
|
||||
return ok, failed
|
||||
|
||||
|
||||
# ---------- HydraProxy provider (management-only, manual broker fulfillment) ----------
|
||||
HYDRA_PRODUCTS = CFG.get("hydraproxy", {}).get("products", {})
|
||||
|
||||
|
||||
@app.route("/hydraproxy")
|
||||
def hydraproxy_page():
|
||||
account = None
|
||||
err = None
|
||||
try:
|
||||
account = hydraproxy.get_account_info()
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
return render_template("hydraproxy.html", products=HYDRA_PRODUCTS,
|
||||
account=account, err=err, user=current_user())
|
||||
|
||||
|
||||
@app.route("/api/v1/hydraproxy/info")
|
||||
def api_hydraproxy_info():
|
||||
try:
|
||||
account = hydraproxy.get_account_info()
|
||||
locations = hydraproxy.get_locations()
|
||||
return jsonify({"account": account, "locations": locations,
|
||||
"products": HYDRA_PRODUCTS})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 502
|
||||
|
||||
|
||||
# ---------- admin ----------
|
||||
ADMIN_PASSWORD = CFG.get("admin_password", "")
|
||||
|
||||
@@ -507,10 +535,16 @@ def admin():
|
||||
ipr_balance = iproyal.get_balance()
|
||||
except Exception:
|
||||
ipr_balance = "unavailable"
|
||||
try:
|
||||
hydra_acct = hydraproxy.get_account_info()
|
||||
hydra_balance = hydra_acct.get("balance_usd", "unavailable")
|
||||
except Exception:
|
||||
hydra_balance = "unavailable"
|
||||
low_balance = isinstance(ipr_balance, (int, float)) and \
|
||||
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)
|
||||
dedicated=deds, ipr_balance=ipr_balance, low_balance=low_balance,
|
||||
hydra_balance=hydra_balance)
|
||||
|
||||
|
||||
@app.route("/admin/credit", methods=["POST"])
|
||||
|
||||
88
hydraproxy.py
Normal file
88
hydraproxy.py
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""HydraProxy management API wrapper.
|
||||
|
||||
Base: https://app.hydraproxy.com/api/ (auth: Authorization: Token <key>)
|
||||
NOTE: HydraProxy's API is MANAGEMENT-ONLY — it CANNOT place/buy proxy orders.
|
||||
Orders are purchased manually from app.hydraproxy.com; this wrapper lists and
|
||||
manages them (details, rotation, location, whitelist, usage history).
|
||||
|
||||
Gotchas (from their docs):
|
||||
- All digit POST params must be sent as STRINGS (not ints).
|
||||
- POST URLs must end with a trailing slash.
|
||||
- API only accepts calls from whitelisted IPs (up to 3, manual, 30-min lockdown).
|
||||
- First item of every response is `status` ("OK" or "ERROR").
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
CFG = json.load(open(os.environ.get("PLEIADES_CONFIG", "/opt/pleiades/config.json")))
|
||||
HP = CFG["hydraproxy"]
|
||||
TOKEN = HP["token"]
|
||||
API = HP["api"].rstrip("/")
|
||||
|
||||
|
||||
def _hdr():
|
||||
return {"Authorization": f"Token {TOKEN}", "Accept": "application/json"}
|
||||
|
||||
|
||||
def get_account_info():
|
||||
"""{status, username, email, balance_usd}"""
|
||||
r = requests.get(f"{API}/get-account-info/", headers=_hdr(), timeout=20)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def get_locations():
|
||||
"""List of available geo locations."""
|
||||
r = requests.get(f"{API}/get-locations-list/", headers=_hdr(), timeout=20)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def proxy_details(order_id):
|
||||
"""Details of one order (network, proxy_info, proxy host/port)."""
|
||||
r = requests.post(f"{API}/proxy-details/", headers=_hdr(),
|
||||
data={"order_id": str(order_id)}, timeout=20)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def force_rotation(order_id):
|
||||
r = requests.post(f"{API}/force-rotation/", headers=_hdr(),
|
||||
data={"order_id": str(order_id)}, timeout=20)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def update_location(order_id, location):
|
||||
r = requests.post(f"{API}/update-location/", headers=_hdr(),
|
||||
data={"order_id": str(order_id), "location": str(location)}, timeout=20)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def update_rotation(order_id, auto=True):
|
||||
r = requests.post(f"{API}/update-rotation/", headers=_hdr(),
|
||||
data={"order_id": str(order_id), "auto": "1" if auto else "0"}, timeout=20)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def update_whitelist_ip(order_id, ip):
|
||||
r = requests.post(f"{API}/update-whitelist-ip/", headers=_hdr(),
|
||||
data={"order_id": str(order_id), "whitelist_ip": str(ip)}, timeout=20)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def view_usage_history(order_id, date_from="", date_to=""):
|
||||
data = {"order_id": str(order_id)}
|
||||
if date_from:
|
||||
data["date_from"] = date_from
|
||||
if date_to:
|
||||
data["date_to"] = date_to
|
||||
r = requests.post(f"{API}/view-usage-history/", headers=_hdr(), data=data, timeout=20)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
@@ -91,6 +91,12 @@ def list_dedicated_locations(product: int = 9) -> str:
|
||||
return json.dumps(_get("/dedicated/locations", params={"product": product}), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def hydraproxy_account() -> str:
|
||||
"""HydraProxy account info, balance, locations, and broker catalog (broker-fulfilled provider)."""
|
||||
return json.dumps(_get("/api/v1/hydraproxy/info"), indent=2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.settings.host = "0.0.0.0"
|
||||
mcp.settings.port = int(os.environ.get("MCP_PORT", "8090"))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<div class="stats-row">
|
||||
<div class="card stat"><span class="lbl">Revenue (paid)</span><span class="val">{{ stats.revenue_sats }} sats</span></div>
|
||||
<div class="card stat"><span class="lbl">IPRoyal balance</span><span class="val">${{ ipr_balance }}</span></div>
|
||||
<div class="card stat"><span class="lbl">HydraProxy balance</span><span class="val">${{ hydra_balance }}</span></div>
|
||||
<div class="card stat"><span class="lbl">Users</span><span class="val">{{ stats.users }} <span style="font-size:14px;color:var(--muted)">({{ stats.active }} active)</span></span></div>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<a href="/">Home</a>
|
||||
<a href="/plans">Residential</a>
|
||||
<a href="/dedicated">Dedicated IPs</a>
|
||||
<a href="/hydraproxy">HydraProxy</a>
|
||||
{% if user %}<a href="/dashboard">Dashboard</a><a href="/logout">Logout</a>
|
||||
{% else %}<a href="/login">Login</a><a class="cta" href="/signup">Get proxies</a>{% endif %}
|
||||
</div>
|
||||
|
||||
30
templates/hydraproxy.html
Normal file
30
templates/hydraproxy.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}HydraProxy — Clean Proxys{% endblock %}
|
||||
{% block body %}
|
||||
<div class="hero" style="padding:64px 0 28px">
|
||||
<h1 style="font-size:36px">Hydra<span class="grad">Proxy</span> tier.</h1>
|
||||
<p class="lede">Residential, 4G mobile, and static-mobile IPs — brokered and delivered by Clean Proxys. You pay us, we place the order with HydraProxy, you connect.</p>
|
||||
</div>
|
||||
|
||||
{% if err %}
|
||||
<div class="flash warn">HydraProxy link is offline right now: {{ err }}<br><span class="muted">(Usually means the broker IP isn't whitelisted at HydraProxy yet — the catalog below still shows what's offered.)</span></div>
|
||||
{% else %}
|
||||
<div class="statsbar">
|
||||
<div class="s"><div class="n grad">${{ account.balance_usd if account else '—' }}</div><div class="l">HydraProxy balance</div></div>
|
||||
<div class="s"><div class="n grad">{{ account.username if account else '—' }}</div><div class="l">Account</div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid">
|
||||
{% for key, p in products.items() %}
|
||||
<div class="card plan">
|
||||
<h3>{{ p.name }}</h3>
|
||||
<p>{{ p.desc }}</p>
|
||||
<div class="price">{{ p.price_sats }} <span style="font-size:16px;color:var(--muted)">sats / {{ p.unit }}</span></div>
|
||||
<p class="muted" style="font-size:13px;margin-top:8px">Brokered — we place the order with HydraProxy after payment and hand you the live proxy.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<p class="muted" style="margin-top:26px">These are broker-fulfilled (order placed manually with HydraProxy). For instant self-serve, see <a href="/plans">residential plans</a> and <a href="/dedicated">dedicated IPs</a>.</p>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user