334 lines
9.9 KiB
JavaScript
334 lines
9.9 KiB
JavaScript
const http = require('http');
|
||
const https = require('https');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const url = require('url');
|
||
const zlib = require('zlib');
|
||
const chalk = require('chalk');
|
||
const os = require('os');
|
||
const open = require("open");
|
||
const Handlebars = require('handlebars');
|
||
const pem = require('pem');
|
||
const mime = require('./mime');
|
||
const Template = require('./templates');
|
||
|
||
const _defaultTemplate = Handlebars.compile(Template.page_dafault);
|
||
const _404TempLate = Handlebars.compile(Template.page_404);
|
||
|
||
const hasTrailingSlash = url => url[url.length - 1] === '/';
|
||
|
||
const ifaces = os.networkInterfaces();
|
||
|
||
class StaticServer {
|
||
constructor(options) {
|
||
this.port = options.port;
|
||
this.indexPage = options.index;
|
||
this.openIndexPage = options.openindex;
|
||
this.openBrowser = options.openbrowser;
|
||
this.charset = options.charset;
|
||
this.cors = options.cors;
|
||
this.protocal = options.https ? 'https' : 'http';
|
||
this.zipMatch = '^\\.(css|js|html)$';
|
||
}
|
||
|
||
/**
|
||
* 响应错误
|
||
*
|
||
* @param {*} err
|
||
* @param {*} res
|
||
* @returns
|
||
* @memberof StaticServer
|
||
*/
|
||
respondError(err, res) {
|
||
res.writeHead(500);
|
||
return res.end(err);
|
||
}
|
||
|
||
/**
|
||
* 响应404
|
||
*
|
||
* @param {*} req
|
||
* @param {*} res
|
||
* @memberof StaticServer
|
||
*/
|
||
respondNotFound(req, res) {
|
||
res.writeHead(404, {
|
||
'Content-Type': 'text/html'
|
||
});
|
||
const html = _404TempLate();
|
||
res.end(html);
|
||
}
|
||
|
||
respond(pathName, req, res) {
|
||
fs.stat(pathName, (err, stat) => {
|
||
if (err) return respondError(err, res);
|
||
this.responseFile(stat, pathName, req, res);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 判断是否需要解压
|
||
*
|
||
* @param {*} pathName
|
||
* @returns
|
||
* @memberof StaticServer
|
||
*/
|
||
shouldCompress(pathName) {
|
||
return path.extname(pathName).match(this.zipMatch);
|
||
}
|
||
|
||
/**
|
||
* 解压文件
|
||
*
|
||
* @param {*} readStream
|
||
* @param {*} req
|
||
* @param {*} res
|
||
* @returns
|
||
* @memberof StaticServer
|
||
*/
|
||
compressHandler(readStream, req, res) {
|
||
const acceptEncoding = req.headers['accept-encoding'];
|
||
if (!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) {
|
||
return readStream;
|
||
} else if (acceptEncoding.match(/\bgzip\b/)) {
|
||
res.setHeader('Content-Encoding', 'gzip');
|
||
return readStream.pipe(zlib.createGzip());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 响应文件路径
|
||
*
|
||
* @param {*} stat
|
||
* @param {*} pathName
|
||
* @param {*} req
|
||
* @param {*} res
|
||
* @memberof StaticServer
|
||
*/
|
||
responseFile(stat, pathName, req, res) {
|
||
// 设置响应头
|
||
res.setHeader('Content-Type', `${mime.lookup(pathName)}; charset=${this.charset}`);
|
||
res.setHeader('Accept-Ranges', 'bytes');
|
||
|
||
// 添加跨域
|
||
if (this.cors) res.setHeader('Access-Control-Allow-Origin', '*');
|
||
|
||
let readStream;
|
||
readStream = fs.createReadStream(pathName);
|
||
if (this.shouldCompress(pathName)) { // 判断是否需要解压
|
||
readStream = this.compressHandler(readStream, req, res);
|
||
}
|
||
readStream.pipe(res);
|
||
}
|
||
|
||
/**
|
||
* 响应重定向
|
||
*
|
||
* @param {*} req
|
||
* @param {*} res
|
||
* @memberof StaticServer
|
||
*/
|
||
respondRedirect(req, res) {
|
||
const location = req.url + '/';
|
||
res.writeHead(301, {
|
||
'Location': location,
|
||
'Content-Type': 'text/html'
|
||
});
|
||
const html = _defaultTemplate({
|
||
htmlStr: `Redirecting to <a href='${location}'>${location}</a>`,
|
||
showFileList: false
|
||
})
|
||
res.end(html);
|
||
}
|
||
|
||
/**
|
||
* 响应文件夹路径
|
||
*
|
||
* @param {*} pathName
|
||
* @param {*} req
|
||
* @param {*} res
|
||
* @memberof StaticServer
|
||
*/
|
||
respondDirectory(pathName, req, res) {
|
||
const indexPagePath = path.join(pathName, this.indexPage);
|
||
// 如果文件夹下存在index.html,则默认打开
|
||
if (this.openIndexPage && fs.existsSync(indexPagePath)) {
|
||
this.respond(indexPagePath, req, res);
|
||
} else {
|
||
fs.readdir(pathName, (err, files) => {
|
||
if (err) {
|
||
respondError(err, res);
|
||
}
|
||
const requestPath = url.parse(req.url).pathname;
|
||
const fileList = [];
|
||
files.forEach(fileName => {
|
||
let itemLink = path.join(requestPath, fileName);
|
||
let isDirectory = false;
|
||
const stat = fs.statSync(path.join(pathName, fileName));
|
||
if (stat && stat.isDirectory()) {
|
||
itemLink = path.join(itemLink, '/');
|
||
isDirectory = true;
|
||
}
|
||
fileList.push({
|
||
link: itemLink,
|
||
name: fileName,
|
||
isDirectory
|
||
});
|
||
});
|
||
// 排序,目录在前,文件在后
|
||
fileList.sort((prev, next) => {
|
||
if (prev.isDirectory && !next.isDirectory) {
|
||
return -1;
|
||
}
|
||
return 1;
|
||
});
|
||
res.writeHead(200, {
|
||
'Content-Type': 'text/html'
|
||
});
|
||
const html = _defaultTemplate({
|
||
requestPath,
|
||
fileList,
|
||
showFileList: true
|
||
})
|
||
res.end(html);
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 路由处理
|
||
*
|
||
* @param {*} pathName
|
||
* @param {*} req
|
||
* @param {*} res
|
||
* @memberof StaticServer
|
||
*/
|
||
routeHandler(pathName, req, res) {
|
||
const realPathName = pathName.split('?')[0];
|
||
fs.stat(realPathName, (err, stat) => {
|
||
this.logGetInfo(err, pathName);
|
||
if (!err) {
|
||
const requestedPath = url.parse(req.url).pathname;
|
||
// 检查url
|
||
// 如果末尾有'/',且是文件夹,则读取文件夹
|
||
// 如果是文件夹,但末尾没'/',则重定向至'xxx/'
|
||
// 如果是文件,则判断是否是压缩文件,是则解压,不是则读取文件
|
||
if (hasTrailingSlash(requestedPath) && stat.isDirectory()) {
|
||
this.respondDirectory(realPathName, req, res);
|
||
} else if (stat.isDirectory()) {
|
||
this.respondRedirect(req, res);
|
||
} else {
|
||
this.respond(realPathName, req, res);
|
||
}
|
||
} else {
|
||
this.respondNotFound(req, res);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 打印ip地址
|
||
*
|
||
* @memberof StaticServer
|
||
*/
|
||
logUsingPort() {
|
||
const me = this;
|
||
console.log(`${chalk.yellow(`Starting up your server\nAvailable on:`)}`);
|
||
Object.keys(ifaces).forEach(function (dev) {
|
||
ifaces[dev].forEach(function (details) {
|
||
if (details.family === 'IPv4') {
|
||
console.log(` ${me.protocal}://${details.address}:${chalk.green(me.port)}`);
|
||
}
|
||
});
|
||
});
|
||
console.log(`${chalk.cyan(Array(50).fill('-').join(''))}`);
|
||
}
|
||
|
||
/**
|
||
* 打印占用端口
|
||
*
|
||
* @param {*} oldPort
|
||
* @param {*} port
|
||
* @memberof StaticServer
|
||
*/
|
||
logUsedPort(oldPort, port) {
|
||
const me = this;
|
||
console.log(`${chalk.red(`The port ${oldPort} is being used, change to port `)}${chalk.green(me.port)} `);
|
||
}
|
||
|
||
/**
|
||
* 打印https证书友好提示
|
||
*
|
||
* @memberof StaticServer
|
||
*/
|
||
logHttpsTrusted() {
|
||
console.log(chalk.green('Currently is using HTTPS certificate (Manually trust it if necessary)'));
|
||
}
|
||
|
||
|
||
/**
|
||
* 打印路由路径输出
|
||
*
|
||
* @param {*} isError
|
||
* @param {*} pathName
|
||
* @memberof StaticServer
|
||
*/
|
||
logGetInfo(isError, pathName) {
|
||
if (isError) {
|
||
console.log(chalk.red(`404 ${pathName}`));
|
||
} else {
|
||
console.log(chalk.cyan(`200 ${pathName}`));
|
||
}
|
||
}
|
||
|
||
startServer(keys) {
|
||
const me = this;
|
||
let isPostBeUsed = false;
|
||
const oldPort = me.port;
|
||
const protocal = me.protocal === 'https' ? https : http;
|
||
const options = me.protocal === 'https' ? { key: keys.serviceKey, cert: keys.certificate } : null;
|
||
const callback = (req, res) => {
|
||
const pathName = path.join(process.cwd(), path.normalize(decodeURI(req.url)));
|
||
me.routeHandler(pathName, req, res);
|
||
};
|
||
const params = [callback];
|
||
if (me.protocal === 'https') params.unshift(options);
|
||
const server = protocal.createServer(...params).listen(me.port);
|
||
server.on('listening', function () { // 执行这块代码说明端口未被占用
|
||
if (isPostBeUsed) {
|
||
me.logUsedPort(oldPort, me.port);
|
||
}
|
||
me.logUsingPort();
|
||
if (me.openBrowser) {
|
||
open(`${me.protocal}://127.0.0.1:${me.port}`);
|
||
}
|
||
});
|
||
|
||
server.on('error', function (err) {
|
||
if (err.code === 'EADDRINUSE') { // 端口已经被使用
|
||
isPostBeUsed = true;
|
||
me.port = parseInt(me.port) + 1;
|
||
server.listen(me.port);
|
||
} else {
|
||
console.log(err);
|
||
}
|
||
})
|
||
}
|
||
|
||
start() {
|
||
const me = this;
|
||
if (this.protocal === 'https') {
|
||
pem.createCertificate({ days: 1, selfSigned: true }, function (err, keys) {
|
||
if (err) {
|
||
throw err
|
||
}
|
||
me.logHttpsTrusted();
|
||
me.startServer(keys);
|
||
})
|
||
} else {
|
||
me.startServer();
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = StaticServer; |