/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.sshd.util.test; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.ServerSocket; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.security.CodeSource; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.ProtectionDomain; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import org.apache.sshd.client.SshClient; import org.apache.sshd.client.config.hosts.HostConfigEntryResolver; import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; import org.apache.sshd.common.Factory; import org.apache.sshd.common.cipher.ECCurves; import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.keyprovider.FileKeyPairProvider; import org.apache.sshd.common.keyprovider.KeyIdentityProvider; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.keyprovider.KeyPairProviderHolder; import org.apache.sshd.common.random.Random; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.common.util.security.SecurityUtils; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; public final class Utils { /** * URL/URI scheme that refers to a file */ public static final String FILE_URL_SCHEME = "file"; /** * Prefix used in URL(s) that reference a file resource */ public static final String FILE_URL_PREFIX = FILE_URL_SCHEME + ":"; /** * Separator used in URL(s) that reference a resource inside a JAR * to denote the sub-path inside the JAR */ public static final char RESOURCE_SUBPATH_SEPARATOR = '!'; /** * Suffix of JAR files */ public static final String JAR_FILE_SUFFIX = ".jar"; /** * URL/URI scheme that refers to a JAR */ public static final String JAR_URL_SCHEME = "jar"; /** * Prefix used in URL(s) that reference a resource inside a JAR */ public static final String JAR_URL_PREFIX = JAR_URL_SCHEME + ":"; /** * Suffix of compile Java class files */ public static final String CLASS_FILE_SUFFIX = ".class"; public static final List<String> TARGET_FOLDER_NAMES = // NOTE: order is important Collections.unmodifiableList( Arrays.asList("target" /* Maven */, "build" /* Gradle */)); public static final String DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM = KeyUtils.RSA_ALGORITHM; // uses a cached instance to avoid re-creating the keys as it is a time-consuming effort private static final AtomicReference<KeyPairProvider> KEYPAIR_PROVIDER_HOLDER = new AtomicReference<>(); // uses a cached instance to avoid re-creating the keys as it is a time-consuming effort private static final Map<String, FileKeyPairProvider> PROVIDERS_MAP = new ConcurrentHashMap<>(); private Utils() { throw new UnsupportedOperationException("No instance"); } public static KeyPairProvider createTestHostKeyProvider(Class<?> anchor) { KeyPairProvider provider = KEYPAIR_PROVIDER_HOLDER.get(); if (provider != null) { return provider; } File targetFolder = Objects.requireNonNull(detectTargetFolder(anchor), "Failed to detect target folder"); File file = new File(targetFolder, "hostkey." + DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM.toLowerCase()); provider = createTestHostKeyProvider(file); KeyPairProvider prev = KEYPAIR_PROVIDER_HOLDER.getAndSet(provider); if (prev != null) { // check if somebody else beat us to it return prev; } else { return provider; } } public static KeyPairProvider createTestHostKeyProvider(File file) { return createTestHostKeyProvider(Objects.requireNonNull(file, "No file").toPath()); } public static KeyPairProvider createTestHostKeyProvider(Path path) { SimpleGeneratorHostKeyProvider keyProvider = new SimpleGeneratorHostKeyProvider(); keyProvider.setPath(Objects.requireNonNull(path, "No path")); keyProvider.setAlgorithm(DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM); return validateKeyPairProvider(keyProvider); } public static KeyPair getFirstKeyPair(KeyPairProviderHolder holder) { return getFirstKeyPair(Objects.requireNonNull(holder, "No holder").getKeyPairProvider()); } public static KeyPair getFirstKeyPair(KeyIdentityProvider provider) { Objects.requireNonNull(provider, "No key pair provider"); Iterable<? extends KeyPair> pairs = Objects.requireNonNull(provider.loadKeys(), "No loaded keys"); Iterator<? extends KeyPair> iter = Objects.requireNonNull(pairs.iterator(), "No keys iterator"); ValidateUtils.checkTrue(iter.hasNext(), "Empty loaded kyes iterator"); return Objects.requireNonNull(iter.next(), "No key pair in iterator"); } public static KeyPair generateKeyPair(String algorithm, int keySize) throws GeneralSecurityException { KeyPairGenerator gen = SecurityUtils.getKeyPairGenerator(algorithm); if (KeyUtils.EC_ALGORITHM.equalsIgnoreCase(algorithm)) { ECCurves curve = ECCurves.fromCurveSize(keySize); if (curve == null) { throw new InvalidKeySpecException("Unknown curve for key size=" + keySize); } gen.initialize(curve.getParameters()); } else { gen.initialize(keySize); } return gen.generateKeyPair(); } public static FileKeyPairProvider createTestKeyPairProvider(String resource) { File file = getFile(resource); String filePath = file.getAbsolutePath(); FileKeyPairProvider provider = PROVIDERS_MAP.get(filePath); if (provider != null) { return provider; } provider = new FileKeyPairProvider(); provider.setFiles(Collections.singletonList(file)); provider = validateKeyPairProvider(provider); FileKeyPairProvider prev = PROVIDERS_MAP.put(filePath, provider); if (prev != null) { // check if somebody else beat us to it return prev; } else { return provider; } } private static <P extends KeyIdentityProvider> P validateKeyPairProvider(P provider) { Objects.requireNonNull(provider, "No provider"); // get the I/O out of the way Iterable<KeyPair> keys = Objects.requireNonNull(provider.loadKeys(), "No keys loaded"); if (keys instanceof Collection<?>) { ValidateUtils.checkNotNullAndNotEmpty((Collection<?>) keys, "Empty keys loaded"); } return provider; } public static Random getRandomizerInstance() { Factory<Random> factory = SecurityUtils.getRandomFactory(); return factory.create(); } public static int getFreePort() throws Exception { try (ServerSocket s = new ServerSocket()) { s.setReuseAddress(true); s.bind(new InetSocketAddress((InetAddress) null, 0)); return s.getLocalPort(); } } private static File getFile(String resource) { URL url = Utils.class.getClassLoader().getResource(resource); try { return new File(url.toURI()); } catch (URISyntaxException e) { return new File(url.getPath()); } } public static Path resolve(Path root, String... children) { if (GenericUtils.isEmpty(children)) { return root; } else { return resolve(root, Arrays.asList(children)); } } public static Path resolve(Path root, Collection<String> children) { Path path = root; if (!GenericUtils.isEmpty(children)) { for (String child : children) { path = path.resolve(child); } } return path; } public static File resolve(File root, String... children) { if (GenericUtils.isEmpty(children)) { return root; } else { return resolve(root, Arrays.asList(children)); } } public static File resolve(File root, Collection<String> children) { File path = root; if (!GenericUtils.isEmpty(children)) { for (String child : children) { path = new File(path, child); } } return path; } /** * Removes the specified file - if it is a directory, then its children * are deleted recursively and then the directory itself. <B>Note:</B> * no attempt is made to make sure that {@link File#delete()} was successful * * @param file The {@link File} to be deleted - ignored if {@code null} * or does not exist anymore * @return The <tt>file</tt> argument */ public static File deleteRecursive(File file) { if ((file == null) || (!file.exists())) { return file; } if (file.isDirectory()) { File[] children = file.listFiles(); if (!GenericUtils.isEmpty(children)) { for (File child : children) { deleteRecursive(child); } } } // seems that if a file is not writable it cannot be deleted if (!file.canWrite()) { file.setWritable(true, false); } if (!file.delete()) { System.err.append("Failed to delete ").println(file.getAbsolutePath()); } return file; } /** * Removes the specified file - if it is a directory, then its children * are deleted recursively and then the directory itself. * * @param path The file {@link Path} to be deleted - ignored if {@code null} * or does not exist anymore * @param options The {@link LinkOption}s to use * @return The <tt>path</tt> argument * @throws IOException If failed to access/remove some file(s) */ public static Path deleteRecursive(Path path, LinkOption... options) throws IOException { if ((path == null) || (!Files.exists(path))) { return path; } if (Files.isDirectory(path)) { try (DirectoryStream<Path> ds = Files.newDirectoryStream(path)) { for (Path child : ds) { deleteRecursive(child, options); } } } try { // seems that if a file is not writable it cannot be deleted if (!Files.isWritable(path)) { path.toFile().setWritable(true, false); } Files.delete(path); } catch (IOException e) { // same logic as deleteRecursive(File) which does not check if deletion succeeded System.err.append("Failed (").append(e.getClass().getSimpleName()).append(")") .append(" to delete ").append(path.toString()) .append(": ").println(e.getMessage()); } return path; } /** * @param anchor An anchor {@link Class} whose container we want to use * as the starting point for the "target" folder lookup up the * hierarchy * @return The "target" <U>folder</U> - {@code null} if not found * @see #detectTargetFolder(File) */ public static File detectTargetFolder(Class<?> anchor) { return detectTargetFolder(getClassContainerLocationFile(anchor)); } /** * @param clazz A {@link Class} object * @return A {@link File} of the location of the class bytes container * - e.g., the root folder, the containing JAR, etc.. Returns * {@code null} if location could not be resolved * @throws IllegalArgumentException If location is not a valid * {@link File} location * @see #getClassContainerLocationURI(Class) * @see #toFileSource(URI) */ public static File getClassContainerLocationFile(Class<?> clazz) throws IllegalArgumentException { try { URI uri = getClassContainerLocationURI(clazz); return toFileSource(uri); } catch (URISyntaxException | MalformedURLException e) { throw new IllegalArgumentException(e.getClass().getSimpleName() + ": " + e.getMessage(), e); } } /** * @param clazz A {@link Class} object * @return A {@link URI} to the location of the class bytes container * - e.g., the root folder, the containing JAR, etc.. Returns * {@code null} if location could not be resolved * @throws URISyntaxException if location is not a valid URI * @see #getClassContainerLocationURL(Class) */ public static URI getClassContainerLocationURI(Class<?> clazz) throws URISyntaxException { URL url = getClassContainerLocationURL(clazz); return (url == null) ? null : url.toURI(); } /** * @param clazz A {@link Class} object * @return A {@link URL} to the location of the class bytes container * - e.g., the root folder, the containing JAR, etc.. Returns * {@code null} if location could not be resolved */ public static URL getClassContainerLocationURL(Class<?> clazz) { ProtectionDomain pd = clazz.getProtectionDomain(); CodeSource cs = (pd == null) ? null : pd.getCodeSource(); URL url = (cs == null) ? null : cs.getLocation(); if (url == null) { url = getClassBytesURL(clazz); if (url == null) { return null; } String srcForm = getURLSource(url); if (GenericUtils.isEmpty(srcForm)) { return null; } try { url = new URL(srcForm); } catch (MalformedURLException e) { throw new IllegalArgumentException("getClassContainerLocationURL(" + clazz.getName() + ")" + " Failed to create URL=" + srcForm + " from " + url.toExternalForm() + ": " + e.getMessage()); } } return url; } /** * Converts a {@link URL} that may refer to an internal resource to * a {@link File} representing is "source" container (e.g., * if it is a resource in a JAR, then the result is the JAR's path) * * @param url The {@link URL} - ignored if {@code null} * @return The matching {@link File} * @throws MalformedURLException If source URL does not refer to a * file location * @see #toFileSource(URI) */ public static File toFileSource(URL url) throws MalformedURLException { if (url == null) { return null; } try { return toFileSource(url.toURI()); } catch (URISyntaxException e) { throw new MalformedURLException("toFileSource(" + url.toExternalForm() + ")" + " cannot (" + e.getClass().getSimpleName() + ")" + " convert to URI: " + e.getMessage()); } } /** * Converts a {@link URI} that may refer to an internal resource to * a {@link File} representing is "source" container (e.g., * if it is a resource in a JAR, then the result is the JAR's path) * * @param uri The {@link URI} - ignored if {@code null} * @return The matching {@link File} * @throws MalformedURLException If source URI does not refer to a * file location * @see #getURLSource(URI) */ public static File toFileSource(URI uri) throws MalformedURLException { String src = getURLSource(uri); if (GenericUtils.isEmpty(src)) { return null; } if (!src.startsWith(FILE_URL_PREFIX)) { throw new MalformedURLException("toFileSource(" + src + ") not a '" + FILE_URL_SCHEME + "' scheme"); } try { return new File(new URI(src)); } catch (URISyntaxException e) { throw new MalformedURLException("toFileSource(" + src + ")" + " cannot (" + e.getClass().getSimpleName() + ")" + " convert to URI: " + e.getMessage()); } } /** * @param uri The {@link URI} value - ignored if {@code null} * @return The URI(s) source path where {@link #JAR_URL_PREFIX} and * any sub-resource are stripped * @see #getURLSource(String) */ public static String getURLSource(URI uri) { return getURLSource((uri == null) ? null : uri.toString()); } /** * @param url The {@link URL} value - ignored if {@code null} * @return The URL(s) source path where {@link #JAR_URL_PREFIX} and * any sub-resource are stripped * @see #getURLSource(String) */ public static String getURLSource(URL url) { return getURLSource((url == null) ? null : url.toExternalForm()); } /** * @param externalForm The {@link URL#toExternalForm()} string - ignored if * {@code null}/empty * @return The URL(s) source path where {@link #JAR_URL_PREFIX} and * any sub-resource are stripped */ public static String getURLSource(String externalForm) { String url = externalForm; if (GenericUtils.isEmpty(url)) { return url; } url = stripJarURLPrefix(externalForm); if (GenericUtils.isEmpty(url)) { return url; } int sepPos = url.indexOf(RESOURCE_SUBPATH_SEPARATOR); if (sepPos < 0) { return adjustURLPathValue(url); } else { return adjustURLPathValue(url.substring(0, sepPos)); } } /** * @param url A {@link URL} - ignored if {@code null} * @return The path after stripping any trailing '/' provided the path * is not '/' itself * @see #adjustURLPathValue(String) */ public static String adjustURLPathValue(URL url) { return adjustURLPathValue((url == null) ? null : url.getPath()); } /** * @param path A URL path value - ignored if {@code null}/empty * @return The path after stripping any trailing '/' provided the path * is not '/' itself */ public static String adjustURLPathValue(final String path) { final int pathLen = (path == null) ? 0 : path.length(); if ((pathLen <= 1) || (path.charAt(pathLen - 1) != '/')) { return path; } return path.substring(0, pathLen - 1); } public static String stripJarURLPrefix(String externalForm) { String url = externalForm; if (GenericUtils.isEmpty(url)) { return url; } if (url.startsWith(JAR_URL_PREFIX)) { return url.substring(JAR_URL_PREFIX.length()); } return url; } /** * @param clazz The request {@link Class} * @return A {@link URL} to the location of the <code>.class</code> file * - {@code null} if location could not be resolved */ public static URL getClassBytesURL(Class<?> clazz) { String className = clazz.getName(); int sepPos = className.indexOf('$'); // if this is an internal class, then need to use its parent as well if (sepPos > 0) { sepPos = className.lastIndexOf('.'); if (sepPos > 0) { className = className.substring(sepPos + 1); } } else { className = clazz.getSimpleName(); } return clazz.getResource(className + CLASS_FILE_SUFFIX); } public static String getClassBytesResourceName(Class<?> clazz) { return getClassBytesResourceName((clazz == null) ? null : clazz.getName()); } /** * @param name The fully qualified class name - ignored if {@code null}/empty * @return The relative path of the class file byte-code resource */ public static String getClassBytesResourceName(String name) { if (GenericUtils.isEmpty(name)) { return name; } else { return name.replace('.', '/') + CLASS_FILE_SUFFIX; } } /** * @param anchorFile An anchor {@link File} we want to use * as the starting point for the "target" or "build" folder * lookup up the hierarchy * @return The "target" <U>folder</U> - {@code null} if not found */ public static File detectTargetFolder(File anchorFile) { for (File file = anchorFile; file != null; file = file.getParentFile()) { if (!file.isDirectory()) { continue; } String name = file.getName(); if (TARGET_FOLDER_NAMES.contains(name)) { return file; } } return null; } public static String resolveRelativeRemotePath(Path root, Path file) { Path relPath = root.relativize(file); return relPath.toString().replace(File.separatorChar, '/'); } public static SshClient setupTestClient(Class<?> anchor) { SshClient client = SshClient.setUpDefaultClient(); client.setServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE); client.setHostConfigEntryResolver(HostConfigEntryResolver.EMPTY); client.setKeyPairProvider(KeyPairProvider.EMPTY_KEYPAIR_PROVIDER); return client; } public static SshServer setupTestServer(Class<?> anchor) { SshServer sshd = SshServer.setUpDefaultServer(); sshd.setKeyPairProvider(createTestHostKeyProvider(anchor)); sshd.setPasswordAuthenticator(BogusPasswordAuthenticator.INSTANCE); sshd.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE); sshd.setShellFactory(EchoShellFactory.INSTANCE); sshd.setCommandFactory(UnknownCommandFactory.INSTANCE); return sshd; } /** * @param path The {@link Path} to write the data to * @param data The data to write (as UTF-8) * @return The UTF-8 data bytes * @throws IOException If failed to write */ public static byte[] writeFile(Path path, String data) throws IOException { try (OutputStream fos = Files.newOutputStream(path, IoUtils.EMPTY_OPEN_OPTIONS)) { byte[] bytes = data.getBytes(StandardCharsets.UTF_8); fos.write(bytes); return bytes; } } }