UNPKG

@tak-ps/node-tak

Version:

Lightweight JavaScript library for communicating with TAK Server

204 lines 7.07 kB
import EventEmitter from 'node:events'; import tls from 'node:tls'; import CoT, { CoTParser } from '@tak-ps/node-cot'; import TAKAPI from './lib/api.js'; export * from './lib/auth.js'; /* eslint-disable no-control-regex */ export const REGEX_CONTROL = /[\u000B-\u001F\u007F-\u009F]/g; // Match <event .../> or <event> but not <events> export const REGEX_EVENT = /(<event[ >][\s\S]*?<\/event>)([\s\S]*)/; export default class TAK extends EventEmitter { id; type; url; auth; open; destroyed; queue; writing; cotOptions; pingInterval; client; version; /** * @constructor * * @param url - Full URL of Streaming COT Endpoint IE: "https://ops.cotak.gov:8089" * @param auth - TAK Certificate Pair * @param opts - Options Object * @param opts.id - When using multiple connections in a script, allows a unique ID per connection * @param opts.type - When using multiple connections in a script, allows specifying a script provided connection type */ constructor(url, auth, opts = {}) { super(); if (!opts) opts = {}; this.id = opts.id || crypto.randomUUID(); this.type = opts.type || 'unknown'; this.url = url; this.auth = auth; this.writing = false; this.cotOptions = opts.cot || {}; this.open = false; this.destroyed = false; this.queue = []; } static async connect(url, auth, opts = {}) { const tak = new TAK(url, auth, opts); if (url.protocol === 'ssl:') { if (!tak.auth.cert) throw new Error('auth.cert required'); if (!tak.auth.key) throw new Error('auth.key required'); return await tak.connect_ssl(); } else { throw new Error('Unknown TAK Server Protocol'); } } connect_ssl() { return new Promise((resolve) => { this.destroyed = false; this.client = tls.connect({ host: this.url.hostname, port: parseInt(this.url.port), rejectUnauthorized: this.auth.rejectUnauthorized ?? false, cert: this.auth.cert, key: this.auth.key, passphrase: this.auth.passphrase, ca: this.auth.ca, }); this.client.setNoDelay(); this.client.on('connect', () => { console.error(`ok - ${this.id} @ connect:${this.client ? this.client.authorized : 'NO CLIENT'} - ${this.client ? this.client.authorizationError : 'NO CLIENT'}`); }); this.client.on('secureConnect', () => { console.error(`ok - ${this.id} @ secure:${this.client ? this.client.authorized : 'NO CLIENT'} - ${this.client ? this.client.authorizationError : 'NO CLIENT'}`); this.emit('secureConnect'); this.ping(); }); let buff = ''; this.client.on('data', async (data) => { // Eventually Parse ProtoBuf buff = buff + data.toString(); let result = TAK.findCoT(buff); while (result && result.event) { try { const cot = await CoTParser.from_xml(result.event, this.cotOptions); if (cot.raw.event._attributes.type === 't-x-c-t-r') { this.open = true; this.emit('ping'); } else if (cot.raw.event._attributes.type === 't-x-takp-v' && cot.raw.event.detail && cot.raw.event.detail.TakControl && cot.raw.event.detail.TakControl.TakServerVersionInfo && cot.raw.event.detail.TakControl.TakServerVersionInfo._attributes) { this.version = cot.raw.event.detail.TakControl.TakServerVersionInfo._attributes.serverVersion; } else { this.emit('cot', cot); } } catch (e) { console.error('Error parsing', e, data.toString()); } buff = result.remainder; result = TAK.findCoT(buff); } }).on('timeout', () => { this.emit('timeout'); }).on('error', (err) => { this.emit('error', err); }).on('end', () => { this.open = false; this.emit('end'); }); this.pingInterval = setInterval(() => { this.ping(); }, 5000); return resolve(this); }); } async reconnect() { if (this.destroyed) { await this.connect_ssl(); } else { this.destroy(); await this.connect_ssl(); } } destroy() { this.destroyed = true; if (this.client) { this.client.destroy(); } if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = undefined; } } async ping() { this.write([CoT.ping()]); } writer(body) { return new Promise((resolve, reject) => { if (!this.client) return reject(new Error('A Connection Client must first be created before it can be written')); const res = this.client.write(body + '\n', () => { return resolve(res); }); }); } async process() { this.writing = true; while (this.queue.length) { const body = this.queue.shift(); if (!body) continue; await this.writer(body); } await this.writer(''); if (this.queue.length) { process.nextTick(() => { this.process(); }); } else { this.writing = false; } } /** * Write a CoT to the TAK Connection * * @param {CoT} cot CoT Object */ async write(cots) { for (const cot of cots) { this.queue.push(await CoTParser.to_xml(cot)); } if (this.queue.length && !this.writing) { this.process(); } } write_xml(body) { this.queue.push(body); if (this.queue.length && !this.writing) { this.process(); } } // https://github.com/vidterra/multitak/blob/main/app/lib/helper.js#L4 static findCoT(str) { str = str.replace(REGEX_CONTROL, ""); const match = str.match(REGEX_EVENT); // find first CoT if (!match) return null; return { event: match[1], remainder: match[2], }; } } export { TAKAPI, CoT, }; //# sourceMappingURL=index.js.map