#!/usr/bin/rre # Atua: A Gopher Server Atua is a gopher server written in Retro. This will get run as an inetd service, which keeps things simple as it prevents needing to handle socket I/O directly. # Features Atua is a minimal server targetting the the basic Gopher0 protocol. It supports a minimal gophermap format for indexes and transfer of static content. -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ # Script Prelude Atua uses Retro's *rre* interface layer. Designed to run a single program then exit, this makes using Retro as a scripting language possible. # Configuration Atua needs to know: - the path to the files to serve - the name of the index file - The maximum length of a selector - The maximum file size ~~~ '/home/crc/atua s:keep 'PATH const '/gophermap s:keep 'DEFAULT-INDEX const #1024 'MAX-SELECTOR-LENGTH const #1000000 #4 * 'MAX-FILE-SIZE const 'forthworks.com s:keep 'SERVER const '70 s:keep 'PORT const ~~~ # I/O Words Retro only supports basic output by default. The RRE interface that Atua uses adds support for files and stdin, so we use these and provide some other helpers. *Console Output* The Gopher protocol uses tabs and cr/lf for signficant things. To aid in this, I define output words for tabs and end of line. ~~~ :eol (-) ASCII:CR c:put ASCII:LF c:put ; ~~~ *Console Input* Input lines end with a cr, lf, or tab. The `eol?` checks for this. The `gets` word could easily be made more generic in terms of what it checks for. This suffices for a Gopher server though. ~~~ :eol? (c-f) [ ASCII:CR eq? ] [ ASCII:LF eq? ] [ ASCII:HT eq? ] tri or or ; :s:get (a-) buffer:set [ c:get dup buffer:add eol? not ] while ; ~~~ # Gopher Namespace Atua uses a `gopher:` namespace to group the server related words. ~~~ {{ ~~~ First up are buffers for the selector string and the file buffer. The variables and buffers are kept private. ~~~ 'Selector d:create MAX-SELECTOR-LENGTH n:inc allot :buffer here ; ~~~ Next up, variables to track information related to the requested selector. Atua will construct filenames based on these. ~~~ 'Requested-File var 'Requested-Index var ~~~ `FID`, the file id, tracks the open file handle that Atua uses when reading in a file. The `Size` variable will hold the size of the file (in bytes). ~~~ 'FID var 'Size var 'Mode var ~~~ I use a `Server-Info` variable to decide whether or not to display the index footer. This will become a configurable option in the future. ~~~ 'Server-Info var ~~~ ~~~ :with-path (-s) PATH &Selector s:chop s:append ; :construct-filenames (-) with-path s:keep !Requested-File with-path '/gophermap s:append s:keep !Requested-Index ; ~~~ A *gophermap* is a file that makes it easier to handle Gopher menus. Atua's gophermap support covers: - comment lines Comment lines are static text without any tabs. They will be reformatted according to protocol and sent. - selector lines Any line with a tab is treated as a selector line and is transferred without changing. ~~~ 'Tab var :eol? [ ASCII:LF eq? ] [ ASCII:CR eq? ] bi or ; :tab? @Tab ; :check-tab dup ASCII:HT eq? [ &Tab v:on ] if ; :gopher:gets (a-) &Tab v:off buffer:set [ @FID file:read dup buffer:add check-tab eol? not ] while buffer:get drop ; ~~~ The internal helpers are now defined, so switch to the part of the namespace that'll be left exposed to the world. ~~~ ---reveal--- ~~~ An information line s:get a format like: i...text...<tab><tab>null.host<tab>port<cr,lf> The `gopher:i` displays a string in this format. It's used later for the index footer. ~~~ :gopher:i (s-) 'i%s\t\tnull.host\t1 s:format s:put eol ; ~~~ ~~~ :gopher:get-selector (-) &Selector s:get ; :gopher:file-requested? (-f) &Selector s:chop s:length n:-zero? ; :gopher:valid-file? (-f) [ @Requested-File file:R file:open file:size n:strictly-positive? ] [ FALSE ] choose ; :gopher:get-index (-s) @Requested-Index file:exists? [ @Requested-Index &Server-Info v:on ] [ PATH '/empty.index s:append ] choose ; :gopher:file-for-request (-s) &Mode v:off construct-filenames gopher:file-requested? [ @Requested-File file:exists? gopher:valid-file? [ @Requested-File ] [ gopher:get-index ] choose ] [ PATH DEFAULT-INDEX s:append &Server-Info v:on ] choose ; :gopher:read-file (f-s) file:R file:open !FID buffer buffer:set @FID file:size !Size @Size [ @FID file:read buffer:add ] times @FID file:close buffer ; :gopher:line (s-) dup [ ASCII:HT eq? ] s:filter s:length #1 gt? [ s:put ] [ s:put tab SERVER s:put tab PORT s:put ] choose eol ; :gopher:generate-index (f-) file:R file:open !FID @FID file:size !Size [ buffer gopher:gets buffer tab? [ gopher:line ] [ gopher:i ] choose @FID file:tell @Size lt? ] while @FID file:close ; ~~~ In a prior version of this I used `s:put` to send the content. That stopped at the first zero value, which kept it from working with binary data. I added `gopher:send` to send the `Size` number of bytes to stdout, fixing this issue. ~~~ :gopher:send (p-) @Size [ fetch-next c:put ] times drop ; ~~~ The only thing left is the top level server. ~~~ :gopher:server gopher:get-selector gopher:file-for-request @Server-Info [ gopher:generate-index '------------------------------------------- gopher:i 'forthworks.com:70_/_atua_/_running_on_retro gopher:i '. s:put eol ] [ gopher:read-file gopher:send ] choose ; ~~~ Close off the helper portion of the namespace. ~~~ }} ~~~ And run the `gopher:server`. ~~~ gopher:server reset ~~~