sicd: implement wire protocol (preamble + nested netstring peeling)
- Read preamble: magic byte \x00 + 4-byte BE length + netstring - Split netstring at first NUL: command\0<payload> - Exec command with payload + trailing stdin forwarded - Nested payloads are netstrings for the next hop (one layer peeled per sicd) - Bare -- survives as opaque data - Malformed input: exit 1 with stderr diagnostic - 14/14 pytest green
This commit is contained in:
+84
-54
@@ -1,68 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""sicd — read netstring-framed argv from stdin and execvp it. Python reference.
|
||||
"""sicd — sic wire protocol daemon, reference implementation.
|
||||
|
||||
Wire format and exec modes: docs/design.md.
|
||||
Usage: ssh <host> sicd < <netstring-frames>
|
||||
Reads one wire frame from stdin, peels off the outer layer, and execs
|
||||
the command with the payload + trailing stdin forwarded to the child.
|
||||
"""
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
|
||||
def read_netstring(buf):
|
||||
"""Read one netstring from buffer. Returns (payload, rest) or None."""
|
||||
i = buf.find(b":")
|
||||
if i < 0:
|
||||
return None
|
||||
def read_exact(n: int) -> bytes:
|
||||
"""Read exactly n bytes from stdin, or raise EOFError."""
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
chunk = sys.stdin.buffer.read(n - len(buf))
|
||||
if not chunk:
|
||||
raise EOFError("unexpected EOF")
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
|
||||
def parse_netstring(data: bytes) -> tuple[bytes, bytes]:
|
||||
"""Parse a DJB netstring from data, return (content, rest)."""
|
||||
# Find the colon separating length from content
|
||||
colon = data.find(b":")
|
||||
if colon == -1:
|
||||
raise ValueError("netstring: missing colon")
|
||||
try:
|
||||
n = int(buf[:i])
|
||||
length = int(data[:colon])
|
||||
except ValueError:
|
||||
return None
|
||||
start = i + 1
|
||||
end = start + n
|
||||
if len(buf) < end + 1 or buf[end : end + 1] != b",":
|
||||
return None
|
||||
return buf[start:end], buf[end + 1 :]
|
||||
raise ValueError("netstring: invalid length")
|
||||
content_start = colon + 1
|
||||
content_end = content_start + length
|
||||
if content_end + 1 > len(data):
|
||||
raise ValueError("netstring: truncated")
|
||||
if data[content_end:content_end+1] != b",":
|
||||
raise ValueError("netstring: missing trailing comma")
|
||||
return data[content_start:content_end], data[content_end+1:]
|
||||
|
||||
|
||||
def run_frame(mode, args):
|
||||
"""Execute a single command frame."""
|
||||
if mode == b"exec":
|
||||
decoded = [a.decode("utf-8", errors="replace") for a in args]
|
||||
result = subprocess.run(decoded)
|
||||
return result.returncode
|
||||
elif mode == b"sh":
|
||||
shell_cmd = args[0].decode("utf-8", errors="replace")
|
||||
result = subprocess.run(shell_cmd, shell=True)
|
||||
return result.returncode
|
||||
else:
|
||||
print(f"sicd: unknown mode {mode!r}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
buf = sys.stdin.buffer.read()
|
||||
mode = None
|
||||
fields = []
|
||||
frames_processed = 0
|
||||
|
||||
while True:
|
||||
item = read_netstring(buf)
|
||||
if item is None:
|
||||
break
|
||||
field, buf = item
|
||||
|
||||
if mode is None:
|
||||
mode = field # first field = mode selector
|
||||
elif field == b"":
|
||||
if mode is not None:
|
||||
run_frame(mode, fields)
|
||||
frames_processed += 1
|
||||
mode = None
|
||||
fields = []
|
||||
def main() -> int:
|
||||
try:
|
||||
# Read preamble: magic byte + 4-byte big-endian length
|
||||
preamble = read_exact(5)
|
||||
if preamble[0:1] != b"\x00":
|
||||
sys.stderr.write("sicd: missing magic byte\n")
|
||||
return 1
|
||||
ns_len = struct.unpack(">I", preamble[1:5])[0]
|
||||
# Read the netstring
|
||||
ns_data = read_exact(ns_len)
|
||||
# Parse netstring
|
||||
content, rest = parse_netstring(ns_data)
|
||||
# Split content at first NUL
|
||||
nul_pos = content.find(b"\x00")
|
||||
if nul_pos == -1:
|
||||
sys.stderr.write("sicd: missing NUL separator\n")
|
||||
return 1
|
||||
command_bytes = content[:nul_pos]
|
||||
payload = content[nul_pos+1:]
|
||||
# Read any remaining stdin (trailing bytes)
|
||||
trailing = sys.stdin.buffer.read()
|
||||
# Build child's stdin: payload + trailing
|
||||
child_stdin = payload + trailing
|
||||
# Split command into argv
|
||||
command_str = command_bytes.decode("ascii", errors="replace")
|
||||
argv = command_str.split()
|
||||
if not argv:
|
||||
sys.stderr.write("sicd: empty command\n")
|
||||
return 1
|
||||
# Fork and exec
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
# Child
|
||||
# Redirect stdin to pipe with child_stdin
|
||||
r, w = os.pipe()
|
||||
os.write(w, child_stdin)
|
||||
os.close(w)
|
||||
os.dup2(r, 0)
|
||||
os.close(r)
|
||||
os.execvp(argv[0], argv)
|
||||
# If exec fails
|
||||
sys.stderr.write(f"sicd: exec failed: {argv[0]}\n")
|
||||
os._exit(1)
|
||||
else:
|
||||
fields.append(field)
|
||||
|
||||
return 0 if frames_processed > 0 else 1
|
||||
# Parent
|
||||
_, status = os.waitpid(pid, 0)
|
||||
if os.WIFEXITED(status):
|
||||
return os.WEXITSTATUS(status)
|
||||
else:
|
||||
return 1
|
||||
except (EOFError, ValueError, OSError) as e:
|
||||
sys.stderr.write(f"sicd: {e}\n")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user