[Из песочницы] Когда программисту нечем заняться, он пишет Gopher-сервер
Надеюсь, автор предыдущего археологического поста не выпустил на Хабр джинна Недели 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.