proxy-scraper-cli 1.11.0__tar.gz → 1.12.0__tar.gz
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.11.0 → proxy_scraper_cli-1.12.0}/PKG-INFO +1 -1
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/PKG-INFO +1 -1
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/SOURCES.txt +1 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/__init__.py +1 -1
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/checker.py +4 -0
- proxy_scraper_cli-1.12.0/proxyscraper/runsteps.py +74 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/index.html +18 -2
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/sites.py +14 -30
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/speed.py +14 -25
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/LICENSE +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/README.md +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/dependency_links.txt +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/entry_points.txt +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/requires.txt +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/top_level.txt +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/__main__.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/action.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/agent.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/api.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/app.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/asndb.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/blocklist.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/charts.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/cli.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/compat.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/completion.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/exporters.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/fetchcache.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/geo.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/geodb.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/handshake.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/history.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/judges.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/mcp_entry.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/mcp_server.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/netio.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/options.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/output.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/pages.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/parsing.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/paths.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/pipeline.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/preferences.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/proxypages.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/publish.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/report.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/__init__.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/core.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/http.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/pool.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/socks.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/status.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/server/upstream.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/apple-touch-icon.png +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/googleaac1161b7853c5b5.html +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/logo.png +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/og.png +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/sources.json +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/sources.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/targets.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/__init__.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/dashboard.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/keys.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/report.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/serve.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/widgets.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/ui/wizard.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/pyproject.toml +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/setup.cfg +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_action.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_agent.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_api.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_app.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_asndb.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_auth.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_blocklist.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_charts.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_checker.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_compat.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_completion.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_discovery_growth.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_exporters.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_fetchcache.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_geodb.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_judges.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_learning.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_live_api.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_mcp_server.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_mine_sources.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_options.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_output_stdout.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_packaging.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_pages.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_parsing.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_parsing_fuzz.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_paths.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_pipeline.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_proxy_pages.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_proxy_sources.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_publish.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_report.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_robustness.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_serve_password.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_serve_refill.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_serve_v2.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_server.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_server_closed_writer.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_server_fixes.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_sites.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_sources_json.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_speed.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_tamper.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_targets.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_ui.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_uptime.py +0 -0
- {proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/tests/test_wizard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxy-scraper-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.12.0
|
|
4
4
|
Summary: Finds free HTTP, SOCKS4 and SOCKS5 proxies that actually work: 700+ sources, real checks, honeypot filtering and a rotating local proxy server.
|
|
5
5
|
Author: Maximilian Feix
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxy-scraper-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.12.0
|
|
4
4
|
Summary: Finds free HTTP, SOCKS4 and SOCKS5 proxies that actually work: 700+ sources, real checks, honeypot filtering and a rotating local proxy server.
|
|
5
5
|
Author: Maximilian Feix
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""proxy-scraper: collects public HTTP/SOCKS4/SOCKS5 proxies from hundreds of sources, checks them
|
|
2
2
|
in parallel with its own protocol handshakes and learns which sources deliver good proxies."""
|
|
3
3
|
|
|
4
|
-
__version__ = "1.
|
|
4
|
+
__version__ = "1.12.0"
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def __getattr__(name):
|
|
@@ -334,6 +334,10 @@ class Checker:
|
|
|
334
334
|
writer.close()
|
|
335
335
|
return 200 <= status < 400
|
|
336
336
|
|
|
337
|
+
async def tls_tunnel(self, ptype: str, proxy: str, host: str, ip: str, port: int = 443):
|
|
338
|
+
"""Public side of _tls_tunnel for the steps after a run (sites, speed): ip as text."""
|
|
339
|
+
return await self._tls_tunnel(ptype, proxy, host, socket.inet_aton(ip), port)
|
|
340
|
+
|
|
337
341
|
async def _tls_tunnel(self, ptype: str, proxy: str, host: str, ip_bytes: bytes, port: int):
|
|
338
342
|
"""Tunnel through the proxy to host:port with verified TLS inside. None = tunnel refused or MITM."""
|
|
339
343
|
loop = asyncio.get_running_loop()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Shared plumbing for the extra steps that run on a finished run's proxies.json in the hourly workflow –
|
|
2
|
+
which sites let a proxy through (sites.py), how fast it downloads (speed.py).
|
|
3
|
+
|
|
4
|
+
Each step loads the run's rows, tries the HTTPS-capable proxies through a verified TLS tunnel with a limit on how
|
|
5
|
+
many at once, and writes the file back in one go: if the workflow kills the step at its timeout, the publish step
|
|
6
|
+
still finds the old file intact.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import socket
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Awaitable, Callable, Iterable, List, Tuple
|
|
16
|
+
|
|
17
|
+
from .paths import atomic_write
|
|
18
|
+
|
|
19
|
+
# what a probe through a free proxy can run into – all of it just means "no answer from this one"
|
|
20
|
+
PROBE_ERRORS = (OSError, asyncio.TimeoutError, asyncio.IncompleteReadError, asyncio.LimitOverrunError, ValueError)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_rows(run_dir: Path) -> Tuple[Path, List[dict]]:
|
|
24
|
+
path = run_dir / "proxies.json"
|
|
25
|
+
return path, json.loads(path.read_text(encoding="utf-8"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def save_rows(path: Path, rows: List[dict]) -> None:
|
|
29
|
+
atomic_write(path, json.dumps(rows, indent=1, ensure_ascii=False))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def https_rows(rows: Iterable[dict]) -> List[dict]:
|
|
33
|
+
"""Only these can tunnel to an https:// site – the others failed the HTTPS test in the run."""
|
|
34
|
+
return [r for r in rows if r.get("https") is True]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_ipv4(host: str, port: int = 443) -> str:
|
|
38
|
+
"""Looked up once per step: SOCKS4 needs the IP, and a thousand lookups of the same host help nobody."""
|
|
39
|
+
return socket.getaddrinfo(host, port, family=socket.AF_INET)[0][4][0]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def run_limited(jobs: Iterable[Callable[[], Awaitable[None]]], concurrency: int) -> None:
|
|
43
|
+
"""Run the jobs with at most `concurrency` at a time."""
|
|
44
|
+
slots = asyncio.Semaphore(concurrency)
|
|
45
|
+
|
|
46
|
+
async def one(job: Callable[[], Awaitable[None]]) -> None:
|
|
47
|
+
async with slots:
|
|
48
|
+
await job()
|
|
49
|
+
|
|
50
|
+
await asyncio.gather(*(one(job) for job in jobs))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Tunnels:
|
|
54
|
+
"""Verified TLS tunnels to a host through a proxy – the same handshake code the checks use."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, timeout: float):
|
|
57
|
+
from .checker import Checker # only here: the checker pulls in more than these steps need
|
|
58
|
+
|
|
59
|
+
self.checker = Checker(judge_ip="127.0.0.1", own_ips=(), timeout=timeout, connect_timeout=timeout / 2)
|
|
60
|
+
|
|
61
|
+
async def open(self, row: dict, host: str, ip: str, port: int = 443):
|
|
62
|
+
"""(reader, writer) of the TLS connection, or None if the proxy refused the tunnel or broke up the TLS."""
|
|
63
|
+
return await self.checker.tls_tunnel(row["ptype"], row["proxy"], host, ip, port)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def browser_get(host: str, path: str, *extra: str) -> bytes:
|
|
67
|
+
"""A plain GET the way a browser would send it (so sites answer as they would to a person)."""
|
|
68
|
+
from .netio import USER_AGENT
|
|
69
|
+
|
|
70
|
+
lines = [f"GET {path} HTTP/1.1", f"Host: {host}", f"User-Agent: {USER_AGENT}",
|
|
71
|
+
"Accept: text/html,application/xhtml+xml,*/*;q=0.8", "Accept-Language: en-US,en;q=0.8", *extra,
|
|
72
|
+
"Connection: close"]
|
|
73
|
+
return ("\r\n".join(lines) + "\r\n\r\n").encode()
|
|
74
|
+
|
|
@@ -341,6 +341,8 @@
|
|
|
341
341
|
.map-wait { display: grid; place-items: center; height: 100%; margin: 0; }
|
|
342
342
|
.status .next { color: var(--muted); }
|
|
343
343
|
.status .next:not(:empty)::before { content: " · "; }
|
|
344
|
+
.bookmarklet .btn { cursor: grab; margin: 6px 0 4px; }
|
|
345
|
+
.link-btn { font: inherit; color: var(--text); background: none; border: 0; padding: 0; text-decoration: underline; cursor: pointer; }
|
|
344
346
|
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 22px; padding: clamp(22px, 3vw, 34px); }
|
|
345
347
|
|
|
346
348
|
/* For AI agents: a real Claude Code exchange, revealed line by line once it scrolls in */
|
|
@@ -379,8 +381,10 @@
|
|
|
379
381
|
.swatch { width: 16px; height: 3px; border-radius: 2px; background: var(--signal-ink); }
|
|
380
382
|
.swatch.dashed { background: repeating-linear-gradient(90deg, var(--muted) 0 4px, transparent 4px 7px); }
|
|
381
383
|
.bars { display: grid; gap: 12px; }
|
|
382
|
-
.bar-row { display: grid; grid-template-columns: 76px minmax(
|
|
383
|
-
.
|
|
384
|
+
.bar-row { display: grid; grid-template-columns: minmax(76px, max-content) minmax(40px, 1fr) 44px; align-items: center; gap: 12px; font-size: 14px; }
|
|
385
|
+
.bars { display: grid; grid-template-columns: minmax(76px, max-content) minmax(40px, 1fr) 44px; gap: 10px 12px; }
|
|
386
|
+
.bars .bar-row { display: contents; } /* one grid for all rows: the labels share a column as wide as the longest */
|
|
387
|
+
.bar-row .k { color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
|
|
384
388
|
.bar-row .v { text-align: right; font-variant-numeric: tabular-nums; }
|
|
385
389
|
.bar { height: 10px; border-radius: 99px; background: var(--raised); overflow: hidden; }
|
|
386
390
|
.bar i { display: block; height: 100%; width: 0; border-radius: 99px; background: var(--text); transition: width 1.2s var(--ease); }
|
|
@@ -703,6 +707,9 @@
|
|
|
703
707
|
proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button" data-copy-pre aria-label="Copy the install commands"><svg aria-hidden="true"><use href="#i-copy"/></svg></button></div></div>
|
|
704
708
|
<div class="panel"><h3>Recheck from your network</h3><p>Starts from this list and checks it again from where you are. Takes about 30 seconds, then serves a rotating proxy on port 8899.</p>
|
|
705
709
|
<div class="cmd"><pre>proxy-scraper --recheck live --serve</pre><button class="icon-btn" type="button" data-copy-pre aria-label="Copy the recheck command"><svg aria-hidden="true"><use href="#i-copy"/></svg></button></div></div>
|
|
710
|
+
<div class="panel bookmarklet"><h3>One click, on any site</h3><p>Drag this button to your bookmarks bar. Clicking it copies a proxy that worked in 90 %+ of this week's checks – on whatever page you're on, nothing to install.</p>
|
|
711
|
+
<a class="btn primary" id="bookmarklet" href="javascript:(async()=>{const%20b="https://maximilianfeix.github.io/proxy-scraper/",g=async%20f=>{const%20r=await%20fetch(b+f,{cache:"no-cache"});if(!r.ok)throw%200;return(await%20r.text()).split("%5Cn").filter(l=>/%5E[a-z0-9]+:%5C/%5C/%5CS+%24/.test(l))};try{let%20l=await%20g("stable.txt");if(!l.length)l=(await%20g("https.txt")).slice(0,30);if(!l.length)throw%200;const%20p=l[Math.floor(Math.random()*l.length)];try{await%20navigator.clipboard.writeText(p);alert("Copied%20a%20working%20proxy:%5Cn"+p)}catch(e){prompt("A%20working%20proxy%20%E2%80%93%20copy%20it:",p)}}catch(e){alert("proxy-scraper:%20the%20list%20could%20not%20be%20loaded")}})()" title="Drag me to your bookmarks bar">Get a proxy</a>
|
|
712
|
+
<p class="note">Or right here: <button class="link-btn" type="button" id="pick-one">copy a reliable one now</button>.</p></div>
|
|
706
713
|
</div>
|
|
707
714
|
<div class="panel"><h3>Plain lists</h3><p>One proxy per line, fastest first, updated with every run.</p><div class="raw" id="raw"></div></div>
|
|
708
715
|
</div>
|
|
@@ -1024,6 +1031,15 @@ proxy-scraper --want 50 --https-only</pre><button class="icon-btn" type="button"
|
|
|
1024
1031
|
}
|
|
1025
1032
|
loadView();
|
|
1026
1033
|
$("#share").onclick = () => copy(new URL(viewUrl(), location.href).href, "Link to this view copied");
|
|
1034
|
+
// the same as the bookmarklet, without the round trip: a random proxy from the reliable ones in the list
|
|
1035
|
+
$("#pick-one").onclick = () => {
|
|
1036
|
+
const pool = state.rows.filter(r => (r.uptime_7d || 0) >= 90); // exactly what stable.txt holds
|
|
1037
|
+
const from = pool.length ? pool : state.rows.filter(r => r.https).slice(0, 30);
|
|
1038
|
+
if (!from.length) return toast("The list isn't loaded yet");
|
|
1039
|
+
const p = from[Math.floor(Math.random() * from.length)];
|
|
1040
|
+
copy(p.url, `Copied ${p.url}`);
|
|
1041
|
+
};
|
|
1042
|
+
$("#bookmarklet").onclick = (e) => { e.preventDefault(); toast("Drag it to your bookmarks bar – it works on any site from there"); };
|
|
1027
1043
|
// chips, the country and sorting let the rows settle in; typing and the slider stay instant
|
|
1028
1044
|
const morph = (fn) => {
|
|
1029
1045
|
fn();
|
|
@@ -12,15 +12,12 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
14
14
|
import asyncio
|
|
15
|
-
import json
|
|
16
|
-
import socket
|
|
17
15
|
import sys
|
|
18
16
|
from dataclasses import dataclass
|
|
19
17
|
from pathlib import Path
|
|
20
18
|
from typing import Awaitable, Callable, Dict, List, Optional, Tuple
|
|
21
19
|
|
|
22
|
-
from .
|
|
23
|
-
from .paths import atomic_write
|
|
20
|
+
from .runsteps import PROBE_ERRORS, Tunnels, browser_get, https_rows, load_rows, resolve_ipv4, run_limited, save_rows
|
|
24
21
|
|
|
25
22
|
TIMEOUT = 10.0
|
|
26
23
|
CONCURRENCY = 300
|
|
@@ -113,20 +110,16 @@ def page_complete(head: bytes, body: int, tail: bytes) -> bool:
|
|
|
113
110
|
|
|
114
111
|
def real_probe(timeout: float = TIMEOUT) -> Probe:
|
|
115
112
|
"""Tunnel to the site through the proxy with verified TLS, send a browser-like GET, judge the answer."""
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
checker = Checker(judge_ip="127.0.0.1", own_ips=(), timeout=timeout, connect_timeout=timeout / 2)
|
|
113
|
+
tunnels = Tunnels(timeout)
|
|
119
114
|
|
|
120
115
|
async def probe(row: dict, site: Site, ip: str) -> Optional[bool]:
|
|
121
116
|
async def go() -> Optional[bool]:
|
|
122
|
-
opened = await
|
|
117
|
+
opened = await tunnels.open(row, site.host, ip)
|
|
123
118
|
if opened is None:
|
|
124
119
|
return None
|
|
125
120
|
reader, writer = opened
|
|
126
121
|
try:
|
|
127
|
-
writer.write(
|
|
128
|
-
"Accept: text/html,application/xhtml+xml,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\n"
|
|
129
|
-
"Connection: close\r\n\r\n".encode())
|
|
122
|
+
writer.write(browser_get(site.host, site.path))
|
|
130
123
|
await writer.drain()
|
|
131
124
|
head = await reader.readuntil(b"\r\n\r\n")
|
|
132
125
|
status, location = parse_head(head)
|
|
@@ -145,46 +138,37 @@ def real_probe(timeout: float = TIMEOUT) -> Probe:
|
|
|
145
138
|
return site.verdict(status, location, body)
|
|
146
139
|
try:
|
|
147
140
|
return await asyncio.wait_for(go(), probe_timeout(site, timeout))
|
|
148
|
-
except
|
|
141
|
+
except PROBE_ERRORS:
|
|
149
142
|
return None
|
|
150
143
|
return probe
|
|
151
144
|
|
|
152
145
|
|
|
153
|
-
def
|
|
154
|
-
return socket.getaddrinfo(host, 443, family=socket.AF_INET)[0][4][0]
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
async def fill_run(run_dir: Path, probe: Optional[Probe] = None, resolve: Callable[[str], str] = _resolve,
|
|
146
|
+
async def fill_run(run_dir: Path, probe: Optional[Probe] = None, resolve: Callable[[str], str] = resolve_ipv4,
|
|
158
147
|
concurrency: int = CONCURRENCY) -> Dict[str, int]:
|
|
159
148
|
"""Probe every HTTPS-capable proxy of a run against every site; writes "sites" into its proxies.json.
|
|
160
149
|
Returns how many proxies got through to each site."""
|
|
161
150
|
probe = probe or real_probe()
|
|
162
|
-
path = run_dir
|
|
163
|
-
rows: List[dict] = json.loads(path.read_text(encoding="utf-8"))
|
|
151
|
+
path, rows = load_rows(run_dir)
|
|
164
152
|
ips = {}
|
|
165
153
|
for site in SITES:
|
|
166
154
|
try:
|
|
167
155
|
ips[site.name] = resolve(site.host)
|
|
168
156
|
except OSError:
|
|
169
157
|
continue # this site can't be looked up right now – the others still can
|
|
170
|
-
slots = asyncio.Semaphore(concurrency)
|
|
171
158
|
|
|
172
|
-
|
|
173
|
-
async
|
|
159
|
+
def job(row: dict, site: Site):
|
|
160
|
+
async def run() -> None:
|
|
174
161
|
verdict = await probe(row, site, ips[site.name])
|
|
175
|
-
|
|
176
|
-
|
|
162
|
+
if verdict is not None:
|
|
163
|
+
row["sites"][site.name] = verdict
|
|
164
|
+
return run
|
|
177
165
|
|
|
178
|
-
jobs = []
|
|
179
166
|
for row in rows:
|
|
180
167
|
row["sites"] = {}
|
|
181
|
-
|
|
182
|
-
jobs += [one(row, site) for site in SITES if site.name in ips]
|
|
183
|
-
await asyncio.gather(*jobs)
|
|
168
|
+
await run_limited((job(row, site) for row in https_rows(rows) for site in SITES if site.name in ips), concurrency)
|
|
184
169
|
for row in rows: # always in the same order, whichever answer came first
|
|
185
170
|
row["sites"] = {s.name: row["sites"][s.name] for s in SITES if s.name in row["sites"]}
|
|
186
|
-
|
|
187
|
-
atomic_write(path, json.dumps(rows, indent=1, ensure_ascii=False))
|
|
171
|
+
save_rows(path, rows)
|
|
188
172
|
return {s.name: sum(1 for r in rows if r["sites"].get(s.name)) for s in SITES}
|
|
189
173
|
|
|
190
174
|
|
|
@@ -13,16 +13,13 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import argparse
|
|
15
15
|
import asyncio
|
|
16
|
-
import json
|
|
17
|
-
import socket
|
|
18
16
|
import statistics
|
|
19
17
|
import sys
|
|
20
18
|
import time
|
|
21
19
|
from pathlib import Path
|
|
22
20
|
from typing import Awaitable, Callable, List, Optional
|
|
23
21
|
|
|
24
|
-
from .
|
|
25
|
-
from .paths import atomic_write
|
|
22
|
+
from .runsteps import PROBE_ERRORS, Tunnels, browser_get, https_rows, load_rows, resolve_ipv4, run_limited, save_rows
|
|
26
23
|
|
|
27
24
|
HOST = "speed.cloudflare.com"
|
|
28
25
|
BYTES = 100_000
|
|
@@ -42,9 +39,7 @@ def kbps(received: int, seconds: float) -> Optional[int]:
|
|
|
42
39
|
|
|
43
40
|
|
|
44
41
|
def real_probe(deadline: float = DEADLINE) -> Probe:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
checker = Checker(judge_ip="127.0.0.1", own_ips=(), timeout=deadline, connect_timeout=deadline / 2)
|
|
42
|
+
tunnels = Tunnels(deadline)
|
|
48
43
|
|
|
49
44
|
async def probe(row: dict, ip: str) -> Optional[int]:
|
|
50
45
|
end = time.monotonic() + deadline
|
|
@@ -53,7 +48,7 @@ def real_probe(deadline: float = DEADLINE) -> Probe:
|
|
|
53
48
|
return await asyncio.wait_for(coro, max(0.01, end - time.monotonic()))
|
|
54
49
|
|
|
55
50
|
try:
|
|
56
|
-
opened = await within(
|
|
51
|
+
opened = await within(tunnels.open(row, HOST, ip))
|
|
57
52
|
if opened is None:
|
|
58
53
|
return None
|
|
59
54
|
reader, writer = opened
|
|
@@ -61,8 +56,7 @@ def real_probe(deadline: float = DEADLINE) -> Probe:
|
|
|
61
56
|
# the clock runs from the request to the last byte: a start at the first body byte would miss
|
|
62
57
|
# whatever already sat in the buffer with the head, and fast proxies would come out absurdly fast
|
|
63
58
|
sent = time.monotonic()
|
|
64
|
-
writer.write(
|
|
65
|
-
"Accept-Encoding: identity\r\nConnection: close\r\n\r\n".encode())
|
|
59
|
+
writer.write(browser_get(HOST, PATH, "Accept-Encoding: identity"))
|
|
66
60
|
await within(writer.drain())
|
|
67
61
|
head = await within(reader.readuntil(b"\r\n\r\n"))
|
|
68
62
|
if b" 200 " not in head.split(b"\r\n", 1)[0] + b" ":
|
|
@@ -79,38 +73,33 @@ def real_probe(deadline: float = DEADLINE) -> Probe:
|
|
|
79
73
|
return kbps(received, time.monotonic() - sent)
|
|
80
74
|
finally:
|
|
81
75
|
writer.close()
|
|
82
|
-
except
|
|
76
|
+
except PROBE_ERRORS:
|
|
83
77
|
return None
|
|
84
78
|
return probe
|
|
85
79
|
|
|
86
80
|
|
|
87
|
-
def
|
|
88
|
-
return socket.getaddrinfo(host, 443, family=socket.AF_INET)[0][4][0]
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
async def fill_run(run_dir: Path, probe: Optional[Probe] = None, resolve: Callable[[str], str] = _resolve,
|
|
81
|
+
async def fill_run(run_dir: Path, probe: Optional[Probe] = None, resolve: Callable[[str], str] = resolve_ipv4,
|
|
92
82
|
concurrency: int = CONCURRENCY) -> Optional[int]:
|
|
93
83
|
"""Measure every HTTPS-capable proxy of a run, write speed_kbps into its proxies.json -> the median."""
|
|
94
84
|
probe = probe or real_probe()
|
|
95
|
-
path = run_dir
|
|
96
|
-
rows: List[dict] = json.loads(path.read_text(encoding="utf-8"))
|
|
85
|
+
path, rows = load_rows(run_dir)
|
|
97
86
|
try:
|
|
98
87
|
ip = resolve(HOST)
|
|
99
88
|
except OSError:
|
|
100
89
|
return None
|
|
101
|
-
slots = asyncio.Semaphore(concurrency)
|
|
102
90
|
|
|
103
|
-
|
|
104
|
-
async
|
|
91
|
+
def job(row: dict):
|
|
92
|
+
async def run() -> None:
|
|
105
93
|
try: # the probe keeps its own deadline – this only catches one that doesn't
|
|
106
94
|
value = await asyncio.wait_for(probe(row, ip), DEADLINE * 1.5)
|
|
107
95
|
except asyncio.TimeoutError:
|
|
108
96
|
value = None
|
|
109
|
-
|
|
110
|
-
|
|
97
|
+
if value:
|
|
98
|
+
row["speed_kbps"] = value
|
|
99
|
+
return run
|
|
111
100
|
|
|
112
|
-
await
|
|
113
|
-
|
|
101
|
+
await run_limited((job(r) for r in https_rows(rows)), concurrency)
|
|
102
|
+
save_rows(path, rows)
|
|
114
103
|
speeds = [r["speed_kbps"] for r in rows if r.get("speed_kbps")]
|
|
115
104
|
return round(statistics.median(speeds)) if speeds else None
|
|
116
105
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxy_scraper_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/apple-touch-icon.png
RENAMED
|
File without changes
|
{proxy_scraper_cli-1.11.0 → proxy_scraper_cli-1.12.0}/proxyscraper/site/googleaac1161b7853c5b5.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|