@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* anisail-tunnel: Bare-compatible Holesail client that binds a local port and
|
||||
* prints it to stdout for the Ansible connection plugin. Keeps running until
|
||||
* stdin closes or SIGTERM/SIGINT.
|
||||
*
|
||||
* Usage: anisail-tunnel --key <hs://...> [--remote-port 22] [--local-port 0] [--timeout 30000]
|
||||
* Output: exactly one JSON line to stdout: {"local_port": <number>, "ready": true}
|
||||
*/
|
||||
|
||||
import 'bare-process/global';
|
||||
|
||||
const DEFAULT_REMOTE_PORT = 22;
|
||||
const DEFAULT_LOCAL_PORT = 0;
|
||||
const DEFAULT_TIMEOUT_MS = 30000;
|
||||
const LOCAL_HOST = '127.0.0.1';
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
let key = null;
|
||||
let remotePort = DEFAULT_REMOTE_PORT;
|
||||
let localPort = DEFAULT_LOCAL_PORT;
|
||||
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--key' && args[i + 1]) {
|
||||
key = args[++i].trim();
|
||||
} else if (args[i] === '--remote-port' && args[i + 1]) {
|
||||
remotePort = parseInt(args[++i], 10) || DEFAULT_REMOTE_PORT;
|
||||
} else if (args[i] === '--local-port' && args[i + 1]) {
|
||||
localPort = parseInt(args[++i], 10);
|
||||
if (isNaN(localPort) || localPort < 0) localPort = DEFAULT_LOCAL_PORT;
|
||||
} else if (args[i] === '--timeout' && args[i + 1]) {
|
||||
timeoutMs = parseInt(args[++i], 10) || DEFAULT_TIMEOUT_MS;
|
||||
}
|
||||
}
|
||||
|
||||
return { key, remotePort, localPort, timeoutMs };
|
||||
}
|
||||
|
||||
function emit(line) {
|
||||
try {
|
||||
process.stdout.write(line + '\n');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const mod = await import('holesail');
|
||||
const Holesail = mod.default || mod;
|
||||
|
||||
const { key, remotePort, localPort, timeoutMs } = parseArgs();
|
||||
|
||||
if (!key || key.length === 0) {
|
||||
const msg = 'anisail-tunnel: --key <hs://...> is required';
|
||||
try {
|
||||
process.stderr.write(msg + '\n');
|
||||
} catch (_) {}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const portToUse = (localPort > 0 ? localPort : 19200 + Math.floor(Math.random() * 1000)) || 19200;
|
||||
|
||||
const readyPromise = new Promise((resolve, reject) => {
|
||||
const t = timeoutMs > 0
|
||||
? setTimeout(() => reject(new Error('Tunnel ready timeout after ' + timeoutMs + 'ms')), timeoutMs)
|
||||
: null;
|
||||
const done = (err, port) => {
|
||||
if (t) clearTimeout(t);
|
||||
if (err) reject(err);
|
||||
else resolve(port);
|
||||
};
|
||||
|
||||
const hs = new Holesail({
|
||||
client: true,
|
||||
key,
|
||||
host: LOCAL_HOST,
|
||||
port: portToUse
|
||||
});
|
||||
|
||||
if (typeof hs.on === 'function') {
|
||||
hs.on('error', (err) => {
|
||||
done(err || new Error('Tunnel error'));
|
||||
});
|
||||
}
|
||||
|
||||
hs.ready()
|
||||
.then(() => {
|
||||
const info = typeof hs.info === 'function' ? hs.info() : (hs.info || {});
|
||||
const boundPort = (info && typeof info.port === 'number')
|
||||
? info.port
|
||||
: portToUse;
|
||||
if (boundPort == null || boundPort < 1 || boundPort > 65535) {
|
||||
done(new Error('Could not determine local tunnel port'));
|
||||
return;
|
||||
}
|
||||
done(null, boundPort);
|
||||
return hs;
|
||||
})
|
||||
.catch(done);
|
||||
|
||||
run._hs = hs;
|
||||
});
|
||||
|
||||
let hs;
|
||||
try {
|
||||
const port = await readyPromise;
|
||||
hs = run._hs;
|
||||
emit(JSON.stringify({ local_port: port, ready: true }));
|
||||
} catch (err) {
|
||||
const msg = (err && err.message) ? err.message : String(err);
|
||||
try {
|
||||
process.stderr.write('anisail-tunnel: ' + msg + '\n');
|
||||
} catch (_) {}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
if (hs && typeof hs.close === 'function') {
|
||||
try {
|
||||
hs.close();
|
||||
} catch (_) {}
|
||||
hs = null;
|
||||
}
|
||||
process.exitCode = 0;
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGTERM', close);
|
||||
process.on('SIGINT', close);
|
||||
|
||||
process.stdin.on('close', () => {
|
||||
close();
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
close();
|
||||
});
|
||||
|
||||
process.stdin.resume();
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
try {
|
||||
process.stderr.write('anisail-tunnel: ' + (err && err.message ? err.message : String(err)) + '\n');
|
||||
} catch (_) {}
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user