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.