Надеюсь, автор предыдущего археологического поста не выпустил на Хабр джинна Недели Gopher'а. Я тоже этого не хочу делать, но раз уж тема была поднята, то осмелюсь взять часть греха на свою душу.
Примером имплементации Gopher-сервера в 140 строк на JS.
Немного предыстории. Некоторое время назад мне действительно было совершенно нечем заняться и в рамках подготовки внутрикорпоративного семинара по Node.js я решил немножко поразмять
Единственным, пожалуй, в текущем историческом контексте достоинством 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 + "rn";
}
}
answer += '7Search in this directory and all subdirectories...' + TAB + query + TAB + SERVER + TAB + PORT + "rn";
answer += EOF;
sock.end(answer);
}
Ещё немножко обвязочного кода, и убеждаемся, что для такого высокоуровнего современного фремворка, как Node.js, имплементация какого-то устаревшего ещё в прошлом веке протокола — действительно, детская задача часа примерно на два с половиной. Оформляем всё это безобразие в виде слайдов (что больше времени заняло), идём на семинар, срываем овации и бурные аплодисменты.
И в этом профит.
Да, чуть не забыл. Мало написать сервер, ведь нужен ещё и клиент. Убеждаемся, что все современные браузеры избавились от поддержки Суслика примерно стодесять лет назад (во имя добра), но остались в мире энтузиасты, написавшие плагин для Firefox'а. На OverbiteFF и отлаживаемся.
Собственно, весь код полностью в виде проекта на GitHub. Если кто-нибудь сподобится написать поддержку type 8, пришлите, пожалуйста, pull request.
Автор: PastorGL