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();
+    }
+}

Reply via email to