1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.vfs2.provider.sftp;
18
19 import static org.apache.commons.vfs2.VfsTestUtils.getTestDirectory;
20
21 import java.io.File;
22 import java.io.FileNotFoundException;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.OutputStream;
26 import java.io.PrintStream;
27 import java.net.InetSocketAddress;
28 import java.net.Socket;
29 import java.nio.file.Files;
30 import java.nio.file.Path;
31 import java.time.Duration;
32 import java.util.ArrayList;
33 import java.util.List;
34 import java.util.TreeMap;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
38 import org.apache.commons.io.file.PathUtils;
39 import org.apache.commons.lang3.StringUtils;
40 import org.apache.commons.vfs2.AbstractProviderTestConfig;
41 import org.apache.commons.vfs2.FileObject;
42 import org.apache.commons.vfs2.FileSystemManager;
43 import org.apache.commons.vfs2.FileSystemOptions;
44 import org.apache.commons.vfs2.ProviderTestSuite;
45 import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
46 import org.apache.ftpserver.ftplet.FtpException;
47 import org.apache.sshd.SshServer;
48 import org.apache.sshd.common.NamedFactory;
49 import org.apache.sshd.common.Session;
50 import org.apache.sshd.common.SshException;
51 import org.apache.sshd.common.session.AbstractSession;
52 import org.apache.sshd.common.util.Buffer;
53 import org.apache.sshd.common.util.SecurityUtils;
54 import org.apache.sshd.server.Command;
55 import org.apache.sshd.server.Environment;
56 import org.apache.sshd.server.ExitCallback;
57 import org.apache.sshd.server.FileSystemFactory;
58 import org.apache.sshd.server.FileSystemView;
59 import org.apache.sshd.server.ForwardingFilter;
60 import org.apache.sshd.server.SshFile;
61 import org.apache.sshd.server.auth.UserAuthNone;
62 import org.apache.sshd.server.command.ScpCommandFactory;
63 import org.apache.sshd.server.filesystem.NativeSshFile;
64 import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider;
65 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
66 import org.apache.sshd.server.session.ServerSession;
67 import org.apache.sshd.server.session.SessionFactory;
68 import org.apache.sshd.server.sftp.SftpSubsystem;
69
70 import com.jcraft.jsch.SftpATTRS;
71 import com.jcraft.jsch.TestIdentityRepositoryFactory;
72
73
74
75
76
77
78
79 abstract class AbstractSftpProviderTestCase extends AbstractProviderTestConfig {
80
81 private static class MySftpSubsystem extends SftpSubsystem {
82 TreeMap<String, Integer> permissions = new TreeMap<>();
83 private int _version;
84
85 @Override
86 protected void process(final Buffer buffer) throws IOException {
87 final int rpos = buffer.rpos();
88 final int length = buffer.getInt();
89 final int type = buffer.getByte();
90 final int id = buffer.getInt();
91
92 switch (type) {
93 case SSH_FXP_SETSTAT:
94 case SSH_FXP_FSETSTAT: {
95
96 final String path = buffer.getString();
97
98 final SftpAttrs attrs = new SftpAttrs(buffer);
99 permissions.put(path, attrs.permissions);
100
101 break;
102 }
103
104 case SSH_FXP_REMOVE: {
105
106 final String path = buffer.getString();
107 permissions.remove(path);
108
109 break;
110 }
111
112 case SSH_FXP_INIT: {
113
114 _version = id;
115 break;
116 }
117 }
118
119 buffer.rpos(rpos);
120 super.process(buffer);
121
122 }
123
124 @Override
125 protected void writeAttrs(final Buffer buffer, final SshFile file, final int flags) throws IOException {
126 if (!file.doesExist()) {
127 throw new FileNotFoundException(file.getAbsolutePath());
128 }
129
130 int p = 0;
131
132 final Integer cached = permissions.get(file.getAbsolutePath());
133 if (cached != null) {
134
135
136 p |= cached;
137 } else {
138
139 if (file.isReadable()) {
140 p |= S_IRUSR;
141 }
142 if (file.isWritable()) {
143 p |= S_IWUSR;
144 }
145 if (file.isExecutable()) {
146 p |= S_IXUSR;
147 }
148 }
149
150 if (_version >= 4) {
151 final long size = file.getSize();
152
153 final long lastModif = file.getLastModified();
154 if (file.isFile()) {
155 buffer.putInt(SSH_FILEXFER_ATTR_PERMISSIONS);
156 buffer.putByte((byte) SSH_FILEXFER_TYPE_REGULAR);
157 buffer.putInt(p);
158 } else if (file.isDirectory()) {
159 buffer.putInt(SSH_FILEXFER_ATTR_PERMISSIONS);
160 buffer.putByte((byte) SSH_FILEXFER_TYPE_DIRECTORY);
161 buffer.putInt(p);
162 } else {
163 buffer.putInt(0);
164 buffer.putByte((byte) SSH_FILEXFER_TYPE_UNKNOWN);
165 }
166 } else {
167 if (file.isFile()) {
168 p |= 0100000;
169 }
170 if (file.isDirectory()) {
171 p |= 0040000;
172 }
173
174 if (file.isFile()) {
175 buffer.putInt(SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_ACMODTIME);
176 buffer.putLong(file.getSize());
177 buffer.putInt(p);
178 buffer.putInt(file.getLastModified() / 1000);
179 buffer.putInt(file.getLastModified() / 1000);
180 } else if (file.isDirectory()) {
181 buffer.putInt(SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_ACMODTIME);
182 buffer.putInt(p);
183 buffer.putInt(file.getLastModified() / 1000);
184 buffer.putInt(file.getLastModified() / 1000);
185 } else {
186 buffer.putInt(0);
187 }
188 }
189 }
190
191 }
192
193 private static class SftpAttrs {
194 int flags;
195 private int uid;
196 long size;
197 private int gid;
198 private int atime;
199 private int permissions;
200 private int mtime;
201 private String[] extended;
202
203 private SftpAttrs(final Buffer buf) {
204 final int flags = buf.getInt();
205
206 if ((flags & SftpATTRS.SSH_FILEXFER_ATTR_SIZE) != 0) {
207 size = buf.getLong();
208 }
209 if ((flags & SftpATTRS.SSH_FILEXFER_ATTR_UIDGID) != 0) {
210 uid = buf.getInt();
211 gid = buf.getInt();
212 }
213 if ((flags & SftpATTRS.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
214 permissions = buf.getInt();
215 }
216 if ((flags & SftpATTRS.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
217 atime = buf.getInt();
218 }
219 if ((flags & SftpATTRS.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
220 mtime = buf.getInt();
221 }
222
223 }
224 }
225
226 static class SftpProviderTestSuite extends ProviderTestSuite {
227 private final boolean isExecChannelClosed;
228 private final SessionFactory sessionFactory;
229
230 public SftpProviderTestSuite(final AbstractSftpProviderTestCase providerConfig) throws Exception {
231 super(providerConfig);
232 isExecChannelClosed = providerConfig.isExecChannelClosed();
233 sessionFactory = providerConfig.sessionFactory();
234 }
235
236 @Override
237 protected void setUp() throws Exception {
238 if (getSystemTestUriOverride() == null) {
239 setUpClass(isExecChannelClosed, sessionFactory);
240 }
241 super.setUp();
242 }
243
244 @Override
245 protected void tearDown() throws Exception {
246
247
248
249 for (final AbstractSession session : server.getActiveSessions()) {
250 session.close(true);
251 }
252 tearDownClass();
253 super.tearDown();
254 }
255
256 }
257
258
259
260
261
262
263
264
265
266 private static class TestCommandFactory extends ScpCommandFactory {
267
268 public static final Pattern NETCAT_COMMAND = Pattern.compile("nc -q 0 localhost (\\d+)");
269 private final boolean isExecChannelClosed;
270
271 public TestCommandFactory(final boolean isExecChannelClosed) {
272 this.isExecChannelClosed = isExecChannelClosed;
273 }
274
275 @Override
276 public Command createCommand(final String command) {
277 return new Command() {
278 public ExitCallback callback;
279 public OutputStream out;
280 public OutputStream err;
281 public InputStream in;
282
283 @Override
284 public void destroy() {
285
286 }
287
288 @Override
289 public void setErrorStream(final OutputStream err) {
290 this.err = err;
291 }
292
293 @Override
294 public void setExitCallback(final ExitCallback callback) {
295 this.callback = callback;
296
297 }
298
299 @Override
300 public void setInputStream(final InputStream in) {
301 this.in = in;
302 }
303
304 @Override
305 public void setOutputStream(final OutputStream out) {
306 this.out = out;
307 }
308
309 @Override
310 public void start(final Environment env) throws IOException {
311 int code = 0;
312 if (command.equals("id -G") || command.equals("id -u")) {
313 if (isExecChannelClosed) {
314 throw new IOException("TestingExecChannelClosed");
315 }
316 new PrintStream(out).println(0);
317 } else if (NETCAT_COMMAND.matcher(command).matches()) {
318 final Matcher matcher = NETCAT_COMMAND.matcher(command);
319 matcher.matches();
320 final int port = Integer.parseInt(matcher.group(1));
321
322 final Socket socket = new Socket((String) null, port);
323
324 if (out != null) {
325 connect("from nc", socket.getInputStream(), out, null);
326 }
327
328 if (in != null) {
329 connect("to nc", in, socket.getOutputStream(), callback);
330 }
331
332 return;
333
334 } else {
335 if (err != null) {
336 new PrintStream(err).format("Unknown command %s%n", command);
337 }
338 code = -1;
339 }
340
341 if (out != null) {
342 out.flush();
343 }
344 if (err != null) {
345 err.flush();
346 }
347 callback.onExit(code);
348 }
349 };
350 }
351 }
352
353
354
355
356 static final class TestFileSystemFactory implements FileSystemFactory {
357
358
359
360 @Override
361 public FileSystemView createFileSystemView(final Session session) throws IOException {
362 final String userName = session.getUsername();
363 if (!DEFAULT_USER.equals(userName)) {
364 return null;
365 }
366 return new TestFileSystemView(getTestDirectory(), userName);
367 }
368 }
369
370
371
372
373 static final class TestFileSystemView implements FileSystemView {
374 private final String homeDirStr;
375
376 private final String userName;
377
378
379
380 public TestFileSystemView(final String homeDirStr, final String userName) {
381 this.homeDirStr = new File(homeDirStr).getAbsolutePath();
382 this.userName = userName;
383 }
384
385 @Override
386 public SshFile getFile(final SshFile baseDir, final String file) {
387 return this.getFile(baseDir.getAbsolutePath(), file);
388 }
389
390 @Override
391 public SshFile getFile(final String file) {
392 return this.getFile(homeDirStr, file);
393 }
394
395 protected SshFile getFile(final String dir, final String file) {
396 final String home = removePrefix(NativeSshFile.normalizeSeparateChar(homeDirStr));
397 String userFileName = removePrefix(NativeSshFile.normalizeSeparateChar(file));
398 final File sshFile = userFileName.startsWith(home) ? new File(userFileName) : new File(home, userFileName);
399 userFileName = removePrefix(NativeSshFile.normalizeSeparateChar(sshFile.getAbsolutePath()));
400 return new TestNativeSshFile(userFileName, sshFile, userName);
401 }
402
403 private String removePrefix(final String s) {
404 final int index = s.indexOf('/');
405 if (index < 1) {
406 return s;
407 }
408 return s.substring(index);
409 }
410 }
411
412
413
414
415
416
417
418 static class TestNativeSshFile extends NativeSshFile {
419 TestNativeSshFile(final String fileName, final File file, final String userName) {
420 super(fileName, file, userName);
421 }
422 }
423
424 private static final String DEFAULT_USER = "testtest";
425
426 protected static String connectionUri;
427
428 protected static SshServer server;
429
430 private static final String TEST_URI = "test.sftp.uri";
431
432
433
434
435
436
437
438
439
440
441 private static void connect(final String name, final InputStream in, final OutputStream out,
442 final ExitCallback callback) {
443 final Thread thread = new Thread((Runnable) () -> {
444 int code = 0;
445 try {
446 final byte[] buffer = new byte[1024];
447 int len;
448 while ((len = in.read(buffer, 0, buffer.length)) != -1) {
449 out.write(buffer, 0, len);
450 out.flush();
451 }
452 } catch (final SshException ex1) {
453
454
455 } catch (final IOException ex2) {
456 if (!ex2.getMessage().equals("Pipe closed")) {
457 code = -1;
458 }
459 }
460 if (callback != null) {
461 callback.onExit(code);
462 }
463 }, name);
464 thread.setDaemon(true);
465 thread.start();
466 }
467
468
469
470
471 protected static String getSystemTestUriOverride() {
472 return System.getProperty(TEST_URI);
473 }
474
475
476
477
478
479
480
481 private static void setUpClass(final boolean isExecChannelClosed, final SessionFactory sessionFactory) throws IOException {
482 if (server != null) {
483 return;
484 }
485
486 final Path tmpDir = PathUtils.getTempDirectory();
487 server = SshServer.setUpDefaultServer();
488 server.setSessionFactory(sessionFactory);
489 server.setPort(0);
490 if (SecurityUtils.isBouncyCastleRegistered()) {
491
492 final Path keyFile = Files.createTempFile(tmpDir, "key", ".pem");
493 keyFile.toFile().deleteOnExit();
494
495 Files.delete(keyFile);
496
497 final PEMGeneratorHostKeyProvider keyProvider = new PEMGeneratorHostKeyProvider(keyFile.toAbsolutePath().toString());
498 server.setKeyPairProvider(keyProvider);
499 } else {
500 server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(tmpDir.resolve("key.ser").toString()));
501 }
502 final List<NamedFactory<Command>> list = new ArrayList<>(1);
503 list.add(new NamedFactory<Command>() {
504
505 @Override
506 public Command create() {
507 return new MySftpSubsystem();
508 }
509
510 @Override
511 public String getName() {
512 return "sftp";
513 }
514 });
515 server.setSubsystemFactories(list);
516 server.setPasswordAuthenticator((username, password, session) -> StringUtils.equals(username, password));
517 server.setPublickeyAuthenticator((username, key, session) -> true);
518 server.setForwardingFilter(new ForwardingFilter() {
519 @Override
520 public boolean canConnect(final InetSocketAddress address, final ServerSession session) {
521 return true;
522 }
523
524 @Override
525 public boolean canForwardAgent(final ServerSession session) {
526 return true;
527 }
528
529 @Override
530 public boolean canForwardX11(final ServerSession session) {
531 return true;
532 }
533
534 @Override
535 public boolean canListen(final InetSocketAddress address, final ServerSession session) {
536 return true;
537 }
538 });
539
540 server.setCommandFactory(new ScpCommandFactory(new TestCommandFactory(isExecChannelClosed)));
541
542
543 server.setFileSystemFactory(new TestFileSystemFactory());
544
545 server.start();
546 final int socketPort = server.getPort();
547 connectionUri = String.format("sftp://%s@localhost:%d", DEFAULT_USER, socketPort);
548
549
550
551 server.getUserAuthFactories().add(new UserAuthNone.Factory());
552
553 }
554
555
556
557
558
559
560 private static void tearDownClass() throws InterruptedException {
561 if (server != null) {
562 server.stop();
563 server = null;
564 }
565 }
566
567
568
569
570 protected SftpFileSystem fileSystem;
571
572
573
574
575 @Override
576 public FileObject getBaseTestFolder(final FileSystemManager manager) throws Exception {
577 String uri = getSystemTestUriOverride();
578 if (uri == null) {
579 uri = connectionUri;
580 }
581
582 final FileSystemOptions fileSystemOptions = new FileSystemOptions();
583 final SftpFileSystemConfigBuilder builder = SftpFileSystemConfigBuilder.getInstance();
584 builder.setStrictHostKeyChecking(fileSystemOptions, "no");
585 builder.setUserInfo(fileSystemOptions, new TrustEveryoneUserInfo());
586 builder.setIdentityRepositoryFactory(fileSystemOptions, new TestIdentityRepositoryFactory());
587 builder.setConnectTimeout(fileSystemOptions, Duration.ofSeconds(60));
588 builder.setSessionTimeout(fileSystemOptions, Duration.ofSeconds(60));
589
590 final FileObject fileObject = manager.resolveFile(uri, fileSystemOptions);
591 fileSystem = (SftpFileSystem) fileObject.getFileSystem();
592 return fileObject;
593 }
594
595 protected abstract boolean isExecChannelClosed();
596
597
598
599
600 @Override
601 public void prepare(final DefaultFileSystemManager manager) throws Exception {
602 manager.addProvider("sftp", new SftpFileProvider());
603 }
604
605 protected SessionFactory sessionFactory() {
606 return null;
607 }
608
609 }