UNPKG

chattervox

Version:

An AX.25 packet radio chat protocol with support for digital signatures and binary compression. Like IRC over radio waves 📡〰.

408 lines (338 loc) • 12.6 kB
#!/usr/bin/env node import { ArgumentParser, SubParser } from 'argparse' import * as fs from 'fs' import * as path from 'path' import * as config from './config' import { Keystore, Key } from './Keystore' import * as chat from './subcommands/chat' import * as addkey from './subcommands/addkey' import * as removekey from './subcommands/removekey' import * as showkey from './subcommands/showkey' import * as genkey from './subcommands/genkey' import * as send from './subcommands/send' import * as receive from './subcommands/receive' import * as exec from './subcommands/exec' import * as tty from './subcommands/tty' import { interactiveInit } from './ui/init' import { isCallsign, isCallsignSSID, isBrokenPipeError } from './utils' function parseArgs(): any { const pkgBuff = fs.readFileSync(path.resolve(__dirname, '..', 'package.json')) const pkgJSON: any = JSON.parse(pkgBuff.toString('utf8')) const parser = new ArgumentParser({ prog: pkgJSON.name, version: pkgJSON.version, description: pkgJSON.description }) parser.addArgument(['--config', '-c'], { help: `Path to config file (default: ${config.defaultConfigPath})`, defaultValue: config.defaultConfigPath }) const subs: SubParser = parser.addSubparsers({ title: 'subcommands', dest: 'subcommand' }) const chat: ArgumentParser = subs.addParser('chat', { addHelp: true, description: 'Enter the chat room.' }) chat; // intentionally unused const send: ArgumentParser = subs.addParser('send', { addHelp: true, description: 'Send chattervox packets.' }) send.addArgument(['--to', '-t'], { type: 'string', help: 'The recipient\'s callsign, callsign-ssid pair, or chatroom name (default: "CQ").', defaultValue: 'CQ', required: false }) send.addArgument(['--dont-sign', '-d'], { action: 'storeTrue', dest: 'dontSign', help: 'Don\'t sign messages.', required: false }) send.addArgument('message', { type: 'string', help: 'A UTF-8 message to be sent.', nargs: '?', required: false }) const receive: ArgumentParser = subs.addParser('receive', { addHelp: true, description: 'Write chattervox packets to stdout.' }) receive.addArgument(['--allow-unsigned', '-u'], { action: 'storeTrue', dest: 'allowUnsigned', help: 'Receive unsigned messages.', }) receive.addArgument(['--allow-untrusted', '-e'], { action: 'storeTrue', dest: 'allowUntrusted', help: 'Receive messages signed by senders not in keyring.', }) receive.addArgument(['--allow-invalid', '-i'], { action: 'storeTrue', dest: 'allowInvalid', help: 'Receive messages with invalid signatures.', }) receive.addArgument(['--all-recipients', '-g'], { action: 'storeTrue', dest: 'allRecipients', help: 'Receive messages to all callsigns and chat rooms.', }) receive.addArgument(['--allow-all', '-a'], { action: 'storeTrue', dest: 'allowAll', help: 'Receive all messages, independent of signatures and destinations.', }) receive.addArgument(['--raw', '-r'], { action: 'storeTrue', dest: 'raw', help: 'Print raw ax25 packets instead of parsed chattervox messages.', }) receive.addArgument('--to', { type: 'string', help: 'The recipient\'s callsign, callsign-ssid pair, or chatroom name (default: "CQ").', defaultValue: 'CQ', required: false }) receive.addArgument(['--verbose', '-v'], { action: 'storeTrue', help: 'Print verbose output from any chattervox packet received to stderr.', }) const showKey: ArgumentParser = subs.addParser('showkey', { addHelp: true, description: 'List keys.' }) showKey.addArgument('callsign', { type: 'string', nargs: '?' }) const addKey: ArgumentParser = subs.addParser('addkey', { addHelp: true, description: 'Add a new public key to the keystore associated with a callsign.' }) addKey.addArgument('callsign', { type: 'string' }) addKey.addArgument('publickey', { type: 'string' }) const removeKey: ArgumentParser = subs.addParser('removekey', { addHelp: true, description: 'Remove a public key from the keystore.' }) removeKey.addArgument('callsign', { type: 'string' }) removeKey.addArgument('publickey', { type: 'string' }) const genKey: ArgumentParser = subs.addParser('genkey', { addHelp: true, description: 'Generate a new keypair for your callsign.' }) genKey.addArgument('--make-signing', { action: 'storeTrue', dest: 'makeSigning', help: 'Make the generated key your default signing key.' }) const exec: ArgumentParser = subs.addParser('exec', { addHelp: true, description: 'Execute a command using chattervox as the stdin and stdout interface.' }) exec.addArgument(['--delay', '-s'], { type: 'int', help: 'Milliseconds after receiving stdin to wait before transmitting stdout (default: 3000).', defaultValue: 3000, required: false }) exec.addArgument(['--to', '-t'], { type: 'string', help: 'The recipient\'s callsign, callsign-ssid pair, or chatroom name (default: "CQ").', defaultValue: 'CQ', required: false }) exec.addArgument(['--dont-sign', '-d'], { action: 'storeTrue', dest: 'dontSign', help: 'Don\'t sign messages.', required: false }) exec.addArgument(['--stderr', '-f'], { action: 'storeTrue', help: 'Also transmit stderr.', defaultValue: false, required: false }) exec.addArgument(['--allow-unsigned', '-u'], { action: 'storeTrue', dest: 'allowUnsigned', help: 'Receive unsigned messages.', }) exec.addArgument(['--allow-untrusted', '-e'], { action: 'storeTrue', dest: 'allowUntrusted', help: 'Receive messages signed by senders not in keyring.', }) exec.addArgument(['--allow-invalid', '-i'], { action: 'storeTrue', dest: 'allowInvalid', help: 'Receive messages with invalid signatures.', }) exec.addArgument(['--all-recipients', '-g'], { action: 'storeTrue', dest: 'allRecipients', help: 'Receive messages to all callsigns and chat rooms.', }) exec.addArgument(['--allow-all', '-a'], { action: 'storeTrue', dest: 'allowAll', help: 'Receive all messages, independent of signatures and destinations.', }) exec.addArgument('command', { type: 'string', help: 'A command to be run' }) const tty: ArgumentParser = subs.addParser('tty', { addHelp: true, description: 'A dumb tty interface. Sends what\'s typed, prints what\'s received.' }) tty.addArgument(['--to', '-t'], { type: 'string', help: 'The recipient\'s callsign, callsign-ssid pair, or chatroom name (default: "CQ").', defaultValue: 'CQ', required: false }) tty.addArgument(['--dont-sign', '-d'], { action: 'storeTrue', dest: 'dontSign', help: 'Don\'t sign messages.', required: false }) tty.addArgument(['--allow-unsigned', '-u'], { action: 'storeTrue', dest: 'allowUnsigned', help: 'Receive unsigned messages.', }) tty.addArgument(['--allow-untrusted', '-e'], { action: 'storeTrue', dest: 'allowUntrusted', help: 'Receive messages signed by senders not in keyring.', }) tty.addArgument(['--allow-invalid', '-i'], { action: 'storeTrue', dest: 'allowInvalid', help: 'Receive messages with invalid signatures.', }) tty.addArgument(['--all-recipients', '-g'], { action: 'storeTrue', dest: 'allRecipients', help: 'Receive messages to all callsigns and chat rooms.', }) tty.addArgument(['--allow-all', '-a'], { action: 'storeTrue', dest: 'allowAll', help: 'Receive all messages, independent of signatures and destinations.', }) return parser.parseArgs() } function validateArgs(args: any): void { if (args.callsign != null && !isCallsign(args.callsign)) { console.error(`${args.callsign} is not a valid callsign.`) if (isCallsignSSID(args.callsign)) { console.error(`callsign should not include an SSID for key management subcommands.`) } process.exit(1) } if (args.to != null && args.to !== 'CQ' && !(isCallsign(args.to) || isCallsignSSID(args.to))) { console.error('--to must be a callsign, callsign-ssid pair, or chatroom name with less than 7 alphanumeric characters.') process.exit(1) } } // not sure if we should add this here... // function validateKeystoreFile(conf: config.Config): void { // if (!fs.existsSync(conf.keystoreFile)) { // console.error(`No keystoreFile exists at location "${conf.keystoreFile}".`) // process.exit(1) // } else { // try { // JSON.parse(fs.readFileSync(conf.keystoreFile).toString('utf8')) // } catch (err) { // console.error(`Error loading keystoreFile file from "${conf.keystoreFile}".`) // console.error(err.message) // process.exit(1) // } // } // } function validateSigningKeyExists(conf: config.Config, ks: Keystore): void { // if there is a signing in the config but it doesn't exist in the keystore if (conf.signingKey != null) { const signing = ks.getKeyPairs(conf.callsign).filter((key: Key) => { return key.public === conf.signingKey }) if (signing.length < 1) { console.error(`Default signing key has no matching private key found in the keystore.`) process.exit(1) } } } async function cleanup(): Promise<void> { return await exec.cleanup() } function onSignal(): void { cleanup() .then(() => process.exit(0)) .catch(err => { console.error(err) process.exit(1) }) } async function main() { process.on('SIGINT', onSignal) // catch ctrl-c process.on('SIGTERM', onSignal) // catch kill const args = parseArgs() validateArgs(args) // initialize a new config if (!config.exists(args.config)) { // if the default config doesn't exist, let's run the interactive init if (args.config === config.defaultConfigPath) { await interactiveInit() } else { console.error(`No config file exists at "${args.config}".`) process.exit(1) } } let conf: config.Config = null try { conf = config.load(args.config) } catch(err) { console.error(`Error loading config file from "${args.config}".`) console.error(err.message) process.exit(1) } // validate that keystore file exists // validateKeystoreFile(conf) const ks: Keystore = new Keystore(conf.keystoreFile) // if this subcommand is any of the commands that signs something if (['chat', 'send', 'receive'].includes(args.subcommand)) { validateSigningKeyExists(conf, ks) } let code = null try { switch (args.subcommand) { case 'chat': code = await chat.main(args, conf, ks); break case 'send': code = await send.main(args, conf, ks); break case 'receive': code = await receive.main(args, conf, ks); break case 'showkey': code = await showkey.main(args, conf, ks); break case 'addkey': code = await addkey.main(args, conf, ks); break case 'removekey': code = await removekey.main(args, conf, ks); break case 'genkey': code = await genkey.main(args, conf, ks); break case 'exec': code = await exec.main(args, conf, ks); break case 'tty': code = await tty.main(args, conf, ks); break } } catch (err) { if (isBrokenPipeError(err)) { console.error(`\nThe connection to KISS TNC ${conf.kissPort} has been closed with a broken pipe.`) code = 1 } else throw err } process.exit(code) } main() .catch(async (err) => { console.error(err) await cleanup() process.exit(1) })