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 3ad7a8412..15de27ff3 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 @@ protected Socket _openDataConnection_(final String command, final String arg) th 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 ed1112bc8..4c1771479 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.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 @@ protected static void trace(final String msg) { 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 @@ protected FTPSClient loginClient() throws SocketException, IOException { assertEquals(62, client.getDataTimeout().getSeconds()); // client.setEndpointCheckingEnabled(endpointCheckingEnabled); + if (hostnameVerifier != null) { + client.setHostnameVerifier(hostnameVerifier); + } client.connect("localhost", SocketPort); // assertClientCode(client); @@ -207,4 +214,8 @@ protected void retrieveFile(final String pathname) throws SocketException, IOExc 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 b7c9ef0f4..32e8eaaf0 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.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 @@ void testHasFeature(final boolean endpointCheckingEnabled) throws SocketExceptio trace("< { + 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 {