UNPKG

11.2 kBJavaScriptView Raw
1var 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
11var version = [0, 7, 1];
12
13Server = function (root, options) {
14 if (root && (typeof(root) === 'object')) { options = root; root = null }
15
16 this.root = path.resolve(root || '.');
17 this.options = options || {};
18 this.cache = 3600;
19
20 this.defaultHeaders = {};
21 this.options.headers = this.options.headers || {};
22
23 if ('cache' in this.options) {
24 if (typeof(this.options.cache) === 'number') {
25 this.cache = this.options.cache;
26 } else if (! this.options.cache) {
27 this.cache = false;
28 }
29 }
30
31 if ('serverInfo' in this.options) {
32 this.serverInfo = this.options.serverInfo.toString();
33 } else {
34 this.serverInfo = 'node-static/' + version.join('.');
35 }
36
37 this.defaultHeaders['server'] = this.serverInfo;
38
39 if (this.cache !== false) {
40 this.defaultHeaders['cache-control'] = 'max-age=' + this.cache;
41 }
42
43 for (var k in this.defaultHeaders) {
44 this.options.headers[k] = this.options.headers[k] ||
45 this.defaultHeaders[k];
46 }
47};
48
49Server.prototype.serveDir = function (pathname, req, res, finish) {
50 var htmlIndex = path.join(pathname, 'index.html'),
51 that = this;
52
53 fs.stat(htmlIndex, function (e, stat) {
54 if (!e) {
55 var status = 200;
56 var headers = {};
57 var originalPathname = decodeURI(url.parse(req.url).pathname);
58 if (originalPathname.length && originalPathname.charAt(originalPathname.length - 1) !== '/') {
59 return finish(301, { 'Location': originalPathname + '/' });
60 } else {
61 that.respond(null, status, headers, [htmlIndex], stat, req, res, finish);
62 }
63 } else {
64 // Stream a directory of files as a single file.
65 fs.readFile(path.join(pathname, 'index.json'), function (e, contents) {
66 if (e) { return finish(404, {}) }
67 var index = JSON.parse(contents);
68 streamFiles(index.files);
69 });
70 }
71 });
72 function streamFiles(files) {
73 util.mstat(pathname, files, function (e, stat) {
74 if (e) { return finish(404, {}) }
75 that.respond(pathname, 200, {}, files, stat, req, res, finish);
76 });
77 }
78};
79
80Server.prototype.serveFile = function (pathname, status, headers, req, res) {
81 var that = this;
82 var promise = new(events.EventEmitter);
83
84 pathname = this.resolve(pathname);
85
86 fs.stat(pathname, function (e, stat) {
87 if (e) {
88 return promise.emit('error', e);
89 }
90 that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) {
91 that.finish(status, headers, req, res, promise);
92 });
93 });
94 return promise;
95};
96
97Server.prototype.finish = function (status, headers, req, res, promise, callback) {
98 var result = {
99 status: status,
100 headers: headers,
101 message: http.STATUS_CODES[status]
102 };
103
104 headers['server'] = this.serverInfo;
105
106 if (!status || status >= 400) {
107 if (callback) {
108 callback(result);
109 } else {
110 if (promise.listeners('error').length > 0) {
111 promise.emit('error', result);
112 }
113 else {
114 res.writeHead(status, headers);
115 res.end();
116 }
117 }
118 } else {
119 // Don't end the request here, if we're streaming;
120 // it's taken care of in `prototype.stream`.
121 if (status !== 200 || req.method !== 'GET') {
122 res.writeHead(status, headers);
123 res.end();
124 }
125 callback && callback(null, result);
126 promise.emit('success', result);
127 }
128};
129
130Server.prototype.servePath = function (pathname, status, headers, req, res, finish) {
131 var that = this,
132 promise = new(events.EventEmitter);
133
134 pathname = this.resolve(pathname);
135
136 // Only allow GET and HEAD requests
137 if (req.method !== 'GET' && req.method !== 'HEAD') {
138 finish(405, { 'Allow': 'GET, HEAD' });
139 return promise;
140 }
141
142 // Make sure we're not trying to access a
143 // file outside of the root.
144 if (pathname.indexOf(that.root) === 0) {
145 fs.stat(pathname, function (e, stat) {
146 if (e) {
147 finish(404, {});
148 } else if (stat.isFile()) { // Stream a single file.
149 that.respond(null, status, headers, [pathname], stat, req, res, finish);
150 } else if (stat.isDirectory()) { // Stream a directory of files.
151 that.serveDir(pathname, req, res, finish);
152 } else {
153 finish(400, {});
154 }
155 });
156 } else {
157 // Forbidden
158 finish(403, {});
159 }
160 return promise;
161};
162
163Server.prototype.resolve = function (pathname) {
164 return path.resolve(path.join(this.root, pathname));
165};
166
167Server.prototype.serve = function (req, res, callback) {
168 var that = this,
169 promise = new(events.EventEmitter),
170 pathname;
171
172 var finish = function (status, headers) {
173 that.finish(status, headers, req, res, promise, callback);
174 };
175
176 try {
177 pathname = decodeURI(url.parse(req.url).pathname);
178 }
179 catch(e) {
180 return process.nextTick(function() {
181 return finish(400, {});
182 });
183 }
184
185 process.nextTick(function () {
186 that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) {
187 promise.emit('success', result);
188 }).on('error', function (err) {
189 promise.emit('error');
190 });
191 });
192 if (! callback) { return promise }
193};
194
195/* Check if we should consider sending a gzip version of the file based on the
196 * file content type and client's Accept-Encoding header value.
197 */
198Server.prototype.gzipOk = function(req, contentType) {
199 var enable = this.options.gzip;
200 if(enable &&
201 (typeof enable === 'boolean' ||
202 (contentType && (enable instanceof RegExp) && enable.test(contentType)))) {
203 var acceptEncoding = req.headers['accept-encoding'];
204 return acceptEncoding && acceptEncoding.indexOf("gzip") >= 0;
205 }
206 return false;
207}
208
209/* Send a gzipped version of the file if the options and the client indicate gzip is enabled and
210 * we find a .gz file mathing the static resource requested.
211 */
212Server.prototype.respondGzip = function(pathname, status, contentType, _headers, files, stat, req, res, finish) {
213 var that = this;
214 if(files.length == 1 && this.gzipOk(req, contentType)) {
215 var gzFile = files[0] + ".gz";
216 fs.stat(gzFile, function(e, gzStat) {
217 if(!e && gzStat.isFile()) {
218 //console.log('Serving', gzFile, 'to gzip-capable client instead of', files[0], 'new size is', gzStat.size, 'uncompressed size', stat.size);
219 var vary = _headers['Vary'];
220 _headers['Vary'] = (vary && vary != 'Accept-Encoding'?vary+', ':'')+'Accept-Encoding';
221 _headers['Content-Encoding'] = 'gzip';
222 stat.size = gzStat.size;
223 files = [gzFile];
224 } else {
225 //console.log('gzip file not found or error finding it', gzFile, String(e), stat.isFile());
226 }
227 that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
228 });
229 } else {
230 // Client doesn't want gzip or we're sending multiple files
231 that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
232 }
233}
234
235Server.prototype.respondNoGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) {
236 var mtime = Date.parse(stat.mtime),
237 key = pathname || files[0],
238 headers = {},
239 clientETag = req.headers['if-none-match'],
240 clientMTime = Date.parse(req.headers['if-modified-since']);
241
242
243 // Copy default headers
244 for (var k in this.options.headers) { headers[k] = this.options.headers[k] }
245 // Copy custom headers
246 for (var k in _headers) { headers[k] = _headers[k] }
247
248 headers['Etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-'));
249 headers['Date'] = new(Date)().toUTCString();
250 headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString();
251 headers['Content-Type'] = contentType;
252 headers['Content-Length'] = stat.size;
253
254 for (var k in _headers) { headers[k] = _headers[k] }
255
256 // Conditional GET
257 // If the "If-Modified-Since" or "If-None-Match" headers
258 // match the conditions, send a 304 Not Modified.
259 if ((clientMTime || clientETag) &&
260 (!clientETag || clientETag === headers['Etag']) &&
261 (!clientMTime || clientMTime >= mtime)) {
262 // 304 response should not contain entity headers
263 ['Content-Encoding',
264 'Content-Language',
265 'Content-Length',
266 'Content-Location',
267 'Content-MD5',
268 'Content-Range',
269 'Content-Type',
270 'Expires',
271 'Last-Modified'].forEach(function(entityHeader) {
272 delete headers[entityHeader];
273 });
274 finish(304, headers);
275 } else {
276 res.writeHead(status, headers);
277
278 this.stream(pathname, files, new(buffer.Buffer)(stat.size), res, function (e, buffer) {
279 if (e) { return finish(500, {}) }
280 finish(status, headers);
281 });
282 }
283};
284
285Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) {
286 var contentType = _headers['Content-Type'] ||
287 mime.lookup(files[0]) ||
288 'application/octet-stream';
289 if(this.options.gzip) {
290 this.respondGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
291 } else {
292 this.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
293 }
294}
295
296Server.prototype.stream = function (pathname, files, buffer, res, callback) {
297 (function streamFile(files, offset) {
298 var file = files.shift();
299
300 if (file) {
301 file = file[0] === '/' ? file : path.join(pathname || '.', file);
302
303 // Stream the file to the client
304 fs.createReadStream(file, {
305 flags: 'r',
306 mode: 0666
307 }).on('data', function (chunk) {
308 chunk.copy(buffer, offset);
309 offset += chunk.length;
310 }).on('close', function () {
311 streamFile(files, offset);
312 }).on('error', function (err) {
313 callback(err);
314 console.error(err);
315 }).pipe(res, { end: false });
316 } else {
317 res.end();
318 callback(null, buffer, offset);
319 }
320 })(files.slice(0), 0);
321};
322
323// Exports
324exports.Server = Server;
325exports.version = version;
326exports.mime = mime;
327
328
329