How to create static HTTP server in Node.JS?

How to create static HTTP server in Node.JS?

Published on: July 10, 2023

If you want to serve static files on your Node.JS web application, you can create a simple static HTTP server in Node.JS. Node.JS is a event-driven server-side JavaScript runtime, that uses V8 JS engine (same as Chromium) and executes code outside a web browser. In this article we will cover basics of implementation of HTTP server in Node.JS and build a simple static HTTP server.

Prerequisities

  • Node.JS installed on your development machine

Built-in HTTP module

Node.JS has built-in http module, which allows Node.JS to communicate over HyperText Transfer Protocol (HTTP). To include that module, we use require() method:

var http = require("http");

"Hello, World" server

In this example, the server sends "Hello, World" response. This server is listening at port 8080: {% codeblock server.js lang:javascript %} //Hello World server var http = require("http"); var port = 8080; var server = http.createServer(function (req, res) { res.writeHead(200, "OK", { "Content-Type": "text/plain" }); res.write("Hello, World!"); res.end(); }); server.listen(port, function() { console.log("Started server at port " + port + "."); }); {% endcodeblock %} For creating HTTP server object we use http.createServer() method. Server will then listen at port 8080. This server writes header indicating, that we're sending plain text. Then it sends "Hello, World" message and ends the connection. Save the code above in a file called server.js and start the server using node server.js.

If you visit localhost:8080 in your web browser, the result will look like this:

Hello World server

Hello World server

Serving files using Node.JS

For reading files, we're using fs module and fs.readFile() method. The code will look like this: {% codeblock server.js lang:javascript %} //Serving index... var http = require("http"); var fs = require("fs"); var port = 8080; var server = http.createServer(function (req, res) { fs.readFile("index.html", function(err, data) { if(err) { res.writeHead(500, "Internal Server Error", { "Content-Type": "text/plain" }); res.end("500 Internal Server Error! Reason: " + err.message); } else { res.writeHead(200, "OK", { "Content-Type": "text/html" }); res.end(data); } }); }); server.listen(port, function() { console.log("Started server at port " + port + "."); }); {% endcodeblock %} This code will serve index.html file, and returns 500 error if there was a problem reading that file. But what if that index file doesn't exist? We will then need to serve 404 error page: {% codeblock server.js lang:javascript %} //Serving 404... var http = require("http"); var fs = require("fs"); var port = 8080; var server = http.createServer(function (req, res) { fs.readFile("index.html", function(err, data) { if(err) { if(err.code == "ENOENT") { //ENOENT means "File doesn't exist" res.writeHead(404, "Not Found", { "Content-Type": "text/plain" }); res.end("404 Not Found"); } else { res.writeHead(500, "Internal Server Error", { "Content-Type": "text/plain" }); res.end("500 Internal Server Error! Reason: " + err.message); } } else { res.writeHead(200, "OK", { "Content-Type": "text/html" }); res.end(data); } }); }); server.listen(port, function() { console.log("Started server at port " + port + "."); }); {% endcodeblock %} Note, that in static HTTP servers, files are determined from resource URL. {% codeblock server.js lang:javascript %} //WARNING!!! PATH TRAVERSAL var http = require("http"); var fs = require("fs"); var port = 8080; var server = http.createServer(function (req, res) { var filename = "." + req.url; if(filename == "./") filename = "./index.html"; fs.readFile(filename, function(err, data) { if(err) { if(err.code == "ENOENT") { //ENOENT means "File doesn't exist" res.writeHead(404, "Not Found", { "Content-Type": "text/plain" }); res.end("404 Not Found"); } else { res.writeHead(500, "Internal Server Error", { "Content-Type": "text/plain" }); res.end("500 Internal Server Error! Reason: " + err.message); } } else { res.writeHead(200, "OK", { "Content-Type": "text/html" }); res.end(data); } }); }); server.listen(port, function() { console.log("Started server at port " + port + "."); }); {% endcodeblock %} But we have introduced path traversal vulnerability (being able to access file outside the web root)! To mitigate that, we'll use a regular expression, that removes all dot-dot-slash sequences from file name: {% codeblock server.js lang:javascript %} //Path traversal mitigated var http = require("http"); var fs = require("fs"); var port = 8080; var server = http.createServer(function (req, res) { var filename = "." + req.url; filename = filename.replace(/\/g,"/").replace(//..?(?=/|$)/g,"/").replace(//+/g,"/"); //Poor mans URL sanitizer if(filename == "./") filename = "./index.html"; fs.readFile(filename, function(err, data) { if(err) { if(err.code == "ENOENT") { //ENOENT means "File doesn't exist" res.writeHead(404, "Not Found", { "Content-Type": "text/plain" }); res.end("404 Not Found"); } else { res.writeHead(500, "Internal Server Error", { "Content-Type": "text/plain" }); res.end("500 Internal Server Error! Reason: " + err.message); } } else { res.writeHead(200, "OK", { "Content-Type": "text/html" }); res.end(data); } }); }); server.listen(port, function() { console.log("Started server at port " + port + "."); }); {% endcodeblock %} That might work fine for HTML files, but if you try other files, there will be content type mismatch. To get MIME types, we use mime-types package, that you can install using npm install mime-types. We will then use that module, along with path module to get file extension. The code will look like this: {% codeblock server.js lang:javascript %} //Adding MIME type support... var http = require("http"); var fs = require("fs"); var mime = require("mime-types"); var path = require("path"); var port = 8080; var server = http.createServer(function (req, res) { var filename = "." + req.url; filename = filename.replace(/\/g,"/").replace(//..?(?=/|$)/g,"/").replace(//+/g,"/"); //Poor mans URL sanitizer if(filename == "./") filename = "./index.html"; var ext = path.extname(filename).substr(1); //path.extname gives "." character, so we're using substr(1) method. fs.readFile(filename, function(err, data) { if(err) { if(err.code == "ENOENT") { //ENOENT means "File doesn't exist" res.writeHead(404, "Not Found", { "Content-Type": "text/plain" }); res.end("404 Not Found"); } else { res.writeHead(500, "Internal Server Error", { "Content-Type": "text/plain" }); res.end("500 Internal Server Error! Reason: " + err.message); } } else { res.writeHead(200, "OK", { "Content-Type": mime.lookup(ext) || undefined }); res.end(data); } }); }); server.listen(port, function() { console.log("Started server at port " + port + "."); }); {% endcodeblock %} But with query strings, it will fail. To prevent that, we'll be using WHATWG URL parser (url.parse is now deprecated): {% codeblock server.js lang:javascript %} //And URL query... var http = require("http"); var fs = require("fs"); var mime = require("mime-types"); var path = require("path"); var port = 8080; var server = http.createServer(function (req, res) { var urlObject = new URL(req.url, "http://localhost"); var filename = "." + urlObject.pathname; filename = filename.replace(/\/g,"/").replace(//..?(?=/|$)/g,"/").replace(//+/g,"/"); //Poor mans URL sanitizer if(filename == "./") filename = "./index.html"; var ext = path.extname(filename).substr(1); //path.extname gives "." character, so we're using substr(1) method. fs.readFile(filename, function(err, data) { if(err) { if(err.code == "ENOENT") { //ENOENT means "File doesn't exist" res.writeHead(404, "Not Found", { "Content-Type": "text/plain" }); res.end("404 Not Found"); } else { res.writeHead(500, "Internal Server Error", { "Content-Type": "text/plain" }); res.end("500 Internal Server Error! Reason: " + err.message); } } else { res.writeHead(200, "OK", { "Content-Type": mime.lookup(ext) || undefined }); res.end(data); } }); }); server.listen(port, function() { console.log("Started server at port " + port + "."); }); {% endcodeblock %} It's nearly finished! But encoded URLs will not work. To fix that, we will use decodeURIComponent() method: {% codeblock server.js lang:javascript %} //And URL decoding... var http = require("http"); var fs = require("fs"); var mime = require("mime-types"); var path = require("path"); var port = 8080; var server = http.createServer(function (req, res) { var urlObject = new URL(req.url, "http://localhost"); var filename = ""; try { filename = "." + decodeURIComponent(urlObject.pathname); } catch(ex) { //Malformed URI means bad request. res.writeHead(400, "Bad Request", { "Content-Type": "text/plain" }); res.end("400 Bad Request"); return; } filename = filename.replace(/\/g,"/").replace(//..?(?=/|$)/g,"/").replace(//+/g,"/"); //Poor mans URL sanitizer if(filename == "./") filename = "./index.html"; var ext = path.extname(filename).substr(1); //path.extname gives "." character, so we're using substr(1) method. fs.readFile(filename, function(err, data) { if(err) { if(err.code == "ENOENT") { //ENOENT means "File doesn't exist" res.writeHead(404, "Not Found", { "Content-Type": "text/plain" }); res.end("404 Not Found"); } else { res.writeHead(500, "Internal Server Error", { "Content-Type": "text/plain" }); res.end("500 Internal Server Error! Reason: " + err.message); } } else { res.writeHead(200, "OK", { "Content-Type": mime.lookup(ext) || undefined }); res.end(data); } }); }); server.listen(port, function() { console.log("Started server at port " + port + "."); }); {% endcodeblock %} There is still one problem - the leak of "server.js" file. We can add a condition: {% codeblock server.js lang:javascript %} //Source code leakage mitigated, but THERE IS A "%00" PROBLEM! var http = require("http"); var fs = require("fs"); var mime = require("mime-types"); var path = require("path"); var os = require("os"); var port = 8080; var server = http.createServer(function (req, res) { var urlObject = new URL(req.url, "http://localhost"); var filename = ""; try { filename = "." + decodeURIComponent(urlObject.pathname); } catch(ex) { //Malformed URI means bad request. res.writeHead(400, "Bad Request", { "Content-Type": "text/plain" }); res.end("400 Bad Request"); return; } filename = filename.replace(/\/g,"/").replace(//..?(?=/|$)/g,"/").replace(//+/g,"/"); //Poor mans URL sanitizer if(filename == "./") filename = "./index.html"; var ext = path.extname(filename).substr(1); //path.extname gives "." character, so we're using substr(1) method. if(filename == ("./" + path.basename(__filename)) || (os.platform() == "win32" && filename.toLowerCase() == ("./" + path.basename(__filename)).toLowerCase())) { //Prevent leakage of server source code res.writeHead(403, "Forbidden", { "Content-Type": "text/plain" }); res.end("403 Forbidden"); return; } fs.readFile(filename, function(err, data) { if(err) { if(err.code == "ENOENT") { //ENOENT means "File doesn't exist" res.writeHead(404, "Not Found", { "Content-Type": "text/plain" }); res.end("404 Not Found"); } else { res.writeHead(500, "Internal Server Error", { "Content-Type": "text/plain" }); res.end("500 Internal Server Error! Reason: " + err.message); } } else { res.writeHead(200, "OK", { "Content-Type": mime.lookup(ext) || undefined }); res.end(data); } }); }); server.listen(port, function() { console.log("Started server at port " + port + "."); }); {% endcodeblock %} Oh no! Our server may be vulnerable to null byte injection! You can remove null bytes and "%00", like this: {% codeblock server.js lang:javascript %} //Null byte injection mitigated! var http = require("http"); var fs = require("fs"); var mime = require("mime-types"); var path = require("path"); var os = require("os"); var port = 8080; var server = http.createServer(function (req, res) { var urlObject = new URL(req.url, "http://localhost"); var filename = ""; try { filename = "." + decodeURIComponent(urlObject.pathname); } catch(ex) { //Malformed URI means bad request. res.writeHead(400, "Bad Request", { "Content-Type": "text/plain" }); res.end("400 Bad Request"); return; } filename = filename.replace(/\/g,"/").replace(/\0|%00/g,"").replace(//..?(?=/|$)/g,"/").replace(//+/g,"/"); //Poor mans URL sanitizer if(filename == "./") filename = "./index.html"; var ext = path.extname(filename).substr(1); //path.extname gives "." character, so we're using substr(1) method. if(filename == ("./" + path.basename(__filename)) || (os.platform() == "win32" && filename.toLowerCase() == ("./" + path.basename(__filename)).toLowerCase())) { //Prevent leakage of server source code res.writeHead(403, "Forbidden", { "Content-Type": "text/plain" }); res.end("403 Forbidden"); return; } fs.readFile(filename, function(err, data) { if(err) { if(err.code == "ENOENT") { //ENOENT means "File doesn't exist" res.writeHead(404, "Not Found", { "Content-Type": "text/plain" }); res.end("404 Not Found"); } else { res.writeHead(500, "Internal Server Error", { "Content-Type": "text/plain" }); res.end("500 Internal Server Error! Reason: " + err.message); } } else { res.writeHead(200, "OK", { "Content-Type": mime.lookup(ext) || undefined }); res.end(data); } }); }); server.listen(port, function() { console.log("Started server at port " + port + "."); }); {% endcodeblock %} We have now very simple HTTP static server, serving at localhost:8080.

Wait... Did we forget about SVR.JS? SVR.JS is a web server running on Node.JS, that supports not only static file serving, but also directory listings, path rewriting, complete URL sanitation, HTTPS, HTTP/2.0, expandability via mods and server-side JavaScript, and it's configurable.