@cliz/inlets
Version:
Cloud Native Tunnel
165 lines (164 loc) • 7.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const doreamon_1 = require("@zodash/doreamon");
const utils_1 = require("../../../utils");
const debug = require('debug')('inlets:tunnel:tcp');
const logger = doreamon_1.default.logger.getLogger('tunnel:http');
function createServer(ctx, options) {
const domain = options.domain;
const subDomainRe = new RegExp(`([^.]+).${domain}`);
let requestCount = 0;
const attach = (server) => {
server.on('connection', (tcpSocket) => {
const socketConfig = {
tcpId: doreamon_1.default.uuid(),
domain: null,
subDomain: null,
isWs: false,
clientId: null,
};
const request = async (tcpId, data, callback) => {
var _a;
requestCount += 1;
if (socketConfig.isWs) {
return;
}
if (!socketConfig.domain) {
const req = new utils_1.Request(data);
socketConfig.domain = req.get('host');
socketConfig.req = req;
if (req.path === ctx.config.wsPath) {
socketConfig.isWs = true;
return;
}
if (socketConfig.domain) {
const matched = socketConfig.domain.match(subDomainRe);
if (matched) {
socketConfig.subDomain = matched[1];
}
}
}
const requestLog = `[${socketConfig.subDomain}.${domain}][request: ${requestCount}] ${(_a = socketConfig.req) === null || _a === void 0 ? void 0 : _a.getRequestLog()}`;
if (!socketConfig.subDomain) {
logger.info(`[404]${requestLog}`);
return destroy(tcpSocket);
}
const wt = ctx.domainMappings.get(socketConfig.subDomain);
let domainSocket = null;
if (!!wt) {
domainSocket = wt.wsSocket;
wt.tcpSocket = tcpSocket;
}
debug('mapping:', socketConfig.domain, socketConfig.subDomain, !!domainSocket);
if (!domainSocket) {
logger.info(`[404]${requestLog}`);
return destroy(tcpSocket);
}
logger.info(requestLog);
const requestId = doreamon_1.default.uuid() + '@' + new Date().getTime();
const id = tcpId + ':' + requestId;
const clientId = domainSocket.clientId || null;
socketConfig.clientId = clientId;
const requestBytes = Buffer.byteLength(data, 'utf8');
if (!ctx.bandwidthLimiter.checkUpload(clientId, requestBytes)) {
logger.warn(`[${socketConfig.subDomain}] Upload bandwidth limit exceeded for client: ${clientId}`);
destroy(tcpSocket);
return;
}
ctx.trafficStats.addRequest(clientId);
ctx.trafficStats.addUploadBytes(clientId, requestBytes);
const adapter = domainSocket.adapter;
const useNewProtocol = !!domainSocket.useNewProtocol;
if (useNewProtocol && adapter) {
const dataBuffer = Buffer.from(data);
await adapter.sendHTTPRequest(id, dataBuffer);
}
else {
const _data = await utils_1.dataProcessor.server.request(data);
domainSocket.emit('request', {
id,
data: _data,
});
}
const wrappedCallback = (responseData) => {
try {
const responseBytes = Buffer.from(responseData, 'base64').length;
if (!ctx.bandwidthLimiter.checkDownload(clientId, responseBytes)) {
logger.warn(`[${socketConfig.subDomain}] Download bandwidth limit exceeded for client: ${clientId}`);
destroy(tcpSocket);
return;
}
ctx.trafficStats.addDownloadBytes(clientId, responseBytes);
}
catch (err) {
debug('Failed to decode response data for stats:', err);
const responseBytes = Buffer.byteLength(responseData, 'utf8');
if (!ctx.bandwidthLimiter.checkDownload(clientId, responseBytes)) {
logger.warn(`[${socketConfig.subDomain}] Download bandwidth limit exceeded for client: ${clientId}`);
destroy(tcpSocket);
return;
}
ctx.trafficStats.addDownloadBytes(clientId, responseBytes);
}
callback(responseData);
};
ctx.callbackContainer.set(tcpId, requestId, wrappedCallback);
};
tcpSocket.on('data', data => {
const _data = data.toString();
request(socketConfig.tcpId, _data, (data) => {
if (tcpSocket.writable) {
let buffer;
try {
buffer = Buffer.from(data, 'base64');
}
catch (err) {
debug('Buffer.from error:', err, 'data(base64):', data);
logger.error('[Base64DecodeError] Failed to decode base64 data for subDomain:', socketConfig.subDomain, 'domain:', socketConfig.domain, 'data(base64):', data, 'error:', err);
return destroy(tcpSocket);
}
debug('response:', buffer.toString('utf8'));
const writeResult = tcpSocket.write(buffer, (err) => {
if (err) {
debug('tcpSocket.write error:', err, 'subDomain:', socketConfig.subDomain, 'domain:', socketConfig.domain);
logger.error('[SocketWriteError] tcpSocket.write error for subDomain:', socketConfig.subDomain, 'domain:', socketConfig.domain, 'error:', err);
destroy(tcpSocket);
}
});
if (!writeResult) {
tcpSocket.once('drain', () => {
debug('tcpSocket drained, ready for more data');
});
}
}
});
});
tcpSocket.on('close', () => {
ctx.callbackContainer.remove(socketConfig.tcpId);
if (socketConfig.clientId) {
const statsInfo = ctx.trafficStats.formatStats(socketConfig.clientId);
const subDomainInfo = socketConfig.subDomain ? `[${socketConfig.subDomain}]` : '';
logger.info(`[tunnel:http]${subDomainInfo} connection closed - Traffic Stats: ${statsInfo}`);
}
});
});
};
return {
attach,
};
}
exports.default = createServer;
function destroy(tcpSocket) {
const data = [
'HTTP/1.1 404 Not Found',
'Content-Type: text/plain; charset=utf-8',
'Content-Length: 9',
`Date: ${new Date().toUTCString()}`,
'Connection: keep-alive',
'Keep-Alive: timeout=5',
'',
'Not Found'
];
tcpSocket.write(data.join('\r\n'));
tcpSocket.destroy();
}