This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-net.git
The following commit(s) were added to refs/heads/master by this push:
new a7972946 Run hostname verifier on FTPS data connection (#404)
a7972946 is described below
commit a79729464f6e69b773171ff30e3d483cab7afd70
Author: Javid Khan <[email protected]>
AuthorDate: Sat Jul 18 23:20:22 2026 +0530
Run hostname verifier on FTPS data connection (#404)
* run hostname verifier on FTPS data connection
* Update FTPSClient.java
---------
Co-authored-by: Gary Gregory <[email protected]>
---
.../org/apache/commons/net/ftp/FTPSClient.java | 3 +++
.../apache/commons/net/ftp/AbstractFtpsTest.java | 11 +++++++++
.../org/apache/commons/net/ftp/FTPSClientTest.java | 27 ++++++++++++++++++++++
3 files changed, 41 insertions(+)
diff --git a/src/main/java/org/apache/commons/net/ftp/FTPSClient.java
b/src/main/java/org/apache/commons/net/ftp/FTPSClient.java
index 3ad7a841..15de27ff 100644
--- a/src/main/java/org/apache/commons/net/ftp/FTPSClient.java
+++ b/src/main/java/org/apache/commons/net/ftp/FTPSClient.java
@@ -299,6 +299,9 @@ public class FTPSClient extends FTPClient {
sslSocket.setEnabledProtocols(protocols);
}
sslSocket.startHandshake();
+ if (isClientMode && hostnameVerifier != null &&
!hostnameVerifier.verify(_hostname_, sslSocket.getSession())) {
+ throw new SSLHandshakeException("Hostname doesn't match
certificate");
+ }
}
return socket;
diff --git a/src/test/java/org/apache/commons/net/ftp/AbstractFtpsTest.java
b/src/test/java/org/apache/commons/net/ftp/AbstractFtpsTest.java
index ed1112bc..4c177147 100644
--- a/src/test/java/org/apache/commons/net/ftp/AbstractFtpsTest.java
+++ b/src/test/java/org/apache/commons/net/ftp/AbstractFtpsTest.java
@@ -27,6 +27,8 @@ import java.net.SocketException;
import java.net.URL;
import java.time.Duration;
+import javax.net.ssl.HostnameVerifier;
+
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang3.ThreadUtils;
@@ -141,6 +143,8 @@ public abstract class AbstractFtpsTest {
private boolean endpointCheckingEnabled;
+ private HostnameVerifier hostnameVerifier;
+
protected void assertClientCode(final FTPSClient client) {
final int replyCode = client.getReplyCode();
assertTrue(FTPReply.isPositiveCompletion(replyCode));
@@ -169,6 +173,9 @@ public abstract class AbstractFtpsTest {
assertEquals(62, client.getDataTimeout().getSeconds());
//
client.setEndpointCheckingEnabled(endpointCheckingEnabled);
+ if (hostnameVerifier != null) {
+ client.setHostnameVerifier(hostnameVerifier);
+ }
client.connect("localhost", SocketPort);
//
assertClientCode(client);
@@ -207,4 +214,8 @@ public abstract class AbstractFtpsTest {
public void setEndpointCheckingEnabled(final boolean value) {
this.endpointCheckingEnabled = value;
}
+
+ protected void setHostnameVerifier(final HostnameVerifier value) {
+ this.hostnameVerifier = value;
+ }
}
diff --git a/src/test/java/org/apache/commons/net/ftp/FTPSClientTest.java
b/src/test/java/org/apache/commons/net/ftp/FTPSClientTest.java
index b7c9ef0f..32e8eaaf 100644
--- a/src/test/java/org/apache/commons/net/ftp/FTPSClientTest.java
+++ b/src/test/java/org/apache/commons/net/ftp/FTPSClientTest.java
@@ -25,9 +25,11 @@ import java.io.IOException;
import java.net.SocketException;
import java.time.Instant;
import java.util.Calendar;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -70,6 +72,31 @@ class FTPSClientTest extends AbstractFtpsTest {
trace("<<testHasFeature");
}
+ @Test
+ @Timeout(TEST_TIMEOUT)
+ void testHostnameVerifierUsedForDataConnection() throws SocketException,
IOException {
+ final AtomicInteger verifyCount = new AtomicInteger();
+ setHostnameVerifier((hostname, session) -> {
+ verifyCount.incrementAndGet();
+ return true;
+ });
+ try {
+ final FTPSClient client = loginClient();
+ try {
+ // control connection has already been verified during login
+ final int afterControl = verifyCount.get();
+ assertTrue(afterControl >= 1, "verifier should run for the
control connection");
+ // listFiles opens a data connection, which must be verified
too
+ assertNotNull(client.listFiles(""));
+ assertTrue(verifyCount.get() > afterControl, "verifier should
run for the data connection");
+ } finally {
+ client.disconnect();
+ }
+ } finally {
+ setHostnameVerifier(null);
+ }
+ }
+
private void testListFiles(final String pathname) throws SocketException,
IOException {
final FTPSClient client = loginClient();
try {