luv

Workshop wiki

web-server.lisp

luvcraft/web-server.lisp

system luvcraft/web · 21 definitions · on GitHub

The deliberately small HTTP and page protocol for the luvcraft web site.

A web-page owns one url subtree. Adding a page means adding an object and a respond-to-web-request method, rather than growing the socket server's knowledge of gallery assets, shader URLs, or future instruments.

in-package#:luvcraft.web
defclassweb-response
status:initarg:status:readerweb-response-status
content-type:initarg:content-type:readerweb-response-content-type
body:initarg:body:readerweb-response-body
defunmake-web-response
statuscontent-typebody
make-instance'web-response:statusstatus:content-typecontent-type:bodybody
defunok-response
content-typebody
make-web-response"200 OK"content-typebody
defunnot-found-response
make-web-response"404 Not Found""text/plain; charset=utf-8"
formatnil"not found~%"
defclassweb-page
path:initarg:path:readerweb-page-path
label:initarg:label:readerweb-page-label
description:initarg:description:readerweb-page-description
:documentation

A semantic page mounted at one url subtree.

defclassweb-application
pages:initarg:pages:readerweb-application-pages
defunmake-web-application
&restpages
make-instance'web-application:pagespages
defgenericrespond-to-web-request
receiverpath
:documentation

Return a web-response for path from a web application or mounted page.

defunhtml-escaped
string
with-output-to-string
output
loopforcharacteracrossstringdo
casecharacter
#\&
write-string"&"output
#\<
write-string"&lt;"output
#\>
write-string"&gt;"output
#\"
write-string"&quot;"output
otherwise
write-charcharacteroutput
defunapplication-index
application
with-output-to-string
output
formatoutput"<!doctype html>~%"
formatoutput"<html lang=\"en\"><head><meta charset=\"utf-8\">~%"
formatoutput"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">~%"
formatoutput"<title>Luvcraft</title><style>~%"
formatoutput":root { color-scheme: dark; font-family: ui-monospace, SFMono-Regular, monospace; background: #10130f; color: #ecf0df }~%"
formatoutput"body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: radial-gradient(circle at 70% 20%, #29352b, #10130f 55%) }~%"
formatoutput"main { width: min(42rem, calc(100% - 3rem)); padding: 4rem 0 } p { color: #aeb9a8; line-height: 1.6 }~%"
formatoutput"ul { padding: 0; display: grid; gap: 1rem } li { list-style: none } a { display: block; padding: 1.25rem; border: 1px solid #49584a; color: inherit; text-decoration: none; background: #171d17cc }~%"
formatoutput"a:hover { border-color: #d49b68; transform: translateY(-1px) } strong { display: block; color: #f2c28f; margin-bottom: .4rem }~%"
formatoutput"</style></head><body><main><p>LUVCRAFT / WEB</p><h1>Little windows into the world.</h1><ul>~%"
dolist
page
web-application-pagesapplication
formatoutput"<li><a href=\"~A/\"><strong>~A</strong>~A</a></li>~%"
html-escaped
web-page-pathpage
html-escaped
web-page-labelpage
html-escaped
web-page-descriptionpage
formatoutput"</ul></main></body></html>~%"
defunpage-relative-path
pagepath
let
prefix
web-page-pathpage
cond
string=pathprefix
"/"
and
>
lengthpath
lengthprefix
uiop:string-prefix-pprefixpath
char=#\/
charpath
lengthprefix
subseqpath
lengthprefix
defmethodrespond-to-web-request
applicationweb-application
path
cond
or
string=path"/"
string=path"/index.html"
ok-response"text/html; charset=utf-8"
string=path"/healthz"
ok-response"text/plain; charset=utf-8"
formatnil"ok~%"
t
loopforpagein
web-application-pagesapplication
forrelative-path=whenrelative-pathreturnfinally
defunresponse-octet-length
string
length
sb-ext:string-to-octetsstring:external-format:utf-8
defunwrite-http-response
streamresponse
formatstream"HTTP/1.1 ~A~C~C"
web-response-statusresponse
#\Return#\Linefeed
formatstream"Content-Type: ~A~C~C"
web-response-content-typeresponse
#\Return#\Linefeed
formatstream"Content-Length: ~D~C~C"
response-octet-length
web-response-bodyresponse
#\Return#\Linefeed
formatstream"Cache-Control: no-store~C~C"#\Return#\Linefeed
formatstream"Connection: close~C~C~C~C"#\Return#\Linefeed#\Return#\Linefeed
write-string
web-response-bodyresponse
stream
finish-outputstream
defunbounded-read-line
streamlimit
let
line
read-linestreamnilnil
when
andline
>
lengthline
limit
error"HTTP line exceeds ~D characters."limit
line
defundiscard-http-headers
stream
looprepeat64forline=until
or
nullline
string=line""
string=line
string#\Return
finally
unless
or
nullline
string=line""
string=line
string#\Return
error"Too many HTTP headers."
defunrequest-path
request-line
let
first-space
andrequest-line
position#\Spacerequest-line
second-spacenil
whenfirst-space
setfsecond-space
position#\Spacerequest-line:start
1+first-space
unless
andfirst-spacesecond-space
string="GET"request-line:end2first-space
error"Only a well-formed GET request is supported."
let*
target
subseqrequest-line
1+first-space
second-space
query
position#\?target
subseqtarget0query
defunserve-web-request
clientapplication
let
stream
sb-bsd-sockets:socket-make-streamclient:inputt:outputt:element-type'character:buffering:full:external-format:utf-8
unwind-protect
handler-case
let*
line
path
formatt"GET ~A~%"path
error
condition
format*error-output*"luvcraft web request failed: ~A~%"condition
ignore-errors
write-http-responsestream
make-web-response"400 Bad Request""text/plain; charset=utf-8"
formatnil"bad request~%"
ignore-errors
closestream
ignore-errors
sb-bsd-sockets:socket-closeclient
defunipv4-address
host
sb-bsd-sockets:host-ent-address
sb-bsd-sockets:get-host-by-namehost
defunserve-web-application
application&key
host"127.0.0.1"
port8765

Serve application on a small blocking HTTP/1.1 loop until interrupted.

let
socket
make-instance'sb-bsd-sockets:inet-socket:type:stream:protocol:tcp
unwind-protect
progn
setf
sb-bsd-sockets:sockopt-reuse-addresssocket
t
sb-bsd-sockets:socket-bindsocketport
sb-bsd-sockets:socket-listensocket16
formatt"luvcraft web: http://~A:~D/~%"hostport
finish-output
handler-case
loopforclient=
sb-bsd-sockets:socket-acceptsocket
do
serve-web-requestclientapplication
sb-sys:interactive-interrupt
formatt"Stopping luvcraft web.~%"
ignore-errors
sb-bsd-sockets:socket-closesocket