diff --git a/pom.xml b/pom.xml
index f8b6b7c..f787edd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
Your chosen policy: BOTH transports must be connected simultaneously.
+ *Connectivity rules: + *
If UDP is enabled and TCP is disabled, the client sends {@link UdpHelloPacket} to obtain + * a {@link ConnectionIdPacket} + {@link ProtocolRequirementsPacket} over UDP.
+ * + * @return true if started, false if already started + * @throws Exception on connection/handshake errors + */ + public synchronized boolean connect() throws Exception { + if (lifecycleUp.get()) return false; - public ClientID getClientId() { - return clientId; - } + // Reset state + connectionId = null; + serverRequiredProtocols = null; - public void setClientId(ClientID clientId) { - if (this.clientId == null) { - this.clientId = Objects.requireNonNull(clientId, "clientId"); - } - } + tcpConnected = false; + udpConnected = false; + udpBound = false; + wasFullyConnected = false; - public boolean isTcpConnected() { - Thread t = tcpReceiveThread; - return tcpSocket != null && tcpSocket.isConnected() && !tcpSocket.isClosed() - && t != null && t.isAlive() && !t.isInterrupted(); - } - - public boolean isUdpConnected() { - return dtlsEndpoint != null && udpChannel != null && udpChannel.isOpen(); - } - - public boolean isFullyConnected() { - return isTcpConnected() && isUdpConnected() && clientId != null; - } - - public synchronized boolean connect() throws ConnectException { - if (isFullyConnected()) return false; - - fullyConnectedEventFired.set(false); + // Mark lifecycle up early so checkFullyConnectedState can succeed during connect. + lifecycleUp.set(true); try { - connectTcp(); - eventManager.executeEvent(new ClientConnectedEvent(this, Transport.TCP)); + if (enabledProtocols.contains(NetworkProtocol.TCP)) { + connectTcpTls(); + } - waitForClientId(); + if (enabledProtocols.contains(NetworkProtocol.UDP)) { + connectUdpDtls(); - connectDtls(); - // UDP is considered connected after DTLS handshake is complete (bind follows immediately) - eventManager.executeEvent(new ClientConnectedEvent(this, Transport.UDP)); + // UDP-only bootstrap: ask server to create/assign an id over UDP. + if (!enabledProtocols.contains(NetworkProtocol.TCP)) { + sendPacket(new UdpHelloPacket(), NetworkProtocol.UDP); + } + } - bindUdpToClientId(); - - fireFullyConnectedIfReady(); return true; - } catch (ConnectException e) { - disconnect(); - throw e; } catch (Exception e) { disconnect(); - throw new ConnectException("Failed to connect: " + e.getMessage()); - } - } - - private void connectTcp() throws Exception { - if (tlsSocketFactory == null) throw new ConnectException("TLS socket factory not set."); - - if (proxy != null) { - Socket raw = new Socket(proxy); - raw.connect(new InetSocketAddress(host, tcpPort), timeout); - tcpSocket = (SSLSocket) tlsSocketFactory.createSocket(raw, host, tcpPort, true); - } else { - tcpSocket = (SSLSocket) tlsSocketFactory.createSocket(host, tcpPort); - } - - if (tlsParameters != null) { - tcpSocket.setSSLParameters(tlsParameters); - } else { - SSLParameters p = tcpSocket.getSSLParameters(); - p.setProtocols(new String[]{"TLSv1.3"}); - tcpSocket.setSSLParameters(p); - } - - tcpSocket.setTcpNoDelay(true); - tcpSocket.setSoTimeout(timeout); - tcpSocket.startHandshake(); - - tcpOut = new ObjectOutputStream(tcpSocket.getOutputStream()); - tcpIn = new ObjectInputStream(tcpSocket.getInputStream()); - - tcpReceiveThread = new Thread(this::tcpReceive, "NetworkClient-TCP-Receive"); - tcpReceiveThread.start(); - } - - private void waitForClientId() throws ConnectException { - long start = System.currentTimeMillis(); - while (clientId == null) { - if (!isTcpConnected()) throw new ConnectException("TCP disconnected before ClientID was assigned."); - if (System.currentTimeMillis() - start > timeout) - throw new ConnectException("Timed out waiting for ClientID over TCP."); - Thread.onSpinWait(); - } - } - - private void connectDtls() throws Exception { - if (dtlsContext == null) throw new ConnectException("DTLS context not set."); - - udpRemote = new InetSocketAddress(host, udpPort); - - udpChannel = DatagramChannel.open(); - udpChannel.configureBlocking(false); - udpChannel.connect(udpRemote); - - dtlsEndpoint = new DtlsEndpoint( - udpChannel, - dtlsContext, - true, - 1400, - timeout, - this::onDtlsApplicationData - ); - - dtlsEndpoint.handshake(udpRemote); - - udpThread = new Thread(this::udpLoop, "NetworkClient-UDP-DTLS"); - udpThread.start(); - } - - private void bindUdpToClientId() throws IOException, ClassNotFoundException { - sendPacket(new UdpBindPacket(clientId), Transport.UDP); - } - - public boolean sendPacket(Packet packet, Transport transport) throws IOException, ClassNotFoundException { - Objects.requireNonNull(packet, "packet"); - Objects.requireNonNull(transport, "transport"); - - return switch (transport) { - case TCP -> sendTcp(packet); - case UDP -> sendUdp(packet); - }; - } - - private boolean sendTcp(Packet packet) throws IOException, ClassNotFoundException { - if (!isTcpConnected()) return false; - - boolean sent = packetHandler.sendPacket(packet, tcpOut); - if (sent) eventManager.executeEvent(new C_PacketSendEvent(this, packet, Transport.TCP)); - else eventManager.executeEvent(new C_PacketSendFailedEvent(this, packet, Transport.TCP)); - return sent; - } - - private boolean sendUdp(Packet packet) throws IOException, ClassNotFoundException { - if (dtlsEndpoint == null || udpRemote == null) return false; - - ByteBuffer encoded = UdpPacketCodec.encode(packetHandler, packet); - dtlsEndpoint.sendApplication(udpRemote, encoded); - - eventManager.executeEvent(new C_PacketSendEvent(this, packet, Transport.UDP)); - return true; - } - - private void udpLoop() { - try { - while (udpChannel != null && udpChannel.isOpen() && !Thread.currentThread().isInterrupted()) { - DtlsEndpoint endpoint = dtlsEndpoint; - if (endpoint != null) endpoint.poll(); - Thread.onSpinWait(); - } - } catch (Exception ignored) { - disconnect(); - } - } - - private void onDtlsApplicationData(java.net.SocketAddress remote, ByteBuffer data) { - try { - Packet decoded = UdpPacketCodec.decodeAndHandle(packetHandler, data); - if (decoded == null) { - eventManager.executeEvent(new C_UnknownObjectReceivedEvent(this, data, Transport.UDP)); - return; - } - eventManager.executeEvent(new C_PacketReceivedEvent(this, decoded, Transport.UDP)); - } catch (Exception ignored) { - } - } - - private void tcpReceive() { - try { - while (isTcpConnected()) { - Object received = tcpIn.readObject(); - handleTcpReceived(received); - } - } catch (Exception ignored) { - disconnect(); - } - } - - private void handleTcpReceived(Object received) throws IOException, ClassNotFoundException { - if (received instanceof Integer id) { - if (packetHandler.isPacketIDRegistered(id)) { - Packet packet = packetHandler.getPacketByID(id); - boolean handled = packetHandler.handlePacket(id, packet, tcpIn); - - if (handled) eventManager.executeEvent(new C_PacketReceivedEvent(this, packet, Transport.TCP)); - else eventManager.executeEvent(new C_PacketReceivedFailedEvent(this, packet, Transport.TCP)); - } else { - eventManager.executeEvent(new C_UnknownObjectReceivedEvent(this, received, Transport.TCP)); - } - } else { - eventManager.executeEvent(new C_UnknownObjectReceivedEvent(this, received, Transport.TCP)); - } - - fireFullyConnectedIfReady(); - } - - private void fireFullyConnectedIfReady() { - if (isFullyConnected() && fullyConnectedEventFired.compareAndSet(false, true)) { - eventManager.executeEvent(new ClientFullyConnectedEvent(this, new Transport[]{Transport.TCP, Transport.UDP})); + throw e; } } + /** + * Disconnects the client and stops background threads. + * + * @return true if a disconnect happened, false if it was already down + */ public synchronized boolean disconnect() { - boolean wasTcpConnected = isTcpConnected(); - boolean wasUdpConnected = isUdpConnected(); - boolean wasFullyConnected = isFullyConnected(); + boolean wasUp = lifecycleUp.getAndSet(false); - if (!wasTcpConnected && !wasUdpConnected) { - cleanup(); - return false; + Thread t1 = tcpThread; + Thread t2 = udpThread; + if (t1 != null) t1.interrupt(); + if (t2 != null) t2.interrupt(); + + try { + if (tcpOut != null) tcpOut.close(); + } catch (Exception ignored) { + } + try { + if (tcpIn != null) tcpIn.close(); + } catch (Exception ignored) { + } + try { + if (tcpSocket != null) tcpSocket.close(); + } catch (Exception ignored) { } try { - Thread t1 = tcpReceiveThread; - Thread t2 = udpThread; - if (t1 != null) t1.interrupt(); - if (t2 != null) t2.interrupt(); - - if (tcpOut != null) tcpOut.close(); - if (tcpIn != null) tcpIn.close(); - if (tcpSocket != null) tcpSocket.close(); - if (udpChannel != null) udpChannel.close(); - } catch (IOException ignored) { + } catch (Exception ignored) { } - // Transport-specific disconnect events - if (wasUdpConnected) { - eventManager.executeEvent(new ClientDisconnectedEvent(this, Transport.UDP)); - } - if (wasTcpConnected) { - eventManager.executeEvent(new ClientDisconnectedEvent(this, Transport.TCP)); - } + boolean tcpWas = tcpConnected; + boolean udpWas = udpConnected; - cleanup(); + tcpConnected = false; + udpConnected = false; + udpBound = false; - if (wasFullyConnected) { - eventManager.executeEvent(new ClientFullyDisconnectedEvent(this, new Transport[]{Transport.TCP, Transport.UDP})); - } - - return true; - } - - private void cleanup() { tcpSocket = null; tcpOut = null; tcpIn = null; + tcpThread = null; udpChannel = null; dtlsEndpoint = null; udpRemote = null; - - tcpReceiveThread = null; udpThread = null; - clientId = null; - fullyConnectedEventFired.set(false); + connectionId = null; + serverRequiredProtocols = null; + + if (tcpWas) { + fireDisconnected(NetworkProtocol.TCP); + } + if (udpWas) { + fireDisconnected(NetworkProtocol.UDP); + } + + if (wasFullyConnected) { + wasFullyConnected = false; + eventManager.executeEvent(new ClientFullyDisconnectedEvent(this, requiredProtocolsForClientView())); + } + + return wasUp; } - private void logInfo(String msg) { - if (logger != null) logger.info(msg); - else System.out.println(msg); + /** + * Returns the server-assigned connection id, if already received. + * + * @return connection id or null + */ + public UUID connectionId() { + return connectionId; + } + + /** + * Returns the protocols the client enabled locally. + * + * @return enabled protocols + */ + public EnumSetNote: UDP packets can be sent before UDP is "bound" because the bind/hello flow itself + * is transported via UDP.
+ * + * @param packet packet + * @param protocol protocol to use + * @return true if sent, false if not possible (not up / protocol not enabled / not ready) + * @throws Exception on errors + */ + public boolean sendPacket(Packet packet, NetworkProtocol protocol) throws Exception { + Objects.requireNonNull(packet, "packet"); + Objects.requireNonNull(protocol, "protocol"); + + if (!lifecycleUp.get()) return false; + if (!enabledProtocols.contains(protocol)) return false; + + try { + boolean sent = switch (protocol) { + case TCP -> sendTcp(packet); + case UDP -> sendUdp(packet); + }; + + if (sent) { + eventManager.executeEvent(new C_PacketSendEvent(this, packet, protocol)); + } else { + eventManager.executeEvent(new C_PacketSendFailedEvent( + this, packet, protocol, new IllegalStateException("Packet not sent") + )); + } + + return sent; + } catch (Exception e) { + eventManager.executeEvent(new C_PacketSendFailedEvent(this, packet, protocol, e)); + throw e; + } + } + + private void connectTcpTls() throws Exception { + InetSocketAddress addr = tcpEndpoint.resolveBest(); + + SSLSocket socket = (SSLSocket) tlsSocketFactory.createSocket(addr.getAddress(), addr.getPort()); + socket.setTcpNoDelay(true); + socket.setSoTimeout(timeoutMillis); + + if (tlsParameters != null) { + socket.setSSLParameters(tlsParameters); + } else { + SSLParameters p = socket.getSSLParameters(); + p.setProtocols(new String[]{"TLSv1.3"}); + socket.setSSLParameters(p); + } + + socket.startHandshake(); + + ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); + ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); + + this.tcpSocket = socket; + this.tcpOut = out; + this.tcpIn = in; + + this.tcpConnected = true; + eventManager.executeEvent(new ClientConnectedEvent(this, NetworkProtocol.TCP)); + checkFullyConnectedState(); + + this.tcpThread = new Thread(this::tcpReceiveLoop, "SecureNetworkClient-TCP-Receive"); + this.tcpThread.start(); + } + + private void connectUdpDtls() throws Exception { + InetSocketAddress addr = udpEndpoint.resolveBest(); + + DatagramChannel ch = DatagramChannel.open(); + ch.configureBlocking(false); + ch.connect(addr); + + DtlsEndpoint endpoint = new DtlsEndpoint( + ch, + dtlsContext, + true, + mtu, + timeoutMillis, + ClientAuthMode.OPTIONAL, // ignored for clientMode=true + this::onDtlsApplicationData + ); + + endpoint.handshake(addr); + + this.udpChannel = ch; + this.dtlsEndpoint = endpoint; + this.udpRemote = addr; + + this.udpConnected = true; + eventManager.executeEvent(new ClientConnectedEvent(this, NetworkProtocol.UDP)); + checkFullyConnectedState(); + + this.udpThread = new Thread(this::udpPollLoop, "SecureNetworkClient-UDP-DTLS"); + this.udpThread.start(); + } + + private boolean sendTcp(Packet packet) throws Exception { + if (!tcpConnected) return false; + + ObjectOutputStream out = tcpOut; + SSLSocket s = tcpSocket; + if (out == null || s == null || s.isClosed()) return false; + + codec.sendToStream(packet, out); + return true; + } + + private boolean sendUdp(Packet packet) throws Exception { + if (!udpConnected) return false; + + DtlsEndpoint endpoint = dtlsEndpoint; + InetSocketAddress remote = udpRemote; + if (endpoint == null || remote == null) return false; + + ByteBuffer buf = codec.encodeToBuffer(packet); + endpoint.sendApplication(remote, buf); + return true; + } + + private void tcpReceiveLoop() { + try { + while (!Thread.currentThread().isInterrupted() && tcpConnected) { + SSLSocket s = tcpSocket; + ObjectInputStream in = tcpIn; + if (s == null || in == null || s.isClosed()) break; + + Packet packet = codec.receiveFromStream(in); + if (packet == null) continue; + + if (processServerControlPacket(packet, NetworkProtocol.TCP)) { + continue; + } + + eventManager.executeEvent(new C_PacketReceivedEvent(this, packet, NetworkProtocol.TCP)); + } + } catch (Exception e) { + eventManager.executeEvent(new C_PacketReceivedFailedEvent(this, null, NetworkProtocol.TCP, e)); + } finally { + boolean tcpWas = tcpConnected; + tcpConnected = false; + + if (tcpWas) { + fireDisconnected(NetworkProtocol.TCP); + } + + checkFullyDisconnectedIfNeeded(); + } + } + + private void udpPollLoop() { + try { + while (!Thread.currentThread().isInterrupted() && udpConnected) { + DatagramChannel ch = udpChannel; + DtlsEndpoint endpoint = dtlsEndpoint; + if (ch == null || endpoint == null || !ch.isOpen()) break; + + endpoint.poll(); + Thread.onSpinWait(); + } + } catch (Exception e) { + eventManager.executeEvent(new C_PacketReceivedFailedEvent(this, null, NetworkProtocol.UDP, e)); + } finally { + boolean udpWas = udpConnected; + + udpConnected = false; + udpBound = false; + + if (udpWas) { + fireDisconnected(NetworkProtocol.UDP); + } + + checkFullyDisconnectedIfNeeded(); + } + } + + private void onDtlsApplicationData(SocketAddress remote, ByteBuffer data) { + try { + Packet packet = codec.decodeFromBuffer(data); + + if (processServerControlPacket(packet, NetworkProtocol.UDP)) { + return; + } + + eventManager.executeEvent(new C_PacketReceivedEvent(this, packet, NetworkProtocol.UDP)); + } catch (Exception e) { + eventManager.executeEvent(new C_PacketReceivedFailedEvent(this, null, NetworkProtocol.UDP, e)); + } + } + + /** + * Processes connection/bootstrap/control packets that affect connectivity state. + * + * @param packet received packet + * @param sourceProtocol source protocol + * @return true if handled (should not be forwarded as application packet) + */ + private boolean processServerControlPacket(Packet packet, NetworkProtocol sourceProtocol) { + if (packet instanceof ConnectionIdPacket idPacket) { + this.connectionId = idPacket.connectionId(); + + // UDP-only bootstrap: once server assigned an id via UDP, the remote is already bound. + if (sourceProtocol == NetworkProtocol.UDP && !enabledProtocols.contains(NetworkProtocol.TCP)) { + this.udpBound = true; + } + + checkFullyConnectedState(); + + // If TCP delivered the id and UDP is enabled, attempt bind now. + if (sourceProtocol == NetworkProtocol.TCP && enabledProtocols.contains(NetworkProtocol.UDP)) { + tryAutoBindUdp(); + } + + return true; + } + + if (packet instanceof ProtocolRequirementsPacket reqPacket) { + this.serverRequiredProtocols = EnumSet.copyOf(reqPacket.requiredProtocols()); + checkFullyConnectedState(); + return true; + } + + if (packet instanceof UdpBindAckPacket ackPacket) { + UUID id = connectionId; + if (id != null && id.equals(ackPacket.connectionId())) { + this.udpBound = true; + checkFullyConnectedState(); + } + return true; + } + + return false; + } + + private void tryAutoBindUdp() { + UUID id = connectionId; + if (id == null) return; + if (!udpConnected) return; + + try { + // Bind request goes over UDP/DTLS. Server responds with UdpBindAckPacket. + sendPacket(new UdpBindPacket(id), NetworkProtocol.UDP); + } catch (Exception e) { + eventManager.executeEvent(new C_PacketSendFailedEvent(this, new UdpBindPacket(id), NetworkProtocol.UDP, e)); + } + } + + private void fireDisconnected(NetworkProtocol protocol) { + eventManager.executeEvent(new ClientDisconnectedEvent(this, protocol)); + } + + private EnumSetNote: In v2 the default transport is {@link dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketCodec} + * based and typically produces {@link Packet}. + * This event exists for backwards compatibility and for custom codecs/handlers.
*/ public final class C_UnknownObjectReceivedEvent extends Event { - private final NetworkClient networkClient; + private final NetworkClient client; private final Object received; - private final Transport transport; + private final NetworkProtocol protocol; /** * Creates a new unknown object received event. * - * @param networkClient client instance - * @param received received object - * @param transport transport the object was received on + * @param client client instance + * @param received received object + * @param protocol protocol the object was received on */ - public C_UnknownObjectReceivedEvent(NetworkClient networkClient, Object received, Transport transport) { - this.networkClient = networkClient; - this.received = received; - this.transport = transport; + public C_UnknownObjectReceivedEvent(NetworkClient client, Object received, NetworkProtocol protocol) { + this.client = Objects.requireNonNull(client, "client"); + this.received = Objects.requireNonNull(received, "received"); + this.protocol = Objects.requireNonNull(protocol, "protocol"); } - public NetworkClient getNetworkClient() { - return networkClient; + /** + * Returns the client instance. + * + * @return client + */ + public NetworkClient getClient() { + return client; } + /** + * Returns the received object. + * + * @return received object + */ public Object getReceived() { return received; } - public Transport getTransport() { - return transport; + /** + * Returns the protocol the object was received on. + * + * @return protocol + */ + public NetworkProtocol getProtocol() { + return protocol; } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendEvent.java index a433072..7867627 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendEvent.java @@ -6,52 +6,71 @@ * See LICENSE-File if exists */ -/* - * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved - * - * You are unauthorized to remove this copyright. - * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ - * See LICENSE-File if exists - */ - package dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.send; import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient; import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; + +import java.util.Objects; /** - * Fired when a packet was sent by the client on a specific transport. + * Fired when a packet was successfully sent by the client on a specific protocol. + * + *v2 mapping: + *
This event is transport-specific: + *
Protocol-specific: *
For your setup this means: TCP (TLS) connected, ClientID received and UDP (DTLS) bound.
+ *In v2 this typically means: + *
This event is transport-specific: + *
Protocol-specific: *
For your policy (TCP+UDP required) this means: - * previously: TCP+UDP connected and ClientID present, - * now: not fully connected anymore (both transports are not satisfying the requirement).
+ * Fired when the client previously satisfied its required protocols and then stopped satisfying them. */ public final class ClientFullyDisconnectedEvent extends Event { private final NetworkClient client; - private final Transport[] requiredTransports; + private final EnumSetPackets are encoded as: + *
+ * [int packetId][payload bytes...] + *+ * Payload is written/read by the packet itself through Java serialization streams. + */ public abstract class Packet { - private final int id; + private final int packetId; - public Packet(int id) { - this.id = id; + /** + * Creates a new packet. + * + * @param packetId unique packet id + */ + protected Packet(int packetId) { + this.packetId = packetId; } - public final int getPacketID() { - return id; + /** + * Returns the packet id. + * + * @return id + */ + public final int packetId() { + return packetId; } - public abstract void write(PacketHandler packetHandler, ObjectOutputStream outputStream) throws IOException, ClassNotFoundException; + /** + * Writes payload fields to the stream. + * + * @param out output stream + * @throws IOException on I/O errors + */ + public abstract void write(ObjectOutputStream out) throws IOException; - public abstract void read(PacketHandler packetHandler, ObjectInputStream outputStream) throws IOException, ClassNotFoundException; -} \ No newline at end of file + /** + * Reads payload fields from the stream. + * + * @param in input stream + * @throws IOException on I/O errors + * @throws ClassNotFoundException on deserialization errors + */ + public abstract void read(ObjectInputStream in) throws IOException, ClassNotFoundException; +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketCodec.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketCodec.java new file mode 100644 index 0000000..830a29b --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketCodec.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ + * See LICENSE-File if exists + */ + +package dev.unlegitdqrk.unlegitlibrary.network.system.packets; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.ByteBuffer; +import java.util.Objects; + +/** + * Encodes and decodes {@link Packet} instances for TCP (stream) and UDP (datagram). + * + *
Wire format (transport-agnostic): + *
+ * [int packetId][ObjectStream payload...] + *+ */ +public final class PacketCodec { + + private final PacketRegistry registry; + + /** + * Creates a new codec. + * + * @param registry packet registry + */ + public PacketCodec(PacketRegistry registry) { + this.registry = Objects.requireNonNull(registry, "registry"); + } + + /** + * Encodes a packet into a byte buffer suitable for UDP/DTLS sending. + * + * @param packet packet to encode + * @return encoded buffer (position=0, limit=length) + * @throws IOException on serialization errors + */ + public ByteBuffer encodeToBuffer(Packet packet) throws IOException { + Objects.requireNonNull(packet, "packet"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(256); + try (DataOutputStream dos = new DataOutputStream(baos)) { + dos.writeInt(packet.packetId()); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + packet.write(oos); + oos.flush(); + } + } + return ByteBuffer.wrap(baos.toByteArray()); + } + + /** + * Decodes a packet from a received datagram buffer. + * + * @param buffer received buffer (position..limit) + * @return decoded packet + * @throws IOException on I/O errors + * @throws ClassNotFoundException on deserialization errors + */ + public Packet decodeFromBuffer(ByteBuffer buffer) throws IOException, ClassNotFoundException { + Objects.requireNonNull(buffer, "buffer"); + + ByteArrayInputStream bais = + new ByteArrayInputStream(buffer.array(), buffer.position(), buffer.remaining()); + + int packetId; + try (DataInputStream dis = new DataInputStream(bais)) { + packetId = dis.readInt(); + } + + Packet packet = registry.create(packetId); + + try (ObjectInputStream ois = new ObjectInputStream(bais)) { + packet.read(ois); + } + + return packet; + } + + /** + * Sends a packet over a TCP/TLS stream. + * + * @param packet packet + * @param out output stream + * @throws IOException on I/O errors + */ + public void sendToStream(Packet packet, ObjectOutputStream out) throws IOException { + Objects.requireNonNull(packet, "packet"); + Objects.requireNonNull(out, "out"); + + out.writeInt(packet.packetId()); + packet.write(out); + out.flush(); + } + + /** + * Receives a packet from a TCP/TLS stream. + * + * @param in input stream + * @return decoded packet + * @throws IOException on I/O errors + * @throws ClassNotFoundException on deserialization errors + */ + public Packet receiveFromStream(ObjectInputStream in) throws IOException, ClassNotFoundException { + Objects.requireNonNull(in, "in"); + + int packetId = in.readInt(); + Packet packet = registry.create(packetId); + packet.read(in); + return packet; + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketHandler.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketHandler.java deleted file mode 100644 index 5214013..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketHandler.java +++ /dev/null @@ -1,71 +0,0 @@ -package dev.unlegitdqrk.unlegitlibrary.network.system.packets; - -import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.NetworkServer; -import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.util.HashMap; -import java.util.Map; - -public final class PacketHandler extends DefaultMethodsOverrider { - - private final Map
This avoids reusing the same packet instance across threads/connections.
+ */ +public final class PacketRegistry { + + private final MapThis id is used to bind the DTLS/UDP endpoint to the already authenticated TCP/TLS connection + * via {@link UdpBindPacket}.
+ */ +public final class ConnectionIdPacket extends Packet { + + private UUID connectionId; + + /** + * Creates an empty packet instance for decoding. + */ + public ConnectionIdPacket() { + super(1); + } + + /** + * Creates a packet instance with a connection id for sending. + * + * @param connectionId connection id (must not be null) + */ + public ConnectionIdPacket(UUID connectionId) { + this(); + this.connectionId = Objects.requireNonNull(connectionId, "connectionId"); + } + + /** + * Returns the assigned connection id. + * + * @return connection id + */ + public UUID connectionId() { + return connectionId; + } + + @Override + public void write(ObjectOutputStream out) throws IOException { + if (connectionId == null) { + throw new IOException("ConnectionIdPacket.connectionId is null"); + } + out.writeLong(connectionId.getMostSignificantBits()); + out.writeLong(connectionId.getLeastSignificantBits()); + } + + @Override + public void read(ObjectInputStream in) throws IOException { + long msb = in.readLong(); + long lsb = in.readLong(); + this.connectionId = new UUID(msb, lsb); + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ProtocolRequirementsPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ProtocolRequirementsPacket.java new file mode 100644 index 0000000..7d6a0d9 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ProtocolRequirementsPacket.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ + * See LICENSE-File if exists + */ + +package dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl; + +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.EnumSet; +import java.util.Objects; + +/** + * Server -> client packet containing the server-required protocol set. + * + *This packet allows the client to evaluate "fully connected" based on the server policy.
+ */ +public final class ProtocolRequirementsPacket extends Packet { + + private EnumSetThe client must only consider UDP "bound/ready" after receiving this ACK with the matching connection id.
+ * + *IMPORTANT: Ensure {@link #PACKET_ID} does not conflict with your other packet ids and + * register it in {@code PacketRegistry}.
+ */ +public final class UdpBindAckPacket extends Packet { + + private UUID connectionId; + + /** + * Constructs an empty packet for deserialization. + */ + public UdpBindAckPacket() { + super(3); + } + + /** + * Constructs the ACK packet. + * + * @param connectionId connection id being acknowledged + */ + public UdpBindAckPacket(UUID connectionId) { + super(3); + this.connectionId = Objects.requireNonNull(connectionId, "connectionId"); + } + + /** + * Returns the acknowledged connection id. + * + * @return connection id + */ + public UUID connectionId() { + return connectionId; + } + + @Override + public void write(ObjectOutputStream out) throws IOException { + Objects.requireNonNull(out, "out"); + out.writeLong(connectionId.getMostSignificantBits()); + out.writeLong(connectionId.getLeastSignificantBits()); + } + + @Override + public void read(ObjectInputStream in) throws IOException { + Objects.requireNonNull(in, "in"); + long msb = in.readLong(); + long lsb = in.readLong(); + this.connectionId = new UUID(msb, lsb); + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindPacket.java index 9bb5113..6bea518 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindPacket.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindPacket.java @@ -9,54 +9,67 @@ package dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl; import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; -import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientID; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.util.Objects; import java.util.UUID; /** - * Binds a DTLS/UDP endpoint to the already assigned {@link ClientID}. + * Client -> Server packet sent over UDP/DTLS to bind the DTLS remote address to an existing TCP/TLS connection. * - *Flow: TCP connect -> receive ClientID -> DTLS handshake -> send UdpBindPacket(clientId)
+ *Flow: + *
The client sends this packet over UDP/DTLS to request the server to create/assign a connection id + * and return bootstrap information (e.g. ConnectionIdPacket + ProtocolRequirementsPacket) over UDP.
+ */ +public final class UdpHelloPacket extends Packet { + + /** + * Constructs an empty packet for serialization/deserialization. + */ + public UdpHelloPacket() { + super(5); + } + + @Override + public void write(ObjectOutputStream out) throws IOException { + Objects.requireNonNull(out, "out"); + // No payload. + } + + @Override + public void read(ObjectInputStream in) throws IOException { + Objects.requireNonNull(in, "in"); + // No payload. + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ClientConnection.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ClientConnection.java new file mode 100644 index 0000000..b692fb7 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ClientConnection.java @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://github.com/UnlegitDqrk + * See LICENSE-File if exists + */ + +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ + * See LICENSE-File if exists + */ + +package dev.unlegitdqrk.unlegitlibrary.network.system.server; + +import dev.unlegitdqrk.unlegitlibrary.event.EventManager; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketCodec; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.send.S_PacketSendEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.send.S_PacketSendFailedEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.DtlsEndpoint; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; + +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.util.EnumSet; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Represents one connected client on the server side, with optional TCP/TLS and/or UDP/DTLS channels. + * + *Connection usability is defined by {@link ServerProtocolMode#required()}.
+ */ +public final class ClientConnection { + + private final UUID connectionId; + private final PacketCodec codec; + private final ServerProtocolMode protocolMode; + private final EventManager eventManager; + + // TCP (TLS) - optional + private final SSLSocket tcpSocket; + private final ObjectOutputStream tcpOut; + private final ObjectInputStream tcpIn; + + // UDP (DTLS) - optional + private volatile SocketAddress udpRemote; + private volatile DtlsEndpoint.DtlsSession udpSession; + private volatile DtlsEndpoint dtlsEndpoint; + private volatile DatagramChannel udpChannel; + private volatile boolean udpBound; + + private final AtomicBoolean closed = new AtomicBoolean(false); + + /** + * Creates a new connection with a predefined connection id. + * + * @param connectionId server-side unique connection id + * @param codec packet codec + * @param protocolMode server protocol mode (supported/required) + * @param eventManager event manager used to dispatch server events + * @param tcpSocket TLS socket (nullable for UDP-only) + * @param tcpOut TCP output stream (nullable for UDP-only) + * @param tcpIn TCP input stream (nullable for UDP-only) + */ + public ClientConnection( + UUID connectionId, + PacketCodec codec, + ServerProtocolMode protocolMode, + EventManager eventManager, + SSLSocket tcpSocket, + ObjectOutputStream tcpOut, + ObjectInputStream tcpIn + ) { + this.connectionId = Objects.requireNonNull(connectionId, "connectionId"); + this.codec = Objects.requireNonNull(codec, "codec"); + this.protocolMode = Objects.requireNonNull(protocolMode, "protocolMode"); + this.eventManager = Objects.requireNonNull(eventManager, "eventManager"); + this.tcpSocket = tcpSocket; + this.tcpOut = tcpOut; + this.tcpIn = tcpIn; + } + + /** + * Creates a new TCP-accepted connection (TLS). + * + * @param codec packet codec + * @param protocolMode server protocol mode (supported/required) + * @param eventManager event manager used to dispatch server events + * @param tcpSocket TLS socket + * @param tcpOut TCP output stream + * @param tcpIn TCP input stream + * @return connection + */ + public static ClientConnection tcpAccepted( + PacketCodec codec, + ServerProtocolMode protocolMode, + EventManager eventManager, + SSLSocket tcpSocket, + ObjectOutputStream tcpOut, + ObjectInputStream tcpIn + ) { + return new ClientConnection( + UUID.randomUUID(), + codec, + protocolMode, + eventManager, + Objects.requireNonNull(tcpSocket, "tcpSocket"), + Objects.requireNonNull(tcpOut, "tcpOut"), + Objects.requireNonNull(tcpIn, "tcpIn") + ); + } + + /** + * Creates a new UDP-only connection. + * + * @param codec packet codec + * @param protocolMode server protocol mode (supported/required) + * @param eventManager event manager used to dispatch server events + * @param connectionId pre-generated connection id + * @return connection + */ + public static ClientConnection udpOnly( + PacketCodec codec, + ServerProtocolMode protocolMode, + EventManager eventManager, + UUID connectionId + ) { + return new ClientConnection( + Objects.requireNonNull(connectionId, "connectionId"), + codec, + protocolMode, + eventManager, + null, + null, + null + ); + } + + /** + * Returns a server-side unique connection id. + * + * @return connection id + */ + public UUID connectionId() { + return connectionId; + } + + /** + * Returns required protocols snapshot (server policy snapshot). + * + * @return required protocols + */ + public EnumSetEmits: + *
Supports TCP (TLS) and UDP (DTLS). UDP is bound after TCP identity assignment.
- */ -public final class ConnectionHandler { - - private final NetworkServer server; - private final ClientID clientId; - private final Thread receiveThread; - private SSLSocket socket; - private ObjectOutputStream outputStream; - private ObjectInputStream inputStream; - private volatile SocketAddress udpRemoteAddress; - private volatile DtlsEndpoint.DtlsSession udpSession; - private volatile DtlsEndpoint dtlsEndpoint; - - /** - * Creates a new connection handler for an already accepted SSL socket. - * - * @param server owning server - * @param socket ssl socket - * @param clientId server-assigned client identity - * @throws IOException if stream creation fails - * @throws ClassNotFoundException if packet serialization fails - */ - public ConnectionHandler(NetworkServer server, SSLSocket socket, ClientID clientId) throws IOException, ClassNotFoundException { - this.server = Objects.requireNonNull(server, "server"); - this.socket = Objects.requireNonNull(socket, "socket"); - this.clientId = Objects.requireNonNull(clientId, "clientId"); - - this.outputStream = new ObjectOutputStream(socket.getOutputStream()); - this.inputStream = new ObjectInputStream(socket.getInputStream()); - - this.receiveThread = new Thread(this::receive, "ConnectionHandler-TCP-Receive-" + clientId.uuid()); - this.receiveThread.start(); - - // Send identity via TCP first. - sendPacket(new ClientIDPacket(this.clientId), Transport.TCP); - - // Transport-specific connect event (TCP) - server.getEventManager().executeEvent(new ConnectionHandlerConnectedEvent(this, Transport.TCP)); - } - - /** - * Attaches a DTLS/UDP remote endpoint to this already TCP-authenticated connection. - * - * @param remote remote UDP address - * @param session DTLS session for the remote - * @param endpoint DTLS endpoint - */ - public void attachUdp(SocketAddress remote, DtlsEndpoint.DtlsSession session, DtlsEndpoint endpoint) { - this.udpRemoteAddress = Objects.requireNonNull(remote, "remote"); - this.udpSession = Objects.requireNonNull(session, "session"); - this.dtlsEndpoint = Objects.requireNonNull(endpoint, "endpoint"); - - // Transport-specific connect event (UDP) - server.getEventManager().executeEvent(new ConnectionHandlerConnectedEvent(this, Transport.UDP)); - } - - public SocketAddress getUdpRemoteAddress() { - return udpRemoteAddress; - } - - public boolean isUdpAttached() { - return udpRemoteAddress != null && udpSession != null && udpSession.isHandshakeComplete(); - } - - public boolean isFullyConnected() { - boolean tcpOk = isConnected(); - boolean udpOk = isUdpAttached(); - - if (server.getTransportPolicy().required().contains(Transport.TCP) && !tcpOk) return false; - return !server.getTransportPolicy().required().contains(Transport.UDP) || udpOk; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (!(obj instanceof ConnectionHandler other)) return false; - return clientId.equals(other.clientId); - } - - @Override - public int hashCode() { - return clientId.hashCode(); - } - - public SSLSocket getSocket() { - return socket; - } - - public ObjectOutputStream getOutputStream() { - return outputStream; - } - - public ObjectInputStream getInputStream() { - return inputStream; - } - - public NetworkServer getServer() { - return server; - } - - public ClientID getClientId() { - return clientId; - } - - public boolean isConnected() { - return socket != null && socket.isConnected() && !socket.isClosed() - && receiveThread.isAlive() && !receiveThread.isInterrupted(); - } - - public synchronized boolean disconnect() { - boolean wasTcpConnected = isConnected(); - boolean wasUdpConnected = isUdpAttached(); - boolean wasFullyConnected = isFullyConnected(); - - if (!wasTcpConnected && !wasUdpConnected) { - // still cleanup - cleanup(); - return false; - } - - try { - receiveThread.interrupt(); - - if (outputStream != null) outputStream.close(); - if (inputStream != null) inputStream.close(); - if (socket != null) socket.close(); - } catch (IOException ignored) { - } - - // Emit transport-specific disconnect events - if (wasUdpConnected) { - server.getEventManager().executeEvent(new ConnectionHandlerDisconnectedEvent(this, Transport.UDP)); - } - if (wasTcpConnected) { - server.getEventManager().executeEvent(new ConnectionHandlerDisconnectedEvent(this, Transport.TCP)); - } - - cleanup(); - - if (wasFullyConnected) { - server.getEventManager().executeEvent(new ConnectionHandlerFullyDisconnectedEvent( - this, - server.getTransportPolicy().required().toArray(new Transport[0]) - )); - } - - return true; - } - - private void cleanup() { - socket = null; - outputStream = null; - inputStream = null; - - udpRemoteAddress = null; - udpSession = null; - dtlsEndpoint = null; - - server.getConnectionHandlers().remove(this); - } - - /** - * Sends a packet via the selected transport. - * - * @param packet packet to send - * @param transport target transport - * @return true if sent, false if not possible - * @throws IOException on I/O errors - * @throws ClassNotFoundException on serialization errors - */ - public boolean sendPacket(Packet packet, Transport transport) throws IOException, ClassNotFoundException { - Objects.requireNonNull(packet, "packet"); - Objects.requireNonNull(transport, "transport"); - - if (!server.getTransportPolicy().supported().contains(transport)) { - return false; - } - - return switch (transport) { - case TCP -> sendTcp(packet); - case UDP -> sendUdp(packet); - }; - } - - private boolean sendTcp(Packet packet) throws IOException, ClassNotFoundException { - if (!isConnected()) return false; - - boolean sent = server.getPacketHandler().sendPacket(packet, outputStream); - if (sent) server.getEventManager().executeEvent(new S_PacketSendEvent(packet, this, Transport.TCP)); - else server.getEventManager().executeEvent(new S_PacketSendFailedEvent(packet, this, Transport.TCP)); - return sent; - } - - private boolean sendUdp(Packet packet) throws IOException, ClassNotFoundException { - if (!isUdpAttached()) return false; - - DtlsEndpoint endpoint = dtlsEndpoint; - if (endpoint == null) return false; - - ByteBuffer encoded = UdpPacketCodec.encode(server.getPacketHandler(), packet); - endpoint.sendApplication(udpRemoteAddress, encoded); - - server.getEventManager().executeEvent(new S_PacketSendEvent(packet, this, Transport.UDP)); - return true; - } - - private void receive() { - while (isConnected()) { - try { - Object received = inputStream.readObject(); - - if (received instanceof Integer id) { - if (server.getPacketHandler().isPacketIDRegistered(id)) { - Packet packet = server.getPacketHandler().getPacketByID(id); - - boolean ok = server.getPacketHandler().handlePacket(id, packet, inputStream); - if (ok) - server.getEventManager().executeEvent(new S_PacketReceivedEvent(this, packet, Transport.TCP)); - else - server.getEventManager().executeEvent(new S_PacketReceivedFailedEvent(this, packet, Transport.TCP)); - } else { - server.getEventManager().executeEvent(new S_UnknownObjectReceivedEvent(received, this, Transport.TCP)); - } - } else { - server.getEventManager().executeEvent(new S_UnknownObjectReceivedEvent(received, this, Transport.TCP)); - } - } catch (SocketException se) { - disconnect(); - } catch (Exception ex) { - if (server.getLogger() != null) { - server.getLogger().exception("Receive thread exception for client " + clientId, ex); - } - disconnect(); - } - } - } -} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServer.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServer.java index 1b323be..a649da6 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServer.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServer.java @@ -9,331 +9,329 @@ package dev.unlegitdqrk.unlegitlibrary.network.system.server; import dev.unlegitdqrk.unlegitlibrary.event.EventManager; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketCodec; import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; -import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler; -import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.ClientIDPacket; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.ConnectionIdPacket; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.ProtocolRequirementsPacket; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpBindAckPacket; import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpBindPacket; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpHelloPacket; import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_PacketReceivedEvent; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_UnknownObjectReceivedEvent; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect.ConnectionHandlerFullyConnectedEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_PacketReceivedFailedEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect.ClientConnectionConnectedEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect.ClientConnectionFullyConnectedEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect.ClientConnectionDisconnectedEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect.ClientConnectionFullyDisconnectedEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.incoming.TCPIncomingConnectionEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.incoming.UDPIncomingConnectionEvent; -import dev.unlegitdqrk.unlegitlibrary.network.system.udp.DtlsEndpoint; -import dev.unlegitdqrk.unlegitlibrary.network.system.udp.UdpPacketCodec; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientID; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportPolicy; -import dev.unlegitdqrk.unlegitlibrary.network.utils.PemUtils; -import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider; -import dev.unlegitdqrk.unlegitlibrary.utils.Logger; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.DtlsEndpoint; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; -import javax.net.ssl.*; -import java.io.File; +import javax.net.ServerSocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocket; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.net.InetSocketAddress; -import java.net.Socket; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; -import java.security.KeyStore; -import java.util.*; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** - * Hybrid server supporting TCP (TLS) and UDP (DTLS) transports. + * Secure server supporting TCP/TLS and UDP/DTLS. * - *Supports policy combinations: - *
Listeners are started based on {@link ServerProtocolMode#required()}.
+ *Fully-connected is evaluated based on {@link ServerProtocolMode#required()}.
*/ public final class NetworkServer { + private final ServerProtocolMode protocolMode; + private final ClientAuthMode clientAuthMode; + + private final PacketCodec codec; + private final EventManager eventManager; + private final int tcpPort; private final int udpPort; - private final PacketHandler packetHandler; - private final EventManager eventManager; - private final Logger logger; + private final int timeoutMillis; + private final int mtu; - private final int timeout; private final SSLServerSocketFactory tlsServerSocketFactory; + private final SSLParameters tlsParameters; // optional private final SSLContext dtlsContext; - private final TransportPolicy transportPolicy; - private final boolean requireClientCert; - private final ListDrop this into your NetworkServer class (replace your existing onDtlsApplicationData).
- */ -final class NetworkServerUdpHooks { - - private NetworkServerUdpHooks() { - } - - /** - * Processes decrypted DTLS application data. - * - * @param server server - * @param remote remote address - * @param data decrypted data - */ - static void onDtlsApplicationData(NetworkServer server, SocketAddress remote, ByteBuffer data) { - Objects.requireNonNull(server, "server"); - Objects.requireNonNull(remote, "remote"); - Objects.requireNonNull(data, "data"); - - try { - Packet decoded = UdpPacketCodec.decodeAndHandle(server.getPacketHandler(), data); - if (decoded == null) { - return; - } - - if (decoded instanceof UdpBindPacket bind) { - // Bind remote to existing TCP handler - ConnectionHandler handler = server.getConnectionHandlerByClientId(bind.getClientId()); - if (handler == null) { - return; - } - if (!handler.isConnected()) { - return; - } - - DtlsEndpoint endpoint = server.getDtlsEndpoint(); - if (endpoint == null) { - return; - } - - boolean wasFullyConnected = handler.isFullyConnected(); - handler.attachUdp(remote, endpoint.session(remote), endpoint); - boolean nowFullyConnected = handler.isFullyConnected(); - - if (!wasFullyConnected && nowFullyConnected) { - server.getEventManager().executeEvent( - new ConnectionHandlerFullyConnectedEvent(handler, server.getTransportPolicy().required().toArray(new Transport[0])) - ); - } - - return; - } - - // For normal UDP packets: route to the bound handler (if any) - ConnectionHandler bound = server.findHandlerByUdpRemote(remote); - if (bound != null) { - server.getEventManager().executeEvent(new S_PacketReceivedEvent(bound, decoded, Transport.UDP)); - } - } catch (Exception ignored) { - // best effort: drop - } - } -} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ServerProtocolMode.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ServerProtocolMode.java new file mode 100644 index 0000000..d9c818f --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ServerProtocolMode.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ + * See LICENSE-File if exists + */ + +package dev.unlegitdqrk.unlegitlibrary.network.system.server; + +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; + +import java.util.EnumSet; +import java.util.Objects; + +/** + * Defines which network protocols a server requires for a connection to be considered usable. + * + *Note: This model exposes only {@link #required()}. + * Therefore, "optional protocols" are not representable here. If you need optional transports, + * the model must include a separate supported-set.
+ */ +public final class ServerProtocolMode { + + private final EnumSetIn v2 the default transport uses {@link dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketCodec} + * and usually yields {@link Packet}. This event is kept + * for custom/legacy decoding paths.
*/ public final class S_UnknownObjectReceivedEvent extends Event { private final Object received; - private final ConnectionHandler connectionHandler; - private final Transport transport; + private final ClientConnection connection; + private final NetworkProtocol protocol; /** * Creates a new event. * - * @param received received object - * @param connectionHandler handler - * @param transport transport + * @param received received object + * @param connection connection (may be null if not attributable) + * @param protocol protocol */ - public S_UnknownObjectReceivedEvent(Object received, ConnectionHandler connectionHandler, Transport transport) { - this.received = received; - this.connectionHandler = connectionHandler; - this.transport = transport; + public S_UnknownObjectReceivedEvent(Object received, ClientConnection connection, NetworkProtocol protocol) { + this.received = Objects.requireNonNull(received, "received"); + this.connection = connection; // may be null + this.protocol = Objects.requireNonNull(protocol, "protocol"); } - public ConnectionHandler getConnectionHandler() { - return connectionHandler; + /** + * Returns the connection, if attributable. + * + * @return connection or null + */ + public ClientConnection getConnection() { + return connection; } + /** + * Returns the received object. + * + * @return received object + */ public Object getReceived() { return received; } - public Transport getTransport() { - return transport; + /** + * Returns the protocol. + * + * @return protocol + */ + public NetworkProtocol getProtocol() { + return protocol; } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendEvent.java index 206bb6d..3799aef 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendEvent.java @@ -6,52 +6,61 @@ * See LICENSE-File if exists */ -/* - * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved - * - * You are unauthorized to remove this copyright. - * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ - * See LICENSE-File if exists - */ - package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.send; import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection; + +import java.util.Objects; /** - * Fired when a packet was sent by the server on a specific transport. + * Fired when a packet was successfully sent by the server on a specific protocol. */ public final class S_PacketSendEvent extends Event { private final Packet packet; - private final ConnectionHandler connectionHandler; - private final Transport transport; + private final ClientConnection connection; + private final NetworkProtocol protocol; /** * Creates a new packet send event. * - * @param packet packet - * @param connectionHandler handler - * @param transport transport used + * @param packet packet + * @param connection connection + * @param protocol protocol used */ - public S_PacketSendEvent(Packet packet, ConnectionHandler connectionHandler, Transport transport) { - this.packet = packet; - this.connectionHandler = connectionHandler; - this.transport = transport; + public S_PacketSendEvent(Packet packet, ClientConnection connection, NetworkProtocol protocol) { + this.packet = Objects.requireNonNull(packet, "packet"); + this.connection = Objects.requireNonNull(connection, "connection"); + this.protocol = Objects.requireNonNull(protocol, "protocol"); } - public ConnectionHandler getConnectionHandler() { - return connectionHandler; + /** + * Returns the connection. + * + * @return connection + */ + public ClientConnection getConnection() { + return connection; } + /** + * Returns the packet. + * + * @return packet + */ public Packet getPacket() { return packet; } - public Transport getTransport() { - return transport; + /** + * Returns the protocol used. + * + * @return protocol + */ + public NetworkProtocol getProtocol() { + return protocol; } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendFailedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendFailedEvent.java index c239a92..e85bdb4 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendFailedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendFailedEvent.java @@ -6,52 +6,78 @@ * See LICENSE-File if exists */ -/* - * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved - * - * You are unauthorized to remove this copyright. - * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ - * See LICENSE-File if exists - */ - package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.send; import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection; + +import java.util.Objects; /** - * Fired when a packet send failed on the server on a specific transport. + * Fired when a packet send failed on the server on a specific protocol. */ public final class S_PacketSendFailedEvent extends Event { private final Packet packet; - private final ConnectionHandler connectionHandler; - private final Transport transport; + private final ClientConnection connection; + private final NetworkProtocol protocol; + private final Exception error; /** * Creates a new packet send failed event. * - * @param packet packet - * @param connectionHandler handler - * @param transport intended transport + * @param packet packet that failed to be sent + * @param connection connection + * @param protocol intended protocol + * @param error root cause */ - public S_PacketSendFailedEvent(Packet packet, ConnectionHandler connectionHandler, Transport transport) { - this.packet = packet; - this.connectionHandler = connectionHandler; - this.transport = transport; + public S_PacketSendFailedEvent( + Packet packet, + ClientConnection connection, + NetworkProtocol protocol, + Exception error + ) { + this.packet = Objects.requireNonNull(packet, "packet"); + this.connection = Objects.requireNonNull(connection, "connection"); + this.protocol = Objects.requireNonNull(protocol, "protocol"); + this.error = Objects.requireNonNull(error, "error"); } - public ConnectionHandler getConnectionHandler() { - return connectionHandler; + /** + * Returns the connection. + * + * @return connection + */ + public ClientConnection getConnection() { + return connection; } + /** + * Returns the packet. + * + * @return packet + */ public Packet getPacket() { return packet; } - public Transport getTransport() { - return transport; + /** + * Returns the intended protocol. + * + * @return protocol + */ + public NetworkProtocol getProtocol() { + return protocol; + } + + /** + * Returns the underlying error. + * + * @return error + */ + public Exception getError() { + return error; } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionConnectedEvent.java new file mode 100644 index 0000000..8e97c9b --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionConnectedEvent.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ + * See LICENSE-File if exists + */ + +package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect; + +import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection; + +import java.util.Objects; + +/** + * Fired when a specific protocol becomes connected for a server-side connection. + * + *Protocol-specific: + *
In v2 this typically means: + *
Transport-specific: - *
For your setup this means: TCP connected and UDP (DTLS) bound.
- */ -public final class ConnectionHandlerFullyConnectedEvent extends Event { - - private final ConnectionHandler connectionHandler; - private final Transport[] requiredTransports; - - /** - * Creates a new event. - * - * @param connectionHandler handler - * @param requiredTransports required transports now established - */ - public ConnectionHandlerFullyConnectedEvent(ConnectionHandler connectionHandler, Transport[] requiredTransports) { - this.connectionHandler = connectionHandler; - this.requiredTransports = requiredTransports; - } - - /** - * Returns the handler. - * - * @return handler - */ - public ConnectionHandler getConnectionHandler() { - return connectionHandler; - } - - /** - * Returns the required transports that are now established. - * - * @return transports - */ - public Transport[] getRequiredTransports() { - return requiredTransports; - } -} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionDisconnectedEvent.java new file mode 100644 index 0000000..b6dad28 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionDisconnectedEvent.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ + * See LICENSE-File if exists + */ + +package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect; + +import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection; + +import java.util.Objects; + +/** + * Fired when a specific protocol becomes disconnected for a server-side connection. + */ +public final class ClientConnectionDisconnectedEvent extends Event { + + private final ClientConnection connection; + private final NetworkProtocol protocol; + + /** + * Creates a new connection disconnected event. + * + * @param connection connection + * @param protocol disconnected protocol + */ + public ClientConnectionDisconnectedEvent(ClientConnection connection, NetworkProtocol protocol) { + this.connection = Objects.requireNonNull(connection, "connection"); + this.protocol = Objects.requireNonNull(protocol, "protocol"); + } + + /** + * Returns the connection. + * + * @return connection + */ + public ClientConnection getConnection() { + return connection; + } + + /** + * Returns the protocol that was disconnected. + * + * @return protocol + */ + public NetworkProtocol getProtocol() { + return protocol; + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionFullyDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionFullyDisconnectedEvent.java new file mode 100644 index 0000000..c32a590 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionFullyDisconnectedEvent.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ + * See LICENSE-File if exists + */ + +package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect; + +import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection; + +import java.util.EnumSet; +import java.util.Objects; + +/** + * Fired when a connection was fully connected and then became fully disconnected + * (i.e., it no longer satisfies the required protocols). + */ +public final class ClientConnectionFullyDisconnectedEvent extends Event { + + private final ClientConnection connection; + private final EnumSetFor your policy (TCP+UDP required) this means: - * previously: TCP connected and UDP attached, - * now: requirements are no longer satisfied.
- */ -public final class ConnectionHandlerFullyDisconnectedEvent extends Event { - - private final ConnectionHandler connectionHandler; - private final Transport[] requiredTransports; - - /** - * Creates a new fully disconnected event. - * - * @param connectionHandler handler - * @param requiredTransports required transports according to policy - */ - public ConnectionHandlerFullyDisconnectedEvent(ConnectionHandler connectionHandler, Transport[] requiredTransports) { - this.connectionHandler = Objects.requireNonNull(connectionHandler, "connectionHandler"); - this.requiredTransports = Objects.requireNonNull(requiredTransports, "requiredTransports"); - } - - public ConnectionHandler getConnectionHandler() { - return connectionHandler; - } - - public Transport[] getRequiredTransports() { - return requiredTransports; - } -} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/TCPIncomingConnectionEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/TCPIncomingConnectionEvent.java index 03d16e8..d262e11 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/TCPIncomingConnectionEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/TCPIncomingConnectionEvent.java @@ -10,22 +10,21 @@ package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.incomi import dev.unlegitdqrk.unlegitlibrary.event.impl.CancellableEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.server.NetworkServer; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; import javax.net.ssl.SSLSocket; import java.util.Objects; /** - * Fired when an incoming connection attempt reaches the server. + * Fired when an incoming TCP/TLS connection attempt reaches the server. * - *Currently this event is emitted for TCP/TLS accept only (because UDP/DTLS is bound later via {@code UdpBindPacket}). - * The {@link #getTransport()} field exists to keep the event model transport-aware and future-proof.
+ *This event is emitted for TCP/TLS accept only. UDP/DTLS is bound later via the bind flow.
*/ public final class TCPIncomingConnectionEvent extends CancellableEvent { private final NetworkServer server; private final SSLSocket socket; - private final Transport transport = Transport.TCP; + private final NetworkProtocol protocol = NetworkProtocol.TCP; /** * Creates a new incoming connection event. @@ -57,11 +56,11 @@ public final class TCPIncomingConnectionEvent extends CancellableEvent { } /** - * Returns the transport associated with this incoming connection. + * Returns the protocol associated with this incoming connection. * - * @return transport + * @return {@link NetworkProtocol#TCP} */ - public Transport getTransport() { - return transport; + public NetworkProtocol getProtocol() { + return protocol; } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/UDPIncomingConnectionEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/UDPIncomingConnectionEvent.java index a9c8744..a828eb5 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/UDPIncomingConnectionEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/UDPIncomingConnectionEvent.java @@ -10,21 +10,20 @@ package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.incomi import dev.unlegitdqrk.unlegitlibrary.event.impl.CancellableEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.server.NetworkServer; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.util.Objects; /** - * Fired when an incoming UDP/DTLS datagram is received by the server - * before it is bound to a {@link dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler}. + * Fired when an incoming UDP datagram is received by the server before it is bound to a connection. * *This event allows inspection or rejection of: *
If cancelled, the datagram is silently dropped.
@@ -34,14 +33,14 @@ public final class UDPIncomingConnectionEvent extends CancellableEvent { private final NetworkServer server; private final SocketAddress remoteAddress; private final ByteBuffer rawData; - private final Transport transport = Transport.UDP; + private final NetworkProtocol protocol = NetworkProtocol.UDP; /** - * Creates a new incoming UDP connection/datagram event. + * Creates a new incoming UDP datagram event. * * @param server server instance * @param remoteAddress remote UDP address - * @param rawData raw received datagram (read-only duplicate recommended) + * @param rawData raw received datagram (a read-only copy will be stored) */ public UDPIncomingConnectionEvent( NetworkServer server, @@ -74,7 +73,7 @@ public final class UDPIncomingConnectionEvent extends CancellableEvent { /** * Returns the raw UDP datagram payload. * - *The buffer is read-only and positioned at the start of the payload.
+ *The buffer is read-only.
* * @return raw datagram data */ @@ -83,11 +82,11 @@ public final class UDPIncomingConnectionEvent extends CancellableEvent { } /** - * Returns the transport type of this incoming connection. + * Returns the protocol type of this incoming datagram. * - * @return {@link Transport#UDP} + * @return {@link NetworkProtocol#UDP} */ - public Transport getTransport() { - return transport; + public NetworkProtocol getProtocol() { + return protocol; } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/UdpPacketCodec.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/UdpPacketCodec.java deleted file mode 100644 index 143b720..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/UdpPacketCodec.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved - * - * You are unauthorized to remove this copyright. - * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ - * See LICENSE-File if exists - */ - -package dev.unlegitdqrk.unlegitlibrary.network.system.udp; - -import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; -import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler; - -import java.io.*; -import java.nio.ByteBuffer; - -/** - * Encodes/decodes packets for UDP transport. - * - *Format: - *
- * [int packetId][ObjectOutputStream payload bytes...] - *- */ -public final class UdpPacketCodec { - - private UdpPacketCodec() { - } - - /** - * Encodes a packet into a byte buffer ready for sending. - * - * @param handler packet handler - * @param packet packet - * @return encoded buffer (position=0, limit=length) - * @throws IOException on I/O errors - * @throws ClassNotFoundException on serialization errors - */ - public static ByteBuffer encode(PacketHandler handler, Packet packet) throws IOException, ClassNotFoundException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(512); - - try (DataOutputStream dos = new DataOutputStream(baos)) { - dos.writeInt(packet.getPacketID()); - - // Keep your existing Packet API (ObjectOutputStream) - try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { - packet.write(handler, oos); - oos.flush(); - } - } - - byte[] bytes = baos.toByteArray(); - return ByteBuffer.wrap(bytes); - } - - /** - * Decodes a packet id and lets the handler read the payload into a packet instance. - * - * @param handler packet handler - * @param datagram datagram buffer (position=0, limit=length) - * @return decoded packet instance (already read/filled), or {@code null} if unknown id - * @throws IOException on errors - * @throws ClassNotFoundException on errors - */ - public static Packet decodeAndHandle(PacketHandler handler, ByteBuffer datagram) throws IOException, ClassNotFoundException { - ByteArrayInputStream bais = new ByteArrayInputStream(datagram.array(), datagram.position(), datagram.remaining()); - - int id; - try (DataInputStream dis = new DataInputStream(bais)) { - id = dis.readInt(); - } - - if (!handler.isPacketIDRegistered(id)) { - return null; - } - - Packet packet = handler.getPacketByID(id); - - // Now decode remaining bytes with ObjectInputStream - int payloadOffset = 4; - ByteArrayInputStream payload = new ByteArrayInputStream(datagram.array(), datagram.position() + payloadOffset, datagram.remaining() - payloadOffset); - - try (ObjectInputStream ois = new ObjectInputStream(payload)) { - boolean ok = handler.handlePacket(id, packet, ois); - return packet; - } - } -} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientAuthMode.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientAuthMode.java new file mode 100644 index 0000000..9a7789a --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientAuthMode.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ + * See LICENSE-File if exists + */ + +package dev.unlegitdqrk.unlegitlibrary.network.system.utils; + +/** + * Defines whether the server requires client certificates during the TLS handshake. + * + *
{@link #REQUIRED} enforces mutual TLS (mTLS): clients must present a certificate.
+ * + *{@link #OPTIONAL} allows clients without a certificate to connect (server will request a certificate, + * but does not fail the handshake if none is provided).
+ */ +public enum ClientAuthMode { + + /** + * Client certificate is mandatory (mTLS). + */ + REQUIRED, + + /** + * Client certificate is optional. + */ + OPTIONAL +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientID.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientID.java deleted file mode 100644 index 64628d8..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientID.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved - * - * You are unauthorized to remove this copyright. - * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ - * See LICENSE-File if exists - */ - -package dev.unlegitdqrk.unlegitlibrary.network.system.utils; - -import java.io.Serial; -import java.io.Serializable; -import java.util.Objects; -import java.util.UUID; - -/** - * Immutable identifier for a client (similar to a Minecraft player's UUID). - */ -public record ClientID(UUID uuid) implements Serializable { - - @Serial - private static final long serialVersionUID = 1L; - - /** - * Creates a new {@link ClientID}. - * - * @param uuid backing UUID (must not be {@code null}) - */ - public ClientID(UUID uuid) { - this.uuid = Objects.requireNonNull(uuid, "uuid"); - } - - /** - * Generates a random {@link ClientID}. - * - * @return random client id - */ - public static ClientID random() { - return new ClientID(UUID.randomUUID()); - } - - /** - * Returns backing UUID. - * - * @return UUID - */ - @Override - public UUID uuid() { - return uuid; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (!(obj instanceof ClientID other)) return false; - return uuid.equals(other.uuid); - } - - @Override - public String toString() { - return "ClientID{uuid=" + uuid + "}"; - } -} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/DtlsEndpoint.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/DtlsEndpoint.java similarity index 63% rename from src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/DtlsEndpoint.java rename to src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/DtlsEndpoint.java index 241269d..8742bc6 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/DtlsEndpoint.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/DtlsEndpoint.java @@ -1,3 +1,19 @@ +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://github.com/UnlegitDqrk + * See LICENSE-File if exists + */ + +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://github.com/UnlegitDqrk + * See LICENSE-File if exists + */ + /* * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved * @@ -6,47 +22,69 @@ * See LICENSE-File if exists */ -package dev.unlegitdqrk.unlegitlibrary.network.system.udp; +package dev.unlegitdqrk.unlegitlibrary.network.system.utils; -import javax.net.ssl.*; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManager; import java.io.IOException; +import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.security.SecureRandom; +import java.security.cert.Certificate; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; /** - * Minimal DTLS endpoint using {@link SSLEngine} over {@link DatagramChannel}. + * Minimal DTLS endpoint using {@link SSLEngine} over a datagram transport. * *This implementation is designed for "best effort" UDP and focuses on: *
Server mode note: A newly created DTLS session must enter handshake + * ({@link SSLEngine#beginHandshake()}) immediately, otherwise initial client handshake datagrams + * may not be processed and the client may time out.
+ * + *Client certificate policy (server-side): + * For {@link ClientAuthMode#REQUIRED} the presence of a peer certificate is enforced after handshake + * completion (via {@link SSLEngine#getSession()} and {@code getPeerCertificates()}), because + * {@link SSLParameters#setNeedClientAuth(boolean)} is not reliably enforced for {@link SSLEngine} across providers.
*/ public final class DtlsEndpoint { - private final DatagramChannel channel; + private static final ByteBuffer EMPTY = ByteBuffer.allocate(0); + + private final java.nio.channels.DatagramChannel channel; private final SSLContext sslContext; private final boolean clientMode; private final int mtu; private final int timeoutMillis; private final ApplicationDataHandler appHandler; + private final ClientAuthMode clientAuthMode; + private final MapClient: call this after creating a session and before sending app data.
- *Server: call this once you detect a new remote (first datagrams arrive) to complete handshake.
- * * @param remote remote address * @throws IOException on I/O error * @throws SSLException on TLS error @@ -109,7 +155,10 @@ public final class DtlsEndpoint { DtlsSession s = session(remote); if (s.isHandshakeComplete()) return; - s.engine().beginHandshake(); + if (clientMode) { + s.engine().beginHandshake(); + } + SSLEngineResult.HandshakeStatus hs = s.engine().getHandshakeStatus(); ByteBuffer netIn = ByteBuffer.allocate(mtu); @@ -126,9 +175,14 @@ public final class DtlsEndpoint { switch (hs) { case NEED_WRAP -> { netOut.clear(); - SSLEngineResult r = s.engine().wrap(ByteBuffer.allocate(0), netOut); + + SSLEngineResult r = s.engine().wrap(EMPTY, netOut); hs = r.getHandshakeStatus(); + if (r.getStatus() == SSLEngineResult.Status.CLOSED) { + throw new SSLException("DTLS engine closed during handshake (wrap)"); + } + netOut.flip(); if (netOut.hasRemaining()) { channel.send(netOut, remote); @@ -138,24 +192,23 @@ public final class DtlsEndpoint { netIn.clear(); SocketAddress from = channel.receive(netIn); if (from == null) { - // best effort: keep looping until timeout continue; } if (!from.equals(remote)) { - // ignore other peers here; their sessions will be handled elsewhere continue; } netIn.flip(); + app.clear(); + SSLEngineResult r = s.engine().unwrap(netIn, app); hs = r.getHandshakeStatus(); if (r.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW) { - // wait for more datagrams continue; } if (r.getStatus() == SSLEngineResult.Status.CLOSED) { - throw new SSLException("DTLS engine closed during handshake"); + throw new SSLException("DTLS engine closed during handshake (unwrap)"); } } case NEED_TASK -> { @@ -165,7 +218,10 @@ public final class DtlsEndpoint { } hs = s.engine().getHandshakeStatus(); } - case FINISHED, NOT_HANDSHAKING -> s.setHandshakeComplete(true); + case FINISHED, NOT_HANDSHAKING -> { + s.setHandshakeComplete(true); + enforceClientAuthIfRequired(s); + } } } } @@ -179,6 +235,9 @@ public final class DtlsEndpoint { * @throws SSLException on TLS errors */ public void sendApplication(SocketAddress remote, ByteBuffer applicationData) throws IOException, SSLException { + Objects.requireNonNull(remote, "remote"); + Objects.requireNonNull(applicationData, "applicationData"); + DtlsSession s = session(remote); if (!s.isHandshakeComplete()) { handshake(remote); @@ -202,8 +261,6 @@ public final class DtlsEndpoint { /** * Polls UDP, unwraps DTLS records and dispatches decrypted application data. * - *Run this in a dedicated thread for server and client.
- * * @throws IOException on I/O errors */ public void poll() throws IOException { @@ -224,27 +281,35 @@ public final class DtlsEndpoint { ByteBuffer app = ByteBuffer.allocate(s.engine().getSession().getApplicationBufferSize()); try { + app.clear(); SSLEngineResult r = s.engine().unwrap(netIn, app); if (r.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK) { Runnable task; - while ((task = s.engine().getDelegatedTask()) != null) task.run(); + while ((task = s.engine().getDelegatedTask()) != null) { + task.run(); + } } if (r.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP) { - // Respond to DTLS handshake flights if needed ByteBuffer netOut = ByteBuffer.allocate(mtu); netOut.clear(); - SSLEngineResult wr = s.engine().wrap(ByteBuffer.allocate(0), netOut); + + SSLEngineResult wr = s.engine().wrap(EMPTY, netOut); if (wr.getStatus() != SSLEngineResult.Status.CLOSED) { netOut.flip(); - if (netOut.hasRemaining()) channel.send(netOut, from); + if (netOut.hasRemaining()) { + channel.send(netOut, from); + } } } if (r.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.FINISHED || r.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { - s.setHandshakeComplete(true); + if (!s.isHandshakeComplete()) { + s.setHandshakeComplete(true); + enforceClientAuthIfRequired(s); + } } if (r.getStatus() == SSLEngineResult.Status.CLOSED) { @@ -258,24 +323,49 @@ public final class DtlsEndpoint { appHandler.onApplicationData(from, app); } } catch (SSLException ignored) { - // best effort: invalid record / handshake mismatch -> drop + // Best effort: invalid record / handshake mismatch -> drop. } finally { netIn.clear(); } } } + private void enforceClientAuthIfRequired(DtlsSession s) throws SSLException { + if (clientMode) return; + if (clientAuthMode != ClientAuthMode.REQUIRED) return; + + // If REQUIRED, ensure the peer presented a certificate. + try { + Certificate[] peer = s.engine().getSession().getPeerCertificates(); + if (peer == null || peer.length == 0) { + sessions.remove(s.remote()); + throw new SSLException("Client certificate required but not provided"); + } + } catch (javax.net.ssl.SSLPeerUnverifiedException e) { + sessions.remove(s.remote()); + throw new SSLException("Client certificate required but peer unverified", e); + } + } + private SSLEngine createEngine(SocketAddress remote) throws SSLException { - SSLEngine engine = sslContext.createSSLEngine(); + final SSLEngine engine; + + if (remote instanceof InetSocketAddress isa) { + // Host string may be an IP or hostname. Both are acceptable for SSLEngine identity. + engine = sslContext.createSSLEngine(isa.getHostString(), isa.getPort()); + } else { + engine = sslContext.createSSLEngine(); + } + engine.setUseClientMode(clientMode); SSLParameters p = engine.getSSLParameters(); - // Prefer DTLSv1.2 (widest support in JSSE DTLS). If your environment supports DTLSv1.3, you can extend this. p.setProtocols(new String[]{"DTLSv1.2"}); - engine.setSSLParameters(p); - // For DTLS it's recommended to set a secure random - engine.getSSLParameters(); + // Do NOT rely on setNeedClientAuth/setWantClientAuth for SSLEngine enforcement. + // Enforce REQUIRED via post-handshake peer certificate check. + + engine.setSSLParameters(p); return engine; } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Endpoint.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Endpoint.java new file mode 100644 index 0000000..f6b8213 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Endpoint.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved + * + * You are unauthorized to remove this copyright. + * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/ + * See LICENSE-File if exists + */ + +package dev.unlegitdqrk.unlegitlibrary.network.system.utils; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * Represents a network endpoint that may be defined by hostname or IP literal. + * + *Fully supports IPv4 and IPv6 (including bracketed IPv6 literals). + * Resolution is deterministic and configurable via {@link IpPreference}.
+ */ +public final class Endpoint { + + private final String host; + private final int port; + private final IpPreference ipPreference; + + /** + * Creates a new endpoint. + * + * @param host hostname or IP literal + * @param port port (1–65535) + * @param ipPreference IP selection preference + */ + public Endpoint(String host, int port, IpPreference ipPreference) { + this.host = normalizeHost(host); + this.port = validatePort(port); + this.ipPreference = Objects.requireNonNull(ipPreference, "ipPreference"); + } + + /** + * Creates a new endpoint with {@link IpPreference#PREFER_IPV6}. + * + * @param host hostname or IP literal + * @param port port + */ + public Endpoint(String host, int port) { + this(host, port, IpPreference.PREFER_IPV6); + } + + /** + * Returns the host or IP literal. + * + * @return host + */ + public String host() { + return host; + } + + /** + * Returns the port. + * + * @return port + */ + public int port() { + return port; + } + + /** + * Resolves all addresses for this endpoint. + * + * @return resolved socket addresses + * @throws UnknownHostException if resolution fails + */ + public ListThis enum is used: + *