node-http-server – Rev 26

Subversion Repositories:
Rev:
#!/usr/bin/env node

/*************************************************************************/
/*    Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3    */
/*************************************************************************/

const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const auth = require("http-auth");

// Checks whether userPath is a child of rootPath.
function isRooted(userPath, rootPath, separator, callback) {
        userPath = userPath.split(separator).filter(Boolean);
        rootPath = rootPath.split(separator).filter(Boolean);
        callback(userPath.length >= rootPath.length &&
                rootPath.every((e, i) => e === userPath[i]));
}

// Serves files.
function files(config, request, response, requestPath, callback) {
        // Check if the file is accessible.
        fs.access(requestPath, fs.constants.R_OK, (error) => {
                if (error) {
                        response.statusCode = 403;
                        response.end();
                        return;
                }

                response.setHeader(
                        'Content-Type',
                        mime.lookup(requestPath)
                );

                var readStream = fs.createReadStream(requestPath)
                        .on('open', () => {
                                response.statusCode = 200;
                                readStream.pipe(response);
                        })
                        .on('error', () => {
                                response.statusCode = 500;
                                response.end();
                        });

        });
}

// Serves a directory listing or the document index in case it exists.
function index(config, request, response, requestPath, requestURL, callback) {
        const root = path.resolve(requestPath, config.site.index);
        fs.stat(root, (error, stats) => {
                if (error) {
                        if (config.site.indexing
                                .some((directory) =>
                                        directory.toUpperCase() === requestURL.toUpperCase())) {
                                fs.readdir(requestPath, (error, paths) => {
                                        if (error) {
                                                process.nextTick(() => {
                                                        callback('Could not list directory: ' +
                                                                requestPath,
                                                                module.exports.error.level.ERROR
                                                        );
                                                });
                                                response.statusCode = 500;
                                                response.end();
                                                return;
                                        }
                                        process.nextTick(() => {
                                                callback('Directory listing requested for: ' +
                                                        requestPath,
                                                        module.exports.error.level.INFO
                                                );
                                        });
                                        response.statusCode = 200;
                                        response.write(JSON.stringify(paths));
                                        response.end();
                                });

                                return;
                        }

                        // Could not access directory index file and directory listing not allowed.
                        response.statusCode = 404;
                        response.end();
                        return;

                }

                // Serve the document index.
                fs.access(root, fs.constants.R_OK, (error) => {
                        if (error) {
                                process.nextTick(() => {
                                        callback('The server was unable to access the filesystem path: ' +
                                                requestPath,
                                                module.exports.error.level.WARN
                                        );
                                });
                                response.statusCode = 403;
                                response.end();
                                return;
                        }

                        // Set MIME content type.
                        response.setHeader(
                                'Content-Type',
                                mime.lookup(root)
                        );

                        var readStream = fs.createReadStream(root)
                                .on('open', () => {
                                        response.statusCode = 200;
                                        readStream.pipe(response);
                                })
                                .on('error', () => {
                                        response.statusCode = 500;
                                        response.end();
                                });

                });
        });
}

// Determines whether the requested filesystem request path is a directory or a file.
function serve(config, request, response, requestPath, requestURL, callback) {
        fs.stat(requestPath, (error, stats) => {
                // Document does not exist.
                if (error) {
                        response.statusCode = 404;
                        response.end();
                        return;
                }

                if (stats.isDirectory()) {
                        // Directory is requested so provide directory indexes.
                        index(config, request, response, requestPath, requestURL, callback);
                        return;
                }
                if (stats.isFile()) {
                        const file = path.parse(requestPath).base;

                        // If the file matches the reject list or is not in the accept list,
                        // then there is no file to serve. 
                        if (config.site.reject.some((expression) => expression.test(file)) ||
                                !config.site.accept.some((expression) => expression.test(file))) {
                                response.statusCode = 404;
                                response.end();
                                return;
                        }

                        // A file was requested so provide the file.
                        files(config, request, response, requestPath, callback);
                }
        });
}

module.exports = {
        error: {
                level: {
                        INFO: 1,
                        WARN: 2,
                        ERROR: 3
                }
        },
        process: (config, request, response, root, callback) => {
                process.nextTick(() => {
                        const requestAddress = request.socket.address();
                        const requestURL = url.parse(
                                request.url, true
                        );

                        const trimmedPath = requestURL
                                .pathname
                                .split('/')
                                .filter(Boolean)
                                .join('/');
                        const requestPath = trimmedPath === '/' ?
                                path.join(root, trimmedPath) :
                                path.resolve(root, trimmedPath);

                        fs.realpath(requestPath, (error, resolvedPath) => {
                                // If the path does not exist, then return early.
                                if (error) {
                                        callback('Unknown path requested: ' +
                                                requestAddress.address + ':' +
                                                requestAddress.port +
                                                ' requesting: ' +
                                                requestURL.pathname,
                                                module.exports.error.level.WARN
                                        );
                                        response.statusCode = 404;
                                        response.end();
                                        return;
                                }
                                // Check for path traversals early on and bail if the requested path does not
                                // lie within the specified document root.
                                isRooted(resolvedPath, root, path.sep, (rooted) => {
                                        if (!rooted) {
                                                process.nextTick(() => {
                                                        callback('Attempted path traversal: ' +
                                                                requestAddress.address + ':' +
                                                                requestAddress.port +
                                                                ' requesting: ' +
                                                                requestURL.pathname,
                                                                module.exports.error.level.WARN
                                                        );
                                                });
                                                response.statusCode = 404;
                                                response.end();
                                                return;
                                        }

                                        // If authentication is required for this path then perform authentication.
                                        if (config.auth.locations.some(
                                                        (authPath) => authPath.toUpperCase() === requestURL.pathname.toUpperCase())) {
                                                // Create digest authentication.
                                                const authentication = auth.digest({
                                                        realm: config.auth.realm,
                                                        file: path.resolve(
                                                                path.dirname(require.main.filename),
                                                                config.auth.digest
                                                        )
                                                });
                                                // Requested location requires authentication.
                                                authentication.check(request, response, (request, response) => {
                                                        process.nextTick(() => {
                                                                callback('Authenticated client: ' +
                                                                        requestAddress.address + ':' +
                                                                        requestAddress.port +
                                                                        ' accessing: ' +
                                                                        requestURL.pathname,
                                                                        module.exports.error.level.INFO
                                                                );
                                                        });
                                                        serve(config,
                                                                request,
                                                                response,
                                                                requestPath,
                                                                requestURL.pathname,
                                                                callback
                                                        );
                                                });
                                                return;
                                        }

                                        // If no authentication is required then serve the request.
                                        process.nextTick(() => {
                                                callback('Client: ' +
                                                        requestAddress.address + ':' +
                                                        requestAddress.port +
                                                        ' accessing: ' +
                                                        requestURL.pathname,
                                                        module.exports.error.level.INFO
                                                );
                                        });
                                        serve(config,
                                                request,
                                                response,
                                                requestPath,
                                                requestURL.pathname,
                                                callback
                                        );
                                });
                        });
                });
        }
};

Generated by GNU Enscript 1.6.5.90.