node-http-server – Rev 32

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");
const stream = require('stream');
const util = require('util');
const EventEmitter = require('events').EventEmitter;

// 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(self, config, file, client) {
        // Check if the file is accessible.
        fs.access(file, fs.constants.R_OK, (error) => {
                if (error) {
                        self.emit('log', {
                                message: 'Client: ' +
                                        client.address + ':' +
                                        client.port +
                                        ' requesting inaccessible path: ' +
                                        file,
                                severity: 'warning'
                        });
                        self.emit('data', {
                                status: 403,
                                data: new stream.Readable({
                                        read(size) {
                                                this.push(null);
                                        }
                                }),
                                type: 'text/plain'
                        });
                        return;
                }
                self.emit('log', {
                        message: 'Client: ' +
                                client.address + ':' +
                                client.port +
                                ' sent file: ' +
                                file,
                        severity: 'info'
                });
                self.emit('data', {
                        status: 200,
                        data: fs
                                .createReadStream(file),
                        type: mime
                                .lookup(file)
                });
        });
}

// Serves a directory listing or the document index in case it exists.
function index(self, config, directory, href, client) {
        const root = path.resolve(directory, config.site.index);
        fs.stat(root, (error, stats) => {
                if (error) {
                        if (config.site.indexing
                                .some((directory) =>
                                        directory.toUpperCase() === href.toUpperCase())) {
                                fs.readdir(directory, (error, paths) => {
                                        if (error) {
                                                self.emit('log', {
                                                        message: 'Client: ' +
                                                                client.address + ':' +
                                                                client.port +
                                                                ' could not access directory: ' +
                                                                directory,
                                                        severity: 'warning'
                                                });
                                                self.emit('data', {
                                                        status: 500,
                                                        data: new stream.Readable({
                                                                read(size) {
                                                                        this.push(null);
                                                                }
                                                        }),
                                                        type: 'text/plain'
                                                });
                                                return;
                                        }
                                        self.emit('log', {
                                                message: 'Client: ' +
                                                        client.address + ':' +
                                                        client.port +
                                                        ' accessed directory listing: ' +
                                                        directory,
                                                severity: 'info'
                                        });
                                        self.emit('data', {
                                                status: 200,
                                                data: new stream.Readable({
                                                        read(size) {
                                                                this.push(JSON.stringify(paths));
                                                                this.push(null);
                                                        }
                                                }),
                                                type: 'application/json'
                                        });
                                });
                                return;
                        }
                        // Could not access directory index file and directory listing not allowed.
                        self.emit('log', {
                                message: 'Client: ' +
                                        client.address + ':' +
                                        client.port +
                                        ' no index file found and accessing forbiden index: ' +
                                        href,
                                severity: 'warning'
                        });
                        self.emit('data', {
                                status: 403,
                                data: new stream.Readable({
                                        read(size) {
                                                this.push(null);
                                        }
                                }),
                                type: 'text/plain'
                        });
                        return;
                }

                // Serve the document index.
                fs.access(root, fs.constants.R_OK, (error) => {
                        if (error) {
                                self.emit('log', {
                                        message: 'Client: ' +
                                                client.address + ':' +
                                                client.port +
                                                ' unable to access path: ' +
                                                directory,
                                        severity: 'warning'
                                });
                                self.emit('data', {
                                        status: 403,
                                        data: new stream.Readable({
                                                read(size) {
                                                        this.push(null);
                                                }
                                        }),
                                        type: 'text/plain'
                                });
                                return;
                        }
                        self.emit('log', {
                                message: 'Client: ' +
                                        client.address + ':' +
                                        client.port +
                                        ' sent file: ' +
                                        root,
                                severity: 'info'
                        });
                        self.emit('data', {
                                status: 200,
                                data: fs.createReadStream(root),
                                type: mime.lookup(root)
                        });
                });
        });
}

// Determines whether the requested filesystem request path is a directory or a file.
function serve(self, config, local, href, address) {
        fs.stat(local, (error, stats) => {
                // Document does not exist.
                if (error) {
                        self.emit('log', {
                                message: 'Client: ' +
                                        address.address + ':' +
                                        address.port +
                                        ' accessing non-existent document: ' +
                                        local,
                                severity: 'warning'
                        });
                        self.emit('data', {
                                status: 404,
                                data: new stream.Readable({
                                        read(size) {
                                                this.push(null);
                                        }
                                }),
                                type: 'text/plain'
                        });
                        return;
                }

                if (stats.isDirectory()) {
                        // Directory is requested so provide directory indexes.
                        index(self, config, local, href, address);
                        return;
                }
                if (stats.isFile()) {
                        const file = path.parse(local).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))) {
                                self.emit('log', {
                                        message: 'Client: ' +
                                                address.address + ':' +
                                                address.port +
                                                ' requested disallowed file: ' +
                                                file,
                                        severity: 'warning'
                                });
                                self.emit('data', {
                                        status: 404,
                                        data: new stream.Readable({
                                                read(size) {
                                                        this.push(null);
                                                }
                                        }),
                                        type: 'text/plain'
                                });
                                return;
                        }

                        // A file was requested so provide the file.
                        files(self, config, local, address);
                }
        });
}

// Constructor.
function Handler() {
        // Create events emitters for logging and data.
        EventEmitter.call(this);
};

// Process a request.
Handler.prototype.process = function(config, request, response, root) {
        EventEmitter.call(this);
        var self = this;

        // Get client details.
        const address = request.socket.address();
        // Get requested URL.
        const requestURL = url.parse(
                request.url, true
        );

        // Perform URL re-writes.
        Object.keys(config.site.rewrite).forEach((key, index) => {
                if (config.site.rewrite[key].test(requestURL.path)) {
                        const originalPath = requestURL.path;
                        requestURL.path = requestURL
                                .path
                                .replace(
                                        config.site.rewrite[key], key
                                );
                        requestURL.pathname = url.parse(
                                        requestURL
                                        .pathname
                                        .replace(
                                                config.site.rewrite[key], key
                                        ),
                                        true
                                )
                                .pathname;
                        self.emit('log', {
                                message: 'Rewrite path: ' +
                                        originalPath +
                                        ' to: ' +
                                        requestURL.path,
                                severity: 'info'
                        });
                }
        });

        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) {
                        self.emit('log', {
                                message: 'Unknown path requested: ' +
                                        address.address + ':' +
                                        address.port +
                                        ' requesting: ' +
                                        requestURL.pathname,
                                severity: 'warning'
                        });
                        self.emit('data', {
                                status: 404,
                                data: new stream.Readable({
                                        read(size) {
                                                this.push(null);
                                        }
                                }),
                                type: 'text/plain'
                        });
                        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) {
                                self.emit('log', {
                                        message: 'Attempted path traversal: ' +
                                                address.address + ':' +
                                                address.port +
                                                ' requesting: ' +
                                                requestURL.pathname,
                                        severity: 'warning'
                                });
                                self.emit('done', {
                                        status: 404,
                                        data: new stream.Readable({
                                                read(size) {
                                                        this.push(null);
                                                }
                                        }),
                                        type: 'text/plain'
                                });
                                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) => {
                                        self.emit('log', {
                                                message: 'Authenticated client: ' +
                                                        address.address + ':' +
                                                        address.port +
                                                        ' accessing: ' +
                                                        requestURL.pathname,
                                                severity: 'info'
                                        });
                                        process.nextTick(() =>
                                                serve(self,
                                                        config,
                                                        requestPath,
                                                        requestURL.pathname,
                                                        address
                                                )
                                        );
                                });
                                return;
                        }

                        // If no authentication is required then serve the request.
                        self.emit('log', {
                                message: 'Client: ' +
                                        address.address + ':' +
                                        address.port +
                                        ' accessing: ' +
                                        requestURL.pathname,
                                severity: 'info'
                        });
                        process.nextTick(() =>
                                serve(self,
                                        config,
                                        requestPath,
                                        requestURL.pathname,
                                        address
                                )
                        );
                });
        });
        
        return this;
};

util.inherits(Handler, EventEmitter);
module.exports = Handler;

Generated by GNU Enscript 1.6.5.90.