1427 lines
36 KiB
JavaScript
1427 lines
36 KiB
JavaScript
/** Copyright Stewart Allen -- All Rights Reserved */
|
|
|
|
Array.prototype.contains = function(v) {
|
|
return this.indexOf(v) >= 0;
|
|
};
|
|
|
|
Array.prototype.appendAll = function(a) {
|
|
this.push.apply(this,a);
|
|
return this;
|
|
};
|
|
|
|
const helper = {
|
|
log: function() {
|
|
console.log(
|
|
moment().format('YYMMDD.HHmmss'),
|
|
[...arguments]
|
|
.map(v => util.inspect(v, {
|
|
maxArrayLength: null,
|
|
breakLength: Infinity,
|
|
colors: debug,
|
|
compact: true,
|
|
sorted: true,
|
|
depth: null
|
|
}))
|
|
.join(' ')
|
|
);
|
|
}
|
|
};
|
|
|
|
function log(o) {
|
|
helper.log(o);
|
|
}
|
|
|
|
function promise(resolve, reject) {
|
|
return new Promise(resolve, reject);
|
|
}
|
|
|
|
function rval() {
|
|
return (Math.round(Math.random()*0xffffffff)).toString(36);
|
|
}
|
|
|
|
function guid() {
|
|
return time().toString(36)+rval()+rval()+rval();
|
|
}
|
|
|
|
function time() {
|
|
return Date.now();
|
|
}
|
|
|
|
/**
|
|
* @param {String[]} path
|
|
*/
|
|
function mkdirs(path) {
|
|
let root = "";
|
|
path.forEach(seg => {
|
|
if (root) {
|
|
root = root + "/" + seg;
|
|
} else {
|
|
root = seg;
|
|
}
|
|
lastmod(root) || fs.mkdirSync(root);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {String} o
|
|
*/
|
|
function obj2string(o) {
|
|
return JSON.stringify(o);
|
|
}
|
|
|
|
function string2obj(s) {
|
|
return JSON.parse(s);
|
|
}
|
|
|
|
/**
|
|
* sync return mtime for file or 0 for no such file
|
|
* @param {String} path
|
|
* @returns {number}
|
|
*/
|
|
function lastmod(path) {
|
|
try {
|
|
return fs.statSync(path).mtime.getTime();
|
|
} catch (e) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {String} filePath
|
|
* @param {Function} fn
|
|
* @returns {*}
|
|
*/
|
|
function getCachedFile(filePath, cpath, fn) {
|
|
let cachePath = ".cache/" + cpath
|
|
.replace(/\//g,'_')
|
|
.replace(/\\/g,'_')
|
|
.replace(/:/g,'_'),
|
|
cached = fileCache[filePath],
|
|
now = time();
|
|
|
|
if (cached) {
|
|
if (now - cached.lastcheck > 60000) {
|
|
let smod = lastmod(filePath),
|
|
cmod = cached.mtime;
|
|
|
|
if (!smod) throw "missing source file";
|
|
if (smod > cmod) cached = null;
|
|
|
|
cached.lastcheck = now;
|
|
}
|
|
}
|
|
|
|
if (!cached) {
|
|
let smod = lastmod(filePath),
|
|
cmod = lastmod(cachePath),
|
|
cacheData;
|
|
|
|
if (cmod >= smod) {
|
|
cacheData = fs.readFileSync(cachePath);
|
|
} else {
|
|
helper.log({update_cache:filePath});
|
|
cacheData = fn(filePath);
|
|
fs.writeFileSync(cachePath, cacheData);
|
|
}
|
|
|
|
cached = {
|
|
data: cacheData,
|
|
mtime: cmod || now,
|
|
lastcheck: now
|
|
};
|
|
|
|
fileCache[filePath] = cached;
|
|
}
|
|
|
|
return cached.data;
|
|
}
|
|
|
|
/**
|
|
* mangle, cache and concatenate scripts
|
|
*/
|
|
function prepareScripts() {
|
|
code.kiri = concatCode(script.kiri);
|
|
code.meta = concatCode(script.meta);
|
|
code.work = concatCode(script.work);
|
|
code.worker = concatCode(script.worker);
|
|
fs.readdir("./web/kiri/filter/FDM", function(err, files) {
|
|
filters_fdm = files || filters_fdm;
|
|
});
|
|
fs.readdir("./web/kiri/filter/SLA", function(err, files) {
|
|
filters_sla = files || filters_sla;
|
|
});
|
|
fs.readdir("./web/kiri/filter/CAM", function(err, files) {
|
|
filters_cam = files || filters_cam;
|
|
});
|
|
fs.readdir("./web/kiri/filter/LASER", function(err, files) {
|
|
filters_laser = files || filters_laser;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {Array} array
|
|
* @returns {String}
|
|
*/
|
|
function concatCode(array) {
|
|
let code = [],
|
|
cached,
|
|
cachepath,
|
|
filepath;
|
|
|
|
// in debug mode, the script should load dependent
|
|
// scripts instead of serving a complete bundle
|
|
if (debug) {
|
|
let code = [ `(function() { let load = [ `];
|
|
array.forEach(file => {
|
|
if (file.indexOf(".js") < 0) {
|
|
file = `/js/${file}.js?${ver.VERSION}`;
|
|
}
|
|
if (file.indexOf(":\\") > 0) {
|
|
file = `/${file}`;
|
|
}
|
|
code.push(`"${file.replace(/\\/g,'/')}",`);
|
|
});
|
|
code.push([
|
|
']; function load_next() {',
|
|
'let file = load.shift();',
|
|
'if (!file) return;',
|
|
'if (!self.document) { importScripts(file); return load_next() }',
|
|
'let s = document.createElement("script");',
|
|
's.type = "text/javascript";',
|
|
's.src = file;',
|
|
's.onload = load_next;',
|
|
'document.head.appendChild(s);',
|
|
'}; load_next(); })();',
|
|
'self.debug=true;'
|
|
].join('\n'));
|
|
return code.join('\n');
|
|
}
|
|
|
|
array.forEach((file, index) => {
|
|
if (file.charAt(0) === "/" || file.indexOf("\\") > 0) {
|
|
filepath = file;
|
|
cachepath = "js_mod" + file
|
|
.replace(/\//g,'_')
|
|
.replace(/\\/g,'_')
|
|
.replace(/:/g,'_'),
|
|
array[index] = cachepath.substring(3).replace('.js','');
|
|
fileMap[cachepath.replace("js_","js/")] = filepath;
|
|
} else {
|
|
filepath = codePrefix + file + ".js";
|
|
cachepath = filepath;
|
|
}
|
|
cached = getCachedFile(filepath, cachepath, function(path) {
|
|
return minify(filepath);
|
|
});
|
|
code.push(cached);
|
|
});
|
|
|
|
return code.join('');
|
|
}
|
|
|
|
/**
|
|
* @param {String} cookie
|
|
* @param {String} [key]
|
|
* @returns {String | null}
|
|
*/
|
|
function getCookieValue(cookie,key) {
|
|
if (!cookie) return null;
|
|
key = (key || "key") + "=";
|
|
let kpos = cookie.lastIndexOf(key);
|
|
if (kpos >= 0) {
|
|
return cookie.substring(kpos+key.length).split(';')[0];
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {String} ip
|
|
*/
|
|
function isNotLocal(ip) {
|
|
return ipLocal.contains(ip) ? null : ip;
|
|
}
|
|
|
|
/**
|
|
* @param {Object} req
|
|
* @returns {String}
|
|
*/
|
|
function remoteIP(req) {
|
|
let fwd = req.headers['x-forwarded-for'],
|
|
sra = req.socket.remoteAddress,
|
|
cra = req.connection.remoteAddress,
|
|
ip = isNotLocal(fwd) || sra || cra || '',
|
|
ipa = ip.split(',');
|
|
if (ip === '' || ipa.length > 1) {
|
|
helper.log({remote:ipa, fwd, sra, cra});
|
|
}
|
|
return ipa[0];
|
|
}
|
|
|
|
/**
|
|
* @param {Object} res
|
|
* @param {number} code
|
|
* @param {String} msg
|
|
*/
|
|
function quickReply(res, code, msg) {
|
|
res.writeHead(code);
|
|
res.end(msg+"\n");
|
|
}
|
|
|
|
/**
|
|
* @param {Object} req
|
|
* @param {Object} res
|
|
*/
|
|
function reply404(req, res) {
|
|
logger.emit([
|
|
'404',
|
|
req.url,
|
|
req.socket.remoteAddress,
|
|
req.headers
|
|
]);
|
|
res.writeHead(404);
|
|
res.end("[404]");
|
|
}
|
|
|
|
/**
|
|
* @param {Object} res
|
|
* @param {String} url
|
|
*/
|
|
function redirect(res, url) {
|
|
res.writeHead(302, { "Location": url });
|
|
res.end();
|
|
}
|
|
|
|
/**
|
|
* @param {Array} array
|
|
* @param {number} length
|
|
* @param {number} timespan
|
|
* @returns {number}
|
|
*/
|
|
function limit(array, length, timespan, inc) {
|
|
let now = time(),
|
|
add = inc || 1,
|
|
limit = 0;
|
|
// age out entries older than timespan
|
|
while (array.length > 0 && now-array[0] > timespan) {
|
|
array.shift();
|
|
}
|
|
// count elements over the limit
|
|
while (array.length > length) {
|
|
limit += add;
|
|
array.shift();
|
|
}
|
|
return limit;
|
|
}
|
|
|
|
function rewrite(req, res, next) {
|
|
if (req.url.indexOf(".html") > 0) {
|
|
let real_write = res.write;
|
|
let real_end = res.end;
|
|
|
|
res.write = function() {
|
|
arguments[0] = arguments[0].toString().replace(/{{version}}/g,ver.VERSION);
|
|
real_write.apply(res, arguments);
|
|
};
|
|
res.end = function() {
|
|
if (arguments[0]) {
|
|
arguments[0] = arguments[0].toString().replace(/{{version}}/g,ver.VERSION);
|
|
}
|
|
real_end.apply(res, arguments);
|
|
};
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* @param {Object} req
|
|
* @param {Object} res
|
|
* @param {Function} next
|
|
*/
|
|
function setup(req, res, next) {
|
|
let host = req.headers['host'] || '',
|
|
beta = getCookieValue(req.headers['cookie'],'beta');
|
|
|
|
if (beta === 'true' && req.url.indexOf('?prod') > 0) {
|
|
res.setHeader('Set-Cookie', 'beta=false; path=/;');
|
|
beta = 'false';
|
|
} else if (beta !== 'true' && req.url.indexOf('?beta') > 0) {
|
|
res.setHeader('Set-Cookie', 'beta=true; path=/;');
|
|
beta = 'true';
|
|
}
|
|
|
|
if (beta === 'true' && host.indexOf('beta.') !== 0) {
|
|
return redirect(res, `//beta.${host}${req.url}`);
|
|
}
|
|
|
|
let parsed = url.parse(req.url, true),
|
|
ipaddr = remoteIP(req),
|
|
dbikey = db.key(["ip",ipaddr]),
|
|
path = parsed.pathname,
|
|
time = new Date().getTime(),
|
|
rec = ipCache[ipaddr] || {
|
|
saved : 0,
|
|
first: time,
|
|
last: [],
|
|
hits: 0,
|
|
api: [],
|
|
ip: ipaddr,
|
|
},
|
|
ua = 'unknown';
|
|
|
|
try {
|
|
ua = agent.parse(req.headers['user-agent'] || '');
|
|
} catch (e) {
|
|
helper.log("ua parse error on : "+req.headers['user-agent']);
|
|
}
|
|
|
|
// cache it
|
|
if (rec.saved === 0) {
|
|
ipCache[ipaddr] = rec;
|
|
if (!rec.host) {
|
|
// prevent overlapping lookups for the same address
|
|
rec.host = 'unknown';
|
|
try {
|
|
dns.reverse(ipaddr, (err,addr) => {
|
|
rec.host = addr;
|
|
});
|
|
} catch (e) {
|
|
helper.log({dns_err: ipaddr})
|
|
}
|
|
}
|
|
}
|
|
|
|
// grid.space request state
|
|
req.gs = {
|
|
ua: ua,
|
|
ip: ipaddr,
|
|
iprec: rec,
|
|
local: ipLocal.contains(ipaddr),
|
|
port: req.socket.address().port,
|
|
url: parsed,
|
|
path: parsed.pathname,
|
|
query: parsed.query,
|
|
};
|
|
|
|
// fixup local addrs
|
|
if (req.gs.local) req.gs.ip = "::1";
|
|
|
|
// track clients & show first instance of IP
|
|
rec.last.push(time);
|
|
rec.hits++;
|
|
|
|
// log request
|
|
logger.emit([
|
|
req.method,
|
|
req.headers['host'] || '',
|
|
req.url,
|
|
req.socket.remoteAddress,
|
|
req.headers['origin'] || '',
|
|
req.headers['user-agent'] || ''
|
|
// m: req.method,
|
|
// u: req.url,
|
|
// i: ipaddr,
|
|
// h: req.headers['host'],
|
|
// o: req.headers['origin'],
|
|
// a: req.headers['user-agent']
|
|
]);
|
|
|
|
// update db ip record
|
|
if (time - rec.saved > ipSaveDelay) db.get(dbikey)
|
|
.then(dbrec => {
|
|
// only on the first pull from disk
|
|
if (dbrec && rec.saved === 0) {
|
|
rec.first = dbrec.first;
|
|
rec.hits += dbrec.hits;
|
|
rec.last = dbrec.last.appendAll(rec.last);
|
|
rec.api = dbrec.api.appendAll(rec.api);
|
|
}
|
|
rec.saved = time;
|
|
return rec;
|
|
})
|
|
.then(dbrec => {
|
|
if (rec.putTO) clearTimeout(rec.putTO);
|
|
rec.putTO = setTimeout(() => {
|
|
rec.putTO = null;
|
|
db.put(dbikey, dbrec);
|
|
}, ipSaveDelay);
|
|
})
|
|
.catch(error => {
|
|
helper.log({dbikey, error});
|
|
});
|
|
|
|
// absolute limit on client requests per minute
|
|
let rateinc = req.headers.host ? 1 : 50;
|
|
if (limit(rec.last, 300, 60000, rateinc) && !req.gs.local) {
|
|
res.writeHead(503);
|
|
res.end("rate limited");
|
|
return log({rate_limit:ipaddr, len:rec.last.length});
|
|
}
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
addCorsHeaders(req, res);
|
|
res.end();
|
|
} else {
|
|
next();
|
|
}
|
|
}
|
|
|
|
function addCorsHeaders(req, res) {
|
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
res.setHeader('Access-Control-Allow-Headers', 'X-Moto-Ajax, Content-Type');
|
|
res.setHeader("Access-Control-Allow-Origin", req.headers['origin'] || '*');
|
|
res.setHeader("Allow", "GET,POST,OPTIONS");
|
|
}
|
|
|
|
/**
|
|
* in debug mode only, serve ANY file path beginning with / that exists
|
|
*/
|
|
function handleDebug(req, res, next) {
|
|
if (debug) {
|
|
let ext = req.url.split('.').pop();
|
|
let type = {
|
|
'html': 'text/html',
|
|
'js': 'application/javascript'
|
|
}[ext] || 'application/unknown';
|
|
if (lastmod(req.url)) {
|
|
res.setHeader('Content-Type',type);
|
|
return res.end(fs.readFileSync(req.url));
|
|
} else if (lastmod(req.url.substring(1))) {
|
|
res.setHeader('Content-Type','application/javascript');
|
|
return res.end(fs.readFileSync(req.url.substring(1)));
|
|
}
|
|
}
|
|
return next();
|
|
}
|
|
|
|
/**
|
|
* meta:moto data storage and retrieval url
|
|
*
|
|
* @param {Object} req
|
|
* @param {Object} res
|
|
* @param {Function} next
|
|
*/
|
|
function handleData(req, res, next) {
|
|
addCorsHeaders(req, res);
|
|
res.setHeader('Cache-Control', 'private, no-cache, max-age=0');
|
|
|
|
let tok = req.gs.url.path.split('/'),
|
|
muid = req.headers['x-moto-ajax'],
|
|
space = tok[2] || null,
|
|
version = tok[3],
|
|
valid = space && space.length >= 4 && space.length <= 8;
|
|
|
|
function genKey() {
|
|
while (true) {
|
|
let k = Math.round(Math.random() * 9999999999).toString(36);
|
|
if (k.length >= 4 && k.length <= 8) return k;
|
|
}
|
|
}
|
|
|
|
function countKey(space) {
|
|
return db.key(['meta/counter',space]);
|
|
}
|
|
|
|
function ownerKey(space) {
|
|
return db.key(['meta/owner',muid,'space',space]);
|
|
}
|
|
|
|
function recordKey(space, version) {
|
|
return db.key(["meta/space",space,version]);
|
|
}
|
|
|
|
// retrieve latest space data
|
|
if (valid && req.method === 'GET' && valid) {
|
|
function send(rec, version) {
|
|
if (rec) {
|
|
res.write(obj2string({space:space,ver:version,rec:rec}));
|
|
res.end();
|
|
} else {
|
|
res.end();
|
|
}
|
|
}
|
|
|
|
function retrieve(version) {
|
|
return db.get(recordKey(space,version))
|
|
.then(record => {
|
|
send(record || null, version);
|
|
})
|
|
}
|
|
|
|
if (version) {
|
|
retrieve(version)
|
|
} else {
|
|
db.get(countKey(space)).then(version => retrieve(version));
|
|
}
|
|
|
|
return;
|
|
|
|
} else if (valid && req.method === 'POST') {
|
|
|
|
let dbOwner = null,
|
|
dbVersion = null,
|
|
postBody = null,
|
|
iprec = req.gs.iprec,
|
|
spacein = space,
|
|
version = 0,
|
|
body = '';
|
|
|
|
function checkDone() {
|
|
if (!(dbVersion && postBody)) return;
|
|
// if not owner, assign new space id
|
|
if (dbVersion > 1) {
|
|
if (!dbOwner) {
|
|
space = genKey();
|
|
version = 1;
|
|
log({forked:space,from:spacein,by:muid});
|
|
}
|
|
}
|
|
// log what we have
|
|
log({
|
|
space: space,
|
|
ver: dbVersion,
|
|
uid: muid,
|
|
ip: iprec.ip,
|
|
hits: iprec.hits,
|
|
size: postBody.length
|
|
});
|
|
if (muid && muid.length > 0) {
|
|
level.put(recordKey(space, dbVersion), body);
|
|
level.put(ownerKey(space), {ip: iprec.ip, time: time(), ver: dbVersion});
|
|
level.put(countKey(space), dbVersion);
|
|
}
|
|
res.end(obj2string({space: space, ver: dbVersion}));
|
|
}
|
|
|
|
// accumulate post body
|
|
req.on('data', data => { body += data });
|
|
req.on('end', () => {
|
|
postBody = body;
|
|
checkDone();
|
|
});
|
|
|
|
// fetch owner and version information
|
|
db.get(ownerKey(space))
|
|
.then(owner => {
|
|
dbOwner = owner;
|
|
return db.get(countKey(space));
|
|
})
|
|
.then(version => {
|
|
dbVersion = parseInt(version || "0") + 1;
|
|
checkDone();
|
|
});
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
function minify(path) {
|
|
let code = fs.readFileSync(path);
|
|
if (skip_minify.indexOf(path) >= 0) {
|
|
return code;
|
|
}
|
|
let mini = uglify.minify(code.toString());
|
|
if (mini.error) {
|
|
console.trace(mini.error);
|
|
throw mini.error;
|
|
}
|
|
return mini.code;
|
|
}
|
|
|
|
const clearok = [
|
|
'js/add-three.js',
|
|
'js/ext-three.js',
|
|
'js/ext-tween.js',
|
|
'js/moto-load-stl.js',
|
|
];
|
|
|
|
/**
|
|
* @param {Object} req
|
|
* @param {Object} res
|
|
* @param {Function} next
|
|
*/
|
|
function handleJS(req, res, next) {
|
|
let spath = req.gs.path.substring(1),
|
|
jspos = spath.indexOf(".js"),
|
|
fpath = jspos > 0 ? spath.substring(0,jspos+3) : spath,
|
|
cached = fileCache[fpath];
|
|
|
|
if (!(req.gs.local || debug || clearok.indexOf(fpath) >= 0)) {
|
|
return reply404(req, res);
|
|
}
|
|
|
|
if (fileMap[fpath]) {
|
|
fpath = fileMap[fpath];
|
|
}
|
|
|
|
fs.stat(fpath, (err, f) => {
|
|
if (err || !f) {
|
|
return reply404(req, res);
|
|
}
|
|
|
|
let mtime = f.mtime.getTime();
|
|
|
|
if (!cached || cached.mtime != mtime) {
|
|
if (debug) {
|
|
fs.readFile(fpath, null, function(err, code) {
|
|
if (err) {
|
|
return reply404(req,res);
|
|
}
|
|
serveCode(req, res, fileCache[fpath] = {
|
|
clear: true,
|
|
mtime: mtime,
|
|
code: code
|
|
});
|
|
});
|
|
return;
|
|
} else {
|
|
let start = new Date().getTime(),
|
|
code = minify(fpath),
|
|
end = new Date().getTime();
|
|
|
|
log({minify:fpath, in:f.size, out:code.length, time:(end-start)});
|
|
|
|
cached = fileCache[fpath] = {
|
|
clear: false,
|
|
mtime: mtime,
|
|
code: code
|
|
};
|
|
}
|
|
}
|
|
|
|
serveCode(req, res, cached);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {Object} req
|
|
* @param {Object} res
|
|
* @param {Function} next
|
|
*
|
|
* expects paths in the format "/code/[token]/the/rest/is/ignored"
|
|
*/
|
|
function handleCode(req, res, next) {
|
|
let key = req.gs.path.split('/')[2].split('.')[0],
|
|
ck = code_src[key],
|
|
js = code[key];
|
|
|
|
if (!js) {
|
|
return reply404(req, res);
|
|
}
|
|
|
|
if (ck) {
|
|
let mod = lastmod(ck.path);
|
|
if (mod > ck.mod) {
|
|
if (debug) {
|
|
js = code[ck.endpoint] = fs.readFileSync(ck.path);
|
|
} else {
|
|
js = code[ck.endpoint] = minify(ck.path);
|
|
}
|
|
ck.mod = mod;
|
|
}
|
|
}
|
|
|
|
addCorsHeaders(req, res);
|
|
serveCode(req, res, {
|
|
code: js,
|
|
mtime: startTime
|
|
});
|
|
}
|
|
|
|
function handleWasm(req, res, next) {
|
|
let [root, file] = req.gs.path.split('/').slice(1);
|
|
let ext = (file || '').split('.')[1];
|
|
let path = `wasm/${file}`;
|
|
let mod = lastmod(path);
|
|
|
|
if (root === 'wasm' && ext === 'wasm' && mod) {
|
|
let imd = ifModifiedDate(req);
|
|
if (imd && mod <= imd) {
|
|
res.writeHead(304, "Not Modified");
|
|
return res.end();
|
|
}
|
|
res.writeHead(200, {
|
|
'Content-Type': 'application/wasm',
|
|
'Cache-Control': 'public, max-age=600',
|
|
'Last-Modified': new Date(mod).toGMTString(),
|
|
});
|
|
res.end(fs.readFileSync(path));
|
|
} else {
|
|
next();
|
|
}
|
|
}
|
|
|
|
function ifModifiedDate(req) {
|
|
let ims = req.headers['if-modified-since'];
|
|
if (ims) {
|
|
// because sys time has a higher resolution than
|
|
// seconds converted from IMS header. so give it
|
|
// an extra second
|
|
return new Date(ims).getTime() + 1000;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @param {Object} req
|
|
* @param {Object} res
|
|
* @param {Obejct} code
|
|
*/
|
|
function serveCode(req, res, code) {
|
|
if (code.deny) {
|
|
return reply404(req, res);
|
|
}
|
|
|
|
let imd = ifModifiedDate(req);
|
|
if (imd && code.mtime <= imd && !code.nocache) {
|
|
res.writeHead(304, "Not Modified");
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
let cacheControl = code.nocache ?
|
|
'private, max-age=0' :
|
|
'public, max-age=600';
|
|
|
|
res.writeHead(200, {
|
|
'Content-Type': 'application/javascript',
|
|
'Cache-Control': cacheControl,
|
|
'Last-Modified': new Date(code.mtime).toGMTString(),
|
|
});
|
|
res.end(code.code);
|
|
}
|
|
|
|
/* *********************************************
|
|
* Setup / Global
|
|
********************************************* */
|
|
|
|
let debug = false,
|
|
nolocal = false,
|
|
procexit = false,
|
|
port = 8080,
|
|
args = process.argv.slice(2);
|
|
|
|
args.forEach((arg, index) => {
|
|
switch (arg) {
|
|
case 'nolocal': nolocal = true; break;
|
|
case 'debug': debug = true; break;
|
|
case 'port': port = process.argv[index+3]; break;
|
|
}
|
|
});
|
|
|
|
let ver = require('../js/license.js'),
|
|
fs = require('fs'),
|
|
url = require('url'),
|
|
dns = require('dns'),
|
|
util = require('util'),
|
|
path = require('path'),
|
|
valid = require('validator'),
|
|
agent = require('express-useragent'),
|
|
spawn = require('child_process').spawn,
|
|
level = require('level')('./persist', {valueEncoding:"json"}),
|
|
https = require('https'),
|
|
moment = require('moment'),
|
|
uglify = require('uglify-es'),
|
|
connect = require('connect'),
|
|
serveStatic = require('serve-static'),
|
|
compression = require('compression')(),
|
|
querystring = require('querystring'),
|
|
ipLocal = nolocal ? [] : ["127.0.0.1", "::1", "::ffff:127.0.0.1"],
|
|
currentDir = process.cwd(),
|
|
ipSaveDelay = 2000,
|
|
startTime = time(),
|
|
codePrefix = "js/",
|
|
fileCache = {},
|
|
fileMap = {},
|
|
filters_fdm = [],
|
|
filters_sla = [],
|
|
filters_cam = [],
|
|
filters_laser = [],
|
|
modPaths = [],
|
|
ipCache = {},
|
|
skip_minify = [
|
|
"js/ext-three.js"
|
|
],
|
|
script = {
|
|
kiri : [
|
|
"ext-three",
|
|
"license",
|
|
"ext-clip2",
|
|
"ext-tween",
|
|
"ext-fsave",
|
|
"ext-earcut",
|
|
"add-array",
|
|
"add-three",
|
|
"geo",
|
|
"geo-debug",
|
|
"geo-render",
|
|
"geo-point",
|
|
"geo-slope",
|
|
"geo-line",
|
|
"geo-bounds",
|
|
"geo-polygon",
|
|
"geo-polygons",
|
|
"geo-gyroid",
|
|
"moto-kv",
|
|
"moto-ajax",
|
|
"moto-ctrl",
|
|
"moto-space",
|
|
"moto-load-stl",
|
|
"moto-db",
|
|
"moto-ui",
|
|
"kiri-icons",
|
|
"kiri-lang",
|
|
"kiri-lang-en",
|
|
"kiri-fill",
|
|
"kiri-db",
|
|
"kiri-slice",
|
|
"kiri-slicer",
|
|
"kiri-driver-fdm",
|
|
"kiri-driver-sla",
|
|
"kiri-driver-cam",
|
|
"kiri-driver-laser",
|
|
"kiri-pack",
|
|
"kiri-layer",
|
|
"kiri-widget",
|
|
"kiri-print",
|
|
"kiri-codec",
|
|
"kiri-work",
|
|
"kiri-conf",
|
|
"kiri",
|
|
"kiri-init",
|
|
"kiri-export"
|
|
],
|
|
meta : [
|
|
"ext-three",
|
|
"license",
|
|
"ext-tween",
|
|
"ext-fsave",
|
|
"ext-earcut",
|
|
"add-array",
|
|
"add-three",
|
|
"geo",
|
|
"geo-debug",
|
|
"geo-render",
|
|
"geo-point",
|
|
"geo-slope",
|
|
"geo-line",
|
|
"geo-bounds",
|
|
"geo-polygon",
|
|
"geo-polygons",
|
|
"geo-gyroid",
|
|
"kiri-layer",
|
|
"moto-kv",
|
|
"moto-ajax",
|
|
"moto-ctrl",
|
|
"moto-space",
|
|
"moto-load-stl",
|
|
"moto-db",
|
|
"moto-ui",
|
|
"kiri-db",
|
|
"meta"
|
|
],
|
|
work : [
|
|
"ext-three",
|
|
"ext-pngjs",
|
|
"license",
|
|
"ext-clip2",
|
|
"add-array",
|
|
"add-three",
|
|
"add-class",
|
|
"geo",
|
|
// "geo-wasm",
|
|
"geo-debug",
|
|
"geo-point",
|
|
"geo-slope",
|
|
"geo-line",
|
|
"geo-bounds",
|
|
"geo-polygon",
|
|
"geo-polygons",
|
|
"geo-gyroid",
|
|
"kiri-fill",
|
|
"kiri-slice",
|
|
"kiri-slicer",
|
|
"kiri-driver-fdm",
|
|
"kiri-driver-sla",
|
|
"kiri-driver-cam",
|
|
"kiri-driver-laser",
|
|
"kiri-pack",
|
|
"kiri-widget",
|
|
"kiri-print",
|
|
"kiri-codec"
|
|
],
|
|
worker : [
|
|
"kiri-worker"
|
|
]
|
|
},
|
|
code_src = {},
|
|
code = {},
|
|
WS = require('ws'),
|
|
wss = new WS.Server({ noServer: true }),
|
|
wss_roots = {},
|
|
loads = [],
|
|
exits = [],
|
|
mods = {},
|
|
logger = open_logger({dir: ".log-main"})
|
|
;
|
|
|
|
/* *********************************************
|
|
* Promises-based leveldb interface
|
|
********************************************* */
|
|
const db = {
|
|
// --------
|
|
key: arr => arr.join("/"),
|
|
// --------
|
|
get: key => {
|
|
if (Array.isArray(key)) key = db.key(key);
|
|
return promise((resolve,reject) => {
|
|
level.get(key,(err,record) => {
|
|
resolve(record,err);
|
|
});
|
|
});
|
|
},
|
|
|
|
// --------
|
|
put: (key, value) => {
|
|
if (Array.isArray(key)) key = db.key(key);
|
|
return promise((resolve,reject) => {
|
|
level.put(key,value,(err) => {
|
|
if (err) reject(err);
|
|
else resolve();
|
|
});
|
|
});
|
|
},
|
|
// --------
|
|
del: key => {
|
|
return promise((resolve,reject) => {
|
|
level.del(key, (err) => {
|
|
if (err) reject(err);
|
|
else resolve();
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
/* *********************************************
|
|
* REST API (extensible in modules)
|
|
********************************************* */
|
|
const api = {
|
|
|
|
rateLimit: (req, res, next) => {
|
|
req.gs.iprec.api.push(time());
|
|
|
|
// allow 60 calls in 60 seconds
|
|
if (limit(req.gs.iprec.api, 60, 60000) && !req.gs.local) {
|
|
quickReply(res, 503, "rate limited");
|
|
try { log({rate_limit_api: req.gs.ip, url: req.gs.url}); } catch (e) { helper.log(e) }
|
|
} else {
|
|
next();
|
|
}
|
|
},
|
|
|
|
"filters-fdm": (req, res, next) => {
|
|
res.setHeader("Content-Type", "application/javascript");
|
|
res.end(obj2string(filters_fdm));
|
|
},
|
|
|
|
"filters-sla": (req, res, next) => {
|
|
res.setHeader("Content-Type", "application/javascript");
|
|
res.end(obj2string(filters_sla));
|
|
},
|
|
|
|
"filters-cam": (req, res, next) => {
|
|
res.setHeader("Content-Type", "application/javascript");
|
|
res.end(obj2string(filters_cam));
|
|
},
|
|
|
|
"filters-laser": (req, res, next) => {
|
|
res.setHeader("Content-Type", "application/javascript");
|
|
res.end(obj2string(filters_laser));
|
|
}
|
|
|
|
};
|
|
|
|
/* *********************************************
|
|
* Dispatch Helpers
|
|
********************************************* */
|
|
|
|
// dispatch for path prefixs
|
|
function prepath(pre) {
|
|
|
|
function handle(req, res, next) {
|
|
pre.uid = pre.uid || guid();
|
|
req.ppi = req.ppi || {};
|
|
|
|
let path = req.gs.path,
|
|
key, fn, i = req.ppi[pre.uid] || 0;
|
|
|
|
while (i < pre.length) {
|
|
key = pre[i][0];
|
|
fn = pre[i++][1];
|
|
if (path.indexOf(key) === 0) {
|
|
return fn(req, res, () => {
|
|
req.ppi[pre.uid] = i;
|
|
handle(req, res, next);
|
|
});
|
|
}
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
return handle;
|
|
}
|
|
|
|
// dispatch full fixed paths
|
|
function fullpath(map) {
|
|
return (req, res, next) => {
|
|
let fn = map[req.gs.path];
|
|
if (fn) fn(req, res, next);
|
|
else next();
|
|
};
|
|
}
|
|
|
|
// dispatch full paths based on a prefix and a function map
|
|
function fixedmap(prefix, map) {
|
|
return (req, res, next) => {
|
|
let path = req.gs.path;
|
|
if (path.indexOf(prefix) != 0) return next();
|
|
let fn = map[path.substring(prefix.length)];
|
|
if (fn) fn(req, res, next);
|
|
else next();
|
|
};
|
|
}
|
|
|
|
// HTTP 302 redirect
|
|
function redir(path) {
|
|
return (req, res, next) => redirect(res, path);
|
|
}
|
|
|
|
// mangle request path
|
|
function remap(path) {
|
|
return (req, res, next) => {
|
|
req.url = req.gs.path = path;
|
|
next();
|
|
}
|
|
}
|
|
|
|
// load module and call returned function with helper object
|
|
function initModule(file, dir) {
|
|
helper.log({module:file});
|
|
require(file)({
|
|
api: api,
|
|
adm: {
|
|
reload: prepareScripts
|
|
},
|
|
const: {
|
|
args: args,
|
|
debug: debug,
|
|
script: script,
|
|
moddir: dir,
|
|
rootdir: currentDir,
|
|
version: ver.VERSION
|
|
},
|
|
pkg: {
|
|
agent,
|
|
moment
|
|
},
|
|
mod: mods,
|
|
util: {
|
|
log: helper.log,
|
|
time: time,
|
|
guid: guid,
|
|
mkdirs: mkdirs,
|
|
lastmod: lastmod,
|
|
obj2string: obj2string,
|
|
string2obj: string2obj,
|
|
getCookieValue: getCookieValue,
|
|
logger: open_logger
|
|
},
|
|
db: {
|
|
api: db,
|
|
level: level
|
|
},
|
|
inject: (code, file, options) => {
|
|
let opt = options || {};
|
|
if (opt.end) {
|
|
script[code].push(dir + "/" + file);
|
|
} else {
|
|
script[code].splice(0, 0, dir + "/" + file);
|
|
}
|
|
},
|
|
path: {
|
|
any: arg => { modPaths.push(arg) },
|
|
pre: arg => { modPaths.push(prepath(arg)) },
|
|
map: arg => { modPaths.push(fixedmap(arg)) },
|
|
full: arg => { modPaths.push(fullpath(arg)) },
|
|
static: (root, pre) => { modPaths.push(handleStatic(root, pre)) },
|
|
code: (endpoint, path) => {
|
|
if (debug) {
|
|
code[endpoint] = fs.readFileSync(path);
|
|
} else {
|
|
code[endpoint] = minify(path);
|
|
}
|
|
code_src[endpoint] = {
|
|
endpoint,
|
|
path,
|
|
mod: lastmod(path)
|
|
};
|
|
},
|
|
redir: redir,
|
|
remap: remap
|
|
},
|
|
handler: {
|
|
addCORS: addCorsHeaders,
|
|
static: handleStatic,
|
|
redirect: redirect,
|
|
reply404: reply404,
|
|
reply: quickReply
|
|
},
|
|
ws: {
|
|
register: ws_register_root
|
|
},
|
|
onload: (fn) => {
|
|
loads.push(fn);
|
|
},
|
|
onexit: (fn) => {
|
|
exits.push(fn)
|
|
}
|
|
});
|
|
}
|
|
|
|
function handleStatic(root, pre) {
|
|
let statServe = serveStatic(root);
|
|
return function(req, res, next) {
|
|
if (pre) {
|
|
if (req.url.indexOf(pre) === 0) {
|
|
if (req.url === pre) {
|
|
req.url = "/";
|
|
} else {
|
|
req.url = req.url.substring(pre.length);
|
|
}
|
|
return statServe(req, res, next);
|
|
}
|
|
return next();
|
|
}
|
|
statServe(req, res, next);
|
|
};
|
|
}
|
|
|
|
// add static assets to be served
|
|
function addStatic(dir, pre) {
|
|
helper.log({static:dir});
|
|
modPaths.push(handleStatic(dir, pre));
|
|
}
|
|
|
|
// either add module assets to path or require(init.js)
|
|
function loadModule(dir) {
|
|
if (dir.indexOf('node_modules') >= 0) {
|
|
return;
|
|
}
|
|
if (lastmod(dir + "/.ignore")) {
|
|
return;
|
|
}
|
|
const modjs = dir + "/init.js";
|
|
lastmod(modjs) ? initModule(modjs, dir) : addStatic(dir);
|
|
}
|
|
|
|
function ws_delete_root(path) {
|
|
ws_register_root(path);
|
|
}
|
|
|
|
function ws_register_root(path, handler) {
|
|
if (handler) {
|
|
wss_roots[path] = handler;
|
|
} else {
|
|
delete wss_roots[path];
|
|
}
|
|
}
|
|
|
|
function open_logger(options) {
|
|
let opt = options || {
|
|
dir: "logs"
|
|
}
|
|
let exiting = false;
|
|
let logfile = null;
|
|
let logstream = null;
|
|
let pattern = opt.pattern || 'YY-MM-DD-HH';
|
|
let last_pattern;
|
|
let count_min = opt.min || 1000;
|
|
let count = 0;
|
|
|
|
try {
|
|
fs.mkdirSync(opt.dir);
|
|
} catch (e) { }
|
|
|
|
// create file write stream
|
|
function open_file_stream() {
|
|
close_file_stream();
|
|
logfile = path.join(opt.dir, last_pattern = moment().format(pattern));
|
|
logstream = fs.createWriteStream(logfile, {flags: 'a'});
|
|
let cur = path.join(opt.dir, "current");
|
|
try { fs.unlinkSync(cur) } catch (e) { }
|
|
try { fs.symlinkSync(last_pattern, cur) } catch (e) { }
|
|
count = 0;
|
|
}
|
|
|
|
function close_file_stream() {
|
|
if (logstream) {
|
|
logstream.end();
|
|
logstream = null;
|
|
}
|
|
}
|
|
|
|
function emit_log(obj) {
|
|
helper.log(obj);
|
|
emit(obj);
|
|
}
|
|
|
|
function emit(obj) {
|
|
if (exiting) {
|
|
// console.log({dir: opt.dir, exiting_skip_log: obj})
|
|
return;
|
|
}
|
|
let next_pattern = moment().format(pattern);
|
|
if (++count > count_min && next_pattern !== last_pattern) {
|
|
open_file_stream();
|
|
}
|
|
let output = [moment().format('YYMMDD-HHmmss'),' ',JSON.stringify(obj),'\n'].join('');
|
|
logstream.write(output);
|
|
}
|
|
|
|
function exit() {
|
|
exiting = true;
|
|
close_file_stream();
|
|
}
|
|
|
|
open_file_stream();
|
|
|
|
exits.push(exit);
|
|
|
|
return { emit, log: emit_log, close: close_file_stream };
|
|
}
|
|
|
|
function processLoad() {
|
|
while (loads.length) {
|
|
try {
|
|
loads.shift()();
|
|
} catch (e) {
|
|
log({on_load_fail: e});
|
|
}
|
|
}
|
|
}
|
|
|
|
function processExit(code) {
|
|
if (procexit) {
|
|
return;
|
|
}
|
|
procexit = true;
|
|
logger.log({proc_exit: code, registered: exits.length, uptime: time() - startTime});
|
|
while (exits.length) {
|
|
try {
|
|
exits.shift()(code);
|
|
} catch (e) {
|
|
log({on_exit_fail: e});
|
|
}
|
|
}
|
|
setTimeout(() => {
|
|
process.exit();
|
|
}, 500);
|
|
}
|
|
|
|
/* *********************************************
|
|
* Start it up
|
|
********************************************* */
|
|
|
|
// load modules
|
|
lastmod("mod") && fs.readdirSync(currentDir + "/mod").forEach(dir => {
|
|
const fullpath = currentDir + "/mod/" + dir;
|
|
if (dir.charAt(0) === '.') return;
|
|
const stats = fs.lstatSync(fullpath);
|
|
if (!(stats.isDirectory() || stats.isSymbolicLink())) return;
|
|
loadModule(fullpath);
|
|
});
|
|
|
|
// create cache dir if missing
|
|
lastmod(".cache") || mkdirs([".cache"]);
|
|
|
|
// precache responses
|
|
prepareScripts();
|
|
|
|
// create web handler chain
|
|
let handler = connect().use(setup);
|
|
|
|
// add path handlers registered by modules
|
|
modPaths.forEach(fn => {
|
|
handler = handler.use(fn);
|
|
});
|
|
|
|
// add the rest of the handler chain
|
|
handler.use(fullpath({
|
|
"/kiri/index.html" : redir("/kiri/"),
|
|
"/kiri" : redir("/kiri/"),
|
|
"/kiri/" : remap("/kiri/index.html"),
|
|
"/meta/index.html" : redir("/meta/"),
|
|
"/meta" : redir("/meta/"),
|
|
"/meta/" : remap("/meta/index.html")
|
|
}))
|
|
.use(prepath([
|
|
[ "/space", redir("/meta/")],
|
|
[ "/api/", api.rateLimit ],
|
|
[ "/data/", handleData ],
|
|
[ "/code/", handleCode ],
|
|
[ "/wasm/", handleWasm ],
|
|
[ "/js/", handleJS ]
|
|
]))
|
|
.use(handleDebug)
|
|
.use(fixedmap("/api/", api))
|
|
.use(compression)
|
|
.use(rewrite)
|
|
.use(handleStatic(currentDir + "/web/"))
|
|
.use(reply404)
|
|
.listen(port)
|
|
.on('upgrade', (request, socket, head) => {
|
|
let handler = wss_roots[request.url];
|
|
if (handler) {
|
|
wss.handleUpgrade(request, socket, head, ws => {
|
|
try {
|
|
handler(ws, request);
|
|
} catch (err) {
|
|
log({wss_handler_error: err});
|
|
ws_delete_root(request.url);
|
|
socket.destroy();
|
|
}
|
|
});
|
|
} else {
|
|
socket.destroy();
|
|
}
|
|
});
|
|
|
|
helper.log("------------------------------------------");
|
|
helper.log({port, debug, nolocal, version: ver.VERSION});
|
|
logger.emit({port, debug, nolocal, version: ver.VERSION});
|
|
|
|
process.on('beforeExit', processExit);
|
|
|
|
process.on('exit', processExit);
|
|
|
|
process.on('SIGINT', function(sig, code) {
|
|
logger.log({exit: code, signal: sig});
|
|
processExit(code);
|
|
});
|
|
|
|
process.on('SIGHUP', function(sig, code) {
|
|
logger.log({exit: code, signal: sig});
|
|
processExit(code);
|
|
});
|
|
|
|
process.on('unhandledRejection', (reason, p) => {
|
|
logger.log({unhandled_rejection: reason, promise: p});
|
|
});
|
|
|
|
process.on('uncaughtException', (err) => {
|
|
logger.log({uncaught_exception: err});
|
|
});
|
|
|
|
processLoad();
|