19Oct

fantastic-wooden-pier-frisco-night
В этой статье мы рассмотрим, как при помощи Node.js создать веб-сервер, который будет возвращать файл пользователю из директории public. Может возникнуть вопрос: зачем здесь Node.js? почему бы не сделать это на другом сервере? Вопрос совершенно уместен. Да, для отдачи файлов, как правило, другие сервера будут более эффективны. С другой стороны, Node.js, во-первых, тоже работает весьма неплохо, а во-вторых, перед отдачей файла может совершить какие-то интеллектуальные действия, например, обратиться к базе данных, проверить, имеет ли пользователь право на доступ к данному файлу, и только если имеет, тогда уже отдавать.

Пожалуйста загрузите код из нашего репозитория, писать сегодня не придется. Прежде чем начать, разбираться необходимо установить модуль mime:

npm install mime

Так выглядит наш server.js:

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(path.normalize(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); // npm install mime
        res.setHeader('Content-Type', mime + "; charset=utf-8");
        res.end(content);
    });

}

http.create здесь очень прост.

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);

Он будет проверять, есть ли доступ к данному файлу, и если есть, то уже отдавать. Для проверки доступа мы будем использовать следующую функцию (которая является по своей сути является Fake function), которая будет выполнять url parse , и если есть параметр secret, который будет равен ‘o_O’, то считается, что доступ есть:

function checkAccess(req) {  
  return url.parse(req.url, true).query.secret == 'o_O';  
} 

В реальной жизни такая проверка, конечно же, будет производиться при помощи cookies, базы данных и т.д.
Основная функция, которая нас интересует, это sendFileSafe:

sendFileSafe(url.parse(req.url).pathname, res);

Именно эта функция должна, получив путь от user, отослать соответствующий файл с директории public, учитывая путь директории. Важнейший аспект, который в ней должен быть заложен, это безопасность. Какой бы путь не передал пользователь, он ни в коем случае не должен получить файл вне директории public. Например, обращение:

http://localhost:3000/index.html?secret=o_O

должно возвращать файл index.html,  картинка здесь взята из директории

<img src ="deep/nodejs.jpg?secret=o_O">

А если бы мы не указали secret:

http://localhost:3000/index.html

то оно должно было бы выдать ошибку с кодом 403.

А если бы я попробовали бы указать вот так:

http://localhost:3000/server.js

то тоже была бы ошибка. И так для любых попыток выйти за пределы этой директории.

Итак, смотрим функцию sendFileSafe, чтобы получить пример безопасной работы с путем от посетителя. Эта функция состоит из нескольких шагов.

function sendFileSafe(filePath, res) {

    try {
        filePath = decodeURIComponent(filePath);
    } catch(e) {
        res.statusCode = 400;
        res.end("Bad Request");
        return;
    }

На первом шаге мы пропускаем путь через decodeURIComponent, ведь по стандарту “http” многие символы кодируются. Получив такой “url”, мы обязаны его декодировать обратно, при помощи вот такого вызова:

decodeURIComponent

При этом, если “url” закодирован заведомо неверно, то возникнет ошибка, которую необходимо поймать и обработать. Код 400 как раз означает, что “url” некорректен и запрос неверен. Можно, конечно, вернуть и 404.
Когда мы раскодировали запрос, время его проверить. Есть специальный нулевой байт, который в строке “url” присутствовать не должен:

if (~filePath.indexOf('\0')) {  
  res.statusCode = 400;  
  res.end("Bad Request");  
  return;  
}  

Если он есть, то это означает, что кто-то его злонамеренно передал, потому что некоторые встроенные функции Node.js будут работать таким байтом некорректно. Соответственно, если такой байт есть, то мы тоже возвращаем «Bad Request», запрос некорректен.

Теперь настало время получить полный путь к файлу на диске. Для этого мы будем использовать модуль path.

filePath = path.normalize(path.join(ROOT, filePath));

Этот встроенный модуль содержит набор самых разных функций для работы с путями. Например, join объединяет пути, normalize – удаляет из пути всякие странные вещи типа “.”, “:”, “//” и  т.д., то есть, делает путь более корректным. Если “url”, который передал пользователь, выглядел так:

//  /deep/nodejs.jpg

то после join с ROOT, который представляет собой вот эту директорию:

var ROOT = __dirname + "/public";

он будет выглядеть уже по-другому, например:

//  /deep/nodejs.jpg ->  /Users/learn/node/path/public/deep/nodejs.jpg  

Наша следующая задача – это убедиться, что этот путь действительно находится внутри директории public. Сейчас, когда у нас уже есть абсолютно точный, корректный путь, это сделать очень просто. Достаточно всего лишь проверить, что вначале находится вот такой префикс:

Users/learn/node/path/public

то есть, что путь начинается с ROOT. Проверяем. Если это не так, то “File not Found”:

filePath = path.normalize(path.join(ROOT, filePath));

    if (filePath.indexOf(path.normalize(ROOT)) != 0) {
        res.statusCode = 404;
        res.end("File not found");
        return;
    }

Если путь разрешен, то проверим, что по нему находиться. Если ничего нет, то fs.stat вернет ошибку err:

fs.stat(filePath, function(err, stats) {  
  if (err || !stats.isFile()) {  
    res.statusCode = 404;  
    res.end("File not found");  
    return;  
  }  

Если даже ошибки нет, то нужно проверить, файл ли это. В том случае, если это не файл, то это ошибка. Ну а если файл, то все проверено, и надо его отослать. Это делает вложенный вызов sendFile:

sendFile(filePath, res);  
  });  
}  

sendFile – функция которая есть в этом же файле, но немного ниже:

function sendFile(filePath, res) {

    fs.readFile(filePath, function(err, content) {
        if (err) throw err;

        var mime = require('mime').lookup(filePath); // npm install mime
        res.setHeader('Content-Type', mime + "; charset=utf-8");
        res.end(content);
    });

}

Она для чтения файла использует вызов fs.readFile, и когда он будет прочитан, то выводит его через res.end:

res.end(content);

Обращаем ваше внимание вот на что:  ошибка в этом callback очень маловероятна

if (err) throw err;

хотя бы потому, что мы уже проверили, файл есть, это действительно файл и его можно отдать. Но, тем не менее, мало ли что. Например, может возникнуть ошибка при чтении с диска. Так или иначе как-то обработать ошибку надо. Далее, мало просто считать содержимое файла и отправить его, ведь различные файлы должны снабжаться различными заголовками “Content-Tyрe”.

Например, HTML  файл должен иметь тип text/html, файл с картинкой JPEG – image/jpeg и т.д. Нужный тип файла определяется по расширению с использованием модуля ‘mime’.

 var mime = require('mime').lookup(filePath); // npm install

Для того чтобы все работало, не забудьте его установить. И, наконец, последнее, эта статья была сосредоточена, в первую очередь, на том, чтобы корректно работать с path от посетителя, чтобы сделать все необходимые проверки, декодирование и т.д. Все это очень важно, но что касается отдачи файла, то этот код неверен:

function sendFile(filePath, res) {

    fs.readFile(filePath, function(err, content) {
        if (err) throw err;

        var mime = require('mime').lookup(filePath); // npm install mime
        res.setHeader('Content-Type', mime + "; charset=utf-8");
        res.end(content);
    });

}

потому что readFile полностью просчитывает файл, потом отсылает его в content. А представьте, что будет, если файл очень большой. А если он превышает количество свободной памяти? Тогда вообще все упадет! Поэтому, для того чтобы отсылать файл, нужно либо дать команду специализированному серверу, либо использовать потоки, которые мы рассмотрим в следующей статье.

tumblr_nvdx0jpq7F1rf2fjgo1_1280
Материалы для статьи взяты из следующего скринкаста.

We are looking forward to meeting you on our website soshace.com

11. Уроки Node.js. Эхо-сервер.

Всем привет! На этом занятии мы создадим эхо-сервер, то есть, такой сервер, который при запросе на url /echo c параметром message выдает значение этого параметра:

// http://127.0.0.1:3000/echo?message=Hello -> Hello
На все другие запросы отвечает: Страница не найдена.

Начнем вот с такого шаблона:

Leave a Reply