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 |
|
11 | var version = [0, 7, 1];
|
12 |
|
13 | Server = 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 |
|
49 | Server.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 |
|
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 |
|
80 | Server.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 |
|
97 | Server.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 |
|
120 |
|
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 |
|
130 | Server.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 |
|
137 | if (req.method !== 'GET' && req.method !== 'HEAD') {
|
138 | finish(405, { 'Allow': 'GET, HEAD' });
|
139 | return promise;
|
140 | }
|
141 |
|
142 |
|
143 |
|
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()) {
|
149 | that.respond(null, status, headers, [pathname], stat, req, res, finish);
|
150 | } else if (stat.isDirectory()) {
|
151 | that.serveDir(pathname, req, res, finish);
|
152 | } else {
|
153 | finish(400, {});
|
154 | }
|
155 | });
|
156 | } else {
|
157 |
|
158 | finish(403, {});
|
159 | }
|
160 | return promise;
|
161 | };
|
162 |
|
163 | Server.prototype.resolve = function (pathname) {
|
164 | return path.resolve(path.join(this.root, pathname));
|
165 | };
|
166 |
|
167 | Server.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 |
|
196 |
|
197 |
|
198 | Server.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 |
|
210 |
|
211 |
|
212 | Server.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 |
|
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 |
|
226 | }
|
227 | that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
|
228 | });
|
229 | } else {
|
230 |
|
231 | that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish);
|
232 | }
|
233 | }
|
234 |
|
235 | Server.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 |
|
244 | for (var k in this.options.headers) { headers[k] = this.options.headers[k] }
|
245 |
|
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 |
|
257 |
|
258 |
|
259 | if ((clientMTime || clientETag) &&
|
260 | (!clientETag || clientETag === headers['Etag']) &&
|
261 | (!clientMTime || clientMTime >= mtime)) {
|
262 |
|
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 |
|
285 | Server.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 |
|
296 | Server.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 |
|
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 |
|
324 | exports.Server = Server;
|
325 | exports.version = version;
|
326 | exports.mime = mime;
|
327 |
|
328 |
|
329 |
|