udphole

Basic UDP wormhole proxy
git clone git://git.finwo.net/app/udphole
Log | Files | Refs | README | LICENSE

helpers.js (6425B)


      1 const { spawn } = require('child_process');
      2 const net = require('net');
      3 const dgram = require('dgram');
      4 const path = require('path');
      5 
      6 const DAEMON_PATH = path.resolve(__dirname, '..', 'udphole');
      7 const TIMEOUT = 2000;
      8 
      9 function sleep(ms) {
     10   return new Promise(resolve => setTimeout(resolve, ms));
     11 }
     12 
     13 function findFreePort() {
     14   return new Promise((resolve, reject) => {
     15     const server = net.createServer();
     16     server.unref();
     17     server.on('error', reject);
     18     server.listen(0, () => {
     19       const addr = server.address();
     20       server.close(() => resolve(addr.port));
     21     });
     22   });
     23 }
     24 
     25 function killAllDaemons() {
     26   return new Promise((resolve) => {
     27     const { execSync } = require('child_process');
     28     try { execSync('pkill -9 udphole 2>/dev/null', { stdio: 'ignore' }); } catch(e) {}
     29     sleep(1000).then(resolve);
     30   });
     31 }
     32 
     33 function spawnDaemon(configPath, command) {
     34   return new Promise(async (resolve, reject) => {
     35     await sleep(500);
     36     const args = ['-f', configPath];
     37     args.push(command || 'daemon');
     38     args.push('-D');
     39 
     40     const daemon = spawn(DAEMON_PATH, args, {
     41       stdio: ['ignore', 'pipe', 'pipe', 'pipe']
     42     });
     43 
     44     let output = '';
     45     const startTimeout = setTimeout(() => {
     46       reject(new Error(`Daemon start timeout. Output: ${output}`));
     47     }, TIMEOUT);
     48 
     49     const started = command === 'cluster' ? 'cluster' : 'daemon started';
     50     daemon.stderr.on('data', (data) => {
     51       process.stderr.write(data.toString());
     52       output += data.toString();
     53       if (output.includes(started)) {
     54         clearTimeout(startTimeout);
     55         sleep(200).then(() => resolve(daemon));
     56       }
     57     });
     58 
     59     daemon.on('error', (err) => {
     60       clearTimeout(startTimeout);
     61       reject(err);
     62     });
     63   });
     64 }
     65 
     66 function killDaemon(daemon) {
     67   return new Promise((resolve) => {
     68     if (!daemon || daemon.killed) {
     69       resolve();
     70       return;
     71     }
     72     daemon.once('exit', resolve);
     73     daemon.kill('SIGTERM');
     74     setTimeout(() => {
     75       if (!daemon.killed) daemon.kill('SIGKILL');
     76       resolve();
     77     }, 1000);
     78   });
     79 }
     80 
     81 function connectApi(port) {
     82   return new Promise((resolve, reject) => {
     83     const sock = net.createConnection({ port, host: '127.0.0.1', noDelay: true });
     84     sock.setEncoding('utf8');
     85     
     86     const timeout = setTimeout(() => {
     87       sock.destroy();
     88       reject(new Error('Connection timeout'));
     89     }, TIMEOUT);
     90     
     91     sock.on('connect', () => {
     92       clearTimeout(timeout);
     93       resolve(sock);
     94     });
     95     
     96     sock.on('error', reject);
     97   });
     98 }
     99 
    100 function connectUnixApi(socketPath) {
    101   return new Promise((resolve, reject) => {
    102     const sock = net.createConnection({ path: socketPath, noDelay: true });
    103     sock.setEncoding('utf8');
    104     
    105     const timeout = setTimeout(() => {
    106       sock.destroy();
    107       reject(new Error('Connection timeout'));
    108     }, TIMEOUT);
    109     
    110     sock.on('connect', () => {
    111       clearTimeout(timeout);
    112       resolve(sock);
    113     });
    114     
    115     sock.on('error', reject);
    116   });
    117 }
    118 
    119 function encodeResp(...args) {
    120   const n = args.length;
    121   let cmd = `*${n}\r\n`;
    122   for (const arg of args) {
    123     const s = String(arg);
    124     cmd += `$${s.length}\r\n${s}\r\n`;
    125   }
    126   return cmd;
    127 }
    128 
    129 function apiCommand(sock, ...args) {
    130   return new Promise((resolve, reject) => {
    131     const cmd = encodeResp(...args);
    132     
    133     let response = '';
    134     const timeout = setTimeout(() => {
    135       sock.destroy();
    136       reject(new Error('API command timeout'));
    137     }, TIMEOUT);
    138     
    139     sock.once('data', (data) => {
    140       response += data;
    141       clearTimeout(timeout);
    142       resolve(parseResp(response));
    143     });
    144     
    145     sock.write(cmd);
    146   });
    147 }
    148 
    149 function parseResp(data) {
    150   data = data.trim();
    151   if (data.startsWith('+')) return data.substring(1).trim();
    152   if (data.startsWith('-')) throw new Error(data.substring(1).trim());
    153   if (data.startsWith(':')) return parseInt(data.substring(1), 10);
    154   if (data.startsWith('*')) {
    155     const count = parseInt(data.substring(1), 10);
    156     if (count === 0) return [];
    157     const lines = data.split('\r\n');
    158     const result = [];
    159     let i = 1;
    160     for (let j = 0; j < count && i < lines.length; j++) {
    161       if (lines[i].startsWith('$')) {
    162         i++;
    163         if (i < lines.length) result.push(lines[i]);
    164       } else if (lines[i].startsWith(':')) {
    165         result.push(parseInt(lines[i].substring(1), 10));
    166       } else if (lines[i].startsWith('+')) {
    167         result.push(lines[i].substring(1));
    168       } else if (lines[i].startsWith('-')) {
    169         throw new Error(lines[i].substring(1));
    170       }
    171       i++;
    172     }
    173     return result;
    174   }
    175   if (data.startsWith('$')) {
    176     const len = parseInt(data.substring(1), 10);
    177     if (len === -1) return null;
    178     const idx = data.indexOf('\r\n');
    179     if (idx >= 0) return data.substring(idx + 2);
    180     return '';
    181   }
    182   return data;
    183 }
    184 
    185 function createUdpEchoServer() {
    186   return new Promise(async (resolve, reject) => {
    187     const server = dgram.createSocket('udp4');
    188     const messages = [];
    189     
    190     server.on('message', (msg, rinfo) => {
    191       messages.push({ data: msg.toString(), rinfo });
    192       server.send(msg, rinfo.port, rinfo.address);
    193     });
    194     
    195     server.on('error', reject);
    196     
    197     const port = await findFreePort();
    198     server.bind(port, '127.0.0.1', () => {
    199       resolve({
    200         port,
    201         socket: server,
    202         getMessages: () => messages,
    203         clearMessages: () => { messages.length = 0; }
    204       });
    205     });
    206   });
    207 }
    208 
    209 function sendUdp(port, host, message) {
    210   return new Promise((resolve, reject) => {
    211     const sock = dgram.createSocket('udp4');
    212     const buf = Buffer.from(message);
    213     sock.send(buf, 0, buf.length, port, host, (err) => {
    214       sock.close();
    215       if (err) reject(err);
    216       else resolve();
    217     });
    218   });
    219 }
    220 
    221 function recvUdp(port, timeout) {
    222   return new Promise((resolve, reject) => {
    223     const sock = dgram.createSocket('udp4');
    224     sock.bind(port, '127.0.0.1');
    225     
    226     const timer = setTimeout(() => {
    227       sock.close();
    228       reject(new Error('Receive timeout'));
    229     }, timeout || TIMEOUT);
    230     
    231     sock.on('message', (msg, rinfo) => {
    232       clearTimeout(timer);
    233       sock.close();
    234       resolve({ data: msg.toString(), rinfo });
    235     });
    236     
    237     sock.on('error', (err) => {
    238       clearTimeout(timer);
    239       reject(err);
    240     });
    241   });
    242 }
    243 
    244 module.exports = {
    245   sleep,
    246   findFreePort,
    247   spawnDaemon,
    248   killDaemon,
    249   killAllDaemons,
    250   connectApi,
    251   connectUnixApi,
    252   apiCommand,
    253   createUdpEchoServer,
    254   sendUdp,
    255   recvUdp,
    256   TIMEOUT
    257 };