Changeset View
Changeset View
Standalone View
Standalone View
src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java
Show All 15 Lines | |||||
16 | * | 16 | * | ||
17 | * You should have received a copy of the GNU General Public License | 17 | * You should have received a copy of the GNU General Public License | ||
18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
19 | */ | 19 | */ | ||
20 | 20 | | |||
21 | package org.kde.kdeconnect.Plugins.SftpPlugin; | 21 | package org.kde.kdeconnect.Plugins.SftpPlugin; | ||
22 | 22 | | |||
23 | import android.content.Context; | 23 | import android.content.Context; | ||
24 | import android.net.Uri; | | |||
25 | import android.util.Log; | 24 | import android.util.Log; | ||
26 | 25 | | |||
27 | import org.apache.sshd.SshServer; | 26 | import org.apache.sshd.SshServer; | ||
28 | import org.apache.sshd.common.Session; | | |||
29 | import org.apache.sshd.common.file.FileSystemFactory; | | |||
30 | import org.apache.sshd.common.file.FileSystemView; | | |||
31 | import org.apache.sshd.common.file.nativefs.NativeFileSystemView; | | |||
32 | import org.apache.sshd.common.file.nativefs.NativeSshFile; | | |||
33 | import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider; | 27 | import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider; | ||
34 | import org.apache.sshd.common.util.SecurityUtils; | 28 | import org.apache.sshd.common.util.SecurityUtils; | ||
35 | import org.apache.sshd.server.PasswordAuthenticator; | 29 | import org.apache.sshd.server.PasswordAuthenticator; | ||
36 | import org.apache.sshd.server.PublickeyAuthenticator; | 30 | import org.apache.sshd.server.PublickeyAuthenticator; | ||
37 | import org.apache.sshd.server.command.ScpCommandFactory; | 31 | import org.apache.sshd.server.command.ScpCommandFactory; | ||
38 | import org.apache.sshd.server.kex.DHG1; | 32 | import org.apache.sshd.server.kex.DHG1; | ||
39 | import org.apache.sshd.server.kex.DHG14; | 33 | import org.apache.sshd.server.kex.DHG14; | ||
40 | import org.apache.sshd.server.session.ServerSession; | 34 | import org.apache.sshd.server.session.ServerSession; | ||
41 | import org.apache.sshd.server.sftp.SftpSubsystem; | 35 | import org.apache.sshd.server.sftp.SftpSubsystem; | ||
42 | import org.kde.kdeconnect.Device; | 36 | import org.kde.kdeconnect.Device; | ||
43 | import org.kde.kdeconnect.Helpers.MediaStoreHelper; | | |||
44 | import org.kde.kdeconnect.Helpers.RandomHelper; | 37 | import org.kde.kdeconnect.Helpers.RandomHelper; | ||
45 | import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper; | 38 | import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper; | ||
46 | import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; | 39 | import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; | ||
47 | 40 | | |||
48 | import java.io.File; | | |||
49 | import java.io.FileOutputStream; | | |||
50 | import java.io.IOException; | | |||
51 | import java.io.OutputStream; | | |||
52 | import java.io.RandomAccessFile; | | |||
53 | import java.net.Inet4Address; | 41 | import java.net.Inet4Address; | ||
54 | import java.net.InetAddress; | 42 | import java.net.InetAddress; | ||
55 | import java.net.NetworkInterface; | 43 | import java.net.NetworkInterface; | ||
56 | import java.net.SocketException; | 44 | import java.net.SocketException; | ||
57 | import java.security.GeneralSecurityException; | 45 | import java.security.GeneralSecurityException; | ||
58 | import java.security.KeyPair; | 46 | import java.security.KeyPair; | ||
59 | import java.security.PrivateKey; | 47 | import java.security.PrivateKey; | ||
60 | import java.security.PublicKey; | 48 | import java.security.PublicKey; | ||
61 | import java.security.Security; | 49 | import java.security.Security; | ||
62 | import java.util.Arrays; | 50 | import java.util.Arrays; | ||
63 | import java.util.Collections; | 51 | import java.util.Collections; | ||
64 | import java.util.Enumeration; | 52 | import java.util.Enumeration; | ||
53 | import java.util.List; | ||||
65 | 54 | | |||
66 | class SimpleSftpServer { | 55 | class SimpleSftpServer { | ||
67 | private static final int STARTPORT = 1739; | 56 | private static final int STARTPORT = 1739; | ||
68 | private static final int ENDPORT = 1764; | 57 | private static final int ENDPORT = 1764; | ||
69 | 58 | | |||
70 | static final String USER = "kdeconnect"; | 59 | static final String USER = "kdeconnect"; | ||
71 | 60 | | |||
72 | private int port = -1; | 61 | private int port = -1; | ||
73 | private boolean started = false; | 62 | private boolean started = false; | ||
74 | 63 | | |||
75 | private final SimplePasswordAuthenticator passwordAuth = new SimplePasswordAuthenticator(); | 64 | private final SimplePasswordAuthenticator passwordAuth = new SimplePasswordAuthenticator(); | ||
76 | private final SimplePublicKeyAuthenticator keyAuth = new SimplePublicKeyAuthenticator(); | 65 | private final SimplePublicKeyAuthenticator keyAuth = new SimplePublicKeyAuthenticator(); | ||
77 | 66 | | |||
78 | static { | 67 | static { | ||
79 | Security.insertProviderAt(SslHelper.BC, 1); | 68 | Security.insertProviderAt(SslHelper.BC, 1); | ||
80 | SecurityUtils.setRegisterBouncyCastle(false); | 69 | SecurityUtils.setRegisterBouncyCastle(false); | ||
81 | } | 70 | } | ||
82 | 71 | | |||
83 | private final SshServer sshd = SshServer.setUpDefaultServer(); | 72 | private final SshServer sshd = SshServer.setUpDefaultServer(); | ||
73 | private AndroidFileSystemFactory fileSystemFactory; | ||||
84 | 74 | | |||
85 | void init(Context context, Device device) throws GeneralSecurityException { | 75 | void init(Context context, Device device) throws GeneralSecurityException { | ||
86 | 76 | | |||
87 | sshd.setKeyExchangeFactories(Arrays.asList( | 77 | sshd.setKeyExchangeFactories(Arrays.asList( | ||
88 | new DHG14.Factory(), | 78 | new DHG14.Factory(), | ||
89 | new DHG1.Factory())); | 79 | new DHG1.Factory())); | ||
90 | 80 | | |||
91 | //Reuse this device keys for the ssh connection as well | 81 | //Reuse this device keys for the ssh connection as well | ||
92 | final KeyPair keyPair; | 82 | final KeyPair keyPair; | ||
93 | PrivateKey privateKey = RsaHelper.getPrivateKey(context); | 83 | PrivateKey privateKey = RsaHelper.getPrivateKey(context); | ||
94 | PublicKey publicKey = RsaHelper.getPublicKey(context); | 84 | PublicKey publicKey = RsaHelper.getPublicKey(context); | ||
95 | keyPair = new KeyPair(publicKey, privateKey); | 85 | keyPair = new KeyPair(publicKey, privateKey); | ||
96 | sshd.setKeyPairProvider(new AbstractKeyPairProvider() { | 86 | sshd.setKeyPairProvider(new AbstractKeyPairProvider() { | ||
97 | @Override | 87 | @Override | ||
98 | public Iterable<KeyPair> loadKeys() { | 88 | public Iterable<KeyPair> loadKeys() { | ||
99 | return Collections.singletonList(keyPair); | 89 | return Collections.singletonList(keyPair); | ||
100 | } | 90 | } | ||
101 | }); | 91 | }); | ||
102 | 92 | | |||
103 | sshd.setFileSystemFactory(new AndroidFileSystemFactory(context)); | 93 | fileSystemFactory = new AndroidFileSystemFactory(context); | ||
94 | sshd.setFileSystemFactory(fileSystemFactory); | ||||
104 | sshd.setCommandFactory(new ScpCommandFactory()); | 95 | sshd.setCommandFactory(new ScpCommandFactory()); | ||
105 | sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystem.Factory())); | 96 | sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystem.Factory())); | ||
106 | 97 | | |||
107 | if (device.publicKey != null) { | 98 | if (device.publicKey != null) { | ||
108 | keyAuth.deviceKey = device.publicKey; | 99 | keyAuth.deviceKey = device.publicKey; | ||
109 | } else { | 100 | } else { | ||
110 | keyAuth.deviceKey = device.certificate.getPublicKey(); | 101 | keyAuth.deviceKey = device.certificate.getPublicKey(); | ||
111 | } | 102 | } | ||
112 | sshd.setPublickeyAuthenticator(keyAuth); | 103 | sshd.setPublickeyAuthenticator(keyAuth); | ||
113 | sshd.setPasswordAuthenticator(passwordAuth); | 104 | sshd.setPasswordAuthenticator(passwordAuth); | ||
114 | } | 105 | } | ||
115 | 106 | | |||
116 | public boolean start() { | 107 | public boolean start(List<SftpPlugin.StorageInfo> storageInfoList) { | ||
117 | if (!started) { | 108 | if (!started) { | ||
118 | 109 | fileSystemFactory.initRoots(storageInfoList); | |||
119 | passwordAuth.password = RandomHelper.randomString(28); | 110 | passwordAuth.password = RandomHelper.randomString(28); | ||
120 | 111 | | |||
121 | port = STARTPORT; | 112 | port = STARTPORT; | ||
122 | while (!started) { | 113 | while (!started) { | ||
123 | try { | 114 | try { | ||
124 | sshd.setPort(port); | 115 | sshd.setPort(port); | ||
125 | sshd.start(); | 116 | sshd.start(); | ||
126 | started = true; | 117 | started = true; | ||
Show All 16 Lines | 133 | public void stop() { | |||
143 | try { | 134 | try { | ||
144 | started = false; | 135 | started = false; | ||
145 | sshd.stop(true); | 136 | sshd.stop(true); | ||
146 | } catch (Exception e) { | 137 | } catch (Exception e) { | ||
147 | e.printStackTrace(); | 138 | e.printStackTrace(); | ||
148 | } | 139 | } | ||
149 | } | 140 | } | ||
150 | 141 | | |||
142 | public boolean isStarted() { | ||||
143 | return started; | ||||
144 | } | ||||
145 | | ||||
151 | String getPassword() { | 146 | String getPassword() { | ||
152 | return passwordAuth.password; | 147 | return passwordAuth.password; | ||
153 | } | 148 | } | ||
154 | 149 | | |||
155 | int getPort() { | 150 | int getPort() { | ||
156 | return port; | 151 | return port; | ||
157 | } | 152 | } | ||
158 | 153 | | |||
Show All 25 Lines | 170 | for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { | |||
184 | } | 179 | } | ||
185 | } | 180 | } | ||
186 | } | 181 | } | ||
187 | } catch (SocketException ignored) { | 182 | } catch (SocketException ignored) { | ||
188 | } | 183 | } | ||
189 | return ip6; | 184 | return ip6; | ||
190 | } | 185 | } | ||
191 | 186 | | |||
192 | static class AndroidFileSystemFactory implements FileSystemFactory { | | |||
193 | | ||||
194 | final private Context context; | | |||
195 | | ||||
196 | AndroidFileSystemFactory(Context context) { | | |||
197 | this.context = context; | | |||
198 | } | | |||
199 | | ||||
200 | @Override | | |||
201 | public FileSystemView createFileSystemView(final Session username) { | | |||
202 | return new AndroidFileSystemView(username.getUsername(), context); | | |||
203 | } | | |||
204 | } | | |||
205 | | ||||
206 | static class AndroidFileSystemView extends NativeFileSystemView { | | |||
207 | | ||||
208 | final private Context context; | | |||
209 | | ||||
210 | AndroidFileSystemView(final String userName, Context context) { | | |||
211 | super(userName, true); | | |||
212 | this.context = context; | | |||
213 | } | | |||
214 | | ||||
215 | // NativeFileSystemView.getFile(), NativeSshFile.getParentFile() and NativeSshFile.listSshFiles() call | | |||
216 | // createNativeSshFile to create new NativeSshFiles so override that instead of getFile() to always create a AndroidSshFile | | |||
217 | @Override | | |||
218 | public AndroidSshFile createNativeSshFile(String name, File file, String username) { | | |||
219 | return new AndroidSshFile(this, name, file, username, context); | | |||
220 | } | | |||
221 | } | | |||
222 | | ||||
223 | static class AndroidSshFile extends NativeSshFile { | | |||
224 | | ||||
225 | final private Context context; | | |||
226 | final private File file; | | |||
227 | | ||||
228 | AndroidSshFile(final AndroidFileSystemView view, String name, final File file, final String userName, Context context) { | | |||
229 | super(view, name, file, userName); | | |||
230 | this.context = context; | | |||
231 | this.file = file; | | |||
232 | } | | |||
233 | | ||||
234 | @Override | | |||
235 | public OutputStream createOutputStream(long offset) throws IOException { | | |||
236 | if (!isWritable()) { | | |||
237 | throw new IOException("No write permission : " + file.getName()); | | |||
238 | } | | |||
239 | | ||||
240 | final RandomAccessFile raf = new RandomAccessFile(file, "rw"); | | |||
241 | try { | | |||
242 | if (offset < raf.length()) { | | |||
243 | throw new IOException("Your SSHFS is bugged"); //SSHFS 3.0 and 3.2 cause data corruption, abort the transfer if this happens | | |||
244 | } | | |||
245 | raf.setLength(offset); | | |||
246 | raf.seek(offset); | | |||
247 | | ||||
248 | return new FileOutputStream(raf.getFD()) { | | |||
249 | public void close() throws IOException { | | |||
250 | super.close(); | | |||
251 | raf.close(); | | |||
252 | } | | |||
253 | }; | | |||
254 | } catch (IOException e) { | | |||
255 | raf.close(); | | |||
256 | throw e; | | |||
257 | } | | |||
258 | } | | |||
259 | | ||||
260 | @Override | | |||
261 | public boolean delete() { | | |||
262 | //Log.e("Sshd", "deleting file"); | | |||
263 | boolean ret = super.delete(); | | |||
264 | if (ret) { | | |||
265 | MediaStoreHelper.indexFile(context, Uri.fromFile(file)); | | |||
266 | } | | |||
267 | return ret; | | |||
268 | | ||||
269 | } | | |||
270 | | ||||
271 | @Override | | |||
272 | public boolean create() throws IOException { | | |||
273 | //Log.e("Sshd", "creating file"); | | |||
274 | boolean ret = super.create(); | | |||
275 | if (ret) { | | |||
276 | MediaStoreHelper.indexFile(context, Uri.fromFile(file)); | | |||
277 | } | | |||
278 | return ret; | | |||
279 | | ||||
280 | } | | |||
281 | | ||||
282 | // Based on https://github.com/wolpi/prim-ftpd/blob/master/primitiveFTPd/src/org/primftpd/filesystem/FsFile.java | | |||
283 | @Override | | |||
284 | public boolean doesExist() { | | |||
285 | boolean exists = file.exists(); | | |||
286 | | ||||
287 | if (!exists) { | | |||
288 | // file.exists() returns false when we don't have read permission | | |||
289 | // try to figure out if it really does not exist | | |||
290 | File parentFile = file.getParentFile(); | | |||
291 | File[] children = parentFile.listFiles(); | | |||
292 | if (children != null) { | | |||
293 | for (File child : children) { | | |||
294 | if (file.equals(child)) { | | |||
295 | exists = true; | | |||
296 | break; | | |||
297 | } | | |||
298 | } | | |||
299 | } | | |||
300 | } | | |||
301 | | ||||
302 | return exists; | | |||
303 | } | | |||
304 | } | | |||
305 | | ||||
306 | static class SimplePasswordAuthenticator implements PasswordAuthenticator { | 187 | static class SimplePasswordAuthenticator implements PasswordAuthenticator { | ||
307 | 188 | | |||
308 | String password; | 189 | String password; | ||
309 | 190 | | |||
310 | @Override | 191 | @Override | ||
311 | public boolean authenticate(String user, String password, ServerSession session) { | 192 | public boolean authenticate(String user, String password, ServerSession session) { | ||
312 | return user.equals(SimpleSftpServer.USER) && password.equals(this.password); | 193 | return user.equals(SimpleSftpServer.USER) && password.equals(this.password); | ||
313 | } | 194 | } | ||
Show All 14 Lines |