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); - } }