BTC_BETTING_SERVER_CONNECTOR/savi.js

402 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// bridge.js — HTTP→TCP bridge for ABS (ABS_POLL + LOG/LOGBT login) using 512-byte MFC-style frames
// Node 18+ (CommonJS)
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 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 ----
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
- 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 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 ----------
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;
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
};
if (!givenEnc && encInfo) {
json.encryption = {
used: true,
utcSeconds: encInfo.utcSeconds,
key: encInfo.key,
encPasswd: passwd
};
} else if (givenEnc) {
json.encryption = { used: false, providedPasswordEnc: true };
}
// --- adjust body offset to account for optional 4byte 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);
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}`);
});