front-server
Version:
front-end dev server, use nginx style configuration
506 lines (505 loc) • 19.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const path = require("path");
const http = require("http");
const mime = require("mime");
const crypto = require("crypto");
const moment = require("moment");
const httpProxy = require("http-proxy");
const VERSION = '0.5.1';
const FAVICON = `AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAApzYMAKc2DACnNgxApzYNsKc2D2ynNg9kpzYNoKc2DDSnNgwAqzoQAAAAAAAAAAAAAAAAAAAAAAAAAAAApzYMAKc2DACnOgwEpzYNQKc2DtynNg18pzYNgKc2DwynNg64pzYM/Kc2DBCnNgwAAAAAAAAAAACnNgwApzYMBKc2DKynNg4gpzYOqKc2DYSnNgxApzIIAKc2DACnNgxopzYN+Kc2DzinNg5QpzYMnKM2DACnNgwApzYMGKc2DbCnNg84pzYObKc2DiinNg9MpzYMjKc2DAAAAAAApzYMAKs+CACnNgyspzYObKc2DzSnNg2cpzYMEKc2DLinNg88pzYNRKc2EAinNgxspzYPLKc2DNCnNgw0pzYNDKc2DXinNg1UpzYMkKc2DBSnNg1UpzYPNKc2DJynNgzcpzYPEKc2DGSnNgwApzYMaKc2DyCnNg0spzYOpKc2DxSnNg6gpzYOxKc2DyynNg04pzYMcKc2DxinNgzApzYM3Kc2DxCnNgxkpzYMAKc2DGinNg8YpzYN3Kc2DsCnNgxwpzYMAKc2DCCnNg5spzYOlKc2DICnNg8YpzYMwKc2DNynNg8QpzYMZKc2DACnNgxopzYPJKc2DPinNgzQpzYNpKc2DjinNg6spzYPZKc2DcCnNgx0pzYPGKc2DMCnNgzcpzYPEKc2DGSnNgwApzYMaKc2DyCnNg0wpzYO9Kc2DxCnNg5UpzYNyKc2DWynNgxwpzYMdKc2DxinNgzApzYM3Kc2DxCnNgxkpzYMAKc2DGinNg8cpzYNdKc2D1ynNg1YpzYMQKc2DJinNg7cpzYNaKc2DGynNg8YpzYMwKc2DNynNg8QpzYMZKc2DACnNgxYpzYOrKc2DMynNg3wpzYPLKc2DwinNg8spzYOkKc2DFynNgx0pzYPGKc2DMCnNgy0pzYPPKc2DVCnNgwUpzYMCKc2DEynNgwQpzYMEKc2DIinNgzYpzYMpKc2DCCnNgwYpzYNbKc2DzSnNgyYpzYMFKc2DaCnNg84pzYOdKc2DLinNhAEpzYMAAAAAAAAAAAApzYMAKc6EASnNgzEpzYOiKc2DzCnNg2IpzYMEKc2DACnNggApzYMnKc2DlSnNg88pzYN/Kc2DGinNhAApzYMAKc2DHCnNg4MpzYPQKc2DkCnNgyQqzoMAKc2DAAAAAAAAAAAAKc2DACnNgwQpzYNAKc2DrynNg8QpzYNhKc2DZSnNg8YpzYOsKc2DPCnNgwMpzYMAAAAAAAAAAAAAAAAAAAAAAAAAAAAo0IgAKc2DACnNgw0pzYNrKc2D2SnNg9cpzYNmKc2DDCnNgwAqzoYAAAAAAAAAAAAAAAAA+B8AAOAHAACAgQAAAcAAAAAAAAAQAAAAEEAAABAAAAAQAAAAEAAAABAAAAAAAAAAA8AAAIGBAADgBwAA+B8AAA==`;
let config;
let refresh_config = {
id: '',
client: '',
enabled: false,
res: []
};
function autoRefresh(content) {
try {
refresh_config.id = crypto
.createHash('sha1')
.update(content)
.digest()
.toString('hex');
refresh_config.client = fs
.readFileSync(path.join(__dirname, './client.js'), 'utf-8')
.replace('server_api', `/refreshapi/${refresh_config.id}`);
let root = null;
config.locations.forEach(item => {
if (item.condition.operator === '' && item.condition.value === '/') {
item.actions.forEach(action => {
if (action.name === 'root')
root = action.value[0];
});
}
});
if (root === null)
return;
let files = [];
let timer;
let id = 1;
fs.watch(root, { recursive: true }, (event, file) => {
files.push(file.replace(/\\/g, '/'));
clearTimeout(timer);
timer = setTimeout(() => {
refresh_config.res.forEach(res => {
try {
res.write(`id:${id++}\nevent:update\ndata:${JSON.stringify({
time: Date.now(),
files
})}\nretry:3000\n\n`);
}
catch (error) { }
});
files = [];
}, 100);
});
refresh_config.enabled = true;
}
catch (error) {
console.error(error);
}
}
let proxyServer = httpProxy.createProxyServer({
cookieDomainRewrite: '',
cookiePathRewrite: '/'
});
proxyServer.on('error', (proxyRes, req, res) => {
res.statusCode = 502;
res.setHeader('Content-Type', 'text/html');
res.end(`<html>
<head><title>502 Bad Gateway</title></head>
<body bgcolor="white">
<center><h1>502 Bad Gateway</h1></center>
<hr><center>front-server/${VERSION}</center>
</body>
</html>`);
});
class Request {
constructor(req, res) {
this.req = req;
this.res = res;
this.vars = {
$request_uri: '',
$request_method: '',
$http_user_agent: '',
$host: '',
$hostname: '',
$uri: '',
$args: '',
$remote_addr: ''
};
}
logger(...args) {
let info = [
`[${moment().format('YYYY-MM-DD HH:mm:ss')}]`,
this.vars.$request_method.padEnd(4),
(this.res.statusCode + '').padEnd(5),
this.vars.$remote_addr.padEnd(15),
this.vars.$request_uri,
...args
];
console.log(info.join(' '));
}
err(code, message) {
this.res.statusCode = code || 500;
this.logger();
this.res.setHeader('Content-Type', 'text/html');
this.res.end(`<html>
<head><title>${code} ${message}</title></head>
<body bgcolor="white">
<center><h1>${code} ${message}</h1></center>
<hr><center>front-server/${VERSION}</center>
</body>
</html>`);
}
err404() {
this.err(404, 'Not Found');
}
err403() {
this.err(403, 'Forbidden');
}
asyncExists(filePath) {
return new Promise((resolve, reject) => {
fs.exists(filePath, exists => {
resolve(exists);
});
});
}
asyncStat(filePath) {
return new Promise((resolve, reject) => {
fs.stat(filePath, (err, stats) => {
if (err) {
reject(err);
}
else {
resolve(stats);
}
});
});
}
async sendFile(filePath, index) {
try {
index = index || 'index.html';
let exists = await this.asyncExists(filePath);
if (exists === false) {
if (this.vars.$uri === '/favicon.ico') {
let buf = Buffer.from(FAVICON, 'base64');
this.res.setHeader('Content-Type', 'image/x-icon');
this.res.end(buf);
this.logger();
return;
}
return this.err404();
}
let stats = await this.asyncStat(filePath);
let size = stats.size;
if (stats.isDirectory() === true) {
if (this.vars.$uri.endsWith('/') === false) {
this.res.statusCode = 301;
this.res.setHeader('Location', this.vars.$uri + '/' + this.vars.$args);
this.res.end();
return;
}
let indexFile = path.join(filePath, index);
let indexExists = await this.asyncExists(indexFile);
if (indexExists === false) {
return this.err403();
}
filePath = indexFile;
stats = await this.asyncStat(filePath);
size = stats.size;
}
let type = mime.getType(filePath) || 'application/octet-stream';
this.res.setHeader('Content-Type', type);
if (refresh_config.enabled === true && filePath.endsWith('.html')) {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) {
this.err(500, 'Internal Server Error');
}
else {
let exp = /(<title>.*<\/title>)/;
let result = data.replace(exp, `$1\r\n <script src="/refreshjs/${refresh_config.id}.js"></script>`);
this.res.end(result);
}
});
}
else {
this.res.setHeader('Content-Length', size);
fs.createReadStream(filePath)
.on('error', error => {
this.err(500, 'Internal Server Error');
})
.pipe(this.res);
}
this.logger('->', filePath.replace(/\\/g, '/'));
}
catch (error) {
this.err(500, 'Internal Server Error');
}
}
matchLocation(uri) {
uri = uri || this.currentUri;
let result = null;
let locations = config.locations;
for (let i = 0; i < locations.length; i++) {
let item = locations[i];
if (item.condition.operator === '=' && uri === item.condition.value) {
return result;
}
if (['~', '~*'].indexOf(item.condition.operator) > -1) {
let exp;
if (item.condition.operator === '~') {
exp = new RegExp(item.condition.value);
}
if (item.condition.operator === '~*') {
exp = new RegExp(item.condition.value, 'i');
}
if (exp.test(uri) === true &&
(result === null ||
(result !== null &&
['~', '~*'].indexOf(result.condition.operator) > -1 &&
item.condition.value.length > result.condition.value.length) ||
(result !== null && result.condition.operator === ''))) {
result = item;
}
}
if (item.condition.operator === '' &&
uri.toLowerCase().startsWith(item.condition.value.toLowerCase()) &&
(result === null ||
(result !== null &&
result.condition.operator === '' &&
item.condition.value.length > result.condition.value.length))) {
result = item;
}
}
return result;
}
getIndex(location) {
let index = 'index.html';
for (let i = 0; i < location.actions.length; i++) {
let action = location.actions[i];
if (action.name === 'index') {
index = action.value[0];
}
}
return index;
}
getProxyHeader(location) {
let headers = {};
for (let i = 0; i < location.actions.length; i++) {
let action = location.actions[i];
if (action.name === 'proxy_set_header') {
headers[action.value[0]] = action.value[1];
}
}
return headers;
}
getTryFiles(location) {
let files = null;
for (let i = 0; i < location.actions.length; i++) {
let action = location.actions[i];
if (action.name === 'try_files') {
files = action.value;
}
}
return files;
}
getProxyPass(location) {
let proxy = null;
for (let i = 0; i < location.actions.length; i++) {
let action = location.actions[i];
if (action.name === 'proxy_pass') {
proxy = action.value[0];
}
}
return proxy;
}
getLocalFile(location) {
let filePath = null;
for (let i = 0; i < location.actions.length; i++) {
let action = location.actions[i];
if (action.name === 'root') {
filePath = path.join(action.value[0], this.currentUri);
}
if (action.name === 'alias') {
filePath = path.join(action.value[0], this.currentUri.replace(new RegExp(location.condition.value), ''));
}
}
return filePath;
}
async matchAction(location, tryFiles = true) {
let proxy = this.getProxyPass(location);
if (proxy !== null) {
this.logger('->', proxy + this.currentUri);
let headers = this.getProxyHeader(location);
return proxyServer.web(this.req, this.res, {
changeOrigin: true,
target: proxy,
headers: headers
});
}
let local = this.getLocalFile(location);
if (local !== null) {
let index = this.getIndex(location);
let files = this.getTryFiles(location);
if (files !== null && tryFiles === true) {
for (let i = 0; i < files.length; i++) {
let file = files[i];
for (let key in this.vars) {
file = file.replace(key, this.vars[key]);
}
this.currentUri = file;
if (i < files.length - 1) {
let filePath = this.getLocalFile(location);
let exists = await this.asyncExists(filePath);
if (exists === true) {
return this.sendFile(filePath, index);
}
}
else {
return this.matchAction(location, false);
}
}
}
else {
return this.sendFile(local, index);
}
}
return null;
}
parse() {
this.vars = {
$request_uri: this.req.url,
$uri: this.req.url.split('?')[0],
$request_method: this.req.method,
$http_user_agent: this.req.headers['user-agent'],
$host: this.req.headers['host'],
$hostname: this.req.headers['host'].replace(/(\:\d+)$/, ''),
$remote_addr: this.req.connection.localAddress.replace('::ffff:', ''),
$args: this.req.url.split('?').length > 1
? '?' + this.req.url.split('?')[1]
: ''
};
this.res.setHeader('Server', `front-server/${VERSION}`);
this.currentUri = this.vars.$uri;
let location = this.matchLocation();
if (location !== null) {
this.matchAction(location).then(result => {
if (result === null) {
return this.err404();
}
});
}
else {
return this.err404();
}
}
}
class Parser {
getPort() {
let str = this.content.trim();
let exp = /listen\s*(\d+);/;
let result = str.match(exp);
if (result !== null) {
return parseInt(result[1]);
}
return 3000;
}
getAutoRefresh() {
let value = 'off';
let str = this.content.trim();
let exp = /auto_refresh\s*(on|off);/;
let result = str.match(exp);
if (result !== null) {
value = result[1];
}
return value;
}
getLocationGroup() {
let str = this.content.trim();
let exp = /location([^{]+)\{([^\}]+)\}/g;
let result = str.match(exp);
if (result !== null) {
let locations = result.map(item => {
return this.getLocation(item);
});
return locations;
}
return [];
}
getAction(str) {
let exp = /(\w+)\s+([^;]+)/;
let result = str.match(exp);
if (result !== null) {
return {
name: result[1],
value: result[2].replace(/(\s{2,})/g, ' ').split(' ')
};
}
return null;
}
getActions(str) {
let exp = /(\w+)\s+([^;]+)/g;
let result = str.match(exp);
let rows = [];
if (result !== null) {
result.forEach(item => {
let val = this.getAction(item);
if (val !== null) {
rows.push(val);
}
});
}
return rows;
}
getLocationExpression(str) {
str = str.trim();
let exp = /([=~*]{1,2})\s*(.+)/;
let result = str.match(exp);
if (result !== null) {
return {
operator: result[1].trim(),
value: result[2].trim()
};
}
else {
return {
operator: '',
value: str
};
}
}
getLocation(str) {
str = str.trim();
let exp = /location([^{]+)\{([^\}]+)\}/;
let result = str.match(exp);
if (result !== null) {
let condition = this.getLocationExpression(result[1]);
let actions = this.getActions(result[2]);
return {
condition: condition,
actions: actions
};
}
}
parse() {
let port = this.getPort();
let auto_refresh = this.getAutoRefresh();
let locations = this.getLocationGroup();
return {
port,
auto_refresh,
locations
};
}
constructor(content) {
this.content = content.replace(/(#.*)$/gm, '');
}
}
module.exports = {
start(conf) {
let content = fs.readFileSync(conf, 'utf-8');
config = new Parser(content).parse();
if (config.auto_refresh === 'on') {
autoRefresh(content);
}
let server = http.createServer((req, res) => {
if (config.auto_refresh === 'on') {
if (req.url.startsWith(`/refreshapi/${refresh_config.id}`)) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Server: `front-server/${VERSION}`,
Connection: 'keep-alive'
});
res.flushHeaders();
refresh_config.res.push(res);
req.on('close', () => {
refresh_config.res = refresh_config.res.filter(item => item !== res);
});
return;
}
if (req.url.startsWith(`/refreshjs/${refresh_config.id}.js`)) {
res.writeHead(200, {
'Content-Type': 'application/javascript',
'Content-Length': refresh_config.client.length,
Server: `front-server/${VERSION}`
});
res.end(refresh_config.client);
return;
}
}
let request = new Request(req, res);
request.parse();
});
server.on('error', err => {
console.trace(err);
process.exit();
});
server.listen(config.port);
console.log(`front-server listening on http://127.0.0.1:${config.port}/`);
}
};