48 lines
3.5 KiB
Python
48 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Inactive manifest-gated one-shot runner for Phase-1.0DQ."""
|
|
from __future__ import annotations
|
|
import hashlib,ipaddress,json,os,socket,time
|
|
from dataclasses import asdict
|
|
from pathlib import Path
|
|
from phase10dq_inventory_protocol import MAX_WIRE,parse_stream
|
|
|
|
ARTIFACT_SIZE=110032;ARTIFACT_SHA256="914fce06a490ad048fdd0a85ae117858e8904b47c72054bf12fbfebc213a6db8";PORT=9021
|
|
FIELDS={"active","run_id","target","port","artifact_size","artifact_sha256","output_path","receipt_path","not_before","not_after","one_connection","one_transfer","one_execution","result_receive","directory_inventory","possible_atime_effect_acknowledged","device_file_content_read","persistent_device_write","installation","autoload","retry","reconnect"}
|
|
class RunnerError(RuntimeError):pass
|
|
def _load(path:Path):
|
|
value=json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(value,dict):raise RunnerError("record object required")
|
|
return value
|
|
def validate_records(activation:dict,approval:dict,now:float):
|
|
if set(activation)!=FIELDS or set(approval)!=FIELDS or activation!=approval:raise RunnerError("record mismatch")
|
|
if activation["active"] is not True or not activation["not_before"]<=now<=activation["not_after"]:raise RunnerError("inactive window")
|
|
if activation["port"]!=PORT or activation["artifact_size"]!=ARTIFACT_SIZE or activation["artifact_sha256"]!=ARTIFACT_SHA256:raise RunnerError("route mismatch")
|
|
if any(activation[k] is not True for k in ("one_connection","one_transfer","one_execution","result_receive","directory_inventory","possible_atime_effect_acknowledged")):raise RunnerError("authority absent")
|
|
if any(activation[k] is not False for k in ("device_file_content_read","persistent_device_write","installation","autoload","retry","reconnect")):raise RunnerError("forbidden authority")
|
|
ipaddress.IPv4Address(activation["target"])
|
|
for key in ("output_path","receipt_path"):
|
|
p=Path(activation[key])
|
|
if not p.is_absolute() or p.exists():raise RunnerError("exclusive path required")
|
|
return activation
|
|
def _exclusive_json(path:Path,value:dict):
|
|
with path.open("x",encoding="utf-8",newline="\n") as out:json.dump(value,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")
|
|
_exclusive_json(Path(plan["receipt_path"]),{"artifact_sha256":ARTIFACT_SHA256,"consumed_before_socket":True,"run_id":plan["run_id"]})
|
|
sock=None;wire=bytearray();deadline=clock()+20.0
|
|
try:
|
|
sock=socket_factory(socket.AF_INET,socket.SOCK_STREAM);sock.settimeout(3.0);sock.connect((plan["target"],PORT));sock.sendall(raw);sock.shutdown(socket.SHUT_WR)
|
|
while True:
|
|
remaining=deadline-clock()
|
|
if remaining<=0:raise RunnerError("result deadline")
|
|
sock.settimeout(remaining);chunk=sock.recv(min(4096,MAX_WIRE-len(wire)))
|
|
if not chunk:break
|
|
wire.extend(chunk)
|
|
if len(wire)==MAX_WIRE:break
|
|
entries=parse_stream(bytes(wire));_exclusive_json(Path(plan["output_path"]),{"entries":[asdict(item) for item in entries],"entry_count":len(entries),"path":"/user/app/FAKE00000","run_id":plan["run_id"]});return entries
|
|
finally:
|
|
for index in range(len(wire)):wire[index]=0
|
|
if sock is not None:sock.close()
|