retroforth/example/Casket-HTTP.forth

185 lines
4.6 KiB
Text
Raw Normal View History

#!/usr/bin/env retro
## Overview
Casket is an HTTP server written in RETRO.
## History
v0 Barebones, HTTP/1.0 server for a single file
v1 Added support for multiple HTML files
v2 Added support for non-HTML files
v3 HTTP/1.1, support virtual hosts
v4 Support 404 error code for file not found
## Casket v4
This is a small HTTP/1.1 server.
First, some configuration options. Since this will run under
inetd there's no need to specify the port. But the path to the
files to serve is rather useful, so define it here.
~~~
'/root/web 'WEBROOT s:const
~~~
Next, I need to handle the incoming requests. In v0 these were
just discarded, but here we actually want to store the request
So an incoming request will look like:
GET / HTTP/1.1
Host: retroforth.org
With the lines ending in a CR,LF sequence.
I need to allocate space for the data I care about. There are
two items:
- The `Requested` file
- The desired virtual `Host`
~~~
'Requested d:create #1025 allot
'Host d:create #1025 allot
~~~
The header processor will read each item and store the `Host`
and `Requested` file. Everything else is ignored.
I implement `eot?` to decide if a line (or field) indicator
has been reached. This is used by `s:get` to decide when the
input should stop. `s:get` records the characters read into
the specified buffer. And finally, `read-request` reads the
input.
~~~
'Done var
:eot? (c-f)
[ ASCII:CR eq? ]
[ ASCII:LF eq? ]
[ ASCII:SPACE eq? ] tri or or ;
:s:get (a-)
buffer:set [ c:get [ buffer:add ] [ eot? ] bi ] until
buffer:get drop ;
:read-request (-)
[ here s:get
here s:to-upper 'GET s:eq? [ &Requested s:get &Done v:inc ] if
here s:to-upper 'HOST: s:eq? [ &Host s:get &Done v:inc ] if
@Done #2 eq? ] until ;
~~~
Next is reading in the desired file. An initial request may be
just a **/**. In this case, Casket will replace the `Requested`
filename with **/index.html**. In the odd case that a file is
requested without a leading **/**, I have a word to add this.
And then a word that constructs a filename.
~~~
:map-/-to-index (-)
&Requested '/ s:eq?
[ '/index.html &Requested s:copy ] if ;
:ensure-leading-/ (-)
@Requested $/ -eq?
[ '/ &Requested s:append s:keep &Requested s:copy ] if ;
:filename (-s)
map-/-to-index ensure-leading-/
&Requested &Host WEBROOT '%s/%s%s s:format ;
~~~
Next, I need to determine the file type. I'll do this by taking
a look at the extension, and mapping this to a MIME type.
~~~
:get-mime-type (-s)
filename [ $. s:index-of ] sip +
(textual_files)
'.txt [ 'text/plain ] s:case
'.md [ 'text/markdown ] s:case
'.htm [ 'text/html ] s:case
'.html [ 'text/html ] s:case
'.css [ 'text/css ] s:case
'.c [ 'text/plain ] s:case
'.h [ 'text/plain ] s:case
'.forth [ 'text/plain ] s:case
'.retro [ 'text/plain ] s:case
(image_files)
'.png [ 'image/png ] s:case
'.jpg [ 'image/jpeg ] s:case
'.jpeg [ 'image/jpeg ] s:case
'.gif [ 'image/gif ] s:case
'.bmp [ 'image/bmp ] s:case
(application_files)
'.pdf [ 'application/pdf ] s:case
'.gz [ 'application/gzip ] s:case
'.zip [ 'application/zip ] s:case
'.json [ 'application/json ] s:case
'.js [ 'application/x-javascript ] s:case
'.xml [ 'application/xml ] s:case
drop 'text/plain ;
~~~
Using these, I can construct a word to read in the file and
send it to the client.
Reading files is now a bit more involved, since images and
other formats have binary data.
~~~
'FID var
:read-file (-an)
here
filename file:R file:open !FID
@FID file:size [ [ @FID file:read , ] times ] sip
@FID file:close ;
:eol (-) ASCII:CR c:put ASCII:LF c:put ;
:send-file (-)
get-mime-type 'Content_type:_%s s:format s:put eol eol
read-file [ fetch-next c:put ] times drop ;
~~~
In the above, `eol` will send an end of line sequence.
The last support word is a handler for 404 errors. This
will send the 404 status code and a human readable error
message.
~~~
:404 'HTTP/1.1_404_OK s:put eol
'Content-type:_text/html s:put eol eol
'ERROR_404:_FILE_NOT_FOUND s:put eol ;
~~~
And now for the top level server.
Receive a request:
~~~
read-request
~~~
See if the file exists:
~~~
filename file:exists?
~~~
Send an "200 OK" response and the file (or a 404 if
the file wasn't found):
~~~
[ 'HTTP/1.1_200_OK s:put eol send-file ]
[ 404 ] choose
~~~
And v4 is done.