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.
Files changed (57) hide show
  1. proxy_scraper_cli-1.7.1.dist-info/METADATA +794 -0
  2. proxy_scraper_cli-1.7.1.dist-info/RECORD +57 -0
  3. proxy_scraper_cli-1.7.1.dist-info/WHEEL +5 -0
  4. proxy_scraper_cli-1.7.1.dist-info/entry_points.txt +4 -0
  5. proxy_scraper_cli-1.7.1.dist-info/licenses/LICENSE +21 -0
  6. proxy_scraper_cli-1.7.1.dist-info/top_level.txt +1 -0
  7. proxyscraper/__init__.py +12 -0
  8. proxyscraper/__main__.py +7 -0
  9. proxyscraper/agent.py +635 -0
  10. proxyscraper/api.py +139 -0
  11. proxyscraper/app.py +595 -0
  12. proxyscraper/asndb.py +181 -0
  13. proxyscraper/blocklist.py +92 -0
  14. proxyscraper/checker.py +514 -0
  15. proxyscraper/cli.py +269 -0
  16. proxyscraper/compat.py +83 -0
  17. proxyscraper/completion.py +196 -0
  18. proxyscraper/exporters.py +118 -0
  19. proxyscraper/fetchcache.py +103 -0
  20. proxyscraper/geo.py +150 -0
  21. proxyscraper/geodb.py +143 -0
  22. proxyscraper/handshake.py +153 -0
  23. proxyscraper/history.py +106 -0
  24. proxyscraper/judges.py +159 -0
  25. proxyscraper/mcp_entry.py +34 -0
  26. proxyscraper/mcp_server.py +220 -0
  27. proxyscraper/netio.py +167 -0
  28. proxyscraper/options.py +274 -0
  29. proxyscraper/output.py +181 -0
  30. proxyscraper/pages.py +212 -0
  31. proxyscraper/parsing.py +192 -0
  32. proxyscraper/paths.py +55 -0
  33. proxyscraper/pipeline.py +434 -0
  34. proxyscraper/preferences.py +24 -0
  35. proxyscraper/publish.py +236 -0
  36. proxyscraper/server/__init__.py +41 -0
  37. proxyscraper/server/core.py +518 -0
  38. proxyscraper/server/http.py +164 -0
  39. proxyscraper/server/pool.py +195 -0
  40. proxyscraper/server/socks.py +65 -0
  41. proxyscraper/server/status.py +108 -0
  42. proxyscraper/server/upstream.py +119 -0
  43. proxyscraper/site/apple-touch-icon.png +0 -0
  44. proxyscraper/site/googleaac1161b7853c5b5.html +1 -0
  45. proxyscraper/site/index.html +934 -0
  46. proxyscraper/site/logo.png +0 -0
  47. proxyscraper/site/og.png +0 -0
  48. proxyscraper/sources.json +395 -0
  49. proxyscraper/sources.py +491 -0
  50. proxyscraper/targets.py +76 -0
  51. proxyscraper/ui/__init__.py +54 -0
  52. proxyscraper/ui/dashboard.py +344 -0
  53. proxyscraper/ui/keys.py +82 -0
  54. proxyscraper/ui/report.py +209 -0
  55. proxyscraper/ui/serve.py +142 -0
  56. proxyscraper/ui/widgets.py +250 -0
  57. proxyscraper/ui/wizard.py +595 -0
@@ -0,0 +1,934 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>proxy-scraper – free proxies that actually work</title>
7
+ <meta name="google-site-verification" content="dpSxu8MDNzVv2DGLn2hf8ShwV6xV29y8Pk-GP6HLRlo">
8
+ <meta name="description" content="Free HTTP, SOCKS4 and SOCKS5 proxies that actually work. Re-checked every hour, honeypots and proxies that tamper with content removed.">
9
+ <meta name="theme-color" content="#121113">
10
+ <link rel="canonical" href="https://maximilianfeix.github.io/proxy-scraper/">
11
+ <meta property="og:type" content="website">
12
+ <meta property="og:url" content="https://maximilianfeix.github.io/proxy-scraper/">
13
+ <meta property="og:site_name" content="proxy-scraper">
14
+ <meta property="og:title" content="proxy-scraper – free proxies that actually work">
15
+ <meta property="og:image" content="https://maximilianfeix.github.io/proxy-scraper/og.png">
16
+ <meta property="og:image:width" content="1280">
17
+ <meta property="og:image:height" content="640">
18
+ <meta property="og:image:alt" content="proxy-scraper logo and the line: free proxies that actually work">
19
+ <meta name="twitter:card" content="summary_large_image">
20
+ <meta name="twitter:image" content="https://maximilianfeix.github.io/proxy-scraper/og.png">
21
+ <link rel="apple-touch-icon" href="apple-touch-icon.png">
22
+ <meta property="og:description" content="Every proxy on this list passed a real handshake, a honeypot check and a content check. Re-checked every hour.">
23
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='18' fill='%23D4F77A'/%3E%3Cpath d='M13 31.5 26.5 44.5 51 18' fill='none' stroke='%23121113' stroke-width='5.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='13' cy='31.5' r='4.6' fill='%23121113'/%3E%3Ccircle cx='51' cy='18' r='4.6' fill='%23121113'/%3E%3Ccircle cx='26.5' cy='44.5' r='7.6' fill='%23D4F77A' stroke='%23121113' stroke-width='5'/%3E%3C/svg%3E">
24
+ <script type="application/ld+json">
25
+ {"@context": "https://schema.org", "@type": "Dataset",
26
+ "name": "Free proxy list, checked every hour",
27
+ "description": "HTTP, SOCKS4 and SOCKS5 proxies from 700+ public lists that passed a real handshake, a honeypot check on two sites and a content check. With country, provider, latency, anonymity and HTTPS support.",
28
+ "url": "https://maximilianfeix.github.io/proxy-scraper/",
29
+ "license": "https://opensource.org/licenses/MIT",
30
+ "isAccessibleForFree": true,
31
+ "creator": {"@type": "Person", "name": "maximilianfeix", "url": "https://github.com/maximilianfeix"},
32
+ "keywords": ["free proxy list", "socks5 proxies", "http proxies", "socks4 proxies", "proxy checker"],
33
+ "distribution": [
34
+ {"@type": "DataDownload", "encodingFormat": "application/json", "contentUrl": "https://maximilianfeix.github.io/proxy-scraper/proxies.json"},
35
+ {"@type": "DataDownload", "encodingFormat": "text/csv", "contentUrl": "https://maximilianfeix.github.io/proxy-scraper/proxies.csv"},
36
+ {"@type": "DataDownload", "encodingFormat": "text/plain", "contentUrl": "https://maximilianfeix.github.io/proxy-scraper/all.txt"}]}
37
+ </script>
38
+ <link rel="preconnect" href="https://fonts.googleapis.com">
39
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
40
+ <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400..700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
41
+ <script>
42
+ // before the first paint: set the theme and decide whether the intro plays (once per session)
43
+ (function () {
44
+ var html = document.documentElement, intro = false;
45
+ html.classList.add("js");
46
+ // saved choice first, otherwise like the system (dark if it doesn't say)
47
+ var t = null;
48
+ try { t = localStorage.getItem("theme"); } catch (e) {}
49
+ html.dataset.theme = t || (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
50
+ try {
51
+ intro = !sessionStorage.getItem("seen") && !matchMedia("(prefers-reduced-motion: reduce)").matches;
52
+ sessionStorage.setItem("seen", "1");
53
+ } catch (e) {}
54
+ if (intro) html.classList.add("intro");
55
+ // independent of the rest of the script: the page becomes visible in any case
56
+ var ready = function () { html.classList.add("ready"); };
57
+ if (intro) setTimeout(ready, 1500);
58
+ else document.addEventListener("DOMContentLoaded", function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); });
59
+ })();
60
+ </script>
61
+ <style>
62
+ :root {
63
+ --bg: #121113; --panel: #1A191C; --raised: #232226; --line: #2B292F; --text: #EDEBE6; --muted: #9A97A0;
64
+ --signal: #D4F77A; --signal-ink: #D4F77A; --on-signal: #121113; --control: #6B6872;
65
+ --http: #8DB8FF; --socks4: #C7A6FF; --socks5: #D4F77A; --warn: #F5C46B; --bad: #FF8C7A;
66
+ --ease: cubic-bezier(.16, 1, .3, 1); --ease-io: cubic-bezier(.76, 0, .24, 1);
67
+ --wrap: 1320px; --gutter: clamp(16px, 4vw, 48px); --nav-h: 68px;
68
+ color-scheme: dark;
69
+ }
70
+ :root[data-theme="light"] {
71
+ --bg: #EFEFEC; --panel: #FFFFFF; --raised: #F6F6F3; --line: #DAD9D4; --text: #121113; --muted: #5F5C66;
72
+ --signal: #D4F77A; --signal-ink: #3F5A00; --on-signal: #121113; --control: #817F79;
73
+ --http: #1F5FD1; --socks4: #7442D6; --socks5: #3F5A00; --warn: #9A5B00; --bad: #C23A26;
74
+ color-scheme: light;
75
+ }
76
+ * { box-sizing: border-box; }
77
+ html { -webkit-text-size-adjust: 100%; }
78
+ html:not(.lenis) { scroll-behavior: smooth; }
79
+ html.lenis, html.lenis body { height: auto; }
80
+ .lenis.lenis-stopped { overflow: hidden; }
81
+ body { margin: 0; background: var(--bg); color: var(--text); font: 400 16px/1.55 "Geist", ui-sans-serif, system-ui, sans-serif;
82
+ -webkit-font-smoothing: antialiased; transition: background-color .5s var(--ease), color .5s var(--ease); overflow-x: hidden; }
83
+ .mono, code, pre { font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace; }
84
+ a { color: inherit; }
85
+ button, input, select { font: inherit; color: inherit; }
86
+ :focus-visible { outline: 2px solid var(--signal-ink); outline-offset: 3px; border-radius: 6px; }
87
+ ::selection { background: var(--signal); color: var(--on-signal); }
88
+ .wrap { max-width: var(--wrap); margin: 0 auto; padding-inline: var(--gutter); }
89
+ .skip { position: fixed; left: 16px; top: 12px; z-index: 200; padding: 10px 16px; border-radius: 99px; background: var(--signal);
90
+ color: var(--on-signal); font-weight: 500; text-decoration: none; transform: translateY(-160%); transition: transform .3s var(--ease); }
91
+ .skip:focus { transform: none; }
92
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
93
+ [id] { scroll-margin-top: calc(var(--nav-h) + 12px); }
94
+ section[tabindex="-1"]:focus { outline: none; }
95
+
96
+ /* Logo */
97
+ .mark { width: 36px; height: 36px; flex: none; display: block; }
98
+
99
+ /* Intro */
100
+ .loader { display: none; }
101
+ html.intro .loader { display: grid; place-items: center; position: fixed; inset: 0; z-index: 100; background: #121113; color: #EDEBE6;
102
+ transition: transform 1s var(--ease-io) .15s; }
103
+ html.intro.ready .loader { transform: translateY(-100%); pointer-events: none; }
104
+ .loader-inner { display: flex; align-items: center; gap: clamp(14px, 2vw, 26px); }
105
+ .loader svg { width: clamp(64px, 9vw, 112px); height: auto; }
106
+ .loader .route { stroke-dasharray: 100; stroke-dashoffset: 100; animation: draw .9s var(--ease) .1s forwards; }
107
+ .loader .node { transform-box: fill-box; transform-origin: center; transform: scale(0); animation: pop .5s var(--ease) forwards; }
108
+ .loader .node.a { animation-delay: .05s; } .loader .node.b { animation-delay: .45s; } .loader .node.c { animation-delay: .85s; }
109
+ .loader-word { font-size: clamp(34px, 6vw, 84px); font-weight: 600; letter-spacing: -.05em; overflow: hidden; }
110
+ .loader-word span { display: inline-block; transform: translateY(105%); animation: rise .9s var(--ease) .35s forwards; }
111
+ @keyframes draw { to { stroke-dashoffset: 0; } }
112
+ @keyframes pop { to { transform: scale(1); } }
113
+ @keyframes rise { to { transform: none; } }
114
+
115
+ /* Navigation */
116
+ .nav { position: fixed; inset: 0 0 auto; z-index: 50; height: var(--nav-h);
117
+ background: color-mix(in srgb, var(--bg) 78%, transparent); backdrop-filter: blur(16px) saturate(1.4); -webkit-backdrop-filter: blur(16px) saturate(1.4);
118
+ border-bottom: 1px solid color-mix(in srgb, var(--line) 70%, transparent); transition: transform .6s var(--ease), background-color .5s; }
119
+ body.nav-hidden .nav { transform: translateY(-100%); }
120
+ body.nav-hidden .nav:focus-within { transform: none; }
121
+ .nav .wrap { height: 100%; display: flex; align-items: center; gap: 28px; }
122
+ .brand { display: flex; align-items: center; gap: 10px; text-decoration: none; font-weight: 600; letter-spacing: -.03em; font-size: 18px; }
123
+ .brand .mark { width: 30px; height: 30px; }
124
+ .nav-links { display: flex; gap: 26px; margin-left: auto; }
125
+ .nav-links a { text-decoration: none; color: var(--muted); font-size: 15px; transition: color .25s; }
126
+ .nav-links a:hover { color: var(--text); }
127
+ .nav-actions { display: flex; align-items: center; gap: 12px; }
128
+ .toggle { position: relative; width: 58px; height: 30px; border-radius: 99px; border: 1px solid var(--control); background: var(--panel); cursor: pointer; padding: 0; }
129
+ .toggle svg { position: absolute; top: 7px; width: 14px; height: 14px; color: var(--muted); }
130
+ .toggle .sun { left: 8px; } .toggle .moon { right: 8px; }
131
+ .toggle::after { content: ""; position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 50%; background: var(--text);
132
+ transition: transform .5s var(--ease); transform: translateX(28px); }
133
+ :root[data-theme="light"] .toggle::after { transform: none; }
134
+ .gh { display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 14px; border-radius: 99px; background: var(--text); color: var(--bg);
135
+ text-decoration: none; font-size: 14px; font-weight: 500; transition: transform .3s var(--ease); }
136
+ .gh:hover { transform: translateY(-1px); }
137
+ .gh svg { width: 16px; height: 16px; }
138
+
139
+ /* Buttons */
140
+ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 10px; height: 52px; padding: 0 26px; border-radius: 99px; cursor: pointer;
141
+ border: 1px solid var(--control); background: transparent; text-decoration: none; font-size: 16px; font-weight: 500; white-space: nowrap;
142
+ transition: background-color .35s var(--ease), border-color .35s, color .35s, transform .35s var(--ease); }
143
+ .btn:hover { border-color: var(--text); }
144
+ .btn:active { transform: scale(.98); }
145
+ .btn.primary { background: var(--signal); border-color: var(--signal); color: var(--on-signal); }
146
+ .btn.primary:hover { background: color-mix(in srgb, var(--signal) 86%, white); }
147
+ .btn.small { height: 40px; padding: 0 18px; font-size: 14px; }
148
+
149
+ /* Hero */
150
+ .hero { min-height: 100svh; display: flex; flex-direction: column; justify-content: flex-end; padding-top: calc(var(--nav-h) + 48px); }
151
+ .headline { margin: 0; font-weight: 600; font-size: clamp(58px, 12.4vw, 204px); line-height: .9; letter-spacing: -.058em; }
152
+ .headline .line { display: block; overflow: hidden; padding-bottom: .06em; margin-bottom: -.06em; }
153
+ .headline .line > span { display: inline-block; }
154
+ html.js .headline .line > span { transform: translateY(108%); }
155
+ html.ready .headline .line > span { transform: none; transition: transform 1.2s var(--ease) calc(var(--i) * 90ms + var(--d, .05s)); }
156
+ html.intro .headline { --d: .75s; }
157
+ .hero-foot { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 32px 48px; align-items: end; margin: clamp(32px, 5vw, 56px) 0 clamp(32px, 5vw, 56px); }
158
+ .lede { margin: 0; max-width: 34ch; font-size: clamp(18px, 1.6vw, 22px); line-height: 1.45; color: var(--muted); }
159
+ .lede strong { color: var(--text); font-weight: 500; }
160
+ .hero-side { display: flex; flex-direction: column; align-items: flex-end; gap: 18px; }
161
+ .status { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 15px; }
162
+ .pulse { width: 9px; height: 9px; border-radius: 50%; background: var(--signal-ink); position: relative; flex: none; }
163
+ .pulse::after { content: ""; position: absolute; inset: -5px; border-radius: 50%; border: 1px solid var(--signal-ink); animation: ping 2.2s var(--ease) infinite; }
164
+ @keyframes ping { from { transform: scale(.4); opacity: 1; } to { transform: scale(1.8); opacity: 0; } }
165
+ .ctas { display: flex; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }
166
+ html.js .hero-foot, html.js .ticker { opacity: 0; transform: translateY(16px); }
167
+ html.ready .hero-foot, html.ready .ticker { opacity: 1; transform: none; transition: opacity 1s var(--ease) calc(var(--d, .05s) + .35s), transform 1s var(--ease) calc(var(--d, .05s) + .35s); }
168
+ html.intro .hero-foot, html.intro .ticker { --d: .75s; }
169
+
170
+ /* Ticker with the fastest proxies */
171
+ .ticker { border-block: 1px solid var(--line); margin-inline: calc(var(--gutter) * -1); display: flex; align-items: center; }
172
+ .ticker-window { flex: 1; min-width: 0; overflow: hidden;
173
+ mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent); -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent); }
174
+ .ticker-track { display: flex; width: max-content; animation: marquee var(--dur, 80s) linear infinite; }
175
+ .ticker:not(.manual):hover .ticker-track, .ticker:not(.manual):focus-within .ticker-track,
176
+ .ticker.paused .ticker-track { animation-play-state: paused; }
177
+ .ticker-toggle { flex: none; margin-inline: 12px var(--gutter); width: 36px; height: 36px;
178
+ border-radius: 50%; border: 1px solid var(--control); background: var(--bg); color: var(--text); cursor: pointer;
179
+ display: grid; place-items: center; }
180
+ .ticker-toggle svg { width: 12px; height: 12px; }
181
+ .ticker-toggle .play, .ticker.paused .ticker-toggle .pause { display: none; }
182
+ .ticker.paused .ticker-toggle .play { display: block; }
183
+ @keyframes marquee { to { transform: translateX(-50%); } }
184
+ .tick { display: inline-flex; align-items: center; gap: 12px; padding: 18px 28px; border: 0; background: none; cursor: pointer; white-space: nowrap;
185
+ border-right: 1px solid var(--line); font-size: 15px; transition: background-color .3s; }
186
+ .tick:hover { background: var(--panel); }
187
+ .tick .addr { font-family: "Geist Mono", ui-monospace, monospace; }
188
+ .tick .meta { color: var(--muted); }
189
+ .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--muted); display: inline-block; }
190
+ .dot.http { background: var(--http); } .dot.socks4 { background: var(--socks4); } .dot.socks5 { background: var(--socks5); }
191
+
192
+ /* Sections */
193
+ section.block { padding-block: clamp(56px, 6.5vw, 104px); }
194
+ .h2 { margin: 0; font-weight: 600; font-size: clamp(40px, 6.6vw, 104px); line-height: .95; letter-spacing: -.05em; max-width: 14ch; }
195
+ .intro-text { margin: 22px 0 0; max-width: 46ch; color: var(--muted); font-size: clamp(17px, 1.4vw, 20px); }
196
+ .head-row { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); align-items: end; gap: 24px 48px; margin-bottom: clamp(40px, 5vw, 72px); }
197
+ .head-row .intro-text { margin: 0; justify-self: end; }
198
+
199
+ /* Big numbers */
200
+ .figures { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(48px, 7vw, 96px) clamp(32px, 5vw, 80px); }
201
+ .fig { border-top: 1px solid var(--line); padding-top: 26px; }
202
+ .fig .big { font-weight: 600; font-size: clamp(64px, 9.5vw, 156px); line-height: .9; letter-spacing: -.06em; font-variant-numeric: tabular-nums; display: flex; align-items: baseline; }
203
+ .fig .unit { font-size: .42em; letter-spacing: -.03em; margin-left: .12em; color: var(--muted); }
204
+ .fig p { margin: 20px 0 0; max-width: 36ch; color: var(--muted); font-size: 17px; }
205
+ .fig p strong { color: var(--text); font-weight: 500; }
206
+
207
+ /* List */
208
+ .filters { position: sticky; top: var(--nav-h); z-index: 20; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; padding: 14px 0;
209
+ background: var(--bg); transition: top .6s var(--ease), background-color .5s; }
210
+ body.nav-hidden .filters { top: 0; }
211
+ .chip { height: 38px; padding: 0 16px; border-radius: 99px; border: 1px solid var(--control); background: transparent; cursor: pointer; font-size: 14px;
212
+ color: var(--muted); display: inline-flex; align-items: center; gap: 8px; transition: background-color .3s var(--ease), color .3s, border-color .3s; }
213
+ .chip:hover { border-color: var(--muted); color: var(--text); }
214
+ .chip[aria-pressed="true"] { background: var(--text); border-color: var(--text); color: var(--bg); }
215
+ .chip[aria-pressed="false"] .dot { opacity: .45; }
216
+ .sep { width: 1px; height: 24px; background: var(--line); margin: 0 4px; }
217
+ .field { height: 38px; border-radius: 99px; border: 1px solid var(--control); background: transparent; padding: 0 16px; font-size: 14px; }
218
+ select.field { padding-right: 34px; appearance: none; cursor: pointer;
219
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5 6 7.5 9 4.5' fill='none' stroke='%239A97A0' stroke-width='1.5'/%3E%3C/svg%3E") right 12px center / 12px no-repeat; }
220
+ select.field option { background: var(--panel); color: var(--text); }
221
+ input.field[type=search] { flex: 1; min-width: 200px; }
222
+ input.field::placeholder { color: var(--muted); }
223
+ .range { display: inline-flex; align-items: center; gap: 10px; font-size: 14px; color: var(--muted); }
224
+ .range input { accent-color: var(--signal-ink); width: 120px; }
225
+ .range output { color: var(--text); min-width: 56px; font-variant-numeric: tabular-nums; }
226
+ .table-panel { position: relative; background: var(--panel); border: 1px solid var(--line); border-radius: 22px; overflow: hidden; margin-top: 8px; }
227
+ .table-scroll { overflow-x: auto; }
228
+ table { width: 100%; border-collapse: collapse; font-size: 15px; }
229
+ th { text-align: left; font-weight: 500; font-size: 13px; color: var(--muted); padding: 0; border-bottom: 1px solid var(--line); white-space: nowrap; }
230
+ .sort { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 16px 18px; width: 100%; text-align: left; }
231
+ .sort:hover, th[aria-sort] .sort { color: var(--text); }
232
+ th[aria-sort] .sort::after { content: ""; display: inline-block; width: 0; height: 0; margin-left: 7px; vertical-align: 2px;
233
+ border: 4px solid transparent; border-top-color: currentColor; border-bottom-width: 0; }
234
+ th[aria-sort="descending"] .sort::after { transform: rotate(180deg); }
235
+ td { padding: 13px 18px; border-bottom: 1px solid var(--line); white-space: nowrap; }
236
+ tbody tr:last-child td { border-bottom: 0; }
237
+ tbody tr { transition: background-color .2s; }
238
+ tbody tr:hover { background: var(--raised); }
239
+ .t-type { display: inline-flex; align-items: center; gap: 9px; }
240
+ .addr-btn { font: 14.5px/1.4 "Geist Mono", ui-monospace, monospace; color: inherit; background: none; border: 0; padding: 4px 6px; margin: -4px -6px;
241
+ border-radius: 8px; cursor: copy; display: inline-flex; align-items: center; gap: 10px; }
242
+ .addr-btn svg { width: 14px; height: 14px; color: var(--muted); opacity: 0; transition: opacity .2s; }
243
+ .addr-btn:hover, .addr-btn:focus-visible { background: var(--line); }
244
+ .addr-btn:hover svg, .addr-btn:focus-visible svg { opacity: 1; }
245
+ .fast { color: var(--signal-ink); } .mid { color: var(--warn); } .slow { color: var(--bad); }
246
+ .quiet { color: var(--muted); }
247
+ .tag { display: inline-block; font-size: 11px; font-weight: 600; line-height: 1; padding: 4px 6px; border-radius: 6px; margin-right: 8px;
248
+ color: var(--warn); background: color-mix(in srgb, var(--warn) 14%, transparent); vertical-align: 1px; }
249
+ .tag.bl { color: var(--bad); background: color-mix(in srgb, var(--bad) 14%, transparent); }
250
+ .stable-badge { color: var(--signal-ink); font-weight: 500; }
251
+ .provider { max-width: 240px; overflow: hidden; text-overflow: ellipsis; color: var(--muted); }
252
+ .icon-btn { width: 34px; height: 34px; border-radius: 10px; border: 0; background: transparent; color: var(--muted); cursor: pointer; display: inline-grid; place-items: center;
253
+ transition: background-color .25s, color .25s; }
254
+ .icon-btn:hover { background: var(--line); color: var(--text); }
255
+ .icon-btn svg { width: 16px; height: 16px; }
256
+ .empty { padding: 64px 24px; text-align: center; color: var(--muted); }
257
+ .list-foot { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; margin-top: 20px; }
258
+ .list-foot .count { color: var(--muted); font-size: 15px; }
259
+ .list-foot .actions { display: flex; gap: 10px; flex-wrap: wrap; }
260
+
261
+ /* Route: how a proxy gets on the list */
262
+ .route { height: 460vh; position: relative; }
263
+ .route-sticky { position: sticky; top: 0; height: 100svh; display: flex; flex-direction: column; justify-content: center; gap: clamp(24px, 4vh, 48px); padding-top: var(--nav-h); }
264
+ .route-title { margin: 0; font-size: clamp(20px, 1.8vw, 26px); font-weight: 500; letter-spacing: -.02em; color: var(--muted); }
265
+ .route-cam { overflow: hidden; margin-inline: calc(var(--gutter) * -1); padding-inline: var(--gutter); }
266
+ .route-svg { width: 100%; height: auto; overflow: visible; display: block; will-change: transform; }
267
+ .route-svg .track { fill: none; stroke: var(--line); stroke-width: 2; }
268
+ .route-svg .done { fill: none; stroke: var(--signal-ink); stroke-width: 3; stroke-linecap: round; }
269
+ .route-svg .gate { fill: var(--bg); stroke: var(--line); stroke-width: 2; transition: fill .5s var(--ease), stroke .5s; }
270
+ .route-svg .gate.on { fill: var(--signal); stroke: var(--signal-ink); }
271
+ .route-svg .gate-label { fill: var(--muted); font-size: 15px; font-family: "Geist", sans-serif; text-anchor: middle; transition: fill .4s; }
272
+ .route-svg .gate-label.on { fill: var(--text); }
273
+ .route-svg .packet { fill: var(--text); }
274
+ .route-svg .halo { fill: var(--signal); opacity: .25; }
275
+ .steps { position: relative; min-height: 14em; }
276
+ .step { position: absolute; inset: 0 auto auto 0; max-width: 820px; opacity: 0; transform: translateY(24px); transition: opacity .6s var(--ease), transform .8s var(--ease); pointer-events: none; }
277
+ .step.on { opacity: 1; transform: none; pointer-events: auto; }
278
+ .step .which { color: var(--muted); font-size: 15px; margin: 0 0 14px; }
279
+ .step h3 { margin: 0; font-weight: 600; font-size: clamp(34px, 5.2vw, 76px); line-height: .98; letter-spacing: -.05em; }
280
+ .step p { margin: 18px 0 0; max-width: 52ch; color: var(--muted); font-size: clamp(17px, 1.4vw, 20px); }
281
+ .step.final h3 { font-size: clamp(48px, 8vw, 128px); }
282
+
283
+ /* Trend, countries, latency */
284
+ .insights { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 20px; }
285
+ .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 22px; padding: clamp(22px, 3vw, 34px); }
286
+ .panel h3 { margin: 0 0 6px; font-size: 20px; font-weight: 600; letter-spacing: -.02em; }
287
+ .panel .note { margin: 0 0 22px; color: var(--muted); font-size: 15px; }
288
+ .trend-panel { grid-row: span 2; display: flex; flex-direction: column; }
289
+ .trend { flex: 1; min-height: 240px; position: relative; }
290
+ .trend svg { width: 100%; height: 100%; display: block; }
291
+ .trend-axis { display: flex; justify-content: space-between; color: var(--muted); font-size: 13px; margin-top: 10px; }
292
+ .legend { display: flex; gap: 20px; flex-wrap: wrap; font-size: 14px; color: var(--muted); margin-top: 16px; }
293
+ .legend span { display: inline-flex; align-items: center; gap: 8px; }
294
+ .swatch { width: 16px; height: 3px; border-radius: 2px; background: var(--signal-ink); }
295
+ .swatch.dashed { background: repeating-linear-gradient(90deg, var(--muted) 0 4px, transparent 4px 7px); }
296
+ .bars { display: grid; gap: 12px; }
297
+ .bar-row { display: grid; grid-template-columns: 76px minmax(0, 1fr) 44px; align-items: center; gap: 12px; font-size: 14px; }
298
+ .bar-row .k { color: var(--muted); }
299
+ .bar-row .v { text-align: right; font-variant-numeric: tabular-nums; }
300
+ .bar { height: 10px; border-radius: 99px; background: var(--raised); overflow: hidden; }
301
+ .bar i { display: block; height: 100%; width: 0; border-radius: 99px; background: var(--text); transition: width 1.2s var(--ease); }
302
+
303
+ /* Use it */
304
+ .use { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 20px; }
305
+ .use-cmds { display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; }
306
+ .use .panel { min-width: 0; }
307
+ .use .panel p { margin: 0 0 22px; color: var(--muted); font-size: 15px; max-width: 52ch; }
308
+ .cmd { display: flex; align-items: flex-start; gap: 8px; background: var(--raised); border: 1px solid var(--line); border-radius: 14px; padding: 12px 8px 12px 16px; }
309
+ .cmd pre { margin: 0; flex: 1; min-width: 0; font-size: 13.5px; line-height: 1.6; white-space: pre; overflow-x: auto; padding-top: 5px; }
310
+ .cmd pre.cmd-wrap { white-space: pre-wrap; overflow-wrap: anywhere; } /* long one-liners: readable before copying */
311
+ .raw { display: grid; gap: 6px; }
312
+ .raw a { display: flex; justify-content: space-between; align-items: center; gap: 12px; text-decoration: none; padding: 10px 14px; border-radius: 12px;
313
+ background: var(--raised); border: 1px solid var(--line); font-size: 14px; transition: border-color .25s; }
314
+ .raw a:hover { border-color: var(--muted); }
315
+ .raw a span:last-child { color: var(--muted); }
316
+ .dev-panel { margin-top: 20px; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 28px; align-items: start; }
317
+ .endpoints { display: grid; gap: 8px; }
318
+ .endpoints a { display: grid; grid-template-columns: 130px minmax(0, 1fr); gap: 12px; text-decoration: none; font-size: 14px;
319
+ padding: 10px 14px; border-radius: 12px; background: var(--raised); border: 1px solid var(--line); }
320
+ .endpoints a:hover { border-color: var(--muted); }
321
+ .endpoints a span:last-child { color: var(--muted); }
322
+ @media (max-width: 1020px) { .dev-panel { grid-template-columns: minmax(0, 1fr); } }
323
+
324
+ /* Footer */
325
+ footer { padding: clamp(56px, 7vw, 104px) 0 36px; border-top: 1px solid var(--line); }
326
+ .wordmark { display: flex; align-items: center; gap: .18em; font-weight: 600; font-size: clamp(38px, 11.2vw, 170px); letter-spacing: -.065em; line-height: .9; white-space: nowrap; }
327
+ .wordmark .mark { width: .8em; height: .8em; }
328
+ .browse { display: grid; gap: 8px; margin-top: clamp(28px, 4vw, 48px); color: var(--muted); font-size: 14px; } /* below the wordmark's descenders */
329
+ .browse p { margin: 0; display: flex; flex-wrap: wrap; gap: 6px 14px; }
330
+ .browse a { color: var(--text); text-decoration: none; }
331
+ .browse a:hover { color: var(--signal-ink); text-decoration: underline; }
332
+ .foot-row { display: flex; justify-content: space-between; gap: 24px 48px; flex-wrap: wrap; margin-top: clamp(40px, 6vw, 72px); color: var(--muted); font-size: 15px; }
333
+ .foot-row p { margin: 0; max-width: 52ch; }
334
+ .foot-links { display: flex; gap: 22px; flex-wrap: wrap; }
335
+ .foot-links a { text-decoration: none; } .foot-links a:hover { color: var(--text); }
336
+
337
+ .toast { position: fixed; left: 50%; bottom: 28px; z-index: 60; transform: translate(-50%, 20px); opacity: 0; pointer-events: none;
338
+ background: var(--text); color: var(--bg); padding: 12px 20px; border-radius: 99px; font-size: 15px; transition: opacity .35s var(--ease), transform .5s var(--ease); }
339
+ .toast.show { opacity: 1; transform: translate(-50%, 0); }
340
+
341
+ @media (max-width: 1020px) {
342
+ .nav-links { display: none; }
343
+ .nav-actions { margin-left: auto; }
344
+ .insights, .use { grid-template-columns: minmax(0, 1fr); }
345
+ .trend-panel { grid-row: auto; }
346
+ }
347
+ @media (max-width: 720px) {
348
+ :root { --nav-h: 60px; }
349
+ .hero-foot { grid-template-columns: minmax(0, 1fr); }
350
+ .hero-side { align-items: flex-start; }
351
+ .ctas { justify-content: flex-start; }
352
+ .figures { grid-template-columns: minmax(0, 1fr); }
353
+ .head-row { grid-template-columns: minmax(0, 1fr); }
354
+ .head-row .intro-text { justify-self: start; }
355
+ .hero { min-height: 88svh; }
356
+ .route { height: 380vh; }
357
+ .route-svg { width: 250%; }
358
+ .route-svg .gate-label { font-size: 19px; }
359
+ .filters { position: static; }
360
+ .t-type .name, th .name { display: none; }
361
+ .addr-btn { font-size: 13.5px; }
362
+ .addr-btn svg { display: none; }
363
+ .sort { padding: 14px 9px; }
364
+ th:first-child .sort { padding-left: 16px; }
365
+ td { padding-inline: 9px; }
366
+ td:first-child { padding-left: 16px; }
367
+ .steps { min-height: 17em; }
368
+ .gh .label { display: none; }
369
+ .gh { padding: 0; width: 36px; justify-content: center; }
370
+ .sep { display: none; }
371
+ .range { flex: 1; } .range input { flex: 1; width: auto; }
372
+ input.field[type=search] { flex-basis: 100%; }
373
+ .filters { gap: 8px; }
374
+ .hide-sm { display: none; }
375
+ .btn { height: 48px; padding: 0 20px; }
376
+ }
377
+ @media (prefers-reduced-motion: reduce) {
378
+ *, *::before, *::after { animation: none !important; transition: none !important; }
379
+ html.js .headline .line > span, html.js .hero-foot, html.js .ticker { transform: none; opacity: 1; }
380
+ .ticker-window { overflow-x: auto; }
381
+ .ticker-toggle { display: none; }
382
+ .route { height: auto; }
383
+ .route-sticky { position: static; height: auto; padding-block: 0; }
384
+ .route-cam { overflow-x: auto; }
385
+ .steps { min-height: 0; display: grid; gap: 48px; }
386
+ .step { position: static; opacity: 1; transform: none; }
387
+ }
388
+ </style>
389
+ </head>
390
+ <body>
391
+ <svg width="0" height="0" style="position:absolute" aria-hidden="true">
392
+ <symbol id="mark" viewBox="0 0 64 64">
393
+ <rect width="64" height="64" rx="18" fill="#D4F77A"/>
394
+ <path d="M13 31.5 26.5 44.5 51 18" fill="none" stroke="#121113" stroke-width="5.5" stroke-linecap="round" stroke-linejoin="round"/>
395
+ <circle cx="13" cy="31.5" r="4.6" fill="#121113"/><circle cx="51" cy="18" r="4.6" fill="#121113"/>
396
+ <circle cx="26.5" cy="44.5" r="7.6" fill="#D4F77A" stroke="#121113" stroke-width="5"/>
397
+ </symbol>
398
+ <symbol id="i-copy" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="2" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M10.5 3.5v-.5a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 3v5A1.5 1.5 0 0 0 4 9.5h.5" fill="none" stroke="currentColor" stroke-width="1.4"/></symbol>
399
+ </svg>
400
+
401
+ <a class="skip" href="#list">Skip to the list</a>
402
+ <div class="loader" aria-hidden="true">
403
+ <div class="loader-inner">
404
+ <svg viewBox="0 0 64 64">
405
+ <rect width="64" height="64" rx="18" fill="#D4F77A"/>
406
+ <path class="route" pathLength="100" d="M13 31.5 26.5 44.5 51 18" fill="none" stroke="#121113" stroke-width="5.5" stroke-linecap="round" stroke-linejoin="round"/>
407
+ <circle class="node a" cx="13" cy="31.5" r="4.6" fill="#121113"/>
408
+ <circle class="node b" cx="26.5" cy="44.5" r="7.6" fill="#D4F77A" stroke="#121113" stroke-width="5"/>
409
+ <circle class="node c" cx="51" cy="18" r="4.6" fill="#121113"/>
410
+ </svg>
411
+ <div class="loader-word"><span>proxy-scraper</span></div>
412
+ </div>
413
+ </div>
414
+
415
+ <header class="nav">
416
+ <div class="wrap">
417
+ <a class="brand" href="#top" aria-label="proxy-scraper, back to top"><svg class="mark" aria-hidden="true"><use href="#mark"/></svg>proxy-scraper</a>
418
+ <nav class="nav-links" aria-label="Sections">
419
+ <a href="#list">The list</a><a href="#checks">How it's checked</a><a href="#runs">Last runs</a><a href="#use">Use it</a>
420
+ </nav>
421
+ <div class="nav-actions">
422
+ <button class="toggle" id="theme" type="button" aria-label="Switch to light theme">
423
+ <svg class="sun" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="3" fill="currentColor"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.4 1.4M11.6 11.6 13 13M3 13l1.4-1.4M11.6 4.4 13 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
424
+ <svg class="moon" viewBox="0 0 16 16" aria-hidden="true"><path d="M13.5 10.2A6 6 0 0 1 5.8 2.5a6 6 0 1 0 7.7 7.7z" fill="currentColor"/></svg>
425
+ </button>
426
+ <a class="gh" href="https://github.com/maximilianfeix/proxy-scraper"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg><span class="label">GitHub</span></a>
427
+ </div>
428
+ </div>
429
+ </header>
430
+
431
+ <main>
432
+ <section class="hero wrap" id="top">
433
+ <h1 class="headline" aria-label="Free proxies that actually work.">
434
+ <span class="line" aria-hidden="true"><span style="--i:0">Free proxies</span></span>
435
+ <span class="line" aria-hidden="true"><span style="--i:1">that actually</span></span>
436
+ <span class="line" aria-hidden="true"><span style="--i:2">work.</span></span>
437
+ </h1>
438
+ <div class="hero-foot">
439
+ <p class="lede">Every proxy on this list passed a <strong>real handshake</strong>, a <strong>honeypot check</strong> and a <strong>content check</strong> in the last run. The whole list is checked again every hour.</p>
440
+ <div class="hero-side">
441
+ <div class="status" aria-live="polite"><span class="pulse" aria-hidden="true"></span><span id="updated">Loading the latest run</span></div>
442
+ <div class="ctas">
443
+ <a class="btn primary" href="#list">Browse the list</a>
444
+ <button class="btn" id="dl-all" type="button">Download all</button>
445
+ </div>
446
+ </div>
447
+ </div>
448
+ <div class="ticker" role="group" aria-label="Fastest proxies right now, click one to copy it">
449
+ <div class="ticker-window"><div class="ticker-track" id="ticker"></div></div>
450
+ <button class="ticker-toggle" id="ticker-toggle" type="button" aria-pressed="false" aria-label="Pause the ticker">
451
+ <svg class="pause" viewBox="0 0 12 12" aria-hidden="true"><rect x="2" y="1" width="3" height="10" rx="1" fill="currentColor"/><rect x="7" y="1" width="3" height="10" rx="1" fill="currentColor"/></svg>
452
+ <svg class="play" viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5v9l7.5-4.5z" fill="currentColor"/></svg>
453
+ </button>
454
+ </div>
455
+ </section>
456
+
457
+ <section class="block wrap" aria-labelledby="numbers-title">
458
+ <div class="head-row">
459
+ <h2 class="h2" id="numbers-title">The last run in numbers</h2>
460
+ <p class="intro-text">Measured from GitHub's servers. From your own network some proxies will be faster, some won't answer at all.</p>
461
+ </div>
462
+ <div class="figures">
463
+ <div class="fig"><div class="big"><span class="count" id="n-total">0</span></div>
464
+ <p><strong>working proxies right now.</strong> Out of about a million candidates, only these answered properly and passed every check.</p></div>
465
+ <div class="fig"><div class="big"><span class="count" id="n-https">0</span><span class="unit">%</span></div>
466
+ <p><strong>tunnel HTTPS.</strong> They carry encrypted traffic through CONNECT, tested with a verified TLS handshake.</p></div>
467
+ <div class="fig"><div class="big"><span class="count" id="n-elite">0</span><span class="unit">%</span></div>
468
+ <p><strong>are elite.</strong> The target site can't tell that a proxy is in between: no forwarded IP, no Via header.</p></div>
469
+ <div class="fig"><div class="big"><span class="count" id="n-lat">0</span><span class="unit">s</span></div>
470
+ <p><strong>median response time.</strong> Half of the list answers faster than that. Sort by latency to get the quickest ones first.</p></div>
471
+ </div>
472
+ </section>
473
+
474
+ <section class="block wrap" id="list" aria-labelledby="list-title">
475
+ <div class="head-row">
476
+ <h2 class="h2" id="list-title">Pick your proxies</h2>
477
+ <p class="intro-text">Filter, sort, then copy or download exactly the ones you need. Click any address to copy it.</p>
478
+ </div>
479
+ <div class="filters" role="group" aria-label="Filters">
480
+ <button class="chip" type="button" data-type="http" aria-pressed="true"><span class="dot http"></span>HTTP</button>
481
+ <button class="chip" type="button" data-type="socks4" aria-pressed="true"><span class="dot socks4"></span>SOCKS4</button>
482
+ <button class="chip" type="button" data-type="socks5" aria-pressed="true"><span class="dot socks5"></span>SOCKS5</button>
483
+ <span class="sep" aria-hidden="true"></span>
484
+ <button class="chip" type="button" id="f-https" aria-pressed="false">HTTPS</button>
485
+ <button class="chip" type="button" id="f-elite" aria-pressed="false">Elite</button>
486
+ <button class="chip" type="button" id="f-dc" aria-pressed="false" title="Exits in datacenters get blocked sooner">No datacenter</button>
487
+ <button class="chip" type="button" id="f-stable" aria-pressed="false" title="Worked in every run for at least 24 hours">Stable 24 h+</button>
488
+ <button class="chip" type="button" id="f-bl" aria-pressed="false" title="Exit IP not on the SpamCop blocklist – fewer captchas">Not blocklisted</button>
489
+ <select class="field" id="f-country" aria-label="Country"><option value="">All countries</option></select>
490
+ <label class="range">Up to <input type="range" id="f-lat" min="200" max="8000" step="100" value="8000" aria-label="Maximum latency"><output id="f-lat-v" for="f-lat">any</output></label>
491
+ <input class="field" type="search" id="f-q" placeholder="Search IP, port, country or provider /" aria-label="Search" aria-keyshortcuts="/">
492
+ </div>
493
+ <div class="table-panel">
494
+ <div class="table-scroll" data-lenis-prevent-horizontal>
495
+ <table>
496
+ <caption class="sr-only">Working proxies from the last run. Activate an address to copy it.</caption>
497
+ <thead><tr>
498
+ <th scope="col" data-key="ptype"><button class="sort" type="button" aria-label="Type"><span class="name">Type</span></button></th>
499
+ <th scope="col" data-key="proxy"><button class="sort" type="button">Proxy</button></th>
500
+ <th scope="col" data-key="country"><button class="sort" type="button">Country</button></th>
501
+ <th scope="col" data-key="latency" aria-sort="ascending"><button class="sort" type="button">Latency</button></th>
502
+ <th scope="col" data-key="https" class="hide-sm"><button class="sort" type="button">HTTPS</button></th>
503
+ <th scope="col" data-key="streak" class="hide-sm"><button class="sort" type="button" title="How long it has been working without a gap">Up for</button></th>
504
+ <th scope="col" data-key="anonymity" class="hide-sm"><button class="sort" type="button">Anonymity</button></th>
505
+ <th scope="col" data-key="org" class="hide-sm"><button class="sort" type="button">Provider</button></th>
506
+ </tr></thead>
507
+ <tbody id="rows"><tr><td colspan="8" class="empty">Loading the list</td></tr></tbody>
508
+ </table>
509
+ </div>
510
+ </div>
511
+ <div class="list-foot">
512
+ <span class="count" id="count" aria-live="polite"></span>
513
+ <div class="actions">
514
+ <button class="btn small" id="more" type="button" hidden>Show more</button>
515
+ <button class="btn small" id="copy-all" type="button">Copy filtered</button>
516
+ <button class="btn small primary" id="download" type="button">Download filtered</button>
517
+ </div>
518
+ </div>
519
+ </section>
520
+
521
+ <section class="route" id="checks" aria-labelledby="checks-title">
522
+ <div class="route-sticky wrap">
523
+ <h2 class="route-title" id="checks-title">How a proxy gets on this list</h2>
524
+ <div class="route-cam"><svg class="route-svg" id="route-svg" viewBox="0 0 1200 230" aria-hidden="true">
525
+ <path class="track" id="route-path" d="M40 150 C 170 150 190 60 320 60 S 480 180 610 140 S 790 40 900 70 S 1090 170 1160 120"/>
526
+ <path class="done" id="route-done" d="M40 150 C 170 150 190 60 320 60 S 480 180 610 140 S 790 40 900 70 S 1090 170 1160 120"/>
527
+ <g id="gates"></g>
528
+ <circle class="halo" id="halo" r="16"/><circle class="packet" id="packet" r="7"/>
529
+ </svg></div>
530
+ <div class="steps" id="steps">
531
+ <article class="step on"><p class="which">Step 1 of 5</p><h3>It starts with 700+ public lists.</h3>
532
+ <p>Every hour a GitHub Action reads more than 700 public proxy lists. Most of what's in them is dead, duplicated or years old.</p></article>
533
+ <article class="step"><p class="which">Step 2 of 5</p><h3>A real handshake, not just an open port.</h3>
534
+ <p>Each candidate has to complete a proper HTTP, SOCKS4 or SOCKS5 handshake and fetch a page. An open port alone proves nothing.</p></article>
535
+ <article class="step"><p class="which">Step 3 of 5</p><h3>Two sites, one foreign IP.</h3>
536
+ <p>The proxy has to reach two unrelated sites and show an exit IP that isn't ours on both. Honeypots that fake a response drop out here.</p></article>
537
+ <article class="step"><p class="which">Step 4 of 5</p><h3>Nothing may be changed on the way.</h3>
538
+ <p>A static page has to arrive byte for byte. When we measured it, 54 of 270 working proxies slipped a script into the page. None of those make the list.</p></article>
539
+ <article class="step"><p class="which">Step 5 of 5</p><h3>Then the details.</h3>
540
+ <p id="step5">HTTPS with verified TLS, anonymity, country, provider and whether the exit IP is on a spam blocklist are checked for everything that's left.</p></article>
541
+ <article class="step final"><h3><span id="final-n">–</span> made it.</h3>
542
+ <p>That's the list above. In an hour it starts over, and anything that stopped working is gone.</p></article>
543
+ </div>
544
+ </div>
545
+ </section>
546
+
547
+ <section class="block wrap" id="runs" aria-labelledby="runs-title">
548
+ <div class="head-row">
549
+ <h2 class="h2" id="runs-title">Run after run</h2>
550
+ <p class="intro-text">Free proxies come and go quickly. This is how the list changed over the last runs and where the proxies are right now.</p>
551
+ </div>
552
+ <div class="insights">
553
+ <div class="panel trend-panel">
554
+ <h3>Working proxies per run</h3>
555
+ <p class="note" id="trend-note">One point every hour.</p>
556
+ <div class="trend" id="trend"></div>
557
+ <div class="trend-axis"><span id="axis-from"></span><span id="axis-to"></span></div>
558
+ <div class="legend"><span><i class="swatch"></i>Working</span><span><i class="swatch dashed"></i>Of those, HTTPS-capable</span></div>
559
+ </div>
560
+ <div class="panel"><h3>Countries</h3><p class="note">Where the exits are.</p><div class="bars" id="countries"></div></div>
561
+ <div class="panel"><h3>Latency</h3><p class="note">How fast they answered.</p><div class="bars" id="latency"></div></div>
562
+ </div>
563
+ </section>
564
+
565
+ <section class="block wrap" id="use" aria-labelledby="use-title">
566
+ <div class="head-row">
567
+ <h2 class="h2" id="use-title">Use it your way</h2>
568
+ <p class="intro-text">Run the same checks on your own machine, start from this list, hand it to your AI agent, or just fetch the plain text files.</p>
569
+ </div>
570
+ <div class="use">
571
+ <div class="use-cmds">
572
+ <div class="panel"><h3>Scan it yourself</h3><p>700+ sources, all the checks, filters and a live dashboard in your terminal.</p>
573
+ <div class="cmd"><pre>pipx install proxy-scraper-cli
574
+ 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>
575
+ <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>
576
+ <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>
577
+ <div class="panel"><h3>Give it to your AI agent</h3><p>An MCP server for Claude Code, Cursor, Codex and co: your agent gets working proxies and can load pages through them, as seen from another country. <a href="https://github.com/maximilianfeix/proxy-scraper#mcp">Setup for other clients</a></p>
578
+ <div class="cmd"><pre class="cmd-wrap">claude mcp add proxy-scraper -- uvx --python "&gt;=3.10" --from "proxy-scraper-cli[mcp]" proxy-scraper-mcp</pre><button class="icon-btn" type="button" data-copy-pre aria-label="Copy the Claude Code command"><svg aria-hidden="true"><use href="#i-copy"/></svg></button></div></div>
579
+ </div>
580
+ <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>
581
+ </div>
582
+ <div class="panel dev-panel">
583
+ <div>
584
+ <h3>Build on the data</h3>
585
+ <p>The same files this page uses, next to it on GitHub Pages. Updated with every run, served with CORS, no key needed.</p>
586
+ <div class="endpoints">
587
+ <a href="proxies.json"><span class="mono">proxies.json</span><span>every proxy with all details, fastest first</span></a>
588
+ <a href="stats.json"><span class="mono">stats.json</span><span>totals, per type, countries, median latency</span></a>
589
+ <a href="history.json"><span class="mono">history.json</span><span>the runs of the last 30 days, for charts</span></a>
590
+ <a href="streaks.json"><span class="mono">streaks.json</span><span>how many runs in a row each proxy worked</span></a>
591
+ </div>
592
+ </div>
593
+ <div class="cmd"><pre>curl -s https://maximilianfeix.github.io/proxy-scraper/proxies.json | jq '.[0]'
594
+ {
595
+ "url": "socks5://203.0.113.10:1080",
596
+ "proxy": "203.0.113.10:1080",
597
+ "ptype": "socks5",
598
+ "latency": 92,
599
+ "country": "DE",
600
+ "https": true,
601
+ "anonymity": "elite",
602
+ "exit_ip": "203.0.113.10",
603
+ "asn": 14061,
604
+ "org": "DigitalOcean, LLC",
605
+ "hosting": true,
606
+ "blocklisted": false,
607
+ "streak": 5
608
+ }</pre><button class="icon-btn" type="button" data-copy-pre aria-label="Copy the example"><svg aria-hidden="true"><use href="#i-copy"/></svg></button></div>
609
+ </div>
610
+ </section>
611
+ </main>
612
+
613
+ <footer>
614
+ <div class="wrap">
615
+ <div class="wordmark" aria-hidden="true"><svg class="mark"><use href="#mark"/></svg>proxy-scraper</div>
616
+ <nav class="browse" aria-label="Lists by protocol and country">
617
+ <p>By protocol: <a href="http/">HTTP</a> <a href="socks4/">SOCKS4</a> <a href="socks5/">SOCKS5</a> <a href="https/">HTTPS</a> <a href="elite/">Elite</a></p>
618
+ <p>By country: <a href="country/us/">United States</a> <a href="country/de/">Germany</a> <a href="country/nl/">Netherlands</a> <a href="country/gb/">United Kingdom</a> <a href="country/fr/">France</a> <a href="country/sg/">Singapore</a> <a href="country/jp/">Japan</a> <a href="country/in/">India</a> <a href="country/br/">Brazil</a> <a href="country/id/">Indonesia</a> <a href="country/ru/">Russia</a> <a href="country/cn/">China</a></p>
619
+ </nav>
620
+ <div class="foot-row">
621
+ <p>Free proxies are run by strangers. Never send passwords or personal data through them.</p>
622
+ <div class="foot-links">
623
+ <a href="https://github.com/maximilianfeix/proxy-scraper">Source on GitHub</a>
624
+ <a href="https://github.com/maximilianfeix/proxy-scraper/releases/latest">Releases</a>
625
+ <a href="https://db-ip.com">Countries by DB-IP</a>
626
+ <span>MIT License</span>
627
+ </div>
628
+ </div>
629
+ </div>
630
+ </footer>
631
+ <div class="toast" id="toast" role="status" aria-live="polite"></div>
632
+
633
+ <script>
634
+ (() => {
635
+ const RAW = "https://raw.githubusercontent.com/maximilianfeix/proxy-scraper/proxy-list";
636
+ const PAGE = 50;
637
+ let RUN_HOURS = 1, STABLE = 24; // hours between runs and runs in a row for "stable" (24 h) – set from stats.json
638
+ const $ = (s) => document.querySelector(s);
639
+ const $$ = (s) => [...document.querySelectorAll(s)];
640
+ const html = document.documentElement;
641
+ const calm = matchMedia("(prefers-reduced-motion: reduce)").matches;
642
+ const fmt = (n) => Number(n).toLocaleString("en-US");
643
+ const esc = (s) => String(s ?? "").replace(/[&<>"']/g, c => ({"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"}[c]));
644
+ const flag = (cc) => /^[A-Z]{2}$/.test(cc || "") ? String.fromCodePoint(...[...cc].map(c => 0x1F1E6 + c.charCodeAt(0) - 65)) : "";
645
+ const pct = (n, of) => Math.round(100 * n / Math.max(of, 1));
646
+ const short = (org) => { const s = String(org || "").replace(/,? (Inc|LLC|Ltd|GmbH|Co|Corporation|Limited|S\.?A|B\.?V)\.?$/i, ""); return s.length > 28 ? s.slice(0, 27) + "…" : s; };
647
+ const uptime = (n) => { // n runs in a row ≈ n × RUN_HOURS
648
+ if (!n) return '<span class="quiet">–</span>';
649
+ const h = n * RUN_HOURS, text = n === 1 ? "new" : h < 48 ? `${h} h` : `${Math.round(h / 24)} days`;
650
+ return n >= STABLE ? `<span class="stable-badge">${text}</span>` : `<span class="quiet">${text}</span>`;
651
+ };
652
+ const latency = (ms) => `<span class="${ms < 1000 ? "fast" : ms < 3000 ? "mid" : "slow"}">${fmt(ms)} ms</span>`;
653
+ const state = { rows: [], shown: PAGE, sort: "latency", dir: 1, types: new Set(["http", "socks4", "socks5"]),
654
+ https: false, elite: false, noDc: false, noBl: false, stable: false, country: "", maxLat: 8000, q: "" };
655
+
656
+ // Theme
657
+ const themeBtn = $("#theme");
658
+ const isLight = () => html.dataset.theme === "light";
659
+ const labelTheme = () => themeBtn.setAttribute("aria-label", isLight() ? "Switch to dark theme" : "Switch to light theme");
660
+ const themeColor = () => { $('meta[name="theme-color"]').content = isLight() ? "#EFEFEC" : "#121113"; };
661
+ labelTheme(); themeColor();
662
+ themeBtn.onclick = () => {
663
+ html.dataset.theme = isLight() ? "dark" : "light";
664
+ try { localStorage.setItem("theme", html.dataset.theme); } catch (e) {}
665
+ themeColor(); labelTheme();
666
+ };
667
+
668
+ // Copying, toasts
669
+ const toastEl = $("#toast");
670
+ function toast(msg) { toastEl.textContent = msg; toastEl.classList.add("show"); clearTimeout(toastEl._t); toastEl._t = setTimeout(() => toastEl.classList.remove("show"), 1800); }
671
+ async function copy(text, msg) { try { await navigator.clipboard.writeText(text); toast(msg); } catch (e) { toast("Copying isn't allowed here. Select the text instead."); } }
672
+ function download(rows, name) {
673
+ const blob = new Blob([rows.map(r => r.url).join("\n") + "\n"], { type: "text/plain" });
674
+ const a = Object.assign(document.createElement("a"), { href: URL.createObjectURL(blob), download: name });
675
+ document.body.append(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 1000);
676
+ }
677
+ function ago(iso) {
678
+ const s = (Date.now() - new Date(iso)) / 1000;
679
+ if (s < 90) return "just now"; if (s < 5400) return `${Math.round(s / 60)} minutes ago`;
680
+ if (s < 129600) return `${Math.round(s / 3600)} hours ago`; return `${Math.round(s / 86400)} days ago`;
681
+ }
682
+
683
+ // Smooth scrolling (Lenis), the nav hides while scrolling down
684
+ let lenis = null;
685
+ window.startLenis = () => { // called by the Lenis script once it has loaded; without it the browser scrolls normally
686
+ if (calm || lenis || !window.Lenis) return;
687
+ lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
688
+ const raf = (t) => { lenis.raf(t); requestAnimationFrame(raf); };
689
+ requestAnimationFrame(raf);
690
+ };
691
+ document.addEventListener("click", (e) => {
692
+ const a = e.target.closest('a[href^="#"]');
693
+ if (!a) return;
694
+ const target = a.getAttribute("href") === "#top" ? 0 : $(a.getAttribute("href"));
695
+ if (target === null) return;
696
+ e.preventDefault();
697
+ if (lenis) lenis.scrollTo(target, { offset: target === 0 ? 0 : -72, duration: 1.4 });
698
+ else if (target === 0) scrollTo({ top: 0 }); else target.scrollIntoView();
699
+ const focusTarget = target === 0 ? $("#top") : target;
700
+ if (!focusTarget.hasAttribute("tabindex")) focusTarget.setAttribute("tabindex", "-1");
701
+ focusTarget.focus({ preventScroll: true });
702
+ });
703
+ let lastY = scrollY, ticking = false;
704
+ const onScroll = () => {
705
+ const y = scrollY;
706
+ document.body.classList.toggle("nav-hidden", y > lastY && y > 400);
707
+ lastY = y;
708
+ updateRoute();
709
+ ticking = false;
710
+ };
711
+ addEventListener("scroll", () => { if (!ticking) { ticking = true; requestAnimationFrame(onScroll); } }, { passive: true });
712
+ addEventListener("resize", () => { placeGates(); updateRoute(); });
713
+
714
+ // Count up once the number is visible
715
+ function countTo(el, to, decimals = 0) {
716
+ const show = (v) => { el.textContent = decimals ? v.toFixed(decimals) : fmt(Math.round(v)); };
717
+ if (calm) return show(to);
718
+ const io = new IntersectionObserver((entries) => {
719
+ const e = entries[0]; // from half visible – or if we already jumped past it
720
+ if (e.intersectionRatio < 0.5 && e.boundingClientRect.top > 0) return;
721
+ io.disconnect();
722
+ const start = performance.now(), dur = 1600;
723
+ const step = (t) => { const k = Math.min(1, (t - start) / dur); show(to * (1 - Math.pow(2, -10 * k)) / (1 - Math.pow(2, -10))); if (k < 1) requestAnimationFrame(step); };
724
+ requestAnimationFrame(step);
725
+ }, { threshold: [0, 0.5] });
726
+ io.observe(el);
727
+ }
728
+
729
+ // Route through the five checks – scroll progress pushes a packet along the line
730
+ const GATES = [["Lists", 0.03], ["Handshake", 0.27], ["Honeypots", 0.5], ["Content", 0.73], ["Details", 0.96]];
731
+ const path = $("#route-path"), done = $("#route-done"), packet = $("#packet"), halo = $("#halo");
732
+ const total = path.getTotalLength();
733
+ const steps = $$(".step");
734
+ done.style.strokeDasharray = total; done.style.strokeDashoffset = total;
735
+ function placeGates() {
736
+ $("#gates").innerHTML = GATES.map(([label, f], i) => {
737
+ const p = path.getPointAtLength(total * f), above = i % 2 === 0;
738
+ return `<circle class="gate" cx="${p.x}" cy="${p.y}" r="11"/><text class="gate-label" x="${p.x}" y="${p.y + (above ? -26 : 38)}">${label}</text>`;
739
+ }).join("");
740
+ }
741
+ placeGates();
742
+ let current = -1;
743
+ function updateRoute() {
744
+ const section = $("#checks");
745
+ let p = 1;
746
+ if (!calm) {
747
+ const r = section.getBoundingClientRect();
748
+ p = Math.min(1, Math.max(0, -r.top / Math.max(1, r.height - innerHeight)));
749
+ }
750
+ const along = Math.min(1, p / 0.9); // the last 10 % belong to the result
751
+ const pt = path.getPointAtLength(total * along);
752
+ packet.setAttribute("cx", pt.x); packet.setAttribute("cy", pt.y); halo.setAttribute("cx", pt.x); halo.setAttribute("cy", pt.y);
753
+ done.style.strokeDashoffset = total * (1 - along);
754
+ // Camera: if the route is wider than the screen, the packet stays roughly in the middle
755
+ const svg = $("#route-svg"), cam = svg.parentElement;
756
+ const extra = svg.clientWidth - (cam.clientWidth - 2 * parseFloat(getComputedStyle(cam).paddingLeft));
757
+ if (extra > 1 && !calm) { // without motion: scrollable sideways instead
758
+ const scale = svg.clientWidth / 1200;
759
+ const shift = Math.min(extra, Math.max(0, pt.x * scale - cam.clientWidth / 2));
760
+ svg.style.transform = `translateX(${-shift}px)`;
761
+ } else svg.style.transform = "";
762
+ const passed = GATES.filter(([, f]) => along >= f - 0.001).length;
763
+ $$("#gates .gate").forEach((g, i) => g.classList.toggle("on", i < passed));
764
+ $$("#gates .gate-label").forEach((g, i) => g.classList.toggle("on", i < passed));
765
+ const which = p >= 0.93 ? steps.length - 1 : Math.max(0, passed - 1);
766
+ if (which !== current && !calm) { steps.forEach((s, i) => s.classList.toggle("on", i === which)); current = which; }
767
+ }
768
+ updateRoute();
769
+
770
+ // List
771
+ function filtered() {
772
+ const q = state.q.trim().toLowerCase();
773
+ const rows = state.rows.filter(r => state.types.has(r.ptype) && (!state.https || r.https) && (!state.elite || r.anonymity === "elite")
774
+ && (!state.noDc || !r.hosting) && (!state.noBl || !r.blocklisted) && (!state.stable || (r.streak || 0) >= STABLE) && (!state.country || r.country === state.country)
775
+ && (state.maxLat >= 8000 || r.latency <= state.maxLat) // all the way right = no limit
776
+ && (!q || `${r.ptype} ${r.proxy} ${r.country} ${r.exit_ip} ${r.org || ""} AS${r.asn || ""}`.toLowerCase().includes(q)));
777
+ const k = state.sort, d = state.dir;
778
+ return rows.sort((a, b) => (a[k] > b[k] ? 1 : a[k] < b[k] ? -1 : 0) * d || a.latency - b.latency);
779
+ }
780
+ function render() {
781
+ const rows = filtered();
782
+ $("#rows").innerHTML = rows.slice(0, state.shown).map(r => `<tr>
783
+ <td><span class="t-type" title="${esc(r.ptype)}"><span class="dot ${esc(r.ptype)}"></span><span class="name">${esc(r.ptype)}</span></span></td>
784
+ <td><button class="addr-btn" type="button" data-url="${esc(r.url)}" aria-label="Copy ${esc(r.url)}">${esc(r.proxy)}<svg aria-hidden="true"><use href="#i-copy"/></svg></button></td>
785
+ <td>${flag(r.country)} ${esc(r.country || "–")}</td>
786
+ <td>${latency(r.latency)}</td>
787
+ <td class="hide-sm">${r.https ? "Yes" : '<span class="quiet">No</span>'}</td>
788
+ <td class="hide-sm">${uptime(r.streak)}</td>
789
+ <td class="hide-sm">${esc(r.anonymity || "–")}</td>
790
+ <td class="hide-sm provider" title="${esc(r.org ? `AS${r.asn} ${r.org}, exit ${r.exit_ip}` : r.exit_ip)}">${r.hosting ? '<span class="tag" title="Datacenter">DC</span>' : ""}${r.blocklisted ? '<span class="tag bl" title="Exit IP on the SpamCop blocklist">BL</span>' : ""}${esc(short(r.org) || r.exit_ip)}</td></tr>`).join("")
791
+ || '<tr><td colspan="8" class="empty">No proxy matches these filters. Try a higher latency limit or fewer filters.</td></tr>';
792
+ $("#count").textContent = rows.length ? `Showing ${fmt(Math.min(state.shown, rows.length))} of ${fmt(rows.length)} matching proxies` : "";
793
+ $("#more").hidden = rows.length <= state.shown;
794
+ $$("th[data-key]").forEach(th => th.dataset.key === state.sort ? th.setAttribute("aria-sort", state.dir > 0 ? "ascending" : "descending") : th.removeAttribute("aria-sort"));
795
+ }
796
+ const refresh = () => { state.shown = PAGE; render(); };
797
+ $$(".chip[data-type]").forEach(b => b.onclick = () => {
798
+ const t = b.dataset.type; state.types.has(t) ? state.types.delete(t) : state.types.add(t);
799
+ b.setAttribute("aria-pressed", state.types.has(t)); refresh();
800
+ });
801
+ const toggle = (id, key) => { const b = $(id); b.onclick = () => { state[key] = !state[key]; b.setAttribute("aria-pressed", state[key]); refresh(); }; };
802
+ toggle("#f-https", "https"); toggle("#f-elite", "elite"); toggle("#f-dc", "noDc"); toggle("#f-stable", "stable"); toggle("#f-bl", "noBl");
803
+ $("#f-country").onchange = (e) => { state.country = e.target.value; refresh(); };
804
+ $("#f-lat").oninput = (e) => { state.maxLat = +e.target.value; $("#f-lat-v").textContent = state.maxLat >= 8000 ? "any" : `${fmt(state.maxLat)} ms`; refresh(); };
805
+ $("#f-q").oninput = (e) => { state.q = e.target.value; refresh(); };
806
+ $("#more").onclick = () => { state.shown += PAGE * 4; render(); };
807
+ $$("th[data-key]").forEach(th => {
808
+ th.querySelector(".sort").onclick = () => { const k = th.dataset.key; state.dir = state.sort === k ? -state.dir : 1; state.sort = k; render(); };
809
+ });
810
+ $("#rows").onclick = (e) => { const b = e.target.closest(".addr-btn"); if (b) copy(b.dataset.url, `Copied ${b.dataset.url}`); };
811
+ $("#copy-all").onclick = () => { const rows = filtered(); copy(rows.map(r => r.url).join("\n") + "\n", `Copied ${fmt(rows.length)} proxies`); };
812
+ $("#download").onclick = () => download(filtered(), "proxies.txt");
813
+ $("#dl-all").onclick = () => state.rows.length ? download(state.rows, "all.txt") : toast("The list is still loading");
814
+ $("#ticker-toggle").onclick = (e) => {
815
+ const t = $(".ticker"), paused = t.classList.toggle("paused");
816
+ t.classList.add("manual");
817
+ e.currentTarget.setAttribute("aria-pressed", paused);
818
+ e.currentTarget.setAttribute("aria-label", paused ? "Play the ticker" : "Pause the ticker");
819
+ };
820
+ // "/" jumps to the search, like on GitHub
821
+ document.addEventListener("keydown", (e) => {
822
+ if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey || e.target.closest("input, select, textarea")) return;
823
+ e.preventDefault();
824
+ $("#f-q").focus({ preventScroll: true });
825
+ $("#f-q").scrollIntoView({ block: "center", behavior: calm ? "auto" : "smooth" });
826
+ });
827
+ $("#ticker").onclick = (e) => { const b = e.target.closest(".tick"); if (b) copy(b.dataset.url, `Copied ${b.dataset.url}`); };
828
+ $$("[data-copy-pre]").forEach(b => b.onclick = () => copy(b.parentElement.querySelector("pre").textContent + "\n", "Copied the command"));
829
+ $("#raw").innerHTML = [["all", "Everything"], ["http", "HTTP"], ["socks4", "SOCKS4"], ["socks5", "SOCKS5"], ["https", "HTTPS-capable"], ["elite", "Elite"]]
830
+ .map(([f, label]) => `<a href="${RAW}/${f}.txt"><span>${label}</span><span class="mono">${f}.txt</span></a>`).join("");
831
+
832
+ // Charts
833
+ function bars(el, entries, label) {
834
+ const max = Math.max(1, ...entries.map(e => e[1]));
835
+ el.innerHTML = entries.map(([k, n]) => `<div class="bar-row"><span class="k">${esc(label(k))}</span><span class="bar"><i data-w="${(100 * n / max).toFixed(1)}"></i></span><span class="v">${fmt(n)}</span></div>`).join("")
836
+ || '<p class="note">No data yet.</p>';
837
+ const grow = () => el.querySelectorAll("i").forEach(i => { i.style.width = i.dataset.w + "%"; });
838
+ if (calm) return grow();
839
+ const io = new IntersectionObserver((e) => { if (e[0].isIntersecting) { io.disconnect(); grow(); } }, { threshold: 0.3 });
840
+ io.observe(el);
841
+ }
842
+ function trend(history) {
843
+ const el = $("#trend");
844
+ const runs = history ? history.length : 0;
845
+ // runs a few minutes apart (started by hand) are noise, not trend: keep the last of each such cluster
846
+ history = (history || []).filter((h, i, a) => i === a.length - 1 || Date.parse(a[i + 1].updated) - Date.parse(h.updated) > 20 * 60e3);
847
+ if (history.length < 2) { el.innerHTML = '<p class="note">The chart fills up with every run.</p>'; return; }
848
+ const W = 800, H = 260, P = 10, max = Math.max(...history.map(h => h.total)) * 1.12 || 1;
849
+ // spaced by time, not by index: the runs weren't always the same distance apart
850
+ const t0 = Date.parse(history[0].updated), span = Math.max(Date.parse(history[history.length - 1].updated) - t0, 1);
851
+ const x = (h) => P + (Date.parse(h.updated) - t0) * (W - 2 * P) / span, y = (v) => H - P - v * (H - 2 * P) / max;
852
+ // monotone cubic (Fritsch–Carlson): smooth, but never overshoots the measured values – the runs aren't
853
+ // evenly spaced, and a plain spline turns close runs into spikes
854
+ const f = (v) => v.toFixed(1);
855
+ const line = (key) => {
856
+ const p = history.map((h) => [x(h), y(key(h))]).filter((q, i, a) => !i || q[0] > a[i - 1][0]);
857
+ const n = p.length, d = [], m = [];
858
+ if (n < 2) return p.length ? `M${f(p[0][0])},${f(p[0][1])}` : "";
859
+ for (let i = 0; i < n - 1; i++) d.push((p[i + 1][1] - p[i][1]) / (p[i + 1][0] - p[i][0]));
860
+ m[0] = d[0]; m[n - 1] = d[n - 2];
861
+ for (let i = 1; i < n - 1; i++) m[i] = d[i - 1] * d[i] <= 0 ? 0 : (d[i - 1] + d[i]) / 2;
862
+ for (let i = 0; i < n - 1; i++) {
863
+ if (!d[i]) { m[i] = m[i + 1] = 0; continue; }
864
+ const a = m[i] / d[i], b = m[i + 1] / d[i], s2 = a * a + b * b;
865
+ if (s2 > 9) { const t = 3 / Math.sqrt(s2); m[i] = t * a * d[i]; m[i + 1] = t * b * d[i]; }
866
+ }
867
+ let path = `M${f(p[0][0])},${f(p[0][1])}`;
868
+ for (let i = 0; i < n - 1; i++) {
869
+ const h = (p[i + 1][0] - p[i][0]) / 3;
870
+ path += `C${f(p[i][0] + h)},${f(p[i][1] + m[i] * h)} ${f(p[i + 1][0] - h)},${f(p[i + 1][1] - m[i + 1] * h)} ${f(p[i + 1][0])},${f(p[i + 1][1])}`;
871
+ }
872
+ return path;
873
+ };
874
+ const area = line(h => h.total) + `L${W - P},${H - P}L${P},${H - P}Z`;
875
+ const last = history[history.length - 1];
876
+ el.innerHTML = `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" role="img" aria-label="Working proxies per run, last run ${fmt(last.total)}">
877
+ <defs><linearGradient id="fade" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="var(--signal-ink)" stop-opacity=".28"/><stop offset="1" stop-color="var(--signal-ink)" stop-opacity="0"/></linearGradient></defs>
878
+ <path d="${area}" fill="url(#fade)"/>
879
+ <path d="${line(h => h.https || 0)}" fill="none" stroke="var(--muted)" stroke-width="1.5" stroke-dasharray="5 4" vector-effect="non-scaling-stroke"/>
880
+ <path d="${line(h => h.total)}" fill="none" stroke="var(--signal-ink)" stroke-width="2.5" stroke-linejoin="round" vector-effect="non-scaling-stroke"/></svg>`;
881
+ const day = (iso) => new Date(iso).toLocaleString("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
882
+ $("#axis-from").textContent = day(history[0].updated); $("#axis-to").textContent = day(last.updated);
883
+ $("#trend-note").textContent = `${fmt(runs)} runs, now one every ${RUN_HOURS === 1 ? "hour" : `${RUN_HOURS} hours`}. Last run: ${fmt(last.total)} working.`;
884
+ }
885
+ function ticker(rows) {
886
+ const fastest = [...rows].sort((a, b) => a.latency - b.latency).slice(0, 24);
887
+ if (!fastest.length) return;
888
+ const item = (r, hidden) => `<button class="tick" type="button" data-url="${esc(r.url)}"${hidden ? ' tabindex="-1" aria-hidden="true"' : ` aria-label="Copy ${esc(r.url)}"`}>
889
+ <span class="dot ${esc(r.ptype)}"></span><span class="addr">${esc(r.proxy)}</span><span class="meta">${flag(r.country)} ${esc(r.country || "")} ${fmt(r.latency)} ms</span></button>`;
890
+ const track = $("#ticker");
891
+ track.innerHTML = fastest.map(r => item(r, false)).join("") + (calm ? "" : fastest.map(r => item(r, true)).join(""));
892
+ track.style.setProperty("--dur", `${fastest.length * 3.2}s`);
893
+ }
894
+
895
+ // the data sits right next to this page
896
+ const load = (f) => fetch(f, { cache: "no-cache" }).then(r => r.ok ? r.json() : null).catch(() => null);
897
+ Promise.all([load("proxies.json"), load("stats.json"), load("history.json")]).then(([rows, st, history]) => {
898
+ if (!rows || !st) {
899
+ $("#rows").innerHTML = '<tr><td colspan="8" class="empty">The list could not be loaded. The plain text files under "Use it" still work.</td></tr>';
900
+ $("#updated").textContent = "The latest run could not be loaded";
901
+ $$(".fig .count").forEach(el => { el.textContent = "–"; });
902
+ return;
903
+ }
904
+ state.rows = rows;
905
+ RUN_HOURS = st.run_hours || 6; // lists from before run_hours existed ran every 6 hours
906
+ STABLE = Math.ceil(24 / RUN_HOURS);
907
+ const when = new Date(st.updated).toLocaleString("en-GB", { dateStyle: "medium", timeStyle: "short" });
908
+ $("#updated").textContent = `${fmt(st.total)} verified, updated ${ago(st.updated)}`;
909
+ $("#updated").title = when;
910
+ countTo($("#n-total"), st.total);
911
+ countTo($("#n-https"), pct(st.https, st.total));
912
+ countTo($("#n-elite"), pct(st.elite, st.total));
913
+ countTo($("#n-lat"), st.median_latency / 1000, 1);
914
+ $("#final-n").textContent = fmt(st.total);
915
+ const dc = st.datacenter != null ? ` ${pct(st.datacenter, st.total)} % exit from a datacenter` : "";
916
+ const bl = st.blocklisted != null ? `${dc ? "," : ""} ${pct(st.blocklisted, st.total)} % from an IP on a spam blocklist` : "";
917
+ $("#step5").textContent = `HTTPS with verified TLS, anonymity, country, provider and spam blocklists. This time ${fmt(st.https)} tunnel HTTPS and ${fmt(st.elite)} are elite.${dc}${bl}${dc || bl ? "." : ""}`;
918
+ ticker(rows);
919
+ trend(history);
920
+ const byCountry = {};
921
+ rows.forEach(r => { if (/^[A-Z]{2}$/.test(r.country || "")) byCountry[r.country] = (byCountry[r.country] || 0) + 1; });
922
+ const countries = Object.entries(byCountry).sort((a, b) => b[1] - a[1]);
923
+ bars($("#countries"), countries.slice(0, 7), (cc) => `${flag(cc)} ${cc}`);
924
+ const buckets = [[300, "< 0.3 s"], [700, "< 0.7 s"], [1500, "< 1.5 s"], [3000, "< 3 s"], [Infinity, "3 s +"]];
925
+ bars($("#latency"), buckets.map(([max, label], i) => [label, rows.filter(r => r.latency < max && (i === 0 || r.latency >= buckets[i - 1][0])).length]), (l) => l);
926
+ const sel = $("#f-country");
927
+ countries.forEach(([cc, n]) => sel.add(new Option(`${flag(cc)} ${cc} (${n})`, cc)));
928
+ render();
929
+ });
930
+ })();
931
+ </script>
932
+ <script src="https://cdn.jsdelivr.net/npm/lenis@1.1.20/dist/lenis.min.js" async onload="startLenis()"></script>
933
+ </body>
934
+ </html>