This repository has been archived on 2024-09-12. You can view files and clone it, but cannot push or open issues or pull requests.
svrjs-blog/source/_posts/How-to-create-static-HTTP-server-in-Node-JS.md

368 lines
14 KiB
Markdown
Raw Normal View History

2024-03-15 23:24:27 +01:00
---
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
2024-03-15 23:24:27 +01:00
---
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.
2024-03-15 23:24:27 +01:00
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
2024-03-15 23:24:27 +01:00
## 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.
2024-03-15 23:24:27 +01:00
{% 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";
2024-03-15 23:24:27 +01:00
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:
2024-03-15 23:24:27 +01:00
{% 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";
2024-03-15 23:24:27 +01:00
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";
2024-03-15 23:24:27 +01:00
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";
2024-03-15 23:24:27 +01:00
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";
2024-03-15 23:24:27 +01:00
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");
2024-03-31 18:27:21 +02:00
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.
2024-03-31 18:27:21 +02:00
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;
}
2024-03-15 23:24:27 +01:00
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.