1 | 'use strict';
|
2 |
|
3 | var fs = require('fs');
|
4 | var path = require('path');
|
5 | var url = require('url');
|
6 | var send = require('send');
|
7 | var mime = require('mime');
|
8 | var merge = require('merge');
|
9 | var async = require('async');
|
10 | var etag = require('etag');
|
11 |
|
12 | var utils = require('./utils');
|
13 | var CacheChain = require('./cache-chain');
|
14 | var TransformChain = require('./transform-chain');
|
15 |
|
16 | var Cache = require('@donotjs/donot-cache');
|
17 | var Transform = require('@donotjs/donot-transform');
|
18 |
|
19 | class Donot {
|
20 | |
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 | constructor(root, opt) {
|
27 |
|
28 | if (typeof root !== 'string') {
|
29 | throw new TypeError('root is required and must be a string');
|
30 | }
|
31 |
|
32 | this.root = root;
|
33 |
|
34 | if (opt && typeof opt !== 'object') {
|
35 | throw new TypeError('opt must be an object');
|
36 | }
|
37 |
|
38 | opt = opt || {};
|
39 |
|
40 |
|
41 | this.options = {};
|
42 |
|
43 |
|
44 | this.options.index = opt.index || ['index.html'];
|
45 | this.options.etag = opt.etag !== false;
|
46 | this.options.lastModified = opt.lastModified !== false;
|
47 | this.options.dotFiles = opt.dotFiles === true;
|
48 | this.options.templates = opt.templates === true;
|
49 | this.options.serveDir = opt.serveDir || '/';
|
50 | this.options.accessControl = opt.accessControl || { deny: [] };
|
51 |
|
52 |
|
53 | this.transforms = [];
|
54 | (opt.transforms || []).forEach((e) => {
|
55 | this.transform(e);
|
56 | });
|
57 |
|
58 |
|
59 | this.options.cache = opt.cache || new Cache();
|
60 |
|
61 |
|
62 | if (this.options.cache.constructor.name !== 'Array') {
|
63 | this.options.cache = [this.options.cache];
|
64 | }
|
65 |
|
66 |
|
67 | if (typeof this.options.serveDir !== 'string') {
|
68 | throw new TypeError('serveDir must be a string');
|
69 | }
|
70 |
|
71 |
|
72 | this.options.serveDir = path.normalize('/' + this.options.serveDir + '/');
|
73 |
|
74 |
|
75 | if (typeof this.options.accessControl !== 'object') {
|
76 | throw new TypeError('accessControl must be of type object');
|
77 | }
|
78 |
|
79 |
|
80 | if (typeof this.options.accessControl.deny !== 'undefined' &&
|
81 | typeof this.options.accessControl.allow !== 'undefined') {
|
82 | throw new TypeError('accessControl cannot have both allow and deny.');
|
83 | }
|
84 |
|
85 |
|
86 | if (this.options.accessControl.deny &&
|
87 | (typeof this.options.accessControl.deny !== 'object' ||
|
88 | this.options.accessControl.deny.constructor.name !== 'Array')) {
|
89 | throw new TypeError('accessControl.deny must be of type array');
|
90 | }
|
91 |
|
92 |
|
93 | if (this.options.accessControl.allow &&
|
94 | (typeof this.options.accessControl.allow !== 'object' ||
|
95 | this.options.accessControl.allow.constructor.name !== 'Array')) {
|
96 | throw new TypeError('accessControl.deny must be of type array');
|
97 | }
|
98 |
|
99 |
|
100 | if (!this.options.accessControl.deny && !this.options.accessControl.allow) {
|
101 | this.options.accessControl.deny = [];
|
102 | }
|
103 |
|
104 |
|
105 | var checkArray = (arr) => {
|
106 | if (typeof arr == 'undefined') return true;
|
107 | for (var idx in arr) {
|
108 | if ((typeof arr[idx] !== 'object' || arr[idx].constructor.name !== 'RegExp') && typeof arr[idx] !== 'string') {
|
109 | return false;
|
110 | }
|
111 | }
|
112 | return true;
|
113 | };
|
114 |
|
115 | if (!checkArray(this.options.accessControl.deny)) {
|
116 | throw new TypeError('accessControl.deny can only contain Strings and RegExps');
|
117 | }
|
118 |
|
119 | if (!checkArray(this.options.accessControl.allow)) {
|
120 | throw new TypeError('accessControl.allow can only contain Strings and RegExps');
|
121 | }
|
122 |
|
123 |
|
124 | this.options.cache.forEach((cache) => {
|
125 | if (!utils.isKindOf(cache, Cache)) {
|
126 | throw new TypeError('cache must be of type Cache.');
|
127 | }
|
128 | });
|
129 |
|
130 |
|
131 | this.cacheChain = new CacheChain(this.options.cache, this.root, this.options.serveDir);
|
132 |
|
133 |
|
134 | var rootStat = fs.statSync(root);
|
135 | if (!rootStat.isDirectory()) throw new Error('root is not a directory');
|
136 | }
|
137 |
|
138 | |
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 | testAccess(filename) {
|
145 |
|
146 | var allows = (typeof this.options.accessControl.allow !== 'undefined');
|
147 | var list = this.options.accessControl.allow || this.options.accessControl.deny;
|
148 |
|
149 | for (var idx in list) {
|
150 | var item = list[idx];
|
151 |
|
152 |
|
153 | if (typeof item === 'string') {
|
154 | item = new RegExp(utils.escapeRegExp(item) + '$', 'i');
|
155 | }
|
156 |
|
157 |
|
158 | if (item.test(filename)) {
|
159 | return allows;
|
160 | }
|
161 | }
|
162 |
|
163 | return !allows;
|
164 |
|
165 | }
|
166 |
|
167 | |
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 | transform(transform, options) {
|
174 |
|
175 | if (!transform || !utils.isKindOf(transform, Transform)) {
|
176 | throw new TypeError('transform is required and must be an instance of Transform');
|
177 | }
|
178 |
|
179 | this.transforms.push(transform);
|
180 |
|
181 | }
|
182 |
|
183 | render(remoteFilename, ctx) {
|
184 | return new Promise((resolved, rejected) => {
|
185 |
|
186 | if (!remoteFilename || typeof remoteFilename !== 'string') {
|
187 | return rejected(TypeError('filename is required and must be a string'));
|
188 | }
|
189 |
|
190 | (new TransformChain(this.transforms, this.cacheChain))
|
191 | .render(utils.remoteToLocalFilename(remoteFilename, this.root, this.options.serveDir),
|
192 | remoteFilename,
|
193 | ctx)
|
194 | .then(resolved, rejected);
|
195 |
|
196 | });
|
197 | }
|
198 |
|
199 | route(req, res, next) {
|
200 |
|
201 |
|
202 | if (!next) {
|
203 | next = (err) => {
|
204 | res.statusCode = err ? (err.status || 500) : 404;
|
205 | res.end(err ? err.stack : 'not found');
|
206 | };
|
207 | }
|
208 |
|
209 |
|
210 | if (req.method != 'GET' && req.method != 'HEAD') return next();
|
211 |
|
212 |
|
213 | var remoteFilename = url.parse(req.url).pathname;
|
214 | var localFilename = utils.remoteToLocalFilename(remoteFilename, this.root, this.options.serveDir);
|
215 |
|
216 |
|
217 | if (!localFilename) return next();
|
218 |
|
219 |
|
220 | if (!this.testAccess(remoteFilename)) return next();
|
221 |
|
222 |
|
223 | if (!this.options.templates) {
|
224 | if (this.transforms.some((transform) => {
|
225 | return !transform.allowAccess(remoteFilename);
|
226 | })) {
|
227 | return next();
|
228 | }
|
229 | }
|
230 |
|
231 |
|
232 | if (!this.options.dotFiles) {
|
233 | var parts = localFilename.split(path.sep);
|
234 | for (var idx in parts) {
|
235 | if (parts[idx].substr(0, 1) == '.') {
|
236 | return next();
|
237 | }
|
238 | }
|
239 | }
|
240 |
|
241 |
|
242 | fs.exists(localFilename, (exists) => {
|
243 |
|
244 |
|
245 | if (!exists) {
|
246 |
|
247 | var sourceMapTester = /\.map$/i;
|
248 | var sourceMap = sourceMapTester.test(localFilename);
|
249 | if (sourceMap) {
|
250 | localFilename = localFilename.replace(sourceMapTester, '');
|
251 | remoteFilename = remoteFilename.replace(sourceMapTester, '');
|
252 | }
|
253 |
|
254 | return this.render(remoteFilename, req).then((result) => {
|
255 |
|
256 |
|
257 | if (!result) return next();
|
258 |
|
259 |
|
260 | if (sourceMap) {
|
261 | result.data = result.map ? new Buffer(JSON.stringify(result.map)) : null;
|
262 | result.filename += '.min';
|
263 | }
|
264 |
|
265 |
|
266 | if (!result.data) return next();
|
267 |
|
268 |
|
269 | res.setHeader('Content-Type', mime.lookup(result.filename) + '; charset=UTF-8');
|
270 |
|
271 |
|
272 | if (this.options.lastModified === true) {
|
273 | res.setHeader('Last-Modified', result.modificationDate.toUTCString());
|
274 | }
|
275 |
|
276 |
|
277 | if (this.options.etag === true) {
|
278 |
|
279 | var tag = etag(result.data);
|
280 | res.setHeader('Etag', tag);
|
281 |
|
282 |
|
283 | if (req.headers['if-none-match'] === tag) {
|
284 | res.statusCode = 304;
|
285 | return res.end();
|
286 | }
|
287 |
|
288 | }
|
289 |
|
290 |
|
291 | return res.end(result.data);
|
292 |
|
293 | }, next);
|
294 | }
|
295 |
|
296 |
|
297 | else {
|
298 |
|
299 |
|
300 | fs.stat(localFilename, (err, stats) => {
|
301 | if (err) return next(err);
|
302 |
|
303 |
|
304 | if (stats.isDirectory()) {
|
305 |
|
306 |
|
307 | var originalUrl = req.url;
|
308 | return async.eachSeries(this.options.index, (index, next) => {
|
309 |
|
310 |
|
311 | var urlObj = url.parse(originalUrl);
|
312 | urlObj.pathname = path.normalize(urlObj.pathname + '/' + index);
|
313 | req.url = url.format(urlObj);
|
314 |
|
315 |
|
316 | this.route(req, res, next);
|
317 |
|
318 | }, next);
|
319 |
|
320 | }
|
321 |
|
322 |
|
323 |
|
324 |
|
325 | send(req, localFilename, {
|
326 | dotfiles: 'allow',
|
327 | index: false,
|
328 | lastModified: true,
|
329 | etag: this.options.etag
|
330 | }).pipe(res);
|
331 |
|
332 | });
|
333 |
|
334 | }
|
335 |
|
336 | });
|
337 |
|
338 | }
|
339 |
|
340 | }
|
341 |
|
342 |
|
343 | exports = module.exports = Donot;
|