Pull to refresh

Когда программисту нечем заняться, он пишет Gopher-сервер

Reading time 4 min
Views 13K
Надеюсь, автор предыдущего археологического поста не выпустил на Хабр джинна Недели Gopher'а. Я тоже этого не хочу делать, но раз уж тема была поднята, то осмелюсь взять часть греха на свою душу.

Примером имплементации Gopher-сервера в 140 строк на JS.

Немного предыстории. Некоторое время назад мне действительно было совершенно нечем заняться и в рамках подготовки внутрикорпоративного семинара по Node.js я решил немножко поразмять мозг имплементацией какого-нибудь древнего, забытого всеми во имя добра, протокола, на такой ультрасовременной и трендовой штуке, как Нода. Изначальный мой выбор пал было на IRC, но прочитав все RFC и поглядев на парочку имплементаций на сях, что-то я закручинился. До семинара оставалась всего неделя, и написать за это время сколько-нибудь работающий IRC-сервер мне показалось не то чтобы нереальным, но явно проблематичным.

Единственным, пожалуй, в текущем историческом контексте достоинством Gopher'а является его поразительная простота. Смотрите, RFC1436 — просто коротюнечка по меркам IETF. Статья в Википедии — ещё короче. И этого вполне достаточно.

Итак, чтобы приготовить свой собственный тупенький Gopher-сервер, нам потребуются следующие ингредиенты.

  • Модуль net, потому что нам надо слушать сокет. Порт по умолчанию 70й, но мы сделаем его конфигурируемым через переменную окружения.
  • Модуль fs, потому что нам надо уметь читать и перечислять содержимое папки. Аналогично, корневую папку сконфигурируем через окружение, либо будем брать текущую.
  • Да, чтобы читать окружение, без модуля os не обойтись.
  • Также понадобится модуль mime — в расширениях Gopher'а предусмотрены специальные ответы для нескольких предопределённых типов файлов.
  • Наконец, Gopher теоретически поддерживает полнотектовый поиск, и мы его тоже сэмулируем. У меня топорно вышло, но вроде работает.


Первым делом вешаем на сокет слушателя, который будет ждать до тех пор, пока клиент не пришлёт нам строку, заканчивающуюся на CRLF — тогда мы должны будем ответить на запрос, либо NULL — тогда мы должны будем закрыть соединение:

 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; 
                     return; 
                 case 0xD: 
                     r = true; 
                     break; 
                 case 0xA: 
                     if (r) { 
                         handleQuery(query, sock); 
                     } 
                     break; 
                 default: 
                     r = false; 
                     query += String.fromCharCode(b); 
             } 
         } 
     }); 
 }); 


Если нам надо ответить на запрос, то мы смотрим, пустая ли была строка. Если пустая, отвечаем менюшкой (а Gopher — это текстовый menu-based протокол, поля которого отделяются символами табуляции), содержащей листинг текущего каталога. Если же нет, то в зависимости от типа затребованного ресурса либо отдаём его содержимое, либо производим полнотекстовый поиск. В полнотекстовом запросе нам обязательно встретится символ табуляции, его наличие и проверяем первым делом.

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"); 
                 return; 
             } 
         }); 
 
         fs.stat(path, function (err, stats) { 
             if (stats.isDirectory()) { 
                 answerDirList(sock, query, path); 
             } else { 
                 fs.readFile(path, function (err, data) { 
                     sock.end(data); 
                 }); 
             } 
         }); 
     } 
 } 


Сам листинг мы генерируем исходя из mime-типа файлов, подставляя соответствующие магические константы.

 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; 
 
     sock.end(answer); 
 } 


Ещё немножко обвязочного кода, и убеждаемся, что для такого высокоуровнего современного фремворка, как Node.js, имплементация какого-то устаревшего ещё в прошлом веке протокола — действительно, детская задача часа примерно на два с половиной. Оформляем всё это безобразие в виде слайдов (что больше времени заняло), идём на семинар, срываем овации и бурные аплодисменты.

И в этом профит.

Да, чуть не забыл. Мало написать сервер, ведь нужен ещё и клиент. Убеждаемся, что все современные браузеры избавились от поддержки Суслика примерно стодесять лет назад (во имя добра), но остались в мире энтузиасты, написавшие плагин для Firefox'а. На OverbiteFF и отлаживаемся.

Собственно, весь код полностью в виде проекта на GitHub. Если кто-нибудь сподобится написать поддержку type 8, пришлите, пожалуйста, pull request.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+22
Comments 11
Comments Comments 11

Articles