diff --git a/pom.xml b/pom.xml
index 170fbac..53b984a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.jenkins-ci.plugins
plugin
- 4.86
+ 4.82
@@ -112,11 +112,26 @@
io.jenkins.plugins
gson-api
-
+
io.jenkins.plugins
eddsa-api
0.3.0-4.v84c6f0f4969e
-
+
+
+ org.jenkins-ci.plugins
+ variant
+
+
+ org.jenkins-ci.plugins
+ ssh-credentials
+ true
+
+
+
+ io.jenkins.plugins.mina-sshd-api
+ mina-sshd-api-core
+ test
+
diff --git a/src/main/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPasswordAuthenticator.java b/src/main/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPasswordAuthenticator.java
new file mode 100644
index 0000000..5b1420f
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPasswordAuthenticator.java
@@ -0,0 +1,197 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2011-2012, CloudBees, Inc., Stephen Connolly.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.jenkinsci.plugins.trileadapi;
+
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator;
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticatorFactory;
+import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
+import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
+import com.trilead.ssh2.Connection;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import org.jenkinsci.plugins.variant.OptionalExtension;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.logging.Logger;
+
+/**
+ * Does password auth with a {@link Connection}.
+ */
+public class TrileadSSHPasswordAuthenticator extends SSHAuthenticator {
+
+ /**
+ * Our logger
+ */
+ private static final Logger LOGGER = Logger.getLogger(TrileadSSHPasswordAuthenticator.class.getName());
+ private static final String PASSWORD = "password";
+ private static final String KEYBOARD_INTERACTIVE = "keyboard-interactive";
+
+ /**
+ * Constructor.
+ *
+ * @param connection the connection we will be authenticating.
+ * @deprecated
+ */
+ @Deprecated
+ public TrileadSSHPasswordAuthenticator(Connection connection, StandardUsernamePasswordCredentials user) {
+ this(connection, user, null);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param connection the connection we will be authenticating.
+ * @since 1.4
+ */
+ public TrileadSSHPasswordAuthenticator(@NonNull Connection connection,
+ @NonNull StandardUsernamePasswordCredentials user,
+ @CheckForNull String username) {
+ super(connection, user, username);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean canAuthenticate() {
+ try {
+ for (String authMethod : getConnection().getRemainingAuthMethods(getUsername())) {
+ if (PASSWORD.equals(authMethod)) {
+ // prefer password
+ return true;
+ }
+ if (KEYBOARD_INTERACTIVE.equals(authMethod)) {
+ return true;
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace(getListener().error("Failed to authenticate"));
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean doAuthenticate() {
+ final StandardUsernamePasswordCredentials user = getUser();
+ final String username = getUsername();
+
+ try {
+ final Connection connection = getConnection();
+ final String password = user.getPassword().getPlainText();
+ boolean tried = false;
+
+ List availableMethods = Arrays.asList(connection.getRemainingAuthMethods(username));
+ if (availableMethods.contains(PASSWORD)) {
+ // prefer password
+ if (connection.authenticateWithPassword(username, password)) {
+ LOGGER.fine("Authentication with 'password' succeeded.");
+ return true;
+ }
+ getListener().error("Failed to authenticate as %s. Wrong password. (credentialId:%s/method:password)",
+ username, user.getId());
+ tried = true;
+ }
+ if (availableMethods.contains(KEYBOARD_INTERACTIVE)) {
+ if (connection.authenticateWithKeyboardInteractive(username, (name, instruction, numPrompts, prompt, echo) -> {
+ // most SSH servers just use keyboard interactive to prompt for the password
+ // match "assword" is safer than "password"... you don't *want* to know why!
+ return prompt != null && prompt.length > 0 && prompt[0].toLowerCase(Locale.ENGLISH)
+ .contains("assword")
+ ? new String[]{password}
+ : new String[0];
+ })) {
+ LOGGER.fine("Authentication with 'keyboard-interactive' succeeded.");
+ return true;
+ }
+ getListener()
+ .error("Failed to authenticate as %s. Wrong password. "
+ + "(credentialId:%s/method:keyboard-interactive)",
+ username, user.getId());
+ tried = true;
+ }
+
+ if (!tried) {
+ getListener().error("The server does not allow password authentication. Available options are %s",
+ availableMethods);
+ }
+ } catch (IOException e) {
+ e.printStackTrace(getListener()
+ .error("Unexpected error while trying to authenticate as %s with credential=%s", username,
+ user.getId()));
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @OptionalExtension(requirePlugins = {"ssh-credentials"})
+ public static class Factory extends SSHAuthenticatorFactory {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected SSHAuthenticator newInstance(@NonNull C connection,
+ @NonNull U user) {
+ return newInstance(connection, user, null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Nullable
+ @Override
+ @SuppressWarnings("unchecked")
+ protected SSHAuthenticator newInstance(@NonNull C connection,
+ @NonNull U user,
+ @CheckForNull String
+ username) {
+ if (supports(connection.getClass(), user.getClass())) {
+ return (SSHAuthenticator) new TrileadSSHPasswordAuthenticator((Connection) connection,
+ (StandardUsernamePasswordCredentials) user, username);
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean supports(@NonNull Class connectionClass,
+ @NonNull Class userClass) {
+ return Connection.class.isAssignableFrom(connectionClass)
+ && StandardUsernamePasswordCredentials.class.isAssignableFrom(userClass);
+ }
+
+ private static final long serialVersionUID = 1L;
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPublicKeyAuthenticator.java b/src/main/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPublicKeyAuthenticator.java
new file mode 100644
index 0000000..89fd6ef
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPublicKeyAuthenticator.java
@@ -0,0 +1,184 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2011-2012, CloudBees, Inc., Stephen Connolly.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.jenkinsci.plugins.trileadapi;
+
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator;
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticatorFactory;
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
+import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
+import com.trilead.ssh2.Connection;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import hudson.util.Secret;
+import org.jenkinsci.plugins.variant.OptionalExtension;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * Does public key auth with a {@link Connection}.
+ */
+public class TrileadSSHPublicKeyAuthenticator extends SSHAuthenticator {
+
+ /**
+ * Our logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(TrileadSSHPublicKeyAuthenticator.class.getName());
+ private static final String PUBLICKEY = "publickey";
+
+ /**
+ * Constructor.
+ *
+ * @param connection the connection we will be authenticating.
+ */
+ public TrileadSSHPublicKeyAuthenticator(Connection connection, SSHUserPrivateKey user) {
+ this(connection, user, null);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param connection the connection we will be authenticating.
+ */
+ public TrileadSSHPublicKeyAuthenticator(@NonNull Connection connection,
+ @NonNull SSHUserPrivateKey user,
+ @CheckForNull String username) {
+ super(connection, user, username);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean canAuthenticate() {
+ try {
+ return getRemainingAuthMethods().contains(PUBLICKEY);
+ } catch (IOException e) {
+ e.printStackTrace(getListener().error("Failed to authenticate"));
+ return false;
+ }
+ }
+
+ private List getRemainingAuthMethods() throws IOException {
+ return Arrays.asList(getConnection().getRemainingAuthMethods(getUsername()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean doAuthenticate() {
+ final SSHUserPrivateKey user = getUser();
+ final String username = getUsername();
+ try {
+ final Connection connection = getConnection();
+ final Secret userPassphrase = user.getPassphrase();
+ final String passphrase = userPassphrase == null ? null : userPassphrase.getPlainText();
+
+ Collection availableMethods = getRemainingAuthMethods();
+ if (availableMethods.contains(PUBLICKEY)) {
+ int count = 0;
+ List ioe = new ArrayList<>();
+ for (String privateKey : getPrivateKeys(user)) {
+ try {
+ if (connection.authenticateWithPublicKey(username, privateKey.toCharArray(), passphrase)) {
+ LOGGER.fine("Authentication with 'publickey' succeeded.");
+ return true;
+ }
+ } catch (IOException e) {
+ ioe.add(e);
+ }
+ count++;
+ getListener()
+ .error("Server rejected the %d private key(s) for %s (credentialId:%s/method:publickey)",
+ count, username, user.getId());
+ }
+ for (IOException e : ioe) {
+ e.printStackTrace(getListener()
+ .error("Failed to authenticate as %s with credential=%s", username, getUser().getId()));
+ }
+ return false;
+ } else {
+ getListener().error("The server does not allow public key authentication. Available options are %s",
+ availableMethods);
+ return false;
+ }
+ } catch (IOException e) {
+ e.printStackTrace(getListener()
+ .error("Failed to authenticate as %s with credential=%s", username, getUser().getId()));
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @OptionalExtension(requirePlugins = {"ssh-credentials"})
+ public static class Factory extends SSHAuthenticatorFactory {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ protected SSHAuthenticator newInstance(@NonNull C connection,
+ @NonNull U user) {
+ return newInstance(connection, user, null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Nullable
+ @Override
+ @SuppressWarnings("unchecked")
+ protected SSHAuthenticator newInstance(@NonNull C connection,
+ @NonNull U user,
+ @CheckForNull String
+ username) {
+ if (supports(connection.getClass(), user.getClass())) {
+ return (SSHAuthenticator) new TrileadSSHPublicKeyAuthenticator((Connection) connection,
+ (SSHUserPrivateKey) user, username);
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean supports(@NonNull Class connectionClass,
+ @NonNull Class userClass) {
+ return Connection.class.isAssignableFrom(connectionClass)
+ && SSHUserPrivateKey.class.isAssignableFrom(userClass);
+ }
+
+ private static final long serialVersionUID = 1L;
+ }
+}
diff --git a/src/test/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPasswordAuthenticatorTest.java b/src/test/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPasswordAuthenticatorTest.java
new file mode 100644
index 0000000..61805ad
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPasswordAuthenticatorTest.java
@@ -0,0 +1,296 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2011-2012, CloudBees, Inc., Stephen Connolly.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.jenkinsci.plugins.trileadapi;
+
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator;
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticatorFactory;
+import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPassword;
+import com.cloudbees.plugins.credentials.CredentialsScope;
+import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
+import com.trilead.ssh2.Connection;
+import com.trilead.ssh2.ServerHostKeyVerifier;
+import hudson.model.Computer;
+import hudson.model.Items;
+import hudson.model.TaskListener;
+import hudson.remoting.VirtualChannel;
+import hudson.slaves.DumbSlave;
+import jenkins.security.MasterToSlaveCallable;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Proxy;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class TrileadSSHPasswordAuthenticatorTest {
+
+ private Connection connection;
+ private StandardUsernamePasswordCredentials user;
+ private Object sshd;
+
+ @Rule public JenkinsRule r = new JenkinsRule();
+
+ @After
+ public void tearDown() {
+ if (connection != null) {
+ connection.close();
+ connection = null;
+ }
+ if (sshd!=null) {
+ try {
+ invoke(sshd, "stop", new Class>[] {Boolean.TYPE}, new Object[] {true});
+ } catch (Throwable t) {
+ Logger.getLogger(getClass().getName()).log(Level.WARNING, "Problems shutting down ssh server", t);
+ }
+ }
+ }
+
+ // disabled as Apache MINA sshd does not provide easy mech for giving a Keyboard Interactive authenticator
+ // so this test relies on having a local sshd which is keyboard interactive only
+ public void dontTestKeyboardInteractive() throws Exception {
+ connection = new Connection("localhost");
+ connection.connect(new NoVerifier());
+ TrileadSSHPasswordAuthenticator instance =
+ new TrileadSSHPasswordAuthenticator(connection, new BasicSSHUserPassword(CredentialsScope.SYSTEM,
+ null, "....", // <---- put your username here
+ "....", // <---- put your password here
+ null));
+ assertThat(instance.canAuthenticate(), is(true));
+ assertThat(instance.authenticate(), is(true));
+ assertThat(instance.isAuthenticated(), is(true));
+ }
+
+ @Before
+ public void setUp() {
+ user =(StandardUsernamePasswordCredentials) Items.XSTREAM.fromXML(Items.XSTREAM.toXML(new BasicSSHUserPassword(CredentialsScope.SYSTEM, null, "foobar", "foomanchu", null)));
+ }
+
+ @Test
+ public void testPassword() throws Exception {
+ sshd = createPasswordAuthenticatedSshServer();
+ invoke(sshd, "start", null, null);
+ int port = (Integer)invoke(sshd, "getPort", null, null);
+ connection = new Connection("localhost", port);
+ connection.connect(new NoVerifier());
+ TrileadSSHPasswordAuthenticator instance =
+ new TrileadSSHPasswordAuthenticator(connection, user);
+ assertThat(instance.getAuthenticationMode(), is(SSHAuthenticator.Mode.AFTER_CONNECT));
+ assertThat(instance.canAuthenticate(), is(true));
+ assertThat(instance.authenticate(), is(true));
+ assertThat(instance.isAuthenticated(), is(true));
+ }
+
+ private Object createPasswordAuthenticatedSshServer() throws InvocationTargetException, NoSuchMethodException, ClassNotFoundException, InstantiationException, IllegalAccessException {
+ return createPasswordAuthenticatedSshServer(null);
+ }
+
+ private Object createPasswordAuthenticatedSshServer(final String username) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, InstantiationException {
+ Object sshd = newDefaultSshServer();
+ Class> keyPairProviderClass = newKeyPairProviderClass();
+ Object provider = newProvider();
+ Class> authenticatorClass = newAuthenticatorClass();
+ Object authenticator = newAuthenticator(authenticatorClass, username);
+ Object factory = newFactory();
+
+ invoke(sshd, "setPort", new Class>[] {Integer.TYPE}, new Object[] {0});
+ invoke(sshd, "setKeyPairProvider", new Class>[] {keyPairProviderClass}, new Object[] {provider});
+ invoke(sshd, "setPasswordAuthenticator", new Class>[] {authenticatorClass}, new Object[] {authenticator});
+ invoke(sshd, "setUserAuthFactories", new Class>[] {List.class}, new Object[] {Collections.singletonList(factory)});
+
+ return sshd;
+ }
+
+ @Test
+ public void testFactory() throws Exception {
+ sshd = createPasswordAuthenticatedSshServer();
+ invoke(sshd, "start", null, null);
+ int port = (Integer)invoke(sshd, "getPort", null, null);
+ connection = new Connection("localhost", port);
+ connection.connect(new NoVerifier());
+ SSHAuthenticator instance = SSHAuthenticator.newInstance(connection, user);
+ assertThat(instance.getAuthenticationMode(), is(SSHAuthenticator.Mode.AFTER_CONNECT));
+ assertThat(instance.canAuthenticate(), is(true));
+ assertThat(instance.authenticate(), is(true));
+ assertThat(instance.isAuthenticated(), is(true));
+ }
+
+ @Test
+ public void testFactoryAltUsername() throws Exception {
+ sshd = createPasswordAuthenticatedSshServer("bill");
+ invoke(sshd, "start", null, null);
+ int port = (Integer)invoke(sshd, "getPort", null, null);
+ connection = new Connection("localhost", port);
+ connection.connect(new NoVerifier());
+ SSHAuthenticator instance = SSHAuthenticator.newInstance(connection, user, null);
+ assertThat(instance.getAuthenticationMode(), is(SSHAuthenticator.Mode.AFTER_CONNECT));
+ assertThat(instance.canAuthenticate(), is(true));
+ assertThat(instance.authenticate(), is(false));
+ assertThat(instance.isAuthenticated(), is(false));
+ connection = new Connection("localhost", port);
+ connection.connect(new NoVerifier());
+ instance = SSHAuthenticator.newInstance(connection, user, "bill");
+ assertThat(instance.getAuthenticationMode(), is(SSHAuthenticator.Mode.AFTER_CONNECT));
+ assertThat(instance.canAuthenticate(), is(true));
+ assertThat(instance.authenticate(), is(true));
+ assertThat(instance.isAuthenticated(), is(true));
+ }
+
+ /**
+ * Brings the {@link SSHAuthenticatorFactory} to a slave.
+ */
+ @Test
+ public void testSlave() throws Exception {
+ Object sshd = createPasswordAuthenticatedSshServer();
+ invoke(sshd, "start", null, null);
+
+ DumbSlave s = r.createSlave();
+ Computer c = s.toComputer();
+ assertNotNull(c);
+ c.connect(false).get();
+
+ final int port = (Integer)invoke(sshd, "getPort", null, null);
+
+ TaskListener l = r.createTaskListener();
+ VirtualChannel channel = c.getChannel();
+ assertNotNull(channel);
+ channel.call(new RemoteConnectionTest(port, user));
+ }
+
+ private static class NoVerifier implements ServerHostKeyVerifier {
+ public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm,
+ byte[] serverHostKey) {
+ return true;
+ }
+ }
+
+ private static final class RemoteConnectionTest extends MasterToSlaveCallable {
+ private final int port;
+ private final StandardUsernamePasswordCredentials user;
+
+ public RemoteConnectionTest(int port, StandardUsernamePasswordCredentials user) {
+ this.port = port;
+ this.user = user;
+ }
+
+ public Void call() throws Exception {
+ Connection connection = new Connection("localhost", port);
+ connection.connect(new NoVerifier());
+ SSHAuthenticator instance = SSHAuthenticator.newInstance(connection,user);
+
+ assertThat(instance.getAuthenticationMode(), is(SSHAuthenticator.Mode.AFTER_CONNECT));
+ assertThat(instance.canAuthenticate(), is(true));
+ instance.authenticateOrFail();
+ assertThat(instance.isAuthenticated(), is(true));
+ connection.close();
+ return null;
+ }
+
+ private static final long serialVersionUID = 1L;
+ }
+
+ private Object invoke(Object target, String methodName, Class>[] parameterTypes, Object[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ return target.getClass().getMethod(methodName, parameterTypes).invoke(target, args);
+ }
+
+ private Object newDefaultSshServer() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ Object server = null;
+ Class> serverClass;
+ try {
+ serverClass = Class.forName("org.apache.sshd.SshServer");
+ } catch (ClassNotFoundException e) {
+ serverClass = Class.forName("org.apache.sshd.server.SshServer");
+ }
+
+ server = serverClass.getDeclaredMethod("setUpDefaultServer").invoke(null);
+ assertNotNull(server);
+
+ return server;
+ }
+
+ private Class> newKeyPairProviderClass() throws ClassNotFoundException {
+ Class> keyPairProviderClass;
+ try {
+ keyPairProviderClass = Class.forName("org.apache.sshd.common.KeyPairProvider");
+ } catch (ClassNotFoundException e) {
+ keyPairProviderClass = Class.forName("org.apache.sshd.common.keyprovider.KeyPairProvider");
+ }
+
+ return keyPairProviderClass;
+ }
+
+ private Object newProvider() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
+ Class> providerClass = Class.forName("org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider");
+ Object provider = providerClass.getConstructor().newInstance();
+ assertNotNull(provider);
+
+ return provider;
+ }
+
+ private Class> newAuthenticatorClass() throws ClassNotFoundException {
+ Class> authenticatorClass;
+ try {
+ authenticatorClass = Class.forName("org.apache.sshd.server.auth.password.PasswordAuthenticator");
+ } catch(ClassNotFoundException e) {
+ authenticatorClass = Class.forName("org.apache.sshd.server.PasswordAuthenticator");
+ }
+
+ return authenticatorClass;
+ }
+
+ private Object newAuthenticator(Class> authenticatorClass, final String userName) throws IllegalArgumentException {
+ Object authenticator = Proxy.newProxyInstance(
+ authenticatorClass.getClassLoader(), new Class>[]{authenticatorClass}, (proxy, method, args) ->
+ method.getName().equals("authenticate") ?
+ (userName == null || userName.equals(args[0])) && "foomanchu".equals(args[1]) :
+ null);
+ assertNotNull(authenticator);
+ return authenticator;
+ }
+
+ private Object newFactory() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
+ Object factory = null;
+ Class> factoryClass;
+ try {
+ factoryClass = Class.forName("org.apache.sshd.server.auth.UserAuthPassword$Factory");
+ } catch (ClassNotFoundException e) {
+ factoryClass = Class.forName("org.apache.sshd.server.auth.password.UserAuthPasswordFactory");
+ }
+
+ factory = factoryClass.getConstructor().newInstance();
+
+ assertNotNull(factory);
+ return factory;
+ }
+}
diff --git a/src/test/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPublicKeyAuthenticatorTest.java b/src/test/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPublicKeyAuthenticatorTest.java
new file mode 100644
index 0000000..f407105
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/trileadapi/TrileadSSHPublicKeyAuthenticatorTest.java
@@ -0,0 +1,308 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2011-2012, CloudBees, Inc., Stephen Connolly.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.jenkinsci.plugins.trileadapi;
+
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator;
+import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
+import com.cloudbees.plugins.credentials.CredentialsDescriptor;
+import com.cloudbees.plugins.credentials.CredentialsScope;
+import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
+import com.trilead.ssh2.Connection;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.util.Secret;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static java.lang.reflect.Proxy.*;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertNotNull;
+
+public class TrileadSSHPublicKeyAuthenticatorTest {
+
+ private Connection connection;
+ private SSHUserPrivateKey user;
+
+ @Rule public JenkinsRule r = new JenkinsRule();
+
+ @After
+ public void tearDown() {
+ if (connection != null) {
+ connection.close();
+ connection = null;
+ }
+ }
+
+ @Before
+ public void setUp() {
+ user = new SSHUserPrivateKey() {
+
+ @NonNull
+ public String getUsername() {
+ return "foobar";
+ }
+
+ @NonNull
+ public String getDescription() {
+ return "";
+ }
+
+ @NonNull
+ public String getId() {
+ return "";
+ }
+
+ public CredentialsScope getScope() {
+ return CredentialsScope.SYSTEM;
+ }
+
+ @NonNull
+ public CredentialsDescriptor getDescriptor() {
+ return new CredentialsDescriptor() {
+ @NonNull
+ @Override
+ public String getDisplayName() {
+ return "";
+ }
+ };
+ }
+
+ @CheckForNull
+ public Secret getPassphrase() {
+ return null;
+ }
+
+ @NonNull
+ public List getPrivateKeys() {
+ // just want a valid key... I generated this and have thrown it away (other than here)
+ // do not use other than in this test
+ return List.of("-----BEGIN RSA PRIVATE KEY-----\n"
+ + "MIICWQIBAAKBgQDADDwooNPJNQB4N4bJPiBgq/rkWKMABApX0w4trSkkX5q+l+CL\n"
+ + "CuddGGAsAu6XPari8v49ipbBmHqRLP9+X3ARGWKU2gDvGTBr99/ReUl2YgVjCwy+\n"
+ + "KMrGCN7SNTgRo6StwVaPhh6pUpNTQciDe/kOwUnQFWSM6/lwkOD1Uod45wIBIwKB\n"
+ + "gHi3O8HELVnmzRhdaqphkLHLL/0/B18Ye4epPBy1/JqFPLJQ1kjFBnUIAe/HVCSN\n"
+ + "KZX30wIcmUZ9GdeYoJiTwsfTy9t2KwHjqrapTfiekVZAW+3iDBqRZMxQ5MoK7b6g\n"
+ + "w5HrrtrtPfYuAsBnYjIS6qsKAVT3vdolJ5eai/RlPO4LAkEA76YuUozC/dW7Ox+R\n"
+ + "1Njd6cWJsRVXGemkSYY/rSh0SbfHAebqL/bDg8xXim9UiuD9Hc6md3glHQj6iKvl\n"
+ + "BxWq4QJBAM0moKiM16WFSFJP1wVDj0Bnx6DkJYSpf5u+C0ghBVoqIYKq6/P/gRE2\n"
+ + "+ColsLu6AYftaEJVpAgxeTU/IsGoJMcCQHRmqMkCipiMYkFJ2R49cxnGWNJa0ojt\n"
+ + "03QrQ3/9tNNZQ2dS5sbW8UAEKoURgNW9vMVVvpHMpE/uaw8u65W6ESsCQDTAyjn4\n"
+ + "VLWIrDJsTTveLCaBFhNt3cMHA45ysnGiF1GzD+5mdzAdITBP9qvAjIgLQjjlRrH4\n"
+ + "w8eXsXQXjJgyjR0CQHfvhiMPG5pWwmXpsEOFo6GKSvOC/5sNEcnddenuO/2T7WWi\n"
+ + "o1LQh9naeuX8gti0vNR8+KtMEaIcJJeWnk56AVY=\n"
+ + "-----END RSA PRIVATE KEY-----\n");
+ }
+ };
+ }
+
+ @Test
+ public void testAuthenticate() throws Exception {
+ Object sshd = newDefaultSshServer();
+ Class> keyPairProviderClass = newKeyPairProviderClass();
+ Object provider = newProvider();
+ Class> authenticatorClass = newAuthenticatorClass();
+ Object authenticator = newAuthenticator(authenticatorClass, "foobar");
+ Object factory = newFactory();
+ assertNotNull(factory);
+
+ invoke(sshd, "setPort", new Class>[] {Integer.TYPE}, new Object[] {0});
+ invoke(sshd, "setKeyPairProvider", new Class>[] {keyPairProviderClass}, new Object[] {provider});
+ invoke(sshd, "setPublickeyAuthenticator", new Class>[] {authenticatorClass}, new Object[] {authenticator});
+ invoke(sshd, "setUserAuthFactories", new Class>[] {List.class}, new Object[] {Collections.singletonList(factory)});
+
+ try {
+ invoke(sshd, "start", null, null);
+ int port = (Integer)invoke(sshd, "getPort", null, null);
+ connection = new Connection("localhost", port);
+ connection.connect((hostname, port1, serverHostKeyAlgorithm, serverHostKey) -> true);
+ TrileadSSHPublicKeyAuthenticator instance =
+ new TrileadSSHPublicKeyAuthenticator(connection, user);
+ assertThat(instance.getAuthenticationMode(), is(SSHAuthenticator.Mode.AFTER_CONNECT));
+ assertThat(instance.canAuthenticate(), is(true));
+ assertThat(instance.authenticate(), is(true));
+ assertThat(instance.isAuthenticated(), is(true));
+ } finally {
+ try {
+ invoke(sshd, "stop", new Class>[] {Boolean.TYPE}, new Object[] {true});
+ } catch (Throwable t) {
+ Logger.getLogger(getClass().getName()).log(Level.WARNING, "Problems shutting down ssh server", t);
+ }
+ }
+ }
+
+ @Test
+ public void testFactory() throws Exception {
+ Object sshd = newDefaultSshServer();
+ Class> keyPairProviderClass = newKeyPairProviderClass();
+ Object provider = newProvider();
+ Class> authenticatorClass = newAuthenticatorClass();
+ Object authenticator = newAuthenticator(authenticatorClass, "foobar");
+ Object factory = newFactory();
+ assertNotNull(factory);
+
+ invoke(sshd, "setPort", new Class>[] {Integer.TYPE}, new Object[] {0});
+ invoke(sshd, "setKeyPairProvider", new Class>[] {keyPairProviderClass}, new Object[] {provider});
+ invoke(sshd, "setPublickeyAuthenticator", new Class>[] {authenticatorClass}, new Object[] {authenticator});
+ invoke(sshd, "setUserAuthFactories", new Class>[] {List.class}, new Object[] {Collections.singletonList(factory)});
+ try {
+ invoke(sshd, "start", null, null);
+ int port = (Integer)invoke(sshd, "getPort", null, null);
+ connection = new Connection("localhost", port);
+ connection.connect((hostname, port1, serverHostKeyAlgorithm, serverHostKey) -> true);
+ SSHAuthenticator