more functional design principles
Some checks failed
ci/woodpecker/pr/pr Pipeline failed

This commit is contained in:
Madison Grubb
2026-02-17 11:17:52 -05:00
parent 1a566e2d80
commit c8d37c98f4
14 changed files with 357 additions and 321 deletions

View File

@@ -9,8 +9,10 @@ import { registerCleanup } from '../utils/shutdown.js'
import { COT_AUTH_TIMEOUT_MS } from '../utils/constants.js'
import { acquire } from '../utils/asyncLock.js'
let tcpServer = null
let tlsServer = null
const serverState = {
tcpServer: null,
tlsServer: null,
}
const relaySet = new Set()
const allSockets = new Set()
const socketBuffers = new WeakMap()
@@ -44,22 +46,27 @@ function broadcast(senderSocket, rawMessage) {
}
}
const createPreview = (payload) => {
try {
const str = payload.toString('utf8')
if (str.startsWith('<')) {
const s = str.length <= 120 ? str : str.slice(0, 120) + '...'
// eslint-disable-next-line no-control-regex -- sanitize control chars for log preview
return s.replace(/[\u0000-\u0008\v\f\u000E-\u001F]/g, '.')
}
return 'hex:' + payload.subarray(0, Math.min(40, payload.length)).toString('hex')
}
catch {
return 'hex:' + payload.subarray(0, Math.min(40, payload.length)).toString('hex')
}
}
async function processFrame(socket, rawMessage, payload, authenticated) {
const requireAuth = socket._cotRequireAuth !== false
const debug = socket._cotDebug === true
const parsed = parseCotPayload(payload)
if (debug) {
let preview = payload.length
try {
const str = payload.toString('utf8')
if (str.startsWith('<')) {
const s = str.length <= 120 ? str : str.slice(0, 120) + '...'
// eslint-disable-next-line no-control-regex -- sanitize control chars for log preview
preview = s.replace(/[\u0000-\u0008\v\f\u000E-\u001F]/g, '.')
}
else preview = 'hex:' + payload.subarray(0, Math.min(40, payload.length)).toString('hex')
}
catch { preview = 'hex:' + payload.subarray(0, Math.min(40, payload.length)).toString('hex') }
const preview = createPreview(payload)
console.log('[cot] payload length:', payload.length, 'parsed:', parsed ? parsed.type : null, 'preview:', preview)
}
if (!parsed) return
@@ -108,10 +115,35 @@ async function processFrame(socket, rawMessage, payload, authenticated) {
}
}
const parseFrame = (buf) => {
const takResult = parseTakStreamFrame(buf)
if (takResult) return { result: takResult, frameType: 'tak' }
if (buf[0] === 0x3C) {
const xmlResult = parseTraditionalXmlFrame(buf)
if (xmlResult) return { result: xmlResult, frameType: 'traditional' }
}
return { result: null, frameType: null }
}
const processBufferedData = async (socket, buf, authenticated) => {
if (buf.length === 0) return buf
const { result, frameType } = parseFrame(buf)
if (result && socket._cotDebug) {
console.log('[cot] frame parsed as', frameType, 'bytesConsumed=', result.bytesConsumed)
}
if (!result) return buf
const { payload, bytesConsumed } = result
const rawMessage = buf.subarray(0, bytesConsumed)
await processFrame(socket, rawMessage, payload, authenticated)
if (socket.destroyed) return null
const remainingBuf = buf.subarray(bytesConsumed)
socketBuffers.set(socket, remainingBuf)
return processBufferedData(socket, remainingBuf, authenticated)
}
async function onData(socket, data) {
let buf = socketBuffers.get(socket)
if (!buf) buf = Buffer.alloc(0)
buf = Buffer.concat([buf, data])
const existingBuf = socketBuffers.get(socket)
const buf = Buffer.concat([existingBuf || Buffer.alloc(0), data])
socketBuffers.set(socket, buf)
const authenticated = Boolean(socket._cotAuthenticated)
@@ -120,22 +152,7 @@ async function onData(socket, data) {
const hex = buf.subarray(0, Math.min(80, buf.length)).toString('hex')
console.log('[cot] first chunk len=', buf.length, 'first bytes (hex):', hex, 'starts with 0xBF:', buf[0] === 0xBF, 'starts with <:', buf[0] === 0x3C)
}
while (buf.length > 0) {
let result = parseTakStreamFrame(buf)
let frameType = 'tak'
if (!result && buf[0] === 0x3C) {
result = parseTraditionalXmlFrame(buf)
frameType = 'traditional'
}
if (result && socket._cotDebug) console.log('[cot] frame parsed as', frameType, 'bytesConsumed=', result.bytesConsumed)
if (!result) break
const { payload, bytesConsumed } = result
const rawMessage = buf.subarray(0, bytesConsumed)
await processFrame(socket, rawMessage, payload, authenticated)
if (socket.destroyed) return
buf = buf.subarray(bytesConsumed)
socketBuffers.set(socket, buf)
}
await processBufferedData(socket, buf, authenticated)
}
function setupSocket(socket, tls = false) {
@@ -182,16 +199,16 @@ function startCotServers() {
key: readFileSync(keyPath),
rejectUnauthorized: false,
}
tlsServer = createTlsServer(tlsOpts, socket => setupSocket(socket, true))
tlsServer.on('error', err => console.error('[cot] TLS server error:', err?.message))
tlsServer.listen(port, '0.0.0.0', () => {
serverState.tlsServer = createTlsServer(tlsOpts, socket => setupSocket(socket, true))
serverState.tlsServer.on('error', err => console.error('[cot] TLS server error:', err?.message))
serverState.tlsServer.listen(port, '0.0.0.0', () => {
console.log('[cot] CoT server listening on 0.0.0.0:' + port + ' (TLS) — use this port in ATAK/iTAK and enable SSL')
})
}
else {
tcpServer = createTcpServer(socket => setupSocket(socket, false))
tcpServer.on('error', err => console.error('[cot] TCP server error:', err?.message))
tcpServer.listen(port, '0.0.0.0', () => {
serverState.tcpServer = createTcpServer(socket => setupSocket(socket, false))
serverState.tcpServer.on('error', err => console.error('[cot] TCP server error:', err?.message))
serverState.tcpServer.listen(port, '0.0.0.0', () => {
console.log('[cot] CoT server listening on 0.0.0.0:' + port + ' (plain TCP) — use this port in ATAK/iTAK with SSL disabled')
})
}
@@ -209,7 +226,18 @@ export default defineNitroPlugin((nitroApp) => {
// Start immediately so CoT is up before first request in dev; ready may fire late in some setups.
setImmediate(startCotServers)
registerCleanup(async () => {
const cleanupServers = () => {
if (serverState.tcpServer) {
serverState.tcpServer.close()
serverState.tcpServer = null
}
if (serverState.tlsServer) {
serverState.tlsServer.close()
serverState.tlsServer = null
}
}
const cleanupSockets = () => {
for (const s of allSockets) {
try {
s.destroy()
@@ -220,34 +248,15 @@ export default defineNitroPlugin((nitroApp) => {
}
allSockets.clear()
relaySet.clear()
if (tcpServer) {
tcpServer.close()
tcpServer = null
}
if (tlsServer) {
tlsServer.close()
tlsServer = null
}
}
registerCleanup(async () => {
cleanupSockets()
cleanupServers()
})
nitroApp.hooks.hook('close', async () => {
for (const s of allSockets) {
try {
s.destroy()
}
catch {
/* ignore */
}
}
allSockets.clear()
relaySet.clear()
if (tcpServer) {
tcpServer.close()
tcpServer = null
}
if (tlsServer) {
tlsServer.close()
tlsServer = null
}
cleanupSockets()
cleanupServers()
})
})