Files

499 lines
19 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""urnet - URnetwork free-tier farm: accounts, API keys, hosted proxies.
One file, standard library only (curl is optional, used as a fallback transport).
Works on Windows / Linux / macOS with Python 3.8+.
python urnet.py farm --count 3 --proxies 5
python urnet.py collect --bank bank.jsonl --out pool.jsonl
python urnet.py verify --pool pool.jsonl
python urnet.py export --pool pool.jsonl --format txt --out proxies.txt
python urnet.py balance --bank bank.jsonl
Docs: README.md (quick start) and GUIDE.md (everything else).
"""
import argparse
import json
import os
import random
import shutil
import ssl
import subprocess
import sys
import time
import urllib.error
import urllib.request
API = "https://api.bringyour.com"
DEFAULT_COUNTRIES = ["us", "de", "nl", "fr", "gb", "ca", "br", "se", "pl", "ch",
"jp", "sg", "it", "es", "at"]
UA = "urnet/1.1 (+https://git.reformboss.com/reform/ur-network-farm)"
HAVE_CURL = shutil.which("curl") is not None
SSLCTX = ssl.create_default_context()
SSLCTX.check_hostname = False
SSLCTX.verify_mode = ssl.CERT_NONE
def setup_stdout():
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8")
except Exception:
pass
def human(n):
if n is None:
return "?"
n = float(n)
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
if abs(n) < 1024 or unit == "TiB":
return ("%.2f %s" % (n, unit)) if unit != "B" else ("%d B" % n)
n /= 1024.0
# ---------------------------------------------------------------- transport
def http_json(path, method="GET", payload=None, token=None, proxy=None,
timeout=60, use_curl=False):
"""Return (status, parsed_body_or_error_string). Never raises."""
url = API + path
body = json.dumps(payload).encode() if payload is not None else None
if proxy and (use_curl or HAVE_CURL):
cmd = ["curl", "-s", "-m", str(timeout), "--proxy", proxy, "-X", method, url,
"-H", "Accept: application/json", "-H", "User-Agent: " + UA]
if token:
cmd += ["-H", "Authorization: Bearer " + token]
if body:
cmd += ["-H", "Content-Type: application/json", "-d", body.decode()]
try:
out = subprocess.run(cmd, capture_output=True, text=True,
timeout=timeout + 25).stdout
except Exception as exc:
return 0, "%s: %s" % (type(exc).__name__, str(exc)[:120])
st, body = _parse(out)
if not out.strip() and not use_curl:
return 0, "curl returned nothing through this proxy"
return st, body
handlers = []
if proxy:
handlers.append(urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
else:
handlers.append(urllib.request.ProxyHandler({})) # ignore env proxies
handlers.append(urllib.request.HTTPSHandler(context=SSLCTX))
opener = urllib.request.build_opener(*handlers)
headers = {"Accept": "application/json", "User-Agent": UA}
if body is not None:
headers["Content-Type"] = "application/json"
if token:
headers["Authorization"] = "Bearer " + token
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with opener.open(req, timeout=timeout) as resp:
return _parse(resp.read(120000).decode("utf-8", "replace"), resp.status)
except urllib.error.HTTPError as exc:
return exc.code, exc.read(4000).decode("utf-8", "replace")
except Exception as exc:
return 0, "%s: %s" % (type(exc).__name__, str(exc)[:140])
def _parse(raw, status=200):
try:
return status, json.loads(raw)
except Exception:
return status, raw[:400]
def err_text(body):
"""Pull a readable message out of an error body."""
if isinstance(body, dict):
e = body.get("error")
if isinstance(e, dict):
return e.get("message") or json.dumps(e)[:200]
if isinstance(e, str):
return e
return json.dumps(body)[:200]
return str(body)[:200]
# ---------------------------------------------------------------- core steps
def create_account(proxy=None, use_curl=False, timeout=60):
st, d = http_json("/auth/network-create", "POST",
{"terms": True, "product_updates": False},
proxy=proxy, use_curl=use_curl, timeout=timeout)
if not isinstance(d, dict) or "network" not in d:
return None, "create failed (%s): %s" % (st, err_text(d))
net = d["network"]
if not net.get("by_jwt"):
return None, "create failed: no by_jwt in response"
return {"network_id": net.get("network_id"),
"name": net.get("network_name"),
"jwt": net["by_jwt"]}, None
def get_seed(jwt, proxy=None, use_curl=False):
st, d = http_json("/auth/regenerate-seedphrase", "POST", {}, jwt,
proxy=proxy, use_curl=use_curl)
if isinstance(d, dict):
seed = d.get("seedphrase")
if isinstance(seed, list):
seed = " ".join(seed)
if seed:
return seed, None
return None, "seed failed (%s): %s" % (st, err_text(d))
def get_key(jwt, name="urnet", proxy=None, use_curl=False):
st, d = http_json("/account/api-key", "POST", {"name": name}, jwt,
proxy=proxy, use_curl=use_curl)
if isinstance(d, dict) and d.get("api_key"):
return d["api_key"], None
return None, "api-key failed (%s): %s" % (st, err_text(d))
def make_proxy(key, cc, proxy=None, use_curl=False):
st, d = http_json("/network/auth-client", "POST", {
"description": "HTTPS proxy · %s" % cc.upper(),
"device_spec": "resident proxy",
"proxy_config": {"initial_device_state": {"country_code": cc}},
}, key, proxy=proxy, use_curl=use_curl, timeout=90)
if isinstance(d, dict):
r = d.get("proxy_config_result") or {}
if r.get("https_proxy_url"):
return {"cc": cc, "url": r["https_proxy_url"],
"socks": r.get("socks_proxy_url"),
"client_id": d.get("client_id")}, None
return None, err_text(d)
return None, "proxy failed (%s): %s" % (st, str(d)[:160])
def list_proxies(key, proxy=None, use_curl=False):
st, d = http_json("/network/proxies", "GET", None, key,
proxy=proxy, use_curl=use_curl)
if not isinstance(d, dict):
return [], "proxies failed (%s): %s" % (st, str(d)[:160])
out = []
for c in d.get("clients") or []:
pc = c.get("proxy_client") or {}
if pc.get("https_proxy_url"):
out.append({"cc": (c.get("description") or "").split("·")[-1].strip().lower(),
"url": pc["https_proxy_url"],
"socks": pc.get("socks_proxy_url"),
"client_id": c.get("client_id")})
return out, None
def get_balance(key, proxy=None, use_curl=False):
st, d = http_json("/subscription/balance", "GET", None, key,
proxy=proxy, use_curl=use_curl)
if not isinstance(d, dict):
return None, "balance failed (%s): %s" % (st, str(d)[:160])
return {"left": d.get("balance_byte_count"),
"start": d.get("start_balance_byte_count"),
"open_transfer": d.get("open_transfer_byte_count")}, None
# ---------------------------------------------------------------- helpers
def load_proxies(path):
if not path or not os.path.exists(path):
return []
out = []
for line in open(path, encoding="utf-8"):
line = line.strip()
if line and not line.startswith("#"):
out.append(line)
return out
def append_jsonl(path, rec):
with open(path, "a", encoding="utf-8") as fh:
fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
fh.flush()
def is_ip_limit(msg):
low = str(msg).lower()
return "too many accounts" in low or "429" in low
# ---------------------------------------------------------------- commands
def cmd_farm(a):
pool = load_proxies(a.proxy_file)
if a.proxy:
pool = [a.proxy]
if a.shuffle and len(pool) > 1:
random.shuffle(pool)
countries = [c.strip().lower() for c in a.countries.split(",") if c.strip()] \
if a.countries else DEFAULT_COUNTRIES
made = proxies = 0
failures = 0
print("farm: %d account(s) x %d proxies | egress: %s | bank: %s"
% (a.count, a.proxies, ("%d rotating" % len(pool)) if pool else "direct", a.bank))
for n in range(a.count):
idx = a.start + n
px = None
if pool:
px = pool[(n + random.randrange(len(pool))) % len(pool)]
rec = {"idx": idx, "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"egress": "rotating" if px else "direct", "proxies": []}
acc, err = create_account(px, a.curl)
if err:
failures += 1
print(" #%d FAIL %s" % (idx, err))
if is_ip_limit(err):
if not pool:
print(" -> this IP has hit the account cap. Add --proxy-file px.txt "
"with proxy URLs you already own, or wait for the reset.")
elif failures >= max(3, len(pool) // 4 + 2):
print(" -> every egress is capped right now; stopping.")
break
append_jsonl(a.bank, dict(rec, error=err))
continue
rec.update(acc)
seed, e1 = get_seed(acc["jwt"], px, a.curl)
if e1:
rec["error"] = e1
else:
rec["seed"] = seed
key, e2 = get_key(acc["jwt"], "urnet-%d" % idx, px, a.curl)
if e2:
rec["error"] = e2
append_jsonl(a.bank, rec)
failures += 1
print(" #%d FAIL %s" % (idx, e2))
continue
rec["urn"] = key
for i in range(a.proxies):
cc = countries[(n * a.proxies + i) % len(countries)]
pr, e3 = make_proxy(key, cc, px, a.curl)
if pr:
rec["proxies"].append(pr)
else:
rec.setdefault("proxy_errors", []).append("%s: %s" % (cc, e3))
time.sleep(a.delay)
append_jsonl(a.bank, rec)
made += 1
proxies += len(rec["proxies"])
print(" #%d OK %-18s proxies=%d [%s]%s"
% (idx, rec.get("name"), len(rec["proxies"]),
",".join(p["cc"] for p in rec["proxies"]),
(" seed=%s…" % (rec.get("seed") or "")[:12]) if a.show_seed else ""))
time.sleep(a.delay)
print(json.dumps({"accounts_ok": made, "proxies_ok": proxies,
"failed": failures, "bank": a.bank}))
if made:
print("\nnext: python urnet.py collect --bank %s --out pool.jsonl" % a.bank)
print(" python urnet.py verify --pool pool.jsonl --limit 10")
return 0 if made else 1
def cmd_collect(a):
rows, seen = [], set()
if a.bank and os.path.exists(a.bank):
for line in open(a.bank, encoding="utf-8"):
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
except Exception:
continue
for p in rec.get("proxies") or []:
url = p.get("url")
if not url or url in seen:
continue
seen.add(url)
rows.append({"acct": rec.get("idx"), "name": rec.get("name"),
"cc": p.get("cc"), "url": url, "socks": p.get("socks"),
"client_id": p.get("client_id")})
with open(a.out, "w", encoding="utf-8") as fh:
for r in rows:
fh.write(json.dumps(r, ensure_ascii=False) + "\n")
ccs = sorted({(r["cc"] or "?")[:2].upper() for r in rows})
print("collected %d unique proxies from %s -> %s (%d countries: %s)"
% (len(rows), a.bank, a.out, len(ccs), ",".join(ccs)))
return 0 if rows else 1
def cmd_verify(a):
if not os.path.exists(a.pool):
print("pool not found: %s (run: python urnet.py collect --bank bank.jsonl)" % a.pool)
return 1
rows = [json.loads(l) for l in open(a.pool, encoding="utf-8") if l.strip()]
if a.limit:
if a.shuffle:
random.shuffle(rows)
rows = rows[:a.limit]
alive, ips, ccs, hosting = 0, set(), set(), []
# hosting ASNs that showed up in real pools; plain legal suffixes (GmbH/LLC/Ltd)
# are NOT hosting - Vodafone GmbH is a residential ISP.
hosting_words = ("hosting", "vpn", "datacenter", "data center", "colocation", "colo",
"cloud", "amazon", "digitalocean", "ovh", "hetzner", "linode",
"vultr", "contabo", "m247", "choopa", "leaseweb", "3xk",
"serverius", "psychz", "hostkey", "aeza")
for r in rows:
d = _ipinfo(r["url"], a.timeout)
d = d or {}
r.update(d)
ip = d.get("ip")
if ip:
alive += 1
ips.add(ip)
ccs.add(d.get("country"))
org = (d.get("org") or "")
flag = ""
if org and any(w in org.lower() for w in hosting_words):
flag = " <- hosting ASN"
hosting.append(ip)
print("%-4s %-3s %-16s %-3s %-14s %-28s%s"
% (r.get("acct"), r.get("cc"), ip or "FAIL", d.get("country") or "--",
(d.get("city") or "")[:14], org[:28], flag))
if a.out:
json.dump(rows, open(a.out, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
print("\nTOTAL %d | alive %d | unique_ip %d | countries %d | hosting_exits %d"
% (len(rows), alive, len(ips), len(ccs), len(hosting)))
return 0 if alive else 1
def _ipinfo(proxy_url, timeout=35):
if HAVE_CURL:
try:
out = subprocess.run(["curl", "-s", "-m", str(timeout), "--proxy", proxy_url,
"https://ipinfo.io/json"],
capture_output=True, text=True, timeout=timeout + 25).stdout
d = json.loads(out)
return {"ip": d.get("ip"), "country": d.get("country"),
"city": d.get("city"), "org": d.get("org")}
except Exception:
return {}
handlers = [urllib.request.ProxyHandler({"http": proxy_url, "https": proxy_url}),
urllib.request.HTTPSHandler(context=SSLCTX)]
opener = urllib.request.build_opener(*handlers)
req = urllib.request.Request("https://ipinfo.io/json", headers={"User-Agent": UA})
try:
with opener.open(req, timeout=timeout) as resp:
d = json.loads(resp.read(8000).decode("utf-8", "replace"))
return {"ip": d.get("ip"), "country": d.get("country"),
"city": d.get("city"), "org": d.get("org")}
except Exception:
return {}
def cmd_export(a):
rows = [json.loads(l) for l in open(a.pool, encoding="utf-8") if l.strip()]
if a.cc:
want = {c.strip().lower() for c in a.cc.split(",")}
rows = [r for r in rows if (r.get("cc") or "").lower() in want]
if a.limit:
rows = rows[:a.limit]
if a.format == "txt":
body = "\n".join(r["url"] for r in rows) + "\n"
elif a.format == "csv":
body = "cc,url\n" + "\n".join("%s,%s" % (r.get("cc"), r["url"]) for r in rows) + "\n"
else: # env
body = "HTTP_PROXY=%s\nHTTPS_PROXY=%s\n" % (rows[0]["url"], rows[0]["url"]) if rows else ""
with open(a.out, "w", encoding="utf-8") as fh:
fh.write(body)
print("exported %d proxies -> %s (%s)" % (len(rows), a.out, a.format))
return 0 if rows else 1
def cmd_balance(a):
rows = [json.loads(l) for l in open(a.bank, encoding="utf-8") if l.strip()]
total = 0
live = 0
for rec in rows:
key = rec.get("urn")
if not key:
continue
b, err = get_balance(key)
if err:
print("%-4s %-18s %s" % (rec.get("idx"), rec.get("name"), err))
continue
live += 1
left = b.get("left") or 0
total += left
print("%-4s %-18s left %-10s of %-10s open %s | proxies %d"
% (rec.get("idx"), rec.get("name"), human(left), human(b.get("start")),
human(b.get("open_transfer")), len(rec.get("proxies") or [])))
print("\naccounts %d | live keys %d | traffic left today %s"
% (len(rows), live, human(total)))
return 0
# ---------------------------------------------------------------- cli
def build_parser():
p = argparse.ArgumentParser(
prog="urnet",
description="URnetwork free-tier farm: instant accounts, API keys, hosted proxies.",
epilog="quick start: python urnet.py farm --count 1 && python urnet.py collect "
"--bank bank.jsonl --out pool.jsonl && python urnet.py verify --pool pool.jsonl")
sub = p.add_subparsers(dest="cmd", required=True)
f = sub.add_parser("farm", help="create accounts, keys and proxies")
f.add_argument("--count", type=int, default=1, help="accounts to create (default 1)")
f.add_argument("--proxies", type=int, default=5, help="proxies per account, max 5 (default 5)")
f.add_argument("--countries", default="", help="comma list of ISO codes, e.g. us,de,nl")
f.add_argument("--bank", default="bank.jsonl", help="output jsonl (default bank.jsonl)")
f.add_argument("--start", type=int, default=1, help="first account index label")
f.add_argument("--proxy", default="", help="single egress proxy URL for every request")
f.add_argument("--proxy-file", default="", help="file with one proxy URL per line (rotates)")
f.add_argument("--shuffle", action="store_true", help="shuffle the proxy file")
f.add_argument("--delay", type=float, default=1.3, help="seconds between proxy calls")
f.add_argument("--curl", action="store_true", help="use curl instead of urllib for proxied calls")
f.add_argument("--show-seed", action="store_true", help="print seed prefix in the log")
f.set_defaults(func=cmd_farm)
c = sub.add_parser("collect", help="flatten bank.jsonl into a proxy pool")
c.add_argument("--bank", default="bank.jsonl")
c.add_argument("--out", default="pool.jsonl")
c.set_defaults(func=cmd_collect)
v = sub.add_parser("verify", help="liveness pass: ipinfo through each proxy")
v.add_argument("--pool", default="pool.jsonl")
v.add_argument("--limit", type=int, default=0, help="check only N proxies (0 = all)")
v.add_argument("--shuffle", action="store_true")
v.add_argument("--timeout", type=int, default=35)
v.add_argument("--out", default="", help="write enriched pool to this file")
v.set_defaults(func=cmd_verify)
e = sub.add_parser("export", help="write pool as txt / csv / env")
e.add_argument("--pool", default="pool.jsonl")
e.add_argument("--format", choices=["txt", "csv", "env"], default="txt")
e.add_argument("--cc", default="", help="only these countries, e.g. us,de")
e.add_argument("--limit", type=int, default=0)
e.add_argument("--out", default="proxies.txt")
e.set_defaults(func=cmd_export)
b = sub.add_parser("balance", help="traffic left per account")
b.add_argument("--bank", default="bank.jsonl")
b.set_defaults(func=cmd_balance)
return p
def main():
setup_stdout()
args = build_parser().parse_args()
try:
return args.func(args)
except KeyboardInterrupt:
print("\ninterrupted - partial progress is already in the bank file")
return 130
if __name__ == "__main__":
sys.exit(main())