--- title: How to create static HTTP server in Node.JS? date: 2023-07-10 02:53:17 tags: - http - node.js - javascript - server categories: Tips thumbnail: /images/covers/How-to-create-static-HTTP-server-in-Node-JS.png --- 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](/images/hello-server.png) ## 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 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. if(filename == "./" + path.basename(__filename)) { //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](https://svrjs.duckdns.org)?** 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.