Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,22 @@ default String password() {
default String directoryName() {
return "myTestDirectory";
}

/**
* Returns the SSH host key fingerprint in SHA256 format (SFTP only).
*
* @return the host key fingerprint or null if not applicable
*/
default String hostKeyFingerprint() {
return null;
}

/**
* Returns the known_hosts entry for this server (SFTP only).
*
* @return the known_hosts entry or null if not applicable
*/
default String knownHostsEntry() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ public boolean isClientAuth() {
+ "zFd/Bk51E65UTmmSrmW0O1ohtzi6HzsDPjXgCtlTt3FqTcfFfI92IlTr4JWqC9UK1QT1ZTeng0MkPQmv68hDANHbt5CpETZHjW5q4OOgWhV"
+ "vj5IyOC2NZHtKlJBkdsMAa15ouOOJLzBvAvbqOR/yUROsEiQ==";

static final String BUNDLED_HOST_KEY_RESOURCE = "hostkey.pem";
static final String BUNDLED_KEYSTORE_RESOURCE = "server.jks";

private String testDirectory;
private List<User> users = new ArrayList<>();
private User admin;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,27 @@
package org.apache.camel.test.infra.ftp.services.embedded;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

import org.apache.camel.spi.annotations.InfraService;
import org.apache.camel.test.infra.ftp.services.FtpInfraService;
import org.apache.ftpserver.FtpServerFactory;
import org.apache.ftpserver.listener.ListenerFactory;
import org.apache.ftpserver.ssl.SslConfigurationFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@InfraService(service = FtpInfraService.class,
description = "Embedded FTPS Server",
serviceAlias = { "ftps" })
public class FtpsEmbeddedInfraService extends FtpEmbeddedInfraService {

private static final Logger LOG = LoggerFactory.getLogger(FtpsEmbeddedInfraService.class);

/**
* Use a default constructor with a default security configuration for camel jbang
*/
Expand Down Expand Up @@ -68,7 +77,8 @@ private SslConfigurationFactory createSslConfiguration(EmbeddedConfiguration emb
SslConfigurationFactory sslConfigFactory = new SslConfigurationFactory();
sslConfigFactory.setSslProtocol(embeddedConfiguration.getSecurityConfiguration().getAuthValue());

sslConfigFactory.setKeystoreFile(new File(embeddedConfiguration.getKeyStore()));
File keystoreFile = resolveKeystoreFile(embeddedConfiguration.getKeyStore());
sslConfigFactory.setKeystoreFile(keystoreFile);
sslConfigFactory.setKeystoreType(embeddedConfiguration.getKeyStoreType());
sslConfigFactory.setKeystoreAlgorithm(embeddedConfiguration.getKeyStoreAlgorithm());
sslConfigFactory.setKeystorePassword(embeddedConfiguration.getKeyStorePassword());
Expand All @@ -77,12 +87,53 @@ private SslConfigurationFactory createSslConfiguration(EmbeddedConfiguration emb
sslConfigFactory.setClientAuthentication(embeddedConfiguration.getSecurityConfiguration().getAuthValue());

if (embeddedConfiguration.getSecurityConfiguration().isClientAuth()) {
sslConfigFactory.setTruststoreFile(new File(embeddedConfiguration.getKeyStore()));
sslConfigFactory.setTruststoreFile(keystoreFile);
sslConfigFactory.setTruststoreType(embeddedConfiguration.getKeyStoreType());
sslConfigFactory.setTruststoreAlgorithm(embeddedConfiguration.getKeyStoreAlgorithm());
sslConfigFactory.setTruststorePassword(embeddedConfiguration.getKeyStorePassword());
}

return sslConfigFactory;
}

/**
* Resolves the keystore file, trying file path first, then classpath resource.
*
* @param configuredPath the configured keystore file path
* @return the resolved keystore file
*/
private File resolveKeystoreFile(String configuredPath) {
// First try the configured file path
File keystoreFile = new File(configuredPath);
if (keystoreFile.exists()) {
LOG.debug("Using keystore file: {}", keystoreFile.getAbsolutePath());
return keystoreFile;
}

// Fall back to classpath resource
return extractKeystoreFromClasspath();
}

/**
* Extracts the bundled keystore from classpath to a temporary file.
*
* @return the temporary file containing the keystore
*/
private File extractKeystoreFromClasspath() {
try (InputStream is = getClass().getClassLoader()
.getResourceAsStream(EmbeddedConfiguration.BUNDLED_KEYSTORE_RESOURCE)) {
if (is != null) {
Path tempFile = Files.createTempFile("ftps-keystore-", ".jks");
Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING);
tempFile.toFile().deleteOnExit();
LOG.info("Using bundled keystore from classpath, extracted to: {}", tempFile);
return tempFile.toFile();
}
} catch (IOException e) {
LOG.warn("Failed to extract bundled keystore from classpath: {}", e.getMessage());
}

throw new IllegalStateException(
"Keystore file not found and bundled keystore not available in classpath");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Collections;
import java.util.List;
import java.util.function.BiConsumer;
Expand All @@ -33,14 +35,19 @@
import org.apache.camel.test.infra.ftp.common.FtpProperties;
import org.apache.camel.test.infra.ftp.services.FtpInfraService;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.PublicKeyEntry;
import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider;
import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.session.helpers.AbstractSession;
import org.apache.sshd.common.signature.BuiltinSignatures;
import org.apache.sshd.common.signature.Signature;
import org.apache.sshd.scp.server.ScpCommandFactory;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -95,7 +102,7 @@ public void setUpServer() throws Exception {
sshd.setPort(port);
}

sshd.setKeyPairProvider(new FileKeyPairProvider(Paths.get(embeddedConfiguration.getKeyPairFile())));
sshd.setKeyPairProvider(createKeyPairProvider());
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
sshd.setCommandFactory(new ScpCommandFactory());
sshd.setPasswordAuthenticator((username, password, session) -> true);
Expand Down Expand Up @@ -124,6 +131,52 @@ protected PublickeyAuthenticator getPublickeyAuthenticator() {
return (username, key, session) -> true;
}

private KeyPairProvider createKeyPairProvider() {
// 1. First try: Use existing file on disk if configured path exists
Path keyPairPath = Paths.get(embeddedConfiguration.getKeyPairFile());
if (Files.exists(keyPairPath)) {
LOG.debug("Using existing host key file: {}", keyPairPath);
return new FileKeyPairProvider(keyPairPath);
}

// 2. Second try: Load bundled host key from classpath
KeyPairProvider classpathProvider = loadKeyFromClasspath();
if (classpathProvider != null) {
return classpathProvider;
}

// 3. Last resort: Generate a new host key
Path generatedKeyPath = testDirectory().resolve("hostkey.ser");
LOG.info("Host key file not found at {}. Generating new host key at: {}", keyPairPath, generatedKeyPath);
SimpleGeneratorHostKeyProvider provider = new SimpleGeneratorHostKeyProvider(generatedKeyPath);
provider.setAlgorithm("RSA");
provider.setKeySize(2048);
return provider;
}

/**
* Attempts to load the bundled host key from the classpath.
*
* @return KeyPairProvider if the bundled key is available and can be loaded, null otherwise
*/
private KeyPairProvider loadKeyFromClasspath() {
try {
ClassLoadableResourceKeyPairProvider provider
= new ClassLoadableResourceKeyPairProvider(EmbeddedConfiguration.BUNDLED_HOST_KEY_RESOURCE);

// Verify the key can actually be loaded
Iterable<KeyPair> keyPairs = provider.loadKeys(null);
if (keyPairs != null && keyPairs.iterator().hasNext()) {
LOG.info("Using bundled host key from classpath: {}",
EmbeddedConfiguration.BUNDLED_HOST_KEY_RESOURCE);
return provider;
}
} catch (Exception e) {
LOG.debug("Failed to load bundled host key from classpath: {}", e.getMessage());
}
return null;
}

public void tearDown() {
tearDownServer();
}
Expand Down Expand Up @@ -187,4 +240,59 @@ public int getPort() {
public int port() {
return port;
}

/**
* Returns the SSH host key fingerprint in SHA256 format. This can be used to verify the server identity when
* connecting.
*
* @return the host key fingerprint (e.g., "SHA256:...") or null if unavailable
*/
@Override
public String hostKeyFingerprint() {
try {
PublicKey publicKey = getHostPublicKey();
if (publicKey != null) {
return KeyUtils.getFingerPrint(publicKey);
}
} catch (Exception e) {
LOG.warn("Failed to get host key fingerprint: {}", e.getMessage(), e);
}
return null;
}

/**
* Returns the known_hosts entry for this server.
*
* @return the known_hosts entry or null if unavailable
*/
@Override
public String knownHostsEntry() {
try {
PublicKey publicKey = getHostPublicKey();
if (publicKey != null) {
StringBuilder sb = new StringBuilder();
PublicKeyEntry.appendPublicKeyEntry(sb, publicKey);
return String.format("[%s]:%d %s",
embeddedConfiguration.getServerAddress(), port, sb);
}
} catch (Exception e) {
LOG.warn("Failed to get known_hosts entry: {}", e.getMessage(), e);
}
return null;
}

private PublicKey getHostPublicKey() {
if (sshd == null) {
return null;
}
try {
Iterable<KeyPair> keyPairs = sshd.getKeyPairProvider().loadKeys(null);
for (KeyPair keyPair : keyPairs) {
return keyPair.getPublic();
}
} catch (Exception e) {
LOG.debug("Failed to load host key: {}", e.getMessage(), e);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@

package org.apache.camel.test.infra.ftp.services.embedded;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;

import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider;
import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -48,10 +52,24 @@ public static boolean hasRequiredAlgorithms(String keyPairFile) {

private static boolean doCheck(String keyPairFile) {
try {
FileKeyPairProvider provider = new FileKeyPairProvider(Paths.get(keyPairFile));
// First try the file path
Path keyPath = Paths.get(keyPairFile);
if (Files.exists(keyPath)) {
FileKeyPairProvider provider = new FileKeyPairProvider(keyPath);
provider.loadKeys(null);
return true;
}

provider.loadKeys(null);
return true;
// Fall back to classpath resource
ClassLoadableResourceKeyPairProvider classpathProvider
= new ClassLoadableResourceKeyPairProvider(EmbeddedConfiguration.BUNDLED_HOST_KEY_RESOURCE);
Iterable<KeyPair> keys = classpathProvider.loadKeys(null);
if (keys != null && keys.iterator().hasNext()) {
return true;
}

LOG.warn("No host key available at {} or in classpath", keyPairFile);
return false;
} catch (Exception e) {
String name = System.getProperty("os.name");
String message = e.getMessage();
Expand Down
15 changes: 15 additions & 0 deletions test-infra/camel-test-infra-ftp/src/main/resources/hostkey.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDdfIWeSV4o68dRrKSzFd/Bk51E65UTmmSrmW0O1ohtzi6HzsDP
jXgCtlTt3FqTcfFfI92IlTr4JWqC9UK1QT1ZTeng0MkPQmv68hDANHbt5CpETZHj
W5q4OOgWhVvj5IyOC2NZHtKlJBkdsMAa15ouOOJLzBvAvbqOR/yUROsEiQIDAQAB
AoGBANG3JDW6NoP8rF/zXoeLgLCj+tfVUPSczhGFVrQkAk4mWfyRkhN0WlwHFOec
K89MpkV1ij/XPVzU4MNbQ2yod1KiDylzvweYv+EaEhASCmYNs6LS03punml42SL9
97tOmWfVJXxlQoLiY6jHPU97vTc65k8gL+gmmrpchsW0aqmZAkEA/c8zfmKvY37T
cxcLLwzwsqqH7g2KZGTf9aRmx2ebdW+QKviJJhbdluDgl1TNNFj5vCLznFDRHiqJ
wq0wkZ39cwJBAN9l5v3kdXj21UrurNPdlV0n2GZBt2vblooQC37XHF97r2zM7Ou+
Lg6MyfJClyguhWL9dxnGbf3btQ0l3KDstxMCQCRaiEqjAfIjWVATzeNIXDWLHXso
b1kf5cA+cwY+vdKdTy4IeUR+Y/DXdvPWDqpf0C11aCVMohdLCn5a5ikFUycCQDhV
K/BuAallJNfmY7JxN87r00fF3ojWMJnT/fIYMFFrkQrwifXQWTDWE76BSDibsosJ
u1TGksnm8zrDh2UVC/0CQFrHTiSl/3DHvWAbOJawGKg46cnlDcAhSyV8Frs8/dlP
7YGG3eqkw++lsghqmFO6mRUTKsBmiiB2wgLGhL5pyYY=
-----END RSA PRIVATE KEY-----
Binary file not shown.