feat(gateway): include production Telegram user gateway
Version the NestJS TDLib gateway with Docker deployment, migrations, automated tests and platform integration documentation while excluding all account sessions, credentials, runtime databases and backups.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
const net = require('net');
|
||||
const http = require('http');
|
||||
|
||||
const listenPort = Number(process.env.SOCKS5_PORT || 1080);
|
||||
const metricsPort = Number(process.env.SOCKS5_METRICS_PORT || 1081);
|
||||
const stats = { accepted: 0, connected: 0, failed: 0, active: 0 };
|
||||
|
||||
function reply(socket, code, addressType = 1) {
|
||||
const address = addressType === 4 ? Buffer.alloc(16) : Buffer.alloc(4);
|
||||
socket.write(Buffer.concat([
|
||||
Buffer.from([5, code, 0, addressType]),
|
||||
address,
|
||||
Buffer.from([0, 0]),
|
||||
]));
|
||||
}
|
||||
|
||||
function consumeRequest(socket, state) {
|
||||
if (state.phase === 'greeting') {
|
||||
if (state.buffer.length < 2) return;
|
||||
const methodsLength = state.buffer[1];
|
||||
if (state.buffer.length < 2 + methodsLength) return;
|
||||
const methods = state.buffer.subarray(2, 2 + methodsLength);
|
||||
state.buffer = state.buffer.subarray(2 + methodsLength);
|
||||
if (state.buffer[0] !== undefined && state.buffer[0] !== 5) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
if (!methods.includes(0)) {
|
||||
socket.end(Buffer.from([5, 255]));
|
||||
return;
|
||||
}
|
||||
socket.write(Buffer.from([5, 0]));
|
||||
state.phase = 'request';
|
||||
}
|
||||
|
||||
if (state.phase !== 'request' || state.buffer.length < 4) return;
|
||||
const [version, command, , addressType] = state.buffer;
|
||||
if (version !== 5 || command !== 1) {
|
||||
reply(socket, command === 1 ? 1 : 7);
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
let offset = 4;
|
||||
let host;
|
||||
if (addressType === 1) {
|
||||
if (state.buffer.length < offset + 4 + 2) return;
|
||||
host = Array.from(state.buffer.subarray(offset, offset + 4)).join('.');
|
||||
offset += 4;
|
||||
} else if (addressType === 3) {
|
||||
if (state.buffer.length < offset + 1) return;
|
||||
const length = state.buffer[offset++];
|
||||
if (state.buffer.length < offset + length + 2) return;
|
||||
host = state.buffer.subarray(offset, offset + length).toString('utf8');
|
||||
offset += length;
|
||||
} else if (addressType === 4) {
|
||||
if (state.buffer.length < offset + 16 + 2) return;
|
||||
const words = [];
|
||||
for (let index = 0; index < 16; index += 2) {
|
||||
words.push(state.buffer.readUInt16BE(offset + index).toString(16));
|
||||
}
|
||||
host = words.join(':');
|
||||
offset += 16;
|
||||
} else {
|
||||
reply(socket, 8);
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const port = state.buffer.readUInt16BE(offset);
|
||||
state.buffer = state.buffer.subarray(offset + 2);
|
||||
state.phase = 'connecting';
|
||||
|
||||
const upstream = net.createConnection({ host, port });
|
||||
upstream.setTimeout(30000);
|
||||
upstream.once('connect', () => {
|
||||
stats.connected += 1;
|
||||
stats.active += 1;
|
||||
console.log(JSON.stringify({ event: 'connect', host, port, addressType }));
|
||||
reply(socket, 0, addressType === 4 ? 4 : 1);
|
||||
if (state.buffer.length) upstream.write(state.buffer);
|
||||
socket.pipe(upstream);
|
||||
upstream.pipe(socket);
|
||||
});
|
||||
upstream.once('error', (error) => {
|
||||
stats.failed += 1;
|
||||
console.error(JSON.stringify({ event: 'connect_error', host, port, code: error.code }));
|
||||
if (!socket.destroyed) {
|
||||
reply(socket, error.code === 'ECONNREFUSED' ? 5 : 4);
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
upstream.once('close', () => {
|
||||
if (stats.active > 0) stats.active -= 1;
|
||||
});
|
||||
socket.once('close', () => upstream.destroy());
|
||||
}
|
||||
|
||||
const server = net.createServer((socket) => {
|
||||
stats.accepted += 1;
|
||||
const state = { phase: 'greeting', buffer: Buffer.alloc(0) };
|
||||
socket.setTimeout(30000);
|
||||
socket.on('data', (chunk) => {
|
||||
if (state.phase === 'connecting') return;
|
||||
state.buffer = Buffer.concat([state.buffer, chunk]);
|
||||
consumeRequest(socket, state);
|
||||
});
|
||||
socket.on('timeout', () => socket.destroy());
|
||||
socket.on('error', () => {});
|
||||
});
|
||||
|
||||
server.listen(listenPort, '0.0.0.0', () => {
|
||||
console.log(`SOCKS5 test proxy listening on ${listenPort}`);
|
||||
});
|
||||
|
||||
http.createServer((request, response) => {
|
||||
if (request.url !== '/stats') {
|
||||
response.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
response.writeHead(200, { 'content-type': 'application/json' });
|
||||
response.end(JSON.stringify(stats));
|
||||
}).listen(metricsPort, '0.0.0.0');
|
||||
Reference in New Issue
Block a user