Skip to content

Artemis HTTP transport: save access without an ns2jb session - #9

Merged
dougchansan merged 8 commits into
mainfrom
feature/artemis-http-transport
Nov 2, 2026
Merged

dougchansan merged 8 commits into
mainfrom
feature/artemis-http-transport

Conversation

@dougchansan

@dougchansan dougchansan commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

What this adds

ns2-savekit can now talk to the installed Artemis Launcher over its own HTTP
server on port 8080, with no ns2jb RPC session. The launcher and its
protected files are not modified; only the four upstream endpoints are used:
GET /status, POST /eval, PUT /save?path=, DELETE /delete?path=.

Stacked on PR #8 (feature/alula-artemis-launcher), because it uses the
installer's pinned upstream/expected-file helpers. Merge #8 first.

Transport

tools/transports/artemis-http.cjs + CLI tools/artemis-http.cjs:

  • connection: explicit host, /24 discovery, timeouts and bounded retries
    (retry transport/5xx only, never a 4xx or a write), structured error codes;
  • health/Lua: HTTP 200, Lua 5.1, n2h.platform (Ounce), engine type;
  • eval: inline or local file; status: error is a failed operation even
    though HTTP is 200; request-size cap enforced before sending;
  • downloads: bounded /eval hex reads with offset/length, exact byte and hex
    length checks, per-chunk SHA-256 state, final SHA-256, resume after a failed
    chunk, local temp file promoted only after verification;
  • uploads: direct /save under the server body cap, chunked /eval appends
    above it, explicit --force for overwrite, protected files refused on both
    client and server. The reported hash is read back from the console and
    compared with the source before success (EHASH on mismatch); --no-verify
    marks the result unverified. An interrupted upload is EINCOMPLETE with the
    partial byte count, and a failed PUT/append is attempted once (writes are
    never retried);
  • delete: DELETE plus read-back stat, error if still present;
  • backup: explicit-file only (scope: explicit-files, complete: false) -
    the server has no listing, so it cannot authorize a destructive full restore;
  • kit: node tools/kit.cjs --http <ip> opens a menu exposing only these
    operations. The ns2jb RPC flow is unchanged.

Hardware validation (Switch 2, firmware 23.0.1, Seedsow Demo 1.0.0)

Server started with button Y; no RPC session.

test result
/status 200 nora2hax/seedsow
Hello World hello world
Lua/platform Lua 5.1 / Ounce
text upload+download 41 B, SHA-256 identical
binary (NUL/0xFF/0x80) 4 KiB, 16 KiB, 64 KiB all byte-identical (cmp + SHA-256)
64 KiB first /save chunk + /eval appends; 4 KiB download reads
missing file stat absent; get exit 9
protected core.lua, save9999.dat upload/delete refused
save:/../escape refused
delete + read-back 5 disposable files gone
remote-verified uploads text + 4/16/64 KiB all reported verified remote hashes and downloaded byte-identical
relaunch game + Y 200, marker and 64 KiB file intact
os.rename probe false (Operation not permitted) -> chunked uploads are non-atomic
explicit-file backup 9 files, 114,443 B, complete:false
legacy migration from that backup original baseline + attestation created; pre-install backup unchanged

Hashes:

text        41 B   de4f9d6de5c0f18f988f31bc267ab415ae8700277f411150ae92b4d4661ccccc
bin-4096  4096 B   e87d333e5f9dc3d12c4e6445700267dcebf57d156ebece144915a0a3742c44cb
bin-16384 16384 B  cb158df680b8ead69b547ca78deef580b70947827a4ea23210e09987866a4d16
bin-65536 65536 B  5cccb2465cf3b2a34201823de07586a765b009cba96601d684228250c9b3eebd
marker       35 B   62c9a3fc97e4c70408f572bf8b28c63f5cf1e8b9d593311be2309cfe76fd5087

Final review round — probe safety and path validation

  • Rename probe (P2): fixed scratch names could collide with files from an
    earlier run, and cleanup deleted names without proving ownership. The probe
    now uses two 128-bit random suffixes, refuses if either exists (reports
    collided), writes recognizable content and reads it back before treating a
    file as disposable, deletes only files that still match a probe payload, and
    reports anything else in left/cleanupError. Cleanup runs in a finally,
    so a timeout after a successful upload is handled. No race-free claim: the API
    has no exclusive create.
  • Path validation (P2): one shared validator for every entry point rejects
    empty names, ., .., any .., separators/backslashes, NUL/control
    characters, unsupported characters and leading / (absolute); reads may
    inspect protected files, writes refuse them. Invalid paths are rejected
    before any HTTP request.
  • Tests grow to 35 HTTP tests (probe success/unsupported/collision/failed
    upload/timeout/interrupt/corrupt destination/cleanup failure/consecutive/
    concurrent, validator acceptance/rejection at every entry point, no-request
    before rejection). Full branch suite: 192 Node assertions across 15 files,
    Python 8, Lua 13+12.
  • Hardware: randomized probe on the console returned left: [], cleaned both
    unique scratch names and all scratch stats were absent; path refusals were
    local; 4/16/64 KiB remote-verified transfers matched and were deleted;
    relaunch + Y reconnected with core.lua/save9999.dat present.
  • Merged up to PR Install alula's Artemis Launcher as a separate, verified option #8 head 3798646; base remains feature/alula-artemis-launcher. The installer PR now records the blocked hardware rollback validation in docs/ARTEMIS-ROLLBACK-HARDWARE.md; this HTTP PR makes no rollback claim.
  • CI: .github/workflows/offline-tests.yml (from PR Install alula's Artemis Launcher as a separate, verified option #8) now runs Lua 5.4 and
    reports pass on this PR.

Limits

  • No complete save enumeration. io.open("save:/") yields an unreadable
    handle and the engine metatable __index is a function, so backups are
    explicit-file and explicitly not a full archive.
  • No atomic chunked upload. os.rename on save:/ is refused; --atomic
    fails closed and removes its temp. Chunked uploads write the target directly.
  • Server body cap 0x7000; the client uses 0x6c00 for the first chunk.
  • Unauthenticated HTTP: trusted LAN only, never expose 8080. /eval is arbitrary
    Lua and can read protected files; the transport refuses protected writes.
  • No other title's save, no native memory writes, no privilege escalation.

Tests

  • tests/test_artemis_http.cjs: 35 passed against a local HTTP server with
    the real endpoints/body cap (status/health, eval errors, cap, stat, binary
    chunked download, resume, hash/size refusal, 4/16/64 KiB uploads, remote-hash
    verification, corrupted upload, interrupted chunked upload, no-retry writes,
    overwrite, protected refusals, delete verification, explicit backup, retry,
    timeout, connection failure, atomic refusal, randomized probe cases, path
    validation, CLI exits).
  • Full suite on this branch: 192 Node assertions across 15 files, Python 8,
    Lua 13 + 12, all passing.
  • This branch is merged up to PR Install alula's Artemis Launcher as a separate, verified option #8 head d69770c; base remains
    feature/alula-artemis-launcher.

Docs

  • docs/ARTEMIS-HTTP.md - usage, protocol details, security, limitations and
    the hardware report.
  • docs/artemis-http-2026-11-01.txt - sanitized command/output transcript.
  • README section and kit table row; screenshot of the running server.

Not merged, not draft. Base is feature/alula-artemis-launcher.

Add tools/transports/artemis-http.cjs and tools/artemis-http.cjs: a
transport that uses only the installed launcher's four upstream endpoints
and needs no ns2jb session.

- status/health (Lua 5.1, n2h.platform), eval (status:error is a failure),
  stat, bounded hex reads, uploads, delete with read-back, discovery;
- downloads: chunked offset reads, byte-count/hex validation, per-chunk and
  final SHA-256, resume from a verified .part state, local atomic promotion;
- uploads: direct /save under the body cap, chunked /eval append above it,
  explicit overwrite authorization, protected-file refusal on client and
  server. os.rename is refused on hardware, so --atomic fails closed and
  chunked writes are documented as non-atomic;
- backup is explicit-file with scope=explicit-files and complete=false:
  the server has no listing, so it cannot authorize a destructive restore;
- kit gains --http <ip> and a menu that exposes only this transport's
  operations; the RPC flow is unchanged.

Hardware (Switch 2 23.0.1, Seedsow demo 1.0.0): status, Hello World, Lua
5.1/Ounce, text + 4 KiB/16 KiB/64 KiB binary transfers with matching
SHA-256, protected/traversal/missing refusals, delete+read-back, relaunch
and reconnect with data intact, explicit-file backup, and a legacy
migration using that backup (original baseline unchanged).

Tests: tests/test_artemis_http.cjs (20). Docs: docs/ARTEMIS-HTTP.md and
docs/artemis-http-2026-11-01.txt.
ArtemisHttp.upload() returned the hash of the PC-side buffer, which only
proves what was sent. It now reads the remote file back through /eval and
compares its SHA-256 with the source before reporting success:

- default verify=true; the returned sha256 is the verified remote hash and
  localSha256 is kept separately; --no-verify marks the result unverified;
- EHASH on a remote mismatch, with the partial size;
- a failed PUT/append is wrapped as EINCOMPLETE with the partial byte count,
  and is attempted once (write requests are never retried);
- atomic uploads verify the temporary copy before rename; os.rename remains
  unavailable on hardware, so --atomic still fails closed.

Tests grow to 24 (remote-hash verification, corrupted upload, interrupted
chunked upload, no-retry write). Hardware: 4/16/64 KiB uploads all reported
remote-verified hashes, downloaded byte-identical, deleted with read-back.
Docs and transcript updated.
Rename probe (P2): fixed .ns2http-probe-a/b names could collide with files
left by an earlier run or another tool, and cleanup deleted names without
proving ownership. The probe now:

- uses two 128-bit random scratch names and refuses if either exists;
- writes recognizable content and reads it back before treating a file as
  disposable;
- cleans up only files whose content still matches a probe payload, leaves
  anything else untouched and reports it (left/cleanupError);
- runs cleanup in a finally block so a timeout after a successful upload is
  handled; reports name collision instead of deleting;
- makes no race-free claim: the HTTP API has no exclusive create, so the
  pre-check plus random names is the available mitigation.

Path validation (P2): saveName()/anyName() accepted '.', '..' and any
'dotdot' string, and only some operations used them. One shared validator
now rejects empty names, '.', '..', any '..', separators/backslashes, NUL and
control characters, unsupported characters and leading '/' (absolute), and
is applied to stat, readChunk, download, upload, putRaw, appendRaw,
renameRaw, remove and backup. Reads may inspect protected files; writes
refuse them.

Tests: 35 HTTP tests (was 24) covering probe success/unsupported/collision/
failed upload/timeout/interrupt/corrupt destination/cleanup failure/
consecutive/concurrent, validator acceptance/rejection at every entry point,
and no-request-before-rejection. Hardware: randomized probe returned
left:[], cleaned both scratch names, path refusals local, 4/16/64 KiB
verified round trips, relaunch reconnect.
@dougchansan
dougchansan changed the base branch from feature/alula-artemis-launcher to main November 2, 2026 21:01
@dougchansan
dougchansan merged commit 700814f into main Nov 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant