ABS polling
This commit is contained in:
parent
4c23c00df7
commit
55b226e729
@ -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
|
||||
) {}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.example.cezenBTC.DTO.CenteralServerConect;
|
||||
|
||||
public record Encryption(
|
||||
boolean used,
|
||||
int utcSeconds,
|
||||
int key,
|
||||
String encPasswd
|
||||
) {}
|
||||
@ -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
|
||||
) {}
|
||||
@ -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
|
||||
) {}
|
||||
@ -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
|
||||
) {}
|
||||
@ -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
|
||||
) {}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
package com.example.cezenBTC.absbridge.model;
|
||||
|
||||
public record HealthResponse(boolean ok, String target, String error) {}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
) {}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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(); }
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user