connectError = new AtomicReference<>();
- server = startServer(port, null, serverReceivedHello);
+ server = startServer(0, null, serverReceivedHello);
+ int port = awaitBoundPort(server);
+ assertTrue(port > 0, "server did not bind an ephemeral port");
IO.Options opts = new IO.Options();
opts.forceNew = true;
@@ -224,10 +225,13 @@ private SocketSslConfig testSslConfig() throws Exception {
return ssl;
}
- private int allocatePort() throws Exception {
- try (ServerSocket ss = new ServerSocket(0)) {
- ss.setReuseAddress(true);
- return ss.getLocalPort();
+ private static int awaitBoundPort(SocketIOServer server) throws InterruptedException {
+ long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
+ int port = server.getConfiguration().getPort();
+ while (port == 0 && System.nanoTime() < deadlineNs) {
+ Thread.sleep(10);
+ port = server.getConfiguration().getPort();
}
+ return port;
}
}
From 8f31846f2737fee54b25df36bb930af0ba47e03e Mon Sep 17 00:00:00 2001
From: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
Date: Wed, 1 Apr 2026 11:00:15 +0800
Subject: [PATCH 5/9] fix(ssl): optimize test cases and ssl restart
Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
---
.../socketio/SocketIOChannelInitializer.java | 23 +++--
.../socketio4j/socketio/SocketSslConfig.java | 67 ++++++++++++-
.../socketio/SocketSslServerRestartTest.java | 72 ++++++++++++++
.../transport/SocketIoJavaClientSslTest.java | 99 +++++++++++++++++++
4 files changed, 253 insertions(+), 8 deletions(-)
create mode 100644 netty-socketio-core/src/test/java/com/socketio4j/socketio/SocketSslServerRestartTest.java
diff --git a/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketIOChannelInitializer.java b/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketIOChannelInitializer.java
index 2f57a994..f7b781d3 100644
--- a/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketIOChannelInitializer.java
+++ b/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketIOChannelInitializer.java
@@ -16,6 +16,7 @@
*/
package com.socketio4j.socketio;
+import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.KeyStore;
@@ -114,7 +115,7 @@ public void start(Configuration configuration, NamespacesHub namespacesHub) {
String connectPath = configuration.getContext() + "/";
SocketSslConfig socketSslConfig = configuration.getSocketSslConfig();
- boolean isSsl = socketSslConfig != null && socketSslConfig.getKeyStore() != null;
+ boolean isSsl = socketSslConfig != null && socketSslConfig.hasKeyStore();
if (isSsl) {
try {
sslContext = createSSLContext(socketSslConfig);
@@ -161,7 +162,7 @@ protected void addSslHandler(ChannelPipeline pipeline) {
engine.setUseClientMode(false);
if (configuration.isNeedClientAuth()
&& configuration.getSocketSslConfig() != null
- && configuration.getSocketSslConfig().getTrustStore() != null) {
+ && configuration.getSocketSslConfig().hasTrustStore()) {
engine.setNeedClientAuth(true);
}
pipeline.addLast(SSL_HANDLER, new SslHandler(engine));
@@ -204,8 +205,12 @@ protected Object newContinueResponse(HttpMessage start, int maxContentLength,
}
private SslContext createSSLContext(SocketSslConfig socketSslConfig) throws Exception {
+ byte[] keyMaterial = socketSslConfig.resolveKeyStoreBytes();
+ if (keyMaterial == null) {
+ throw new IllegalStateException("SocketSslConfig key store material is missing");
+ }
KeyStore ks = KeyStore.getInstance(socketSslConfig.getKeyStoreFormat());
- try (InputStream keyStoreStream = socketSslConfig.getKeyStore()) {
+ try (InputStream keyStoreStream = new ByteArrayInputStream(keyMaterial)) {
ks.load(keyStoreStream, socketSslConfig.getKeyStorePassword().toCharArray());
}
@@ -227,10 +232,14 @@ private SslContext createSSLContext(SocketSslConfig socketSslConfig) throws Exce
sslProtocol);
}
}
- if (socketSslConfig.getTrustStore() != null) {
+ if (socketSslConfig.hasTrustStore()) {
+ byte[] trustMaterial = socketSslConfig.resolveTrustStoreBytes();
+ if (trustMaterial == null) {
+ throw new IllegalStateException("SocketSslConfig trust store material is missing");
+ }
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore ts = KeyStore.getInstance(socketSslConfig.getTrustStoreFormat());
- try (InputStream trustStoreStream = socketSslConfig.getTrustStore()) {
+ try (InputStream trustStoreStream = new ByteArrayInputStream(trustMaterial)) {
ts.load(trustStoreStream, socketSslConfig.getTrustStorePassword().toCharArray());
}
tmf.init(ts);
@@ -241,11 +250,11 @@ private SslContext createSSLContext(SocketSslConfig socketSslConfig) throws Exce
private static boolean isTlsProtocolVersion(String value) {
// Common enabled-protocol tokens used by JSSE/Netty.
- // Accept "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3".
+ // Accept "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"; reject "TLSv1.0" and other dotted minors.
if (value == null) {
return false;
}
- return value.matches("^TLSv1(\\.[0-3])?$");
+ return value.matches("^TLSv1(\\.(1|2|3))?$");
}
@Override
diff --git a/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketSslConfig.java b/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketSslConfig.java
index 6b76b8fc..d4314b8d 100644
--- a/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketSslConfig.java
+++ b/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketSslConfig.java
@@ -16,6 +16,7 @@
*/
package com.socketio4j.socketio;
+import java.io.IOException;
import java.io.InputStream;
import javax.net.ssl.KeyManagerFactory;
@@ -31,6 +32,10 @@ public class SocketSslConfig {
private InputStream trustStore;
private String trustStorePassword;
+ private final Object sslMaterialLock = new Object();
+ private byte[] cachedKeyStoreBytes;
+ private byte[] cachedTrustStoreBytes;
+
private String keyManagerFactoryAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
/**
@@ -47,7 +52,12 @@ public String getKeyStorePassword() {
}
/**
- * SSL key store stream, maybe appointed to any source
+ * SSL key store stream, maybe appointed to any source.
+ *
+ * On the first TLS context build when the server starts, the stream is read fully into memory and closed;
+ * later start/stop cycles reuse the buffered bytes so the same {@code SocketSslConfig} instance remains valid.
+ * After buffering, {@link #getKeyStore()} returns {@code null}.
+ *
*
* @param keyStore - key store input stream
*/
@@ -59,6 +69,31 @@ public InputStream getKeyStore() {
return keyStore;
}
+ /**
+ * Whether a key store is configured (stream not yet consumed or already buffered).
+ */
+ public boolean hasKeyStore() {
+ synchronized (sslMaterialLock) {
+ return keyStore != null || cachedKeyStoreBytes != null;
+ }
+ }
+
+ byte[] resolveKeyStoreBytes() throws IOException {
+ synchronized (sslMaterialLock) {
+ if (cachedKeyStoreBytes != null) {
+ return cachedKeyStoreBytes;
+ }
+ if (keyStore == null) {
+ return null;
+ }
+ try (InputStream in = keyStore) {
+ cachedKeyStoreBytes = in.readAllBytes();
+ }
+ keyStore = null;
+ return cachedKeyStoreBytes;
+ }
+ }
+
/**
* Key store format
*
@@ -85,10 +120,40 @@ public InputStream getTrustStore() {
return trustStore;
}
+ /**
+ * Trust store stream. Same buffering and lifecycle as {@link #setKeyStore(InputStream)}.
+ *
+ * @param trustStore trust store input stream
+ */
public void setTrustStore(InputStream trustStore) {
this.trustStore = trustStore;
}
+ /**
+ * Whether a trust store is configured (stream not yet consumed or already buffered).
+ */
+ public boolean hasTrustStore() {
+ synchronized (sslMaterialLock) {
+ return trustStore != null || cachedTrustStoreBytes != null;
+ }
+ }
+
+ byte[] resolveTrustStoreBytes() throws IOException {
+ synchronized (sslMaterialLock) {
+ if (cachedTrustStoreBytes != null) {
+ return cachedTrustStoreBytes;
+ }
+ if (trustStore == null) {
+ return null;
+ }
+ try (InputStream in = trustStore) {
+ cachedTrustStoreBytes = in.readAllBytes();
+ }
+ trustStore = null;
+ return cachedTrustStoreBytes;
+ }
+ }
+
public String getTrustStorePassword() {
return trustStorePassword;
}
diff --git a/netty-socketio-core/src/test/java/com/socketio4j/socketio/SocketSslServerRestartTest.java b/netty-socketio-core/src/test/java/com/socketio4j/socketio/SocketSslServerRestartTest.java
new file mode 100644
index 00000000..7322c7b7
--- /dev/null
+++ b/netty-socketio-core/src/test/java/com/socketio4j/socketio/SocketSslServerRestartTest.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2025 The Socketio4j Project
+ * Parent project : Copyright (c) 2012-2025 Nikita Koksharov
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.socketio4j.socketio;
+
+import java.io.InputStream;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+
+import com.socketio4j.socketio.nativeio.TransportType;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Ensures TLS material from {@link SocketSslConfig} survives stop/start when streams are not reusable.
+ */
+public class SocketSslServerRestartTest {
+
+ @Test
+ public void shouldStartStopStartWithSameSocketSslConfig() throws Exception {
+ Configuration cfg = new Configuration();
+ cfg.setPort(0);
+ cfg.setOrigin("*");
+ cfg.setTransportType(TransportType.NIO);
+
+ SocketSslConfig ssl = new SocketSslConfig();
+ ssl.setSSLProtocol("TLSv1.2");
+ ssl.setKeyStoreFormat("PKCS12");
+ ssl.setKeyStorePassword("password");
+ InputStream ks = SocketSslServerRestartTest.class.getClassLoader()
+ .getResourceAsStream("ssl/test-socketio.p12");
+ assertNotNull(ks, "Missing test keystore ssl/test-socketio.p12");
+ ssl.setKeyStore(ks);
+
+ cfg.setSocketSslConfig(ssl);
+
+ SocketIOServer server = new SocketIOServer(cfg);
+ server.start();
+ int port = awaitBoundPort(server);
+ assertTrue(port > 0);
+ server.stop();
+
+ assertDoesNotThrow(server::start, "second start should rebuild SSL from buffered keystore bytes");
+ server.stop();
+ }
+
+ private static int awaitBoundPort(SocketIOServer server) throws InterruptedException {
+ long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
+ int port = server.getConfiguration().getPort();
+ while (port == 0 && System.nanoTime() < deadlineNs) {
+ Thread.sleep(10);
+ port = server.getConfiguration().getPort();
+ }
+ return port;
+ }
+}
diff --git a/netty-socketio-core/src/test/java/com/socketio4j/socketio/transport/SocketIoJavaClientSslTest.java b/netty-socketio-core/src/test/java/com/socketio4j/socketio/transport/SocketIoJavaClientSslTest.java
index 53d05606..a3897d56 100644
--- a/netty-socketio-core/src/test/java/com/socketio4j/socketio/transport/SocketIoJavaClientSslTest.java
+++ b/netty-socketio-core/src/test/java/com/socketio4j/socketio/transport/SocketIoJavaClientSslTest.java
@@ -22,6 +22,7 @@
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLContext;
@@ -42,6 +43,7 @@
import io.socket.client.Socket;
import okhttp3.OkHttpClient;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -140,6 +142,96 @@ public X509Certificate[] getAcceptedIssuers() {
}
}
+ /**
+ * Exercises polling first (POST body via {@code PollingTransport.onPost}), then upgrade to WebSocket.
+ * Server pushes an event right after connect so delivery runs while the client may still be on polling.
+ */
+ @Test
+ public void shouldPollUpgradeToWebSocketWithServerPushAndClientHelloAckOverWss() throws Exception {
+ CountDownLatch serverReceivedHello = new CountDownLatch(1);
+ CountDownLatch clientReceivedAck = new CountDownLatch(1);
+ CountDownLatch clientReceivedWelcome = new CountDownLatch(1);
+ CountDownLatch engineHandshakeDone = new CountDownLatch(1);
+ AtomicReference connectError = new AtomicReference<>();
+ AtomicBoolean unexpectedDisconnect = new AtomicBoolean(false);
+
+ server = startServer(0, testSslConfig(), serverReceivedHello, true);
+ int port = awaitBoundPort(server);
+ assertTrue(port > 0, "server did not bind an ephemeral port");
+
+ X509TrustManager trustAll = new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ };
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, new TrustManager[] { trustAll }, new SecureRandom());
+
+ OkHttpClient okHttp = new OkHttpClient.Builder()
+ .sslSocketFactory(sslContext.getSocketFactory(), trustAll)
+ .hostnameVerifier((hostname, session) -> true)
+ .readTimeout(1, TimeUnit.MINUTES)
+ .build();
+
+ IO.Options opts = new IO.Options();
+ opts.forceNew = true;
+ opts.reconnection = false;
+ opts.transports = new String[] { "polling", "websocket" };
+ opts.webSocketFactory = okHttp;
+ opts.callFactory = okHttp;
+
+ Socket socket = IO.socket("https://127.0.0.1:" + port, opts);
+ try {
+ socket.on(Socket.EVENT_DISCONNECT, args -> unexpectedDisconnect.set(true));
+ socket.on(Socket.EVENT_CONNECT_ERROR, args -> {
+ Object first = args.length > 0 ? args[0] : null;
+ if (first instanceof Throwable) {
+ connectError.set((Throwable) first);
+ } else {
+ connectError.set(new IllegalStateException(String.valueOf(first)));
+ }
+ engineHandshakeDone.countDown();
+ });
+ socket.on("welcome", args -> clientReceivedWelcome.countDown());
+ socket.on(Socket.EVENT_CONNECT, args -> {
+ try {
+ JSONObject payload = new JSONObject();
+ payload.put("a", 1);
+ socket.emit("hello", payload, (Ack) ackArgs -> {
+ if (ackArgs.length > 0 && "ok".equals(String.valueOf(ackArgs[0]))) {
+ clientReceivedAck.countDown();
+ }
+ });
+ } catch (Exception e) {
+ connectError.set(e);
+ } finally {
+ engineHandshakeDone.countDown();
+ }
+ });
+ socket.connect();
+
+ assertTrue(engineHandshakeDone.await(20, TimeUnit.SECONDS),
+ () -> "Engine.IO handshake did not complete: " + connectError.get());
+ assertNull(connectError.get(), () -> "connect_error: " + connectError.get());
+ assertFalse(unexpectedDisconnect.get(), "client disconnected before assertions");
+ assertTrue(clientReceivedWelcome.await(15, TimeUnit.SECONDS), "client did not receive server welcome on polling/upgrade path");
+ assertTrue(serverReceivedHello.await(15, TimeUnit.SECONDS), "server did not receive hello event");
+ assertTrue(clientReceivedAck.await(15, TimeUnit.SECONDS), "client did not receive ack");
+ assertFalse(unexpectedDisconnect.get(), "client disconnected during polling upgrade or ack");
+ } finally {
+ socket.disconnect();
+ }
+ }
+
@Test
public void shouldReceiveHelloEventAndAckOverPlainWebSocketFromJavaClient() throws Exception {
CountDownLatch serverReceivedHello = new CountDownLatch(1);
@@ -195,6 +287,10 @@ public void shouldReceiveHelloEventAndAckOverPlainWebSocketFromJavaClient() thro
}
private SocketIOServer startServer(int port, SocketSslConfig ssl, CountDownLatch hello) {
+ return startServer(port, ssl, hello, false);
+ }
+
+ private SocketIOServer startServer(int port, SocketSslConfig ssl, CountDownLatch hello, boolean sendWelcomeOnConnect) {
Configuration cfg = new Configuration();
cfg.setPort(port);
cfg.setOrigin("*");
@@ -208,6 +304,9 @@ private SocketIOServer startServer(int port, SocketSslConfig ssl, CountDownLatch
hello.countDown();
ackSender.sendAckData("ok");
});
+ if (sendWelcomeOnConnect) {
+ s.addConnectListener(client -> client.sendEvent("welcome", "from-server"));
+ }
s.start();
return s;
}
From 58e2eb656e7059fd77ef570ff45a5bf9c2528706 Mon Sep 17 00:00:00 2001
From: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
Date: Wed, 1 Apr 2026 17:01:18 +0800
Subject: [PATCH 6/9] fix(ssl): optimize config loading compliance to Java 8
Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
---
.../socketio4j/socketio/SocketSslConfig.java | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketSslConfig.java b/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketSslConfig.java
index d4314b8d..93640aa3 100644
--- a/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketSslConfig.java
+++ b/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketSslConfig.java
@@ -16,6 +16,7 @@
*/
package com.socketio4j.socketio;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -87,7 +88,13 @@ byte[] resolveKeyStoreBytes() throws IOException {
return null;
}
try (InputStream in = keyStore) {
- cachedKeyStoreBytes = in.readAllBytes();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int read;
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ cachedKeyStoreBytes = out.toByteArray();
}
keyStore = null;
return cachedKeyStoreBytes;
@@ -147,7 +154,13 @@ byte[] resolveTrustStoreBytes() throws IOException {
return null;
}
try (InputStream in = trustStore) {
- cachedTrustStoreBytes = in.readAllBytes();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int read;
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ cachedTrustStoreBytes = out.toByteArray();
}
trustStore = null;
return cachedTrustStoreBytes;
From fb01ac943e970b9eae4b89bd8333f51dc765612f Mon Sep 17 00:00:00 2001
From: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
Date: Thu, 2 Apr 2026 08:59:42 +0800
Subject: [PATCH 7/9] fix(ssl): fix check style
Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
---
.../socketio4j/socketio/SocketIOChannelInitializer.java | 9 +++++++--
.../socketio4j/socketio/transport/PollingTransport.java | 4 ++--
.../socketio/transport/WebSocketTransport.java | 2 +-
3 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketIOChannelInitializer.java b/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketIOChannelInitializer.java
index f7b781d3..6170c013 100644
--- a/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketIOChannelInitializer.java
+++ b/netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketIOChannelInitializer.java
@@ -20,9 +20,9 @@
import java.io.InputStream;
import java.security.KeyStore;
+import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManagerFactory;
-import javax.net.ssl.KeyManagerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -217,7 +217,12 @@ private SslContext createSSLContext(SocketSslConfig socketSslConfig) throws Exce
KeyManagerFactory kmf = KeyManagerFactory.getInstance(socketSslConfig.getKeyManagerFactoryAlgorithm());
kmf.init(ks, socketSslConfig.getKeyStorePassword().toCharArray());
- SslProvider sslProvider = OpenSsl.isAvailable() ? SslProvider.OPENSSL : SslProvider.JDK;
+ SslProvider sslProvider;
+ if (OpenSsl.isAvailable()) {
+ sslProvider = SslProvider.OPENSSL;
+ } else {
+ sslProvider = SslProvider.JDK;
+ }
SslContextBuilder builder = SslContextBuilder.forServer(kmf).sslProvider(sslProvider);
String sslProtocol = socketSslConfig.getSSLProtocol();
diff --git a/netty-socketio-core/src/main/java/com/socketio4j/socketio/transport/PollingTransport.java b/netty-socketio-core/src/main/java/com/socketio4j/socketio/transport/PollingTransport.java
index f6dcc7c5..1e34aeae 100644
--- a/netty-socketio-core/src/main/java/com/socketio4j/socketio/transport/PollingTransport.java
+++ b/netty-socketio-core/src/main/java/com/socketio4j/socketio/transport/PollingTransport.java
@@ -41,12 +41,12 @@
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
-import io.netty.handler.codec.http.HttpRequestDecoder;
-import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.websocketx.WebSocket13FrameDecoder;
diff --git a/netty-socketio-core/src/main/java/com/socketio4j/socketio/transport/WebSocketTransport.java b/netty-socketio-core/src/main/java/com/socketio4j/socketio/transport/WebSocketTransport.java
index 616b9224..d1fb1609 100644
--- a/netty-socketio-core/src/main/java/com/socketio4j/socketio/transport/WebSocketTransport.java
+++ b/netty-socketio-core/src/main/java/com/socketio4j/socketio/transport/WebSocketTransport.java
@@ -51,11 +51,11 @@
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
-import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocket13FrameDecoder;
+import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
From 5e5aaf198de1184a96792870ac039d53c3bad958 Mon Sep 17 00:00:00 2001
From: sanjomo
Date: Thu, 2 Apr 2026 15:47:04 +0530
Subject: [PATCH 8/9] test with just polling transport - ssl and non ssl
---
.../transport/SocketIoJavaClientSslTest.java | 133 ++++++++++++++++++
1 file changed, 133 insertions(+)
diff --git a/netty-socketio-core/src/test/java/com/socketio4j/socketio/transport/SocketIoJavaClientSslTest.java b/netty-socketio-core/src/test/java/com/socketio4j/socketio/transport/SocketIoJavaClientSslTest.java
index a3897d56..ab5a262d 100644
--- a/netty-socketio-core/src/test/java/com/socketio4j/socketio/transport/SocketIoJavaClientSslTest.java
+++ b/netty-socketio-core/src/test/java/com/socketio4j/socketio/transport/SocketIoJavaClientSslTest.java
@@ -286,6 +286,139 @@ public void shouldReceiveHelloEventAndAckOverPlainWebSocketFromJavaClient() thro
}
}
+ @Test
+ public void shouldReceiveHelloEventAndAckOverPollingFromJavaClient() throws Exception {
+ CountDownLatch serverReceivedHello = new CountDownLatch(1);
+ CountDownLatch clientReceivedAck = new CountDownLatch(1);
+ CountDownLatch engineHandshakeDone = new CountDownLatch(1);
+ AtomicReference connectError = new AtomicReference<>();
+
+ server = startServer(0, testSslConfig(), serverReceivedHello);
+ int port = awaitBoundPort(server);
+ assertTrue(port > 0, "server did not bind an ephemeral port");
+
+ X509TrustManager trustAll = new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ };
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, new TrustManager[] { trustAll }, new SecureRandom());
+
+ OkHttpClient okHttp = new OkHttpClient.Builder()
+ .sslSocketFactory(sslContext.getSocketFactory(), trustAll)
+ .hostnameVerifier((hostname, session) -> true)
+ .readTimeout(1, TimeUnit.MINUTES)
+ .build();
+
+ IO.Options opts = new IO.Options();
+ opts.forceNew = true;
+ opts.reconnection = false;
+ opts.transports = new String[] { "polling" };
+ opts.webSocketFactory = okHttp;
+ opts.callFactory = okHttp;
+
+ Socket socket = IO.socket("https://127.0.0.1:" + port, opts);
+ try {
+ socket.on(Socket.EVENT_CONNECT_ERROR, args -> {
+ Object first = args.length > 0 ? args[0] : null;
+ if (first instanceof Throwable) {
+ connectError.set((Throwable) first);
+ } else {
+ connectError.set(new IllegalStateException(String.valueOf(first)));
+ }
+ engineHandshakeDone.countDown();
+ });
+ socket.on(Socket.EVENT_CONNECT, args -> {
+ try {
+ JSONObject payload = new JSONObject();
+ payload.put("a", 1);
+ socket.emit("hello", payload, (Ack) ackArgs -> {
+ if (ackArgs.length > 0 && "ok".equals(String.valueOf(ackArgs[0]))) {
+ clientReceivedAck.countDown();
+ }
+ });
+ } catch (Exception e) {
+ connectError.set(e);
+ } finally {
+ engineHandshakeDone.countDown();
+ }
+ });
+ socket.connect();
+
+ assertTrue(engineHandshakeDone.await(20, TimeUnit.SECONDS),
+ () -> "Engine.IO handshake did not complete: " + connectError.get());
+ assertNull(connectError.get(), () -> "connect_error: " + connectError.get());
+ assertTrue(serverReceivedHello.await(15, TimeUnit.SECONDS), "server did not receive hello event");
+ assertTrue(clientReceivedAck.await(15, TimeUnit.SECONDS), "client did not receive ack");
+ } finally {
+ socket.disconnect();
+ }
+ }
+
+ @Test
+ public void shouldReceiveHelloEventAndAckOverPlainPollingFromJavaClient() throws Exception {
+ CountDownLatch serverReceivedHello = new CountDownLatch(1);
+ CountDownLatch clientReceivedAck = new CountDownLatch(1);
+ CountDownLatch engineHandshakeDone = new CountDownLatch(1);
+ AtomicReference connectError = new AtomicReference<>();
+
+ server = startServer(0, null, serverReceivedHello);
+ int port = awaitBoundPort(server);
+ assertTrue(port > 0, "server did not bind an ephemeral port");
+
+ IO.Options opts = new IO.Options();
+ opts.forceNew = true;
+ opts.reconnection = false;
+ opts.transports = new String[] { "polling" };
+
+ Socket socket = IO.socket("http://127.0.0.1:" + port, opts);
+ try {
+ socket.on(Socket.EVENT_CONNECT_ERROR, args -> {
+ Object first = args.length > 0 ? args[0] : null;
+ if (first instanceof Throwable) {
+ connectError.set((Throwable) first);
+ } else {
+ connectError.set(new IllegalStateException(String.valueOf(first)));
+ }
+ engineHandshakeDone.countDown();
+ });
+ socket.on(Socket.EVENT_CONNECT, args -> {
+ try {
+ JSONObject payload = new JSONObject();
+ payload.put("a", 1);
+ socket.emit("hello", payload, (Ack) ackArgs -> {
+ if (ackArgs.length > 0 && "ok".equals(String.valueOf(ackArgs[0]))) {
+ clientReceivedAck.countDown();
+ }
+ });
+ } catch (Exception e) {
+ connectError.set(e);
+ } finally {
+ engineHandshakeDone.countDown();
+ }
+ });
+ socket.connect();
+
+ assertTrue(engineHandshakeDone.await(20, TimeUnit.SECONDS),
+ () -> "Engine.IO handshake did not complete: " + connectError.get());
+ assertNull(connectError.get(), () -> "connect_error: " + connectError.get());
+ assertTrue(serverReceivedHello.await(15, TimeUnit.SECONDS), "server did not receive hello event");
+ assertTrue(clientReceivedAck.await(15, TimeUnit.SECONDS), "client did not receive ack");
+ } finally {
+ socket.disconnect();
+ }
+ }
+
private SocketIOServer startServer(int port, SocketSslConfig ssl, CountDownLatch hello) {
return startServer(port, ssl, hello, false);
}
From 4e21900b8252f9b821ea6db7ae6988d944dee75f Mon Sep 17 00:00:00 2001
From: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
Date: Fri, 3 Apr 2026 14:46:10 +0800
Subject: [PATCH 9/9] fix(ssl): fix new keystore mechanism tests
Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
---
.../starter/config/SocketIOOriginConfigurationTest.java | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/netty-socketio-spring-boot-starter/src/test/java/com/socketio4j/socketio/test/spring/boot/starter/config/SocketIOOriginConfigurationTest.java b/netty-socketio-spring-boot-starter/src/test/java/com/socketio4j/socketio/test/spring/boot/starter/config/SocketIOOriginConfigurationTest.java
index 4918911e..186c078a 100644
--- a/netty-socketio-spring-boot-starter/src/test/java/com/socketio4j/socketio/test/spring/boot/starter/config/SocketIOOriginConfigurationTest.java
+++ b/netty-socketio-spring-boot-starter/src/test/java/com/socketio4j/socketio/test/spring/boot/starter/config/SocketIOOriginConfigurationTest.java
@@ -34,10 +34,11 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
@DisplayName("Test for Socket.IO configuration properties")
public class SocketIOOriginConfigurationTest extends BaseSpringApplicationTest {
- private static final int PORT = 9090;
+ private static final int PORT = 19090;
private static final int MAX_HEADER_SIZE = 1024;
private static final boolean TCP_KEEP_ALIVE = true;
@@ -149,7 +150,8 @@ public void testSocketConfigProperties() {
@Test
@DisplayName("Test SSL configuration properties")
public void testSslConfigProperties() {
- assertNotNull(nettySocketIOSslConfigProperties.getKeyStore(), "Key store should be loaded");
+ assertTrue(nettySocketIOSslConfigProperties.hasKeyStore(),
+ "Key store should be configured (stream may be null after TLS material is buffered on server start)");
assertNotNull(nettySocketIOSslConfigProperties.getKeyStorePassword(), "Key store password should be loaded");
SocketSslConfig socketSslConfig = new SocketSslConfig();