This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit eed4e1c55ed9e4c5efe71e1e9cb4514fd44b7789 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Thu Oct 3 12:05:39 2019 +0300 [SSHD-947] Added SessionListener#sessionEstablished method to allow for early session customization based on the peer's address --- CHANGES.md | 4 ++- docs/event-listeners.md | 10 ++++-- .../java/org/apache/sshd/common/util/Invoker.java | 40 ++++++++++++++++++---- .../sshd/client/session/AbstractClientSession.java | 11 +++--- .../sshd/common/session/SessionListener.java | 23 +++++++++++-- .../common/session/helpers/AbstractSession.java | 28 +++++++++++---- .../sshd/common/session/helpers/SessionHelper.java | 30 ++++++++++++++++ 7 files changed, 122 insertions(+), 24 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7fa2f29..02f2b7d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,9 +36,11 @@ the standard does not specifically specify the behavior regarding symbolic links ## Minor code helpers -* `SessionListener` supports `sessionPeerIdentificationReceived` that is invoked once successful +* `SessionListener` supports `sessionPeerIdentificationReceived` method that is invoked once successful peer version data is received. +* `SessionListener` supports `sessionEstablished` method that is invoked when initial constructor is executed. + * `ChannelIdTrackingUnknownChannelReferenceHandler` extends the functionality of the `DefaultUnknownChannelReferenceHandler` by tracking the initialized channels identifiers and being lenient only if command is received for a channel that was initialized in the past. diff --git a/docs/event-listeners.md b/docs/event-listeners.md index 49a4a83..6aa59cf 100644 --- a/docs/event-listeners.md +++ b/docs/event-listeners.md @@ -38,8 +38,9 @@ registered listener. ### `SessionListener` Informs about session related events. One can modify the session - although the modification effect depends on the session's **state**. E.g., if one -changes the ciphers *after* the key exchange (KEX) phase, then they will take effect only if the keys are re-negotiated. It is important to read the -documentation very carefully and understand at which stage each listener method is invoked and what are the repercussions of changes at that stage. +changes the ciphers *after* the key exchange (KEX) phase, then they will take effect only if the keys are re-negotiated. Furthermore, invoking some +session API(s) - event `getSomeValue` at the wrong time might yield unexpected results. It is important to read the documentation very carefully and +understand at which stage each listener method is invoked, what are the limitations and what are the repercussions of changes at that stage. In this context, it is worth mentioning that one can attach to sessions **arbitrary attributes** that can be retrieved by the user's code later on: @@ -50,6 +51,11 @@ In this context, it is worth mentioning that one can attach to sessions **arbitr sshClient/Server.addSessionListener(new SessionListener() { @Override + public void sessionEstablished(Session session) { + // examine the peer address or the connection context and set some attributes + } + + @Override public void sessionCreated(Session session) { session.setAttribute(STR_KEY, "Some string value"); session.setAttribute(LONG_KEY, 3777347L); diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/Invoker.java b/sshd-common/src/main/java/org/apache/sshd/common/util/Invoker.java index 71cbebd..1fc2115 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/Invoker.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/Invoker.java @@ -24,7 +24,7 @@ import java.util.Map; /** * The complement to the {@code Callable} interface - accepts one argument - * and possibly throws somethind + * and possibly throws something * * @param <ARG> Argument type * @param <RET> Return type @@ -34,7 +34,19 @@ import java.util.Map; public interface Invoker<ARG, RET> { RET invoke(ARG arg) throws Throwable; - static <ARG> Invoker<ARG, Void> wrapAll(Collection<? extends Invoker<? super ARG, ?>> invokers) { + /** + * Wraps a bunch of {@link Invoker}-s that return no value into one that + * invokes them in the same <U>order</U> as they appear. <B>Note:</B> + * <U>all</U> invokers are used and any thrown exceptions are <U>accumulated</U> + * and thrown as a single exception at the end of invoking all of them. + * + * @param <ARG> The argument type + * @param invokers The invokers to wrap - ignored if {@code null}/empty + * @return The wrapper + * @see #invokeAll(Object, Collection) invokeAll + */ + static <ARG> Invoker<ARG, Void> wrapAll( + Collection<? extends Invoker<? super ARG, ?>> invokers) { return arg -> { invokeAll(arg, invokers); return null; @@ -51,7 +63,9 @@ public interface Invoker<ARG, RET> { * (also ignores {@code null} members) * @throws Throwable If invocation failed */ - static <ARG> void invokeAll(ARG arg, Collection<? extends Invoker<? super ARG, ?>> invokers) throws Throwable { + static <ARG> void invokeAll( + ARG arg, Collection<? extends Invoker<? super ARG, ?>> invokers) + throws Throwable { if (GenericUtils.isEmpty(invokers)) { return; } @@ -74,9 +88,21 @@ public interface Invoker<ARG, RET> { } } - static <ARG> Invoker<ARG, Void> wrapFirst(Collection<? extends Invoker<? super ARG, ?>> invokers) { + /** + * Wraps a bunch of {@link Invoker}-s that return no value into one that + * invokes them in the same <U>order</U> as they appear. <B>Note:</B> + * stops when <U>first</U> invoker throws an exception (otherwise invokes all) + * + * @param <ARG> The argument type + * @param invokers The invokers to wrap - ignored if {@code null}/empty + * @return The wrapper + * @see #invokeTillFirstFailure(Object, Collection) invokeTillFirstFailure + */ + static <ARG> Invoker<ARG, Void> wrapFirst( + Collection<? extends Invoker<? super ARG, ?>> invokers) { return arg -> { - Map.Entry<Invoker<? super ARG, ?>, Throwable> result = invokeTillFirstFailure(arg, invokers); + Map.Entry<Invoker<? super ARG, ?>, Throwable> result = + invokeTillFirstFailure(arg, invokers); if (result != null) { throw result.getValue(); } @@ -94,7 +120,9 @@ public interface Invoker<ARG, RET> { * @return A {@link SimpleImmutableEntry} representing the <U>first</U> failed * invocation - {@code null} if all were successful (or none invoked). */ - static <ARG> SimpleImmutableEntry<Invoker<? super ARG, ?>, Throwable> invokeTillFirstFailure(ARG arg, Collection<? extends Invoker<? super ARG, ?>> invokers) { + static <ARG> SimpleImmutableEntry<Invoker<? super ARG, ?>, Throwable> + invokeTillFirstFailure( + ARG arg, Collection<? extends Invoker<? super ARG, ?>> invokers) { if (GenericUtils.isEmpty(invokers)) { return null; } diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java index 285d2a1..38d854c 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java @@ -43,7 +43,6 @@ import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.common.AttributeRepository; import org.apache.sshd.common.FactoryManager; import org.apache.sshd.common.NamedResource; -import org.apache.sshd.common.PropertyResolverUtils; import org.apache.sshd.common.RuntimeSshException; import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.SshException; @@ -96,12 +95,12 @@ public abstract class AbstractClientSession extends AbstractSession implements C protected AbstractClientSession(ClientFactoryManager factoryManager, IoSession ioSession) { super(false, factoryManager, ioSession); - sendImmediateClientIdentification = PropertyResolverUtils.getBooleanProperty( - factoryManager, ClientFactoryManager.SEND_IMMEDIATE_IDENTIFICATION, + sendImmediateClientIdentification = this.getBooleanProperty( + ClientFactoryManager.SEND_IMMEDIATE_IDENTIFICATION, ClientFactoryManager.DEFAULT_SEND_IMMEDIATE_IDENTIFICATION); - sendImmediateKexInit = PropertyResolverUtils.getBooleanProperty( - factoryManager, ClientFactoryManager.SEND_IMMEDIATE_KEXINIT, - ClientFactoryManager.DEFAULT_SEND_KEXINIT); + sendImmediateKexInit = this.getBooleanProperty( + ClientFactoryManager.SEND_IMMEDIATE_KEXINIT, + ClientFactoryManager.DEFAULT_SEND_KEXINIT); identitiesProvider = AuthenticationIdentitiesProvider.wrapIdentities(identities); connectionContext = (AttributeRepository) ioSession.getAttribute(AttributeRepository.class); diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java index d322f9d..e93d024 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/SessionListener.java @@ -35,6 +35,20 @@ public interface SessionListener extends SshdEventListener { } /** + * An initial session connection has been established - <B>Caveat emptor:</B> + * the main difference between this callback and {@link #sessionCreated(Session)} + * is that when this callback is called, the session is not yet fully initialized + * so not all API(s) will respond as expected. The main purpose of this callback + * is to allow the user to customize some session properties based on the peer's + * address and/or any provided connection context. + * + * @param session The established {@code Session} + */ + default void sessionEstablished(Session session) { + // ignored + } + + /** * A new session just been created * * @param session The created {@link Session} @@ -64,7 +78,8 @@ public interface SessionListener extends SshdEventListener { * @param serverProposal The server proposal options (un-modifiable) */ default void sessionNegotiationStart(Session session, - Map<KexProposalOption, String> clientProposal, Map<KexProposalOption, String> serverProposal) { + Map<KexProposalOption, String> clientProposal, + Map<KexProposalOption, String> serverProposal) { // ignored } @@ -79,8 +94,10 @@ public interface SessionListener extends SshdEventListener { * @param reason Negotiation end reason - {@code null} if successful */ default void sessionNegotiationEnd(Session session, - Map<KexProposalOption, String> clientProposal, Map<KexProposalOption, String> serverProposal, - Map<KexProposalOption, String> negotiatedOptions, Throwable reason) { + Map<KexProposalOption, String> clientProposal, + Map<KexProposalOption, String> serverProposal, + Map<KexProposalOption, String> negotiatedOptions, + Throwable reason) { // ignored } 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 5144e95..27e87a8 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 @@ -230,6 +230,16 @@ public abstract class AbstractSession extends SessionHelper { sessionListenerProxy = EventListenerUtils.proxyWrapper(SessionListener.class, loader, sessionListeners); channelListenerProxy = EventListenerUtils.proxyWrapper(ChannelListener.class, loader, channelListeners); tunnelListenerProxy = EventListenerUtils.proxyWrapper(PortForwardingEventListener.class, loader, tunnelListeners); + + try { + signalSessionEstablished(ioSession); + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new RuntimeSshException(e); + } + } } @Override @@ -2040,7 +2050,8 @@ public abstract class AbstractSession extends SessionHelper { } /** - * @param seed The result of the KEXINIT handshake - required for correct session key establishment + * @param seed The result of the KEXINIT handshake - required + * for correct session key establishment */ protected abstract void setKexSeed(byte... seed); @@ -2052,7 +2063,8 @@ public abstract class AbstractSession extends SessionHelper { * @see #getFactoryManager() * @see #resolveAvailableSignaturesProposal(FactoryManager) */ - protected String resolveAvailableSignaturesProposal() throws IOException, GeneralSecurityException { + protected String resolveAvailableSignaturesProposal() + throws IOException, GeneralSecurityException { return resolveAvailableSignaturesProposal(getFactoryManager()); } @@ -2064,7 +2076,7 @@ public abstract class AbstractSession extends SessionHelper { * @throws GeneralSecurityException If failed to generate the keys */ protected abstract String resolveAvailableSignaturesProposal(FactoryManager manager) - throws IOException, GeneralSecurityException; + throws IOException, GeneralSecurityException; /** * Indicates the the key exchange is completed and the exchanged keys @@ -2091,7 +2103,9 @@ public abstract class AbstractSession extends SessionHelper { return seed; } - protected abstract void receiveKexInit(Map<KexProposalOption, String> proposal, byte[] seed) throws IOException; + protected abstract void receiveKexInit( + Map<KexProposalOption, String> proposal, byte[] seed) + throws IOException; /** * Retrieve the SSH session from the I/O session. If the session has not been attached, @@ -2102,7 +2116,8 @@ public abstract class AbstractSession extends SessionHelper { * @see #getSession(IoSession, boolean) * @throws MissingAttachedSessionException if no attached SSH session */ - public static AbstractSession getSession(IoSession ioSession) throws MissingAttachedSessionException { + public static AbstractSession getSession(IoSession ioSession) + throws MissingAttachedSessionException { return getSession(ioSession, false); } @@ -2134,7 +2149,8 @@ public abstract class AbstractSession extends SessionHelper { * @return the session attached to the I/O session or {@code null} * @throws MissingAttachedSessionException if no attached session and <tt>allowNull=false</tt> */ - public static AbstractSession getSession(IoSession ioSession, boolean allowNull) throws MissingAttachedSessionException { + public static AbstractSession getSession(IoSession ioSession, boolean allowNull) + throws MissingAttachedSessionException { AbstractSession session = (AbstractSession) ioSession.getAttribute(SESSION); if ((session == null) && (!allowNull)) { throw new MissingAttachedSessionException("No session attached to " + ioSession); diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java index 55324a5..b9ed2f1 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java @@ -520,6 +520,36 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements return writeFuture; } + protected void signalSessionEstablished(IoSession ioSession) throws Exception { + try { + invokeSessionSignaller(l -> { + signalSessionEstablished(l); + return null; + }); + } catch (Throwable err) { + Throwable e = GenericUtils.peelException(err); + if (log.isDebugEnabled()) { + log.debug("Failed ({}) to announce session={} established: {}", + e.getClass().getSimpleName(), ioSession, e.getMessage()); + } + if (log.isTraceEnabled()) { + log.trace("Session=" + ioSession + " establish failure details", e); + } + if (e instanceof Exception) { + throw (Exception) e; + } else { + throw new RuntimeSshException(e); + } + } + } + + protected void signalSessionEstablished(SessionListener listener) { + if (listener == null) { + return; + } + listener.sessionEstablished(this); + } + protected void signalSessionCreated(IoSession ioSession) throws Exception { try { invokeSessionSignaller(l -> {