Files
Archive/socratex/lib/web.mjs
2024-03-05 02:32:38 -08:00

168 lines
6.2 KiB
JavaScript

import { callosum, event, utilitas } from 'utilitas';
import { render } from './log.mjs';
import {
CLRF, ERROR_CODES, HEADERS, HTTP_BODIES, HTTP_METHODS, HTTP_RESPONSES,
IDLE_CLIENT_TIMEOUT, MAX_CLIENT_COUNT
} from './consts.mjs';
const [SECURITY_LOG, BYPASS_LIST, MAX_BYPASS_COUNT]
= ['SECURITY_LOG', 'BYPASS_LIST', 1000];
const { AUTH_REQUIRED, NOT_FOUND, NOT_OK, OK, TIMED_OUT, UNAUTHORIZED, }
= HTTP_RESPONSES;
const { ENOTFOUND, EPIPE, EPROTO, ETIMEDOUT } = ERROR_CODES;
const { GET } = HTTP_METHODS;
const packResult = (status, body) => ({ status, body: body || '' });
const log = message => utilitas.log(message, import.meta.url, { time: true });
const lastVisit = date => ({ lastVisit: date ? new Date(date) : new Date() });
const getSecurityLog = async address => await getMapping(SECURITY_LOG, address);
const getBypassList = async address => await getMapping(BYPASS_LIST, address);
const getBypassHost = async () => Object.keys(await getBypassList());
const setSecurityLog = async (add, d) => await setMapping(SECURITY_LOG, add, d);
const setBypassList = async (add, d) => await setMapping(BYPASS_LIST, add, d);
const getAllMapping = () => Promise.all([getSecurityLog(), getBypassList()]);
function FindProxyForURL(url, host) {
// Socratex by @LeaskH
// https://github.com/Leask/socratex
const [local, bypass] = [[
'*.lan', '*.local', '*.internal',
'10.*.*.*', '127.*.*.*', '172.16.*.*', '192.168.*.*',
], {
/*BYPASS*/
}];
for (let item of local) { if (shExpMatch(host, item)) { return 'DIRECT'; } }
if (isPlainHostName(host) || bypass[host]) { return 'DIRECT'; }
return '/*PROXY*/';
};
const setMapping = async (key, address, date) => await callosum.assign(
key, { [address]: lastVisit(date) }
);
const getMapping = async (key, address) => {
const resp = await callosum.get(key, ...address ? [address] : []);
return resp || (address ? null : {});
};
const makePac = (bypass, proxy) => {
let [rules, pac] = [{
'/*BYPASS*/': bypass.map(x => `'${x}': 1`).join(',\n '),
'/*PROXY*/': proxy, // '; DIRECT' // @todo by @LeaskH
}, FindProxyForURL.toString()];
for (let i in rules) {
pac = pac.replace(new RegExp(RegExp.escape(i), 'ig'), rules[i]);
}
return pac;
};
const route = async (method, path, protocol, req) => {
const objUrl = new URL(path, `https://${globalThis._socratex?.domain}`);
const token = objUrl.searchParams.get('token');
const authenticated = token === globalThis._socratex?.token;
const reeStr = [
String(method).toUpperCase(), String(objUrl.pathname).toLowerCase(),
].join(' ');
switch (reeStr) {
case `${GET} /`:
return packResult(OK, '42');
case `${GET} /favicon.ico`:
return packResult(OK, '');
case `${GET} /console`:
return authenticated
? packResult(OK, await render())
: await error({ code: UNAUTHORIZED });
case `${GET} /proxy.pac`:
case `${GET} /wpad.dat`:
return authenticated ? packResult(`${OK}${CLRF}${HEADERS.PAC}`,
makePac(await getBypassHost(), _socratex.address)
) : await error({ code: UNAUTHORIZED });
default:
return await error({ code: ENOTFOUND });
}
};
const error = async (err) => {
switch (err.code) {
case ETIMEDOUT:
return packResult(TIMED_OUT);
case ENOTFOUND:
return packResult(NOT_FOUND, HTTP_BODIES.NOT_FOUND);
case EPROTO:
return packResult(NOT_OK, HTTP_BODIES.NOT_FOUND);
case UNAUTHORIZED:
return packResult(UNAUTHORIZED, HTTP_BODIES.UNAUTHORIZED);
case AUTH_REQUIRED:
return packResult(AUTH_REQUIRED, HTTP_BODIES.AUTH_REQUIRED);
case EPIPE: default:
return packResult(NOT_OK);
}
};
const init = async (options) => {
Function.isFunction(options.getStatus) && (async () => {
const config = (await utilitas.resolve(options.getStatus()))?.config;
const sLog = config?.securityLog || {};
const list = config?.bypassList || {};
for (let i in sLog) { await setSecurityLog(i, sLog[i].lastVisit); }
for (let i in list) { await setBypassList(i, list[i].lastVisit); }
const [securityLog, bypassList] = await getAllMapping();
log(`Restored ${Object.keys(securityLog).length} session(s), `
+ `${Object.keys(bypassList).length} bypass-item(s).`);
})();
const releaseClient = async (add) => {
await callosum.del(SECURITY_LOG, add);
log(`Released inactive client: ${add}.`);
};
const removeBypass = async (host) => {
await callosum.del(BYPASS_LIST, host);
log(`Remove inactive bypass-item: ${host}.`);
};
const cleanStatus = async () => {
const [now, ips, hosts] = [new Date(), [], []];
let [securityLog, bypassList] = await getAllMapping();
// clean security log
for (let a in securityLog) {
if (securityLog[a].lastVisit + IDLE_CLIENT_TIMEOUT < now) {
await releaseClient(a);
} else { ips.push([securityLog[a].lastVisit, a]); }
}
ips.sort((a, b) => a[0] - b[0]);
while (ips.length > MAX_CLIENT_COUNT) {
await releaseClient(ips.shift()[1]);
}
// clean bypass list
for (let h in bypassList) { hosts.push([bypassList[h].lastVisit, h]); }
hosts.sort((a, b) => a[0] - b[0]);
while (hosts.length > MAX_BYPASS_COUNT) {
await removeBypass(hosts.shift()[1]);
}
// save
[securityLog, bypassList] = await getAllMapping();
if (!Function.isFunction(options.setStatus)) { return; }
await options.setStatus({ securityLog, bypassList });
log(`Saved ${Object.keys(securityLog).length} session(s), `
+ `${Object.keys(bypassList).length} bypass-item(s).`);
};
return await event.loop(
cleanStatus, 60, 60, 0, utilitas.basename(import.meta.url),
{ silent: true }
);
};
export default init;
export {
error,
getBypassHost,
getBypassList,
getSecurityLog,
init,
route,
setBypassList,
setSecurityLog,
};