Repository: mina-sshd Updated Branches: refs/heads/master f8f1e0872 -> 7df595887
[SSHD-688] Attempt to send the welcome banner sooner in the authentication protocol Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/7df59588 Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/7df59588 Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/7df59588 Branch: refs/heads/master Commit: 7df59588718dccc2a2451d5514449e82edc17ae7 Parents: f8f1e08 Author: Lyor Goldstein <lyor.goldst...@gmail.com> Authored: Wed Aug 17 21:49:12 2016 +0300 Committer: Lyor Goldstein <lyor.goldst...@gmail.com> Committed: Wed Aug 17 21:49:12 2016 +0300 ---------------------------------------------------------------------- .../sshd/common/PropertyResolverUtils.java | 56 +++++++ .../java/org/apache/sshd/common/Service.java | 1 - .../common/session/helpers/AbstractSession.java | 5 +- .../server/ServerAuthenticationManager.java | 54 +++++++ .../sshd/server/ServerFactoryManager.java | 42 ------ .../java/org/apache/sshd/server/SshServer.java | 8 +- .../sshd/server/auth/WelcomeBannerPhase.java | 53 +++++++ .../server/session/AbstractServerSession.java | 23 ++- .../server/session/ServerUserAuthService.java | 110 ++++++++++---- .../java/org/apache/sshd/WelcomeBannerTest.java | 129 ----------------- .../sshd/common/PropertyResolverUtilsTest.java | 50 ++++++- .../sshd/common/auth/AuthenticationTest.java | 23 +-- .../common/auth/SinglePublicKeyAuthTest.java | 4 +- .../server/auth/WelcomeBannerPhaseTest.java | 135 +++++++++++++++++ .../sshd/server/auth/WelcomeBannerTest.java | 145 +++++++++++++++++++ 15 files changed, 612 insertions(+), 226 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java index 903a796..40b2510 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java @@ -19,7 +19,9 @@ package org.apache.sshd.common; +import java.util.Collection; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Objects; import org.apache.sshd.common.util.GenericUtils; @@ -136,6 +138,60 @@ public final class PropertyResolverUtils { } } + /** + * Converts an enumerated configuration value: + * <UL> + * <P><LI> + * If value is {@code null} then return {@code null} + * </LI></P> + * + * <P><LI> + * If value already of the expected type then simply + * cast and return it. + * </LI></P> + * + * <P><LI> + * If value is a {@link CharSequence} then convert it + * to a string and look for a matching enumerated value + * name - case <U>insensitive</U>. + * </LI></P>> + * </UL> + * + * @param <E> Type of enumerated value + * @param enumType The enumerated class type + * @param value The configured value - ignored if {@code null} + * @param failIfNoMatch Whether to fail if no matching name found + * @param available The available values to compare the name + * @return The matching enumerated value - {@code null} if no match found + * @throws IllegalArgumentException If value is neither {@code null}, + * nor the enumerated type nor a {@link CharSequence} + * @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) { + if (value == null) { + return null; + } else if (enumType.isInstance(value)) { + return enumType.cast(value); + } else if (value instanceof CharSequence) { + String name = value.toString(); + if (GenericUtils.size(available) > 0) { + for (E v : available) { + if (name.equalsIgnoreCase(v.name())) { + return v; + } + } + } + + if (failIfNoMatch) { + throw new NoSuchElementException("No match found for " + enumType.getSimpleName() + "[" + name + "]"); + } + + return null; + } else { + throw new IllegalArgumentException("Bad value type for enum conversion: " + value.getClass().getSimpleName()); + } + } public static Object updateProperty(PropertyResolver resolver, String name, long value) { return updateProperty(resolver.getProperties(), name, value); } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/main/java/org/apache/sshd/common/Service.java ---------------------------------------------------------------------- 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 35bc7c0..8807c87 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 @@ -41,5 +41,4 @@ public interface Service extends Closeable { * @throws Exception If failed to process the command */ void process(int cmd, Buffer buffer) throws Exception; - } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java ---------------------------------------------------------------------- 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 ad663e4..acf8e70 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 @@ -665,7 +665,7 @@ public abstract class AbstractSession extends AbstractKexFactoryManager implemen handleServiceRequest(buffer.getString(), buffer); } - protected void handleServiceRequest(String serviceName, Buffer buffer) throws Exception { + protected boolean handleServiceRequest(String serviceName, Buffer buffer) throws Exception { if (log.isDebugEnabled()) { log.debug("handleServiceRequest({}) SSH_MSG_SERVICE_REQUEST '{}'", this, serviceName); } @@ -682,7 +682,7 @@ public abstract class AbstractSession extends AbstractKexFactoryManager implemen log.trace("handleServiceRequest(" + this + ") service=" + serviceName + " rejection details", e); } disconnect(SshConstants.SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE, "Bad service request: " + serviceName); - return; + return false; } if (log.isDebugEnabled()) { @@ -692,6 +692,7 @@ public abstract class AbstractSession extends AbstractKexFactoryManager implemen Buffer response = createBuffer(SshConstants.SSH_MSG_SERVICE_ACCEPT, Byte.SIZE + GenericUtils.length(serviceName)); response.putString(serviceName); writePacket(response); + return true; } protected void handleServiceAccept(Buffer buffer) throws Exception { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/main/java/org/apache/sshd/server/ServerAuthenticationManager.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/ServerAuthenticationManager.java b/sshd-core/src/main/java/org/apache/sshd/server/ServerAuthenticationManager.java index 0d8a6e8..b1978e1 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/ServerAuthenticationManager.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/ServerAuthenticationManager.java @@ -31,6 +31,7 @@ import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.server.auth.BuiltinUserAuthFactories; import org.apache.sshd.server.auth.UserAuth; +import org.apache.sshd.server.auth.WelcomeBannerPhase; import org.apache.sshd.server.auth.gss.GSSAuthenticator; import org.apache.sshd.server.auth.gss.UserAuthGSSFactory; import org.apache.sshd.server.auth.hostbased.HostBasedAuthenticator; @@ -60,6 +61,59 @@ public interface ServerAuthenticationManager { int DEFAULT_MAX_AUTH_REQUESTS = 20; /** + * Key used to retrieve the value of welcome banner that will be displayed + * when a user connects to the server. If {@code null}/empty then no banner + * will be sent. + * @see <A HREF="https://www.ietf.org/rfc/rfc4252.txt">RFC-4252 section 5.4</A> + */ + String WELCOME_BANNER = "welcome-banner"; + + /** + * Special value that can be set for the {@link #WELCOME_BANNER} property + * indicating that the server should generate a banner consisting of the + * random art of the server's keys (if any are provided). If no server + * keys are available, then no banner will be sent + */ + String AUTO_WELCOME_BANNER_VALUE = "#auto-welcome-banner"; + + /** + * Key used to denote the language code for the welcome banner (if such + * a banner is configured). If not set, then {@link ServerAuthenticationManager#DEFAULT_WELCOME_BANNER_LANGUAGE} + * is used + */ + String WELCOME_BANNER_LANGUAGE = "welcome-banner-language"; + + /** + * Default value for {@link #WELCOME_BANNER_LANGUAGE} is not overwritten + */ + String DEFAULT_WELCOME_BANNER_LANGUAGE = "en"; + + /** + * The {@link WelcomeBannerPhase} value - either as an enum or + * a string + */ + String WELCOME_BANNER_PHASE = "welcome-banner-phase"; + + /** + * Default value for {@link #WELCOME_BANNER_PHASE} if none specified + */ + WelcomeBannerPhase DEFAULT_BANNER_PHASE = WelcomeBannerPhase.IMMEDIATE; + + /** + * This key is used when configuring multi-step authentications. + * The value needs to be a blank separated list of comma separated list + * of authentication method names. + * For example, an argument of + * <code>publickey,password publickey,keyboard-interactive</code> + * would require the user to complete public key authentication, + * followed by either password or keyboard interactive authentication. + * Only methods that are next in one or more lists are offered at each + * stage, so for this example, it would not be possible to attempt + * password or keyboard-interactive authentication before public key. + */ + String AUTH_METHODS = "auth-methods"; + + /** * Retrieve the list of named factories for <code>UserAuth</code> objects. * * @return a list of named <code>UserAuth</code> factories, never {@code null}/empty http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java index 0a488e8..a08fb30 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java @@ -67,48 +67,6 @@ public interface ServerFactoryManager String SERVER_IDENTIFICATION = "server-identification"; /** - * Key used to retrieve the value of welcome banner that will be displayed - * when a user connects to the server. If {@code null}/empty then no banner - * will be sent. - * @see <A HREF="https://www.ietf.org/rfc/rfc4252.txt">RFC-4252 section 5.4</A> - */ - String WELCOME_BANNER = "welcome-banner"; - - /** - * Special value that can be set for the {@link #WELCOME_BANNER} property - * indicating that the server should generate a banner consisting of the - * random art of the server's keys (if any are provided). If no server - * keys are available, then no banner will be sent - */ - String AUTO_WELCOME_BANNER_VALUE = "#auto-welcome-banner"; - - /** - * Key used to denote the language code for the welcome banner (if such - * a banner is configured). If not set, then {@link #DEFAULT_WELCOME_BANNER_LANGUAGE} - * is used - */ - String WELCOME_BANNER_LANGUAGE = "welcome-banner-language"; - - /** - * Default value for {@link #WELCOME_BANNER_LANGUAGE} is not overwritten - */ - String DEFAULT_WELCOME_BANNER_LANGUAGE = "en"; - - /** - * This key is used when configuring multi-step authentications. - * The value needs to be a blank separated list of comma separated list - * of authentication method names. - * For example, an argument of - * <code>publickey,password publickey,keyboard-interactive</code> - * would require the user to complete public key authentication, - * followed by either password or keyboard interactive authentication. - * Only methods that are next in one or more lists are offered at each - * stage, so for this example, it would not be possible to attempt - * password or keyboard-interactive authentication before public key. - */ - String AUTH_METHODS = "auth-methods"; - - /** * Key used to configure the timeout used when receiving a close request * on a channel to wait until the command cleanly exits after setting * an EOF on the input stream. In milliseconds. http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java b/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java index 35757c1..a6e635b 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/SshServer.java @@ -617,7 +617,7 @@ public class SshServer extends AbstractFactoryManager implements ServerFactoryMa ? null : Objects.toString(options.remove(SshConfigFileReader.VISUAL_HOST_KEY), null); if (SshConfigFileReader.parseBooleanValue(bannerOption)) { - bannerOption = ServerFactoryManager.AUTO_WELCOME_BANNER_VALUE; + bannerOption = ServerAuthenticationManager.AUTO_WELCOME_BANNER_VALUE; } } @@ -627,7 +627,7 @@ public class SshServer extends AbstractFactoryManager implements ServerFactoryMa return null; } - if (ServerFactoryManager.AUTO_WELCOME_BANNER_VALUE.equalsIgnoreCase(bannerOption)) { + if (ServerAuthenticationManager.AUTO_WELCOME_BANNER_VALUE.equalsIgnoreCase(bannerOption)) { banner = KeyRandomArt.combine(' ', server.getKeyPairProvider()); } else { Path path = Paths.get(bannerOption); @@ -642,9 +642,9 @@ public class SshServer extends AbstractFactoryManager implements ServerFactoryMa } if (GenericUtils.length(banner) > 0) { - PropertyResolverUtils.updateProperty(server, ServerFactoryManager.WELCOME_BANNER, banner); + PropertyResolverUtils.updateProperty(server, ServerAuthenticationManager.WELCOME_BANNER, banner); } - return PropertyResolverUtils.getString(server, ServerFactoryManager.WELCOME_BANNER); + return PropertyResolverUtils.getString(server, ServerAuthenticationManager.WELCOME_BANNER); } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/main/java/org/apache/sshd/server/auth/WelcomeBannerPhase.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/WelcomeBannerPhase.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/WelcomeBannerPhase.java new file mode 100644 index 0000000..66becc1 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/WelcomeBannerPhase.java @@ -0,0 +1,53 @@ +/* + * 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.server.auth; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +/** + * Used to indicate at which authentication phase to send the welcome + * banner (if any configured) + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + * @see <a href="https://tools.ietf.org/html/rfc4252#section-5.4">RFC-4252 section 5.4</a> + */ +public enum WelcomeBannerPhase { + /** Immediately after receiving "ssh-userauth" request */ + IMMEDIATE, + /** On first {@code SSH_MSG_USERAUTH_REQUEST} */ + FIRST_REQUEST, + /** On first {@code SSH_MSG_USERAUTH_XXX} extension command */ + FIRST_AUTHCMD, + /** On first {@code SSH_MSG_USERAUTH_FAILURE} */ + FIRST_FAILURE, + /** After user successfully authenticates */ + POST_SUCCESS, + /** + * Do not send a welcome banner even if one is configured. <B>Note:</B> + * this option is useful when a global welcome banner has been configured + * but we want to disable it for a specific session. + */ + NEVER; + + public static final Set<WelcomeBannerPhase> VALUES = + Collections.unmodifiableSet(EnumSet.allOf(WelcomeBannerPhase.class)); +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java index 46879ad..2ec01da 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/session/AbstractServerSession.java @@ -35,6 +35,7 @@ import org.apache.sshd.common.RuntimeSshException; import org.apache.sshd.common.ServiceFactory; import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.SshException; +import org.apache.sshd.common.auth.AbstractUserAuthServiceFactory; import org.apache.sshd.common.future.SshFutureListener; import org.apache.sshd.common.io.IoService; import org.apache.sshd.common.io.IoSession; @@ -49,6 +50,7 @@ import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; import org.apache.sshd.server.ServerFactoryManager; import org.apache.sshd.server.auth.UserAuth; +import org.apache.sshd.server.auth.WelcomeBannerPhase; import org.apache.sshd.server.auth.gss.GSSAuthenticator; import org.apache.sshd.server.auth.hostbased.HostBasedAuthenticator; import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator; @@ -183,9 +185,28 @@ public abstract class AbstractServerSession extends AbstractSession implements S } @Override + protected boolean handleServiceRequest(String serviceName, Buffer buffer) throws Exception { + boolean started = super.handleServiceRequest(serviceName, buffer); + if (!started) { + return false; + } + + if (AbstractUserAuthServiceFactory.DEFAULT_NAME.equals(serviceName) + && (currentService instanceof ServerUserAuthService)) { + ServerUserAuthService authService = (ServerUserAuthService) currentService; + if (WelcomeBannerPhase.IMMEDIATE.equals(authService.getWelcomePhase())) { + authService.sendWelcomeBanner(this); + } + } + + return true; + } + + @Override public void startService(String name) throws Exception { + FactoryManager factoryManager = getFactoryManager(); currentService = ServiceFactory.Utils.create( - getFactoryManager().getServiceFactories(), + factoryManager.getServiceFactories(), ValidateUtils.checkNotNullAndNotEmpty(name, "No service name"), this); /* http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java ---------------------------------------------------------------------- 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 0e0186e..b0593f6 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 @@ -18,10 +18,12 @@ */ package org.apache.sshd.server.session; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.sshd.common.Factory; import org.apache.sshd.common.NamedFactory; @@ -31,6 +33,7 @@ import org.apache.sshd.common.Service; import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.SshException; import org.apache.sshd.common.config.keys.KeyRandomArt; +import org.apache.sshd.common.io.IoWriteFuture; import org.apache.sshd.common.session.Session; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; @@ -40,13 +43,15 @@ import org.apache.sshd.server.ServerAuthenticationManager; import org.apache.sshd.server.ServerFactoryManager; import org.apache.sshd.server.auth.UserAuth; import org.apache.sshd.server.auth.UserAuthNoneFactory; +import org.apache.sshd.server.auth.WelcomeBannerPhase; /** * @author <a href="mailto:d...@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 WelcomeBannerPhase welcomePhase; private List<NamedFactory<UserAuth>> userAuthFactories; private List<List<String>> authMethods; private String authUserName; @@ -57,13 +62,16 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, private int maxAuthRequests; private int nbAuthRequests; - public ServerUserAuthService(Session s) throws SshException { + public ServerUserAuthService(Session s) throws IOException { ValidateUtils.checkTrue(s instanceof ServerSession, "Server side service used on client side"); if (s.isAuthenticated()) { throw new SshException("Session already authenticated"); } serverSession = (ServerSession) s; + Object phase = PropertyResolverUtils.getObject(s, ServerAuthenticationManager.WELCOME_BANNER_PHASE); + phase = PropertyResolverUtils.toEnum(WelcomeBannerPhase.class, phase, true, WelcomeBannerPhase.VALUES); + welcomePhase = (phase == null) ? ServerAuthenticationManager.DEFAULT_BANNER_PHASE : (WelcomeBannerPhase) phase; maxAuthRequests = PropertyResolverUtils.getIntProperty(s, ServerAuthenticationManager.MAX_AUTH_REQUESTS, ServerAuthenticationManager.DEFAULT_MAX_AUTH_REQUESTS); List<NamedFactory<UserAuth>> factories = ValidateUtils.checkNotNullAndNotEmpty( @@ -72,7 +80,7 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, // Get authentication methods authMethods = new ArrayList<>(); - String mths = PropertyResolverUtils.getString(s, ServerFactoryManager.AUTH_METHODS); + String mths = PropertyResolverUtils.getString(s, ServerAuthenticationManager.AUTH_METHODS); if (GenericUtils.isEmpty(mths)) { for (NamedFactory<UserAuth> uaf : factories) { authMethods.add(new ArrayList<>(Collections.singletonList(uaf.getName()))); @@ -101,6 +109,10 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, } } + public WelcomeBannerPhase getWelcomePhase() { + return welcomePhase; + } + @Override public void start() { // do nothing @@ -122,6 +134,10 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, ServerSession session = getServerSession(); if (cmd == SshConstants.SSH_MSG_USERAUTH_REQUEST) { + if (WelcomeBannerPhase.FIRST_REQUEST.equals(getWelcomePhase())) { + sendWelcomeBanner(session); + } + if (currentAuth != null) { try { currentAuth.destroy(); @@ -180,6 +196,10 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, } } } else { + if (WelcomeBannerPhase.FIRST_AUTHCMD.equals(getWelcomePhase())) { + sendWelcomeBanner(session); + } + if (this.currentAuth == null) { // This should not happen throw new IllegalStateException("No current authentication mechanism for cmd=" + SshConstants.getCommandMessageName(cmd)); @@ -249,34 +269,8 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, } } - /* - * TODO check if we can send the banner sooner. According to RFC-4252 section 5.4: - * - * The SSH server may send an SSH_MSG_USERAUTH_BANNER message at any - * time after this authentication protocol starts and before - * authentication is successful. This message contains text to be - * displayed to the client user before authentication is attempted. - */ - String welcomeBanner = PropertyResolverUtils.getString(session, ServerFactoryManager.WELCOME_BANNER); - if ((GenericUtils.length(welcomeBanner) > 0) - && ServerFactoryManager.AUTO_WELCOME_BANNER_VALUE.equalsIgnoreCase(welcomeBanner)) { - welcomeBanner = KeyRandomArt.combine(' ', session.getKeyPairProvider()); - } - - if (GenericUtils.length(welcomeBanner) > 0) { - String lang = PropertyResolverUtils.getStringProperty(session, - ServerFactoryManager.WELCOME_BANNER_LANGUAGE, - ServerFactoryManager.DEFAULT_WELCOME_BANNER_LANGUAGE); - buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_BANNER, - welcomeBanner.length() + GenericUtils.length(lang) + Long.SIZE); - buffer.putString(welcomeBanner); - buffer.putString(lang); - - if (log.isDebugEnabled()) { - log.debug("handleAuthenticationSuccess({}@{}) send banner (length={}, lang={})", - username, session, welcomeBanner.length(), lang); - } - session.writePacket(buffer); + if (WelcomeBannerPhase.POST_SUCCESS.equals(getWelcomePhase())) { + sendWelcomeBanner(session); } buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_SUCCESS, Byte.SIZE); @@ -316,8 +310,12 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, } protected void handleAuthenticationFailure(int cmd, Buffer buffer) throws Exception { - String username = (currentAuth == null) ? null : currentAuth.getUsername(); ServerSession session = getServerSession(); + if (WelcomeBannerPhase.FIRST_FAILURE.equals(getWelcomePhase())) { + sendWelcomeBanner(session); + } + + String username = (currentAuth == null) ? null : currentAuth.getUsername(); if (log.isDebugEnabled()) { log.debug("handleAuthenticationFailure({}@{}) {}", username, session, SshConstants.getCommandMessageName(cmd)); @@ -355,6 +353,54 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, } } + /** + * Sends the welcome banner (if any configured) and if not already invoked + * + * @param session The {@link ServerSession} to send the welcome banner to + * @return The sent welcome banner {@link IoWriteFuture} - {@code null} if none sent + * @throws IOException If failed to send the banner + */ + public IoWriteFuture sendWelcomeBanner(ServerSession session) throws IOException { + if (welcomeSent.getAndSet(true)) { + if (log.isDebugEnabled()) { + log.debug("sendWelcomeBanner({}) already sent", session); + } + return null; + } + + String welcomeBanner = PropertyResolverUtils.getString(session, ServerAuthenticationManager.WELCOME_BANNER); + if ((GenericUtils.length(welcomeBanner) > 0) + && ServerAuthenticationManager.AUTO_WELCOME_BANNER_VALUE.equalsIgnoreCase(welcomeBanner)) { + try { + welcomeBanner = KeyRandomArt.combine(' ', session.getKeyPairProvider()); + } catch (Exception e) { + if (e instanceof IOException) { + throw (IOException) e; + } + + throw new IOException(e); + } + } + + if (GenericUtils.isEmpty(welcomeBanner)) { + return null; + } + + String lang = PropertyResolverUtils.getStringProperty(session, + ServerAuthenticationManager.WELCOME_BANNER_LANGUAGE, + ServerAuthenticationManager.DEFAULT_WELCOME_BANNER_LANGUAGE); + Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_BANNER, + welcomeBanner.length() + GenericUtils.length(lang) + Long.SIZE); + buffer.putString(welcomeBanner); + buffer.putString(lang); + + if (log.isDebugEnabled()) { + log.debug("sendWelcomeBanner({}) send banner (length={}, lang={})", + session, welcomeBanner.length(), lang); + } + return session.writePacket(buffer); + } + public ServerFactoryManager getFactoryManager() { return serverSession.getFactoryManager(); } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/test/java/org/apache/sshd/WelcomeBannerTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/WelcomeBannerTest.java b/sshd-core/src/test/java/org/apache/sshd/WelcomeBannerTest.java deleted file mode 100644 index 508b224..0000000 --- a/sshd-core/src/test/java/org/apache/sshd/WelcomeBannerTest.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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; - -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -import org.apache.sshd.client.SshClient; -import org.apache.sshd.client.auth.keyboard.UserInteraction; -import org.apache.sshd.client.session.ClientSession; -import org.apache.sshd.common.PropertyResolverUtils; -import org.apache.sshd.common.config.keys.KeyRandomArt; -import org.apache.sshd.common.keyprovider.KeyPairProvider; -import org.apache.sshd.server.ServerFactoryManager; -import org.apache.sshd.server.SshServer; -import org.apache.sshd.util.test.BaseTestSupport; -import org.junit.After; -import org.junit.Before; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; - -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class WelcomeBannerTest extends BaseTestSupport { - private SshServer sshd; - private int port; - - public WelcomeBannerTest() { - super(); - } - - @Before - public void setUp() throws Exception { - sshd = setupTestServer(); - sshd.start(); - port = sshd.getPort(); - } - - @After - public void tearDown() throws Exception { - if (sshd != null) { - sshd.stop(true); - } - } - - @Test - public void testSimpleBanner() throws Exception { - final String expectedWelcome = "Welcome to SSHD WelcomeBannerTest"; - PropertyResolverUtils.updateProperty(sshd, ServerFactoryManager.WELCOME_BANNER, expectedWelcome); - testBanner(expectedWelcome); - } - - @Test // see SSHD-686 - public void testAutoGeneratedBanner() throws Exception { - KeyPairProvider keys = sshd.getKeyPairProvider(); - PropertyResolverUtils.updateProperty(sshd, ServerFactoryManager.WELCOME_BANNER, ServerFactoryManager.AUTO_WELCOME_BANNER_VALUE); - testBanner(KeyRandomArt.combine(' ', keys)); - } - - private void testBanner(String expectedWelcome) throws Exception { - try (SshClient client = setupTestClient()) { - final AtomicReference<String> welcomeHolder = new AtomicReference<>(null); - final AtomicReference<ClientSession> sessionHolder = new AtomicReference<>(null); - client.setUserInteraction(new UserInteraction() { - @Override - public boolean isInteractionAllowed(ClientSession session) { - return true; - } - - @Override - public void serverVersionInfo(ClientSession session, List<String> lines) { - validateSession("serverVersionInfo", session); - } - - @Override - public void welcome(ClientSession session, String banner, String lang) { - validateSession("welcome", session); - assertNull("Multiple banner invocations", welcomeHolder.getAndSet(banner)); - } - - @Override - public String[] interactive(ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) { - validateSession("interactive", session); - return null; - } - - @Override - public String getUpdatedPassword(ClientSession clientSession, String prompt, String lang) { - throw new UnsupportedOperationException("Unexpected call"); - } - - private void validateSession(String phase, ClientSession session) { - ClientSession prev = sessionHolder.getAndSet(session); - if (prev != null) { - assertSame("Mismatched " + phase + " client session", prev, session); - } - } - }); - client.start(); - - try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { - session.addPasswordIdentity(getCurrentTestName()); - session.auth().verify(5L, TimeUnit.SECONDS); - assertSame("Mismatched sessions", session, sessionHolder.get()); - } finally { - client.stop(); - } - - assertEquals("Mismatched banner", expectedWelcome, welcomeHolder.get()); - } - } -} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/test/java/org/apache/sshd/common/PropertyResolverUtilsTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/common/PropertyResolverUtilsTest.java b/sshd-core/src/test/java/org/apache/sshd/common/PropertyResolverUtilsTest.java index 4879ee5..d83a9b6 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/PropertyResolverUtilsTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/PropertyResolverUtilsTest.java @@ -19,8 +19,13 @@ package org.apache.sshd.common; +import java.util.Collection; +import java.util.Date; +import java.util.EnumSet; import java.util.Map; +import java.util.NoSuchElementException; import java.util.TreeMap; +import java.util.concurrent.TimeUnit; import org.apache.sshd.common.session.Session; import org.apache.sshd.common.util.GenericUtils; @@ -202,13 +207,54 @@ public class PropertyResolverUtilsTest extends BaseTestSupport { } } + @Test + public void testToEnumFromString() { + Collection<TimeUnit> units = EnumSet.allOf(TimeUnit.class); + for (TimeUnit expected : units) { + String name = expected.name(); + for (int index = 1, count = name.length(); index <= count; index++) { + TimeUnit actual = PropertyResolverUtils.toEnum(TimeUnit.class, name, true, units); + assertSame("Mismatched instance for name=" + name, expected, actual); + name = shuffleCase(name); + } + } + } + + @Test + public void testToEnumFromEnum() { + Collection<TimeUnit> units = EnumSet.allOf(TimeUnit.class); + for (TimeUnit expected : units) { + TimeUnit actual = PropertyResolverUtils.toEnum(TimeUnit.class, expected, true, null); + assertSame("Mismatched resolved value", expected, actual); + } + } + + @Test + public void testToEnumFromNonString() { + Collection<TimeUnit> units = EnumSet.allOf(TimeUnit.class); + for (Object value : new Object[]{this, getClass(), new Date()}) { + try { + TimeUnit unit = PropertyResolverUtils.toEnum(TimeUnit.class, value, false, units); + fail("Unexpected success for value=" + value + ": " + unit); + } catch (IllegalArgumentException e) { + // expected - ignored + } + } + } + + @Test(expected = NoSuchElementException.class) + public void testToEnumNoMatchFound() { + TimeUnit result = PropertyResolverUtils.toEnum(TimeUnit.class, getCurrentTestName(), true, EnumSet.allOf(TimeUnit.class)); + fail("Unexpected success: " + result); + } + private Session createMockSession() { - Map<String, Object> managerProps = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER); + Map<String, Object> managerProps = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); FactoryManager manager = Mockito.mock(FactoryManager.class); Mockito.when(manager.getProperties()).thenReturn(managerProps); Mockito.when(manager.getParentPropertyResolver()).thenReturn(null); - Map<String, Object> sessionProps = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER); + Map<String, Object> sessionProps = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); Session session = Mockito.mock(Session.class); Mockito.when(session.getUsername()).thenReturn(getCurrentTestName()); Mockito.when(session.getFactoryManager()).thenReturn(manager); http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java index 8e29598..7fefdb3 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java @@ -59,6 +59,7 @@ import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.apache.sshd.server.ServerAuthenticationManager; import org.apache.sshd.server.ServerFactoryManager; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.hostbased.HostBasedAuthenticator; @@ -163,7 +164,7 @@ public class AuthenticationTest extends BaseTestSupport { public boolean authenticate(String username, String password, ServerSession session) throws PasswordChangeRequiredException { if (attemptsCount.incrementAndGet() == 1) { - throw new PasswordChangeRequiredException(attemptsCount.toString(), getCurrentTestName(), ServerFactoryManager.DEFAULT_WELCOME_BANNER_LANGUAGE); + throw new PasswordChangeRequiredException(attemptsCount.toString(), getCurrentTestName(), ServerAuthenticationManager.DEFAULT_WELCOME_BANNER_LANGUAGE); } return delegate.authenticate(username, password, session); @@ -191,7 +192,7 @@ public class AuthenticationTest extends BaseTestSupport { } } )); - PropertyResolverUtils.updateProperty(sshd, ServerFactoryManager.AUTH_METHODS, UserAuthPasswordFactory.NAME); + PropertyResolverUtils.updateProperty(sshd, ServerAuthenticationManager.AUTH_METHODS, UserAuthPasswordFactory.NAME); try (SshClient client = setupTestClient()) { final AtomicInteger updatesCount = new AtomicInteger(0); @@ -209,7 +210,7 @@ public class AuthenticationTest extends BaseTestSupport { @Override public String getUpdatedPassword(ClientSession session, String prompt, String lang) { assertEquals("Mismatched prompt", getCurrentTestName(), prompt); - assertEquals("Mismatched language", ServerFactoryManager.DEFAULT_WELCOME_BANNER_LANGUAGE, lang); + assertEquals("Mismatched language", ServerAuthenticationManager.DEFAULT_WELCOME_BANNER_LANGUAGE, lang); assertEquals("Unexpected repeated call", 1, updatesCount.incrementAndGet()); return getCurrentTestName(); } @@ -235,7 +236,7 @@ public class AuthenticationTest extends BaseTestSupport { }; } })); - PropertyResolverUtils.updateProperty(client, ServerFactoryManager.AUTH_METHODS, UserAuthPasswordFactory.NAME); + PropertyResolverUtils.updateProperty(client, ServerAuthenticationManager.AUTH_METHODS, UserAuthPasswordFactory.NAME); client.start(); @@ -396,7 +397,7 @@ public class AuthenticationTest extends BaseTestSupport { challenge.addPrompt(prompt, (GenericUtils.size(challenge.getPrompts()) & 0x1) != 0); } - PropertyResolverUtils.updateProperty(sshd, ServerFactoryManager.AUTH_METHODS, UserAuthKeyboardInteractiveFactory.NAME); + PropertyResolverUtils.updateProperty(sshd, ServerAuthenticationManager.AUTH_METHODS, UserAuthKeyboardInteractiveFactory.NAME); final AtomicInteger genCount = new AtomicInteger(0); final AtomicInteger authCount = new AtomicInteger(0); sshd.setKeyboardInteractiveAuthenticator(new KeyboardInteractiveAuthenticator() { @@ -422,7 +423,7 @@ public class AuthenticationTest extends BaseTestSupport { return true; } }); - PropertyResolverUtils.updateProperty(sshd, ServerFactoryManager.AUTH_METHODS, UserAuthKeyboardInteractiveFactory.NAME); + PropertyResolverUtils.updateProperty(sshd, ServerAuthenticationManager.AUTH_METHODS, UserAuthKeyboardInteractiveFactory.NAME); try (SshClient client = setupTestClient()) { final AtomicInteger interactiveCount = new AtomicInteger(0); @@ -459,7 +460,7 @@ public class AuthenticationTest extends BaseTestSupport { throw new UnsupportedOperationException("Unexpected call"); } }); - PropertyResolverUtils.updateProperty(client, ServerFactoryManager.AUTH_METHODS, UserAuthKeyboardInteractiveFactory.NAME); + PropertyResolverUtils.updateProperty(client, ServerAuthenticationManager.AUTH_METHODS, UserAuthKeyboardInteractiveFactory.NAME); client.start(); @@ -483,13 +484,13 @@ public class AuthenticationTest extends BaseTestSupport { public boolean authenticate(String username, String password, ServerSession session) throws PasswordChangeRequiredException { if (attemptsCount.incrementAndGet() == 1) { - throw new PasswordChangeRequiredException(attemptsCount.toString(), getCurrentTestName(), ServerFactoryManager.DEFAULT_WELCOME_BANNER_LANGUAGE); + throw new PasswordChangeRequiredException(attemptsCount.toString(), getCurrentTestName(), ServerAuthenticationManager.DEFAULT_WELCOME_BANNER_LANGUAGE); } return delegate.authenticate(username, password, session); } }); - PropertyResolverUtils.updateProperty(sshd, ServerFactoryManager.AUTH_METHODS, UserAuthPasswordFactory.NAME); + PropertyResolverUtils.updateProperty(sshd, ServerAuthenticationManager.AUTH_METHODS, UserAuthPasswordFactory.NAME); try (SshClient client = setupTestClient()) { final AtomicInteger updatesCount = new AtomicInteger(0); @@ -507,12 +508,12 @@ public class AuthenticationTest extends BaseTestSupport { @Override public String getUpdatedPassword(ClientSession session, String prompt, String lang) { assertEquals("Mismatched prompt", getCurrentTestName(), prompt); - assertEquals("Mismatched language", ServerFactoryManager.DEFAULT_WELCOME_BANNER_LANGUAGE, lang); + assertEquals("Mismatched language", ServerAuthenticationManager.DEFAULT_WELCOME_BANNER_LANGUAGE, lang); assertEquals("Unexpected repeated call", 1, updatesCount.incrementAndGet()); return getCurrentTestName(); } }); - PropertyResolverUtils.updateProperty(client, ServerFactoryManager.AUTH_METHODS, UserAuthPasswordFactory.NAME); + PropertyResolverUtils.updateProperty(client, ServerAuthenticationManager.AUTH_METHODS, UserAuthPasswordFactory.NAME); client.start(); http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/test/java/org/apache/sshd/common/auth/SinglePublicKeyAuthTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/SinglePublicKeyAuthTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/SinglePublicKeyAuthTest.java index d35a5c8..96039bd 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/auth/SinglePublicKeyAuthTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/SinglePublicKeyAuthTest.java @@ -31,7 +31,7 @@ import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.PropertyResolverUtils; import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.keyprovider.KeyPairProvider; -import org.apache.sshd.server.ServerFactoryManager; +import org.apache.sshd.server.ServerAuthenticationManager; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.pubkey.CachingPublicKeyAuthenticator; import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; @@ -68,7 +68,7 @@ public class SinglePublicKeyAuthTest extends BaseTestSupport { @Before public void setUp() throws Exception { sshd = setupTestServer(); - PropertyResolverUtils.updateProperty(sshd, ServerFactoryManager.AUTH_METHODS, UserAuthPublicKeyFactory.NAME); + PropertyResolverUtils.updateProperty(sshd, ServerAuthenticationManager.AUTH_METHODS, UserAuthPublicKeyFactory.NAME); sshd.setPublickeyAuthenticator(new PublickeyAuthenticator() { @SuppressWarnings("synthetic-access") @Override http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerPhaseTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerPhaseTest.java b/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerPhaseTest.java new file mode 100644 index 0000000..2541558 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerPhaseTest.java @@ -0,0 +1,135 @@ +/* + * 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.server.auth; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.auth.keyboard.UserInteraction; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.PropertyResolverUtils; +import org.apache.sshd.server.ServerAuthenticationManager; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.util.test.BaseTestSupport; +import org.apache.sshd.util.test.Utils; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests +public class WelcomeBannerPhaseTest extends BaseTestSupport { + private static SshServer sshd; + private static SshClient client; + private static int port; + + private WelcomeBannerPhase phase; + + public WelcomeBannerPhaseTest(WelcomeBannerPhase phase) { + this.phase = phase; + } + + @Parameters(name = "{0}") + public static List<Object[]> parameters() { + return parameterize(WelcomeBannerPhase.VALUES); + } + + @BeforeClass + public static void setupClientAndServer() throws Exception { + sshd = Utils.setupTestServer(WelcomeBannerPhaseTest.class); + sshd.start(); + port = sshd.getPort(); + + client = Utils.setupTestClient(WelcomeBannerPhaseTest.class); + client.start(); + } + + @AfterClass + public static void tearDownClientAndServer() throws Exception { + if (sshd != null) { + try { + sshd.stop(true); + } finally { + sshd = null; + } + } + + if (client != null) { + try { + client.stop(); + } finally { + client = null; + } + } + } + + @Test + public void testWelcomeBannerPhase() throws Exception { + PropertyResolverUtils.updateProperty(sshd, ServerAuthenticationManager.WELCOME_BANNER_PHASE, phase); + PropertyResolverUtils.updateProperty(sshd, ServerAuthenticationManager.WELCOME_BANNER, phase.name()); + + AtomicReference<String> welcomeHolder = new AtomicReference<>(null); + client.setUserInteraction(new UserInteraction() { + @Override + public boolean isInteractionAllowed(ClientSession session) { + return true; + } + + @Override + public void welcome(ClientSession session, String banner, String lang) { + assertNull("Multiple banner invocations", welcomeHolder.getAndSet(banner)); + } + + @Override + public String getUpdatedPassword(ClientSession clientSession, String prompt, String lang) { + throw new UnsupportedOperationException("Unexpected call"); + } + + @Override + public String[] interactive(ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) { + return null; + } + }); + + + try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { + session.addPasswordIdentity(getCurrentTestName()); + session.auth().verify(5L, TimeUnit.SECONDS); + } + + Object banner = welcomeHolder.getAndSet(null); + if (WelcomeBannerPhase.NEVER.equals(phase)) { + assertNull("Unexpected banner", banner); + } else { + WelcomeBannerPhase value = PropertyResolverUtils.toEnum(WelcomeBannerPhase.class, banner, false, WelcomeBannerPhase.VALUES); + assertSame("Mismatched banner value", phase, value); + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7df59588/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerTest.java b/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerTest.java new file mode 100644 index 0000000..3126b6b --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerTest.java @@ -0,0 +1,145 @@ +/* + * 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.server.auth; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.auth.keyboard.UserInteraction; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.PropertyResolverUtils; +import org.apache.sshd.common.config.keys.KeyRandomArt; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.server.ServerAuthenticationManager; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.util.test.BaseTestSupport; +import org.apache.sshd.util.test.Utils; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class WelcomeBannerTest extends BaseTestSupport { + private static SshServer sshd; + private static int port; + private static SshClient client; + + public WelcomeBannerTest() { + super(); + } + + + @BeforeClass + public static void setupClientAndServer() throws Exception { + sshd = Utils.setupTestServer(WelcomeBannerPhaseTest.class); + sshd.start(); + port = sshd.getPort(); + + client = Utils.setupTestClient(WelcomeBannerPhaseTest.class); + client.start(); + } + + @AfterClass + public static void tearDownClientAndServer() throws Exception { + if (sshd != null) { + try { + sshd.stop(true); + } finally { + sshd = null; + } + } + + if (client != null) { + try { + client.stop(); + } finally { + client = null; + } + } + } + + @Test + public void testSimpleBanner() throws Exception { + final String expectedWelcome = "Welcome to SSHD WelcomeBannerTest"; + PropertyResolverUtils.updateProperty(sshd, ServerAuthenticationManager.WELCOME_BANNER, expectedWelcome); + testBanner(expectedWelcome); + } + + @Test // see SSHD-686 + public void testAutoGeneratedBanner() throws Exception { + KeyPairProvider keys = sshd.getKeyPairProvider(); + PropertyResolverUtils.updateProperty(sshd, ServerAuthenticationManager.WELCOME_BANNER, ServerAuthenticationManager.AUTO_WELCOME_BANNER_VALUE); + testBanner(KeyRandomArt.combine(' ', keys)); + } + + private void testBanner(String expectedWelcome) throws Exception { + AtomicReference<String> welcomeHolder = new AtomicReference<>(null); + AtomicReference<ClientSession> sessionHolder = new AtomicReference<>(null); + client.setUserInteraction(new UserInteraction() { + @Override + public boolean isInteractionAllowed(ClientSession session) { + return true; + } + + @Override + public void serverVersionInfo(ClientSession session, List<String> lines) { + validateSession("serverVersionInfo", session); + } + + @Override + public void welcome(ClientSession session, String banner, String lang) { + validateSession("welcome", session); + assertNull("Multiple banner invocations", welcomeHolder.getAndSet(banner)); + } + + @Override + public String[] interactive(ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) { + validateSession("interactive", session); + return null; + } + + @Override + public String getUpdatedPassword(ClientSession clientSession, String prompt, String lang) { + throw new UnsupportedOperationException("Unexpected call"); + } + + private void validateSession(String phase, ClientSession session) { + ClientSession prev = sessionHolder.getAndSet(session); + if (prev != null) { + assertSame("Mismatched " + phase + " client session", prev, session); + } + } + }); + + try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { + session.addPasswordIdentity(getCurrentTestName()); + session.auth().verify(5L, TimeUnit.SECONDS); + assertSame("Mismatched sessions", session, sessionHolder.get()); + } + + assertEquals("Mismatched banner", expectedWelcome, welcomeHolder.get()); + } +}