/* * AbstractSFTPFileSystemTest.java * Copyright 2016 Rob Spoor * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.robtimus.filesystems.sftp; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.spy; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.FileSystemException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.Base64; import java.util.Collection; import java.util.Iterator; import java.util.Map; import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; import org.apache.sshd.common.keyprovider.MappedKeyPairProvider; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.password.PasswordAuthenticator; import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import com.github.robtimus.filesystems.sftp.server.FixedSftpSubsystem; import com.jcraft.jsch.SftpException; @SuppressWarnings({ "nls", "javadoc" }) public abstract class AbstractSFTPFileSystemTest { private static final String USERNAME = "TEST_USER"; private static final String PASSWORD = "TEST_PASSWORD"; private static final PublicKey PUBLIC_KEY = readPublicKey("id_rsa.pkcs8"); private static final PublicKey PUBLIC_KEY_NOPASS = readPublicKey("id_rsa_nopass.pkcs8"); private static int port; private static SshServer sshServer; private static Path rootPath; private static Path defaultDir; private static ExceptionFactoryWrapper exceptionFactory; private static SFTPFileSystem sftpFileSystem; private static SFTPFileSystem sftpFileSystem2; protected final SFTPFileSystem fileSystem = sftpFileSystem; protected final SFTPFileSystem fileSystem2 = sftpFileSystem2; private static PublicKey readPublicKey(String resource) { try { ByteArrayOutputStream output = new ByteArrayOutputStream(); try (InputStream input = AbstractSFTPFileSystemTest.class.getResourceAsStream(resource)) { byte[] buffer = new byte[1024]; int len; while ((len = input.read(buffer)) != -1) { output.write(buffer, 0, len); } } // public key parsing based on https://gist.github.com/destan/b708d11bd4f403506d6d5bb5fe6a82c5 String publicKeyContent = new String(output.toString("UTF-8")) .replace("\\n", "") .replace("-----BEGIN PUBLIC KEY-----", "") .replace("-----END PUBLIC KEY-----", ""); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getMimeDecoder().decode(publicKeyContent)); return keyFactory.generatePublic(keySpec); } catch (IOException | GeneralSecurityException e) { throw new IllegalStateException(e); } } @BeforeAll public static void setupClass() throws NoSuchAlgorithmException, IOException { setupClass(new FixedSftpSubsystem.Factory()); } protected static void setupClass(SftpSubsystemFactory subSystemFactory) throws NoSuchAlgorithmException, IOException { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); KeyPair keyPair = generator.generateKeyPair(); port = findFreePort(); sshServer = SshServer.setUpDefaultServer(); sshServer.setPort(port); sshServer.setKeyPairProvider(new MappedKeyPairProvider(keyPair)); sshServer.setPasswordAuthenticator(new PasswordAuthenticator() { @Override public boolean authenticate(String username, String password, ServerSession session) { return USERNAME.equals(username) && PASSWORD.equals(password); } }); sshServer.setPublickeyAuthenticator(new PublickeyAuthenticator() { @Override public boolean authenticate(String username, PublicKey key, ServerSession session) { return USERNAME.equals(username) && (PUBLIC_KEY.equals(key) || PUBLIC_KEY_NOPASS.equals(key)); } }); sshServer.setSubsystemFactories(Arrays.asList(subSystemFactory)); rootPath = Files.createTempDirectory("sftp-fs"); defaultDir = rootPath.resolve("home"); Files.createDirectory(defaultDir); VirtualFileSystemFactory fsFactory = new VirtualFileSystemFactory(rootPath); sshServer.setFileSystemFactory(fsFactory); sshServer.start(); exceptionFactory = new ExceptionFactoryWrapper(); exceptionFactory.delegate = DefaultFileSystemExceptionFactory.INSTANCE; sftpFileSystem = createFileSystem(); sftpFileSystem2 = createFileSystem(3); } private static int findFreePort() throws IOException { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); } } @AfterAll public static void cleanupClass() throws IOException { Files.deleteIfExists(defaultDir); Files.deleteIfExists(rootPath); sftpFileSystem.close(); sshServer.stop(); sshServer = null; } private static SFTPFileSystem createFileSystem() throws IOException { Map<String, ?> env = createEnv(); return (SFTPFileSystem) new SFTPFileSystemProvider().newFileSystem(URI.create("sftp://localhost:" + port), env); } private static SFTPFileSystem createFileSystem(int clientConnectionCount) throws IOException { Map<String, ?> env = createEnv().withClientConnectionCount(clientConnectionCount); return (SFTPFileSystem) new SFTPFileSystemProvider().newFileSystem(URI.create("sftp://localhost:" + port), env); } protected static SFTPEnvironment createEnv() { return new SFTPEnvironment() .withUsername(USERNAME) .withUserInfo(new SimpleUserInfo(PASSWORD.toCharArray())) .withHostKeyRepository(TrustAllHostKeyRepository.INSTANCE) .withDefaultDirectory(defaultDir.getFileName().toString()) .withClientConnectionCount(1) .withFileSystemExceptionFactory(exceptionFactory); } @BeforeEach public void setup() throws IOException { Files.createDirectories(defaultDir); exceptionFactory.delegate = spy(DefaultFileSystemExceptionFactory.INSTANCE); } @AfterEach public void cleanup() throws IOException { exceptionFactory.delegate = null; purgePath(rootPath); } private void purgePath(final Path path) throws IOException { Files.walkFileTree(path, new SimpleFileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { dir.toFile().setWritable(true, false); return super.preVisitDirectory(dir, attrs); } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { file.toFile().setWritable(true, false); Files.delete(file); return super.visitFile(file, attrs); } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { if (!rootPath.equals(dir) && !defaultDir.equals(dir)) { Files.delete(dir); } return super.postVisitDirectory(dir, exc); } }); } protected final String getBaseUrl() { return "sftp://" + USERNAME + "@localhost:" + port; } protected final URI getURI() { return URI.create("sftp://localhost:" + port); } protected final SFTPPath createPath(String path) { return new SFTPPath(sftpFileSystem, path); } protected final SFTPPath createPath(SFTPFileSystem fs, String path) { return new SFTPPath(fs, path); } protected final FileSystemExceptionFactory getExceptionFactory() { return exceptionFactory.delegate; } protected final Path getPath(String path) { while (path.startsWith("/")) { path = path.substring(1); } return rootPath.resolve(path); } protected final Path getFile(String path) { Path result = getPath(path); assertTrue(Files.isRegularFile(result, LinkOption.NOFOLLOW_LINKS)); return result; } protected final Path getDirectory(String path) { Path result = getPath(path); assertTrue(Files.isDirectory(result, LinkOption.NOFOLLOW_LINKS)); return result; } protected final Path getSymLink(String path) { Path result = getPath(path); assertTrue(Files.isSymbolicLink(result)); return result; } protected final Path addFile(String path) throws IOException { Path file = getPath(path); Path parent = file.getParent(); if (parent != null) { Files.createDirectories(parent); } Files.createFile(file); return file; } protected final Path addDirectory(String path) throws IOException { Path directory = getPath(path); Path parent = directory.getParent(); if (parent != null) { Files.createDirectories(parent); } Files.createDirectory(directory); return directory; } protected final Path addSymLink(String path, Path target) throws IOException { Path symLink = getPath(path); Path parent = symLink.getParent(); if (parent != null) { Files.createDirectories(parent); } Files.createSymbolicLink(symLink, target); return symLink; } protected final boolean delete(String path) throws IOException { Path dir = getPath(path); try { purgePath(dir); return true; } catch (NoSuchFileException e) { if (e.getFile().equals(dir.toString())) { return false; } throw e; } } protected final int getChildCount(String path) throws IOException { try (DirectoryStream<Path> stream = Files.newDirectoryStream(getPath(path))) { int count = 0; for (Iterator<?> i = stream.iterator(); i.hasNext(); ) { i.next(); count++; } return count; } } protected final byte[] getContents(Path file) throws IOException { return Files.readAllBytes(file); } protected final String getStringContents(Path file) throws IOException { return new String(getContents(file), StandardCharsets.UTF_8); } protected final void setContents(Path file, byte[] contents) throws IOException { try (OutputStream out = Files.newOutputStream(file, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { out.write(contents); } } protected final void setContents(Path file, String contents) throws IOException { setContents(file, contents.getBytes(StandardCharsets.UTF_8)); } protected final long getTotalSize() throws IOException { return getTotalSize(rootPath); } private long getTotalSize(Path path) throws IOException { BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); long size = attributes.size(); if (attributes.isDirectory()) { try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) { for (Path p : stream) { size += getTotalSize(p); } } } return size; } private static class ExceptionFactoryWrapper implements FileSystemExceptionFactory { private FileSystemExceptionFactory delegate; @Override public FileSystemException createGetFileException(String file, SftpException exception) { return delegate.createGetFileException(file, exception); } @Override public FileSystemException createReadLinkException(String link, SftpException exception) { return delegate.createReadLinkException(link, exception); } @Override public FileSystemException createListFilesException(String directory, SftpException exception) { return delegate.createListFilesException(directory, exception); } @Override public FileSystemException createChangeWorkingDirectoryException(String directory, SftpException exception) { return delegate.createChangeWorkingDirectoryException(directory, exception); } @Override public FileSystemException createCreateDirectoryException(String directory, SftpException exception) { return delegate.createCreateDirectoryException(directory, exception); } @Override public FileSystemException createDeleteException(String file, SftpException exception, boolean isDirectory) { return delegate.createDeleteException(file, exception, isDirectory); } @Override public FileSystemException createNewInputStreamException(String file, SftpException exception) { return delegate.createNewInputStreamException(file, exception); } @Override public FileSystemException createNewOutputStreamException(String file, SftpException exception, Collection<? extends OpenOption> options) { return delegate.createNewOutputStreamException(file, exception, options); } @Override public FileSystemException createCopyException(String file, String other, SftpException exception) { return delegate.createCopyException(file, other, exception); } @Override public FileSystemException createMoveException(String file, String other, SftpException exception) { return delegate.createMoveException(file, other, exception); } @Override public FileSystemException createSetOwnerException(String file, SftpException exception) { return delegate.createSetOwnerException(file, exception); } @Override public FileSystemException createSetGroupException(String file, SftpException exception) { return delegate.createSetGroupException(file, exception); } @Override public FileSystemException createSetPermissionsException(String file, SftpException exception) { return delegate.createSetPermissionsException(file, exception); } @Override public FileSystemException createSetModificationTimeException(String file, SftpException exception) { return delegate.createSetModificationTimeException(file, exception); } } }