UNPKG

7.08 kBJavaScriptView Raw
1'use strict';
2
3const Path = require('path');
4
5const Boom = require('@hapi/boom');
6const Bounce = require('@hapi/bounce');
7const Hoek = require('@hapi/hoek');
8const Validate = require('@hapi/validate');
9
10const File = require('./file');
11const Fs = require('./fs');
12
13
14const internals = {};
15
16
17internals.schema = Validate.object({
18 path: Validate.alternatives(Validate.array().items(Validate.string()).single(), Validate.func()).required(),
19 index: Validate.alternatives(Validate.boolean(), Validate.array().items(Validate.string()).single()).default(true),
20 listing: Validate.boolean(),
21 showHidden: Validate.boolean(),
22 redirectToSlash: Validate.boolean(),
23 lookupCompressed: Validate.boolean(),
24 lookupMap: Validate.object().min(1).pattern(/.+/, Validate.string()),
25 etagMethod: Validate.string().valid('hash', 'simple').allow(false),
26 defaultExtension: Validate.string().alphanum()
27});
28
29
30internals.resolvePathOption = function (result) {
31
32 if (result instanceof Error) {
33 throw result;
34 }
35
36 if (typeof result === 'string') {
37 return [result];
38 }
39
40 if (Array.isArray(result)) {
41 return result;
42 }
43
44 throw Boom.internal('Invalid path function');
45};
46
47
48exports.handler = function (route, options) {
49
50 const settings = Validate.attempt(options, internals.schema, 'Invalid directory handler options (' + route.path + ')');
51 Hoek.assert(route.path[route.path.length - 1] === '}', 'The route path for a directory handler must end with a parameter:', route.path);
52
53 const paramName = /\w+/.exec(route.path.slice(route.path.lastIndexOf('{')))[0];
54 const basePath = route.settings.files.relativeTo;
55
56 const normalized = (Array.isArray(settings.path) ? settings.path : null); // Array or function
57 const indexNames = (settings.index === true) ? ['index.html'] : (settings.index || []);
58
59 // Declare handler
60
61 const handler = async (request, reply) => {
62
63 const paths = normalized || internals.resolvePathOption(settings.path.call(null, request));
64
65 // Append parameter
66
67 const selection = request.params[paramName];
68 if (selection &&
69 !settings.showHidden &&
70 internals.isFileHidden(selection)) {
71
72 throw Boom.notFound(null, {});
73 }
74
75 if (!selection &&
76 (request.server.settings.router.stripTrailingSlash || !request.path.endsWith('/'))) {
77
78 request.path += '/';
79 }
80
81 // Generate response
82
83 const resource = request.path;
84 const hasTrailingSlash = resource.endsWith('/');
85 const fileOptions = {
86 confine: null,
87 lookupCompressed: settings.lookupCompressed,
88 lookupMap: settings.lookupMap,
89 etagMethod: settings.etagMethod
90 };
91
92 const each = async (baseDir) => {
93
94 fileOptions.confine = baseDir;
95
96 let path = selection || '';
97 let error;
98
99 try {
100 return await File.load(path, request, fileOptions);
101 }
102 catch (err) {
103 Bounce.ignore(err, 'boom');
104 error = err;
105 }
106
107 // Handle Not found
108
109 if (internals.isNotFound(error)) {
110 if (!settings.defaultExtension) {
111 throw error;
112 }
113
114 if (hasTrailingSlash) {
115 path = path.slice(0, -1);
116 }
117
118 return await File.load(path + '.' + settings.defaultExtension, request, fileOptions);
119 }
120
121 // Handle Directory
122
123 if (internals.isDirectory(error)) {
124 if (settings.redirectToSlash !== false && // Defaults to true
125 !request.server.settings.router.stripTrailingSlash &&
126 !hasTrailingSlash) {
127
128 return reply.redirect(resource + '/');
129 }
130
131 for (const indexName of indexNames) {
132 const indexFile = Path.join(path, indexName);
133 try {
134 return await File.load(indexFile, request, fileOptions);
135 }
136 catch (err) {
137 Bounce.ignore(err, 'boom');
138
139 if (!internals.isNotFound(err)) {
140 throw Boom.internal(indexName + ' is a directory', err);
141 }
142
143 // Not found - try next
144 }
145 }
146
147 // None of the index files were found
148
149 if (settings.listing) {
150 return internals.generateListing(Path.join(basePath, baseDir, path), resource, selection, hasTrailingSlash, settings, request);
151 }
152 }
153
154 throw error;
155 };
156
157 for (let i = 0; i < paths.length; ++i) {
158 try {
159 return await each(paths[i]);
160 }
161 catch (err) {
162 Bounce.ignore(err, 'boom');
163
164 // Propagate any non-404 errors
165
166 if (!internals.isNotFound(err) ||
167 i === paths.length - 1) {
168 throw err;
169 }
170 }
171 }
172
173 throw Boom.notFound(null, {});
174 };
175
176 return handler;
177};
178
179
180internals.generateListing = async function (path, resource, selection, hasTrailingSlash, settings, request) {
181
182 let files;
183 try {
184 files = await Fs.readdir(path);
185 }
186 catch (err) {
187 Bounce.rethrow(err, 'system');
188 throw Boom.internal('Error accessing directory', err);
189 }
190
191 resource = decodeURIComponent(resource);
192 const display = Hoek.escapeHtml(resource);
193 let html = '<html><head><title>' + display + '</title></head><body><h1>Directory: ' + display + '</h1><ul>';
194
195 if (selection) {
196 const parent = resource.substring(0, resource.lastIndexOf('/', resource.length - (hasTrailingSlash ? 2 : 1))) + '/';
197 html = html + '<li><a href="' + internals.pathEncode(parent) + '">Parent Directory</a></li>';
198 }
199
200 for (let i = 0; i < files.length; ++i) {
201 if (settings.showHidden ||
202 !internals.isFileHidden(files[i])) {
203
204 html = html + '<li><a href="' + internals.pathEncode(resource + (!hasTrailingSlash ? '/' : '') + files[i]) + '">' + Hoek.escapeHtml(files[i]) + '</a></li>';
205 }
206 }
207
208 html = html + '</ul></body></html>';
209
210 return request.generateResponse(html);
211};
212
213
214internals.isFileHidden = function (path) {
215
216 return /(^|[\\\/])\.([^.\\\/]|\.[^\\\/])/.test(path); // Starts with a '.' or contains '/.' or '\.', which is not followed by a '/' or '\' or '.'
217};
218
219
220internals.pathEncode = function (path) {
221
222 return encodeURIComponent(path).replace(/%2F/g, '/').replace(/%5C/g, '\\');
223};
224
225
226internals.isNotFound = function (boom) {
227
228 return boom.output.statusCode === 404;
229};
230
231
232internals.isDirectory = function (boom) {
233
234 return boom.output.statusCode === 403 && boom.data.code === 'EISDIR';
235};