21Oct

fantastic-wooden-pier-frisco-night

This article will deal with how to create a web server in Node.js, which will return a file to a user from a public directory. You may wonder: why do we need Node.js here? Why can’t we use another server? You question surely does make sense. Yes, for returning files other servers are generally more effective. From the other side, Node.js works pretty well, too. Second, before returning a file it can also perform some intellectual activities: for example, refer to a database, check out whether a user is permitted to access the file and give the file to him, if it’s permitted.

Please, submit coding from our repository, we won’t write anything today. But before you start, you will need to install the mime module:

npm install mime

That’s how our server.js looks like:

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 is very simple here.

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

It will check whether you are permitted to access the file and give it to you, if the answer is positive. To check the access right, we will use the following function (which is Fake function as it is). This function will perform url parse and if there is a parameter named secret equal to ‘o_O’, it means you’ve got an access:

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

In practice, a check of this kind will be done using cookies, databases, etc.
The key function we are interested in is sendFileSafe:

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

This is the very function that upon receiving the path from a user has to send a respective file from the public directory considering the directory path. The key aspect that should be contained here is safety. Whatever path is delivered by a user, he has to receive a file within the public directory. For example, this request:

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

should return an index.html file. The picture is taken here from a directory

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

And if we hadn’t indicated secret:

http://localhost:3000/index.html

it will have shown an error with the code 403.

And if I had indicated it the following way:

http://localhost:3000/server.js

it will have shown an error, too. The principle is the same for any attempt to get away from this directory.

So, we look for the function sendFileSafe, in order to get an example of a safe work with the path from a visitor. This function contains a number of steps.

function sendFileSafe(filePath, res) {

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

The first step is when we handle the path with decodeURIComponent, since the “HTTP” standard means coding of many symbols. Once this kind of url is received, we have to decode it in return using this call:

decodeURIComponent

Thus, if url has been coded incorrectly, an error will occur, and we will need to catch and handle it. Code 400 means that your url is incorrect and the request is invalid. Well, you can get back the code 404, too.
Once the request is decoded, it is high time to check it. There is a specialized ‘zero byte’ that must not be contained in the url string:

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

If it is there, it means someone has maliciously brought it here because some built-in Node.js functions will work incorrectly with this byte. Respectively, if you’ve got it, you need to get back Bad Request, and your request is incorrect.

Now it’s time to get a full path to the file on a disk. To do so, we will use our path module.

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

This built-in module contains a set of various functions to work with paths. For example join unites paths, normalize eliminates different strange things from a path: such as “.”, “:”, “//”, etc., which means it gives a path a correct form. If an url delivered by a user looked like this:

//  /deep/nodejs.jpg

after join with ROOT that represents this directory:

var ROOT = __dirname + "/public";

it would look differently, for example:

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

Our next task is to make sure this path is within the public directory. Right now, when we already have a correct and precise path, it is quite simple. You only need to make sure this prefix is contained in the beginning:

Users/learn/node/path/public

Which means the path starts with ROOT. Let’s check it out. If it’s not, you will see 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;
    }

If your access is permitted, let us check what the path includes. If there is nothing, fs.stat will bring back err:

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

Even if there is no error, you still need to make sure whether it is a file. If it isn’t a file, it is an error. If it is a file, it means everything is checked and you need to send the file. This can be done using the built-in call sendFile:

sendFile(filePath, res);  
  });  
}  

sendFile is a function contained in the file, but a little bit below:

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

}

For reading a file it uses the call fs.readFile, and upon being read it will be outputted through res.end:

res.end(content);

Pay your attention to the following: any mistake in this callback is barely possible

if (err) throw err;

at least because we’ve already checked the file does exist, it is a file and it can be delivered. But you never know what may happen. For example, an error can occur while the file is being read on a disk. Anyway, we have to handle this possible error. Just to read the file and send it won’t be enough, since various files should contain various Content-Type titles.

For example, an HTML file needs to be of a text/html type, a file with a JPEG picture – of a image/jpeg type, etc. The needed file type gets defined according to its extension using the mime module.

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

In order to make it work, do not forget to install it. And, eventually, this article deals on how to work with a path from a visitor correctly, in order to do all needed checks, decoding, etc. All these things are extremely important, but if it comes to the file return, this code is incorrect:

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

}

because readFile computes the whole file and then sends it to content. Just imagine what will happen, if a file is very big. And what if it’s even bigger than the memory left? It will crash! So, in order to send a file you need to give a command to a specialized server or use the streams that we will talk about in our next article.

tumblr_nvdx0jpq7F1rf2fjgo1_1280
The article materials were borrowed from the following screencast.

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

Leave a Reply