lantern: handle-scoped lantern://local/<handle> fs-serving route
Add a secure filesystem-serving route to the lantern:// scheme so a container app (Slate) can serve a real local file's bytes to the webview as a streamed <img src="lantern://local/<handle>"> load instead of a base64 data: URI (33% bloat + a giant DOM string).
Security boundary (handle-scoped): the resolver serves ONLY files explicitly registered via the grant-checked fs.serve-file command, each yielding an opaque handle; every unregistered handle 404s. The scheme can never become an arbitrary full-disk read. fs.serve-file grant-checks the path (fs-stat) before registering, so a scoped embedding can only serve files it may already read -- the route adds no authority beyond the existing fs.read-file.
- (lantern serve): make-servable-registry + servable-register!/ unregister!/lookup (opaque per-run handles), make-fs-resolver (handle -> bytes+mime, exact-match lookup, no path join), parse-lantern-uri + make-scheme-resolver (host-routed: app -> assets, local -> fs route). Harden normalize-request-path to strip ALL leading slashes -- a single strip left an absolute remainder that path-join resolved off the asset root (arbitrary read on the app origin); now tested. - (lantern backend gtk): hand the resolver the full geturi (host intact); getpath strips the host, collapsing local onto app. Probed on WebKitGTK. - (lantern app): lantern-plugin gains routes:; lantern-run composes the asset resolver with plugin host routes. - (lantern-system fs): fs.serve-file / fs.unserve-file, grant-checked. - (lantern-system plugin): one shared servable registry wires the commands and the local route together.
Tests: lantern 39 passed, lantern-system 28 passed. The scoping test is proven fallible (serving by path turns it red). WebKitGTK geturi/getpath behavior confirmed with a real probe.
lantern-system/src/lantern-system/fs.sgl | 35 ++++++++++++++++++++++++++++++--
lantern-system/src/lantern-system/plugin.sgl | 18 ++++++++++++++---
lantern-system/test/test-lantern-system.sgl | 43 +++++++++++++++++++++++++++++++++++++++
lantern/src/lantern/app.sgl | 24 ++++++++++++++++++----
lantern/src/lantern/backend/gtk.sgl | 15 +++++++-------
lantern/src/lantern/serve.sgl | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
lantern/test/test-lantern.sgl | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 401 insertions(+), 22 deletions(-)lantern-system/src/lantern-system/fs.sglmodified
(sigil path) ; path-dirname / path-join (lantern-system base64) ; base64-encode / base64-decode (no crypto dep) (sigil system fs) (lantern app)) ; lantern-command (export fs-commands) (lantern app) ; lantern-command (lantern serve)) ; servable-register! / servable-unregister! (export fs-commands fs-serve-commands) (begin ;; A sentinel distinct from every JSON value, so a required argument that is (lantern-command "fs.delete" (lambda (args ctx) (fs-delete g (req args path:) recursive: (dict-ref args recursive: #f)) (dict ok: #t))))) ;; fs.serve-file / fs.unserve-file — the lantern://local/<handle> streaming ;; route's registration commands, bound to grants `g` and the servable ;; registry `reg` the plugin shares with its `local` resolver route (so the ;; command that registers a path and the resolver that serves it agree on one ;; table). Kept separate from `fs-commands` because they need `reg`, not just ;; `g`; the plugin appends both lists. (define (fs-serve-commands g reg) (list ;; fs.serve-file #{ path: } -> #{ handle: url: }. Registers `path` for ;; streamed serving over lantern://local/<handle> and returns the URL an ;; <img>/<video>/… loads. GRANT-CHECKED: fs-stat raises outside the grant, ;; so a scoped (deny-by-default) embedding can only serve files it may ;; already read — and the handle-scoped resolver then serves ONLY this ;; path, never an arbitrary one. Adds no authority beyond fs.read-file. (lantern-command "fs.serve-file" (lambda (args ctx) (let ((path (req args path:))) (fs-stat g path) (let ((handle (servable-register! reg path))) (dict handle: handle url: (string-append "lantern://local/" handle)))))) ;; fs.unserve-file #{ handle: } -> #{ ok: #t }. Revokes a handle so its ;; file stops being servable — called when the image view closes, so a ;; handle's authority never outlives the view that needed it. (lantern-command "fs.unserve-file" (lambda (args ctx) (servable-unregister! reg (req args handle:)) (dict ok: #t)))))))lantern-system/src/lantern-system/plugin.sglmodified
(import (sigil core) (sigil system grant) (lantern app) ; lantern-plugin (lantern serve) ; make-servable-registry / make-fs-resolver (lantern-system fs) (lantern-system pty)) (export make-lantern-system-plugin ;; The combined command list for a grants table — the fs (Resource) commands ;; followed by the pty (Session) commands. Exposed for a harness that drives ;; the commands through a registry directly. ;; the commands through a registry directly. (The serve commands need a ;; servable registry too, so they are added by make-lantern-system-plugin, not ;; here.) (define (lantern-system-commands g) (append (fs-commands g) (pty-commands g))) ;; The plugin value for `plugins:` in a lantern-app. `grants:` overrides the ;; default allow-all owner posture. ;; ;; ONE servable registry is created here and shared two ways: the ;; `fs.serve-file`/`fs.unserve-file` COMMANDS register/revoke paths in it, and ;; the `local` HOST ROUTE (make-fs-resolver over the same registry) serves the ;; bytes over lantern://local/<handle>. That shared table is the whole point: ;; the resolver serves ONLY what a grant-checked command registered. (define (make-lantern-system-plugin (keys: (grants #f))) (let ((g (or grants (grant-allow-all)))) (let ((g (or grants (grant-allow-all))) (reg (make-servable-registry))) (lantern-plugin "system" commands: (lantern-system-commands g)))))) commands: (append (lantern-system-commands g) (fs-serve-commands g reg)) routes: (list (cons "local" (make-fs-resolver reg))))))))lantern-system/test/test-lantern-system.sglmodified
(lantern registry) (lantern webview) (lantern bridge) (lantern serve) ; make-servable-registry / make-fs-resolver (lantern-system plugin) (lantern-system fs) ; fs-serve-commands (lantern-system pty));; ============================================================ (assert-equal (dict-ref r name: #f) "a.txt") (assert-true (string? (dict-ref r type: #f))))));; ---- fs.serve-file / the lantern://local/<handle> round-trip ----;; The load-bearing new path: a grant-checked command registers a real file, the;; handle-scoped fs route then serves its bytes, and unserve revokes it. The;; command and the route share ONE registry, exactly as the plugin wires them.(test "fs.serve-file registers a file the fs route serves; unserve revokes it" (let* ((dir (make-temp-directory)) (g (make-grants)) (_ (grant-add! g (string-append "fs:rw:" dir))) (img (path-join dir "pic.png")) (__ (write-file-string img "PNGDATA")) (reg (make-servable-registry)) (serve-cmds (fs-serve-commands g reg)) (serve (handler-for serve-cmds "fs.serve-file")) (unserve (handler-for serve-cmds "fs.unserve-file")) (resolve (make-fs-resolver reg)) (r (serve (dict path: img) #f)) (handle (dict-ref r handle: #f))) (assert-true (string? handle)) (assert-equal (dict-ref r url: #f) (string-append "lantern://local/" handle)) ;; the fs route now serves the registered file (200 + real-extension mime) (let ((resp (resolve (string-append "/" handle)))) (assert-equal (dict-ref resp status: #f) 200) (assert-equal (dict-ref resp mime: #f) "image/png")) ;; unserve revokes the handle -> 404 (authority does not outlive the view) (unserve (dict handle: handle) #f) (assert-equal (dict-ref (resolve (string-append "/" handle)) status: #f) 404)));; Registration HONORS the grant scope (fs-stat gates it): a path outside the;; grant raises, so a scoped embedding cannot register a file it may not read.(test "fs.serve-file refuses a path outside the grant scope" (let* ((dir (make-temp-directory)) (g (make-grants)) (_ (grant-add! g (string-append "fs:rw:" dir))) (reg (make-servable-registry)) (serve (handler-for (fs-serve-commands g reg) "fs.serve-file")) (raised (guard (e (#t #t)) (serve (dict path: "/etc/hostname") #f) #f))) (assert-true raised) ;; and nothing got registered for a would-be handle (assert-equal (servable-lookup reg "f1") #f)))(test "fs.read-dir lists entries with kinds" (let* ((fx (fs-fixture)) (rd (handler-for (dict-ref fx cmds:) "fs.read-dir")))lantern/src/lantern/app.sglmodified
(define (lantern-command name handler (keys: (spec #f))) (dict lantern/command: #t name: name handler: handler spec: spec)) (define (lantern-plugin name (keys: (commands (list)) (init #f) (js #f))) (dict lantern/plugin: #t name: name commands: commands init: init js: js)) ;; `routes:` lets a plugin serve its OWN lantern:// host(s): an alist of ;; (host-string . (path->response)) folded into the scheme resolver, so e.g. ;; the lantern-system plugin owns lantern://local/<handle> for streamed ;; filesystem serving alongside the app's bundled assets on lantern://app. (define (lantern-plugin name (keys: (commands (list)) (init #f) (js #f) (routes (list)))) (dict lantern/plugin: #t name: name commands: commands init: init js: js routes: routes)) (define (lantern-app (keys: (name "lantern-app") (assets "./dist") (dict-ref app plugins: (list)))))) (append from-modules from-app from-plugins))) ;; Every plugin's lantern:// host routes, folded into one alist for the scheme ;; resolver. A collision (two plugins claiming one host) keeps the FIRST — a ;; deliberate host clash is a config error, and assoc's first-wins is the least ;; surprising resolution. (define (collect-routes app) (apply append (map (lambda (p) (dict-ref p routes: (list))) (dict-ref app plugins: (list))))) ;; ---- origin gating ---------------------------------------------------- (define (starts-with? s prefix) width: width height: height decorated: decorated transparent: transparent)))) ;; Serve local assets over lantern://; a URL loads directly. ;; Serve local assets over lantern://app; plugin routes (e.g. ;; lantern://local/<handle> from lantern-system) serve their own hosts. A ;; URL asset loads directly, so no scheme is registered in that mode. (unless is-url (webview-register-scheme wv "lantern" (make-asset-resolver assets))) (webview-register-scheme wv "lantern" (make-scheme-resolver (make-asset-resolver assets) (collect-routes app)))) ;; Inject the wasm streaming->buffer fallback FIRST (before the app boots ;; and calls compileStreaming): WebKitGTK will not stream-compile wasm overlantern/src/lantern/backend/gtk.sglmodified
;; Tab/Shift+Tab into the page (v1 is single-window; see the callback note). (define *main-view* #f) (define (path-of request) (let ((p (webkit-uri-scheme-request-get-path request))) (if (or (not p) (= (string-length p) 0) (string=? p "/")) "/index.html" p))) ;; The FULL request URI (host intact): "lantern://app/index.html", ;; "lantern://local/<handle>". The resolver is a uri->response that parses the ;; host itself and dispatches (make-scheme-resolver) — get_path would strip the ;; host, collapsing lantern://local onto lantern://app, so we hand over get_uri. (define (request-uri request) (webkit-uri-scheme-request-get-uri request)) ;; Scheme handler (2-arg C->Sigil callback). Resolves synchronously and ;; finishes the request in-place - no suspension, all inside a guard. (define (on-scheme-request request user-data) (guarded "scheme-request" #f (lambda () (let* ((path (path-of request)) (resp (and *scheme-resolver* (*scheme-resolver* path)))) (let* ((uri (request-uri request)) (resp (and *scheme-resolver* (string? uri) (*scheme-resolver* uri)))) (if (and resp (dict-ref resp bytes: #f)) (let* ((bytes (dict-ref resp bytes: #f)) (mime (dict-ref resp mime: "application/octet-stream"))lantern/src/lantern/serve.sglmodified
(sigil path) (sigil fs)) (export make-asset-resolver mime-for-path) mime-for-path make-servable-registry servable-register! servable-unregister! servable-lookup make-fs-resolver parse-lantern-uri make-scheme-resolver) (begin ;; Extension -> MIME. An alist (string keys) looked up with assoc/string=?. (let ((i (string-index s (lambda (c) (char=? c ch))))) (if i (substring s 0 i) s))) ;; Drop every leading '/' so the result is a path RELATIVE to the asset root. ;; Stripping only ONE leaves an absolute remainder ("//etc/passwd" -> "/etc/ ;; passwd"), and path-join with an absolute second arg DISCARDS the base dir — ;; i.e. it would read /etc/passwd straight off the disk. A request path is ;; always relative to the asset dir, so all leading slashes are noise. (define (strip-leading-slashes s) (let loop ((i 0)) (if (and (< i (string-length s)) (char=? (string-ref s i) #\/)) (loop (+ i 1)) (substring s i (string-length s))))) ;; Normalize a request path to a relative asset path: drop any query string ;; or fragment (cache-busting URLs like app.js?v=2 must still resolve to ;; app.js), strip the leading slash, and reject parent-directory traversal. ;; app.js), strip ALL leading slashes (see above — one is not enough; an ;; absolute remainder escapes the asset dir), and reject parent-directory ;; traversal. (define (normalize-request-path path) (let* ((clean (cut-at (cut-at path #\?) #\#)) (rel (if (and (> (string-length clean) 0) (char=? (string-ref clean 0) #\/)) (substring clean 1 (string-length clean)) clean))) (rel (strip-leading-slashes clean))) (if (string-contains? rel "..") #f rel))) ;; Encode an ASCII string to a bytevector. Framework-generated bodies (the mime: "text/plain; charset=utf-8" bytes: (ascii->bytes (string-append "lantern: not found: " path)))) ;; ---- servable-file registry (the lantern://local/<handle> fs route) ------ ;; ;; THE SECURITY BOUNDARY for filesystem serving. The fs resolver serves ONLY ;; files that were explicitly registered (each registration yields an opaque ;; handle); an unregistered handle 404s. So the lantern:// scheme can never be ;; turned into an arbitrary full-disk read by web content — a page may request ;; lantern://local/<anything>, but only handles the HOST registered ever ;; resolve. The host command that registers a path (lantern-system's ;; fs.serve-file) grant-checks it first, so registration also honors the ;; embedding's grant scope; this route adds no authority beyond that command. ;; ;; A registry is a dict of op-closures over one shared mutable table. Handles ;; are a per-run monotonic counter (the same globally-unique-for-the-run ;; scheme the pty session ids use) — sufficient for the scoping guarantee, ;; since only registered handles ever resolve. A page cannot forge authority ;; by guessing a handle: an unregistered one 404s, and a registered one only ;; ever names a path the host already chose to serve. ;; Remove one (handle . path) pair from an alist, preserving order. (define (alist-remove al key) (let loop ((xs al) (acc (list))) (cond ((null? xs) (reverse acc)) ((string=? (car (car xs)) key) (loop (cdr xs) acc)) (else (loop (cdr xs) (cons (car xs) acc)))))) (define (make-servable-registry) (let ((entries (list)) ; alist: handle (string) -> absolute path (counter 0)) (dict register!: (lambda (abs-path) (set! counter (+ counter 1)) (let ((handle (string-append "f" (number->string counter)))) (set! entries (cons (cons handle abs-path) entries)) handle)) unregister!: (lambda (handle) (set! entries (alist-remove entries handle))) lookup: (lambda (handle) (let ((hit (assoc handle entries))) (if hit (cdr hit) #f)))))) ;; Register `abs-path`, returning its opaque handle (a bare string; the caller ;; forms the lantern://local/<handle> URL). Nothing validates the path here — ;; the grant check lives in the host command that calls this. (define (servable-register! reg abs-path) ((dict-ref reg register!: #f) abs-path)) ;; Drop a handle so its file is no longer servable (view close / cleanup). (define (servable-unregister! reg handle) ((dict-ref reg unregister!: #f) handle)) ;; The absolute path for a handle, or #f if it was never registered. (define (servable-lookup reg handle) ((dict-ref reg lookup: #f) handle)) ;; A request path -> its opaque handle: drop any query/fragment (an <img> may ;; append ?v=… ; a handle carries none but be robust), strip the leading ;; slash, and reject the empty handle. The handle is matched EXACTLY against ;; the registry, so a multi-segment or dotted request (lantern://local/f1/x) ;; simply misses and 404s — there is no path join, hence no traversal surface. (define (request->handle path) (let* ((clean (cut-at (cut-at path #\?) #\#)) (h (if (and (> (string-length clean) 0) (char=? (string-ref clean 0) #\/)) (substring clean 1 (string-length clean)) clean))) (if (= (string-length h) 0) #f h))) ;; Build an fs resolver over a servable registry: path -> response. Serves the ;; registered file's bytes with the MIME of its REAL extension (the handle has ;; none); an unregistered handle, or a registered path that has since ;; vanished, 404s. No caching — an image is fetched once and browser-cached, ;; and re-reading avoids serving stale bytes; the GTK backend retains the ;; returned buffer for the stream's life. (define (make-fs-resolver reg) (lambda (path) (let ((handle (request->handle path))) (if (not handle) (not-found path) (let ((full (servable-lookup reg handle))) (if (and full (file-exists? full)) (dict status: 200 mime: (mime-for-path full) bytes: (read-file-bytes full)) (not-found path))))))) ;; ---- host-routed scheme resolver (app assets + fs handles) --------------- ;; ;; The webview scheme handler hands us the FULL request URI (get_uri returns ;; it verbatim, host intact — probed on WebKitGTK; get_path strips the host, ;; so a path-only resolver could not tell lantern://local from lantern://app). ;; We parse the authority (host) ourselves and dispatch: a registered host ;; (e.g. "local" -> the fs handle route) serves its own way; every other host ;; ("app", or anything) falls through to the bundled-asset resolver. ;; "lantern://<host>/<path>" -> (cons host path); path keeps its leading ;; slash. "lantern://app" (no path) -> (cons "app" "/"). A string that is not ;; a lantern:// URI is treated as a bare asset path under the empty host, so a ;; caller that still passes a plain path keeps working. (define lantern-uri-prefix "lantern://") (define (has-prefix? s prefix) (let ((pl (string-length prefix))) (and (>= (string-length s) pl) (string=? (substring s 0 pl) prefix)))) (define (parse-lantern-uri uri) (if (not (has-prefix? uri lantern-uri-prefix)) (cons "" uri) (let* ((rest (substring uri (string-length lantern-uri-prefix) (string-length uri))) (slash (string-index rest (lambda (c) (char=? c #\/))))) (if slash (cons (substring rest 0 slash) (substring rest slash (string-length rest))) (cons rest "/"))))) ;; Compose an asset resolver (path->response) with host-routed resolvers into ;; the uri->response the scheme handler calls. `routes` is an alist of ;; (host-string . (path->response)). The asset branch applies the root -> ;; /index.html default (the SPA entry) the backend's path-of used to. (define (make-scheme-resolver asset-resolver routes) (lambda (uri) (let* ((hp (parse-lantern-uri uri)) (host (car hp)) (path (cdr hp)) (route (assoc host routes))) (if route ((cdr route) path) (asset-resolver (if (or (= (string-length path) 0) (string=? path "/")) "/index.html" path)))))) ;; Build a resolver over a directory of static assets, with a small in-memory ;; cache (which also keeps served byte buffers alive for the stream's life). (define (make-asset-resolver dir)lantern/test/test-lantern.sglmodified
(resolve (make-asset-resolver dir))) (assert-equal (dict-ref (resolve "/../../etc/passwd") status: #f) 404)));; ---- servable-file registry + fs resolver (lantern://local/<handle>) ----;; The handle-scoped fs-serving route: only explicitly-registered files resolve.(test "servable registry registers, looks up, and unregisters a handle" (let* ((reg (make-servable-registry)) (h (servable-register! reg "/tmp/some/image.png"))) (assert-true (string? h)) (assert-equal (servable-lookup reg h) "/tmp/some/image.png") (servable-unregister! reg h) (assert-equal (servable-lookup reg h) #f)))(test "servable registry issues distinct handles per registration" (let* ((reg (make-servable-registry)) (h1 (servable-register! reg "/a.png")) (h2 (servable-register! reg "/b.png"))) (assert-true (not (string=? h1 h2))) (assert-equal (servable-lookup reg h1) "/a.png") (assert-equal (servable-lookup reg h2) "/b.png")))(test "fs resolver serves a registered file with its real-extension mime" (let* ((dir (make-temp-directory)) (img (path-join dir "pic.png")) (_ (write-file-string img "PNGDATA")) (reg (make-servable-registry)) (h (servable-register! reg img)) (resolve (make-fs-resolver reg)) (resp (resolve (string-append "/" h)))) (assert-equal (dict-ref resp status: #f) 200) (assert-equal (dict-ref resp mime: #f) "image/png") (assert-true (bytevector? (dict-ref resp bytes: #f))) (assert-true (> (bytevector-length (dict-ref resp bytes: #f)) 0))))(test "fs resolver ignores a cache-busting query or fragment on the handle" (let* ((dir (make-temp-directory)) (img (path-join dir "pic.jpg")) (_ (write-file-string img "JPG")) (reg (make-servable-registry)) (h (servable-register! reg img)) (resolve (make-fs-resolver reg))) (assert-equal (dict-ref (resolve (string-append "/" h "?v=2")) status: #f) 200) (assert-equal (dict-ref (resolve (string-append "/" h "#frag")) status: #f) 200)));; THE SCOPING GUARANTEE, proven fallible: a REAL file addressed by its own;; absolute path (never registered) 404s — the resolver serves by HANDLE, never;; by path. If make-fs-resolver ever served `path` from disk directly (dropping;; the registry lookup), this exact request would return 200 and leak the file,;; so the test goes red. This is the security boundary David reviews.(test "fs resolver 404s a real file addressed by path instead of a handle" (let* ((dir (make-temp-directory)) (secret (path-join dir "secret.txt")) (_ (write-file-string secret "TOPSECRET")) (reg (make-servable-registry)) (resolve (make-fs-resolver reg)) ;; request the real absolute path AS IF it were a handle — must NOT serve (resp (resolve (string-append "/" secret)))) (assert-equal (dict-ref resp status: #f) 404)))(test "fs resolver 404s an arbitrary unregistered handle" (let* ((reg (make-servable-registry)) (resolve (make-fs-resolver reg))) (assert-equal (dict-ref (resolve "/f1") status: #f) 404)))(test "fs resolver 404s traversal-shaped and empty requests (no path join)" (let* ((reg (make-servable-registry)) (resolve (make-fs-resolver reg))) (assert-equal (dict-ref (resolve "/../../etc/passwd") status: #f) 404) (assert-equal (dict-ref (resolve "/") status: #f) 404) (assert-equal (dict-ref (resolve "") status: #f) 404)))(test "fs resolver 404s a registered handle whose file does not exist" (let* ((reg (make-servable-registry)) (h (servable-register! reg "/nonexistent/nope.png")) (resolve (make-fs-resolver reg))) (assert-equal (dict-ref (resolve (string-append "/" h)) status: #f) 404)));; ---- parse-lantern-uri + make-scheme-resolver (host-routed dispatch) ----(test "parse-lantern-uri splits host and path, preserving the leading slash" (assert-equal (parse-lantern-uri "lantern://app/index.html") (cons "app" "/index.html")) (assert-equal (parse-lantern-uri "lantern://local/f7") (cons "local" "/f7")) (assert-equal (parse-lantern-uri "lantern://local/sub/dir/x.png") (cons "local" "/sub/dir/x.png")) ;; no path after the host -> "/" (assert-equal (parse-lantern-uri "lantern://app") (cons "app" "/")) (assert-equal (parse-lantern-uri "lantern://app/") (cons "app" "/")) ;; not a lantern:// URI -> empty host, whole string as path (defensive) (assert-equal (parse-lantern-uri "/plain/path") (cons "" "/plain/path")))(test "scheme resolver routes app host to assets (root -> index.html)" (let* ((dir (make-temp-directory)) (_ (write-file-string (path-join dir "index.html") "<h1>root</h1>")) (__ (write-file-string (path-join dir "app.js") "1")) (resolve (make-scheme-resolver (make-asset-resolver dir) (list)))) (assert-equal (dict-ref (resolve "lantern://app/index.html") status: #f) 200) (assert-equal (dict-ref (resolve "lantern://app/") status: #f) 200) ; -> index.html (assert-equal (dict-ref (resolve "lantern://app") status: #f) 200) ; -> index.html (assert-equal (dict-ref (resolve "lantern://app/app.js") status: #f) 200) (assert-equal (dict-ref (resolve "lantern://app/missing.js") status: #f) 404)))(test "scheme resolver routes local host to the fs handle route" (let* ((dir (make-temp-directory)) (_ (write-file-string (path-join dir "index.html") "<h1>x</h1>")) (img (path-join dir "pic.png")) (___ (write-file-string img "PNG")) (reg (make-servable-registry)) (h (servable-register! reg img)) (resolve (make-scheme-resolver (make-asset-resolver dir) (list (cons "local" (make-fs-resolver reg)))))) (assert-equal (dict-ref (resolve (string-append "lantern://local/" h)) status: #f) 200) (assert-equal (dict-ref (resolve (string-append "lantern://local/" h)) mime: #f) "image/png") (assert-equal (dict-ref (resolve "lantern://local/f999") status: #f) 404))) ; unregistered;; Cross-host scoping, proven fallible: a real file addressed on EITHER host by;; its own path must not leak. The app host joins under the assets dir (an;; absolute /etc path lands outside and 404s); the local host is handle-scoped.(test "scheme resolver leaks no file addressed by absolute path on either host" (let* ((dir (make-temp-directory)) (_ (write-file-string (path-join dir "index.html") "<h1>x</h1>")) (secret (path-join dir "secret.txt")) (__ (write-file-string secret "TOPSECRET")) (reg (make-servable-registry)) ; secret registered NOWHERE (resolve (make-scheme-resolver (make-asset-resolver dir) (list (cons "local" (make-fs-resolver reg)))))) (assert-equal (dict-ref (resolve (string-append "lantern://local/" secret)) status: #f) 404) (assert-equal (dict-ref (resolve (string-append "lantern://app/" secret)) status: #f) 404)));; ============================================================;; (lantern registry);; ============================================================