@moquyun/proxy
Version:
Multi-user & multi-env web debugging proxy based on whistle
203 lines (191 loc) • 6.16 kB
JavaScript
const assert = require('assert');
const { createServer } = require('http');
const { onClose } = require('@nohost/router');
const handleUIRequest = require('./main');
const { passToWhistle } = require('./main/util');
const { isUIRequest, setAdmin, checkDomain } = require('./main/storage');
const NOHOST_ENV = 'x-whistle-nohost-env';
const NOHOST_RULE = 'x-whistle-nohost-rule';
const NOHOST_VALUE = 'x-whistle-nohost-value';
const WHISTLE_RULE = 'x-whistle-rule-value';
const WHISTLE_VALUE = 'x-whistle-key-value';
const WORKER_RE = /^\$(\d+)?$/;
const DEFAULT_PORT = 8080;
const LOCALHOST = '127.0.0.1';
const HOST_RE = /^(?:([\w.-]+):)?([1-9]\d{0,4})$/;
const HOST_LIST_RE = /^(?:([\w.-]+):)?([1-9]\d{0,4}(?:[-~][1-9]\d{0,4})?(?:,[1-9]\d{0,4}(?:[-~][1-9]\d{0,4})?)*)$/;
const HTTP_RE = /^\w+\s+\S+\s+HTTP\/1.\d$/mi;
const resolveHost = (host) => {
return HOST_RE.test(host) ? {
host: RegExp.$1,
port: parseInt(RegExp.$2, 10),
} : {};
};
const resolveHostList = (str) => {
if (!str || typeof str !== 'string') {
return;
}
str = str.split(/\||;/);
let list;
str.forEach((host) => {
if (!HOST_LIST_RE.test(host)) {
return;
}
host = RegExp.$1 || LOCALHOST;
RegExp.$2.split(',').forEach((port) => {
if (/^([1-9]\d{0,4})[-~]([1-9]\d{0,4})$/.test(port)) {
const p1 = parseInt(RegExp.$1, 10);
const p2 = parseInt(RegExp.$2, 10);
const max = Math.min(128, p2 - p1 + 1);
for (let i = 0; i < max; ++i) {
list = list || [];
list.push({ host, port: p1 + i });
}
} else {
list = list || [];
list.push({ host, port: parseInt(port, 10) });
}
});
});
return list;
};
const sendEstablished = (socket, err) => {
const msg = err ? 'Bad Gateway' : 'Connection Established';
const body = String((err && err.stack) || '');
socket.write([
`HTTP/1.1 ${err ? 502 : 200} ${msg}`,
`Content-Length: ${Buffer.byteLength(body)}`,
'Proxy-Agent: nohost/server',
'\r\n',
].join('\r\n'));
};
const CLIENT_ID_HEAD = 'x-whistle-client-id';
const CLIENT_PORT_HEAD = 'x-whistle-client-port';
const CLIENT_IP_HEAD = 'x-forwarded-for';
const PROXY_AUTH_HEAD = 'proxy-authorization';
const ID_HEAD = `x-whistle-.-${Math.random()}`;
const restoreClientInfo = function(req) {
const { headers, socket } = req;
let { clientInfo } = socket;
if (!clientInfo) {
clientInfo = headers[ID_HEAD];
if (clientInfo) {
delete headers[ID_HEAD];
try {
clientInfo = clientInfo && JSON.parse(decodeURIComponent(clientInfo));
req.socket.clientInfo = clientInfo;
} catch (e) {
return;
}
}
}
if (clientInfo) {
const {
clientIp,
clientPort,
clientId,
auth,
} = clientInfo;
if (clientIp) {
headers[CLIENT_IP_HEAD] = clientIp;
}
if (clientPort) {
headers[CLIENT_PORT_HEAD] = clientPort;
}
if (clientId) {
headers[CLIENT_ID_HEAD] = clientId;
}
if (auth) {
headers[PROXY_AUTH_HEAD] = auth;
}
}
};
const addClientInfo = (socket, chunk, statusLine) => {
chunk = chunk.slice(Buffer.byteLength(statusLine));
const { remoteAddress, remotePort, headers } = socket;
const auth = headers[PROXY_AUTH_HEAD];
const clientIp = headers[CLIENT_IP_HEAD] || remoteAddress;
const clientPort = headers[CLIENT_PORT_HEAD] || remotePort;
const clientId = headers[CLIENT_ID_HEAD];
const clientInfo = JSON.stringify({ auth, clientIp, clientPort, clientId });
statusLine += `\r\n${ID_HEAD}: ${encodeURIComponent(clientInfo)}`;
socket.emit('data', Buffer.concat([Buffer.from(statusLine), chunk]));
};
module.exports = (options, cb) => {
const { username, password, cluster } = options;
if (username || password) {
if (username !== '+') {
assert(username && typeof username === 'string', 'username is required.');
assert(/^[\w.-]{1,32}$/.test(username), 'username is incorrect (/^[\\w.-]{1,32}$/).');
assert(password && typeof password === 'string', 'password is required.');
setAdmin({ username, password });
}
}
const { host, port } = resolveHost(options.port);
options.host = host;
options.port = port || DEFAULT_PORT;
options.storage = resolveHostList(options.storage);
options.portStr = `${options.port}`;
const { nohostDomain } = options;
delete options.nohostDomain;
options.domain = checkDomain(nohostDomain) ? nohostDomain.toLowerCase() : '';
const handleRequest = (req, res) => {
const { headers } = req;
if (!cluster && WORKER_RE.test(headers[NOHOST_ENV])) {
if (headers[NOHOST_RULE]) {
headers[WHISTLE_RULE] = headers[NOHOST_RULE];
}
if (headers[NOHOST_VALUE]) {
headers[WHISTLE_VALUE] = headers[NOHOST_VALUE];
}
passToWhistle(req, res, RegExp.$1);
} else {
passToWhistle(req, res);
}
};
const server = createServer((req, res) => {
onClose(res);
++options.totalReqs;
if (isUIRequest(req)) {
++options.uiReqs;
restoreClientInfo(req);
const { headers } = req;
delete headers.referer;
// 强制替换域名
headers['x-whistle-real-host'] = 'local.whistlejs.com';
headers.host = 'local.whistlejs.com';
handleUIRequest(req, res);
} else {
handleRequest(req, res);
}
});
server.timeout = 360000;
server.on('upgrade', (req, socket) => {
onClose(socket);
++options.upgradeReqs;
++options.totalReqs;
handleRequest(req, socket);
});
server.on('connect', async (req, socket) => {
onClose(socket);
++options.tunnelReqs;
++options.totalReqs;
if (isUIRequest(req, true)) {
sendEstablished(socket);
socket.headers = req.headers;
socket.once('data', (chunk) => {
if (HTTP_RE.test(`${chunk}`)) {
const statusLine = RegExp['$&'];
server.emit('connection', socket);
addClientInfo(socket, chunk, statusLine);
} else {
socket.destroy();
}
});
} else {
handleRequest(req, socket);
}
});
server.listen(options.port, host, cb);
return server;
};