chattervox
Version:
An AX.25 packet radio chat protocol with support for digital signatures and binary compression. Like IRC over radio waves 📡〰.
383 lines • 14.3 kB
JavaScript
#!/usr/bin/env node
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const argparse_1 = require("argparse");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const config = __importStar(require("./config"));
const Keystore_1 = require("./Keystore");
const chat = __importStar(require("./subcommands/chat"));
const addkey = __importStar(require("./subcommands/addkey"));
const removekey = __importStar(require("./subcommands/removekey"));
const showkey = __importStar(require("./subcommands/showkey"));
const genkey = __importStar(require("./subcommands/genkey"));
const send = __importStar(require("./subcommands/send"));
const receive = __importStar(require("./subcommands/receive"));
const exec = __importStar(require("./subcommands/exec"));
const tty = __importStar(require("./subcommands/tty"));
const init_1 = require("./ui/init");
const utils_1 = require("./utils");
function parseArgs() {
const pkgBuff = fs.readFileSync(path.resolve(__dirname, '..', 'package.json'));
const pkgJSON = JSON.parse(pkgBuff.toString('utf8'));
const parser = new argparse_1.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 = parser.addSubparsers({
title: 'subcommands',
dest: 'subcommand'
});
const chat = subs.addParser('chat', {
addHelp: true,
description: 'Enter the chat room.'
});
chat; // intentionally unused
const send = 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 = 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 = subs.addParser('showkey', {
addHelp: true,
description: 'List keys.'
});
showKey.addArgument('callsign', { type: 'string', nargs: '?' });
const addKey = 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 = 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 = 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 = 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 = 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) {
if (args.callsign != null && !utils_1.isCallsign(args.callsign)) {
console.error(`${args.callsign} is not a valid callsign.`);
if (utils_1.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' && !(utils_1.isCallsign(args.to) || utils_1.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, ks) {
// 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) => {
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);
}
}
}
function cleanup() {
return __awaiter(this, void 0, void 0, function* () {
return yield exec.cleanup();
});
}
function onSignal() {
cleanup()
.then(() => process.exit(0))
.catch(err => {
console.error(err);
process.exit(1);
});
}
function main() {
return __awaiter(this, void 0, void 0, function* () {
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) {
yield init_1.interactiveInit();
}
else {
console.error(`No config file exists at "${args.config}".`);
process.exit(1);
}
}
let conf = 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 = new Keystore_1.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 = yield chat.main(args, conf, ks);
break;
case 'send':
code = yield send.main(args, conf, ks);
break;
case 'receive':
code = yield receive.main(args, conf, ks);
break;
case 'showkey':
code = yield showkey.main(args, conf, ks);
break;
case 'addkey':
code = yield addkey.main(args, conf, ks);
break;
case 'removekey':
code = yield removekey.main(args, conf, ks);
break;
case 'genkey':
code = yield genkey.main(args, conf, ks);
break;
case 'exec':
code = yield exec.main(args, conf, ks);
break;
case 'tty':
code = yield tty.main(args, conf, ks);
break;
}
}
catch (err) {
if (utils_1.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((err) => __awaiter(this, void 0, void 0, function* () {
console.error(err);
yield cleanup();
process.exit(1);
}));
//# sourceMappingURL=main.js.map