ABS polling

This commit is contained in:
MathewFrancis 2025-08-22 18:28:24 +05:30
parent 4c23c00df7
commit 55b226e729
15 changed files with 623 additions and 3 deletions

View File

@ -0,0 +1,14 @@
package com.example.cezenBTC.DTO.CenteralServerConect;
public record ApiResponse(
boolean ok,
String target,
String sentHeaderHex,
String sentBodyHex,
int replyBytes,
String replyHexFirst64,
RcvHeaderRaw rcvHeaderRaw,
boolean success,
Encryption encryption,
Log log
) {}

View File

@ -0,0 +1,8 @@
package com.example.cezenBTC.DTO.CenteralServerConect;
public record Encryption(
boolean used,
int utcSeconds,
int key,
String encPasswd
) {}

View File

@ -0,0 +1,44 @@
package com.example.cezenBTC.DTO.CenteralServerConect;
public record Log(
String cDateTime,
String cUsrNm,
String cUsrId,
String cSupNm,
String cSupId,
String cUsrTyp,
double fOpenBal,
double fTktSalesByVoucher,
double fTktSalesByCash,
double fTktSalesByMemCard,
int nTktSalesByVoucherCount,
int nTktSalesByCashCount,
int nTktSalesByMemCardCount,
double fPayoutByVoucher,
double fPayoutByCash,
double fPayoutByMemCard,
int nPayoutByVoucherCount,
int nPayoutByCashCount,
int nPayoutByMemCardCount,
double fCancelByVoucher,
double fCancelByCash,
double fCancelByMemCard,
int nCancelByVoucherCount,
int nCancelByCashCount,
int nCancelByMemCardCount,
double fDeposit,
double fWithdrawAmt,
double fVoucherSales,
double fVoucherEncash,
double fCloseBal,
double fSaleTarget,
int size
) {}

View File

@ -0,0 +1,10 @@
package com.example.cezenBTC.DTO.CenteralServerConect;
public record RcvHeaderRaw(
int nTxnCd,
int nRetCd,
int nNumRecs,
int nTxnId,
int cBtMake,
int size
) {}

View File

@ -0,0 +1,5 @@
package com.example.cezenBTC.DTO.bridge;
public record RcvHeader(
int nTxnCd, int nRetCd, int nNumRecs, int nTxnId, int cBtMake, int size
) {}

View File

@ -0,0 +1,14 @@
package com.example.cezenBTC.DTO.bridge;
public record RcvLog(
String cDateTime, String cUsrNm, String cUsrId, String cSupNm, String cSupId, String cUsrTyp,
float fOpenBal, float fTktSalesByVoucher, float fTktSalesByCash, float fTktSalesByMemCard,
int nTktSalesByVoucherCount, int nTktSalesByCashCount, int nTktSalesByMemCardCount,
float fPayoutByVoucher, float fPayoutByCash, float fPayoutByMemCard,
int nPayoutByVoucherCount, int nPayoutByCashCount, int nPayoutByMemCardCount,
float fCancelByVoucher, float fCancelByCash, float fCancelByMemCard,
int nCancelByVoucherCount, int nCancelByCashCount, int nCancelByMemCardCount,
float fDeposit, float fWithdrawAmt, float fVoucherSales, float fVoucherEncash,
float fCloseBal, float fSaleTarget,
int size
) {}

View File

@ -0,0 +1,153 @@
package com.example.cezenBTC.absbridge.core;
import com.example.cezenBTC.DTO.bridge.RcvHeader;
import com.example.cezenBTC.DTO.bridge.RcvLog;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
public final class AbsProtocol {
private AbsProtocol() {}
// ---- constants ----
public static final int ABS_POLL = 178;
public static final int LOG = 100;
public static final int LOGBT = 6013;
public static final int SUCCESS = 0;
// sSndHeader (24 bytes)
public static byte[] packSndHeader(int nTxnCd, int nOpCd, int nNumRecsSent,
int nNumRecsRqrd, int nTxnId, int cBtMake) {
ByteBuffer bb = ByteBuffer.allocate(24).order(ByteOrder.LITTLE_ENDIAN);
bb.putInt(nTxnCd);
bb.putInt(nOpCd);
bb.putInt(nNumRecsSent);
bb.putInt(nNumRecsRqrd);
bb.putInt(nTxnId);
bb.put((byte)(cBtMake & 0xFF));
return bb.array();
}
// normalize to 18byte header (for display/compat)
public static byte[] buildRcvHeader18(RcvHeader h) {
ByteBuffer bb = ByteBuffer.allocate(18).order(ByteOrder.LITTLE_ENDIAN);
bb.putInt(h.nTxnCd());
bb.putInt(h.nRetCd());
bb.putInt(h.nNumRecs());
bb.putInt(h.nTxnId());
bb.put((byte)(h.cBtMake() & 0xFF));
return bb.array();
}
// flexible parser: 18 or 24 bytes present
public static RcvHeader parseRcvHeaderFlexible(byte[] reply) throws Exception {
if (reply.length < 18) throw new Exception("Reply too short for RcvHeader: " + reply.length);
ByteBuffer bb = ByteBuffer.wrap(reply).order(ByteOrder.LITTLE_ENDIAN);
int nTxnCd = bb.getInt();
int nRetCd = bb.getInt();
int nNumRecs = bb.getInt();
int nTxnId = bb.getInt();
int cBtMake = Byte.toUnsignedInt(bb.get());
int size = reply.length >= 24 ? 24 : 18;
return new RcvHeader(nTxnCd, nRetCd, nNumRecs, nTxnId, cBtMake, size);
}
// sSndLog (38 bytes): cUsrId[5], cOpCardCd[17], cPasswd[11], cBtId[5]
public static byte[] packSndLog(String usrId, String opCard, String passwd, String btId) {
ByteBuffer bb = ByteBuffer.allocate(38).order(ByteOrder.LITTLE_ENDIAN);
FixedAscii.writeFixedAscii(bb, usrId, 5);
FixedAscii.writeFixedAscii(bb, opCard, 17);
FixedAscii.writeFixedAscii(bb, passwd, 11);
FixedAscii.writeFixedAscii(bb, btId, 5);
return bb.array();
}
// MFC-like password "encryption"
public static Enc encryptPasswordLikeMFC(String plain) {
int sec = Instant.now().getEpochSecond() % 60 == 0 ? 0 : (int)(Instant.now().getEpochSecond() % 60);
// key = first digit of UTC seconds
String secStr = String.valueOf((int)(Instant.now().getEpochSecond() % 60));
int key = (secStr.length() > 0) ? (secStr.charAt(0) - '0') : 0;
String s = plain == null ? "" : plain;
StringBuilder out = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
int d = (ch >= '0' && ch <= '9') ? (ch - '0') : 0;
out.append((d + key) % 10);
}
out.append(key);
return new Enc(out.toString(), key, (int)(Instant.now().getEpochSecond() % 60));
}
public record Enc(String enc, int key, int utcSeconds) {}
// parse sRcvLog body starting at offset
public static RcvLog parseRcvLog(byte[] buf, int offset) {
int o = offset;
String cDateTime = FixedAscii.readFixedAscii(buf, o, 20); o += 20;
String cUsrNm = FixedAscii.readFixedAscii(buf, o, 31); o += 31;
String cUsrId = FixedAscii.readFixedAscii(buf, o, 5); o += 5;
String cSupNm = FixedAscii.readFixedAscii(buf, o, 31); o += 31;
String cSupId = FixedAscii.readFixedAscii(buf, o, 5); o += 5;
String cUsrTyp = FixedAscii.readFixedAscii(buf, o, 5); o += 5;
ByteBuffer bb = ByteBuffer.wrap(buf, o, buf.length - o).order(ByteOrder.LITTLE_ENDIAN);
float fOpenBal = bb.getFloat();
float fTktSalesByVoucher = bb.getFloat();
float fTktSalesByCash = bb.getFloat();
float fTktSalesByMemCard = bb.getFloat();
int nTktSalesByVoucherCount = bb.getInt();
int nTktSalesByCashCount = bb.getInt();
int nTktSalesByMemCardCount = bb.getInt();
float fPayoutByVoucher = bb.getFloat();
float fPayoutByCash = bb.getFloat();
float fPayoutByMemCard = bb.getFloat();
int nPayoutByVoucherCount = bb.getInt();
int nPayoutByCashCount = bb.getInt();
int nPayoutByMemCardCount = bb.getInt();
float fCancelByVoucher = bb.getFloat();
float fCancelByCash = bb.getFloat();
float fCancelByMemCard = bb.getFloat();
int nCancelByVoucherCount = bb.getInt();
int nCancelByCashCount = bb.getInt();
int nCancelByMemCardCount = bb.getInt();
float fDeposit = bb.getFloat();
float fWithdrawAmt = bb.getFloat();
float fVoucherSales = bb.getFloat();
float fVoucherEncash = bb.getFloat();
float fCloseBal = bb.getFloat();
float fSaleTarget = bb.getFloat();
int consumed = 20+31+5+31+5+5 + ( // strings
4*3 + // first 3 floats already? (we counted below anyway)
0);
// compute actual bytes consumed via buffer:
int size = (20+31+5+31+5+5) + ( // strings
4 * 16 // floats count (16 floats)
+ 4 * 9 // ints count (9 ints)
);
int totalOffset = (buf.length - bb.remaining());
size = totalOffset - offset;
return new RcvLog(
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
);
}
}

View File

@ -0,0 +1,23 @@
package com.example.cezenBTC.absbridge.core;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public final class FixedAscii {
private FixedAscii() {}
public static void writeFixedAscii(ByteBuffer bb, String s, int len) {
String str = s == null ? "" : s;
byte[] src = str.getBytes(StandardCharsets.US_ASCII);
int toCopy = Math.min(src.length, len);
bb.put(src, 0, toCopy);
for (int i = toCopy; i < len; i++) bb.put((byte)0x00);
}
public static String readFixedAscii(byte[] buf, int offset, int len) {
int end = offset;
int max = offset + len;
while (end < max && buf[end] != 0) end++;
return new String(buf, offset, end - offset, StandardCharsets.US_ASCII).stripTrailing();
}
}

View File

@ -0,0 +1,3 @@
package com.example.cezenBTC.absbridge.model;
public record HealthResponse(boolean ok, String target, String error) {}

View File

@ -0,0 +1,10 @@
package com.example.cezenBTC.absbridge.model;
public class LoginRequest {
public String opCard;
public String btId;
public String usrId;
public String password;
public String passwordEnc;
public Object btMake; // number or single-char string accepted
}

View File

@ -0,0 +1,15 @@
package com.example.cezenBTC.absbridge.model;
import com.example.cezenBTC.DTO.bridge.RcvHeader;
public record PollResponse(
boolean ok,
String target,
String sentHeaderHex,
int replyBytes,
String replyHexFirst64,
RcvHeader parsedRcvHeaderRaw,
RcvHeader normalizedRcvHeader,
String normalizedHeaderHex,
boolean success
) {}

View File

@ -0,0 +1,87 @@
package com.example.cezenBTC.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
@Component
public class AbsClient {
public static final int PKT_SIZE = 512;
public static final int PAYLOAD_PER_PKT = PKT_SIZE - 1;
private final String host;
private final int port;
public AbsClient(
@Value("${abs.host:192.0.0.14}") String host,
@Value("${abs.port:7000}") int port
) {
this.host = host;
this.port = port;
}
public String target() { return host + ":" + port; }
public byte[] roundtrip(byte[] payload, int timeoutMs) throws Exception {
try (Socket sock = new Socket()) {
sock.connect(new InetSocketAddress(host, port), timeoutMs);
sock.setSoTimeout(timeoutMs);
try (OutputStream out = sock.getOutputStream();
InputStream in = sock.getInputStream()) {
send(out, payload);
return recv(in, timeoutMs);
}
}
}
private static void send(OutputStream out, byte[] payload) throws Exception {
int off = 0;
while (off < payload.length) {
int remaining = payload.length - off;
int toCopy = Math.min(PAYLOAD_PER_PKT, remaining);
byte[] pkt = new byte[PKT_SIZE];
pkt[0] = (byte)((off + toCopy >= payload.length) ? '0' : '1');
if (toCopy > 0) {
System.arraycopy(payload, off, pkt, 1, toCopy);
off += toCopy;
}
out.write(pkt);
}
out.flush();
}
private static byte[] recv(InputStream in, int timeoutMs) throws Exception {
List<byte[]> chunks = new ArrayList<>();
byte[] buf = new byte[PKT_SIZE];
while (true) {
int read = 0;
while (read < PKT_SIZE) {
int n = in.read(buf, read, PKT_SIZE - read);
if (n < 0) throw new Exception("Socket ended before final '0' packet");
read += n;
}
byte flag = buf[0];
byte[] body = new byte[PKT_SIZE - 1];
System.arraycopy(buf, 1, body, 0, body.length);
chunks.add(body);
if (flag == '0') break;
}
int total = chunks.stream().mapToInt(a -> a.length).sum();
byte[] out = new byte[total];
int o = 0;
for (byte[] c : chunks) {
System.arraycopy(c, 0, out, o, c.length);
o += c.length;
}
return out;
}
}

View File

@ -93,7 +93,7 @@ public class CezenRoutsSecurityChain {
//.csrf(AbstractHttpConfigurer::disable)
.csrf((csrf) ->
csrf.csrfTokenRequestHandler(requestHandler).
ignoringRequestMatchers("/open/signup","/user/getXSRfToken","/user/ping")
ignoringRequestMatchers("/open/signup","/user/getXSRfToken","/user/ping", "/abs/*")
//.csrfTokenRepository(new CookieCsrfTokenRepository())
.csrfTokenRepository(cookieCsrfTokenRepo)
)
@ -113,7 +113,8 @@ public class CezenRoutsSecurityChain {
"/btc/get_race_card",
"/cezen/set_aors",
"/cezen/set_password",
"/cezen/add_extension"
"/cezen/add_extension",
"/abs/*"
).hasAnyRole("admin")
//any one who is authenticated can access /logout
.requestMatchers("/user/getXSRfToken","/user/ping", "/logout").authenticated()

View File

@ -0,0 +1,233 @@
package com.example.cezenBTC.controller;
import com.example.cezenBTC.DTO.bridge.RcvHeader;
import com.example.cezenBTC.DTO.bridge.RcvLog;
import com.example.cezenBTC.absbridge.core.AbsProtocol;
import com.example.cezenBTC.absbridge.model.HealthResponse;
import com.example.cezenBTC.absbridge.model.LoginRequest;
import com.example.cezenBTC.config.AbsClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
@RequestMapping("/abs")
public class AbsController {
private final AbsClient client;
private final String target;
@Value("${server.port}")
private int httpPort;
public AbsController(AbsClient client) {
this.client = client;
this.target = client.target();
}
@GetMapping("/test")
public String root() {
return "ABS bridge is up";
}
@GetMapping("/health")
public HealthResponse health() {
try {
byte[] header = AbsProtocol.packSndHeader(AbsProtocol.ABS_POLL, 0, 0, 0, 0, 0);
byte[] reply = client.roundtrip(header, 2000); // 2s tight timeout
var hdr = AbsProtocol.parseRcvHeaderFlexible(
reply.length >= 24 ? java.util.Arrays.copyOf(reply, 24)
: java.util.Arrays.copyOf(reply, reply.length)
);
boolean ok = (hdr.nRetCd() == AbsProtocol.SUCCESS);
return new HealthResponse(ok, client.target(), ok ? null : ("ABS poll retCd=" + hdr.nRetCd()));
} catch (Exception e) {
return new HealthResponse(false, client.target(), "ABS poll failed: " + e.getMessage());
}
}
@PostMapping(value = "/abs/poll", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public Object poll(@RequestBody(required = false) Map<String, Object> body) {
try {
int btMake = extractBtMake(body);
byte[] header = AbsProtocol.packSndHeader(
AbsProtocol.ABS_POLL, 0, 0, 0, 0, btMake
);
byte[] reply = client.roundtrip(header, 7000);
byte[] hdrSlice = slice(reply, 0, Math.min(reply.length, 24));
RcvHeader parsed = AbsProtocol.parseRcvHeaderFlexible(hdrSlice);
RcvHeader normalized = new RcvHeader(
parsed.nTxnCd() == 0 ? AbsProtocol.ABS_POLL : parsed.nTxnCd(),
parsed.nRetCd(),
parsed.nNumRecs(),
parsed.nTxnId(),
parsed.cBtMake(),
18
);
Map<String, Object> out = new LinkedHashMap<>();
out.put("ok", true);
out.put("target", target);
out.put("sentHeaderHex", toHex(header));
out.put("replyBytes", reply.length);
out.put("replyHexFirst64", toHex(slice(reply, 0, Math.min(64, reply.length))));
out.put("parsedRcvHeaderRaw", parsed);
out.put("normalizedRcvHeader", normalized);
out.put("normalizedHeaderHex", toHex(AbsProtocol.buildRcvHeader18(normalized)));
out.put("success", parsed.nRetCd() == AbsProtocol.SUCCESS);
return out;
} catch (Exception e) {
return Map.of("ok", false, "error", e.toString());
}
}
@PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public Object login(@RequestBody LoginRequest req) {
try {
if (isEmpty(req.opCard) || isEmpty(req.btId) || (isEmpty(req.password) && isEmpty(req.passwordEnc))) {
return Map.of("ok", false, "error", "Missing opCard, btId, and either password or passwordEnc");
}
String passwd;
Map<String, Object> encMeta = new LinkedHashMap<>();
if (req.passwordEnc != null && !req.passwordEnc.isBlank()) {
passwd = req.passwordEnc;
encMeta.put("used", false);
encMeta.put("providedPasswordEnc", true);
} else {
AbsProtocol.Enc enc = AbsProtocol.encryptPasswordLikeMFC(req.password);
passwd = enc.enc();
encMeta.put("used", true);
encMeta.put("utcSeconds", enc.utcSeconds());
encMeta.put("key", enc.key());
encMeta.put("encPasswd", passwd);
}
int btMake = extractBtMake(req.btMake);
byte[] sndHeader = AbsProtocol.packSndHeader(
AbsProtocol.LOG, AbsProtocol.LOGBT, 1, 1, 0, btMake
);
byte[] sndLog = AbsProtocol.packSndLog(
safe(req.usrId), safe(req.opCard), passwd, safe(req.btId)
);
byte[] sendBuf = concat(sndHeader, sndLog);
byte[] reply = client.roundtrip(sendBuf, 10_000);
byte[] hdrSlice = slice(reply, 0, Math.min(reply.length, 24));
RcvHeader rcvHeader = AbsProtocol.parseRcvHeaderFlexible(hdrSlice);
Map<String, Object> json = new LinkedHashMap<>();
json.put("ok", true);
json.put("target", target);
json.put("sentHeaderHex", toHex(sndHeader));
json.put("sentBodyHex", toHex(sndLog));
json.put("replyBytes", reply.length);
json.put("replyHexFirst64", toHex(slice(reply, 0, Math.min(64, reply.length))));
json.put("rcvHeaderRaw", rcvHeader);
json.put("success", rcvHeader.nRetCd() == AbsProtocol.SUCCESS);
json.put("encryption", encMeta);
// ---- body offset logic (like Node) ----
int bodyOffset = rcvHeader.size();
if (reply.length >= bodyOffset + 4) {
int maybeOp = leInt(reply, bodyOffset);
if (maybeOp == AbsProtocol.LOGBT || maybeOp == AbsProtocol.LOG || maybeOp == 0
|| (maybeOp >= 1000 && maybeOp <= 10000)) {
bodyOffset += 4;
json.put("rcvExtraOpCd", maybeOp);
} else {
boolean looksYear = looksYear(reply, bodyOffset);
boolean looksYearFwd = looksYear(reply, bodyOffset + 4);
if (!looksYear && looksYearFwd) bodyOffset += 4;
}
}
if (bodyOffset >= 4 && byteAt(reply, bodyOffset) == (byte)'/') {
if (byteAt(reply, bodyOffset - 4) == '2' && byteAt(reply, bodyOffset - 3) == '0') {
bodyOffset -= 4;
}
}
json.put("offsetProbe", Map.of(
"bodyOffset", bodyOffset,
"around", new String(slice(reply, Math.max(0, bodyOffset - 8),
Math.min(reply.length, bodyOffset + 16)), StandardCharsets.US_ASCII)
));
if ((boolean) json.get("success") && reply.length >= bodyOffset + 20) {
try {
RcvLog log = AbsProtocol.parseRcvLog(reply, bodyOffset);
json.put("log", log);
} catch (Exception ex) {
json.put("parseLogError", ex.toString());
}
}
return json;
} catch (Exception e) {
return Map.of("ok", false, "error", e.toString());
}
}
// ---------- helpers ----------
private static byte[] slice(byte[] a, int off, int end) {
int n = Math.max(0, end - off);
byte[] out = new byte[n];
System.arraycopy(a, Math.max(0, off), out, 0, n);
return out;
}
private static String toHex(byte[] a) {
return HexFormat.of().formatHex(a);
}
private static byte[] concat(byte[] a, byte[] b) {
byte[] out = new byte[a.length + b.length];
System.arraycopy(a, 0, out, 0, a.length);
System.arraycopy(b, 0, out, a.length, b.length);
return out;
}
private static int leInt(byte[] a, int off) {
ByteBuffer bb = ByteBuffer.wrap(a, off, 4).order(ByteOrder.LITTLE_ENDIAN);
return bb.getInt();
}
private static boolean looksYear(byte[] a, int off) {
if (off + 1 >= a.length) return false;
return a[off] == '2' && a[off + 1] == '0';
}
private static byte byteAt(byte[] a, int off) {
return off >= 0 && off < a.length ? a[off] : 0;
}
private static int extractBtMake(Object btMakeAny) {
if (btMakeAny == null) return 0;
if (btMakeAny instanceof Number n) return n.intValue();
if (btMakeAny instanceof String s) {
return s.isEmpty() ? 0 : (int) s.charAt(0);
}
return 0;
}
private static int extractBtMake(Map<String, Object> body) {
if (body == null) return 0;
Object v = body.get("btMake");
return extractBtMake(v);
}
private static String safe(String s) { return s == null ? "" : s; }
private static boolean isEmpty(String s) { return s == null || s.isBlank(); }
}

View File

@ -1,6 +1,6 @@
spring.application.name=cezenHorse
#server.port=8083
#server.port=8083
server.port=8081
server.address=0.0.0.0
spring.datasource.url = jdbc:postgresql://localhost:5434/horse