View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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   * Tests cases for the SFTP provider.
75   * <p>
76   * Starts and stops an embedded Apache SSHd (MINA) server.
77   * </p>
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                      // Get the path
96                      final String path = buffer.getString();
97                      // Get the permission
98                      final SftpAttrs attrs = new SftpAttrs(buffer);
99                      permissions.put(path, attrs.permissions);
100                     // System.err.format("Setting [%s] permission to %o%n", path, attrs.permissions);
101                     break;
102                 }
103 
104                 case SSH_FXP_REMOVE: {
105                     // Remove cached attributes
106                     final String path = buffer.getString();
107                     permissions.remove(path);
108                     // System.err.format("Removing [%s] permission cache%n", path);
109                     break;
110                 }
111 
112                 case SSH_FXP_INIT: {
113                     // Just grab the version here
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                 // Use cached permissions
135                 // System.err.format("Using cached [%s] permission of %o%n", file.getAbsolutePath(), cached);
136                 p |= cached;
137             } else {
138                 // Use permissions from Java file
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                 // String username = session.getUsername();
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             // Close all active sessions
247             // Note that it should be done by super.tearDown()
248             // while closing
249             for (final AbstractSession session : server.getActiveSessions()) {
250                 session.close(true);
251             }
252             tearDownClass();
253             super.tearDown();
254         }
255 
256     }
257 
258     /**
259      * The command factory for the SSH server: Handles these commands
260      * <p>
261      * <li>{@code id -u} (permissions test)</li>
262      * <li>{@code id -G} (permission tests)</li>
263      * <li>{@code nc -q 0 localhost port} (Stream proxy tests)</li>
264      * </p>
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                     // empty
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      * Implements FileSystemFactory because SSHd does not know about users and home directories.
355      */
356     static final class TestFileSystemFactory implements FileSystemFactory {
357         /**
358          * Accepts only the known test user.
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      * Implements FileSystemView because SSHd does not know about users and home directories.
372      */
373     static final class TestFileSystemView implements FileSystemView {
374         private final String homeDirStr;
375 
376         private final String userName;
377 
378         // private boolean caseInsensitive;
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     // private static final String DEFAULT_PWD = "testtest";
413 
414     /**
415      * Extends NativeSshFile because its constructor is protected and I do not want to create a whole NativeSshFile
416      * implementation for testing.
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      * Creates a pipe thread that connects an input to an output
434      *
435      * @param name     The name of the thread (for debugging purposes)
436      * @param in       The input stream
437      * @param out      The output stream
438      * @param callback An object whose method {@linkplain ExitCallback#onExit(int)} will be called when the pipe is
439      *                 broken. The integer argument is 0 if everything went well.
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                 // Nothing to do, this occurs when the connection
454                 // is closed on the remote side
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      * True if we are testing the SFTP stream proxy
470      */
471     protected static String getSystemTestUriOverride() {
472         return System.getProperty(TEST_URI);
473     }
474 
475     /**
476      * Creates and starts an embedded Apache SSHd Server (MINA).
477      *
478      * @throws FtpException
479      * @throws IOException
480      */
481     private static void setUpClass(final boolean isExecChannelClosed, final SessionFactory sessionFactory) throws IOException {
482         if (server != null) {
483             return;
484         }
485         // System.setProperty("vfs.sftp.sshdir", getTestDirectory() + "/../vfs.sftp.sshdir");
486         final Path tmpDir = PathUtils.getTempDirectory();
487         server = SshServer.setUpDefaultServer();
488         server.setSessionFactory(sessionFactory);
489         server.setPort(0);
490         if (SecurityUtils.isBouncyCastleRegistered()) {
491             // A temporary file will hold the key
492             final Path keyFile = Files.createTempFile(tmpDir, "key", ".pem");
493             keyFile.toFile().deleteOnExit();
494             // It has to be deleted in order to be generated
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         // Allows the execution of commands
540         server.setCommandFactory(new ScpCommandFactory(new TestCommandFactory(isExecChannelClosed)));
541         // HACK Start
542         // How do we really do simple user to directory matching?
543         server.setFileSystemFactory(new TestFileSystemFactory());
544         // HACK End
545         server.start();
546         final int socketPort = server.getPort();
547         connectionUri = String.format("sftp://%s@localhost:%d", DEFAULT_USER, socketPort);
548         // HACK Start
549         // How do we really do simple security?
550         // Do this after we start the server to simplify this set up code.
551         server.getUserAuthFactories().add(new UserAuthNone.Factory());
552         // HACK End
553     }
554 
555     /**
556      * Stops the embedded Apache SSHd Server (MINA).
557      *
558      * @throws InterruptedException
559      */
560     private static void tearDownClass() throws InterruptedException {
561         if (server != null) {
562             server.stop();
563             server = null;
564         }
565     }
566 
567     /**
568      * The underlying file system
569      */
570     protected SftpFileSystem fileSystem;
571 
572     /**
573      * Returns the base folder for tests.
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      * Prepares the file system manager.
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 }