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