Creating a Gopher Server with Perl

What IS Gopher?

Long before the Go programming language adopt the gopher as its logo, and before the “World Wide Web” came to dominate the the internet with HTTP, there was Gopher. Gopher was a menu driven text only protocol for delivering content over a network. Colleges, Universities, and Companies all used Gopher for hosting and sharing information and files on both private and outwardly facing networks. For a brief moment in history, Gopher was the equal of todays “web pages”. 

With the main stream adoption of GUI based operating systems, text only browsing rapidly fell out of favor, and the more visually appealing HTTP took over. But while it has been heavily marginalized, Gopher still exists. Seeing as it is an incredibly simple protocol, writing a server daemon for it is a fun exercise in network programming.

The Gopher Protocol

Like HTTP, Gopher servers listen for incoming connections on a designated port, and respond to properly formatted requests with properly formatted responses. The main feature of the Gopher protocol is whats called “selector strings” which are synonymous with links on modern websites. These selector strings are comprised of tab delimited fields that formated in the following way:

0file <tab> filename <tab> server address <tab> port

or

1directory <tab> directoryname <tab> server address <tab> port

selector strings must all end with a carriage return followed by a line break:

\r\n

When first connected a client will send a blank selector string, to which the server responds by sending the default GopherMap which can be anything from a textfile, to a directory listing formatted as selector strings. Our implementation will send a combination of both.

Setting up the server

I chose to implement my Gopher server in Perl5 using the IO::Socket library, which is an OOP wrapper for perls Unix style socket library. It makes network programming with perl even simpler than the Socket.pm.

First things first, we need to include the IO::Socket library and define some variables:

#!/usr/bin/perl
use warnings;
use IO::Socket;
my $name = "PerlGopher v0.1"; #servername
my $port = 7070; #Gopher default port 70
my $dir = "./*"; #default Gopher directory
my $lineend = "\r\n";

In order to get our server up an running we need to open a port and listen for incoming TCP connections:

my $server = IO::Socket::INET->new(LocalPort => $port,
				   Type => SOCK_STREAM,
				   Reuse => 1,
				   Listen => 10 )
or die "Can't open server on port $port : $1 \n";

We use a loop to accept connections and pass them on to a request handler:

while ($client = $server->accept()) {
  $remote_addr = $client->peerhost();
  print "New Connection from: $remote_addr\n";
  $request = <$client>;  
  &requestHandler($client, $request, $dir);
}

Now that we are listening on a port and accepting connections to our server, we have to interact with the client to process requests and serve content. Our request handler is where the magic happens. 

Initially, a connecting client will send a blank request, to which our server is expected to respond with a Gophermap as outlined above. Aside from this blank request, requests simply take the form of a file name.

For us to generate a gopher map the first think we need to do is gather a listing of the files for the directory which we are treating as our server root, which we set above in $dir.

sub requestHandler { 
   my ($client, $request, $dir) = @_;
   my $shouldserv = "nil"; 
   print "Revieved request: " . $request;
   $localhost = $client->sockhost(); #server address
   @dirlist = glob ( $dir ); #read file directory

After reading in the directory listening, we will compare the file names to our request to see if the client is requesting one of the files we are serving:

   foreach $file (@dirlist) {
	$file =~ s/^.\///; #remove preceding directory
        chomp($file);
        if ($request =~ /$file/)  #check if request is for available file
        {
          $shouldserv = $file; #designate file to be served up
        }
   }

If the request matches one of the files we are serving, we send that file, line by line as plain text to the client:

   if ($shouldserv ne "nil") #if a file has been requested
   {
	#send over the file as plain text
        print "Sending File: $shouldserv\n";
	open(FILE, $shouldserv);
        @toSend = <FILE>;
        close(FILE);
        foreach (@toSend) {
	   print $client $_, "\n";
        }

Other wise, we generate a Gopher map of the server root and send the client that as well:

   } else { #if not, 
     #send over the current directory as a gophermap
     foreach $file (@dirlist) {
        if ( -d $file) {
          #format proper selector link for a directory
          $reply = "1" . $file . "\t" . $file . "\t". $localhost . "\t" . $port;
        }
        if ( -f $file) {
          #format proper selector link for a file
          $reply = "0" . $file . "\t". $file . "\t" . $localhost . "\t".$port;
        }
        print $reply . $lineend;
        print $client $reply . $lineend;
     }
   }
   #close connection
   $client->close();
}

once the request has been processed and replied to, the server closes the connection.

Thats it! Like i said, it is a VERY simple protocol. One of the great things about it though is that it is FAST. everything is sent as plain text, so interacting with a Gopher server seems practically instantaneous.

If you want to see Gopher live in action, you can use a browser to connect with

gopher://maxgcoding.com:7000/

Which is being served by the perl script we just put together above!

The source code for this example is available both at the above linked gopher server, and as always, on my github:

https://github.com/maxgoren/PerlGopherD/blob/main/gopherD.pl

Leave a Reply

Your email address will not be published. Required fields are marked *