BTC_BETTING_SERVER_CONNECTOR/bridge.js

208 lines
6.7 KiB
JavaScript

// 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}`);
});