diff --git a/.gitignore b/.gitignore
index e79ceb27..ab17c7ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,4 +25,5 @@ nbactions.xml
nb-configuration.xml
#vscode
-.vscode/
\ No newline at end of file
+.vscode/*
+
diff --git a/pom.xml b/pom.xml
index 8d2e06e3..d9b43164 100644
--- a/pom.xml
+++ b/pom.xml
@@ -105,6 +105,38 @@
testcontainers
1.20.2
test
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
+ org.slf4j
+ slf4j-jdk14
+ 1.7.36
+ test
+
+
+
+ org.xmlunit
+ xmlunit-core
+ 2.10.0
+ test
+
+
+ org.xmlunit
+ xmlunit-assertj3
+ 2.10.0
+ test
+
+
+ org.xmlunit
+ xmlunit-matchers
+ 2.10.0
+ test
diff --git a/src/com/trilead/ssh2/Connection.java b/src/com/trilead/ssh2/Connection.java
index 4c87e919..d9107dba 100644
--- a/src/com/trilead/ssh2/Connection.java
+++ b/src/com/trilead/ssh2/Connection.java
@@ -94,22 +94,22 @@ public static synchronized String[] getAvailableServerHostKeyAlgorithms()
private boolean authenticated = false;
private ChannelManager cm;
- private CryptoWishList cryptoWishList = new CryptoWishList();
+ protected CryptoWishList cryptoWishList = new CryptoWishList();
- private DHGexParameters dhgexpara = new DHGexParameters();
+ protected DHGexParameters dhgexpara = new DHGexParameters();
- private final String hostname;
+ protected final String hostname;
private final String sourceAddress;
- private final int port;
+ protected final int port;
- private TransportManager tm;
+ protected TransportManager tm;
- private boolean tcpNoDelay = false;
+ protected boolean tcpNoDelay = false;
- private ProxyData proxyData = null;
+ protected ProxyData proxyData = null;
- private Vector connectionMonitors = new Vector();
+ protected Vector connectionMonitors = new Vector();
/**
* Prepares a fresh Connection
object which can then be used
@@ -150,7 +150,7 @@ public Connection(String hostname, int port, String sourceAddress)
this.port = port;
this.sourceAddress = sourceAddress;
}
-
+
/**
* After a successful connect, one has to authenticate oneself. This method
* is based on DSA (it uses DSA to sign a challenge sent by the server).
@@ -1069,7 +1069,7 @@ public synchronized boolean isAuthMethodAvailable(String user, String method) th
return false;
}
- private final SecureRandom getOrCreateSecureRND()
+ protected final SecureRandom getOrCreateSecureRND()
{
if (generator == null)
generator = RandomFactory.create();
diff --git a/src/com/trilead/ssh2/transport/Acceptor.java b/src/com/trilead/ssh2/transport/Acceptor.java
new file mode 100644
index 00000000..3f559f4e
--- /dev/null
+++ b/src/com/trilead/ssh2/transport/Acceptor.java
@@ -0,0 +1,67 @@
+package com.trilead.ssh2.transport;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.net.Socket;
+import java.net.ServerSocket;
+
+import com.trilead.ssh2.Connection;
+import com.trilead.ssh2.ConnectionInfo;
+import com.trilead.ssh2.ServerHostKeyVerifier;
+
+/**
+ * This class is similar to {@link Connection} but is
+ * used to accept incoming connections from clients.
+ * Example use-cases are 'NETCONF Call Home' or
+ * 'reverse SSH'.
+ *
+ */
+public class Acceptor extends Connection{
+
+ /**
+ * Constuctor
+ * @param hostname is the hostname that this class is running on.
+ * @param port is the port that is used for incoming connections.
+ */
+ public Acceptor(String hostname,int port){
+ super(hostname,port);
+ }
+ /**
+ * This method reuses most of methods for {@link Connection#connect(ServerHostKeyVerifier, int, int, int)}. Parameters and descriptions applies here too.
+ * The main difference between
+ * this class and {@link Connection} is that we use {@link ServerSocket} and we bind with the port specified in constructor. The {@link ServerSocket#accept()}
+ * will wait (blocks) for an incoming connection for max {@param connectTimeout} . If connection is completed a {@link Socket} is returned and we set a timeout of this socket using
+ * {@param readTimeout}.
+ *
+ * @throws SocketTimeoutException If there is no incoming connection within {@param connectTimeout}.
+ *
+ */
+ public ConnectionInfo accept(ServerHostKeyVerifier verifier, int connectTimeout, int readTimeout, int kexTimeout) throws IOException{
+ if (tm != null) {
+ throw new IOException("Connection to " + hostname + " is already in connected state!");
+ }
+ if (connectTimeout < 0)
+ throw new IllegalArgumentException("connectTimeout must be non-negative!");
+
+ if (kexTimeout < 0)
+ throw new IllegalArgumentException("kexTimeout must be non-negative!");
+
+ tm = new TransportManager(hostname, port);
+ tm.setEnabledCallHomeSSH(true);
+
+ tm.setConnectionMonitors(connectionMonitors);
+ try {
+ tm.initialize(cryptoWishList, verifier, dhgexpara, connectTimeout, readTimeout, getOrCreateSecureRND(),
+ proxyData);
+ } catch (SocketTimeoutException ste) {
+ throw (SocketTimeoutException) new SocketTimeoutException(
+ "The accept() operation on the socket timed out.").initCause(ste);
+ }
+
+ tm.setTcpNoDelay(tcpNoDelay);
+
+ /* Wait until first KEX has finished */
+ return tm.getConnectionInfo(1);
+ }
+
+}
diff --git a/src/com/trilead/ssh2/transport/TransportManager.java b/src/com/trilead/ssh2/transport/TransportManager.java
index ad31f8c9..52c9e1df 100644
--- a/src/com/trilead/ssh2/transport/TransportManager.java
+++ b/src/com/trilead/ssh2/transport/TransportManager.java
@@ -6,6 +6,7 @@
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
+import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.SecureRandom;
@@ -133,7 +134,7 @@ public void run()
final private String sourceAddress;
String hostname;
int port;
- final Socket sock = new Socket();
+ Socket sock = new Socket();
final Object connectionSemaphore = new Object();
@@ -151,6 +152,9 @@ public void run()
Vector connectionMonitors = new Vector();
boolean monitorsWereInformed = false;
private ClientServerHello versions;
+ private boolean enabledCallHomeSSH = false;
+
+
/**
* There were reports that there are JDKs which use
@@ -365,6 +369,10 @@ private void establishConnection(ProxyData proxyData, int connectTimeout, int re
if (proxyData == null)
{
+ if(enabledCallHomeSSH){
+ establishCallHomeConnection(connectTimeout,readTimeout);
+ return;
+ }
if (sourceAddress != null)
{
@@ -474,6 +482,27 @@ private void establishConnection(ProxyData proxyData, int connectTimeout, int re
throw new IOException("Unsupported ProxyData");
}
+
+ private void establishCallHomeConnection(int connectTimeout,int readTimeout) {
+ Socket socket = null;
+ try (ServerSocket serverSocket = new ServerSocket()){
+ // Create a ServerSocket bound to a specific hostname and port
+ serverSocket.bind(new InetSocketAddress(port));
+ serverSocket.setSoTimeout(connectTimeout);
+
+ log.log(50,"SSH Call Home Server listen on " + hostname + " at port " + port);
+
+ // Accept a client connection (blocks until a connection is made)
+ socket = serverSocket.accept();
+ log.log(100,"Call Home SSH accepted connection on host "+sock.getLocalAddress().getHostAddress()+" on port "+port);
+ if(socket != null){
+ socket.setSoTimeout(readTimeout);
+ }
+ }catch (Exception e){
+ log.log(100,"Could not create client socket "+sock.getLocalAddress().getHostAddress()+" on port "+port,e);
+ }
+ sock = socket;
+ }
public void initialize(CryptoWishList cwl, ServerHostKeyVerifier verifier, DHGexParameters dhgex,
int connectTimeout, SecureRandom rnd, ProxyData proxyData) throws IOException {
@@ -820,6 +849,23 @@ public void receiveLoop() throws IOException
}
}
+ /**
+ * Get the value for SSH Call Home enaled.
+ *
+ * @return true if SSH Call Home enabled.
+ */
+ public boolean isEnabledCallHomeSSH() {
+ return enabledCallHomeSSH;
+ }
+ /**
+ * Set SSH Call Home
+ *
+ * @param enabledCallHomeSSH if set to true it will enable SSH Call Home.
+ */
+ public void setEnabledCallHomeSSH(boolean enabledCallHomeSSH) {
+ this.enabledCallHomeSSH = enabledCallHomeSSH;
+ }
+
/**
* Advertised maximum SSH packet size that the other side can send to us.
*/
diff --git a/test/com/trilead/ssh2/transport/CallHomeTest.java b/test/com/trilead/ssh2/transport/CallHomeTest.java
new file mode 100644
index 00000000..38f926f2
--- /dev/null
+++ b/test/com/trilead/ssh2/transport/CallHomeTest.java
@@ -0,0 +1,119 @@
+package com.trilead.ssh2.transport;
+
+
+import org.assertj.core.util.xml.XmlStringPrettyFormatter;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.testcontainers.Testcontainers;
+import org.testcontainers.containers.GenericContainer;
+
+
+import com.trilead.ssh2.log.Logger;
+
+import static org.xmlunit.assertj3.XmlAssert.assertThat;
+
+
+
+public class CallHomeTest {
+ private static final Logger LOGGER = Logger.getLogger(CallHomeTest.class);
+ private SshCallHomeClient sshCallHomeClient;
+ private GenericContainer> netopeer2;
+
+ @Before
+ public void setup() {
+ //setup logging from java.util.logger used in this project.
+ JULLoggerSetup.setupJULLogger();
+ }
+
+ /**
+ * This test creates a NETCONF server ( from Dockerfile) and
+ * configures it and triggers a SSH Call Home ( server acts as client and vice versa).
+ * Then we start an SSH Client ( using this library). We wait for incoming
+ * connection from server in accept(). Then when client and server are connected client send
+ * NETCONF {@code } message and read the same from NETCONF server.
+ * For NETCONF server we use Netopeer2.
+ *
+ * @see https://github.com/CESNET/netopeer2/a>
+ *
+ * @throws Exception if we fail
+ */
+ @Test()
+ public void triggerCallHome () throws Exception {
+
+ // https://www.testcontainers.org/features/networking/
+ Testcontainers.exposeHostPorts(4334);
+
+ //Start server and trigger SSH Call Home.
+ netopeer2 = new Netopeer2TestContainer().getNetopeer2Container();
+
+ //Start client and wait for incoming calls from server
+ sshCallHomeClient = new SshCallHomeClient();
+ sshCallHomeClient.accept();
+ //Send hello message from client.
+ sshCallHomeClient.send(clientHelloMsg());
+ //Wait to get a hello message from server.
+ String message = sshCallHomeClient.read();
+ LOGGER.log(50,"Message from node "+message);
+ assertThat(XmlStringPrettyFormatter.xmlPrettyFormat(message)).and(XmlStringPrettyFormatter.xmlPrettyFormat(serverHelloMsg())).areSimilar();
+
+
+ }
+
+ @After
+ public void cleanUp() {
+ sshCallHomeClient.disconnect();
+ netopeer2.stop();
+ netopeer2.close();
+ }
+
+
+ private String clientHelloMsg(){
+ return """
+
+
+ urn:ietf:params:netconf:base:1.0
+
+
+]]>]]>
+ """;
+ }
+
+ private String serverHelloMsg(){
+ return """
+
+
+ urn:ietf:params:netconf:base:1.0
+ urn:ietf:params:netconf:base:1.1
+ urn:ietf:params:netconf:capability:writable-running:1.0
+ urn:ietf:params:netconf:capability:candidate:1.0
+ urn:ietf:params:netconf:capability:confirmed-commit:1.1
+ urn:ietf:params:netconf:capability:rollback-on-error:1.0
+ urn:ietf:params:netconf:capability:validate:1.1
+ urn:ietf:params:netconf:capability:startup:1.0
+ urn:ietf:params:netconf:capability:xpath:1.0
+ urn:ietf:params:netconf:capability:with-defaults:1.0?basic-mode=explicit&also-supported=report-all,report-all-tagged,trim,explicit
+ urn:ietf:params:netconf:capability:notification:1.0
+ urn:ietf:params:netconf:capability:interleave:1.0
+ urn:ietf:params:netconf:capability:url:1.0?scheme=ftp,ftps,http,https,scp,sftp
+ urn:ietf:params:xml:ns:yang:ietf-yang-metadata?module=ietf-yang-metadata&revision=2016-08-05
+ urn:ietf:params:xml:ns:yang:ietf-inet-types?module=ietf-inet-types&revision=2013-07-15
+ urn:ietf:params:xml:ns:yang:ietf-yang-types?module=ietf-yang-types&revision=2013-07-15
+ urn:ietf:params:xml:ns:yang:ietf-netconf-acm?module=ietf-netconf-acm&revision=2018-02-14
+ urn:ietf:params:netconf:capability:yang-library:1.1?revision=2019-01-04&content-id=2008448144
+ urn:sysrepo:plugind?module=sysrepo-plugind&revision=2022-08-26
+ urn:ietf:params:xml:ns:netconf:base:1.0?module=ietf-netconf&revision=2013-09-29&features=writable-running,candidate,confirmed-commit,rollback-on-error,validate,startup,url,xpath
+ urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults?module=ietf-netconf-with-defaults&revision=2011-06-01
+ urn:ietf:params:xml:ns:yang:ietf-netconf-notifications?module=ietf-netconf-notifications&revision=2012-02-06
+ urn:ietf:params:xml:ns:netconf:notification:1.0?module=notifications&revision=2008-07-14
+ urn:ietf:params:xml:ns:netmod:notification?module=nc-notifications&revision=2008-07-14
+ urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring?module=ietf-netconf-monitoring&revision=2010-10-04
+ urn:ietf:params:xml:ns:yang:ietf-x509-cert-to-name?module=ietf-x509-cert-to-name&revision=2014-12-10
+ urn:ietf:params:xml:ns:yang:iana-crypt-hash?module=iana-crypt-hash&revision=2014-04-04&features=crypt-hash-md5,crypt-hash-sha-256,crypt-hash-sha-512
+
+ 1
+
+ """;
+ }
+
+}
diff --git a/test/com/trilead/ssh2/transport/JULLoggerSetup.java b/test/com/trilead/ssh2/transport/JULLoggerSetup.java
new file mode 100644
index 00000000..2ec0cac1
--- /dev/null
+++ b/test/com/trilead/ssh2/transport/JULLoggerSetup.java
@@ -0,0 +1,42 @@
+package com.trilead.ssh2.transport;
+
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.logging.SimpleFormatter;
+
+/**
+ * We have a dependency to bridge logging from
+ * testcontainers SLF4J logs to java.util.logging (JUL).
+ * {@code
+ *
+ * org.slf4j
+ * slf4j-jdk14
+ * 2.0.0
+ *
+* }
+* This class only setup JUL and then configures logging
+* to console.
+ *
+ *
+ */
+class JULLoggerSetup {
+ public static void setupJULLogger() {
+ Logger rootLogger = Logger.getLogger("");
+ rootLogger.setLevel(Level.ALL); // Set global logging level
+
+ // Remove default handlers
+ for (var handler : rootLogger.getHandlers()) {
+ rootLogger.removeHandler(handler);
+ }
+
+ // Add a ConsoleHandler for JUL to print logs to the console
+ ConsoleHandler consoleHandler = new ConsoleHandler();
+ consoleHandler.setLevel(Level.ALL); // Log everything
+ consoleHandler.setFormatter(new SimpleFormatter()); // Use simple log format
+
+ // Add the new handler to the root logger
+ rootLogger.addHandler(consoleHandler);
+ }
+
+}
diff --git a/test/com/trilead/ssh2/transport/JulLogConsumer.java b/test/com/trilead/ssh2/transport/JulLogConsumer.java
new file mode 100644
index 00000000..2c977bfd
--- /dev/null
+++ b/test/com/trilead/ssh2/transport/JulLogConsumer.java
@@ -0,0 +1,35 @@
+package com.trilead.ssh2.transport;
+
+import org.testcontainers.containers.output.OutputFrame;
+import org.testcontainers.containers.output.OutputFrame.OutputType;
+import com.trilead.ssh2.log.Logger;
+import java.util.function.Consumer;
+
+/**
+ * Consumer used to get logging for testcontainers.
+ */
+ class JulLogConsumer implements Consumer {
+ private final Logger logger;
+
+ // Constructor to initialize the JUL Logger
+ public JulLogConsumer(Logger logger) {
+ this.logger = logger;
+ }
+
+ @Override
+ public void accept(OutputFrame outputFrame) {
+ if (outputFrame != null) {
+ String message = outputFrame.getUtf8String().trim(); // Get log message
+ OutputType type = outputFrame.getType(); // Get output type (STDOUT, STDERR)
+
+ // Map OutputFrame types to appropriate log levels
+ if (type == OutputType.STDOUT) {
+ logger.log(800,message); // Standard output as INFO logs
+ } else if (type == OutputType.STDERR) {
+ logger.log(900,message); // Standard error as WARNING logs
+ } else if (type == OutputType.END) {
+ logger.log(1000,"Container log stream closed.");
+ }
+ }
+ }
+}
diff --git a/test/com/trilead/ssh2/transport/MyServerHostKeyVerifierImpl.java b/test/com/trilead/ssh2/transport/MyServerHostKeyVerifierImpl.java
new file mode 100644
index 00000000..49c65322
--- /dev/null
+++ b/test/com/trilead/ssh2/transport/MyServerHostKeyVerifierImpl.java
@@ -0,0 +1,14 @@
+package com.trilead.ssh2.transport;
+
+import com.trilead.ssh2.ServerHostKeyVerifier;
+
+ class MyServerHostKeyVerifierImpl implements ServerHostKeyVerifier{
+
+ @Override
+ public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) {
+ // Always accept any host key (Fake verifier)
+ System.out.println("Fake HostKeyVerifier: Host " + hostname + " accepted without verification.");
+ return true;
+ }
+
+}
diff --git a/test/com/trilead/ssh2/transport/Netopeer2TestContainer.java b/test/com/trilead/ssh2/transport/Netopeer2TestContainer.java
new file mode 100644
index 00000000..f1a74179
--- /dev/null
+++ b/test/com/trilead/ssh2/transport/Netopeer2TestContainer.java
@@ -0,0 +1,181 @@
+package com.trilead.ssh2.transport;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+
+import org.testcontainers.DockerClientFactory;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+import com.trilead.ssh2.log.Logger;
+
+
+/**
+ * This class uses testcontainers to create a generic testcontainer for
+ * Netopeer2 NETCONF server.
+ */
+ class Netopeer2TestContainer {
+
+ private static final Logger LOGGER = Logger.getLogger(Netopeer2TestContainer.class);
+ private static final String SSH_AUTH_CONFIG_FILE = "docker/ssh_listen.xml";
+ private static final String SSH_CALL_HOME_CONFIG_FILE = "docker/ssh_callhome.xml";
+ private static final String NACM_FILE = "docker/nacm.xml";
+ private static final String DOCKER_IMAGE_NAME = "ghcr.io/jenkinsci/trilead-ssh2:netopeer-3.5.1";
+ private static final int SSH_CALL_HOME_PORT = 4334;
+ public static final String TESTCONTAINERS_HOST_NAME = "host.testcontainers.internal";
+ private final Integer [] exposedPorts = {830,4334};
+ private GenericContainer> netopeer2;
+
+
+
+
+ public Netopeer2TestContainer() throws Exception{
+ initialize();
+ }
+
+
+ /**
+ * Create dockerContainer
+ * from Image with Netopeer2 and start 'netopeer2-server'.
+ *
+ * @throws Exception if we fail to create and start server.
+ */
+ private void initialize() throws Exception {
+
+ netopeer2 = createContainer().withNetworkAliases(DOCKER_IMAGE_NAME);
+ netopeer2.start();
+
+ disableNacm(netopeer2);
+ enableSshAuthMethod();
+ enableSshCallHome();
+ triggerSSHCallHome();
+ }
+
+ /**
+ * Get the container with all the methods that can be performed
+ * on the container using the testcontainer API {@code GenericContainer}
+ *
+ * @return netopeer2 running as test container
+ */
+ public GenericContainer> getNetopeer2Container() {
+ return netopeer2;
+ }
+
+ @SuppressWarnings("resource")
+ private GenericContainer> createContainer() {
+ assertTrue("Docker is not installed or docker client cannot be created!",DockerClientFactory.instance().isDockerAvailable());
+
+ final JulLogConsumer julLogConsumer = new JulLogConsumer(LOGGER);
+ return new GenericContainer<>(DockerImageName.parse(DOCKER_IMAGE_NAME)).withNetwork(null).withLogConsumer(julLogConsumer)
+ .withExposedPorts(exposedPorts).withAccessToHost(true)
+ .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Listening on 0.0.0.0:830 for SSH connections.*"));
+ }
+
+ private void disableNacm(GenericContainer> netopeer2)
+ throws InterruptedException, UnsupportedOperationException, IOException {
+ MountableFile mountableFile = MountableFile
+ .forClasspathResource(NACM_FILE);
+ netopeer2.copyFileToContainer(mountableFile, "/opt/dev/nacm.xml");
+ Container.ExecResult nacmFileCreatedRes = netopeer2.execInContainer("/bin/sh", "-c", "test -f /opt/dev/nacm.xml");
+ LOGGER.log(50,"Message '"+nacmFileCreatedRes.getStdout()+"' and result '"+nacmFileCreatedRes.getExitCode()+"' of NACM file imported into docker.");
+ Container.ExecResult nacmConfiguredRes = netopeer2.execInContainer("/bin/sh", "-c",
+ "/usr/bin/sysrepocfg --import=/opt/dev/nacm.xml --datastore running --module ietf-netconf-acm");
+ LOGGER.log(50,"Message '"+nacmConfiguredRes.getStdout()+"' and result '"+nacmConfiguredRes.getExitCode()+"' of NACM file executed in docker.");
+
+ }
+
+ private void enableSshAuthMethod() throws InterruptedException, UnsupportedOperationException, IOException {
+ MountableFile mountableFile = MountableFile
+ .forClasspathResource(SSH_AUTH_CONFIG_FILE);
+ netopeer2.copyFileToContainer(mountableFile, "/opt/dev/ssh_listen.xml");
+ Container.ExecResult sshAuthMethodFileCreatedRes = netopeer2.execInContainer("/bin/sh", "-c",
+ "test -f /opt/dev/ssh_listen.xml");
+ LOGGER.log(50,"Message '"+sshAuthMethodFileCreatedRes.getStdout()+"' and result '"+sshAuthMethodFileCreatedRes.getExitCode()+"' of SSH Auth method file imported into docker.");
+ Container.ExecResult sshAuthMethodConfiguredRes = netopeer2.execInContainer("/bin/sh", "-c",
+ "/usr/bin/sysrepocfg --import=/opt/dev/ssh_listen.xml --datastore running --module ietf-netconf-server");
+ LOGGER.log(50,"Message '"+sshAuthMethodConfiguredRes.getStdout()+"' and result '"+sshAuthMethodConfiguredRes.getExitCode()+"' of SSH Auth method file executed.");
+
+ }
+
+ private void enableSshCallHome() throws InterruptedException, UnsupportedOperationException, IOException{
+ MountableFile mountableFile = MountableFile
+ .forClasspathResource(SSH_CALL_HOME_CONFIG_FILE);
+ netopeer2.copyFileToContainer(mountableFile, "/opt/dev/ssh_callhome.xml");
+ Container.ExecResult sshCallHomeFileCreatedRes = netopeer2.execInContainer("/bin/sh", "-c","test -f /opt/dev/ssh_callhome.xml");
+ LOGGER.log(50,"Message '"+sshCallHomeFileCreatedRes.getStdout()+"' and result '"+sshCallHomeFileCreatedRes.getExitCode()+"' of SSH Call Home file imported into docker.");
+ Container.ExecResult sshCallHomeConfiguredRes = netopeer2.execInContainer("/bin/sh", "-c",
+ "/usr/bin/sysrepocfg --edit=/opt/dev/ssh_callhome.xml --datastore running --module ietf-netconf-server");
+ LOGGER.log(50,"Message '"+sshCallHomeConfiguredRes.getStdout()+"' and result '"+sshCallHomeConfiguredRes.getExitCode()+"' of SSH Call Home file executed.");
+ }
+
+ private void triggerSSHCallHome() throws InterruptedException, UnsupportedOperationException, IOException{
+ String content = triggerSSHCallHomeQuery(TESTCONTAINERS_HOST_NAME,SSH_CALL_HOME_PORT);
+ File tempFile = createTempFileWithContent(content);
+ netopeer2.copyFileToContainer(MountableFile.forHostPath(tempFile.getAbsolutePath()), "/opt/dev/enable_callhome.xml");
+ Container.ExecResult sshEnableCallHomeFileCreatedRes = netopeer2.execInContainer("/bin/sh", "-c","test -f /opt/dev/enable_callhome.xml");
+ LOGGER.log(50,"Message '"+sshEnableCallHomeFileCreatedRes.getStdout()+"' and result '"+sshEnableCallHomeFileCreatedRes.getExitCode()+"' of Enable SSH Call Home Query file imported into docker.");
+ Container.ExecResult sshEnableCallHomeConfiguredRes = netopeer2.execInContainer("/bin/sh", "-c", "/usr/bin/sysrepocfg --edit=/opt/dev/enable_callhome.xml --datastore running");
+ LOGGER.log(50,"Message '"+sshEnableCallHomeConfiguredRes.getStdout()+"' and result '"+sshEnableCallHomeConfiguredRes.getExitCode()+"' of Enable SSH Call Home Query file sent.");
+
+ }
+
+ private static File createTempFileWithContent(String content) throws IOException {
+ File tempFile = File.createTempFile("testfile", ".txt");
+
+ try (FileWriter writer = new FileWriter(tempFile)) {
+ writer.write(content);
+ }
+
+ return tempFile;
+ }
+
+ private String triggerSSHCallHomeQuery(String ip, int port) {
+ String message = """
+
+
+
+ default-client
+
+
+ default-ssh
+
+
+ %s
+ %d
+
+
+
+
+ default-key
+
+ genkey
+
+
+
+
+ default-ssh
+
+
+
+
+
+
+
+
+
+
+
+ """;
+ return message.formatted(ip, port);
+
+ }
+
+
+}
diff --git a/test/com/trilead/ssh2/transport/SshCallHomeClient.java b/test/com/trilead/ssh2/transport/SshCallHomeClient.java
new file mode 100644
index 00000000..f0d39398
--- /dev/null
+++ b/test/com/trilead/ssh2/transport/SshCallHomeClient.java
@@ -0,0 +1,111 @@
+package com.trilead.ssh2.transport;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.net.SocketTimeoutException;
+import java.nio.charset.Charset;
+
+import java.util.concurrent.TimeUnit;
+
+import com.trilead.ssh2.ServerHostKeyVerifier;
+import com.trilead.ssh2.Session;
+
+import com.trilead.ssh2.log.Logger;
+
+/**
+ * This is a very simple client wrapping this library.
+ * It supports accepting incoming connections
+ * from a NETCONF server. Once accept is complete we can
+ * send a simple NETCONF {@code } message and expect
+ * to receive the same from server.
+ *
+ * This client is only used to test a limited send/read sequence.
+ * Nor is it a complete NETCONF client.
+ */
+class SshCallHomeClient {
+ private static final Logger LOGGER = Logger.getLogger(SshCallHomeClient.class);
+ private static final String HOSTNAME = "host.testcontainers.internal";
+ private static final int SSH_CALL_HOME_PORT = 4334;
+ private static final String USER = "netconf";
+ private static final String PASSWORD = "netconf";
+ private static final int BUFFER_SIZE = 9 * 1024;
+ private static final String NETCONF_PROMPT = "]]>]]>";
+ private static final int DEFAULT_SEND_TIME_OUT = 5000;
+ private static final String SUBSYSTEM = "netconf";
+
+ private Acceptor acceptor;
+ private Session session;
+
+
+
+ void accept() throws IOException {
+ acceptor = new Acceptor(HOSTNAME,SSH_CALL_HOME_PORT);
+ // Implementing a fake HostKeyVerifier that always returns true
+ ServerHostKeyVerifier serverHostKeyVerifier = new MyServerHostKeyVerifierImpl();
+ acceptor.accept(serverHostKeyVerifier,400000,400000,40000);
+ auth();
+ session = acceptor.openSession();
+ session.startSubSystem(SUBSYSTEM);
+
+ }
+
+ void send(String msg) {
+ OutputStream stdin = session.getStdin();
+ try {
+ stdin.write(msg.getBytes());
+ LOGGER.log(50,"--> Sent message"+msg);
+ stdin.flush();
+ } catch (IOException e) {
+ LOGGER.log(50,"Could not send message");
+
+
+ }
+
+
+ }
+
+ String read()throws IOException{
+ InputStream stdout = session.getStdout();
+ final char[] buffer = new char[BUFFER_SIZE];
+ final StringBuilder rpcReply = new StringBuilder();
+ final long startTime = System.nanoTime();
+ final Reader in = new InputStreamReader(stdout, Charset.forName("UTF-8"));
+ boolean timeoutNotExceeded = true;
+ int promptPosition;
+ while ((promptPosition = rpcReply.indexOf(NETCONF_PROMPT)) < 0 &&
+ (timeoutNotExceeded = (TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) < DEFAULT_SEND_TIME_OUT))) {
+ int charsRead = in.read(buffer, 0, buffer.length);
+ if (charsRead < 0) throw new IOException("Input Stream has been closed during reading.");
+ rpcReply.append(buffer, 0, charsRead);
+ }
+
+ if (!timeoutNotExceeded){
+ throw new SocketTimeoutException("Command send timeout limit was exceeded: " + DEFAULT_SEND_TIME_OUT );
+ }
+ // fixing the rpc reply by removing device prompt
+ LOGGER.log(50,"<-- Received message:\n "+rpcReply);
+ rpcReply.setLength(promptPosition);
+ return rpcReply.toString();
+
+ }
+
+ void disconnect(){
+ acceptor.close();
+ }
+
+ private void auth() throws IOException {
+ boolean isAuthenticated = acceptor.authenticateWithPassword(USER,
+ PASSWORD);
+ if (!isAuthenticated) {
+ throw new IOException("Authentication failed.");
+ }
+
+ }
+
+
+
+
+}
diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile
new file mode 100644
index 00000000..df49ae1a
--- /dev/null
+++ b/test/docker/Dockerfile
@@ -0,0 +1,142 @@
+FROM ubuntu:22.04
+
+# This Docker image is build and push to the repository it takes about 6 min to build
+# The reason to pre-build it is because it compiles five libraries and an application everytime we run the test
+# docker build -t ghcr.io/jenkinsci/trilead-ssh2:netopeer-3.5.1 .
+
+
+# defaults
+RUN \
+ apt-get update && apt-get install -y \
+ net-tools \
+ git \
+ wget \
+ libssl-dev \
+ libtool \
+ build-essential \
+ vim \
+ autogen \
+ autoconf \
+ automake \
+ pkg-config \
+ libgtk-3-dev \
+ make \
+ vim \
+ valgrind \
+ doxygen \
+ libev-dev \
+ libpcre3-dev \
+ unzip \
+ sudo \
+ python3 \
+ build-essential \
+ bison \
+ flex \
+ swig \
+ libcmocka0 \
+ libcmocka-dev \
+ cmake \
+ gcc \
+ libpsl-dev \
+ supervisor \
+ libpam0g-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+
+
+# Adding netconf user
+RUN adduser --system netconf
+RUN mkdir -p /home/netconf/.ssh
+RUN echo "netconf:netconf" | chpasswd && adduser netconf sudo
+
+
+# Clearing and setting authorized ssh keys
+RUN \
+ echo '' > /home/netconf/.ssh/authorized_keys && \
+ ssh-keygen -A && \
+ ssh-keygen -t rsa -b 4096 -P '' -f /home/netconf/.ssh/id_rsa && \
+ cat /home/netconf/.ssh/id_rsa.pub >> /home/netconf/.ssh/authorized_keys
+
+
+# Updating shell to bash
+RUN sed -i s#/home/netconf:/bin/false#/home/netconf:/bin/bash# /etc/passwd
+
+RUN mkdir /opt/dev && sudo chown -R netconf /opt/dev
+
+# set password for user (same as the username)
+RUN echo "root:root" | chpasswd
+
+# libyang
+RUN \
+ cd /opt/dev && \
+ git clone https://github.com/CESNET/libyang.git && \
+ cd libyang && git checkout tags/v3.4.2 && mkdir build && cd build && \
+ cmake -DCMAKE_BUILD_TYPE:String="Release" -DCMAKE_INSTALL_PREFIX=/usr -DENABLE_BUILD_TESTS=OFF .. && \
+ make -j2 && \
+ make install && \
+ ldconfig
+
+# sysrepo
+RUN \
+ cd /opt/dev && \
+ git clone https://github.com/sysrepo/sysrepo.git && \
+ cd sysrepo && git checkout tags/v2.11.7 && mkdir build && cd build && \
+ cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE="Release" .. && \
+ make -j2 && \
+ make install && \
+ ldconfig
+
+#libssh (for libnetconf2)
+RUN \
+ cd /opt/dev && \
+ git clone http://git.libssh.org/projects/libssh.git && cd libssh && git checkout tags/libssh-0.11.1 &&\
+ mkdir build && cd build && \
+ cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE="Release" -DWITH_ZLIB=ON -DWITH_NACL=OFF -DWITH_PCAP=OFF .. && \
+ make -j2 && \
+ make install && \
+ ldconfig
+
+#curl (for libnetconf2)
+
+RUN \
+cd /opt/dev && \
+git clone https://github.com/curl/curl.git && cd curl && git checkout tags/curl-8_9_1 &&\
+autoreconf -fi && \
+./configure --prefix=/usr --with-openssl && \
+make -j2 && \
+make install && \
+ldconfig
+
+
+# libnetconf2
+RUN \
+ cd /opt/dev && \
+ git clone https://github.com/CESNET/libnetconf2.git && \
+ cd libnetconf2 && git checkout tags/v3.5.1 && mkdir build && cd build && \
+ cmake -DCMAKE_FIND_PACKAGE_NO_SYSTEM_PATH=ON -DCMAKE_INSTALL_PREFIX:PATH=/usr -DENABLE_TESTS=OFF .. &&\
+ make -j2 && \
+ make install && \
+ ldconfig
+
+
+# netopeer2
+RUN \
+ cd /opt/dev && \
+ git clone https://github.com/CESNET/Netopeer2.git && cd Netopeer2 && \
+ git checkout tags/v2.2.31 && \
+ mkdir build && cd build && \
+ cmake -DNACM_RECOVERY_UID=102 -DCMAKE_INSTALL_PREFIX:PATH=/usr .. && \
+ make -j2 && \
+ make install
+
+ RUN rm -fr /opt/dev
+ ENV EDITOR vim
+ EXPOSE 830
+ EXPOSE 4334
+
+# start netopeer2 server.
+CMD ["/usr/sbin/netopeer2-server", "-d", "-v2 3"]
+
+#Comment above and uncomment below if you want to debug SSH specifc actions on Netopeer2 when running.
+#CMD ["/usr/sbin/netopeer2-server", "-d", "-c", "SSH"]
+
\ No newline at end of file
diff --git a/test/docker/nacm.xml b/test/docker/nacm.xml
new file mode 100644
index 00000000..10e50bb2
--- /dev/null
+++ b/test/docker/nacm.xml
@@ -0,0 +1,3 @@
+
+ false
+
diff --git a/test/docker/ssh_callhome.xml b/test/docker/ssh_callhome.xml
new file mode 100644
index 00000000..6598483a
--- /dev/null
+++ b/test/docker/ssh_callhome.xml
@@ -0,0 +1,33 @@
+
+
+
+ default-client
+
+
+ ssh-default
+
+
+ localhost
+
+
+
+
+ default-key
+
+ genkey
+
+
+
+
+ default-ssh
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/docker/ssh_listen.xml b/test/docker/ssh_listen.xml
new file mode 100644
index 00000000..47e27939
--- /dev/null
+++ b/test/docker/ssh_listen.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ default-ssh
+
+
+ 0.0.0.0
+
+
+
+
+ default-key
+
+ genkey
+
+
+
+
+
+
+ netconf
+ $0$netconf
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file