diff --git a/README.md b/README.md index ff97186..edb2a2f 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,10 @@ sic -- [arg ...] # explicit separator ``` ``` -sic quotesu echo hello world -sic quotesu touch 'a b' # one file named "a b" -sic quotesu echo '$HOME' # literal, no expansion -sic --sh quotesu 'echo hi | wc -c' +sic host1 echo hello world +sic host1 touch 'a b' # one file named "a b" +sic host1 echo '$HOME' # literal, no expansion +sic --sh host1 'echo hi | wc -c' ``` ## Build diff --git a/SKILL.md b/SKILL.md index 21c2cf6..50f5300 100644 --- a/SKILL.md +++ b/SKILL.md @@ -22,11 +22,11 @@ Pass each argument as a separate argument to `sic`. Do not construct netstrings; `sic` frames them. ``` -sic quotesu echo hello world -sic quotesu touch 'my file.txt' -sic quotesu echo '$HOME' # literal, no expansion -sic quotesu python3 -c 'import sys; print(sys.argv)' a 'b c' -sic --sh quotesu 'grep foo *.log | wc -l' +sic host1 echo hello world +sic host1 touch 'my file.txt' +sic host1 echo '$HOME' # literal, no expansion +sic host1 python3 -c 'import sys; print(sys.argv)' a 'b c' +sic --sh host1 'grep foo *.log | wc -l' ``` ## exec vs --sh diff --git a/cmd/sic/main.go b/cmd/sic/main.go index 535cc98..fba4670 100644 --- a/cmd/sic/main.go +++ b/cmd/sic/main.go @@ -11,9 +11,9 @@ // // Examples: // -// sic quotesu echo hello world -// sic quotesu touch 'a b' # ONE file, not two -// sic --sh quotesu 'echo hi | wc -c' +// sic host1 echo hello world +// sic host1 touch 'a b' # ONE file, not two +// sic --sh host1 'echo hi | wc -c' package main import ( diff --git a/demos/quoting-hell.py b/demos/quoting-hell.py index 2aba74b..f561772 100755 --- a/demos/quoting-hell.py +++ b/demos/quoting-hell.py @@ -3,18 +3,20 @@ Each scenario is run both ways: A) Plain SSH — shows the breakage - B) sic tool — shows the fix + B) sic — shows the fix -Usage: - python3 demo-quoting-hell.py +Point it at any host that runs sicd (reachable over SSH with your key): + + python3 quoting-hell.py """ import subprocess import sys -import shlex -import tempfile import os -HOST = sys.argv[1] if len(sys.argv) > 1 else "quotesu" +if len(sys.argv) < 2: + sys.exit("usage: python3 quoting-hell.py # any host running sicd") + +HOST = sys.argv[1] SIC = os.path.expanduser("~/bin/sic") SSH = ["/usr/bin/ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5"] @@ -29,7 +31,7 @@ def ssh(cmd_str): def sic(*args): - """Run argv via sic tool (which frames netstrings for sicd).""" + """Run argv via sic (which frames netstrings for sicd).""" return subprocess.run( [SIC, HOST] + list(args), capture_output=True, text=True, timeout=10 @@ -62,7 +64,7 @@ def result(label, r): # ── 1. Space in filename ────────────────────────────────────────── section("1. Space in filename — the classic") -sub("SSH: ssh host touch 'my file.txt'") +sub(f"SSH: ssh {HOST} touch 'my file.txt'") # SSH concatenates args with spaces, remote shell re-splits r = ssh("touch 'my file.txt'") r2 = ssh("ls my file.txt 2>&1; echo ---; ls 'my file.txt' 2>&1") @@ -72,10 +74,10 @@ print(" → 'touch' sees TWO args: 'my' and 'file.txt' — two files created" # Clean up ssh("rm -f my file.txt 'my file.txt'") -sub("rs: rs quotesu touch 'my file.txt'") +sub(f"sic: sic {HOST} touch 'my file.txt'") r = sic("touch", "my file.txt") r2 = sic("ls", "-la", "my file.txt") -result("rs", r2) +result("sic", r2) print(" → 'touch' sees ONE arg: 'my file.txt' — one file created") sic("rm", "-f", "my file.txt") @@ -83,59 +85,59 @@ sic("rm", "-f", "my file.txt") # ── 2. Dollar sign / variable expansion ─────────────────────────── section("2. Dollar sign — unintended variable expansion") -sub("SSH: ssh host 'echo \$HOME'") +sub(f"SSH: ssh {HOST} 'echo \$HOME'") r = ssh("echo $HOME") result("SSH", r) print(" → Remote shell expands $HOME — leaks local values") -sub("SSH escaped: ssh host 'echo \\$HOME'") +sub(f"SSH escaped: ssh {HOST} 'echo \\$HOME'") r = ssh("echo \\$HOME") result("SSH escaped", r) -sub("rs: rs quotesu echo '\$HOME'") +sub(f"sic: sic {HOST} echo '\$HOME'") r = sic("echo", "$HOME") -result("rs", r) +result("sic", r) print(" → No shell involved — literal $HOME printed") # ── 3. Backticks / command substitution ────────────────────────── section("3. Backticks — unintended command execution") -sub("SSH: ssh host 'echo `hostname`'") +sub(f"SSH: ssh {HOST} 'echo `hostname`'") r = ssh("echo `hostname`") result("SSH", r) -print(" → Backticks execute on remote — prints quotesu") +print(" → Backticks execute on remote — prints the remote hostname") -sub("rs: rs quotesu echo '`hostname`'") +sub(f"sic: sic {HOST} echo '`hostname`'") r = sic("echo", "`hostname`") -result("rs", r) +result("sic", r) print(" → Literal backticks — no execution") # ── 4. Nested quotes ───────────────────────────────────────────── section("4. Nested quotes — escaping hell") -sub('SSH: ssh host "echo \\"hello world\\""') +sub(f'SSH: ssh {HOST} "echo \\"hello world\\""') r = ssh('echo "hello world"') result("SSH", r) -sub('rs: rs quotesu echo "hello world"') +sub(f'sic: sic {HOST} echo "hello world"') r = sic("echo", "hello world") -result("rs", r) +result("sic", r) print(" → No nested quoting needed — just pass the string") # ── 5. Newlines in arguments ────────────────────────────────────── section("5. Newlines in arguments") -sub("SSH: ssh host 'echo \"line1\\nline2\"'") +sub(f"SSH: ssh {HOST} 'echo \"line1\\nline2\"'") r = ssh('echo "line1\nline2"') result("SSH", r) print(" → Newline in SSH command string is fragile") -sub("rs: rs quotesu echo 'line1\\nline2'") +sub(f"sic: sic {HOST} echo 'line1\\nline2'") r = sic("echo", "line1\nline2") -result("rs", r) +result("sic", r) print(" → Newline is just a byte in a netstring field") @@ -149,7 +151,7 @@ r = ssh(script) result("SSH", r) print(" → Try to get all those quoting levels right...") -sub("rs: rs quotesu python3 -c ... with clean args") +sub("sic: sic python3 -c ... with clean args") r = sic( "python3", "-c", "import sys; [print(i, repr(a)) for i,a in enumerate(sys.argv)]", @@ -160,7 +162,7 @@ r = sic( "arg with `ls`", 'nested "quotes"' ) -result("rs", r) +result("sic", r) print(" → Each argument is a separate field — no escaping needed") @@ -169,7 +171,7 @@ section("SUMMARY") print(""" The pattern in every case: SSH → flattens argv to one string, shell re-parses → QUOTING HELL - rs → sends argv as framed fields, execvp directly → NO ESCAPING + sic → sends argv as framed fields, execvp directly → NO ESCAPING Netstrings (length:bytes,) are the key: the length tells the parser exactly where each field ends, so NO delimiter needs escaping. diff --git a/quotesu/setup.md b/quotesu/setup.md deleted file mode 100644 index 6093a23..0000000 --- a/quotesu/setup.md +++ /dev/null @@ -1,62 +0,0 @@ -# quotesu — test target for sicd - -Incus container on `dcw2` (192.168.88.114), created as the target host for -sicd development and quoting-hell demonstrations. - -## Host facts - -| Property | Value | -|---|---| -| Host machine | `dcw2` (192.168.88.114, aarch64, Debian 13) | -| Container IP | 192.168.88.196/24 | -| Network | `br0` (unmanaged bridge) | -| Storage | `usb` pool (btrfs, /mnt/usb-noether) | -| OS | Debian 13 "trixie" (arm64) | -| Incus version | 6.0.4 | -| SSH user | `mfritsche` (key auth + passwordless sudo) | - -## Setup steps - -```sh -# 1. Create container -incus init debian-13 quotesu --storage usb --network br0 -incus start quotesu - -# 2. Install SSH + user -incus exec quotesu -- apt-get update -qq -incus exec quotesu -- apt-get install -y -qq openssh-server sudo python3 -incus exec quotesu -- adduser --disabled-password --gecos "" mfritsche -incus file push ~/.ssh/authorized_keys quotesu/home/mfritsche/.ssh/authorized_keys -incus exec quotesu -- chown -R mfritsche:mfritsche ~mfritsche/.ssh -incus exec quotesu -- chmod 700 ~mfritsche/.ssh -incus exec quotesu -- chmod 600 ~mfritsche/.ssh/authorized_keys -incus exec quotesu -- adduser mfritsche sudo -echo "mfritsche ALL=(ALL) NOPASSWD:ALL" | \ - incus exec quotesu -- tee -a /etc/sudoers.d/mfritsche -incus exec quotesu -- systemctl restart ssh - -# 3. Deploy sicd -incus exec quotesu -- mkdir -p /opt/sicd -# Copy ../gateway/sicd to the container, then: -incus exec quotesu -- chmod 755 /usr/local/bin/sicd -incus exec quotesu -- ln -sf /usr/local/bin/sicd /usr/local/bin/sicd -``` - -## Verification - -```sh -# Test the gateway -printf '4:exec,2:id,0:,' | ssh quotesu sicd -# → uid=1000(mfritsche) gid=1000(... -``` - -## DNS note - -dcw2 had a broken `/etc/resolv.conf` (NetworkManager header, no nameservers). -Fixed by setting: -``` -nameserver 1.1.1.1 -nameserver 192.168.88.1 -``` -This was needed for `incus image copy images:debian/13 local:...` to resolve -the image server hostname.