diff --git a/CHANGES.md b/CHANGES.md
index a3f065759..4e89dd62e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -56,7 +56,18 @@ Provide (read-only) public access to internal session state values related to KE
* *getSessionKexDetails*
* *getSessionCountersDetails*
+### Added `SessionListener#sessionPeerIdentificationSent` callback
+
+Invoked after successful or failed attempt to send client/server identification to peer. The callback provides the failure error if such occurred.
+
## Potential compatibility issues
+### Added finite wait time for default implementation of `ClientSession#executeRemoteCommand`
+
+* `CoreModuleProperties#EXEC_CHANNEL_OPEN_TIMEOUT` - default = 30 seconds.
+* `CoreModuleProperties#EXEC_CHANNEL_CMD_TIMEOUT` - default = 30 seconds.
+
+This may cause failures for code that was running long execution commands using the default method implementations.
+
## Major Code Re-factoring
diff --git a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
index e3eca3dbc..105095730 100644
--- a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
+++ b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
@@ -703,7 +703,7 @@ public static void outputDebugMessage(String format, Object o) {
public static void outputDebugMessage(String format, Object... args) {
if (OUTPUT_DEBUG_MESSAGES) {
- outputDebugMessage(String.format(format, args));
+ outputDebugMessage(GenericUtils.isEmpty(args) ? format : String.format(format, args));
}
}
@@ -713,6 +713,24 @@ public static void outputDebugMessage(Object message) {
}
}
+ public static void failWithWrittenErrorMessage(String format, Object... args) {
+ failWithWrittenErrorMessage(GenericUtils.isEmpty(args) ? format : String.format(format, args));
+ }
+
+ public static void failWithWrittenErrorMessage(Object message) {
+ writeErrorMessage(message);
+ fail(Objects.toString(message));
+ }
+
+ public static void writeErrorMessage(String format, Object... args) {
+ writeErrorMessage(GenericUtils.isEmpty(args) ? format : String.format(format, args));
+ }
+
+ public static void writeErrorMessage(Object message) {
+ System.err.append("===[ERROR]=== ").println(message);
+ System.err.flush();
+ }
+
/* ---------------------------------------------------------------------------- */
public static void replaceJULLoggers() {
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index a4b1c1410..f8fb17561 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -59,6 +59,7 @@
import org.apache.sshd.common.util.io.output.NoCloseOutputStream;
import org.apache.sshd.common.util.io.output.NullOutputStream;
import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.core.CoreModuleProperties;
/**
*
@@ -304,10 +305,12 @@ default void executeRemoteCommand(
ClientChannel channel = createExecChannel(command)) {
channel.setOut(channelOut);
channel.setErr(channelErr);
- channel.open().await(); // TODO use verify and a configurable timeout
- // TODO use a configurable timeout
- Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, 0L);
+ Duration openTimeout = CoreModuleProperties.EXEC_CHANNEL_OPEN_TIMEOUT.getRequired(channel);
+ channel.open().verify(openTimeout);
+
+ Duration execTimeout = CoreModuleProperties.EXEC_CHANNEL_CMD_TIMEOUT.getRequired(channel);
+ Collection waitMask = channel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, execTimeout);
if (waitMask.contains(ClientChannelEvent.TIMEOUT)) {
throw new SocketTimeoutException("Failed to retrieve command result in time: " + command);
}
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
index 19e1b680f..116f5882f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
@@ -74,6 +74,7 @@ public class ClientSessionImpl extends AbstractClientSession {
public ClientSessionImpl(ClientFactoryManager client, IoSession ioSession) throws Exception {
super(client, ioSession);
+
if (log.isDebugEnabled()) {
log.debug("Client session created: {}", ioSession);
}
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java
index 74780b996..ae8d4d707 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java
@@ -102,6 +102,21 @@ default void sessionPeerIdentificationLine(
Session session, String line, List extraLines) {
// ignored
}
+ /**
+ * Identification sent to peer
+ *
+ * @param session The {@link Session} instance
+ * @param version The resolved identification version
+ * @param extraLines Extra data preceding the identification to be sent. Note: the list is modifiable only if
+ * this is a server session. The user may modify it based on the peer.
+ * @param error {@code null} if sending was successful
+ * @see RFC 4253 - section 4.2 - Protocol
+ * Version Exchange
+ */
+ default void sessionPeerIdentificationSent(
+ Session session, String version, List extraLines, Throwable error) {
+ // ignored
+ }
/**
* The peer's identification version was received
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
index 43b2a5bbd..912fc7f1b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
@@ -601,7 +601,8 @@ protected void doHandleMessage(Buffer buffer) throws Exception {
&& CoreModuleProperties.USE_STRICT_KEX.getRequired(this)
&& (cmd != SshConstants.SSH_MSG_KEXINIT)) {
log.error("doHandleMessage({}) invalid 1st message: {}", this, SshConstants.getCommandMessageName(cmd));
- throw new SshException(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, "Strict KEX Error");
+ disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, "Strict KEX Error");
+ return;
}
if (log.isDebugEnabled()) {
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
index a5b45a722..df9376968 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
@@ -49,6 +49,7 @@
import org.apache.sshd.common.channel.throttle.ChannelStreamWriterResolverManager;
import org.apache.sshd.common.digest.Digest;
import org.apache.sshd.common.forward.Forwarder;
+import org.apache.sshd.common.future.SshFutureListener;
import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.io.IoWriteFuture;
import org.apache.sshd.common.kex.AbstractKexFactoryManager;
@@ -77,6 +78,7 @@
/**
* Contains split code in order to make {@link AbstractSession} class smaller
*/
+@SuppressWarnings("checkstyle:MethodCount")
public abstract class SessionHelper extends AbstractKexFactoryManager implements Session {
// Session timeout measurements
@@ -628,6 +630,31 @@ protected void signalSendIdentification(SessionListener listener, String version
listener.sessionPeerIdentificationSend(this, version, extraLines);
}
+ protected void signalIdentificationSent(String version, List extraLines, Throwable error) throws Exception {
+ try {
+ invokeSessionSignaller(l -> {
+ signalIdentificationSent(l, version, extraLines, error);
+ return null;
+ });
+ } catch (Throwable err) {
+ Throwable e = ExceptionUtils.peelException(err);
+ if (e instanceof Exception) {
+ throw (Exception) e;
+ } else {
+ throw new RuntimeSshException(e);
+ }
+ }
+ }
+
+ protected void signalIdentificationSent(
+ SessionListener listener, String version, List extraLines, Throwable err) throws Exception {
+ if (listener == null) {
+ return;
+ }
+
+ listener.sessionPeerIdentificationSent(this, version, extraLines, err);
+ }
+
protected void signalReadPeerIdentificationLine(String line, List extraLines) throws Exception {
try {
invokeSessionSignaller(l -> {
@@ -833,28 +860,45 @@ protected IoWriteFuture sendIdentification(String version, List extraLin
ReservedSessionMessagesHandler handler = getReservedSessionMessagesHandler();
IoWriteFuture future = (handler == null) ? null : handler.sendIdentification(this, version, extraLines);
boolean debugEnabled = log.isDebugEnabled();
- if (future != null) {
+ if (future == null) {
+ String ident = version;
+ if (GenericUtils.size(extraLines) > 0) {
+ ident = GenericUtils.join(extraLines, "\r\n") + "\r\n" + version;
+ }
+
+ if (debugEnabled) {
+ log.debug("sendIdentification({}): {}",
+ this, ident.replace('\r', '|').replace('\n', '|'));
+ }
+
+ IoSession networkSession = getIoSession();
+ byte[] data = (ident + "\r\n").getBytes(StandardCharsets.UTF_8);
+ future = networkSession.writeBuffer(new ByteArrayBuffer(data));
+ } else {
if (debugEnabled) {
log.debug("sendIdentification({})[{}] sent {} lines via reserved handler",
this, version, GenericUtils.size(extraLines));
}
-
- return future;
}
- String ident = version;
- if (GenericUtils.size(extraLines) > 0) {
- ident = GenericUtils.join(extraLines, "\r\n") + "\r\n" + version;
- }
+ future.addListener(new SshFutureListener() {
+ @Override
+ public void operationComplete(IoWriteFuture future) {
+ try {
+ signalIdentificationSent(version, extraLines, future.getException());
+ } catch(Throwable err) {
+ Throwable e = ExceptionUtils.peelException(err);
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ } else {
+ throw new RuntimeSshException(e);
+ }
- if (debugEnabled) {
- log.debug("sendIdentification({}): {}",
- this, ident.replace('\r', '|').replace('\n', '|'));
- }
+ }
+ }
+ });
- IoSession networkSession = getIoSession();
- byte[] data = (ident + "\r\n").getBytes(StandardCharsets.UTF_8);
- return networkSession.writeBuffer(new ByteArrayBuffer(data));
+ return future;
}
/**
diff --git a/sshd-core/src/main/java/org/apache/sshd/core/CoreModuleProperties.java b/sshd-core/src/main/java/org/apache/sshd/core/CoreModuleProperties.java
index b276b42ff..300ed1374 100644
--- a/sshd-core/src/main/java/org/apache/sshd/core/CoreModuleProperties.java
+++ b/sshd-core/src/main/java/org/apache/sshd/core/CoreModuleProperties.java
@@ -61,6 +61,21 @@ public final class CoreModuleProperties {
public static final Property CHANNEL_OPEN_TIMEOUT
= Property.duration("ssh-agent-server-channel-open-timeout", Duration.ofSeconds(30));
+ /**
+ * Value that can be set on the {@link org.apache.sshd.common.FactoryManager} the session or the channel to
+ * configure the channel open timeout value (millis) for executing a remote command using default implementation.
+ */
+ public static final Property EXEC_CHANNEL_OPEN_TIMEOUT
+ = Property.duration("ssh-exec-channel-open-timeout", Duration.ofSeconds(30));
+
+ /**
+ * Value that can be set on the {@link org.apache.sshd.common.FactoryManager} the session or the channel to
+ * configure the channel command execution timeout value (millis) for executing a remote command using default
+ * implementation.
+ */
+ public static final Property EXEC_CHANNEL_CMD_TIMEOUT
+ = Property.duration("ssh-exec-channel-cmd-timeout", Duration.ofSeconds(30));
+
/**
* Value used to configure the type of proxy forwarding channel to be used. See also
* https://tools.ietf.org/html/draft-ietf-secsh-agent-02
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
index a41c2e1fc..bcd76e0e8 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
@@ -1510,7 +1510,6 @@ public void testKeyboardInteractiveInSessionUserInteractiveFailure() throws Exce
CoreModuleProperties.PASSWORD_PROMPTS.set(client, maxPrompts);
AtomicInteger numberOfRequests = new AtomicInteger();
UserAuthKeyboardInteractiveFactory auth = new UserAuthKeyboardInteractiveFactory() {
-
@Override
public UserAuthKeyboardInteractive createUserAuth(ClientSession session) throws IOException {
return new UserAuthKeyboardInteractive() {
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/StrictKexTest.java b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/StrictKexTest.java
new file mode 100644
index 000000000..2e2b9a796
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/kex/extension/StrictKexTest.java
@@ -0,0 +1,396 @@
+/*
+ * 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
+ *
+ * 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 org.apache.sshd.common.kex.extension;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.future.KeyExchangeFuture;
+import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionListener;
+import org.apache.sshd.common.session.helpers.SessionCountersDetails;
+import org.apache.sshd.common.session.helpers.SessionKexDetails;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.core.CoreModuleProperties;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.command.Command;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.EchoCommand;
+import org.apache.sshd.util.test.EchoCommandFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author Apache MINA SSHD Project
+ * @see Terrapin Mitigation: "strict-kex"
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class StrictKexTest extends BaseTestSupport {
+ private SshServer sshd;
+ private SshClient client;
+
+ public StrictKexTest() {
+ super();
+ }
+
+ @Override
+ protected SshServer setupTestServer() {
+ SshServer server = super.setupTestServer();
+ CoreModuleProperties.USE_STRICT_KEX.set(server, true);
+ return server;
+ }
+
+ @Override
+ protected SshClient setupTestClient() {
+ SshClient sshc = super.setupTestClient();
+ CoreModuleProperties.USE_STRICT_KEX.set(sshc, true);
+ return sshc;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ sshd = setupTestServer();
+ client = setupTestClient();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (sshd != null) {
+ sshd.stop(true);
+ }
+ if (client != null) {
+ client.stop();
+ }
+ }
+
+ @Test
+ public void testConnectionClosedIfFirstPacketFromClientNotKexInit() throws Exception {
+ testConnectionClosedIfFirstPacketFromPeerNotKexInit(true);
+ }
+
+ @Test
+ public void testConnectionClosedIfFirstPacketFromServerNotKexInit() throws Exception {
+ testConnectionClosedIfFirstPacketFromPeerNotKexInit(false);
+ }
+
+ private void testConnectionClosedIfFirstPacketFromPeerNotKexInit(boolean clientInitiates) throws Exception {
+ AtomicBoolean disconnectSignalled = new AtomicBoolean();
+ SessionListener disconnectListener = new SessionListener() {
+ @Override
+ public void sessionDisconnect(
+ Session session, int reason, String msg, String language, boolean initiator) {
+ if (reason != SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR) {
+ failWithWrittenErrorMessage("Invalid disconnect reason(%d): %s", reason, msg);
+ }
+
+ synchronized (disconnectSignalled) {
+ disconnectSignalled.set(true);
+ disconnectSignalled.notifyAll();
+ }
+ }
+ };
+
+ SessionListener messageInitiator = new SessionListener() {
+ @Override // At this stage KEX-INIT not sent yet
+ public void sessionNegotiationOptionsCreated(Session session, Map proposal) {
+ try {
+ IoWriteFuture future =
+ session.sendDebugMessage(true, getCurrentTestName(), null);
+ boolean completed = future.verify(CONNECT_TIMEOUT).isWritten();
+ if (!completed) {
+ failWithWrittenErrorMessage("Debug message not sent on time");
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+
+ if (clientInitiates) {
+ client.addSessionListener(messageInitiator);
+ sshd.addSessionListener(disconnectListener);
+ } else {
+ sshd.addSessionListener(messageInitiator);
+ client.addSessionListener(disconnectListener);
+ }
+
+ try (ClientSession session = obtainInitialTestClientSession()) {
+ fail("Unexpected session success");
+ } catch (SshException e) {
+ synchronized (disconnectSignalled) {
+ for (long remWait = CONNECT_TIMEOUT.toMillis(); remWait > 0L;) {
+ if (disconnectSignalled.get()) {
+ break;
+ }
+
+ long waitStart = System.currentTimeMillis();
+ disconnectSignalled.wait(remWait);
+ long waitEnd = System.currentTimeMillis();
+
+ // Handle spurious wake-up
+ if (waitEnd > waitStart) {
+ remWait -= (waitEnd - waitStart);
+ } else {
+ remWait -= 125L;
+ }
+ }
+ }
+
+ assertTrue("Disconnect signalled", disconnectSignalled.get());
+ } finally {
+ client.stop();
+ }
+ }
+
+ @Test
+ public void testStrictKexIgnoredByClientIfNotFirstKexInit() throws Exception {
+ testStrictKexIgnoredByPeerIfNotFirstKexInit(false);
+ }
+
+ @Test
+ public void testStrictKexIgnoredByServerIfNotFirstKexInit() throws Exception {
+ testStrictKexIgnoredByPeerIfNotFirstKexInit(true);
+ }
+
+ private void testStrictKexIgnoredByPeerIfNotFirstKexInit(boolean clientInitiates) throws Exception {
+ // TODO
+ }
+
+ @Test
+ public void testRekeyResetsPacketSequenceNumbers() throws Exception {
+ sshd.addSessionListener(new SessionListener() {
+ private SessionKexDetails beforeDetails;
+ private SessionCountersDetails beforeCounters;
+
+ @Override
+ public void sessionNegotiationEnd(
+ Session session, Map clientProposal,
+ Map serverProposal, Map negotiatedOptions,
+ Throwable reason) {
+ SessionKexDetails details = session.getSessionKexDetails();
+ assertTrue("StrictKexSignalled[server]", details.isStrictKexSignalled());
+
+ SessionCountersDetails counters = session.getSessionCountersDetails();
+ if (beforeDetails == null) {
+ beforeDetails = details;
+ }
+ if (beforeCounters == null) {
+ beforeCounters = counters;
+ }
+
+ if ((details.getNewKeysSentCount() > 1)
+ && (details.getNewKeysReceivedCount() > 1)) {
+ assertSessionSequenceNumbersReset(session, beforeDetails, beforeCounters);
+ }
+ }
+ });
+ sshd.setCommandFactory(new TestEchoCommandFactory());
+
+ TestEchoCommand.latch = new CountDownLatch(1);
+ try (ClientSession session = obtainInitialTestClientSession()) {
+ SessionKexDetails beforeDetails = session.getSessionKexDetails();
+ assertTrue("StrictKexSignalled[client]", beforeDetails.isStrictKexSignalled());
+
+ /*
+ * Create some traffic in order to "inflate" the sequence numbers enough
+ * so that when we re-key and (we assume) the sequence number are reset
+ * they will not have increased to the same values due to the NEWKEY exchange.
+ */
+ String response = session.executeRemoteCommand(getCurrentTestName());
+ assertNotNull("No shell echo response", response);
+
+ boolean shellFinished = TestEchoCommand.latch.await(AUTH_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
+ assertTrue("Shell finished", shellFinished);
+
+ SessionCountersDetails beforeCounters = session.getSessionCountersDetails();
+ KeyExchangeFuture rekeyFuture = session.reExchangeKeys();
+ boolean exchanged = rekeyFuture.verify(AUTH_TIMEOUT).isDone();
+ assertTrue("Rekey exchange completed", exchanged);
+ assertSessionSequenceNumbersReset(session, beforeDetails, beforeCounters);
+ } finally {
+ client.stop();
+ }
+ }
+
+ // NOTE: we use failWithWrittenErrorMessage in order to compensate for session timeout in case of a debug breakpoint
+ static void assertSessionSequenceNumbersReset(
+ Session session, SessionKexDetails beforeDetails, SessionCountersDetails beforeCounters) {
+ long incomingPacketSequenceNumberBefore = beforeCounters.getInputPacketSequenceNumber();
+ long outputPacketSequenceNumberBefore = beforeCounters.getOutputPacketSequenceNumber();
+
+ SessionCountersDetails afterCounters = session.getSessionCountersDetails();
+ long incomingPacketSequenceNumberAfter = afterCounters.getInputPacketSequenceNumber();
+ long outputPacketSequenceNumberAfter = afterCounters.getOutputPacketSequenceNumber();
+
+ String sessionType = session.isServerSession() ? "server" : "client";
+ if (incomingPacketSequenceNumberAfter > incomingPacketSequenceNumberBefore) {
+ failWithWrittenErrorMessage(sessionType + ": Incoming packet sequence number not reset: before="
+ + incomingPacketSequenceNumberBefore + ", after=" + incomingPacketSequenceNumberAfter);
+ }
+
+ if (outputPacketSequenceNumberAfter > outputPacketSequenceNumberBefore) {
+ failWithWrittenErrorMessage(sessionType + ": Outgoing packet sequence number not reset: before="
+ + incomingPacketSequenceNumberBefore + ", after=" + incomingPacketSequenceNumberAfter);
+ }
+
+ SessionKexDetails afterDetails = session.getSessionKexDetails();
+ int beforeSentNewKeys = beforeDetails.getNewKeysSentCount();
+ int afterSentNewKeys = afterDetails.getNewKeysSentCount();
+ if (beforeSentNewKeys >= afterSentNewKeys) {
+ failWithWrittenErrorMessage(sessionType + ": sent NEWKEY count not updated: before=" + beforeSentNewKeys
+ + ", after=" + afterSentNewKeys);
+ }
+
+ int beforeRcvdNewKeys = beforeDetails.getNewKeysReceivedCount();
+ int afterRcvdNewKeys = afterDetails.getNewKeysReceivedCount();
+ if (beforeRcvdNewKeys >= afterRcvdNewKeys) {
+ failWithWrittenErrorMessage(sessionType + ": received NEWKEY count not updated: before=" + beforeRcvdNewKeys
+ + ", after=" + afterRcvdNewKeys);
+ }
+ }
+
+ @Test
+ public void testStrictKexNotActivatedIfClientDoesNotSupportIt() throws Exception {
+ testStrictKexNotActivatedIfNotSupportByPeer(false);
+ }
+
+ @Test
+ public void testStrictKexNotActivatedIfServerDoesNotSupportIt() throws Exception {
+ testStrictKexNotActivatedIfNotSupportByPeer(true);
+ }
+
+ private void testStrictKexNotActivatedIfNotSupportByPeer(boolean clientSupported) throws Exception {
+ if (clientSupported) {
+ CoreModuleProperties.USE_STRICT_KEX.set(sshd, false);
+ } else {
+ CoreModuleProperties.USE_STRICT_KEX.set(client, false);
+ }
+
+ sshd.addSessionListener(new SessionListener() {
+ @Override
+ public void sessionNegotiationEnd(
+ Session session, Map clientProposal,
+ Map serverProposal, Map negotiatedOptions,
+ Throwable reason) {
+ SessionKexDetails details = session.getSessionKexDetails();
+ assertEquals("StrictKexEnabled[server]", !clientSupported, details.isStrictKexEnabled());
+ assertFalse("StrictKexSignalled[server]", details.isStrictKexSignalled());
+ }
+ });
+
+ try (ClientSession session = obtainInitialTestClientSession()) {
+ SessionKexDetails details = session.getSessionKexDetails();
+ assertEquals("StrictKexEnabled[client]", clientSupported, details.isStrictKexEnabled());
+ assertFalse("StrictKexSignalled[client]", details.isStrictKexSignalled());
+ } finally {
+ client.stop();
+ }
+ }
+
+ private ClientSession obtainInitialTestClientSession() throws IOException {
+ sshd.start();
+ int port = sshd.getPort();
+
+ client.start();
+ return createTestClientSession(port);
+ }
+
+ private ClientSession createTestClientSession(int port) throws IOException {
+ ClientSession session = createTestClientSession(TEST_LOCALHOST, port);
+ try {
+ InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
+ assertEquals("Mismatched connect host", TEST_LOCALHOST, addr.getHostString());
+
+ ClientSession returnValue = session;
+ session = null; // avoid 'finally' close
+ return returnValue;
+ } finally {
+ if (session != null) {
+ session.close();
+ }
+ }
+ }
+
+ private ClientSession createTestClientSession(String host, int port) throws IOException {
+ ClientSession session = client.connect(getCurrentTestName(), host, port)
+ .verify(CONNECT_TIMEOUT).getSession();
+ try {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(AUTH_TIMEOUT);
+
+ InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
+ assertNotNull("No reported connect address", addr);
+ assertEquals("Mismatched connect port", port, addr.getPort());
+
+ ClientSession returnValue = session;
+ session = null; // avoid 'finally' close
+ return returnValue;
+ } finally {
+ if (session != null) {
+ session.close();
+ }
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////
+
+ public static class TestEchoCommandFactory extends EchoCommandFactory {
+ public TestEchoCommandFactory() {
+ super();
+ }
+
+ @Override
+ public Command createCommand(ChannelSession channel, String command) throws IOException {
+ return new TestEchoCommand(command);
+ }
+ }
+
+ public static class TestEchoCommand extends EchoCommand {
+ // CHECKSTYLE:OFF
+ public static CountDownLatch latch;
+ // CHECKSTYLE:ON
+
+ public TestEchoCommand(String command) {
+ super(command);
+ }
+
+ @Override
+ public void destroy(ChannelSession channel) throws Exception {
+ if (latch != null) {
+ latch.countDown();
+ }
+ super.destroy(channel);
+ }
+ }
+}
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/EchoCommand.java b/sshd-core/src/test/java/org/apache/sshd/util/test/EchoCommand.java
new file mode 100644
index 000000000..530ff894c
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/EchoCommand.java
@@ -0,0 +1,40 @@
+/*
+ * 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
+ *
+ * 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 org.apache.sshd.util.test;
+
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @author Apache MINA SSHD Project
+ */
+public class EchoCommand extends CommandExecutionHelper {
+ public EchoCommand(String command) {
+ super(command);
+ }
+
+ @Override
+ protected boolean handleCommandLine(String command) throws Exception {
+ OutputStream out = getOutputStream();
+ out.write(command.getBytes(StandardCharsets.UTF_8));
+ out.flush();
+ return true;
+ }
+}
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/EchoCommandFactory.java b/sshd-core/src/test/java/org/apache/sshd/util/test/EchoCommandFactory.java
new file mode 100644
index 000000000..b4fccfe5d
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/EchoCommandFactory.java
@@ -0,0 +1,40 @@
+/*
+ * 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
+ *
+ * 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 org.apache.sshd.util.test;
+
+import java.io.IOException;
+
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.command.Command;
+import org.apache.sshd.server.command.CommandFactory;
+
+/**
+ * @author Apache MINA SSHD Project
+ */
+public class EchoCommandFactory implements CommandFactory {
+ public EchoCommandFactory() {
+ super();
+ }
+
+ @Override
+ public Command createCommand(ChannelSession channel, String command) throws IOException {
+ return new EchoCommand(command);
+ }
+}
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java b/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java
index b7115870c..5226a6615 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java
@@ -36,6 +36,5 @@ protected boolean handleCommandLine(String command) throws Exception {
out.flush();
return !"exit".equals(command);
-
}
}