When a programmer has nothing to do, he writes a Gopher server

I hope that the author of the previous archaeological post did not release the genie of Gopher Week on Habr. I don’t want to do this either, but since the topic has been raised, I dare to take some of the sin onto my soul.

An example of implementation of a Gopher server in 140 lines in JS.

A bit of background. Some time ago, I really had absolutely nothing to do, and as part of the preparation of the intradepartmental seminar on Node.js, I decided to stretch my brain a little by implementing some ancient protocol, forgotten by everyone in the name of good, on such an ultramodern and trendy thing as Noda. My initial choice fell on the IRC, but after reading all the RFCs and looking at a couple of implementations on the spot, I twisted something. There was only a week left before the seminar, and to write a somewhat working IRC server during this time seemed to me not only unrealistic, but clearly problematic.

Perhaps the only advantage of Gopher in the current historical context is its amazing simplicity. Look, RFC1436 is just a little short by the standards of the IETF.The Wikipedia article is even shorter. And this is quite enough.

So, to prepare your own dumb Gopher server, we need the following ingredients.

  • The net module , because we need to listen to the socket. The default port is 70th, but we will make it configurable via an environment variable.
  • The fs module , because we need to be able to read and list the contents of the folder. Similarly, we configure the root folder through the environment, or we will take the current one.
  • Yes, to read the environment, the os module is indispensable.
  • You will also need the mime module - Gopher extensions provide special answers for several predefined file types.
  • Finally, Gopher theoretically supports full-text search, and we emulate it too . It happened clumsily for me, but it seems to work.

First of all, we hang on a listener socket, which will wait until the client sends us a line ending in CRLF - then we will have to answer the request, or NULL - then we will have to close the connection:

 var server = net.createServer(function (sock) { 
     var query = ""; 
     console.log('Client connected from ' + sock.remoteAddress + ' port ' + sock.remotePort); 
     sock.on('end', function () { 
         console.log('Client disconnected'); 
     sock.on('data', function (buf) { 
         console.log('Received ' + buf.length + ' byte(s) of data'); 
         var r = false; 
         for (var i = 0; i < buf.length; i++) { 
             var b = buf.readUInt8(i); 
             switch (b) { 
                 case 0x0: 
                     r = false; 
                 case 0xD: 
                     r = true; 
                 case 0xA: 
                     if (r) { 
                         handleQuery(query, sock); 
                     r = false; 
                     query += String.fromCharCode(b); 

If we need to answer the request, then we look to see if the string was empty. If empty, we respond with a menu (and Gopher is a text menu-based protocol, the fields of which are separated by tabs) containing a listing of the current directory. If not, then depending on the type of resource requested, we either give its contents or do a full-text search. In a full-text query, we will definitely meet a tab character, its presence and check first of all.

function handleQuery(query, sock) { 
     var paramPos = query.indexOf(TAB); 
     if (paramPos > -1) { 
         var search = query.substr(paramPos + 1); 
         query = query.substr(0, paramPos); 
         var path = fs.realpathSync(query == '' ? ROOT_DIR + '/' : ROOT_DIR + query); 
         console.log('Handling search query ' + search + ' in the path ' + query); 
         answerInfo(sock, 'Search results for query ' + search + ' in current directory and all subdirectories:'); 
         printList(sock, path, query, indexer.searchFor(path, search)); 
     } else { 
         var path = fs.realpathSync(query == '' ? ROOT_DIR + '/' : ROOT_DIR + query); 
         console.log('Handling path query ' + path); 
         fs.exists(path, function (exists) { 
             if (!exists) { 
                 answerError(sock, 'File ' + path + " doesn't exists"); 
         fs.stat(path, function (err, stats) { 
             if (stats.isDirectory()) { 
                 answerDirList(sock, query, path); 
             } else { 
                 fs.readFile(path, function (err, data) { 

The listing itself we generate based on the mime-type of files, substituting the corresponding magic constants.

 function printList(sock, path, query, entries) { 
     var answer = ""; 
     if (entries.length == 0) { 
         answerInfo(sock, 'Nothing to display here'); 
     } else { 
         for (var i = 0; i < entries.length; i++) { 
             var entry = entries[i]; 
             var stat = fs.statSync(path + '/' + entry); 
             if (stat.isDirectory()) { 
                 answer += "1"; 
             } else { 
                 var mt = mime.lookup(entry); 
                 if ((mt.indexOf('text/html') == 0) || (mt.indexOf('application/xhtml+xml') == 0)) { 
                     answer += 'h'; 
                 } else if (mt.indexOf('uue') > -1) { 
                     answer += '6'; 
                 } else if (mt.indexOf('text/') == 0) { 
                     answer += '0'; 
                 } else if (mt.indexOf('image/gif') == 0) { 
                     answer += 'g'; 
                 } else if (mt.indexOf('image/') == 0) { 
                     answer += 'I'; 
                 } else if (mt.indexOf('audio/') == 0) { 
                     answer += 's'; 
                 } else if (mt.indexOf('binhex') > -1) { 
                     answer += '4'; 
                 } else if ((mt.indexOf('compressed') > -1) || (mt.indexOf('archive') > -1)) { 
                     answer += '5'; 
                 } else { 
                     answer += '9'; 
             answer += entry + TAB + query + '/' + entry + TAB + SERVER + TAB + PORT + "\r\n"; 
     answer += '7Search in this directory and all subdirectories...' + TAB + query + TAB + SERVER + TAB + PORT + "\r\n"; 
     answer += EOF; 

Just a little bit of the binding code, and we are convinced that for such a high-level modern framework as Node.js, the implementation of some protocol that was outdated in the last century is really a children’s task for about two and a half hours. We make out all this disgrace in the form of slides (which took more time), go to a seminar, break ovations and loud applause.

And this is the profit.

Yes, I almost forgot. It's not enough to write a server, because you also need a client. We are convinced that all modern browsers got rid of Suslik’s support about a hundred and ten years ago (in the name of goodness), but the enthusiasts who wrote the plug-in for Firefox remained in the world. On OverbiteFF and debugging.

Actually, all the code is completely in the form of a project on GitHub. If anyone is able to write type 8 support, please send a pull request.

Also popular now: