package org.dcache.services.ssh2;
import org.apache.sshd.common.Factory;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.keyprovider.AbstractFileKeyPairProvider;
import org.apache.sshd.common.util.SecurityUtils;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
import org.apache.sshd.server.session.ServerSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import javax.security.auth.Subject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import diskCacheV111.util.AuthorizedKeyParser;
import diskCacheV111.util.CacheException;
import diskCacheV111.util.PermissionDeniedCacheException;
import dmg.cells.nucleus.CellCommandListener;
import dmg.cells.nucleus.CellLifeCycleAware;
import org.dcache.auth.LoginReply;
import org.dcache.auth.LoginStrategy;
import org.dcache.auth.Origin;
import org.dcache.auth.PasswordCredential;
import org.dcache.auth.Subjects;
import org.dcache.util.Files;
import static java.util.stream.Collectors.toList;
/**
* This class starts the ssh server. It is however not started in the
* constructor, but in afterStart() to avoid race conditions. The class starts
* the UserAdminShell via the factory CommandFactory, which in turn create the
* Command_ConsoleReader that actually creates an instance of UserAdminShell.
*
* @author bernardt
*/
public class Ssh2Admin implements CellCommandListener, CellLifeCycleAware
{
private static final Logger _log = LoggerFactory.getLogger(Ssh2Admin.class);
private final SshServer _server;
// UniversalSpringCell injected parameters
private List<File> _hostKeys;
private File _authorizedKeyList;
private String _host;
private int _port;
private int _adminGroupId;
private LoginStrategy _loginStrategy;
private TimeUnit _idleTimeoutUnit;
private long _idleTimeout;
public Ssh2Admin() {
_server = SshServer.setUpDefaultServer();
}
public LoginStrategy getLoginStrategy() {
return _loginStrategy;
}
public void setLoginStrategy(LoginStrategy loginStrategy) {
_loginStrategy = loginStrategy;
}
public void setPort(int port) {
_log.debug("Ssh2 port set to: {}", String.valueOf(port));
_port = port;
}
public int getPort() {
return _port;
}
public void setHost(String host) {
_host = host;
}
public String getHost() {
return _host;
}
public void setAdminGroupId(int groupId) {
_adminGroupId = groupId;
}
public int getAdminGroupId() {
return _adminGroupId;
}
public void setHostKeys(String[] keys) {
_hostKeys = Stream.of(keys).map(File::new).collect(toList());
}
public File getAuthorizedKeyList() {
return _authorizedKeyList;
}
public void setAuthorizedKeyList(File authorizedKeyList) {
_authorizedKeyList = authorizedKeyList;
}
@Required
public void setShellFactory(Factory<Command> shellCommand)
{
_server.setShellFactory(shellCommand);
}
@Required
public void setSubsystemFactories(List<NamedFactory<Command>> subsystemFactories)
{
_server.setSubsystemFactories(subsystemFactories);
}
@Required
public void setIdleTimeout(long timeout)
{
_idleTimeout = timeout;
}
@Required
public void setIdleTimeoutUnit(TimeUnit unit) {
_idleTimeoutUnit = unit;
}
public void configureAuthentication() {
_server.setPasswordAuthenticator(new AdminPasswordAuthenticator());
_server.setPublickeyAuthenticator(new AdminPublickeyAuthenticator());
}
@Override
public void afterStart() {
configureAuthentication();
configureKeyFiles();
startServer();
_log.debug("Ssh2 Admin Interface started!");
}
@Override
public void beforeStop() {
try {
_server.stop();
} catch (IOException e) {
_log.warn("SSH failure during shutdown: " + e.getMessage());
}
}
private void configureKeyFiles() {
try {
for (File key : _hostKeys) {
Files.checkFile(key);
}
AbstractFileKeyPairProvider fKeyPairProvider = SecurityUtils.createFileKeyPairProvider();
fKeyPairProvider.setFiles(_hostKeys);
_server.setKeyPairProvider(fKeyPairProvider);
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
private void startServer() {
// MINA SSH uses int to store timeout. Strip the long valued to max int if required.
int effectiveTimeout = (int)Math.min((long)Integer.MAX_VALUE,
_idleTimeoutUnit.toMillis(_idleTimeout > 0? _idleTimeout : Long.MAX_VALUE));
_server.getProperties().put(SshServer.IDLE_TIMEOUT, Integer.toString(effectiveTimeout));
_server.setPort(_port);
_server.setHost(_host);
try {
_server.start();
} catch (IOException ioe) {
throw new RuntimeException("Ssh2 server was interrupted while starting: ", ioe);
}
}
private void addOrigin(ServerSession session, Subject subject)
{
SocketAddress remote = session.getIoSession().getRemoteAddress();
if (remote instanceof InetSocketAddress) {
InetAddress address = ((InetSocketAddress) remote).getAddress();
subject.getPrincipals().add(new Origin(address));
}
}
private class AdminPasswordAuthenticator implements PasswordAuthenticator {
@Override
public boolean authenticate(String userName, String password,
ServerSession session) {
Subject subject = new Subject();
addOrigin(session, subject);
subject.getPrivateCredentials().add(new PasswordCredential(userName,
password));
try {
LoginReply reply = _loginStrategy.login(subject);
Subject authenticatedSubject = reply.getSubject();
if (!Subjects.hasGid(authenticatedSubject, _adminGroupId)) {
throw new PermissionDeniedCacheException("not member of admin gid");
}
return true;
} catch (PermissionDeniedCacheException e) {
_log.warn("Login for {} denied: {}", userName, e.getMessage());
} catch (CacheException e) {
_log.warn("Login for {} failed: {}", userName, e.toString());
}
return false;
}
}
private class AdminPublickeyAuthenticator implements PublickeyAuthenticator {
private PublicKey toPublicKey(String s) {
try {
AuthorizedKeyParser decoder = new AuthorizedKeyParser();
return decoder.decodePublicKey(s);
} catch (InvalidKeySpecException | NoSuchAlgorithmException | IllegalArgumentException e) {
_log.warn("can't decode public key from file: {} ", e.getMessage());
}
return null;
}
@Override
public boolean authenticate(String userName, PublicKey key,
ServerSession session) {
_log.debug("Authentication username set to: {} publicKey: {}",
userName, key);
try {
try(Stream<String> fileStream = java.nio.file.Files.lines(_authorizedKeyList.toPath())) {
return fileStream
.filter(l -> !l.isEmpty() && !l.matches(" *#.*"))
.map(this::toPublicKey)
.filter(k -> k != null)
.filter(key::equals)
.findFirst()
.isPresent();
}
} catch (FileNotFoundException e) {
_log.debug("File not found: {}", _authorizedKeyList);
} catch (IOException e) {
_log.error("Failed to read {}: {}", _authorizedKeyList,
e.getMessage());
}
return false;
}
}
}