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
+
+
+
+
+
+
+
+
+
+
+
Working Proxies
+
-
+
of - in database
+
+
+
Tests This Session
+
-
+
- passed / - failed
+
+
+
+
Test Rate
+
-
+
tests/sec average
+
+
+
Uptime
+
-
+
session duration
+
+
+
+
+
+
+
Test Rate History (10 min)
+
+
- current
+
- peak
+
- pass/s
+
+
+
+
+
Success Rate History
+
+
+
+
+
+
+
+
Latency Analysis
+
+
+
Average -
+
Min -
+
Max -
+
+
+
+
Response Time Distribution
+
+
+
+
+
+
+
+
Protocol Performance
+
+
+
🌐
+
HTTP
+
-
+
of - tested
+
-
+
+
+
🔌
+
SOCKS4
+
-
+
of - tested
+
-
+
+
+
🔒
+
SOCKS5
+
-
+
of - tested
+
-
+
+
+
+
+
+
+
+
+
+
Geographic Distribution (Session)
+
+
+
+
+
+
+
System
+
Threads -
+
+
Queue Size -
+
Check Type -
+
+
+
Judge Services
+
Available -
+
In Cooldown -
+
Top Performers
+
+
+
+
+
+
+
+
+
+
+
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()