367 lines
14 KiB
Markdown
367 lines
14 KiB
Markdown
---
|
|
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 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 %}
|
|
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.
|