retroforth/example/Atua-WWW.retro
crc c8a5b047bd image: rename s:index-of, s:index-of-string, a:index-of, a:index-of-string
FossilOrigin-Name: 88416f7a177a6171739a75db5ad4d399735ecd29836424897629ca1787d776d2
2021-06-04 18:34:59 +00:00

311 lines
7.4 KiB
Forth
Executable file

#!/usr/bin/env retro
# Atua-WWW: Serving Gopher to Web Browsers
Atua is a Gopher server written in Retro. I've used it for a few
years, but most people don't use Gopher clients these days. This
is a variation of Atua that translates the Gopher directories to
HTML and speaks enough HTTP to serve files to the bloated
browsers in use today.
This will get run as an inetd service, which keeps things simple
as it prevents needing to handle socket I/O directly.
Atua-WWW uses Retro's *retro-unix* interface layer. Designed to
run a single program then exit, this makes using Retro very
useful on Unix (or Linux, Haiku, or other POSIX-ish systems).
# Configuration
Atua needs to know:
- the path to the files to serve
- the name of the index file
- The maximum length of a selector
~~~
'/home/crc/atua 'PATH s:const
'/gophermap 'DEFAULT-INDEX s:const
#1024 'MAX-SELECTOR-LENGTH 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 map these
to words 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 `s:get` 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:SPACE eq? ] tri or or ]
[ $? eq? ] bi or ;
:s:get (a-)
buffer:set
[ c:get dup buffer:add eol? not ] while ;
~~~
~~~
{{
~~~
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
~~~
These are just simple accessor words to aid in overall readability.
~~~
:requested-file (-s) @Requested-File ;
:requested-index (-s) @Requested-Index ;
~~~
~~~
:get-mime-type (s-s)
[ $. s:index/char ] sip +
(textual_files)
'.md [ 'text/markdown ] s:case
'.htm [ 'text/html ] s:case
'.html [ 'text/html ] s:case
'.retro.html [ 'text/html ] s:case
'.muri.html [ 'text/html ] s:case
'.forth.html [ 'text/html ] s:case
'.css [ 'text/css ] 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 ;
~~~
~~~
: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:s:get (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-)
s:put eol ;
~~~
The `gopher:icon` displays an indicator for menu items.
~~~
:gopher:icon
$0 [ '_TXT__ s:put ] case
$1 [ '[DIR]_ s:put ] case
$2 [ '_CSO__ s:put ] case
$3 [ '_ERR__ s:put ] case
$4 [ '_BIN__ s:put ] case
$5 [ '_BIN__ s:put ] case
$6 [ '_UUE__ s:put ] case
$7 [ '[FND]_ s:put ] case
$8 [ '_TEL__ s:put ] case
$9 [ '_BIN__ s:put ] case
$I [ '_IMG__ s:put ] case
$S [ '_AUD__ s:put ] case
$g [ '_GIF__ s:put ] case
$h [ '_HTM__ s:put ] case
drop '_???__ s:put
;
~~~
~~~
:gopher:get-selector (-)
&Selector s:get &Selector s:get ;
(Rewrite_This:_It's_too_big)
:gopher:file-for-request (-s)
&Mode v:off
construct-filenames
&Selector s:chop s:length n:-zero?
[ requested-file file:exists?
[ requested-file file:R file:open file:size n:strictly-positive? ] [ FALSE ] choose
[ requested-file ]
[ requested-index file:exists?
[ requested-index &Server-Info v:on ]
[ PATH '/empty.index.html s:append ] choose
] choose
]
[ PATH DEFAULT-INDEX s:append &Server-Info v:on ] choose
;
:gopher:read-file (f-)
file:R file:open !FID
@FID file:size !Size
@Size [ @FID file:read c:put ] times
@FID file:close
;
~~~
~~~
:link
dup fetch $h eq? push
dup fetch gopher:icon n:inc
[ ASCII:HT [ #0 ] case ] s:map
dup s:length over + n:inc
pop [ #4 + ] if
'<a_href="%s">%s</a> s:format s:put ;
:gopher:generate-index (f-)
'Content-type:_text/html s:put eol eol
'<!DOCTYPE_HTML_PUBLIC_"-//W3C//DTD_HTML_4.01//EN" s:put sp
'"http://www.w3.org/TR/html4/strict.dtd"> s:put eol
{ '<html><head>
'<meta_http-equiv="Content-Type"_content="text/html;_charset=utf-8">
'<link_href=data:,_rel=icon>
'<style_type="text/css">p_{_font-family:_monospace;_white-space:_pre;_}
'_*_{_color:_#bbb;_background:_#090909;_font-size:_large;_}
'a_{_color:_#FF6600;_}
'</style><title>forthworks.com</title></head>
} [ s:put eol ] a:for-each
'<body><p> s:put
file:R file:open !FID
@FID file:size !Size
[ buffer gopher:s:get
buffer tab? [ link eol ] [ gopher:i ] choose
@FID file:tell @Size lt? ] while
@FID file:close
'</p></body></html> s:put
;
~~~
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-)
requested-file get-mime-type 'Content-type:_ s:put s:put eol eol ;
~~~
The only thing left is the top level server.
~~~
:gopher:server
gopher:get-selector
'HTTP/1.0_200_OK s:put eol
gopher:file-for-request
@Server-Info
[ gopher:generate-index ]
[ gopher:send gopher:read-file ] choose
;
~~~
Close off the helper portion of the namespace.
~~~
}}
~~~
And run the `gopher:server`.
~~~
gopher:server
reset
~~~