diff --git a/httpd.py b/httpd.py index b689d0e..d4a5a52 100644 --- a/httpd.py +++ b/httpd.py @@ -1,75 +1,850 @@ #!/usr/bin/env python2 # -*- coding: utf-8 -*- -"""Simple HTTP API server for querying working proxies.""" +"""HTTP API server with advanced web dashboard for PPF.""" import BaseHTTPServer import json import threading +import time import mysqlite from misc import _log +# Detect if gevent has monkey-patched the environment +try: + from gevent import monkey + GEVENT_PATCHED = monkey.is_module_patched('socket') +except ImportError: + GEVENT_PATCHED = False + +if GEVENT_PATCHED: + from gevent.pywsgi import WSGIServer + +# Theme colors - modern dark palette +THEME = { + 'bg': '#0d1117', + 'card': '#161b22', + 'card_alt': '#1c2128', + 'border': '#30363d', + 'text': '#e6edf3', + 'dim': '#7d8590', + 'green': '#3fb950', + 'red': '#f85149', + 'yellow': '#d29922', + 'blue': '#58a6ff', + 'purple': '#a371f7', + 'cyan': '#39c5cf', + 'orange': '#db6d28', + 'pink': '#db61a2', +} + +DASHBOARD_CSS = ''' +:root { + --bg: {bg}; --card: {card}; --card-alt: {card_alt}; --border: {border}; + --text: {text}; --dim: {dim}; --green: {green}; --red: {red}; + --yellow: {yellow}; --blue: {blue}; --purple: {purple}; + --cyan: {cyan}; --orange: {orange}; --pink: {pink}; +} +* { box-sizing: border-box; margin: 0; padding: 0; } +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + font-size: 13px; background: var(--bg); color: var(--text); + padding: 16px; min-height: 100vh; line-height: 1.4; +} +.container { max-width: 1400px; margin: 0 auto; } + +/* Header */ +.hdr { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--border); } +.hdr h1 { font-size: 18px; font-weight: 600; display: flex; align-items: center; gap: 10px; } +.hdr h1::before { content: ""; width: 10px; height: 10px; background: var(--green); border-radius: 50%; box-shadow: 0 0 8px var(--green); } +.status { display: flex; align-items: center; gap: 12px; font-size: 12px; color: var(--dim); } +.status-item { display: flex; align-items: center; gap: 4px; } +.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; } +.dot.err { background: var(--red); animation: none; } +@keyframes pulse { 50% { opacity: 0.5; } } + +/* Grid */ +.g { display: grid; gap: 12px; margin-bottom: 16px; } +.g2 { grid-template-columns: repeat(2, 1fr); } +.g3 { grid-template-columns: repeat(3, 1fr); } +.g4 { grid-template-columns: repeat(4, 1fr); } +.g5 { grid-template-columns: repeat(5, 1fr); } +.g6 { grid-template-columns: repeat(6, 1fr); } +@media (max-width: 1200px) { .g5, .g6 { grid-template-columns: repeat(4, 1fr); } } +@media (max-width: 900px) { .g3, .g4, .g5, .g6 { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 600px) { .g2, .g3, .g4, .g5, .g6 { grid-template-columns: 1fr; } } + +/* Cards */ +.c { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 14px; } +.c-lg { padding: 16px 18px; } +.c-sm { padding: 10px 12px; } +.lbl { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; font-weight: 500; } +.val { font-size: 26px; font-weight: 700; font-feature-settings: "tnum"; letter-spacing: -0.5px; } +.val-md { font-size: 20px; } +.val-sm { font-size: 16px; } +.sub { font-size: 11px; color: var(--dim); margin-top: 4px; } +.grn { color: var(--green); } .red { color: var(--red); } .yel { color: var(--yellow); } +.blu { color: var(--blue); } .pur { color: var(--purple); } .cyn { color: var(--cyan); } +.org { color: var(--orange); } .pnk { color: var(--pink); } + +/* Section headers */ +.sec { margin-bottom: 16px; } +.sec-hdr { font-size: 11px; font-weight: 600; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; } +.sec-hdr::before { content: ""; width: 3px; height: 12px; background: var(--blue); border-radius: 2px; } + +/* Progress bars */ +.bar-wrap { height: 6px; background: var(--border); border-radius: 3px; margin-top: 8px; overflow: hidden; } +.bar { height: 100%; border-radius: 3px; transition: width 0.4s ease; } +.bar.grn { background: linear-gradient(90deg, #238636, #3fb950); } +.bar.red { background: linear-gradient(90deg, #da3633, #f85149); } +.bar.yel { background: linear-gradient(90deg, #9e6a03, #d29922); } +.bar.blu { background: linear-gradient(90deg, #1f6feb, #58a6ff); } + +/* Charts */ +.chart { width: 100%; height: 80px; margin-top: 8px; } +.chart-lg { height: 120px; } +.chart svg { width: 100%; height: 100%; } +.chart-line { fill: none; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; } +.chart-area { opacity: 0.15; } +.chart-grid { stroke: var(--border); stroke-width: 0.5; } +.chart-label { font-size: 9px; fill: var(--dim); } + +/* Histogram bars */ +.histo { display: flex; align-items: flex-end; gap: 2px; height: 60px; margin-top: 8px; } +.histo-bar { flex: 1; background: var(--blue); border-radius: 2px 2px 0 0; min-height: 2px; transition: height 0.3s; position: relative; } +.histo-bar:hover { opacity: 0.8; } +.histo-bar::after { content: attr(data-label); position: absolute; bottom: -16px; left: 50%; transform: translateX(-50%); font-size: 8px; color: var(--dim); white-space: nowrap; } +.histo-labels { display: flex; justify-content: space-between; margin-top: 20px; font-size: 9px; color: var(--dim); } + +/* Stat rows */ +.stat-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; font-size: 12px; } +.stat-row + .stat-row { border-top: 1px solid rgba(48,54,61,0.5); } +.stat-lbl { color: var(--dim); display: flex; align-items: center; gap: 6px; } +.stat-val { font-weight: 600; font-feature-settings: "tnum"; } +.stat-bar { width: 60px; height: 4px; background: var(--border); border-radius: 2px; margin-left: 8px; overflow: hidden; } +.stat-bar-fill { height: 100%; border-radius: 2px; } + +/* Leaderboard */ +.lb { font-size: 12px; } +.lb-item { display: flex; align-items: center; gap: 8px; padding: 5px 0; } +.lb-item + .lb-item { border-top: 1px solid rgba(48,54,61,0.3); } +.lb-rank { width: 18px; height: 18px; border-radius: 4px; background: var(--card-alt); display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; color: var(--dim); } +.lb-rank.top { background: var(--yellow); color: var(--bg); } +.lb-name { flex: 1; font-family: ui-monospace, monospace; color: var(--text); } +.lb-val { font-weight: 600; font-feature-settings: "tnum"; } + +/* Tags */ +.tag { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; } +.tag-ok { background: rgba(63,185,80,0.15); color: var(--green); } +.tag-err { background: rgba(248,81,73,0.15); color: var(--red); } +.tag-warn { background: rgba(210,153,34,0.15); color: var(--yellow); } +.tag-info { background: rgba(88,166,255,0.15); color: var(--blue); } + +/* Mini stats */ +.mini { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px; } +.mini-item { display: flex; align-items: baseline; gap: 4px; } +.mini-val { font-size: 14px; font-weight: 600; font-feature-settings: "tnum"; } +.mini-lbl { font-size: 10px; color: var(--dim); } + +/* Proto cards */ +.proto-card { text-align: center; } +.proto-icon { font-size: 20px; margin-bottom: 4px; } +.proto-name { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; } +.proto-val { font-size: 18px; font-weight: 700; margin: 4px 0; } +.proto-rate { font-size: 11px; padding: 2px 6px; border-radius: 3px; display: inline-block; } + +/* Pie charts */ +.pie-wrap { display: flex; gap: 16px; align-items: center; } +.pie { width: 90px; height: 90px; border-radius: 50%; flex-shrink: 0; } +.legend { flex: 1; } +.legend-item { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-size: 12px; } +.legend-dot { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; } +.legend-name { flex: 1; color: var(--dim); } +.legend-val { font-weight: 600; font-feature-settings: "tnum"; } + +/* Tor/Judge cards */ +.host-card { display: flex; justify-content: space-between; align-items: center; } +.host-addr { font-family: ui-monospace, monospace; font-size: 12px; } +.host-stats { font-size: 11px; color: var(--dim); } + +.judge-item { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; font-size: 11px; } +.judge-item + .judge-item { border-top: 1px solid rgba(48,54,61,0.3); } +.judge-name { font-family: ui-monospace, monospace; color: var(--dim); flex: 1; } +.judge-stats { display: flex; gap: 12px; } + +/* Percentile badges */ +.pct-badges { display: flex; gap: 8px; margin-top: 8px; } +.pct-badge { flex: 1; text-align: center; padding: 8px; background: var(--card-alt); border-radius: 6px; } +.pct-label { font-size: 10px; color: var(--dim); text-transform: uppercase; } +.pct-value { font-size: 16px; font-weight: 700; margin-top: 2px; } + +/* Footer */ +.ftr { text-align: center; font-size: 11px; color: var(--dim); padding: 16px 0; margin-top: 8px; border-top: 1px solid var(--border); } +''' + +DASHBOARD_JS = ''' +var $ = function(id) { return document.getElementById(id); }; +var fmt = function(n) { return n == null ? '-' : n.toLocaleString(); }; +var fmtDec = function(n, d) { return n == null ? '-' : n.toFixed(d || 1); }; +var pct = function(n, t) { return t > 0 ? ((n / t) * 100).toFixed(1) : '0.0'; }; + +function fmtTime(s) { + if (!s) return '-'; + var d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60); + if (d > 0) return d + 'd ' + h + 'h'; + if (h > 0) return h + 'h ' + m + 'm'; + return m + 'm ' + Math.floor(s % 60) + 's'; +} + +function fmtMs(ms) { + if (!ms || ms <= 0) return '-'; + if (ms < 1000) return Math.round(ms) + 'ms'; + return (ms / 1000).toFixed(1) + 's'; +} + +function setBar(id, val, max, cls) { + var el = $(id); + if (el) { el.style.width = Math.min(val / max * 100, 100) + '%'; el.className = 'bar ' + (cls || 'grn'); } +} + +function conicGrad(segs) { + var parts = [], deg = 0; + segs.forEach(function(s) { parts.push(s.c + ' ' + deg + 'deg ' + (deg + s.d) + 'deg'); deg += s.d; }); + return 'conic-gradient(' + parts.join(', ') + ')'; +} + +function renderLineChart(id, data, color, maxVal) { + var el = $(id); if (!el || !data || data.length < 2) return; + var w = el.clientWidth || 300, h = el.clientHeight || 80; + var max = maxVal || Math.max.apply(null, data) || 1; + var padY = 5, padX = 2; + var points = data.map(function(v, i) { + var x = padX + (i / (data.length - 1)) * (w - 2 * padX); + var y = h - padY - ((v / max) * (h - 2 * padY)); + return x + ',' + y; + }); + var areaPoints = padX + ',' + (h - padY) + ' ' + points.join(' ') + ' ' + (w - padX) + ',' + (h - padY); + el.innerHTML = '' + + '' + + ''; +} + +function renderHistogram(id, data) { + var el = $(id); if (!el || !data || !data.length) return; + var max = Math.max.apply(null, data.map(function(d) { return d.pct; })) || 1; + var colors = ['#3fb950', '#3fb950', '#58a6ff', '#58a6ff', '#d29922', '#f85149', '#f85149']; + var html = ''; + data.forEach(function(d, i) { + var h = Math.max(4, (d.pct / max) * 100); + var c = colors[Math.min(i, colors.length - 1)]; + html += '
'; + }); + el.innerHTML = html; +} + +function renderLeaderboard(id, data, nameKey, valKey, limit) { + var el = $(id); if (!el) return; + limit = limit || 8; + if (!data || !data.length) { el.innerHTML = '
No data
'; return; } + var html = ''; + data.slice(0, limit).forEach(function(item, i) { + var name = Array.isArray(item) ? item[0] : item[nameKey]; + var val = Array.isArray(item) ? item[1] : item[valKey]; + html += '
' + (i + 1) + '
'; + html += '' + name + '' + fmt(val) + '
'; + }); + el.innerHTML = html; +} + +function update(d) { + $('dot').className = 'dot'; $('statusTxt').textContent = 'Live'; + + // Main stats - db stats are nested under d.db + var db = d.db || {}; + $('working').textContent = fmt(db.working); + $('total').textContent = fmt(db.total); + $('tested').textContent = fmt(d.tested); + $('passed').textContent = fmt(d.passed); + $('failed').textContent = fmt(d.failed); + + // Success rate + var sr = d.success_rate || 0; + $('successRate').textContent = fmtDec(sr, 1) + '%'; + $('successRate').className = 'val-md ' + (sr < 20 ? 'red' : sr < 50 ? 'yel' : 'grn'); + setBar('srBar', sr, 100, sr < 20 ? 'red' : sr < 50 ? 'yel' : 'grn'); + + var rsr = d.recent_success_rate || 0; + $('recentSuccessRate').textContent = fmtDec(rsr, 1) + '%'; + $('recentSuccessRate').className = 'stat-val ' + (rsr < 20 ? 'red' : rsr < 50 ? 'yel' : 'grn'); + + // Rates + $('rate').textContent = fmtDec(d.rate, 2); + $('recentRate').textContent = fmtDec(d.recent_rate, 2); + $('peakRate').textContent = fmtDec(d.peak_rate, 2); + $('passRate').textContent = fmtDec(d.pass_rate, 3); + + // Latency + var lat = d.avg_latency || 0; + $('avgLatency').textContent = fmtMs(lat); + $('minLatency').textContent = fmtMs(d.min_latency); + $('maxLatency').textContent = fmtMs(d.max_latency); + + var pctl = d.latency_percentiles || {}; + $('p50').textContent = fmtMs(pctl.p50); + $('p90').textContent = fmtMs(pctl.p90); + $('p99').textContent = fmtMs(pctl.p99); + + // System + $('threads').textContent = d.threads + '/' + d.max_threads; + setBar('threadBar', d.threads, d.max_threads, 'blu'); + $('queue').textContent = fmt(d.queue_size); + $('uptime').textContent = fmtTime(d.uptime_seconds); + $('checktype').textContent = d.checktype || '-'; + + // Charts + renderLineChart('rateChart', d.rate_history, '#58a6ff', d.peak_rate * 1.1); + renderLineChart('srChart', d.success_rate_history, '#3fb950', 100); + renderHistogram('latencyHisto', d.latency_histogram); + + // Protocol breakdown + var ps = d.proto_stats || {}; + ['http', 'socks4', 'socks5'].forEach(function(p) { + var s = ps[p] || {passed: 0, tested: 0, success_rate: 0}; + $(p + 'Passed').textContent = fmt(s.passed); + $(p + 'Tested').textContent = fmt(s.tested); + var rateEl = $(p + 'Rate'); + rateEl.textContent = fmtDec(s.success_rate, 0) + '%'; + rateEl.className = 'proto-rate ' + (s.success_rate < 20 ? 'tag-err' : s.success_rate < 50 ? 'tag-warn' : 'tag-ok'); + }); + + // Results pie + var passed = d.passed || 0, failed = d.failed || 0, total = passed + failed; + if (total > 0) { + var passedDeg = (passed / total) * 360; + $('resultsPie').style.background = conicGrad([{c:'#3fb950',d:passedDeg},{c:'#f85149',d:360-passedDeg}]); + } + $('passedLeg').textContent = fmt(passed); + $('passedPct').textContent = pct(passed, total) + '%'; + $('failedLeg').textContent = fmt(failed); + $('failedPct').textContent = pct(failed, total) + '%'; + + // Failures breakdown + var fhtml = '', colors = ['#f85149','#db6d28','#d29922','#58a6ff','#a371f7','#39c5cf','#db61a2','#7d8590']; + if (d.failures && Object.keys(d.failures).length > 0) { + var cats = Object.keys(d.failures).sort(function(a,b) { return d.failures[b] - d.failures[a]; }); + var failTotal = cats.reduce(function(s, c) { return s + d.failures[c]; }, 0); + var segs = []; + cats.forEach(function(cat, i) { + var n = d.failures[cat], col = colors[i % colors.length]; + segs.push({c: col, d: (n / failTotal) * 360}); + fhtml += '
'; + fhtml += '' + cat + '' + n + '
'; + }); + $('failPie').style.background = conicGrad(segs); + } else { + $('failPie').style.background = 'var(--border)'; + fhtml = '
No failures yet
'; + } + $('failLegend').innerHTML = fhtml; + + // Leaderboards (session data) + renderLeaderboard('topCountries', d.top_countries_session, 'code', 'count'); + renderLeaderboard('topAsns', d.top_asns_session, 'asn', 'count'); + + // Tor pool + var thtml = ''; + if (d.tor_pool && d.tor_pool.hosts) { + d.tor_pool.hosts.forEach(function(h) { + thtml += '
'; + thtml += '' + h.address + ''; + thtml += '' + (h.healthy ? 'OK' : 'DOWN') + ''; + thtml += '
' + fmtMs(h.latency_ms) + ' / ' + fmtDec(h.success_rate, 0) + '% success
'; + }); + } + $('torPool').innerHTML = thtml || '
No Tor hosts
'; + + // Judges + if (d.judges) { + $('judgesAvail').textContent = d.judges.available + '/' + d.judges.total; + $('judgesCooldown').textContent = d.judges.in_cooldown; + var jhtml = ''; + if (d.judges.top_judges) { + d.judges.top_judges.slice(0, 6).forEach(function(j) { + jhtml += '
' + j.judge + ''; + jhtml += '
' + j.success + '/' + j.tests + ''; + jhtml += '' + fmtDec(j.rate, 0) + '%
'; + }); + } + $('topJudges').innerHTML = jhtml || '
No data
'; + } + + // Database stats + if (d.db) { + var dbs = d.db; + $('dbByProto').innerHTML = ['http', 'socks4', 'socks5'].map(function(p) { + var c = dbs.by_proto && dbs.by_proto[p] || 0; + return '
' + p.toUpperCase() + '' + fmt(c) + '
'; + }).join(''); + + var chtml = ''; + if (dbs.top_countries) { + dbs.top_countries.slice(0, 5).forEach(function(c, i) { + var name = c.code || c[0], cnt = c.count || c[1]; + chtml += '
' + (i + 1) + '
'; + chtml += '' + name + '' + fmt(cnt) + '
'; + }); + } + $('dbCountries').innerHTML = chtml || '
No data
'; + } + + // Scraper/Engine stats + $('engAvail').textContent = fmt(d.engines_available); + $('engBackoff').textContent = fmt(d.engines_backoff); + $('engTotal').textContent = fmt(d.engines_total); + if (d.scraper && d.scraper.engines) { + var ehtml = ''; + d.scraper.engines.slice(0, 5).forEach(function(e, i) { + var statusCls = e.available ? 'tag-ok' : 'tag-warn'; + var statusTxt = e.available ? 'OK' : (e.backoff_remaining > 0 ? e.backoff_remaining + 's' : 'OFF'); + ehtml += '
' + (i + 1) + '
'; + ehtml += '' + e.name + ''; + ehtml += '' + statusTxt + ''; + ehtml += '' + fmt(e.successes) + '
'; + }); + $('topEngines').innerHTML = ehtml || '
No engines
'; + } else { + $('topEngines').innerHTML = '
Scraper disabled
'; + } + + // SSL/TLS stats + if (d.ssl) { + var ssl = d.ssl; + $('sslTested').textContent = fmt(ssl.tested); + $('sslPassed').textContent = fmt(ssl.passed); + $('sslFailed').textContent = fmt(ssl.failed); + var sslRate = ssl.success_rate || 0; + setBar('sslBar', sslRate, 100, sslRate < 50 ? 'red' : sslRate < 80 ? 'yel' : 'grn'); + $('mitmDetected').textContent = fmt(ssl.mitm_detected); + $('mitmDetected').className = 'stat-val ' + (ssl.mitm_detected > 0 ? 'red' : 'grn'); + $('certErrors').textContent = fmt(ssl.cert_errors); + $('certErrors').className = 'stat-val ' + (ssl.cert_errors > 0 ? 'yel' : 'grn'); + } + + $('lastUpdate').textContent = new Date().toLocaleTimeString(); +} + +function fetchStats() { + fetch('/api/stats') + .then(function(r) { return r.json(); }) + .then(update) + .catch(function(e) { $('dot').className = 'dot err'; $('statusTxt').textContent = 'Error'; }); +} + +fetchStats(); +setInterval(fetchStats, 3000); +''' + +DASHBOARD_HTML = ''' + + + + PPF Dashboard + + + + +
+
+

PPF Dashboard

+
+
Connecting
+
Updated: -
+
+
+ + +
+
+
Working Proxies
+
-
+
of - in database
+
+
+
Tests This Session
+
-
+
- passed / - failed
+
+
+
Success Rate
+
-
+
+
+
+
Test Rate
+
-
+
tests/sec average
+
+
+
Uptime
+
-
+
session duration
+
+
+ + +
+
+
Test Rate History (10 min)
+
+
-current
+
-peak
+
-pass/s
+
+
+
+
+
Success Rate History
+
+
-recent
+
+
+
+
+ + +
+
Latency Analysis
+
+
+
Average-
+
Min-
+
Max-
+
+
P50
-
+
P90
-
+
P99
-
+
+
+
+
Response Time Distribution
+
+
+
+
+ + +
+
Protocol Performance
+
+
+
🌐
+
HTTP
+
-
+
of - tested
+
-
+
+
+
🔌
+
SOCKS4
+
-
+
of - tested
+
-
+
+
+
🔒
+
SOCKS5
+
-
+
of - tested
+
-
+
+
+
+ + +
+
+
Test Results
+
+
+
+
Passed--
+
Failed--
+
+
+
+
+
Failure Breakdown
+
+
+
+
+
+
+ + +
+
Geographic Distribution (Session)
+
+
+
Top Countries
+
+
+
+
Top ASNs
+
+
+
+
+ + +
+
+
System
+
Threads-
+
+
Queue Size-
+
Check Type-
+
+
+
Judge Services
+
Available-
+
In Cooldown-
+
Top Performers
+
+
+
+ + +
+
Tor Pool
+
+
+ + +
+
+
Search Engines
+
Available-
+
In Backoff-
+
Total-
+
Top Engines
+
+
+
+
SSL/TLS Security
+
SSL Tests-
+
Passed-
+
Failed-
+
+
MITM Detected-
+
Cert Errors-
+
+
+ + +
+
Database Overview
+
+
+
Working by Protocol
+
+
+
+
Top Countries (All Time)
+
+
+
+
+ +
PPF Proxy Fetcher
+
+ + +''' + class ProxyAPIHandler(BaseHTTPServer.BaseHTTPRequestHandler): """HTTP request handler for proxy API.""" - # Shared database connection (set by server) database = None + stats_provider = None def log_message(self, format, *args): - """Suppress default logging, use our own.""" pass - def send_json(self, data, status=200): - """Send JSON response.""" - body = json.dumps(data, indent=2) + def send_response_body(self, body, content_type, status=200): self.send_response(status) - self.send_header('Content-Type', 'application/json') + self.send_header('Content-Type', content_type) self.send_header('Content-Length', len(body)) - self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Cache-Control', 'no-cache') + if content_type == 'application/json': + self.send_header('Access-Control-Allow-Origin', '*') self.end_headers() self.wfile.write(body) + def send_json(self, data, status=200): + self.send_response_body(json.dumps(data, indent=2), 'application/json', status) + def send_text(self, text, status=200): - """Send plain text response.""" - self.send_response(status) - self.send_header('Content-Type', 'text/plain') - self.send_header('Content-Length', len(text)) - self.end_headers() - self.wfile.write(text) + self.send_response_body(text, 'text/plain', status) + + def send_html(self, html, status=200): + self.send_response_body(html, 'text/html; charset=utf-8', status) + + def send_css(self, css, status=200): + self.send_response_body(css, 'text/css; charset=utf-8', status) + + def send_js(self, js, status=200): + self.send_response_body(js, 'application/javascript; charset=utf-8', status) def do_GET(self): - """Handle GET requests.""" path = self.path.split('?')[0] - - if path == '/': - self.handle_index() - elif path == '/proxies': - self.handle_proxies() - elif path == '/proxies/count': - self.handle_count() - elif path == '/health': - self.handle_health() + routes = { + '/': self.handle_index, + '/dashboard': self.handle_dashboard, + '/static/style.css': self.handle_css, + '/static/dashboard.js': self.handle_js, + '/api/stats': self.handle_stats, + '/proxies': self.handle_proxies, + '/proxies/count': self.handle_count, + '/health': self.handle_health, + } + handler = routes.get(path) + if handler: + handler() else: self.send_json({'error': 'not found'}, 404) def handle_index(self): - """Show available endpoints.""" - endpoints = { + self.send_json({ 'endpoints': { + '/dashboard': 'web dashboard (HTML)', + '/api/stats': 'runtime statistics (JSON)', '/proxies': 'list working proxies (params: limit, proto, country, asn)', '/proxies/count': 'count working proxies', '/health': 'health check', } + }) + + def handle_dashboard(self): + self.send_html(DASHBOARD_HTML) + + def handle_css(self): + self.send_css(DASHBOARD_CSS) + + def handle_js(self): + self.send_js(DASHBOARD_JS) + + def get_db_stats(self): + """Get statistics from database.""" + try: + db = mysqlite.mysqlite(self.database, str) + stats = {} + + # By protocol + rows = db.execute( + 'SELECT proto, COUNT(*) FROM proxylist WHERE failed=0 GROUP BY proto' + ).fetchall() + stats['by_proto'] = {r[0] or 'unknown': r[1] for r in rows} + + # Top countries + rows = db.execute( + 'SELECT country, COUNT(*) as c FROM proxylist WHERE failed=0 AND country IS NOT NULL ' + 'GROUP BY country ORDER BY c DESC LIMIT 10' + ).fetchall() + stats['top_countries'] = [(r[0], r[1]) for r in rows] + + # Top ASNs + rows = db.execute( + 'SELECT asn, COUNT(*) as c FROM proxylist WHERE failed=0 AND asn IS NOT NULL ' + 'GROUP BY asn ORDER BY c DESC LIMIT 10' + ).fetchall() + stats['top_asns'] = [(r[0], r[1]) for r in rows] + + # Age distribution (when added) + now = int(time.time()) + rows = db.execute( + 'SELECT ' + 'SUM(CASE WHEN added > ? THEN 1 ELSE 0 END) as hour, ' + 'SUM(CASE WHEN added > ? THEN 1 ELSE 0 END) as day, ' + 'SUM(CASE WHEN added > ? THEN 1 ELSE 0 END) as week, ' + 'COUNT(*) as total ' + 'FROM proxylist WHERE failed=0', + (now - 3600, now - 86400, now - 604800) + ).fetchone() + if rows: + stats['age'] = { + 'last_hour': rows[0] or 0, + 'last_day': rows[1] or 0, + 'last_week': rows[2] or 0, + 'total': rows[3] or 0, + } + + return stats + except Exception as e: + return {'error': str(e)} + + def handle_stats(self): + stats = { + 'working_proxies': 0, + 'total_proxies': 0, + 'tested': 0, + 'passed': 0, + 'failed': 0, + 'success_rate': 0, + 'rate': 0, + 'pass_rate': 0, + 'recent_rate': 0, + 'recent_success_rate': 0, + 'peak_rate': 0, + 'avg_latency': 0, + 'min_latency': 0, + 'max_latency': 0, + 'latency_percentiles': {'p50': 0, 'p90': 0, 'p99': 0}, + 'latency_histogram': [], + 'uptime_seconds': 0, + 'threads': 0, + 'min_threads': 0, + 'max_threads': 0, + 'queue_size': 0, + 'checktype': '', + 'by_proto': {}, + 'proto_stats': {}, + 'rate_history': [], + 'success_rate_history': [], + 'failures': {}, + 'top_countries': [], + 'top_asns': [], + 'tor_pool': {'hosts': [], 'total_requests': 0, 'success_rate': 0}, + 'judges': None, + 'db_stats': {}, } - self.send_json(endpoints) + + # Database counts + try: + db = mysqlite.mysqlite(self.database, str) + row = db.execute('SELECT COUNT(*) FROM proxylist WHERE failed=0').fetchone() + stats['working_proxies'] = row[0] if row else 0 + row = db.execute('SELECT COUNT(*) FROM proxylist').fetchone() + stats['total_proxies'] = row[0] if row else 0 + except Exception: + pass + + # Runtime stats from provider + if self.stats_provider: + try: + stats.update(self.stats_provider()) + except Exception as e: + _log('stats_provider error: %s' % str(e), 'error') + + # Database-level stats + stats['db_stats'] = self.get_db_stats() + + self.send_json(stats) def handle_proxies(self): - """List working proxies.""" - # Parse query params params = {} if '?' in self.path: - query = self.path.split('?')[1] - for pair in query.split('&'): + for pair in self.path.split('?')[1].split('&'): if '=' in pair: k, v = pair.split('=', 1) params[k] = v @@ -78,10 +853,9 @@ class ProxyAPIHandler(BaseHTTPServer.BaseHTTPRequestHandler): proto = params.get('proto', '') country = params.get('country', '') asn = params.get('asn', '') - format = params.get('format', 'json') + fmt = params.get('format', 'json') - # Build query - sql = 'SELECT ip, port, proto, country, asn FROM proxylist WHERE failed=0' + sql = 'SELECT ip, port, proto, country, asn, avg_latency FROM proxylist WHERE failed=0' args = [] if proto: @@ -94,78 +868,193 @@ class ProxyAPIHandler(BaseHTTPServer.BaseHTTPRequestHandler): sql += ' AND asn=?' args.append(int(asn)) - sql += ' ORDER BY tested DESC LIMIT ?' + sql += ' ORDER BY avg_latency ASC, tested DESC LIMIT ?' args.append(limit) try: db = mysqlite.mysqlite(self.database, str) rows = db.execute(sql, args).fetchall() - if format == 'plain': - # Plain text format: ip:port per line - lines = ['%s:%s' % (row[0], row[1]) for row in rows] - self.send_text('\n'.join(lines)) + if fmt == 'plain': + self.send_text('\n'.join('%s:%s' % (r[0], r[1]) for r in rows)) else: - # JSON format - proxies = [] - for row in rows: - proxies.append({ - 'ip': row[0], - 'port': row[1], - 'proto': row[2], - 'country': row[3], - 'asn': row[4] - }) + proxies = [{ + 'ip': r[0], 'port': r[1], 'proto': r[2], + 'country': r[3], 'asn': r[4], 'latency': r[5] + } for r in rows] self.send_json({'count': len(proxies), 'proxies': proxies}) - except Exception as e: self.send_json({'error': str(e)}, 500) def handle_count(self): - """Count working proxies.""" try: db = mysqlite.mysqlite(self.database, str) row = db.execute('SELECT COUNT(*) FROM proxylist WHERE failed=0').fetchone() - count = row[0] if row else 0 - self.send_json({'count': count}) + self.send_json({'count': row[0] if row else 0}) except Exception as e: self.send_json({'error': str(e)}, 500) def handle_health(self): - """Health check endpoint.""" - self.send_json({'status': 'ok'}) + self.send_json({'status': 'ok', 'timestamp': int(time.time())}) class ProxyAPIServer(threading.Thread): - """Threaded HTTP API server.""" + """Threaded HTTP API server. - def __init__(self, host, port, database): + Uses gevent's WSGIServer when running in a gevent-patched environment, + otherwise falls back to standard BaseHTTPServer. + """ + + def __init__(self, host, port, database, stats_provider=None): threading.Thread.__init__(self) self.host = host self.port = port self.database = database + self.stats_provider = stats_provider self.daemon = True self.server = None + self._stop_event = threading.Event() if not GEVENT_PATCHED else None + + def _wsgi_app(self, environ, start_response): + """WSGI application wrapper for gevent.""" + path = environ.get('PATH_INFO', '/').split('?')[0] + method = environ.get('REQUEST_METHOD', 'GET') + + if method != 'GET': + start_response('405 Method Not Allowed', [('Content-Type', 'text/plain')]) + return [b'Method not allowed'] + + # Route handling + try: + response_body, content_type, status = self._handle_route(path) + status_line = '%d %s' % (status, 'OK' if status == 200 else 'Error') + headers = [ + ('Content-Type', content_type), + ('Content-Length', str(len(response_body))), + ('Cache-Control', 'no-cache'), + ] + if content_type == 'application/json': + headers.append(('Access-Control-Allow-Origin', '*')) + start_response(status_line, headers) + return [response_body.encode('utf-8') if isinstance(response_body, unicode) else response_body] + except Exception as e: + error_body = json.dumps({'error': str(e)}) + start_response('500 Internal Server Error', [ + ('Content-Type', 'application/json'), + ('Content-Length', str(len(error_body))), + ]) + return [error_body] + + def _handle_route(self, path): + """Handle route and return (body, content_type, status).""" + if path == '/': + body = json.dumps({ + 'endpoints': { + '/dashboard': 'web dashboard (HTML)', + '/api/stats': 'runtime statistics (JSON)', + '/proxies': 'list working proxies (params: limit, proto, country, asn)', + '/proxies/count': 'count working proxies', + '/health': 'health check', + } + }, indent=2) + return body, 'application/json', 200 + elif path == '/dashboard': + return DASHBOARD_HTML, 'text/html; charset=utf-8', 200 + elif path == '/static/style.css': + # Use str.format() instead of % to avoid issues with % escaping + css = DASHBOARD_CSS + for key, val in THEME.items(): + css = css.replace('{' + key + '}', val) + return css, 'text/css; charset=utf-8', 200 + elif path == '/static/dashboard.js': + return DASHBOARD_JS, 'application/javascript; charset=utf-8', 200 + elif path == '/api/stats': + stats = {} + if self.stats_provider: + stats = self.stats_provider() + # Add database stats + try: + db = mysqlite.mysqlite(self.database, str) + stats['db'] = self._get_db_stats(db) + except Exception: + pass + return json.dumps(stats, indent=2), 'application/json', 200 + elif path == '/proxies': + try: + db = mysqlite.mysqlite(self.database, str) + rows = db.execute( + 'SELECT proxy, proto, country, asn FROM proxylist WHERE failed=0 LIMIT 100' + ).fetchall() + proxies = [{'proxy': r[0], 'proto': r[1], 'country': r[2], 'asn': r[3]} for r in rows] + return json.dumps({'proxies': proxies}, indent=2), 'application/json', 200 + except Exception as e: + return json.dumps({'error': str(e)}), 'application/json', 500 + elif path == '/proxies/count': + try: + db = mysqlite.mysqlite(self.database, str) + row = db.execute('SELECT COUNT(*) FROM proxylist WHERE failed=0').fetchone() + return json.dumps({'count': row[0] if row else 0}), 'application/json', 200 + except Exception as e: + return json.dumps({'error': str(e)}), 'application/json', 500 + elif path == '/health': + return json.dumps({'status': 'ok', 'timestamp': int(time.time())}), 'application/json', 200 + else: + return json.dumps({'error': 'not found'}), 'application/json', 404 + + def _get_db_stats(self, db): + """Get database statistics.""" + stats = {} + try: + # By protocol + rows = db.execute( + 'SELECT proto, COUNT(*) FROM proxylist WHERE failed=0 GROUP BY proto' + ).fetchall() + stats['by_proto'] = {r[0]: r[1] for r in rows if r[0]} + + # Top countries + rows = db.execute( + 'SELECT country, COUNT(*) as cnt FROM proxylist WHERE failed=0 AND country IS NOT NULL ' + 'GROUP BY country ORDER BY cnt DESC LIMIT 10' + ).fetchall() + stats['top_countries'] = [{'code': r[0], 'count': r[1]} for r in rows] + + # Total counts + row = db.execute('SELECT COUNT(*) FROM proxylist WHERE failed=0').fetchone() + stats['working'] = row[0] if row else 0 + row = db.execute('SELECT COUNT(*) FROM proxylist').fetchone() + stats['total'] = row[0] if row else 0 + except Exception: + pass + return stats def run(self): - """Start the HTTP server.""" ProxyAPIHandler.database = self.database - self.server = BaseHTTPServer.HTTPServer((self.host, self.port), ProxyAPIHandler) - _log('httpd listening on %s:%d' % (self.host, self.port), 'info') - self.server.serve_forever() + ProxyAPIHandler.stats_provider = self.stats_provider + + if GEVENT_PATCHED: + # Use gevent's WSGIServer for proper async handling + self.server = WSGIServer((self.host, self.port), self._wsgi_app, log=None) + _log('httpd listening on %s:%d (gevent)' % (self.host, self.port), 'info') + self.server.serve_forever() + else: + # Standard BaseHTTPServer for non-gevent environments + self.server = BaseHTTPServer.HTTPServer((self.host, self.port), ProxyAPIHandler) + _log('httpd listening on %s:%d' % (self.host, self.port), 'info') + self.server.serve_forever() def stop(self): - """Stop the HTTP server.""" if self.server: - self.server.shutdown() + if GEVENT_PATCHED: + self.server.stop() + else: + self.server.shutdown() if __name__ == '__main__': - # Test server import sys host = '127.0.0.1' port = 8081 - database = 'websites.sqlite' + database = 'data/proxies.sqlite' if len(sys.argv) > 1: database = sys.argv[1] @@ -176,7 +1065,6 @@ if __name__ == '__main__': try: while True: - import time time.sleep(1) except KeyboardInterrupt: server.stop()