UNPKG

whistle.nohost

Version:

Nohost plugin for whistle

292 lines (262 loc) 7.12 kB
const crypto = require('crypto'); const net = require('net'); const qs = require('querystring'); const parseurl = require('parseurl'); const http = require('http'); const getAuth = require('basic-auth'); const ENV_MAX_AGE = 60 * 60 * 24 * 3; const CONF_KEY_RE = /^([\w-]{1,64}:?|[\w.-]{1,64}:)$/; exports.AUTH_KEY = `${Date.now()}/${Math.random()}`; exports.ENV_MAX_AGE = ENV_MAX_AGE; exports.WHISTLE_ENV_HEADER = 'x-whistle-nohost-env'; exports.CONFIG_DATA_TYPE = 'PROXY_CONFIG'; exports.COOKIE_NAME = 'whistle_nohost_env'; exports.WHISTLE_RULE_VALUE = 'x-whistle-rule-value'; exports.BASE_URL = 'http://local.whistlejs.com/plugin.nohost/'; const STRIDE = 5; let curPort = Math.floor(20001 + (10000 * Math.random())); curPort = (curPort + 1) - (curPort % STRIDE); const checkPort = (p) => { const server = http.createServer(); return new Promise((resolve, reject) => { server.on('error', reject); server.listen(p, () => { server.removeAllListeners(); server.close(() => { resolve(p); }); }); }); }; const getPort = (callback) => { curPort += STRIDE; if (curPort >= 36000) { curPort = 16001; } Promise.all([ checkPort(curPort), checkPort(curPort + 1), checkPort(curPort + 2), ]).then(() => callback(curPort), () => getPort(callback)); }; exports.getPort = getPort; /* eslint-disable no-empty */ const shasum = (str) => { if (typeof str !== 'string') { str = ''; } const result = crypto.createHash('sha1'); result.update(str); return result.digest('hex'); }; exports.shasum = shasum; const getLoginKey = (ctx, username, password) => { const ip = ctx.ip || '127.0.0.1'; return shasum(`${username || ''}\n${password || ''}\n${ip}`); }; exports.checkLogin = (ctx, authConf) => { const { username, password, nameKey, authKey, } = authConf; if (!username || !password) { return true; } const curName = ctx.cookies.get(nameKey); const lkey = ctx.cookies.get(authKey); const correctKey = getLoginKey(ctx, username, password); if (curName === username && correctKey === lkey) { return true; } /* eslint-disable prefer-const */ let { name, pass } = getAuth(ctx.req) || {}; pass = shasum(pass); if (name === username && pass === password) { const options = { expires: new Date(Date.now() + (ENV_MAX_AGE * 1000)), path: '/', }; ctx.cookies.set(nameKey, username, options); ctx.cookies.set(authKey, correctKey, options); return true; } ctx.status = 401; ctx.set('WWW-Authenticate', ' Basic realm=User Login'); ctx.set('Content-Type', 'text/html; charset=utf8'); ctx.body = 'Access denied, please <a href="javascript:;" onclick="location.reload()">try again</a>.'; return false; }; const decodeURIComponentSafe = (str) => { if (typeof str !== 'string') { return ''; } try { return decodeURIComponent(str); } catch (e) {} return str; }; exports.decodeURIComponentSafe = decodeURIComponentSafe; const parseJSON = (str) => { try { const result = JSON.parse(str); if (typeof result === 'object') { return result; } } catch (e) {} }; exports.parseJSON = parseJSON; const parseExtraData = (str) => { if (typeof str !== 'string') { return {}; } str = str.trim(); if (str) { try { return JSON.parse(str); } catch (e) {} try { return JSON.parse(decodeURIComponent(str)); } catch (e) {} try { return qs.parse(str); } catch (e) {} } return {}; }; const parseNohostConfig = (str) => { let data = parseExtraData(str).nohost; try { if (typeof data === 'string') { data = JSON.parse(data); } } catch (e) {} return data || {}; }; const transformReq = (req, port, host) => { const options = parseurl(req); options.host = host || '127.0.0.1'; options.method = req.method; options.headers = req.headers; delete options.headers.referer; delete options.protocol; delete options.hostname; if (port > 0) { options.port = port; } return new Promise((resolve, reject) => { const client = http.request(options, resolve); client.on('error', reject); req.pipe(client); }); }; exports.transformReq = transformReq; exports.transformWhistle = async (ctx, port) => { const { req } = ctx; const res = await transformReq(req, port); ctx.status = res.statusCode; ctx.set(res.headers); ctx.body = res; }; exports.parseConfig = (ctn) => { if (typeof ctn !== 'string') { return; } ctn = ctn.trim().split(/\r\n|\r|\n/g); let conf; ctn.forEach((line) => { line = line.replace(/#.*$/, '').trim().split(/\s+/); if (line.length) { let key = line[0].toLowerCase(); if (!key || !CONF_KEY_RE.test(key)) { return; } conf = conf || {}; key = key.replace(':', ''); if (key !== 'host') { key = `x-nohost-${key}`; } if (conf[key] == null && (!conf.headers || conf.headers[key] == null)) { if (key === 'host') { conf.host = line[1]; } else { conf.headers = conf.headers || {}; conf.headers[key] = line[1] || ''; } } } }); return conf; }; const KEY_RE = /(?:@([^\s@]+)|\{([^\s@]+)\})/ig; const resolveConf = (ctn, values) => { if (!ctn || !values) { return ctn; } return ctn.replace(KEY_RE, (all, $1, $2) => { return values[$1 || $2] || ''; }); }; exports.resolveConfList = (list, publicList) => { if (!list.length || !publicList || !publicList.length) { return list; } const map = {}; publicList.forEach((conf) => { map[conf.name] = conf.rules || ''; }); list.forEach((conf) => { conf.rules = resolveConf(conf.rules, map); }); return list; }; let REQ_FROM_HEADER; let RULE_VALUE_HEADER; exports.initPlugin = (options) => { REQ_FROM_HEADER = options.REQ_FROM_HEADER; RULE_VALUE_HEADER = options.RULE_VALUE_HEADER; exports.config = options.config; exports.pluginConfig = parseNohostConfig(options.config.extra); const hosts = options.config.pluginHosts.nohost || []; if (hosts[0]) { exports.BASE_URL = `http://${hosts[0]}/`; } }; exports.getRuleValue = (ctx) => { const ruleValue = ctx.get(RULE_VALUE_HEADER); return ruleValue ? decodeURIComponent(ruleValue) : ''; }; exports.isFromComposer = (ctx) => { return ctx.get(REQ_FROM_HEADER) === 'W2COMPOSER'; }; exports.getDomain = function (hostname) { if (net.isIP(hostname)) { return hostname; } let list = hostname.split('.'); let len = list.length; if (len < 3) { return hostname; } let wildcard = len > 3; if (wildcard) { list = list.slice(-3); } if (list[1].length > 3 || list[1] === 'url' || list[2] === 'com') { wildcard = true; list = list.slice(-2); } if (wildcard) { list.unshift(''); } return list.join('.'); }; exports.getClientId = (ctx) => { const clientId = ctx.get('x-whistle-nohost-client-id') || ctx.get('x-whistle-client-id'); if (clientId && typeof clientId === 'string') { return (clientId.trim() || ctx.ip).substring(0, 100); } return ctx.ip; };