Protecting files in NodeJS

Posted in software by Christopher R. Wirz on Thu Jan 21 2016



NodeJS is a flexible asynchronous server. However, some of the many features that were available in Apache (like htaccess) are no longer an option. Instead, one must implement their own file protection.

Note: The following has been tested in Node v4.2.6

The advantage of a custom implementation is flexibility. Permissions cna be driven by a user database or within the logic of the express server.

The following code shows a simple implementation that uses a file-by-file approach to protection.


// server.js
#!/bin/env node
var express = require('express'),;
var fs = require('fs');
var path = require('path');

var App = function() {
    var self = this;
	self.port = 8080;
	self.ipAddress = "127.0.0.1";

	self.getFileExtension = function(file){
		if (typeof(file)=='string'){
			var splits = file.split('.');
			return splits[splits.length-1];
		}
		return '';
	};

    self.getMimeType = function(ext){
        if (typeof(ext)!='string'){
            ext = '';
        }
        ext = ext.toLowerCase();

        switch(ext)
        {
            case 'js' :
                return 'application/x-javascript';
            case 'json' :
                return 'application/json';
            case 'jpg' :
            case 'jpeg' :
            case 'jpe' :
                return 'image/jpg';
            case 'png' :
            case 'gif' :
            case 'bmp' :
            case 'tiff' :
                return 'image/'+ ext.toLowerCase();
            case 'css' :
            case 'map' :
                return 'text/css';
            case 'xml' :
                return 'application/xml';
            case 'doc' :
            case 'docx' :
                return 'application/msword';
            case 'xls' :
            case 'xlt' :
            case 'xlm' :
            case 'xld' :
            case 'xla' :
            case 'xlc' :
            case 'xlw' :
            case 'xll' :
                return 'application/vnd.ms-excel';
            case 'ppt' :
            case 'pps' :
                return 'application/vnd.ms-powerpoint';
            case 'rtf' :
                return 'application/rtf';
            case 'pdf' :
                return 'application/pdf';
            case 'html' :
            case 'htm' :
            case 'php' :
                return 'text/html';
            case 'swift' :
            case 'txt' :
                return 'text/plain';
            case 'mpeg' :
            case 'mpg' :
            case 'mpe' :
                return 'video/mpeg';
            case 'mp3' :
                return 'audio/mpeg3';
            case 'wav' :
                return 'audio/wav';
            case 'aiff' :
            case 'aif' :
                return 'audio/aiff';
            case 'avi' :
                return 'video/msvideo';
            case 'wmv' :
                return 'video/x-ms-wmv';
            case 'mov' :
                return 'video/quicktime';
            case 'zip' :
                return 'application/zip';
            case 'tar' :
                return 'application/x-tar';
            case 'swf' :
                return 'application/x-shockwave-flash';
            default :
                return 'application/octet-stream';
        }
    };

    self.getFileMimeType = function(file){
        return self.getMimeType(self.getFileExtension(file));
    };

    self.getAuthLogin = function(req){
        var ret = {login: null, password: null};
        var b64auth = '';
        if (typeof(req)!="undefined"){
            if (typeof(req.headers)!="undefined"){
                if (typeof(req.headers.authorization)!="undefined"){
                    var splits = req.headers.authorization.split(' ');
                    if (splits.length > 1){
                        b64auth = splits[1];
                    }
                }
            }
        }

        var bufferSplits = new Buffer(b64auth, 'base64').toString().split(':');
        ret.login = bufferSplits[0];
        if (bufferSplits.length >1) {
            ret.password = bufferSplits[1];
        }
        return ret;
    };

    self.protectFile = function(auth, file, req, res){
        var a = self.getAuthLogin(req);

        if (!a.login || !a.password || a.login !== auth.login || a.password !== auth.password) {
            res.set('WWW-Authenticate', 'Basic realm="Protected"');
            res.status(401).send('Authentication required.');
            return;
        }

        var filePath = path.resolve(__dirname, file);
        res.setHeader('Content-Type', self.getFileMimeType(filePath));
        return res.send(fs.readFileSync(filePath));
    };

    self.initializeServer = function() {
        self.app = express();

		self.app.get('/protected-files/:file', function(req, res){
			if (typeof(req.params.file)=="string"){
				var file = req.params.file.toString().toLowerCase();

				// define a protected file alias
				if (file === 'myprotectedpdf.pdf') {

					// the file is named e7d2555a-5b06-4d48-b085-527b39f37759.pdf
					// that's a pretty secret name!
					// the file is located in the adjacent 'privatefiles' directory
					return self.protectFile({login: 'pdfuser', password: 'secret'}, "privatefiles/e7d2555a-5b06-4d48-b085-527b39f37759.pdf", req, res);
				}

				// maybe don't rename the file this time
				if (file === 'test.txt') {
					return self.protectFile({login: 'test', password: 'test'}, "privatefiles/test.txt", req, res);
				}
			}

			return res.status(404).send('Not Found');
		});
    };


    /**
     *  Initializes the sample application.
     */
    self.initialize = function(callback) {
		self.initializeServer();
		if (typeof(callback)=="function"){
			callback();
		}
    };

    self.start = function() {
        self.app.listen(self.port, self.ipAddress, function() {
            console.log('%s: Node server started on %s:%d ...', Date(Date.now() ), self.ipAddress, self.port);
        });
    };
};


/**
 *  Main code.
 */
var app = new App();
app.initialize(function(){
    app.start();
});

Now when a user tries to access a file, they must first enter a username and password.