X-Git-Url: https://git.josue.xyz/?a=blobdiff_plain;f=node_modules%2Fnode-static%2Flib%2Fnode-static.js;fp=node_modules%2Fnode-static%2Flib%2Fnode-static.js;h=15d814420beb9bfd8d29129a9e3110b945f9522b;hb=0f4e331e6d75c244e978860b62a6e1aed8d446e0;hp=0000000000000000000000000000000000000000;hpb=0cb383f1c0646575a831f4f812cd85c9e24d9a18;p=VSoRC%2F.git diff --git a/node_modules/node-static/lib/node-static.js b/node_modules/node-static/lib/node-static.js new file mode 100644 index 0000000..15d8144 --- /dev/null +++ b/node_modules/node-static/lib/node-static.js @@ -0,0 +1,393 @@ +var fs = require('fs') + , events = require('events') + , buffer = require('buffer') + , http = require('http') + , url = require('url') + , path = require('path') + , mime = require('mime') + , util = require('./node-static/util'); + +// Current version +var version = [0, 7, 9]; + +var Server = function (root, options) { + if (root && (typeof(root) === 'object')) { options = root; root = null } + + // resolve() doesn't normalize (to lowercase) drive letters on Windows + this.root = path.normalize(path.resolve(root || '.')); + this.options = options || {}; + this.cache = 3600; + + this.defaultHeaders = {}; + this.options.headers = this.options.headers || {}; + + this.options.indexFile = this.options.indexFile || "index.html"; + + if ('cache' in this.options) { + if (typeof(this.options.cache) === 'number') { + this.cache = this.options.cache; + } else if (! this.options.cache) { + this.cache = false; + } + } + + if ('serverInfo' in this.options) { + this.serverInfo = this.options.serverInfo.toString(); + } else { + this.serverInfo = 'node-static/' + version.join('.'); + } + + this.defaultHeaders['server'] = this.serverInfo; + + if (this.cache !== false) { + this.defaultHeaders['cache-control'] = 'max-age=' + this.cache; + } + + for (var k in this.defaultHeaders) { + this.options.headers[k] = this.options.headers[k] || + this.defaultHeaders[k]; + } +}; + +Server.prototype.serveDir = function (pathname, req, res, finish) { + var htmlIndex = path.join(pathname, this.options.indexFile), + that = this; + + fs.stat(htmlIndex, function (e, stat) { + if (!e) { + var status = 200; + var headers = {}; + var originalPathname = decodeURI(url.parse(req.url).pathname); + if (originalPathname.length && originalPathname.charAt(originalPathname.length - 1) !== '/') { + return finish(301, { 'Location': originalPathname + '/' }); + } else { + that.respond(null, status, headers, [htmlIndex], stat, req, res, finish); + } + } else { + // Stream a directory of files as a single file. + fs.readFile(path.join(pathname, 'index.json'), function (e, contents) { + if (e) { return finish(404, {}) } + var index = JSON.parse(contents); + streamFiles(index.files); + }); + } + }); + function streamFiles(files) { + util.mstat(pathname, files, function (e, stat) { + if (e) { return finish(404, {}) } + that.respond(pathname, 200, {}, files, stat, req, res, finish); + }); + } +}; + +Server.prototype.serveFile = function (pathname, status, headers, req, res) { + var that = this; + var promise = new(events.EventEmitter); + + pathname = this.resolve(pathname); + + fs.stat(pathname, function (e, stat) { + if (e) { + return promise.emit('error', e); + } + that.respond(null, status, headers, [pathname], stat, req, res, function (status, headers) { + that.finish(status, headers, req, res, promise); + }); + }); + return promise; +}; + +Server.prototype.finish = function (status, headers, req, res, promise, callback) { + var result = { + status: status, + headers: headers, + message: http.STATUS_CODES[status] + }; + + headers['server'] = this.serverInfo; + + if (!status || status >= 400) { + if (callback) { + callback(result); + } else { + if (promise.listeners('error').length > 0) { + promise.emit('error', result); + } + else { + res.writeHead(status, headers); + res.end(); + } + } + } else { + // Don't end the request here, if we're streaming; + // it's taken care of in `prototype.stream`. + if (status !== 200 || req.method !== 'GET') { + res.writeHead(status, headers); + res.end(); + } + callback && callback(null, result); + promise.emit('success', result); + } +}; + +Server.prototype.servePath = function (pathname, status, headers, req, res, finish) { + var that = this, + promise = new(events.EventEmitter); + + pathname = this.resolve(pathname); + + // Make sure we're not trying to access a + // file outside of the root. + if (pathname.indexOf(that.root) === 0) { + fs.stat(pathname, function (e, stat) { + if (e) { + finish(404, {}); + } else if (stat.isFile()) { // Stream a single file. + that.respond(null, status, headers, [pathname], stat, req, res, finish); + } else if (stat.isDirectory()) { // Stream a directory of files. + that.serveDir(pathname, req, res, finish); + } else { + finish(400, {}); + } + }); + } else { + // Forbidden + finish(403, {}); + } + return promise; +}; + +Server.prototype.resolve = function (pathname) { + return path.resolve(path.join(this.root, pathname)); +}; + +Server.prototype.serve = function (req, res, callback) { + var that = this, + promise = new(events.EventEmitter), + pathname; + + var finish = function (status, headers) { + that.finish(status, headers, req, res, promise, callback); + }; + + try { + pathname = decodeURI(url.parse(req.url).pathname); + } + catch(e) { + return process.nextTick(function() { + return finish(400, {}); + }); + } + + process.nextTick(function () { + that.servePath(pathname, 200, {}, req, res, finish).on('success', function (result) { + promise.emit('success', result); + }).on('error', function (err) { + promise.emit('error'); + }); + }); + if (! callback) { return promise } +}; + +/* Check if we should consider sending a gzip version of the file based on the + * file content type and client's Accept-Encoding header value. + */ +Server.prototype.gzipOk = function (req, contentType) { + var enable = this.options.gzip; + if(enable && + (typeof enable === 'boolean' || + (contentType && (enable instanceof RegExp) && enable.test(contentType)))) { + var acceptEncoding = req.headers['accept-encoding']; + return acceptEncoding && acceptEncoding.indexOf("gzip") >= 0; + } + return false; +} + +/* Send a gzipped version of the file if the options and the client indicate gzip is enabled and + * we find a .gz file mathing the static resource requested. + */ +Server.prototype.respondGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) { + var that = this; + if (files.length == 1 && this.gzipOk(req, contentType)) { + var gzFile = files[0] + ".gz"; + fs.stat(gzFile, function (e, gzStat) { + if (!e && gzStat.isFile()) { + var vary = _headers['Vary']; + _headers['Vary'] = (vary && vary != 'Accept-Encoding' ? vary + ', ' : '') + 'Accept-Encoding'; + _headers['Content-Encoding'] = 'gzip'; + stat.size = gzStat.size; + files = [gzFile]; + } + that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); + }); + } else { + // Client doesn't want gzip or we're sending multiple files + that.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); + } +} + +Server.prototype.parseByteRange = function (req, stat) { + var byteRange = { + from: 0, + to: 0, + valid: false + } + + var rangeHeader = req.headers['range']; + var flavor = 'bytes='; + + if (rangeHeader) { + if (rangeHeader.indexOf(flavor) == 0 && rangeHeader.indexOf(',') == -1) { + /* Parse */ + rangeHeader = rangeHeader.substr(flavor.length).split('-'); + byteRange.from = parseInt(rangeHeader[0]); + byteRange.to = parseInt(rangeHeader[1]); + + /* Replace empty fields of differential requests by absolute values */ + if (isNaN(byteRange.from) && !isNaN(byteRange.to)) { + byteRange.from = stat.size - byteRange.to; + byteRange.to = stat.size ? stat.size - 1 : 0; + } else if (!isNaN(byteRange.from) && isNaN(byteRange.to)) { + byteRange.to = stat.size ? stat.size - 1 : 0; + } + + /* General byte range validation */ + if (!isNaN(byteRange.from) && !!byteRange.to && 0 <= byteRange.from && byteRange.from < byteRange.to) { + byteRange.valid = true; + } else { + console.warn("Request contains invalid range header: ", rangeHeader); + } + } else { + console.warn("Request contains unsupported range header: ", rangeHeader); + } + } + return byteRange; +} + +Server.prototype.respondNoGzip = function (pathname, status, contentType, _headers, files, stat, req, res, finish) { + var mtime = Date.parse(stat.mtime), + key = pathname || files[0], + headers = {}, + clientETag = req.headers['if-none-match'], + clientMTime = Date.parse(req.headers['if-modified-since']), + startByte = 0, + length = stat.size, + byteRange = this.parseByteRange(req, stat); + + /* Handle byte ranges */ + if (files.length == 1 && byteRange.valid) { + if (byteRange.to < length) { + + // Note: HTTP Range param is inclusive + startByte = byteRange.from; + length = byteRange.to - byteRange.from + 1; + status = 206; + + // Set Content-Range response header (we advertise initial resource size on server here (stat.size)) + headers['Content-Range'] = 'bytes ' + byteRange.from + '-' + byteRange.to + '/' + stat.size; + + } else { + byteRange.valid = false; + console.warn("Range request exceeds file boundaries, goes until byte no", byteRange.to, "against file size of", length, "bytes"); + } + } + + /* In any case, check for unhandled byte range headers */ + if (!byteRange.valid && req.headers['range']) { + console.error(new Error("Range request present but invalid, might serve whole file instead")); + } + + // Copy default headers + for (var k in this.options.headers) { headers[k] = this.options.headers[k] } + // Copy custom headers + for (var k in _headers) { headers[k] = _headers[k] } + + headers['Etag'] = JSON.stringify([stat.ino, stat.size, mtime].join('-')); + headers['Date'] = new(Date)().toUTCString(); + headers['Last-Modified'] = new(Date)(stat.mtime).toUTCString(); + headers['Content-Type'] = contentType; + headers['Content-Length'] = length; + + for (var k in _headers) { headers[k] = _headers[k] } + + // Conditional GET + // If the "If-Modified-Since" or "If-None-Match" headers + // match the conditions, send a 304 Not Modified. + if ((clientMTime || clientETag) && + (!clientETag || clientETag === headers['Etag']) && + (!clientMTime || clientMTime >= mtime)) { + // 304 response should not contain entity headers + ['Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-Location', + 'Content-MD5', + 'Content-Range', + 'Content-Type', + 'Expires', + 'Last-Modified'].forEach(function (entityHeader) { + delete headers[entityHeader]; + }); + finish(304, headers); + } else { + res.writeHead(status, headers); + + this.stream(key, files, length, startByte, res, function (e) { + if (e) { return finish(500, {}) } + finish(status, headers); + }); + } +}; + +Server.prototype.respond = function (pathname, status, _headers, files, stat, req, res, finish) { + var contentType = _headers['Content-Type'] || + mime.lookup(files[0]) || + 'application/octet-stream'; + + if(this.options.gzip) { + this.respondGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); + } else { + this.respondNoGzip(pathname, status, contentType, _headers, files, stat, req, res, finish); + } +} + +Server.prototype.stream = function (pathname, files, length, startByte, res, callback) { + + (function streamFile(files, offset) { + var file = files.shift(); + + if (file) { + file = path.resolve(file) === path.normalize(file) ? file : path.join(pathname || '.', file); + + // Stream the file to the client + fs.createReadStream(file, { + flags: 'r', + mode: 0666, + start: startByte, + end: startByte + (length ? length - 1 : 0) + }).on('data', function (chunk) { + // Bounds check the incoming chunk and offset, as copying + // a buffer from an invalid offset will throw an error and crash + if (chunk.length && offset < length && offset >= 0) { + offset += chunk.length; + } + }).on('close', function () { + streamFile(files, offset); + }).on('error', function (err) { + callback(err); + console.error(err); + }).pipe(res, { end: false }); + } else { + res.end(); + callback(null, offset); + } + })(files.slice(0), 0); +}; + +// Exports +exports.Server = Server; +exports.version = version; +exports.mime = mime; + + +