udphole

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

helpers.js (6283B)


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