; web.wisp — a small HTTP/1.1 server that runs inside a REPL session. ; Serves files under `public/` (relative to the session's filesystem root), ; one request per connection: read the request line, answer, close. ; The session's on-connection/on-data handlers call web-connection/web-data ; (see site/serve.wisp), so (reload web) changes the live server. (module web (provide web-connection web-data web-path content-type) (define byte-space (string-ref " " 0)) (define root "public") ; index of the first byte equal to `b` in `s` at or after `i`; -1 if none. (define find-byte (lambda (s b i) (if (= i (string-len s)) -1 (if (= (string-ref s i) b) i (find-byte s b (+ i 1)))))) ; does `s` end with `suffix`? (define ends-with (lambda (s suffix) (let (n (string-len s)) (let (k (string-len suffix)) (if (< n k) 0 (string=? (substring s (- n k) n) suffix)))))) ; does `s` start with `prefix`? (define starts-with (lambda (s prefix) (if (< (string-len s) (string-len prefix)) 0 (string=? (substring s 0 (string-len prefix)) prefix)))) ; index of `pat` in `s` at or after `i`, or -1. Jumps between bytes that ; match pat's first byte, so it allocates only at those candidates. (define find-str (lambda (s pat i) (let (j (find-byte s (string-ref pat 0) i)) (if (< j 0) -1 (if (< (string-len s) (+ j (string-len pat))) -1 (if (string=? (substring s j (+ j (string-len pat))) pat) j (find-str s pat (+ j 1)))))))) ; does `s` contain `pat` at or after `i`? (define contains (lambda (s pat i) (if (< (find-str s pat i) 0) 0 1))) ; the request target from "GET /path HTTP/1.1...", or "" if malformed. (define web-path (lambda (req) (let (a (find-byte req byte-space 0)) (if (< a 0) "" (let (b (find-byte req byte-space (+ a 1))) (if (< b 0) "" (substring req (+ a 1) b))))))) ; strip a query string: "/a?x=1" -> "/a" (define strip-query (lambda (p) (let (q (find-byte p (string-ref "?" 0) 0)) (if (< q 0) p (substring p 0 q))))) (define content-type (lambda (p) (if (ends-with p ".html") "text/html; charset=utf-8" (if (ends-with p ".css") "text/css; charset=utf-8" (if (ends-with p ".js") "text/javascript; charset=utf-8" (if (ends-with p ".svg") "image/svg+xml" (if (ends-with p ".txt") "text/plain; charset=utf-8" (if (ends-with p ".wisp") "text/plain; charset=utf-8" "application/octet-stream")))))))) ; join a list of strings (string-append takes exactly two) (define cat (lambda (xs) (if (null? xs) "" (string-append (car xs) (cat (cdr xs)))))) ; non-negative integer -> decimal text (define num->str (lambda (n) (if (< n 10) (substring "0123456789" n (+ n 1)) (string-append (num->str (/ n 10)) (num->str (- n (* 10 (/ n 10)))))))) (define response (lambda (status ctype body) (cat (list "HTTP/1.1 " status "\r\n" "Content-Type: " ctype "\r\n" "Content-Length: " (num->str (string-len body)) "\r\n" "Connection: close\r\n\r\n" body)))) ; "/" -> public/index.html, "/about" -> public/about.html, "/x.css" -> public/x.css (define file-for (lambda (p) (if (string=? p "/source") "web.wisp" (if (string=? p "/") (string-append root "/index.html") (if (ends-with p "/") (cat (list root p "index.html")) (if (< (find-byte p (string-ref "." 0) 0) 0) (cat (list root p ".html")) (string-append root p))))))) ; every occurrence of `pat` in `s` replaced by `rep`, scanning from `i` (define replace-from (lambda (s pat rep i) (let (j (find-str s pat i)) (if (< j 0) s (replace-from (cat (list (substring s 0 j) rep (substring s (+ j (string-len pat)) (string-len s)))) pat rep (+ j (string-len rep))))))) ; the value for `k` in an association list of (key value) pairs; 0 if absent (define assoc-get (lambda (k xs) (if (null? xs) 0 (if (string=? (car (car xs)) k) (car (cdr (car xs))) (assoc-get k (cdr xs)))))) ; ── live introspection ──────────────────────────────────────────────── ; Public: counts, names, statuses and request paths only. Paths come from ; visitors, so they are escaped and cut short. (define b-lt (string-ref "<" 0)) (define b-gt (string-ref ">" 0)) (define b-amp (string-ref "&" 0)) (define b-quot (string-ref "\"" 0)) (define b-apos (string-ref "'" 0)) (define esc-byte (lambda (s i) (let (c (string-ref s i)) (if (= c b-lt) "<" (if (= c b-gt) ">" (if (= c b-amp) "&" (if (= c b-quot) """ (if (= c b-apos) "'" (substring s i (+ i 1)))))))))) (define esc-from (lambda (s i acc) (if (= i (string-len s)) acc (esc-from s (+ i 1) (string-append acc (esc-byte s i)))))) (define html-escape (lambda (s) (esc-from (if (< 80 (string-len s)) (string-append (substring s 0 80) "...") s) 0 ""))) (define take (lambda (xs n) (if (null? xs) xs (if (= n 0) (list) (cons (car xs) (take (cdr xs) (- n 1))))))) ; on-data results (request paths) from firing entries (seq handler result), ; oldest first in, newest first out (define paths-of (lambda (entries acc) (if (null? entries) acc (paths-of (cdr entries) (if (string=? (car (cdr (car entries))) "on-data") (cons (car (cdr (cdr (car entries)))) acc) acc))))) (define recent-items (lambda (xs) (if (null? xs) "" (string-append (cat (list "
" (html-escape (car xs)) "" (html-escape name) " "
(substring id 0 8) " · " (html-escape (status-of id)) "" (short-hash hash) " " (html-escape type)
" " (num->str size) " bytes" (short-hash head) "Recording just started. Reload to see it.
"))))) ; live values for an HTML page: ; {{events}} events this session has handled (3 per request) ; {{heap}} this session's heap, in KB ; {{actors}}Nothing here.
")) ; the full response for a raw request (define answer (lambda (req) (let (p (strip-query (web-path req))) (if (string=? p "") (response "400 Bad Request" "text/plain" "bad request\n") (if (contains p ".." 0) not-found (let (f (file-for p)) (match (fs-read f) ((ok body) (let (text (bytes->string body)) (response "200 OK" (content-type f) ; docs (public/docs/) are built pages: no live keys to fill (if (ends-with f ".html") (if (starts-with f "public/docs/") text (fill text)) text)))) ((err e) not-found)))))))) (define web-connection (lambda (cid) (begin (tcp-activate cid) (tcp-set-active cid "active") cid))) ; e = (conn-id bytes). Answers and closes; returns "METHOD path" for the log. (define web-data (lambda (e) (let (cid (car e)) (let (req (bytes->string (car (cdr e)))) (begin (tcp-send cid (answer req)) (tcp-close cid) (web-path req)))))))