В этой главе мы рассмотрим, как при помощи Node.JS создать веб сервер, который будет возвращать файл юзеру из директории public. Может возникнуть вопрос, зачем здесь Node.JS? Почему бы не сделать это на другом сервере, например Nginx. Вопрос совершенно уместен, да для отдачи файлов, как правило, другие сервера будут более эффективны. С Другой стороны node, во первых, работает тоже весьма неплохо, а во вторых, может, перед отдачей файла, совершить какие то интеллектуальные действия. Например обратится к базе данных, проверить имеет ли юзер право на доступ к файлам, и только если имеет, тогда уже отдавать.
Итак начинаем.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
var http = require('http'); var fs = require('fs'); var url = require('url'); var path = require('path'); var ROOT = __dirname + "/public"; http.createServer(function(req, res){ if(!checkAccess(req)){ res.statusCode = 403; res.end("Tell me the secret to access!"); return; } sendFileSafe(url.parse(req.url).pathname, res); }).listen(3000); function checkAccess(req){ return url.parse(req.url, true).query.secret == 'o_O'; } function sendFileSafe(filePath, res){ try{ filePath = decodeURIComponent(filePath); }catch(e){ res.statusCode = 400; res.end("Bad Request"); return; } if(~filePath.indexOf('\0')){ res.statusCode = 400; res.end("Bad Request"); return; } filePath = path.normalize(path.join(ROOT, filePath)); if(filePath.indexOf(ROOT) != 0){ res.statusCode = 404; res.end("File not found"); return; } fs.stat(filePath, function(err, stats){ if(err || !stats.isFile()){ res.statusCode = 404; res.end("File not found"); return; } sendFile(filePath, res); }); } function sendFile(filePath, res){ fs.readFile(filePath, function(err, content){ if (err) throw err; var mime = require('mime').lookup(filePath); res.setHeader('Content-Type', mime + "; charset=utf-8"); res.end(content); }); } |
Вот такой у нас получился код, с массой проверок, сейчас мы его подробно разберем.
http.createServer(… ) здесь очень прост
7 8 9 10 11 12 13 14 15 16 17 18 19 |
http.createServer(function(req, res){ if(!checkAccess(req)){ res.statusCode = 403; res.end("Tell me the secret to access!"); return; } sendFileSafe(url.parse(req.url).pathname, res); }).listen(3000); |
он будет проверять, есть ли доступ к данному файлу
9 10 11 12 13 14 |
if(!checkAccess(req)){ res.statusCode = 403; res.end("Tell me the secret to access!"); return; } |
и если есть, отдавать
15 16 17 |
sendFileSafe(url.parse(req.url).pathname, res); |
Для проверки доступа, мы будем использовать следующую, по сути заглушечную функцию,
19 20 21 22 23 |
function checkAccess(req){ return url.parse(req.url, true).query.secret == 'o_O'; } |
которая будет парсить url и если есть параметр ‘secret’, который равен ‘o_O’, то считается, что доступ есть. В реальной жизни такая проверка будет производиться, конечно же при помощи cookie, базы данных и так далее. Основная функция, которая нас здесь интересует, это sendFileSafe(…. ). Именно эта функция должна, получив, в качестве первого параметра, путь от юзера — «url.parse(req.url).pathname» отослать соответствующие файлы из директории «public», учитывая поддиректории. И важнейший аспект, который в ней должен быть заложен, это безопасность. Какой бы путь не передал юзер, он ни в коем случае не должен получить файл вне этой директории. Например, вот такое обращение
должно возвращать файл index.html и картинка здесь взята из директории deep\nodejs.jpg
А если бы я не указал seceret=o_O, то оно должно было выдать мне ошибку с кодом 403
Ну а если я попробовал указать вот так вот
тоже ошибка. И так для любых попыток выйти за пределы директории.
Итак смотрим функцию sendFileSafe(filePath, res), чтобы получить пример безопасной работы с путем от посетителя.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
function sendFileSafe(filePath, res){ try{ filePath = decodeURIComponent(filePath); }catch(e){ res.statusCode = 400; res.end("Bad Request"); return; } if(~filePath.indexOf('\0')){ res.statusCode = 400; res.end("Bad Request"); return; } filePath = path.normalize(path.join(ROOT, filePath)); if(filePath.indexOf(ROOT) != 0){ res.statusCode = 404; res.end("File not found"); return; } fs.stat(filePath, function(err, stats){ if(err || !stats.isFile()){ res.statusCode = 404; res.end("File not found"); return; } sendFile(filePath, res); }); } |
Эта функция состоит из нескольких шагов. На первом шаге я пропускаю путь через decodeURIComponent(filePath),
25 26 27 |
try{ filePath = decodeURIComponent(filePath); }catch(e){ |
ведь по стандарту http многие символы кодируются, в частности русская буква «я» будет иметь вот такой вид в url -«%D1%8F» и это корректно. Получив такой url мы обязаны его декодировать обратно в русскую букву «я» при помощи вызова decodeURIComponent(…. ), при этом если url закодирован неверно, то возникнет ошибка, которую необходимо поймать и обработать. В catch мы как раз указываем, resStatusCode = 400, что означает, что url некорректен, запрос неверен, можно конечно и просто вернуть res.statusCode = 404.
Далее когда мы раскодировали запрос, время его проверить
32 33 34 35 36 37 38 |
if(~filePath.indexOf('\0')){ res.statusCode = 400; res.end("Bad Request"); return; } |
есть такой специальный нулевой байт, который, по идеи, в строке url присутствовать не должен. Если он есть, это означает, что кто то его злонамеренно передал, потому что некоторые встроенные функции Node.JS будут работать с таким байтом некорректно. Соответственно, если такой байт есть, то мы тоже возвращаем- до свидание, запрос некорректен.
Теперь настало получить полный путь к файлу на диске. Для этого мы будем использовать модуль path.
38 39 40 |
filePath = path.normalize(path.join(ROOT, filePath)); |
Этот модуль содержит пачку самых разных функций для работы с путями. Например join объединяет пути, normalize — удаляет из пути, всякие странные вещи типа «.» «..» «\\» и так далее, то есть делает путь более корректным. Если url который передал юзер выглядел так — «/deep/nodejs.jpg», то после join с ROOT, который представляет собой вот эту — «var ROOT = __dirname + «\\public»» директорию, он будет выглядеть уже по другому — «C:\node\server\public\deep\nodejs.jpg»
Наша следующая задача это убедится, что путь действительно находится внутри директории public. Сейчас, когда у нас уже есть абсолютно точный, корректный абсолютный путь, это сделать очень просто — достаточно всего лишь проверить, что в начале находится вот такой вот префикс — «C:\node\server\public\» то есть, что путь начинается с ROOT. Проверяем и если это не так, то до свидание файла нет
40 41 42 43 44 45 46 |
if(filePath.indexOf(ROOT) != 0){ res.statusCode = 404; res.end("File not found"); return; } |
Далее, если путь разрешен, то проверим, что по нему лежит. Если ничего нет, то fs.stat вернет ошибку ну или если даже ошибки нет, то нужно проверить файл ли это
46 47 48 49 50 51 52 53 |
fs.stat(filePath, function(err, stats){ if(err || !stats.isFile()){ res.statusCode = 404; res.end("File not found"); return; } |
В том случае если это не файл — ошибка, ну а если файл, то все проверено, там файл, надо его отослать. Это делает вложенный вызов sendFile(…. ).
sendFile(…. ), функция которая есть в этом же файле чуть чуть ниже.
57 58 59 60 61 62 63 64 65 66 |
function sendFile(filePath, res){ fs.readFile(filePath, function(err, content){ if (err) throw err; var mime = require('mime').lookup(filePath); res.setHeader('Content-Type', mime + "; charset=utf-8"); res.end(content); }); } |
Она для чтения файла использует вызов fs.readFile(…. ) и когда он будет прочитан, то выводит его через res.end(…). Обращаю ваше внимание вот на что, во первых ошибка в этом callback очень мало вероятна, хотя бы потому что мы уже проверили, что файл есть, это действительно файл, то есть его можно отдать, но тем не менее мало ли что, например может возникнуть ошибка при чтении с диска, так или иначе, как то обработать ошибку надо — «if (err) throw err» .
Далее, мало просто считать содержимое файла и отправить его, ведь различные файлы должны снабжаться различными заголовками contant-type — «res.setHeader(‘Content-Type’, mime + «; charset=utf-8″)». Например html файл должен иметь тип text/html, файл с картинкой jpg — image/jpg и так далее. Нужный тип файла определяется по расширению с использованием модуля «mime», для того чтоб это работало, нужно его поставить дополнительно «npm install mime», и затем вызвать.
Ну и на конец последнее: эта глава была сосредоточена на том, чтобы корректно работать с путем от посетителя, чтобы сделать все необходимые проверки, но что касается отдачи файла, этот код не верен, я про функцию sendFile(… ), потому что readFile полностью прочитывает файл и потом в content его отсылает. А представьте, что будет если файл очень большой, а если он превышает количество свободной памяти, вообще же все упадет. По этому для того чтобы отсылать файл нужно либо дать команду специализированному серверу, либо использовать потоки которые мы рассмотрим в следующих главах.
Спасибо прочитал ваш учебник на одном дыхании
особенно понравилась тема :
21. Событийный цикл, библиотека libUV
в книжках описание данного вопроса как то не нашел
Извиняюсь, ошибся. Позже верно, ошибка в самом скрипте, должны быть две черты:
var ROOT = __dirname + «\\public»;
Также, на момент отправки этого комментария, у mime метод lookup ныне переименован в getType (об этом тут: https://www.npmjs.com/package/mime-lookup#difference-from-node-mime)
Соответственно, в конце должно быть
var mime = require(‘mime’).getType(filePath);
В противном случае скрипт не срабатывает
В
‘который представляет собой вот эту — «var ROOT = __dirname + «\\public»» директорию’
лишнее ‘\’ у public