2014-11-16 23:14:06 -08:00
|
|
|
/*
|
|
|
|
* Copyright 2014 Samoilenko Yuri <kinnalru@gmail.com>
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU General Public License as
|
|
|
|
* published by the Free Software Foundation; either version 2 of
|
|
|
|
* the License or (at your option) version 3 or any later version
|
|
|
|
* accepted by the membership of KDE e.V. (or its successor approved
|
|
|
|
* by the membership of KDE e.V.), which shall act as a proxy
|
|
|
|
* defined in Section 14 of version 3 of the license.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2014-01-07 17:40:21 +04:00
|
|
|
package org.kde.kdeconnect.Plugins.SftpPlugin;
|
|
|
|
|
2014-01-13 00:15:17 +04:00
|
|
|
import android.content.Context;
|
2016-12-11 13:31:17 +01:00
|
|
|
import android.net.Uri;
|
2014-01-07 17:40:21 +04:00
|
|
|
import android.util.Log;
|
|
|
|
|
|
|
|
import org.apache.sshd.SshServer;
|
|
|
|
import org.apache.sshd.common.NamedFactory;
|
2014-01-13 00:15:17 +04:00
|
|
|
import org.apache.sshd.common.Session;
|
2018-08-31 21:04:22 +02:00
|
|
|
import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider;
|
2016-03-03 11:53:34 -08:00
|
|
|
import org.apache.sshd.common.util.SecurityUtils;
|
2014-01-07 17:40:21 +04:00
|
|
|
import org.apache.sshd.server.Command;
|
2016-05-19 08:45:24 -07:00
|
|
|
import org.apache.sshd.server.FileSystemFactory;
|
|
|
|
import org.apache.sshd.server.FileSystemView;
|
2014-01-07 17:40:21 +04:00
|
|
|
import org.apache.sshd.server.PasswordAuthenticator;
|
2014-01-16 09:51:32 +04:00
|
|
|
import org.apache.sshd.server.PublickeyAuthenticator;
|
2016-05-19 08:45:24 -07:00
|
|
|
import org.apache.sshd.server.SshFile;
|
2014-01-07 17:40:21 +04:00
|
|
|
import org.apache.sshd.server.command.ScpCommandFactory;
|
2016-05-19 08:45:24 -07:00
|
|
|
import org.apache.sshd.server.filesystem.NativeFileSystemView;
|
|
|
|
import org.apache.sshd.server.filesystem.NativeSshFile;
|
2016-06-09 13:42:15 +02:00
|
|
|
import org.apache.sshd.server.kex.DHG1;
|
|
|
|
import org.apache.sshd.server.kex.DHG14;
|
2014-01-07 17:40:21 +04:00
|
|
|
import org.apache.sshd.server.session.ServerSession;
|
|
|
|
import org.apache.sshd.server.sftp.SftpSubsystem;
|
2014-01-16 09:51:32 +04:00
|
|
|
import org.kde.kdeconnect.Device;
|
2016-12-11 13:54:21 +01:00
|
|
|
import org.kde.kdeconnect.Helpers.MediaStoreHelper;
|
2016-02-17 04:48:01 -08:00
|
|
|
import org.kde.kdeconnect.Helpers.RandomHelper;
|
2018-08-31 21:04:22 +02:00
|
|
|
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
|
2016-03-15 12:55:25 -07:00
|
|
|
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
2014-01-07 17:40:21 +04:00
|
|
|
|
2014-01-13 00:15:17 +04:00
|
|
|
import java.io.File;
|
2017-08-06 18:09:55 +02:00
|
|
|
import java.io.FileOutputStream;
|
2016-12-11 13:31:17 +01:00
|
|
|
import java.io.IOException;
|
2017-08-06 18:09:55 +02:00
|
|
|
import java.io.OutputStream;
|
|
|
|
import java.io.RandomAccessFile;
|
2016-01-10 08:22:56 -08:00
|
|
|
import java.net.Inet4Address;
|
2014-01-07 17:40:21 +04:00
|
|
|
import java.net.InetAddress;
|
|
|
|
import java.net.NetworkInterface;
|
|
|
|
import java.net.SocketException;
|
2018-08-31 21:04:22 +02:00
|
|
|
import java.security.KeyPair;
|
|
|
|
import java.security.PrivateKey;
|
2014-01-16 09:51:32 +04:00
|
|
|
import java.security.PublicKey;
|
2016-03-03 12:22:22 -08:00
|
|
|
import java.security.Security;
|
2014-01-07 17:40:21 +04:00
|
|
|
import java.util.Arrays;
|
2015-08-10 00:26:58 -07:00
|
|
|
import java.util.Collections;
|
2014-01-07 17:40:21 +04:00
|
|
|
import java.util.Enumeration;
|
|
|
|
|
|
|
|
class SimpleSftpServer {
|
|
|
|
private static final int STARTPORT = 1739;
|
|
|
|
private static final int ENDPORT = 1764;
|
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
static final String USER = "kdeconnect";
|
2014-01-07 17:40:21 +04:00
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
private int port = -1;
|
|
|
|
private boolean started = false;
|
2014-01-07 17:40:21 +04:00
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
private final SimplePasswordAuthenticator passwordAuth = new SimplePasswordAuthenticator();
|
|
|
|
private final SimplePublicKeyAuthenticator keyAuth = new SimplePublicKeyAuthenticator();
|
2014-01-07 17:40:21 +04:00
|
|
|
|
2016-03-03 12:22:22 -08:00
|
|
|
static {
|
2016-06-06 23:59:25 +02:00
|
|
|
Security.insertProviderAt(SslHelper.BC, 1);
|
2016-03-03 12:22:22 -08:00
|
|
|
SecurityUtils.setRegisterBouncyCastle(false);
|
|
|
|
}
|
2018-03-03 16:06:52 +01:00
|
|
|
|
2016-03-03 12:22:22 -08:00
|
|
|
private final SshServer sshd = SshServer.setUpDefaultServer();
|
2014-01-07 17:40:21 +04:00
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
public void init(Context context, Device device) {
|
|
|
|
|
2016-03-03 12:22:22 -08:00
|
|
|
sshd.setKeyExchangeFactories(Arrays.asList(
|
|
|
|
new DHG14.Factory(),
|
|
|
|
new DHG1.Factory()));
|
|
|
|
|
2018-08-31 21:04:22 +02:00
|
|
|
//Reuse this device keys for the ssh connection as well
|
|
|
|
KeyPair[] keyPairList = new KeyPair[1];
|
|
|
|
try {
|
|
|
|
PrivateKey privateKey = RsaHelper.getPrivateKey(context);
|
|
|
|
PublicKey publicKey = RsaHelper.getPublicKey(context);
|
|
|
|
keyPairList[0] = new KeyPair(publicKey, privateKey);
|
|
|
|
} catch (Exception e) {
|
|
|
|
Log.e("SimpleSftpServer", "Can't load device keys");
|
|
|
|
throw new RuntimeException("BLA");
|
|
|
|
}
|
|
|
|
sshd.setKeyPairProvider(new AbstractKeyPairProvider() {
|
|
|
|
@Override
|
|
|
|
protected KeyPair[] loadKeys() {
|
|
|
|
return keyPairList;
|
|
|
|
}
|
|
|
|
});
|
2014-01-07 17:40:21 +04:00
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
sshd.setFileSystemFactory(new AndroidFileSystemFactory(context));
|
2014-01-07 17:40:21 +04:00
|
|
|
sshd.setCommandFactory(new ScpCommandFactory());
|
2018-03-03 16:06:52 +01:00
|
|
|
sshd.setSubsystemFactories(Collections.singletonList((NamedFactory<Command>) new SftpSubsystem.Factory()));
|
2014-01-07 17:40:21 +04:00
|
|
|
|
2016-02-17 04:06:35 -08:00
|
|
|
if (device.publicKey != null) {
|
2016-12-11 13:31:17 +01:00
|
|
|
keyAuth.deviceKey = device.publicKey;
|
2016-02-17 04:06:35 -08:00
|
|
|
sshd.setPublickeyAuthenticator(keyAuth);
|
|
|
|
}
|
2016-06-09 15:00:42 +02:00
|
|
|
sshd.setPasswordAuthenticator(passwordAuth);
|
2014-01-07 17:40:21 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
public boolean start() {
|
|
|
|
if (!started) {
|
2016-02-17 04:48:01 -08:00
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
passwordAuth.password = RandomHelper.randomString(28);
|
2014-01-07 17:40:21 +04:00
|
|
|
|
|
|
|
port = STARTPORT;
|
2018-03-03 16:06:52 +01:00
|
|
|
while (!started) {
|
2014-01-07 17:40:21 +04:00
|
|
|
try {
|
|
|
|
sshd.setPort(port);
|
|
|
|
sshd.start();
|
|
|
|
started = true;
|
2018-03-03 16:06:52 +01:00
|
|
|
} catch (Exception e) {
|
2016-03-03 12:22:22 -08:00
|
|
|
e.printStackTrace();
|
2014-01-07 17:40:21 +04:00
|
|
|
port++;
|
|
|
|
if (port >= ENDPORT) {
|
|
|
|
port = -1;
|
|
|
|
Log.e("SftpServer", "No more ports available");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void stop() {
|
|
|
|
try {
|
|
|
|
started = false;
|
2017-06-21 03:04:22 +02:00
|
|
|
sshd.stop(true);
|
2016-03-03 11:53:34 -08:00
|
|
|
} catch (Exception e) {
|
|
|
|
e.printStackTrace();
|
2014-01-07 17:40:21 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
public String getPassword() {
|
|
|
|
return passwordAuth.password;
|
|
|
|
}
|
|
|
|
|
|
|
|
public int getPort() {
|
|
|
|
return port;
|
|
|
|
}
|
|
|
|
|
2014-01-07 17:40:21 +04:00
|
|
|
public String getLocalIpAddress() {
|
2014-01-15 19:18:43 +01:00
|
|
|
String ip6 = null;
|
2014-01-07 17:40:21 +04:00
|
|
|
try {
|
2018-03-03 16:06:52 +01:00
|
|
|
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
|
2014-01-07 17:40:21 +04:00
|
|
|
NetworkInterface intf = en.nextElement();
|
2017-05-06 14:26:37 +02:00
|
|
|
|
|
|
|
// Anything with rmnet is related to cellular connections or USB
|
|
|
|
// tethering mechanisms. See:
|
|
|
|
//
|
|
|
|
// https://android.googlesource.com/kernel/msm/+/android-msm-flo-3.4-kitkat-mr1/Documentation/usb/gadget_rmnet.txt
|
|
|
|
//
|
|
|
|
// If we run across an interface that has this, we can safely
|
|
|
|
// ignore it. In fact, it's much safer to do. If we don't, we
|
|
|
|
// might get invalid IP adddresses out of it.
|
2018-03-03 16:06:52 +01:00
|
|
|
if (intf.getDisplayName().contains("rmnet")) continue;
|
2017-05-06 14:26:37 +02:00
|
|
|
|
2018-03-03 16:06:52 +01:00
|
|
|
for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
|
2014-01-07 17:40:21 +04:00
|
|
|
InetAddress inetAddress = enumIpAddr.nextElement();
|
|
|
|
if (!inetAddress.isLoopbackAddress()) {
|
2014-01-15 19:18:43 +01:00
|
|
|
String address = inetAddress.getHostAddress();
|
2018-03-03 16:06:52 +01:00
|
|
|
if (inetAddress instanceof Inet4Address) { //Prefer IPv4 over IPv6, because sshfs doesn't seem to like IPv6
|
2014-01-15 19:18:43 +01:00
|
|
|
return address;
|
|
|
|
} else {
|
|
|
|
ip6 = address;
|
|
|
|
}
|
2014-01-07 17:40:21 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (SocketException ex) {
|
|
|
|
}
|
2014-01-15 19:18:43 +01:00
|
|
|
return ip6;
|
2014-01-07 17:40:21 +04:00
|
|
|
}
|
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
static class AndroidFileSystemFactory implements FileSystemFactory {
|
|
|
|
|
|
|
|
final private Context context;
|
2014-01-13 00:15:17 +04:00
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
public AndroidFileSystemFactory(Context context) {
|
|
|
|
this.context = context;
|
|
|
|
}
|
2014-01-13 00:15:17 +04:00
|
|
|
|
2016-06-06 23:59:25 +02:00
|
|
|
@Override
|
2016-05-19 08:45:24 -07:00
|
|
|
public FileSystemView createFileSystemView(final Session username) {
|
2016-12-11 13:31:17 +01:00
|
|
|
return new AndroidFileSystemView(username.getUsername(), context);
|
2016-05-19 08:45:24 -07:00
|
|
|
}
|
2014-01-13 00:15:17 +04:00
|
|
|
}
|
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
static class AndroidFileSystemView extends NativeFileSystemView {
|
|
|
|
|
|
|
|
final private String userName;
|
|
|
|
final private Context context;
|
|
|
|
|
|
|
|
public AndroidFileSystemView(final String userName, Context context) {
|
|
|
|
super(userName, true);
|
2016-05-19 08:45:24 -07:00
|
|
|
this.userName = userName;
|
2016-12-11 13:31:17 +01:00
|
|
|
this.context = context;
|
2016-05-19 08:45:24 -07:00
|
|
|
}
|
2016-12-11 13:31:17 +01:00
|
|
|
|
2016-05-19 08:45:24 -07:00
|
|
|
@Override
|
2016-12-11 13:31:17 +01:00
|
|
|
protected SshFile getFile(final String dir, final String file) {
|
|
|
|
File fileObj = new File(dir, file);
|
|
|
|
return new AndroidSshFile(fileObj, userName, context);
|
2016-05-19 08:45:24 -07:00
|
|
|
}
|
2016-12-11 13:31:17 +01:00
|
|
|
}
|
2014-01-13 00:15:17 +04:00
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
static class AndroidSshFile extends NativeSshFile {
|
|
|
|
|
|
|
|
final private Context context;
|
2016-12-11 13:54:21 +01:00
|
|
|
final private File file;
|
2016-12-11 13:31:17 +01:00
|
|
|
|
|
|
|
public AndroidSshFile(final File file, final String userName, Context context) {
|
|
|
|
super(file.getAbsolutePath(), file, userName);
|
|
|
|
this.context = context;
|
2016-12-11 13:54:21 +01:00
|
|
|
this.file = file;
|
2016-05-19 08:45:24 -07:00
|
|
|
}
|
2014-01-13 00:15:17 +04:00
|
|
|
|
2017-08-06 18:09:55 +02:00
|
|
|
@Override
|
|
|
|
public OutputStream createOutputStream(long offset) throws IOException {
|
|
|
|
if (!isWritable()) {
|
|
|
|
throw new IOException("No write permission : " + file.getName());
|
|
|
|
}
|
|
|
|
|
|
|
|
final RandomAccessFile raf = new RandomAccessFile(file, "rw");
|
|
|
|
try {
|
|
|
|
if (offset < raf.length()) {
|
|
|
|
throw new IOException("Your SSHFS is bugged"); //SSHFS 3.0 and 3.2 cause data corruption, abort the transfer if this happens
|
|
|
|
}
|
|
|
|
raf.setLength(offset);
|
|
|
|
raf.seek(offset);
|
|
|
|
|
|
|
|
return new FileOutputStream(raf.getFD()) {
|
|
|
|
public void close() throws IOException {
|
|
|
|
super.close();
|
|
|
|
raf.close();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
} catch (IOException e) {
|
|
|
|
raf.close();
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
@Override
|
|
|
|
public boolean delete() {
|
|
|
|
//Log.e("Sshd", "deleting file");
|
|
|
|
boolean ret = super.delete();
|
|
|
|
if (ret) {
|
2016-12-11 13:54:21 +01:00
|
|
|
MediaStoreHelper.indexFile(context, Uri.fromFile(file));
|
2016-12-11 13:31:17 +01:00
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
|
2016-05-19 08:45:24 -07:00
|
|
|
}
|
2014-01-13 00:15:17 +04:00
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
@Override
|
|
|
|
public boolean create() throws IOException {
|
|
|
|
//Log.e("Sshd", "creating file");
|
|
|
|
boolean ret = super.create();
|
|
|
|
if (ret) {
|
2016-12-11 13:54:21 +01:00
|
|
|
MediaStoreHelper.indexFile(context, Uri.fromFile(file));
|
2016-12-11 13:31:17 +01:00
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
|
2016-05-19 08:45:24 -07:00
|
|
|
}
|
2014-11-16 23:14:06 -08:00
|
|
|
}
|
2016-06-06 23:59:25 +02:00
|
|
|
|
|
|
|
static class SimplePasswordAuthenticator implements PasswordAuthenticator {
|
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
public String password;
|
2016-06-06 23:59:25 +02:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean authenticate(String user, String password, ServerSession session) {
|
2016-12-11 13:31:17 +01:00
|
|
|
return user.equals(SimpleSftpServer.USER) && password.equals(this.password);
|
2016-06-06 23:59:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static class SimplePublicKeyAuthenticator implements PublickeyAuthenticator {
|
|
|
|
|
2016-12-11 13:31:17 +01:00
|
|
|
public PublicKey deviceKey;
|
2016-06-06 23:59:25 +02:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean authenticate(String user, PublicKey key, ServerSession session) {
|
2016-12-11 13:31:17 +01:00
|
|
|
return deviceKey.equals(key);
|
2016-06-06 23:59:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|