fluffy – Rev 1

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;

// Local imports.
const Cache = require(
    path
    .resolve(
        path.dirname(require.main.filename),
        'src',
        'cache'
    )
);
const was = require(
    path
    .resolve(
        path.dirname(require.main.filename),
        'src',
        'was'
    )
);

// Serves files.
function files(self, config, file, client, cache) {
    // 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;
        }

        cache.process(file, fs.createReadStream(file), mime.lookup(file))
            .on('data', (result) => self.emit('data', result))
            .on('log', (data) => self.emit('log', data));
    });
}

// Serves a directory listing or the document index in case it exists.
function index(self, config, directory, href, client, cache) {
    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;
                    }
                    cache.process(directory, new stream.Readable({
                            read(size) {
                                this.push(JSON.stringify(paths));
                                this.push(null);
                            }
                        }), 'application/json')
                        .on('data', (result) => self.emit('data', result))
                        .on('log', (data) => self.emit('log', data));
                });
                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;
            }
            cache.process(root, fs.createReadStream(root), mime.lookup(root))
                .on('data', (result) => self.emit('data', result))
                .on('log', (data) => self.emit('log', data));
        });
    });
}

// Determines whether the requested filesystem request path is a directory or a file.
function serve(self, config, local, href, address, cache) {
    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, cache);
            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, cache);
        }
    });
}

// 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.
        was.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,
                            new Cache(config, address, request, response)
                        )
                    );
                });
                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,
                    new Cache(config, address, request, response)
                )
            );
        });
    });

    return this;
};

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

Generated by GNU Enscript 1.6.5.90.