66 lines
3.6 KiB
Python
66 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Manifest-gated one-shot raw-elfldr snapshot runner."""
|
|
from __future__ import annotations
|
|
import hashlib, ipaddress, json, os, socket, time
|
|
from pathlib import Path
|
|
from phase10dn_snapshot_receiver import SnapshotReceiver
|
|
|
|
ARTIFACT_SIZE=109896
|
|
ARTIFACT_SHA256="147b5bede0f0b5b7d2be903bc72ff0d0541a2cdc28eae7d86b6bf95e1978ebdf"
|
|
PORT=9021
|
|
CONNECT_TIMEOUT=3.0
|
|
TOTAL_TIMEOUT=30.0
|
|
MAX_WIRE=67108992
|
|
FIELDS={"active","run_id","target","port","artifact_size","artifact_sha256","snapshot_path","receipt_path","not_before","not_after","one_connection","one_transfer","one_execution","result_receive","target_file_read","device_write","installation","autoload","retry","reconnect"}
|
|
|
|
class RunnerError(RuntimeError):
|
|
pass
|
|
|
|
def _load(path: Path) -> dict:
|
|
value=json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(value,dict): raise RunnerError("record is not an object")
|
|
return value
|
|
|
|
def validate_records(activation: dict,approval: dict,now: float)->dict:
|
|
if set(activation)!=FIELDS or set(approval)!=FIELDS: raise RunnerError("record fields differ")
|
|
if activation!=approval: raise RunnerError("activation and approval differ")
|
|
if activation["active"] is not True: raise RunnerError("activation is inactive")
|
|
if not activation["not_before"]<=now<=activation["not_after"]: raise RunnerError("approval window inactive")
|
|
if activation["port"]!=PORT or activation["artifact_size"]!=ARTIFACT_SIZE or activation["artifact_sha256"]!=ARTIFACT_SHA256: raise RunnerError("artifact route mismatch")
|
|
if any(activation[k] is not True for k in ("one_connection","one_transfer","one_execution","result_receive","target_file_read")): raise RunnerError("required authority absent")
|
|
if any(activation[k] is not False for k in ("device_write","installation","autoload","retry","reconnect")): raise RunnerError("forbidden authority present")
|
|
ipaddress.IPv4Address(activation["target"])
|
|
for key in ("snapshot_path","receipt_path"):
|
|
path=Path(activation[key])
|
|
if not path.is_absolute() or path.exists(): raise RunnerError("exclusive absolute evidence path required")
|
|
return activation
|
|
|
|
def _consume(plan: dict)->None:
|
|
path=Path(plan["receipt_path"])
|
|
with path.open("x",encoding="utf-8",newline="\n") as out:
|
|
json.dump({"run_id":plan["run_id"],"artifact_sha256":ARTIFACT_SHA256,"consumed_before_socket":True},out,sort_keys=True)
|
|
out.write("\n");out.flush();os.fsync(out.fileno())
|
|
|
|
def run(artifact:Path,activation_path:Path,approval_path:Path,socket_factory=socket.socket,clock=time.monotonic,wall_clock=time.time):
|
|
plan=validate_records(_load(activation_path),_load(approval_path),wall_clock())
|
|
raw=artifact.read_bytes()
|
|
if len(raw)!=ARTIFACT_SIZE or hashlib.sha256(raw).hexdigest()!=ARTIFACT_SHA256: raise RunnerError("artifact identity mismatch")
|
|
_consume(plan)
|
|
receiver=SnapshotReceiver(Path(plan["snapshot_path"]));sock=None;received=0;deadline=clock()+TOTAL_TIMEOUT
|
|
try:
|
|
sock=socket_factory(socket.AF_INET,socket.SOCK_STREAM);sock.settimeout(CONNECT_TIMEOUT);sock.connect((plan["target"],PORT));sock.sendall(raw);sock.shutdown(socket.SHUT_WR)
|
|
while True:
|
|
remain=deadline-clock()
|
|
if remain<=0: raise RunnerError("result deadline")
|
|
sock.settimeout(remain);chunk=sock.recv(min(65536,MAX_WIRE-received))
|
|
if not chunk: break
|
|
received+=len(chunk)
|
|
if received>MAX_WIRE: raise RunnerError("wire bound")
|
|
receiver.feed(chunk)
|
|
return receiver.finish()
|
|
except Exception:
|
|
receiver.abort();raise
|
|
finally:
|
|
if sock is not None: sock.close()
|