'use strict'; const Path = require('path'); const Boom = require('@hapi/boom'); const Bounce = require('@hapi/bounce'); const Hoek = require('@hapi/hoek'); const Validate = require('@hapi/validate'); const File = require('./file'); const Fs = require('./fs'); const internals = {}; internals.schema = Validate.object({ path: Validate.alternatives(Validate.array().items(Validate.string()).single(), Validate.func()).required(), index: Validate.alternatives(Validate.boolean(), Validate.array().items(Validate.string()).single()).default(true), listing: Validate.boolean(), showHidden: Validate.boolean(), redirectToSlash: Validate.boolean(), lookupCompressed: Validate.boolean(), lookupMap: Validate.object().min(1).pattern(/.+/, Validate.string()), etagMethod: Validate.string().valid('hash', 'simple').allow(false), defaultExtension: Validate.string().alphanum() }); internals.resolvePathOption = function (result) { if (result instanceof Error) { throw result; } if (typeof result === 'string') { return [result]; } if (Array.isArray(result)) { return result; } throw Boom.internal('Invalid path function'); }; exports.handler = function (route, options) { const settings = Validate.attempt(options, internals.schema, 'Invalid directory handler options (' + route.path + ')'); Hoek.assert(route.path[route.path.length - 1] === '}', 'The route path for a directory handler must end with a parameter:', route.path); const paramName = /\w+/.exec(route.path.slice(route.path.lastIndexOf('{')))[0]; const basePath = route.settings.files.relativeTo; const normalized = (Array.isArray(settings.path) ? settings.path : null); // Array or function const indexNames = (settings.index === true) ? ['index.html'] : (settings.index || []); // Declare handler const handler = async (request, reply) => { const paths = normalized || internals.resolvePathOption(settings.path.call(null, request)); // Append parameter const selection = request.params[paramName] || ''; if (Path.isAbsolute(selection)) { throw Boom.notFound(null, {}); } if (selection && !settings.showHidden && internals.isFileHidden(selection)) { throw Boom.notFound(null, {}); } if (!selection && (request.server.settings.router.stripTrailingSlash || !request.path.endsWith('/'))) { request.path += '/'; } // Generate response const resource = request.path; const hasTrailingSlash = resource.endsWith('/'); const fileOptions = { confine: null, lookupCompressed: settings.lookupCompressed, lookupMap: settings.lookupMap, etagMethod: settings.etagMethod }; const each = async (baseDir) => { fileOptions.confine = baseDir; let path = selection; let error; try { return await File.load(path, request, fileOptions); } catch (err) { Bounce.ignore(err, 'boom'); error = err; } // Handle Not found if (internals.isNotFound(error)) { if (!settings.defaultExtension) { throw error; } if (hasTrailingSlash) { path = path.slice(0, -1); } return await File.load(path + '.' + settings.defaultExtension, request, fileOptions); } // Handle Directory if (internals.isDirectory(error)) { if (settings.redirectToSlash !== false && // Defaults to true !request.server.settings.router.stripTrailingSlash && !hasTrailingSlash) { return reply.redirect(resource + '/'); } for (const indexName of indexNames) { const indexFile = Path.join(path, indexName); try { return await File.load(indexFile, request, fileOptions); } catch (err) { Bounce.ignore(err, 'boom'); if (!internals.isNotFound(err)) { throw Boom.internal(indexName + ' is a directory', err); } // Not found - try next } } // None of the index files were found if (settings.listing) { return internals.generateListing(Path.join(basePath, baseDir, path), resource, selection, hasTrailingSlash, settings, request); } } throw error; }; for (let i = 0; i < paths.length; ++i) { try { return await each(paths[i]); } catch (err) { Bounce.ignore(err, 'boom'); // Propagate any non-404 errors if (!internals.isNotFound(err) || i === paths.length - 1) { throw err; } } } throw Boom.notFound(null, {}); }; return handler; }; internals.generateListing = async function (path, resource, selection, hasTrailingSlash, settings, request) { let files; try { files = await Fs.readdir(path); } catch (err) { Bounce.rethrow(err, 'system'); throw Boom.internal('Error accessing directory', err); } resource = decodeURIComponent(resource); const display = Hoek.escapeHtml(resource); let html = '