A few days ago, I noticed a post about building a Gopher Server in Perl 6. I had already implemented a Gopher Client in Factor, and thought it might be fun to show a simple Gopher Server in Factor in around 50 lines of code.
Using the io.servers vocabulary, we will define a new multi-threaded server that has a directory to serve content from and hostname that it can be accessed at:
TUPLE: gopher-server < threaded-server { serving-hostname string } { serving-directory string } ;
When a file is requested, it can be streamed back to clients:
: send-file ( path -- ) binary [ [ write ] each-block ] with-file-reader ;
The Gopher protocol is defined in RFC 1436 and lists a few differentiated file types. We use the mime.types vocabulary to return the correct one.
: gopher-type ( entry -- type ) dup directory? [ drop "1" ] [ name>> mime-type { { [ dup "text/" head? ] [ drop "0" ] } { [ dup "image/gif" = ] [ drop "g" ] } { [ dup "image/" head? ] [ drop "I" ] } [ drop "9" ] } cond ] if ;
When a directory is requested, we can send a listing of all the sub-directories and files it contains, sending their relative path to the root directory being served so they can be requested properly by the client:
:: send-directory ( server path -- ) path [ [ [ gopher-type ] [ name>> ] bi dup path prepend-path server serving-directory>> ?head drop server serving-hostname>> server insecure>> "%s%s\t%s\t%s\t%d\r\n" sprintf utf8 encode write ] each ] with-directory-entries ;
To know which path was requested, we read the line, split on the first tab, carriage return, or newline character we see:
: read-gopher-path ( -- path ) readln [ "\t\r\n" member? ] split1-when drop ;
With all of that built, we can now implement a word to handle a client request:
M: gopher-server handle-client* dup serving-directory>> read-gopher-path append-path dup file-info directory? [ send-directory ] [ send-file drop ] if flush ;
Initializing a gopher-server
instance and providing a convenience word to start one:
: <gopher-server> ( directory port -- server ) utf8 gopher-server new-threaded-server "gopher.server" >>name swap >>insecure binary >>encoding "localhost" >>serving-hostname swap resolve-symlinks >>serving-directory ; : start-gopher-server ( directory port -- server ) <gopher-server> start-server ;
This is available in the gopher.server vocabulary with a few improvements such as:
- Support for
.gophermap
files for alternate results when content is requested. - Support for
.gopherhead
files to print headers above directory listings. - Navigation to parent directories using
..
links. - Display file modified timestamp and file sizes.
- Improved error handling.