From ab54d8bf440399f1c492630be598997876202ddf Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 20 Jan 2026 15:04:30 +0100 Subject: [PATCH] Rewritten NetworkSystem --- pom.xml | 2 +- .../unlegitlibrary/bank/CreditCard.java | 10 +- .../network/system/client/NetworkClient.java | 1143 +++++++++-------- .../receive/C_PacketReceivedEvent.java | 55 +- .../receive/C_PacketReceivedFailedEvent.java | 73 +- .../receive/C_UnknownObjectReceivedEvent.java | 60 +- .../packets/send/C_PacketSendEvent.java | 65 +- .../packets/send/C_PacketSendFailedEvent.java | 72 +- .../state/connect/ClientConnectedEvent.java | 44 +- .../connect/ClientFullyConnectedEvent.java | 49 +- .../disconnect/ClientDisconnectedEvent.java | 44 +- .../ClientFullyDisconnectedEvent.java | 43 +- .../network/system/packets/Packet.java | 56 +- .../network/system/packets/PacketCodec.java | 123 ++ .../network/system/packets/PacketHandler.java | 71 - .../system/packets/PacketRegistry.java | 78 ++ .../system/packets/impl/ClientIDPacket.java | 58 - .../packets/impl/ConnectionIdPacket.java | 70 + .../impl/ProtocolRequirementsPacket.java | 81 ++ .../system/packets/impl/UdpBindAckPacket.java | 71 + .../system/packets/impl/UdpBindPacket.java | 67 +- .../system/packets/impl/UdpHelloPacket.java | 44 + .../system/server/ClientConnection.java | 363 ++++++ .../system/server/ConnectionHandler.java | 275 ---- .../network/system/server/NetworkServer.java | 909 ++++++------- .../system/server/NetworkServerUdpHooks.java | 88 -- .../system/server/ServerProtocolMode.java | 69 + .../receive/S_PacketReceivedEvent.java | 57 +- .../receive/S_PacketReceivedFailedEvent.java | 74 +- .../receive/S_UnknownObjectReceivedEvent.java | 62 +- .../packets/send/S_PacketSendEvent.java | 57 +- .../packets/send/S_PacketSendFailedEvent.java | 74 +- .../ClientConnectionConnectedEvent.java | 59 + .../ClientConnectionFullyConnectedEvent.java | 60 + .../ConnectionHandlerConnectedEvent.java | 57 - .../ConnectionHandlerFullyConnectedEvent.java | 61 - .../ClientConnectionDisconnectedEvent.java | 53 + ...lientConnectionFullyDisconnectedEvent.java | 55 + .../ConnectionHandlerDisconnectedEvent.java | 51 - ...nnectionHandlerFullyDisconnectedEvent.java | 55 - .../incoming/TCPIncomingConnectionEvent.java | 17 +- .../incoming/UDPIncomingConnectionEvent.java | 25 +- .../network/system/udp/UdpPacketCodec.java | 88 -- .../network/system/utils/ClientAuthMode.java | 30 + .../network/system/utils/ClientID.java | 63 - .../system/{udp => utils}/DtlsEndpoint.java | 168 ++- .../network/system/utils/Endpoint.java | 145 +++ .../network/system/utils/NetworkProtocol.java | 34 + .../network/system/utils/Transport.java | 28 - .../network/system/utils/TransportPolicy.java | 76 -- 50 files changed, 3072 insertions(+), 2460 deletions(-) create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketCodec.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketHandler.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketRegistry.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ClientIDPacket.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ConnectionIdPacket.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ProtocolRequirementsPacket.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindAckPacket.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpHelloPacket.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ClientConnection.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ConnectionHandler.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServerUdpHooks.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ServerProtocolMode.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionConnectedEvent.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionFullyConnectedEvent.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerConnectedEvent.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerFullyConnectedEvent.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionDisconnectedEvent.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionFullyDisconnectedEvent.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerDisconnectedEvent.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerFullyDisconnectedEvent.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/UdpPacketCodec.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientAuthMode.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientID.java rename src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/{udp => utils}/DtlsEndpoint.java (63%) create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Endpoint.java create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/NetworkProtocol.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Transport.java delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/TransportPolicy.java diff --git a/pom.xml b/pom.xml index f8b6b7c..f787edd 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ dev.unlegitdqrk unlegitlibrary - 1.7.1 + 1.7.2 https://unlegitdqrk.dev/ Just a big library diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/bank/CreditCard.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/bank/CreditCard.java index 149ea47..a6d3629 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/bank/CreditCard.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/bank/CreditCard.java @@ -19,14 +19,16 @@ public class CreditCard { private final Random random; public CreditCard(CardBrand cardBrand) { - this.cardBrand = cardBrand; - this.random = new Random(); - this.cardNumber = generateCardNumber(); + this(cardBrand, new Random()); } public CreditCard(CardBrand cardBrand, int seed) { + this(cardBrand, new Random(seed)); + } + + public CreditCard(CardBrand cardBrand, Random random) { this.cardBrand = cardBrand; - this.random = new Random(seed); + this.random = random; this.cardNumber = generateCardNumber(); } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/NetworkClient.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/NetworkClient.java index 3d76fb0..7c47b6d 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/NetworkClient.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/NetworkClient.java @@ -2,7 +2,7 @@ * 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/ + * 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 */ @@ -11,674 +11,713 @@ package dev.unlegitdqrk.unlegitlibrary.network.system.client; import dev.unlegitdqrk.unlegitlibrary.event.EventManager; import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.receive.C_PacketReceivedEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.receive.C_PacketReceivedFailedEvent; -import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.receive.C_UnknownObjectReceivedEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.send.C_PacketSendEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.send.C_PacketSendFailedEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.connect.ClientConnectedEvent; -import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.disconnect.ClientDisconnectedEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.connect.ClientFullyConnectedEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.disconnect.ClientDisconnectedEvent; import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.disconnect.ClientFullyDisconnectedEvent; +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.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.utils.PemUtils; -import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider; -import dev.unlegitdqrk.unlegitlibrary.utils.Logger; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpHelloPacket; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.DtlsEndpoint; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Endpoint; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; -import javax.net.ssl.*; -import java.io.*; -import java.net.ConnectException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.Socket; +import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; -import java.security.KeyStore; -import java.security.cert.CertificateFactory; +import java.util.EnumSet; import java.util.Objects; +import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; /** - * Hybrid client supporting TCP (TLS) and UDP (DTLS). + * Secure client supporting TCP/TLS and UDP/DTLS (independently). * - *

Your chosen policy: BOTH transports must be connected simultaneously.

+ *

Connectivity rules: + *

*/ public final class NetworkClient { - private final String host; - private final int tcpPort; - private final int udpPort; + private final Endpoint tcpEndpoint; + private final Endpoint udpEndpoint; + private final EnumSet enabledProtocols; - private final PacketHandler packetHandler; - private final EventManager eventManager; - private final Logger logger; - - private final int timeout; + private final PacketCodec codec; private final SSLSocketFactory tlsSocketFactory; - private final SSLParameters tlsParameters; + private final SSLParameters tlsParameters; // optional private final SSLContext dtlsContext; - private final Proxy proxy; - private final AtomicBoolean fullyConnectedEventFired = new AtomicBoolean(false); - private SSLSocket tcpSocket; - private ObjectOutputStream tcpOut; - private ObjectInputStream tcpIn; - private DatagramChannel udpChannel; - private DtlsEndpoint dtlsEndpoint; - private InetSocketAddress udpRemote; - private volatile ClientID clientId; - private volatile Thread tcpReceiveThread; + + private final int timeoutMillis; + private final int mtu; + + private final EventManager eventManager; + + private final AtomicBoolean lifecycleUp = new AtomicBoolean(false); + + private volatile boolean tcpConnected; + private volatile boolean udpConnected; // DTLS handshake ok + private volatile boolean udpBound; // server acknowledged bind (or UDP-only hello assigned) + private volatile boolean wasFullyConnected; + + private volatile SSLSocket tcpSocket; + private volatile ObjectOutputStream tcpOut; + private volatile ObjectInputStream tcpIn; + private volatile Thread tcpThread; + + private volatile DatagramChannel udpChannel; + private volatile DtlsEndpoint dtlsEndpoint; + private volatile InetSocketAddress udpRemote; private volatile Thread udpThread; - public NetworkClient( - String host, - int tcpPort, - int udpPort, - PacketHandler packetHandler, - EventManager eventManager, - Logger logger, - int timeout, + private volatile UUID connectionId; + + private volatile EnumSet serverRequiredProtocols; // received from server + + private NetworkClient( + Endpoint tcpEndpoint, + Endpoint udpEndpoint, + EnumSet enabledProtocols, + PacketCodec codec, SSLSocketFactory tlsSocketFactory, SSLParameters tlsParameters, SSLContext dtlsContext, - Proxy proxy + int timeoutMillis, + int mtu, + EventManager eventManager ) { - this.host = Objects.requireNonNull(host, "host"); - this.tcpPort = tcpPort; - this.udpPort = udpPort; + this.tcpEndpoint = Objects.requireNonNull(tcpEndpoint, "tcpEndpoint"); + this.udpEndpoint = Objects.requireNonNull(udpEndpoint, "udpEndpoint"); + this.enabledProtocols = EnumSet.copyOf(Objects.requireNonNull(enabledProtocols, "enabledProtocols")); - this.packetHandler = Objects.requireNonNull(packetHandler, "packetHandler"); - this.eventManager = Objects.requireNonNull(eventManager, "eventManager"); - this.logger = logger; - - this.timeout = timeout; - this.tlsSocketFactory = tlsSocketFactory; + this.codec = Objects.requireNonNull(codec, "codec"); + this.tlsSocketFactory = Objects.requireNonNull(tlsSocketFactory, "tlsSocketFactory"); this.tlsParameters = tlsParameters; - this.dtlsContext = dtlsContext; - this.proxy = proxy; + this.dtlsContext = Objects.requireNonNull(dtlsContext, "dtlsContext"); - this.packetHandler.setClientInstance(this); - this.packetHandler.registerPacket(new ClientIDPacket()); - this.packetHandler.registerPacket(new UdpBindPacket()); + this.timeoutMillis = timeoutMillis; + this.mtu = mtu; + + this.eventManager = Objects.requireNonNull(eventManager, "eventManager"); } - public EventManager getEventManager() { - return eventManager; - } + /** + * Connects the client. + * + *

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 EnumSet enabledProtocols() { + return EnumSet.copyOf(enabledProtocols); + } + + /** + * Returns the server-required protocol set if received, otherwise null. + * + * @return required protocols from server or null + */ + public EnumSet serverRequiredProtocols() { + EnumSet r = serverRequiredProtocols; + return r == null ? null : EnumSet.copyOf(r); + } + + /** + * Returns a snapshot of currently connected protocols (ready for app traffic). + * + * @return connected protocols + */ + public EnumSet connectedProtocols() { + EnumSet set = EnumSet.noneOf(NetworkProtocol.class); + if (isTcpConnected()) set.add(NetworkProtocol.TCP); + if (isUdpConnected()) set.add(NetworkProtocol.UDP); + return set; + } + + /** + * Returns true if the client is fully connected: + *
    + *
  • lifecycle is up
  • + *
  • connection id assigned
  • + *
  • server-required protocols received
  • + *
  • all server-required protocols are connected/ready
  • + *
+ * + * @return true if fully connected + */ + public boolean isConnected() { + if (!lifecycleUp.get()) return false; + if (connectionId == null) return false; + if (serverRequiredProtocols == null) return false; + + EnumSet required = EnumSet.copyOf(serverRequiredProtocols); + + boolean tcpOk = !required.contains(NetworkProtocol.TCP) || isTcpConnected(); + boolean udpOk = !required.contains(NetworkProtocol.UDP) || isUdpConnected(); + + return tcpOk && udpOk; + } + + + /** + * Returns true if TCP is up. + * + * @return true if TCP connected + */ + public boolean isTcpConnected() { + return tcpConnected; + } + + /** + * Returns true if UDP is up AND bound/acknowledged AND client id is set. + * + * @return true if UDP is ready for application traffic + */ + public boolean isUdpConnected() { + return udpConnected && udpBound && connectionId != null; + } + + /** + * Sends a packet via TCP/TLS or UDP/DTLS. + * + *

Note: 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 EnumSet requiredProtocolsForClientView() { + EnumSet r = serverRequiredProtocols; + if (r != null) return EnumSet.copyOf(r); + + // Fallback for UI/debug/events only; does NOT influence isConnected(). + return EnumSet.copyOf(enabledProtocols); + } + + private void checkFullyConnectedState() { + if (wasFullyConnected) return; + + if (!isConnected()) return; + + wasFullyConnected = true; + eventManager.executeEvent(new ClientFullyConnectedEvent(this, requiredProtocolsForClientView())); + } + + private void checkFullyDisconnectedIfNeeded() { + if (!wasFullyConnected) return; + + if (isConnected()) return; + + wasFullyConnected = false; + eventManager.executeEvent(new ClientFullyDisconnectedEvent(this, requiredProtocolsForClientView())); } /** * Builder for {@link NetworkClient}. */ - public static final class ClientBuilder extends DefaultMethodsOverrider { + public static final class Builder { - private String host; - private int tcpPort; - private int udpPort; - - private PacketHandler packetHandler; - private EventManager eventManager; - private Logger logger; - - private int timeout = 5000; + private Endpoint tcpEndpoint; + private Endpoint udpEndpoint; + private EnumSet enabledProtocols = EnumSet.of(NetworkProtocol.TCP, NetworkProtocol.UDP); + private PacketCodec codec; private SSLSocketFactory tlsSocketFactory; private SSLParameters tlsParameters; private SSLContext dtlsContext; - private File caFolder; - private File clientCertFolder; - private File clientKeyFolder; + private int timeoutMillis = 5000; + private int mtu = 1400; - private Proxy proxy; + private EventManager eventManager; - /** - * Creates a TLS socket factory for TCP using PEM certificates/keys. - * - * @param caFolder folder containing CA PEM files - * @param clientCertFolder folder containing client certificate files - * @param clientKeyFolder folder containing client key files - * @return TLS socket factory - * @throws Exception on any certificate/key errors - */ - public static SSLSocketFactory createTLSSocketFactory(File caFolder, File clientCertFolder, File clientKeyFolder) throws Exception { - Objects.requireNonNull(caFolder, "caFolder"); - Objects.requireNonNull(clientCertFolder, "clientCertFolder"); - Objects.requireNonNull(clientKeyFolder, "clientKeyFolder"); + public Builder setTcpEndpoint(Endpoint endpoint) { + this.tcpEndpoint = endpoint; + return this; + } - // Trust store (CAs) - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(null, null); + public Builder setUdpEndpoint(Endpoint endpoint) { + this.udpEndpoint = endpoint; + return this; + } - int caIndex = 1; - File[] caFiles = caFolder.listFiles((f) -> f.isFile() && f.getName().endsWith(".pem")); - if (caFiles != null) { - for (File caFile : caFiles) { - try (FileInputStream fis = new FileInputStream(caFile)) { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - java.security.cert.Certificate caCert = cf.generateCertificate(fis); - trustStore.setCertificateEntry("ca" + (caIndex++), caCert); - } - } + public Builder setEnabledProtocols(EnumSet protocols) { + this.enabledProtocols = EnumSet.copyOf(Objects.requireNonNull(protocols, "protocols")); + if (this.enabledProtocols.isEmpty()) { + throw new IllegalArgumentException("enabledProtocols must not be empty"); } - - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(trustStore); - - // Key store (client cert + key) - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(null, null); - - int clientIndex = 1; - File[] certFiles = clientCertFolder.listFiles((f) -> f.isFile() && (f.getName().endsWith(".crt") || f.getName().endsWith(".pem"))); - if (certFiles != null) { - for (File certFile : certFiles) { - String baseName = certFile.getName() - .replace(".crt", "") - .replace(".pem", ""); - File keyFile = new File(clientKeyFolder, baseName + ".key"); - if (!keyFile.exists()) continue; - - java.security.PrivateKey key = PemUtils.loadPrivateKey(keyFile); - java.security.cert.Certificate cert = PemUtils.loadCertificate(certFile); - - keyStore.setKeyEntry("client" + (clientIndex++), key, null, new java.security.cert.Certificate[]{cert}); - } - } - - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(keyStore, null); - - SSLContext tls = SSLContext.getInstance("TLSv1.3"); - tls.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - return tls.getSocketFactory(); - } - - /** - * Creates a DTLS context using the same key/trust setup as TLS. - * - * @param caFolder folder containing CA PEM files - * @param clientCertFolder folder containing client certificate files - * @param clientKeyFolder folder containing client key files - * @return DTLS SSL context - * @throws Exception on any certificate/key errors - */ - public static SSLContext createDTLSContext(File caFolder, File clientCertFolder, File clientKeyFolder) throws Exception { - Objects.requireNonNull(caFolder, "caFolder"); - Objects.requireNonNull(clientCertFolder, "clientCertFolder"); - Objects.requireNonNull(clientKeyFolder, "clientKeyFolder"); - - // Trust store - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(null, null); - - int caIndex = 1; - File[] caFiles = caFolder.listFiles((f) -> f.isFile() && f.getName().endsWith(".pem")); - if (caFiles != null) { - for (File caFile : caFiles) { - try (FileInputStream fis = new FileInputStream(caFile)) { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - java.security.cert.Certificate caCert = cf.generateCertificate(fis); - trustStore.setCertificateEntry("ca" + (caIndex++), caCert); - } - } - } - - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(trustStore); - - // Key store - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(null, null); - - int clientIndex = 1; - File[] certFiles = clientCertFolder.listFiles((f) -> f.isFile() && (f.getName().endsWith(".crt") || f.getName().endsWith(".pem"))); - if (certFiles != null) { - for (File certFile : certFiles) { - String baseName = certFile.getName() - .replace(".crt", "") - .replace(".pem", ""); - File keyFile = new File(clientKeyFolder, baseName + ".key"); - if (!keyFile.exists()) continue; - - java.security.PrivateKey key = PemUtils.loadPrivateKey(keyFile); - java.security.cert.Certificate cert = PemUtils.loadCertificate(certFile); - - keyStore.setKeyEntry("client" + (clientIndex++), key, null, new java.security.cert.Certificate[]{cert}); - } - } - - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(keyStore, null); - - SSLContext dtls = SSLContext.getInstance("DTLS"); - dtls.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - return dtls; - } - - /** - * Sets server host. - */ - public ClientBuilder setHost(String host) { - this.host = host; return this; } - /** - * Sets TCP port. - */ - public ClientBuilder setTcpPort(int tcpPort) { - this.tcpPort = tcpPort; + public Builder setCodec(PacketCodec codec) { + this.codec = codec; return this; } - /** - * Sets UDP port. - */ - public ClientBuilder setUdpPort(int udpPort) { - this.udpPort = udpPort; - return this; - } - - /** - * Sets packet handler. - */ - public ClientBuilder setPacketHandler(PacketHandler handler) { - this.packetHandler = handler; - return this; - } - - /** - * Sets event manager. - */ - public ClientBuilder setEventManager(EventManager manager) { - this.eventManager = manager; - return this; - } - - /** - * Sets logger. - */ - public ClientBuilder setLogger(Logger logger) { - this.logger = logger; - return this; - } - - /** - * Sets timeout in millis. - */ - public ClientBuilder setTimeout(int timeoutMillis) { - this.timeout = timeoutMillis; - return this; - } - - /** - * Sets TLS socket factory explicitly. - */ - public ClientBuilder setTLSSocketFactory(SSLSocketFactory factory) { + public Builder setTlsSocketFactory(SSLSocketFactory factory) { this.tlsSocketFactory = factory; return this; } - /** - * Sets TLS parameters (optional). - */ - public ClientBuilder setTLSParameters(SSLParameters params) { - this.tlsParameters = params; + public Builder setTlsParameters(SSLParameters parameters) { + this.tlsParameters = parameters; return this; } - /** - * Sets DTLS context explicitly. - */ - public ClientBuilder setDTLSContext(SSLContext dtlsContext) { - this.dtlsContext = dtlsContext; + public Builder setDtlsContext(SSLContext context) { + this.dtlsContext = context; return this; } - /** - * Sets root CA folder for auto-creating TLS/DTLS when factories/contexts are not provided. - */ - public ClientBuilder setRootCAFolder(File folder) { - this.caFolder = folder; + public Builder setTimeoutMillis(int timeoutMillis) { + this.timeoutMillis = timeoutMillis; return this; } - /** - * Sets client certificate + key folders for auto-creating TLS/DTLS. - */ - public ClientBuilder setClientCertificatesFolder(File clientCertFolder, File clientKeyFolder) { - this.clientCertFolder = clientCertFolder; - this.clientKeyFolder = clientKeyFolder; + public Builder setMtu(int mtu) { + this.mtu = mtu; return this; } - /** - * Sets optional proxy. - */ - public ClientBuilder setProxy(Proxy proxy) { - this.proxy = proxy; + public Builder setEventManager(EventManager eventManager) { + this.eventManager = eventManager; return this; } - /** - * Builds the client. - * - * @return client - */ public NetworkClient build() { - if (host == null) throw new IllegalStateException("Host not set"); - if (tcpPort <= 0) throw new IllegalStateException("TCP port not set"); - if (udpPort <= 0) throw new IllegalStateException("UDP port not set"); - if (packetHandler == null) throw new IllegalStateException("PacketHandler not set"); - if (eventManager == null) throw new IllegalStateException("EventManager not set"); + if (codec == null) throw new IllegalStateException("codec not set"); + if (eventManager == null) throw new IllegalStateException("eventManager not set"); - // Auto-create TLS/DTLS from folders if not explicitly provided - if ((tlsSocketFactory == null || dtlsContext == null) && caFolder != null) { - if (clientCertFolder == null || clientKeyFolder == null) { - throw new IllegalStateException("Client cert/key folders must be set when using CA folder auto-config."); - } - try { - if (tlsSocketFactory == null) { - tlsSocketFactory = createTLSSocketFactory(caFolder, clientCertFolder, clientKeyFolder); - } - if (dtlsContext == null) { - dtlsContext = createDTLSContext(caFolder, clientCertFolder, clientKeyFolder); - } - } catch (Exception e) { - throw new RuntimeException("Failed to create TLS/DTLS client configuration", e); - } + if (enabledProtocols.contains(NetworkProtocol.TCP)) { + if (tcpEndpoint == null) throw new IllegalStateException("tcpEndpoint not set (TCP enabled)"); + if (tlsSocketFactory == null) throw new IllegalStateException("tlsSocketFactory not set (TLS mandatory)"); } - if (tlsSocketFactory == null) throw new IllegalStateException("TLS socket factory missing"); - if (dtlsContext == null) throw new IllegalStateException("DTLS context missing"); + if (enabledProtocols.contains(NetworkProtocol.UDP)) { + if (udpEndpoint == null) throw new IllegalStateException("udpEndpoint not set (UDP enabled)"); + if (dtlsContext == null) throw new IllegalStateException("dtlsContext not set (DTLS mandatory for UDP)"); + } + + if (timeoutMillis <= 0) throw new IllegalStateException("timeoutMillis must be > 0"); + if (mtu < 256) throw new IllegalStateException("mtu too small"); + + // Placeholders are never used when protocol is disabled, but must be non-null for constructor invariants. + Endpoint tcpEp = tcpEndpoint != null ? tcpEndpoint : new Endpoint("127.0.0.1", 1); + Endpoint udpEp = udpEndpoint != null ? udpEndpoint : new Endpoint("127.0.0.1", 1); + SSLContext dtls = dtlsContext != null ? dtlsContext : emptyDtls(); return new NetworkClient( - host, - tcpPort, - udpPort, - packetHandler, - eventManager, - logger, - timeout, - tlsSocketFactory, + tcpEp, + udpEp, + enabledProtocols, + codec, + tlsSocketFactory != null ? tlsSocketFactory : (SSLSocketFactory) SSLSocketFactory.getDefault(), tlsParameters, - dtlsContext, - proxy + dtls, + timeoutMillis, + mtu, + eventManager ); } + + private static SSLContext emptyDtls() { + try { + return SSLContext.getInstance("DTLS"); + } catch (Exception e) { + throw new IllegalStateException("Failed to create DTLS context", e); + } + } } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedEvent.java index 6e24468..47e3da8 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedEvent.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.client.events.packets.receive; 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 received by the client on a specific transport. + * Fired when a packet was received by the client on a specific protocol. */ public final class C_PacketReceivedEvent extends Event { - private final NetworkClient networkClient; + private final NetworkClient client; private final Packet packet; - private final Transport transport; + private final NetworkProtocol protocol; /** * Creates a new packet received event. * - * @param networkClient client instance - * @param packet received packet - * @param transport transport the packet was received on + * @param client client instance + * @param packet received packet + * @param protocol protocol the packet was received on */ - public C_PacketReceivedEvent(NetworkClient networkClient, Packet packet, Transport transport) { - this.networkClient = networkClient; - this.packet = packet; - this.transport = transport; + public C_PacketReceivedEvent(NetworkClient client, Packet packet, NetworkProtocol protocol) { + this.client = Objects.requireNonNull(client, "client"); + this.packet = Objects.requireNonNull(packet, "packet"); + 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 packet. + * + * @return packet + */ public Packet getPacket() { return packet; } - public Transport getTransport() { - return transport; + /** + * Returns the protocol the packet was received on. + * + * @return protocol + */ + public NetworkProtocol getProtocol() { + return protocol; } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedFailedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedFailedEvent.java index 4a3326c..61aad33 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedFailedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedFailedEvent.java @@ -6,52 +6,79 @@ * 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.receive; 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 receive failed on the client on a specific transport. + * Fired when receiving or decoding a packet failed on the client + * for a specific protocol. */ public final class C_PacketReceivedFailedEvent extends Event { - private final NetworkClient networkClient; + private final NetworkClient client; private final Packet packet; - private final Transport transport; + private final NetworkProtocol protocol; + private final Exception error; /** * Creates a new packet receive failed event. * - * @param networkClient client instance - * @param packet packet that failed to be read/handled - * @param transport transport the packet was received on + * @param client client instance + * @param packet packet that failed to be processed (may be null if undecodable) + * @param protocol protocol the failure happened on + * @param error root cause */ - public C_PacketReceivedFailedEvent(NetworkClient networkClient, Packet packet, Transport transport) { - this.networkClient = networkClient; - this.packet = packet; - this.transport = transport; + public C_PacketReceivedFailedEvent( + NetworkClient client, + Packet packet, + NetworkProtocol protocol, + Exception error + ) { + this.client = Objects.requireNonNull(client, "client"); + this.packet = packet; // may be null + this.protocol = Objects.requireNonNull(protocol, "protocol"); + this.error = Objects.requireNonNull(error, "error"); } - public NetworkClient getNetworkClient() { - return networkClient; + /** + * Returns the client instance. + * + * @return client + */ + public NetworkClient getClient() { + return client; } + /** + * Returns the packet that failed to be processed, if available. + * + * @return packet or null + */ public Packet getPacket() { return packet; } - public Transport getTransport() { - return transport; + /** + * Returns the protocol the failure occurred on. + * + * @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/client/events/packets/receive/C_UnknownObjectReceivedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_UnknownObjectReceivedEvent.java index f748218..33e83f9 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_UnknownObjectReceivedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_UnknownObjectReceivedEvent.java @@ -6,51 +6,65 @@ * 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.receive; import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; + +import java.util.Objects; /** - * Fired when an unknown object was received on a specific transport. + * Fired when an unknown (non-packet) object was received on a specific protocol. + * + *

Note: 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: + *

    + *
  • {@link NetworkProtocol#TCP} → TLS/TCP
  • + *
  • {@link NetworkProtocol#UDP} → DTLS/UDP
  • + *
*/ public final class C_PacketSendEvent extends Event { - private final NetworkClient networkClient; + private final NetworkClient client; private final Packet packet; - private final Transport transport; + private final NetworkProtocol protocol; /** * Creates a new packet send event. * - * @param networkClient client instance - * @param packet sent packet - * @param transport used transport + * @param client client instance + * @param packet sent packet + * @param protocol used protocol */ - public C_PacketSendEvent(NetworkClient networkClient, Packet packet, Transport transport) { - this.networkClient = networkClient; - this.packet = packet; - this.transport = transport; + public C_PacketSendEvent( + NetworkClient client, + Packet packet, + NetworkProtocol protocol + ) { + this.client = Objects.requireNonNull(client, "client"); + this.packet = Objects.requireNonNull(packet, "packet"); + this.protocol = Objects.requireNonNull(protocol, "protocol"); } - public NetworkClient getNetworkClient() { - return networkClient; + /** + * Returns the client. + * + * @return client + */ + public NetworkClient getClient() { + return client; } + /** + * Returns the sent 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/client/events/packets/send/C_PacketSendFailedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendFailedEvent.java index ee63090..e415816 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendFailedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_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.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 send failed on the client on a specific transport. + * Fired when a packet send failed on the client on a specific protocol. */ public final class C_PacketSendFailedEvent extends Event { - private final NetworkClient networkClient; + private final NetworkClient client; private final Packet packet; - private final Transport transport; + private final NetworkProtocol protocol; + private final Exception error; /** * Creates a new packet send failed event. * - * @param networkClient client instance - * @param packet packet that failed to be sent - * @param transport intended transport + * @param client client instance + * @param packet packet that failed to be sent + * @param protocol intended protocol + * @param error root cause */ - public C_PacketSendFailedEvent(NetworkClient networkClient, Packet packet, Transport transport) { - this.networkClient = networkClient; - this.packet = packet; - this.transport = transport; + public C_PacketSendFailedEvent( + NetworkClient client, + Packet packet, + NetworkProtocol protocol, + Exception error + ) { + this.client = Objects.requireNonNull(client, "client"); + this.packet = Objects.requireNonNull(packet, "packet"); + this.protocol = Objects.requireNonNull(protocol, "protocol"); + this.error = Objects.requireNonNull(error, "error"); } - public NetworkClient getNetworkClient() { - return networkClient; + /** + * Returns the client. + * + * @return client + */ + public NetworkClient getClient() { + return client; } + /** + * Returns the packet that failed to be sent. + * + * @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/client/events/state/connect/ClientConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientConnectedEvent.java index d0d0643..db290f1 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientConnectedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientConnectedEvent.java @@ -6,53 +6,37 @@ * 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 - */ - -/* - * 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.state.connect; import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; import java.util.Objects; /** - * Fired when the client established a specific transport connection. + * Fired when the client established a specific protocol connection. * - *

This event is transport-specific: + *

Protocol-specific: *

    - *
  • {@link Transport#TCP}: after TLS handshake + TCP streams are ready
  • - *
  • {@link Transport#UDP}: after DTLS handshake + UDP bind was initiated
  • + *
  • {@link NetworkProtocol#TCP}: after TLS handshake + TCP streams are ready
  • + *
  • {@link NetworkProtocol#UDP}: after DTLS handshake (channel/engine ready)
  • *
*/ public final class ClientConnectedEvent extends Event { private final NetworkClient client; - private final Transport transport; + private final NetworkProtocol protocol; /** * Creates a new client connected event. * - * @param client client instance - * @param transport connected transport + * @param client client instance + * @param protocol connected protocol */ - public ClientConnectedEvent(NetworkClient client, Transport transport) { + public ClientConnectedEvent(NetworkClient client, NetworkProtocol protocol) { this.client = Objects.requireNonNull(client, "client"); - this.transport = Objects.requireNonNull(transport, "transport"); + this.protocol = Objects.requireNonNull(protocol, "protocol"); } /** @@ -65,11 +49,11 @@ public final class ClientConnectedEvent extends Event { } /** - * Returns the transport that was connected. + * Returns the protocol that was connected. * - * @return transport + * @return protocol */ - public Transport getTransport() { - return transport; + public NetworkProtocol getProtocol() { + return protocol; } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientFullyConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientFullyConnectedEvent.java index d836019..660691c 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientFullyConnectedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientFullyConnectedEvent.java @@ -6,47 +6,38 @@ * 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 - */ - -/* - * 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.state.connect; import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; + +import java.util.EnumSet; +import java.util.Objects; /** - * Fired when the client has fully established the configured transport policy. + * Fired when the client satisfies its enabled protocol requirements. * - *

For your setup this means: TCP (TLS) connected, ClientID received and UDP (DTLS) bound.

+ *

In v2 this typically means: + *

    + *
  • TCP is connected (TLS ok)
  • + *
  • and if UDP is enabled: DTLS is established and the bind flow is completed
  • + *
*/ public final class ClientFullyConnectedEvent extends Event { private final NetworkClient client; - private final Transport[] requiredTransports; + private final EnumSet requiredProtocols; /** * Creates a new event. * - * @param client client instance - * @param requiredTransports transports that are required and now satisfied + * @param client client instance + * @param requiredProtocols protocols that are required and now satisfied */ - public ClientFullyConnectedEvent(NetworkClient client, Transport[] requiredTransports) { - this.client = client; - this.requiredTransports = requiredTransports; + public ClientFullyConnectedEvent(NetworkClient client, EnumSet requiredProtocols) { + this.client = Objects.requireNonNull(client, "client"); + this.requiredProtocols = EnumSet.copyOf(Objects.requireNonNull(requiredProtocols, "requiredProtocols")); } /** @@ -59,11 +50,11 @@ public final class ClientFullyConnectedEvent extends Event { } /** - * Returns the required transports that are now established. + * Returns the required protocols that are now established. * - * @return required transports + * @return required protocols (copy) */ - public Transport[] getRequiredTransports() { - return requiredTransports; + public EnumSet getRequiredProtocols() { + return EnumSet.copyOf(requiredProtocols); } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientDisconnectedEvent.java index cac7034..59bb34b 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientDisconnectedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientDisconnectedEvent.java @@ -6,53 +6,37 @@ * 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 - */ - -/* - * 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.state.disconnect; import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; import java.util.Objects; /** - * Fired when the client lost/closed a specific transport connection. + * Fired when the client lost/closed a specific protocol connection. * - *

This event is transport-specific: + *

Protocol-specific: *

    - *
  • {@link Transport#TCP}: TCP/TLS socket closed or receive loop ended
  • - *
  • {@link Transport#UDP}: UDP channel/DTLS endpoint closed
  • + *
  • {@link NetworkProtocol#TCP}: TCP/TLS socket closed or receive loop ended
  • + *
  • {@link NetworkProtocol#UDP}: UDP channel/DTLS endpoint closed
  • *
*/ public final class ClientDisconnectedEvent extends Event { private final NetworkClient client; - private final Transport transport; + private final NetworkProtocol protocol; /** * Creates a new client disconnected event. * - * @param client client instance - * @param transport disconnected transport + * @param client client instance + * @param protocol disconnected protocol */ - public ClientDisconnectedEvent(NetworkClient client, Transport transport) { + public ClientDisconnectedEvent(NetworkClient client, NetworkProtocol protocol) { this.client = Objects.requireNonNull(client, "client"); - this.transport = Objects.requireNonNull(transport, "transport"); + this.protocol = Objects.requireNonNull(protocol, "protocol"); } /** @@ -65,11 +49,11 @@ public final class ClientDisconnectedEvent extends Event { } /** - * Returns the transport that was disconnected. + * Returns the protocol that was disconnected. * - * @return transport + * @return protocol */ - public Transport getTransport() { - return transport; + public NetworkProtocol getProtocol() { + return protocol; } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientFullyDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientFullyDisconnectedEvent.java index 278530a..b78e0b2 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientFullyDisconnectedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientFullyDisconnectedEvent.java @@ -6,51 +6,32 @@ * 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 - */ - -/* - * 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.state.disconnect; import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; +import java.util.EnumSet; import java.util.Objects; /** - * Fired when the client was fully connected and then became fully disconnected. - * - *

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 EnumSet requiredProtocols; /** * Creates a new fully disconnected event. * - * @param client client instance - * @param requiredTransports required transports according to policy + * @param client client instance + * @param requiredProtocols protocols that were required for full connectivity */ - public ClientFullyDisconnectedEvent(NetworkClient client, Transport[] requiredTransports) { + public ClientFullyDisconnectedEvent(NetworkClient client, EnumSet requiredProtocols) { this.client = Objects.requireNonNull(client, "client"); - this.requiredTransports = Objects.requireNonNull(requiredTransports, "requiredTransports"); + this.requiredProtocols = EnumSet.copyOf(Objects.requireNonNull(requiredProtocols, "requiredProtocols")); } /** @@ -63,11 +44,11 @@ public final class ClientFullyDisconnectedEvent extends Event { } /** - * Returns transports that were required for full connectivity. + * Returns protocols that were required for full connectivity. * - * @return required transports + * @return required protocols (copy) */ - public Transport[] getRequiredTransports() { - return requiredTransports; + public EnumSet getRequiredProtocols() { + return EnumSet.copyOf(requiredProtocols); } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/Packet.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/Packet.java index a8b9de5..a226c86 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/Packet.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/Packet.java @@ -1,22 +1,62 @@ +/* + * 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.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +/** + * Base type for packets used by the v2 secure network system. + * + *

Packets 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 packets = new HashMap<>(); - - private NetworkClient clientInstance; - private NetworkServer serverInstance; - - public NetworkClient getClientInstance() { - return clientInstance; - } - - public void setClientInstance(NetworkClient clientInstance) { - if (this.clientInstance == null) this.clientInstance = clientInstance; - } - - public NetworkServer getServerInstance() { - return serverInstance; - } - - public void setServerInstance(NetworkServer serverInstance) { - if (this.serverInstance == null) this.serverInstance = serverInstance; - } - - public boolean isPacketIDRegistered(int id) { - return packets.containsKey(id); - } - - public Packet getPacketByID(int id) { - return packets.get(id); - } - - public boolean registerPacket(Packet packet) { - int id = packet.getPacketID(); - - if (isPacketIDRegistered(id)) return false; - - packets.put(id, packet); - return true; - } - - public boolean handlePacket(int id, Packet packet, ObjectInputStream inputStream) throws IOException, ClassNotFoundException { - if (!isPacketIDRegistered(id) || (packet != null && id != packet.getPacketID()) || (packet != null && !isPacketIDRegistered(packet.getPacketID()))) - return false; - - packet.read(this, inputStream); - return true; - } - - public boolean sendPacket(Packet packet, ObjectOutputStream outputStream) throws IOException, ClassNotFoundException { - int id = packet.getPacketID(); - if (!isPacketIDRegistered(id)) return false; - - outputStream.writeObject(id); - packet.write(this, outputStream); - outputStream.flush(); - - return true; - } -} \ No newline at end of file diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketRegistry.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketRegistry.java new file mode 100644 index 0000000..bbd0b33 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketRegistry.java @@ -0,0 +1,78 @@ +/* + * 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 dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.*; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * Thread-safe registry that maps packet ids to factories creating new packet instances. + * + *

This avoids reusing the same packet instance across threads/connections.

+ */ +public final class PacketRegistry { + + private final Map> factories = new ConcurrentHashMap<>(); + + public PacketRegistry() { + register(ConnectionIdPacket::new); + register(ProtocolRequirementsPacket::new); + register(UdpBindAckPacket::new); + register(UdpBindPacket::new); + register(UdpHelloPacket::new); + } + + /** + * Registers a packet factory for the given id. + * + * @param factory instance factory + * @return true if newly registered, false if id already present + */ + public boolean register(Supplier factory) { + Objects.requireNonNull(factory, "factory"); + return factories.putIfAbsent(factory.get().packetId(), factory) == null; + } + + /** + * Returns true if a packet id is registered. + * + * @param packetId id + * @return true if registered + */ + public boolean isRegistered(int packetId) { + return factories.containsKey(packetId); + } + + /** + * Creates a new packet instance for the given id. + * + * @param packetId id + * @return new packet instance + * @throws IllegalArgumentException if id is not registered or factory returns null + */ + public Packet create(int packetId) { + Supplier supplier = factories.get(packetId); + if (supplier == null) { + throw new IllegalArgumentException("Unknown packetId: " + packetId); + } + Packet p = supplier.get(); + if (p == null) { + throw new IllegalArgumentException("Packet factory returned null for packetId: " + packetId); + } + if (p.packetId() != packetId) { + throw new IllegalArgumentException("Packet factory produced mismatching id: expected=" + + packetId + ", got=" + p.packetId()); + } + return p; + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ClientIDPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ClientIDPacket.java deleted file mode 100644 index 07eb3cc..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ClientIDPacket.java +++ /dev/null @@ -1,58 +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.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.UUID; - -/** - * Transfers the server-assigned {@link ClientID} to the client. - */ -public final class ClientIDPacket extends Packet { - - private ClientID clientId; - - public ClientIDPacket() { - super(0); - } - - public ClientIDPacket(ClientID clientId) { - this(); - this.clientId = clientId; - } - - public ClientID getClientId() { - return clientId; - } - - @Override - public void write(PacketHandler packetHandler, ObjectOutputStream outputStream) throws IOException { - if (clientId == null) throw new IOException("ClientIDPacket.clientId is null"); - UUID uuid = clientId.uuid(); - outputStream.writeLong(uuid.getMostSignificantBits()); - outputStream.writeLong(uuid.getLeastSignificantBits()); - } - - @Override - public void read(PacketHandler packetHandler, ObjectInputStream inputStream) throws IOException { - long msb = inputStream.readLong(); - long lsb = inputStream.readLong(); - this.clientId = new ClientID(new UUID(msb, lsb)); - - if (packetHandler.getClientInstance() != null) { - packetHandler.getClientInstance().setClientId(this.clientId); - } - } -} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ConnectionIdPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ConnectionIdPacket.java new file mode 100644 index 0000000..af0ad2d --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ConnectionIdPacket.java @@ -0,0 +1,70 @@ +/* + * 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 java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Objects; +import java.util.UUID; + +/** + * Server -> Client packet that assigns a server-side connection id. + * + *

This 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 EnumSet requiredProtocols = EnumSet.noneOf(NetworkProtocol.class); + + /** + * Constructs an empty packet for deserialization. + */ + public ProtocolRequirementsPacket() { + super(2); + } + + /** + * Constructs the packet. + * + * @param requiredProtocols required protocol set + */ + public ProtocolRequirementsPacket(EnumSet requiredProtocols) { + super(2); + this.requiredProtocols = EnumSet.copyOf(Objects.requireNonNull(requiredProtocols, "requiredProtocols")); + } + + /** + * Returns the required protocol set. + * + * @return required protocols + */ + public EnumSet requiredProtocols() { + return EnumSet.copyOf(requiredProtocols); + } + + @Override + public void write(ObjectOutputStream out) throws IOException { + Objects.requireNonNull(out, "out"); + out.writeInt(toMask(requiredProtocols)); + } + + @Override + public void read(ObjectInputStream in) throws IOException { + Objects.requireNonNull(in, "in"); + int mask = in.readInt(); + this.requiredProtocols = fromMask(mask); + } + + private static int toMask(EnumSet set) { + int mask = 0; + if (set.contains(NetworkProtocol.TCP)) mask |= 1; + if (set.contains(NetworkProtocol.UDP)) mask |= 2; + return mask; + } + + private static EnumSet fromMask(int mask) { + EnumSet set = EnumSet.noneOf(NetworkProtocol.class); + if ((mask & 1) != 0) set.add(NetworkProtocol.TCP); + if ((mask & 2) != 0) set.add(NetworkProtocol.UDP); + return set; + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindAckPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindAckPacket.java new file mode 100644 index 0000000..610728b --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindAckPacket.java @@ -0,0 +1,71 @@ +/* + * 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 java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Objects; +import java.util.UUID; + +/** + * Server -> client acknowledgement for {@link UdpBindPacket}. + * + *

The 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: + *

    + *
  1. Client connects via TCP/TLS
  2. + *
  3. Server sends {@link ConnectionIdPacket} over TCP
  4. + *
  5. Client completes DTLS handshake and sends {@link UdpBindPacket} over UDP/DTLS with the same connection id
  6. + *
  7. Server attaches UDP remote to the matching {@code SecureConnection}
  8. + *
*/ public final class UdpBindPacket extends Packet { - public static final int PACKET_ID = 1; - - private ClientID clientId; + private UUID connectionId; + /** + * Creates an empty packet instance for decoding. + */ public UdpBindPacket() { - super(PACKET_ID); + super(4); } - public UdpBindPacket(ClientID clientId) { + /** + * Creates a bind packet for sending. + * + * @param connectionId server-assigned connection id (must not be null) + */ + public UdpBindPacket(UUID connectionId) { this(); - this.clientId = clientId; + this.connectionId = Objects.requireNonNull(connectionId, "connectionId"); } - public ClientID getClientId() { - return clientId; + /** + * Returns the connection id this UDP endpoint wants to bind to. + * + * @return connection id + */ + public UUID connectionId() { + return connectionId; } @Override - public void write(PacketHandler packetHandler, ObjectOutputStream outputStream) throws IOException { - if (clientId == null) throw new IOException("UdpBindPacket.clientId is null"); - UUID uuid = clientId.uuid(); - outputStream.writeLong(uuid.getMostSignificantBits()); - outputStream.writeLong(uuid.getLeastSignificantBits()); - } - - @Override - public void read(PacketHandler packetHandler, ObjectInputStream inputStream) throws IOException { - long msb = inputStream.readLong(); - long lsb = inputStream.readLong(); - this.clientId = new ClientID(new UUID(msb, lsb)); - // Server-side binding is handled by UDP receiver, because it knows the remote address. - if (packetHandler.getClientInstance() != null) { - packetHandler.getClientInstance().setClientId(this.clientId); + public void write(ObjectOutputStream out) throws IOException { + if (connectionId == null) { + throw new IOException("UdpBindPacket.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/UdpHelloPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpHelloPacket.java new file mode 100644 index 0000000..659c662 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpHelloPacket.java @@ -0,0 +1,44 @@ +/* + * 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 java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Objects; + +/** + * Client -> server packet used for UDP-only bootstrap. + * + *

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 EnumSet requiredProtocols() { + return protocolMode.required(); + } + + /** + * Returns true if the TCP/TLS channel is currently connected. + * + * @return true if TCP connected + */ + public boolean isTcpConnected() { + if (closed.get()) return false; + SSLSocket s = tcpSocket; + return s != null && !s.isClosed(); + } + + /** + * Returns true if the UDP/DTLS channel is currently connected and bound. + * + * @return true if UDP connected + */ + public boolean isUdpConnected() { + if (closed.get()) return false; + if (!udpBound) return false; + if (udpRemote == null || dtlsEndpoint == null || udpSession == null) return false; + return udpSession.isHandshakeComplete(); + } + + /** + * Returns true if this connection satisfies the server required protocol set. + * + * @return true if fully connected according to {@link ServerProtocolMode#required()} + */ + public boolean isFullyConnected() { + EnumSet required = protocolMode.required(); + boolean tcpOk = !required.contains(NetworkProtocol.TCP) || isTcpConnected(); + boolean udpOk = !required.contains(NetworkProtocol.UDP) || isUdpConnected(); + return tcpOk && udpOk; + } + + /** + * Returns the currently bound UDP remote, or null. + * + * @return udp remote + */ + public SocketAddress udpRemote() { + return udpRemote; + } + + /** + * Sets whether UDP is bound/acknowledged. + * + * @param udpBound bound flag + */ + public void setUdpBound(boolean udpBound) { + this.udpBound = udpBound; + } + + /** + * Attaches the DTLS side (after bind/hello flow). + * + * @param udpRemote remote address + * @param udpSession DTLS session + * @param dtlsEndpoint DTLS endpoint + * @param udpChannel UDP channel (server channel) + */ + public void attachUdp( + SocketAddress udpRemote, + DtlsEndpoint.DtlsSession udpSession, + DtlsEndpoint dtlsEndpoint, + DatagramChannel udpChannel + ) { + this.udpRemote = Objects.requireNonNull(udpRemote, "udpRemote"); + this.udpSession = Objects.requireNonNull(udpSession, "udpSession"); + this.dtlsEndpoint = Objects.requireNonNull(dtlsEndpoint, "dtlsEndpoint"); + this.udpChannel = Objects.requireNonNull(udpChannel, "udpChannel"); + } + + /** + * Sends a packet via the selected protocol (TCP/TLS or UDP/DTLS). + * + *

Emits: + *

    + *
  • {@link S_PacketSendEvent} on success
  • + *
  • {@link S_PacketSendFailedEvent} on failure
  • + *
+ * + * @param packet packet to send + * @param protocol protocol to use + * @return true if sent, false if not possible/unsupported/not ready/closed + * @throws IOException on I/O errors + */ + public boolean sendPacket(Packet packet, NetworkProtocol protocol) throws IOException { + Objects.requireNonNull(packet, "packet"); + Objects.requireNonNull(protocol, "protocol"); + + if (closed.get()) return false; + + try { + boolean ok = switch (protocol) { + case TCP -> sendTcp(packet); + case UDP -> sendUdp(packet); + }; + + if (ok) { + eventManager.executeEvent(new S_PacketSendEvent(packet, this, protocol)); + } else { + eventManager.executeEvent(new S_PacketSendFailedEvent( + packet, this, protocol, new IllegalStateException("Packet not sent") + )); + } + + return ok; + } catch (IOException | RuntimeException e) { + eventManager.executeEvent(new S_PacketSendFailedEvent(packet, this, protocol, e)); + throw e; + } + } + + /** + * Receives the next packet from the TCP stream. + * + * @return packet + * @throws IOException on I/O errors + * @throws ClassNotFoundException on deserialization errors + */ + public Packet receiveTcp() throws IOException, ClassNotFoundException { + if (closed.get()) throw new IOException("Connection closed"); + if (tcpIn == null) throw new IOException("TCP not available for this connection"); + return codec.receiveFromStream(tcpIn); + } + + /** + * Returns the TCP output stream (nullable). + * + * @return tcp output stream or null + */ + public ObjectOutputStream tcpOut() { + return tcpOut; + } + + /** + * Returns the TCP input stream (nullable). + * + * @return tcp input stream or null + */ + public ObjectInputStream tcpIn() { + return tcpIn; + } + + /** + * Closes this connection (TCP socket if present). UDP association is cleared. + * + * @throws IOException on close errors + */ + public void close() throws IOException { + if (!closed.compareAndSet(false, true)) return; + + 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) { + } + + udpRemote = null; + udpSession = null; + dtlsEndpoint = null; + udpChannel = null; + udpBound = false; + } + + private boolean sendTcp(Packet packet) throws IOException { + if (!isTcpConnected()) return false; + if (tcpOut == null) return false; + codec.sendToStream(packet, tcpOut); + return true; + } + + private boolean sendUdp(Packet packet) throws IOException { + if (closed.get()) return false; + + SocketAddress remote = udpRemote; + DtlsEndpoint endpoint = dtlsEndpoint; + if (remote == null || endpoint == null) return false; + + // Allow sending bind/hello/ack even if udpBound==false (handshake must still be complete). + DtlsEndpoint.DtlsSession s = udpSession; + if (s == null || !s.isHandshakeComplete()) return false; + + ByteBuffer buf = codec.encodeToBuffer(packet); + endpoint.sendApplication(remote, buf); + return true; + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ConnectionHandler.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ConnectionHandler.java deleted file mode 100644 index 0e4f296..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ConnectionHandler.java +++ /dev/null @@ -1,275 +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.server; - -import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; -import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.ClientIDPacket; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_PacketReceivedEvent; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_PacketReceivedFailedEvent; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_UnknownObjectReceivedEvent; -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.server.events.state.connect.ConnectionHandlerConnectedEvent; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect.ConnectionHandlerDisconnectedEvent; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect.ConnectionHandlerFullyDisconnectedEvent; -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 javax.net.ssl.SSLSocket; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.net.SocketAddress; -import java.net.SocketException; -import java.nio.ByteBuffer; -import java.util.Objects; - -/** - * Handles one connected client socket and dispatches received packets. - * - *

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: - *

    - *
  • TCP only
  • - *
  • UDP only
  • - *
  • both supported
  • - *
  • both required simultaneously
  • - *
+ *

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 List connectionHandlers = Collections.synchronizedList(new ArrayList<>()); - private final Map handlerByClientId = new ConcurrentHashMap<>(); - private SSLServerSocket tcpServerSocket; - private DatagramChannel udpChannel; - private DtlsEndpoint dtlsEndpoint; + private volatile SSLServerSocket tcpServerSocket; + private volatile DatagramChannel udpChannel; + private volatile DtlsEndpoint dtlsEndpoint; + private volatile Thread tcpAcceptThread; private volatile Thread udpThread; + private final Map connectionsById = new ConcurrentHashMap<>(); + private final Map connectionsByUdpRemote = new ConcurrentHashMap<>(); + private final List connections = java.util.Collections.synchronizedList(new ArrayList<>()); + private NetworkServer( + ServerProtocolMode protocolMode, + ClientAuthMode clientAuthMode, + PacketCodec codec, + EventManager eventManager, int tcpPort, int udpPort, - PacketHandler packetHandler, - EventManager eventManager, - Logger logger, - int timeout, - SSLServerSocketFactory tlsFactory, - SSLContext dtlsContext, - TransportPolicy transportPolicy, - boolean requireClientCert + int timeoutMillis, + int mtu, + SSLServerSocketFactory tlsServerSocketFactory, + SSLParameters tlsParameters, + SSLContext dtlsContext ) { + this.protocolMode = Objects.requireNonNull(protocolMode, "protocolMode"); + this.clientAuthMode = Objects.requireNonNull(clientAuthMode, "clientAuthMode"); + this.codec = Objects.requireNonNull(codec, "codec"); + this.eventManager = Objects.requireNonNull(eventManager, "eventManager"); this.tcpPort = tcpPort; this.udpPort = udpPort; - - this.packetHandler = Objects.requireNonNull(packetHandler, "packetHandler"); - this.eventManager = Objects.requireNonNull(eventManager, "eventManager"); - this.logger = logger; - - this.timeout = timeout; - this.tlsServerSocketFactory = tlsFactory; - this.dtlsContext = dtlsContext; - - this.transportPolicy = Objects.requireNonNull(transportPolicy, "transportPolicy"); - this.requireClientCert = requireClientCert; - - this.packetHandler.setServerInstance(this); - this.packetHandler.registerPacket(new ClientIDPacket()); - this.packetHandler.registerPacket(new UdpBindPacket()); + this.timeoutMillis = timeoutMillis; + this.mtu = mtu; + this.tlsServerSocketFactory = Objects.requireNonNull(tlsServerSocketFactory, "tlsServerSocketFactory"); + this.tlsParameters = tlsParameters; + this.dtlsContext = Objects.requireNonNull(dtlsContext, "dtlsContext"); } /** - * Returns the configured transport policy. + * Starts the server. * - * @return policy + * @return true if started, false if already started + * @throws Exception on startup errors */ - public TransportPolicy getTransportPolicy() { - return transportPolicy; - } + public synchronized boolean start() throws Exception { + if (tcpAcceptThread != null || udpThread != null) return false; - /** - * Returns the TCP port (TLS). - * - * @return tcp port - */ - public int getTcpPort() { - return tcpPort; - } + // FIX: only required exists -> listener start is based on required() + EnumSet required = protocolMode.required(); - /** - * Returns the UDP port (DTLS). - * - * @return udp port - */ - public int getUdpPort() { - return udpPort; - } - - /** - * Returns packet handler. - * - * @return packet handler - */ - public PacketHandler getPacketHandler() { - return packetHandler; - } - - /** - * Returns event manager. - * - * @return event manager - */ - public EventManager getEventManager() { - return eventManager; - } - - /** - * Returns logger. - * - * @return logger (may be null) - */ - public Logger getLogger() { - return logger; - } - - /** - * Returns current DTLS endpoint instance (may be null if UDP not supported or not started). - * - * @return dtls endpoint - */ - public DtlsEndpoint getDtlsEndpoint() { - return dtlsEndpoint; - } - - /** - * Returns live connection handlers. - * - * @return list of handlers - */ - public List getConnectionHandlers() { - return connectionHandlers; - } - - /** - * Looks up a handler by client id. - * - * @param clientId client id - * @return handler or null - */ - public ConnectionHandler getConnectionHandlerByClientId(ClientID clientId) { - return handlerByClientId.get(clientId); - } - - /** - * Starts the server according to the configured transport policy. - * - * @return true if started successfully - */ - public synchronized boolean start() { - try { - if (transportPolicy.supported().contains(Transport.TCP)) { - if (tlsServerSocketFactory == null) { - throw new IllegalStateException("TLS ServerSocketFactory missing (TCP supported)."); - } - if (tcpPort <= 0) { - throw new IllegalStateException("TCP port not set (TCP supported)."); - } - - tcpServerSocket = (SSLServerSocket) tlsServerSocketFactory.createServerSocket(tcpPort); - tcpServerSocket.setNeedClientAuth(requireClientCert); - tcpServerSocket.setSoTimeout(timeout); - tcpServerSocket.setEnabledProtocols(new String[]{"TLSv1.3"}); - - tcpAcceptThread = new Thread(this::acceptTcpLoop, "NetworkServer-TCP-Acceptor"); - tcpAcceptThread.start(); - } - - if (transportPolicy.supported().contains(Transport.UDP)) { - if (dtlsContext == null) { - throw new IllegalStateException("DTLS SSLContext missing (UDP supported)."); - } - if (udpPort <= 0) { - throw new IllegalStateException("UDP port not set (UDP supported)."); - } - - udpChannel = DatagramChannel.open(); - udpChannel.bind(new InetSocketAddress(udpPort)); - udpChannel.configureBlocking(false); - - dtlsEndpoint = new DtlsEndpoint( - udpChannel, - dtlsContext, - false, - 1400, - timeout, - this::onDtlsApplicationData - ); - - udpThread = new Thread(this::udpLoop, "NetworkServer-UDP-DTLS"); - udpThread.start(); - } - - logInfo("Server started. tcp=" + tcpPort + ", udp=" + udpPort - + ", supported=" + transportPolicy.supported() - + ", required=" + transportPolicy.required()); - return true; - } catch (Exception e) { - logException("Failed to start", e); - stop(); - return false; + if (required.contains(NetworkProtocol.TCP)) { + if (tcpPort <= 0) throw new IllegalStateException("tcpPort not set (TCP required)"); + startTcp(); } + + if (required.contains(NetworkProtocol.UDP)) { + if (udpPort <= 0) throw new IllegalStateException("udpPort not set (UDP required)"); + startUdp(); + } + + return true; } - /** - * Stops the server and disconnects clients. - * - * @return true if stopped successfully - */ + public synchronized boolean stop() { - for (ConnectionHandler h : new ArrayList<>(connectionHandlers)) { + boolean wasRunning = (tcpAcceptThread != null) || (udpThread != null); + + Thread t1 = tcpAcceptThread; + Thread t2 = udpThread; + if (t1 != null) t1.interrupt(); + if (t2 != null) t2.interrupt(); + + tcpAcceptThread = null; + udpThread = null; + + try { + if (tcpServerSocket != null) tcpServerSocket.close(); + } catch (Exception ignored) { + } finally { + tcpServerSocket = null; + } + + try { + if (udpChannel != null) udpChannel.close(); + } catch (Exception ignored) { + } finally { + udpChannel = null; + dtlsEndpoint = null; + } + + EnumSet required = protocolMode.required(); + + for (ClientConnection c : new ArrayList<>(connections)) { + boolean wasFully = isFullyConnected(c); + + boolean tcpWas = isConnected(c, NetworkProtocol.TCP); + boolean udpWas = isConnected(c, NetworkProtocol.UDP); + + SocketAddress udpRemote = c.udpRemote(); + try { - h.disconnect(); + c.close(); } catch (Exception ignored) { } + + if (udpRemote != null) { + connectionsByUdpRemote.remove(udpRemote); + } + + if (tcpWas) { + eventManager.executeEvent(new ClientConnectionDisconnectedEvent(c, NetworkProtocol.TCP)); + } + if (udpWas) { + eventManager.executeEvent(new ClientConnectionDisconnectedEvent(c, NetworkProtocol.UDP)); + } + if (wasFully) { + eventManager.executeEvent(new ClientConnectionFullyDisconnectedEvent(c, required)); + } } - try { - if (tcpAcceptThread != null) tcpAcceptThread.interrupt(); - if (udpThread != null) udpThread.interrupt(); + connections.clear(); + connectionsById.clear(); + connectionsByUdpRemote.clear(); - if (tcpServerSocket != null) tcpServerSocket.close(); - tcpServerSocket = null; + return wasRunning; + } - if (udpChannel != null) udpChannel.close(); - udpChannel = null; + public ServerProtocolMode protocolMode() { + return protocolMode; + } - dtlsEndpoint = null; + public boolean isConnected(ClientConnection connection, NetworkProtocol protocol) { + Objects.requireNonNull(connection, "connection"); + Objects.requireNonNull(protocol, "protocol"); - handlerByClientId.clear(); - connectionHandlers.clear(); + return switch (protocol) { + case TCP -> connection.isTcpConnected(); + case UDP -> connection.isUdpConnected(); + }; + } - logInfo("Server stopped"); - return true; - } catch (Exception e) { - logException("Failed to stop", e); - return false; + public boolean isFullyConnected(ClientConnection connection) { + Objects.requireNonNull(connection, "connection"); + return connection.isFullyConnected(); + } + + public List connections() { + synchronized (connections) { + return List.copyOf(connections); } } - private void acceptTcpLoop() { + private void startTcp() throws Exception { + SSLServerSocket serverSocket = (SSLServerSocket) tlsServerSocketFactory.createServerSocket(); + serverSocket.bind(new InetSocketAddress(tcpPort)); + serverSocket.setSoTimeout(timeoutMillis); + + if (clientAuthMode == ClientAuthMode.REQUIRED) { + serverSocket.setNeedClientAuth(true); + } else { + serverSocket.setWantClientAuth(true); + } + + if (tlsParameters != null) { + serverSocket.setSSLParameters(tlsParameters); + } else { + serverSocket.setEnabledProtocols(new String[]{"TLSv1.3"}); + } + + this.tcpServerSocket = serverSocket; + + this.tcpAcceptThread = new Thread(this::tcpAcceptLoop, "SecureNetworkServer-TCP-Accept"); + this.tcpAcceptThread.start(); + } + + private void startUdp() throws Exception { + DatagramChannel ch = DatagramChannel.open(); + ch.bind(new InetSocketAddress(udpPort)); + ch.configureBlocking(false); + + DtlsEndpoint endpoint = new DtlsEndpoint( + ch, + dtlsContext, + false, + mtu, + timeoutMillis, + clientAuthMode, + this::onDtlsApplicationData + ); + + this.udpChannel = ch; + this.dtlsEndpoint = endpoint; + + this.udpThread = new Thread(this::udpLoop, "SecureNetworkServer-UDP-DTLS"); + this.udpThread.start(); + } + + private void tcpAcceptLoop() { try { while (tcpServerSocket != null && !tcpServerSocket.isClosed() && !Thread.currentThread().isInterrupted()) { - Socket socket = tcpServerSocket.accept(); - if (!(socket instanceof SSLSocket ssl)) { - socket.close(); - continue; - } + SSLSocket socket = (SSLSocket) tcpServerSocket.accept(); + socket.setTcpNoDelay(true); + socket.setSoTimeout(timeoutMillis); - ssl.setTcpNoDelay(true); - ssl.setSoTimeout(timeout); - - try { - ssl.startHandshake(); - } catch (Exception handshakeEx) { - logException("TLS handshake failed", handshakeEx); - try { - ssl.close(); - } catch (Exception ignored) { - } - continue; - } - - TCPIncomingConnectionEvent event = new TCPIncomingConnectionEvent(this, ssl); - eventManager.executeEvent(event); - if (event.isCancelled()) { - try { - ssl.close(); - } catch (Exception ignored) { - } + TCPIncomingConnectionEvent incoming = new TCPIncomingConnectionEvent(this, socket); + eventManager.executeEvent(incoming); + if (incoming.isCancelled()) { + safeClose(socket); continue; } try { - ClientID clientId = ClientID.random(); - ConnectionHandler handler = new ConnectionHandler(this, ssl, clientId); - connectionHandlers.add(handler); - handlerByClientId.put(clientId, handler); - - // NOTE: - // TCP connect event is fired inside ConnectionHandler constructor (Transport.TCP). - // FullyConnected will be fired later after UDP bind (if policy requires UDP). - } catch (Exception ex) { - try { - ssl.close(); - } catch (Exception ignored) { - } + socket.startHandshake(); + } catch (Exception e) { + safeClose(socket); + continue; } + + try { + ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); + ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); + + ClientConnection connection = ClientConnection.tcpAccepted(codec, protocolMode, eventManager, socket, out, in); + + connections.add(connection); + connectionsById.put(connection.connectionId(), connection); + + eventManager.executeEvent(new ClientConnectionConnectedEvent(connection, NetworkProtocol.TCP)); + + codec.sendToStream(new ConnectionIdPacket(connection.connectionId()), connection.tcpOut()); + codec.sendToStream(new ProtocolRequirementsPacket(protocolMode.required()), connection.tcpOut()); + + if (isFullyConnected(connection)) { + eventManager.executeEvent(new ClientConnectionFullyConnectedEvent(connection, protocolMode.required())); + } + + Thread rx = new Thread(() -> tcpReceiveLoop(connection), + "SecureNetworkServer-TCP-Rx-" + connection.connectionId()); + rx.start(); + } catch (Exception e) { + safeClose(socket); + } + } + } catch (Exception ignored) { + } + } + + private void tcpReceiveLoop(ClientConnection connection) { + boolean wasFully = isFullyConnected(connection); + + try { + while (!Thread.currentThread().isInterrupted() && connection.isTcpConnected()) { + Packet packet = codec.receiveFromStream(connection.tcpIn()); + if (packet == null) continue; + + eventManager.executeEvent(new S_PacketReceivedEvent(connection, packet, NetworkProtocol.TCP)); } } catch (Exception e) { - if (!Thread.currentThread().isInterrupted()) { - logException("TCP accept loop failed", e); - } + eventManager.executeEvent(new S_PacketReceivedFailedEvent(connection, null, NetworkProtocol.TCP, e)); + } finally { + cleanupConnection(connection, wasFully); } } @@ -341,300 +339,249 @@ public final class NetworkServer { try { while (udpChannel != null && udpChannel.isOpen() && !Thread.currentThread().isInterrupted()) { DtlsEndpoint endpoint = dtlsEndpoint; - if (endpoint != null) { - endpoint.poll(); - } + if (endpoint != null) endpoint.poll(); Thread.onSpinWait(); } - } catch (Exception e) { - if (!Thread.currentThread().isInterrupted()) { - logException("UDP loop failed", e); - } + } catch (Exception ignored) { } } private void onDtlsApplicationData(SocketAddress remote, ByteBuffer data) { + UDPIncomingConnectionEvent incoming = new UDPIncomingConnectionEvent(this, remote, data.asReadOnlyBuffer()); + eventManager.executeEvent(incoming); + if (incoming.isCancelled()) { + return; + } + try { - UDPIncomingConnectionEvent event = - new UDPIncomingConnectionEvent(this, remote, data); - eventManager.executeEvent(event); - if (event.isCancelled()) { + Packet packet = codec.decodeFromBuffer(data); + + if (packet instanceof UdpHelloPacket) { + handleUdpHello(remote); return; } - Packet decoded = UdpPacketCodec.decodeAndHandle(packetHandler, data); - if (decoded == null) { - // No handler associated here; keep null for handler - eventManager.executeEvent(new S_UnknownObjectReceivedEvent(data, null, Transport.UDP)); + if (packet instanceof UdpBindPacket bind) { + handleUdpBind(remote, bind); return; } - // Bind flow: DTLS is up, now bind remote <-> clientId using UdpBindPacket(ClientID) - if (decoded instanceof UdpBindPacket bind) { - ConnectionHandler handler = handlerByClientId.get(bind.getClientId()); - if (handler == null) { - // Unknown/expired client id - ignore - return; - } - - boolean wasFullyConnected = handler.isFullyConnected(); - - // attachUdp fires ConnectionHandlerConnectedEvent(Transport.UDP) internally - DtlsEndpoint endpoint = dtlsEndpoint; - if (endpoint == null) { - return; - } - handler.attachUdp(remote, endpoint.session(remote), endpoint); - - boolean nowFullyConnected = handler.isFullyConnected(); - if (!wasFullyConnected && nowFullyConnected) { - eventManager.executeEvent(new ConnectionHandlerFullyConnectedEvent( - handler, - transportPolicy.required().toArray(new Transport[0]) - )); - } - return; - } - - // Route other UDP packets by already bound remote address - ConnectionHandler bound = findHandlerByUdpRemote(remote); - if (bound != null) { - eventManager.executeEvent(new S_PacketReceivedEvent(bound, decoded, Transport.UDP)); + ClientConnection conn = connectionsByUdpRemote.get(remote); + if (conn != null) { + eventManager.executeEvent(new S_PacketReceivedEvent(conn, packet, NetworkProtocol.UDP)); } } catch (Exception ignored) { - // best effort: drop } } - /** - * Finds a handler by its bound UDP remote address. - * - * @param remote remote address - * @return handler or null - */ - public ConnectionHandler findHandlerByUdpRemote(SocketAddress remote) { - synchronized (connectionHandlers) { - for (ConnectionHandler h : connectionHandlers) { - if (remote != null && remote.equals(h.getUdpRemoteAddress())) { - return h; - } + private void handleUdpHello(SocketAddress remote) { + DtlsEndpoint endpoint = dtlsEndpoint; + DatagramChannel ch = udpChannel; + if (endpoint == null || ch == null) return; + + try { + endpoint.handshake(remote); + + UUID id = UUID.randomUUID(); + ClientConnection conn = ClientConnection.udpOnly(codec, protocolMode, eventManager, id); + + boolean wasFullyBefore = isFullyConnected(conn); + + conn.attachUdp(remote, endpoint.session(remote), endpoint, ch); + conn.setUdpBound(true); + + connections.add(conn); + connectionsById.put(conn.connectionId(), conn); + connectionsByUdpRemote.put(remote, conn); + + eventManager.executeEvent(new ClientConnectionConnectedEvent(conn, NetworkProtocol.UDP)); + + conn.sendPacket(new ConnectionIdPacket(conn.connectionId()), NetworkProtocol.UDP); + conn.sendPacket(new ProtocolRequirementsPacket(protocolMode.required()), NetworkProtocol.UDP); + + boolean fullyNow = isFullyConnected(conn); + if (!wasFullyBefore && fullyNow) { + eventManager.executeEvent(new ClientConnectionFullyConnectedEvent(conn, protocolMode.required())); } - } - return null; - } - - private void logInfo(String msg) { - if (logger != null) logger.log(msg); - else System.out.println(msg); - } - - private void logException(String msg, Exception e) { - if (logger != null) logger.exception(msg, e); - else { - System.err.println(msg + ": " + e.getMessage()); - e.printStackTrace(); + } catch (Exception ignored) { } } - // --- Builder --- - public static class ServerBuilder extends DefaultMethodsOverrider { + private void handleUdpBind(SocketAddress remote, UdpBindPacket bind) { + ClientConnection conn = connectionsById.get(bind.connectionId()); + if (conn == null) return; + + DtlsEndpoint endpoint = dtlsEndpoint; + DatagramChannel ch = udpChannel; + if (endpoint == null || ch == null) return; + + boolean wasFullyBefore = isFullyConnected(conn); + + try { + endpoint.handshake(remote); + + conn.attachUdp(remote, endpoint.session(remote), endpoint, ch); + conn.setUdpBound(true); + + connectionsByUdpRemote.put(remote, conn); + + eventManager.executeEvent(new ClientConnectionConnectedEvent(conn, NetworkProtocol.UDP)); + + conn.sendPacket(new UdpBindAckPacket(conn.connectionId()), NetworkProtocol.UDP); + + boolean fullyNow = isFullyConnected(conn); + if (!wasFullyBefore && fullyNow) { + eventManager.executeEvent(new ClientConnectionFullyConnectedEvent(conn, protocolMode.required())); + } + } catch (Exception ignored) { + } + } + + private void cleanupConnection(ClientConnection connection, boolean wasFully) { + boolean tcpWas = connection.isTcpConnected(); + boolean udpWas = connection.isUdpConnected(); + + SocketAddress udpRemote = connection.udpRemote(); + + try { + connection.close(); + } catch (Exception ignored) { + } + + connections.remove(connection); + connectionsById.remove(connection.connectionId()); + + if (udpRemote != null) { + connectionsByUdpRemote.remove(udpRemote); + } + + if (tcpWas) { + eventManager.executeEvent(new ClientConnectionDisconnectedEvent(connection, NetworkProtocol.TCP)); + } + if (udpWas) { + eventManager.executeEvent(new ClientConnectionDisconnectedEvent(connection, NetworkProtocol.UDP)); + } + if (wasFully) { + eventManager.executeEvent(new ClientConnectionFullyDisconnectedEvent(connection, protocolMode.required())); + } + } + + private static void safeClose(SSLSocket socket) { + try { + socket.close(); + } catch (Exception ignored) { + } + } + + public static final class Builder { + + private ServerProtocolMode protocolMode = ServerProtocolMode.bothRequired(); + private ClientAuthMode clientAuthMode = ClientAuthMode.OPTIONAL; + + private PacketCodec codec; + private EventManager eventManager; private int tcpPort; private int udpPort; - private PacketHandler packetHandler; - private EventManager eventManager; - private Logger logger; + private int timeoutMillis = 5000; + private int mtu = 1400; - private int timeout = 5000; - - private SSLServerSocketFactory tlsFactory; + private SSLServerSocketFactory tlsServerSocketFactory; + private SSLParameters tlsParameters; private SSLContext dtlsContext; - private TransportPolicy transportPolicy = TransportPolicy.bothRequired(); - private boolean requireClientCert; - - private File caFolder; - private File serverCertFile; - private File serverKeyFile; - - /** - * Creates a TLS (TCP) server socket factory using your PEM setup. - * - * @param caFolder root CA folder - * @param serverCert server certificate (PEM) - * @param serverKey server key (PEM) - * @return TLS server socket factory - * @throws Exception if configuration fails - */ - public static SSLServerSocketFactory createTLSServerSocketFactory(File caFolder, File serverCert, File serverKey) throws Exception { - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(null, null); - - int caIndex = 1; - File[] caFiles = caFolder.listFiles((f) -> f.getName().endsWith(".pem")); - if (caFiles != null) { - for (File caFile : caFiles) { - java.security.cert.Certificate cert = PemUtils.loadCertificate(caFile); - trustStore.setCertificateEntry("ca" + (caIndex++), cert); - } - } - - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(trustStore); - - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(null, null); - - java.security.PrivateKey key = PemUtils.loadPrivateKey(serverKey); - java.security.cert.Certificate cert = PemUtils.loadCertificate(serverCert); - keyStore.setKeyEntry("server", key, null, new java.security.cert.Certificate[]{cert}); - - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(keyStore, null); - - SSLContext tls = SSLContext.getInstance("TLSv1.3"); - tls.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - - return tls.getServerSocketFactory(); - } - - /** - * Creates a DTLS SSLContext using the same key/trust setup. - * - * @param caFolder root CA folder - * @param serverCert server certificate (PEM) - * @param serverKey server key (PEM) - * @return DTLS context - * @throws Exception if configuration fails - */ - public static SSLContext createDTLSContext(File caFolder, File serverCert, File serverKey) throws Exception { - KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(null, null); - - int caIndex = 1; - File[] caFiles = caFolder.listFiles((f) -> f.getName().endsWith(".pem")); - if (caFiles != null) { - for (File caFile : caFiles) { - java.security.cert.Certificate cert = PemUtils.loadCertificate(caFile); - trustStore.setCertificateEntry("ca" + (caIndex++), cert); - } - } - - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(trustStore); - - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(null, null); - - java.security.PrivateKey key = PemUtils.loadPrivateKey(serverKey); - java.security.cert.Certificate cert = PemUtils.loadCertificate(serverCert); - keyStore.setKeyEntry("server", key, null, new java.security.cert.Certificate[]{cert}); - - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(keyStore, null); - - return DtlsEndpoint.createDtlsContext(kmf.getKeyManagers(), tmf.getTrustManagers()); - } - - public ServerBuilder setTcpPort(int port) { - this.tcpPort = port; + public Builder setProtocolMode(ServerProtocolMode protocolMode) { + this.protocolMode = protocolMode; return this; } - public ServerBuilder setUdpPort(int port) { - this.udpPort = port; + public Builder setClientAuthMode(ClientAuthMode clientAuthMode) { + this.clientAuthMode = clientAuthMode; return this; } - public ServerBuilder setPacketHandler(PacketHandler handler) { - this.packetHandler = handler; + public Builder setCodec(PacketCodec codec) { + this.codec = codec; return this; } - public ServerBuilder setEventManager(EventManager manager) { - this.eventManager = manager; + public Builder setEventManager(EventManager eventManager) { + this.eventManager = eventManager; return this; } - public ServerBuilder setLogger(Logger logger) { - this.logger = logger; + public Builder setTcpPort(int tcpPort) { + this.tcpPort = tcpPort; return this; } - public ServerBuilder setTimeout(int timeoutMillis) { - this.timeout = timeoutMillis; + public Builder setUdpPort(int udpPort) { + this.udpPort = udpPort; return this; } - public ServerBuilder setTransportPolicy(TransportPolicy policy) { - this.transportPolicy = policy; + public Builder setTimeoutMillis(int timeoutMillis) { + this.timeoutMillis = timeoutMillis; return this; } - public ServerBuilder setRequireClientCertificate(boolean requireClientCertificate) { - this.requireClientCert = requireClientCertificate; + public Builder setMtu(int mtu) { + this.mtu = mtu; return this; } - public ServerBuilder setRootCAFolder(File folder) { - this.caFolder = folder; + public Builder setTlsServerSocketFactory(SSLServerSocketFactory tlsServerSocketFactory) { + this.tlsServerSocketFactory = tlsServerSocketFactory; return this; } - public ServerBuilder setServerCertificate(File certFile, File keyFile) { - this.serverCertFile = certFile; - this.serverKeyFile = keyFile; + public Builder setTlsParameters(SSLParameters tlsParameters) { + this.tlsParameters = tlsParameters; return this; } - public ServerBuilder setTLSServerSocketFactory(SSLServerSocketFactory factory) { - this.tlsFactory = factory; - return this; - } - - public ServerBuilder setDTLSContext(SSLContext dtlsContext) { + public Builder setDtlsContext(SSLContext dtlsContext) { this.dtlsContext = dtlsContext; return this; } public NetworkServer build() { - if (packetHandler == null) throw new IllegalStateException("PacketHandler not set"); - if (eventManager == null) throw new IllegalStateException("EventManager not set"); - if (transportPolicy == null) throw new IllegalStateException("TransportPolicy not set"); + if (codec == null) throw new IllegalStateException("codec not set"); + if (eventManager == null) throw new IllegalStateException("eventManager not set"); + if (protocolMode == null) throw new IllegalStateException("protocolMode not set"); + if (clientAuthMode == null) throw new IllegalStateException("clientAuthMode not set"); - if (transportPolicy.supported().contains(Transport.TCP) && tcpPort <= 0) { - throw new IllegalStateException("TCP port not set"); - } - if (transportPolicy.supported().contains(Transport.UDP) && udpPort <= 0) { - throw new IllegalStateException("UDP port not set"); - } + SSLServerSocketFactory tcpFactory = tlsServerSocketFactory != null + ? tlsServerSocketFactory + : (SSLServerSocketFactory) ServerSocketFactory.getDefault(); - if ((tlsFactory == null || dtlsContext == null) && caFolder != null && serverCertFile != null && serverKeyFile != null) { - try { - if (tlsFactory == null && transportPolicy.supported().contains(Transport.TCP)) { - tlsFactory = createTLSServerSocketFactory(caFolder, serverCertFile, serverKeyFile); - } - if (dtlsContext == null && transportPolicy.supported().contains(Transport.UDP)) { - dtlsContext = createDTLSContext(caFolder, serverCertFile, serverKeyFile); - } - } catch (Exception e) { - throw new RuntimeException("Failed to create TLS/DTLS configuration", e); - } - } + SSLContext dtls = dtlsContext != null ? dtlsContext : emptyDtls(); + + if (timeoutMillis <= 0) throw new IllegalStateException("timeoutMillis must be > 0"); + if (mtu < 256) throw new IllegalStateException("mtu too small"); return new NetworkServer( + protocolMode, + clientAuthMode, + codec, + eventManager, tcpPort, udpPort, - packetHandler, - eventManager, - logger, - timeout, - tlsFactory, - dtlsContext, - transportPolicy, - requireClientCert + timeoutMillis, + mtu, + tcpFactory, + tlsParameters, + dtls ); } + + private static SSLContext emptyDtls() { + try { + return SSLContext.getInstance("DTLS"); + } catch (Exception e) { + throw new IllegalStateException("Failed to create DTLS context", e); + } + } } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServerUdpHooks.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServerUdpHooks.java deleted file mode 100644 index 05e889c..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServerUdpHooks.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.server; - -import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; -import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpBindPacket; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect.ConnectionHandlerFullyConnectedEvent; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_PacketReceivedEvent; -import dev.unlegitdqrk.unlegitlibrary.network.system.udp.DtlsEndpoint; -import dev.unlegitdqrk.unlegitlibrary.network.system.udp.UdpPacketCodec; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; - -import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.util.Objects; - -/** - * Helper methods for server-side DTLS/UDP handling. - * - *

Drop 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 EnumSet required; + + private ServerProtocolMode(EnumSet required) { + this.required = EnumSet.copyOf(Objects.requireNonNull(required, "required")); + if (this.required.isEmpty()) { + throw new IllegalArgumentException("required must not be empty"); + } + } + + /** + * Returns required protocols. + * + * @return required protocols + */ + public EnumSet required() { + return EnumSet.copyOf(required); + } + + /** + * Server requires TCP only. + * + * @return mode + */ + public static ServerProtocolMode tcpOnly() { + return new ServerProtocolMode(EnumSet.of(NetworkProtocol.TCP)); + } + + /** + * Server requires UDP only. + * + * @return mode + */ + public static ServerProtocolMode udpOnly() { + return new ServerProtocolMode(EnumSet.of(NetworkProtocol.UDP)); + } + + /** + * Server requires both TCP and UDP. + * + * @return mode + */ + public static ServerProtocolMode bothRequired() { + return new ServerProtocolMode(EnumSet.of(NetworkProtocol.TCP, NetworkProtocol.UDP)); + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedEvent.java index f633eef..45bf602 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedEvent.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.receive; 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 received on the server on a specific transport. + * Fired when a packet was received on the server on a specific protocol. */ public final class S_PacketReceivedEvent extends Event { - private final ConnectionHandler connectionHandler; + private final ClientConnection connection; private final Packet packet; - private final Transport transport; + private final NetworkProtocol protocol; /** * Creates a new packet received event. * - * @param connectionHandler handler - * @param packet packet - * @param transport transport used + * @param connection connection + * @param packet packet + * @param protocol protocol used */ - public S_PacketReceivedEvent(ConnectionHandler connectionHandler, Packet packet, Transport transport) { - this.connectionHandler = connectionHandler; - this.packet = packet; - this.transport = transport; + public S_PacketReceivedEvent(ClientConnection connection, Packet packet, NetworkProtocol protocol) { + this.connection = Objects.requireNonNull(connection, "connection"); + this.packet = Objects.requireNonNull(packet, "packet"); + 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 the packet was received on. + * + * @return protocol + */ + public NetworkProtocol getProtocol() { + return protocol; } } diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedFailedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedFailedEvent.java index 1e0d292..3a8653a 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedFailedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedFailedEvent.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.receive; 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 receive failed on the server on a specific transport. + * Fired when receiving or decoding a packet failed on the server for a specific protocol. */ public final class S_PacketReceivedFailedEvent extends Event { - private final ConnectionHandler connectionHandler; + private final ClientConnection connection; private final Packet packet; - private final Transport transport; + private final NetworkProtocol protocol; + private final Exception error; /** * Creates a new packet receive failed event. * - * @param connectionHandler handler - * @param packet packet - * @param transport transport used + * @param connection connection (may be null if not attributable) + * @param packet packet that failed to be processed (may be null if undecodable) + * @param protocol protocol the failure happened on + * @param error root cause */ - public S_PacketReceivedFailedEvent(ConnectionHandler connectionHandler, Packet packet, Transport transport) { - this.connectionHandler = connectionHandler; - this.packet = packet; - this.transport = transport; + public S_PacketReceivedFailedEvent( + ClientConnection connection, + Packet packet, + NetworkProtocol protocol, + Exception error + ) { + this.connection = connection; // may be null + this.packet = packet; // may be null + this.protocol = Objects.requireNonNull(protocol, "protocol"); + this.error = Objects.requireNonNull(error, "error"); } - public ConnectionHandler getConnectionHandler() { - return connectionHandler; + /** + * Returns the connection, if attributable. + * + * @return connection or null + */ + public ClientConnection getConnection() { + return connection; } + /** + * Returns the packet that failed to be processed, if available. + * + * @return packet or null + */ public Packet getPacket() { return packet; } - public Transport getTransport() { - return transport; + /** + * Returns the protocol the failure occurred on. + * + * @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/packets/receive/S_UnknownObjectReceivedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_UnknownObjectReceivedEvent.java index 2d3f4e3..5b89393 100644 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_UnknownObjectReceivedEvent.java +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_UnknownObjectReceivedEvent.java @@ -6,51 +6,65 @@ * 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.receive; import dev.unlegitdqrk.unlegitlibrary.event.impl.Event; -import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol; +import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection; + +import java.util.Objects; /** - * Fired when an unknown object was received on a specific transport. + * Fired when an unknown (non-packet) object was received on a specific protocol. + * + *

In 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: + *

    + *
  • {@link NetworkProtocol#TCP}: connection created after TLS handshake
  • + *
  • {@link NetworkProtocol#UDP}: connection received valid UDP bind and attached DTLS session
  • + *
+ */ +public final class ClientConnectionConnectedEvent extends Event { + + private final ClientConnection connection; + private final NetworkProtocol protocol; + + /** + * Creates a new connection connected event. + * + * @param connection connection + * @param protocol connected protocol + */ + public ClientConnectionConnectedEvent(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 connected. + * + * @return protocol + */ + public NetworkProtocol getProtocol() { + return protocol; + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionFullyConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionFullyConnectedEvent.java new file mode 100644 index 0000000..88e317b --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionFullyConnectedEvent.java @@ -0,0 +1,60 @@ +/* + * 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.EnumSet; +import java.util.Objects; + +/** + * Fired when a connection satisfies the server protocol requirements. + * + *

In v2 this typically means: + *

    + *
  • TCP is connected (TLS ok)
  • + *
  • and if UDP is enabled: DTLS is established and the bind flow is completed
  • + *
+ */ +public final class ClientConnectionFullyConnectedEvent extends Event { + + private final ClientConnection connection; + private final EnumSet requiredProtocols; + + /** + * Creates a new event. + * + * @param connection connection + * @param requiredProtocols required protocols now established + */ + public ClientConnectionFullyConnectedEvent(ClientConnection connection, EnumSet requiredProtocols) { + this.connection = Objects.requireNonNull(connection, "connection"); + this.requiredProtocols = EnumSet.copyOf(Objects.requireNonNull(requiredProtocols, "requiredProtocols")); + } + + /** + * Returns the connection. + * + * @return connection + */ + public ClientConnection getConnection() { + return connection; + } + + /** + * Returns the required protocols that are now established. + * + * @return required protocols (copy) + */ + public EnumSet getRequiredProtocols() { + return EnumSet.copyOf(requiredProtocols); + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerConnectedEvent.java deleted file mode 100644 index 928c278..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerConnectedEvent.java +++ /dev/null @@ -1,57 +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 - */ - -/* - * 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.server.ConnectionHandler; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; - -import java.util.Objects; - -/** - * Fired when a specific transport becomes connected for a handler. - * - *

Transport-specific: - *

    - *
  • {@link Transport#TCP}: handler created after TLS handshake
  • - *
  • {@link Transport#UDP}: handler received valid UDP bind and attached DTLS session
  • - *
- */ -public final class ConnectionHandlerConnectedEvent extends Event { - - private final ConnectionHandler connectionHandler; - private final Transport transport; - - /** - * Creates a new handler connected event. - * - * @param connectionHandler handler - * @param transport connected transport - */ - public ConnectionHandlerConnectedEvent(ConnectionHandler connectionHandler, Transport transport) { - this.connectionHandler = Objects.requireNonNull(connectionHandler, "connectionHandler"); - this.transport = Objects.requireNonNull(transport, "transport"); - } - - public ConnectionHandler getConnectionHandler() { - return connectionHandler; - } - - public Transport getTransport() { - return transport; - } -} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerFullyConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerFullyConnectedEvent.java deleted file mode 100644 index 7e43e98..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerFullyConnectedEvent.java +++ /dev/null @@ -1,61 +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 - */ - -/* - * 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.server.ConnectionHandler; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; - -/** - * Fired when a connection handler satisfies the server transport policy. - * - *

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 EnumSet requiredProtocols; + + /** + * Creates a new fully disconnected event. + * + * @param connection connection + * @param requiredProtocols required protocols according to policy + */ + public ClientConnectionFullyDisconnectedEvent(ClientConnection connection, EnumSet requiredProtocols) { + this.connection = Objects.requireNonNull(connection, "connection"); + this.requiredProtocols = EnumSet.copyOf(Objects.requireNonNull(requiredProtocols, "requiredProtocols")); + } + + /** + * Returns the connection. + * + * @return connection + */ + public ClientConnection getConnection() { + return connection; + } + + /** + * Returns the protocols that were required for full connectivity. + * + * @return required protocols (copy) + */ + public EnumSet getRequiredProtocols() { + return EnumSet.copyOf(requiredProtocols); + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerDisconnectedEvent.java deleted file mode 100644 index c7d2286..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerDisconnectedEvent.java +++ /dev/null @@ -1,51 +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 - */ - -/* - * 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.server.ConnectionHandler; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; - -import java.util.Objects; - -/** - * Fired when a specific transport becomes disconnected for a handler. - */ -public final class ConnectionHandlerDisconnectedEvent extends Event { - - private final ConnectionHandler connectionHandler; - private final Transport transport; - - /** - * Creates a new handler disconnected event. - * - * @param connectionHandler handler - * @param transport disconnected transport - */ - public ConnectionHandlerDisconnectedEvent(ConnectionHandler connectionHandler, Transport transport) { - this.connectionHandler = Objects.requireNonNull(connectionHandler, "connectionHandler"); - this.transport = Objects.requireNonNull(transport, "transport"); - } - - public ConnectionHandler getConnectionHandler() { - return connectionHandler; - } - - public Transport getTransport() { - return transport; - } -} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerFullyDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerFullyDisconnectedEvent.java deleted file mode 100644 index 1790c71..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerFullyDisconnectedEvent.java +++ /dev/null @@ -1,55 +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 - */ - -/* - * 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.server.ConnectionHandler; -import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport; - -import java.util.Objects; - -/** - * Fired when a handler was fully connected and then became fully disconnected. - * - *

For 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: *

    *
  • DTLS handshake traffic
  • - *
  • {@code UdpBindPacket}
  • - *
  • Any unbound UDP packet
  • + *
  • bind packets
  • + *
  • any unbound UDP packet
  • *
* *

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: *

    *
  • DTLS handshake via SSLEngine
  • *
  • wrap/unwrap application data into datagrams
  • - *
  • per remote address sessions
  • + *
  • per-remote address sessions
  • *
+ * + *

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 Map sessions = new ConcurrentHashMap<>(); /** * Creates a DTLS endpoint. * - * @param channel underlying datagram channel (bound for server, unbound or bound for client) - * @param sslContext DTLS SSL context (created with "DTLS") - * @param clientMode true for client sessions, false for server sessions - * @param mtu maximum datagram size - * @param timeoutMillis handshake/read timeout for polling - * @param appHandler application data handler + * @param channel underlying datagram channel (bound for server, connected or unconnected for client) + * @param sslContext DTLS SSL context (created with "DTLS") + * @param clientMode true for client sessions, false for server sessions + * @param mtu maximum datagram size + * @param timeoutMillis handshake/read timeout for polling + * @param clientAuthMode server-side client auth policy (OPTIONAL/REQUIRED). Ignored for clientMode=true. + * @param appHandler application data handler */ public DtlsEndpoint( DatagramChannel channel, @@ -54,21 +92,24 @@ public final class DtlsEndpoint { boolean clientMode, int mtu, int timeoutMillis, + ClientAuthMode clientAuthMode, ApplicationDataHandler appHandler - ) { + ) + { this.channel = Objects.requireNonNull(channel, "channel"); this.sslContext = Objects.requireNonNull(sslContext, "sslContext"); this.clientMode = clientMode; this.mtu = mtu; this.timeoutMillis = timeoutMillis; + this.clientAuthMode = Objects.requireNonNull(clientAuthMode, "clientAuthMode"); this.appHandler = Objects.requireNonNull(appHandler, "appHandler"); } /** - * Creates a DTLS SSLContext from an existing key+trust configuration. + * Creates a DTLS {@link SSLContext} from an existing key+trust configuration. * - * @param keyManagers key managers - * @param trustManagers trust managers + * @param keyManagers key managers (nullable) + * @param trustManagers trust managers (nullable) * @return DTLS SSL context * @throws Exception on errors */ @@ -86,9 +127,17 @@ public final class DtlsEndpoint { * @throws SSLException if engine creation fails */ public DtlsSession session(SocketAddress remote) throws SSLException { + Objects.requireNonNull(remote, "remote"); return sessions.computeIfAbsent(remote, r -> { try { - return new DtlsSession(createEngine(r), r, mtu); + SSLEngine engine = createEngine(r); + + // Critical: server-side sessions must enter handshake immediately. + if (!clientMode) { + engine.beginHandshake(); + } + + return new DtlsSession(engine, r, mtu); } catch (SSLException e) { throw new RuntimeException(e); } @@ -98,9 +147,6 @@ public final class DtlsEndpoint { /** * Performs a DTLS handshake for a remote address. * - *

Client: 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 List resolveAll() throws UnknownHostException { + InetAddress[] addresses = InetAddress.getAllByName(host); + if (addresses.length == 0) { + throw new UnknownHostException("No addresses resolved for " + host); + } + + List result = new ArrayList<>(addresses.length); + for (InetAddress a : addresses) { + result.add(new InetSocketAddress(a, port)); + } + + result.sort(Comparator.comparingInt(this::score)); + return result; + } + + /** + * Resolves the best address according to {@link IpPreference}. + * + * @return best socket address + * @throws UnknownHostException if resolution fails + */ + public InetSocketAddress resolveBest() throws UnknownHostException { + return resolveAll().get(0); + } + + private int score(InetSocketAddress addr) { + boolean ipv6 = addr.getAddress() instanceof java.net.Inet6Address; + boolean ipv4 = addr.getAddress() instanceof java.net.Inet4Address; + + return switch (ipPreference) { + case IPV6_ONLY -> ipv6 ? 0 : 100; + case IPV4_ONLY -> ipv4 ? 0 : 100; + case PREFER_IPV6 -> ipv6 ? 0 : 10; + case PREFER_IPV4 -> ipv4 ? 0 : 10; + case ANY -> 0; + }; + } + + private static String normalizeHost(String host) { + Objects.requireNonNull(host, "host"); + String h = host.trim(); + if (h.isEmpty()) throw new IllegalArgumentException("host must not be empty"); + + // Remove IPv6 brackets if present + if (h.startsWith("[") && h.endsWith("]") && h.length() > 2) { + h = h.substring(1, h.length() - 1).trim(); + } + return h; + } + + private static int validatePort(int port) { + if (port <= 0 || port > 65535) { + throw new IllegalArgumentException("port out of range: " + port); + } + return port; + } + + /** + * Controls IP family selection when resolving hostnames. + */ + public enum IpPreference { + ANY, + PREFER_IPV6, + PREFER_IPV4, + IPV6_ONLY, + IPV4_ONLY + } +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/NetworkProtocol.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/NetworkProtocol.java new file mode 100644 index 0000000..96accf6 --- /dev/null +++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/NetworkProtocol.java @@ -0,0 +1,34 @@ +/* + * 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 dev.unlegitdqrk.unlegitlibrary.network.system.server.ServerProtocolMode; + +/** + * Supported network protocols for packet transport. + * + *

This enum is used: + *

    + *
  • by client and server to declare supported/enabled protocols
  • + *
  • when sending packets to explicitly choose the transport
  • + *
  • by {@link ServerProtocolMode} to define server capabilities
  • + *
+ */ +public enum NetworkProtocol { + + /** + * TCP transport secured via TLS. + */ + TCP, + + /** + * UDP transport secured via DTLS. + */ + UDP +} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Transport.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Transport.java deleted file mode 100644 index e344e58..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Transport.java +++ /dev/null @@ -1,28 +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.util.EnumSet; - -/** - * Supported transport mechanisms for packet sending. - */ -public enum Transport { - TCP, - UDP; - - /** - * Returns a default transport set for a typical hybrid server/client. - * - * @return TCP + UDP set - */ - public static EnumSet both() { - return EnumSet.of(TCP, UDP); - } -} diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/TransportPolicy.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/TransportPolicy.java deleted file mode 100644 index 152bc4e..0000000 --- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/TransportPolicy.java +++ /dev/null @@ -1,76 +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.util.EnumSet; - -/** - * Defines which transports a server supports and which are mandatory to be connected. - */ -public record TransportPolicy(EnumSet supported, EnumSet required) { - - /** - * Creates a new transport policy. - * - * @param supported transports supported by the server - * @param required transports required for a connection to be considered "fully connected" - */ - public TransportPolicy(EnumSet supported, EnumSet required) { - this.supported = EnumSet.copyOf(supported); - this.required = EnumSet.copyOf(required); - - if (!this.supported.containsAll(this.required)) { - throw new IllegalArgumentException("Required transports must be a subset of supported transports."); - } - if (this.supported.isEmpty()) { - throw new IllegalArgumentException("Supported transports cannot be empty."); - } - } - - /** - * Convenience: TCP only. - */ - public static TransportPolicy tcpOnly() { - return new TransportPolicy(EnumSet.of(Transport.TCP), EnumSet.of(Transport.TCP)); - } - - /** - * Convenience: UDP only. - */ - public static TransportPolicy udpOnly() { - return new TransportPolicy(EnumSet.of(Transport.UDP), EnumSet.of(Transport.UDP)); - } - - /** - * Convenience: supports TCP+UDP and requires BOTH simultaneously (your chosen setting). - */ - public static TransportPolicy bothRequired() { - return new TransportPolicy(EnumSet.of(Transport.TCP, Transport.UDP), EnumSet.of(Transport.TCP, Transport.UDP)); - } - - /** - * Supported transports. - * - * @return supported set - */ - @Override - public EnumSet supported() { - return EnumSet.copyOf(supported); - } - - /** - * Required transports. - * - * @return required set - */ - @Override - public EnumSet required() { - return EnumSet.copyOf(required); - } -}