From a1db4b228b227c399ee8d0ce36a0367ac54f0f85 Mon Sep 17 00:00:00 2001 From: Sibin Sabu Date: Wed, 27 Aug 2025 12:33:02 +0530 Subject: [PATCH] commit : working till bt info --- bridge.js | 207 ----------- btinfo.js | 764 ++++++++++++++++++++++++++++++++++++++++ btinfo.sh | 4 + debug.log | 0 login.sh | 6 + login_logs.json | 114 ------ mathew.js | 764 ++++++++++++++++++++++++++++++++++++++++ new_bridge.js | 391 -------------------- poll.sh | 2 +- rpinfo.sh | 4 + login_abs.js => savi.js | 66 ++-- 11 files changed, 1581 insertions(+), 741 deletions(-) delete mode 100644 bridge.js create mode 100644 btinfo.js create mode 100755 btinfo.sh create mode 100644 debug.log create mode 100755 login.sh delete mode 100644 login_logs.json create mode 100644 mathew.js delete mode 100644 new_bridge.js mode change 100644 => 100755 poll.sh create mode 100755 rpinfo.sh rename login_abs.js => savi.js (87%) diff --git a/bridge.js b/bridge.js deleted file mode 100644 index 7353aa9..0000000 --- a/bridge.js +++ /dev/null @@ -1,207 +0,0 @@ -// 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/btinfo.js b/btinfo.js new file mode 100644 index 0000000..2efc1fa --- /dev/null +++ b/btinfo.js @@ -0,0 +1,764 @@ +// server.js — robust HTTP→TCP bridge for ABS (ABS_POLL, LOG, BTI, RPI) +// Node 18+ (CommonJS) + +const express = require("express"); +const net = require("net"); + +const ABS_HOST = process.env.ABS_HOST || "192.0.0.14"; +const ABS_PORT = Number(process.env.ABS_PORT || 7000); + +const ABS_POLL = 178; +const LOG = 100; +const LOGBT = 6013; +const BTI = 6014; +const RPI = 6015; +const SUCCESS = 0; + +const PKT_SIZE = 512; +const PAYLOAD_PER_PKT = PKT_SIZE - 1; + +const DEFAULT_TIMEOUT_MS = Number(process.env.BRIDGE_TIMEOUT_MS || 20000); +const RETRIES = Number(process.env.BRIDGE_RETRIES || 2); +const RETRY_DELAY_MS = Number(process.env.BRIDGE_RETRY_DELAY_MS || 500); + +const app = express(); + +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({}); } + }); + }); +} + +// ---------- helpers ---------- +function writeFixedAscii(buf, offset, s, len) { + const str = (s ?? "").toString(); + for (let i = 0; i < len; i++) buf[offset + i] = i < str.length ? (str.charCodeAt(i) & 0xff) : 0x00; + return offset + len; +} +function readFixedAscii(buf, offset, len) { + let end = offset; + const max = offset + len; + while (end < max && buf[end] !== 0) end++; + const raw = buf.subarray(offset, end).toString("ascii"); + return { value: raw.trimEnd(), next: offset + len }; +} +function hexSlice(buf, off, len) { + const s = Math.max(0, off); + const e = Math.min(buf.length, off + len); + return buf.subarray(s, e).toString("hex"); +} +function remaining(buf, off){ return Math.max(0, buf.length - off); } + +// ---------- packetization ---------- +function absSend(sock, payload) { + return new Promise((resolve, reject) => { + let off = 0; + function sendNext() { + 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 : 49; + 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(); + sendNext(); + }); + } + if (!payload || payload.length === 0) { + const pkt = Buffer.alloc(PKT_SIZE, 0); + pkt[0] = 48; + sock.write(pkt, (err) => err ? reject(err) : resolve()); + } else { + sendNext(); + } + }); +} +function absRecv(sock, timeoutMs = DEFAULT_TIMEOUT_MS) { + 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(); + } + } + + 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 = DEFAULT_TIMEOUT_MS) { + 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 || Buffer.alloc(0)); + const replyConcat = await absRecv(sock, timeoutMs); + sock.end(); + return replyConcat; + } catch (e) { + try { sock.destroy(); } catch {} + throw e; + } +} +async function withRetry(fn, tries = RETRIES, delayMs = RETRY_DELAY_MS) { + let lastErr; + for (let i=0;isetTimeout(r, delayMs)); } + } + throw lastErr; +} + +// ---------- MFC-style password "encryption" ---------- +function encryptPasswordLikeMFC(plain) { + const sec = new Date().getUTCSeconds(); + const key = Number(String(sec)[0] || "0"); + let out = ""; + const s = (plain ?? "").toString(); + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + const d = (ch >= "0" && ch <= "9") ? (ch.charCodeAt(0) - 48) : 0; + out += String((d + key) % 10); + } + return { enc: out + String(key), key, utcSeconds: sec }; +} + +// ---------- headers ---------- +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 & 0xff, o); + return b; +} + +/** Smart header parser: test 18 & 24, choose plausible. */ +function parseRcvHeaderFlexible(buf) { + if (buf.length < 18) throw new Error(`Reply too short for RcvHeader: ${buf.length} bytes`); + function readAs(len) { + 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: len }; + } + const h18 = readAs(18); + const ok18 = (h18.nTxnCd >= 50 && h18.nTxnCd < 10000) && (h18.nRetCd >= 0 && h18.nRetCd < 10000); + let h24=null, ok24=false; + if (buf.length >= 24) { + h24 = readAs(24); + ok24 = (h24.nTxnCd >= 50 && h24.nTxnCd < 10000) && (h24.nRetCd >= 0 && h24.nRetCd < 10000); + } + if (ok18 && !ok24) return h18; + if (!ok18 && ok24) return h24; + if (ok18 && ok24) return h18; // prefer 18 to avoid over-advancing + return h18; +} +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 & 0xff, o); + return b; +} + +// ---------- aligners / detectors ---------- +function looksLikeHeaderAt(buf, off){ + if (remaining(buf, off) < 18) return false; + try { + const nTxnCd = buf.readInt32LE(off); + return (nTxnCd >= 50 && nTxnCd < 10000); + } catch { return false; } +} +/** align to plausible header; try +0 / +4 */ +function alignToPossibleHeader(reply, off) { + if (looksLikeHeaderAt(reply, off)) return off; + if (remaining(reply, off) >= 22 && looksLikeHeaderAt(reply, off + 4)) return off + 4; + return off; +} +/** align to where ASCII year "20" begins for login body */ +function alignToAsciiYear(reply, off) { + const tryOffsets = [0, 2, 4, -4]; + for (const d of tryOffsets) { + const o = off + d; + if (o >= 0 && o + 1 < reply.length) { + const b0 = reply[o], b1 = reply[o+1]; + if (b0 === 0x32 && b1 === 0x30) return o; // '2''0' + if (reply[o] === 0x2f && o >= 4 && reply[o-4] === 0x32 && reply[o-3] === 0x30) return o-4; + } + } + return off; +} + +function adjustForOptionalOp(replyBuf, bodyOffset) { + if (replyBuf.length >= bodyOffset + 4) { + const maybeOp = replyBuf.readInt32LE(bodyOffset); + if (maybeOp === LOGBT || maybeOp === LOG || maybeOp === 0 || (maybeOp >= 100 && maybeOp <= 10000)) { + return { offset: bodyOffset + 4, extraOp: maybeOp }; + } else { + const looksLikeYear = replyBuf[bodyOffset] === 0x32 && replyBuf[bodyOffset + 1] === 0x30; + const looksLikeYearFwd = replyBuf[bodyOffset + 4] === 0x32 && replyBuf[bodyOffset + 5] === 0x30; + if (!looksLikeYear && looksLikeYearFwd) return { offset: bodyOffset + 4, extraOp: null }; + } + } + if (bodyOffset >= 4 && replyBuf[bodyOffset] === 0x2f) { + const couldBeYear = replyBuf[bodyOffset - 4] === 0x32 && replyBuf[bodyOffset - 3] === 0x30; + if (couldBeYear) return { offset: bodyOffset - 4, extraOp: null }; + } + return { offset: bodyOffset, extraOp: null }; +} + +function safeAdvanceOrBreak(buf, off, want, outObj){ + if (remaining(buf, off) < want){ + outObj._truncated = true; + return false; + } + return true; +} + +// ---------- parsers ---------- +function packSndLog({ usrId = "", opCard, passwd, btId }) { + const b = Buffer.alloc(38, 0); + let o = 0; + o = writeFixedAscii(b, o, usrId, 5); + o = writeFixedAscii(b, o, opCard, 17); + o = writeFixedAscii(b, o, passwd, 11); + o = writeFixedAscii(b, o, btId, 5); + return b; +} +function parseRcvLog(buf, offset = 0) { + let o = offset; + const t1 = readFixedAscii(buf, o, 20); const cDateTime = t1.value; o = t1.next; + const t2 = readFixedAscii(buf, o, 31); const cUsrNm = t2.value; o = t2.next; + const t3 = readFixedAscii(buf, o, 5 ); const cUsrId = t3.value; o = t3.next; + const t4 = readFixedAscii(buf, o, 31); const cSupNm = t4.value; o = t4.next; + const t5 = readFixedAscii(buf, o, 5 ); const cSupId = t5.value; o = t5.next; + const t6 = readFixedAscii(buf, o, 5 ); const cUsrTyp = t6.value; o = t6.next; + + function rf() { const v = buf.readFloatLE(o); o += 4; return v; } + function ri() { const v = buf.readInt32LE(o); o += 4; return v; } + + const fOpenBal = rf(); + const fTktSalesByVoucher = rf(); + const fTktSalesByCash = rf(); + const fTktSalesByMemCard = rf(); + const nTktSalesByVoucherCount = ri(); + const nTktSalesByCashCount = ri(); + const nTktSalesByMemCardCount = ri(); + const fPayoutByVoucher = rf(); + const fPayoutByCash = rf(); + const fPayoutByMemCard = rf(); + const nPayoutByVoucherCount = ri(); + const nPayoutByCashCount = ri(); + const nPayoutByMemCardCount = ri(); + const fCancelByVoucher = rf(); + const fCancelByCash = rf(); + const fCancelByMemCard = rf(); + const nCancelByVoucherCount = ri(); + const nCancelByCashCount = ri(); + const nCancelByMemCardCount = ri(); + const fDeposit = rf(); + const fWithdrawAmt = rf(); + const fVoucherSales = rf(); + const fVoucherEncash = rf(); + const fCloseBal = rf(); + const fSaleTarget = rf(); + + return { + cDateTime, cUsrNm, cUsrId, cSupNm, cSupId, cUsrTyp, + fOpenBal, fTktSalesByVoucher, fTktSalesByCash, fTktSalesByMemCard, + nTktSalesByVoucherCount, nTktSalesByCashCount, nTktSalesByMemCardCount, + fPayoutByVoucher, fPayoutByCash, fPayoutByMemCard, + nPayoutByVoucherCount, nPayoutByCashCount, nPayoutByMemCardCount, + fCancelByVoucher, fCancelByCash, fCancelByMemCard, + nCancelByVoucherCount, nCancelByCashCount, nCancelByMemCardCount, + fDeposit, fWithdrawAmt, fVoucherSales, fVoucherEncash, + fCloseBal, fSaleTarget, + size: o - offset + }; +} + + + +// function parseRcvBtiMst(buf, offset) { +// let o = offset; + +// const a1 = readFixedAscii(buf, o, 5); const cBtId = a1.value; o = a1.next; +// const a2 = readFixedAscii(buf, o, 31); const cBtName = a2.value; o = a2.next; +// const a3 = readFixedAscii(buf, o, 5); const cEnclCd = a3.value; o = a3.next; +// const a4 = readFixedAscii(buf, o, 4); const cGrpCd = a4.value; o = a4.next; + +// const cBtMode = String.fromCharCode(buf[o]); o += 1; + +// const a5 = readFixedAscii(buf, o, 4); const cMoneyTyp = a5.value; o = a5.next; + +// // Debug: raw hex of these fields +// console.error("fMinSaleBet raw:", buf.subarray(o, o+4).toString("hex")); +// const fMinSaleBet = buf.readInt32LE(o); o += 4; + +// console.error("fMaxSaleBet raw:", buf.subarray(o, o+4).toString("hex")); +// const fMaxSaleBet = buf.readInt32LE(o); o += 4; + +// console.error("fPayMaxAmt raw:", buf.subarray(o, o+4).toString("hex")); +// const fPayMaxAmt = buf.readInt32LE(o); o += 4; + +// return { +// cBtId, cBtName, cEnclCd, cGrpCd, +// cBtMode, cMoneyTyp, +// fMinSaleBet, fMaxSaleBet, fPayMaxAmt, +// size: o - offset +// }; +// } + + +function parseRcvBtiMst(buf, offset) { + let o = offset; + + const a1 = readFixedAscii(buf, o, 5); const cBtId = a1.value; o = a1.next; + const a2 = readFixedAscii(buf, o, 31); const cBtName = a2.value; o = a2.next; + const a3 = readFixedAscii(buf, o, 5); const cEnclCd = a3.value; o = a3.next; + const a4 = readFixedAscii(buf, o, 4); const cGrpCd = a4.value; o = a4.next; + + const cBtMode = String.fromCharCode(buf[o]); o += 1; + + const a5 = readFixedAscii(buf, o, 4); const cMoneyTyp = a5.value; o = a5.next; + + // align to 4-byte boundary (for float fields) + o = (o + 3) & ~3; + + console.error("fMinSaleBet raw:", buf.subarray(o, o+4).toString("hex")); + const fMinSaleBet = buf.readFloatLE(o); o += 4; + + console.error("fMaxSaleBet raw:", buf.subarray(o, o+4).toString("hex")); + const fMaxSaleBet = buf.readFloatLE(o); o += 4; + + console.error("fPayMaxAmt raw:", buf.subarray(o, o+4).toString("hex")); + const fPayMaxAmt = buf.readFloatLE(o); o += 4; + + return { + cBtId, cBtName, cEnclCd, cGrpCd, + cBtMode, cMoneyTyp, + fMinSaleBet, fMaxSaleBet, fPayMaxAmt, + size: o - offset + }; +} + + +function parseRcvBtiDtl(buf, offset) { + let o = offset; + const cDtlTyp = String.fromCharCode(buf[o]); o += 1; + + // If not enough bytes to try the happy path, take what's available + if (remaining(buf, o) < 4) { + const avail = Math.max(0, remaining(buf, o)); + const aRem = readFixedAscii(buf, o, avail); + const cPoolOrVenue = aRem.value; + o = aRem.next; + const cDtlSts = o < buf.length ? String.fromCharCode(buf[o]) : ""; + if (o < buf.length) o += 1; + return { cDtlTyp, cPoolOrVenue, cDtlSts, size: o - offset }; + } + + // Try 3-byte pool/venue first (most common) + const a3 = readFixedAscii(buf, o, 3); + const cPoolOrVenue3 = a3.value; + const after3 = a3.next; + + // Candidate status byte (must be printable ASCII — typical statuses are letters/digits) + const statusCandidate = buf[after3]; + const isPrintable = (statusCandidate >= 0x20 && statusCandidate <= 0x7E); + + if (isPrintable) { + o = after3; + const cDtlSts = String.fromCharCode(buf[o]); o += 1; + return { cDtlTyp, cPoolOrVenue: cPoolOrVenue3, cDtlSts, size: o - offset }; + } + + // Fallback: read 4-byte pool/venue (older/odd layouts) + const a4 = readFixedAscii(buf, o, 4); + const cPoolOrVenue4 = a4.value; + o = a4.next; + const cDtlSts = o < buf.length ? String.fromCharCode(buf[o]) : ""; + if (o < buf.length) o += 1; + return { cDtlTyp, cPoolOrVenue: cPoolOrVenue4, cDtlSts, size: o - offset }; +} + + + + + +function parseRcvRpi1(buf, offset) { + let o = offset; + const nRaceDt = buf.readInt32LE(o); o += 4; + const cVenueNum = buf.readUInt8(o); o += 1; + const cVenueSts = String.fromCharCode(buf[o]); o += 1; + const aR = readFixedAscii(buf, o, 15); const cRaces = aR.value; o = aR.next; + const aAd = readFixedAscii(buf, o, 41); const cAdvertisement = aAd.value; o = aAd.next; + return { nRaceDt, cVenueNum, cVenueSts, cRaces, cAdvertisement, size: o - offset }; +} +function parseRcvRpi2(buf, offset) { + let o = offset; + const nRaceDt = buf.readInt32LE(o); o += 4; + const cVenueNum = buf.readUInt8(o); o += 1; + const cRaceNum = buf.readUInt8(o); o += 1; + const aH = readFixedAscii(buf, o, 25); const cHorses = aH.value; o = aH.next; + const nRaceStartTm = buf.readInt32LE(o); o += 4; + return { nRaceDt, cVenueNum, cRaceNum, cHorses, nRaceStartTm, size: o - offset }; +} +function parseRcvRpi3(buf, offset) { + let o = offset; + const nRaceDt = buf.readInt32LE(o); o += 4; + const cVenueNum = buf.readUInt8(o); o += 1; + const cRaceNum = buf.readUInt8(o); o += 1; + const aP = readFixedAscii(buf, o, 9); const cPools = aP.value; o = aP.next; + return { nRaceDt, cVenueNum, cRaceNum, cPools, size: o - offset }; +} +function parseRcvRpi4(buf, offset) { + let o = offset; + const nRaceDt = buf.readInt32LE(o); o += 4; + const cVenueNum = buf.readUInt8(o); o += 1; + const cRaceNum = buf.subarray(o, o + 3).toString("ascii").replace(/\0/g, ""); o += 3; + const aSel = readFixedAscii(buf, o, 11); const cSelectRaceNum = aSel.value; o = aSel.next; + const cPoolSts = String.fromCharCode(buf[o]); o += 1; + return { nRaceDt, cVenueNum, cRaceNum, cSelectRaceNum, cPoolSts, size: o - offset }; +} +function parseRcvPvm(buf, offset) { + let o = offset; + const a1 = readFixedAscii(buf, o, 4); const cRaceVenue = a1.value; o = a1.next; + const a2 = readFixedAscii(buf, o, 4); const cPoolId = a2.value; o = a2.next; + const fUnitPrice = buf.readFloatLE(o); o += 4; + const fTaxRate = buf.readFloatLE(o); o += 4; + const fDeductRate = buf.readFloatLE(o); o += 4; + const a3 = readFixedAscii(buf, o, 31); const cRemarks = a3.value; o = a3.next; + return { cRaceVenue, cPoolId, fUnitPrice, fTaxRate, fDeductRate, cRemarks, size: o - offset }; +} + +// ---------- 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) }); }); +}); + +// ---------- ABS_POLL ---------- +app.post("/abs/poll", async (req, res) => { + try { + const body = await readJsonBody(req); + 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, cBtMake: btMake }); + + const reply = await withRetry(() => absRoundtrip(header, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS); + const hdr = parseRcvHeaderFlexible(reply.subarray(0, Math.min(reply.length, 24))); + + res.json({ + ok: true, + target: `${ABS_HOST}:${ABS_PORT}`, + sentHeaderHex: header.toString("hex"), + replyBytes: reply.length, + replyHexFirst64: reply.subarray(0, 64).toString("hex"), + parsedRcvHeaderRaw: hdr, + normalizedRcvHeader: { ...hdr, size: 18 }, + normalizedHeaderHex: buildRcvHeader18(hdr).toString("hex"), + success: hdr.nRetCd === SUCCESS + }); + } catch (e) { + res.status(502).json({ ok: false, error: String(e) }); + } +}); + +// ---------- LOGIN ---------- +app.post("/login", async (req, res) => { + try { + const body = await readJsonBody(req); + const opCard = (body.opCard ?? "").toString(); + const btId = (body.btId ?? "").toString(); + const usrId = (body.usrId ?? "").toString(); + const plain = (body.password ?? ""); + const givenEnc = (body.passwordEnc ?? ""); + + if (!opCard || !btId || (!plain && !givenEnc)) { + return res.status(400).json({ ok: false, error: "Missing opCard, btId, and either password or passwordEnc" }); + } + + let encInfo = null; + let passwd = givenEnc ? String(givenEnc) : (encInfo = encryptPasswordLikeMFC(String(plain))).enc; + + const btMake = (typeof body.btMake === "string") + ? body.btMake.charCodeAt(0) + : (Number.isFinite(body.btMake) ? (body.btMake|0) : 0x00); + + const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: LOGBT, nNumRecsSent: 1, nNumRecsRqrd: 1, nTxnId: 0, cBtMake: btMake }); + const sndLog = packSndLog({ usrId, opCard, passwd, btId }); + const sendBuf = Buffer.concat([sndHeader, sndLog]); + + const reply = await withRetry(() => absRoundtrip(sendBuf, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS); + + const rcvHeader = parseRcvHeaderFlexible(reply.subarray(0, Math.min(reply.length, 24))); + + const json = { + ok: true, + target: `${ABS_HOST}:${ABS_PORT}`, + sentHeaderHex: sndHeader.toString("hex"), + sentBodyHex: sndLog.toString("hex"), + replyBytes: reply.length, + replyHexFirst64: reply.subarray(0, 64).toString("hex"), + rcvHeaderRaw: rcvHeader, + success: rcvHeader.nRetCd === SUCCESS + }; + + if (!givenEnc && encInfo) { + json.encryption = { used: true, utcSeconds: encInfo.utcSeconds, key: encInfo.key, encPasswd: passwd }; + } else if (givenEnc) { + json.encryption = { used: false, providedPasswordEnc: true }; + } + + // locate login body start + let bodyOffset = rcvHeader.size; + bodyOffset = adjustForOptionalOp(reply, bodyOffset).offset; + bodyOffset = alignToAsciiYear(reply, bodyOffset); + + json.offsetProbe = { + bodyOffset, + around: reply.subarray(Math.max(0, bodyOffset - 8), Math.min(reply.length, bodyOffset + 48)).toString("ascii") + }; + + if (json.success && reply.length >= bodyOffset + 20) { + try { json.log = parseRcvLog(reply, bodyOffset); } catch (e) { json.parseLogError = String(e); } + } + + res.json(json); + } catch (e) { + res.status(502).json({ ok: false, error: String(e) }); + } +}); + +// ---------- DOWNLOAD BT INFO (LOG/BTI) ---------- +app.post("/download/btinfo", async (req, res) => { + try { + const body = await readJsonBody(req); + const opCard = (body.opCard ?? "").toString(); + const btId = (body.btId ?? "").toString(); + const usrId = (body.usrId ?? "").toString(); + const plain = (body.password ?? ""); + const encPass = (body.passwordEnc ?? ""); + if (!opCard || !btId || (!plain && !encPass)) { + return res.status(400).json({ ok: false, error: "Missing opCard, btId, and either password or passwordEnc" }); + } + const passwd = encPass ? String(encPass) : encryptPasswordLikeMFC(String(plain)).enc; + + const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: BTI, nNumRecsSent: 1, nNumRecsRqrd: 1, nTxnId: 0, cBtMake: 0 }); + const sndLog = packSndLog({ usrId, opCard, passwd, btId }); + const sendBuf = Buffer.concat([sndHeader, sndLog]); + + const reply = await withRetry(() => absRoundtrip(sendBuf, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS); + + const result = { ok: true, replyBytes: reply.length, replyHexFirst64: reply.subarray(0,64).toString('hex') }; + + let offset = 0; + if (remaining(reply, offset) < 18) return res.json({ ok:false, error:"truncated: no header" }); + // const hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset + 24))); + // result.primaryHeader = hdr; + // offset += hdr.size; + + const hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset + 24))); +result.primaryHeader = hdr; + +// Advance to end of header *and* align to the natural 4-byte boundary +// (C structs are commonly padded so sizeof(sRcvHeader) will often be 20, not 18) +const hdrLenAligned = (hdr.size + 3) & ~3; // e.g. 18 -> 20, 24 -> 24 +// safety: ensure we have at least hdrLenAligned bytes in reply +if (!safeAdvanceOrBreak(reply, 0, hdrLenAligned, result)) { + result.error = "truncated: incomplete header (after alignment)"; + return res.json(result); +} +offset = hdrLenAligned; + + + if (hdr.nRetCd !== SUCCESS) { + result.error = `server returned nRetCd=${hdr.nRetCd}`; + return res.json(result); + } + + if (!safeAdvanceOrBreak(reply, offset, 5+31+5+4+1+4+4+4+4, result)) { + result.error = "truncated before BTI master"; + return res.json(result); + } + const btiMst = parseRcvBtiMst(reply, offset); + offset += btiMst.size; + result.btiMst = btiMst; + + const details = []; + let readCount = 0; + while (readCount < hdr.nNumRecs) { + if (looksLikeHeaderAt(reply, offset)) break; + // if (!safeAdvanceOrBreak(reply, offset, 1+4+1, result)) break; + if (!safeAdvanceOrBreak(reply, offset, 1+3+1, result)) break; + + const dt = parseRcvBtiDtl(reply, offset); + offset += dt.size; + details.push(dt); + readCount++; + } + result.btiDetails = details; + result.btiReadCount = readCount; + result.parsedBytes = offset; + + res.json(result); + } catch (e) { + res.status(502).json({ ok: false, error: String(e), stack: e.stack }); + } +}); + +// ---------- DOWNLOAD RP INFO (LOG/RPI) ---------- +app.post("/download/rpinfo", async (req, res) => { + try { + const body = await readJsonBody(req); + const debug = String((req.query.debug ?? "")).toLowerCase() === "1"; + const opCard = (body.opCard ?? "").toString(); + const btId = (body.btId ?? "").toString(); + const usrId = (body.usrId ?? "").toString(); + const plain = (body.password ?? ""); + const encPass = (body.passwordEnc ?? ""); + if (!opCard || !btId || (!plain && !encPass)) { + return res.status(400).json({ ok: false, error: "Missing opCard, btId, and either password or passwordEnc" }); + } + const passwd = encPass ? String(encPass) : encryptPasswordLikeMFC(String(plain)).enc; + + const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: RPI, nNumRecsSent: 1, nNumRecsRqrd: 0, nTxnId: 0, cBtMake: 0 }); + const sndLog = packSndLog({ usrId, opCard, passwd, btId }); + const sendBuf = Buffer.concat([sndHeader, sndLog]); + + const reply = await withRetry(() => absRoundtrip(sendBuf, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS); + + const result = { ok: true, replyBytes: reply.length, replyHexFirst64: reply.subarray(0,64).toString('hex') }; + + let offset = 0; + if (remaining(reply, offset) < 18) return res.json({ ok:false, error:"truncated: no top header" }); + const topHdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset+24))); + offset += topHdr.size; + result.topHeader = topHdr; + if (topHdr.nRetCd !== SUCCESS) { + result.error = `server returned nRetCd=${topHdr.nRetCd}`; + return res.json(result); + } + if (debug) result.debug_afterTopHdr = hexSlice(reply, 0, 128); + + // Helper to parse a section safely + function parseSection(minRecBytes, recParser, outArrName, outHeaderName) { + if (remaining(reply, offset) < 18) { result._truncated = true; return { header:null, items:[], read:0 }; } + offset = alignToPossibleHeader(reply, offset); + const hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset+24))); + offset += hdr.size; + const adj = adjustForOptionalOp(reply, offset); + offset = adj.offset; + + const items = []; + let read = 0; + while (read < hdr.nNumRecs) { + if (looksLikeHeaderAt(reply, offset)) break; + if (!safeAdvanceOrBreak(reply, offset, minRecBytes, result)) break; + const rec = recParser(reply, offset); + offset += rec.size; + items.push(rec); + read++; + } + result[outHeaderName] = hdr; + result[outArrName] = items; + result[outArrName.replace(/s?$/, "ReadCount")] = read; + return { header: hdr, items, read }; + } + + // RPI1 .. RPI4 .. PVM + parseSection(4+1+1+15+41, parseRcvRpi1, "rpi1", "rpi1Header"); + if (debug) result.debug_afterRpi1 = hexSlice(reply, Math.max(0, offset-64), 128); + + parseSection(4+1+1+25+4, parseRcvRpi2, "rpi2", "rpi2Header"); + if (debug) result.debug_afterRpi2 = hexSlice(reply, Math.max(0, offset-64), 128); + + parseSection(4+1+1+9, parseRcvRpi3, "rpi3", "rpi3Header"); + if (debug) result.debug_afterRpi3 = hexSlice(reply, Math.max(0, offset-64), 128); + + parseSection(4+1+3+11+1, parseRcvRpi4, "rpi4", "rpi4Header"); + if (debug) result.debug_afterRpi4 = hexSlice(reply, Math.max(0, offset-64), 128); + + parseSection(4+4+4+4+4+31, parseRcvPvm, "pvms", "pvmHeader"); + + result.parsedBytes = offset; + + // Build summary + const raceCard = {}; + for (const v of result.rpi1 || []) { + const key = `${v.nRaceDt}_${v.cVenueNum}`; + raceCard[key] = { date: v.nRaceDt, venueNum: v.cVenueNum, status: v.cVenueSts, races: {}, advertisement: v.cAdvertisement }; + } + for (const r of result.rpi2 || []) { + const key = `${r.nRaceDt}_${r.cVenueNum}`; + const raceKey = `${r.cRaceNum}`; + if (!raceCard[key]) raceCard[key] = { date: r.nRaceDt, venueNum: r.cVenueNum, races: {} }; + raceCard[key].races[raceKey] = { raceNum: r.cRaceNum, horses: (r.cHorses || "").split(/\s+/).filter(Boolean), startTime: r.nRaceStartTm, pools: [] }; + } + for (const p of result.rpi3 || []) { + const key = `${p.nRaceDt}_${p.cVenueNum}`; + const raceKey = `${p.cRaceNum}`; + if (raceCard[key] && raceCard[key].races[raceKey]) raceCard[key].races[raceKey].pools = (p.cPools || "").split("").filter(Boolean); + } + for (const s of result.rpi4 || []) { + const key = `${s.nRaceDt}_${s.cVenueNum}`; + const raceKey = `${s.cRaceNum}`; + if (raceCard[key] && raceCard[key].races[raceKey]) { + raceCard[key].races[raceKey].poolStatus = s.cPoolSts; + raceCard[key].races[raceKey].selectRaceNum = s.cSelectRaceNum; + } + } + result.raceCard = raceCard; + + res.json(result); + } catch (e) { + res.status(502).json({ ok: false, error: String(e), stack: e.stack }); + } +}); + +// ---- 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/btinfo.sh b/btinfo.sh new file mode 100755 index 0000000..f6f4aa2 --- /dev/null +++ b/btinfo.sh @@ -0,0 +1,4 @@ +#!/bin/bash +curl -sS -X POST "http://localhost:8080/download/btinfo" \ + -H "Content-Type: application/json" \ + -d '{"opCard":"021804111066","password":"0660000","btId":"0483","usrId":"","btMake":0}' | jq diff --git a/debug.log b/debug.log new file mode 100644 index 0000000..e69de29 diff --git a/login.sh b/login.sh new file mode 100755 index 0000000..ec2285b --- /dev/null +++ b/login.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +curl -sS -X POST "http://localhost:8080/login" \ + -H "Content-Type: application/json" \ + -d '{"opCard":"021804111066","password":"0660000","btId":"0483","usrId":"","btMake":0}' | jq + diff --git a/login_logs.json b/login_logs.json deleted file mode 100644 index 0f850c0..0000000 --- a/login_logs.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "info": { - "_postman_id": "3dd79a7d-47b7-4f2a-86b9-e35f48b54bb3", - "name": "login_abs", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "46024117", - "_collection_link": "https://johnlogan-6539403.postman.co/workspace/john-logan's-Workspace~cde4aa19-228c-45b3-88cb-692cf4289226/collection/46024117-3dd79a7d-47b7-4f2a-86b9-e35f48b54bb3?action=share&source=collection_link&creator=46024117" - }, - "item": [ - { - "name": "http://localhost:8080/login", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"opCard\": \"021804111066\",\n \"password\": \"0660000\",\n \"btId\": \"0483\",\n \"usrId\": \"\",\n \"btMake\": \"btmake\"\n}\n", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://localhost:8080/login", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "login" - ] - } - }, - "response": [ - { - "name": "http://localhost:8080/login", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"opCard\": \"021804111066\",\n \"password\": \"0660000\",\n \"btId\": \"0483\",\n \"usrId\": \"\",\n \"btMake\": \"btmake\"\n}\n", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "http://localhost:8080/login", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "8080", - "path": [ - "login" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "", - "header": [ - { - "key": "X-Powered-By", - "value": "Express" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8" - }, - { - "key": "Content-Length", - "value": "1189" - }, - { - "key": "ETag", - "value": "W/\"4a5-+Kk5viaxNtYc9xJLAnpb48uH9QQ\"" - }, - { - "key": "Date", - "value": "Wed, 20 Aug 2025 11:07:59 GMT" - }, - { - "key": "Connection", - "value": "keep-alive" - }, - { - "key": "Keep-Alive", - "value": "timeout=5" - } - ], - "cookie": [], - "body": "{\n \"ok\": true,\n \"target\": \"192.0.0.14:7000\",\n \"sentHeaderHex\": \"640000007d17000001000000010000000000000062000000\",\n \"sentBodyHex\": \"0000000000303231383034313131303636000000000035313135353535350000003034383300\",\n \"replyBytes\": 511,\n \"replyHexFirst64\": \"6400000000000000020000000000000062000000323032352f30382f32302f31363a33373a33350052454e554b41505241534144202e4b000000000000000000\",\n \"rcvHeaderRaw\": {\n \"nTxnCd\": 100,\n \"nRetCd\": 0,\n \"nNumRecs\": 2,\n \"nTxnId\": 0,\n \"cBtMake\": 98,\n \"size\": 24\n },\n \"success\": true,\n \"encryption\": {\n \"used\": true,\n \"utcSeconds\": 59,\n \"key\": 5,\n \"encPasswd\": \"51155555\"\n },\n \"log\": {\n \"cDateTime\": \"/08/20/16:37:35\",\n \"cUsrNm\": \"KAPRASAD .K\",\n \"cUsrId\": \"\",\n \"cSupNm\": \"\",\n \"cSupId\": \"\",\n \"cUsrTyp\": \"\",\n \"fOpenBal\": 0,\n \"fTktSalesByVoucher\": 0,\n \"fTktSalesByCash\": 0,\n \"fTktSalesByMemCard\": 0,\n \"nTktSalesByVoucherCount\": 0,\n \"nTktSalesByCashCount\": 0,\n \"nTktSalesByMemCardCount\": 0,\n \"fPayoutByVoucher\": 0,\n \"fPayoutByCash\": 0,\n \"fPayoutByMemCard\": 0,\n \"nPayoutByVoucherCount\": 0,\n \"nPayoutByCashCount\": 0,\n \"nPayoutByMemCardCount\": 0,\n \"fCancelByVoucher\": 0,\n \"fCancelByCash\": 0,\n \"fCancelByMemCard\": 0,\n \"nCancelByVoucherCount\": 0,\n \"nCancelByCashCount\": 0,\n \"nCancelByMemCardCount\": 0,\n \"fDeposit\": 0,\n \"fWithdrawAmt\": 0,\n \"fVoucherSales\": 0,\n \"fVoucherEncash\": 0,\n \"fCloseBal\": 0,\n \"fSaleTarget\": 0,\n \"size\": 197\n }\n}" - } - ] - } - ] -} \ No newline at end of file diff --git a/mathew.js b/mathew.js new file mode 100644 index 0000000..2efc1fa --- /dev/null +++ b/mathew.js @@ -0,0 +1,764 @@ +// server.js — robust HTTP→TCP bridge for ABS (ABS_POLL, LOG, BTI, RPI) +// Node 18+ (CommonJS) + +const express = require("express"); +const net = require("net"); + +const ABS_HOST = process.env.ABS_HOST || "192.0.0.14"; +const ABS_PORT = Number(process.env.ABS_PORT || 7000); + +const ABS_POLL = 178; +const LOG = 100; +const LOGBT = 6013; +const BTI = 6014; +const RPI = 6015; +const SUCCESS = 0; + +const PKT_SIZE = 512; +const PAYLOAD_PER_PKT = PKT_SIZE - 1; + +const DEFAULT_TIMEOUT_MS = Number(process.env.BRIDGE_TIMEOUT_MS || 20000); +const RETRIES = Number(process.env.BRIDGE_RETRIES || 2); +const RETRY_DELAY_MS = Number(process.env.BRIDGE_RETRY_DELAY_MS || 500); + +const app = express(); + +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({}); } + }); + }); +} + +// ---------- helpers ---------- +function writeFixedAscii(buf, offset, s, len) { + const str = (s ?? "").toString(); + for (let i = 0; i < len; i++) buf[offset + i] = i < str.length ? (str.charCodeAt(i) & 0xff) : 0x00; + return offset + len; +} +function readFixedAscii(buf, offset, len) { + let end = offset; + const max = offset + len; + while (end < max && buf[end] !== 0) end++; + const raw = buf.subarray(offset, end).toString("ascii"); + return { value: raw.trimEnd(), next: offset + len }; +} +function hexSlice(buf, off, len) { + const s = Math.max(0, off); + const e = Math.min(buf.length, off + len); + return buf.subarray(s, e).toString("hex"); +} +function remaining(buf, off){ return Math.max(0, buf.length - off); } + +// ---------- packetization ---------- +function absSend(sock, payload) { + return new Promise((resolve, reject) => { + let off = 0; + function sendNext() { + 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 : 49; + 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(); + sendNext(); + }); + } + if (!payload || payload.length === 0) { + const pkt = Buffer.alloc(PKT_SIZE, 0); + pkt[0] = 48; + sock.write(pkt, (err) => err ? reject(err) : resolve()); + } else { + sendNext(); + } + }); +} +function absRecv(sock, timeoutMs = DEFAULT_TIMEOUT_MS) { + 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(); + } + } + + 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 = DEFAULT_TIMEOUT_MS) { + 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 || Buffer.alloc(0)); + const replyConcat = await absRecv(sock, timeoutMs); + sock.end(); + return replyConcat; + } catch (e) { + try { sock.destroy(); } catch {} + throw e; + } +} +async function withRetry(fn, tries = RETRIES, delayMs = RETRY_DELAY_MS) { + let lastErr; + for (let i=0;isetTimeout(r, delayMs)); } + } + throw lastErr; +} + +// ---------- MFC-style password "encryption" ---------- +function encryptPasswordLikeMFC(plain) { + const sec = new Date().getUTCSeconds(); + const key = Number(String(sec)[0] || "0"); + let out = ""; + const s = (plain ?? "").toString(); + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + const d = (ch >= "0" && ch <= "9") ? (ch.charCodeAt(0) - 48) : 0; + out += String((d + key) % 10); + } + return { enc: out + String(key), key, utcSeconds: sec }; +} + +// ---------- headers ---------- +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 & 0xff, o); + return b; +} + +/** Smart header parser: test 18 & 24, choose plausible. */ +function parseRcvHeaderFlexible(buf) { + if (buf.length < 18) throw new Error(`Reply too short for RcvHeader: ${buf.length} bytes`); + function readAs(len) { + 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: len }; + } + const h18 = readAs(18); + const ok18 = (h18.nTxnCd >= 50 && h18.nTxnCd < 10000) && (h18.nRetCd >= 0 && h18.nRetCd < 10000); + let h24=null, ok24=false; + if (buf.length >= 24) { + h24 = readAs(24); + ok24 = (h24.nTxnCd >= 50 && h24.nTxnCd < 10000) && (h24.nRetCd >= 0 && h24.nRetCd < 10000); + } + if (ok18 && !ok24) return h18; + if (!ok18 && ok24) return h24; + if (ok18 && ok24) return h18; // prefer 18 to avoid over-advancing + return h18; +} +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 & 0xff, o); + return b; +} + +// ---------- aligners / detectors ---------- +function looksLikeHeaderAt(buf, off){ + if (remaining(buf, off) < 18) return false; + try { + const nTxnCd = buf.readInt32LE(off); + return (nTxnCd >= 50 && nTxnCd < 10000); + } catch { return false; } +} +/** align to plausible header; try +0 / +4 */ +function alignToPossibleHeader(reply, off) { + if (looksLikeHeaderAt(reply, off)) return off; + if (remaining(reply, off) >= 22 && looksLikeHeaderAt(reply, off + 4)) return off + 4; + return off; +} +/** align to where ASCII year "20" begins for login body */ +function alignToAsciiYear(reply, off) { + const tryOffsets = [0, 2, 4, -4]; + for (const d of tryOffsets) { + const o = off + d; + if (o >= 0 && o + 1 < reply.length) { + const b0 = reply[o], b1 = reply[o+1]; + if (b0 === 0x32 && b1 === 0x30) return o; // '2''0' + if (reply[o] === 0x2f && o >= 4 && reply[o-4] === 0x32 && reply[o-3] === 0x30) return o-4; + } + } + return off; +} + +function adjustForOptionalOp(replyBuf, bodyOffset) { + if (replyBuf.length >= bodyOffset + 4) { + const maybeOp = replyBuf.readInt32LE(bodyOffset); + if (maybeOp === LOGBT || maybeOp === LOG || maybeOp === 0 || (maybeOp >= 100 && maybeOp <= 10000)) { + return { offset: bodyOffset + 4, extraOp: maybeOp }; + } else { + const looksLikeYear = replyBuf[bodyOffset] === 0x32 && replyBuf[bodyOffset + 1] === 0x30; + const looksLikeYearFwd = replyBuf[bodyOffset + 4] === 0x32 && replyBuf[bodyOffset + 5] === 0x30; + if (!looksLikeYear && looksLikeYearFwd) return { offset: bodyOffset + 4, extraOp: null }; + } + } + if (bodyOffset >= 4 && replyBuf[bodyOffset] === 0x2f) { + const couldBeYear = replyBuf[bodyOffset - 4] === 0x32 && replyBuf[bodyOffset - 3] === 0x30; + if (couldBeYear) return { offset: bodyOffset - 4, extraOp: null }; + } + return { offset: bodyOffset, extraOp: null }; +} + +function safeAdvanceOrBreak(buf, off, want, outObj){ + if (remaining(buf, off) < want){ + outObj._truncated = true; + return false; + } + return true; +} + +// ---------- parsers ---------- +function packSndLog({ usrId = "", opCard, passwd, btId }) { + const b = Buffer.alloc(38, 0); + let o = 0; + o = writeFixedAscii(b, o, usrId, 5); + o = writeFixedAscii(b, o, opCard, 17); + o = writeFixedAscii(b, o, passwd, 11); + o = writeFixedAscii(b, o, btId, 5); + return b; +} +function parseRcvLog(buf, offset = 0) { + let o = offset; + const t1 = readFixedAscii(buf, o, 20); const cDateTime = t1.value; o = t1.next; + const t2 = readFixedAscii(buf, o, 31); const cUsrNm = t2.value; o = t2.next; + const t3 = readFixedAscii(buf, o, 5 ); const cUsrId = t3.value; o = t3.next; + const t4 = readFixedAscii(buf, o, 31); const cSupNm = t4.value; o = t4.next; + const t5 = readFixedAscii(buf, o, 5 ); const cSupId = t5.value; o = t5.next; + const t6 = readFixedAscii(buf, o, 5 ); const cUsrTyp = t6.value; o = t6.next; + + function rf() { const v = buf.readFloatLE(o); o += 4; return v; } + function ri() { const v = buf.readInt32LE(o); o += 4; return v; } + + const fOpenBal = rf(); + const fTktSalesByVoucher = rf(); + const fTktSalesByCash = rf(); + const fTktSalesByMemCard = rf(); + const nTktSalesByVoucherCount = ri(); + const nTktSalesByCashCount = ri(); + const nTktSalesByMemCardCount = ri(); + const fPayoutByVoucher = rf(); + const fPayoutByCash = rf(); + const fPayoutByMemCard = rf(); + const nPayoutByVoucherCount = ri(); + const nPayoutByCashCount = ri(); + const nPayoutByMemCardCount = ri(); + const fCancelByVoucher = rf(); + const fCancelByCash = rf(); + const fCancelByMemCard = rf(); + const nCancelByVoucherCount = ri(); + const nCancelByCashCount = ri(); + const nCancelByMemCardCount = ri(); + const fDeposit = rf(); + const fWithdrawAmt = rf(); + const fVoucherSales = rf(); + const fVoucherEncash = rf(); + const fCloseBal = rf(); + const fSaleTarget = rf(); + + return { + cDateTime, cUsrNm, cUsrId, cSupNm, cSupId, cUsrTyp, + fOpenBal, fTktSalesByVoucher, fTktSalesByCash, fTktSalesByMemCard, + nTktSalesByVoucherCount, nTktSalesByCashCount, nTktSalesByMemCardCount, + fPayoutByVoucher, fPayoutByCash, fPayoutByMemCard, + nPayoutByVoucherCount, nPayoutByCashCount, nPayoutByMemCardCount, + fCancelByVoucher, fCancelByCash, fCancelByMemCard, + nCancelByVoucherCount, nCancelByCashCount, nCancelByMemCardCount, + fDeposit, fWithdrawAmt, fVoucherSales, fVoucherEncash, + fCloseBal, fSaleTarget, + size: o - offset + }; +} + + + +// function parseRcvBtiMst(buf, offset) { +// let o = offset; + +// const a1 = readFixedAscii(buf, o, 5); const cBtId = a1.value; o = a1.next; +// const a2 = readFixedAscii(buf, o, 31); const cBtName = a2.value; o = a2.next; +// const a3 = readFixedAscii(buf, o, 5); const cEnclCd = a3.value; o = a3.next; +// const a4 = readFixedAscii(buf, o, 4); const cGrpCd = a4.value; o = a4.next; + +// const cBtMode = String.fromCharCode(buf[o]); o += 1; + +// const a5 = readFixedAscii(buf, o, 4); const cMoneyTyp = a5.value; o = a5.next; + +// // Debug: raw hex of these fields +// console.error("fMinSaleBet raw:", buf.subarray(o, o+4).toString("hex")); +// const fMinSaleBet = buf.readInt32LE(o); o += 4; + +// console.error("fMaxSaleBet raw:", buf.subarray(o, o+4).toString("hex")); +// const fMaxSaleBet = buf.readInt32LE(o); o += 4; + +// console.error("fPayMaxAmt raw:", buf.subarray(o, o+4).toString("hex")); +// const fPayMaxAmt = buf.readInt32LE(o); o += 4; + +// return { +// cBtId, cBtName, cEnclCd, cGrpCd, +// cBtMode, cMoneyTyp, +// fMinSaleBet, fMaxSaleBet, fPayMaxAmt, +// size: o - offset +// }; +// } + + +function parseRcvBtiMst(buf, offset) { + let o = offset; + + const a1 = readFixedAscii(buf, o, 5); const cBtId = a1.value; o = a1.next; + const a2 = readFixedAscii(buf, o, 31); const cBtName = a2.value; o = a2.next; + const a3 = readFixedAscii(buf, o, 5); const cEnclCd = a3.value; o = a3.next; + const a4 = readFixedAscii(buf, o, 4); const cGrpCd = a4.value; o = a4.next; + + const cBtMode = String.fromCharCode(buf[o]); o += 1; + + const a5 = readFixedAscii(buf, o, 4); const cMoneyTyp = a5.value; o = a5.next; + + // align to 4-byte boundary (for float fields) + o = (o + 3) & ~3; + + console.error("fMinSaleBet raw:", buf.subarray(o, o+4).toString("hex")); + const fMinSaleBet = buf.readFloatLE(o); o += 4; + + console.error("fMaxSaleBet raw:", buf.subarray(o, o+4).toString("hex")); + const fMaxSaleBet = buf.readFloatLE(o); o += 4; + + console.error("fPayMaxAmt raw:", buf.subarray(o, o+4).toString("hex")); + const fPayMaxAmt = buf.readFloatLE(o); o += 4; + + return { + cBtId, cBtName, cEnclCd, cGrpCd, + cBtMode, cMoneyTyp, + fMinSaleBet, fMaxSaleBet, fPayMaxAmt, + size: o - offset + }; +} + + +function parseRcvBtiDtl(buf, offset) { + let o = offset; + const cDtlTyp = String.fromCharCode(buf[o]); o += 1; + + // If not enough bytes to try the happy path, take what's available + if (remaining(buf, o) < 4) { + const avail = Math.max(0, remaining(buf, o)); + const aRem = readFixedAscii(buf, o, avail); + const cPoolOrVenue = aRem.value; + o = aRem.next; + const cDtlSts = o < buf.length ? String.fromCharCode(buf[o]) : ""; + if (o < buf.length) o += 1; + return { cDtlTyp, cPoolOrVenue, cDtlSts, size: o - offset }; + } + + // Try 3-byte pool/venue first (most common) + const a3 = readFixedAscii(buf, o, 3); + const cPoolOrVenue3 = a3.value; + const after3 = a3.next; + + // Candidate status byte (must be printable ASCII — typical statuses are letters/digits) + const statusCandidate = buf[after3]; + const isPrintable = (statusCandidate >= 0x20 && statusCandidate <= 0x7E); + + if (isPrintable) { + o = after3; + const cDtlSts = String.fromCharCode(buf[o]); o += 1; + return { cDtlTyp, cPoolOrVenue: cPoolOrVenue3, cDtlSts, size: o - offset }; + } + + // Fallback: read 4-byte pool/venue (older/odd layouts) + const a4 = readFixedAscii(buf, o, 4); + const cPoolOrVenue4 = a4.value; + o = a4.next; + const cDtlSts = o < buf.length ? String.fromCharCode(buf[o]) : ""; + if (o < buf.length) o += 1; + return { cDtlTyp, cPoolOrVenue: cPoolOrVenue4, cDtlSts, size: o - offset }; +} + + + + + +function parseRcvRpi1(buf, offset) { + let o = offset; + const nRaceDt = buf.readInt32LE(o); o += 4; + const cVenueNum = buf.readUInt8(o); o += 1; + const cVenueSts = String.fromCharCode(buf[o]); o += 1; + const aR = readFixedAscii(buf, o, 15); const cRaces = aR.value; o = aR.next; + const aAd = readFixedAscii(buf, o, 41); const cAdvertisement = aAd.value; o = aAd.next; + return { nRaceDt, cVenueNum, cVenueSts, cRaces, cAdvertisement, size: o - offset }; +} +function parseRcvRpi2(buf, offset) { + let o = offset; + const nRaceDt = buf.readInt32LE(o); o += 4; + const cVenueNum = buf.readUInt8(o); o += 1; + const cRaceNum = buf.readUInt8(o); o += 1; + const aH = readFixedAscii(buf, o, 25); const cHorses = aH.value; o = aH.next; + const nRaceStartTm = buf.readInt32LE(o); o += 4; + return { nRaceDt, cVenueNum, cRaceNum, cHorses, nRaceStartTm, size: o - offset }; +} +function parseRcvRpi3(buf, offset) { + let o = offset; + const nRaceDt = buf.readInt32LE(o); o += 4; + const cVenueNum = buf.readUInt8(o); o += 1; + const cRaceNum = buf.readUInt8(o); o += 1; + const aP = readFixedAscii(buf, o, 9); const cPools = aP.value; o = aP.next; + return { nRaceDt, cVenueNum, cRaceNum, cPools, size: o - offset }; +} +function parseRcvRpi4(buf, offset) { + let o = offset; + const nRaceDt = buf.readInt32LE(o); o += 4; + const cVenueNum = buf.readUInt8(o); o += 1; + const cRaceNum = buf.subarray(o, o + 3).toString("ascii").replace(/\0/g, ""); o += 3; + const aSel = readFixedAscii(buf, o, 11); const cSelectRaceNum = aSel.value; o = aSel.next; + const cPoolSts = String.fromCharCode(buf[o]); o += 1; + return { nRaceDt, cVenueNum, cRaceNum, cSelectRaceNum, cPoolSts, size: o - offset }; +} +function parseRcvPvm(buf, offset) { + let o = offset; + const a1 = readFixedAscii(buf, o, 4); const cRaceVenue = a1.value; o = a1.next; + const a2 = readFixedAscii(buf, o, 4); const cPoolId = a2.value; o = a2.next; + const fUnitPrice = buf.readFloatLE(o); o += 4; + const fTaxRate = buf.readFloatLE(o); o += 4; + const fDeductRate = buf.readFloatLE(o); o += 4; + const a3 = readFixedAscii(buf, o, 31); const cRemarks = a3.value; o = a3.next; + return { cRaceVenue, cPoolId, fUnitPrice, fTaxRate, fDeductRate, cRemarks, size: o - offset }; +} + +// ---------- 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) }); }); +}); + +// ---------- ABS_POLL ---------- +app.post("/abs/poll", async (req, res) => { + try { + const body = await readJsonBody(req); + 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, cBtMake: btMake }); + + const reply = await withRetry(() => absRoundtrip(header, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS); + const hdr = parseRcvHeaderFlexible(reply.subarray(0, Math.min(reply.length, 24))); + + res.json({ + ok: true, + target: `${ABS_HOST}:${ABS_PORT}`, + sentHeaderHex: header.toString("hex"), + replyBytes: reply.length, + replyHexFirst64: reply.subarray(0, 64).toString("hex"), + parsedRcvHeaderRaw: hdr, + normalizedRcvHeader: { ...hdr, size: 18 }, + normalizedHeaderHex: buildRcvHeader18(hdr).toString("hex"), + success: hdr.nRetCd === SUCCESS + }); + } catch (e) { + res.status(502).json({ ok: false, error: String(e) }); + } +}); + +// ---------- LOGIN ---------- +app.post("/login", async (req, res) => { + try { + const body = await readJsonBody(req); + const opCard = (body.opCard ?? "").toString(); + const btId = (body.btId ?? "").toString(); + const usrId = (body.usrId ?? "").toString(); + const plain = (body.password ?? ""); + const givenEnc = (body.passwordEnc ?? ""); + + if (!opCard || !btId || (!plain && !givenEnc)) { + return res.status(400).json({ ok: false, error: "Missing opCard, btId, and either password or passwordEnc" }); + } + + let encInfo = null; + let passwd = givenEnc ? String(givenEnc) : (encInfo = encryptPasswordLikeMFC(String(plain))).enc; + + const btMake = (typeof body.btMake === "string") + ? body.btMake.charCodeAt(0) + : (Number.isFinite(body.btMake) ? (body.btMake|0) : 0x00); + + const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: LOGBT, nNumRecsSent: 1, nNumRecsRqrd: 1, nTxnId: 0, cBtMake: btMake }); + const sndLog = packSndLog({ usrId, opCard, passwd, btId }); + const sendBuf = Buffer.concat([sndHeader, sndLog]); + + const reply = await withRetry(() => absRoundtrip(sendBuf, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS); + + const rcvHeader = parseRcvHeaderFlexible(reply.subarray(0, Math.min(reply.length, 24))); + + const json = { + ok: true, + target: `${ABS_HOST}:${ABS_PORT}`, + sentHeaderHex: sndHeader.toString("hex"), + sentBodyHex: sndLog.toString("hex"), + replyBytes: reply.length, + replyHexFirst64: reply.subarray(0, 64).toString("hex"), + rcvHeaderRaw: rcvHeader, + success: rcvHeader.nRetCd === SUCCESS + }; + + if (!givenEnc && encInfo) { + json.encryption = { used: true, utcSeconds: encInfo.utcSeconds, key: encInfo.key, encPasswd: passwd }; + } else if (givenEnc) { + json.encryption = { used: false, providedPasswordEnc: true }; + } + + // locate login body start + let bodyOffset = rcvHeader.size; + bodyOffset = adjustForOptionalOp(reply, bodyOffset).offset; + bodyOffset = alignToAsciiYear(reply, bodyOffset); + + json.offsetProbe = { + bodyOffset, + around: reply.subarray(Math.max(0, bodyOffset - 8), Math.min(reply.length, bodyOffset + 48)).toString("ascii") + }; + + if (json.success && reply.length >= bodyOffset + 20) { + try { json.log = parseRcvLog(reply, bodyOffset); } catch (e) { json.parseLogError = String(e); } + } + + res.json(json); + } catch (e) { + res.status(502).json({ ok: false, error: String(e) }); + } +}); + +// ---------- DOWNLOAD BT INFO (LOG/BTI) ---------- +app.post("/download/btinfo", async (req, res) => { + try { + const body = await readJsonBody(req); + const opCard = (body.opCard ?? "").toString(); + const btId = (body.btId ?? "").toString(); + const usrId = (body.usrId ?? "").toString(); + const plain = (body.password ?? ""); + const encPass = (body.passwordEnc ?? ""); + if (!opCard || !btId || (!plain && !encPass)) { + return res.status(400).json({ ok: false, error: "Missing opCard, btId, and either password or passwordEnc" }); + } + const passwd = encPass ? String(encPass) : encryptPasswordLikeMFC(String(plain)).enc; + + const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: BTI, nNumRecsSent: 1, nNumRecsRqrd: 1, nTxnId: 0, cBtMake: 0 }); + const sndLog = packSndLog({ usrId, opCard, passwd, btId }); + const sendBuf = Buffer.concat([sndHeader, sndLog]); + + const reply = await withRetry(() => absRoundtrip(sendBuf, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS); + + const result = { ok: true, replyBytes: reply.length, replyHexFirst64: reply.subarray(0,64).toString('hex') }; + + let offset = 0; + if (remaining(reply, offset) < 18) return res.json({ ok:false, error:"truncated: no header" }); + // const hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset + 24))); + // result.primaryHeader = hdr; + // offset += hdr.size; + + const hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset + 24))); +result.primaryHeader = hdr; + +// Advance to end of header *and* align to the natural 4-byte boundary +// (C structs are commonly padded so sizeof(sRcvHeader) will often be 20, not 18) +const hdrLenAligned = (hdr.size + 3) & ~3; // e.g. 18 -> 20, 24 -> 24 +// safety: ensure we have at least hdrLenAligned bytes in reply +if (!safeAdvanceOrBreak(reply, 0, hdrLenAligned, result)) { + result.error = "truncated: incomplete header (after alignment)"; + return res.json(result); +} +offset = hdrLenAligned; + + + if (hdr.nRetCd !== SUCCESS) { + result.error = `server returned nRetCd=${hdr.nRetCd}`; + return res.json(result); + } + + if (!safeAdvanceOrBreak(reply, offset, 5+31+5+4+1+4+4+4+4, result)) { + result.error = "truncated before BTI master"; + return res.json(result); + } + const btiMst = parseRcvBtiMst(reply, offset); + offset += btiMst.size; + result.btiMst = btiMst; + + const details = []; + let readCount = 0; + while (readCount < hdr.nNumRecs) { + if (looksLikeHeaderAt(reply, offset)) break; + // if (!safeAdvanceOrBreak(reply, offset, 1+4+1, result)) break; + if (!safeAdvanceOrBreak(reply, offset, 1+3+1, result)) break; + + const dt = parseRcvBtiDtl(reply, offset); + offset += dt.size; + details.push(dt); + readCount++; + } + result.btiDetails = details; + result.btiReadCount = readCount; + result.parsedBytes = offset; + + res.json(result); + } catch (e) { + res.status(502).json({ ok: false, error: String(e), stack: e.stack }); + } +}); + +// ---------- DOWNLOAD RP INFO (LOG/RPI) ---------- +app.post("/download/rpinfo", async (req, res) => { + try { + const body = await readJsonBody(req); + const debug = String((req.query.debug ?? "")).toLowerCase() === "1"; + const opCard = (body.opCard ?? "").toString(); + const btId = (body.btId ?? "").toString(); + const usrId = (body.usrId ?? "").toString(); + const plain = (body.password ?? ""); + const encPass = (body.passwordEnc ?? ""); + if (!opCard || !btId || (!plain && !encPass)) { + return res.status(400).json({ ok: false, error: "Missing opCard, btId, and either password or passwordEnc" }); + } + const passwd = encPass ? String(encPass) : encryptPasswordLikeMFC(String(plain)).enc; + + const sndHeader = packSndHeader({ nTxnCd: LOG, nOpCd: RPI, nNumRecsSent: 1, nNumRecsRqrd: 0, nTxnId: 0, cBtMake: 0 }); + const sndLog = packSndLog({ usrId, opCard, passwd, btId }); + const sendBuf = Buffer.concat([sndHeader, sndLog]); + + const reply = await withRetry(() => absRoundtrip(sendBuf, DEFAULT_TIMEOUT_MS), RETRIES, RETRY_DELAY_MS); + + const result = { ok: true, replyBytes: reply.length, replyHexFirst64: reply.subarray(0,64).toString('hex') }; + + let offset = 0; + if (remaining(reply, offset) < 18) return res.json({ ok:false, error:"truncated: no top header" }); + const topHdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset+24))); + offset += topHdr.size; + result.topHeader = topHdr; + if (topHdr.nRetCd !== SUCCESS) { + result.error = `server returned nRetCd=${topHdr.nRetCd}`; + return res.json(result); + } + if (debug) result.debug_afterTopHdr = hexSlice(reply, 0, 128); + + // Helper to parse a section safely + function parseSection(minRecBytes, recParser, outArrName, outHeaderName) { + if (remaining(reply, offset) < 18) { result._truncated = true; return { header:null, items:[], read:0 }; } + offset = alignToPossibleHeader(reply, offset); + const hdr = parseRcvHeaderFlexible(reply.subarray(offset, Math.min(reply.length, offset+24))); + offset += hdr.size; + const adj = adjustForOptionalOp(reply, offset); + offset = adj.offset; + + const items = []; + let read = 0; + while (read < hdr.nNumRecs) { + if (looksLikeHeaderAt(reply, offset)) break; + if (!safeAdvanceOrBreak(reply, offset, minRecBytes, result)) break; + const rec = recParser(reply, offset); + offset += rec.size; + items.push(rec); + read++; + } + result[outHeaderName] = hdr; + result[outArrName] = items; + result[outArrName.replace(/s?$/, "ReadCount")] = read; + return { header: hdr, items, read }; + } + + // RPI1 .. RPI4 .. PVM + parseSection(4+1+1+15+41, parseRcvRpi1, "rpi1", "rpi1Header"); + if (debug) result.debug_afterRpi1 = hexSlice(reply, Math.max(0, offset-64), 128); + + parseSection(4+1+1+25+4, parseRcvRpi2, "rpi2", "rpi2Header"); + if (debug) result.debug_afterRpi2 = hexSlice(reply, Math.max(0, offset-64), 128); + + parseSection(4+1+1+9, parseRcvRpi3, "rpi3", "rpi3Header"); + if (debug) result.debug_afterRpi3 = hexSlice(reply, Math.max(0, offset-64), 128); + + parseSection(4+1+3+11+1, parseRcvRpi4, "rpi4", "rpi4Header"); + if (debug) result.debug_afterRpi4 = hexSlice(reply, Math.max(0, offset-64), 128); + + parseSection(4+4+4+4+4+31, parseRcvPvm, "pvms", "pvmHeader"); + + result.parsedBytes = offset; + + // Build summary + const raceCard = {}; + for (const v of result.rpi1 || []) { + const key = `${v.nRaceDt}_${v.cVenueNum}`; + raceCard[key] = { date: v.nRaceDt, venueNum: v.cVenueNum, status: v.cVenueSts, races: {}, advertisement: v.cAdvertisement }; + } + for (const r of result.rpi2 || []) { + const key = `${r.nRaceDt}_${r.cVenueNum}`; + const raceKey = `${r.cRaceNum}`; + if (!raceCard[key]) raceCard[key] = { date: r.nRaceDt, venueNum: r.cVenueNum, races: {} }; + raceCard[key].races[raceKey] = { raceNum: r.cRaceNum, horses: (r.cHorses || "").split(/\s+/).filter(Boolean), startTime: r.nRaceStartTm, pools: [] }; + } + for (const p of result.rpi3 || []) { + const key = `${p.nRaceDt}_${p.cVenueNum}`; + const raceKey = `${p.cRaceNum}`; + if (raceCard[key] && raceCard[key].races[raceKey]) raceCard[key].races[raceKey].pools = (p.cPools || "").split("").filter(Boolean); + } + for (const s of result.rpi4 || []) { + const key = `${s.nRaceDt}_${s.cVenueNum}`; + const raceKey = `${s.cRaceNum}`; + if (raceCard[key] && raceCard[key].races[raceKey]) { + raceCard[key].races[raceKey].poolStatus = s.cPoolSts; + raceCard[key].races[raceKey].selectRaceNum = s.cSelectRaceNum; + } + } + result.raceCard = raceCard; + + res.json(result); + } catch (e) { + res.status(502).json({ ok: false, error: String(e), stack: e.stack }); + } +}); + +// ---- 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/new_bridge.js b/new_bridge.js deleted file mode 100644 index 3978684..0000000 --- a/new_bridge.js +++ /dev/null @@ -1,391 +0,0 @@ -// bridge.js — HTTP→TCP bridge for ABS (ABS_POLL + LOG/LOGBT login) using 512-byte MFC-style frames -// CommonJS (Node 18+) - -const express = require("express"); -const net = require("net"); - -// ---- ABS endpoint (no server changes) ---- -const ABS_HOST = process.env.ABS_HOST || "192.0.0.14"; -const ABS_PORT = Number(process.env.ABS_PORT || 7000); - -// ---- transaction/opcode constants (from your grep) ---- -const ABS_POLL = 178; // connectivity ping -const LOG = 100; // login transaction code -const LOGBT = 6013; // login opcode -const SUCCESS = 0; - -// ---- 512-byte framing ---- -const PKT_SIZE = 512; -const PAYLOAD_PER_PKT = PKT_SIZE - 1; - -const app = express(); - -// we intentionally avoid global express.json() — we accept empty bodies safely -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({}); } - }); - }); -} - -// ---------- helpers: fixed-width strings, trimming ---------- -function writeFixedAscii(buf, offset, s, len) { - const str = (s ?? "").toString(); - for (let i = 0; i < len; i++) { - buf[offset + i] = i < str.length ? (str.charCodeAt(i) & 0xff) : 0x00; - } - return offset + len; -} -function readFixedAscii(buf, offset, len) { - let end = offset; - const max = offset + len; - while (end < max && buf[end] !== 0) end++; - const raw = buf.subarray(offset, end).toString("ascii"); - return { value: raw.trimEnd(), next: offset + len }; -} - -// ---------- struct packers / parsers (little-endian) ---------- - -// sSndHeader (24 bytes) -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 & 0xff, o); - return b; -} - -// sRcvHeader (18 or 24 bytes on the wire) -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 }; -} - -// normalized 18-byte header (for display) -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 & 0xff, o); - return b; -} - -// ---------- ABS 512-byte packetization ---------- -function absSend(sock, payload) { - return new Promise((resolve, reject) => { - let off = 0; - function sendNext() { - 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 - sendNext(); - }); - } - sendNext(); - }); -} - -function absRecv(sock, timeoutMs = 7000) { - 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; - } -} - -// ---------- MFC-style password "encryption" ---------- -/* - Mirrors the MFC EncryptPassword(): - - key = first digit of current UTC seconds (0..5, or 0..9 if seconds >= 100 ever, but it's 0..5) - - for each input char: interpret as digit (non-digit -> 0), add key, keep ones digit - - append key at the end - Result length = input.length + 1 (cPasswd field in sSndLog is 11 bytes, so 10-digit inputs fit). -*/ -function encryptPasswordLikeMFC(plain) { - const sec = new Date().getUTCSeconds(); // GetSystemTime (UTC) seconds - const key = Number(String(sec)[0] || "0"); // first digit - let out = ""; - const s = (plain ?? "").toString(); - for (let i = 0; i < s.length; i++) { - const ch = s[i]; - const d = (ch >= "0" && ch <= "9") ? (ch.charCodeAt(0) - 48) : 0; // atoi on non-digit -> 0 - out += String((d + key) % 10); - } - return { enc: out + String(key), key, utcSeconds: sec }; -} - -// ---------- SND/RCV: LOGIN ---------- - -// sSndLog (38 bytes): cUsrId[5], cOpCardCd[17], cPasswd[11], cBtId[5] -function packSndLog({ usrId = "", opCard, passwd, btId }) { - const b = Buffer.alloc(38, 0); - let o = 0; - o = writeFixedAscii(b, o, usrId, 5); - o = writeFixedAscii(b, o, opCard, 17); - o = writeFixedAscii(b, o, passwd, 11); - o = writeFixedAscii(b, o, btId, 5); - return b; -} - -// sRcvLog parser (follows sRcvHeader if nRetCd == 0) -function parseRcvLog(buf, offset = 0) { - let o = offset; - const t1 = readFixedAscii(buf, o, 20); const cDateTime = t1.value; o = t1.next; - const t2 = readFixedAscii(buf, o, 31); const cUsrNm = t2.value; o = t2.next; - const t3 = readFixedAscii(buf, o, 5 ); const cUsrId = t3.value; o = t3.next; - const t4 = readFixedAscii(buf, o, 31); const cSupNm = t4.value; o = t4.next; - const t5 = readFixedAscii(buf, o, 5 ); const cSupId = t5.value; o = t5.next; - const t6 = readFixedAscii(buf, o, 5 ); const cUsrTyp = t6.value; o = t6.next; - - function rf() { const v = buf.readFloatLE(o); o += 4; return v; } - function ri() { const v = buf.readInt32LE(o); o += 4; return v; } - - const fOpenBal = rf(); - const fTktSalesByVoucher = rf(); - const fTktSalesByCash = rf(); - const fTktSalesByMemCard = rf(); - const nTktSalesByVoucherCount = ri(); - const nTktSalesByCashCount = ri(); - const nTktSalesByMemCardCount = ri(); - const fPayoutByVoucher = rf(); - const fPayoutByCash = rf(); - const fPayoutByMemCard = rf(); - const nPayoutByVoucherCount = ri(); - const nPayoutByCashCount = ri(); - const nPayoutByMemCardCount = ri(); - const fCancelByVoucher = rf(); - const fCancelByCash = rf(); - const fCancelByMemCard = rf(); - const nCancelByVoucherCount = ri(); - const nCancelByCashCount = ri(); - const nCancelByMemCardCount = ri(); - const fDeposit = rf(); - const fWithdrawAmt = rf(); - const fVoucherSales = rf(); - const fVoucherEncash = rf(); - const fCloseBal = rf(); - const fSaleTarget = rf(); - - return { - cDateTime, cUsrNm, cUsrId, cSupNm, cSupId, cUsrTyp, - fOpenBal, fTktSalesByVoucher, fTktSalesByCash, fTktSalesByMemCard, - nTktSalesByVoucherCount, nTktSalesByCashCount, nTktSalesByMemCardCount, - fPayoutByVoucher, fPayoutByCash, fPayoutByMemCard, - nPayoutByVoucherCount, nPayoutByCashCount, nPayoutByMemCardCount, - fCancelByVoucher, fCancelByCash, fCancelByMemCard, - nCancelByVoucherCount, nCancelByCashCount, nCancelByMemCardCount, - fDeposit, fWithdrawAmt, fVoucherSales, fVoucherEncash, - fCloseBal, fSaleTarget, - size: o - offset - }; -} - -// ---------- 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) }); }); -}); - -// ---------- ABS_POLL ---------- -app.post("/abs/poll", async (req, res) => { - try { - const body = await readJsonBody(req); - 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, cBtMake: btMake - }); - - const reply = await absRoundtrip(header, 7000); - const headerSlice = reply.subarray(0, Math.min(reply.length, 24)); - const parsed = parseRcvHeaderFlexible(headerSlice); - - const success = parsed.nRetCd === SUCCESS; - 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, - normalizedRcvHeader: normalized, - normalizedHeaderHex: buildRcvHeader18(normalized).toString("hex"), - success - }); - } catch (e) { - res.status(502).json({ ok: false, error: String(e) }); - } -}); - -// ---------- LOGIN ---------- -// POST /login -// Body (JSON): -// { -// "opCard": "OPCARD17", // REQUIRED (<=17) -// "password": "1234567890", // plain 0–9; will be encrypted for you -// // OR: "passwordEnc": "45673" // already-encrypted string (skip client encryption) -// "btId": "0483", // REQUIRED (<=5) -// "usrId": "", // optional (<=5) -// "btMake": 0 // optional: numeric or single-char string -// } -app.post("/login", async (req, res) => { - try { - const body = await readJsonBody(req); - - const opCard = (body.opCard ?? "").toString(); - const btId = (body.btId ?? "").toString(); - const usrId = (body.usrId ?? "").toString(); - - const plain = (body.password ?? ""); - const givenEnc = (body.passwordEnc ?? ""); - - if (!opCard || !btId || (!plain && !givenEnc)) { - return res.status(400).json({ ok: false, error: "Missing opCard, btId, and either password or passwordEnc" }); - } - - // Encrypt if only plain is supplied (MFC-compatible) - let encInfo = null; - let passwd = givenEnc ? String(givenEnc) : (encInfo = encryptPasswordLikeMFC(String(plain))).enc; - - // C field sizes: cPasswd[11] – warn if plain > 10 (enc becomes > 11) - if (!givenEnc && String(plain).length > 10) { - // still send (field will truncate), but tell caller - } - - const btMake = (typeof body.btMake === "string") - ? body.btMake.charCodeAt(0) - : (Number.isFinite(body.btMake) ? (body.btMake|0) : 0x00); - - const sndHeader = packSndHeader({ - nTxnCd: LOG, nOpCd: LOGBT, nNumRecsSent: 1, nNumRecsRqrd: 1, nTxnId: 0, cBtMake: btMake - }); - const sndLog = packSndLog({ usrId, opCard, passwd, btId }); - const sendBuf = Buffer.concat([sndHeader, sndLog]); - - const reply = await absRoundtrip(sendBuf, 10000); - - const hdrSlice = reply.subarray(0, Math.min(reply.length, 24)); - const rcvHeader = parseRcvHeaderFlexible(hdrSlice); - - const json = { - ok: true, - target: `${ABS_HOST}:${ABS_PORT}`, - sentHeaderHex: sndHeader.toString("hex"), - sentBodyHex: sndLog.toString("hex"), - replyBytes: reply.length, - replyHexFirst64: reply.subarray(0, 64).toString("hex"), - rcvHeaderRaw: rcvHeader, - success: rcvHeader.nRetCd === SUCCESS - }; - - // include encryption debug (non-sensitive) if we did it here - if (!givenEnc && encInfo) { - json.encryption = { - used: true, - utcSeconds: encInfo.utcSeconds, - key: encInfo.key, - encPasswd: passwd - }; - } else if (givenEnc) { - json.encryption = { used: false, providedPasswordEnc: true }; - } - - const bodyOffset = rcvHeader.size; // 18 or 24 - if (json.success && reply.length >= bodyOffset + 20) { - try { - const log = parseRcvLog(reply, bodyOffset); - json.log = log; - } catch (e) { - json.parseLogError = String(e); - } - } - - res.json(json); - } 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 old mode 100644 new mode 100755 index c24df9e..c3dbce4 --- a/poll.sh +++ b/poll.sh @@ -2,5 +2,5 @@ while true; do curl -s -X POST http://localhost:8080/abs/poll |jq - sleep 2 + sleep 5 done diff --git a/rpinfo.sh b/rpinfo.sh new file mode 100755 index 0000000..e60f59f --- /dev/null +++ b/rpinfo.sh @@ -0,0 +1,4 @@ +#!/bin/bash +curl -sS -X POST "http://localhost:8080/download/rpinfo" \ + -H "Content-Type: application/json" \ + -d '{"opCard":"021804111066","password":"0660000","btId":"0483","usrId":"","btMake":0}' | jq diff --git a/login_abs.js b/savi.js similarity index 87% rename from login_abs.js rename to savi.js index 3978684..aa84bce 100644 --- a/login_abs.js +++ b/savi.js @@ -1,5 +1,5 @@ // bridge.js — HTTP→TCP bridge for ABS (ABS_POLL + LOG/LOGBT login) using 512-byte MFC-style frames -// CommonJS (Node 18+) +// Node 18+ (CommonJS) const express = require("express"); const net = require("net"); @@ -8,10 +8,10 @@ const net = require("net"); const ABS_HOST = process.env.ABS_HOST || "192.0.0.14"; const ABS_PORT = Number(process.env.ABS_PORT || 7000); -// ---- transaction/opcode constants (from your grep) ---- -const ABS_POLL = 178; // connectivity ping -const LOG = 100; // login transaction code -const LOGBT = 6013; // login opcode +// ---- transaction/opcode constants (from Abs.h / your grep) ---- +const ABS_POLL = 178; // connectivity ping +const LOG = 100; // login transaction code +const LOGBT = 6013; // login opcode const SUCCESS = 0; // ---- 512-byte framing ---- @@ -157,10 +157,10 @@ async function absRoundtrip(payload, timeoutMs = 7000) { // ---------- MFC-style password "encryption" ---------- /* Mirrors the MFC EncryptPassword(): - - key = first digit of current UTC seconds (0..5, or 0..9 if seconds >= 100 ever, but it's 0..5) + - key = first digit of current UTC seconds - for each input char: interpret as digit (non-digit -> 0), add key, keep ones digit - append key at the end - Result length = input.length + 1 (cPasswd field in sSndLog is 11 bytes, so 10-digit inputs fit). + Result length = input.length + 1 (cPasswd in sSndLog is 11 bytes, so 10-digit inputs fit). */ function encryptPasswordLikeMFC(plain) { const sec = new Date().getUTCSeconds(); // GetSystemTime (UTC) seconds @@ -295,24 +295,12 @@ app.post("/abs/poll", async (req, res) => { }); // ---------- LOGIN ---------- -// POST /login -// Body (JSON): -// { -// "opCard": "OPCARD17", // REQUIRED (<=17) -// "password": "1234567890", // plain 0–9; will be encrypted for you -// // OR: "passwordEnc": "45673" // already-encrypted string (skip client encryption) -// "btId": "0483", // REQUIRED (<=5) -// "usrId": "", // optional (<=5) -// "btMake": 0 // optional: numeric or single-char string -// } app.post("/login", async (req, res) => { try { - const body = await readJsonBody(req); - + const body = await readJsonBody(req); const opCard = (body.opCard ?? "").toString(); const btId = (body.btId ?? "").toString(); const usrId = (body.usrId ?? "").toString(); - const plain = (body.password ?? ""); const givenEnc = (body.passwordEnc ?? ""); @@ -324,11 +312,6 @@ app.post("/login", async (req, res) => { let encInfo = null; let passwd = givenEnc ? String(givenEnc) : (encInfo = encryptPasswordLikeMFC(String(plain))).enc; - // C field sizes: cPasswd[11] – warn if plain > 10 (enc becomes > 11) - if (!givenEnc && String(plain).length > 10) { - // still send (field will truncate), but tell caller - } - const btMake = (typeof body.btMake === "string") ? body.btMake.charCodeAt(0) : (Number.isFinite(body.btMake) ? (body.btMake|0) : 0x00); @@ -341,7 +324,7 @@ app.post("/login", async (req, res) => { const reply = await absRoundtrip(sendBuf, 10000); - const hdrSlice = reply.subarray(0, Math.min(reply.length, 24)); + const hdrSlice = reply.subarray(0, Math.min(reply.length, 24)); const rcvHeader = parseRcvHeaderFlexible(hdrSlice); const json = { @@ -355,7 +338,6 @@ app.post("/login", async (req, res) => { success: rcvHeader.nRetCd === SUCCESS }; - // include encryption debug (non-sensitive) if we did it here if (!givenEnc && encInfo) { json.encryption = { used: true, @@ -367,7 +349,35 @@ app.post("/login", async (req, res) => { json.encryption = { used: false, providedPasswordEnc: true }; } - const bodyOffset = rcvHeader.size; // 18 or 24 + // --- adjust body offset to account for optional 4‑byte nOpCd (forward) --- + let bodyOffset = rcvHeader.size; // 18 or 24 + if (reply.length >= bodyOffset + 4) { + const maybeOp = reply.readInt32LE(bodyOffset); + if (maybeOp === LOGBT || maybeOp === LOG || maybeOp === 0 || (maybeOp >= 1000 && maybeOp <= 10000)) { + bodyOffset += 4; // explicit opcode present + json.rcvExtraOpCd = maybeOp; + } else { + // if current doesn't look like '20' but +4 does, move forward + const looksLikeYear = reply[bodyOffset] === 0x32 && reply[bodyOffset + 1] === 0x30; // '2','0' + const looksLikeYearFwd = reply[bodyOffset + 4] === 0x32 && reply[bodyOffset + 5] === 0x30; + if (!looksLikeYear && looksLikeYearFwd) bodyOffset += 4; + } + } + + // --- back guard: if we landed on '/', back up 4 to include '2025' --- + if (bodyOffset >= 4 && reply[bodyOffset] === 0x2f /* '/' */) { + const couldBeYear = reply[bodyOffset - 4] === 0x32 /* '2' */ && + reply[bodyOffset - 3] === 0x30 /* '0' */; + if (couldBeYear) bodyOffset -= 4; + } + + // optional: quick probe to verify alignment during testing + json.offsetProbe = { + bodyOffset, + around: reply.subarray(Math.max(0, bodyOffset - 8), bodyOffset + 16).toString("ascii") + }; + + // parse body if success if (json.success && reply.length >= bodyOffset + 20) { try { const log = parseRcvLog(reply, bodyOffset);