| /* |
| * 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.IOException; |
| import java.net.SocketAddress; |
| import java.nio.charset.StandardCharsets; |
| import java.security.GeneralSecurityException; |
| import java.security.KeyPair; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.stream.Collectors; |
| |
| import org.apache.sshd.common.FactoryManager; |
| import org.apache.sshd.common.NamedResource; |
| 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.config.keys.KeyUtils; |
| import org.apache.sshd.common.config.keys.OpenSshCertificate; |
| import org.apache.sshd.common.io.IoService; |
| import org.apache.sshd.common.io.IoSession; |
| import org.apache.sshd.common.io.IoWriteFuture; |
| import org.apache.sshd.common.kex.KexFactoryManager; |
| import org.apache.sshd.common.kex.KexProposalOption; |
| import org.apache.sshd.common.kex.KexState; |
| import org.apache.sshd.common.kex.extension.KexExtensionHandler; |
| import org.apache.sshd.common.kex.extension.KexExtensionHandler.AvailabilityPhase; |
| import org.apache.sshd.common.kex.extension.KexExtensionHandler.KexPhase; |
| import org.apache.sshd.common.keyprovider.HostKeyCertificateProvider; |
| import org.apache.sshd.common.keyprovider.KeyPairProvider; |
| import org.apache.sshd.common.session.ConnectionService; |
| import org.apache.sshd.common.session.SessionContext; |
| import org.apache.sshd.common.session.SessionDisconnectHandler; |
| import org.apache.sshd.common.session.helpers.AbstractSession; |
| import org.apache.sshd.common.signature.SignatureFactory; |
| 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.buffer.ByteArrayBuffer; |
| import org.apache.sshd.core.CoreModuleProperties; |
| import org.apache.sshd.server.ServerAuthenticationManager; |
| import org.apache.sshd.server.ServerFactoryManager; |
| import org.apache.sshd.server.auth.UserAuthFactory; |
| 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; |
| import org.apache.sshd.server.auth.password.PasswordAuthenticator; |
| import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; |
| |
| /** |
| * Provides default implementations for {@link ServerSession} related methods |
| * |
| * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> |
| */ |
| public abstract class AbstractServerSession extends AbstractSession implements ServerSession { |
| private ServerProxyAcceptor proxyAcceptor; |
| private SocketAddress clientAddress; |
| private PasswordAuthenticator passwordAuthenticator; |
| private PublickeyAuthenticator publickeyAuthenticator; |
| private KeyboardInteractiveAuthenticator interactiveAuthenticator; |
| private GSSAuthenticator gssAuthenticator; |
| private HostBasedAuthenticator hostBasedAuthenticator; |
| private List<UserAuthFactory> userAuthFactories; |
| private KeyPairProvider keyPairProvider; |
| private HostKeyCertificateProvider hostKeyCertificateProvider; |
| |
| protected AbstractServerSession(ServerFactoryManager factoryManager, IoSession ioSession) { |
| super(true, factoryManager, ioSession); |
| } |
| |
| @Override |
| public ServerFactoryManager getFactoryManager() { |
| return (ServerFactoryManager) super.getFactoryManager(); |
| } |
| |
| @Override |
| public ServerProxyAcceptor getServerProxyAcceptor() { |
| return resolveEffectiveProvider( |
| ServerProxyAcceptor.class, proxyAcceptor, getFactoryManager().getServerProxyAcceptor()); |
| } |
| |
| @Override |
| public void setServerProxyAcceptor(ServerProxyAcceptor proxyAcceptor) { |
| this.proxyAcceptor = proxyAcceptor; |
| } |
| |
| @Override |
| public SocketAddress getClientAddress() { |
| return resolvePeerAddress(clientAddress); |
| } |
| |
| public void setClientAddress(SocketAddress clientAddress) { |
| this.clientAddress = clientAddress; |
| } |
| |
| @Override |
| public PasswordAuthenticator getPasswordAuthenticator() { |
| ServerFactoryManager manager = getFactoryManager(); |
| return resolveEffectiveProvider( |
| PasswordAuthenticator.class, passwordAuthenticator, manager.getPasswordAuthenticator()); |
| } |
| |
| @Override |
| public void setPasswordAuthenticator(PasswordAuthenticator passwordAuthenticator) { |
| this.passwordAuthenticator = passwordAuthenticator; // OK if null - inherit from parent |
| } |
| |
| @Override |
| public PublickeyAuthenticator getPublickeyAuthenticator() { |
| ServerFactoryManager manager = getFactoryManager(); |
| return resolveEffectiveProvider( |
| PublickeyAuthenticator.class, publickeyAuthenticator, manager.getPublickeyAuthenticator()); |
| } |
| |
| @Override |
| public void setPublickeyAuthenticator(PublickeyAuthenticator publickeyAuthenticator) { |
| this.publickeyAuthenticator = publickeyAuthenticator; // OK if null - inherit from parent |
| } |
| |
| @Override |
| public KeyboardInteractiveAuthenticator getKeyboardInteractiveAuthenticator() { |
| ServerFactoryManager manager = getFactoryManager(); |
| return resolveEffectiveProvider( |
| KeyboardInteractiveAuthenticator.class, interactiveAuthenticator, |
| manager.getKeyboardInteractiveAuthenticator()); |
| } |
| |
| @Override |
| public void setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator interactiveAuthenticator) { |
| this.interactiveAuthenticator = interactiveAuthenticator; // OK if null - inherit from parent |
| } |
| |
| @Override |
| public GSSAuthenticator getGSSAuthenticator() { |
| ServerFactoryManager manager = getFactoryManager(); |
| return resolveEffectiveProvider( |
| GSSAuthenticator.class, gssAuthenticator, manager.getGSSAuthenticator()); |
| } |
| |
| @Override |
| public void setGSSAuthenticator(GSSAuthenticator gssAuthenticator) { |
| this.gssAuthenticator = gssAuthenticator; // OK if null - inherit from parent |
| } |
| |
| @Override |
| public HostBasedAuthenticator getHostBasedAuthenticator() { |
| ServerFactoryManager manager = getFactoryManager(); |
| return resolveEffectiveProvider( |
| HostBasedAuthenticator.class, hostBasedAuthenticator, manager.getHostBasedAuthenticator()); |
| } |
| |
| @Override |
| public void setHostBasedAuthenticator(HostBasedAuthenticator hostBasedAuthenticator) { |
| this.hostBasedAuthenticator = hostBasedAuthenticator; |
| } |
| |
| @Override |
| public List<UserAuthFactory> getUserAuthFactories() { |
| ServerFactoryManager manager = getFactoryManager(); |
| return resolveEffectiveFactories(userAuthFactories, manager.getUserAuthFactories()); |
| } |
| |
| @Override |
| public void setUserAuthFactories(List<UserAuthFactory> userAuthFactories) { |
| this.userAuthFactories = userAuthFactories; // OK if null/empty - inherit from parent |
| } |
| |
| @Override |
| public KeyPairProvider getKeyPairProvider() { |
| KexFactoryManager parent = getDelegate(); |
| return resolveEffectiveProvider(KeyPairProvider.class, keyPairProvider, |
| (parent == null) ? null : ((ServerAuthenticationManager) parent).getKeyPairProvider()); |
| } |
| |
| @Override |
| public HostKeyCertificateProvider getHostKeyCertificateProvider() { |
| ServerFactoryManager manager = getFactoryManager(); |
| return resolveEffectiveProvider(HostKeyCertificateProvider.class, |
| hostKeyCertificateProvider, manager.getHostKeyCertificateProvider()); |
| } |
| |
| @Override |
| public void setHostKeyCertificateProvider(HostKeyCertificateProvider hostKeyCertificateProvider) { |
| this.hostKeyCertificateProvider = hostKeyCertificateProvider; |
| } |
| |
| @Override |
| public void setKeyPairProvider(KeyPairProvider keyPairProvider) { |
| this.keyPairProvider = keyPairProvider; |
| } |
| |
| /** |
| * Sends the server identification + any extra header lines |
| * |
| * @param headerLines Extra header lines to be prepended to the actual identification string - ignored if |
| * {@code null}/empty |
| * @return An {@link IoWriteFuture} that can be used to be notified of identification data being written |
| * successfully or failing |
| * @throws Exception If failed to send identification |
| * @see <A HREF="https://tools.ietf.org/html/rfc4253#section-4.2">RFC 4253 - section 4.2</A> |
| */ |
| protected IoWriteFuture sendServerIdentification(List<String> headerLines) throws Exception { |
| serverVersion = resolveIdentificationString(CoreModuleProperties.SERVER_IDENTIFICATION.getName()); |
| signalSendIdentification(serverVersion, headerLines); |
| return sendIdentification(serverVersion, headerLines); |
| } |
| |
| @Override |
| protected void checkKeys() { |
| // nothing |
| } |
| |
| @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, Buffer buffer) throws Exception { |
| FactoryManager factoryManager = getFactoryManager(); |
| currentService = ServiceFactory.create( |
| factoryManager.getServiceFactories(), |
| ValidateUtils.checkNotNullAndNotEmpty(name, "No service name specified"), |
| this); |
| /* |
| * According to RFC4253: |
| * |
| * If the server rejects the service request, it SHOULD send an appropriate SSH_MSG_DISCONNECT message and MUST |
| * disconnect. |
| */ |
| if (currentService == null) { |
| try { |
| SessionDisconnectHandler handler = getSessionDisconnectHandler(); |
| if ((handler != null) |
| && handler.handleUnsupportedServiceDisconnectReason( |
| this, SshConstants.SSH_MSG_SERVICE_REQUEST, name, buffer)) { |
| if (log.isDebugEnabled()) { |
| log.debug("startService({}) ignore unknown service={} by handler", this, name); |
| } |
| return; |
| } |
| } catch (IOException | RuntimeException e) { |
| warn("startService({})[{}] failed ({}) to invoke disconnect handler: {}", |
| this, name, e.getClass().getSimpleName(), e.getMessage(), e); |
| } |
| |
| throw new SshException(SshConstants.SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE, "Unknown service: " + name); |
| } |
| } |
| |
| @Override |
| public IoWriteFuture signalAuthenticationSuccess( |
| String username, String authService, Buffer buffer) |
| throws Exception { |
| KexState curState = kexState.get(); |
| if (!KexState.DONE.equals(curState)) { |
| throw new SshException( |
| SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, |
| "Authentication success signalled though KEX state=" + curState); |
| } |
| |
| /* |
| * According to https://tools.ietf.org/html/rfc8308#section-2.4 |
| * |
| * If a server sends SSH_MSG_EXT_INFO, it MAY send it at zero, one, or both of the following opportunities: |
| * |
| * ... |
| * |
| * + Immediately preceding the server's SSH_MSG_USERAUTH_SUCCESS |
| */ |
| KexExtensionHandler extHandler = getKexExtensionHandler(); |
| if ((extHandler != null) && extHandler.isKexExtensionsAvailable(this, AvailabilityPhase.AUTHOK)) { |
| extHandler.sendKexExtensions(this, KexPhase.AUTHOK); |
| } |
| |
| Buffer response = createBuffer(SshConstants.SSH_MSG_USERAUTH_SUCCESS, Byte.SIZE); |
| IoWriteFuture future; |
| IoSession networkSession = getIoSession(); |
| synchronized (encodeLock) { |
| Buffer packet = resolveOutputPacket(response); |
| |
| setUsername(username); |
| // must be AFTER the USERAUTH-SUCCESS packet created in case delayed compression is used |
| setAuthenticated(); |
| startService(authService, buffer); |
| |
| // Now we can inform the peer that authentication is successful |
| future = networkSession.writeBuffer(packet); |
| } |
| |
| resetIdleTimeout(); |
| log.info("Session {}@{} authenticated", username, networkSession.getRemoteAddress()); |
| return future; |
| } |
| |
| @Override |
| protected void handleServiceAccept(String serviceName, Buffer buffer) throws Exception { |
| super.handleServiceAccept(serviceName, buffer); |
| |
| try { |
| SessionDisconnectHandler handler = getSessionDisconnectHandler(); |
| if ((handler != null) |
| && handler.handleUnsupportedServiceDisconnectReason( |
| this, SshConstants.SSH_MSG_SERVICE_ACCEPT, serviceName, buffer)) { |
| if (log.isDebugEnabled()) { |
| log.debug("handleServiceAccept({}) ignore unknown service={} by handler", this, serviceName); |
| } |
| return; |
| } |
| } catch (IOException | RuntimeException e) { |
| warn("handleServiceAccept({}) failed ({}) to invoke disconnect handler of unknown service={}: {}", |
| this, e.getClass().getSimpleName(), serviceName, e.getMessage(), e); |
| } |
| |
| // TODO: can services be initiated by the server-side ? |
| disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, |
| "Unsupported packet: SSH_MSG_SERVICE_ACCEPT for " + serviceName); |
| } |
| |
| @Override |
| protected byte[] sendKexInit(Map<KexProposalOption, String> proposal) throws IOException { |
| mergeProposals(serverProposal, proposal); |
| return super.sendKexInit(proposal); |
| } |
| |
| @Override |
| protected void setKexSeed(byte... seed) { |
| setServerKexData(seed); |
| } |
| |
| @Override |
| protected String resolveAvailableSignaturesProposal(FactoryManager proposedManager) |
| throws IOException, GeneralSecurityException { |
| /* |
| * Make sure we can provide key(s) for the available signatures |
| */ |
| ValidateUtils.checkTrue(proposedManager == getFactoryManager(), |
| "Mismatched signatures proposed factory manager"); |
| |
| KeyPairProvider kpp = getKeyPairProvider(); |
| Collection<String> provided = null; |
| try { |
| if (kpp != null) { |
| provided = GenericUtils.stream(kpp.getKeyTypes(this)).collect(Collectors.toSet()); |
| |
| HostKeyCertificateProvider hostKeyCertificateProvider = getHostKeyCertificateProvider(); |
| if (hostKeyCertificateProvider != null) { |
| Iterable<OpenSshCertificate> certificates = hostKeyCertificateProvider.loadCertificates(this); |
| for (OpenSshCertificate certificate : certificates) { |
| // Add the certificate alg only if the corresponding keyPair type is available |
| String rawKeyType = certificate.getRawKeyType(); |
| if (provided.contains(rawKeyType)) { |
| provided.add(certificate.getKeyType()); |
| } else { |
| log.info( |
| "resolveAvailableSignaturesProposal({}) No private key of type={} available in provided certificate", |
| this, rawKeyType); |
| } |
| } |
| } |
| } |
| } catch (Error e) { |
| warn("resolveAvailableSignaturesProposal({}) failed ({}) to get key types: {}", |
| this, e.getClass().getSimpleName(), e.getMessage(), e); |
| |
| throw new RuntimeSshException(e); |
| } |
| |
| Collection<String> available = NamedResource.getNameList(getSignatureFactories()); |
| if ((provided == null) || GenericUtils.isEmpty(available)) { |
| return resolveEmptySignaturesProposal(available, provided); |
| } |
| |
| Collection<String> supported = SignatureFactory.resolveSignatureFactoryNamesProposal(provided, available); |
| if (GenericUtils.isEmpty(supported)) { |
| return resolveEmptySignaturesProposal(available, provided); |
| } else { |
| return GenericUtils.join(supported, ','); |
| } |
| } |
| |
| /** |
| * Called by {@link #resolveAvailableSignaturesProposal(FactoryManager)} if none of the provided keys is supported - |
| * last chance for the derived implementation to do something |
| * |
| * @param supported The supported key types - may be {@code null}/empty |
| * @param provided The available signature types - may be {@code null}/empty |
| * @return The resolved proposal - {@code null} by default |
| */ |
| protected String resolveEmptySignaturesProposal( |
| Iterable<String> supported, Iterable<String> provided) { |
| if (log.isDebugEnabled()) { |
| log.debug("resolveEmptySignaturesProposal({})[{}] none of the keys appears in supported list: {}", |
| this, provided, supported); |
| } |
| return null; |
| } |
| |
| @Override |
| protected boolean readIdentification(Buffer buffer) throws Exception { |
| ServerProxyAcceptor acceptor = getServerProxyAcceptor(); |
| int rpos = buffer.rpos(); |
| boolean debugEnabled = log.isDebugEnabled(); |
| if (acceptor != null) { |
| try { |
| boolean completed = acceptor.acceptServerProxyMetadata(this, buffer); |
| if (!completed) { |
| buffer.rpos(rpos); // restore original buffer position |
| return false; // more data required |
| } |
| } catch (Throwable t) { |
| warn("readIdentification({}) failed ({}) to accept proxy metadata: {}", |
| this, t.getClass().getSimpleName(), t.getMessage(), t); |
| |
| if (t instanceof IOException) { |
| throw (IOException) t; |
| } else { |
| throw new SshException(t); |
| } |
| } |
| } |
| |
| List<String> ident = doReadIdentification(buffer, true); |
| int numLines = GenericUtils.size(ident); |
| clientVersion = (numLines <= 0) ? null : ident.remove(numLines - 1); |
| if (GenericUtils.isEmpty(clientVersion)) { |
| buffer.rpos(rpos); // restore original buffer position |
| return false; // more data required |
| } |
| |
| if (debugEnabled) { |
| log.debug("readIdentification({}) client version string: {}", this, clientVersion); |
| } |
| |
| IOException err; |
| if (SessionContext.isValidVersionPrefix(clientVersion)) { |
| /* |
| * NOTE: because of the way that "doReadIdentification" works we are assured that there are no extra lines |
| * beyond the version one, but we check this nevertheless |
| */ |
| err = (numLines > 1) |
| ? new SshException( |
| SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, |
| "Unexpected extra " + (numLines - 1) + " lines from client=" + clientVersion) |
| : null; |
| } else { |
| err = new SshException( |
| SshConstants.SSH2_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, |
| "Unsupported protocol version: " + clientVersion); |
| } |
| |
| if (err != null) { |
| IoSession networkSession = getIoSession(); |
| networkSession.writeBuffer( |
| new ByteArrayBuffer((err.getMessage() + "\n").getBytes(StandardCharsets.UTF_8))) |
| .addListener(future -> close(true)); |
| throw err; |
| } |
| |
| signalPeerIdentificationReceived(clientVersion, ident); |
| |
| kexState.set(KexState.INIT); |
| sendKexInit(); |
| return true; |
| } |
| |
| @Override |
| protected void receiveKexInit(Map<KexProposalOption, String> proposal, byte[] seed) |
| throws IOException { |
| mergeProposals(clientProposal, proposal); |
| setClientKexData(seed); |
| } |
| |
| @Override |
| public KeyPair getHostKey() { |
| String proposedKey = getNegotiatedKexParameter(KexProposalOption.SERVERKEYS); |
| String keyType = KeyUtils.getCanonicalKeyType(proposedKey); |
| if (GenericUtils.isEmpty(keyType)) { |
| return null; // OK if not negotiated yet |
| } |
| |
| KeyPairProvider provider = Objects.requireNonNull(getKeyPairProvider(), "No host keys provider"); |
| try { |
| HostKeyCertificateProvider hostKeyCertificateProvider = getHostKeyCertificateProvider(); |
| if (hostKeyCertificateProvider != null) { |
| OpenSshCertificate publicKey = hostKeyCertificateProvider.loadCertificate(this, keyType); |
| if (publicKey != null) { |
| String rawKeyType = publicKey.getRawKeyType(); |
| |
| if (log.isDebugEnabled()) { |
| log.debug("getHostKey({}) using certified key {}/{} with ID={}", |
| this, keyType, rawKeyType, publicKey.getId()); |
| } |
| |
| KeyPair keyPair = provider.loadKey(this, rawKeyType); |
| ValidateUtils.checkNotNull(keyPair, "No certified private key of type=%s available", rawKeyType); |
| return new KeyPair(publicKey, keyPair.getPrivate()); |
| } |
| } |
| |
| return provider.loadKey(this, keyType); |
| } catch (IOException | GeneralSecurityException | Error e) { |
| warn("getHostKey({}) failed ({}) to load key of type={}[{}]: {}", |
| this, e.getClass().getSimpleName(), proposedKey, keyType, e.getMessage(), e); |
| |
| throw new RuntimeSshException(e); |
| } |
| } |
| |
| @Override |
| public int getActiveSessionCountForUser(String userName) { |
| if (GenericUtils.isEmpty(userName)) { |
| return 0; |
| } |
| |
| IoSession networkSession = getIoSession(); |
| IoService service = networkSession.getService(); |
| Map<?, IoSession> sessionsMap = service.getManagedSessions(); |
| if (GenericUtils.isEmpty(sessionsMap)) { |
| return 0; |
| } |
| |
| int totalCount = 0; |
| for (IoSession is : sessionsMap.values()) { |
| ServerSession session = (ServerSession) getSession(is, true); |
| if (session == null) { |
| continue; |
| } |
| |
| String sessionUser = session.getUsername(); |
| if ((!GenericUtils.isEmpty(sessionUser)) |
| && Objects.equals(sessionUser, userName)) { |
| totalCount++; |
| } |
| } |
| |
| return totalCount; |
| } |
| |
| /** |
| * @return The underlying {@link IoSession} id. |
| */ |
| public long getId() { |
| IoSession networkSession = getIoSession(); |
| return networkSession.getId(); |
| } |
| |
| @Override |
| protected ConnectionService getConnectionService() { |
| return (this.currentService instanceof ConnectionService) |
| ? (ConnectionService) this.currentService |
| : null; |
| } |
| } |