node-http-server – Rev 24

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) {
    fs.realpath(userPath, (error, resolved) => {
        if (error)
            return false;
        resolved = resolved.split(separator).filter(Boolean);
        rootPath = rootPath.split(separator).filter(Boolean);
        callback(resolved.length >= rootPath.length &&
            rootPath.every((e, i) => e === resolved[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);

            // Check for path traversals early on and bail if the requested path does not
            // lie within the specified document root.
            isRooted(requestPath, 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.