diff --git a/bridge.js b/bridge.js new file mode 100644 index 0000000..7353aa9 --- /dev/null +++ b/bridge.js @@ -0,0 +1,207 @@ +// bridge.js — HTTP→TCP bridge for ABS_POLL using 512-byte packet framing (CommonJS) +const express = require("express"); +const net = require("net"); + +// ---- ABS endpoint (yours) ---- +const ABS_HOST = process.env.ABS_HOST || "192.0.0.14"; +const ABS_PORT = Number(process.env.ABS_PORT || 7000); + +// ---- constants (matching your code) ---- +const ABS_POLL = 178; // Checks if the connection exists +const SUCCESS = 0; + +const PKT_SIZE = 512; +const PAYLOAD_PER_PKT = PKT_SIZE - 1; + +const app = express(); + +// no global express.json(); we read the body safely per request +function readJsonBody(req) { + return new Promise((resolve) => { + const chunks = []; + req.on("data", (c) => chunks.push(c)); + req.on("end", () => { + if (!chunks.length) return resolve({}); + const txt = Buffer.concat(chunks).toString("utf8").trim(); + if (!txt) return resolve({}); + try { resolve(JSON.parse(txt)); } catch { resolve({}); } + }); + }); +} + +// ------ packers / parsers (little-endian wire format) ------ + +// 24-byte sSndHeader +function packSndHeader({ nTxnCd, nOpCd = 0, nNumRecsSent = 0, nNumRecsRqrd = 0, nTxnId = 0, cBtMake = 0 }) { + const b = Buffer.alloc(24, 0); + let o = 0; + b.writeInt32LE(nTxnCd, o); o += 4; + b.writeInt32LE(nOpCd, o); o += 4; + b.writeInt32LE(nNumRecsSent, o); o += 4; + b.writeInt32LE(nNumRecsRqrd, o); o += 4; + b.writeInt32LE(nTxnId, o); o += 4; + b.writeUInt8(cBtMake, o); // +3 padding remain 0 + return b; +} + +// parse 18/24-byte sRcvHeader +function parseRcvHeaderFlexible(buf) { + if (buf.length < 18) throw new Error(`Reply too short for RcvHeader: ${buf.length} bytes`); + let o = 0; + const nTxnCd = buf.readInt32LE(o); o += 4; + const nRetCd = buf.readInt32LE(o); o += 4; + const nNumRecs = buf.readInt32LE(o); o += 4; + const nTxnId = buf.readInt32LE(o); o += 4; + const cBtMake = buf.readUInt8(o); + return { nTxnCd, nRetCd, nNumRecs, nTxnId, cBtMake, size: buf.length >= 24 ? 24 : 18 }; +} + +// build a normalized 18-byte header (for display only) +function buildRcvHeader18({ nTxnCd, nRetCd, nNumRecs = 0, nTxnId = 0, cBtMake = 0 }) { + const b = Buffer.alloc(18, 0); + let o = 0; + b.writeInt32LE(nTxnCd, o); o += 4; + b.writeInt32LE(nRetCd, o); o += 4; + b.writeInt32LE(nNumRecs, o); o += 4; + b.writeInt32LE(nTxnId, o); o += 4; + b.writeUInt8(cBtMake, o); + return b; +} + +// ------ 512-byte packetization (MFC style) ------ + +function absSend(sock, payload) { + return new Promise((resolve, reject) => { + let off = 0; + function writeNext() { + const remaining = payload.length - off; + const toCopy = Math.min(PAYLOAD_PER_PKT, Math.max(remaining, 0)); + const pkt = Buffer.alloc(PKT_SIZE, 0); + pkt[0] = (off + toCopy >= payload.length) ? 48 /* '0' */ : 49 /* '1' */; + if (toCopy > 0) payload.copy(pkt, 1, off, off + toCopy); + off += toCopy; + sock.write(pkt, (err) => { + if (err) return reject(err); + if (pkt[0] === 48) return resolve(); // last + writeNext(); + }); + } + writeNext(); + }); +} + +function absRecv(sock, timeoutMs = 5000) { + return new Promise((resolve, reject) => { + const chunks = []; + let buf = Buffer.alloc(0); + const timer = timeoutMs ? setTimeout(() => done(new Error("TCP timeout")), timeoutMs) : null; + + function done(err) { + if (timer) clearTimeout(timer); + sock.off("data", onData); + if (err) reject(err); + else resolve(Buffer.concat(chunks)); + } + + function onData(data) { + buf = Buffer.concat([buf, data]); + while (buf.length >= PKT_SIZE) { + const pkt = buf.subarray(0, PKT_SIZE); + buf = buf.subarray(PKT_SIZE); + const flag = pkt[0]; + chunks.push(pkt.subarray(1)); + if (flag === 48) return done(); // '0' + } + } + + sock.on("data", onData); + sock.on("error", (e) => done(e)); + sock.on("end", () => done(new Error("Socket ended before final '0' packet"))); + }); +} + +async function absRoundtrip(payload, timeoutMs = 7000) { + const sock = new net.Socket(); + await new Promise((res, rej) => sock.connect(ABS_PORT, ABS_HOST, res).once("error", rej)); + try { + await absSend(sock, payload); + const replyConcat = await absRecv(sock, timeoutMs); + sock.end(); + return replyConcat; + } catch (e) { + try { sock.destroy(); } catch {} + throw e; + } +} + +// ------ routes ------ + +app.get("/", (_req, res) => res.send("ABS bridge is up")); + +app.get("/health", (_req, res) => { + const s = new net.Socket(); + s.setTimeout(1500); + s.connect(ABS_PORT, ABS_HOST, () => { s.destroy(); res.json({ ok: true, target: `${ABS_HOST}:${ABS_PORT}` }); }); + s.on("timeout", () => { s.destroy(); res.status(504).json({ ok: false, error: "TCP timeout" }); }); + s.on("error", (e) => { s.destroy(); res.status(502).json({ ok: false, error: String(e) }); }); +}); + +// POST /abs/poll -> send SndHeader(nTxnCd=ABS_POLL) and return normalized + raw headers +app.post("/abs/poll", async (req, res) => { + try { + const body = await readJsonBody(req); // optional + const btMake = (typeof body.btMake === "string") + ? body.btMake.charCodeAt(0) + : (Number.isFinite(body.btMake) ? (body.btMake|0) : 0x00); + + const header = packSndHeader({ + nTxnCd: ABS_POLL, + nOpCd: 0, + nNumRecsSent: 0, + nNumRecsRqrd: 0, + nTxnId: 0, // for poll, server ignores; keep 0 + cBtMake: btMake + }); + + const reply = await absRoundtrip(header, 7000); + + // parse raw reply header (18 or 24) + const headerSlice = reply.subarray(0, Math.min(reply.length, 24)); + const parsed = parseRcvHeaderFlexible(headerSlice); + + // success = nRetCd == SUCCESS (server may zero nTxnCd for poll) + const success = parsed.nRetCd === SUCCESS; + + // normalized view: always show ABS_POLL in JSON so Postman sees a “proper” header + const normalized = { + nTxnCd: parsed.nTxnCd || ABS_POLL, + nRetCd: parsed.nRetCd, + nNumRecs: parsed.nNumRecs, + nTxnId: parsed.nTxnId, + cBtMake: parsed.cBtMake, + size: 18 + }; + + res.json({ + ok: true, + target: `${ABS_HOST}:${ABS_PORT}`, + sentHeaderHex: header.toString("hex"), + replyBytes: reply.length, + replyHexFirst64: reply.subarray(0, 64).toString("hex"), + parsedRcvHeaderRaw: parsed, // actual header from server + normalizedRcvHeader: normalized, // “proper” poll header for display + normalizedHeaderHex: buildRcvHeader18(normalized).toString("hex"), + success + }); + } catch (e) { + res.status(502).json({ ok: false, error: String(e) }); + } +}); + +// ------ start HTTP server ------ +const HTTP_PORT = Number(process.env.HTTP_BRIDGE_PORT || 8080); +app.listen(HTTP_PORT, () => { + console.log(`ABS HTTP bridge listening on :${HTTP_PORT}`); + console.log(`Target ABS server: ${ABS_HOST}:${ABS_PORT}`); +}); + diff --git a/poll.sh b/poll.sh new file mode 100644 index 0000000..c24df9e --- /dev/null +++ b/poll.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +while true; do + curl -s -X POST http://localhost:8080/abs/poll |jq + sleep 2 +done