diff --git a/app.py b/app.py index 52b30c9..89230ec 100644 --- a/app.py +++ b/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"]) diff --git a/hydraproxy.py b/hydraproxy.py new file mode 100644 index 0000000..54c38a1 --- /dev/null +++ b/hydraproxy.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""HydraProxy management API wrapper. + +Base: https://app.hydraproxy.com/api/ (auth: Authorization: Token ) +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() diff --git a/mcp_server.py b/mcp_server.py index ea719f2..d2e5c2f 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -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")) diff --git a/templates/admin.html b/templates/admin.html index 0fabce1..8971a24 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -9,6 +9,7 @@
Revenue (paid){{ stats.revenue_sats }} sats
IPRoyal balance${{ ipr_balance }}
+
HydraProxy balance${{ hydra_balance }}
Users{{ stats.users }} ({{ stats.active }} active)
diff --git a/templates/base.html b/templates/base.html index 04065d4..3dedd4a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -39,6 +39,7 @@ Home Residential Dedicated IPs + HydraProxy {% if user %}DashboardLogout {% else %}LoginGet proxies{% endif %}
diff --git a/templates/hydraproxy.html b/templates/hydraproxy.html new file mode 100644 index 0000000..0d83b93 --- /dev/null +++ b/templates/hydraproxy.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block title %}HydraProxy — Clean Proxys{% endblock %} +{% block body %} +
+

HydraProxy tier.

+

Residential, 4G mobile, and static-mobile IPs — brokered and delivered by Clean Proxys. You pay us, we place the order with HydraProxy, you connect.

+
+ +{% if err %} +
HydraProxy link is offline right now: {{ err }}
(Usually means the broker IP isn't whitelisted at HydraProxy yet — the catalog below still shows what's offered.)
+{% else %} +
+
${{ account.balance_usd if account else '—' }}
HydraProxy balance
+
{{ account.username if account else '—' }}
Account
+
+{% endif %} + +
+ {% for key, p in products.items() %} +
+

{{ p.name }}

+

{{ p.desc }}

+
{{ p.price_sats }} sats / {{ p.unit }}
+

Brokered — we place the order with HydraProxy after payment and hand you the live proxy.

+
+ {% endfor %} +
+ +

These are broker-fulfilled (order placed manually with HydraProxy). For instant self-serve, see residential plans and dedicated IPs.

+{% endblock %}