From f1d1ac9fd9ca54a1180d42f3d77f5a76b336356d Mon Sep 17 00:00:00 2001 From: zhangzhuo Date: Tue, 2 Dec 2025 17:15:25 +0800 Subject: [PATCH 01/11] add HSMS and SECSII message --- .../github/aside8/eap/protocol/Message.java | 2 + .../github/aside8/eap/protocol/Protocol.java | 15 + .../aside8/eap/protocol/hsms/HsmsHeader.java | 60 +++ .../aside8/eap/protocol/hsms/HsmsMessage.java | 108 +++++ .../eap/protocol/hsms/HsmsMessageType.java | 65 +++ .../eap/protocol/hsms/SelectStatus.java | 37 ++ .../aside8/eap/protocol/secs2/SECSII.java | 150 +++++++ .../eap/protocol/secs2/SecsDataItem.java | 412 ++++++++++++++++++ .../eap/protocol/secs2/SecsFormatCode.java | 46 ++ 9 files changed, 895 insertions(+) create mode 100644 EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Protocol.java create mode 100644 EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsHeader.java create mode 100644 EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java create mode 100644 EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessageType.java create mode 100644 EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/SelectStatus.java create mode 100644 EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java create mode 100644 EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java create mode 100644 EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsFormatCode.java diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Message.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Message.java index 44a06c3..f0c5eef 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Message.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Message.java @@ -7,4 +7,6 @@ * A message is a serializable object that can be encoded and decoded. */ public interface Message extends Codec, Traceable, Serializable { + + Protocol getProtocol(); } diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Protocol.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Protocol.java new file mode 100644 index 0000000..729fcab --- /dev/null +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Protocol.java @@ -0,0 +1,15 @@ +package com.github.aside8.eap.protocol; + +public enum Protocol { + HSMS("HSMS"), + ; + private final String name; + + Protocol(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsHeader.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsHeader.java new file mode 100644 index 0000000..f9e75d1 --- /dev/null +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsHeader.java @@ -0,0 +1,60 @@ +package com.github.aside8.eap.protocol.hsms; + +import com.github.aside8.eap.protocol.Codec; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HsmsHeader implements Codec { + // HEADER SIZE fixed to 10 bytes + private static final int HEADER_SIZE_BYTES = 10; + + private static final byte WBIT_MASK = (byte) 0b10000000; + private static final byte STREAM_MASK = (byte) 0b01111111; + + private short sessionId; // a.k.a. Device ID + private byte stream; + private byte function; + private boolean wbit; + private byte ptype; + private byte stype; + private int systemBytes; + + @Override + public ByteBuf encode(ByteBufAllocator allocator) { + ByteBuf buf = allocator.buffer(HEADER_SIZE_BYTES); + buf.writeShort(sessionId); + buf.writeByte(stream); + buf.writeByte(function); + buf.writeByte(ptype); + buf.writeByte(stype); + buf.writeInt(systemBytes); + // set W-Bit + if (wbit) { + buf.setByte(2, buf.getByte(2) | WBIT_MASK); + } + return buf; + } + + @Override + public void decode(ByteBuf in) { + if (in.readableBytes() < HEADER_SIZE_BYTES) { + throw new IllegalArgumentException("Header must be 10 bytes"); + } + this.sessionId = in.readShort(); + byte streamAndWbit = in.readByte(); + this.wbit = (streamAndWbit & WBIT_MASK) != 0; + this.stream = (byte) (streamAndWbit & STREAM_MASK); + this.function = in.readByte(); + this.ptype = in.readByte(); + this.stype = in.readByte(); + this.systemBytes = in.readInt(); + } +} \ No newline at end of file diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java new file mode 100644 index 0000000..c04081b --- /dev/null +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java @@ -0,0 +1,108 @@ +package com.github.aside8.eap.protocol.hsms; + +import com.github.aside8.eap.protocol.Message; +import com.github.aside8.eap.protocol.Protocol; +import com.github.aside8.eap.protocol.secs2.SECSII; +import com.github.aside8.eap.protocol.secs2.SecsDataItem; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Optional; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HsmsMessage implements Message { + + private HsmsHeader header; + + private SECSII body; + + // Delegate getter methods to the header object + public int getStream() { + return Optional.ofNullable(header) + .map(HsmsHeader::getStream) + .orElseThrow(() -> new IllegalStateException("HSMS header cannot be null.")); + } + + public int getFunction() { + return Optional.ofNullable(header) + .map(HsmsHeader::getFunction) + .orElseThrow(() -> new IllegalStateException("HSMS header cannot be null.")); + } + + public HsmsMessageType getMessageType() { + return Optional.ofNullable(header) + .map(HsmsHeader::getStype) + .map(HsmsMessageType::fromSType) + .orElseThrow(() -> new IllegalStateException("HSMS header cannot be null.")); + } + + public int getSystemBytes() { + return Optional.ofNullable(header) + .map(HsmsHeader::getSystemBytes) + .orElse(0); + } + + public void setSystemBytes(int systemBytes) { + if (header == null) { + header = new HsmsHeader(); + } + header.setSystemBytes(systemBytes); + } + + + @Override + public ByteBuf encode(ByteBufAllocator allocator) { + if (header == null) { + throw new IllegalStateException("HSMS header cannot be null."); + } + + ByteBuf headerBuf = header.encode(allocator); + ByteBuf bodyBuf = null; + if (body != null) { + bodyBuf = body.encode(allocator); + } + + int bodyLength = (bodyBuf != null) ? bodyBuf.readableBytes() : 0; + int totalLength = headerBuf.readableBytes() + bodyLength; + + ByteBuf lengthBuf = allocator.buffer(4).writeInt(totalLength); + + CompositeByteBuf compositeBuf = allocator.compositeBuffer(); + compositeBuf.addComponents(true, lengthBuf, headerBuf); + if (bodyBuf != null) { + compositeBuf.addComponents(true, bodyBuf); + } + + return compositeBuf; + } + + @Override + public void decode(ByteBuf in) { + this.header = new HsmsHeader(); + header.decode(in); + + if (in.readableBytes() > 0) { + this.body = new SecsDataItem(); // Use the public no-arg constructor + body.decode(in); + } + } + + @Override + public Protocol getProtocol() { + return Protocol.HSMS; + } + + @Override + public String getTraceId() { + return String.valueOf(getSystemBytes()); + } + +} diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessageType.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessageType.java new file mode 100644 index 0000000..00d36c4 --- /dev/null +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessageType.java @@ -0,0 +1,65 @@ +package com.github.aside8.eap.protocol.hsms; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public enum HsmsMessageType { + /** + * HSMS Data Message (pType = 0, sType = 0) + */ + DATA_MESSAGE(0, 0), + /** + * SELECT.REQ (pType = 0, sType = 1) + */ + SELECT_REQ(0, 1), + /** + * SELECT.RSP (pType = 0, sType = 2) + */ + SELECT_RSP(0, 2), + /** + * DESELECT.REQ (pType = 0, sType = 3) + */ + DESELECT_REQ(0, 3), + /** + * DESELECT.RSP (pType = 0, sType = 4) + */ + DESELECT_RSP(0, 4), + /** + * LINKTEST.REQ (pType = 0, sType = 5) + */ + LINKTEST_REQ(0, 5), + /** + * LINKTEST.RSP (pType = 0, sType = 6) + */ + LINKTEST_RSP(0, 6), + /** + * ABORT.REQ (pType = 0, sType = 9) + */ + ABORT_REQ(0, 9); + + private final int pType; + + private final int sType; + + private static final Map S_TYPE_MAP = + Arrays.stream(values()).collect(Collectors.toMap(HsmsMessageType::getSType, Function.identity())); + + HsmsMessageType(int pType, int sType) { + this.pType = pType; + this.sType = sType; + } + + public int getPType() { + return pType; + } + + public int getSType() { + return sType; + } + + public static HsmsMessageType fromSType(int sType) { + return S_TYPE_MAP.getOrDefault(sType, DATA_MESSAGE); + } +} diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/SelectStatus.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/SelectStatus.java new file mode 100644 index 0000000..a9ef84e --- /dev/null +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/SelectStatus.java @@ -0,0 +1,37 @@ +package com.github.aside8.eap.protocol.hsms; + +/** + * Enumerates the possible status codes for a Select.rsp message, + * which are conveyed in the P-Type field of the HSMS header. + */ +public enum SelectStatus { + /** + * Status 0: Connection Established. The selection was successful. + */ + CONNECTION_ESTABLISHED(0), + + /** + * Status 1: Connection Not Ready. The entity is not ready to communicate. + */ + CONNECTION_NOT_READY(1), + + /** + * Status 2: Session ID Not Found. The requested Session ID (Device ID) is unknown. + */ + SESSION_ID_NOT_FOUND(2), + + /** + * Status 3: Already Selected. The entity is already in a selected state. + */ + ALREADY_SELECTED(3); + + private final byte code; + + SelectStatus(int code) { + this.code = (byte) code; + } + + public byte getCode() { + return code; + } +} diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java new file mode 100644 index 0000000..c6a8132 --- /dev/null +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java @@ -0,0 +1,150 @@ +package com.github.aside8.eap.protocol.secs2; + +import com.github.aside8.eap.protocol.Codec; + +/** + * Interface for all SECS-II message bodies. + */ +public interface SECSII extends Codec { + // Specific SECS-II methods can be added here, e.g., to inspect data items. + + /** + * Creates a SECS-II data item of type List. + * + * @param items the SECS-II data items to include in the list. + * @return a SECSII object representing the List data item. + */ + static SECSII list(SECSII... items) { + return SecsDataItem.ofList(items); + } + + /** + * Creates a SECS-II data item of type Binary (B). + * + * @param values the byte values. + * @return a SECSII object representing the Binary data item. + */ + static SECSII binary(byte... values) { + return SecsDataItem.ofBinary(values); + } + + /** + * Creates a SECS-II data item of type Boolean (BOOLEAN). + * + * @param values the boolean values. + * @return a SECSII object representing the Boolean data item. + */ + static SECSII bool(boolean... values) { + return SecsDataItem.ofBoolean(values); + } + + /** + * Creates a SECS-II data item of type ASCII (A). + * + * @param value the ASCII string value. + * @return a SECSII object representing the ASCII data item. + */ + static SECSII ascii(String value) { + return SecsDataItem.ofAscii(value); + } + + /** + * Creates a SECS-II data item of type 8-byte Integer (I8). + * + * @param values the 8-byte integer values. + * @return a SECSII object representing the I8 data item. + */ + static SECSII i8(long... values) { + return SecsDataItem.ofI8(values); + } + + /** + * Creates a SECS-II data item of type 8-byte Unsigned Integer (U8). + * + * @param values the 8-byte unsigned integer values. + * @return a SECSII object representing the U8 data item. + */ + static SECSII u8(long... values) { + return SecsDataItem.ofU8(values); + } + + /** + * Creates a SECS-II data item of type 8-byte Floating Point (F8). + * + * @param values the 8-byte floating point values. + * @return a SECSII object representing the F8 data item. + */ + static SECSII f8(double... values) { + return SecsDataItem.ofF8(values); + } + + /** + * Creates a SECS-II data item of type 4-byte Floating Point (F4). + * + * @param values the 4-byte floating point values. + * @return a SECSII object representing the F4 data item. + */ + static SECSII f4(float... values) { + return SecsDataItem.ofF4(values); + } + + /** + * Creates a SECS-II data item of type 4-byte Integer (I4). + * + * @param values the 4-byte integer values. + * @return a SECSII object representing the I4 data item. + */ + static SECSII i4(int... values) { + return SecsDataItem.ofI4(values); + } + + /** + * Creates a SECS-II data item of type 4-byte Unsigned Integer (U4). + * + * @param values the unsigned 4-byte integer values. + * @return a SECSII object representing the U4 data item. + */ + static SECSII u4(long... values) { + return SecsDataItem.ofU4(values); + } + + /** + * Creates a SECS-II data item of type 2-byte Integer (I2). + * + * @param values the 2-byte integer values. + * @return a SECSII object representing the I2 data item. + */ + static SECSII i2(short... values) { + return SecsDataItem.ofI2(values); + } + + /** + * Creates a SECS-II data item of type 2-byte Unsigned Integer (U2). + * + * @param values the 2-byte unsigned integer values. + * @return a SECSII object representing the U2 data item. + */ + static SECSII u2(int... values) { + return SecsDataItem.ofU2(values); + } + + /** + * Creates a SECS-II data item of type 1-byte Integer (I1). + * + * @param values the 1-byte integer values. + * @return a SECSII object representing the I1 data item. + */ + static SECSII i1(byte... values) { + return SecsDataItem.ofI1(values); + } + + /** + * Creates a SECS-II data item of type 1-byte Unsigned Integer (U1). + * + * @param values the 1-byte unsigned integer values. + * @return a SECSII object representing the U1 data item. + */ + static SECSII u1(short... values) { + return SecsDataItem.ofU1(values); + } +} \ No newline at end of file diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java new file mode 100644 index 0000000..40f1c38 --- /dev/null +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java @@ -0,0 +1,412 @@ +package com.github.aside8.eap.protocol.secs2; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public final class SecsDataItem implements SECSII { + private SecsFormatCode formatCode; + private byte[] value; + private List listItems; + + public SecsDataItem() { + this.formatCode = SecsFormatCode.L; // Default to List, will be updated during decode + this.listItems = Collections.emptyList(); + this.value = new byte[0]; + } + + // Private constructors for different data types + private SecsDataItem(SecsFormatCode formatCode, byte[] value, List listItems) { + this.formatCode = formatCode; + this.value = value; + this.listItems = listItems; + } + + // Static factory methods + public static SecsDataItem ofList(SECSII... items) { + List itemList = (items != null) ? Arrays.asList(items) : Collections.emptyList(); + return new SecsDataItem(SecsFormatCode.L, null, itemList); + } + + public static SecsDataItem ofU4(long... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.U4, new byte[0], null); + } + for (long value : values) { + if (value < 0 || value > 0xFFFFFFFFL) { + throw new IllegalArgumentException("U4 values must be between 0 and 0xFFFFFFFFL"); + } + } + ByteBuffer bb = ByteBuffer.allocate(values.length * 4); + for (long value : values) { + bb.putInt((int) value); + } + return new SecsDataItem(SecsFormatCode.U4, bb.array(), null); + } + + public static SecsDataItem ofBinary(byte... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.B, new byte[0], null); + } + return new SecsDataItem(SecsFormatCode.B, values, null); + } + + public static SecsDataItem ofBoolean(boolean... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.BOOLEAN, new byte[0], null); + } + byte[] byteValues = new byte[values.length]; + for (int i = 0; i < values.length; i++) { + byteValues[i] = (byte) (values[i] ? 1 : 0); + } + return new SecsDataItem(SecsFormatCode.BOOLEAN, byteValues, null); + } + + public static SecsDataItem ofAscii(String value, Charset charset) { + if (value == null) { + return new SecsDataItem(SecsFormatCode.A, new byte[0], null); + } + return new SecsDataItem(SecsFormatCode.A, value.getBytes(charset), null); + } + + public static SecsDataItem ofAscii(String value) { + // Per SECS standard, 'A' format is US-ASCII. + return ofAscii(value, StandardCharsets.US_ASCII); + } + + public static SecsDataItem ofI8(long... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.I8, new byte[0], null); + } + ByteBuffer bb = ByteBuffer.allocate(values.length * 8); + for (long value : values) { + bb.putLong(value); + } + return new SecsDataItem(SecsFormatCode.I8, bb.array(), null); + } + + public static SecsDataItem ofU8(long... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.U8, new byte[0], null); + } + // ByteBuffer handles 8-byte longs directly + ByteBuffer bb = ByteBuffer.allocate(values.length * 8); + for (long value : values) { + bb.putLong(value); + } + return new SecsDataItem(SecsFormatCode.U8, bb.array(), null); + } + + public static SecsDataItem ofF8(double... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.F8, new byte[0], null); + } + ByteBuffer bb = ByteBuffer.allocate(values.length * 8); + for (double value : values) { + bb.putDouble(value); + } + return new SecsDataItem(SecsFormatCode.F8, bb.array(), null); + } + + public static SecsDataItem ofF4(float... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.F4, new byte[0], null); + } + ByteBuffer bb = ByteBuffer.allocate(values.length * 4); + for (float value : values) { + bb.putFloat(value); + } + return new SecsDataItem(SecsFormatCode.F4, bb.array(), null); + } + + public static SecsDataItem ofI4(int... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.I4, new byte[0], null); + } + ByteBuffer bb = ByteBuffer.allocate(values.length * 4); + for (int value : values) { + bb.putInt(value); + } + return new SecsDataItem(SecsFormatCode.I4, bb.array(), null); + } + + public static SecsDataItem ofI2(short... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.I2, new byte[0], null); + } + ByteBuffer bb = ByteBuffer.allocate(values.length * 2); + for (short value : values) { + bb.putShort(value); + } + return new SecsDataItem(SecsFormatCode.I2, bb.array(), null); + } + + public static SecsDataItem ofU2(int... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.U2, new byte[0], null); + } + for (int value : values) { + if (value < 0 || value > 0xFFFF) { + throw new IllegalArgumentException("U2 values must be between 0 and 0xFFFF"); + } + } + ByteBuffer bb = ByteBuffer.allocate(values.length * 2); + for (int value : values) { + bb.putShort((short) value); + } + return new SecsDataItem(SecsFormatCode.U2, bb.array(), null); + } + + public static SecsDataItem ofI1(byte... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.I1, new byte[0], null); + } + return new SecsDataItem(SecsFormatCode.I1, values, null); + } + + public static SecsDataItem ofU1(short... values) { + if (values == null) { + return new SecsDataItem(SecsFormatCode.U1, new byte[0], null); + } + byte[] byteValues = new byte[values.length]; + for (int i = 0; i < values.length; i++) { + if (values[i] < 0 || values[i] > 0xFF) { + throw new IllegalArgumentException("U1 values must be between 0 and 0xFF"); + } + byteValues[i] = (byte) values[i]; + } + return new SecsDataItem(SecsFormatCode.U1, byteValues, null); + } + + + @Override + public ByteBuf encode(ByteBufAllocator allocator) { + ByteBuf dataBuf; + int length; + + if (formatCode == SecsFormatCode.L) { + dataBuf = allocator.buffer(); + if (listItems != null) { + for (SECSII item : listItems) { + ByteBuf itemBuf = item.encode(allocator); + dataBuf.writeBytes(itemBuf); + itemBuf.release(); + } + } + length = listItems != null ? listItems.size() : 0; + } else { + dataBuf = (value != null) ? Unpooled.wrappedBuffer(value) : Unpooled.EMPTY_BUFFER; + length = (value != null) ? value.length : 0; + } + + int lengthBytesIndicator; + if (length <= 0xFF) { + lengthBytesIndicator = 1; + } else if (length <= 0xFFFF) { + lengthBytesIndicator = 2; + } else if (length <= 0xFFFFFF) { + lengthBytesIndicator = 3; + } else { + throw new IllegalArgumentException("Data item length " + length + " is too large for SECS-II"); + } + + ByteBuf headerBuf = allocator.buffer(1 + lengthBytesIndicator); + headerBuf.writeByte(formatCode.getValue() | lengthBytesIndicator); + + switch (lengthBytesIndicator) { + case 1 -> headerBuf.writeByte(length); + case 2 -> headerBuf.writeShort(length); + case 3 -> headerBuf.writeMedium(length); + } + + return Unpooled.wrappedBuffer(headerBuf, dataBuf); + } + + @Override + public void decode(ByteBuf in) { + if (!in.isReadable()) { + this.listItems = Collections.emptyList(); + this.value = new byte[0]; + return; + } + + byte formatByte = in.readByte(); + this.formatCode = SecsFormatCode.fromByte(formatByte); + int lengthBytes = formatByte & 0x03; + + int length = switch (lengthBytes) { + case 1 -> in.readUnsignedByte(); + case 2 -> in.readUnsignedShort(); + case 3 -> in.readUnsignedMedium(); + default -> throw new IllegalArgumentException("Invalid length bytes for data item: " + lengthBytes); + }; + + this.listItems = null; // Clear previous values + this.value = null; + + if (formatCode == SecsFormatCode.L) { + List decodedList = new ArrayList<>(); + ByteBuf subBuffer = in.readSlice(length); // This is where the error was, it should be totalLength + // The previous code had a bug here. It should be based on number of items, not bytes for lists. + // But the length *is* number of items for L. So it's not bytes. + for (int i = 0; i < length; i++) { + SecsDataItem item = new SecsDataItem(); + item.decode(in); + decodedList.add(item); + } + this.listItems = decodedList; + + } else { + this.value = new byte[length]; + in.readBytes(this.value); + } + } + + public SecsFormatCode getFormatCode() { + return formatCode; + } + + public List getListItems() { + if (formatCode != SecsFormatCode.L) { + throw new IllegalStateException("Not a List item."); + } + return listItems != null ? Collections.unmodifiableList(listItems) : Collections.emptyList(); + } + + public byte[] getRawBytes() { + if (formatCode == SecsFormatCode.L) { + throw new IllegalStateException("Cannot get raw bytes for a List item."); + } + return value; + } + + public String getAscii(Charset charset) { + if (formatCode != SecsFormatCode.A) { + throw new IllegalStateException("Not an ASCII item."); + } + return (value != null) ? new String(value, charset) : ""; + } + + public String getAscii() { + // Default to UTF-8 as requested, though US-ASCII is standard. + return getAscii(StandardCharsets.UTF_8); + } + + // Helper for reading from byte array + private ByteBuffer getByteBuffer() { + return (value != null) ? ByteBuffer.wrap(value) : ByteBuffer.allocate(0); + } + + public long[] getU4() { + if (formatCode != SecsFormatCode.U4) throw new IllegalStateException("Format is " + formatCode); + ByteBuffer bb = getByteBuffer(); + long[] result = new long[bb.remaining() / 4]; + for (int i = 0; i < result.length; i++) { + result[i] = bb.getInt() & 0xFFFFFFFFL; + } + return result; + } + + public byte[] getBinary() { + if (formatCode != SecsFormatCode.B) throw new IllegalStateException("Format is " + formatCode); + return value; + } + + public boolean[] getBoolean() { + if (formatCode != SecsFormatCode.BOOLEAN) throw new IllegalStateException("Format is " + formatCode); + ByteBuffer bb = getByteBuffer(); + boolean[] result = new boolean[bb.remaining()]; + for (int i = 0; i < result.length; i++) { + result[i] = bb.get() != 0; + } + return result; + } + + public long[] getI8() { + if (formatCode != SecsFormatCode.I8) throw new IllegalStateException("Format is " + formatCode); + ByteBuffer bb = getByteBuffer(); + long[] result = new long[bb.remaining() / 8]; + for (int i = 0; i < result.length; i++) { + result[i] = bb.getLong(); + } + return result; + } + + public long[] getU8() { + if (formatCode != SecsFormatCode.U8) throw new IllegalStateException("Format is " + formatCode); + return getI8(); // Java doesn't have unsigned longs, so treat as signed for retrieval + } + + public double[] getF8() { + if (formatCode != SecsFormatCode.F8) throw new IllegalStateException("Format is " + formatCode); + ByteBuffer bb = getByteBuffer(); + double[] result = new double[bb.remaining() / 8]; + for (int i = 0; i < result.length; i++) { + result[i] = bb.getDouble(); + } + return result; + } + + public float[] getF4() { + if (formatCode != SecsFormatCode.F4) throw new IllegalStateException("Format is " + formatCode); + ByteBuffer bb = getByteBuffer(); + float[] result = new float[bb.remaining() / 4]; + for (int i = 0; i < result.length; i++) { + result[i] = bb.getFloat(); + } + return result; + } + + public int[] getI4() { + if (formatCode != SecsFormatCode.I4) throw new IllegalStateException("Format is " + formatCode); + ByteBuffer bb = getByteBuffer(); + int[] result = new int[bb.remaining() / 4]; + for (int i = 0; i < result.length; i++) { + result[i] = bb.getInt(); + } + return result; + } + + public short[] getI2() { + if (formatCode != SecsFormatCode.I2) throw new IllegalStateException("Format is " + formatCode); + ByteBuffer bb = getByteBuffer(); + short[] result = new short[bb.remaining() / 2]; + for (int i = 0; i < result.length; i++) { + result[i] = bb.getShort(); + } + return result; + } + + public int[] getU2() { + if (formatCode != SecsFormatCode.U2) throw new IllegalStateException("Format is " + formatCode); + ByteBuffer bb = getByteBuffer(); + int[] result = new int[bb.remaining() / 2]; + for (int i = 0; i < result.length; i++) { + result[i] = bb.getShort() & 0xFFFF; + } + return result; + } + + public byte[] getI1() { + if (formatCode != SecsFormatCode.I1) throw new IllegalStateException("Format is " + formatCode); + return value; + } + + public short[] getU1() { + if (formatCode != SecsFormatCode.U1) throw new IllegalStateException("Format is " + formatCode); + ByteBuffer bb = getByteBuffer(); + short[] result = new short[bb.remaining()]; + for (int i = 0; i < result.length; i++) { + result[i] = (short) (bb.get() & 0xFF); + } + return result; + } +} diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsFormatCode.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsFormatCode.java new file mode 100644 index 0000000..900a9a4 --- /dev/null +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsFormatCode.java @@ -0,0 +1,46 @@ +package com.github.aside8.eap.protocol.secs2; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public enum SecsFormatCode { + L((byte) 0x00), + B((byte) 0x10), + BOOLEAN((byte) 0x20), + A((byte) 0x40), + I8((byte) 0x60), + U8((byte) 0x70), + F8((byte) 0x80), + F4((byte) 0x90), + I4((byte) 0xA0), + U4((byte) 0xB0), + I2((byte) 0xC0), + U2((byte) 0xD0), + I1((byte) 0xE0), + U1((byte) 0xF0); + + private final byte value; + + private static final Map FORMAT_CODE_MAP = + Arrays.stream(values()).collect(Collectors.toMap(SecsFormatCode::getValue, Function.identity())); + + SecsFormatCode(byte value) { + this.value = value; + } + + public byte getValue() { + return value; + } + + public static SecsFormatCode fromByte(byte code) { + // Mask out the length bits + byte formatCode = (byte) (code & 0xFC); + SecsFormatCode secsFormatCode = FORMAT_CODE_MAP.get(formatCode); + if (secsFormatCode == null) { + throw new IllegalArgumentException("Unsupported SECS-II format code: " + String.format("0x%02X", formatCode)); + } + return secsFormatCode; + } +} From 2d869be1ede9ab044fead004a25361f44dfe22cb Mon Sep 17 00:00:00 2001 From: aside8 Date: Tue, 2 Dec 2025 17:18:17 +0800 Subject: [PATCH 02/11] Add develop branch to Maven CI workflow --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index fe2124b..2231aec 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -10,7 +10,7 @@ name: Java CI with Maven on: push: - branches: [ "main" ] + branches: [ "main" ,"develop"] pull_request: branches: [ "main" ] From 6e59fb8dcf9dab900751734cb0c3de0fe9351952 Mon Sep 17 00:00:00 2001 From: zhangzhuo Date: Tue, 2 Dec 2025 22:52:28 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=B8=AD=E7=9A=84=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EAP-Protocol/pom.xml | 5 + .../aside8/eap/protocol/secs2/SECSII.java | 114 ++++--- .../eap/protocol/secs2/SecsDataItem.java | 316 +++++++++--------- .../eap/protocol/secs2/SecsFormatCode.java | 49 +-- .../eap/protocol/secs2/SecsDataItemTest.java | 30 ++ 5 files changed, 288 insertions(+), 226 deletions(-) create mode 100644 EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java diff --git a/EAP-Protocol/pom.xml b/EAP-Protocol/pom.xml index 5d4b852..2b4e89c 100644 --- a/EAP-Protocol/pom.xml +++ b/EAP-Protocol/pom.xml @@ -22,6 +22,11 @@ io.netty netty-buffer + + org.junit.jupiter + junit-jupiter-api + test + \ No newline at end of file diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java index c6a8132..e77888d 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java @@ -2,6 +2,8 @@ import com.github.aside8.eap.protocol.Codec; +import java.nio.charset.Charset; + /** * Interface for all SECS-II message bodies. */ @@ -15,7 +17,7 @@ public interface SECSII extends Codec { * @return a SECSII object representing the List data item. */ static SECSII list(SECSII... items) { - return SecsDataItem.ofList(items); + return SecsDataItem.list(items); } /** @@ -25,7 +27,7 @@ static SECSII list(SECSII... items) { * @return a SECSII object representing the Binary data item. */ static SECSII binary(byte... values) { - return SecsDataItem.ofBinary(values); + return SecsDataItem.binary(values); } /** @@ -35,7 +37,7 @@ static SECSII binary(byte... values) { * @return a SECSII object representing the Boolean data item. */ static SECSII bool(boolean... values) { - return SecsDataItem.ofBoolean(values); + return SecsDataItem.bool(values); } /** @@ -45,47 +47,47 @@ static SECSII bool(boolean... values) { * @return a SECSII object representing the ASCII data item. */ static SECSII ascii(String value) { - return SecsDataItem.ofAscii(value); + return SecsDataItem.ascii(value); } /** - * Creates a SECS-II data item of type 8-byte Integer (I8). + * Creates a SECS-II data item of type ASCII (A). * - * @param values the 8-byte integer values. - * @return a SECSII object representing the I8 data item. + * @param value the ASCII string value. + * @return a SECSII object representing the ASCII data item. */ - static SECSII i8(long... values) { - return SecsDataItem.ofI8(values); + static SECSII ascii(String value, Charset charset) { + return SecsDataItem.ascii(value, charset); } /** - * Creates a SECS-II data item of type 8-byte Unsigned Integer (U8). + * Creates a SECS-II data item of type 8-byte Integer (I8). * - * @param values the 8-byte unsigned integer values. - * @return a SECSII object representing the U8 data item. + * @param values the 8-byte integer values. + * @return a SECSII object representing the I8 data item. */ - static SECSII u8(long... values) { - return SecsDataItem.ofU8(values); + static SECSII int8(long... values) { + return SecsDataItem.int8(values); } /** - * Creates a SECS-II data item of type 8-byte Floating Point (F8). + * Creates a SECS-II data item of type 1-byte Integer (I1). * - * @param values the 8-byte floating point values. - * @return a SECSII object representing the F8 data item. + * @param values the 1-byte integer values. + * @return a SECSII object representing the I1 data item. */ - static SECSII f8(double... values) { - return SecsDataItem.ofF8(values); + static SECSII int1(byte... values) { + return SecsDataItem.int1(values); } /** - * Creates a SECS-II data item of type 4-byte Floating Point (F4). + * Creates a SECS-II data item of type 2-byte Integer (I2). * - * @param values the 4-byte floating point values. - * @return a SECSII object representing the F4 data item. + * @param values the 2-byte integer values. + * @return a SECSII object representing the I2 data item. */ - static SECSII f4(float... values) { - return SecsDataItem.ofF4(values); + static SECSII int2(short... values) { + return SecsDataItem.int2(values); } /** @@ -94,57 +96,67 @@ static SECSII f4(float... values) { * @param values the 4-byte integer values. * @return a SECSII object representing the I4 data item. */ - static SECSII i4(int... values) { - return SecsDataItem.ofI4(values); + static SECSII int4(int... values) { + return SecsDataItem.int4(values); } /** - * Creates a SECS-II data item of type 4-byte Unsigned Integer (U4). + * Creates a SECS-II data item of type 8-byte Floating Point (F8). * - * @param values the unsigned 4-byte integer values. - * @return a SECSII object representing the U4 data item. + * @param values the 8-byte floating point values. + * @return a SECSII object representing the F8 data item. */ - static SECSII u4(long... values) { - return SecsDataItem.ofU4(values); + static SECSII float8(double... values) { + return SecsDataItem.float8(values); } /** - * Creates a SECS-II data item of type 2-byte Integer (I2). + * Creates a SECS-II data item of type 4-byte Floating Point (F4). * - * @param values the 2-byte integer values. - * @return a SECSII object representing the I2 data item. + * @param values the 4-byte floating point values. + * @return a SECSII object representing the F4 data item. */ - static SECSII i2(short... values) { - return SecsDataItem.ofI2(values); + static SECSII float4(float... values) { + return SecsDataItem.float4(values); } /** - * Creates a SECS-II data item of type 2-byte Unsigned Integer (U2). + * Creates a SECS-II data item of type 8-byte Unsigned Integer (U8). * - * @param values the 2-byte unsigned integer values. - * @return a SECSII object representing the U2 data item. + * @param values the 8-byte unsigned integer values. + * @return a SECSII object representing the U8 data item. */ - static SECSII u2(int... values) { - return SecsDataItem.ofU2(values); + static SECSII uint8(long... values) { + return SecsDataItem.uint8(values); } /** - * Creates a SECS-II data item of type 1-byte Integer (I1). + * Creates a SECS-II data item of type 1-byte Unsigned Integer (U1). * - * @param values the 1-byte integer values. - * @return a SECSII object representing the I1 data item. + * @param values the 1-byte unsigned integer values. + * @return a SECSII object representing the U1 data item. */ - static SECSII i1(byte... values) { - return SecsDataItem.ofI1(values); + static SECSII uint1(short... values) { + return SecsDataItem.uint1(values); } /** - * Creates a SECS-II data item of type 1-byte Unsigned Integer (U1). + * Creates a SECS-II data item of type 2-byte Unsigned Integer (U2). * - * @param values the 1-byte unsigned integer values. - * @return a SECSII object representing the U1 data item. + * @param values the 2-byte unsigned integer values. + * @return a SECSII object representing the U2 data item. + */ + static SECSII uint2(int... values) { + return SecsDataItem.uint2(values); + } + + /** + * Creates a SECS-II data item of type 4-byte Unsigned Integer (U4). + * + * @param values the unsigned 4-byte integer values. + * @return a SECSII object representing the U4 data item. */ - static SECSII u1(short... values) { - return SecsDataItem.ofU1(values); + static SECSII uint4(long... values) { + return SecsDataItem.uint4(values); } } \ No newline at end of file diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java index 40f1c38..76f37bb 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java @@ -3,6 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import lombok.Getter; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -13,12 +14,13 @@ import java.util.List; public final class SecsDataItem implements SECSII { + @Getter private SecsFormatCode formatCode; private byte[] value; private List listItems; public SecsDataItem() { - this.formatCode = SecsFormatCode.L; // Default to List, will be updated during decode + this.formatCode = SecsFormatCode.LIST; // Default to List, will be updated during decode this.listItems = Collections.emptyList(); this.value = new byte[0]; } @@ -31,35 +33,19 @@ private SecsDataItem(SecsFormatCode formatCode, byte[] value, List listI } // Static factory methods - public static SecsDataItem ofList(SECSII... items) { + public static SecsDataItem list(SECSII... items) { List itemList = (items != null) ? Arrays.asList(items) : Collections.emptyList(); - return new SecsDataItem(SecsFormatCode.L, null, itemList); + return new SecsDataItem(SecsFormatCode.LIST, null, itemList); } - public static SecsDataItem ofU4(long... values) { + public static SecsDataItem binary(byte... values) { if (values == null) { - return new SecsDataItem(SecsFormatCode.U4, new byte[0], null); + return new SecsDataItem(SecsFormatCode.BINARY, new byte[0], null); } - for (long value : values) { - if (value < 0 || value > 0xFFFFFFFFL) { - throw new IllegalArgumentException("U4 values must be between 0 and 0xFFFFFFFFL"); - } - } - ByteBuffer bb = ByteBuffer.allocate(values.length * 4); - for (long value : values) { - bb.putInt((int) value); - } - return new SecsDataItem(SecsFormatCode.U4, bb.array(), null); + return new SecsDataItem(SecsFormatCode.BINARY, values, null); } - public static SecsDataItem ofBinary(byte... values) { - if (values == null) { - return new SecsDataItem(SecsFormatCode.B, new byte[0], null); - } - return new SecsDataItem(SecsFormatCode.B, values, null); - } - - public static SecsDataItem ofBoolean(boolean... values) { + public static SecsDataItem bool(boolean... values) { if (values == null) { return new SecsDataItem(SecsFormatCode.BOOLEAN, new byte[0], null); } @@ -70,129 +56,157 @@ public static SecsDataItem ofBoolean(boolean... values) { return new SecsDataItem(SecsFormatCode.BOOLEAN, byteValues, null); } - public static SecsDataItem ofAscii(String value, Charset charset) { + public static SecsDataItem ascii(String value, Charset charset) { if (value == null) { - return new SecsDataItem(SecsFormatCode.A, new byte[0], null); + return new SecsDataItem(SecsFormatCode.ASCII, new byte[0], null); } - return new SecsDataItem(SecsFormatCode.A, value.getBytes(charset), null); + return new SecsDataItem(SecsFormatCode.ASCII, value.getBytes(charset), null); } - - public static SecsDataItem ofAscii(String value) { + + public static SecsDataItem ascii(String value) { // Per SECS standard, 'A' format is US-ASCII. - return ofAscii(value, StandardCharsets.US_ASCII); + return ascii(value, StandardCharsets.US_ASCII); } - public static SecsDataItem ofI8(long... values) { + + public static SecsDataItem int8(long... values) { + SecsFormatCode formatCode = SecsFormatCode.INT8; if (values == null) { - return new SecsDataItem(SecsFormatCode.I8, new byte[0], null); + return new SecsDataItem(formatCode, new byte[0], null); } - ByteBuffer bb = ByteBuffer.allocate(values.length * 8); + ByteBuffer bb = ByteBuffer.allocate(values.length * formatCode.getSize()); for (long value : values) { bb.putLong(value); } - return new SecsDataItem(SecsFormatCode.I8, bb.array(), null); + return new SecsDataItem(formatCode, bb.array(), null); } - public static SecsDataItem ofU8(long... values) { + public static SecsDataItem int1(byte... values) { + SecsFormatCode formatCode = SecsFormatCode.INT1; if (values == null) { - return new SecsDataItem(SecsFormatCode.U8, new byte[0], null); + return new SecsDataItem(formatCode, new byte[0], null); } - // ByteBuffer handles 8-byte longs directly - ByteBuffer bb = ByteBuffer.allocate(values.length * 8); - for (long value : values) { - bb.putLong(value); + return new SecsDataItem(formatCode, values, null); + } + + public static SecsDataItem int2(short... values) { + SecsFormatCode formatCode = SecsFormatCode.INT2; + if (values == null) { + return new SecsDataItem(formatCode, new byte[0], null); + } + ByteBuffer bb = ByteBuffer.allocate(values.length * formatCode.getSize()); + for (short value : values) { + bb.putShort(value); + } + return new SecsDataItem(formatCode, bb.array(), null); + } + + public static SecsDataItem int4(int... values) { + SecsFormatCode formatCode = SecsFormatCode.INT4; + if (values == null) { + return new SecsDataItem(formatCode, new byte[0], null); + } + ByteBuffer bb = ByteBuffer.allocate(values.length * formatCode.getSize()); + for (int value : values) { + bb.putInt(value); } - return new SecsDataItem(SecsFormatCode.U8, bb.array(), null); + return new SecsDataItem(formatCode, bb.array(), null); } - public static SecsDataItem ofF8(double... values) { + public static SecsDataItem float8(double... values) { + SecsFormatCode formatCode = SecsFormatCode.FLOAT8; if (values == null) { - return new SecsDataItem(SecsFormatCode.F8, new byte[0], null); + return new SecsDataItem(formatCode, new byte[0], null); } - ByteBuffer bb = ByteBuffer.allocate(values.length * 8); + ByteBuffer bb = ByteBuffer.allocate(values.length * formatCode.getSize()); for (double value : values) { bb.putDouble(value); } - return new SecsDataItem(SecsFormatCode.F8, bb.array(), null); + return new SecsDataItem(formatCode, bb.array(), null); } - public static SecsDataItem ofF4(float... values) { + public static SecsDataItem float4(float... values) { + SecsFormatCode formatCode = SecsFormatCode.FLOAT4; if (values == null) { - return new SecsDataItem(SecsFormatCode.F4, new byte[0], null); + return new SecsDataItem(formatCode, new byte[0], null); } - ByteBuffer bb = ByteBuffer.allocate(values.length * 4); + ByteBuffer bb = ByteBuffer.allocate(values.length * formatCode.getSize()); for (float value : values) { bb.putFloat(value); } - return new SecsDataItem(SecsFormatCode.F4, bb.array(), null); + return new SecsDataItem(formatCode, bb.array(), null); } - public static SecsDataItem ofI4(int... values) { + public static SecsDataItem uint8(long... values) { + SecsFormatCode formatCode = SecsFormatCode.UINT8; if (values == null) { - return new SecsDataItem(SecsFormatCode.I4, new byte[0], null); + return new SecsDataItem(formatCode, new byte[0], null); } - ByteBuffer bb = ByteBuffer.allocate(values.length * 4); - for (int value : values) { - bb.putInt(value); + // ByteBuffer handles 8-byte longs directly + ByteBuffer bb = ByteBuffer.allocate(values.length * formatCode.getSize()); + for (long value : values) { + bb.putLong(value); } - return new SecsDataItem(SecsFormatCode.I4, bb.array(), null); + return new SecsDataItem(formatCode, bb.array(), null); } - public static SecsDataItem ofI2(short... values) { + public static SecsDataItem uint1(short... values) { + SecsFormatCode formatCode = SecsFormatCode.UINT1; if (values == null) { - return new SecsDataItem(SecsFormatCode.I2, new byte[0], null); + return new SecsDataItem(formatCode, new byte[0], null); } - ByteBuffer bb = ByteBuffer.allocate(values.length * 2); - for (short value : values) { - bb.putShort(value); + byte[] byteValues = new byte[values.length]; + for (int i = 0; i < values.length; i++) { + if (values[i] < 0 || values[i] > 0xFF) { + throw new IllegalArgumentException("U1 values must be between 0 and 0xFF"); + } + byteValues[i] = (byte) values[i]; } - return new SecsDataItem(SecsFormatCode.I2, bb.array(), null); + return new SecsDataItem(formatCode, byteValues, null); } - public static SecsDataItem ofU2(int... values) { + public static SecsDataItem uint2(int... values) { + SecsFormatCode formatCode = SecsFormatCode.UINT2; if (values == null) { - return new SecsDataItem(SecsFormatCode.U2, new byte[0], null); + return new SecsDataItem(formatCode, new byte[0], null); } for (int value : values) { if (value < 0 || value > 0xFFFF) { throw new IllegalArgumentException("U2 values must be between 0 and 0xFFFF"); } } - ByteBuffer bb = ByteBuffer.allocate(values.length * 2); + ByteBuffer bb = ByteBuffer.allocate(values.length * formatCode.getSize()); for (int value : values) { bb.putShort((short) value); } - return new SecsDataItem(SecsFormatCode.U2, bb.array(), null); + return new SecsDataItem(formatCode, bb.array(), null); } - public static SecsDataItem ofI1(byte... values) { + public static SecsDataItem uint4(long... values) { + SecsFormatCode formatCode = SecsFormatCode.UINT4; if (values == null) { - return new SecsDataItem(SecsFormatCode.I1, new byte[0], null); + return new SecsDataItem(formatCode, new byte[0], null); } - return new SecsDataItem(SecsFormatCode.I1, values, null); - } - public static SecsDataItem ofU1(short... values) { - if (values == null) { - return new SecsDataItem(SecsFormatCode.U1, new byte[0], null); - } - byte[] byteValues = new byte[values.length]; - for (int i = 0; i < values.length; i++) { - if (values[i] < 0 || values[i] > 0xFF) { - throw new IllegalArgumentException("U1 values must be between 0 and 0xFF"); + for (long value : values) { + if (value < 0 || value > 0xFFFFFFFFL) { + throw new IllegalArgumentException("U4 values must be between 0 and 0xFFFFFFFFL"); } - byteValues[i] = (byte) values[i]; } - return new SecsDataItem(SecsFormatCode.U1, byteValues, null); - } + ByteBuffer bb = ByteBuffer.allocate(values.length * formatCode.getSize()); + for (long value : values) { + bb.putInt((int) value); + } + return new SecsDataItem(formatCode, bb.array(), null); + } @Override public ByteBuf encode(ByteBufAllocator allocator) { ByteBuf dataBuf; int length; - if (formatCode == SecsFormatCode.L) { + if (formatCode == SecsFormatCode.LIST) { dataBuf = allocator.buffer(); if (listItems != null) { for (SECSII item : listItems) { @@ -252,11 +266,8 @@ public void decode(ByteBuf in) { this.listItems = null; // Clear previous values this.value = null; - if (formatCode == SecsFormatCode.L) { + if (formatCode == SecsFormatCode.LIST) { List decodedList = new ArrayList<>(); - ByteBuf subBuffer = in.readSlice(length); // This is where the error was, it should be totalLength - // The previous code had a bug here. It should be based on number of items, not bytes for lists. - // But the length *is* number of items for L. So it's not bytes. for (int i = 0; i < length; i++) { SecsDataItem item = new SecsDataItem(); item.decode(in); @@ -270,26 +281,43 @@ public void decode(ByteBuf in) { } } - public SecsFormatCode getFormatCode() { - return formatCode; + public byte[] getRawBytes() { + if (formatCode == SecsFormatCode.LIST) { + throw new IllegalStateException("Cannot get raw bytes for a List item."); + } + return value; } - public List getListItems() { - if (formatCode != SecsFormatCode.L) { + // Helper for reading from byte array + private ByteBuffer getByteBuffer() { + return (value != null) ? ByteBuffer.wrap(value) : ByteBuffer.allocate(0); + } + + + public List getList() { + if (formatCode != SecsFormatCode.LIST) { throw new IllegalStateException("Not a List item."); } return listItems != null ? Collections.unmodifiableList(listItems) : Collections.emptyList(); } - - public byte[] getRawBytes() { - if (formatCode == SecsFormatCode.L) { - throw new IllegalStateException("Cannot get raw bytes for a List item."); - } + + public byte[] getBinary() { + if (formatCode != SecsFormatCode.BINARY) throw new IllegalStateException("Format is " + formatCode); return value; } + public boolean[] getBoolean() { + if (formatCode != SecsFormatCode.BOOLEAN) throw new IllegalStateException("Format is " + formatCode); + ByteBuffer bb = getByteBuffer(); + boolean[] result = new boolean[bb.remaining()]; + for (int i = 0; i < result.length; i++) { + result[i] = bb.get() != 0; + } + return result; + } + public String getAscii(Charset charset) { - if (formatCode != SecsFormatCode.A) { + if (formatCode != SecsFormatCode.ASCII) { throw new IllegalStateException("Not an ASCII item."); } return (value != null) ? new String(value, charset) : ""; @@ -299,113 +327,93 @@ public String getAscii() { // Default to UTF-8 as requested, though US-ASCII is standard. return getAscii(StandardCharsets.UTF_8); } - - // Helper for reading from byte array - private ByteBuffer getByteBuffer() { - return (value != null) ? ByteBuffer.wrap(value) : ByteBuffer.allocate(0); - } - public long[] getU4() { - if (formatCode != SecsFormatCode.U4) throw new IllegalStateException("Format is " + formatCode); + public long[] getInt8() { + if (formatCode != SecsFormatCode.INT8) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); - long[] result = new long[bb.remaining() / 4]; + long[] result = new long[bb.remaining() / formatCode.getSize()]; for (int i = 0; i < result.length; i++) { - result[i] = bb.getInt() & 0xFFFFFFFFL; + result[i] = bb.getLong(); } return result; } - - public byte[] getBinary() { - if (formatCode != SecsFormatCode.B) throw new IllegalStateException("Format is " + formatCode); + + public byte[] getInt1() { + if (formatCode != SecsFormatCode.INT1) throw new IllegalStateException("Format is " + formatCode); return value; } - - public boolean[] getBoolean() { - if (formatCode != SecsFormatCode.BOOLEAN) throw new IllegalStateException("Format is " + formatCode); + + public short[] getInt2() { + if (formatCode != SecsFormatCode.INT2) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); - boolean[] result = new boolean[bb.remaining()]; + short[] result = new short[bb.remaining() / formatCode.getSize()]; for (int i = 0; i < result.length; i++) { - result[i] = bb.get() != 0; + result[i] = bb.getShort(); } return result; } - public long[] getI8() { - if (formatCode != SecsFormatCode.I8) throw new IllegalStateException("Format is " + formatCode); + public int[] getInt4() { + if (formatCode != SecsFormatCode.INT4) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); - long[] result = new long[bb.remaining() / 8]; + int[] result = new int[bb.remaining() / formatCode.getSize()]; for (int i = 0; i < result.length; i++) { - result[i] = bb.getLong(); + result[i] = bb.getInt(); } return result; } - - public long[] getU8() { - if (formatCode != SecsFormatCode.U8) throw new IllegalStateException("Format is " + formatCode); - return getI8(); // Java doesn't have unsigned longs, so treat as signed for retrieval - } - - public double[] getF8() { - if (formatCode != SecsFormatCode.F8) throw new IllegalStateException("Format is " + formatCode); + + public double[] getFloat8() { + if (formatCode != SecsFormatCode.FLOAT8) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); - double[] result = new double[bb.remaining() / 8]; + double[] result = new double[bb.remaining() / formatCode.getSize()]; for (int i = 0; i < result.length; i++) { result[i] = bb.getDouble(); } return result; } - public float[] getF4() { - if (formatCode != SecsFormatCode.F4) throw new IllegalStateException("Format is " + formatCode); + public float[] getFloat4() { + if (formatCode != SecsFormatCode.FLOAT4) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); - float[] result = new float[bb.remaining() / 4]; + float[] result = new float[bb.remaining() / formatCode.getSize()]; for (int i = 0; i < result.length; i++) { result[i] = bb.getFloat(); } return result; } - - public int[] getI4() { - if (formatCode != SecsFormatCode.I4) throw new IllegalStateException("Format is " + formatCode); - ByteBuffer bb = getByteBuffer(); - int[] result = new int[bb.remaining() / 4]; - for (int i = 0; i < result.length; i++) { - result[i] = bb.getInt(); - } - return result; + + public long[] getUint8() { + if (formatCode != SecsFormatCode.UINT8) throw new IllegalStateException("Format is " + formatCode); + return getInt8(); // Java doesn't have unsigned longs, so treat as signed for retrieval } - - public short[] getI2() { - if (formatCode != SecsFormatCode.I2) throw new IllegalStateException("Format is " + formatCode); + + public short[] getUint1() { + if (formatCode != SecsFormatCode.UINT1) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); - short[] result = new short[bb.remaining() / 2]; + short[] result = new short[bb.remaining()]; for (int i = 0; i < result.length; i++) { - result[i] = bb.getShort(); + result[i] = (short) (bb.get() & 0xFF); } return result; } - public int[] getU2() { - if (formatCode != SecsFormatCode.U2) throw new IllegalStateException("Format is " + formatCode); + public int[] getUint2() { + if (formatCode != SecsFormatCode.UINT2) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); - int[] result = new int[bb.remaining() / 2]; + int[] result = new int[bb.remaining() / formatCode.getSize()]; for (int i = 0; i < result.length; i++) { result[i] = bb.getShort() & 0xFFFF; } return result; } - - public byte[] getI1() { - if (formatCode != SecsFormatCode.I1) throw new IllegalStateException("Format is " + formatCode); - return value; - } - - public short[] getU1() { - if (formatCode != SecsFormatCode.U1) throw new IllegalStateException("Format is " + formatCode); + + public long[] getUint4() { + if (formatCode != SecsFormatCode.UINT4) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); - short[] result = new short[bb.remaining()]; + long[] result = new long[bb.remaining() / formatCode.getSize()]; for (int i = 0; i < result.length; i++) { - result[i] = (short) (bb.get() & 0xFF); + result[i] = bb.getInt() & 0xFFFFFFFFL; } return result; } diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsFormatCode.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsFormatCode.java index 900a9a4..a7b2eb1 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsFormatCode.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsFormatCode.java @@ -1,37 +1,44 @@ package com.github.aside8.eap.protocol.secs2; +import lombok.Getter; + import java.util.Arrays; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; +@Getter public enum SecsFormatCode { - L((byte) 0x00), - B((byte) 0x10), - BOOLEAN((byte) 0x20), - A((byte) 0x40), - I8((byte) 0x60), - U8((byte) 0x70), - F8((byte) 0x80), - F4((byte) 0x90), - I4((byte) 0xA0), - U4((byte) 0xB0), - I2((byte) 0xC0), - U2((byte) 0xD0), - I1((byte) 0xE0), - U1((byte) 0xF0); - - private final byte value; + LIST((byte) 0x00, -1,"L"), + BINARY((byte) 0x20, 1,"B"), + BOOLEAN((byte) 0x24, 1,"BOOLEAN"), + ASCII((byte) 0x40, 1,"A"), + JIS8((byte) 0x44, 1,"J"), + UNICODE((byte) 0x48, 2,"UNICODE"), + INT8((byte) 0x60, 8,"I8"), + INT1((byte) 0x64, 1,"I1"), + INT2((byte) 0x68, 2,"I2"), + INT4((byte) 0x70, 4,"I4"), + FLOAT8((byte) 0x80, 8,"F8"), + FLOAT4((byte) 0x90, 4,"F4"), + UINT8((byte) 0xA0, 8,"U8"), + UINT1((byte) 0xA4, 1,"U1"), + UINT2((byte) 0xA8, 2,"U2"), + UINT4((byte) 0xB0, 4,"U4"); + + private byte value; + + private int size; + + private String symbol; private static final Map FORMAT_CODE_MAP = Arrays.stream(values()).collect(Collectors.toMap(SecsFormatCode::getValue, Function.identity())); - SecsFormatCode(byte value) { + SecsFormatCode(byte value, Integer size, String symbol) { this.value = value; - } - - public byte getValue() { - return value; + this.size = size; + this.symbol = symbol; } public static SecsFormatCode fromByte(byte code) { diff --git a/EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java b/EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java new file mode 100644 index 0000000..0474b78 --- /dev/null +++ b/EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java @@ -0,0 +1,30 @@ +package com.github.aside8.eap.protocol.secs2; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SecsDataItemTest { + + @Test + void encode() { + ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + SecsDataItem dataItem = SecsDataItem.int8(1L, 2L, 3L); + ByteBuf encoded = dataItem.encode(allocator); + assertEquals(26, encoded.readableBytes()); + + assertThrowsExactly(IllegalStateException.class, dataItem::getUint8); + + long[] i8 = dataItem.getInt8(); + assertEquals(3, i8.length); + assertEquals(1L, i8[0]); + assertEquals(2L, i8[1]); + assertEquals(3L, i8[2]); + + SecsDataItem compare = new SecsDataItem(); + compare.decode(encoded); + assertArrayEquals(dataItem.getInt8(), compare.getInt8()); + } +} \ No newline at end of file From 9c45842abbe542f119090a90b775a191b87097f3 Mon Sep 17 00:00:00 2001 From: zhangzhuo Date: Tue, 2 Dec 2025 23:07:59 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0ToString=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eap/protocol/secs2/SecsDataItem.java | 95 +++++++++++++++++++ .../eap/protocol/secs2/SecsDataItemTest.java | 45 ++++++++- 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java index 76f37bb..bef1d67 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java @@ -417,4 +417,99 @@ public long[] getUint4() { } return result; } + + @Override + public String toString() { + return toFormatString(); + } + + public String toFormatString() { + StringBuilder sb = new StringBuilder(); + toFormatString(sb, 0); + return sb.toString(); + } + + private void toFormatString(StringBuilder sb, int indent) { + sb.append(" ".repeat(Math.max(0, indent))); + sb.append("<").append(formatCode.getSymbol()).append(" ["); + + if (formatCode == SecsFormatCode.LIST) { + int size = (listItems != null) ? listItems.size() : 0; + sb.append(size).append("]"); + + if (size == 0) { + sb.append(">\n"); + return; + } + + sb.append("\n"); + + if (listItems != null) { + for (SECSII item : listItems) { + if (item instanceof SecsDataItem) { + ((SecsDataItem) item).toFormatString(sb, indent + 1); + } + } + } + sb.append(" ".repeat(Math.max(0, indent))); + sb.append(">\n"); + + } else { + int length = (value != null) ? value.length : 0; + int size = (formatCode.getSize() > 0 && length > 0) ? length / formatCode.getSize() : length; + sb.append(size).append("] "); + + if (value != null) { + switch (formatCode) { + case ASCII: + sb.append("\"").append(getAscii()).append("\""); + break; + case INT1: + sb.append(Arrays.toString(getInt1()).replaceAll("[\\[\\],]", "")); + break; + case INT2: + sb.append(Arrays.toString(getInt2()).replaceAll("[\\[\\],]", "")); + break; + case INT4: + sb.append(Arrays.toString(getInt4()).replaceAll("[\\[\\],]", "")); + break; + case INT8: + sb.append(Arrays.toString(getInt8()).replaceAll("[\\[\\],]", "")); + break; + case UINT1: + sb.append(Arrays.toString(getUint1()).replaceAll("[\\[\\],]", "")); + break; + case UINT2: + sb.append(Arrays.toString(getUint2()).replaceAll("[\\[\\],]", "")); + break; + case UINT4: + sb.append(Arrays.toString(getUint4()).replaceAll("[\\[\\],]", "")); + break; + case UINT8: + sb.append(Arrays.toString(getUint8()).replaceAll("[\\[\\],]", "")); + break; + case FLOAT4: + sb.append(Arrays.toString(getFloat4()).replaceAll("[\\[\\],]", "")); + break; + case FLOAT8: + sb.append(Arrays.toString(getFloat8()).replaceAll("[\\[\\],]", "")); + break; + case BOOLEAN: + sb.append(Arrays.toString(getBoolean()).replaceAll("[\\[\\],]", "")); + break; + case BINARY: + StringBuilder hex = new StringBuilder(); + for (byte b : value) { + hex.append(String.format("0x%02X ", b)); + } + sb.append(hex.toString().trim()); + break; + default: + sb.append("..."); + break; + } + } + sb.append(">\n"); + } + } } diff --git a/EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java b/EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java index 0474b78..909a95c 100644 --- a/EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java +++ b/EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java @@ -10,7 +10,7 @@ class SecsDataItemTest { @Test void encode() { - ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; SecsDataItem dataItem = SecsDataItem.int8(1L, 2L, 3L); ByteBuf encoded = dataItem.encode(allocator); assertEquals(26, encoded.readableBytes()); @@ -27,4 +27,47 @@ void encode() { compare.decode(encoded); assertArrayEquals(dataItem.getInt8(), compare.getInt8()); } + + @Test + void testToString() { + SecsDataItem nestedItem = SecsDataItem.list( + SecsDataItem.ascii("2025060316400217"), + SecsDataItem.uint1((short) 1), + SecsDataItem.ascii("12B1111") + ); + + SecsDataItem item = SecsDataItem.list( + SecsDataItem.uint4(0), + SecsDataItem.uint4(32000), + SecsDataItem.list( + SecsDataItem.list( + SecsDataItem.uint4(32000), + nestedItem + ) + ) + ); + + String expected = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " >\n" + + " >\n" + + " >\n" + + ">\n"; + + String actual = item.toString(); + assertEquals(expected.replace("\r\n", "\n"), actual.replace("\r\n", "\n")); + + expected = "\n"; + item = SecsDataItem.binary((byte) 0); + assertEquals(expected.replace("\r\n", "\n"), item.toString()); + } } \ No newline at end of file From 27e4dc9c6d519d7ce48c428ba2e9f25e76e46185 Mon Sep 17 00:00:00 2001 From: aside8 Date: Wed, 3 Dec 2025 10:07:41 +0800 Subject: [PATCH 05/11] add get method for secsii inteface --- .../aside8/eap/protocol/secs2/SECSII.java | 57 ++++++++ .../eap/protocol/secs2/SecsDataItem.java | 123 +++++++++++++++++- .../eap/protocol/secs2/SecsDataItemTest.java | 26 ++++ 3 files changed, 200 insertions(+), 6 deletions(-) diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java index e77888d..3c448a6 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SECSII.java @@ -3,6 +3,7 @@ import com.github.aside8.eap.protocol.Codec; import java.nio.charset.Charset; +import java.util.List; /** * Interface for all SECS-II message bodies. @@ -159,4 +160,60 @@ static SECSII uint2(int... values) { static SECSII uint4(long... values) { return SecsDataItem.uint4(values); } + + List getList(); + + SECSII get(int index); + + byte[] getBinary(); + + byte getBinary(int index); + + boolean[] getBoolean(); + + boolean getBoolean(int index); + + String getAscii(Charset charset); + + String getAscii(); + + long[] getInt8(); + + long getInt8(int index); + + byte[] getInt1(); + + byte getInt1(int index); + + short[] getInt2(); + + short getInt2(int index); + + int[] getInt4(); + + int getInt4(int index); + + double[] getFloat8(); + + float getFloat8(int index); + + float[] getFloat4(); + + float getFloat4(int index); + + long[] getUint8(); + + long getUint8(int index); + + short[] getUint1(); + + short getUint1(int index); + + int[] getUint2(); + + int getUint2(int index); + + long[] getUint4(); + + long getUint4(int index); } \ No newline at end of file diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java index bef1d67..9670c43 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/secs2/SecsDataItem.java @@ -301,11 +301,28 @@ public List getList() { return listItems != null ? Collections.unmodifiableList(listItems) : Collections.emptyList(); } + public SECSII get(int index) { + List list = getList(); + if (index < 0 || index >= list.size()) { + return null; + } + return list.get(index); + } + public byte[] getBinary() { if (formatCode != SecsFormatCode.BINARY) throw new IllegalStateException("Format is " + formatCode); return value; } + @Override + public byte getBinary(int index) { + byte[] binary = getBinary(); + if (index < 0 || index >= binary.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + binary.length); + } + return binary[index]; + } + public boolean[] getBoolean() { if (formatCode != SecsFormatCode.BOOLEAN) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); @@ -316,6 +333,15 @@ public boolean[] getBoolean() { return result; } + @Override + public boolean getBoolean(int index) { + boolean[] booleans = getBoolean(); + if (index < 0 || index >= booleans.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + booleans.length); + } + return booleans[index]; + } + public String getAscii(Charset charset) { if (formatCode != SecsFormatCode.ASCII) { throw new IllegalStateException("Not an ASCII item."); @@ -337,12 +363,29 @@ public long[] getInt8() { } return result; } + @Override + public long getInt8(int index) { + long[] int8s = getInt8(); + if (index < 0 || index >= int8s.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + int8s.length); + } + return int8s[index]; + } public byte[] getInt1() { if (formatCode != SecsFormatCode.INT1) throw new IllegalStateException("Format is " + formatCode); return value; } - + + @Override + public byte getInt1(int index) { + byte[] int1s = getInt1(); + if (index < 0 || index >= int1s.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + int1s.length); + } + return int1s[index]; + } + public short[] getInt2() { if (formatCode != SecsFormatCode.INT2) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); @@ -353,6 +396,15 @@ public short[] getInt2() { return result; } + @Override + public short getInt2(int index) { + short[] int2s = getInt2(); + if (index < 0 || index >= int2s.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + int2s.length); + } + return int2s[index]; + } + public int[] getInt4() { if (formatCode != SecsFormatCode.INT4) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); @@ -363,6 +415,15 @@ public int[] getInt4() { return result; } + @Override + public int getInt4(int index) { + int[] int4s = getInt4(); + if (index < 0 || index >= int4s.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + int4s.length); + } + return int4s[index]; + } + public double[] getFloat8() { if (formatCode != SecsFormatCode.FLOAT8) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); @@ -373,6 +434,15 @@ public double[] getFloat8() { return result; } + @Override + public float getFloat8(int index) { + double[] float8s = getFloat8(); + if (index < 0 || index >= float8s.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + float8s.length); + } + return (float) float8s[index]; + } + public float[] getFloat4() { if (formatCode != SecsFormatCode.FLOAT4) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); @@ -382,12 +452,29 @@ public float[] getFloat4() { } return result; } + @Override + public float getFloat4(int index) { + float[] float4s = getFloat4(); + if (index < 0 || index >= float4s.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + float4s.length); + } + return float4s[index]; + } public long[] getUint8() { if (formatCode != SecsFormatCode.UINT8) throw new IllegalStateException("Format is " + formatCode); return getInt8(); // Java doesn't have unsigned longs, so treat as signed for retrieval } + @Override + public long getUint8(int index) { + long[] uint8s = getUint8(); + if (index < 0 || index >= uint8s.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + uint8s.length); + } + return uint8s[index]; + } + public short[] getUint1() { if (formatCode != SecsFormatCode.UINT1) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); @@ -398,6 +485,15 @@ public short[] getUint1() { return result; } + @Override + public short getUint1(int index) { + short[] uint1s = getUint1(); + if (index < 0 || index >= uint1s.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + uint1s.length); + } + return uint1s[index]; + } + public int[] getUint2() { if (formatCode != SecsFormatCode.UINT2) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); @@ -408,6 +504,15 @@ public int[] getUint2() { return result; } + @Override + public int getUint2(int index) { + int[] uint2s = getUint2(); + if (index < 0 || index >= uint2s.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + uint2s.length); + } + return uint2s[index]; + } + public long[] getUint4() { if (formatCode != SecsFormatCode.UINT4) throw new IllegalStateException("Format is " + formatCode); ByteBuffer bb = getByteBuffer(); @@ -417,6 +522,14 @@ public long[] getUint4() { } return result; } + @Override + public long getUint4(int index) { + long[] uint4s = getUint4(); + if (index < 0 || index >= uint4s.length) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + uint4s.length); + } + return uint4s[index]; + } @Override public String toString() { @@ -444,11 +557,9 @@ private void toFormatString(StringBuilder sb, int indent) { sb.append("\n"); - if (listItems != null) { - for (SECSII item : listItems) { - if (item instanceof SecsDataItem) { - ((SecsDataItem) item).toFormatString(sb, indent + 1); - } + for (SECSII item : listItems) { + if (item instanceof SecsDataItem) { + ((SecsDataItem) item).toFormatString(sb, indent + 1); } } sb.append(" ".repeat(Math.max(0, indent))); diff --git a/EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java b/EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java index 909a95c..a18c1f0 100644 --- a/EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java +++ b/EAP-Protocol/src/test/java/com/github/aside8/eap/protocol/secs2/SecsDataItemTest.java @@ -1,5 +1,6 @@ package com.github.aside8.eap.protocol.secs2; +import com.github.aside8.eap.protocol.hsms.HsmsMessage; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import org.junit.jupiter.api.Test; @@ -70,4 +71,29 @@ void testToString() { item = SecsDataItem.binary((byte) 0); assertEquals(expected.replace("\r\n", "\n"), item.toString()); } + + @Test + void decode() { + ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(); + String buffer = "0000860b00000000000a0103b10400000000b10400007d0001010102b10400007d000103411032303235303630333136343030323137a50101410731324231313131"; + for (int i = 0; i < buffer.length(); i += 2) { + int b = Integer.parseInt(buffer.substring(i, i + 2), 16); + byteBuf.writeByte((byte)b); + } + HsmsMessage hsmsMessage = new HsmsMessage(); + hsmsMessage.decode(byteBuf); + assertEquals(6, hsmsMessage.getStream()); + assertEquals(11, hsmsMessage.getFunction()); + + SECSII secs2 = hsmsMessage.getBody(); + assertEquals(0L, secs2.get(0).getUint4(0)); + assertEquals(32000L, secs2.get(1).getUint4(0)); + SECSII properties = secs2.get(2).get(0); + + assertEquals(32000, properties.get(0).getUint4(0)); + assertEquals("2025060316400217", properties.get(1).get(0).getAscii()); + assertEquals((short) 1, properties.get(1).get(1).getUint1(0)); + assertEquals("12B1111", properties.get(1).get(2).getAscii()); + System.out.println(hsmsMessage.getBody().toString()); + } } \ No newline at end of file From 7c7511e2c44618c6ae6137c173237546e1cbbfe7 Mon Sep 17 00:00:00 2001 From: aside8 Date: Fri, 5 Dec 2025 15:18:39 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E5=AE=9E=E7=8E=B0Hsms=E7=9A=84client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aisde8/eap/connect/client/EapClient.java | 4 +- .../eap/connect/client/hsms/ClientOption.java | 16 ++ .../client/hsms/FixedLength4ByteDecoder.java | 11 ++ .../client/hsms/FixedLength4ByteEncoder.java | 13 ++ .../eap/connect/client/hsms/HsmsClient.java | 142 ++++++++++++++++++ .../client/hsms/HsmsClientLogicHandler.java | 62 ++++++++ .../client/hsms/HsmsMessageDecoder.java | 18 +++ .../client/hsms/HsmsMessageEncoder.java | 15 ++ .../eap/connect/client/hsms/TimeConfig.java | 20 +++ .../github/aside8/eap/protocol/Message.java | 2 +- .../github/aside8/eap/protocol/Traceable.java | 10 -- .../aside8/eap/protocol/hsms/HsmsMessage.java | 24 +-- 12 files changed, 302 insertions(+), 35 deletions(-) create mode 100644 EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/ClientOption.java create mode 100644 EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteDecoder.java create mode 100644 EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteEncoder.java create mode 100644 EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java create mode 100644 EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java create mode 100644 EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsMessageDecoder.java create mode 100644 EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsMessageEncoder.java create mode 100644 EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/TimeConfig.java delete mode 100644 EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Traceable.java diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/EapClient.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/EapClient.java index e18af71..7ae8d03 100644 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/EapClient.java +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/EapClient.java @@ -7,11 +7,9 @@ public interface EapClient { /** * 异步连接到服务器 - * @param host 服务器地址 - * @param port 服务器端口 * @return a Mono that completes when the connection is established or errors. */ - Mono connect(String host, int port); + Mono connect(); /** * 断开连接 diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/ClientOption.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/ClientOption.java new file mode 100644 index 0000000..2efa99d --- /dev/null +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/ClientOption.java @@ -0,0 +1,16 @@ +package com.github.aisde8.eap.connect.client.hsms; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ClientOption { + + @Builder.Default + public int connectTimeoutMillis = 5000; + + private String host; + + private int port; +} diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteDecoder.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteDecoder.java new file mode 100644 index 0000000..0a15927 --- /dev/null +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteDecoder.java @@ -0,0 +1,11 @@ +package com.github.aisde8.eap.connect.client.hsms; + +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.FixedLengthFrameDecoder; + +public class FixedLength4ByteDecoder extends FixedLengthFrameDecoder { + + public FixedLength4ByteDecoder() { + super(4); + } +} diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteEncoder.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteEncoder.java new file mode 100644 index 0000000..ef9df6c --- /dev/null +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteEncoder.java @@ -0,0 +1,13 @@ +package com.github.aisde8.eap.connect.client.hsms; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +public class FixedLength4ByteEncoder extends MessageToByteEncoder { + @Override + protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, ByteBuf byteBuf2) throws Exception { + byteBuf2.writeInt(byteBuf.readableBytes()); + byteBuf2.writeBytes(byteBuf); + } +} diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java new file mode 100644 index 0000000..67a5f35 --- /dev/null +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java @@ -0,0 +1,142 @@ +package com.github.aisde8.eap.connect.client.hsms; + +import com.github.aisde8.eap.connect.client.EapClient; +import com.github.aside8.eap.protocol.Message; +import com.github.aside8.eap.protocol.hsms.HsmsMessage; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.FixedLengthFrameDecoder; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoSink; +import reactor.core.publisher.Sinks; + +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class HsmsClient implements EapClient { + + private EventLoopGroup group; + + private volatile Channel channel; + + private final ClientOption clientOption; + + private final AtomicInteger systemBytesGenerator = new AtomicInteger(0); + + private final Map> pendingReplies = new ConcurrentHashMap<>(); + + private final Sinks.Many messageSink = Sinks.many().multicast().onBackpressureBuffer(); + + public HsmsClient(ClientOption clientOption) { + this.clientOption = clientOption; + } + + @Override + public Mono connect() { + group = new NioEventLoopGroup(); + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .option(ChannelOption.SO_KEEPALIVE, true) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, clientOption.getConnectTimeoutMillis()) + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new FixedLength4ByteDecoder()); + pipeline.addLast(new HsmsMessageDecoder()); + + pipeline.addLast(new HsmsMessageEncoder()); + pipeline.addLast(new FixedLength4ByteEncoder()); + + pipeline.addLast(new HsmsClientLogicHandler(pendingReplies, messageSink)); + } + }); + + ChannelFuture future = bootstrap.connect(clientOption.getHost(), clientOption.getPort()); + return Mono.create(sink -> future.addListener((ChannelFutureListener) f -> { + if (f.isSuccess()) { + channel = f.channel(); + sink.success(); + } else { + f.channel().close(); + sink.error(f.cause()); + } + })); + } + + @Override + public Mono disconnect() { + return Mono.create(sink -> { + if (group == null) { + sink.success(); + return; + } + + pendingReplies.forEach((id, replySink) -> replySink.error(new IllegalStateException("Client Disconnected"))); + pendingReplies.clear(); + + group.shutdownGracefully().addListener(future -> { + if (future.isSuccess()) { + sink.success(); + } else { + sink.error(future.cause()); + } + }); + }); + } + + @Override + public Flux receive() { + return messageSink.asFlux(); + } + + @Override + public Mono send(Message message) { + if (!isConnected()) { + return Mono.error(new IllegalStateException("Not connected")); + } + return Mono.create(sink -> channel.writeAndFlush(message).addListener(f -> { + if (f.isSuccess()) { + sink.success(); + } else { + sink.error(f.cause()); + } + })); + } + + @Override + public Mono sendRequest(Message request) { + if (!isConnected()) { + return Mono.error(new IllegalStateException("Not connected")); + } + if (!(request instanceof HsmsMessage hsmsRequest)) { + return Mono.error(new IllegalArgumentException("Request must be an instance of HsmsMessage")); + } + + return Mono.create(sink -> { + int systemBytes = systemBytesGenerator.incrementAndGet(); + hsmsRequest.setSystemBytes(systemBytes); + sink.onDispose(() -> pendingReplies.remove(systemBytes)); + pendingReplies.put(systemBytes, sink); + channel.writeAndFlush(hsmsRequest).addListener(future -> { + if (!future.isSuccess()) { + pendingReplies.remove(systemBytes); + sink.error(future.cause()); + } + }); + }).timeout(Duration.ofMillis(clientOption.getConnectTimeoutMillis())).cast(Message.class); + } + + @Override + public boolean isConnected() { + return channel != null && channel.isActive(); + } +} diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java new file mode 100644 index 0000000..3289ae8 --- /dev/null +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java @@ -0,0 +1,62 @@ +package com.github.aisde8.eap.connect.client.hsms; + +import com.github.aside8.eap.protocol.Message; +import com.github.aside8.eap.protocol.hsms.HsmsMessage; +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import reactor.core.publisher.MonoSink; +import reactor.core.publisher.Sinks; + +import java.util.Map; + +public class HsmsClientLogicHandler extends SimpleChannelInboundHandler { + + private final Map> pendingReplies; + + private final Sinks.Many messageSink; + + public HsmsClientLogicHandler(Map> pendingReplies, Sinks.Many messageSink) { + this.pendingReplies = pendingReplies; + this.messageSink = messageSink; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, HsmsMessage msg) throws Exception { + int systemBytes = msg.getSystemBytes(); + MonoSink sink = pendingReplies.remove(systemBytes); + if (sink != null) { + // 找到了对应的请求,完成 Mono + sink.success(msg); + } else { + // 没找到,这是一个服务器主动推送的非请求消息 + messageSink.tryEmitNext(msg); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + // 连接建立后可以发送 HSMS 握手消息,例如 SELECT.REQ (S1F13) + // 为了演示,这里不自动发送,由 DefaultEapClient.connect() 的调用者决定 + System.out.println("HSMS Channel Active: " + ctx.channel().remoteAddress()); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + System.out.println("HSMS Channel Inactive: " + ctx.channel().remoteAddress()); + // 连接断开时,所有等待中的 Mono 都应失败 + pendingReplies.forEach((id, sink) -> sink.error(new ChannelException("Channel disconnected unexpectedly."))); + pendingReplies.clear(); + super.channelInactive(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + System.err.println("HSMS Client Handler caught exception: " + cause.getMessage()); + // 异常发生时,所有等待中的 Mono 都应失败 + pendingReplies.forEach((id, sink) -> sink.error(cause)); + pendingReplies.clear(); + ctx.close(); + } +} diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsMessageDecoder.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsMessageDecoder.java new file mode 100644 index 0000000..4a4ac20 --- /dev/null +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsMessageDecoder.java @@ -0,0 +1,18 @@ +package com.github.aisde8.eap.connect.client.hsms; + +import com.github.aside8.eap.protocol.hsms.HsmsMessage; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; + +import java.util.List; + +public class HsmsMessageDecoder extends MessageToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List out) throws Exception { + HsmsMessage hsmsMessage = new HsmsMessage(); + hsmsMessage.decode(byteBuf); + out.add(hsmsMessage); + } +} diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsMessageEncoder.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsMessageEncoder.java new file mode 100644 index 0000000..f90f3ec --- /dev/null +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsMessageEncoder.java @@ -0,0 +1,15 @@ +package com.github.aisde8.eap.connect.client.hsms; + +import com.github.aside8.eap.protocol.hsms.HsmsMessage; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; + +import java.util.List; + +public class HsmsMessageEncoder extends MessageToMessageEncoder { + + @Override + protected void encode(ChannelHandlerContext channelHandlerContext, HsmsMessage hsmsMessage, List out) throws Exception { + out.add(hsmsMessage.encode(channelHandlerContext.alloc())); + } +} diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/TimeConfig.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/TimeConfig.java new file mode 100644 index 0000000..c978f3d --- /dev/null +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/TimeConfig.java @@ -0,0 +1,20 @@ +package com.github.aisde8.eap.connect.client.hsms; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class TimeConfig { + + @Builder.Default + private Integer T3 = 15; + + @Builder.Default + private Integer T1 = 10; + + @Builder.Default + private Integer T2 = 5; + + +} diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Message.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Message.java index f0c5eef..99e0b21 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Message.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Message.java @@ -6,7 +6,7 @@ * 通用消息接口/基类 * A message is a serializable object that can be encoded and decoded. */ -public interface Message extends Codec, Traceable, Serializable { +public interface Message extends Codec, Serializable { Protocol getProtocol(); } diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Traceable.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Traceable.java deleted file mode 100644 index ed8c281..0000000 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/Traceable.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.aside8.eap.protocol; - -public interface Traceable { - - /** - * 获取消息的跟踪ID - * @return 跟踪ID - */ - String getTraceId(); -} diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java index c04081b..030b37c 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java @@ -64,23 +64,11 @@ public ByteBuf encode(ByteBufAllocator allocator) { throw new IllegalStateException("HSMS header cannot be null."); } - ByteBuf headerBuf = header.encode(allocator); - ByteBuf bodyBuf = null; - if (body != null) { - bodyBuf = body.encode(allocator); - } - - int bodyLength = (bodyBuf != null) ? bodyBuf.readableBytes() : 0; - int totalLength = headerBuf.readableBytes() + bodyLength; - - ByteBuf lengthBuf = allocator.buffer(4).writeInt(totalLength); - CompositeByteBuf compositeBuf = allocator.compositeBuffer(); - compositeBuf.addComponents(true, lengthBuf, headerBuf); - if (bodyBuf != null) { - compositeBuf.addComponents(true, bodyBuf); + compositeBuf.addComponents(true, header.encode(allocator)); + if (body != null) { + compositeBuf.addComponents(true, body.encode(allocator)); } - return compositeBuf; } @@ -99,10 +87,4 @@ public void decode(ByteBuf in) { public Protocol getProtocol() { return Protocol.HSMS; } - - @Override - public String getTraceId() { - return String.valueOf(getSystemBytes()); - } - } From 9041fc3c3a526eef606fa83439e2cb4b6eac9860 Mon Sep 17 00:00:00 2001 From: aside8 Date: Fri, 5 Dec 2025 16:10:05 +0800 Subject: [PATCH 07/11] =?UTF-8?q?=E5=9C=A8=E5=8D=8F=E8=AE=AE=E5=86=85?= =?UTF-8?q?=E9=83=A8=E6=B7=BB=E5=8A=A0systemBytes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eap/connect/client/hsms/HsmsClient.java | 22 +++++++++++++------ .../aside8/eap/protocol/hsms/HsmsMessage.java | 9 ++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java index 67a5f35..ab0a27b 100644 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java @@ -8,7 +8,6 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.codec.FixedLengthFrameDecoder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoSink; @@ -103,13 +102,22 @@ public Mono send(Message message) { if (!isConnected()) { return Mono.error(new IllegalStateException("Not connected")); } - return Mono.create(sink -> channel.writeAndFlush(message).addListener(f -> { - if (f.isSuccess()) { - sink.success(); - } else { - sink.error(f.cause()); + if (!(message instanceof HsmsMessage hsmsMessage)) { + return Mono.error(new IllegalArgumentException("Request must be an instance of HsmsMessage")); + } + + return Mono.create(sink -> { + if (hsmsMessage.isRequest()) { + hsmsMessage.setSystemBytes(systemBytesGenerator.incrementAndGet()); } - })); + channel.writeAndFlush(hsmsMessage).addListener(f -> { + if (f.isSuccess()) { + sink.success(); + } else { + sink.error(f.cause()); + } + }); + }); } @Override diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java index 030b37c..f05596e 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java @@ -37,6 +37,15 @@ public int getFunction() { .orElseThrow(() -> new IllegalStateException("HSMS header cannot be null.")); } + public boolean isRequest() { + HsmsMessageType messageType = getMessageType(); + return (messageType == HsmsMessageType.DATA_MESSAGE && getFunction() % 2 == 1) + || messageType == HsmsMessageType.SELECT_REQ + || messageType == HsmsMessageType.DESELECT_REQ + || messageType == HsmsMessageType.LINKTEST_REQ + || messageType == HsmsMessageType.ABORT_REQ; + } + public HsmsMessageType getMessageType() { return Optional.ofNullable(header) .map(HsmsHeader::getStype) From 4fb8e86f39693b11c61f87c7441854d1ec9dbdc5 Mon Sep 17 00:00:00 2001 From: aside8 Date: Tue, 9 Dec 2025 16:52:58 +0800 Subject: [PATCH 08/11] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/hsms/FixedLength4ByteDecoder.java | 7 ++-- .../client/hsms/HsmsClientLogicHandler.java | 2 ++ .../connect/client/hsms/HsmsClientTest.java | 32 +++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 EAP-Connect/src/test/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientTest.java diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteDecoder.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteDecoder.java index 0a15927..7232686 100644 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteDecoder.java +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteDecoder.java @@ -1,11 +1,10 @@ package com.github.aisde8.eap.connect.client.hsms; -import io.netty.handler.codec.ByteToMessageDecoder; -import io.netty.handler.codec.FixedLengthFrameDecoder; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; -public class FixedLength4ByteDecoder extends FixedLengthFrameDecoder { +public class FixedLength4ByteDecoder extends LengthFieldBasedFrameDecoder { public FixedLength4ByteDecoder() { - super(4); + super(1024 * 1024, 0, 4, 0, 4); } } diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java index 3289ae8..5c29375 100644 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java @@ -1,7 +1,9 @@ package com.github.aisde8.eap.connect.client.hsms; import com.github.aside8.eap.protocol.Message; +import com.github.aside8.eap.protocol.hsms.HsmsHeader; import com.github.aside8.eap.protocol.hsms.HsmsMessage; +import com.github.aside8.eap.protocol.hsms.HsmsMessageType; import io.netty.channel.ChannelException; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; diff --git a/EAP-Connect/src/test/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientTest.java b/EAP-Connect/src/test/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientTest.java new file mode 100644 index 0000000..2f59f1f --- /dev/null +++ b/EAP-Connect/src/test/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientTest.java @@ -0,0 +1,32 @@ +package com.github.aisde8.eap.connect.client.hsms; + +import com.github.aside8.eap.protocol.hsms.HsmsHeader; +import com.github.aside8.eap.protocol.hsms.HsmsMessage; +import com.github.aside8.eap.protocol.hsms.HsmsMessageType; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class HsmsClientTest { + + @Test + @Disabled + void testConnect() throws InterruptedException { + HsmsClient hsmsClient = new HsmsClient(ClientOption.builder().host("172.16.57.40").port(9401).build()); + hsmsClient.receive().map(message -> (HsmsMessage) message) + .subscribe(message -> System.out.println(message.toString())); + hsmsClient.connect().block(); + HsmsMessage hsmsMessage = new HsmsMessage(); + HsmsHeader header = new HsmsHeader(); + header.setSessionId((short) 0); + header.setPtype((byte) 0); + header.setStype((byte) HsmsMessageType.SELECT_REQ.getSType()); + header.setStream((byte) 0); + header.setFunction((byte) 0); + header.setSystemBytes((byte) 0); + hsmsMessage.setHeader(header); + hsmsClient.sendRequest(hsmsMessage); + Thread.sleep(1000 * 60); + } +} \ No newline at end of file From 686cf2884004d7823b572bb59a281e3877ca88c1 Mon Sep 17 00:00:00 2001 From: aside8 Date: Tue, 9 Dec 2025 17:04:03 +0800 Subject: [PATCH 09/11] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=B8=A7=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/hsms/FixedLength4ByteEncoder.java | 13 ------------- .../aisde8/eap/connect/client/hsms/HsmsClient.java | 5 ++--- ...teDecoder.java => LengthField4FrameDecoder.java} | 4 ++-- .../client/hsms/LengthField4FrameEncoder.java | 10 ++++++++++ 4 files changed, 14 insertions(+), 18 deletions(-) delete mode 100644 EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteEncoder.java rename EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/{FixedLength4ByteDecoder.java => LengthField4FrameDecoder.java} (58%) create mode 100644 EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/LengthField4FrameEncoder.java diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteEncoder.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteEncoder.java deleted file mode 100644 index ef9df6c..0000000 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteEncoder.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.aisde8.eap.connect.client.hsms; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToByteEncoder; - -public class FixedLength4ByteEncoder extends MessageToByteEncoder { - @Override - protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, ByteBuf byteBuf2) throws Exception { - byteBuf2.writeInt(byteBuf.readableBytes()); - byteBuf2.writeBytes(byteBuf); - } -} diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java index ab0a27b..edbee2f 100644 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java @@ -49,12 +49,11 @@ public Mono connect() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(new FixedLength4ByteDecoder()); + pipeline.addLast(new LengthField4FrameDecoder()); pipeline.addLast(new HsmsMessageDecoder()); pipeline.addLast(new HsmsMessageEncoder()); - pipeline.addLast(new FixedLength4ByteEncoder()); - + pipeline.addLast(new LengthField4FrameEncoder()); pipeline.addLast(new HsmsClientLogicHandler(pendingReplies, messageSink)); } }); diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteDecoder.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/LengthField4FrameDecoder.java similarity index 58% rename from EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteDecoder.java rename to EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/LengthField4FrameDecoder.java index 7232686..701d5b1 100644 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/FixedLength4ByteDecoder.java +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/LengthField4FrameDecoder.java @@ -2,9 +2,9 @@ import io.netty.handler.codec.LengthFieldBasedFrameDecoder; -public class FixedLength4ByteDecoder extends LengthFieldBasedFrameDecoder { +public class LengthField4FrameDecoder extends LengthFieldBasedFrameDecoder { - public FixedLength4ByteDecoder() { + public LengthField4FrameDecoder() { super(1024 * 1024, 0, 4, 0, 4); } } diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/LengthField4FrameEncoder.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/LengthField4FrameEncoder.java new file mode 100644 index 0000000..eb61de9 --- /dev/null +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/LengthField4FrameEncoder.java @@ -0,0 +1,10 @@ +package com.github.aisde8.eap.connect.client.hsms; + +import io.netty.handler.codec.LengthFieldPrepender; + +public class LengthField4FrameEncoder extends LengthFieldPrepender { + + public LengthField4FrameEncoder() { + super(4, 0); + } +} From 6466cf50ecaa21899bdb82fe5e7054ce57cb402f Mon Sep 17 00:00:00 2001 From: aside8 Date: Tue, 9 Dec 2025 20:46:48 +0800 Subject: [PATCH 10/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0LinkTest=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eap/connect/client/hsms/HsmsClient.java | 2 +- .../client/hsms/HsmsClientLogicHandler.java | 41 ++++++++++++--- .../aside8/eap/protocol/hsms/HsmsMessage.java | 2 +- .../eap/protocol/hsms/HsmsMessageType.java | 6 +-- .../eap/protocol/hsms/HsmsMessages.java | 52 +++++++++++++++++++ 5 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessages.java diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java index edbee2f..c0ecc31 100644 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java @@ -54,7 +54,7 @@ public void initChannel(SocketChannel ch) throws Exception { pipeline.addLast(new HsmsMessageEncoder()); pipeline.addLast(new LengthField4FrameEncoder()); - pipeline.addLast(new HsmsClientLogicHandler(pendingReplies, messageSink)); + pipeline.addLast(new HsmsClientLogicHandler(pendingReplies, messageSink, systemBytesGenerator)); } }); diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java index 5c29375..b171e9d 100644 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientLogicHandler.java @@ -1,26 +1,40 @@ package com.github.aisde8.eap.connect.client.hsms; import com.github.aside8.eap.protocol.Message; -import com.github.aside8.eap.protocol.hsms.HsmsHeader; import com.github.aside8.eap.protocol.hsms.HsmsMessage; -import com.github.aside8.eap.protocol.hsms.HsmsMessageType; +import com.github.aside8.eap.protocol.hsms.HsmsMessages; import io.netty.channel.ChannelException; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.util.concurrent.ScheduledFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.MonoSink; import reactor.core.publisher.Sinks; import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; public class HsmsClientLogicHandler extends SimpleChannelInboundHandler { + private static final Logger logger = LoggerFactory.getLogger(HsmsClientLogicHandler.class); + private final Map> pendingReplies; private final Sinks.Many messageSink; - public HsmsClientLogicHandler(Map> pendingReplies, Sinks.Many messageSink) { + private final AtomicInteger systemBytesGenerator; + + private ScheduledFuture linkTestFuture; + + public HsmsClientLogicHandler(Map> pendingReplies, + Sinks.Many messageSink, + AtomicInteger systemBytesGenerator) { + this.pendingReplies = pendingReplies; this.messageSink = messageSink; + this.systemBytesGenerator = systemBytesGenerator; } @Override @@ -38,15 +52,25 @@ protected void channelRead0(ChannelHandlerContext ctx, HsmsMessage msg) throws E @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { - // 连接建立后可以发送 HSMS 握手消息,例如 SELECT.REQ (S1F13) - // 为了演示,这里不自动发送,由 DefaultEapClient.connect() 的调用者决定 - System.out.println("HSMS Channel Active: " + ctx.channel().remoteAddress()); + logger.info("HSMS Channel Active: {} -> {}", ctx.channel().localAddress(), ctx.channel().remoteAddress()); + + // 发送 SELECT_REQ 消息 + ctx.writeAndFlush(HsmsMessages.selectReq(systemBytesGenerator.incrementAndGet())); + + // 启动定时器发送 LINK_TEST_REQ 消息 + linkTestFuture = ctx.executor().scheduleAtFixedRate(() -> ctx.writeAndFlush(HsmsMessages.linkTestReq(systemBytesGenerator.incrementAndGet())), 3, 3, TimeUnit.SECONDS); super.channelActive(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { - System.out.println("HSMS Channel Inactive: " + ctx.channel().remoteAddress()); + logger.info("HSMS Channel Inactive: {} -> {}", ctx.channel().localAddress(), ctx.channel().remoteAddress()); + + // 取消定时器 + if (linkTestFuture != null) { + linkTestFuture.cancel(false); + } + // 连接断开时,所有等待中的 Mono 都应失败 pendingReplies.forEach((id, sink) -> sink.error(new ChannelException("Channel disconnected unexpectedly."))); pendingReplies.clear(); @@ -55,7 +79,8 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - System.err.println("HSMS Client Handler caught exception: " + cause.getMessage()); + logger.error("HSMS Client Handler caught exception: {}", cause.getMessage(), cause); + // 异常发生时,所有等待中的 Mono 都应失败 pendingReplies.forEach((id, sink) -> sink.error(cause)); pendingReplies.clear(); diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java index f05596e..80b0c3e 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessage.java @@ -42,7 +42,7 @@ public boolean isRequest() { return (messageType == HsmsMessageType.DATA_MESSAGE && getFunction() % 2 == 1) || messageType == HsmsMessageType.SELECT_REQ || messageType == HsmsMessageType.DESELECT_REQ - || messageType == HsmsMessageType.LINKTEST_REQ + || messageType == HsmsMessageType.LINK_TEST_REQ || messageType == HsmsMessageType.ABORT_REQ; } diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessageType.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessageType.java index 00d36c4..c7aae8a 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessageType.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessageType.java @@ -29,11 +29,11 @@ public enum HsmsMessageType { /** * LINKTEST.REQ (pType = 0, sType = 5) */ - LINKTEST_REQ(0, 5), + LINK_TEST_REQ(0, 5), /** - * LINKTEST.RSP (pType = 0, sType = 6) + * LINK_TEST.RSP (pType = 0, sType = 6) */ - LINKTEST_RSP(0, 6), + LINK_TEST_RSP(0, 6), /** * ABORT.REQ (pType = 0, sType = 9) */ diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessages.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessages.java new file mode 100644 index 0000000..6c3686f --- /dev/null +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessages.java @@ -0,0 +1,52 @@ +package com.github.aside8.eap.protocol.hsms; + +public class HsmsMessages { + + public static HsmsMessage selectReq(int systemBytes) { + HsmsHeader header = HsmsHeader.builder() + .sessionId((short) 0xFFFF) + .ptype((byte) HsmsMessageType.SELECT_REQ.getPType()) + .stype((byte) HsmsMessageType.SELECT_REQ.getSType()) + .stream((byte) 0x00) + .function((byte) 0x00) + .systemBytes(systemBytes) + .build(); + return new HsmsMessage(header, null); + } + + public static HsmsMessage selectResp(HsmsMessage req, SelectStatus selectStatus) { + HsmsHeader header = HsmsHeader.builder() + .sessionId(req.getHeader().getSessionId()) + .ptype((byte) HsmsMessageType.SELECT_RSP.getPType()) + .stype((byte) HsmsMessageType.SELECT_RSP.getSType()) + .stream(selectStatus.getCode()) + .function((byte) 0x00) + .systemBytes(req.getHeader().getSystemBytes()) + .build(); + return new HsmsMessage(header, null); + } + + public static HsmsMessage linkTestReq(int systemBytes) { + HsmsHeader header = HsmsHeader.builder() + .sessionId((short) 0xFFFF) + .ptype((byte) HsmsMessageType.LINK_TEST_REQ.getPType()) + .stype((byte) HsmsMessageType.LINK_TEST_REQ.getSType()) + .stream((byte) 0x00) + .function((byte) 0x00) + .systemBytes(systemBytes) + .build(); + return new HsmsMessage(header, null); + } + + public static HsmsMessage linkTestResp(HsmsMessage req) { + HsmsHeader header = HsmsHeader.builder() + .sessionId(req.getHeader().getSessionId()) + .ptype((byte) HsmsMessageType.LINK_TEST_RSP.getPType()) + .stype((byte) HsmsMessageType.LINK_TEST_RSP.getSType()) + .stream((byte) 0x00) + .function((byte) 0x00) + .systemBytes(req.getHeader().getSystemBytes()) + .build(); + return new HsmsMessage(header, null); + } +} From b628df2a44bcc7763d6880d6a0b00a35e84ed557 Mon Sep 17 00:00:00 2001 From: aside8 Date: Tue, 9 Dec 2025 22:15:03 +0800 Subject: [PATCH 11/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0ClientManager=E6=9D=A5?= =?UTF-8?q?=E5=AF=B9client=E8=BF=9B=E8=A1=8C=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eap/connect/client/EapClientManager.java | 41 +++++++++++++++++++ .../eap/connect/client/hsms/ClientOption.java | 8 ++-- .../eap/connect/client/hsms/HsmsClient.java | 18 ++++++-- .../eap/connect/client/hsms/TimeConfig.java | 27 ++++++++++-- .../connect/client/hsms/HsmsClientTest.java | 4 +- .../eap/protocol/hsms/HsmsMessages.java | 26 ++++++++++++ 6 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/EapClientManager.java diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/EapClientManager.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/EapClientManager.java new file mode 100644 index 0000000..3fbc701 --- /dev/null +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/EapClientManager.java @@ -0,0 +1,41 @@ +package com.github.aisde8.eap.connect.client; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class EapClientManager { + + private static final Logger logger = LoggerFactory.getLogger(EapClientManager.class); + + private final Map clients = new ConcurrentHashMap<>(); + + public void addClient(String host, int port, EapClient client) { + String key = generateKey(host, port); + clients.put(key, client); + logger.info("Client added: {}", key); + } + + public void removeClient(String host, int port) { + String key = generateKey(host, port); + clients.remove(key); + logger.info("Client removed: {}", key); + } + + public EapClient getClient(String host, int port) { + String key = generateKey(host, port); + return clients.get(key); + } + + public Map getAllClients() { + return clients; + } + + private String generateKey(String host, int port) { + return host + ":" + port; + } +} diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/ClientOption.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/ClientOption.java index 2efa99d..13b019c 100644 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/ClientOption.java +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/ClientOption.java @@ -7,10 +7,12 @@ @Builder public class ClientOption { - @Builder.Default - public int connectTimeoutMillis = 5000; - private String host; private int port; + + private int deviceId; + + @Builder.Default + private TimeConfig timeConfig = new TimeConfig(); } diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java index c0ecc31..82a4813 100644 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/HsmsClient.java @@ -1,6 +1,7 @@ package com.github.aisde8.eap.connect.client.hsms; import com.github.aisde8.eap.connect.client.EapClient; +import com.github.aisde8.eap.connect.client.EapClientManager; import com.github.aside8.eap.protocol.Message; import com.github.aside8.eap.protocol.hsms.HsmsMessage; import io.netty.bootstrap.Bootstrap; @@ -8,6 +9,8 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoSink; @@ -19,6 +22,7 @@ import java.util.concurrent.atomic.AtomicInteger; public class HsmsClient implements EapClient { + private static final Logger logger = LoggerFactory.getLogger(HsmsClient.class); private EventLoopGroup group; @@ -32,8 +36,11 @@ public class HsmsClient implements EapClient { private final Sinks.Many messageSink = Sinks.many().multicast().onBackpressureBuffer(); - public HsmsClient(ClientOption clientOption) { + private final EapClientManager eapClientManager; + + public HsmsClient(ClientOption clientOption, EapClientManager eapClientManager) { this.clientOption = clientOption; + this.eapClientManager = eapClientManager; } @Override @@ -44,7 +51,7 @@ public Mono connect() { .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.SO_KEEPALIVE, true) - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, clientOption.getConnectTimeoutMillis()) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, clientOption.getTimeConfig().getT4() * 1000) .handler(new ChannelInitializer() { @Override public void initChannel(SocketChannel ch) throws Exception { @@ -54,7 +61,7 @@ public void initChannel(SocketChannel ch) throws Exception { pipeline.addLast(new HsmsMessageEncoder()); pipeline.addLast(new LengthField4FrameEncoder()); - pipeline.addLast(new HsmsClientLogicHandler(pendingReplies, messageSink, systemBytesGenerator)); + pipeline.addLast(new HsmsClientLogicHandler(pendingReplies, messageSink, systemBytesGenerator)); } }); @@ -62,6 +69,7 @@ public void initChannel(SocketChannel ch) throws Exception { return Mono.create(sink -> future.addListener((ChannelFutureListener) f -> { if (f.isSuccess()) { channel = f.channel(); + eapClientManager.addClient(clientOption.getHost(), clientOption.getPort(), this); sink.success(); } else { f.channel().close(); @@ -78,6 +86,7 @@ public Mono disconnect() { return; } + eapClientManager.removeClient(clientOption.getHost(), clientOption.getPort()); pendingReplies.forEach((id, replySink) -> replySink.error(new IllegalStateException("Client Disconnected"))); pendingReplies.clear(); @@ -91,6 +100,7 @@ public Mono disconnect() { }); } + @Override public Flux receive() { return messageSink.asFlux(); @@ -139,7 +149,7 @@ public Mono sendRequest(Message request) { sink.error(future.cause()); } }); - }).timeout(Duration.ofMillis(clientOption.getConnectTimeoutMillis())).cast(Message.class); + }).timeout(Duration.ofMillis(clientOption.getTimeConfig().getT4() * 1000L)).cast(Message.class); } @Override diff --git a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/TimeConfig.java b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/TimeConfig.java index c978f3d..5ac2c66 100644 --- a/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/TimeConfig.java +++ b/EAP-Connect/src/main/java/com/github/aisde8/eap/connect/client/hsms/TimeConfig.java @@ -1,20 +1,41 @@ package com.github.aisde8.eap.connect.client.hsms; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Builder +@NoArgsConstructor +@AllArgsConstructor public class TimeConfig { + //Linktest Timer 用于周期性地发送 Linktest 消息以确认网络连接是否仍然有效。如果 T1 超时后未收到 Linktest 响应,则认为连接断开。 @Builder.Default - private Integer T3 = 15; + private int T1 = 10; + //Reply Timeout 用于等待发送的 SECS 消息的回复 (例如,对于S1F13 等 W-bit 为 1 的消息)。如果 T2 超时,认为消息未送达或丢失。用于等待发送的 SECS 消息的回复 (例如,对于 $\text{S1F13}$ 等 $\text{W}$-bit 为 1 的消息)。如果 T2 超时,认为消息未送达或丢失。 @Builder.Default - private Integer T1 = 10; + private int T2 = 5; + //Control Transaction Timeout 用于等待控制消息的确认 (例如 Select Request 消息的 Select Response 消息)。如果 T3 超时,认为连接失败。 @Builder.Default - private Integer T2 = 5; + private int T3 = 15; + //Connection Timeout 在 HSMS-SS 中,网络连接由操作系统和网络堆栈管理 。如果 T4 超时,认为连接失败。 + @Builder.Default + private int T4 = 30; + + //Connect Modulo (连接模数)定义了在尝试建立连接时,重复发送 Select Request 消息的间隔时间。 + @Builder.Default + private int T5 = 30; + //Control Transaction Retries 定义了在等待控制消息确认时,最大允许的重试次数。如果超过 T6 次重试仍未收到确认,认为连接失败。 + @Builder.Default + private int T6 = 30; + + //Not Selected Timeout 连接建立后,等待 Select Request 或 Select Response 消息的最长等待时间 用于确保在连接建立后能迅速进入 $\text{Selected}$ 状态。 + @Builder.Default + private int T7 = 30; } diff --git a/EAP-Connect/src/test/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientTest.java b/EAP-Connect/src/test/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientTest.java index 2f59f1f..e8878df 100644 --- a/EAP-Connect/src/test/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientTest.java +++ b/EAP-Connect/src/test/java/com/github/aisde8/eap/connect/client/hsms/HsmsClientTest.java @@ -1,5 +1,6 @@ package com.github.aisde8.eap.connect.client.hsms; +import com.github.aisde8.eap.connect.client.EapClientManager; import com.github.aside8.eap.protocol.hsms.HsmsHeader; import com.github.aside8.eap.protocol.hsms.HsmsMessage; import com.github.aside8.eap.protocol.hsms.HsmsMessageType; @@ -13,7 +14,8 @@ class HsmsClientTest { @Test @Disabled void testConnect() throws InterruptedException { - HsmsClient hsmsClient = new HsmsClient(ClientOption.builder().host("172.16.57.40").port(9401).build()); + EapClientManager eapClientManager = new EapClientManager(); + HsmsClient hsmsClient = new HsmsClient(ClientOption.builder().host("172.16.57.40").port(9401).build(), eapClientManager); hsmsClient.receive().map(message -> (HsmsMessage) message) .subscribe(message -> System.out.println(message.toString())); hsmsClient.connect().block(); diff --git a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessages.java b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessages.java index 6c3686f..853b3f9 100644 --- a/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessages.java +++ b/EAP-Protocol/src/main/java/com/github/aside8/eap/protocol/hsms/HsmsMessages.java @@ -1,7 +1,33 @@ package com.github.aside8.eap.protocol.hsms; +import com.github.aside8.eap.protocol.secs2.SECSII; + public class HsmsMessages { + public static HsmsMessage dataReq(int deviceId, int stream, int function, int systemBytes, SECSII data) { + HsmsHeader header = HsmsHeader.builder() + .sessionId((short) deviceId) + .ptype((byte) HsmsMessageType.DATA_MESSAGE.getPType()) + .stype((byte) HsmsMessageType.DATA_MESSAGE.getSType()) + .stream((byte) stream) + .function((byte) function) + .systemBytes(systemBytes) + .build(); + return new HsmsMessage(header, data); + } + + public static HsmsMessage dataRes(HsmsMessage req, SECSII data) { + HsmsHeader header = HsmsHeader.builder() + .sessionId(req.getHeader().getSessionId()) + .ptype((byte) HsmsMessageType.DATA_MESSAGE.getPType()) + .stype((byte) HsmsMessageType.DATA_MESSAGE.getSType()) + .stream(req.getHeader().getStream()) + .function((byte) (req.getHeader().getFunction() + 1)) + .systemBytes(req.getHeader().getSystemBytes()) + .build(); + return new HsmsMessage(header, data); + } + public static HsmsMessage selectReq(int systemBytes) { HsmsHeader header = HsmsHeader.builder() .sessionId((short) 0xFFFF)