This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.22.x by this push:
     new c4e5184700ff [backport camel-4.22.x] CAMEL-26322: camel-as2 - scope 
the async MDN allow-list to host and port, and add an option to require https 
(#29056)
c4e5184700ff is described below

commit c4e5184700fff19673612e9b7efa732bf34d0b48
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Nov 4 09:27:55 2026 +0100

    [backport camel-4.22.x] CAMEL-26322: camel-as2 - scope the async MDN 
allow-list to host and port, and add an option to require https (#29056)
    
    * [backport camel-4.22.x] CAMEL-26322: camel-as2 - scope the async MDN 
allow-list to host and port, and add an option to require https
    
    * fix: add missing SSLContext import in 
AS2AsynchronousMDNManagerDeliveryAddressTest
    
    * fix: regenerate JSON descriptors and fix import order for CI
    
    * docs: add upgrade note for asyncMdnAllowedHosts host+port matching change
    
    * fix: remove CAMEL-24474 HTTPS delivery from the 4.22.x backport, keep 
host+port allow-list scoping only
    
    * fix: reject port 0 in asyncMdnAllowedHosts allow-list (parsePort lower 
bound >= 1)
    
    * fix: regenerate as2 descriptors and compact apiProperties for CI check
    
    * fix: add missing trailing newline to as2.json generated descriptors
---
 .../org/apache/camel/catalog/components/as2.json   |  2 +-
 .../apache/camel/catalog/docs/as2-component.adoc   | 12 +++
 .../as2/api/AS2AsynchronousMDNManager.java         | 92 ++++++++++++++++++----
 ...2AsynchronousMDNManagerDeliveryAddressTest.java | 70 ++++++++++++++--
 .../org/apache/camel/component/as2/as2.json        |  2 +-
 .../src/main/docs/as2-component.adoc               | 12 +++
 .../camel/component/as2/AS2Configuration.java      | 13 +--
 .../as2/AS2AsyncMdnBasicAuthHeaderTest.java        |  6 +-
 .../component/as2/AS2AsyncMdnContextReuseTest.java |  2 +-
 .../as2/AS2AsyncMdnTokenAuthHeaderTest.java        |  6 +-
 .../as2/AS2ServerBasicAuthHeaderTest.java          |  7 +-
 .../as2/AS2ServerBearerAuthHeaderTest.java         |  7 +-
 .../AS2ServerTwoConsumerAsyncMdnSettingsTest.java  | 21 +++--
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    | 10 +++
 .../endpoint/dsl/AS2EndpointBuilderFactory.java    | 57 +++++++++-----
 15 files changed, 251 insertions(+), 68 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/as2.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/as2.json
index dd6e71df9345..5b1369e2fc58 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/as2.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/as2.json
@@ -68,7 +68,7 @@
     "expectContinue": { "index": 32, "kind": "parameter", "displayName": 
"Expect Continue", "group": "producer", "label": "producer", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "Controls whether the Expect: 100-Continue 
header is included in outbound AS2 messages. W [...]
     "lazyStartProducer": { "index": 33, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
     "accessToken": { "index": 34, "kind": "parameter", "displayName": "Access 
Token", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "The access token that is used by the client 
for bearer authentication." },
-    "asyncMdnAllowedHosts": { "index": 35, "kind": "parameter", "displayName": 
"Async Mdn Allowed Hosts", "group": "security", "label": "security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "Comma separated list of host names an 
asynchronous MDN may be delivered to. The delivery  [...]
+    "asyncMdnAllowedHosts": { "index": 35, "kind": "parameter", "displayName": 
"Async Mdn Allowed Hosts", "group": "security", "label": "security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "Comma separated list of hosts an asynchronous 
MDN may be delivered to. The delivery addre [...]
     "decryptingPrivateKey": { "index": 36, "kind": "parameter", "displayName": 
"Decrypting Private Key", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": "java.security.PrivateKey", "deprecated": 
false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "The key used to encrypt the EDI message." },
     "encryptingAlgorithm": { "index": 37, "kind": "parameter", "displayName": 
"Encrypting Algorithm", "group": "security", "label": "security", "required": 
false, "type": "enum", "javaType": 
"org.apache.camel.component.as2.api.AS2EncryptionAlgorithm", "enum": [ 
"AES128_CBC", "AES192_CBC", "AES256_CBC", "AES128_CCM", "AES192_CCM", 
"AES256_CCM", "AES128_GCM", "AES192_GCM", "AES256_GCM", "CAMELLIA128_CBC", 
"CAMELLIA192_CBC", "CAMELLIA256_CBC", "CAST5_CBC", "DES_CBC", "DES_EDE3_CBC", 
"GOST28 [...]
     "encryptingCertificateChain": { "index": 38, "kind": "parameter", 
"displayName": "Encrypting Certificate Chain", "group": "security", "label": 
"security", "required": false, "type": "object", "javaType": 
"java.security.cert.Certificate[]", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "The chain of certificates used to encrypt EDI 
message." },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/as2-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/as2-component.adoc
index 0f016f5783d9..93032413f675 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/as2-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/as2-component.adoc
@@ -62,3 +62,15 @@ consumer applies its own keys and certificates and its own 
asynchronous MDN sett
 The settings of the shared server connection still come from the first 
endpoint created on the port:
 `sslContextParameters` and `sslContext` (used for the TLS listener),
 `mdnMessageTemplate`, `server`, `serverFqdn` and `as2Version`. They are per 
port, not per consumer.
+
+=== Delivering asynchronous MDNs
+
+When a received message asks for an asynchronous MDN, the 
`as2://server/listen` consumer delivers the MDN to the
+address that the sender gave in the `Receipt-Delivery-Option` header. When 
`asyncMdnAllowedHosts` is set, the MDN is
+delivered only to an address on the list, and the `mdnUserName`, `mdnPassword` 
and `mdnAccessToken` credentials are
+attached only for such an address. Each entry of the list is a host with an 
optional port:
+
+* a bare host, such as `partner.example`, matches only the default port (80 
for `http`);
+* a host and a port, such as `partner.example:8080`, match that port only;
+* an IPv6 literal is written in square brackets, as in a URL: `[::1]` matches 
the default port, and `[::1]:8443`
+matches port 8443. Without the brackets, `::1:8443` is read as another IPv6 
address, not as `::1` with a port.
diff --git 
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManager.java
 
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManager.java
index 8cf1c9a30dc6..ab3e59e364f1 100644
--- 
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManager.java
+++ 
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManager.java
@@ -161,9 +161,6 @@ public class AS2AsynchronousMDNManager {
         // header), so it is untrusted input that selects an outbound 
destination.
         URI uri = URI.create(recipientDeliveryAddress);
         String scheme = uri.getScheme() == null ? null : 
uri.getScheme().toLowerCase(Locale.US);
-        // Only http. This class delivers over a plain Socket and has no TLS 
of any kind, so accepting https
-        // would mean writing the request - including the Authorization header 
- in cleartext to the TLS port.
-        // https delivery has never worked here for that reason, so refusing 
it removes nothing that functioned.
         if (!"http".equals(scheme)) {
             throw new HttpException(
                     "Refusing to deliver the asynchronous MDN: the delivery 
address must use http."
@@ -173,12 +170,15 @@ public class AS2AsynchronousMDNManager {
         if (host == null) {
             throw new HttpException("Refusing to deliver the asynchronous MDN: 
the delivery address has no host");
         }
-        int port = uri.getPort() != -1 ? uri.getPort() : 80;
+        int defaultPort = 80;
+        int port = uri.getPort() != -1 ? uri.getPort() : defaultPort;
 
-        boolean hostIsAllowed = isAllowedHost(host);
-        if (allowedHosts != null && !allowedHosts.isBlank() && !hostIsAllowed) 
{
+        // Match the full authority - host and port - of the delivery address 
against the allow-list, not the host
+        // alone, so a listed host does not authorise every port on that host.
+        boolean authorityIsAllowed = isAllowedAuthority(host, port, 
defaultPort);
+        if (allowedHosts != null && !allowedHosts.isBlank() && 
!authorityIsAllowed) {
             throw new HttpException(
-                    "Refusing to deliver the asynchronous MDN: the delivery 
address host is not in asyncMdnAllowedHosts");
+                    "Refusing to deliver the asynchronous MDN: the delivery 
address is not in asyncMdnAllowedHosts");
         }
 
         int buffSize = 8 * 1024;
@@ -195,14 +195,14 @@ public class AS2AsynchronousMDNManager {
 
             ClassicHttpRequest request = new BasicClassicHttpRequest("POST", 
uri);
             request.setHeader(AS2Header.CONTENT_TYPE, contentType);
-            // Credentials are only attached to a host the operator has 
vouched for. Without an allow-list the
-            // destination is entirely sender-chosen, so the MDN is still 
delivered but without them.
-            if (hostIsAllowed) {
+            // Credentials are only attached to an authority - host and port - 
the operator has vouched for. Without an
+            // allow-list the destination is entirely sender-chosen, so the 
MDN is still delivered but without them.
+            if (authorityIsAllowed) {
                 AS2HeaderUtils.addAuthorizationHeader(request, userName, 
password, accessToken);
             } else if (userName != null || accessToken != null) {
-                LOG.warn("Asynchronous MDN credentials not sent to 
sender-supplied host {}:"
+                LOG.warn("Asynchronous MDN credentials not sent to 
sender-supplied authority {}:"
                          + " set asyncMdnAllowedHosts to authorise it",
-                        host);
+                        formatAuthority(host, port));
             }
             httpContext.setRequest(request);
             multipartMimeEntity.setMainBody(true);
@@ -234,18 +234,82 @@ public class AS2AsynchronousMDNManager {
         return host;
     }
 
-    private boolean isAllowedHost(String host) {
+    /**
+     * Writes a delivery authority the way an {@code asyncMdnAllowedHosts} 
entry is written, so a log message names an
+     * entry the operator can copy. The host has been through normalizeHost, 
so an IPv6 literal (the only host that
+     * contains a colon) is put back in square brackets ({@code [::1]:8443}): 
unbracketed, {@code ::1:8443} would be
+     * read as another IPv6 address on the default port. Any other host is 
written as {@code host:port}.
+     */
+    static String formatAuthority(String host, int port) {
+        return host.indexOf(':') >= 0 ? "[" + host + "]:" + port : host + ":" 
+ port;
+    }
+
+    /**
+     * Matches the resolved delivery authority - host and port - against 
{@code asyncMdnAllowedHosts}. Each entry is a
+     * host with an optional port ({@code host} or {@code host:port}); a 
bare-host entry matches only the default port
+     * for the scheme ({@code defaultPort}), so a listed host does not 
authorise every port on that host.
+     */
+    boolean isAllowedAuthority(String host, int port, int defaultPort) {
         if (allowedHosts == null || allowedHosts.isBlank()) {
             return false;
         }
         for (String allowed : allowedHosts.split(",")) {
-            if (allowed.trim().equalsIgnoreCase(host)) {
+            String entry = allowed.trim();
+            if (!entry.isEmpty() && authorityMatches(entry, host, port, 
defaultPort)) {
                 return true;
             }
         }
         return false;
     }
 
+    private static boolean authorityMatches(String entry, String host, int 
port, int defaultPort) {
+        String entryHost;
+        int entryPort;
+        if (entry.charAt(0) == '[') {
+            // bracketed IPv6 literal, optionally followed by :port
+            int close = entry.indexOf(']');
+            if (close < 0) {
+                return false;
+            }
+            entryHost = entry.substring(1, close);
+            entryPort = defaultPort;
+            if (close + 1 < entry.length()) {
+                if (entry.charAt(close + 1) != ':') {
+                    return false;
+                }
+                Integer parsed = parsePort(entry.substring(close + 2));
+                if (parsed == null) {
+                    return false;
+                }
+                entryPort = parsed;
+            }
+        } else {
+            int colon = entry.indexOf(':');
+            if (colon >= 0 && entry.indexOf(':', colon + 1) < 0) {
+                // a single colon separates host and port; an unbracketed IPv6 
literal has several and keeps the default
+                entryHost = entry.substring(0, colon);
+                Integer parsed = parsePort(entry.substring(colon + 1));
+                if (parsed == null) {
+                    return false;
+                }
+                entryPort = parsed;
+            } else {
+                entryHost = entry;
+                entryPort = defaultPort;
+            }
+        }
+        return entryPort == port && entryHost.equalsIgnoreCase(host);
+    }
+
+    private static Integer parsePort(String value) {
+        try {
+            int parsed = Integer.parseInt(value.trim());
+            return parsed >= 1 && parsed <= 65535 ? parsed : null;
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
     private HttpResponse send(HttpClientConnection httpConnection, 
ClassicHttpRequest request, HttpCoreContext httpContext)
             throws HttpException, IOException {
 
diff --git 
a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManagerDeliveryAddressTest.java
 
b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManagerDeliveryAddressTest.java
index 09ea7b6cca42..ebfdc91be1ba 100644
--- 
a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManagerDeliveryAddressTest.java
+++ 
b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManagerDeliveryAddressTest.java
@@ -21,6 +21,7 @@ import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpException;
 import org.junit.jupiter.api.Test;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -34,10 +35,8 @@ class AS2AsynchronousMDNManagerDeliveryAddressTest {
     private static final String ALLOWED = "partner.example";
 
     /**
-     * https in particular: this class delivers over a plain {@link 
java.net.Socket} and has no TLS, so accepting an
-     * https address would write the request - including the Authorization 
header - in cleartext to the TLS port. Before
-     * the delivery address was validated at all, an https address without an 
explicit port failed closed on
-     * {@code new Socket(host, -1)}; resolving it to 443 instead would have 
been a regression.
+     * Any scheme other than http is refused. This class delivers over a plain 
{@link java.net.Socket} and has no TLS
+     * support, so https and all other schemes are rejected.
      */
     @Test
     void aSchemeOtherThanHttpIsRefused() {
@@ -71,11 +70,66 @@ class AS2AsynchronousMDNManagerDeliveryAddressTest {
     }
 
     @Test
-    void anIpv6AllowListEntryIsMatchedWithoutItsBrackets() {
-        // URI.getHost() yields "[::1]", but an operator writes "::1" in the 
allow-list
-        Exception e = assertThrows(Exception.class, () -> 
deliver("http://[::1]:1/receipts";, "::1"));
+    void anIpv6AllowListEntryIsMatchedOnHostAndPort() {
+        // URI.getHost() yields "[::1]"; the operator lists the bracketed 
literal and its port as "[::1]:1"
+        Exception e = assertThrows(Exception.class, () -> 
deliver("http://[::1]:1/receipts";, "[::1]:1"));
         assertTrue(!(e instanceof HttpException) || 
!e.getMessage().contains("asyncMdnAllowedHosts"),
-                "an IPv6 literal should match its unbracketed allow-list 
entry: " + e.getMessage());
+                "an IPv6 literal should match its allow-list entry on host and 
port: " + e.getMessage());
+    }
+
+    /**
+     * An entry may pin the port in host:port form. A delivery address whose 
host and port both match the entry is
+     * accepted, so it gets past the address checks and fails on the 
connection instead - which is what tells us the
+     * address itself was accepted.
+     */
+    @Test
+    void aMatchingHostAndPortIsAccepted() {
+        Exception e = assertThrows(Exception.class, () -> 
deliver("http://localhost:1/receipts";, "localhost:1"));
+        assertTrue(!(e instanceof HttpException) || 
!e.getMessage().contains("asyncMdnAllowedHosts"),
+                "a delivery matching an allow-list entry on host and port must 
be accepted: " + e.getMessage());
+    }
+
+    /**
+     * The entry lists the host only, so it matches the default port (80 for 
http). A delivery to another port on that
+     * host is refused before the connection is opened, and the credentials 
are not attached.
+     */
+    @Test
+    void aDifferentPortOnAListedHostIsRefused() {
+        HttpException e = assertThrows(HttpException.class,
+                () -> deliver("http://partner.example:8443/receipts";, 
ALLOWED));
+        assertTrue(e.getMessage().contains("asyncMdnAllowedHosts"), 
"unexpected message: " + e.getMessage());
+    }
+
+    /**
+     * A bare-host entry matches the default port for the scheme (80 for http).
+     */
+    @Test
+    void aBareHostEntryMatchesTheDefaultPort() {
+        AS2AsynchronousMDNManager manager = new AS2AsynchronousMDNManager(
+                "1.1", "Camel", "sender.example.com", null, null, "user", 
"password", null, ALLOWED);
+        assertTrue(manager.isAllowedAuthority("partner.example", 80, 80),
+                "a bare-host entry must match the default port for the 
scheme");
+    }
+
+    /**
+     * When credentials are withheld, the warning names the delivery authority 
as an asyncMdnAllowedHosts entry the
+     * operator can copy. An IPv6 literal is written in square brackets: 
unbracketed, ::1:8443 is itself an IPv6 address
+     * and the entry would be read as that host on the default port.
+     */
+    @Test
+    void theWarningNamesTheAuthorityAsAnAllowListEntry() {
+        assertEquals("partner.example:4080", 
AS2AsynchronousMDNManager.formatAuthority("partner.example", 4080));
+        assertEquals("[::1]:8443", 
AS2AsynchronousMDNManager.formatAuthority("::1", 8443));
+
+        for (String[] authority : new String[][] { { "partner.example", "4080" 
}, { "::1", "8443" } }) {
+            String host = authority[0];
+            int port = Integer.parseInt(authority[1]);
+            AS2AsynchronousMDNManager manager = new AS2AsynchronousMDNManager(
+                    "1.1", "Camel", "sender.example.com", null, null, "user", 
"password", null,
+                    AS2AsynchronousMDNManager.formatAuthority(host, port));
+            assertTrue(manager.isAllowedAuthority(host, port, 80),
+                    "the entry the warning names must authorise the authority 
it was written for: " + host + " " + port);
+        }
     }
 
     private static void deliver(String deliveryAddress, String allowedHosts) 
throws Exception {
diff --git 
a/components/camel-as2/camel-as2-component/src/generated/resources/META-INF/org/apache/camel/component/as2/as2.json
 
b/components/camel-as2/camel-as2-component/src/generated/resources/META-INF/org/apache/camel/component/as2/as2.json
index dd6e71df9345..5b1369e2fc58 100644
--- 
a/components/camel-as2/camel-as2-component/src/generated/resources/META-INF/org/apache/camel/component/as2/as2.json
+++ 
b/components/camel-as2/camel-as2-component/src/generated/resources/META-INF/org/apache/camel/component/as2/as2.json
@@ -68,7 +68,7 @@
     "expectContinue": { "index": 32, "kind": "parameter", "displayName": 
"Expect Continue", "group": "producer", "label": "producer", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "Controls whether the Expect: 100-Continue 
header is included in outbound AS2 messages. W [...]
     "lazyStartProducer": { "index": 33, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
     "accessToken": { "index": 34, "kind": "parameter", "displayName": "Access 
Token", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "The access token that is used by the client 
for bearer authentication." },
-    "asyncMdnAllowedHosts": { "index": 35, "kind": "parameter", "displayName": 
"Async Mdn Allowed Hosts", "group": "security", "label": "security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "Comma separated list of host names an 
asynchronous MDN may be delivered to. The delivery  [...]
+    "asyncMdnAllowedHosts": { "index": 35, "kind": "parameter", "displayName": 
"Async Mdn Allowed Hosts", "group": "security", "label": "security", 
"required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "Comma separated list of hosts an asynchronous 
MDN may be delivered to. The delivery addre [...]
     "decryptingPrivateKey": { "index": 36, "kind": "parameter", "displayName": 
"Decrypting Private Key", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": "java.security.PrivateKey", "deprecated": 
false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "The key used to encrypt the EDI message." },
     "encryptingAlgorithm": { "index": 37, "kind": "parameter", "displayName": 
"Encrypting Algorithm", "group": "security", "label": "security", "required": 
false, "type": "enum", "javaType": 
"org.apache.camel.component.as2.api.AS2EncryptionAlgorithm", "enum": [ 
"AES128_CBC", "AES192_CBC", "AES256_CBC", "AES128_CCM", "AES192_CCM", 
"AES256_CCM", "AES128_GCM", "AES192_GCM", "AES256_GCM", "CAMELLIA128_CBC", 
"CAMELLIA192_CBC", "CAMELLIA256_CBC", "CAST5_CBC", "DES_CBC", "DES_EDE3_CBC", 
"GOST28 [...]
     "encryptingCertificateChain": { "index": 38, "kind": "parameter", 
"displayName": "Encrypting Certificate Chain", "group": "security", "label": 
"security", "required": false, "type": "object", "javaType": 
"java.security.cert.Certificate[]", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.as2.AS2Configuration", "configurationField": 
"configuration", "description": "The chain of certificates used to encrypt EDI 
message." },
diff --git 
a/components/camel-as2/camel-as2-component/src/main/docs/as2-component.adoc 
b/components/camel-as2/camel-as2-component/src/main/docs/as2-component.adoc
index 0f016f5783d9..93032413f675 100644
--- a/components/camel-as2/camel-as2-component/src/main/docs/as2-component.adoc
+++ b/components/camel-as2/camel-as2-component/src/main/docs/as2-component.adoc
@@ -62,3 +62,15 @@ consumer applies its own keys and certificates and its own 
asynchronous MDN sett
 The settings of the shared server connection still come from the first 
endpoint created on the port:
 `sslContextParameters` and `sslContext` (used for the TLS listener),
 `mdnMessageTemplate`, `server`, `serverFqdn` and `as2Version`. They are per 
port, not per consumer.
+
+=== Delivering asynchronous MDNs
+
+When a received message asks for an asynchronous MDN, the 
`as2://server/listen` consumer delivers the MDN to the
+address that the sender gave in the `Receipt-Delivery-Option` header. When 
`asyncMdnAllowedHosts` is set, the MDN is
+delivered only to an address on the list, and the `mdnUserName`, `mdnPassword` 
and `mdnAccessToken` credentials are
+attached only for such an address. Each entry of the list is a host with an 
optional port:
+
+* a bare host, such as `partner.example`, matches only the default port (80 
for `http`);
+* a host and a port, such as `partner.example:8080`, match that port only;
+* an IPv6 literal is written in square brackets, as in a URL: `[::1]` matches 
the default port, and `[::1]:8443`
+matches port 8443. Without the brackets, `::1:8443` is read as another IPv6 
address, not as `::1` with a port.
diff --git 
a/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Configuration.java
 
b/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Configuration.java
index 3bc6aec16704..7ecc9921f85c 100644
--- 
a/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Configuration.java
+++ 
b/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Configuration.java
@@ -698,12 +698,15 @@ public class AS2Configuration {
     }
 
     /**
-     * Comma separated list of host names an asynchronous MDN may be delivered 
to. The delivery address is chosen by the
-     * sender of the AS2 message, so this is the list of hosts the operator 
vouches for.
+     * Comma separated list of hosts an asynchronous MDN may be delivered to. 
The delivery address is chosen by the
+     * sender of the AS2 message, so this is the list of destinations the 
operator vouches for. Each entry is a host
+     * with an optional port in host:port form; an IPv6 literal is written in 
square brackets, as in a URL, optionally
+     * followed by :port. A bare-host entry (for example partner.example) 
matches only the default port for the scheme
+     * (80 or 443), so a non-default port is listed explicitly (for example 
partner.example:8443).
      *
-     * When set, an asynchronous MDN whose delivery address names a host 
outside the list is refused, and the configured
-     * mdnUserName/mdnPassword/mdnAccessToken are attached only to a host on 
the list. When not set, the MDN is still
-     * delivered to the sender-supplied address but no credentials are 
attached.
+     * When set, an asynchronous MDN whose delivery address is not on the list 
is refused, and the configured
+     * mdnUserName/mdnPassword/mdnAccessToken are attached only to an address 
on the list. When not set, the MDN is
+     * still delivered to the sender-supplied address but no credentials are 
attached.
      *
      * The option applies to the consumer endpoint it is set on: consumers 
sharing a serverPortNumber each apply their
      * own list.
diff --git 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnBasicAuthHeaderTest.java
 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnBasicAuthHeaderTest.java
index be603853b859..14ce48759e2f 100644
--- 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnBasicAuthHeaderTest.java
+++ 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnBasicAuthHeaderTest.java
@@ -197,9 +197,9 @@ public class AS2AsyncMdnBasicAuthHeaderTest extends 
AbstractAS2ITSupport {
                 "TBD", null, null,
                 // server authorization config
                 MDN_USER_NAME, MDN_PASSWORD, MDN_ACCESS_TOKEN,
-                // the MDN delivery address comes from the 
receiptDeliveryOption header, so the host it names
-                // has to be authorised before the credentials above are 
attached to it
-                "localhost");
+                // the MDN delivery address comes from the 
receiptDeliveryOption header, so the host and port it
+                // names have to be authorised before the credentials above 
are attached to it
+                "localhost:" + jettyPort.getPort() + ",localhost:" + 
receiptServerPort.getPort());
         targetPort = serverConnection.getLocalPort();
         serverConnection.listen("/", new 
AS2AsyncMDNServerManagerIT.RequestHandler());
     }
diff --git 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnContextReuseTest.java
 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnContextReuseTest.java
index 6a5c2bd3aa30..36a225db0c11 100644
--- 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnContextReuseTest.java
+++ 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnContextReuseTest.java
@@ -57,7 +57,7 @@ public class AS2AsyncMdnContextReuseTest extends 
AbstractAS2ITSupport {
         serverConnection = new AS2ServerConnection(
                 "1.1", "AS2AsyncMdnContextReuseTest Server", 
"server.example.com", 0,
                 AS2SignatureAlgorithm.SHA256WITHRSA, null, null, null, "TBD", 
null, null,
-                null, null, null, "localhost");
+                null, null, null, "localhost:" + jettyPort.getPort());
         targetPort = serverConnection.getLocalPort();
         serverConnection.listen("/", new 
AS2AsyncMDNServerManagerIT.RequestHandler());
     }
diff --git 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnTokenAuthHeaderTest.java
 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnTokenAuthHeaderTest.java
index afc5e3a70ae1..747ba82c4420 100644
--- 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnTokenAuthHeaderTest.java
+++ 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnTokenAuthHeaderTest.java
@@ -148,9 +148,9 @@ public class AS2AsyncMdnTokenAuthHeaderTest extends 
AbstractAS2ITSupport {
                 "TBD", null, null,
                 // server authorization config
                 null, null, MDN_ACCESS_TOKEN,
-                // the MDN delivery address comes from the 
receiptDeliveryOption header, so the host it names
-                // has to be authorised before the credentials above are 
attached to it
-                "localhost");
+                // the MDN delivery address comes from the 
receiptDeliveryOption header, so the host and port it
+                // names have to be authorised before the credentials above 
are attached to it
+                "localhost:" + receiptServerPort.getPort());
         targetPort = serverConnection.getLocalPort();
         serverConnection.listen("/", new 
AS2AsyncMDNServerManagerIT.RequestHandler());
     }
diff --git 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerBasicAuthHeaderTest.java
 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerBasicAuthHeaderTest.java
index f7d18367df56..98cbf1f3ea06 100644
--- 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerBasicAuthHeaderTest.java
+++ 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerBasicAuthHeaderTest.java
@@ -128,8 +128,9 @@ public class AS2ServerBasicAuthHeaderTest extends 
AbstractAS2ITSupport {
     @Override
     protected void customizeConfiguration(AS2Configuration configuration) {
         configuration.setServerPortNumber(targetPort.getPort());
-        // The MDN credentials are only attached to a delivery host the 
operator has authorised, and the
-        // delivery address here comes from the receiptDeliveryOption header, 
so localhost has to be listed.
-        configuration.setAsyncMdnAllowedHosts("localhost");
+        // The MDN credentials are only attached to a delivery address the 
operator has authorised, and the
+        // delivery address here comes from the receiptDeliveryOption header, 
so localhost and the port of the
+        // receipt endpoint have to be listed.
+        configuration.setAsyncMdnAllowedHosts("localhost:" + 
jettyPort.getPort());
     }
 }
diff --git 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerBearerAuthHeaderTest.java
 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerBearerAuthHeaderTest.java
index 94df68a32b4a..35aa89a72950 100644
--- 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerBearerAuthHeaderTest.java
+++ 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerBearerAuthHeaderTest.java
@@ -125,8 +125,9 @@ public class AS2ServerBearerAuthHeaderTest extends 
AbstractAS2ITSupport {
     @Override
     protected void customizeConfiguration(AS2Configuration configuration) {
         configuration.setServerPortNumber(targetPort.getPort());
-        // The MDN credentials are only attached to a delivery host the 
operator has authorised, and the
-        // delivery address here comes from the receiptDeliveryOption header, 
so localhost has to be listed.
-        configuration.setAsyncMdnAllowedHosts("localhost");
+        // The MDN credentials are only attached to a delivery address the 
operator has authorised, and the
+        // delivery address here comes from the receiptDeliveryOption header, 
so localhost and the port of the
+        // receipt endpoint have to be listed.
+        configuration.setAsyncMdnAllowedHosts("localhost:" + 
jettyPort.getPort());
     }
 }
diff --git 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerTwoConsumerAsyncMdnSettingsTest.java
 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerTwoConsumerAsyncMdnSettingsTest.java
index 2420baafa10c..45e1a06c5221 100644
--- 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerTwoConsumerAsyncMdnSettingsTest.java
+++ 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerTwoConsumerAsyncMdnSettingsTest.java
@@ -124,12 +124,21 @@ class AS2ServerTwoConsumerAsyncMdnSettingsTest extends 
AS2ServerSecTestBase {
                 null, receiptDeliveryAddress, null, null, null);
     }
 
+    // the host and port of each receipt endpoint, as listed in 
asyncMdnAllowedHosts
+    private String localhostReceiptAuthority() {
+        return "localhost:" + localhostReceiptPort.getPort();
+    }
+
+    private String loopbackReceiptAuthority() {
+        return "127.0.0.1:" + loopbackReceiptPort.getPort();
+    }
+
     private String localhostReceiptAddress() {
-        return "http://localhost:"; + localhostReceiptPort.getPort() + 
RECEIPT_PATH;
+        return "http://"; + localhostReceiptAuthority() + RECEIPT_PATH;
     }
 
     private String loopbackReceiptAddress() {
-        return "http://127.0.0.1:"; + loopbackReceiptPort.getPort() + 
RECEIPT_PATH;
+        return "http://"; + loopbackReceiptAuthority() + RECEIPT_PATH;
     }
 
     @Override
@@ -139,12 +148,13 @@ class AS2ServerTwoConsumerAsyncMdnSettingsTest extends 
AS2ServerSecTestBase {
                 // the first consumer creates the server connection shared by 
every consumer on the port
                 from("as2://server/listen?requestUriPattern=" + FIRST_PATTERN
                      + "&mdnUserName=" + FIRST_USER_NAME + "&mdnPassword=" + 
FIRST_PASSWORD
-                     + "&asyncMdnAllowedHosts=localhost,127.0.0.1")
+                     + "&asyncMdnAllowedHosts=" + localhostReceiptAuthority() 
+ "," + loopbackReceiptAuthority())
                         .routeId("first")
                         .to("mock:first");
 
                 from("as2://server/listen?requestUriPattern=" + SECOND_PATTERN
-                     + "&mdnAccessToken=" + SECOND_ACCESS_TOKEN + 
"&asyncMdnAllowedHosts=127.0.0.1")
+                     + "&mdnAccessToken=" + SECOND_ACCESS_TOKEN
+                     + "&asyncMdnAllowedHosts=" + loopbackReceiptAuthority())
                         .routeId("second")
                         .to("mock:second");
 
@@ -155,7 +165,8 @@ class AS2ServerTwoConsumerAsyncMdnSettingsTest extends 
AS2ServerSecTestBase {
 
                 // the root pattern, on the connection another consumer created
                 from("as2://server/listen?requestUriPattern=" + ROOT_PATTERN
-                     + "&mdnAccessToken=" + ROOT_ACCESS_TOKEN + 
"&asyncMdnAllowedHosts=localhost")
+                     + "&mdnAccessToken=" + ROOT_ACCESS_TOKEN
+                     + "&asyncMdnAllowedHosts=" + localhostReceiptAuthority())
                         .routeId("root")
                         .to("mock:root");
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 5f6313200ede..4026193f4267 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -37,6 +37,16 @@ The text of a body part now ends exactly where the boundary 
delimiter begins: th
 line is removed as the sender wrote it, CRLF or a bare LF. Before, two 
characters were always removed, so a part
 framed with bare line feeds (as `openssl cms -sign` writes them without 
`-crlfeol`) lost its last byte.
 
+=== camel-as2 - the asynchronous MDN allow-list matches the host and port
+
+The `asyncMdnAllowedHosts` option now matches the full authority - host and 
port - of the asynchronous MDN delivery
+address, rather than the host alone. Each entry is a host with an optional 
port in `host:port` form (IPv6 literals use
+bracket notation, for example `[::1]` or `[::1]:8080`). A bare-host entry (for 
example `partner.example`) now matches
+only the default port of `http` (80); to deliver to another port, list it 
explicitly (for example
+`partner.example:8080`).
+
+A deployment that lists a bare host and receives asynchronous MDNs on a 
non-default port must add the port to the entry.
+
 === camel-azure-eventhubs - an event that completes when the checkpoint timer 
of its window has run is checkpointed
 
 With `checkpointBatchTimeout`, the consumer now starts a new checkpoint window 
when the timer of the current window
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AS2EndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AS2EndpointBuilderFactory.java
index 97e5a5dc260e..3e65f4f6f47f 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AS2EndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AS2EndpointBuilderFactory.java
@@ -623,13 +623,18 @@ public interface AS2EndpointBuilderFactory {
             return this;
         }
         /**
-         * Comma separated list of host names an asynchronous MDN may be
-         * delivered to. The delivery address is chosen by the sender of the 
AS2
-         * message, so this is the list of hosts the operator vouches for. When
-         * set, an asynchronous MDN whose delivery address names a host outside
-         * the list is refused, and the configured
-         * mdnUserName/mdnPassword/mdnAccessToken are attached only to a host 
on
-         * the list. When not set, the MDN is still delivered to the
+         * Comma separated list of hosts an asynchronous MDN may be delivered
+         * to. The delivery address is chosen by the sender of the AS2 message,
+         * so this is the list of destinations the operator vouches for. Each
+         * entry is a host with an optional port in host:port form; an IPv6
+         * literal is written in square brackets, as in a URL, optionally
+         * followed by :port. A bare-host entry (for example partner.example)
+         * matches only the default port for the scheme (80 or 443), so a
+         * non-default port is listed explicitly (for example
+         * partner.example:8443). When set, an asynchronous MDN whose delivery
+         * address is not on the list is refused, and the configured
+         * mdnUserName/mdnPassword/mdnAccessToken are attached only to an
+         * address on the list. When not set, the MDN is still delivered to the
          * sender-supplied address but no credentials are attached. The option
          * applies to the consumer endpoint it is set on: consumers sharing a
          * serverPortNumber each apply their own list.
@@ -1765,13 +1770,18 @@ public interface AS2EndpointBuilderFactory {
             return this;
         }
         /**
-         * Comma separated list of host names an asynchronous MDN may be
-         * delivered to. The delivery address is chosen by the sender of the 
AS2
-         * message, so this is the list of hosts the operator vouches for. When
-         * set, an asynchronous MDN whose delivery address names a host outside
-         * the list is refused, and the configured
-         * mdnUserName/mdnPassword/mdnAccessToken are attached only to a host 
on
-         * the list. When not set, the MDN is still delivered to the
+         * Comma separated list of hosts an asynchronous MDN may be delivered
+         * to. The delivery address is chosen by the sender of the AS2 message,
+         * so this is the list of destinations the operator vouches for. Each
+         * entry is a host with an optional port in host:port form; an IPv6
+         * literal is written in square brackets, as in a URL, optionally
+         * followed by :port. A bare-host entry (for example partner.example)
+         * matches only the default port for the scheme (80 or 443), so a
+         * non-default port is listed explicitly (for example
+         * partner.example:8443). When set, an asynchronous MDN whose delivery
+         * address is not on the list is refused, and the configured
+         * mdnUserName/mdnPassword/mdnAccessToken are attached only to an
+         * address on the list. When not set, the MDN is still delivered to the
          * sender-supplied address but no credentials are attached. The option
          * applies to the consumer endpoint it is set on: consumers sharing a
          * serverPortNumber each apply their own list.
@@ -2848,13 +2858,18 @@ public interface AS2EndpointBuilderFactory {
             return this;
         }
         /**
-         * Comma separated list of host names an asynchronous MDN may be
-         * delivered to. The delivery address is chosen by the sender of the 
AS2
-         * message, so this is the list of hosts the operator vouches for. When
-         * set, an asynchronous MDN whose delivery address names a host outside
-         * the list is refused, and the configured
-         * mdnUserName/mdnPassword/mdnAccessToken are attached only to a host 
on
-         * the list. When not set, the MDN is still delivered to the
+         * Comma separated list of hosts an asynchronous MDN may be delivered
+         * to. The delivery address is chosen by the sender of the AS2 message,
+         * so this is the list of destinations the operator vouches for. Each
+         * entry is a host with an optional port in host:port form; an IPv6
+         * literal is written in square brackets, as in a URL, optionally
+         * followed by :port. A bare-host entry (for example partner.example)
+         * matches only the default port for the scheme (80 or 443), so a
+         * non-default port is listed explicitly (for example
+         * partner.example:8443). When set, an asynchronous MDN whose delivery
+         * address is not on the list is refused, and the configured
+         * mdnUserName/mdnPassword/mdnAccessToken are attached only to an
+         * address on the list. When not set, the MDN is still delivered to the
          * sender-supplied address but no credentials are attached. The option
          * applies to the consumer endpoint it is set on: consumers sharing a
          * serverPortNumber each apply their own list.

Reply via email to