package org.primftpd.services; import android.os.Looper; import android.widget.Toast; import org.apache.ftpserver.ftplet.Authentication; import org.apache.ftpserver.ftplet.AuthenticationFailedException; import org.apache.ftpserver.usermanager.AnonymousAuthentication; import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication; import org.apache.ftpserver.util.IoUtils; import org.apache.sshd.SshServer; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.Session; import org.apache.sshd.common.file.FileSystemFactory; import org.apache.sshd.common.file.FileSystemView; import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory; import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider; import org.apache.sshd.server.Command; import org.apache.sshd.server.PasswordAuthenticator; import org.apache.sshd.server.command.ScpCommandFactory; import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.sftp.SftpSubsystem; import org.primftpd.AndroidPrefsUserManager; import org.primftpd.PrimitiveFtpdActivity; import org.primftpd.R; import org.primftpd.filesystem.SshFileSystemView; import org.primftpd.util.Defaults; import org.primftpd.util.KeyInfoProvider; import org.primftpd.util.StringUtils; import java.io.FileInputStream; import java.io.IOException; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.util.ArrayList; import java.util.List; /** * Implements a SSH server. Intended to be used for sftp. */ public class SshServerService extends AbstractServerService { private SshServer sshServer; @Override protected ServerServiceHandler createServiceHandler( Looper serviceLooper, AbstractServerService service) { return new ServerServiceHandler(serviceLooper, service, getServiceName()); } @Override protected Object getServer() { return sshServer; } @Override protected int getPort() { return prefsBean.getSecurePort(); } @Override protected String getServiceName() { return "sftp-ssh"; } @Override protected void stopServer() { try { sshServer.stop(); } catch (InterruptedException e) { logger.error("could not stop ssh server", e); } sshServer = null; } @Override protected boolean launchServer() { sshServer = SshServer.setUpDefaultServer(); sshServer.setPort(prefsBean.getSecurePort()); // causes exception when not set sshServer.setIoServiceFactoryFactory(new MinaServiceFactoryFactory()); // enable scp and sftp sshServer.setCommandFactory(new ScpCommandFactory()); List<NamedFactory<Command>> factoryList = new ArrayList<>(1); factoryList.add(new SftpSubsystem.Factory()); sshServer.setSubsystemFactories(factoryList); // PasswordAuthenticator based on android preferences if (StringUtils.isNotEmpty(prefsBean.getPassword()) || prefsBean.isAnonymousLogin()) { final AndroidPrefsUserManager userManager = new AndroidPrefsUserManager(prefsBean); sshServer.setPasswordAuthenticator(new PasswordAuthenticator() { @Override public boolean authenticate( String username, String password, ServerSession session) { logger.debug("password auth for user: {}", username); Authentication authentication = AndroidPrefsUserManager.ANONYMOUS_USER_NAME.equals(username) ? new AnonymousAuthentication() : new UsernamePasswordAuthentication(username, password); try { userManager.authenticate(authentication); } catch (AuthenticationFailedException e) { logger.debug("AuthenticationFailed", e); return false; } return true; } }); } if (prefsBean.isPubKeyAuth()) { String pubKeyPath = Defaults.PUB_KEY_AUTH_KEY_PATH; String pubKeyPathOld = Defaults.PUB_KEY_AUTH_KEY_PATH_OLD; KeyInfoProvider keyInfoProvider = new KeyInfoProvider(); final List<PublicKey> pubKeys = keyInfoProvider.readKeyAuthKeys(pubKeyPath, false); pubKeys.addAll(keyInfoProvider.readKeyAuthKeys(pubKeyPathOld, true)); logger.info("loaded {} keys for public key auth", pubKeys.size()); if (!pubKeys.isEmpty()) { sshServer.setPublickeyAuthenticator(new PubKeyAuthenticator(pubKeys)); } else { Toast.makeText( getApplicationContext(), getText(R.string.couldNotReadKeyAuthKey), Toast.LENGTH_SHORT).show(); } } // android filesystem view sshServer.setFileSystemFactory(new FileSystemFactory() { @Override public FileSystemView createFileSystemView(Session session) throws IOException { return new SshFileSystemView(prefsBean.getStartDir(), session); } }); try { // XXX preference to enable shell? seems to need root to access /dev/tty // sshServer.setShellFactory(new ProcessShellFactory(new String[] { // "/system/bin/sh", // "-i", // "-l" // })); // read keys here, cannot open private files on server callback final List<KeyPair> keys = loadKeys(); // keys may not be present when started via widget if (!keys.isEmpty()) { // setKeyPairProvider sshServer.setKeyPairProvider(new AbstractKeyPairProvider() { @Override public Iterable<KeyPair> loadKeys() { // just return keys that have been loaded before return keys; } }); sshServer.start(); return true; } } catch (Exception e) { sshServer = null; handleServerStartError(e); } return false; } protected List<KeyPair> loadKeys() { List<KeyPair> keyPairList = new ArrayList<>(1); FileInputStream pubkeyFis = null; FileInputStream privkeyFis = null; try { // read pub key KeyInfoProvider keyInfoProvider = new KeyInfoProvider(); pubkeyFis = openFileInput(PrimitiveFtpdActivity.PUBLICKEY_FILENAME); PublicKey publicKey = keyInfoProvider.readPublicKey(pubkeyFis); // read priv key from it's own file privkeyFis = openFileInput(PrimitiveFtpdActivity.PRIVATEKEY_FILENAME); PrivateKey privateKey = keyInfoProvider.readPrivatekey(privkeyFis); // return key pair keyPairList.add(new KeyPair(publicKey, privateKey)); } catch (Exception e) { logger.debug("could not read key: " + e.getMessage(), e); } finally { if (pubkeyFis != null) { IoUtils.close(pubkeyFis); } if (privkeyFis != null) { IoUtils.close(privkeyFis); } } return keyPairList; } }