Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
DazaSeal
GitHub Repository: DazaSeal/Lunanom
Path: blob/master/node_modules/node-static/lib/node-static.js
334 views
1
var fs = require('fs')
2
, events = require('events')
3
, buffer = require('buffer')
4
, http = require('http')
5
, url = require('url')
6
, path = require('path')
7
, mime = require('mime')
8
, util = require('./node-static/util');
9
10
// Current version
11
var version = [0, 7, 9];
12
13
var Server = function (root, options) {
14
if (root && (typeof(root) === 'object')) { options = root; root = null }
15
16
// resolve() doesn't normalize (to lowercase) drive letters on Windows
17
this.root = path.normalize(path.resolve(root || '.'));
18
this.options = options || {};
19
this.cache = 3600;
20
21
this.defaultHeaders = {};
22
this.options.headers = this.options.headers || {};
23
24
this.options.indexFile = this.options.indexFile || "index.html";
25
26
if ('cache' in this.options) {
27
if (typeof(this.options.cache) === 'number') {
28
this.cache = this.options.cache;
29
} else if (! this.options.cache) {
30
this.cache = false;
31
}
32
}
33
34
if ('serverInfo' in this.options) {
35
this.serverInfo = this.options.serverInfo.toString();
36
} else {
37
this.serverInfo = 'node-static/' + version.join('.');
38
}
39
40
this.defaultHeaders['server'] = this.serverInfo;
41
42
if (this.cache !== false) {
43
this.defaultHeaders['cache-control'] = 'max-age=' + this.cache;
44
}
45
46
for (var k in this.defaultHeaders) {
47
this.options.headers[k] = this.options.headers[k] ||
48
this.defaultHeaders[k];
49
}
50
};
51
52
Server.prototype.serveDir = function (pathname, req, res, finish) {
53
var htmlIndex = path.join(pathname, this.options.indexFile),
54
that = this;
55
56
fs.stat(htmlIndex, function (e, stat) {
57
if (!e) {
58
var status = 200;
59
var headers = {};
60
var originalPathname = decodeURI(url.parse(req.url).pathname);
61
if (originalPathname.length && originalPathname.charAt(originalPathname.length - 1) !== '/') {
62
return finish(301, { 'Location': originalPathname + '/' });
63
} else {
64
that.respond(null, status, headers, [htmlIndex], stat, req, res, finish);
65
}
66
} else {
67
// Stream a directory of files as a single file.
68
fs.readFile(path.join(pathname, 'index.json'), function (e, contents) {
69
if (e) { return finish(404, {}) }
70
var index = JSON.parse(contents);
71
streamFiles(index.files);
72
});
73
}
74
});
75
function streamFiles(files) {
76
util.mstat(pathname, files, function (e, stat) {
77
if (e) { return finish(404, {}) }
78
that.respond(pathname, 200, {}, files, stat, req, res, finish);
79
});
80
}
81
};
82
83
Server.prototype.serveFile = function (pathname, status, headers, req, res) {
84
var that = this;
85
var promise = new(events.EventEmitter);
86
87
pathname = this.resolve(pathname);
88
89
fs.stat(pathname, function (e, stat) {
90
if (e) {
91
return promise.emit('error', e);
92
}
93
that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) {
94
that.finish(status, headers, req, res, promise);
95
});
96
});
97
return promise;
98
};
99
100
Server.prototype.finish = function (status, headers, req, res, promise, callback) {
101
var result = {
102
status: status,
103
headers: headers,
104
message: http.STATUS_CODES[status]
105
};
106
107
headers['server'] = this.serverInfo;
108
109
if (!status || status >= 400) {
110
if (callback) {
111
callback(result);
112
} else {
113
if (promise.listeners('error').length > 0) {
114
promise.emit('error', result);
115
}
116
else {
117
res.writeHead(status, headers);
118
res.end();
119
}
120
}
121
} else {
122
// Don't end the request here, if we're streaming;
123
// it's taken care of in `prototype.stream`.
124
if (status !== 200 || req.method !== 'GET') {
125
res.writeHead(status, headers);
126
res.end();
127
}
128
callback && callback(null, result);
129
promise.emit('success', result);
130
}
131
};
132
133
Server.prototype.servePath = function (pathname, status, headers, req, res, finish) {
134
var that = this,
135
promise = new(events.EventEmitter);
136
137
pathname = this.resolve(pathname);
138
139
// Make sure we're not trying to access a
140
// file outside of the root.
141
if (pathname.indexOf(that.root) === 0) {
142
fs.stat(pathname, function (e, stat) {
143
if (e) {
144
finish(404, {});
145
} else if (stat.isFile()) { // Stream a single file.
146
that.respond(null, status, headers, [pathname], stat, req, res, finish);
147
} else if (stat.isDirectory()) { // Stream a directory of files.
148
that.serveDir(pathname, req, res, finish);
149
} else {
150
finish(400, {});
151
}
152
});
153
} else {
154
// Forbidden
155
finish(403, {});
156
}
157
return promise;
158
};
159
160
Server.prototype.resolve = function (pathname) {
161
return path.resolve(path.join(this.root, pathname));
162
};
163
164
Server.prototype.serve = function (req, res, callback) {
165
var that = this,
166
promise = new(events.EventEmitter),
167
pathname;
168
169
var finish = function (status, headers) {
170
that.finish(status, headers, req, res, promise, callback);
171
};
172
173
try {
174
pathname = decodeURI(url.parse(req.url).pathname);
175
}
176
catch(e) {
177
return process.nextTick(function() {
178
return finish(400, {});
179
});
180
}
181
182
process.nextTick(function () {
183
that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) {
184
promise.emit('success', result);
185
}).on('error', function (err) {
186
promise.emit('error');
187
});
188
});
189
if (! callback) { return promise }
190
};
191
192
/* Check if we should consider sending a gzip version of the file based on the
193
* file content type and client's Accept-Encoding header value.
194
*/
195
Server.prototype.gzipOk = function (req, contentType) {
196
var enable = this.options.gzip;
197
if(enable &&
198
(typeof enable === 'boolean' ||
199
(contentType && (enable instanceof RegExp) && enable.test(contentType)))) {
200
var acceptEncoding = req.headers['accept-encoding'];
201
return acceptEncoding && acceptEncoding.indexOf("gzip") >= 0;
202
}
203
return false;
204
}
205
206
/* Send a gzipped version of the file if the options and the client indicate gzip is enabled and
207
* we find a .gz file mathing the static resource requested.
208
*/
209
Server.prototype.respondGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) {
210
var that = this;
211
if (files.length == 1 && this.gzipOk(req, contentType)) {
212
var gzFile = files[0] + ".gz";
213
fs.stat(gzFile, function (e, gzStat) {
214
if (!e && gzStat.isFile()) {
215
var vary = _headers['Vary'];
216
_headers['Vary'] = (vary && vary != 'Accept-Encoding' ? vary + ', ' : '') + 'Accept-Encoding';
217
_headers['Content-Encoding'] = 'gzip';
218
stat.size = gzStat.size;
219
files = [gzFile];
220
}
221
that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
222
});
223
} else {
224
// Client doesn't want gzip or we're sending multiple files
225
that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
226
}
227
}
228
229
Server.prototype.parseByteRange = function (req, stat) {
230
var byteRange = {
231
from: 0,
232
to: 0,
233
valid: false
234
}
235
236
var rangeHeader = req.headers['range'];
237
var flavor = 'bytes=';
238
239
if (rangeHeader) {
240
if (rangeHeader.indexOf(flavor) == 0 && rangeHeader.indexOf(',') == -1) {
241
/* Parse */
242
rangeHeader = rangeHeader.substr(flavor.length).split('-');
243
byteRange.from = parseInt(rangeHeader[0]);
244
byteRange.to = parseInt(rangeHeader[1]);
245
246
/* Replace empty fields of differential requests by absolute values */
247
if (isNaN(byteRange.from) && !isNaN(byteRange.to)) {
248
byteRange.from = stat.size - byteRange.to;
249
byteRange.to = stat.size ? stat.size - 1 : 0;
250
} else if (!isNaN(byteRange.from) && isNaN(byteRange.to)) {
251
byteRange.to = stat.size ? stat.size - 1 : 0;
252
}
253
254
/* General byte range validation */
255
if (!isNaN(byteRange.from) && !!byteRange.to && 0 <= byteRange.from && byteRange.from < byteRange.to) {
256
byteRange.valid = true;
257
} else {
258
console.warn("Request contains invalid range header: ", rangeHeader);
259
}
260
} else {
261
console.warn("Request contains unsupported range header: ", rangeHeader);
262
}
263
}
264
return byteRange;
265
}
266
267
Server.prototype.respondNoGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) {
268
var mtime = Date.parse(stat.mtime),
269
key = pathname || files[0],
270
headers = {},
271
clientETag = req.headers['if-none-match'],
272
clientMTime = Date.parse(req.headers['if-modified-since']),
273
startByte = 0,
274
length = stat.size,
275
byteRange = this.parseByteRange(req, stat);
276
277
/* Handle byte ranges */
278
if (files.length == 1 && byteRange.valid) {
279
if (byteRange.to < length) {
280
281
// Note: HTTP Range param is inclusive
282
startByte = byteRange.from;
283
length = byteRange.to - byteRange.from + 1;
284
status = 206;
285
286
// Set Content-Range response header (we advertise initial resource size on server here (stat.size))
287
headers['Content-Range'] = 'bytes ' + byteRange.from + '-' + byteRange.to + '/' + stat.size;
288
289
} else {
290
byteRange.valid = false;
291
console.warn("Range request exceeds file boundaries, goes until byte no", byteRange.to, "against file size of", length, "bytes");
292
}
293
}
294
295
/* In any case, check for unhandled byte range headers */
296
if (!byteRange.valid && req.headers['range']) {
297
console.error(new Error("Range request present but invalid, might serve whole file instead"));
298
}
299
300
// Copy default headers
301
for (var k in this.options.headers) { headers[k] = this.options.headers[k] }
302
// Copy custom headers
303
for (var k in _headers) { headers[k] = _headers[k] }
304
305
headers['Etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-'));
306
headers['Date'] = new(Date)().toUTCString();
307
headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString();
308
headers['Content-Type'] = contentType;
309
headers['Content-Length'] = length;
310
311
for (var k in _headers) { headers[k] = _headers[k] }
312
313
// Conditional GET
314
// If the "If-Modified-Since" or "If-None-Match" headers
315
// match the conditions, send a 304 Not Modified.
316
if ((clientMTime || clientETag) &&
317
(!clientETag || clientETag === headers['Etag']) &&
318
(!clientMTime || clientMTime >= mtime)) {
319
// 304 response should not contain entity headers
320
['Content-Encoding',
321
'Content-Language',
322
'Content-Length',
323
'Content-Location',
324
'Content-MD5',
325
'Content-Range',
326
'Content-Type',
327
'Expires',
328
'Last-Modified'].forEach(function (entityHeader) {
329
delete headers[entityHeader];
330
});
331
finish(304, headers);
332
} else {
333
res.writeHead(status, headers);
334
335
this.stream(key, files, length, startByte, res, function (e) {
336
if (e) { return finish(500, {}) }
337
finish(status, headers);
338
});
339
}
340
};
341
342
Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) {
343
var contentType = _headers['Content-Type'] ||
344
mime.lookup(files[0]) ||
345
'application/octet-stream';
346
347
if(this.options.gzip) {
348
this.respondGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
349
} else {
350
this.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
351
}
352
}
353
354
Server.prototype.stream = function (pathname, files, length, startByte, res, callback) {
355
356
(function streamFile(files, offset) {
357
var file = files.shift();
358
359
if (file) {
360
file = path.resolve(file) === path.normalize(file) ? file : path.join(pathname || '.', file);
361
362
// Stream the file to the client
363
fs.createReadStream(file, {
364
flags: 'r',
365
mode: 0666,
366
start: startByte,
367
end: startByte + (length ? length - 1 : 0)
368
}).on('data', function (chunk) {
369
// Bounds check the incoming chunk and offset, as copying
370
// a buffer from an invalid offset will throw an error and crash
371
if (chunk.length && offset < length && offset >= 0) {
372
offset += chunk.length;
373
}
374
}).on('close', function () {
375
streamFile(files, offset);
376
}).on('error', function (err) {
377
callback(err);
378
console.error(err);
379
}).pipe(res, { end: false });
380
} else {
381
res.end();
382
callback(null, offset);
383
}
384
})(files.slice(0), 0);
385
};
386
387
// Exports
388
exports.Server = Server;
389
exports.version = version;
390
exports.mime = mime;
391
392
393
394
395