You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2021/01/09 05:41:48 UTC

[mina-sshd] 02/04: [SSHD-1097] Added more SessionListener callbacks related to the initial version and key exchange

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

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 0b5544a8af5bf58c23e229f39acce6639a5feb7e
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Jan 8 07:02:10 2021 +0200

    [SSHD-1097] Added more SessionListener callbacks related to the initial version and key exchange
---
 CHANGES.md                                         |   2 +
 .../sshd/client/session/AbstractClientSession.java |   4 +
 .../common/kex/extension/KexExtensionHandler.java  |   6 +-
 .../sshd/common/session/SessionListener.java       |  42 +++++++++
 .../common/session/helpers/AbstractSession.java    |   2 +
 .../sshd/common/session/helpers/SessionHelper.java | 103 ++++++++++++++++++---
 .../sshd/server/session/AbstractServerSession.java |   8 +-
 .../sshd/server/session/ServerSessionImpl.java     |  10 +-
 .../session/helpers/AbstractSessionTest.java       |  20 ++--
 9 files changed, 169 insertions(+), 28 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 69e03b3..7d2b02c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -25,6 +25,8 @@
 ## Behavioral changes and enhancements
 
 * [SSHD-1085](https://issues.apache.org/jira/browse/SSHD-1085) Added more notifications related to channel state change for detecting channel closing or closed earlier.
+* [SSHD-1091](https://issues.apache.org/jira/browse/SSHD-1091) Renamed `sshd-contrib` top-level package in order to align naming convention.
+* [SSHD-1097](https://issues.apache.org/jira/browse/SSHD-1097) Added more `SessionListener` callbacks related to the initial version and key exchange
 * [SSHD-1109](https://issues.apache.org/jira/browse/SSHD-1109) Replace log4j with logback as the slf4j logger implementation for tests
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side password authentication progress
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side public key authentication progress
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index 6b1faff..ce8e529 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -23,6 +23,7 @@ import java.io.IOException;
 import java.net.SocketAddress;
 import java.security.KeyPair;
 import java.security.PublicKey;
+import java.util.Collections;
 import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
@@ -353,6 +354,9 @@ public abstract class AbstractClientSession extends AbstractSession implements C
 
     protected IoWriteFuture sendClientIdentification() throws Exception {
         clientVersion = resolveIdentificationString(CoreModuleProperties.CLIENT_IDENTIFICATION.getName());
+        // Note: we intentionally use an unmodifiable list in order to enforce the fact that client cannot send header lines
+        signalSendIdentification(clientVersion, Collections.emptyList());
+
         return sendIdentification(clientVersion);
     }
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java
index b5428c6..ae039f8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/KexExtensionHandler.java
@@ -84,7 +84,7 @@ public interface KexExtensionHandler {
      * @param  initiator   {@code true} if the proposal is about to be sent, {@code false} if this is a proposal
      *                     received from the peer.
      * @param  proposal    The proposal contents - <B>Caveat emptor:</B> the proposal is <U>modifiable</U> i.e., the
-     *                     handler can modify before being sent or before being processed (if incoming)
+     *                     handler can modify it before being sent or before being processed (if incoming)
      * @throws IOException If failed to handle the request
      */
     default void handleKexInitProposal(
@@ -117,7 +117,7 @@ public interface KexExtensionHandler {
 
     /**
      * The phase at which {@code sendKexExtensions} is invoked
-     * 
+     *
      * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
      */
     enum KexPhase {
@@ -130,7 +130,7 @@ public interface KexExtensionHandler {
     /**
      * Invoked in order to allow the handler to send an {@code SSH_MSG_EXT_INFO} message. <B>Note:</B> this method is
      * called only if {@code isKexExtensionsAvailable} returns {@code true} for the session.
-     * 
+     *
      * @param  session     The {@link Session}
      * @param  phase       The phase at which the handler is invoked
      * @throws IOException If failed to handle the invocation
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 36f7275..7b89478 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
@@ -58,6 +58,38 @@ public interface SessionListener extends SshdEventListener {
     }
 
     /**
+     * About to send identification to peer
+     *
+     * @param session    The {@link Session} instance
+     * @param version    The resolved identification version
+     * @param extraLines Extra data preceding the identification to be sent. <B>Note:</B> the list is modifiable only if
+     *                   this is a server session. The user may modify it based on the peer.
+     * @see              <A HREF="https://tools.ietf.org/html/rfc4253#section-4.2">RFC 4253 - section 4.2 - Protocol
+     *                   Version Exchange</A>
+     */
+    default void sessionPeerIdentificationSend(
+            Session session, String version, List<String> extraLines) {
+        // ignored
+    }
+
+    /**
+     * Successfully read a line as part of the initial peer identification
+     *
+     * @param session    The {@link Session} instance
+     * @param line       The data that was read so far - <B>Note:</B> might not be a full line if more packets are
+     *                   required for full identification data. Furthermore, it may be <U>repeated</U> data due to
+     *                   packets segmentation and re-assembly mechanism
+     * @param extraLines Previous lines that were before this one - <B>Note:</B> it may be <U>repeated</U> data due to
+     *                   packets segmentation and re-assembly mechanism
+     * @see              <A HREF="https://tools.ietf.org/html/rfc4253#section-4.2">RFC 4253 - section 4.2 - Protocol
+     *                   Version Exchange</A>
+     */
+    default void sessionPeerIdentificationLine(
+            Session session, String line, List<String> extraLines) {
+        // ignored
+    }
+
+    /**
      * The peer's identification version was received
      *
      * @param session    The {@link Session} instance
@@ -72,6 +104,16 @@ public interface SessionListener extends SshdEventListener {
     }
 
     /**
+     *
+     * @param session  The referenced {@link Session}
+     * @param proposal The proposals that will be sent to the peer - <B>Caveat emptor:</B> the proposal is
+     *                 <U>modifiable</U> i.e., the handler can modify it before being sent
+     */
+    default void sessionNegotiationOptionsCreated(Session session, Map<KexProposalOption, String> proposal) {
+        // ignored
+    }
+
+    /**
      * Signals the start of the negotiation options handling
      *
      * @param session        The referenced {@link Session}
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 124895f..13bb2f9 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
@@ -2282,6 +2282,8 @@ public abstract class AbstractSession extends SessionHelper {
             }
         }
 
+        signalNegotiationOptionsCreated(proposal);
+
         byte[] seed;
         synchronized (kexState) {
             seed = sendKexInit(proposal);
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 c62719a..7c0e74d 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
@@ -610,6 +610,57 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
         listener.sessionCreated(this);
     }
 
+    protected void signalSendIdentification(String version, List<String> extraLines) throws Exception {
+        try {
+            invokeSessionSignaller(l -> {
+                signalSendIdentification(l, version, extraLines);
+                return null;
+            });
+        } catch (Throwable err) {
+            Throwable e = GenericUtils.peelException(err);
+            if (e instanceof Exception) {
+                throw (Exception) e;
+            } else {
+                throw new RuntimeSshException(e);
+            }
+        }
+    }
+
+    protected void signalSendIdentification(SessionListener listener, String version, List<String> extraLines) {
+        if (listener == null) {
+            return;
+        }
+
+        listener.sessionPeerIdentificationSend(this, version, extraLines);
+    }
+
+    protected void signalReadPeerIdentificationLine(String line, List<String> extraLines) throws Exception {
+        try {
+            invokeSessionSignaller(l -> {
+                signalReadPeerIdentificationLine(l, line, extraLines);
+                return null;
+            });
+        } catch (Throwable err) {
+            Throwable e = GenericUtils.peelException(err);
+            debug("signalReadPeerIdentificationLine({}) Failed ({}) to announce peer={}: {}",
+                    this, e.getClass().getSimpleName(), line, e.getMessage(), e);
+            if (e instanceof Exception) {
+                throw (Exception) e;
+            } else {
+                throw new RuntimeSshException(e);
+            }
+        }
+    }
+
+    protected void signalReadPeerIdentificationLine(
+            SessionListener listener, String version, List<String> extraLines) {
+        if (listener == null) {
+            return;
+        }
+
+        listener.sessionPeerIdentificationLine(this, version, extraLines);
+    }
+
     protected void signalPeerIdentificationReceived(String version, List<String> extraLines) throws Exception {
         try {
             invokeSessionSignaller(l -> {
@@ -626,10 +677,10 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
                 throw new RuntimeSshException(e);
             }
         }
-
     }
 
-    protected void signalPeerIdentificationReceived(SessionListener listener, String version, List<String> extraLines) {
+    protected void signalPeerIdentificationReceived(
+            SessionListener listener, String version, List<String> extraLines) {
         if (listener == null) {
             return;
         }
@@ -684,6 +735,7 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
             if (l == null) {
                 continue;
             }
+
             try {
                 invoker.invoke(l);
             } catch (Throwable t) {
@@ -801,14 +853,13 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
      * state and a {@code null} value will be returned. Else the identification string will be returned and the data
      * read will be consumed from the buffer.
      *
-     * @param  buffer      the buffer containing the identification string
-     * @param  server      {@code true} if it is called by the server session, {@code false} if by the client session
-     * @return             A {@link List} of all received remote identification lines until the version line was read or
-     *                     {@code null} if more data is needed. The identification line is the <U>last</U> one in the
-     *                     list
-     * @throws IOException if malformed identification found
+     * @param  buffer    the buffer containing the identification string
+     * @param  server    {@code true} if it is called by the server session, {@code false} if by the client session
+     * @return           A {@link List} of all received remote identification lines until the version line was read or
+     *                   {@code null} if more data is needed. The identification line is the <U>last</U> one in the list
+     * @throws Exception if malformed identification found
      */
-    protected List<String> doReadIdentification(Buffer buffer, boolean server) throws IOException {
+    protected List<String> doReadIdentification(Buffer buffer, boolean server) throws Exception {
         int maxIdentSize = CoreModuleProperties.MAX_IDENTIFICATION_SIZE.getRequired(this);
         List<String> ident = null;
         int rpos = buffer.rpos();
@@ -869,6 +920,8 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
             if (ident == null) {
                 ident = new ArrayList<>();
             }
+
+            signalReadPeerIdentificationLine(str, ident);
             ident.add(str);
 
             // if this is a server then only one line is expected from the client
@@ -943,6 +996,32 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
         return proposal;
     }
 
+    protected void signalNegotiationOptionsCreated(Map<KexProposalOption, String> proposal) {
+        try {
+            invokeSessionSignaller(l -> {
+                signalNegotiationOptionsCreated(l, proposal);
+                return null;
+            });
+        } catch (Throwable t) {
+            Throwable err = GenericUtils.peelException(t);
+            if (err instanceof RuntimeException) {
+                throw (RuntimeException) err;
+            } else if (err instanceof Error) {
+                throw (Error) err;
+            } else {
+                throw new RuntimeException(err);
+            }
+        }
+    }
+
+    protected void signalNegotiationOptionsCreated(SessionListener listener, Map<KexProposalOption, String> proposal) {
+        if (listener == null) {
+            return;
+        }
+
+        listener.sessionNegotiationOptionsCreated(this, proposal);
+    }
+
     protected void signalNegotiationStart(
             Map<KexProposalOption, String> c2sOptions, Map<KexProposalOption, String> s2cOptions) {
         try {
@@ -950,7 +1029,8 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
                 signalNegotiationStart(l, c2sOptions, s2cOptions);
                 return null;
             });
-        } catch (Throwable err) {
+        } catch (Throwable t) {
+            Throwable err = GenericUtils.peelException(t);
             if (err instanceof RuntimeException) {
                 throw (RuntimeException) err;
             } else if (err instanceof Error) {
@@ -978,7 +1058,8 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
                 signalNegotiationEnd(l, c2sOptions, s2cOptions, negotiatedGuess, reason);
                 return null;
             });
-        } catch (Throwable err) {
+        } catch (Throwable t) {
+            Throwable err = GenericUtils.peelException(t);
             if (err instanceof RuntimeException) {
                 throw (RuntimeException) err;
             } else if (err instanceof Error) {
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java
index 4c2c9b3..d663c10 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java
@@ -219,16 +219,18 @@ public abstract class AbstractServerSession extends AbstractSession implements S
      *                     {@code null}/empty
      * @return             An {@link IoWriteFuture} that can be used to be notified of identification data being written
      *                     successfully or failing
-     * @throws IOException If failed to send identification
+     * @throws Exception   If failed to send identification
      * @see                <A HREF="https://tools.ietf.org/html/rfc4253#section-4.2">RFC 4253 - section 4.2</A>
      */
-    protected IoWriteFuture sendServerIdentification(String... headerLines) throws IOException {
+    protected IoWriteFuture sendServerIdentification(List<String> headerLines) throws Exception {
         serverVersion = resolveIdentificationString(CoreModuleProperties.SERVER_IDENTIFICATION.getName());
+        signalSendIdentification(serverVersion, headerLines);
 
         String ident = serverVersion;
-        if (GenericUtils.length(headerLines) > 0) {
+        if (GenericUtils.size(headerLines) > 0) {
             ident = GenericUtils.join(headerLines, "\r\n") + "\r\n" + serverVersion;
         }
+
         return sendIdentification(ident);
     }
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java
index 06d7c97..9fee875 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java
@@ -18,6 +18,10 @@
  */
 package org.apache.sshd.server.session;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.core.CoreModuleProperties;
@@ -35,6 +39,10 @@ public class ServerSessionImpl extends AbstractServerSession {
 
         String headerConfig = CoreModuleProperties.SERVER_EXTRA_IDENTIFICATION_LINES.getOrNull(this);
         String[] headers = GenericUtils.split(headerConfig, CoreModuleProperties.SERVER_EXTRA_IDENT_LINES_SEPARATOR);
-        sendServerIdentification(headers);
+        // We intentionally create a modifiable array so as to allow users to modify it via SessionListener
+        List<String> extraLines = GenericUtils.isEmpty(headers)
+                ? new ArrayList<>()
+                : new ArrayList<>(Arrays.asList(headers));
+        sendServerIdentification(extraLines);
     }
 }
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/session/helpers/AbstractSessionTest.java b/sshd-core/src/test/java/org/apache/sshd/common/session/helpers/AbstractSessionTest.java
index d8fd35f..a143cab 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/session/helpers/AbstractSessionTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/session/helpers/AbstractSessionTest.java
@@ -86,28 +86,28 @@ public class AbstractSessionTest extends BaseTestSupport {
     }
 
     @Test
-    public void testReadIdentSimple() throws IOException {
+    public void testReadIdentSimple() throws Exception {
         Buffer buf = new ByteArrayBuffer("SSH-2.0-software\r\n".getBytes(StandardCharsets.UTF_8));
         String ident = readIdentification(session, buf);
         assertEquals("SSH-2.0-software", ident);
     }
 
     @Test
-    public void testReadIdentWithoutCR() throws IOException {
+    public void testReadIdentWithoutCR() throws Exception {
         Buffer buf = new ByteArrayBuffer("SSH-2.0-software\n".getBytes(StandardCharsets.UTF_8));
         String ident = readIdentification(session, buf);
         assertEquals("SSH-2.0-software", ident);
     }
 
     @Test
-    public void testReadIdentWithHeaders() throws IOException {
+    public void testReadIdentWithHeaders() throws Exception {
         Buffer buf = new ByteArrayBuffer("a header line\r\nSSH-2.0-software\r\n".getBytes(StandardCharsets.UTF_8));
         String ident = readIdentification(session, buf);
         assertEquals("SSH-2.0-software", ident);
     }
 
     @Test
-    public void testReadIdentWithSplitPackets() throws IOException {
+    public void testReadIdentWithSplitPackets() throws Exception {
         Buffer buf = new ByteArrayBuffer("header line\r\nSSH".getBytes(StandardCharsets.UTF_8));
         String ident = readIdentification(session, buf);
         assertNull("Unexpected identification for header only", ident);
@@ -118,14 +118,14 @@ public class AbstractSessionTest extends BaseTestSupport {
     }
 
     @Test(expected = StreamCorruptedException.class)
-    public void testReadIdentBadLineEnding() throws IOException {
+    public void testReadIdentBadLineEnding() throws Exception {
         Buffer buf = new ByteArrayBuffer("SSH-2.0-software\ra".getBytes(StandardCharsets.UTF_8));
         String ident = readIdentification(session, buf);
         fail("Unexpected success: " + ident);
     }
 
     @Test(expected = StreamCorruptedException.class)
-    public void testReadIdentLongLine() throws IOException {
+    public void testReadIdentLongLine() throws Exception {
         StringBuilder sb = new StringBuilder(SessionContext.MAX_VERSION_LINE_LENGTH + Integer.SIZE);
         sb.append("SSH-2.0-software");
         do {
@@ -138,7 +138,7 @@ public class AbstractSessionTest extends BaseTestSupport {
     }
 
     @Test(expected = StreamCorruptedException.class)
-    public void testReadIdentWithNullChar() throws IOException {
+    public void testReadIdentWithNullChar() throws Exception {
         String id = "SSH-2.0" + '\0' + "-software\r\n";
         Buffer buf = new ByteArrayBuffer(id.getBytes(StandardCharsets.UTF_8));
         String ident = readIdentification(session, buf);
@@ -146,7 +146,7 @@ public class AbstractSessionTest extends BaseTestSupport {
     }
 
     @Test(expected = StreamCorruptedException.class)
-    public void testReadIdentLongHeader() throws IOException {
+    public void testReadIdentLongHeader() throws Exception {
         int maxIdentSize = CoreModuleProperties.MAX_IDENTIFICATION_SIZE.getRequiredDefault();
         StringBuilder sb = new StringBuilder(maxIdentSize + Integer.SIZE);
         do {
@@ -300,7 +300,7 @@ public class AbstractSessionTest extends BaseTestSupport {
         }
     }
 
-    private static String readIdentification(MySession session, Buffer buf) throws IOException {
+    private static String readIdentification(MySession session, Buffer buf) throws Exception {
         List<String> lines = session.doReadIdentification(buf);
         return GenericUtils.isEmpty(lines) ? null : lines.get(lines.size() - 1);
     }
@@ -439,7 +439,7 @@ public class AbstractSessionTest extends BaseTestSupport {
             return false;
         }
 
-        public List<String> doReadIdentification(Buffer buffer) throws IOException {
+        public List<String> doReadIdentification(Buffer buffer) throws Exception {
             return super.doReadIdentification(buffer, false);
         }