From 05d85a2655d92818a4a4725d9912406590cc5520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fridrich=20=C5=A0trba?= Date: Mon, 22 Sep 2025 17:41:31 +0200 Subject: [PATCH] Don't depend on apache-ssh test-jars --- .../maven-scm-provider-gittest/pom.xml | 12 - .../git/BogusPasswordAuthenticator.java | 47 ++ .../provider/git/CommandExecutionHelper.java | 91 ++++ .../provider/git/CommonTestSupportUtils.java | 504 ++++++++++++++++++ .../provider/git/CoreTestSupportUtils.java | 83 +++ .../maven/scm/provider/git/EchoShell.java | 40 ++ .../scm/provider/git/EchoShellFactory.java | 41 ++ .../maven/scm/provider/git/GitSshServer.java | 2 - 8 files changed, 806 insertions(+), 14 deletions(-) create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/BogusPasswordAuthenticator.java create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommandExecutionHelper.java create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommonTestSupportUtils.java create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CoreTestSupportUtils.java create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShell.java create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShellFactory.java diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml index 97a4a84bc..bd894bedb 100644 --- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml @@ -50,18 +50,6 @@ sshd-git ${minaSshdVersion} - - org.apache.sshd - sshd-common - ${minaSshdVersion} - test-jar - - - org.apache.sshd - sshd-core - ${minaSshdVersion} - test-jar - org.bouncycastle diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/BogusPasswordAuthenticator.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/BogusPasswordAuthenticator.java new file mode 100644 index 000000000..1bcfa2e1d --- /dev/null +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/BogusPasswordAuthenticator.java @@ -0,0 +1,47 @@ +/* + * 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.maven.scm.provider.git; + +import org.apache.sshd.common.util.logging.AbstractLoggingBean; +import org.apache.sshd.server.auth.password.PasswordAuthenticator; +import org.apache.sshd.server.session.ServerSession; + +/** + * A test {@link PasswordAuthenticator} that accepts an authentication attempt if the username is not {@code null} and + * same as password + * + * @author Apache MINA SSHD Project + */ +public class BogusPasswordAuthenticator extends AbstractLoggingBean implements PasswordAuthenticator { + public static final BogusPasswordAuthenticator INSTANCE = new BogusPasswordAuthenticator(); + + public BogusPasswordAuthenticator() { + super(); + } + + @Override + public boolean authenticate(String username, String password, ServerSession session) { + boolean result = (username != null) && username.equals(password); + if (log.isDebugEnabled()) { + log.debug("authenticate({}) {} / {} - success={}", session, username, password, result); + } + + return result; + } +} diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommandExecutionHelper.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommandExecutionHelper.java new file mode 100644 index 000000000..75011879a --- /dev/null +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommandExecutionHelper.java @@ -0,0 +1,91 @@ +/* + * 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.maven.scm.provider.git; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.apache.sshd.server.command.AbstractCommandSupport; + +/** + * @author Apache MINA SSHD Project + */ +public abstract class CommandExecutionHelper extends AbstractCommandSupport { + protected CommandExecutionHelper() { + this(null); + } + + protected CommandExecutionHelper(String command) { + super(command, null); + } + + @Override + public void run() { + String command = getCommand(); + try { + if (command == null) { + try (BufferedReader r = + new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8))) { + for (; ; ) { + command = r.readLine(); + if (command == null) { + return; + } + + if (!handleCommandLine(command)) { + return; + } + } + } + } else { + handleCommandLine(command); + } + } catch (InterruptedIOException e) { + // Ignore - signaled end + } catch (Exception e) { + String message = + "Failed (" + e.getClass().getSimpleName() + ") to handle '" + command + "': " + e.getMessage(); + try { + OutputStream stderr = getErrorStream(); + stderr.write(message.getBytes(StandardCharsets.US_ASCII)); + } catch (IOException ioe) { + log.warn( + "Failed ({}) to write error message={}: {}", + e.getClass().getSimpleName(), + message, + ioe.getMessage()); + } finally { + onExit(-1, message); + } + } finally { + onExit(0); + } + } + + /** + * @param command The command line + * @return {@code true} if continue accepting command + * @throws Exception If failed to handle the command line + */ + protected abstract boolean handleCommandLine(String command) throws Exception; +} diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommonTestSupportUtils.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommonTestSupportUtils.java new file mode 100644 index 000000000..be4fdda4f --- /dev/null +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommonTestSupportUtils.java @@ -0,0 +1,504 @@ +/* + * 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.maven.scm.provider.git; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +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.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.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; + +/** + * TODO Add javadoc + * + * @author Apache MINA SSHD Project + */ +public final class CommonTestSupportUtils { + /** + * 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 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.EC_ALGORITHM; + public static final int DEFAULT_TEST_HOST_KEY_SIZE = 256; + public static final String DEFAULT_TEST_HOST_KEY_TYPE = + ECCurves.fromCurveSize(DEFAULT_TEST_HOST_KEY_SIZE).getKeyType(); + + // uses a cached instance to avoid re-creating the keys as it is a time-consuming effort + private static final AtomicReference 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 PROVIDERS_MAP = new ConcurrentHashMap<>(); + + private CommonTestSupportUtils() { + throw new UnsupportedOperationException("No instance allowed"); + } + + /** + * @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; + } + + /** + * @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 .class 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; + } + } + + 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 children) { + Path path = root; + if (!GenericUtils.isEmpty(children)) { + for (String child : children) { + path = path.resolve(child); + } + } + + 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" folder - {@code null} if not found + * @see #detectTargetFolder(Path) + */ + public static Path detectTargetFolder(Class anchor) { + Path path = detectTargetFolder(getClassContainerLocationPath(anchor)); + if (path == null) { + String basedir = System.getProperty("basedir"); + path = detectTargetFolder(Paths.get(basedir, "target")); + } + return path; + } + + /** + * @param clazz A {@link Class} object + * @return A {@link Path} 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 Path} location + * @see #getClassContainerLocationURI(Class) + * @see #toPathSource(URI) + */ + public static Path getClassContainerLocationPath(Class clazz) throws IllegalArgumentException { + try { + URI uri = getClassContainerLocationURI(clazz); + return toPathSource(uri); + } catch (URISyntaxException | MalformedURLException e) { + throw new IllegalArgumentException(e.getClass().getSimpleName() + ": " + e.getMessage(), e); + } + } + + /** + * Converts a {@link URL} that may refer to an internal resource to a {@link Path} 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 Path} + * @throws MalformedURLException If source URL does not refer to a file location + * @see #toPathSource(URI) + */ + public static Path toPathSource(URL url) throws MalformedURLException { + if (url == null) { + return null; + } + + try { + return toPathSource(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 Path} 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 Path} + * @throws MalformedURLException If source URI does not refer to a file location + * @see #getURLSource(URI) + */ + public static Path toPathSource(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 Paths.get(new URI(src)); + } catch (URISyntaxException e) { + throw new MalformedURLException("toFileSource(" + src + ")" + + " cannot (" + e.getClass().getSimpleName() + ")" + + " convert to URI: " + e.getMessage()); + } + } + + /** + * @param anchorFile An anchor {@link Path} we want to use as the starting point for the "target" or + * "build" folder lookup up the hierarchy + * @return The "target" folder - {@code null} if not found + */ + public static Path detectTargetFolder(Path anchorFile) { + for (Path file = anchorFile; file != null; file = file.getParent()) { + if (!Files.isDirectory(file)) { + continue; + } + + String name = Objects.toString(file.getFileName(), ""); + if (TARGET_FOLDER_NAMES.contains(name)) { + return file; + } + } + + return null; + } + + 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 KeyPairProvider createTestHostKeyProvider(Class anchor) { + KeyPairProvider provider = KEYPAIR_PROVIDER_HOLDER.get(); + if (provider != null) { + return provider; + } + + Path targetFolder = Objects.requireNonNull( + CommonTestSupportUtils.detectTargetFolder(anchor), "Failed to detect target folder"); + Path file = targetFolder.resolve("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(Path path) { + SimpleGeneratorHostKeyProvider keyProvider = new SimpleGeneratorHostKeyProvider(); + keyProvider.setPath(Objects.requireNonNull(path, "No path")); + keyProvider.setAlgorithm(DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM); + keyProvider.setKeySize(DEFAULT_TEST_HOST_KEY_SIZE); + 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 pairs; + try { + pairs = Objects.requireNonNull(provider.loadKeys(null), "No loaded keys"); + } catch (IOException | GeneralSecurityException e) { + throw new RuntimeException( + "Unexpected " + e.getClass().getSimpleName() + ")" + " keys loading exception: " + e.getMessage(), + e); + } + + Iterator 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"); + } + + private static Path getFile(String resource) { + URL url = CommonTestSupportUtils.class.getClassLoader().getResource(resource); + try { + return Paths.get(url.toURI()); + } catch (URISyntaxException e) { + return Paths.get(url.getPath()); + } + } + + public static FileKeyPairProvider createTestKeyPairProvider(String resource) { + Path file = getFile(resource); + file = file.toAbsolutePath(); + String filePath = Objects.toString(file, ""); + FileKeyPairProvider provider = PROVIDERS_MAP.get(filePath); + if (provider != null) { + return provider; + } + + provider = new FileKeyPairProvider(); + provider.setPaths(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; + } + } + + public static

P validateKeyPairProvider(P provider) { + Objects.requireNonNull(provider, "No provider"); + + // get the I/O out of the way + Iterable keys; + try { + keys = Objects.requireNonNull(provider.loadKeys(null), "No keys loaded"); + } catch (IOException | GeneralSecurityException e) { + throw new RuntimeException( + "Unexpected " + e.getClass().getSimpleName() + ")" + " keys loading exception: " + e.getMessage(), + e); + } + + if (keys instanceof Collection) { + ValidateUtils.checkNotNullAndNotEmpty((Collection) keys, "Empty keys loaded"); + } + + return provider; + } +} diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CoreTestSupportUtils.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CoreTestSupportUtils.java new file mode 100644 index 000000000..26781170d --- /dev/null +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CoreTestSupportUtils.java @@ -0,0 +1,83 @@ +/* + * 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.maven.scm.provider.git; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.time.Duration; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.core.CoreModuleProperties; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator; +import org.apache.sshd.server.shell.UnknownCommandFactory; + +public final class CoreTestSupportUtils { + public static final Duration READ_TIMEOUT = getTimeout("read.nio2", Duration.ofSeconds(60)); + + private CoreTestSupportUtils() { + throw new UnsupportedOperationException("No instance"); + } + + public static int getFreePort() throws Exception { + try (ServerSocket s = new ServerSocket()) { + s.setReuseAddress(true); + s.bind(new InetSocketAddress((InetAddress) null, 0)); + return s.getLocalPort(); + } + } + + public static SshServer setupTestServer(Class anchor) { + return setupTestServer(SshServer.setUpDefaultServer(), anchor); + } + + public static S setupTestServer(S sshd, Class anchor) { + sshd.setKeyPairProvider(CommonTestSupportUtils.createTestHostKeyProvider(anchor)); + sshd.setPasswordAuthenticator(BogusPasswordAuthenticator.INSTANCE); + sshd.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE); + sshd.setShellFactory(EchoShellFactory.INSTANCE); + sshd.setCommandFactory(UnknownCommandFactory.INSTANCE); + CoreModuleProperties.NIO2_READ_TIMEOUT.set(sshd, READ_TIMEOUT); + return sshd; + } + + public static Duration getTimeout(String property, Duration defaultValue) { + // Do we have a specific timeout value ? + String str = System.getProperty("org.apache.sshd.test.timeout." + property); + if (GenericUtils.isNotEmpty(str)) { + return Duration.ofMillis(Long.parseLong(str)); + } + + // Do we have a specific factor ? + str = System.getProperty("org.apache.sshd.test.timeout.factor." + property); + if (GenericUtils.isEmpty(str)) { + // Do we have a global factor ? + str = System.getProperty("org.apache.sshd.test.timeout.factor"); + } + + if (GenericUtils.isNotEmpty(str)) { + double factor = Double.parseDouble(str); + long dur = Math.round(defaultValue.toMillis() * factor); + return Duration.ofMillis(dur); + } + + return defaultValue; + } +} diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShell.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShell.java new file mode 100644 index 000000000..65771a451 --- /dev/null +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShell.java @@ -0,0 +1,40 @@ +/* + * 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.maven.scm.provider.git; + +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +/** + * @author Apache MINA SSHD Project + */ +public class EchoShell extends CommandExecutionHelper { + public EchoShell() { + super(); + } + + @Override + protected boolean handleCommandLine(String command) throws Exception { + OutputStream out = getOutputStream(); + out.write((command + "\n").getBytes(StandardCharsets.UTF_8)); + out.flush(); + + return !"exit".equals(command); + } +} diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShellFactory.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShellFactory.java new file mode 100644 index 000000000..2a8b162ee --- /dev/null +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShellFactory.java @@ -0,0 +1,41 @@ +/* + * 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.maven.scm.provider.git; + +import org.apache.sshd.server.channel.ChannelSession; +import org.apache.sshd.server.command.Command; +import org.apache.sshd.server.shell.ShellFactory; + +/** + * TODO Add javadoc + * + * @author Apache MINA SSHD Project + */ +public class EchoShellFactory implements ShellFactory { + public static final EchoShellFactory INSTANCE = new EchoShellFactory(); + + public EchoShellFactory() { + super(); + } + + @Override + public Command createShell(ChannelSession channel) { + return new EchoShell(); + } +} diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/GitSshServer.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/GitSshServer.java index a58525907..054990fd3 100644 --- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/GitSshServer.java +++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/GitSshServer.java @@ -40,8 +40,6 @@ import org.apache.sshd.server.auth.pubkey.KeySetPublickeyAuthenticator; import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; import org.apache.sshd.server.session.ServerSession; -import org.apache.sshd.util.test.CommonTestSupportUtils; -import org.apache.sshd.util.test.CoreTestSupportUtils; import org.bouncycastle.openssl.PKCS8Generator; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator; -- 2.51.0