#!/usr/bin/env python2 # -*- coding: utf-8 -*- """HTTP API server with advanced web dashboard for PPF.""" import BaseHTTPServer import json import threading import time import os import mysqlite from misc import _log def get_system_stats(): """Collect system resource statistics.""" stats = {} # Load average (1, 5, 15 min) try: load = os.getloadavg() stats['load_1m'] = round(load[0], 2) stats['load_5m'] = round(load[1], 2) stats['load_15m'] = round(load[2], 2) except (OSError, AttributeError): stats['load_1m'] = stats['load_5m'] = stats['load_15m'] = 0 # CPU count try: stats['cpu_count'] = os.sysconf('SC_NPROCESSORS_ONLN') except (ValueError, OSError, AttributeError): stats['cpu_count'] = 1 # Memory from /proc/meminfo (Linux) try: with open('/proc/meminfo', 'r') as f: meminfo = {} for line in f: parts = line.split() if len(parts) >= 2: meminfo[parts[0].rstrip(':')] = int(parts[1]) * 1024 # KB to bytes total = meminfo.get('MemTotal', 0) available = meminfo.get('MemAvailable', meminfo.get('MemFree', 0)) stats['mem_total'] = total stats['mem_available'] = available stats['mem_used'] = total - available stats['mem_pct'] = round((total - available) / total * 100, 1) if total > 0 else 0 except (IOError, KeyError, ZeroDivisionError): stats['mem_total'] = stats['mem_available'] = stats['mem_used'] = 0 stats['mem_pct'] = 0 # Disk usage for data directory try: st = os.statvfs('data' if os.path.exists('data') else '.') total = st.f_blocks * st.f_frsize free = st.f_bavail * st.f_frsize used = total - free stats['disk_total'] = total stats['disk_free'] = free stats['disk_used'] = used stats['disk_pct'] = round(used / total * 100, 1) if total > 0 else 0 except (OSError, ZeroDivisionError): stats['disk_total'] = stats['disk_free'] = stats['disk_used'] = 0 stats['disk_pct'] = 0 # Process stats from /proc/self/status try: with open('/proc/self/status', 'r') as f: for line in f: if line.startswith('VmRSS:'): stats['proc_rss'] = int(line.split()[1]) * 1024 # KB to bytes elif line.startswith('Threads:'): stats['proc_threads'] = int(line.split()[1]) except (IOError, ValueError, IndexError): stats['proc_rss'] = 0 stats['proc_threads'] = 0 return stats def get_db_health(db): """Get database health and statistics.""" stats = {} try: # Database file size db_path = db.path if hasattr(db, 'path') else 'data/proxies.sqlite' if os.path.exists(db_path): stats['db_size'] = os.path.getsize(db_path) else: stats['db_size'] = 0 # Page stats from pragma row = db.execute('PRAGMA page_count').fetchone() stats['page_count'] = row[0] if row else 0 row = db.execute('PRAGMA page_size').fetchone() stats['page_size'] = row[0] if row else 0 row = db.execute('PRAGMA freelist_count').fetchone() stats['freelist_count'] = row[0] if row else 0 # Anonymity breakdown rows = db.execute( 'SELECT anonymity, COUNT(*) FROM proxylist WHERE failed=0 GROUP BY anonymity' ).fetchall() stats['anonymity'] = {r[0] or 'unknown': r[1] for r in rows} # Latency stats row = db.execute( 'SELECT AVG(avg_latency), MIN(avg_latency), MAX(avg_latency) ' 'FROM proxylist WHERE failed=0 AND avg_latency > 0' ).fetchone() if row and row[0]: stats['db_avg_latency'] = round(row[0], 1) stats['db_min_latency'] = round(row[1], 1) stats['db_max_latency'] = round(row[2], 1) else: stats['db_avg_latency'] = stats['db_min_latency'] = stats['db_max_latency'] = 0 # Recent activity now = int(time.time()) row = db.execute( 'SELECT COUNT(*) FROM proxylist WHERE tested > ?', (now - 3600,) ).fetchone() stats['tested_last_hour'] = row[0] if row else 0 row = db.execute( 'SELECT COUNT(*) FROM proxylist WHERE added > ?', (now - 86400,) ).fetchone() stats['added_last_day'] = row[0] if row else 0 # Dead proxies count (permanently dead = -1, failing = positive) row = db.execute( 'SELECT COUNT(*) FROM proxylist WHERE failed = -1' ).fetchone() stats['dead_count'] = row[0] if row else 0 # Failing proxies count (positive fail count but not permanently dead) row = db.execute( 'SELECT COUNT(*) FROM proxylist WHERE failed > 0' ).fetchone() stats['failing_count'] = row[0] if row else 0 except Exception: pass return stats # 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; } } .mode-badge { padding: 4px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } .mode-ssl { background: rgba(63,185,80,0.2); color: var(--green); border: 1px solid var(--green); } .mode-judges { background: rgba(88,166,255,0.2); color: var(--blue); border: 1px solid var(--blue); } .mode-http { background: rgba(210,153,34,0.2); color: var(--yellow); border: 1px solid var(--yellow); } .mode-irc { background: rgba(163,113,247,0.2); color: var(--purple); border: 1px solid var(--purple); } /* System monitor bar */ .sysbar { display: flex; gap: 16px; padding: 8px 12px; background: var(--card); border: 1px solid var(--border); border-radius: 6px; margin-bottom: 16px; font-size: 11px; } .sysbar-item { display: flex; align-items: center; gap: 6px; } .sysbar-lbl { color: var(--dim); } .sysbar-val { font-weight: 600; font-feature-settings: "tnum"; } .sysbar-bar { width: 50px; height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; } .sysbar-fill { height: 100%; border-radius: 2px; transition: width 0.3s; } /* 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 fmtBytes(b) { if (!b || b <= 0) return '-'; var units = ['B', 'KB', 'MB', 'GB', 'TB']; var i = 0; while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; } return b.toFixed(i > 0 ? 1 : 0) + ' ' + units[i]; } 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 setBarColor(el, pct) { if (pct > 90) return 'background:var(--red)'; if (pct > 70) return 'background:var(--yellow)'; return 'background:var(--green)'; } 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'; // Check type badge (prominent display) var ct = d.checktype || 'unknown'; var ctBadge = $('checktypeBadge'); if (ctBadge) { ctBadge.textContent = ct.toUpperCase(); ctBadge.className = 'mode-badge mode-' + ct; } // System monitor bar var sys = d.system || {}; $('sysLoad').textContent = (sys.load_1m || 0) + ' / ' + (sys.cpu_count || 1); $('sysMemVal').textContent = fmtBytes(sys.mem_used) + ' / ' + fmtBytes(sys.mem_total); $('sysMemPct').textContent = (sys.mem_pct || 0) + '%'; var memFill = $('sysMemFill'); if (memFill) { memFill.style.width = (sys.mem_pct || 0) + '%'; memFill.style.cssText = 'width:' + (sys.mem_pct || 0) + '%;' + setBarColor(memFill, sys.mem_pct || 0); } $('sysDiskVal').textContent = fmtBytes(sys.disk_used) + ' / ' + fmtBytes(sys.disk_total); $('sysDiskPct').textContent = (sys.disk_pct || 0) + '%'; var diskFill = $('sysDiskFill'); if (diskFill) { diskFill.style.width = (sys.disk_pct || 0) + '%'; diskFill.style.cssText = 'width:' + (sys.disk_pct || 0) + '%;' + setBarColor(diskFill, sys.disk_pct || 0); } $('sysProcMem').textContent = fmtBytes(sys.proc_rss); // 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); // 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'); } // Anonymity breakdown var dbh = d.db_health || {}; if (dbh.anonymity) { var anonHtml = ''; var anonColors = {elite: 'grn', anonymous: 'blu', transparent: 'yel', unknown: 'dim'}; var anonOrder = ['elite', 'anonymous', 'transparent', 'unknown']; anonOrder.forEach(function(level) { var count = dbh.anonymity[level] || 0; if (count > 0) { anonHtml += '
' + level.charAt(0).toUpperCase() + level.slice(1) + ''; anonHtml += '' + fmt(count) + '
'; } }); $('anonBreakdown').innerHTML = anonHtml || '
No data
'; } // Database health if (dbh.db_size) { $('dbSize').textContent = fmtBytes(dbh.db_size); $('dbTestedHour').textContent = fmt(dbh.tested_last_hour); $('dbAddedDay').textContent = fmt(dbh.added_last_day); $('dbDead').textContent = fmt(dbh.dead_count); } // Tor pool enhanced stats if (d.tor_pool) { var tp = d.tor_pool; $('torTotal').textContent = fmt(tp.total_requests || 0); $('torSuccess').textContent = fmtDec(tp.success_rate || 0, 1) + '%'; $('torHealthy').textContent = (tp.healthy_count || 0) + '/' + (tp.total_count || 0); if (tp.avg_latency) { $('torLatency').textContent = fmtMs(tp.avg_latency); } } $('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: -
Load:-
Memory:-
-
Disk:-
-
Process:-
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
Worker Pool
Active Threads-
Job Queue-
Judge Services
Available-
In Cooldown-
Top Performers
Tor Pool
Total Requests-
Success Rate-
Healthy Nodes-
Avg Latency-
Exit Nodes
Anonymity Levels
Elite = no headers, Anonymous = adds headers, Transparent = reveals IP
Search Engines
Available-
In Backoff-
Total-
Top Engines
SSL/TLS Security
SSL Tests-
Passed-
Failed-
MITM Detected-
Cert Errors-
Database Overview
Database Size
-
Tested (1h)
-
Added (24h)
-
Dead Proxies
-
Working by Protocol
Top Countries (All Time)
PPF Proxy Fetcher
''' class ProxyAPIHandler(BaseHTTPServer.BaseHTTPRequestHandler): """HTTP request handler for proxy API.""" database = None stats_provider = None def log_message(self, format, *args): pass def send_response_body(self, body, content_type, status=200): self.send_response(status) self.send_header('Content-Type', content_type) self.send_header('Content-Length', len(body)) 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): 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): path = self.path.split('?')[0] 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): 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 = {} # 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 # 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'] = [{'code': r[0], 'count': 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] return stats except Exception as e: return {'error': str(e)} def handle_stats(self): stats = {} # 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') # Add system stats stats['system'] = get_system_stats() # Add database stats try: db = mysqlite.mysqlite(self.database, str) stats['db'] = self.get_db_stats() stats['db_health'] = get_db_health(db) except Exception: pass self.send_json(stats) def handle_proxies(self): params = {} if '?' in self.path: for pair in self.path.split('?')[1].split('&'): if '=' in pair: k, v = pair.split('=', 1) params[k] = v limit = min(int(params.get('limit', 100)), 1000) proto = params.get('proto', '') country = params.get('country', '') asn = params.get('asn', '') fmt = params.get('format', 'json') sql = 'SELECT ip, port, proto, country, asn, avg_latency FROM proxylist WHERE failed=0' args = [] if proto: sql += ' AND proto=?' args.append(proto) if country: sql += ' AND country=?' args.append(country.upper()) if asn: sql += ' AND asn=?' args.append(int(asn)) 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 fmt == 'plain': self.send_text('\n'.join('%s:%s' % (r[0], r[1]) for r in rows)) else: 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): try: db = mysqlite.mysqlite(self.database, str) row = db.execute('SELECT COUNT(*) FROM proxylist WHERE failed=0').fetchone() 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): self.send_json({'status': 'ok', 'timestamp': int(time.time())}) class ProxyAPIServer(threading.Thread): """Threaded HTTP API server. 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 system stats stats['system'] = get_system_stats() # Add database stats try: db = mysqlite.mysqlite(self.database, str) stats['db'] = self._get_db_stats(db) stats['db_health'] = get_db_health(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): ProxyAPIHandler.database = self.database 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): if self.server: if GEVENT_PATCHED: self.server.stop() else: self.server.shutdown() if __name__ == '__main__': import sys host = '127.0.0.1' port = 8081 database = 'data/proxies.sqlite' if len(sys.argv) > 1: database = sys.argv[1] _log('starting test server on %s:%d (db: %s)' % (host, port, database), 'info') server = ProxyAPIServer(host, port, database) server.start() try: while True: time.sleep(1) except KeyboardInterrupt: server.stop() _log('server stopped', 'info')