UNPKG

whistle.nohost

Version:

Nohost plugin for whistle

443 lines (408 loc) 11.1 kB
const getValue = require('lodash.get'); const { isIP } = require('net'); const { parseJSON, shasum } = require('./util'); const MAX_ACCOUNT_COUNT = 120; const MAX_PASSWORD_LENGTH = 24; const KEY_RE = /^@([\w.-]{1,64})(\/[^\s]+)?$/mg; const NAME_RE = /^[\w.-]{1,24}$/; const CRLF_RE = /\s*[\r\n]+\s*/g; const CONFIG_RE = /^([\w-]{1,64}:?|[\w.-]{1,64}:)(?:\s+([\w.:/-]*[\w-]))?$/mg; const ACCOUNT_NAME_VAR = /\$\{name\}/g; const VAR_RE = /\$\{([\w.]+)\}/g; const JSON_DATA_LEN = 30720; const RULES_TPL_LEN = 3072; const DEFAULT_RULES_LEN = 5120; const NOHOST_VAR_RE = /^nohost_\w+$/; const AUTH_KEY_RE = /^[\w.@-]{1,32}$/; const SPECIAL_CHAR_RE = /[.$]/g; const VALUE_RE = /(?:^[^\n\r\S]*(```+)[^\n\r\S]*\S+[^\n\r\S]*[\r\n][\s\S]+?[\r\n][^\n\r\S]*\1\s*|#.*)$/mg; let accoutMap; let storage; const trimRules = (rules) => { return rules.trim().replace(VALUE_RE, all => (all[0] === '#' ? '' : all)).replace(CRLF_RE, '\n'); }; const parseRule = (rules, defaultRules, testRules, varRegExp, keyValueMap) => { const headers = {}; rules = trimRules(rules).replace(CONFIG_RE, (_, key, value) => { if (value) { if (key.slice(-1) === ':') { key = key.slice(0, -1); } key = `x-nohost-${key}`; if (!headers[key]) { headers[key] = value; } } return ''; }).trim(); const env = headers['x-nohost-env']; delete headers['x-nohost-env']; rules = [rules]; if (testRules && env !== 'prod' && env !== 'production' && Object.keys(headers).length) { rules.push(testRules); } if (defaultRules) { rules.push(defaultRules); } rules = trimRules(rules.join('\n')); if (keyValueMap) { rules = rules.replace(varRegExp, (all, space, key) => { return `${space || ''}${keyValueMap[key]}`; }); } return { rules, headers }; }; const getString = (data) => { const type = typeof data; if (type === 'number') { return String(data); } return typeof data === 'string' ? data : ''; }; const getStringProperty = (name) => { return getString(storage.getProperty(name)); }; class AccountMgr { constructor() { this.parseJsonData(getStringProperty('jsonData')); this.parseRules(); } parseTpl(name) { let { pureRulesTpl, jsonData } = this; if (!pureRulesTpl) { return ''; } pureRulesTpl = pureRulesTpl.replace(ACCOUNT_NAME_VAR, name); pureRulesTpl = pureRulesTpl.replace(VAR_RE, (all, varName) => getString(getValue(jsonData, varName))); return pureRulesTpl; } parseRules() { const result = {}; const accountList = this.loadAllAccounts(); const list = []; const rulesTpl = getStringProperty('rulesTpl'); let defaultRules = getStringProperty('defaultRules'); let testRules = getStringProperty('testRules'); this.rulesTpl = rulesTpl; this.entryRules = getStringProperty('entryRules'); this.pureRulesTpl = trimRules(rulesTpl); this.defaultRules = defaultRules; this.testRules = testRules; this.pluginRules = getStringProperty('pluginRules'); this.parsedMap = {}; accoutMap = {}; accountList.forEach((account) => { const { name, envList, active } = account; accoutMap[name] = account; if (!active) { return; } envList.forEach((env) => { const rules = trimRules(`${env.rules}`); const key = `${name}/${env.name}`; env = { key, rules }; result[key] = rules; list.push(env); }); }); const dependencies = {}; list.forEach((env) => { const { key, rules } = env; if (!rules) { return; } result[key] = trimRules(rules).replace(KEY_RE, (all, name, envName) => { envName = envName && envName.slice(1); if (!envName) { return this.parseTpl(name); } all = `${name}/${envName}`; if (all === key || result[all] == null) { return ''; } const deps = dependencies[key]; if (deps) { const index = deps.indexOf(all); if (index === -1) { deps.push(all); } } else { dependencies[key] = [all]; } return all; }); }); list.forEach((env) => { const { key } = env; const allDeps = { [key]: 1 }; const parseDeps = (k) => { let rules = result[k]; if (!rules) { allDeps[k] = 1; return ''; } const deps = dependencies[k]; if (deps) { deps.forEach((d) => { const exists = allDeps[d]; rules = rules.split(d); if (!exists) { allDeps[d] = 1; rules.splice(rules.length - 1, 0, parseDeps(d)); } rules = rules.join(''); }); } return rules; }; env.rules = parseDeps(key); }); defaultRules = trimRules(defaultRules); testRules = trimRules(testRules); list.forEach((env) => { this.parsedMap[env.key] = parseRule(env.rules, defaultRules, testRules, this.varRegExp, this.keyValueMap); }); } parseJsonData(data) { this.jsonDataStr = data; data = parseJSON(data) || {}; this.jsonData = data; this.keyValueMap = null; this.varRegExp = null; const regExp = []; Object.keys(data).forEach((key) => { const value = data[key]; if (value) { const isVar = NOHOST_VAR_RE.test(key); if (isVar || (isIP(key) === 4 && isIP(value) === 4)) { this.keyValueMap = this.keyValueMap || {}; key = `${isVar ? '$' : ''}${key}`; this.keyValueMap[key] = value; regExp.push(`${key.replace(SPECIAL_CHAR_RE, '\\$&')}`); } } }); if (this.keyValueMap) { this.varRegExp = new RegExp(`(^|\\s)(${regExp.join('|')})\\b`, 'mg'); } } setJsonData(str) { str = getString(str); if (str.length > JSON_DATA_LEN) { return; } this.parseJsonData(str); storage.setProperty('jsonData', str); this.parseRules(); } setRulesTpl(str) { str = getString(str); if (str.length > RULES_TPL_LEN) { return; } storage.setProperty('rulesTpl', str); this.parseRules(); } setEntryRules(str) { str = getString(str); if (str.length > DEFAULT_RULES_LEN) { return; } storage.setProperty('entryRules', str); this.parseRules(); } setDefaultRules(str) { str = getString(str); if (str.length > DEFAULT_RULES_LEN) { return; } storage.setProperty('defaultRules', str); this.parseRules(); } setTestRules(str) { str = getString(str); if (str.length > DEFAULT_RULES_LEN) { return; } storage.setProperty('testRules', str); this.parseRules(); } setPluginRules(str) { str = getString(str); if (str.length > DEFAULT_RULES_LEN) { return; } storage.setProperty('pluginRules', str); this.pluginRules = str; } loadAllAccounts() { let count = MAX_ACCOUNT_COUNT; const result = []; storage.getFileList().some((file) => { const account = parseJSON(file.data); if (!account || !this.checkName(account.name) || this.checkPassword(account.password) || !Array.isArray(account.envList)) { return false; } if (--count < 0) { return true; } result.push(account); return false; }); return result; } addEnvList(accountName, envList) { const account = this.getAccount(accountName); if (!account) { return false; } const list = []; envList.forEach(({ name, data }) => { if (name) { list.push({ name, rules: data, }); } }); account.envList = list; this.saveAccount(account); this.parseRules(); return true; } checkName(name) { return typeof name === 'string' && NAME_RE.test(name); } checkPassword(password) { if (typeof password !== 'string') { return false; } const len = password.length; return len > 0 && len <= MAX_PASSWORD_LENGTH; } changePassword(name, password) { if (!this.checkPassword(password)) { return false; } const account = this.getAccount(name); if (!account) { return false; } account.password = shasum(password); this.saveAccount(account); return true; } addAccount(account) { const { name, password } = account; if (!this.checkName(name) || !this.checkPassword(password) || storage.existsFile(name)) { return false; } if (Object.keys(accoutMap).length >= MAX_ACCOUNT_COUNT) { return false; } account = { name, password: shasum(password), active: true, envList: [], }; this.saveAccount(account); this.parseRules(); return true; } moveAccount(fromName, toName) { return storage.moveTo(fromName, toName); } removeAccount(name) { const account = this.getAccount(name); if (account) { delete accoutMap[name]; storage.removeFile(name); } this.parseRules(); return true; } activeAccount(name, active) { const account = this.getAccount(name); if (!account) { return false; } account.active = active; this.saveAccount(account); this.parseRules(); return true; } saveAccount(account) { storage.writeFile(account.name, JSON.stringify(account)); } getAccount(name) { return this.checkName(name) ? accoutMap[name] : null; } getRules(name, envName) { return this.parsedMap[`${name}/${envName}`] || ''; } getAllAccounts() { const accountList = []; this.loadAllAccounts().forEach((account) => { account = this.getAccount(account.name); if (account) { accountList.push({ name: account.name, active: account.active, }); } }); return accountList; } getAccountList(parsed) { const list = []; const { parsedMap } = this; this.loadAllAccounts().forEach((account) => { const accountName = account.name; account = this.getAccount(accountName); if (!account || !account.active) { return; } const envList = []; account.envList.forEach(({ name, rules }) => { if (!name) { return; } if (parsed === '1') { rules = parsedMap[`${accountName}/${name}`]; } else if (parsed !== '0') { rules = undefined; } envList.push({ name, id: encodeURIComponent(name), rules, }); }); list.push({ name: accountName, active: account.active, envList, }); }); return list; } getAuthKey() { return storage.getProperty('authKey') || ''; } setAuthKey(authKey) { authKey = authKey || ''; if (!AUTH_KEY_RE.test(authKey)) { return; } storage.setProperty('authKey', authKey); } } module.exports = (s) => { storage = s; module.exports = new AccountMgr(); return module.exports; };