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