diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 140f808..4b1f9cf 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -14,6 +14,9 @@ * new example: konilo-wiki.retro * GNUmakefile fix from drakonis * addition of an i/o device for handling some errors +* fixed `f:tan` and `f:cos` +* added `file:read/bytes` and `file:write/bytes` +* Casket HTTP server is much, much faster ================================================================ diff --git a/example/Casket-HTTP.retro b/example/Casket-HTTP.retro index 53d4a67..729443e 100755 --- a/example/Casket-HTTP.retro +++ b/example/Casket-HTTP.retro @@ -1,21 +1,57 @@ #!/usr/bin/env retro -## Casket v5 +================================================================ + ____ _ _ __ + / ___|__ _ ___| | _____| |_ __ __/ /_ + | | / _` / __| |/ / _ \ __| \ \ / / '_ \ + | |__| (_| \__ \ < __/ |_ \ V /| (_) | + \____\__,_|___/_|\_\___|\__| \_/ \___/ -This is a small HTTP/1.1 server written in Retro Forth. +================================================================ + +Casket is a small HTTP/1.1 server written in RetroForth. + +Some notes on this: + +- runs under inetd +- use w/stunnel if you need HTTPS +- I recommend also using timelimit(1) with this + +Changes from v5: + +- removed support for running .fsp files +- now using `file:read/bytes` and `file:write/bytes` +- support for HEAD requests +- reject POST, PUT, DELETE requests +- various refactorings + +================================================================ 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. +files to serve is needed, so define it here. ~~~ -'/home/crc/www 'WEBROOT s:const +'/home/crc/wip-test '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 +The server supports virtual servers. For this, create a separate +directory for each under the `WEBROOT`. E.g., I use a setup like -So an incoming request will look like: + /www/retroforth.org + /www/retroforth.org:443 + /www/ilo.retroforth.org + +Note the port number: if using a port other than 80 this is +needed. I recommend just using a symlink. + +================================================================ + +Next, I need to handle the incoming requests. HTTP allows for a +large number of header fields, but I really only care about two: +GET and Host. + +An incoming request will look like: GET / HTTP/1.1 Host: retroforth.org @@ -32,7 +68,8 @@ three items: ~~~ 'Requested d:create #8193 allot 'Host d:create #1025 allot -'GET-Query var +'Method var +'Query var ~~~ The header processor will read each item and store the `Host` @@ -53,16 +90,27 @@ input. [ ASCII:SPACE eq? ] tri or or ; :s:get (a-) - buffer:set [ c:get [ buffer:add ] [ eot? ] bi ] until + buffer:set [ c:get &buffer:add &eot? bi ] until buffer:get drop ; +:req? (s-f) here s:to-upper swap s:eq? ; + :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 + 'GET req? [ &Requested s:get &Done v:inc #1 !Method ] if + 'HEAD req? [ &Requested s:get &Done v:inc #2 !Method ] if + + (unsupported) + 'PUT req? [ &Requested s:get &Done v:inc #0 !Method ] if + 'POST req? [ &Requested s:get &Done v:inc #0 !Method ] if + 'DELETE req? [ &Requested s:get &Done v:inc #0 !Method ] if + + 'HOST: req? [ &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 @@ -81,14 +129,20 @@ the requested file from any query string that may be present. @Requested $/ -eq? [ '/ &Requested s:append s:keep &Requested s:copy ] if ; +:s:terminate (s-) #0 swap store ; + :check-for-params (-) &Requested $? s:contains/char? - [ &Requested $? s:split/char drop dup n:inc !GET-Query #0 swap store ] if ; + [ &Requested $? s:split/char drop + [ n:inc !Query ] &s:terminate bi ] if ; + +:prepare + check-for-params map-/-to-index ensure-leading-/ ; :filename (-s) - check-for-params map-/-to-index ensure-leading-/ - &Requested &Host WEBROOT '%s/%s%s s:format - dup '%s/index.html s:format file:exists? [ '%s/index.html s:format ] if ; + prepare &Requested &Host WEBROOT '%s/%s%s s:format + dup '%s/index.html s:format + file:exists? [ '%s/index.html s:format ] if ; ~~~ Next, I need to determine the file type. I'll do this by taking @@ -101,18 +155,11 @@ a look at the extension, and mapping this to a MIME type. :get-mime-type (-s) filename-w/o-path [ $. s:index/char ] sip + - (fsp) - '.fsp [ 'application/fsp ] s:case (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 @@ -131,37 +178,61 @@ a look at the extension, and mapping this to a MIME type. 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. -If the mime type is application/fsp, this will run the code -in the file. The code should output the necessary headers -and content. +`transfer` performs the actual process of reading the requested +file and sending it to the client. This makes use of the +`file:read/bytes` and `file:write/bytes` to do the actual i/o. +These are used as they are faster than reading & writing on a +per-byte basis. + +Adjust the `FileBuffer` size (and matching size constant) as +desired. The requsted file will be read in chunks of this size, +so matching the splits better for te files you are using can +aid in performance. ~~~ -'FID var +{{ + 'FID var 'FOUT var 'ChunkSize var + 'FileBuffer d:create #16385 allot -:read-file (-an) - here - filename file:R file:open !FID - @FID file:size [ [ @FID file:read , ] times ] sip - @FID file:close ; + :open-files (-) + filename file:R file:open !FID + '/dev/stdout file:W file:open !FOUT ; + :prepare (-nn) + #16384 !ChunkSize + @FID file:size @ChunkSize n:divmod ; + + :read (-) &FileBuffer @ChunkSize @FID file:read/bytes drop ; + :write (-) &FileBuffer @ChunkSize @FOUT file:write/bytes drop ; + :chunk (-) read write ; + :cleanup (-) @FID file:close @FOUT file:close ; +---reveal--- + :transfer (-) + open-files prepare &chunk times !ChunkSize chunk cleanup ; +}} +~~~ + +~~~ :eol (-) ASCII:CR c:put ASCII:LF c:put ; :send-file (-) get-mime-type - dup 'application/fsp s:eq? - [ drop filename include ] - [ 'Content-type:_%s s:format s:put eol eol - read-file [ fetch-next c:put ] times drop ] choose ; + 'Content-type:_%s s:format s:put eol eol + @Method #1 eq? &transfer if ; ~~~ 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. @@ -169,9 +240,20 @@ 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 ; + '