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 2019/02/28 03:05:46 UTC

[mina-sshd] branch SSHD-901 created (now e83a26a)

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

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


      at e83a26a  [SSHD-901] Provided hook for user customized session heartbeat mechanism

This branch includes the following new commits:

     new 19d60f4  [SSHD-782] Moved heartbeat support to AbstractConnectionService
     new dcdb146  [SSHD-782] Added session level heartbeat via SSH_MSG_IGNORE
     new 4211021  [SSHD-901] Added capability to request a reply for the client's keep-alive request in order to avoid client-side session timeout due to no traffic from the server
     new e83a26a  [SSHD-901] Provided hook for user customized session heartbeat mechanism

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[mina-sshd] 04/04: [SSHD-901] Provided hook for user customized session heartbeat mechanism

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit e83a26a0272c15e4510195c0f369a5c4a29e8b9b
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Feb 28 05:12:39 2019 +0200

    [SSHD-901] Provided hook for user customized session heartbeat mechanism
---
 CHANGES.md                                         |  8 ++++-
 docs/client-setup.md                               |  9 +++++-
 docs/event-listeners.md                            |  3 ++
 docs/server-setup.md                               |  6 ++++
 .../common/session/SessionHeartbeatController.java |  9 ++++--
 .../session/ReservedSessionMessagesHandler.java    | 17 ++++++++++
 .../session/helpers/AbstractConnectionService.java | 36 ++++++++++++++++------
 7 files changed, 74 insertions(+), 14 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 206c5ed..d458670 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -38,7 +38,13 @@ then the internal `DefaultClientKexExtensionHandler` is used.
 
 ## Behavioral changes and enhancements
 
-* [SSHD-782](https://issues.apache.org/jira/browse/SSHD-882) - Added session level heartbeat mechanism via `SSH_MSG_IGNORE`.
+* [SSHD-782](https://issues.apache.org/jira/browse/SSHD-882) - Added session level heartbeat mechanism via `SSH_MSG_IGNORE`
+or customized user provided code.
+
+In order to support customized user code for this feature, the `ReservedSessionMessagesHandler` can be used to
+implement any kind of user-defined heartbeat. *Note:* if the user configured such a mechanism, then the
+`sendReservedHeartbeat` method **must** be implemented since the default throws `UnsupportedOperationException`
+which will cause the session to be terminated the 1st time the method is invoked.
 
 * [SSHD-882](https://issues.apache.org/jira/browse/SSHD-882) - Provide hooks to allow users to register a consumer
 for STDERR data sent via the `ChannelSession` - especially for the SFTP subsystem.
diff --git a/docs/client-setup.md b/docs/client-setup.md
index ce0fffc..797d7b6 100644
--- a/docs/client-setup.md
+++ b/docs/client-setup.md
@@ -182,6 +182,13 @@ regardless of the user's own traffic:
     willing to wait for the server's reply to the global request.
 
 
+* Customized user code
+
+    In order to support customized user code for this feature, the `ReservedSessionMessagesHandler` can be used to
+    implement any kind of user-defined heartbeat. *Note:* if the user configured such a mechanism, then the
+    `sendReservedHeartbeat` method **must** be implemented since the default throws `UnsupportedOperationException`
+    which will cause the session to be terminated the 1st time the method is invoked.
+
 **Note(s):**
 
 * Both options are disabled by default - they need to be activated explicitly.
@@ -189,7 +196,7 @@ regardless of the user's own traffic:
 * Both options can be activated either on the `SshClient` (for **global** setup) and/or
 the `ClientSession` (for specific session configuration).
 
-* The `keepalive@,,,,` mechanism **supersedes** the `SSH_MSG_IGNORE` one if both activated.
+* The `keepalive@,,,,` mechanism **supersedes** the the other mechanisms if activated.
 
     * If specified timeout expires for the `wantReply` option then session will be **closed**.
 
diff --git a/docs/event-listeners.md b/docs/event-listeners.md
index 2557a69..eda2d27 100644
--- a/docs/event-listeners.md
+++ b/docs/event-listeners.md
@@ -94,6 +94,9 @@ Can be used to handle the following cases:
 * [SSH_MSG_IGNORE](https://tools.ietf.org/html/rfc4253#section-11.2)
 * [SSH_MSG_DEBUG](https://tools.ietf.org/html/rfc4253#section-11.3)
 * [SSH_MSG_UNIMPLEMENTED](https://tools.ietf.org/html/rfc4253#section-11.4)
+* Implementing a custom session heartbeat mechanism - for **both**
+[client](./client-setup.md#keeping-the-session-alive-while-no-traffic)
+or [server](./server-setup.md#providing-server-side-heartbeat).
 * Any other unrecognized message received in the session.
 
 **Note:** The `handleUnimplementedMessage` method serves both for handling `SSH_MSG_UNIMPLEMENTED` and any other unrecognized
diff --git a/docs/server-setup.md b/docs/server-setup.md
index 3992c63..3e6b2db 100644
--- a/docs/server-setup.md
+++ b/docs/server-setup.md
@@ -150,3 +150,9 @@ it is highly recommended to use the API - unless one needs to control these prop
 
 If one is using the SSHD CLI code, then these options are controlled via `-o ServerAliveInterval=NNNN` where the value is
 the requested **global** interval in **seconds**. *Note*: any non-positive value is treated as if the feature is disabled.
+
+In order to support customized user code for this feature, the `ReservedSessionMessagesHandler` can be used to
+implement any kind of user-defined heartbeat. *Note:* if the user configured such a mechanism, then the
+`sendReservedHeartbeat` method **must** be implemented since the default throws `UnsupportedOperationException`
+which will cause the session to be terminated the 1st time the method is invoked.
+
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/session/SessionHeartbeatController.java b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionHeartbeatController.java
index ce02b80..be1f75d 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/session/SessionHeartbeatController.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionHeartbeatController.java
@@ -36,7 +36,9 @@ public interface SessionHeartbeatController extends PropertyResolver {
         /** No heartbeat */
         NONE,
         /** Use {@code SSH_MSG_IGNORE} packets */
-        IGNORE;
+        IGNORE,
+        /** Custom mechanism via {@code ReservedSessionMessagesHandler} */
+        RESERVED;
 
         public static final Set<HeartbeatType> VALUES = EnumSet.allOf(HeartbeatType.class);
 
@@ -60,13 +62,16 @@ public interface SessionHeartbeatController extends PropertyResolver {
     /** Property used to register the interval for the heartbeat - if not set or non-positive then disabled */
     String SESSION_HEARTBEAT_INTERVAL = "session-connection-heartbeat-interval";
 
+    /** Default value for {@value #SESSION_HEARTBEAT_INTERVAL} if none set */
+    long DEFAULT_CONNECTION_HEARTBEAT_INTERVAL = 0L;
+
     default HeartbeatType getSessionHeartbeatType() {
         Object value = getObject(SESSION_HEARTBEAT_TYPE);
         return PropertyResolverUtils.toEnum(HeartbeatType.class, value, false, HeartbeatType.VALUES);
     }
 
     default long getSessionHeartbeatInterval() {
-        return getLongProperty(SESSION_HEARTBEAT_INTERVAL, 0L);
+        return getLongProperty(SESSION_HEARTBEAT_INTERVAL, DEFAULT_CONNECTION_HEARTBEAT_INTERVAL);
     }
 
     /**
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java
index e229eb0..3eea967 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/ReservedSessionMessagesHandler.java
@@ -69,4 +69,21 @@ public interface ReservedSessionMessagesHandler extends SshdEventListener {
     default boolean handleUnimplementedMessage(Session session, int cmd, Buffer buffer) throws Exception {
         return false;
     }
+
+    /**
+     * Invoked if the user configured usage of a proprietary heartbeat mechanism.
+     * <B>Note:</B> by default throws {@code UnsupportedOperationException} so
+     * users who configure a proprietary heartbeat mechanism option must provide
+     * an implementation for this method.
+     *
+     * @param service The {@link ConnectionService} through which the heartbeat
+     * is being executed.
+     * @return {@code true} whether heartbeat actually sent - <B>Note:</B> used
+     * mainly for debugging purposes.
+     * @throws Exception If failed to send the heartbeat - <B>Note:</B> causes
+     * associated session termination.
+     */
+    default boolean sendReservedHeartbeat(ConnectionService service) throws Exception {
+        throw new UnsupportedOperationException("Reserved heartbeat not implemented for " + service);
+    }
 }
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
index 9ac6077..4b40ff1 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
@@ -56,6 +56,7 @@ import org.apache.sshd.common.forward.PortForwardingEventListenerManager;
 import org.apache.sshd.common.io.AbstractIoWriteFuture;
 import org.apache.sshd.common.io.IoWriteFuture;
 import org.apache.sshd.common.session.ConnectionService;
+import org.apache.sshd.common.session.ReservedSessionMessagesHandler;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.session.UnknownChannelReferenceHandler;
 import org.apache.sshd.common.util.EventListenerUtils;
@@ -226,20 +227,35 @@ public abstract class AbstractConnectionService
                 session, heartbeatType, interval);
         }
 
-        if ((heartbeatType == null) || (heartbeatType == HeartbeatType.NONE)
-                || (interval <= 0L) || (heartBeat == null)) {
+        if ((heartbeatType == null) || (interval <= 0L) || (heartBeat == null)) {
             return false;
         }
 
         try {
-            Buffer buffer = session.createBuffer(
-                SshConstants.SSH_MSG_IGNORE, DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING.length() + Byte.SIZE);
-            buffer.putString(DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING);
-
-            IoWriteFuture future = session.writePacket(buffer);
-            future.addListener(this::futureDone);
-            return true;
-        } catch (IOException | RuntimeException | Error e) {
+            switch (heartbeatType) {
+                case NONE:
+                    return false;
+                case IGNORE: {
+                    Buffer buffer = session.createBuffer(
+                        SshConstants.SSH_MSG_IGNORE, DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING.length() + Byte.SIZE);
+                    buffer.putString(DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING);
+
+                    IoWriteFuture future = session.writePacket(buffer);
+                    future.addListener(this::futureDone);
+                    return true;
+                }
+                case RESERVED: {
+                    ReservedSessionMessagesHandler handler =
+                        Objects.requireNonNull(
+                            session.getReservedSessionMessagesHandler(),
+                            "No customized heartbeat handler registered");
+                    return handler.sendReservedHeartbeat(this);
+                }
+                default:
+                    throw new UnsupportedOperationException("Unsupported heartbeat type: " + heartbeatType);
+            }
+
+        } catch (Throwable e) {
             session.exceptionCaught(e);
             if (log.isDebugEnabled()) {
                 log.debug("sendHeartBeat({}) failed ({}) to send heartbeat #{} request={}: {}",


[mina-sshd] 02/04: [SSHD-782] Added session level heartbeat via SSH_MSG_IGNORE

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit dcdb1467c81b5b8773cc3a7e6df04c55622a05a8
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Tue Feb 26 18:25:06 2019 +0200

    [SSHD-782] Added session level heartbeat via SSH_MSG_IGNORE
---
 CHANGES.md                                         |   2 +
 docs/client-setup.md                               |  35 +++++++
 docs/internals.md                                  |   7 +-
 docs/server-setup.md                               |  13 +++
 .../sshd/cli/client/SshClientCliSupport.java       |  25 ++++-
 .../sshd/cli/server/SshServerCliSupport.java       |  17 +++-
 .../org/apache/sshd/cli/server/SshServerMain.java  |   2 +
 .../org/apache/sshd/common/PropertyResolver.java   |   7 ++
 .../apache/sshd/common/PropertyResolverUtils.java  |   9 +-
 .../apache/sshd/common/session/SessionContext.java |   3 +-
 .../common/session/SessionHeartbeatController.java |  86 +++++++++++++++++
 .../client/config/SshClientConfigFileReader.java   |   6 ++
 .../client/session/ClientConnectionService.java    |  22 ++---
 .../sshd/client/session/ClientUserAuthService.java |  11 ++-
 .../org/apache/sshd/common/FactoryManager.java     |   3 +-
 .../main/java/org/apache/sshd/common/Service.java  |  12 ++-
 .../sshd/common/session/ConnectionService.java     |   1 +
 .../session/helpers/AbstractConnectionService.java | 107 ++++++++++++++++++---
 .../server/config/SshServerConfigFileReader.java   |   3 +
 .../sshd/server/session/ServerUserAuthService.java |  26 +++--
 .../sshd/deprecated/ClientUserAuthServiceOld.java  |   9 ++
 21 files changed, 352 insertions(+), 54 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index fa3f7c6..a9f5839 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -38,6 +38,8 @@ then the internal `DefaultClientKexExtensionHandler` is used.
 
 ## Behavioral changes and enhancements
 
+* [SSHD-782](https://issues.apache.org/jira/browse/SSHD-882) - Added session level heartbeat mechanism via `SSH_MSG_IGNORE`.
+
 * [SSHD-882](https://issues.apache.org/jira/browse/SSHD-882) - Provide hooks to allow users to register a consumer
 for STDERR data sent via the `ChannelSession` - especially for the SFTP subsystem.
 
diff --git a/docs/client-setup.md b/docs/client-setup.md
index fbdd284..4b30dad 100644
--- a/docs/client-setup.md
+++ b/docs/client-setup.md
@@ -154,6 +154,41 @@ sessions depends on the actual changed configuration. Here is how a typical usag
 
 ```
 
+## Keeping the session alive while no traffic
+
+The client-side implementation has a 2 builtin mechanisms for maintaining the session alive as far as the **server** is concerned
+regardless of the user's own traffic:
+
+* Sending `SSH_MSG_IGNORE` messages every once in a while.
+
+    This mechanism is along the lines of [PUTTY null packets configuration](https://patrickmn.com/aside/how-to-keep-alive-ssh-sessions/).
+    It generates small [`SSH_MSG_IGNORE`](https://tools.ietf.org/html/rfc4253#section-11.2) messages. The way to set this mechanism
+    up is via the `setSessionHeartbeat` API.
+
+    *Note:* the same effect can also be achieved by setting the relevant properties documented in `SessionHeartbeatController`, but
+    it is highly recommended to use the API - unless one needs to control these properties **externally** via `-Dxxx` JVM options.
+
+* Sending `keepalive@...` [global requests](https://tools.ietf.org/html/rfc4254#section-4).
+
+    The feature is controlled via the `ClientFactoryManager#HEARTBEAT_REQUEST` and `HEARTBEAT_INTERVAL` properties - see the relevant
+    documentation for these features. The simplest way to activate this feature is to set the `HEARTBEAT_INTERVAL` property value
+    to the **milliseconds** value of the requested heartbeat interval.
+
+**Note(s):**
+
+* Both options are disabled by default - they need to be activated explicitly.
+
+* Both options can be activated either on the `SshClient` (for **global** setup) and/or
+the `ClientSession` (for specific session configuration).
+
+* The `keepalive@,,,,` mechanism **supersedes** the `SSH_MSG_IGNORE` one if both activated.
+
+* When using the CLI, these options can be configured using the following `-o key=value` properties:
+
+    * `ClientAliveInterval` - if positive the defines the heartbeat interval in **seconds**.
+
+    * `ClientAliveUseNullPackets` - *true* if use the `SSH_MSG_IGNORE` mechanism, *false* if use global request (default).
+
 ## Running a command or opening a shell
 
 When running a command or opening a shell, there is an extra concern regarding the PTY configuration and/or the
diff --git a/docs/internals.md b/docs/internals.md
index 88e480f..23c7284 100644
--- a/docs/internals.md
+++ b/docs/internals.md
@@ -240,22 +240,17 @@ the handler may choose to build and send the response within its own code, in wh
 
 * `exit-signal`, `exit-status` - As described in [RFC4254 section 6.10](https://tools.ietf.org/html/rfc4254#section-6.10)
 
-
 * `*@putty.projects.tartarus.org` - As described in [Appendix F: SSH-2 names specified for PuTTY](http://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixF.html)
 
-
 * `hostkeys-prove-00@openssh.com`, `hostkeys-00@openssh.com` - As described in [OpenSSH protocol - section 2.5](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)
 
-
 * `tcpip-forward`, `cancel-tcpip-forward` - As described in [RFC4254 section 7](https://tools.ietf.org/html/rfc4254#section-7)
 
-
-* `keepalive@*` - Used by many implementations (including this one) to "ping" the peer and make sure the connection is still alive.
+* `keepalive@*` - Used by many client implementations (including this one) to "ping" the server and keep/make sure the connection is still alive.
 In this context, the SSHD code allows the user to configure both the frequency and content of the heartbeat request (including whether
 to send this request at all) via the `ClientFactoryManager`-s `HEARTBEAT_INTERVAL`, `HEARTBEAT_REQUEST` and `DEFAULT_KEEP_ALIVE_HEARTBEAT_STRING`
 configuration properties.
 
-
 * `no-more-sessions@*` - As described in [OpenSSH protocol section 2.2](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL).
 In this context, the code consults the `ServerFactoryManagder.MAX_CONCURRENT_SESSIONS` server-side configuration property in order to
 decide whether to accept a successfully authenticated session.
diff --git a/docs/server-setup.md b/docs/server-setup.md
index fc25267..3992c63 100644
--- a/docs/server-setup.md
+++ b/docs/server-setup.md
@@ -137,3 +137,16 @@ configurations (except the port) can still be *overridden* while the server is r
 only **new** clients that connect to the server after the change will be affected - with the exception of the negotiation
 options (keys, macs, ciphers, etc...) which take effect the next time keys are re-exchanged, that can affect live sessions
 and not only new ones.
+
+## Providing server-side heartbeat
+
+The server can generate [`SSH_MSG_IGNORE`](https://tools.ietf.org/html/rfc4253#section-11.2) messages towards its
+client sessions in order to make sure that the client does not time out on waiting for traffic if no user generated
+data is available. By default, this feature is **disabled** - however it can be enabled by invoking the `setSessionHeartbeat`
+API either on the server (for **global** setting) or a specific session (for targeted control of the feature).
+
+*Note:* the same effect can also be achieved by setting the relevant properties documented in `SessionHeartbeatController`, but
+it is highly recommended to use the API - unless one needs to control these properties **externally** via `-Dxxx` JVM options.
+
+If one is using the SSHD CLI code, then these options are controlled via `-o ServerAliveInterval=NNNN` where the value is
+the requested **global** interval in **seconds**. *Note*: any non-positive value is treated as if the feature is disabled.
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
index 926015f..ae83310 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
@@ -37,6 +37,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
 import java.util.logging.ConsoleHandler;
 import java.util.logging.Formatter;
 import java.util.logging.Handler;
@@ -81,6 +82,7 @@ import org.apache.sshd.common.kex.extension.KexExtensionHandler;
 import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
 import org.apache.sshd.common.mac.BuiltinMacs;
 import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.session.SessionHeartbeatController.HeartbeatType;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.ValidateUtils;
@@ -376,9 +378,30 @@ public abstract class SshClientCliSupport extends CliSupport {
         return ptyModes;
     }
 
+    public static void setupClientHeartbeat(
+            SshClient client, Map<String, ?> options, PrintStream stdout) {
+        long interval = PropertyResolverUtils.getLongProperty(
+            options, SshClientConfigFileReader.CLIENT_LIVECHECK_INTERVAL_PROP, SshClientConfigFileReader.DEFAULT_ALIVE_INTERVAL);
+        if (interval <= 0L) {
+            return;
+        }
+
+        if (PropertyResolverUtils.getBooleanProperty(
+                options, SshClientConfigFileReader.CLIENT_LIVECHECK_USE_NULLS, SshClientConfigFileReader.DEFAULT_LIVECHECK_USE_NULLS)) {
+            PropertyResolverUtils.updateProperty(
+                client, ClientFactoryManager.HEARTBEAT_INTERVAL, TimeUnit.SECONDS.toMillis(interval));
+            stdout.println("Using SSH_MSG_IGNORE heartbeat every " + interval + " seconds");
+        } else {
+            client.setSessionHeartbeat(HeartbeatType.IGNORE, TimeUnit.SECONDS, interval);
+            stdout.println("Using global request heartbeat every " + interval + " seconds");
+        }
+    }
+
     public static SshClient setupDefaultClient(
             Map<String, ?> options, Level level, PrintStream stdout, PrintStream stderr, String... args) {
-        return setupIoServiceFactory(SshClient.setUpDefaultClient(), options, level, stdout, stderr, args);
+        SshClient client = setupIoServiceFactory(SshClient.setUpDefaultClient(), options, level, stdout, stderr, args);
+        setupClientHeartbeat(client, options, stdout);
+        return client;
     }
 
     // returns null if error encountered
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
index c2dc94f..ad5cdeb 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
@@ -30,9 +30,11 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.ServiceLoader;
 import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -48,6 +50,7 @@ import org.apache.sshd.common.config.keys.BuiltinIdentities;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.keyprovider.MappedKeyPairProvider;
+import org.apache.sshd.common.session.SessionHeartbeatController.HeartbeatType;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.resource.PathResource;
@@ -68,8 +71,6 @@ import org.apache.sshd.server.subsystem.sftp.SftpEventListener;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
 
 /**
- * TODO Add javadoc
- *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public abstract class SshServerCliSupport extends CliSupport {
@@ -80,6 +81,18 @@ public abstract class SshServerCliSupport extends CliSupport {
         super();
     }
 
+    public static void setupServerHeartbeat(
+            SshServer sshd, Map<String, ?> options, PrintStream stdout) {
+        long interval = PropertyResolverUtils.getLongProperty(
+            options, SshServerConfigFileReader.SERVER_ALIVE_INTERVAL_PROP, SshServerConfigFileReader.DEFAULT_ALIVE_INTERVAL);
+        if (interval <= 0L) {
+            return;
+        }
+
+        sshd.setSessionHeartbeat(HeartbeatType.IGNORE, TimeUnit.SECONDS, interval);
+        stdout.println("Generate server-side SSH_MSG_IGNORE heartbeat every " + interval + " seconds");
+    }
+
     public static KeyPairProvider resolveServerKeys(
             PrintStream stderr, String hostKeyType, int hostKeySize, Collection<String> keyFiles)
                 throws Exception {
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
index f9a294e..442a9ec 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
@@ -164,6 +164,8 @@ public class SshServerMain extends SshServerCliSupport {
         Map<String, Object> props = sshd.getProperties();
         props.putAll(options);
 
+        setupServerHeartbeat(sshd, options, System.out);
+
         PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(options);
         KeyPairProvider hostKeyProvider = resolveServerKeys(System.err, hostKeyType, hostKeySize, keyFiles);
         sshd.setKeyPairProvider(hostKeyProvider);
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java b/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java
index d7f5f19..09363c5 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java
@@ -19,6 +19,7 @@
 
 package org.apache.sshd.common;
 
+import java.nio.charset.Charset;
 import java.util.Collections;
 import java.util.Map;
 
@@ -121,4 +122,10 @@ public interface PropertyResolver {
     default Object getObject(String name) {
         return PropertyResolverUtils.getObject(this, name);
     }
+
+    default Charset getCharset(String name, Charset defaultValue) {
+        Object value = getObject(name);
+        return (value == null) ? defaultValue : PropertyResolverUtils.toCharset(value);
+    }
+
 }
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java
index d95278b..d16a4d7 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java
@@ -187,7 +187,8 @@ public final class PropertyResolverUtils {
      * @throws NoSuchElementException If no matching string name found and
      * <tt>failIfNoMatch</tt> is {@code true}
      */
-    public static <E extends Enum<E>> E toEnum(Class<E> enumType, Object value, boolean failIfNoMatch, Collection<E> available) {
+    public static <E extends Enum<E>> E toEnum(
+            Class<E> enumType, Object value, boolean failIfNoMatch, Collection<E> available) {
         if (value == null) {
             return null;
         } else if (enumType.isInstance(value)) {
@@ -208,7 +209,8 @@ public final class PropertyResolverUtils {
 
             return null;
         } else {
-            throw new IllegalArgumentException("Bad value type for enum conversion: " + value.getClass().getSimpleName());
+            throw new IllegalArgumentException(
+                "Bad value type for enum conversion: " + value.getClass().getSimpleName());
         }
     }
 
@@ -332,7 +334,8 @@ public final class PropertyResolverUtils {
         } else if (value instanceof CharSequence) {
             return parseBoolean(value.toString());
         } else {
-            throw new UnsupportedOperationException("Cannot convert " + value.getClass().getSimpleName() + "[" + value + "] to boolean");
+            throw new UnsupportedOperationException(
+                "Cannot convert " + value.getClass().getSimpleName() + "[" + value + "] to boolean");
         }
     }
 
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java
index d19dd9c..23d5c60 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java
@@ -22,7 +22,6 @@ package org.apache.sshd.common.session;
 import java.util.Map;
 
 import org.apache.sshd.common.AttributeStore;
-import org.apache.sshd.common.PropertyResolver;
 import org.apache.sshd.common.auth.UsernameHolder;
 import org.apache.sshd.common.kex.KexProposalOption;
 import org.apache.sshd.common.kex.KexState;
@@ -37,7 +36,7 @@ import org.apache.sshd.common.util.net.ConnectionEndpointsIndicator;
 public interface SessionContext
         extends ConnectionEndpointsIndicator,
                 UsernameHolder,
-                PropertyResolver,
+                SessionHeartbeatController,
                 AttributeStore {
     /**
      * Default prefix expected for the client / server identification string
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/session/SessionHeartbeatController.java b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionHeartbeatController.java
new file mode 100644
index 0000000..ce02b80
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionHeartbeatController.java
@@ -0,0 +1,86 @@
+/*
+ * 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.session;
+
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.PropertyResolver;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SessionHeartbeatController extends PropertyResolver {
+    enum HeartbeatType {
+        /** No heartbeat */
+        NONE,
+        /** Use {@code SSH_MSG_IGNORE} packets */
+        IGNORE;
+
+        public static final Set<HeartbeatType> VALUES = EnumSet.allOf(HeartbeatType.class);
+
+        public static HeartbeatType fromName(String name) {
+            return GenericUtils.isEmpty(name)
+                 ? null
+                 : VALUES.stream()
+                     .filter(v -> name.equalsIgnoreCase(v.name()))
+                     .findAny()
+                     .orElse(null);
+        }
+    }
+
+    /**
+     * Property used to register the {@link HeartbeatType} - if non-existent
+     * or {@code NONE} then disabled. Same if some unknown string value is
+     * set as the property value.
+     */
+    String SESSION_HEARTBEAT_TYPE = "session-connection-heartbeat-type";
+
+    /** Property used to register the interval for the heartbeat - if not set or non-positive then disabled */
+    String SESSION_HEARTBEAT_INTERVAL = "session-connection-heartbeat-interval";
+
+    default HeartbeatType getSessionHeartbeatType() {
+        Object value = getObject(SESSION_HEARTBEAT_TYPE);
+        return PropertyResolverUtils.toEnum(HeartbeatType.class, value, false, HeartbeatType.VALUES);
+    }
+
+    default long getSessionHeartbeatInterval() {
+        return getLongProperty(SESSION_HEARTBEAT_INTERVAL, 0L);
+    }
+
+    /**
+     * Disables the session heartbeat feature - <B>Note:</B> if heartbeat already
+     * in progress then it may be ignored.
+     */
+    default void disableSessionHeartbeat() {
+        setSessionHeartbeat(HeartbeatType.NONE, TimeUnit.MILLISECONDS, 0L);
+    }
+
+    default void setSessionHeartbeat(HeartbeatType type, TimeUnit unit, long count) {
+        Objects.requireNonNull(type, "No heartbeat type specified");
+        Objects.requireNonNull(unit, "No heartbeat time unit provided");
+        PropertyResolverUtils.updateProperty(this, SESSION_HEARTBEAT_TYPE, type);
+        PropertyResolverUtils.updateProperty(this, SESSION_HEARTBEAT_INTERVAL, TimeUnit.MILLISECONDS.convert(count, unit));
+    }
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java b/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java
index eb87062..669040d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java
@@ -32,6 +32,12 @@ public final class SshClientConfigFileReader {
     public static final String SENDENV_PROP = "SendEnv";
     public static final String REQUEST_TTY_OPTION = "RequestTTY";
 
+    public static final String CLIENT_LIVECHECK_INTERVAL_PROP = "ClientAliveInterval";
+    public static final long DEFAULT_ALIVE_INTERVAL = 0L;
+
+    public static final String CLIENT_LIVECHECK_USE_NULLS = "ClientAliveUseNullPackets";
+    public static final boolean DEFAULT_LIVECHECK_USE_NULLS = false;
+
     private SshClientConfigFileReader() {
         throw new UnsupportedOperationException("No instance allowed");
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
index 06c6000..bd5873c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
@@ -30,6 +30,7 @@ import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.io.AbstractIoWriteFuture;
 import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.session.helpers.AbstractConnectionService;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
@@ -51,9 +52,9 @@ public class ClientConnectionService
     public ClientConnectionService(AbstractClientSession s) throws SshException {
         super(s);
 
-        heartbeatRequest = s.getStringProperty(
+        heartbeatRequest = this.getStringProperty(
             ClientFactoryManager.HEARTBEAT_REQUEST, ClientFactoryManager.DEFAULT_KEEP_ALIVE_HEARTBEAT_STRING);
-        heartbeatInterval = s.getLongProperty(
+        heartbeatInterval = this.getLongProperty(
             ClientFactoryManager.HEARTBEAT_INTERVAL, ClientFactoryManager.DEFAULT_HEARTBEAT_INTERVAL);
     }
 
@@ -115,20 +116,22 @@ public class ClientConnectionService
             return super.sendHeartBeat();
         }
 
-        ClientSession session = getClientSession();
+        Session session = getSession();
         try {
             Buffer buf = session.createBuffer(
                 SshConstants.SSH_MSG_GLOBAL_REQUEST, heartbeatRequest.length() + Byte.SIZE);
             buf.putString(heartbeatRequest);
             buf.putBoolean(false);
+
             IoWriteFuture future = session.writePacket(buf);
             future.addListener(this::futureDone);
+            heartbeatCount.incrementAndGet();
             return future;
-        } catch (IOException | RuntimeException e) {
+        } catch (IOException | RuntimeException | Error e) {
             session.exceptionCaught(e);
             if (log.isDebugEnabled()) {
-                log.debug("sendHeartBeat({}) failed ({}) to send request={}: {}",
-                    session, e.getClass().getSimpleName(), heartbeatRequest, e.getMessage());
+                log.debug("sendHeartBeat({}) failed ({}) to send heartbeat #{} request={}: {}",
+                    session, e.getClass().getSimpleName(), heartbeatCount, heartbeatRequest, e.getMessage());
             }
             if (log.isTraceEnabled()) {
                 log.trace("sendHeartBeat(" + session + ") exception details", e);
@@ -142,13 +145,6 @@ public class ClientConnectionService
         }
     }
 
-    protected void futureDone(IoWriteFuture future) {
-        Throwable t = future.getException();
-        if (t != null) {
-            getSession().exceptionCaught(t);
-        }
-    }
-
     @Override
     public AgentForwardSupport getAgentForwardSupport() {
         throw new IllegalStateException("Server side operation");
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java
index ea426cb..e45fed7 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java
@@ -23,7 +23,9 @@ import java.io.InterruptedIOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.sshd.client.ClientAuthenticationManager;
@@ -58,6 +60,7 @@ public class ClientUserAuthService
      * isSuccess -> authenticated, else if isDone -> server waiting for user auth, else authenticating.
      */
     private final AtomicReference<AuthFuture> authFutureHolder = new AtomicReference<>();
+    private final Map<String, Object> properties = new ConcurrentHashMap<>();
 
     private final ClientSessionImpl clientSession;
     private final List<String> clientMethods;
@@ -69,7 +72,8 @@ public class ClientUserAuthService
     private int currentMethod;
 
     public ClientUserAuthService(Session s) {
-        clientSession = ValidateUtils.checkInstanceOf(s, ClientSessionImpl.class, "Client side service used on server side: %s", s);
+        clientSession = ValidateUtils.checkInstanceOf(
+            s, ClientSessionImpl.class, "Client side service used on server side: %s", s);
         authFactories = ValidateUtils.checkNotNullAndNotEmpty(
             clientSession.getUserAuthFactories(), "No user auth factories for %s", s);
         clientMethods = new ArrayList<>();
@@ -116,6 +120,11 @@ public class ClientUserAuthService
     }
 
     @Override
+    public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    @Override
     public void start() {
         // ignored
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
index d6bb13e..a6c599c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
@@ -38,6 +38,7 @@ import org.apache.sshd.common.random.Random;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.session.ReservedSessionMessagesManager;
 import org.apache.sshd.common.session.SessionDisconnectHandlerManager;
+import org.apache.sshd.common.session.SessionHeartbeatController;
 import org.apache.sshd.common.session.SessionListenerManager;
 import org.apache.sshd.common.session.UnknownChannelReferenceHandlerManager;
 import org.apache.sshd.server.forward.AgentForwardingFilter;
@@ -62,7 +63,7 @@ public interface FactoryManager
                 PortForwardingEventListenerManager,
                 IoServiceEventListenerManager,
                 AttributeStore,
-                PropertyResolver {
+                SessionHeartbeatController {
 
     /**
      * Key used to retrieve the value of the channel window size in the
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/Service.java b/sshd-core/src/main/java/org/apache/sshd/common/Service.java
index f6caeb0..69426e9 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/Service.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/Service.java
@@ -22,15 +22,19 @@ import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.buffer.Buffer;
 
 /**
- * See RFC 4253 [SSH-TRANS] and the SSH_MSG_SERVICE_REQUEST packet. Examples include ssh-userauth
- * and ssh-connection but developers are also free to implement their own custom service.
+ * See RFC 4253 [SSH-TRANS] and the SSH_MSG_SERVICE_REQUEST packet. Examples include &quot;ssh-userauth&quot;
+ * and &quot;ssh-connection&quot; but developers are also free to implement their own custom service.
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public interface Service extends Closeable {
+public interface Service extends PropertyResolver, Closeable {
     Session getSession();
 
-    // TODO: this is specific to clients
+    @Override
+    default PropertyResolver getParentPropertyResolver() {
+        return getSession();
+    }
+
     void start();
 
     /**
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/ConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/common/session/ConnectionService.java
index ac977fa..ace0584 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/ConnectionService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/ConnectionService.java
@@ -35,6 +35,7 @@ import org.apache.sshd.server.x11.X11ForwardSupport;
  */
 public interface ConnectionService
         extends Service,
+        SessionHeartbeatController,
         UnknownChannelReferenceHandlerManager,
         PortForwardingEventListenerManager,
         PortForwardingEventListenerManagerHolder {
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
index 4aacf7d..2a0d585 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
@@ -26,9 +26,12 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.IntUnaryOperator;
 
@@ -89,6 +92,9 @@ public abstract class AbstractConnectionService
      */
     public static final IntUnaryOperator RESPONSE_BUFFER_GROWTH_FACTOR = Int2IntFunction.add(Byte.SIZE);
 
+    /** Used in {@code SSH_MSH_IGNORE} messages for the keep-alive mechanism */
+    public static final String DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING = "ignore@sshd.apache.org";
+
     /**
      * Map of channels keyed by the identifier
      */
@@ -97,6 +103,7 @@ public abstract class AbstractConnectionService
      * Next channel identifier
      */
     protected final AtomicInteger nextChannelId = new AtomicInteger(0);
+    protected final AtomicLong heartbeatCount = new AtomicLong(0L);
 
     private ScheduledFuture<?> heartBeat;
 
@@ -106,6 +113,7 @@ public abstract class AbstractConnectionService
     private final AtomicBoolean allowMoreSessions = new AtomicBoolean(true);
     private final Collection<PortForwardingEventListener> listeners = new CopyOnWriteArraySet<>();
     private final Collection<PortForwardingEventListenerManager> managersHolder = new CopyOnWriteArraySet<>();
+    private final Map<String, Object> properties = new ConcurrentHashMap<>();
     private final PortForwardingEventListener listenerProxy;
     private final AbstractSession sessionInstance;
     private UnknownChannelReferenceHandler unknownChannelReferenceHandler;
@@ -117,6 +125,11 @@ public abstract class AbstractConnectionService
     }
 
     @Override
+    public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    @Override
     public PortForwardingEventListener getPortForwardingEventListenerProxy() {
         return listenerProxy;
     }
@@ -175,12 +188,28 @@ public abstract class AbstractConnectionService
 
     @Override
     public void start() {
-        startHeartBeat();
+        heartBeat = startHeartBeat();
     }
 
     protected synchronized ScheduledFuture<?> startHeartBeat() {
-        // TODO SSHD-782
-        return null;
+        stopHeartBeat();    // make sure any existing heartbeat is stopped
+
+        HeartbeatType heartbeatType = getSessionHeartbeatType();
+        long interval = getSessionHeartbeatInterval();
+        Session session = getSession();
+        boolean debugEnabled = log.isDebugEnabled();
+        if (debugEnabled) {
+            log.debug("startHeartbeat({}) heartbeat type={}, interval={}", session, heartbeatType, interval);
+        }
+
+        if ((heartbeatType == null) || (heartbeatType == HeartbeatType.NONE) || (interval <= 0)) {
+            return null;
+        }
+
+        FactoryManager manager = session.getFactoryManager();
+        ScheduledExecutorService service = manager.getScheduledExecutorService();
+        return service.scheduleAtFixedRate(
+            this::sendHeartBeat, interval, interval, TimeUnit.MILLISECONDS);
     }
 
     /**
@@ -190,8 +219,51 @@ public abstract class AbstractConnectionService
      * message write completion - {@code null} if no heartbeat sent
      */
     protected IoWriteFuture sendHeartBeat() {
-        // TODO SSHD-782
-        return null;
+        HeartbeatType heartbeatType = getSessionHeartbeatType();
+        long interval = getSessionHeartbeatInterval();
+        Session session = getSession();
+        boolean traceEnabled = log.isTraceEnabled();
+        if (traceEnabled) {
+            log.trace("sendHeartbeat({}) heartbeat type={}, interval={}",
+                session, heartbeatType, interval);
+        }
+
+        if ((heartbeatType == null) || (heartbeatType == HeartbeatType.NONE) || (interval <= 0)) {
+            return null;
+        }
+
+        try {
+            Buffer buffer = session.createBuffer(
+                SshConstants.SSH_MSG_IGNORE, DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING.length() + Byte.SIZE);
+            buffer.putString(DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING);
+            IoWriteFuture future = session.writePacket(buffer);
+            future.addListener(this::futureDone);
+            return future;
+        } catch (IOException | RuntimeException | Error e) {
+            session.exceptionCaught(e);
+            if (log.isDebugEnabled()) {
+                log.debug("sendHeartBeat({}) failed ({}) to send heartbeat #{} request={}: {}",
+                    session, e.getClass().getSimpleName(), heartbeatCount, heartbeatType, e.getMessage());
+            }
+            if (log.isTraceEnabled()) {
+                log.trace("sendHeartBeat(" + session + ") exception details", e);
+            }
+
+            return new AbstractIoWriteFuture(DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING, null) {
+                {
+                    setValue(e);
+                }
+            };
+        }
+
+    }
+
+    protected void futureDone(IoWriteFuture future) {
+        Throwable t = future.getException();
+        if (t != null) {
+            Session session = getSession();
+            session.exceptionCaught(t);
+        }
     }
 
     protected synchronized void stopHeartBeat() {
@@ -229,7 +301,8 @@ public abstract class AbstractConnectionService
                 return forwarder;
             }
 
-            forwarder = ValidateUtils.checkNotNull(createForwardingFilter(session), "No forwarder created for %s", session);
+            forwarder = ValidateUtils.checkNotNull(
+                createForwardingFilter(session), "No forwarder created for %s", session);
             forwarderHolder.set(forwarder);
         }
 
@@ -267,7 +340,8 @@ public abstract class AbstractConnectionService
                 return x11Support;
             }
 
-            x11Support = ValidateUtils.checkNotNull(createX11ForwardSupport(session), "No X11 forwarder created for %s", session);
+            x11Support = ValidateUtils.checkNotNull(
+                createX11ForwardSupport(session), "No X11 forwarder created for %s", session);
             x11ForwardHolder.set(x11Support);
         }
 
@@ -291,7 +365,8 @@ public abstract class AbstractConnectionService
                 return agentForward;
             }
 
-            agentForward = ValidateUtils.checkNotNull(createAgentForwardSupport(session), "No agent forward created for %s", session);
+            agentForward = ValidateUtils.checkNotNull(
+                createAgentForwardSupport(session), "No agent forward created for %s", session);
             agentForwardHolder.set(agentForward);
         }
 
@@ -349,7 +424,8 @@ public abstract class AbstractConnectionService
     }
 
     protected void handleChannelRegistrationFailure(Channel channel, int channelId) throws IOException {
-        RuntimeException reason = new IllegalStateException("Channel id=" + channelId + " not registered because session is being closed: " + this);
+        RuntimeException reason = new IllegalStateException(
+            "Channel id=" + channelId + " not registered because session is being closed: " + this);
         AbstractChannel notifier =
             ValidateUtils.checkInstanceOf(channel, AbstractChannel.class, "Non abstract channel for id=%d", channelId);
         notifier.signalChannelClosed(reason);
@@ -678,7 +754,8 @@ public abstract class AbstractConnectionService
         Channel channel = NamedFactory.create(manager.getChannelFactories(), type);
         if (channel == null) {
             // TODO add language tag configurable control
-            sendChannelOpenFailure(buffer, sender, SshConstants.SSH_OPEN_UNKNOWN_CHANNEL_TYPE, "Unsupported channel type: " + type, "");
+            sendChannelOpenFailure(buffer, sender,
+                SshConstants.SSH_OPEN_UNKNOWN_CHANNEL_TYPE, "Unsupported channel type: " + type, "");
             return;
         }
 
@@ -719,14 +796,16 @@ public abstract class AbstractConnectionService
             } catch (IOException e) {
                 if (debugEnabled) {
                     log.debug("operationComplete({}) {}: {}",
-                              AbstractConnectionService.this, e.getClass().getSimpleName(), e.getMessage());
+                          AbstractConnectionService.this, e.getClass().getSimpleName(), e.getMessage());
                 }
                 session.exceptionCaught(e);
             }
         });
     }
 
-    protected IoWriteFuture sendChannelOpenFailure(Buffer buffer, int sender, int reasonCode, String message, String lang) throws IOException {
+    protected IoWriteFuture sendChannelOpenFailure(
+            Buffer buffer, int sender, int reasonCode, String message, String lang)
+                throws IOException {
         if (log.isDebugEnabled()) {
             log.debug("sendChannelOpenFailure({}) sender={}, reason={}, lang={}, message='{}'",
                   this, sender, SshConstants.getOpenErrorCodeName(reasonCode), lang, message);
@@ -797,7 +876,9 @@ public abstract class AbstractConnectionService
         return sendGlobalResponse(buffer, req, RequestHandler.Result.Unsupported, wantReply);
     }
 
-    protected IoWriteFuture sendGlobalResponse(Buffer buffer, String req, RequestHandler.Result result, boolean wantReply) throws IOException {
+    protected IoWriteFuture sendGlobalResponse(
+            Buffer buffer, String req, RequestHandler.Result result, boolean wantReply)
+                throws IOException {
         if (log.isDebugEnabled()) {
             log.debug("sendGlobalResponse({})[{}] result={}, want-reply={}", this, req, result, wantReply);
         }
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/SshServerConfigFileReader.java b/sshd-core/src/main/java/org/apache/sshd/server/config/SshServerConfigFileReader.java
index bf4a958..eead6b8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/config/SshServerConfigFileReader.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/config/SshServerConfigFileReader.java
@@ -59,6 +59,9 @@ public final class SshServerConfigFileReader {
     public static final String VISUAL_HOST_KEY = "VisualHostKey";
     public static final String DEFAULT_VISUAL_HOST_KEY = "no";
 
+    public static final String SERVER_ALIVE_INTERVAL_PROP = "ServerAliveInterval";
+    public static final long DEFAULT_ALIVE_INTERVAL = 0L;
+
     private SshServerConfigFileReader() {
         throw new UnsupportedOperationException("No instance allowed");
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java
index cf8cb4d..b6a1e55 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java
@@ -33,7 +33,9 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
@@ -65,8 +67,9 @@ import org.apache.sshd.server.auth.WelcomeBannerPhase;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class ServerUserAuthService extends AbstractCloseable implements Service, ServerSessionHolder {
-    private final ServerSession serverSession;
     private final AtomicBoolean welcomeSent = new AtomicBoolean(false);
+    private final Map<String, Object> properties = new ConcurrentHashMap<>();
+    private final ServerSession serverSession;
     private final WelcomeBannerPhase welcomePhase;
     private List<NamedFactory<UserAuth>> userAuthFactories;
     private List<List<String>> authMethods;
@@ -86,10 +89,11 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
             throw new SshException("Session already authenticated");
         }
 
-        Object phase = PropertyResolverUtils.getObject(s, ServerAuthenticationManager.WELCOME_BANNER_PHASE);
-        phase = PropertyResolverUtils.toEnum(WelcomeBannerPhase.class, phase, true, WelcomeBannerPhase.VALUES);
+        Object phase = this.getObject(ServerAuthenticationManager.WELCOME_BANNER_PHASE);
+        phase = PropertyResolverUtils.toEnum(WelcomeBannerPhase.class, phase, false, WelcomeBannerPhase.VALUES);
         welcomePhase = (phase == null) ? ServerAuthenticationManager.DEFAULT_BANNER_PHASE : (WelcomeBannerPhase) phase;
-        maxAuthRequests = s.getIntProperty(ServerAuthenticationManager.MAX_AUTH_REQUESTS, ServerAuthenticationManager.DEFAULT_MAX_AUTH_REQUESTS);
+        maxAuthRequests = this.getIntProperty(
+            ServerAuthenticationManager.MAX_AUTH_REQUESTS, ServerAuthenticationManager.DEFAULT_MAX_AUTH_REQUESTS);
 
         List<NamedFactory<UserAuth>> factories = ValidateUtils.checkNotNullAndNotEmpty(
             serverSession.getUserAuthFactories(), "No user auth factories for %s", s);
@@ -97,7 +101,7 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
         // Get authentication methods
         authMethods = new ArrayList<>();
 
-        String mths = s.getString(ServerAuthenticationManager.AUTH_METHODS);
+        String mths = this.getString(ServerAuthenticationManager.AUTH_METHODS);
         if (GenericUtils.isEmpty(mths)) {
             for (NamedFactory<UserAuth> uaf : factories) {
                 authMethods.add(new ArrayList<>(Collections.singletonList(uaf.getName())));
@@ -149,6 +153,11 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
     }
 
     @Override
+    public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    @Override
     public synchronized void process(int cmd, Buffer buffer) throws Exception {
         Boolean authed = Boolean.FALSE;
         ServerSession session = getServerSession();
@@ -448,7 +457,7 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
             return null;
         }
 
-        String lang = PropertyResolverUtils.getStringProperty(session,
+        String lang = this.getStringProperty(
             ServerAuthenticationManager.WELCOME_BANNER_LANGUAGE,
             ServerAuthenticationManager.DEFAULT_WELCOME_BANNER_LANGUAGE);
         Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_BANNER,
@@ -464,7 +473,7 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
     }
 
     protected String resolveWelcomeBanner(ServerSession session) throws IOException {
-        Object bannerValue = session.getObject(ServerAuthenticationManager.WELCOME_BANNER);
+        Object bannerValue = this.getObject(ServerAuthenticationManager.WELCOME_BANNER);
         if (bannerValue == null) {
             return null;
         }
@@ -523,7 +532,8 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
         }
 
         if (bannerValue instanceof URL) {
-            Charset cs = PropertyResolverUtils.getCharset(session, ServerAuthenticationManager.WELCOME_BANNER_CHARSET, Charset.defaultCharset());
+            Charset cs = this.getCharset(
+                ServerAuthenticationManager.WELCOME_BANNER_CHARSET, Charset.defaultCharset());
             return loadWelcomeBanner(session, (URL) bannerValue, cs);
         }
 
diff --git a/sshd-core/src/test/java/org/apache/sshd/deprecated/ClientUserAuthServiceOld.java b/sshd-core/src/test/java/org/apache/sshd/deprecated/ClientUserAuthServiceOld.java
index 44d8c70..9b4b963 100644
--- a/sshd-core/src/test/java/org/apache/sshd/deprecated/ClientUserAuthServiceOld.java
+++ b/sshd-core/src/test/java/org/apache/sshd/deprecated/ClientUserAuthServiceOld.java
@@ -19,6 +19,8 @@
 package org.apache.sshd.deprecated;
 
 import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.future.AuthFuture;
@@ -54,6 +56,8 @@ public class ClientUserAuthServiceOld extends AbstractCloseable implements Servi
         }
     }
 
+    private final Map<String, Object> properties = new ConcurrentHashMap<>();
+
     /**
      * When !authFuture.isDone() the current authentication
      */
@@ -84,6 +88,11 @@ public class ClientUserAuthServiceOld extends AbstractCloseable implements Servi
     }
 
     @Override
+    public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    @Override
     public void start() {
         synchronized (lock) {
             log.debug("accepted");


[mina-sshd] 01/04: [SSHD-782] Moved heartbeat support to AbstractConnectionService

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 19d60f41882efb6573a2527a7a8180d5a3b16f3a
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Tue Feb 26 11:53:53 2019 +0200

    [SSHD-782] Moved heartbeat support to AbstractConnectionService
---
 .../client/session/ClientConnectionService.java    | 80 +++++++++++++---------
 .../session/helpers/AbstractConnectionService.java | 51 +++++++++++++-
 2 files changed, 98 insertions(+), 33 deletions(-)

diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
index 497e61e..06c6000 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
@@ -31,6 +31,7 @@ import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.io.AbstractIoWriteFuture;
 import org.apache.sshd.common.io.IoWriteFuture;
 import org.apache.sshd.common.session.helpers.AbstractConnectionService;
+import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.server.x11.X11ForwardSupport;
 
@@ -42,11 +43,18 @@ import org.apache.sshd.server.x11.X11ForwardSupport;
 public class ClientConnectionService
         extends AbstractConnectionService
         implements ClientSessionHolder {
-
-    private ScheduledFuture<?> heartBeat;
+    protected final String heartbeatRequest;
+    protected final long heartbeatInterval;
+    /** Non-null only if using the &quot;keep-alive&quot; request mechanism */
+    protected ScheduledFuture<?> clientHeartbeat;
 
     public ClientConnectionService(AbstractClientSession s) throws SshException {
         super(s);
+
+        heartbeatRequest = s.getStringProperty(
+            ClientFactoryManager.HEARTBEAT_REQUEST, ClientFactoryManager.DEFAULT_KEEP_ALIVE_HEARTBEAT_STRING);
+        heartbeatInterval = s.getLongProperty(
+            ClientFactoryManager.HEARTBEAT_INTERVAL, ClientFactoryManager.DEFAULT_HEARTBEAT_INTERVAL);
     }
 
     @Override
@@ -65,60 +73,70 @@ public class ClientConnectionService
         if (!session.isAuthenticated()) {
             throw new IllegalStateException("Session is not authenticated");
         }
-        startHeartBeat();
+        super.start();
     }
 
     @Override
-    protected void preClose() {
-        stopHeartBeat();
-        super.preClose();
-    }
+    protected synchronized ScheduledFuture<?> startHeartBeat() {
+        if ((heartbeatInterval > 0L) && GenericUtils.isNotEmpty(heartbeatRequest)) {
+            stopHeartBeat();
 
-    protected synchronized void startHeartBeat() {
-        stopHeartBeat();
-        ClientSession session = getClientSession();
-        long interval = session.getLongProperty(ClientFactoryManager.HEARTBEAT_INTERVAL, ClientFactoryManager.DEFAULT_HEARTBEAT_INTERVAL);
-        if (interval > 0L) {
+            ClientSession session = getClientSession();
             FactoryManager manager = session.getFactoryManager();
             ScheduledExecutorService service = manager.getScheduledExecutorService();
-            heartBeat = service.scheduleAtFixedRate(this::sendHeartBeat, interval, interval, TimeUnit.MILLISECONDS);
+            clientHeartbeat = service.scheduleAtFixedRate(
+                this::sendHeartBeat, heartbeatInterval, heartbeatInterval, TimeUnit.MILLISECONDS);
             if (log.isDebugEnabled()) {
-                log.debug("startHeartbeat - started at interval={}", interval);
+                log.debug("startHeartbeat({}) - started at interval={} with request={}",
+                    session, heartbeatInterval, heartbeatRequest);
             }
+
+            return clientHeartbeat;
+        } else {
+            return super.startHeartBeat();
         }
     }
 
+    @Override
     protected synchronized void stopHeartBeat() {
-        if (heartBeat != null) {
-            heartBeat.cancel(true);
-            heartBeat = null;
+        try {
+            super.stopHeartBeat();
+        } finally {
+            // No need to cancel since this is the same reference as the superclass heartbeat future
+            if (clientHeartbeat != null) {
+                clientHeartbeat = null;
+            }
         }
     }
 
-    /**
-     * Sends a heartbeat message
-     * @return The {@link IoWriteFuture} that can be used to wait for the
-     * message write completion
-     */
+    @Override
     protected IoWriteFuture sendHeartBeat() {
+        if (clientHeartbeat == null) {
+            return super.sendHeartBeat();
+        }
+
         ClientSession session = getClientSession();
-        String request = session.getStringProperty(ClientFactoryManager.HEARTBEAT_REQUEST, ClientFactoryManager.DEFAULT_KEEP_ALIVE_HEARTBEAT_STRING);
         try {
-            Buffer buf = session.createBuffer(SshConstants.SSH_MSG_GLOBAL_REQUEST, request.length() + Byte.SIZE);
-            buf.putString(request);
+            Buffer buf = session.createBuffer(
+                SshConstants.SSH_MSG_GLOBAL_REQUEST, heartbeatRequest.length() + Byte.SIZE);
+            buf.putString(heartbeatRequest);
             buf.putBoolean(false);
             IoWriteFuture future = session.writePacket(buf);
             future.addListener(this::futureDone);
             return future;
-        } catch (IOException e) {
-            getSession().exceptionCaught(e);
+        } catch (IOException | RuntimeException e) {
+            session.exceptionCaught(e);
             if (log.isDebugEnabled()) {
-                log.debug("Error (" + e.getClass().getSimpleName() + ") sending keepalive message=" + request + ": " + e.getMessage());
+                log.debug("sendHeartBeat({}) failed ({}) to send request={}: {}",
+                    session, e.getClass().getSimpleName(), heartbeatRequest, e.getMessage());
+            }
+            if (log.isTraceEnabled()) {
+                log.trace("sendHeartBeat(" + session + ") exception details", e);
             }
-            Throwable t = e;
-            return new AbstractIoWriteFuture(request, null) {
+
+            return new AbstractIoWriteFuture(heartbeatRequest, null) {
                 {
-                    setValue(t);
+                    setValue(e);
                 }
             };
         }
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
index fcac6f2..4aacf7d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
@@ -26,6 +26,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
@@ -52,6 +53,7 @@ import org.apache.sshd.common.forward.PortForwardingEventListenerManager;
 import org.apache.sshd.common.io.AbstractIoWriteFuture;
 import org.apache.sshd.common.io.IoWriteFuture;
 import org.apache.sshd.common.session.ConnectionService;
+import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.session.UnknownChannelReferenceHandler;
 import org.apache.sshd.common.util.EventListenerUtils;
 import org.apache.sshd.common.util.GenericUtils;
@@ -96,6 +98,8 @@ public abstract class AbstractConnectionService
      */
     protected final AtomicInteger nextChannelId = new AtomicInteger(0);
 
+    private ScheduledFuture<?> heartBeat;
+
     private final AtomicReference<AgentForwardSupport> agentForwardHolder = new AtomicReference<>();
     private final AtomicReference<X11ForwardSupport> x11ForwardHolder = new AtomicReference<>();
     private final AtomicReference<ForwardingFilter> forwarderHolder = new AtomicReference<>();
@@ -108,7 +112,8 @@ public abstract class AbstractConnectionService
 
     protected AbstractConnectionService(AbstractSession session) {
         sessionInstance = Objects.requireNonNull(session, "No session");
-        listenerProxy = EventListenerUtils.proxyWrapper(PortForwardingEventListener.class, getClass().getClassLoader(), listeners);
+        listenerProxy = EventListenerUtils.proxyWrapper(
+            PortForwardingEventListener.class, getClass().getClassLoader(), listeners);
     }
 
     @Override
@@ -170,7 +175,48 @@ public abstract class AbstractConnectionService
 
     @Override
     public void start() {
-        // do nothing
+        startHeartBeat();
+    }
+
+    protected synchronized ScheduledFuture<?> startHeartBeat() {
+        // TODO SSHD-782
+        return null;
+    }
+
+    /**
+     * Sends a heartbeat message/packet
+     *
+     * @return The {@link IoWriteFuture} that can be used to wait for the
+     * message write completion - {@code null} if no heartbeat sent
+     */
+    protected IoWriteFuture sendHeartBeat() {
+        // TODO SSHD-782
+        return null;
+    }
+
+    protected synchronized void stopHeartBeat() {
+        boolean debugEnabled = log.isDebugEnabled();
+        Session session = getSession();
+        if (heartBeat == null) {
+            if (debugEnabled) {
+                log.debug("stopHeartBeat({}) no heartbeat to stop", session);
+            }
+            return;
+        }
+
+        if (debugEnabled) {
+            log.debug("stopHeartBeat({}) stopping", session);
+        }
+
+        try {
+            heartBeat.cancel(true);
+        } finally {
+            heartBeat = null;
+        }
+
+        if (debugEnabled) {
+            log.debug("stopHeartBeat({}) stopped", session);
+        }
     }
 
     @Override
@@ -195,6 +241,7 @@ public abstract class AbstractConnectionService
 
     @Override
     protected void preClose() {
+        stopHeartBeat();
         this.listeners.clear();
         this.managersHolder.clear();
         super.preClose();


[mina-sshd] 03/04: [SSHD-901] Added capability to request a reply for the client's keep-alive request in order to avoid client-side session timeout due to no traffic from the server

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 4211021eb898ebd2a1dc227feeba1bcfcefcd2c9
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Wed Feb 27 17:47:10 2019 +0200

    [SSHD-901] Added capability to request a reply for the client's keep-alive request in order to avoid client-side session timeout due to no traffic from the server
---
 CHANGES.md                                         |  3 ++
 docs/client-setup.md                               | 16 +++++++++++
 .../sshd/cli/client/SshClientCliSupport.java       |  8 ++++++
 .../apache/sshd/client/ClientFactoryManager.java   | 12 +++++++-
 .../client/config/SshClientConfigFileReader.java   |  3 ++
 .../sshd/client/global/OpenSshHostKeysHandler.java |  4 ++-
 .../client/session/ClientConnectionService.java    | 33 ++++++++++++++--------
 .../global/AbstractOpenSshHostKeysHandler.java     | 11 ++++++--
 .../session/helpers/AbstractConnectionService.java | 27 ++++++++----------
 .../common/session/helpers/AbstractSession.java    |  6 ++--
 .../server/global/CancelTcpipForwardHandler.java   |  4 ++-
 .../sshd/server/global/KeepAliveHandler.java       | 18 +++++++++---
 .../sshd/server/global/NoMoreSessionsHandler.java  |  4 ++-
 .../sshd/server/global/OpenSshHostKeysHandler.java |  6 ++--
 .../sshd/server/global/TcpipForwardHandler.java    |  4 ++-
 15 files changed, 114 insertions(+), 45 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index a9f5839..206c5ed 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -89,3 +89,6 @@ client.setSignatureFactories(
 
 * [SSHD-897](https://issues.apache.org/jira/browse/SSHD-897) - The default CLI code automatically tries to detect the PTY settings to use
 if opening a shell or command channel.
+
+* [SSHD-901](https://issues.apache.org/jira/browse/SSHD-901) - Added capability to request a reply for the `keepalive@...` heartbeat request
+in order to avoid client-side session timeout due to no traffic from server.
diff --git a/docs/client-setup.md b/docs/client-setup.md
index 4b30dad..ce0fffc 100644
--- a/docs/client-setup.md
+++ b/docs/client-setup.md
@@ -174,6 +174,14 @@ regardless of the user's own traffic:
     documentation for these features. The simplest way to activate this feature is to set the `HEARTBEAT_INTERVAL` property value
     to the **milliseconds** value of the requested heartbeat interval.
 
+    This configuration only ensures that the **server** does not terminate the session due to no traffic. If the
+    incoming traffic from the server may also suffer from long "quiet" periods, one runs the risk of a **client** time-out. In order
+    to avoid this, it is possible to activate the `wantReply` option for the global request. This way, there is bound to be some
+    packet response (even if failure - which will be ignored by the heartbeat code). In order to activate this option one needs to
+    set the `HEARTBEAT_REPLY_WAIT` property value to a **positive** value specifying the number of **milliseconds** the client is
+    willing to wait for the server's reply to the global request.
+
+
 **Note(s):**
 
 * Both options are disabled by default - they need to be activated explicitly.
@@ -183,12 +191,20 @@ the `ClientSession` (for specific session configuration).
 
 * The `keepalive@,,,,` mechanism **supersedes** the `SSH_MSG_IGNORE` one if both activated.
 
+    * If specified timeout expires for the `wantReply` option then session will be **closed**.
+
+    * *Any* response - including [`SSH_MSH_REQUEST_FAILURE`](https://tools.ietf.org/html/rfc4254#page-4)
+    is considered a "good" response for the heartbeat request.
+
 * When using the CLI, these options can be configured using the following `-o key=value` properties:
 
     * `ClientAliveInterval` - if positive the defines the heartbeat interval in **seconds**.
 
     * `ClientAliveUseNullPackets` - *true* if use the `SSH_MSG_IGNORE` mechanism, *false* if use global request (default).
 
+    * `ClientAliveReplyWait` - if positive, then activates the `wantReply` mechanism and specific the expected
+    response timeout in **seconds**.
+
 ## Running a command or opening a shell
 
 When running a command or opening a shell, there is an extra concern regarding the PTY configuration and/or the
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
index ae83310..3857ac2 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
@@ -394,6 +394,14 @@ public abstract class SshClientCliSupport extends CliSupport {
         } else {
             client.setSessionHeartbeat(HeartbeatType.IGNORE, TimeUnit.SECONDS, interval);
             stdout.println("Using global request heartbeat every " + interval + " seconds");
+
+            interval = PropertyResolverUtils.getLongProperty(
+                options, SshClientConfigFileReader.CLIENT_LIVECHECK_REPLIES_WAIT, SshClientConfigFileReader.DEFAULT_LIVECHECK_REPLY_WAIT);
+            if (interval > 0L) {
+                PropertyResolverUtils.updateProperty(
+                    client, ClientFactoryManager.HEARTBEAT_REPLY_WAIT, TimeUnit.SECONDS.toMillis(interval));
+                stdout.println("Expecting heartbeat reply within at most " + interval + " seconds");
+            }
         }
     }
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
index 6e3fa02..026467b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
@@ -63,11 +63,21 @@ public interface ClientFactoryManager
     String HEARTBEAT_REQUEST = "heartbeat-request";
 
     /**
-     * Default value for {@link ClientFactoryManager#HEARTBEAT_REQUEST} is none configured
+     * Default value for {@value #HEARTBEAT_REQUEST} is none configured
      */
     String DEFAULT_KEEP_ALIVE_HEARTBEAT_STRING = "keepalive@sshd.apache.org";
 
     /**
+     * Key used to indicate that the heartbeat request is also
+     * expecting a reply - time in <U>milliseconds</U> to wait for
+     * the reply. If non-positive then no reply is expected (nor requested).
+     */
+    String HEARTBEAT_REPLY_WAIT = "heartbeat-reply-wait";
+
+    /** Default value for {@value #HEARTBEAT_REPLY_WAIT} if none is configured */
+    long DEFAULT_HEARTBEAT_REPLY_WAIT = 0L;
+
+    /**
      * Whether to ignore invalid identities files when pre-initializing
      * the client session
      * @see ClientIdentityLoader#isValidLocation(org.apache.sshd.common.NamedResource)
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java b/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java
index 669040d..3daab7b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java
@@ -38,6 +38,9 @@ public final class SshClientConfigFileReader {
     public static final String CLIENT_LIVECHECK_USE_NULLS = "ClientAliveUseNullPackets";
     public static final boolean DEFAULT_LIVECHECK_USE_NULLS = false;
 
+    public static final String CLIENT_LIVECHECK_REPLIES_WAIT = "ClientAliveReplyWait";
+    public static final long DEFAULT_LIVECHECK_REPLY_WAIT = 0L;
+
     private SshClientConfigFileReader() {
         throw new UnsupportedOperationException("No instance allowed");
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/global/OpenSshHostKeysHandler.java b/sshd-core/src/main/java/org/apache/sshd/client/global/OpenSshHostKeysHandler.java
index 128a0f1..a98eadc 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/global/OpenSshHostKeysHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/global/OpenSshHostKeysHandler.java
@@ -50,7 +50,9 @@ public class OpenSshHostKeysHandler extends AbstractOpenSshHostKeysHandler {
     }
 
     @Override
-    protected Result handleHostKeys(Session session, Collection<? extends PublicKey> keys, boolean wantReply, Buffer buffer) throws Exception {
+    protected Result handleHostKeys(
+            Session session, Collection<? extends PublicKey> keys, boolean wantReply, Buffer buffer)
+                throws Exception {
         // according to the spec, no reply should be required
         ValidateUtils.checkTrue(!wantReply, "Unexpected reply required for the host keys of %s", session);
         if (log.isDebugEnabled()) {
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
index bd5873c..dd472bd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
@@ -28,7 +28,6 @@ import org.apache.sshd.client.ClientFactoryManager;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.io.AbstractIoWriteFuture;
 import org.apache.sshd.common.io.IoWriteFuture;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.session.helpers.AbstractConnectionService;
@@ -46,6 +45,7 @@ public class ClientConnectionService
         implements ClientSessionHolder {
     protected final String heartbeatRequest;
     protected final long heartbeatInterval;
+    protected final long heartbeatReplyMaxWait;
     /** Non-null only if using the &quot;keep-alive&quot; request mechanism */
     protected ScheduledFuture<?> clientHeartbeat;
 
@@ -56,6 +56,8 @@ public class ClientConnectionService
             ClientFactoryManager.HEARTBEAT_REQUEST, ClientFactoryManager.DEFAULT_KEEP_ALIVE_HEARTBEAT_STRING);
         heartbeatInterval = this.getLongProperty(
             ClientFactoryManager.HEARTBEAT_INTERVAL, ClientFactoryManager.DEFAULT_HEARTBEAT_INTERVAL);
+        heartbeatReplyMaxWait = this.getLongProperty(
+            ClientFactoryManager.HEARTBEAT_REPLY_WAIT, ClientFactoryManager.DEFAULT_HEARTBEAT_REPLY_WAIT);
     }
 
     @Override
@@ -111,22 +113,33 @@ public class ClientConnectionService
     }
 
     @Override
-    protected IoWriteFuture sendHeartBeat() {
+    protected boolean sendHeartBeat() {
         if (clientHeartbeat == null) {
             return super.sendHeartBeat();
         }
 
         Session session = getSession();
         try {
+            boolean withReply = heartbeatReplyMaxWait > 0L;
             Buffer buf = session.createBuffer(
                 SshConstants.SSH_MSG_GLOBAL_REQUEST, heartbeatRequest.length() + Byte.SIZE);
             buf.putString(heartbeatRequest);
-            buf.putBoolean(false);
-
-            IoWriteFuture future = session.writePacket(buf);
-            future.addListener(this::futureDone);
+            buf.putBoolean(withReply);
+
+            if (withReply) {
+                Buffer reply = session.request(heartbeatRequest, buf, heartbeatReplyMaxWait, TimeUnit.MILLISECONDS);
+                if (reply != null) {
+                    if (log.isTraceEnabled()) {
+                        log.trace("sendHeartBeat({}) received reply size={} for request={}",
+                            session, reply.available(), heartbeatRequest);
+                    }
+                }
+            } else {
+                IoWriteFuture future = session.writePacket(buf);
+                future.addListener(this::futureDone);
+            }
             heartbeatCount.incrementAndGet();
-            return future;
+            return true;
         } catch (IOException | RuntimeException | Error e) {
             session.exceptionCaught(e);
             if (log.isDebugEnabled()) {
@@ -137,11 +150,7 @@ public class ClientConnectionService
                 log.trace("sendHeartBeat(" + session + ") exception details", e);
             }
 
-            return new AbstractIoWriteFuture(heartbeatRequest, null) {
-                {
-                    setValue(e);
-                }
-            };
+            return false;
         }
     }
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/global/AbstractOpenSshHostKeysHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/global/AbstractOpenSshHostKeysHandler.java
index bcb70ea..7b9d881 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/global/AbstractOpenSshHostKeysHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/global/AbstractOpenSshHostKeysHandler.java
@@ -43,7 +43,8 @@ public abstract class AbstractOpenSshHostKeysHandler extends AbstractConnectionS
         this(request, BufferPublicKeyParser.DEFAULT);
     }
 
-    protected AbstractOpenSshHostKeysHandler(String request, BufferPublicKeyParser<? extends PublicKey> parser) {
+    protected AbstractOpenSshHostKeysHandler(
+            String request, BufferPublicKeyParser<? extends PublicKey> parser) {
         this.request = ValidateUtils.checkNotNullAndNotEmpty(request, "No request identifier");
         this.parser = Objects.requireNonNull(parser, "No public keys extractor");
     }
@@ -57,7 +58,9 @@ public abstract class AbstractOpenSshHostKeysHandler extends AbstractConnectionS
     }
 
     @Override
-    public Result process(ConnectionService connectionService, String request, boolean wantReply, Buffer buffer) throws Exception {
+    public Result process(
+            ConnectionService connectionService, String request, boolean wantReply, Buffer buffer)
+                throws Exception {
         String expected = getRequestName();
         if (!expected.equals(request)) {
             return super.process(connectionService, request, wantReply, buffer);
@@ -82,7 +85,9 @@ public abstract class AbstractOpenSshHostKeysHandler extends AbstractConnectionS
         return handleHostKeys(connectionService.getSession(), keys, wantReply, buffer);
     }
 
-    protected abstract Result handleHostKeys(Session session, Collection<? extends PublicKey> keys, boolean wantReply, Buffer buffer) throws Exception;
+    protected abstract Result handleHostKeys(
+        Session session, Collection<? extends PublicKey> keys, boolean wantReply, Buffer buffer)
+            throws Exception;
 
     @Override
     public String toString() {
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
index 2a0d585..9ac6077 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
@@ -202,7 +202,7 @@ public abstract class AbstractConnectionService
             log.debug("startHeartbeat({}) heartbeat type={}, interval={}", session, heartbeatType, interval);
         }
 
-        if ((heartbeatType == null) || (heartbeatType == HeartbeatType.NONE) || (interval <= 0)) {
+        if ((heartbeatType == null) || (heartbeatType == HeartbeatType.NONE) || (interval <= 0L)) {
             return null;
         }
 
@@ -214,11 +214,9 @@ public abstract class AbstractConnectionService
 
     /**
      * Sends a heartbeat message/packet
-     *
-     * @return The {@link IoWriteFuture} that can be used to wait for the
-     * message write completion - {@code null} if no heartbeat sent
+     * @return {@code true} if heartbeat successfully sent
      */
-    protected IoWriteFuture sendHeartBeat() {
+    protected boolean sendHeartBeat() {
         HeartbeatType heartbeatType = getSessionHeartbeatType();
         long interval = getSessionHeartbeatInterval();
         Session session = getSession();
@@ -228,17 +226,19 @@ public abstract class AbstractConnectionService
                 session, heartbeatType, interval);
         }
 
-        if ((heartbeatType == null) || (heartbeatType == HeartbeatType.NONE) || (interval <= 0)) {
-            return null;
+        if ((heartbeatType == null) || (heartbeatType == HeartbeatType.NONE)
+                || (interval <= 0L) || (heartBeat == null)) {
+            return false;
         }
 
         try {
             Buffer buffer = session.createBuffer(
                 SshConstants.SSH_MSG_IGNORE, DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING.length() + Byte.SIZE);
             buffer.putString(DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING);
+
             IoWriteFuture future = session.writePacket(buffer);
             future.addListener(this::futureDone);
-            return future;
+            return true;
         } catch (IOException | RuntimeException | Error e) {
             session.exceptionCaught(e);
             if (log.isDebugEnabled()) {
@@ -249,13 +249,8 @@ public abstract class AbstractConnectionService
                 log.trace("sendHeartBeat(" + session + ") exception details", e);
             }
 
-            return new AbstractIoWriteFuture(DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING, null) {
-                {
-                    setValue(e);
-                }
-            };
+            return false;
         }
-
     }
 
     protected void futureDone(IoWriteFuture future) {
@@ -396,7 +391,7 @@ public abstract class AbstractConnectionService
     @Override
     public int registerChannel(Channel channel) throws IOException {
         AbstractSession session = getSession();
-        int maxChannels = session.getIntProperty(MAX_CONCURRENT_CHANNELS_PROP, DEFAULT_MAX_CHANNELS);
+        int maxChannels = this.getIntProperty(MAX_CONCURRENT_CHANNELS_PROP, DEFAULT_MAX_CHANNELS);
         int curSize = channels.size();
         if (curSize > maxChannels) {
             throw new IllegalStateException("Currently active channels (" + curSize + ") at max.: " + maxChannels);
@@ -894,7 +889,7 @@ public abstract class AbstractConnectionService
         byte cmd = RequestHandler.Result.ReplySuccess.equals(result)
              ? SshConstants.SSH_MSG_REQUEST_SUCCESS
              : SshConstants.SSH_MSG_REQUEST_FAILURE;
-        AbstractSession session = getSession();
+        Session session = getSession();
         Buffer rsp = session.createBuffer(cmd, 2);
         return session.writePacket(rsp);
     }
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 7f41db3..6b5b89e 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
@@ -899,7 +899,8 @@ public abstract class AbstractSession extends SessionHelper {
 
         long maxWaitMillis = TimeUnit.MILLISECONDS.convert(timeout, unit);
         if (maxWaitMillis <= 0L) {
-            throw new IllegalArgumentException("Requested timeout for " + request + " below 1 msec: " + timeout + " " + unit);
+            throw new IllegalArgumentException(
+                "Requested timeout for " + request + " below 1 msec: " + timeout + " " + unit);
         }
 
         boolean debugEnabled = log.isDebugEnabled();
@@ -934,7 +935,8 @@ public abstract class AbstractSession extends SessionHelper {
                     result = requestResult.getAndSet(null);
                 }
             } catch (InterruptedException e) {
-                throw (InterruptedIOException) new InterruptedIOException("Interrupted while waiting for request=" + request + " result").initCause(e);
+                throw (InterruptedIOException) new InterruptedIOException(
+                    "Interrupted while waiting for request=" + request + " result").initCause(e);
             }
         }
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/global/CancelTcpipForwardHandler.java b/sshd-core/src/main/java/org/apache/sshd/server/global/CancelTcpipForwardHandler.java
index be42a6e..6b79351 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/global/CancelTcpipForwardHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/global/CancelTcpipForwardHandler.java
@@ -50,7 +50,9 @@ public class CancelTcpipForwardHandler extends AbstractConnectionServiceRequestH
     }
 
     @Override
-    public Result process(ConnectionService connectionService, String request, boolean wantReply, Buffer buffer) throws Exception {
+    public Result process(
+            ConnectionService connectionService, String request, boolean wantReply, Buffer buffer)
+                throws Exception {
         if (!REQUEST.equals(request)) {
             return super.process(connectionService, request, wantReply, buffer);
         }
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/global/KeepAliveHandler.java b/sshd-core/src/main/java/org/apache/sshd/server/global/KeepAliveHandler.java
index 6b6ca2c..51d1a59 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/global/KeepAliveHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/global/KeepAliveHandler.java
@@ -18,7 +18,9 @@
  */
 package org.apache.sshd.server.global;
 
+import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.session.ConnectionService;
+import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.session.helpers.AbstractConnectionServiceRequestHandler;
 import org.apache.sshd.common.util.buffer.Buffer;
 
@@ -35,11 +37,19 @@ public class KeepAliveHandler extends AbstractConnectionServiceRequestHandler {
     }
 
     @Override
-    public Result process(ConnectionService connectionService, String request, boolean wantReply, Buffer buffer) throws Exception {
-        if (request.startsWith("keepalive@")) {
-            return Result.ReplyFailure;
-        } else {
+    public Result process(
+            ConnectionService connectionService, String request, boolean wantReply, Buffer buffer)
+                throws Exception {
+        if (!request.startsWith("keepalive@")) {
             return super.process(connectionService, request, wantReply, buffer);
         }
+
+        if (wantReply) {
+            Session session = connectionService.getSession();
+            buffer = session.createBuffer(SshConstants.SSH_MSG_REQUEST_SUCCESS, Integer.BYTES);
+            session.writePacket(buffer);
+        }
+
+        return Result.Replied;
     }
 }
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/global/NoMoreSessionsHandler.java b/sshd-core/src/main/java/org/apache/sshd/server/global/NoMoreSessionsHandler.java
index c29a509..1635aec 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/global/NoMoreSessionsHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/global/NoMoreSessionsHandler.java
@@ -36,7 +36,9 @@ public class NoMoreSessionsHandler extends AbstractConnectionServiceRequestHandl
     }
 
     @Override
-    public Result process(ConnectionService connectionService, String request, boolean wantReply, Buffer buffer) throws Exception {
+    public Result process(
+            ConnectionService connectionService, String request, boolean wantReply, Buffer buffer)
+                throws Exception {
         if (request.startsWith("no-more-sessions@")) {
             connectionService.setAllowMoreSessions(false);
             return Result.ReplyFailure;
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/global/OpenSshHostKeysHandler.java b/sshd-core/src/main/java/org/apache/sshd/server/global/OpenSshHostKeysHandler.java
index c3f9477..4b38afc 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/global/OpenSshHostKeysHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/global/OpenSshHostKeysHandler.java
@@ -92,9 +92,9 @@ public class OpenSshHostKeysHandler extends AbstractOpenSshHostKeysHandler imple
         ValidateUtils.checkTrue(wantReply, "No reply required for host keys of %s", session);
         Collection<? extends NamedFactory<Signature>> factories =
             ValidateUtils.checkNotNullAndNotEmpty(
-                    SignatureFactoriesManager.resolveSignatureFactories(this, session),
-                    "No signature factories available for host keys of session=%s",
-                    session);
+                SignatureFactoriesManager.resolveSignatureFactories(this, session),
+                "No signature factories available for host keys of session=%s",
+                session);
         if (log.isDebugEnabled()) {
             log.debug("handleHostKeys({})[want-reply={}] received {} keys - factories={}",
                       session, wantReply, GenericUtils.size(keys), NamedResource.getNames(factories));
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/global/TcpipForwardHandler.java b/sshd-core/src/main/java/org/apache/sshd/server/global/TcpipForwardHandler.java
index a35def2..4990e2b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/global/TcpipForwardHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/global/TcpipForwardHandler.java
@@ -51,7 +51,9 @@ public class TcpipForwardHandler extends AbstractConnectionServiceRequestHandler
     }
 
     @Override
-    public Result process(ConnectionService connectionService, String request, boolean wantReply, Buffer buffer) throws Exception {
+    public Result process(
+            ConnectionService connectionService, String request, boolean wantReply, Buffer buffer)
+                throws Exception {
         if (!REQUEST.equals(request)) {
             return super.process(connectionService, request, wantReply, buffer);
         }