@cliz/inlets
Version:
Cloud Native Tunnel
131 lines (130 loc) • 5.68 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const cli_1 = require("@cliz/cli");
const net = require("net");
const debug = require('debug')('inlets:tunnel:tcp');
const logger = cli_1.doreamon.logger.getLogger('tunnel:tcp ');
async function createServer(ctx, options) {
const { containerId, domain } = options;
const container = ctx.container.get(containerId);
if (!container) {
throw new Error(`unknown container with id: ${containerId}`);
}
if (!container.wsSocket) {
throw new Error(`container wsSocket is required with id: ${containerId}`);
}
const sourcePort = container.sourcePort || await cli_1.api.network.getAvailablePort();
ctx.container.set(containerId, 'sourcePort', sourcePort);
container.wsSocket.emit('tcp:ready', {
host: domain,
port: sourcePort,
});
createSourceTCPServer(domain, sourcePort, (server) => {
container.sourceServer = server;
}, (sourceSocket) => {
ctx.container.registerRequest(containerId, sourceSocket.requestId, sourceSocket);
const adapter = container.wsSocket.adapter;
const useNewProtocol = !!container.wsSocket.useNewProtocol;
if (useNewProtocol && adapter) {
const streamId = `${containerId}:${sourceSocket.requestId}`;
setupTCPStreamOverWebSocket(ctx, containerId, streamId, sourceSocket, adapter, container.wsSocket);
}
container.wsSocket.emit('tcp:connect', {
id: containerId,
requestId: sourceSocket.requestId,
ip: sourceSocket.remoteAddress,
});
});
}
exports.default = createServer;
function createSourceTCPServer(domain, port, onServerCreate, onSocketConnect) {
const server = net.createServer(socket => {
const requestId = cli_1.doreamon.uuid();
socket.requestId = requestId;
logger.info(`[tunnel:tcp ][user][request][start] request id: ${socket.requestId}, ip: ${socket.remoteAddress}`);
socket.on('error', error => {
logger.error(`socket error:`, error);
});
socket.on('end', () => {
logger.info(`[tunnel:tcp ][user][request][end] request id: ${socket.requestId}, ip: ${socket.remoteAddress}`);
});
onSocketConnect(socket);
});
server.listen(port, '0.0.0.0', () => {
logger.info(`[tunnel:tcp ] listen at 0.0.0.0:${port}`);
onServerCreate(server);
});
}
function setupTCPStreamOverWebSocket(ctx, containerId, streamId, sourceSocket, adapter, wsSocket) {
const container = ctx.container.get(containerId);
const clientId = (container === null || container === void 0 ? void 0 : container.clientId) || null;
ctx.trafficStats.addConnection(clientId);
sourceSocket.on('data', async (data) => {
try {
if (!ctx.bandwidthLimiter.checkUpload(clientId, data.length)) {
logger.warn(`[${streamId}] Upload bandwidth limit exceeded for client: ${clientId}`);
await new Promise(resolve => setTimeout(resolve, 100));
if (!ctx.bandwidthLimiter.checkUpload(clientId, data.length)) {
logger.error(`[${streamId}] Upload bandwidth limit still exceeded, dropping data`);
return;
}
}
ctx.trafficStats.addUploadBytes(clientId, data.length);
await adapter.sendTCPData(streamId, data);
}
catch (error) {
logger.error(`[${streamId}] Failed to send TCP data over WebSocket:`, error);
sourceSocket.destroy();
}
});
const unsubscribe = adapter.onTCPData(async (id, data) => {
if (id !== streamId)
return;
try {
if (!ctx.bandwidthLimiter.checkDownload(clientId, data.length)) {
logger.warn(`[${streamId}] Download bandwidth limit exceeded for client: ${clientId}`);
await new Promise(resolve => setTimeout(resolve, 100));
if (!ctx.bandwidthLimiter.checkDownload(clientId, data.length)) {
logger.error(`[${streamId}] Download bandwidth limit still exceeded, dropping data`);
return;
}
}
ctx.trafficStats.addDownloadBytes(clientId, data.length);
if (sourceSocket.writable) {
const writeResult = sourceSocket.write(data);
if (!writeResult) {
sourceSocket.once('drain', () => {
debug(`[${streamId}] Source socket drained`);
});
}
}
}
catch (error) {
logger.error(`[${streamId}] Failed to write to source socket:`, error);
sourceSocket.destroy();
}
});
sourceSocket.on('close', () => {
debug(`[${streamId}] Source socket closed, cleaning up`);
const statsInfo = ctx.trafficStats.formatStats(clientId);
logger.info(`[tunnel:tcp ][${streamId}] connection closed - Traffic Stats: ${statsInfo}`);
unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
});
sourceSocket.on('error', (error) => {
logger.error(`[${streamId}] Source socket error:`, error);
});
}
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();
}