Надеюсь, автор предыдущего археологического поста не выпустил на Хабр джинна Недели Gopher'а. Я тоже этого не хочу делать, но раз уж тема была поднята, то осмелюсь взять часть греха на свою душу.
Примером имплементации Gopher-сервера в 140 строк на JS.
Немного предыстории. Некоторое время назад мне действительно было совершенно нечем заняться и в рамках подготовки внутрикорпоративного семинара по Node.js я решил немножко поразмять мозг имплементацией какого-нибудь древнего, забытого всеми во имя добра, протокола, на такой ультрасовременной и трендовой штуке, как Нода. Изначальный мой выбор пал было на IRC, но прочитав все RFC и поглядев на парочку имплементаций на сях, что-то я закручинился. До семинара оставалась всего неделя, и написать за это время сколько-нибудь работающий IRC-сервер мне показалось не то чтобы нереальным, но явно проблематичным.
Единственным, пожалуй, в текущем историческом контексте достоинством Gopher'а является его поразительная простота. Смотрите, RFC1436 — просто коротюнечка по меркам IETF. Статья в Википедии — ещё короче. И этого вполне достаточно.
Итак, чтобы приготовить свой собственный тупенький Gopher-сервер, нам потребуются следующие ингредиенты.
Первым делом вешаем на сокет слушателя, который будет ждать до тех пор, пока клиент не пришлёт нам строку, заканчивающуюся на CRLF — тогда мы должны будем ответить на запрос, либо NULL — тогда мы должны будем закрыть соединение:
Если нам надо ответить на запрос, то мы смотрим, пустая ли была строка. Если пустая, отвечаем менюшкой (а Gopher — это текстовый menu-based протокол, поля которого отделяются символами табуляции), содержащей листинг текущего каталога. Если же нет, то в зависимости от типа затребованного ресурса либо отдаём его содержимое, либо производим полнотекстовый поиск. В полнотекстовом запросе нам обязательно встретится символ табуляции, его наличие и проверяем первым делом.
Сам листинг мы генерируем исходя из mime-типа файлов, подставляя соответствующие магические константы.
Ещё немножко обвязочного кода, и убеждаемся, что для такого высокоуровнего современного фремворка, как Node.js, имплементация какого-то устаревшего ещё в прошлом веке протокола — действительно, детская задача часа примерно на два с половиной. Оформляем всё это безобразие в виде слайдов (что больше времени заняло), идём на семинар, срываем овации и бурные аплодисменты.
И в этом профит.
Да, чуть не забыл. Мало написать сервер, ведь нужен ещё и клиент. Убеждаемся, что все современные браузеры избавились от поддержки Суслика примерностодесять лет назад (во имя добра), но остались в мире энтузиасты, написавшие плагин для Firefox'а. На OverbiteFF и отлаживаемся.
Собственно, весь код полностью в виде проекта на GitHub. Если кто-нибудь сподобится написать поддержку type 8, пришлите, пожалуйста, pull request.
Примером имплементации 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, имплементация какого-то устаревшего ещё в прошлом веке протокола — действительно, детская задача часа примерно на два с половиной. Оформляем всё это безобразие в виде слайдов (что больше времени заняло), идём на семинар, срываем овации и бурные аплодисменты.
И в этом профит.
Да, чуть не забыл. Мало написать сервер, ведь нужен ещё и клиент. Убеждаемся, что все современные браузеры избавились от поддержки Суслика примерно
Собственно, весь код полностью в виде проекта на GitHub. Если кто-нибудь сподобится написать поддержку type 8, пришлите, пожалуйста, pull request.
