diff --git a/httpd.py b/httpd.py index 330bb73..f5f74ad 100644 --- a/httpd.py +++ b/httpd.py @@ -9,9 +9,44 @@ import threading import time import os import gc +import sys import mysqlite from misc import _log +# Static directories (relative to this file) +_STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static') +_STATIC_LIB_DIR = os.path.join(_STATIC_DIR, 'lib') + +# Content type mapping for static files +_CONTENT_TYPES = { + '.js': 'application/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.woff': 'font/woff', + '.woff2': 'font/woff2', +} + +# Cache for static library files (loaded once at startup) +_LIB_CACHE = {} + +# Cache for dashboard static files (HTML, CSS, JS) +_STATIC_CACHE = {} + +# Optional memory profiling (installed via requirements.txt) +try: + import objgraph + _has_objgraph = True +except ImportError: + _has_objgraph = False + +try: + from pympler import muppy, summary + _has_pympler = True +except ImportError: + _has_pympler = False + # Memory tracking for leak detection _memory_samples = [] _memory_sample_max = 60 # Keep last 60 samples (5 min at 5s intervals) @@ -188,14 +223,78 @@ except ImportError: if GEVENT_PATCHED: from gevent.pywsgi import WSGIServer -# Theme colors - modern dark palette + +def load_static_libs(): + """Load static library files into cache at startup.""" + global _LIB_CACHE + if not os.path.isdir(_STATIC_LIB_DIR): + _log('static/lib directory not found: %s' % _STATIC_LIB_DIR, 'warn') + return + for fname in os.listdir(_STATIC_LIB_DIR): + fpath = os.path.join(_STATIC_LIB_DIR, fname) + if os.path.isfile(fpath): + try: + with open(fpath, 'rb') as f: + _LIB_CACHE[fname] = f.read() + _log('loaded static lib: %s (%d bytes)' % (fname, len(_LIB_CACHE[fname])), 'debug') + except IOError as e: + _log('failed to load %s: %s' % (fname, e), 'warn') + _log('loaded %d static library files' % len(_LIB_CACHE), 'info') + + +def get_static_lib(filename): + """Get a cached static library file.""" + return _LIB_CACHE.get(filename) + + +def load_static_files(theme): + """Load dashboard static files into cache at startup. + + Args: + theme: dict of color name -> color value for CSS variable substitution + """ + global _STATIC_CACHE + files = { + 'dashboard.html': 'static/dashboard.html', + 'map.html': 'static/map.html', + 'mitm.html': 'static/mitm.html', + 'style.css': 'static/style.css', + 'dashboard.js': 'static/dashboard.js', + 'map.js': 'static/map.js', + 'mitm.js': 'static/mitm.js', + } + for key, relpath in files.items(): + fpath = os.path.join(os.path.dirname(os.path.abspath(__file__)), relpath) + if os.path.isfile(fpath): + try: + with open(fpath, 'rb') as f: + content = f.read() + # Apply theme substitution to CSS + if key == 'style.css' and theme: + for name, val in theme.items(): + content = content.replace('{' + name + '}', val) + _STATIC_CACHE[key] = content + _log('loaded static file: %s (%d bytes)' % (key, len(content)), 'debug') + except IOError as e: + _log('failed to load %s: %s' % (fpath, e), 'warn') + else: + _log('static file not found: %s' % fpath, 'warn') + _log('loaded %d dashboard static files' % len(_STATIC_CACHE), 'info') + + +def get_static_file(filename): + """Get a cached dashboard static file.""" + return _STATIC_CACHE.get(filename) + + +# Theme colors - dark tiles on lighter background THEME = { - 'bg': '#0d1117', - 'card': '#161b22', - 'card_alt': '#1c2128', - 'border': '#30363d', - 'text': '#e6edf3', - 'dim': '#7d8590', + 'bg': '#1e2738', + 'card': '#181f2a', + 'card_alt': '#212a36', + 'border': '#3a4858', + 'text': '#e8eef5', + 'dim': '#8b929b', 'green': '#3fb950', 'red': '#f85149', 'yellow': '#d29922', @@ -204,870 +303,9 @@ THEME = { 'cyan': '#39c5cf', 'orange': '#db6d28', 'pink': '#db61a2', + 'map_bg': '#1e2738', # Match dashboard background } -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; -} -a { color: var(--blue); text-decoration: none; } -a:hover { text-decoration: underline; } -h1 { font-size: 18px; font-weight: 600; color: var(--text); margin-bottom: 16px; } -h2 { font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 12px; } -h3 { font-size: 13px; font-weight: 600; color: var(--dim); margin-bottom: 8px; } -.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-profile { background: rgba(255,165,0,0.2); color: #ffa500; border: 1px solid #ffa500; margin-left: 6px; } -.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; } - -/* Map page */ -.nav { margin-bottom: 16px; font-size: 12px; } -.map-stats { margin-bottom: 16px; color: var(--dim); font-size: 12px; padding: 8px 12px; background: var(--card); border: 1px solid var(--border); border-radius: 6px; } -.country-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 8px; } -.country { padding: 12px 8px; border-radius: 6px; text-align: center; background: var(--card); border: 1px solid var(--border); transition: transform 0.15s, box-shadow 0.15s; } -.country:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); } -.country .code { font-weight: bold; font-size: 1.1em; letter-spacing: 0.5px; } -.country .count { font-size: 0.85em; color: var(--dim); margin-top: 4px; font-feature-settings: "tnum"; } -.country.t1 { background: #0d4429; border-color: #238636; } -.country.t1 .code { color: #7ee787; } -.country.t2 { background: #1a3d2e; border-color: #2ea043; } -.country.t2 .code { color: #7ee787; } -.country.t3 { background: #1f3d2a; border-color: #3fb950; } -.country.t3 .code { color: #56d364; } -.country.t4 { background: #2a4a35; border-color: #56d364; } -.country.t4 .code { color: #3fb950; } -.country.t5 { background: #35573f; border-color: #7ee787; } -.country.t5 .code { color: #3fb950; } -.map-legend { display: flex; gap: 16px; margin-top: 20px; flex-wrap: wrap; padding: 12px; background: var(--card); border: 1px solid var(--border); border-radius: 6px; } -.map-legend .legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--dim); } -.map-legend .legend-dot { width: 12px; height: 12px; border-radius: 3px; } - -/* 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 = 'org:Cloudflare organization nameissuer:DigiCert certificate issuercn:*.example.com common nameproxy:192.168.1 proxy IP addressfp:a1b2c3 fingerprint prefixserial:12345 serial numberexpires:2024 expiration yearexpired:yes show expired certscloudflare search all fields"security proxy" exact phraseorg: or proxy: for precise results' + escapeHtml(field.prefix) + '';
+ html += '' + escapeHtml(field.desc) + '';
+ html += '