diff --git a/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/ApiResponse.java b/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/ApiResponse.java new file mode 100644 index 0000000..aa3d871 --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/ApiResponse.java @@ -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 +) {} diff --git a/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/Encryption.java b/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/Encryption.java new file mode 100644 index 0000000..4d54d0d --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/Encryption.java @@ -0,0 +1,8 @@ +package com.example.cezenBTC.DTO.CenteralServerConect; + +public record Encryption( + boolean used, + int utcSeconds, + int key, + String encPasswd +) {} diff --git a/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/Log.java b/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/Log.java new file mode 100644 index 0000000..370dcc0 --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/Log.java @@ -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 +) {} diff --git a/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/RcvHeaderRaw.java b/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/RcvHeaderRaw.java new file mode 100644 index 0000000..2163768 --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/DTO/CenteralServerConect/RcvHeaderRaw.java @@ -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 +) {} \ No newline at end of file diff --git a/springHorse/src/main/java/com/example/cezenBTC/DTO/bridge/RcvHeader.java b/springHorse/src/main/java/com/example/cezenBTC/DTO/bridge/RcvHeader.java new file mode 100644 index 0000000..bdc1dd2 --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/DTO/bridge/RcvHeader.java @@ -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 +) {} diff --git a/springHorse/src/main/java/com/example/cezenBTC/DTO/bridge/RcvLog.java b/springHorse/src/main/java/com/example/cezenBTC/DTO/bridge/RcvLog.java new file mode 100644 index 0000000..4eb9622 --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/DTO/bridge/RcvLog.java @@ -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 +) {} \ No newline at end of file diff --git a/springHorse/src/main/java/com/example/cezenBTC/absbridge/core/AbsProtocol.java b/springHorse/src/main/java/com/example/cezenBTC/absbridge/core/AbsProtocol.java new file mode 100644 index 0000000..2390b1d --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/absbridge/core/AbsProtocol.java @@ -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 18‑byte 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 + ); + } +} diff --git a/springHorse/src/main/java/com/example/cezenBTC/absbridge/core/FixedAscii.java b/springHorse/src/main/java/com/example/cezenBTC/absbridge/core/FixedAscii.java new file mode 100644 index 0000000..fab6051 --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/absbridge/core/FixedAscii.java @@ -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(); + } +} \ No newline at end of file diff --git a/springHorse/src/main/java/com/example/cezenBTC/absbridge/model/HealthResponse.java b/springHorse/src/main/java/com/example/cezenBTC/absbridge/model/HealthResponse.java new file mode 100644 index 0000000..e6356e5 --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/absbridge/model/HealthResponse.java @@ -0,0 +1,3 @@ +package com.example.cezenBTC.absbridge.model; + +public record HealthResponse(boolean ok, String target, String error) {} \ No newline at end of file diff --git a/springHorse/src/main/java/com/example/cezenBTC/absbridge/model/LoginRequest.java b/springHorse/src/main/java/com/example/cezenBTC/absbridge/model/LoginRequest.java new file mode 100644 index 0000000..694b0e1 --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/absbridge/model/LoginRequest.java @@ -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 +} \ No newline at end of file diff --git a/springHorse/src/main/java/com/example/cezenBTC/absbridge/model/PollResponse.java b/springHorse/src/main/java/com/example/cezenBTC/absbridge/model/PollResponse.java new file mode 100644 index 0000000..ea418cc --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/absbridge/model/PollResponse.java @@ -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 +) {} \ No newline at end of file diff --git a/springHorse/src/main/java/com/example/cezenBTC/config/AbsClient.java b/springHorse/src/main/java/com/example/cezenBTC/config/AbsClient.java new file mode 100644 index 0000000..84ff7e1 --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/config/AbsClient.java @@ -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 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; + } +} diff --git a/springHorse/src/main/java/com/example/cezenBTC/config/CezenRoutsSecurityChain.java b/springHorse/src/main/java/com/example/cezenBTC/config/CezenRoutsSecurityChain.java index f996740..509f92b 100755 --- a/springHorse/src/main/java/com/example/cezenBTC/config/CezenRoutsSecurityChain.java +++ b/springHorse/src/main/java/com/example/cezenBTC/config/CezenRoutsSecurityChain.java @@ -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() diff --git a/springHorse/src/main/java/com/example/cezenBTC/controller/AbsController.java b/springHorse/src/main/java/com/example/cezenBTC/controller/AbsController.java new file mode 100644 index 0000000..0a6e0c5 --- /dev/null +++ b/springHorse/src/main/java/com/example/cezenBTC/controller/AbsController.java @@ -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 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 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 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 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 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(); } +} \ No newline at end of file diff --git a/springHorse/src/main/resources/application.properties b/springHorse/src/main/resources/application.properties index ff3c3f6..c5d6303 100755 --- a/springHorse/src/main/resources/application.properties +++ b/springHorse/src/main/resources/application.properties @@ -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