package automately.core.services.ssh; import automately.core.data.User; import automately.core.data.UserData; import automately.core.file.VirtualFileSystem; import automately.core.services.core.AutomatelyService; import com.hazelcast.core.IMap; import com.hazelcast.query.Predicates; import io.jsync.app.core.Cluster; import io.jsync.app.core.Logger; import io.jsync.json.JsonObject; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.compression.BuiltinCompressions; import org.apache.sshd.common.compression.Compression; import org.apache.sshd.server.Command; import org.apache.sshd.server.ServerBuilder; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.apache.sshd.server.session.ServerSession; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Path; import java.nio.file.Paths; import java.security.KeyPair; import java.security.PublicKey; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** * The SSHDaemonService is an {@link automately.core.services.core.AutomatelyService} that allows the cluster * to have SSH operations. This includes direct access to the {@link automately.core.file.VirtualFileSystem} using * SFTP. * */ public class SSHDaemonService extends AutomatelyService { private Cluster cluster; private Logger logger; private SshServer server = null; private Map<String, FileSystem> cachedFileSystems = new HashMap<>(); @Override public void start(Cluster owner) { this.cluster = owner; this.logger = cluster.logger(); JsonObject sshdConfig = coreConfig().getObject("sshd", new JsonObject()); if(!sshdConfig.containsField("sftp_enabled") || !sshdConfig.containsField("port") || !sshdConfig.containsField("host") || !sshdConfig.containsField("host_key")){ logger.info("Creating default configuration."); sshdConfig.putBoolean("sftp_enabled", true); sshdConfig.putNumber("port", 2282); sshdConfig.putString("host", "0.0.0.0"); sshdConfig.putString("host_key", "sshd.ser"); coreConfig().putObject("sshd", sshdConfig); cluster.config().save(); } if(sshdConfig.getBoolean("sftp_enabled", false)){ logger.info("The SSH Daemon is enabled. This is experimental right now."); server = ServerBuilder.builder().build(true); server.setFileSystemFactory(session -> { try { String username = session.getUsername(); if(cachedFileSystems.containsKey(username)){ return cachedFileSystems.get(username); } Collection<User> users = users().values(Predicates.equal("username", username)); if(users.size() == 0){ throw new IOException("The user \"" + session.getUsername() + "\" could not be found."); } FileSystem fs = VirtualFileSystem.getUserFileSystem(users.iterator().next()); cachedFileSystems.put(username, fs); return fs; } catch (Exception e){ e.printStackTrace(); } return null; }); server.setCommandFactory(new SSHCommandFactory()); List<NamedFactory<Command>> subSystemFactoryList = new ArrayList<>(); subSystemFactoryList.add(new SubSystemFactory()); server.setSubsystemFactories(subSystemFactoryList); List<NamedFactory<Compression>> compressionFactoryList = new ArrayList<>(); compressionFactoryList.add(BuiltinCompressions.none); compressionFactoryList.add(BuiltinCompressions.zlib); compressionFactoryList.add(BuiltinCompressions.delayedZlib); server.setCompressionFactories(compressionFactoryList); IMap<String, Integer> authAttempts = cluster().data().getMap("ssh.auth.attempts"); // TODO Finish implementing brute force protection server.setPublickeyAuthenticator(new PublickeyAuthenticator() { private Map<String, DefaultAuthorizedKeysAuthenticator> authenticatorMap = new ConcurrentHashMap<>(); @Override public boolean authenticate(String username, PublicKey publicKey, ServerSession serverSession) { if(!authenticatorMap.containsKey(username)){ try { serverSession.setUsername(username); FileSystem fs = server.getFileSystemFactory().createFileSystem(serverSession); Path authorizedFiles = fs.getPath("/private/authorized_keys"); authenticatorMap.put(username, new DefaultAuthorizedKeysAuthenticator(username, authorizedFiles, false)); } catch (IOException e) { return false; } } return authenticatorMap.get(username).authenticate(username, publicKey, serverSession); } }); server.setPasswordAuthenticator((user, passwordOrKey, serverSession) -> { User mUser = UserData.getUserByUsername(user); // If there has been more than 10 failed attempts within the next 3 minutes we need to deny it.. if (mUser != null && authAttempts.getOrDefault(mUser.username, 0) < 10) { logger.info("Password Authentication: " + mUser.username); boolean passed = UserData.validateUserPassword(mUser, passwordOrKey) || UserData.validateUserKey(mUser, passwordOrKey) || cluster.config().isDebug(); if (!passed) { logger.info("Authentication Failed: " + mUser.username); Integer attempts = authAttempts.getOrDefault(mUser.username, 0); attempts++; authAttempts.put(mUser.username, attempts, 3, TimeUnit.MINUTES); } else { logger.info("Authentication Success: " + mUser.username); } return passed; } return false; }); // By default it is set to listen on localhost for secure purposes. server.setHost(sshdConfig.getString("host", "127.0.0.1")); server.setPort(sshdConfig.getNumber("port", 2282).intValue()); String hostKey = sshdConfig.getString("host_key","sshd_hostkey.ser"); logger.info("Using the hostkey " + hostKey); SimpleGeneratorHostKeyProvider generator = new SimpleGeneratorHostKeyProvider(Paths.get(hostKey)); generator.setKeySize(4098); generator.setAlgorithm("RSA"); List<KeyPair> loadedKeys = generator.loadKeys(); if(loadedKeys.size() > 0){ KeyPair keyPair = loadedKeys.get(0); byte[] encodedPublicKey = keyPair.getPublic().getEncoded(); JsonObject data = new JsonObject(); data.putString("algorithm", keyPair.getPublic().getAlgorithm()); data.putBinary("bytes", encodedPublicKey); cluster().data().persistentMap("sshd.service.storage").set("public_key", data); } server.setKeyPairProvider(generator); try { logger.info("Starting the SshServer Daemon on port " + server.getPort()); server.start(); } catch (Exception e) { e.printStackTrace(); } } } @Override public void stop() { if(server != null){ try { server.stop(true); } catch (Exception e) { e.printStackTrace(); } } } @Override public String name() { return getClass().getCanonicalName(); } }