This is an automated email from the ASF dual-hosted git repository.

chrisdutz pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x-extras.git

commit afc56ed6b0c457110a32d98251f44bebb6731264
Author: Christofer Dutz <[email protected]>
AuthorDate: Tue Jun 23 19:14:09 2026 +0200

    feat: Updated the plc4x server and driver to operate via TLS and to support 
authentication.
---
 RELEASE_NOTES                                      |  19 ++
 plc4j/tools/plc4x-server/pom.xml                   |  22 +-
 .../plc4x/java/tools/plc4xserver/Plc4xServer.java  | 285 ++++++++++++++-----
 .../java/tools/plc4xserver/ServerTlsContext.java   | 153 +++++++++++
 .../plc4xserver/protocol/Plc4xServerAdapter.java   | 302 ++++++++++++---------
 .../protocol/SocketTransportInstance.java          | 121 +++++++++
 .../java/tools/plc4xserver/Plc4xServerTest.java    |  58 ++--
 7 files changed, 733 insertions(+), 227 deletions(-)

diff --git a/RELEASE_NOTES b/RELEASE_NOTES
index 1fc99b6..9dc6606 100644
--- a/RELEASE_NOTES
+++ b/RELEASE_NOTES
@@ -4,9 +4,28 @@
 
 New Features
 ------------
+- PLC4X Server: Now supports TLS as a transport (the default).
+  When no keystore is configured, the server generates an
+  ephemeral self-signed certificate on startup and logs its
+  SHA-256 fingerprint so clients can pin/trust it. Plaintext TCP
+  remains available as an explicit opt-in
+  (plc4x.server.plaintext=true).
+- PLC4X Server: Now enforces mandatory username/password
+  authentication; no operation is permitted before a successful
+  auth handshake. If no credentials are configured, the server
+  starts with the default user "toddy" and a generated secure
+  password that is printed to the console once at startup.
+  Configured credentials are never logged.
 
 Incompatible changes
 --------------------
+- PLC4X Server: Was migrated from the SPI2 (Netty) core to SPI3.
+  It now listens via a plain ServerSocket/SSLServerSocket and no
+  longer depends on Netty.
+- PLC4X Server: Authentication is now mandatory and TLS is the
+  default transport. Existing clients must provide credentials
+  and connect via TLS (or explicitly opt into plaintext). See the
+  matching note in the core 'plc4x' driver release notes.
 
 Bug Fixes
 ---------
diff --git a/plc4j/tools/plc4x-server/pom.xml b/plc4j/tools/plc4x-server/pom.xml
index 9366bea..42daebe 100644
--- a/plc4j/tools/plc4x-server/pom.xml
+++ b/plc4j/tools/plc4x-server/pom.xml
@@ -120,15 +120,25 @@
       <scope>runtime</scope>
     </dependency>
 
+    <!-- SPI3 transport primitives (RingBuffer / TransportInstance) used to 
adapt accepted
+         sockets to the PLC4X message codec. -->
     <dependency>
-      <groupId>io.netty</groupId>
-      <artifactId>netty-buffer</artifactId>
-      <version>4.1.123.Final</version>
+      <groupId>org.apache.plc4x</groupId>
+      <artifactId>plc4j-transports-api</artifactId>
+      <version>${plc4x.version}</version>
+    </dependency>
+
+    <!-- Bouncy Castle is used to generate an ephemeral self-signed server 
certificate when
+         no keystore is configured. -->
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk18on</artifactId>
+      <version>1.84</version>
     </dependency>
     <dependency>
-      <groupId>io.netty</groupId>
-      <artifactId>netty-transport</artifactId>
-      <version>4.1.123.Final</version>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcpkix-jdk18on</artifactId>
+      <version>1.84</version>
     </dependency>
 
     <dependency>
diff --git 
a/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/Plc4xServer.java
 
b/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/Plc4xServer.java
index 2227ec8..40657f6 100644
--- 
a/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/Plc4xServer.java
+++ 
b/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/Plc4xServer.java
@@ -21,54 +21,84 @@ package org.apache.plc4x.java.tools.plc4xserver;
 
 import static java.lang.Runtime.getRuntime;
 
-import io.netty.bootstrap.ServerBootstrap;
-import io.netty.buffer.ByteBuf;
-import io.netty.channel.ChannelFuture;
-import io.netty.channel.ChannelInitializer;
-import io.netty.channel.ChannelOption;
-import io.netty.channel.ChannelPipeline;
-import io.netty.channel.EventLoopGroup;
-import io.netty.channel.nio.NioEventLoopGroup;
-import io.netty.channel.socket.SocketChannel;
-import io.netty.channel.socket.nio.NioServerSocketChannel;
 import java.io.IOException;
 import java.net.ServerSocket;
-import java.util.Arrays;
+import java.net.Socket;
+import java.security.SecureRandom;
+import java.util.Base64;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
-import java.util.function.ToIntFunction;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import org.apache.plc4x.java.api.PlcConnectionManager;
 import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
+import org.apache.plc4x.java.plc4x.Plc4xMessageCodec;
 import org.apache.plc4x.java.plc4x.readwrite.Constants;
-import org.apache.plc4x.java.plc4x.readwrite.Plc4xMessage;
-import org.apache.plc4x.java.spi.connection.GeneratedProtocolMessageCodec;
-import org.apache.plc4x.java.spi.generation.ByteOrder;
+import org.apache.plc4x.java.spi.drivers.exceptions.MessageCodecException;
 import org.apache.plc4x.java.tools.plc4xserver.protocol.Plc4xServerAdapter;
+import 
org.apache.plc4x.java.tools.plc4xserver.protocol.SocketTransportInstance;
+import org.apache.plc4x.java.utils.cache.CachedPlcConnectionManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+/**
+ * TCP/TLS server that relays {@code plc4x} proxy requests to PLCs reachable 
from this machine.
+ *
+ * <p>Security model:</p>
+ * <ul>
+ *   <li><b>Authentication is mandatory.</b> Every connection must present a 
username and
+ *   password before any operation. If none are configured the server uses the 
default user
+ *   {@code toddy} with a freshly generated secure password that is printed to 
the console once
+ *   at startup. Explicitly configured credentials are never logged.</li>
+ *   <li><b>TLS is the default transport.</b> Without a configured keystore 
the server generates
+ *   an ephemeral self-signed certificate and prints its SHA-256 fingerprint 
so clients can pin
+ *   it. Plaintext TCP is available as an explicit opt-in for trusted 
networks/testing.</li>
+ * </ul>
+ */
 public class Plc4xServer {
 
     public static final String SERVER_PORT_PROPERTY = "plc4x.server.port";
     public static final String SERVER_PORT_ENVIRONMENT_VARIABLE = 
"PLC4X_SERVER_PORT";
+    public static final String SERVER_USERNAME_PROPERTY = 
"plc4x.server.username";
+    public static final String SERVER_USERNAME_ENVIRONMENT_VARIABLE = 
"PLC4X_SERVER_USERNAME";
+    public static final String SERVER_PASSWORD_PROPERTY = 
"plc4x.server.password";
+    public static final String SERVER_PASSWORD_ENVIRONMENT_VARIABLE = 
"PLC4X_SERVER_PASSWORD";
+    public static final String SERVER_PLAINTEXT_PROPERTY = 
"plc4x.server.plaintext";
+    public static final String SERVER_PLAINTEXT_ENVIRONMENT_VARIABLE = 
"PLC4X_SERVER_PLAINTEXT";
+    public static final String SERVER_KEYSTORE_PROPERTY = 
"plc4x.server.keystore";
+    public static final String SERVER_KEYSTORE_PASSWORD_PROPERTY = 
"plc4x.server.keystore-password";
+
+    public static final String DEFAULT_USERNAME = "toddy";
     public static int DEFAULT_PORT = Constants.PLC4XTCPDEFAULTPORT;
 
-    private static final Logger LOG = 
LoggerFactory.getLogger(Plc4xServerAdapter.class);
+    private static final Logger LOG = 
LoggerFactory.getLogger(Plc4xServer.class);
+
+    private final PlcConnectionManager connectionManager = 
CachedPlcConnectionManager.getBuilder().build();
 
-    private EventLoopGroup loopGroup;
-    private EventLoopGroup workerGroup;
-    private ChannelFuture channelFuture;
     private Integer port;
+    private String username;
+    private String password;
+    private boolean plaintext = false;
+    private String keystorePath;
+    private String keystorePassword;
+
+    private ServerSocket serverSocket;
+    private Thread acceptThread;
+    private ExecutorService connectionExecutor;
+    private volatile boolean running = false;
 
     public static void main(String[] args) throws Exception {
         final Plc4xServer server = new Plc4xServer();
 
         Future<Void> serverFuture = server.start(
-                Arrays.stream(args).findFirst() // port number given as first 
command line argument
-                        .or(() -> 
Optional.ofNullable(System.getProperty(SERVER_PORT_PROPERTY)))
-                        .or(() -> 
Optional.ofNullable(System.getenv(SERVER_PORT_ENVIRONMENT_VARIABLE)))
-                        .map(Integer::parseInt)
-                        .orElse(DEFAULT_PORT)
+            Arrays_findFirst(args) // port number given as first command line 
argument
+                .or(() -> 
Optional.ofNullable(System.getProperty(SERVER_PORT_PROPERTY)))
+                .or(() -> 
Optional.ofNullable(System.getenv(SERVER_PORT_ENVIRONMENT_VARIABLE)))
+                .map(Integer::parseInt)
+                .orElse(DEFAULT_PORT)
         );
         CompletableFuture<Void> serverRunning = new CompletableFuture<>();
         getRuntime().addShutdownHook(new Thread(() -> 
serverRunning.complete(null)));
@@ -86,89 +116,196 @@ public class Plc4xServer {
         }
     }
 
+    private static Optional<String> Arrays_findFirst(String[] args) {
+        return args.length > 0 ? Optional.of(args[0]) : Optional.empty();
+    }
+
     public Integer getPort() {
         return port;
     }
 
+    /**
+     * The effective username clients must authenticate with.
+     */
+    public String getUsername() {
+        return username;
+    }
+
+    /**
+     * The effective password clients must authenticate with (generated if 
none was configured).
+     */
+    public String getPassword() {
+        return password;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public void setPlaintext(boolean plaintext) {
+        this.plaintext = plaintext;
+    }
+
+    public void setKeystore(String keystorePath, String keystorePassword) {
+        this.keystorePath = keystorePath;
+        this.keystorePassword = keystorePassword;
+    }
+
     public Future<Void> start() {
         return start(0);
     }
 
     public Future<Void> start(int port) {
-        if (port == 0) {
-            this.port = findRandomFreePort();
-        } else {
-            this.port = port;
-        }
-
-        if (loopGroup != null) {
+        if (running) {
             return CompletableFuture.completedFuture(null);
         }
+        this.port = (port == 0) ? findRandomFreePort() : port;
+
+        resolveCredentials();
+        resolvePlaintext();
 
-        loopGroup = new NioEventLoopGroup();
-        workerGroup = new NioEventLoopGroup();
+        try {
+            serverSocket = plaintext ? new ServerSocket(this.port) : 
createTlsServerSocket(this.port);
+        } catch (Exception e) {
+            return CompletableFuture.failedFuture(
+                new PlcRuntimeException("Failed to start PLC4X server", e));
+        }
 
-        channelFuture = new ServerBootstrap()
-                .group(loopGroup, workerGroup)
-                .channel(NioServerSocketChannel.class)
-                .childHandler(new SocketChannelChannelInitializer())
-                .option(ChannelOption.SO_BACKLOG, 128)
-                .childOption(ChannelOption.SO_KEEPALIVE, true)
-                .bind(this.port);
+        running = true;
+        connectionExecutor = Executors.newVirtualThreadPerTaskExecutor();
+        acceptThread = new Thread(this::acceptLoop, "Plc4xServer-Accept");
+        acceptThread.setDaemon(true);
+        acceptThread.start();
 
-        return channelFuture;
+        LOG.info("PLC4X server listening on port {} ({})", this.port, 
plaintext ? "plaintext TCP" : "TLS");
+        return CompletableFuture.completedFuture(null);
     }
 
     public void stop() {
-        if (workerGroup == null) {
-            return;
+        running = false;
+        if (serverSocket != null) {
+            try {
+                serverSocket.close();
+            } catch (IOException e) {
+                LOG.debug("Error closing server socket", e);
+            }
+            serverSocket = null;
         }
+        if (connectionExecutor != null) {
+            connectionExecutor.shutdownNow();
+            connectionExecutor = null;
+        }
+    }
 
-        channelFuture.cancel(true);
-
-        workerGroup.shutdownGracefully();
-        loopGroup.shutdownGracefully();
-
-        workerGroup = null;
-        loopGroup = null;
+    private void acceptLoop() {
+        while (running) {
+            final Socket socket;
+            try {
+                socket = serverSocket.accept();
+            } catch (IOException e) {
+                if (running) {
+                    LOG.debug("Accept failed", e);
+                }
+                return;
+            }
+            connectionExecutor.submit(() -> handleConnection(socket));
+        }
     }
 
-    private static class SocketChannelChannelInitializer extends 
ChannelInitializer<SocketChannel> {
+    private void handleConnection(Socket socket) {
+        try (socket) {
+            SocketTransportInstance transport = new 
SocketTransportInstance(socket);
+            // The codec needs a message handler and the adapter needs the 
codec to send replies,
+            // so wire them through a one-slot holder to break the 
construction cycle.
+            final Plc4xServerAdapter[] holder = new Plc4xServerAdapter[1];
+            Plc4xMessageCodec codec = new Plc4xMessageCodec(transport, msg -> 
holder[0].handle(msg));
+            holder[0] = new Plc4xServerAdapter(connectionManager, codec, 
username, password);
 
-        @Override
-        public void initChannel(SocketChannel channel) {
-            ChannelPipeline pipeline = channel.pipeline();
-            pipeline.addLast(
-                    new GeneratedProtocolMessageCodec<>(
-                            Plc4xMessage.class,
-                            Plc4xMessage::staticParse,
-                            ByteOrder.BIG_ENDIAN,
-                            new ByteLengthEstimator(),
-                            null
-                    )
-            );
-            pipeline.addLast(new Plc4xServerAdapter());
+            while (running && transport.fill()) {
+                codec.processIncomingData();
+            }
+        } catch (MessageCodecException e) {
+            LOG.debug("Protocol error - dropping connection", e);
+        } catch (IOException e) {
+            LOG.debug("Connection I/O error", e);
         }
     }
 
-    private static class ByteLengthEstimator implements ToIntFunction<ByteBuf> 
{
+    private SSLServerSocket createTlsServerSocket(int port) throws Exception {
+        ServerTlsContext tlsContext;
+        String keystore = keystorePath != null ? keystorePath : 
System.getProperty(SERVER_KEYSTORE_PROPERTY);
+        if (keystore != null) {
+            String pwd = keystorePassword != null
+                ? keystorePassword : 
System.getProperty(SERVER_KEYSTORE_PASSWORD_PROPERTY);
+            tlsContext = ServerTlsContext.fromKeystore(keystore, pwd, null);
+        } else {
+            tlsContext = ServerTlsContext.selfSigned();
+            LOG.info("No keystore configured - generated an ephemeral 
self-signed certificate.");
+            LOG.info("Server certificate SHA-256 fingerprint: {}", 
tlsContext.getCertificateFingerprint());
+        }
+        SSLServerSocketFactory factory = 
tlsContext.getSslContext().getServerSocketFactory();
+        return (SSLServerSocket) factory.createServerSocket(port);
+    }
 
-        @Override
-        public int applyAsInt(ByteBuf byteBuf) {
-            if (byteBuf.readableBytes() >= 3) {
-                return byteBuf.getUnsignedShort(byteBuf.readerIndex() + 1);
+    /**
+     * Resolves the effective credentials from explicit config, system 
properties or environment,
+     * falling back to the default user with a generated password. Generated 
passwords are printed
+     * once; configured passwords are never logged.
+     */
+    private void resolveCredentials() {
+        if (username == null) {
+            username = 
Optional.ofNullable(System.getProperty(SERVER_USERNAME_PROPERTY))
+                .or(() -> 
Optional.ofNullable(System.getenv(SERVER_USERNAME_ENVIRONMENT_VARIABLE)))
+                .orElse(DEFAULT_USERNAME);
+        }
+        boolean generated = false;
+        if (password == null) {
+            String configured = 
Optional.ofNullable(System.getProperty(SERVER_PASSWORD_PROPERTY))
+                .or(() -> 
Optional.ofNullable(System.getenv(SERVER_PASSWORD_ENVIRONMENT_VARIABLE)))
+                .orElse(null);
+            if (configured != null) {
+                password = configured;
+            } else {
+                password = generateSecurePassword();
+                generated = true;
             }
-            return -1;
+        }
+        if (generated) {
+            // Intentionally printed to stdout (not just the log) so it is 
visible on first start.
+            
System.out.println("============================================================");
+            System.out.println(" No PLC4X server credentials configured - 
generated defaults:");
+            System.out.println("   username: " + username);
+            System.out.println("   password: " + password);
+            System.out.println(" Provide plc4x.server.username/password to set 
your own.");
+            
System.out.println("============================================================");
+        } else {
+            LOG.info("Using configured credentials for user '{}'", username);
         }
     }
 
+    private void resolvePlaintext() {
+        if (!plaintext) {
+            plaintext = 
Boolean.parseBoolean(System.getProperty(SERVER_PLAINTEXT_PROPERTY))
+                || 
Boolean.parseBoolean(System.getenv(SERVER_PLAINTEXT_ENVIRONMENT_VARIABLE) == 
null
+                    ? "false" : 
System.getenv(SERVER_PLAINTEXT_ENVIRONMENT_VARIABLE));
+        }
+    }
+
+    private static String generateSecurePassword() {
+        byte[] bytes = new byte[24];
+        new SecureRandom().nextBytes(bytes);
+        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
+    }
+
     private static int findRandomFreePort() {
-        final int port;
         try (ServerSocket socket = new ServerSocket(0)) {
-            port = socket.getLocalPort();
+            return socket.getLocalPort();
         } catch (IOException e) {
-            throw new RuntimeException("Couldn't find any free port.", e);
+            throw new PlcRuntimeException("Couldn't find any free port.", e);
         }
-        return port;
     }
 }
diff --git 
a/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/ServerTlsContext.java
 
b/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/ServerTlsContext.java
new file mode 100644
index 0000000..3a0b8a2
--- /dev/null
+++ 
b/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/ServerTlsContext.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   https://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 org.apache.plc4x.java.tools.plc4xserver;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.HexFormat;
+
+/**
+ * Holds the {@link SSLContext} the server listens with, plus the SHA-256 
fingerprint of the
+ * presented certificate. Either loads an operator-provided keystore or, when 
none is configured,
+ * generates an ephemeral self-signed certificate so TLS works with zero 
configuration. The
+ * fingerprint lets a client pin/trust the auto-generated identity.
+ */
+public final class ServerTlsContext {
+
+    private final SSLContext sslContext;
+    private final String certificateFingerprint;
+    private final boolean selfSigned;
+
+    private ServerTlsContext(SSLContext sslContext, String 
certificateFingerprint, boolean selfSigned) {
+        this.sslContext = sslContext;
+        this.certificateFingerprint = certificateFingerprint;
+        this.selfSigned = selfSigned;
+    }
+
+    public SSLContext getSslContext() {
+        return sslContext;
+    }
+
+    public String getCertificateFingerprint() {
+        return certificateFingerprint;
+    }
+
+    public boolean isSelfSigned() {
+        return selfSigned;
+    }
+
+    /**
+     * Builds a TLS context from an existing PKCS12/JKS keystore.
+     */
+    public static ServerTlsContext fromKeystore(String keystorePath, String 
keystorePassword,
+                                                String keystoreType) throws 
Exception {
+        char[] password = keystorePassword == null ? new char[0] : 
keystorePassword.toCharArray();
+        KeyStore keyStore = KeyStore.getInstance(keystoreType == null ? 
"PKCS12" : keystoreType);
+        try (var in = new java.io.FileInputStream(keystorePath)) {
+            keyStore.load(in, password);
+        }
+        SSLContext sslContext = buildContext(keyStore, password);
+        return new ServerTlsContext(sslContext, fingerprintOfFirst(keyStore), 
false);
+    }
+
+    /**
+     * Generates an ephemeral self-signed certificate and wraps it in a fresh 
TLS context.
+     */
+    public static ServerTlsContext selfSigned() throws Exception {
+        KeyPairGenerator keyPairGenerator = 
KeyPairGenerator.getInstance("RSA");
+        keyPairGenerator.initialize(2048, new SecureRandom());
+        KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+        X500Name subject = new X500Name("CN=PLC4X-Server");
+        Instant now = Instant.now();
+        Date notBefore = Date.from(now.minus(1, ChronoUnit.HOURS));
+        Date notAfter = Date.from(now.plus(3650, ChronoUnit.DAYS));
+        BigInteger serial = BigInteger.valueOf(System.nanoTime());
+
+        X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+            subject, serial, notBefore, notAfter, subject, 
keyPair.getPublic());
+        // Add localhost / loopback SANs so common-name verification can 
succeed for local use.
+        GeneralNames sans = new GeneralNames(new GeneralName[]{
+            new GeneralName(GeneralName.dNSName, "localhost"),
+            new GeneralName(GeneralName.iPAddress, "127.0.0.1")
+        });
+        certBuilder.addExtension(Extension.subjectAlternativeName, false, 
sans);
+
+        ContentSigner signer = new 
JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
+        X509Certificate certificate = new JcaX509CertificateConverter()
+            .getCertificate(certBuilder.build(signer));
+
+        char[] password = new char[0];
+        KeyStore keyStore = KeyStore.getInstance("PKCS12");
+        keyStore.load(null, password);
+        keyStore.setKeyEntry("plc4x-server", keyPair.getPrivate(), password,
+            new X509Certificate[]{certificate});
+
+        SSLContext sslContext = buildContext(keyStore, password);
+        return new ServerTlsContext(sslContext, fingerprint(certificate), 
true);
+    }
+
+    private static SSLContext buildContext(KeyStore keyStore, char[] password) 
throws Exception {
+        KeyManagerFactory keyManagerFactory =
+            
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        keyManagerFactory.init(keyStore, password);
+        SSLContext sslContext = SSLContext.getInstance("TLS");
+        sslContext.init(keyManagerFactory.getKeyManagers(), null, new 
SecureRandom());
+        return sslContext;
+    }
+
+    private static String fingerprintOfFirst(KeyStore keyStore) throws 
Exception {
+        var aliases = keyStore.aliases();
+        while (aliases.hasMoreElements()) {
+            String alias = aliases.nextElement();
+            var cert = keyStore.getCertificate(alias);
+            if (cert instanceof X509Certificate x509) {
+                return fingerprint(x509);
+            }
+        }
+        return "unknown";
+    }
+
+    private static String fingerprint(X509Certificate certificate) throws 
Exception {
+        MessageDigest digest = MessageDigest.getInstance("SHA-256");
+        byte[] hash = digest.digest(certificate.getEncoded());
+        return HexFormat.ofDelimiter(":").withUpperCase().formatHex(hash);
+    }
+
+}
diff --git 
a/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/protocol/Plc4xServerAdapter.java
 
b/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/protocol/Plc4xServerAdapter.java
index 7194b24..3086fc1 100644
--- 
a/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/protocol/Plc4xServerAdapter.java
+++ 
b/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/protocol/Plc4xServerAdapter.java
@@ -18,8 +18,6 @@
  */
 package org.apache.plc4x.java.tools.plc4xserver.protocol;
 
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelInboundHandlerAdapter;
 import org.apache.plc4x.java.api.PlcConnection;
 import org.apache.plc4x.java.api.PlcConnectionManager;
 import org.apache.plc4x.java.api.messages.PlcReadRequest;
@@ -28,149 +26,209 @@ import org.apache.plc4x.java.api.messages.PlcWriteRequest;
 import org.apache.plc4x.java.api.messages.PlcWriteResponse;
 import org.apache.plc4x.java.api.types.PlcResponseCode;
 import org.apache.plc4x.java.api.value.PlcValue;
+import org.apache.plc4x.java.plc4x.Plc4xMessageCodec;
 import org.apache.plc4x.java.plc4x.readwrite.*;
-import org.apache.plc4x.java.utils.cache.CachedPlcConnectionManager;
+import org.apache.plc4x.java.spi.drivers.exceptions.MessageCodecException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
 
-public class Plc4xServerAdapter extends ChannelInboundHandlerAdapter {
+/**
+ * Per-connection handler for the PLC4X proxy protocol. One instance lives for 
the lifetime of a
+ * single accepted socket; the {@link SocketTransportInstance} read loop feeds 
the codec, which
+ * invokes {@link #handle(Plc4xMessage)} for every decoded message.
+ *
+ * <p>Authentication is mandatory: a connection must complete a successful 
{@code AUTH_REQUEST}
+ * before any {@code CONNECT}/{@code READ}/{@code WRITE} is honoured. Anything 
else is answered
+ * with {@code ACCESS_DENIED}.</p>
+ */
+public class Plc4xServerAdapter implements Consumer<Plc4xMessage> {
 
     private final Logger logger = 
LoggerFactory.getLogger(Plc4xServerAdapter.class);
 
     private final PlcConnectionManager connectionManager;
-    private final AtomicInteger connectionIdGenerator;
-    private final ConcurrentHashMap<Integer, String> connectionUrls;
+    private final Plc4xMessageCodec codec;
+    private final byte[] expectedUsername;
+    private final byte[] expectedPassword;
+
+    private final AtomicInteger connectionIdGenerator = new AtomicInteger(1);
+    private final ConcurrentHashMap<Integer, String> connectionUrls = new 
ConcurrentHashMap<>();
 
-    public Plc4xServerAdapter() {
-        connectionManager = CachedPlcConnectionManager.getBuilder().build();
-        connectionIdGenerator = new AtomicInteger(1);
-        connectionUrls = new ConcurrentHashMap<>();
+    private volatile boolean authenticated = false;
+
+    public Plc4xServerAdapter(PlcConnectionManager connectionManager, 
Plc4xMessageCodec codec,
+                              String expectedUsername, String 
expectedPassword) {
+        this.connectionManager = connectionManager;
+        this.codec = codec;
+        this.expectedUsername = 
expectedUsername.getBytes(StandardCharsets.UTF_8);
+        this.expectedPassword = 
expectedPassword.getBytes(StandardCharsets.UTF_8);
     }
 
     @Override
-    public void channelRead(ChannelHandlerContext ctx, Object msg) {
-        if (msg instanceof Plc4xMessage) {
-            final Plc4xMessage plc4xMessage = (Plc4xMessage) msg;
-            switch (plc4xMessage.getRequestType()) {
-                case CONNECT_REQUEST: {
-                    Plc4xConnectRequest request = (Plc4xConnectRequest) 
plc4xMessage;
-                    try (final PlcConnection ignored = 
connectionManager.getConnection(request.getConnectionString())) {
-                        //connection.ping().get();
-                        final int connectionId = 
connectionIdGenerator.getAndIncrement();
-                        connectionUrls.put(connectionId, 
request.getConnectionString());
-                        Plc4xConnectResponse response = new 
Plc4xConnectResponse(
-                            request.getRequestId(), connectionId, 
Plc4xResponseCode.OK);
-                        ctx.writeAndFlush(response);
-                    } catch (Exception e) {
-                        Plc4xConnectResponse response = new 
Plc4xConnectResponse(
-                            request.getRequestId(), 0, 
Plc4xResponseCode.INVALID_ADDRESS);
-                        ctx.writeAndFlush(response);
-                    }
-                    break;
+    public void accept(Plc4xMessage message) {
+        handle(message);
+    }
+
+    public void handle(Plc4xMessage plc4xMessage) {
+        switch (plc4xMessage.getRequestType()) {
+            case AUTH_REQUEST:
+                handleAuth((Plc4xAuthRequest) plc4xMessage);
+                break;
+            case CONNECT_REQUEST:
+                if (requireAuth(plc4xMessage)) {
+                    handleConnect((Plc4xConnectRequest) plc4xMessage);
+                }
+                break;
+            case READ_REQUEST:
+                if (requireAuth(plc4xMessage)) {
+                    handleRead((Plc4xReadRequest) plc4xMessage);
                 }
-                case READ_REQUEST: {
-                    final Plc4xReadRequest request = (Plc4xReadRequest) 
plc4xMessage;
-                    String connectionUrl = 
connectionUrls.get(request.getConnectionId());
-                    try (final PlcConnection connection = 
connectionManager.getConnection(connectionUrl)) {
-                        // Build a read request for all tags in the request.
-                        final PlcReadRequest.Builder builder = 
connection.readRequestBuilder();
-                        for (Plc4xTagRequest requestTag : request.getTags()) {
-                            
builder.addTagAddress(requestTag.getTag().getName(), 
requestTag.getTag().getTagQuery());
-                        }
-                        final PlcReadRequest rr = builder.build();
-
-                        // Execute the query.
-                        // (It has to be synchronously when working with the 
connection cache)
-                        final PlcReadResponse apiReadResponse = 
rr.execute().get();
-
-                        // Create the response.
-                        List<Plc4xTagValueResponse> tags = new 
ArrayList<>(apiReadResponse.getTagNames().size());
-                        for (Plc4xTagRequest plc4xRequestTag : 
request.getTags()) {
-                            final PlcResponseCode responseCode = 
apiReadResponse.getResponseCode(plc4xRequestTag.getTag().getName());
-                            Plc4xResponseCode resCode;
-                            Plc4xValueType valueType;
-                            PlcValue value;
-                            if(responseCode == PlcResponseCode.OK) {
-                                resCode = Plc4xResponseCode.OK;
-                                value = 
apiReadResponse.getPlcValue(plc4xRequestTag.getTag().getName());
-                                final String valueTypeName = 
value.getClass().getSimpleName();
-                                // Cut off the "Plc" prefix to get the name of 
the PlcValueType.
-                                valueType = 
Plc4xValueType.valueOf(valueTypeName.substring(3));
-                            } else {
-                                resCode = Plc4xResponseCode.INVALID_ADDRESS;
-                                value = null;
-                                valueType = Plc4xValueType.NULL;
-                            }
-                            tags.add(new Plc4xTagValueResponse(
-                                plc4xRequestTag.getTag(), resCode, valueType, 
value));
-                        }
-                        Plc4xReadResponse response = new Plc4xReadResponse(
-                            request.getRequestId(), request.getConnectionId(), 
Plc4xResponseCode.OK, tags);
-
-                        // Send the response.
-                        ctx.writeAndFlush(response);
-                    } catch (Exception e) {
-                        logger.error("Error executing request", e);
-                        Plc4xReadResponse response = new Plc4xReadResponse(
-                            request.getRequestId(), request.getConnectionId(),
-                            Plc4xResponseCode.INVALID_ADDRESS, 
Collections.emptyList());
-                        ctx.writeAndFlush(response);
-                    }
-                    break;
+                break;
+            case WRITE_REQUEST:
+                if (requireAuth(plc4xMessage)) {
+                    handleWrite((Plc4xWriteRequest) plc4xMessage);
+                }
+                break;
+            default:
+                logger.debug("Ignoring unsupported request type {}", 
plc4xMessage.getRequestType());
+        }
+    }
+
+    private void handleAuth(Plc4xAuthRequest request) {
+        boolean ok = constantTimeEquals(expectedUsername, 
request.getUsername().getBytes(StandardCharsets.UTF_8))
+            & constantTimeEquals(expectedPassword, 
request.getPassword().getBytes(StandardCharsets.UTF_8));
+        authenticated = ok;
+        // Never log the supplied credentials, just the outcome.
+        logger.info("Authentication {}", ok ? "succeeded" : "failed");
+        send(new Plc4xAuthResponse(request.getRequestId(),
+            ok ? Plc4xResponseCode.OK : Plc4xResponseCode.ACCESS_DENIED));
+    }
+
+    /**
+     * Returns {@code true} if the connection is authenticated. Otherwise 
emits a type-appropriate
+     * {@code ACCESS_DENIED} response and returns {@code false}.
+     */
+    private boolean requireAuth(Plc4xMessage message) {
+        if (authenticated) {
+            return true;
+        }
+        logger.warn("Rejecting {} - connection is not authenticated", 
message.getRequestType());
+        switch (message.getRequestType()) {
+            case CONNECT_REQUEST -> send(new Plc4xConnectResponse(
+                message.getRequestId(), 0, Plc4xResponseCode.ACCESS_DENIED));
+            case READ_REQUEST -> send(new 
Plc4xReadResponse(message.getRequestId(),
+                ((Plc4xReadRequest) message).getConnectionId(), 
Plc4xResponseCode.ACCESS_DENIED,
+                Collections.emptyList()));
+            case WRITE_REQUEST -> send(new 
Plc4xWriteResponse(message.getRequestId(),
+                ((Plc4xWriteRequest) message).getConnectionId(), 
Plc4xResponseCode.ACCESS_DENIED,
+                Collections.emptyList()));
+            default -> { /* nothing to answer */ }
+        }
+        return false;
+    }
+
+    private void handleConnect(Plc4xConnectRequest request) {
+        try (final PlcConnection ignored = 
connectionManager.getConnection(request.getConnectionString())) {
+            final int connectionId = connectionIdGenerator.getAndIncrement();
+            connectionUrls.put(connectionId, request.getConnectionString());
+            send(new Plc4xConnectResponse(request.getRequestId(), 
connectionId, Plc4xResponseCode.OK));
+        } catch (Exception e) {
+            send(new Plc4xConnectResponse(request.getRequestId(), 0, 
Plc4xResponseCode.INVALID_ADDRESS));
+        }
+    }
+
+    private void handleRead(Plc4xReadRequest request) {
+        String connectionUrl = connectionUrls.get(request.getConnectionId());
+        try (final PlcConnection connection = 
connectionManager.getConnection(connectionUrl)) {
+            final PlcReadRequest.Builder builder = 
connection.readRequestBuilder();
+            for (Plc4xTagRequest requestTag : request.getTags()) {
+                builder.addTagAddress(requestTag.getTag().getName(), 
requestTag.getTag().getTagQuery());
+            }
+            final PlcReadRequest rr = builder.build();
+
+            // Execute synchronously (required when working with the 
connection cache).
+            final PlcReadResponse apiReadResponse = rr.execute().get();
+
+            List<Plc4xTagValueResponse> tags = new 
ArrayList<>(apiReadResponse.getTagNames().size());
+            for (Plc4xTagRequest plc4xRequestTag : request.getTags()) {
+                final PlcResponseCode responseCode =
+                    
apiReadResponse.getResponseCode(plc4xRequestTag.getTag().getName());
+                Plc4xResponseCode resCode;
+                Plc4xValueType valueType;
+                PlcValue value;
+                if (responseCode == PlcResponseCode.OK) {
+                    resCode = Plc4xResponseCode.OK;
+                    value = 
apiReadResponse.getPlcValue(plc4xRequestTag.getTag().getName());
+                    final String valueTypeName = 
value.getClass().getSimpleName();
+                    // Cut off the "Plc" prefix to get the name of the 
PlcValueType.
+                    valueType = 
Plc4xValueType.valueOf(valueTypeName.substring(3));
+                } else {
+                    resCode = Plc4xResponseCode.INVALID_ADDRESS;
+                    value = null;
+                    valueType = Plc4xValueType.NULL;
                 }
+                tags.add(new Plc4xTagValueResponse(plc4xRequestTag.getTag(), 
resCode, valueType, value));
+            }
+            send(new Plc4xReadResponse(request.getRequestId(), 
request.getConnectionId(),
+                Plc4xResponseCode.OK, tags));
+        } catch (Exception e) {
+            logger.error("Error executing read request", e);
+            send(new Plc4xReadResponse(request.getRequestId(), 
request.getConnectionId(),
+                Plc4xResponseCode.INVALID_ADDRESS, Collections.emptyList()));
+        }
+    }
 
-                case WRITE_REQUEST:
-                    final Plc4xWriteRequest plc4xWriteRequest = 
(Plc4xWriteRequest) plc4xMessage;
-                    String connectionUrl = 
connectionUrls.get(plc4xWriteRequest.getConnectionId());
-                    try (final PlcConnection connection = 
connectionManager.getConnection(connectionUrl)) {
-                        // Build a write request for all tags in the request.
-                        final PlcWriteRequest.Builder builder = 
connection.writeRequestBuilder();
-                        for (Plc4xTagValueRequest plc4xRequestTag : 
plc4xWriteRequest.getTags()) {
-                            
builder.addTagAddress(plc4xRequestTag.getTag().getName(),
-                                plc4xRequestTag.getTag().getTagQuery(), 
plc4xRequestTag.getValue().getObject());
-                        }
-                        final PlcWriteRequest apiWriteRequest = 
builder.build();
-
-                        // Execute the query
-                        // (It has to be synchronously when working with the 
connection cache)
-                        final PlcWriteResponse apiWriteResponse = 
apiWriteRequest.execute().get();
-
-                        // Create the response.
-                        List<Plc4xTagResponse> plc4xTags =
-                            new 
ArrayList<>(apiWriteResponse.getTagNames().size());
-                        for (Plc4xTagValueRequest plc4xRequestTag : 
plc4xWriteRequest.getTags()) {
-                            final PlcResponseCode apiResponseCode =
-                                
apiWriteResponse.getResponseCode(plc4xRequestTag.getTag().getName());
-                            Plc4xResponseCode resCode;
-                            if(apiResponseCode == PlcResponseCode.OK) {
-                                resCode = Plc4xResponseCode.OK;
-                            } else {
-                                resCode = Plc4xResponseCode.INVALID_ADDRESS;
-                            }
-                            plc4xTags.add(new 
Plc4xTagResponse(plc4xRequestTag.getTag(), resCode));
-                        }
-                        Plc4xWriteResponse plc4xWriteResponse = new 
Plc4xWriteResponse(
-                            plc4xWriteRequest.getRequestId(), 
plc4xWriteRequest.getConnectionId(),
-                            Plc4xResponseCode.OK, plc4xTags);
-
-                        // Send the response.
-                        ctx.writeAndFlush(plc4xWriteResponse);
-                    } catch (Exception e) {
-                        logger.error("Error executing request", e);
-                        Plc4xWriteResponse response = new Plc4xWriteResponse(
-                            plc4xWriteRequest.getRequestId(), 
plc4xWriteRequest.getConnectionId(),
-                            Plc4xResponseCode.INVALID_ADDRESS, 
Collections.emptyList());
-                        ctx.writeAndFlush(response);
-                    }
-                    break;
+    private void handleWrite(Plc4xWriteRequest request) {
+        String connectionUrl = connectionUrls.get(request.getConnectionId());
+        try (final PlcConnection connection = 
connectionManager.getConnection(connectionUrl)) {
+            final PlcWriteRequest.Builder builder = 
connection.writeRequestBuilder();
+            for (Plc4xTagValueRequest plc4xRequestTag : request.getTags()) {
+                builder.addTagAddress(plc4xRequestTag.getTag().getName(),
+                    plc4xRequestTag.getTag().getTagQuery(), 
plc4xRequestTag.getValue().getObject());
             }
+            final PlcWriteRequest apiWriteRequest = builder.build();
+
+            // Execute synchronously (required when working with the 
connection cache).
+            final PlcWriteResponse apiWriteResponse = 
apiWriteRequest.execute().get();
+
+            List<Plc4xTagResponse> plc4xTags = new 
ArrayList<>(apiWriteResponse.getTagNames().size());
+            for (Plc4xTagValueRequest plc4xRequestTag : request.getTags()) {
+                final PlcResponseCode apiResponseCode =
+                    
apiWriteResponse.getResponseCode(plc4xRequestTag.getTag().getName());
+                Plc4xResponseCode resCode = apiResponseCode == 
PlcResponseCode.OK
+                    ? Plc4xResponseCode.OK : Plc4xResponseCode.INVALID_ADDRESS;
+                plc4xTags.add(new Plc4xTagResponse(plc4xRequestTag.getTag(), 
resCode));
+            }
+            send(new Plc4xWriteResponse(request.getRequestId(), 
request.getConnectionId(),
+                Plc4xResponseCode.OK, plc4xTags));
+        } catch (Exception e) {
+            logger.error("Error executing write request", e);
+            send(new Plc4xWriteResponse(request.getRequestId(), 
request.getConnectionId(),
+                Plc4xResponseCode.INVALID_ADDRESS, Collections.emptyList()));
+        }
+    }
+
+    private void send(Plc4xMessage message) {
+        try {
+            codec.send(message);
+        } catch (MessageCodecException e) {
+            logger.error("Failed to send response", e);
         }
     }
 
+    /**
+     * Length-aware constant-time comparison to avoid leaking credential 
length/content via timing.
+     */
+    private static boolean constantTimeEquals(byte[] expected, byte[] actual) {
+        return MessageDigest.isEqual(expected, actual);
+    }
+
 }
diff --git 
a/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/protocol/SocketTransportInstance.java
 
b/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/protocol/SocketTransportInstance.java
new file mode 100644
index 0000000..cc05687
--- /dev/null
+++ 
b/plc4j/tools/plc4x-server/src/main/java/org/apache/plc4x/java/tools/plc4xserver/protocol/SocketTransportInstance.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   https://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 org.apache.plc4x.java.tools.plc4xserver.protocol;
+
+import org.apache.plc4x.java.spi.transports.api.RingBuffer;
+import org.apache.plc4x.java.spi.transports.api.TransportInstance;
+import org.apache.plc4x.java.spi.transports.api.config.TransportConfiguration;
+import org.apache.plc4x.java.spi.transports.api.exceptions.TransportException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+/**
+ * Adapts an accepted server-side {@link Socket} to the SPI3 {@link 
TransportInstance} contract
+ * so the regular PLC4X message codec can be reused on the server side.
+ *
+ * <p>SPI3 has no server transport (it is purely client/outbound), but a 
{@code MessageCodecBase}
+ * only needs the byte-level peek/read/write surface. Inbound bytes are pulled 
from the socket by
+ * {@link #fill()} into a {@link RingBuffer}; the codec then drains complete 
messages from that
+ * buffer via {@link #getNumBytesAvailable()} / {@link 
#peekReadableBytes(int)} / {@link #read(int)}.
+ * The maximum PLC4X proxy message is bounded by a {@code uint16} length, so a 
buffer slightly
+ * larger than 64&nbsp;KiB always holds a full frame.</p>
+ */
+public class SocketTransportInstance implements 
TransportInstance<TransportConfiguration> {
+
+    private static final int BUFFER_CAPACITY = 256 * 1024;
+    private static final int READ_CHUNK = 8 * 1024;
+
+    private final Socket socket;
+    private final InputStream in;
+    private final OutputStream out;
+    private final RingBuffer ringBuffer = new RingBuffer(BUFFER_CAPACITY);
+    private final byte[] readChunk = new byte[READ_CHUNK];
+
+    public SocketTransportInstance(Socket socket) throws IOException {
+        this.socket = socket;
+        this.in = socket.getInputStream();
+        this.out = socket.getOutputStream();
+    }
+
+    /**
+     * Blocks until at least one byte arrives from the socket and appends it 
to the ring buffer.
+     *
+     * @return {@code false} on end-of-stream (peer closed), {@code true} 
otherwise.
+     */
+    public boolean fill() throws IOException {
+        int read = in.read(readChunk);
+        if (read == -1) {
+            return false;
+        }
+        if (read > 0) {
+            ringBuffer.write(readChunk, 0, read);
+        }
+        return true;
+    }
+
+    @Override
+    public TransportConfiguration getConfiguration() {
+        return null;
+    }
+
+    @Override
+    public boolean isOpen() {
+        return !socket.isClosed() && socket.isConnected();
+    }
+
+    @Override
+    public int getNumBytesAvailable() {
+        return ringBuffer.availableForReading();
+    }
+
+    @Override
+    public byte[] peekReadableBytes(int numBytes) {
+        return ringBuffer.peek(numBytes);
+    }
+
+    @Override
+    public byte[] read(int numBytes) {
+        return ringBuffer.read(numBytes);
+    }
+
+    @Override
+    public void write(byte[] bytes) throws TransportException {
+        try {
+            synchronized (out) {
+                out.write(bytes);
+                out.flush();
+            }
+        } catch (IOException e) {
+            throw new TransportException("Failed to write to socket", e);
+        }
+    }
+
+    @Override
+    public void close() throws TransportException {
+        try {
+            socket.close();
+        } catch (IOException e) {
+            throw new TransportException("Failed to close socket", e);
+        }
+    }
+
+}
diff --git 
a/plc4j/tools/plc4x-server/src/test/java/org/apache/plc4x/java/tools/plc4xserver/Plc4xServerTest.java
 
b/plc4j/tools/plc4x-server/src/test/java/org/apache/plc4x/java/tools/plc4xserver/Plc4xServerTest.java
index 0e35957..326bed3 100644
--- 
a/plc4j/tools/plc4x-server/src/test/java/org/apache/plc4x/java/tools/plc4xserver/Plc4xServerTest.java
+++ 
b/plc4j/tools/plc4x-server/src/test/java/org/apache/plc4x/java/tools/plc4xserver/Plc4xServerTest.java
@@ -22,6 +22,7 @@ package org.apache.plc4x.java.tools.plc4xserver;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -29,6 +30,7 @@ import java.util.concurrent.TimeoutException;
 import org.apache.plc4x.java.DefaultPlcDriverManager;
 import org.apache.plc4x.java.api.PlcConnection;
 import org.apache.plc4x.java.api.PlcConnectionManager;
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
 import org.apache.plc4x.java.api.messages.PlcReadRequest;
 import org.apache.plc4x.java.api.messages.PlcReadResponse;
 import org.apache.plc4x.java.api.messages.PlcWriteRequest;
@@ -40,8 +42,13 @@ import org.junit.jupiter.api.Test;
 
 public class Plc4xServerTest {
 
+    private static final String USERNAME = "test-user";
+    private static final String PASSWORD = "test-password";
+
     private static final Plc4xServer SERVER = new Plc4xServer();
-    private static final String CONNECTION_STRING_TEMPLATE = 
"plc4x://localhost:%d?remote-connection-string=%s";
+    // TLS (the default) with verify-ssl=false so the client trusts the 
server's self-signed cert.
+    private static final String CONNECTION_STRING_TEMPLATE =
+        
"plc4x:tls://localhost:%d?remote-connection-string=%s&username=%s&password=%s&tls.verify-ssl=false";
     private static final String CONNECTION_STRING_SIMULATED_ENCODED = 
"simulated%3A%2F%2Flocalhost";
     private static final long TIMEOUT_VALUE = 10;
     private static final TimeUnit TIMEOUT_UNIT = TimeUnit.SECONDS;
@@ -50,6 +57,8 @@ public class Plc4xServerTest {
 
     @BeforeAll
     public static void setUp() throws ExecutionException, 
InterruptedException, TimeoutException {
+        SERVER.setUsername(USERNAME);
+        SERVER.setPassword(PASSWORD);
         SERVER.start().get(TIMEOUT_VALUE, TIMEOUT_UNIT);
     }
 
@@ -58,18 +67,18 @@ public class Plc4xServerTest {
         SERVER.stop();
     }
 
+    private String connectionString(String username, String password) {
+        return String.format(CONNECTION_STRING_TEMPLATE, SERVER.getPort(),
+            CONNECTION_STRING_SIMULATED_ENCODED, username, password);
+    }
+
     @Test
     public void testWrite() throws Exception {
         final PlcWriteResponse response;
 
-        try (PlcConnection connection = connectionManager.getConnection(
-                String.format(CONNECTION_STRING_TEMPLATE, SERVER.getPort(), 
CONNECTION_STRING_SIMULATED_ENCODED))) {
+        try (PlcConnection connection = 
connectionManager.getConnection(connectionString(USERNAME, PASSWORD))) {
             final PlcWriteRequest request = connection.writeRequestBuilder()
-                    .addTagAddress(
-                            "foo",
-                            "STATE/foo:DINT",
-                            42
-                    )
+                    .addTagAddress("foo", "STATE/foo:DINT", 42)
                     .build();
             response = request.execute().get(TIMEOUT_VALUE, TIMEOUT_UNIT);
         }
@@ -81,13 +90,9 @@ public class Plc4xServerTest {
     public void testRead() throws Exception {
         final PlcReadResponse response;
 
-        try (PlcConnection connection = connectionManager.getConnection(
-                String.format(CONNECTION_STRING_TEMPLATE, SERVER.getPort(), 
CONNECTION_STRING_SIMULATED_ENCODED))) {
+        try (PlcConnection connection = 
connectionManager.getConnection(connectionString(USERNAME, PASSWORD))) {
             final PlcReadRequest request = connection.readRequestBuilder()
-                    .addTagAddress(
-                            "foo",
-                            "RANDOM/foo:DINT"
-                    )
+                    .addTagAddress("foo", "RANDOM/foo:DINT")
                     .build();
             response = request.execute().get(TIMEOUT_VALUE, TIMEOUT_UNIT);
         }
@@ -102,22 +107,14 @@ public class Plc4xServerTest {
         final PlcWriteResponse writeResponse;
         final PlcReadResponse readResponse;
 
-        try (PlcConnection connection = connectionManager.getConnection(
-                String.format(CONNECTION_STRING_TEMPLATE, SERVER.getPort(), 
CONNECTION_STRING_SIMULATED_ENCODED))) {
+        try (PlcConnection connection = 
connectionManager.getConnection(connectionString(USERNAME, PASSWORD))) {
             final PlcWriteRequest writeRequest = 
connection.writeRequestBuilder()
-                    .addTagAddress(
-                            "foo",
-                            "STATE/foo:DINT",
-                            21
-                    )
+                    .addTagAddress("foo", "STATE/foo:DINT", 21)
                     .build();
             writeResponse = writeRequest.execute().get(TIMEOUT_VALUE, 
TIMEOUT_UNIT);
 
             final PlcReadRequest readRequest = connection.readRequestBuilder()
-                    .addTagAddress(
-                            "foo",
-                            "STATE/foo:DINT"
-                    )
+                    .addTagAddress("foo", "STATE/foo:DINT")
                     .build();
             readResponse = readRequest.execute().get(TIMEOUT_VALUE, 
TIMEOUT_UNIT);
         }
@@ -128,4 +125,15 @@ public class Plc4xServerTest {
         assertInstanceOf(Integer.class, 
readResponse.getPlcValue("foo").getObject());
         assertEquals(21, readResponse.getInteger("foo"));
     }
+
+    @Test
+    public void testWrongPasswordIsRejected() {
+        // A connection with bad credentials must fail during the mandatory 
auth handshake.
+        assertThrows(PlcConnectionException.class, () -> {
+            try (PlcConnection ignored = connectionManager.getConnection(
+                    connectionString(USERNAME, "wrong-password"))) {
+                // Should never get here.
+            }
+        });
+    }
 }

Reply via email to