nxkit
Version:
This is a collection of tools, independent of any other libraries
489 lines (488 loc) • 17.1 kB
JavaScript
;
/* ***** BEGIN LICENSE BLOCK *****
* Distributed under the BSD license:
*
* Copyright (c) 2015, xuewen.chu
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of xuewen.chu nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL xuewen.chu BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* ***** END LICENSE BLOCK ***** */
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("./util");
const fs = require("./fs");
const fs2 = require("./fs2");
const service_1 = require("./service");
const http = require("http");
const zlib = require("zlib");
var g_static_cache = {};
//set util
function setHeader(self, expires) {
var res = self.response;
res.setHeader('Server', 'Ngui utils');
res.setHeader('Date', new Date().toUTCString());
if (self.request.method == 'GET') {
expires = expires === undefined ? self.server.expires : expires;
if (expires) {
if (!self.m_no_cache /*!res.headers['Cache-Control'] && !res.headers['Expires']*/) {
// console.log(new Date().addMs(6e4 * expires).toUTCString());
res.setHeader('Expires', new Date().add(6e4 * expires).toUTCString());
res.setHeader('Cache-Control', 'public, max-age=' + (expires * 60));
}
}
}
res.setHeader('Access-Control-Allow-Origin', self.server.allowOrigin);
}
function getContentType(self, baseType) {
if (/javascript|text|json|xml/i.test(baseType)) {
return baseType + '; charset=' + self.server.textEncoding;
}
return baseType;
}
// 文件是否可支持gzip压缩
function isGzip(self, filename) {
if (!self.server.gzip) {
return false;
}
var ae = self.request.headers['accept-encoding'];
var type = self.server.getMime(filename);
return !!(ae && ae.match(/gzip/i) && type.match(self.server.gzip));
}
//返回目录
function tryReturnDirectory(self, filename) {
//读取目录
if (!filename.match(/\/$/)) // 目录不正确,重定向
return returnRedirect(self, self.pathname + '/');
//返回目录
function result(self, filename) {
if (self.server.autoIndex) {
return returnDirectory(self, filename);
}
else {
return returnErrorStatus(self, 403);
}
}
var def = self.server.defaults;
if (!def.length) { //默认页
return result(self, filename);
}
fs.readdir(filename, function (err, files) {
if (err) {
console.log(err);
return returnErrorStatus(self, 404);
}
for (var i = 0, name; (name = def[i]); i++) {
if (files.indexOf(name) != -1)
return returnFile(self, filename.replace(/\/?$/, '/') + name);
}
result(self, filename);
});
}
function returnRedirect(self, path) {
self.response.setHeader('Location', path);
self.response.writeHead(302);
self.response.end();
}
function returnDirectory(self, filename) {
var res = self.response;
var req = self.request;
//读取目录
if (!filename.match(/\/$/)) { //目录不正确,重定向
return returnRedirect(self, self.pathname + '/');
}
fs.list(filename).then(function (files) {
var dir = filename.replace(self._root[0], '');
var html = String.format('<!DOCTYPE html><html><head><title>Index of {0}</title>', dir) +
'<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' +
'<style type="text/css">*{font-family:Courier New}div,span{line-height:20px;height:20px;}\
span{display:block;float:right;width:220px}</style>' +
'</head><body bgcolor="white">' +
String.format('<h1>Index of {0}</h1><hr/><pre><div><a href="{1}">../</a></div>', dir, dir ? '../' : 'javascript:');
var ls1 = [];
var ls2 = [];
for (var stat of files) {
var name = stat.name;
if (name.slice(0, 1) == '.') {
continue;
}
var link = name;
var size = (stat.size / 1024).toFixed(2) + ' KB';
var isdir = stat.isDirectory();
if (isdir) {
link += '/';
size = '-';
}
var s = String.format('<div><a href="{0}">{0}</a><span>{2}</span><span>{1}</span></div>', link, stat.ctime.toString('yyyy-MM-dd hh:mm:ss'), size);
isdir ? ls1.push(s) : ls2.push(s);
}
html += ls1.join('') + ls2.join('') + '</pre><hr/></body></html>';
setHeader(self);
// var type = self.server.getMime('html');
res.writeHead(200);
res.end(html);
}).catch(function () {
returnErrorStatus(self, 404);
});
}
//返回缓存
function return_cache(self, filename) {
var cache = g_static_cache[filename];
if (cache && cache.data) {
var req = self.request;
var res = self.response;
var type = self.server.getMime(filename);
var ims = req.headers['if-modified-since'];
var mtime = cache.time;
setHeader(self);
res.setHeader('Last-Modified', mtime.toUTCString());
res.setHeader('Content-Type', getContentType(self, type));
if (cache.gzip) {
res.setHeader('Content-Encoding', 'gzip');
}
res.setHeader('Content-Length', cache.size);
if (ims && Math.abs(new Date(ims).valueOf() - mtime.valueOf()) < 1000) { //使用 304 缓存
res.writeHead(304);
res.end();
}
else {
res.writeHead(200);
res.end(cache.data);
}
return true;
}
return false;
}
//返回数据
function result_data(self, filename, type, time, gzip, err, data) {
if (err) {
delete g_static_cache[filename];
return returnErrorStatus(self, 404);
}
var res = self.response;
var cache = {
data: data,
time: time,
gzip: gzip,
size: data.length
};
if (self.server.fileCacheTime) { // 创建内存缓存
g_static_cache[filename] = cache;
setTimeout(function () { delete cache.data; }, self.server.fileCacheTime * 1e3);
}
if (gzip) {
res.setHeader('Content-Encoding', 'gzip');
}
res.setHeader('Content-Length', data.length);
res.setHeader('Content-Type', getContentType(self, type));
res.writeHead(200);
res.end(data);
}
// 返回文件数据范围
function resultFileData(self, filename, type, size, start_range, end_range) {
var res = self.response;
var end = false;
var read;
res.setHeader('Content-Type', getContentType(self, type));
if (start_range != -1 && end_range != -1) {
res.setHeader('Content-Length', end_range - start_range);
res.setHeader('Content-Range', `bytes ${start_range}-${end_range - 1}/${size}`);
res.writeHead(206);
if (start_range >= end_range) {
return res.end();
}
read = fs.createReadStream(filename, { start: start_range, end: end_range - 1 });
}
else {
res.setHeader('Content-Length', size);
res.writeHead(200);
read = fs.createReadStream(filename);
}
read.on('data', function (buff) {
res.write(buff);
});
read.on('end', function () {
end = true;
res.end();
});
read.on('error', function (e) {
read.destroy();
console.error(e);
end = true;
res.end();
});
res.on('error', function () {
if (!end) { // 意外断开
end = true;
read.destroy();
}
});
res.on('close', function () {
if (!end) { // 意外断开
end = true;
read.destroy();
}
});
}
//返回异常状态
function resultError(self, statusCode, html) {
var res = self.response;
var type = self.server.getMime('html');
setHeader(self);
res.setHeader('Content-Type', getContentType(self, type));
res.writeHead(statusCode);
res.end('<!DOCTYPE html><html><body><h3>' +
statusCode + ': ' + (http.STATUS_CODES[statusCode] || '') +
'</h3><br/>' + (html || '') + '</body></html>');
}
function _returnFile(self, filename, stat) {
var req = self.request;
var res = self.response;
//for file
if (stat.size > self.server.maxFileSize) { //File size exceeds the limit
return returnErrorStatus(self, 403);
}
var mtime = stat.mtime;
var ims = req.headers['if-modified-since'];
var range = req.headers['range'];
var type = self.server.getMime(filename);
var gzip = isGzip(self, filename);
setHeader(self);
res.setHeader('Last-Modified', mtime.toUTCString());
res.setHeader('Accept-Ranges', 'bytes');
if (range) { // return Range
if (range.substr(0, 6) == 'bytes=') {
var ranges = range.substr(6).split('-');
var start_range = ranges[0] ? Number(ranges[0]) : 0;
var end_range = ranges[1] ? Number(ranges[1]) : stat.size - 1;
if (isNaN(start_range) || isNaN(end_range)) {
return returnErrorStatus(self, 400);
}
if (!ranges[0]) { // 选择文件最后100字节 bytes=-100
start_range = Math.max(0, stat.size - end_range);
end_range = stat.size - 1;
}
end_range++;
end_range = Math.min(stat.size, end_range);
start_range = Math.min(start_range, end_range);
// var ir = req.headers['if-range'];
// if (ir && Math.abs(new Date(ims) - mtime) < 1000) {
// }
return resultFileData(self, filename, type, stat.size, start_range, end_range);
}
}
if (ims && Math.abs(new Date(ims).valueOf() - mtime.valueOf()) < 1000) { //use 304 cache
res.setHeader('Content-Type', getContentType(self, type));
res.writeHead(304);
res.end();
return;
}
if (stat.size > 5 * 1024 * 1024) { // 数据大于5MB使用这个函数处理
return resultFileData(self, filename, type, stat.size, -1, -1);
}
else if (!gzip) { //no use gzip format
return fs.readFile(filename, function (err, data) {
result_data(self, filename, type, mtime, false, err, data);
});
}
fs.readFile(filename, function (err, data) {
if (err) {
console.error(err);
return returnErrorStatus(self, 404);
}
zlib.gzip(data, function (err, data) {
result_data(self, filename, type, mtime, true, err, data);
});
});
}
function returnFile(self, filename) {
if (util_1.default.debug || !return_cache(self, filename)) { //high speed Cache
fs.stat(filename, function (err, stat) {
if (err) {
returnErrorStatus(self, 404);
}
else if (stat.isDirectory()) { //dir
tryReturnDirectory(self, filename);
}
else if (stat.isFile()) {
_returnFile(self, filename, stat);
}
else {
returnErrorStatus(self, 404);
}
});
}
}
function returnErrorStatus(self, statusCode, html) {
var filename = self.server.errorStatus[statusCode];
if (filename) {
filename = self._root[0] + filename;
fs.stat(filename, function (err) {
if (err) {
resultError(self, statusCode, html);
}
else {
if (util_1.default.debug && html) {
resultError(self, statusCode, html);
}
else {
returnFile(self, filename);
}
}
});
}
else {
resultError(self, statusCode, html);
}
}
/**
* @class StaticService
*/
class StaticService extends service_1.Service {
/**
* @constructor
* @arg req {http.ServerRequest}
* @arg res {http.ServerResponse}
* @arg info {Object}
*/
constructor(req, res) {
super(req);
this.response = res;
// this.setTimeout(this.server.timeout * 1e3);
}
get _root() {
return this.server.root;
}
get isCompleteResponse() {
return this.m_markCompleteResponse ? true : false;
}
markCompleteResponse() {
util_1.default.assert(!this.m_markCompleteResponse, 'markCompleteResponse');
this.m_markCompleteResponse = true;
}
/**
* @overwrite
*/
action(info) {
var method = this.request.method;
if (method == 'GET' || method == 'HEAD') {
var filename = this.pathname;
var virtual = this.server.virtual;
if (virtual) { //是否有虚拟目录
var index = filename.indexOf(virtual + '/');
if (index === 0) {
filename = filename.substr(virtual.length);
}
else {
return this.returnErrorStatus(404);
}
}
if (this.server.disable.test(filename)) { //禁止访问的路径
return this.returnErrorStatus(403);
}
this.returnSiteFile(filename.substr(1));
}
else {
this.returnErrorStatus(405);
}
}
/**
* returnRedirect
* @param {String} path
*/
returnRedirect(path) {
this.markCompleteResponse();
returnRedirect(this, path);
}
/**
* return the state to the browser
* @param {Number} statusCode
* @param {String} text (Optional) not default status ,return text
*/
returnErrorStatus(statusCode, html) {
this.markCompleteResponse();
returnErrorStatus(this, statusCode, html);
}
/**
* 返回站点文件
*/
async returnSiteFile(name) {
this.markCompleteResponse();
var self = this;
if (util_1.default.debug || !return_cache(self, name)) { //high speed Cache
for (var root of this._root) {
var filename = root + '/' + name;
try {
var stat = await fs2.stat(filename);
if (stat.isDirectory()) {
return tryReturnDirectory(self, filename);
}
else if (stat.isFile()) {
return _returnFile(self, filename, stat);
}
else {
return returnErrorStatus(self, 404);
}
}
catch (err) {
}
}
returnErrorStatus(self, 404);
}
}
isAcceptGzip(filename) {
if (this.server.gzip) {
var ae = this.request.headers['accept-encoding'];
return !!(ae && ae.match(/gzip/i));
}
return false;
}
isGzip(filename) {
return isGzip(this, filename);
}
setDefaultHeader(expires) {
setHeader(this, expires);
}
setNoCache() {
this.m_no_cache = true;
this.response.setHeader('Cache-Control', 'no-cache');
this.response.setHeader('Expires', '-1');
}
/**
* return file to browser
* @param {String} filename
*/
returnFile(filename) {
this.markCompleteResponse();
return returnFile(this, filename);
}
/**
* return dir
* @param {String} filename
*/
returnDirectory(filename) {
this.markCompleteResponse();
return returnDirectory(this, filename);
}
}
exports.StaticService = StaticService;
service_1.default.set('StaticService', StaticService);