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.
The article materials were borrowed from the following screencast.
We are looking forward to meeting you on our website soshace.com