proxy-scraper-cli 1.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- proxy_scraper_cli-1.7.1.dist-info/METADATA +794 -0
- proxy_scraper_cli-1.7.1.dist-info/RECORD +57 -0
- proxy_scraper_cli-1.7.1.dist-info/WHEEL +5 -0
- proxy_scraper_cli-1.7.1.dist-info/entry_points.txt +4 -0
- proxy_scraper_cli-1.7.1.dist-info/licenses/LICENSE +21 -0
- proxy_scraper_cli-1.7.1.dist-info/top_level.txt +1 -0
- proxyscraper/__init__.py +12 -0
- proxyscraper/__main__.py +7 -0
- proxyscraper/agent.py +635 -0
- proxyscraper/api.py +139 -0
- proxyscraper/app.py +595 -0
- proxyscraper/asndb.py +181 -0
- proxyscraper/blocklist.py +92 -0
- proxyscraper/checker.py +514 -0
- proxyscraper/cli.py +269 -0
- proxyscraper/compat.py +83 -0
- proxyscraper/completion.py +196 -0
- proxyscraper/exporters.py +118 -0
- proxyscraper/fetchcache.py +103 -0
- proxyscraper/geo.py +150 -0
- proxyscraper/geodb.py +143 -0
- proxyscraper/handshake.py +153 -0
- proxyscraper/history.py +106 -0
- proxyscraper/judges.py +159 -0
- proxyscraper/mcp_entry.py +34 -0
- proxyscraper/mcp_server.py +220 -0
- proxyscraper/netio.py +167 -0
- proxyscraper/options.py +274 -0
- proxyscraper/output.py +181 -0
- proxyscraper/pages.py +212 -0
- proxyscraper/parsing.py +192 -0
- proxyscraper/paths.py +55 -0
- proxyscraper/pipeline.py +434 -0
- proxyscraper/preferences.py +24 -0
- proxyscraper/publish.py +236 -0
- proxyscraper/server/__init__.py +41 -0
- proxyscraper/server/core.py +518 -0
- proxyscraper/server/http.py +164 -0
- proxyscraper/server/pool.py +195 -0
- proxyscraper/server/socks.py +65 -0
- proxyscraper/server/status.py +108 -0
- proxyscraper/server/upstream.py +119 -0
- proxyscraper/site/apple-touch-icon.png +0 -0
- proxyscraper/site/googleaac1161b7853c5b5.html +1 -0
- proxyscraper/site/index.html +934 -0
- proxyscraper/site/logo.png +0 -0
- proxyscraper/site/og.png +0 -0
- proxyscraper/sources.json +395 -0
- proxyscraper/sources.py +491 -0
- proxyscraper/targets.py +76 -0
- proxyscraper/ui/__init__.py +54 -0
- proxyscraper/ui/dashboard.py +344 -0
- proxyscraper/ui/keys.py +82 -0
- proxyscraper/ui/report.py +209 -0
- proxyscraper/ui/serve.py +142 -0
- proxyscraper/ui/widgets.py +250 -0
- proxyscraper/ui/wizard.py +595 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Remembers the last choice from the setup wizard (as command line arguments)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from .paths import DATA_DIR, atomic_write
|
|
9
|
+
|
|
10
|
+
PREFERENCES_FILE = DATA_DIR / "preferences.json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_last_argv() -> Optional[List[str]]:
|
|
14
|
+
try:
|
|
15
|
+
argv = json.loads(PREFERENCES_FILE.read_text(encoding="utf-8")).get("last")
|
|
16
|
+
except (OSError, ValueError, AttributeError):
|
|
17
|
+
return None
|
|
18
|
+
if isinstance(argv, list) and all(isinstance(a, str) for a in argv):
|
|
19
|
+
return argv
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def save_last_argv(argv: List[str]) -> None:
|
|
24
|
+
atomic_write(PREFERENCES_FILE, json.dumps({"last": argv}, indent=1))
|
proxyscraper/publish.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Prepares the hits of a run for the `proxy-list` branch (runs in GitHub Actions).
|
|
2
|
+
|
|
3
|
+
python -m proxyscraper.publish results/<run> public/ --min 20
|
|
4
|
+
|
|
5
|
+
Writes lists per protocol, HTTPS and elite lists, JSON/CSV, badge files for shields.io,
|
|
6
|
+
a README and the website for GitHub Pages (site/index.html plus history.json for the trend).
|
|
7
|
+
If there are fewer than `--min` hits (e.g. because the runner was unlucky), it exits with
|
|
8
|
+
code 78 and writes nothing – the old list then stays online.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import csv
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import statistics
|
|
18
|
+
import sys
|
|
19
|
+
from collections import Counter
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Dict, List, Optional
|
|
23
|
+
|
|
24
|
+
from .pages import write_pages
|
|
25
|
+
from .parsing import PROXY_TYPES
|
|
26
|
+
|
|
27
|
+
SKIP_EXIT_CODE = 78
|
|
28
|
+
SITE = Path(__file__).resolve().parent / "site" # index.html plus the images it links (logo, preview, touch icon)
|
|
29
|
+
RUN_HOURS = 1 # the proxy-list workflow runs every hour; clients read it from stats.json as run_hours
|
|
30
|
+
HISTORY_LIMIT = 30 * 24 // RUN_HOURS # 30 days of runs for the chart
|
|
31
|
+
RAW_BASE = "https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_public(row: dict) -> bool:
|
|
35
|
+
"""Proxies with credentials never belong in the public list – the login comes from some
|
|
36
|
+
third-party list, and once published it would be visible to everyone."""
|
|
37
|
+
return "@" not in row.get("proxy", "")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_rows(run_dir: Path) -> List[dict]:
|
|
41
|
+
rows = json.loads((run_dir / "proxies.json").read_text(encoding="utf-8"))
|
|
42
|
+
return sorted((r for r in rows if is_public(r)), key=lambda r: r["latency"])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def num(n: int) -> str:
|
|
46
|
+
"""Thousands separators: 12345 -> 12,345"""
|
|
47
|
+
return f"{n:,}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def badge(label: str, count: int, color: str) -> dict:
|
|
51
|
+
"""Format for https://shields.io/badges/endpoint-badge"""
|
|
52
|
+
return {"schemaVersion": 1, "label": label, "message": num(count), "color": color}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def stats_for(rows: List[dict], now: datetime) -> dict:
|
|
56
|
+
return {
|
|
57
|
+
"updated": now.isoformat(timespec="seconds"),
|
|
58
|
+
"total": len(rows),
|
|
59
|
+
"by_type": {t: sum(1 for r in rows if r["ptype"] == t) for t in PROXY_TYPES},
|
|
60
|
+
"https": sum(1 for r in rows if r.get("https")),
|
|
61
|
+
"elite": sum(1 for r in rows if r.get("anonymity") == "elite"),
|
|
62
|
+
# only with provider data – otherwise "0" would wrongly mean "no datacenters"
|
|
63
|
+
"datacenter": sum(1 for r in rows if r.get("hosting")) if any(r.get("org") for r in rows) else None,
|
|
64
|
+
# only when the lookup ran – otherwise "0" would wrongly mean "none listed"
|
|
65
|
+
"blocklisted": sum(1 for r in rows if r.get("blocklisted"))
|
|
66
|
+
if any(r.get("blocklisted") is not None for r in rows) else None,
|
|
67
|
+
"stable": sum(1 for r in rows if r.get("streak", 0) >= STABLE_RUNS),
|
|
68
|
+
"run_hours": RUN_HOURS,
|
|
69
|
+
"countries": dict(Counter(r["country"] for r in rows if r.get("country")).most_common(15)),
|
|
70
|
+
"median_latency": round(statistics.median(r["latency"] for r in rows)) if rows else 0,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def write_lists(rows: List[dict], out: Path) -> Dict[str, int]:
|
|
75
|
+
files = {
|
|
76
|
+
"all.txt": [r["url"] for r in rows],
|
|
77
|
+
"https.txt": [r["url"] for r in rows if r.get("https")],
|
|
78
|
+
"elite.txt": [r["url"] for r in rows if r.get("anonymity") == "elite"],
|
|
79
|
+
}
|
|
80
|
+
for t in PROXY_TYPES:
|
|
81
|
+
files[f"{t}.txt"] = [r["proxy"] for r in rows if r["ptype"] == t]
|
|
82
|
+
for name, lines in files.items():
|
|
83
|
+
(out / name).write_text("".join(f"{line}\n" for line in lines), encoding="utf-8")
|
|
84
|
+
return {name: len(lines) for name, lines in files.items()}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def readme(stats: dict, counts: Dict[str, int]) -> str:
|
|
88
|
+
updated = datetime.fromisoformat(stats["updated"]).strftime("%Y-%m-%d %H:%M UTC")
|
|
89
|
+
rows = [
|
|
90
|
+
("All (`type://ip:port`)", "all.txt"),
|
|
91
|
+
("HTTP (`ip:port`)", "http.txt"),
|
|
92
|
+
("SOCKS4 (`ip:port`)", "socks4.txt"),
|
|
93
|
+
("SOCKS5 (`ip:port`)", "socks5.txt"),
|
|
94
|
+
("HTTPS-capable (`type://ip:port`)", "https.txt"),
|
|
95
|
+
("Elite (`type://ip:port`)", "elite.txt"),
|
|
96
|
+
]
|
|
97
|
+
table = "\n".join(f"| {label} | {num(counts[name])} | [{name}]({RAW_BASE}/{name}) |" for label, name in rows)
|
|
98
|
+
details = f"[proxies.json]({RAW_BASE}/proxies.json) · [proxies.csv]({RAW_BASE}/proxies.csv)"
|
|
99
|
+
countries = " · ".join(f"{cc} {num(n)}" for cc, n in stats["countries"].items()) or "–"
|
|
100
|
+
return f"""# Live proxy list
|
|
101
|
+
|
|
102
|
+
Generated automatically by [proxy-scraper](https://github.com/maximilianfeix/proxy-scraper) with GitHub Actions.
|
|
103
|
+
Every proxy here really worked in the last run – sorted by latency, fastest first.
|
|
104
|
+
Browse and filter it on the [website](https://maximilianfeix.github.io/proxy-scraper/).
|
|
105
|
+
|
|
106
|
+
**Updated:** {updated} · **{num(stats["total"])} proxies** · median latency {num(stats["median_latency"])} ms
|
|
107
|
+
|
|
108
|
+
| List | Count | File |
|
|
109
|
+
|---|---:|---|
|
|
110
|
+
{table}
|
|
111
|
+
| Details (latency, country, HTTPS, anonymity) | {num(stats["total"])} | {details} |
|
|
112
|
+
|
|
113
|
+
**Top countries:** {countries}
|
|
114
|
+
|
|
115
|
+
> Public proxies are run by strangers. Never send passwords or personal data through them.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def step_summary(stats: dict) -> str:
|
|
120
|
+
by_type = " · ".join(f"{t}: {n}" for t, n in stats["by_type"].items())
|
|
121
|
+
return (f"### Proxy list updated\n\n**{stats['total']}** working proxies ({by_type}), "
|
|
122
|
+
f"{stats['https']} HTTPS-capable, {stats['elite']} elite, median latency {stats['median_latency']} ms\n")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def write_json(path: Path, data, indent: Optional[int] = None) -> None:
|
|
126
|
+
path.write_text(json.dumps(data, indent=indent, ensure_ascii=False), encoding="utf-8")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_history(path: Optional[Path]) -> List[dict]:
|
|
130
|
+
"""History of the last runs (fetched from the branch) – broken or missing means: start over."""
|
|
131
|
+
if not path:
|
|
132
|
+
return []
|
|
133
|
+
try:
|
|
134
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
135
|
+
except (OSError, ValueError):
|
|
136
|
+
return []
|
|
137
|
+
if not isinstance(data, list):
|
|
138
|
+
return []
|
|
139
|
+
return [e for e in data if isinstance(e, dict) and isinstance(e.get("total"), int)]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def history_entry(stats: dict) -> dict:
|
|
143
|
+
return {"updated": stats["updated"], "total": stats["total"], "https": stats["https"],
|
|
144
|
+
"by_type": stats["by_type"], "median_latency": stats["median_latency"], "run_hours": RUN_HOURS}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def streak_factor(runs: List[dict]) -> int:
|
|
148
|
+
"""Old streaks count runs of the previous interval: entries from before run_hours existed ran every 6 hours.
|
|
149
|
+
Multiplying keeps "up for 5 days" at 5 days when the interval gets shorter."""
|
|
150
|
+
if not runs:
|
|
151
|
+
return 1
|
|
152
|
+
before = runs[-1].get("run_hours", 6)
|
|
153
|
+
return max(1, before // RUN_HOURS) if type(before) is int and before > 0 else 1
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
STABLE_RUNS = 24 // RUN_HOURS # this many runs in a row (= 24 hours) means "stable"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def load_streaks(path: Optional[Path]) -> Dict[str, int]:
|
|
160
|
+
"""url -> runs in a row it has been on the list (fetched from the branch); broken or missing = start over."""
|
|
161
|
+
if not path:
|
|
162
|
+
return {}
|
|
163
|
+
try:
|
|
164
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
165
|
+
except (OSError, ValueError):
|
|
166
|
+
return {}
|
|
167
|
+
if not isinstance(data, dict):
|
|
168
|
+
return {}
|
|
169
|
+
return {k: v for k, v in data.items() if isinstance(k, str) and type(v) is int and v > 0} # bool is an int too
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def publish(run_dir: Path, out: Path, minimum: int = 20, now: Optional[datetime] = None,
|
|
173
|
+
history: Optional[Path] = None, streaks: Optional[Path] = None) -> int:
|
|
174
|
+
rows = load_rows(run_dir)
|
|
175
|
+
if len(rows) < minimum:
|
|
176
|
+
print(f"Only {len(rows)} hits (< {minimum}) – the old list stays online.")
|
|
177
|
+
return SKIP_EXIT_CODE
|
|
178
|
+
now = now or datetime.now(timezone.utc)
|
|
179
|
+
# whoever was there in the last run keeps counting – everyone else starts at 1, whoever is missing drops out
|
|
180
|
+
past_runs = load_history(history)
|
|
181
|
+
factor = streak_factor(past_runs)
|
|
182
|
+
previous = {url: n * factor for url, n in load_streaks(streaks).items()}
|
|
183
|
+
for row in rows:
|
|
184
|
+
row["streak"] = previous.get(row["url"], 0) + 1
|
|
185
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
counts = write_lists(rows, out)
|
|
187
|
+
# don't just copy: rewrite the details so nothing with credentials ends up there either
|
|
188
|
+
(out / "proxies.json").write_text(json.dumps(rows, indent=1, ensure_ascii=False), encoding="utf-8")
|
|
189
|
+
with (run_dir / "proxies.csv").open(newline="", encoding="utf-8") as src, \
|
|
190
|
+
(out / "proxies.csv").open("w", newline="", encoding="utf-8") as dst:
|
|
191
|
+
reader = csv.DictReader(src)
|
|
192
|
+
writer = csv.DictWriter(dst, fieldnames=reader.fieldnames or ["proxy"])
|
|
193
|
+
writer.writeheader()
|
|
194
|
+
writer.writerows(r for r in reader if is_public(r))
|
|
195
|
+
stats = stats_for(rows, now)
|
|
196
|
+
write_json(out / "stats.json", stats, indent=1)
|
|
197
|
+
badges = out / "badges"
|
|
198
|
+
badges.mkdir(exist_ok=True)
|
|
199
|
+
colors = {"total": "brightgreen", "http": "blue", "socks4": "blueviolet", "socks5": "green"}
|
|
200
|
+
write_json(badges / "total.json", badge("working proxies", stats["total"], colors["total"]))
|
|
201
|
+
for t in PROXY_TYPES:
|
|
202
|
+
write_json(badges / f"{t}.json", badge(t, stats["by_type"][t], colors[t]))
|
|
203
|
+
write_json(badges / "updated.json", {"schemaVersion": 1, "label": "updated",
|
|
204
|
+
"message": now.strftime("%Y-%m-%d %H:%M UTC"), "color": "grey"})
|
|
205
|
+
(out / "README.md").write_text(readme(stats, counts), encoding="utf-8")
|
|
206
|
+
# website: a static page that loads proxies.json/stats.json/history.json from next door
|
|
207
|
+
for asset in SITE.iterdir():
|
|
208
|
+
if asset.suffix in (".html", ".png"):
|
|
209
|
+
(out / asset.name).write_bytes(asset.read_bytes())
|
|
210
|
+
(out / ".nojekyll").write_text("", encoding="utf-8") # Pages should serve the files unchanged
|
|
211
|
+
write_pages(rows, out, now) # static pages per protocol and country plus sitemap.xml, for search engines
|
|
212
|
+
runs = [*past_runs, history_entry(stats)][-HISTORY_LIMIT:]
|
|
213
|
+
write_json(out / "history.json", runs)
|
|
214
|
+
write_json(out / "streaks.json", {row["url"]: row["streak"] for row in rows})
|
|
215
|
+
|
|
216
|
+
summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
|
|
217
|
+
if summary_file:
|
|
218
|
+
with open(summary_file, "a", encoding="utf-8") as fh:
|
|
219
|
+
fh.write(step_summary(stats))
|
|
220
|
+
print(f"{stats['total']} proxies written to {out}.")
|
|
221
|
+
return 0
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
225
|
+
p = argparse.ArgumentParser(description="prepare the hits of a run for the proxy-list branch")
|
|
226
|
+
p.add_argument("run_dir", type=Path)
|
|
227
|
+
p.add_argument("out_dir", type=Path)
|
|
228
|
+
p.add_argument("--min", type=int, default=20, help="at least this many hits, otherwise write nothing")
|
|
229
|
+
p.add_argument("--history", type=Path, help="history.json from last time (for the chart on the website)")
|
|
230
|
+
p.add_argument("--streaks", type=Path, help="streaks.json from last time (how long each proxy has been listed)")
|
|
231
|
+
args = p.parse_args(argv)
|
|
232
|
+
return publish(args.run_dir, args.out_dir, args.min, history=args.history, streaks=args.streaks)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
sys.exit(main())
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Local rotating proxy server (--serve).
|
|
2
|
+
|
|
3
|
+
pool.py which proxies there are and which one comes next
|
|
4
|
+
http.py take HTTP requests apart and rebuild them, check upstream responses
|
|
5
|
+
upstream.py connect through a proxy from the pool
|
|
6
|
+
core.py the server itself: accept clients, relay, switch on errors
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .core import FIRST_CHUNK_WAIT, MAX_ATTEMPTS, MAX_REPLAY_BODY, RequestLog, RotatingServer, ServerStats
|
|
10
|
+
from .http import (
|
|
11
|
+
PROXY_AUTH_REQUIRED,
|
|
12
|
+
SCREEN_LIMIT,
|
|
13
|
+
ResponseScreen,
|
|
14
|
+
forward_request,
|
|
15
|
+
origin_request,
|
|
16
|
+
parse_request_head,
|
|
17
|
+
plausible_answer,
|
|
18
|
+
)
|
|
19
|
+
from .pool import DISABLE_AFTER, PoolEntry, ProxyPool
|
|
20
|
+
from .upstream import UpstreamError, open_upstream
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"DISABLE_AFTER",
|
|
24
|
+
"FIRST_CHUNK_WAIT",
|
|
25
|
+
"MAX_ATTEMPTS",
|
|
26
|
+
"MAX_REPLAY_BODY",
|
|
27
|
+
"PROXY_AUTH_REQUIRED",
|
|
28
|
+
"SCREEN_LIMIT",
|
|
29
|
+
"PoolEntry",
|
|
30
|
+
"ProxyPool",
|
|
31
|
+
"RequestLog",
|
|
32
|
+
"ResponseScreen",
|
|
33
|
+
"RotatingServer",
|
|
34
|
+
"ServerStats",
|
|
35
|
+
"UpstreamError",
|
|
36
|
+
"forward_request",
|
|
37
|
+
"open_upstream",
|
|
38
|
+
"origin_request",
|
|
39
|
+
"parse_request_head",
|
|
40
|
+
"plausible_answer",
|
|
41
|
+
]
|