blob: b388a6c36bdf2e85c4fa574f788274aa06b6d4d6 [file] [log] [blame]
/*
* 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;
}
}