#!/usr/bin/env python3 """ flare.py — Ephemeral script monitoring server + dashboard Pure stdlib, no dependencies. Drop alongside flare.html and run. Serves per-run resources under randomized paths: // → flare.html dashboard (auto-configured via query params) //wrapper.* → curl-able wrappers with credentials pre-baked //events → SSE stream / → signed event ingest Usage: python3 flare.py [--port 8080] [--host 0.0.0.0] [--token TOKEN] [--secret SECRET] [--get-path PATH] [--post-path PATH] [--html /path/to/flare.html] Generates random token, secret, topic, and paths if not provided. Kill with Ctrl+C — all data lives in memory only. """ import argparse import hashlib import hmac import json import logging import os import queue import secrets import socket import sys import threading import time from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path from socketserver import ThreadingMixIn from typing import Dict, List from urllib.parse import urlparse, parse_qs # Config (set from CLI args at startup) CFG: Dict = {} # In-memory state messages: Dict[str, List[str]] = {} messages_lock = threading.Lock() subscribers: Dict[str, List[queue.Queue]] = {} subscribers_lock = threading.Lock() def _topic_init(topic: str): with messages_lock: messages.setdefault(topic, []) with subscribers_lock: subscribers.setdefault(topic, []) def publish(topic: str, blob: dict): """Store message and fan out to all SSE subscribers.""" _topic_init(topic) sse_event = { "id": secrets.token_hex(6), "time": int(time.time()), "expires": int(time.time()) + 43200, "event": "message", "topic": topic, "title": blob.get("title", ""), "message": blob.get("message", ""), "tags": blob.get("tags", []), "priority": blob.get("priority", 3), } line = "data: " + json.dumps(sse_event) + "\n\n" with messages_lock: messages[topic].append(line) with subscribers_lock: for q in subscribers.get(topic, []): try: q.put_nowait(line) except queue.Full: pass def subscribe(topic: str) -> "queue.Queue[str]": _topic_init(topic) q: queue.Queue = queue.Queue(maxsize=256) with subscribers_lock: subscribers[topic].append(q) return q def unsubscribe(topic: str, q: queue.Queue): with subscribers_lock: try: subscribers[topic].remove(q) except (ValueError, KeyError): pass def history(topic: str) -> List[str]: with messages_lock: return list(messages.get(topic, [])) def _skip_ws(text: str, idx: int) -> int: while idx < len(text) and text[idx] in " \t\r\n": idx += 1 return idx def _extract_raw_content_json(envelope_text: str) -> str: """Return the exact JSON substring used for envelope.content.""" decoder = json.JSONDecoder() idx = _skip_ws(envelope_text, 0) obj, _ = decoder.raw_decode(envelope_text, idx) if not isinstance(obj, dict): raise ValueError("Envelope must be a JSON object") idx = _skip_ws(envelope_text, idx) if idx >= len(envelope_text) or envelope_text[idx] != "{": raise ValueError("Envelope must begin with '{'") idx = _skip_ws(envelope_text, idx + 1) key, idx = decoder.raw_decode(envelope_text, idx) if key != "content": raise ValueError("Envelope must begin with a content field") idx = _skip_ws(envelope_text, idx) if idx >= len(envelope_text) or envelope_text[idx] != ":": raise ValueError("Malformed content field") idx = _skip_ws(envelope_text, idx + 1) _, end = decoder.raw_decode(envelope_text, idx) return envelope_text[idx:end] def _validate_event_blob(blob: dict) -> None: """Raise ValueError when an incoming event fails validation.""" secret = CFG.get("secret", "") topic = blob.get("topic") if not topic: raise ValueError("Missing topic") if not secret: return envelope_text = blob.get("message") if not isinstance(envelope_text, str) or not envelope_text.strip(): raise ValueError("Missing signed message envelope") try: envelope = json.loads(envelope_text) except json.JSONDecodeError as exc: raise ValueError("Invalid signed message envelope") from exc if not isinstance(envelope, dict): raise ValueError("Signed message envelope must be an object") content = envelope.get("content") nonce = envelope.get("nonce") supplied_hmac = envelope.get("hmac") if not isinstance(content, dict): raise ValueError("Signed content must be an object") if not isinstance(nonce, str) or not nonce: raise ValueError("Missing nonce") if not isinstance(supplied_hmac, str) or not supplied_hmac: raise ValueError("Missing hmac") content_topic = content.get("topic") if content_topic != topic: raise ValueError("Topic mismatch between envelope and request body") raw_candidates = [] try: raw_candidates.append(_extract_raw_content_json(envelope_text)) except ValueError: pass canonical_content = json.dumps( content, sort_keys=True, separators=(",", ":"), ensure_ascii=False, ) if canonical_content not in raw_candidates: raw_candidates.append(canonical_content) secret_bytes = secret.encode("utf-8") nonce_bytes = nonce.encode("utf-8") for candidate in raw_candidates: expected_hmac = hmac.new( secret_bytes, candidate.encode("utf-8") + nonce_bytes, hashlib.sha256, ).hexdigest() if hmac.compare_digest(expected_hmac, supplied_hmac): return raise ValueError("Invalid event HMAC") def _event_meta(blob: dict) -> dict: """Best-effort metadata extraction for logging.""" meta = { "topic": blob.get("topic", ""), "host": "", "seq": "", "title": blob.get("title", ""), } envelope_text = blob.get("message") if not isinstance(envelope_text, str) or not envelope_text.strip(): return meta try: envelope = json.loads(envelope_text) except json.JSONDecodeError: return meta if not isinstance(envelope, dict): return meta content = envelope.get("content") if not isinstance(content, dict): return meta meta["host"] = content.get("host", "") meta["seq"] = content.get("seq", "") meta["title"] = content.get("title", meta["title"]) return meta # Wrapper templates — credentials header is dynamic, body is static raw string def _creds_header_bash(url, topic, token, secret): """Bash-style credential exports.""" insecure = "\nexport FLARE_INSECURE=1" if url.startswith("http://") else "" return ( f'export FLARE_URL="{url}"\n' f'export FLARE_TOPIC="{topic}"\n' f'export FLARE_TOKEN="{token}"\n' f'export FLARE_SECRET="{secret}"' f'{insecure}\n' ) def _creds_header_ps1(url, topic, token, secret): """PowerShell credential assignments.""" return ( f'$env:FLARE_URL = "{url}"\n' f'$env:FLARE_TOPIC = "{topic}"\n' f'$env:FLARE_TOKEN = "{token}"\n' f'$env:FLARE_SECRET = "{secret}"\n' ) # wrapper.sh body (original bash + embedded python) _WRAPPER_SH_BODY = r''' _FLARE_SRC=$(cat << '__FLARE_EOF__' #!/usr/bin/env python3 import hashlib as h,hmac as _h,json as j,os,socket,ssl,sys,time,urllib.error as ue,urllib.request as ur _E=os.environ.get _cfg=None def _d(m):print(f"flare: {m}",file=sys.stderr);sys.exit(1) def _w(m):print(f"flare: {m}",file=sys.stderr) def _ssl(): if _E("FLARE_INSECURE","").strip()=="1": c=ssl.create_default_context();c.check_hostname=False;c.verify_mode=ssl.CERT_NONE;return c def _config(): global _cfg if _cfg:return _cfg url=_E("FLARE_URL","").strip();topic=_E("FLARE_TOPIC","").strip();token=_E("FLARE_TOKEN","").strip() if not url or not topic or not token:_d("FLARE_URL, FLARE_TOPIC, FLARE_TOKEN required") raw_detail=_E("FLARE_DETAIL","") try:detail=j.loads(raw_detail) if raw_detail else {} except j.JSONDecodeError:detail={} _cfg={"url":url,"topic":topic,"token":token,"secret":_E("FLARE_SECRET","").strip(), "error":bool(_E("FLARE_ERROR")),"complete":bool(_E("FLARE_COMPLETE")), "title":_E("FLARE_TITLE","(no title)"),"detail":detail} return _cfg def _sign(cnt,secret): nonce=os.urandom(16).hex() canon=j.dumps(cnt,sort_keys=True,separators=(",",":"),ensure_ascii=False) mac=_h.new(secret.encode(),(canon+nonce).encode("utf-8"),h.sha256).hexdigest() return nonce,mac def _post(cfg,pay): url=cfg["url"].rstrip("/")+"/";data=j.dumps(pay,separators=(",",":")).encode() req=ur.Request(url,data=data,method="POST",headers={ "Content-Type":"application/json","Authorization":f"Bearer {cfg['token']}"}) ctx=_ssl() for a in range(3): try: with ur.urlopen(req,timeout=10,context=ctx) as r: if r.status==200:return True _w(f"HTTP {r.status}") except ue.HTTPError as e: _w(f"HTTP {e.code}: {e.reason}") if e.code<500:return False except Exception as e:_w(f"Error: {e}") if a<2:_w(f"Retrying ({a+2}/3)...");time.sleep(2) return False def send(): cfg=_config() cnt={"topic":cfg["topic"],"seq":int(time.time()*1000)&0xFFFFFFFF, "timestamp":int(time.time()),"host":socket.getfqdn(), "error":1 if cfg["error"] else 0,"complete":1 if cfg["complete"] else 0, "title":cfg["title"],"detail":cfg["detail"]} env={"content":cnt} if cfg["secret"]:env["nonce"],env["hmac"]=_sign(cnt,cfg["secret"]) tag="rotating_light" if cfg["error"] else "white_check_mark" if cfg["complete"] else "hourglass_flowing_sand" ok=_post(cfg,{"topic":cfg["topic"],"title":cfg["title"], "message":j.dumps(env,separators=(",",":")), "tags":[tag],"priority":4 if cfg["error"] else 3}) sgn=" [signed]" if cfg["secret"] else "" lbl=" [ERROR]" if cfg["error"] else " [COMPLETE]" if cfg["complete"] else "" print(f"{'OK' if ok else 'FAIL'} flare{lbl}{sgn}: {cfg['title']} -> {cfg['url']}/{cfg['topic']}",file=sys.stderr) return ok sys.exit(0 if send() else 1) __FLARE_EOF__ ) _mon() { local title="$1"; shift local _c="" _e="" _d="" while [[ $# -gt 0 ]]; do case "$1" in --complete) _c=1 ; shift ;; --error) _e=1 ; shift ;; --detail) _d="$2" ; shift 2 ;; *) shift ;; esac done export FLARE_TITLE="$title" export FLARE_COMPLETE="$_c" export FLARE_ERROR="$_e" export FLARE_DETAIL="$_d" echo "$_FLARE_SRC" | python3 local rc=$? unset FLARE_TITLE FLARE_COMPLETE FLARE_ERROR FLARE_DETAIL return $rc } mon() { _mon "$1"; } mon_complete() { _mon "$1" --complete; } mon_error() { _mon "$1" --error --complete; } mon_detail() { _mon "$1" --detail "$2"; } export -f _mon mon mon_complete mon_error mon_detail ''' # wrapper.bash body (pure bash, no python) _WRAPPER_BASH_BODY = r''' _flare_post() { local json="$1" local url="${FLARE_URL%/}/" local curl_opts=( -s -o /dev/null -w '%{http_code}' -X POST -H "Content-Type: application/json" -H "Authorization: Bearer ${FLARE_TOKEN}" --max-time 10 ) [[ "${FLARE_INSECURE:-}" == "1" ]] && curl_opts+=( -k ) local attempt code for attempt in 1 2 3; do code=$(curl "${curl_opts[@]}" -d "$json" "$url" 2>/dev/null) || code="000" [[ "$code" == "200" ]] && return 0 [[ "$code" =~ ^4 ]] && { echo "flare: HTTP $code (not retrying)" >&2; return 1; } [[ $attempt -lt 3 ]] && { echo "flare: HTTP $code, retrying ($((attempt+1))/3)..." >&2; sleep 2; } done echo "flare: failed after 3 attempts (last HTTP $code)" >&2 return 1 } _flare_escape_json() { local s="$1" s="${s//\\/\\\\}" s="${s//\"/\\\"}" s="${s//$'\n'/\\n}" s="${s//$'\r'/\\r}" s="${s//$'\t'/\\t}" printf '%s' "$s" } _flare_hmac() { printf '%s' "$1" | openssl dgst -sha256 -hmac "$2" -hex 2>/dev/null | sed 's/^.* //' } _mon() { local title="$1"; shift local _complete="" _error="" _detail_raw="" while [[ $# -gt 0 ]]; do case "$1" in --complete) _complete=1 ; shift ;; --error) _error=1 ; shift ;; --detail) _detail_raw="$2"; shift 2 ;; *) shift ;; esac done local host host=$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "unknown") local ts ts=$(date +%s) local seq=$(( (ts * 1000 + RANDOM) & 0xFFFFFFFF )) local title_esc title_esc=$(_flare_escape_json "$title") local detail="{}" [[ -n "$_detail_raw" ]] && detail="$_detail_raw" local error_val=0 complete_val=0 [[ -n "$_error" ]] && error_val=1 [[ -n "$_complete" ]] && complete_val=1 local content content=$(printf '{"complete":%d,"detail":%s,"error":%d,"host":"%s","seq":%d,"timestamp":%d,"title":"%s","topic":"%s"}' \ "$complete_val" "$detail" "$error_val" "$(_flare_escape_json "$host")" "$seq" "$ts" "$title_esc" "$(_flare_escape_json "$FLARE_TOPIC")") local envelope if [[ -n "${FLARE_SECRET:-}" ]]; then local nonce nonce=$(openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | od -A n -t x1 | tr -d ' \n') local mac mac=$(_flare_hmac "${content}${nonce}" "$FLARE_SECRET") envelope=$(printf '{"content":%s,"hmac":"%s","nonce":"%s"}' "$content" "$mac" "$nonce") else envelope=$(printf '{"content":%s}' "$content") fi local tag="hourglass_flowing_sand" priority=3 if [[ $error_val -eq 1 ]]; then tag="rotating_light"; priority=4 elif [[ $complete_val -eq 1 ]]; then tag="white_check_mark" fi local envelope_esc envelope_esc=$(_flare_escape_json "$envelope") local payload payload=$(printf '{"message":"%s","priority":%d,"tags":["%s"],"title":"%s","topic":"%s"}' \ "$envelope_esc" "$priority" "$tag" "$title_esc" "$(_flare_escape_json "$FLARE_TOPIC")") _flare_post "$payload" local rc=$? local sgn="" lbl="" [[ -n "${FLARE_SECRET:-}" ]] && sgn=" [signed]" [[ $error_val -eq 1 ]] && lbl=" [ERROR]" [[ $complete_val -eq 1 && $error_val -eq 0 ]] && lbl=" [COMPLETE]" echo "$( [[ $rc -eq 0 ]] && echo OK || echo FAIL ) flare${lbl}${sgn}: ${title} -> ${FLARE_URL}/${FLARE_TOPIC}" >&2 return $rc } mon() { _mon "$1"; } mon_complete() { _mon "$1" --complete; } mon_error() { _mon "$1" --error --complete; } mon_detail() { _mon "$1" --detail "$2"; } export -f _flare_post _flare_escape_json _flare_hmac _mon mon mon_complete mon_error mon_detail ''' # wrapper.ps1 body (pure PowerShell) _WRAPPER_PS1_BODY = r''' if ($env:FLARE_URL -match '^http://') { # Skip cert validation for plain HTTP / self-signed if (-not ([System.Management.Automation.PSTypeName]'TrustAll').Type) { Add-Type @" using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; public class TrustAll { public static void Enable() { ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; } } "@ } [TrustAll]::Enable() } function _flare_post { param([string]$Json) $url = $env:FLARE_URL.TrimEnd('/') + '/' $headers = @{ 'Content-Type' = 'application/json' 'Authorization' = "Bearer $env:FLARE_TOKEN" } $bytes = [System.Text.Encoding]::UTF8.GetBytes($Json) for ($attempt = 1; $attempt -le 3; $attempt++) { try { $r = Invoke-WebRequest -Uri $url -Method POST -Headers $headers ` -Body $bytes -TimeoutSec 10 -UseBasicParsing -ErrorAction Stop if ($r.StatusCode -eq 200) { return $true } Write-Host "flare: HTTP $($r.StatusCode)" -ForegroundColor Yellow } catch { $code = 0 if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode } if ($code -ge 400 -and $code -lt 500) { Write-Host "flare: HTTP $code (not retrying)" -ForegroundColor Red return $false } Write-Host "flare: $($_.Exception.Message)" -ForegroundColor Yellow } if ($attempt -lt 3) { Write-Host "flare: retrying ($($attempt+1)/3)..." -ForegroundColor Yellow Start-Sleep -Seconds 2 } } Write-Host "flare: failed after 3 attempts" -ForegroundColor Red return $false } function _flare_hmac { param([string]$Message, [string]$Secret) $hmac = New-Object System.Security.Cryptography.HMACSHA256 $hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($Secret) $hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Message)) return ($hash | ForEach-Object { $_.ToString('x2') }) -join '' } function _flare_escape_json { param([string]$s) $s = $s -replace '\\', '\\' -replace '"', '\"' $s = $s -replace "`n", '\n' -replace "`r", '\r' -replace "`t", '\t' return $s } function _mon { param( [Parameter(Mandatory)][string]$Title, [switch]$Complete, [switch]$Error, [string]$Detail = '' ) $fqdn = try { [System.Net.Dns]::GetHostEntry('').HostName } catch { $env:COMPUTERNAME } $epoch = [int][DateTimeOffset]::UtcNow.ToUnixTimeSeconds() $seq = [long](([long][DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + (Get-Random -Max 1000)) % 4294967296) $err = if ($Error) { 1 } else { 0 } $comp = if ($Complete) { 1 } else { 0 } $detail_json = if ($Detail) { $Detail } else { '{}' } # Canonical key order (alphabetical) for HMAC $host_esc = _flare_escape_json $fqdn $title_esc = _flare_escape_json $Title $topic_esc = _flare_escape_json $env:FLARE_TOPIC $content = "{`"complete`":$comp,`"detail`":$detail_json,`"error`":$err," + "`"host`":`"$host_esc`",`"seq`":$seq,`"timestamp`":$epoch," + "`"title`":`"$title_esc`",`"topic`":`"$topic_esc`"}" if ($env:FLARE_SECRET) { $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() $nb = New-Object byte[] 16; $rng.GetBytes($nb) $nonce = ($nb | ForEach-Object { $_.ToString('x2') }) -join '' $mac = _flare_hmac -Message ($content + $nonce) -Secret $env:FLARE_SECRET $envelope = "{`"content`":$content,`"hmac`":`"$mac`",`"nonce`":`"$nonce`"}" } else { $envelope = "{`"content`":$content}" } $tag = 'hourglass_flowing_sand'; $pri = 3 if ($err -eq 1) { $tag = 'rotating_light'; $pri = 4 } elseif ($comp -eq 1) { $tag = 'white_check_mark' } $env_esc = _flare_escape_json $envelope $payload = "{`"message`":`"$env_esc`",`"priority`":$pri," + "`"tags`":[`"$tag`"],`"title`":`"$title_esc`"," + "`"topic`":`"$topic_esc`"}" $ok = _flare_post -Json $payload $sgn = if ($env:FLARE_SECRET) { ' [signed]' } else { '' } $lbl = if ($err -eq 1) { ' [ERROR]' } elseif ($comp -eq 1) { ' [COMPLETE]' } else { '' } $st = if ($ok) { 'OK' } else { 'FAIL' } Write-Host "$st flare${lbl}${sgn}: $Title -> $env:FLARE_URL/$env:FLARE_TOPIC" } function mon { param([string]$Title) _mon -Title $Title } function mon_complete { param([string]$Title) _mon -Title $Title -Complete } function mon_error { param([string]$Title) _mon -Title $Title -Error -Complete } function mon_detail { param([string]$Title, [string]$Detail) _mon -Title $Title -Detail $Detail } ''' # Render functions def render_wrapper_sh() -> str: """wrapper.sh — bash + embedded Python (original, most compatible).""" source_url, post_url, topic, token, secret = _cred_tuple() header = ( f'#!/usr/bin/env bash\n' f'# ── flare wrapper (bash+python) ── sourced from {source_url}/wrapper.sh\n' f'# Requires: bash, python3\n' f'# Usage: eval "$( curl -s {source_url}/wrapper.sh )"\n\n' ) return header + _creds_header_bash(post_url, topic, token, secret) + _WRAPPER_SH_BODY def render_wrapper_bash() -> str: """wrapper.bash — pure bash, no Python needed.""" source_url, post_url, topic, token, secret = _cred_tuple() header = ( f'#!/usr/bin/env bash\n' f'# ── flare wrapper (pure bash) ── sourced from {source_url}/wrapper.bash\n' f'# Requires: bash 4+, curl, openssl\n' f'# Usage: eval "$( curl -s {source_url}/wrapper.bash )"\n\n' ) return header + _creds_header_bash(post_url, topic, token, secret) + _WRAPPER_BASH_BODY def render_wrapper_ps1() -> str: """wrapper.ps1 — pure PowerShell, no external dependencies.""" source_url, post_url, topic, token, secret = _cred_tuple() header = ( f'# ── flare wrapper (PowerShell) ── sourced from {source_url}/wrapper.ps1\n' f'# Requires: PowerShell 5.1+ (Windows) or pwsh 7+ (cross-platform)\n' f'# Usage: iex (irm {source_url}/wrapper.ps1)\n\n' ) return header + _creds_header_ps1(post_url, topic, token, secret) + _WRAPPER_PS1_BODY def _cred_tuple(): return ( CFG["get_base_url"], CFG["post_url"], CFG["default_topic"], CFG["token"], CFG.get("secret", ""), ) def _normalize_path(raw: str, label: str = "Path") -> str: cleaned = (raw or "").strip().strip("/") if not cleaned: raise ValueError(f"{label} must not be empty") return "/" + cleaned def _normalize_get_path(raw: str) -> str: return _normalize_path(raw, "GET path") def _normalize_post_path(raw: str) -> str: return _normalize_path(raw, "POST path") def _strip_get_base(request_path: str) -> str | None: base = CFG.get("get_base_path", "") if not base: return None if request_path == base or request_path == base + "/": return "/" prefix = base + "/" if request_path.startswith(prefix): return "/" + request_path[len(prefix):].lstrip("/") return None # HTTP handler class Handler(BaseHTTPRequestHandler): def log_message(self, fmt, *args): logging.debug(f"{self.address_string()} {fmt % args}") def _send(self, code: int, ctype: str, body: bytes): self.send_response(code) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(body) def _auth(self, qs: dict = None) -> bool: token = CFG.get("token", "") if not token: return True if self.headers.get("Authorization", "") == f"Bearer {token}": return True if qs: import base64 raw = (qs.get("auth") or [None])[0] if raw: try: decoded = base64.b64decode(raw + "==").decode("utf-8") if decoded == f"Bearer {token}": return True except Exception: pass return False def do_OPTIONS(self): self.send_response(204) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Headers", "Authorization, Content-Type") self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") self.end_headers() def do_GET(self): parsed = urlparse(self.path) raw_path = parsed.path or "/" qs = parse_qs(parsed.query) inner_path = _strip_get_base(raw_path) if inner_path is None: self._send(404, "text/plain", b"Not found") return path = inner_path.rstrip("/") or "/" # ── Serve wrappers (no auth — credentials are already scoped) ── wrapper_map = { "/wrapper.sh": ("text/plain; charset=utf-8", render_wrapper_sh), "/wrapper.bash": ("text/plain; charset=utf-8", render_wrapper_bash), "/wrapper.ps1": ("text/plain; charset=utf-8", render_wrapper_ps1), } if path in wrapper_map: ctype, renderer = wrapper_map[path] body = renderer().encode() self._send(200, ctype, body) return # ── Serve flare.html ── if path in ("/", "/flare.html"): html_path = CFG.get("html") if html_path and Path(html_path).exists(): body = Path(html_path).read_bytes() self._send(200, "text/html; charset=utf-8", body) else: self._send(404, "text/plain", b"flare.html not found") return # ── SSE stream: GET //events?topic=... ── if path == "/events": topic = (qs.get("topic") or [None])[0] if topic: if not self._auth(qs): self._send(401, "text/plain", b"Unauthorized") return self._stream_sse(topic, qs) return self._send(404, "text/plain", b"Not found") def _stream_sse(self, topic: str, qs: dict): since_all = qs.get("since", [""])[0] == "all" self.send_response(200) self.send_header("Content-Type", "text/event-stream") self.send_header("Cache-Control", "no-cache") self.send_header("X-Accel-Buffering", "no") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() q = subscribe(topic) try: if since_all: for line in history(topic): self.wfile.write(line.encode()) self.wfile.flush() while True: try: line = q.get(timeout=25) self.wfile.write(line.encode()) self.wfile.flush() except queue.Empty: self.wfile.write(b": ping\n\n") self.wfile.flush() except (BrokenPipeError, ConnectionResetError): pass finally: unsubscribe(topic, q) def do_POST(self): parsed = urlparse(self.path) req_path = (parsed.path or "/").rstrip("/") or "/" post_path = (CFG.get("post_path") or "/").rstrip("/") or "/" if req_path != post_path: self._send(404, "text/plain", b"Not found") return if not self._auth(): self._send(401, "text/plain", b"Unauthorized") return length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) try: blob = json.loads(body) except json.JSONDecodeError: self._send(400, "text/plain", b"Invalid JSON") return topic = blob.get("topic") if not topic: self._send(400, "text/plain", b"Missing topic") return meta = _event_meta(blob) try: _validate_event_blob(blob) if CFG.get("verbose"): logging.debug( "%s topic=%s host=%s seq=%s title=%r", "HMAC validation OK" if CFG.get("secret") else "Unsigned mode accepted event", meta.get("topic", ""), meta.get("host", ""), meta.get("seq", ""), meta.get("title", ""), ) except ValueError as exc: if CFG.get("verbose"): logging.debug( "HMAC validation FAILED topic=%s host=%s seq=%s title=%r reason=%s", meta.get("topic", ""), meta.get("host", ""), meta.get("seq", ""), meta.get("title", ""), exc, ) self._send(400, "text/plain", str(exc).encode("utf-8")) return publish(topic, blob) resp = json.dumps({"id": secrets.token_hex(6), "time": int(time.time())}).encode() self._send(200, "application/json", resp) class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True # Startup def _local_ip() -> str: try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() return ip except Exception: return "127.0.0.1" def _generate_topic() -> str: return secrets.token_urlsafe(18) def print_banner(host: str, port: int): ip = _local_ip() if host in ("0.0.0.0", "") else host root_url = f"http://{ip}:{port}" get_url = CFG["get_base_url"] post_url = CFG["post_url"] token = CFG["token"] secret = CFG.get("secret", "") topic = CFG["default_topic"] dash_params = f"topic={topic}&token={token}" if secret: dash_params += f"&secret={secret}" dash_url = f"{get_url}/?{dash_params}" div = "─" * 66 print() print(" ███████╗██╗ █████╗ ██████╗ ███████╗") print(" ██╔════╝██║ ██╔══██╗██╔══██╗██╔════╝") print(" █████╗ ██║ ███████║██████╔╝█████╗ ") print(" ██╔══╝ ██║ ██╔══██║██╔══██╗██╔══╝ ") print(" ██║ ███████╗██║ ██║██║ ██║███████╗") print(" ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝") print() print(div) print() print(" Dashboard") print(f" {dash_url}") print() print(div) print() print(" Source the wrapper in any script:") print() print(" bash+py") print(f' eval "$( curl -s {get_url}/wrapper.sh )"') print() print(" bash") print(f' eval "$( curl -s {get_url}/wrapper.bash )"') print() print(" pwsh") print(f' iex (irm {get_url}/wrapper.ps1)') print() print(" Then call:") print(' mon "starting backup"') print(' mon_detail "installing" \'{"packages":["nginx","fail2ban"]}\'') print(' mon_complete "all done"') print(' mon_error "something broke"') print() print(div) print() print(" POST target") print(f" {post_url}/") print() print(div) print() print(" ENV vars (if you prefer manual export):") print() print(f' export FLARE_URL="{post_url}"') print(f' export FLARE_TOPIC="{topic}"') print(f' export FLARE_TOKEN="{token}"') if secret: print(f' export FLARE_SECRET="{secret}"') print() print(div) print() print(" Listening on all interfaces — Ctrl+C to stop.") print(" All data is in-memory, discarded on exit.") print() def main(): parser = argparse.ArgumentParser( description="Flare — ephemeral script monitoring server + dashboard." ) parser.add_argument("--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0)") parser.add_argument("--port", default=8080, type=int, help="Port (default: 8080)") parser.add_argument("--token", default="", help="Bearer token (generated if omitted)") parser.add_argument("--secret", default="", help="HMAC secret (generated if omitted)") parser.add_argument("--topic", default="", help="Default topic name (generated if omitted)") parser.add_argument("--html", default="", help="Path to flare.html (auto-detected if omitted)") parser.add_argument("--get-path", default="", help="Per-run GET base path (generated if omitted)") parser.add_argument("--post-path", default="", help="Per-run POST path (generated if omitted)") parser.add_argument("--no-secret", action="store_true", help="Disable HMAC secret generation") parser.add_argument("--verbose", action="store_true") args = parser.parse_args() logging.basicConfig( level=logging.DEBUG if args.verbose else logging.WARNING, format="%(asctime)s %(levelname)s %(message)s", ) html_path = args.html if not html_path: candidate = Path(__file__).parent / "flare.html" if candidate.exists(): html_path = str(candidate) token = args.token or secrets.token_urlsafe(20) secret = "" if args.no_secret else (args.secret or secrets.token_hex(16)) topic = args.topic or _generate_topic() get_path = _normalize_get_path(args.get_path or secrets.token_urlsafe(12)) post_path = _normalize_post_path(args.post_path or secrets.token_urlsafe(18)) ip = _local_ip() if args.host in ("0.0.0.0", "") else args.host root_url = f"http://{ip}:{args.port}" CFG.update({ "token": token, "secret": secret, "default_topic": topic, "html": html_path, "root_url": root_url, "get_base_path": get_path, "get_base_url": f"{root_url}{get_path}", "post_path": post_path, "post_url": f"{root_url}{post_path}", "verbose": args.verbose, }) print_banner(args.host, args.port) server = ThreadedHTTPServer((args.host, args.port), Handler) try: server.serve_forever() except KeyboardInterrupt: print("\n\nServer stopped and data discarded.") if __name__ == "__main__": main()