Files
2026-06-09 17:31:10 -07:00

970 lines
33 KiB
Python

#!/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:
/<GET_PATH>/ → flare.html dashboard (auto-configured via query params)
/<GET_PATH>/wrapper.* → curl-able wrappers with credentials pre-baked
/<GET_PATH>/events → SSE stream
/<POST_PATH> → 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 /<get_path>/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()