whistle.nohost
Version:
Nohost plugin for whistle
292 lines (262 loc) • 7.12 kB
JavaScript
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;
};