This is an automated email from the ASF dual-hosted git repository.
exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 6d369851722 NIFI-14719 Added Passive Port Range property to ListenFTP
(#11784)
6d369851722 is described below
commit 6d3698517223ebef79b0ee60d291b8ec57de79ce
Author: Vincenzo Lombardo <[email protected]>
AuthorDate: Mon Oct 26 20:34:43 2026 +0100
NIFI-14719 Added Passive Port Range property to ListenFTP (#11784)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Signed-off-by: David Handermann <[email protected]>
---
.../apache/nifi/processors/standard/ListenFTP.java | 66 ++++++++++++++++++++
.../processors/standard/ftp/NifiFtpServer.java | 22 +++++--
.../standard/ftp/PassivePortSelection.java | 47 ++++++++++++++
.../additionalDetails.md | 8 +++
.../nifi/processors/standard/TestListenFTP.java | 71 ++++++++++++++++++++++
5 files changed, 209 insertions(+), 5 deletions(-)
diff --git
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenFTP.java
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenFTP.java
index 7ce37be1b01..3148ff954a6 100644
---
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenFTP.java
+++
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ListenFTP.java
@@ -27,6 +27,7 @@ import org.apache.nifi.annotation.lifecycle.OnStopped;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.ValidationContext;
import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.Validator;
import org.apache.nifi.components.listen.ListenComponent;
import org.apache.nifi.components.listen.ListenPort;
import org.apache.nifi.components.listen.StandardListenPort;
@@ -42,6 +43,7 @@ import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.processors.standard.ftp.FtpServer;
import org.apache.nifi.processors.standard.ftp.NifiFtpServer;
+import org.apache.nifi.processors.standard.ftp.PassivePortSelection;
import org.apache.nifi.ssl.SSLContextProvider;
import java.net.InetAddress;
@@ -52,6 +54,8 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
@InputRequirement(Requirement.INPUT_FORBIDDEN)
@Tags({"ingest", "FTP", "FTPS", "listen"})
@@ -119,9 +123,66 @@ public class ListenFTP extends
AbstractSessionFactoryProcessor implements Listen
.sensitive(true)
.build();
+ private static final Pattern PORT_RANGE_PATTERN =
Pattern.compile("^(\\d{1,5})-(\\d{1,5})$");
+
+ private static final Validator PORT_RANGE_VALIDATOR = (subject, input,
context) -> {
+ if (context.isExpressionLanguageSupported(subject) &&
context.isExpressionLanguagePresent(input)) {
+ return new ValidationResult.Builder()
+ .subject(subject)
+ .input(input)
+ .explanation("Expression Language Present")
+ .valid(true)
+ .build();
+ }
+
+ final String explanation;
+ final Matcher matcher = PORT_RANGE_PATTERN.matcher(input);
+ if (matcher.matches()) {
+ final int startPort = Integer.parseInt(matcher.group(1));
+ final int endPort = Integer.parseInt(matcher.group(2));
+ if (startPort < 1 || endPort > 65535) {
+ explanation = "Ports must be between 1 and 65535";
+ } else if (startPort > endPort) {
+ explanation = "Start port must be less than or equal to end
port";
+ } else {
+ explanation = null;
+ }
+ } else {
+ explanation = "Port range must be in the format <start>-<end>,
e.g. 50000-50099";
+ }
+ return new ValidationResult.Builder()
+ .subject(subject)
+ .input(input)
+ .explanation(explanation)
+ .valid(explanation == null)
+ .build();
+ };
+
+ public static final PropertyDescriptor PASSIVE_PORT_SELECTION = new
PropertyDescriptor.Builder()
+ .name("Passive Port Selection")
+ .description("Strategy for selecting the port that the FTP server
uses for data connections in passive mode.")
+ .required(true)
+ .allowableValues(PassivePortSelection.class)
+ .defaultValue(PassivePortSelection.RANDOM)
+ .build();
+
+ public static final PropertyDescriptor PASSIVE_PORT_RANGE = new
PropertyDescriptor.Builder()
+ .name("Passive Port Range")
+ .description("""
+ The range of ports, in the format <start>-<end> (e.g.
50000-50099), that the FTP server uses for data connections in passive mode.
+ Restricting the range simplifies firewall and container
port mapping configurations.
+ """)
+ .required(true)
+ .expressionLanguageSupported(ExpressionLanguageScope.ENVIRONMENT)
+ .addValidator(PORT_RANGE_VALIDATOR)
+ .dependsOn(PASSIVE_PORT_SELECTION, PassivePortSelection.CONFIGURED)
+ .build();
+
private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS =
List.of(
ADDRESS,
PORT,
+ PASSIVE_PORT_SELECTION,
+ PASSIVE_PORT_RANGE,
USERNAME,
PASSWORD,
SSL_CONTEXT_SERVICE
@@ -168,6 +229,10 @@ public class ListenFTP extends
AbstractSessionFactoryProcessor implements Listen
String password =
context.getProperty(PASSWORD).evaluateAttributeExpressions().getValue();
String bindAddress =
context.getProperty(ADDRESS).evaluateAttributeExpressions().getValue();
int port =
context.getProperty(PORT).evaluateAttributeExpressions().asInteger();
+ final PassivePortSelection passivePortSelection =
context.getProperty(PASSIVE_PORT_SELECTION).asAllowableValue(PassivePortSelection.class);
+ final String passivePortRange = passivePortSelection ==
PassivePortSelection.CONFIGURED
+ ?
context.getProperty(PASSIVE_PORT_RANGE).evaluateAttributeExpressions().getValue()
+ : null;
final SSLContextProvider sslContextProvider =
context.getProperty(SSL_CONTEXT_SERVICE).asControllerService(SSLContextProvider.class);
try {
@@ -181,6 +246,7 @@ public class ListenFTP extends
AbstractSessionFactoryProcessor implements Listen
.username(username)
.password(password)
.sslContextProvider(sslContextProvider)
+ .passivePortRange(passivePortRange)
.build();
ftpServer.start();
} catch (ProcessException processException) {
diff --git
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ftp/NifiFtpServer.java
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ftp/NifiFtpServer.java
index a18f841a979..cc3672dde26 100644
---
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ftp/NifiFtpServer.java
+++
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ftp/NifiFtpServer.java
@@ -108,6 +108,7 @@ public class NifiFtpServer implements
org.apache.nifi.processors.standard.ftp.Ft
private String username;
private String password;
private SSLContextProvider sslContextProvider;
+ private String passivePortRange;
public Builder sessionFactory(AtomicReference<ProcessSessionFactory>
sessionFactory) {
this.sessionFactory = sessionFactory;
@@ -153,6 +154,11 @@ public class NifiFtpServer implements
org.apache.nifi.processors.standard.ftp.Ft
return this;
}
+ public Builder passivePortRange(String passivePortRange) {
+ this.passivePortRange = passivePortRange;
+ return this;
+ }
+
public NifiFtpServer build() throws ProcessException {
try {
boolean anonymousLoginEnabled = (username == null);
@@ -161,7 +167,7 @@ public class NifiFtpServer implements
org.apache.nifi.processors.standard.ftp.Ft
CommandMapFactory commandMapFactory = new
CommandMapFactory(sessionFactory, sessionFactorySetSignal, relationshipSuccess);
Map<String, Command> commandMap =
commandMapFactory.createCommandMap();
ConnectionConfig connectionConfig =
createConnectionConfig(anonymousLoginEnabled);
- Listener listener = createListener(bindAddress, port,
sslContextProvider);
+ Listener listener = createListener(bindAddress, port,
sslContextProvider, passivePortRange);
User user = createUser(username, password, HOME_DIRECTORY);
return new NifiFtpServer(commandMap, fileSystemFactory,
connectionConfig, listener, user);
@@ -176,10 +182,16 @@ public class NifiFtpServer implements
org.apache.nifi.processors.standard.ftp.Ft
return connectionConfigFactory.createConnectionConfig();
}
- private Listener createListener(String bindAddress, int port,
SSLContextProvider sslContextProvider) throws FtpServerConfigurationException {
+ private Listener createListener(String bindAddress, int port,
SSLContextProvider sslContextProvider, String passivePortRange) throws
FtpServerConfigurationException {
ListenerFactory listenerFactory = new ListenerFactory();
listenerFactory.setServerAddress(bindAddress);
listenerFactory.setPort(port);
+
+ DataConnectionConfigurationFactory
dataConnectionConfigurationFactory = new DataConnectionConfigurationFactory();
+ if (passivePortRange != null) {
+
dataConnectionConfigurationFactory.setPassivePorts(passivePortRange);
+ }
+
if (sslContextProvider != null) {
final SSLContext sslContext =
sslContextProvider.createContext();
SslConfiguration sslConfiguration = new
StandardSslConfiguration(sslContext);
@@ -189,12 +201,12 @@ public class NifiFtpServer implements
org.apache.nifi.processors.standard.ftp.Ft
listenerFactory.setImplicitSsl(true);
// Set implicit security for the data connection
- DataConnectionConfigurationFactory
dataConnectionConfigurationFactory = new DataConnectionConfigurationFactory();
dataConnectionConfigurationFactory.setImplicitSsl(true);
dataConnectionConfigurationFactory.setSslConfiguration(sslConfiguration);
- DataConnectionConfiguration dataConnectionConfiguration =
dataConnectionConfigurationFactory.createDataConnectionConfiguration();
-
listenerFactory.setDataConnectionConfiguration(dataConnectionConfiguration);
}
+
+ DataConnectionConfiguration dataConnectionConfiguration =
dataConnectionConfigurationFactory.createDataConnectionConfiguration();
+
listenerFactory.setDataConnectionConfiguration(dataConnectionConfiguration);
return listenerFactory.createListener();
}
diff --git
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ftp/PassivePortSelection.java
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ftp/PassivePortSelection.java
new file mode 100644
index 00000000000..b6df48d7283
--- /dev/null
+++
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ftp/PassivePortSelection.java
@@ -0,0 +1,47 @@
+/*
+ * 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.nifi.processors.standard.ftp;
+
+import org.apache.nifi.components.DescribedValue;
+
+public enum PassivePortSelection implements DescribedValue {
+ RANDOM("Random", "Use a random port selected by the operating system for
each passive data connection."),
+ CONFIGURED("Configured", "Use a port from the configured Passive Port
Range for each passive data connection.");
+
+ private final String displayName;
+ private final String description;
+
+ PassivePortSelection(final String displayName, final String description) {
+ this.displayName = displayName;
+ this.description = description;
+ }
+
+ @Override
+ public String getValue() {
+ return name();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+}
diff --git
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/docs/org.apache.nifi.processors.standard.ListenFTP/additionalDetails.md
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/docs/org.apache.nifi.processors.standard.ListenFTP/additionalDetails.md
index 569f0b5a4ad..9f75ee5887d 100644
---
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/docs/org.apache.nifi.processors.standard.ListenFTP/additionalDetails.md
+++
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/docs/org.apache.nifi.processors.standard.ListenFTP/additionalDetails.md
@@ -27,6 +27,14 @@ Before starting the processor, the following properties can
be set:
default). If set to a valid address, the server is only available on that
specific address.
* **Listening Port:** the port on which the server listens for incoming
connections. Root privileges are required on
Linux to be able to use port numbers below 1024.
+* **Passive Port Selection:** the strategy for selecting the port used for
data connections in passive mode. The default
+ value _Random_ uses a port selected by the operating system. The value
_Configured_ restricts data connections to the
+ ports of the Passive Port Range property.
+* **Passive Port Range:** the range of ports, in the format _start-end_ (e.g.
_50000-50099_), that the FTP server
+ uses for data connections in passive mode when Passive Port Selection is
_Configured_. Setting a narrow range makes it
+ possible to open only these ports on firewalls or to publish them from a
container (e.g. with Docker). Each concurrent
+ passive transfer uses one port of the range, so the range should be large
enough for the expected number of
+ simultaneous transfers.
* **Username and Password:** Either both of them need to be set, or none of
them. If set, the FTP server only allows
users to log in with the username-password pair specified in these
properties. If the Username and Password properties
are left blank, the FTP server allows anonymous connections, meaning that
the client can connect to the FTP server by
diff --git
a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenFTP.java
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenFTP.java
new file mode 100644
index 00000000000..5f9d3213ed9
--- /dev/null
+++
b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestListenFTP.java
@@ -0,0 +1,71 @@
+/*
+ * 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.nifi.processors.standard;
+
+import org.apache.nifi.processors.standard.ftp.PassivePortSelection;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class TestListenFTP {
+
+ private TestRunner runner;
+
+ @BeforeEach
+ void setRunner() {
+ runner = TestRunners.newTestRunner(ListenFTP.class);
+ }
+
+ @AfterEach
+ void shutdownRunner() {
+ runner.shutdown();
+ }
+
+ @Test
+ void testPassivePortSelectionDefaultValid() {
+ runner.assertValid();
+ }
+
+ @Test
+ void testPassivePortSelectionConfiguredRangeRequired() {
+ runner.setProperty(ListenFTP.PASSIVE_PORT_SELECTION,
PassivePortSelection.CONFIGURED);
+
+ runner.assertNotValid();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"50000-50099", "50000-50000", "1-65535",
"${PASSIVE_PORT_RANGE}"})
+ void testPassivePortRangeValid(final String passivePortRange) {
+ runner.setProperty(ListenFTP.PASSIVE_PORT_SELECTION,
PassivePortSelection.CONFIGURED);
+ runner.setProperty(ListenFTP.PASSIVE_PORT_RANGE, passivePortRange);
+
+ runner.assertValid();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"50000", "50099-50000", "0-100", "50000-65536",
"50000-50099,50200", "start-end", " "})
+ void testPassivePortRangeInvalid(final String passivePortRange) {
+ runner.setProperty(ListenFTP.PASSIVE_PORT_SELECTION,
PassivePortSelection.CONFIGURED);
+ runner.setProperty(ListenFTP.PASSIVE_PORT_RANGE, passivePortRange);
+
+ runner.assertNotValid();
+ }
+}