This is an automated email from the ASF dual-hosted git repository.
jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push:
new d66ff81ea8 fixes #6909 ssh: extend test coverage
d66ff81ea8 is described below
commit d66ff81ea854384b910be4caa47b74b07dea9d01
Author: Jiri Ondrusek <[email protected]>
AuthorDate: Wed Jan 15 10:54:46 2025 +0100
fixes #6909 ssh: extend test coverage
---
.../component/ssh/deployment/SshProcessor.java | 29 ++++-
.../ssh/runtime/SubstituteEdDSAEngine.java | 78 ++++++++++++++
.../ssh/runtime/SubstituteSecurityUtils.java | 65 ------------
integration-tests/ssh/generate-certs.sh | 50 +++++++++
integration-tests/ssh/pom.xml | 27 ++++-
.../quarkus/component/ssh/it/SshResource.java | 93 +++++++++++++---
.../camel/quarkus/component/ssh/it/SshRoutes.java | 81 ++++++++++++++
.../ssh/src/main/resources/application.properties | 17 +++
.../ssh/src/main/resources/edDSA/known_hosts | 1 +
.../camel/quarkus/component/ssh/it/SshTest.java | 88 ++++++++++++++++
.../quarkus/component/ssh/it/SshTestResource.java | 64 +++++++++--
.../component/ssh/it/TestEchoCommandFactory.java | 117 +++++++++++++++++++++
.../ssh/src/test/resources/edDSA/key_ed25519.pem | 7 ++
.../src/test/resources/edDSA/key_ed25519.pem.pub | 1 +
pom.xml | 1 +
15 files changed, 627 insertions(+), 92 deletions(-)
diff --git
a/extensions/ssh/deployment/src/main/java/org/apache/camel/quarkus/component/ssh/deployment/SshProcessor.java
b/extensions/ssh/deployment/src/main/java/org/apache/camel/quarkus/component/ssh/deployment/SshProcessor.java
index c1ed5f65dd..7935194f56 100644
---
a/extensions/ssh/deployment/src/main/java/org/apache/camel/quarkus/component/ssh/deployment/SshProcessor.java
+++
b/extensions/ssh/deployment/src/main/java/org/apache/camel/quarkus/component/ssh/deployment/SshProcessor.java
@@ -26,14 +26,18 @@ import javax.crypto.Mac;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
+import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
import
io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem;
import
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
+import net.i2p.crypto.eddsa.EdDSAEngine;
import org.apache.sshd.common.channel.ChannelListener;
import org.apache.sshd.common.forward.PortForwardingEventListener;
import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory;
import org.apache.sshd.common.session.SessionListener;
+import org.jboss.jandex.IndexView;
class SshProcessor {
@@ -51,9 +55,10 @@ class SshProcessor {
KeyAgreement.class,
KeyFactory.class,
Signature.class,
- Mac.class).methods().build());
- reflectiveClasses.produce(
-
ReflectiveClassBuildItem.builder(Nio2ServiceFactoryFactory.class).build());
+ Mac.class,
+ Nio2ServiceFactoryFactory.class,
+ EdDSAEngine.class,
+
net.i2p.crypto.eddsa.KeyFactory.class).methods().build());
}
@BuildStep
@@ -71,4 +76,22 @@ class SshProcessor {
}
}
+ @BuildStep
+ ReflectiveClassBuildItem registerForReflection(CombinedIndexBuildItem
combinedIndex) {
+ IndexView index = combinedIndex.getIndex();
+
+ String[] dtos = index.getKnownClasses().stream()
+ .map(ci -> ci.name().toString())
+ .filter(n ->
n.startsWith("org.bouncycastle.crypto.signers.Ed25519"))
+ .sorted()
+ .toArray(String[]::new);
+
+ return
ReflectiveClassBuildItem.builder(dtos).methods().fields().build();
+ }
+
+ @BuildStep
+ IndexDependencyBuildItem registerDependencyForIndex2() {
+ return new IndexDependencyBuildItem("org.bouncycastle",
"bcprov-jdk18on");
+ }
+
}
diff --git
a/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteEdDSAEngine.java
b/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteEdDSAEngine.java
new file mode 100644
index 0000000000..3054c049cc
--- /dev/null
+++
b/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteEdDSAEngine.java
@@ -0,0 +1,78 @@
+/*
+ * 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.camel.quarkus.component.ssh.runtime;
+
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+import com.oracle.svm.core.annotate.Alias;
+import com.oracle.svm.core.annotate.Substitute;
+import com.oracle.svm.core.annotate.TargetClass;
+import net.i2p.crypto.eddsa.EdDSAEngine;
+import net.i2p.crypto.eddsa.EdDSAKey;
+import net.i2p.crypto.eddsa.EdDSAPublicKey;
+
+/**
+ * We're substituting those offending methods that would require the presence
of
+ * net.i2p.crypto:eddsa library which is not supported by Camel SSH component
+ */
+@TargetClass(EdDSAEngine.class)
+final class SubstituteEdDSAEngine {
+
+ @Alias
+ private MessageDigest digest;
+
+ @Alias
+ private EdDSAKey key;
+
+ @Alias
+ private void reset() {
+ }
+
+ @Substitute
+ protected void engineInitVerify(PublicKey publicKey) throws
InvalidKeyException {
+ reset();
+ if (publicKey instanceof EdDSAPublicKey) {
+ key = (EdDSAPublicKey) publicKey;
+
+ if (digest == null) {
+ // Instantiate the digest from the key parameters
+ try {
+ digest =
MessageDigest.getInstance(key.getParams().getHashAlgorithm());
+ } catch (NoSuchAlgorithmException e) {
+ throw new InvalidKeyException(
+ "cannot get required digest " +
key.getParams().getHashAlgorithm() + " for private key.");
+ }
+ } else if
(!key.getParams().getHashAlgorithm().equals(digest.getAlgorithm()))
+ throw new InvalidKeyException("Key hash algorithm does not
match chosen digest");
+ } //following line differs from the original method
+ else if (publicKey.getFormat().equals("X.509")) {
+ // X509Certificate will sometimes contain an X509Key rather than
the EdDSAPublicKey itself; the contained
+ // key is valid but needs to be instanced as an EdDSAPublicKey
before it can be used.
+ EdDSAPublicKey parsedPublicKey;
+ try {
+ parsedPublicKey = new EdDSAPublicKey(new
X509EncodedKeySpec(publicKey.getEncoded()));
+ } catch (InvalidKeySpecException ex) {
+ throw new InvalidKeyException("cannot handle X.509 EdDSA
public key: " + publicKey.getAlgorithm());
+ }
+ engineInitVerify(parsedPublicKey);
+ } else {
+ throw new InvalidKeyException("cannot identify EdDSA public key: "
+ publicKey.getClass());
+ }
+ }
+}
diff --git
a/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteSecurityUtils.java
b/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteSecurityUtils.java
deleted file mode 100644
index 2bca5bc96a..0000000000
---
a/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteSecurityUtils.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.camel.quarkus.component.ssh.runtime;
-
-import java.security.GeneralSecurityException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-
-import com.oracle.svm.core.annotate.Substitute;
-import com.oracle.svm.core.annotate.TargetClass;
-import org.apache.sshd.common.util.buffer.Buffer;
-import org.apache.sshd.common.util.security.SecurityUtils;
-
-/**
- * We're substituting those offending methods that would require the presence
of
- * net.i2p.crypto:eddsa library which is not supported by Camel SSH component
- */
-@TargetClass(SecurityUtils.class)
-final class SubstituteSecurityUtils {
-
- @Substitute
- public static boolean compareEDDSAPPublicKeys(PublicKey k1, PublicKey k2) {
- throw new UnsupportedOperationException("EdDSA Signer not available");
- }
-
- @Substitute
- public static boolean compareEDDSAPrivateKeys(PrivateKey k1, PrivateKey
k2) {
- throw new UnsupportedOperationException("EdDSA Signer not available");
- }
-
- @Substitute
- public static PublicKey generateEDDSAPublicKey(String keyType, byte[]
seed) throws GeneralSecurityException {
- throw new UnsupportedOperationException("EdDSA Signer not available");
- }
-
- @Substitute
- public static org.apache.sshd.common.signature.Signature getEDDSASigner() {
- throw new UnsupportedOperationException("EdDSA Signer not available");
- }
-
- @Substitute
- public static <B extends Buffer> B putRawEDDSAPublicKey(B buffer,
PublicKey key) {
- throw new UnsupportedOperationException("EdDSA Signer not available");
- }
-
- @Substitute
- public static PublicKey recoverEDDSAPublicKey(PrivateKey key) throws
GeneralSecurityException {
- throw new UnsupportedOperationException("EdDSA Signer not available");
- }
-
-}
diff --git a/integration-tests/ssh/generate-certs.sh
b/integration-tests/ssh/generate-certs.sh
new file mode 100755
index 0000000000..b25265c620
--- /dev/null
+++ b/integration-tests/ssh/generate-certs.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+#
+# 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.
+#
+
+
+set -e
+set -x
+
+keyType="ed25519"
+
+destinationDir="src/main/resources/edDSA"
+testDestinationDir="src/test/resources/edDSA"
+
+# see https://stackoverflow.com/a/54924640
+export MSYS_NO_PATHCONV=1
+
+if ! [[ -x "$(command -v openssl)" ]] ; then
+ echo 'Error: openssl is not installed.' >&2
+ exit 1
+fi
+
+mkdir -p "$destinationDir"
+mkdir -p "$testDestinationDir"
+
+# Ed25519 private key
+#openssl genpkey -algorithm ed25519 -out "$destinationDir/key_ed25519.pem"
+ssh-keygen -t $keyType -o -a 100 -N "" -f
"$testDestinationDir/key_ed25519.pem" -C "test@localhost"
+
+# Ed25519 public key
+#openssl pkey -in "$destinationDir/key_ed25519.pem" -pubout -out
"$destinationDir/key_ed25519.pem.pub"
+ssh-keygen -y -f "$testDestinationDir/key_ed25519.pem" >
"$testDestinationDir/key_ed25519.pem.pub"
+
+#generate known-hosts
+echo -n "127.0.0.1 $(sed 's/\(.*\) \([^ ]*\)$/\1/'
"$testDestinationDir/key_ed25519.pem.pub")" >> "$destinationDir/known_hosts"
+
+
diff --git a/integration-tests/ssh/pom.xml b/integration-tests/ssh/pom.xml
index 42b639eb61..9f03ec2cd1 100644
--- a/integration-tests/ssh/pom.xml
+++ b/integration-tests/ssh/pom.xml
@@ -35,10 +35,18 @@
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-ssh</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-direct</artifactId>
+ </dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-resteasy-jackson</artifactId>
+ </dependency>
<!-- test dependencies -->
<dependency>
@@ -67,9 +75,13 @@
<artifactId>quarkus-junit4-mock</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+
<artifactId>camel-quarkus-integration-tests-support-certificate-generator</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
-
<profiles>
<profile>
<id>native</id>
@@ -120,6 +132,19 @@
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-direct-deployment</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
</dependencies>
</profile>
<profile>
diff --git
a/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshResource.java
b/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshResource.java
index 858666f740..2b316d4e0d 100644
---
a/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshResource.java
+++
b/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshResource.java
@@ -16,19 +16,22 @@
*/
package org.apache.camel.quarkus.component.ssh.it;
+import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
-import jakarta.ws.rs.Consumes;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
+import org.apache.camel.CamelContext;
+import org.apache.camel.ConsumerTemplate;
+import org.apache.camel.Exchange;
import org.apache.camel.ProducerTemplate;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -36,17 +39,32 @@ import
org.eclipse.microprofile.config.inject.ConfigProperty;
@ApplicationScoped
public class SshResource {
- private final String user = "test";
- private final String password = "password";
+ public enum ServerType {
+ userPassword, user01Key, edKey
+ };
@ConfigProperty(name = "quarkus.ssh.host")
- private String host;
+ String host;
@ConfigProperty(name = "quarkus.ssh.port")
- private String port;
+ String port;
+ @ConfigProperty(name = "quarkus.ssh.secured-port")
+ String securedPort;
+ @ConfigProperty(name = "quarkus.ssh.ed-port")
+ String edPort;
+ @ConfigProperty(name = "ssh.username")
+ String username;
+ @ConfigProperty(name = "ssh.password")
+ String password;
+
+ @Inject
+ CamelContext camelContext;
@Inject
ProducerTemplate producerTemplate;
+ @Inject
+ ConsumerTemplate consumerTemplate;
+
@POST
@Path("/file/{fileName}")
@Consumes(MediaType.TEXT_PLAIN)
@@ -55,7 +73,7 @@ public class SshResource {
String sshWriteFileCommand = String.format("printf \"%s\" > %s",
content, fileName);
producerTemplate.sendBody(
- String.format("ssh:%s:%s?username=%s&password=%s", host, port,
user, password),
+ String.format("ssh:%s:%s?username=%s&password=%s", host, port,
username, password),
sshWriteFileCommand);
return Response
@@ -69,13 +87,60 @@ public class SshResource {
public Response readFile(@PathParam("fileName") String fileName) throws
URISyntaxException {
String sshReadFileCommand = String.format("cat %s", fileName);
- String content = producerTemplate.requestBody(
- String.format("ssh:%s:%s?username=%s&password=%s", host, port,
user, password),
- sshReadFileCommand,
+ String content = consumerTemplate.receiveBody(
+
String.format("ssh:%s:%s?username=%s&password=%s&pollCommand=%s", host, port,
username, password,
+ sshReadFileCommand),
String.class);
return Response
.ok(content)
.build();
}
+
+ @POST
+ @Path("/send")
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Map<String, String> send(@QueryParam("command") String command,
+ @QueryParam("component") @DefaultValue("ssh") String component,
+ @QueryParam("serverType") @DefaultValue("userPassword") String
serverType,
+ @QueryParam("pathSuffix") String pathSuffix,
+ Map<String, Object> headers)
+ throws URISyntaxException {
+
+ var port = switch (ServerType.valueOf(serverType)) {
+ case userPassword -> this.port;
+ case edKey -> edPort;
+ case user01Key -> securedPort;
+ };
+
+ String url = String.format("%s:%s@%s:%s", component, username, host,
port);
+ if (pathSuffix != null) {
+ url += "?" + pathSuffix;
+ }
+ Exchange exchange = producerTemplate.request(url,
+ e -> {
+ e.getIn().setHeaders(headers == null ?
Collections.emptyMap() : headers);
+ e.getIn().setBody(command == null ? "" : command);
+ });
+
+ Map<String, String> result = new HashMap<>();
+ result.put("body", exchange.getMessage().getBody(String.class));
+ result.putAll(exchange.getMessage().getHeaders().entrySet()
+ .stream()
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ //convert inputStreams
+ entry -> String.valueOf(entry.getValue() instanceof
InputStream
+ ?
camelContext.getTypeConverter().convertTo(String.class, entry.getValue())
+ : entry.getValue()))));
+
+ return result;
+ }
+
+ @Path("/sendToDirect/{direct}")
+ @POST
+ public String sendToDirect(@PathParam("direct") String direct, String
body) throws Exception {
+ return producerTemplate.requestBody("direct:" + direct, body,
String.class).trim();
+ }
+
}
diff --git
a/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshRoutes.java
b/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshRoutes.java
new file mode 100644
index 0000000000..569c103cb5
--- /dev/null
+++
b/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshRoutes.java
@@ -0,0 +1,81 @@
+/*
+ * 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.camel.quarkus.component.ssh.it;
+
+import java.nio.file.Paths;
+import java.security.Security;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Named;
+import net.i2p.crypto.eddsa.EdDSASecurityProvider;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.ssh.SshComponent;
+import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+@ApplicationScoped
+public class SshRoutes extends RouteBuilder {
+
+ @ConfigProperty(name = "quarkus.ssh.host")
+ String host;
+ @ConfigProperty(name = "quarkus.ssh.port")
+ String port;
+ @ConfigProperty(name = "ssh.username")
+ String username;
+ @ConfigProperty(name = "ssh.password")
+ String password;
+
+ @PostConstruct
+ public void init() {
+ Security.addProvider(new EdDSASecurityProvider());
+ }
+
+ @Override
+ public void configure() throws Exception {
+ // Route without SSL
+ from("direct:exampleProducer")
+ .toF("ssh://%s:%s@%s:%s", username, password, host, port);
+
+ }
+
+ /**
+ * We need to implement some conditional configuration of the {@link
SshComponent} thus we create it
+ * programmatically and publish via CDI.
+ *
+ * @return a configured {@link SshComponent}
+ */
+ @Named("ssh-with-key-provider")
+ SshComponent sshWithKeyProvider() throws IllegalAccessException,
NoSuchFieldException, InstantiationException {
+ final SshComponent sshComponent = new SshComponent();
+ sshComponent.setCamelContext(getContext());
+ sshComponent.getConfiguration()
+ .setKeyPairProvider(new
FileKeyPairProvider(Paths.get("target/certs/user01.key")));
+ sshComponent.getConfiguration().setKeyType(KeyPairProvider.SSH_RSA);
+ return sshComponent;
+ }
+
+ @Named("ssh-cert")
+ SshComponent sshCert() throws IllegalAccessException,
NoSuchFieldException, InstantiationException {
+ final SshComponent sshComponent = new SshComponent();
+ sshComponent.setCamelContext(getContext());
+ sshComponent.getConfiguration().setKeyType(null);
+ return sshComponent;
+ }
+
+}
diff --git a/integration-tests/ssh/src/main/resources/application.properties
b/integration-tests/ssh/src/main/resources/application.properties
new file mode 100644
index 0000000000..f4421ffda3
--- /dev/null
+++ b/integration-tests/ssh/src/main/resources/application.properties
@@ -0,0 +1,17 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+quarkus.native.resources.includes=edDSA/known_hosts
diff --git a/integration-tests/ssh/src/main/resources/edDSA/known_hosts
b/integration-tests/ssh/src/main/resources/edDSA/known_hosts
new file mode 100644
index 0000000000..97799187d0
--- /dev/null
+++ b/integration-tests/ssh/src/main/resources/edDSA/known_hosts
@@ -0,0 +1 @@
+127.0.0.1 ssh-ed25519
AAAAC3NzaC1lZDI1NTE5AAAAIAZTZxr7m1h9J0LJ4eyVYVqjlaL7wGE5rT0w3TGjC7dQ
\ No newline at end of file
diff --git
a/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTest.java
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTest.java
index 4681ee0a94..dd79848030 100644
---
a/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTest.java
+++
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTest.java
@@ -16,14 +16,27 @@
*/
package org.apache.camel.quarkus.component.ssh.it;
+import java.util.Map;
+
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
+import io.smallrye.certs.Format;
+import io.smallrye.certs.junit5.Certificate;
+import org.apache.camel.component.ssh.SshConstants;
+import org.apache.camel.quarkus.test.DisabledIfFipsMode;
+import org.apache.camel.quarkus.test.support.certificate.TestCertificates;
+import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+@TestCertificates(certificates = {
+ @Certificate(name = "user01", formats = {
+ Format.PEM }, password = "changeit"),
+ @Certificate(name = "eddsa", formats = {
+ Format.PEM }, password = "changeit") })
@QuarkusTest
@QuarkusTestResource(SshTestResource.class)
class SshTest {
@@ -50,4 +63,79 @@ class SshTest {
assertEquals(fileContent, sshFileContent);
}
+ @Test
+ public void testHeaders() {
+ RestAssured.given()
+ .contentType(ContentType.JSON)
+ .body(Map.of(SshConstants.USERNAME_HEADER,
SshTestResource.USERNAME, SshConstants.PASSWORD_HEADER,
+ SshTestResource.PASSWORD))
+ .queryParam("command", "wrong")
+ .post("/ssh/send/")
+ .then()
+ .statusCode(200)
+ .body("", Matchers.hasEntry(SshConstants.EXIT_VALUE, "127"))
+ .body("",
Matchers.hasEntry(Matchers.matchesRegex(SshConstants.STDERR),
+ Matchers.containsString("command not found")));
+ }
+
+ @Test
+ public void testProducerInRoute() {
+ RestAssured.given()
+ .body("echo Hello World")
+ .post("/ssh/sendToDirect/exampleProducer")
+ .then()
+ .statusCode(200)
+ .body(Matchers.equalTo("Hello World"));
+ }
+
+ @Test
+ public void testKeyProvider() {
+ RestAssured.given()
+ .contentType(ContentType.JSON)
+ .queryParam("component", "ssh-with-key-provider")
+ .queryParam("command", "echo test")
+ .queryParam("serverType", "user01Key")
+ .post("/ssh/send")
+ .then()
+ .statusCode(200)
+ .body("", Matchers.hasEntry(SshConstants.EXIT_VALUE, "0"))
+ //expecting error from command factory
+ .body("", Matchers.hasEntry(SshConstants.STDERR, "Expected
Error:echo test"));
+ }
+
+ @Test
+ public void testCertificate() {
+ RestAssured.given()
+ .contentType(ContentType.JSON)
+ .queryParam("component", "ssh-cert")
+ .queryParam("command", "echo test")
+ .queryParam("serverType", "user01Key")
+ .queryParam("pathSuffix",
"certResource=file:target/certs/user01.key&certResourcePassword=changeit")
+ .post("/ssh/send")
+ .then()
+ .statusCode(200)
+ .body("", Matchers.hasEntry(SshConstants.EXIT_VALUE, "0"))
+ //expecting error from command factory
+ .body("", Matchers.hasEntry(SshConstants.STDERR, "Expected
Error:echo test"));
+ }
+
+ @DisabledIfFipsMode //ED25519 keys are not allowed in FIPS mode
+ @Test
+ public void testProducerWithEdDSAKeyType() {
+ RestAssured.given()
+ .contentType(ContentType.JSON)
+ .queryParam("command", "echo Hello!")
+ .queryParam("serverType", "edKey")
+ .queryParam("pathSuffix",
+
"timeout=3000&knownHostsResource=/edDSA/known_hosts&failOnUnknownHost=true")
+ .body(Map.of(SshConstants.USERNAME_HEADER,
SshTestResource.USERNAME, SshConstants.PASSWORD_HEADER,
+ SshTestResource.PASSWORD))
+ .post("/ssh/send")
+ .then()
+ .statusCode(200)
+ .body("", Matchers.hasEntry(SshConstants.EXIT_VALUE, "0"))
+ //expecting error from command factory
+ .body("", Matchers.hasEntry(SshConstants.STDERR, "Expected
Error:echo Hello!"));
+ }
+
}
diff --git
a/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTestResource.java
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTestResource.java
index 56db5ec472..186d1eb434 100644
---
a/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTestResource.java
+++
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTestResource.java
@@ -16,10 +16,15 @@
*/
package org.apache.camel.quarkus.component.ssh.it;
+import java.nio.file.Paths;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
-import org.apache.camel.util.CollectionHelper;
+import org.apache.camel.quarkus.test.AvailablePortFinder;
+import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
+import org.apache.sshd.server.SshServer;
import org.eclipse.microprofile.config.ConfigProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,8 +38,12 @@ public class SshTestResource implements
QuarkusTestResourceLifecycleManager {
private static final int SSH_PORT = 2222;
private static final String SSH_IMAGE =
ConfigProvider.getConfig().getValue("openssh-server.container.image",
String.class);
+ static final String USERNAME = "user01";
+ static final String PASSWORD = "changeit";
private GenericContainer container;
+ protected List<SshServer> sshds = new LinkedList<>();
+ protected int securedPort, edPort;
@Override
public Map<String, String> start() {
@@ -45,8 +54,8 @@ public class SshTestResource implements
QuarkusTestResourceLifecycleManager {
container = new GenericContainer(SSH_IMAGE)
.withExposedPorts(SSH_PORT)
.withEnv("PASSWORD_ACCESS", "true")
- .withEnv("USER_NAME", "test")
- .withEnv("USER_PASSWORD", "password")
+ .withEnv("USER_NAME", USERNAME)
+ .withEnv("USER_PASSWORD", PASSWORD)
.waitingFor(Wait.forListeningPort());
container.start();
@@ -54,11 +63,41 @@ public class SshTestResource implements
QuarkusTestResourceLifecycleManager {
LOGGER.info("Started SSH container to {}:{}", container.getHost(),
container.getMappedPort(SSH_PORT).toString());
- return CollectionHelper.mapOf(
- "quarkus.ssh.host",
- container.getHost(),
- "quarkus.ssh.port",
- container.getMappedPort(SSH_PORT).toString());
+ securedPort = AvailablePortFinder.getNextAvailable();
+
+ var sshd = SshServer.setUpDefaultServer();
+ sshd.setPort(securedPort);
+ sshd.setKeyPairProvider(new
FileKeyPairProvider(Paths.get("target/certs/user01.key")));
+ sshd.setCommandFactory(new TestEchoCommandFactory());
+ sshd.setPasswordAuthenticator((username, password, session) ->
true);
+ sshd.setPublickeyAuthenticator((username, key, session) -> true);
+ sshd.start();
+
+ sshds.add(sshd);
+
+ edPort = AvailablePortFinder.getNextAvailable();
+
+ sshd = SshServer.setUpDefaultServer();
+ sshd.setPort(edPort);
+ sshd.setKeyPairProvider(new FileKeyPairProvider(
+
Paths.get(Thread.currentThread().getContextClassLoader().getResource("edDSA/key_ed25519.pem").toURI())));
+ sshd.setCommandFactory(new TestEchoCommandFactory());
+ sshd.setPasswordAuthenticator((username, password, session) ->
true);
+ sshd.setPublickeyAuthenticator((username, key, session) -> true);
+ sshd.start();
+
+ sshds.add(sshd);
+
+ LOGGER.info("Started SSHD server to {}:{}", container.getHost(),
+ securedPort);
+
+ return Map.of(
+ "quarkus.ssh.host", "localhost",
+ "quarkus.ssh.port",
container.getMappedPort(SSH_PORT).toString(),
+ "quarkus.ssh.secured-port", securedPort + "",
+ "quarkus.ssh.ed-port", edPort + "",
+ "ssh.username", USERNAME,
+ "ssh.password", PASSWORD);
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -66,12 +105,19 @@ public class SshTestResource implements
QuarkusTestResourceLifecycleManager {
@Override
public void stop() {
- LOGGER.info("Stopping SSH container");
+ LOGGER.info("Stopping SSH container and servers");
try {
if (container != null) {
container.stop();
}
+ sshds.forEach(s -> {
+ try {
+ s.stop(true);
+ } catch (Exception e) {
+ // ignored
+ }
+ });
} catch (Exception e) {
// ignored
}
diff --git
a/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/TestEchoCommandFactory.java
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/TestEchoCommandFactory.java
new file mode 100644
index 0000000000..26af147266
--- /dev/null
+++
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/TestEchoCommandFactory.java
@@ -0,0 +1,117 @@
+/*
+ * 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.camel.quarkus.component.ssh.it;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.CountDownLatch;
+
+import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.command.Command;
+import org.apache.sshd.server.command.CommandFactory;
+
+public class TestEchoCommandFactory implements CommandFactory {
+
+ @Override
+ public Command createCommand(ChannelSession channelSession, String
command) {
+ return new TestEchoCommand(command);
+ }
+
+ public static class TestEchoCommand extends EchoCommand {
+ public static CountDownLatch latch = new CountDownLatch(1);
+
+ public TestEchoCommand(String command) {
+ super(command);
+ }
+
+ @Override
+ public void destroy(ChannelSession channelSession) throws Exception {
+ if (latch != null) {
+ latch.countDown();
+ }
+ super.destroy(channelSession);
+ }
+ }
+
+ protected static class EchoCommand implements Command, Runnable {
+ private String command;
+ private OutputStream out;
+ private OutputStream err;
+ private ExitCallback callback;
+ private Thread thread;
+
+ public EchoCommand(String command) {
+ this.command = command;
+ }
+
+ @Override
+ public void setInputStream(InputStream in) {
+ }
+
+ @Override
+ public void setOutputStream(OutputStream out) {
+ this.out = out;
+ }
+
+ @Override
+ public void setErrorStream(OutputStream err) {
+ this.err = err;
+ }
+
+ @Override
+ public void setExitCallback(ExitCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void start(ChannelSession channelSession, Environment
environment) throws IOException {
+ thread = new Thread(this, "EchoCommand");
+ thread.start();
+ }
+
+ @Override
+ public void destroy(ChannelSession channelSession) throws Exception {
+ // noop
+ }
+
+ @Override
+ public void run() {
+ boolean succeeded = true;
+ String message = null;
+ try {
+ // we set the error with the same command message
+ err.write("Expected Error:".getBytes());
+ err.write(command.getBytes());
+ err.flush();
+ out.write(command.getBytes());
+ out.flush();
+ } catch (Exception e) {
+ succeeded = false;
+ message = e.toString();
+ } finally {
+ if (succeeded) {
+ callback.onExit(0);
+ } else {
+ callback.onExit(1, message);
+ }
+ }
+ }
+ }
+}
diff --git a/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem
b/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem
new file mode 100644
index 0000000000..9d29f1c2ec
--- /dev/null
+++ b/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAGU2ca+5tYfSdCyeHslWFao5Wi+8BhOa09MN0xowu3UAAAAJjAekuswHpL
+rAAAAAtzc2gtZWQyNTUxOQAAACAGU2ca+5tYfSdCyeHslWFao5Wi+8BhOa09MN0xowu3UA
+AAAEA9x2k0q80wlvdiEsXoPCYMSFcD1qQu8IPHlwhCyYBEEgZTZxr7m1h9J0LJ4eyVYVqj
+laL7wGE5rT0w3TGjC7dQAAAADnRlc3RAbG9jYWxob3N0AQIDBAUGBw==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem.pub
b/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem.pub
new file mode 100644
index 0000000000..5463e28814
--- /dev/null
+++ b/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem.pub
@@ -0,0 +1 @@
+ssh-ed25519
AAAAC3NzaC1lZDI1NTE5AAAAIAZTZxr7m1h9J0LJ4eyVYVqjlaL7wGE5rT0w3TGjC7dQ
test@localhost
diff --git a/pom.xml b/pom.xml
index 58f78ed57f..a7a80fd3c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -604,6 +604,7 @@
<exclude>**/my-property</exclude>
<exclude>release-utils/*.sh</exclude>
<exclude>**/*.wasm</exclude>
+ <exclude>**/known_hosts</exclude>
</excludes>
</licenseSet>
</licenseSets>