| /* |
| * 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.session; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.MalformedURLException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.nio.charset.Charset; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| 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.concurrent.atomic.AtomicReference; |
| import java.util.stream.Collectors; |
| |
| import org.apache.sshd.common.NamedResource; |
| 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.session.SessionDisconnectHandler; |
| import org.apache.sshd.common.util.GenericUtils; |
| import org.apache.sshd.common.util.NumberUtils; |
| import org.apache.sshd.common.util.ValidateUtils; |
| import org.apache.sshd.common.util.buffer.Buffer; |
| import org.apache.sshd.common.util.closeable.AbstractCloseable; |
| import org.apache.sshd.common.util.io.IoUtils; |
| import org.apache.sshd.core.CoreModuleProperties; |
| import org.apache.sshd.server.ServerFactoryManager; |
| import org.apache.sshd.server.auth.AsyncAuthException; |
| import org.apache.sshd.server.auth.UserAuth; |
| import org.apache.sshd.server.auth.UserAuthFactory; |
| import org.apache.sshd.server.auth.UserAuthNoneFactory; |
| 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 AtomicBoolean welcomeSent = new AtomicBoolean(false); |
| private final Map<String, Object> properties = new ConcurrentHashMap<>(); |
| private final ServerSession serverSession; |
| private final WelcomeBannerPhase welcomePhase; |
| private List<UserAuthFactory> userAuthFactories; |
| private List<List<String>> authMethods; |
| private String authUserName; |
| private String authMethod; |
| private String authService; |
| private UserAuth currentAuth; |
| |
| private int maxAuthRequests; |
| private int nbAuthRequests; |
| |
| public ServerUserAuthService(Session s) throws IOException { |
| boolean debugEnabled = log.isDebugEnabled(); |
| serverSession = ValidateUtils.checkInstanceOf( |
| s, ServerSession.class, "Server side service used on client side: %s", s); |
| if (s.isAuthenticated()) { |
| throw new SshException("Session already authenticated"); |
| } |
| |
| welcomePhase = CoreModuleProperties.WELCOME_BANNER_PHASE.getRequired(this); |
| maxAuthRequests = CoreModuleProperties.MAX_AUTH_REQUESTS.getRequired(this); |
| |
| List<UserAuthFactory> factories = ValidateUtils.checkNotNullAndNotEmpty( |
| serverSession.getUserAuthFactories(), "No user auth factories for %s", s); |
| userAuthFactories = new ArrayList<>(factories); |
| // Get authentication methods |
| authMethods = new ArrayList<>(); |
| |
| String mths = CoreModuleProperties.AUTH_METHODS.getOrNull(this); |
| if (GenericUtils.isEmpty(mths)) { |
| for (UserAuthFactory uaf : factories) { |
| authMethods.add(new ArrayList<>(Collections.singletonList(uaf.getName()))); |
| } |
| } else { |
| if (debugEnabled) { |
| log.debug("ServerUserAuthService({}) using configured methods={}", s, mths); |
| } |
| for (String mthl : mths.split("\\s")) { |
| authMethods.add(new ArrayList<>(Arrays.asList(GenericUtils.split(mthl, ',')))); |
| } |
| } |
| // Verify all required methods are supported |
| for (List<String> l : authMethods) { |
| for (String m : l) { |
| UserAuthFactory factory = NamedResource.findByName(m, String.CASE_INSENSITIVE_ORDER, userAuthFactories); |
| if (factory == null) { |
| throw new SshException("Configured method is not supported: " + m); |
| } |
| } |
| } |
| |
| if (debugEnabled) { |
| log.debug("ServerUserAuthService({}) authorized authentication methods: {}", |
| s, NamedResource.getNames(userAuthFactories)); |
| } |
| |
| s.resetAuthTimeout(); |
| } |
| |
| public WelcomeBannerPhase getWelcomePhase() { |
| return welcomePhase; |
| } |
| |
| @Override |
| public void start() { |
| // do nothing |
| } |
| |
| @Override |
| public ServerSession getSession() { |
| return getServerSession(); |
| } |
| |
| @Override |
| public ServerSession getServerSession() { |
| return serverSession; |
| } |
| |
| @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(); |
| boolean debugEnabled = log.isDebugEnabled(); |
| |
| if (cmd == SshConstants.SSH_MSG_USERAUTH_REQUEST) { |
| AtomicReference<Boolean> authHolder = new AtomicReference<>(authed); |
| if (!handleUserAuthRequestMessage(session, buffer, authHolder)) { |
| return; |
| } |
| |
| authed = authHolder.get(); |
| } 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)); |
| } |
| |
| if (debugEnabled) { |
| log.debug("process({}) Received authentication message={} for mechanism={}", |
| session, SshConstants.getCommandMessageName(cmd), currentAuth.getName()); |
| } |
| |
| buffer.rpos(buffer.rpos() - 1); |
| try { |
| authed = currentAuth.next(buffer); |
| } catch (AsyncAuthException async) { |
| async.addListener(authenticated -> asyncAuth(cmd, buffer, authenticated)); |
| return; |
| } catch (Exception e) { |
| // Continue |
| warn("process({}) Failed ({}) to authenticate using current method={}: {}", |
| session, e.getClass().getSimpleName(), currentAuth.getName(), e.getMessage(), e); |
| } |
| } |
| |
| if (authed == null) { |
| handleAuthenticationInProgress(cmd, buffer); |
| } else if (authed) { |
| handleAuthenticationSuccess(cmd, buffer); |
| } else { |
| handleAuthenticationFailure(cmd, buffer); |
| } |
| } |
| |
| protected boolean handleUserAuthRequestMessage( |
| ServerSession session, Buffer buffer, AtomicReference<Boolean> authHolder) |
| throws Exception { |
| boolean debugEnabled = log.isDebugEnabled(); |
| /* |
| * According to RFC4252 section 5.1: |
| * |
| * |
| * When SSH_MSG_USERAUTH_SUCCESS has been sent, any further authentication requests received after that SHOULD |
| * be silently ignored. |
| */ |
| if (session.isAuthenticated()) { |
| String username = buffer.getString(); |
| String service = buffer.getString(); |
| String method = buffer.getString(); |
| |
| if (debugEnabled) { |
| log.debug("handleUserAuthRequestMessage({}) ignore user={}, service={}, method={}" |
| + " auth. request since session already authenticated", |
| session, username, service, method); |
| } |
| return false; |
| } |
| |
| if (WelcomeBannerPhase.FIRST_REQUEST.equals(getWelcomePhase())) { |
| sendWelcomeBanner(session); |
| } |
| |
| if (currentAuth != null) { |
| try { |
| currentAuth.destroy(); |
| } finally { |
| currentAuth = null; |
| } |
| } |
| |
| String username = buffer.getString(); |
| String service = buffer.getString(); |
| String method = buffer.getString(); |
| if (debugEnabled) { |
| log.debug("handleUserAuthRequestMessage({}) Received SSH_MSG_USERAUTH_REQUEST user={}, service={}, method={}", |
| session, username, service, method); |
| } |
| |
| if ((this.authUserName == null) || (this.authService == null)) { |
| this.authUserName = username; |
| this.authService = service; |
| } else if (this.authUserName.equals(username) && this.authService.equals(service)) { |
| nbAuthRequests++; |
| if (nbAuthRequests > maxAuthRequests) { |
| boolean disconnectSession = true; |
| try { |
| SessionDisconnectHandler handler = session.getSessionDisconnectHandler(); |
| disconnectSession = (handler == null) |
| || (!handler.handleAuthCountDisconnectReason( |
| session, this, service, method, username, nbAuthRequests, maxAuthRequests)); |
| } catch (IOException | RuntimeException e) { |
| warn("handleUserAuthRequestMessage({}) failed ({}) to invoke disconnect handler due to" |
| + " user={}/{}, service={}/{} - {}/{} auth requests: {}", |
| session, e.getClass().getSimpleName(), |
| this.authUserName, username, this.authService, service, |
| nbAuthRequests, maxAuthRequests, e.getMessage(), e); |
| } |
| |
| if (disconnectSession) { |
| session.disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, |
| "Too many authentication failures: " + nbAuthRequests); |
| return false; |
| } |
| |
| if (debugEnabled) { |
| log.debug( |
| "handleUserAuthRequestMessage({}) ignore mismatched authentication counts: user={}/{}, service={}/{}: {}/{}", |
| session, this.authUserName, username, this.authService, service, nbAuthRequests, maxAuthRequests); |
| } |
| } |
| } else { |
| boolean disconnectSession = true; |
| try { |
| SessionDisconnectHandler handler = session.getSessionDisconnectHandler(); |
| disconnectSession = (handler == null) |
| || (!handler.handleAuthParamsDisconnectReason( |
| session, this, this.authUserName, username, this.authService, service)); |
| } catch (IOException | RuntimeException e) { |
| warn("handleUserAuthRequestMessage({}) failed ({}) to invoke disconnect handler due to" |
| + " user={}/{}, service={}/{} mismatched parameters: {}", |
| session, e.getClass().getSimpleName(), |
| this.authUserName, username, this.authService, service, e.getMessage(), e); |
| } |
| |
| if (disconnectSession) { |
| session.disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, |
| "Change of username or service is not allowed (" + this.authUserName + ", " + this.authService + ")" |
| + " -> (" + username + ", " + service + ")"); |
| } else { |
| if (debugEnabled) { |
| log.debug( |
| "handleUserAuthRequestMessage({}) ignore mismatched authentication parameters: user={}/{}, service={}/{}", |
| session, this.authUserName, username, this.authService, service); |
| } |
| } |
| return false; |
| } |
| |
| // TODO: verify that the service is supported |
| this.authMethod = method; |
| if (debugEnabled) { |
| log.debug( |
| "handleUserAuthRequestMessage({}) Authenticating user '{}' with service '{}' and method '{}' (attempt {} / {})", |
| session, username, service, method, nbAuthRequests, maxAuthRequests); |
| } |
| |
| UserAuthFactory factory = NamedResource.findByName( |
| method, String.CASE_INSENSITIVE_ORDER, userAuthFactories); |
| if (factory == null) { |
| if (debugEnabled) { |
| log.debug("handleUserAuthRequestMessage({}) no authentication factory for method={}", session, method); |
| } |
| |
| return true; |
| } |
| |
| currentAuth = ValidateUtils.checkNotNull( |
| factory.createUserAuth(session), "No authenticator created for method=%s", method); |
| try { |
| Boolean authed = currentAuth.auth(session, username, service, buffer); |
| authHolder.set(authed); |
| } catch (AsyncAuthException async) { |
| async.addListener(authenticated -> asyncAuth(SshConstants.SSH_MSG_USERAUTH_REQUEST, buffer, authenticated)); |
| return false; |
| } catch (Exception e) { |
| warn("handleUserAuthRequestMessage({}) Failed ({}) to authenticate using factory method={}: {}", |
| session, e.getClass().getSimpleName(), method, e.getMessage(), e); |
| } |
| |
| return true; |
| } |
| |
| protected synchronized void asyncAuth(int cmd, Buffer buffer, boolean authed) { |
| try { |
| if (authed) { |
| handleAuthenticationSuccess(cmd, buffer); |
| } else { |
| handleAuthenticationFailure(cmd, buffer); |
| } |
| } catch (Exception e) { |
| ServerSession session = getServerSession(); |
| warn("asyncAuth({}) Error ({}) performing async authentication via cmd={}: {}", |
| session, e.getClass().getSimpleName(), cmd, e.getMessage(), e); |
| } |
| } |
| |
| protected void handleAuthenticationInProgress(int cmd, Buffer buffer) throws Exception { |
| String username = (currentAuth == null) ? null : currentAuth.getUsername(); |
| if (log.isDebugEnabled()) { |
| log.debug("handleAuthenticationInProgress({}@{}) {}", |
| username, getServerSession(), SshConstants.getCommandMessageName(cmd)); |
| } |
| } |
| |
| protected void handleAuthenticationSuccess(int cmd, Buffer buffer) throws Exception { |
| String username = Objects.requireNonNull(currentAuth, "No current auth").getUsername(); |
| ServerSession session = getServerSession(); |
| boolean debugEnabled = log.isDebugEnabled(); |
| if (debugEnabled) { |
| log.debug("handleAuthenticationSuccess({}@{}) {}", |
| username, session, SshConstants.getCommandMessageName(cmd)); |
| } |
| |
| boolean success = false; |
| for (List<String> l : authMethods) { |
| if ((GenericUtils.size(l) > 0) && l.get(0).equals(authMethod)) { |
| l.remove(0); |
| success |= l.isEmpty(); |
| } |
| } |
| |
| if (success) { |
| Integer maxSessionCount = CoreModuleProperties.MAX_CONCURRENT_SESSIONS.getOrNull(session); |
| if (maxSessionCount != null) { |
| int currentSessionCount = session.getActiveSessionCountForUser(username); |
| if (currentSessionCount >= maxSessionCount) { |
| boolean disconnectSession = true; |
| try { |
| SessionDisconnectHandler handler = session.getSessionDisconnectHandler(); |
| disconnectSession = (handler == null) |
| || (!handler.handleSessionsCountDisconnectReason( |
| session, this, username, currentSessionCount, maxSessionCount)); |
| } catch (IOException | RuntimeException e) { |
| warn( |
| "handleAuthenticationSuccess({}@{}) failed ({}) to invoke disconnect handler due to {}/{} sessions count: {}", |
| username, session, e.getClass().getSimpleName(), currentSessionCount, maxSessionCount, |
| e.getMessage(), e); |
| } |
| |
| if (disconnectSession) { |
| session.disconnect(SshConstants.SSH2_DISCONNECT_TOO_MANY_CONNECTIONS, |
| "Too many concurrent connections (" + currentSessionCount + ") - max. allowed: " |
| + maxSessionCount); |
| return; |
| } |
| |
| if (debugEnabled) { |
| log.debug("handleAuthenticationSuccess({}@{}) ignore {}/{} sessions count due to handler intervention", |
| username, session, currentSessionCount, maxSessionCount); |
| } |
| } |
| } |
| |
| if (WelcomeBannerPhase.POST_SUCCESS.equals(getWelcomePhase())) { |
| sendWelcomeBanner(session); |
| } |
| |
| session.signalAuthenticationSuccess(username, authService, buffer); |
| } else { |
| String remaining = authMethods.stream() |
| .filter(GenericUtils::isNotEmpty) |
| .map(l -> l.get(0)) |
| .collect(Collectors.joining(",")); |
| |
| if (debugEnabled) { |
| log.debug("handleAuthenticationSuccess({}@{}) remaining methods={}", username, session, remaining); |
| } |
| |
| Buffer response = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_FAILURE, remaining.length() + Byte.SIZE); |
| response.putString(remaining); |
| response.putBoolean(true); // partial success ... |
| session.writePacket(response); |
| } |
| |
| try { |
| currentAuth.destroy(); |
| } finally { |
| currentAuth = null; |
| } |
| } |
| |
| protected void handleAuthenticationFailure(int cmd, Buffer buffer) throws Exception { |
| ServerSession session = getServerSession(); |
| boolean debugEnabled = log.isDebugEnabled(); |
| if (WelcomeBannerPhase.FIRST_FAILURE.equals(getWelcomePhase())) { |
| sendWelcomeBanner(session); |
| } |
| |
| String username = (currentAuth == null) ? null : currentAuth.getUsername(); |
| if (debugEnabled) { |
| log.debug("handleAuthenticationFailure({}@{}) {}", |
| username, session, SshConstants.getCommandMessageName(cmd)); |
| } |
| |
| StringBuilder sb = new StringBuilder((authMethods.size() + 1) * Byte.SIZE); |
| for (List<String> l : authMethods) { |
| if (GenericUtils.size(l) > 0) { |
| String m = l.get(0); |
| if (!UserAuthNoneFactory.NAME.equals(m)) { |
| if (sb.length() > 0) { |
| sb.append(','); |
| } |
| sb.append(m); |
| } |
| } |
| } |
| |
| String remaining = sb.toString(); |
| if (debugEnabled) { |
| log.debug("handleAuthenticationFailure({}@{}) remaining methods: {}", username, session, remaining); |
| } |
| |
| buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_FAILURE, remaining.length() + Byte.SIZE); |
| buffer.putString(remaining); |
| buffer.putBoolean(false); // no partial success ... |
| session.writePacket(buffer); |
| |
| if (currentAuth != null) { |
| try { |
| currentAuth.destroy(); |
| } finally { |
| currentAuth = null; |
| } |
| } |
| } |
| |
| /** |
| * 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 = resolveWelcomeBanner(session); |
| if (GenericUtils.isEmpty(welcomeBanner)) { |
| return null; |
| } |
| |
| String lang = CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequired(this); |
| 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); |
| } |
| |
| protected String resolveWelcomeBanner(ServerSession session) throws IOException { |
| Object bannerValue = CoreModuleProperties.WELCOME_BANNER.getOrNull(this); |
| if (bannerValue == null) { |
| return null; |
| } |
| |
| if (bannerValue instanceof CharSequence) { |
| String message = bannerValue.toString(); |
| if (GenericUtils.isEmpty(message)) { |
| return null; |
| } |
| |
| if (CoreModuleProperties.AUTO_WELCOME_BANNER_VALUE.equalsIgnoreCase(message)) { |
| try { |
| return KeyRandomArt.combine(session, ' ', session.getKeyPairProvider()); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new IOException(e); |
| } |
| } |
| |
| if (!message.contains("://")) { |
| return message; |
| } |
| |
| try { |
| bannerValue = new URI(message); |
| } catch (URISyntaxException e) { |
| log.error("resolveWelcomeBanner({}) bad path URI {}: {}", session, message, e.getMessage()); |
| throw new MalformedURLException( |
| e.getClass().getSimpleName() + " - bad URI (" + message + "): " + e.getMessage()); |
| } |
| |
| if (message.startsWith("file:/")) { |
| bannerValue = Paths.get((URI) bannerValue); |
| } |
| } |
| |
| if (bannerValue instanceof File) { |
| bannerValue = ((File) bannerValue).toPath(); |
| } |
| |
| if (bannerValue instanceof Path) { |
| Path path = (Path) bannerValue; |
| if ((!Files.exists(path)) || (Files.size(path) <= 0L)) { |
| if (log.isDebugEnabled()) { |
| log.debug("resolveWelcomeBanner({}) file is empty/does not exist {}", session, path); |
| } |
| return null; |
| } |
| bannerValue = path.toUri(); |
| } |
| |
| if (bannerValue instanceof URI) { |
| bannerValue = ((URI) bannerValue).toURL(); |
| } |
| |
| if (bannerValue instanceof URL) { |
| Charset cs = CoreModuleProperties.WELCOME_BANNER_CHARSET.getRequired(this); |
| return loadWelcomeBanner(session, (URL) bannerValue, cs); |
| } |
| |
| return bannerValue.toString(); |
| } |
| |
| protected String loadWelcomeBanner(ServerSession session, URL url, Charset cs) throws IOException { |
| try (InputStream stream = url.openStream()) { |
| byte[] bytes = IoUtils.toByteArray(stream); |
| return NumberUtils.isEmpty(bytes) ? "" : new String(bytes, cs); |
| } |
| } |
| |
| public ServerFactoryManager getFactoryManager() { |
| return serverSession.getFactoryManager(); |
| } |
| } |