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 5b6e3226d477 [backport camel-4.22.x] CAMEL-25918: camel-as2 -
consumers sharing a port use their own asynchronous MDN settings (#28347)
5b6e3226d477 is described below
commit 5b6e3226d47779bb51daa4c5c19e3e3dfa68570c
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Nov 2 10:25:48 2026 +0100
[backport camel-4.22.x] CAMEL-25918: camel-as2 - consumers sharing a port
use their own asynchronous MDN settings (#28347)
* [backport camel-4.22.x] CAMEL-25918: camel-as2 - consumers sharing a port
use their own asynchronous MDN settings
* [Backport camel-4.22.x] CAMEL-25918: fix AS2AsynchronousMDNManager
constructor call - remove SSLContext arg not present on this branch
* ci: trigger CI after constructor fix
* CAMEL-25918: Fix doc inaccuracy (sslContext not used for async MDN on
4.22.x) and log exception in MDN sign failure
* CAMEL-25918: Address davsclaus review: fix docs and upgrade guide
placement
- Remove extra == Usage section from as2-component.adoc (and catalog copy)
that came from #28026 (main-only), not from #28274
- Move upgrade guide entry under 'Upgrading from 4.22.2 to 4.22.3'
(was incorrectly placed under 'Upgrading Camel 4.21 to 4.22')
---
.../org/apache/camel/catalog/components/as2.json | 8 +-
.../apache/camel/catalog/docs/as2-component.adoc | 8 +
.../component/as2/api/AS2ServerConnection.java | 186 ++++++++++++++-------
.../AS2ConsumerConfigurationMdnSettingsTest.java | 91 ++++++++++
.../org/apache/camel/component/as2/as2.json | 8 +-
.../src/main/docs/as2-component.adoc | 8 +
.../camel/component/as2/AS2Configuration.java | 13 +-
.../apache/camel/component/as2/AS2Consumer.java | 28 ++--
.../AS2ServerTwoConsumerAsyncMdnSettingsTest.java | 170 +++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 15 ++
.../endpoint/dsl/AS2EndpointBuilderFactory.java | 42 +++--
11 files changed, 480 insertions(+), 97 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 e64448cd6565..dd6e71df9345 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,14 +68,14 @@
"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 host names an
asynchronous MDN may be delivered to. The delivery [...]
"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." },
"hostnameVerifier": { "index": 39, "kind": "parameter", "displayName":
"Hostname Verifier", "group": "security", "label": "security", "required":
false, "type": "object", "javaType": "javax.net.ssl.HostnameVerifier",
"deprecated": false, "autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.as2.AS2Configuration", "configurationField":
"configuration", "description": "Set hostname verifier for SSL session." },
- "mdnAccessToken": { "index": 40, "kind": "parameter", "displayName": "Mdn
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 server
when it sends an async MDN." },
- "mdnPassword": { "index": 41, "kind": "parameter", "displayName": "Mdn
Password", "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 password that is used by the server for
basic authentication when it sends an async [...]
- "mdnUserName": { "index": 42, "kind": "parameter", "displayName": "Mdn
User Name", "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 user-name that is used by the server for
basic authentication when it sends an asyn [...]
+ "mdnAccessToken": { "index": 40, "kind": "parameter", "displayName": "Mdn
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 server
when it sends an async MDN. The optio [...]
+ "mdnPassword": { "index": 41, "kind": "parameter", "displayName": "Mdn
Password", "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 password that is used by the server for
basic authentication when it sends an async [...]
+ "mdnUserName": { "index": 42, "kind": "parameter", "displayName": "Mdn
User Name", "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 user-name that is used by the server for
basic authentication when it sends an asyn [...]
"password": { "index": 43, "kind": "parameter", "displayName": "Password",
"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 password that is used by the client for
basic authentication." },
"signatureVerificationRequired": { "index": 44, "kind": "parameter",
"displayName": "Signature Verification Required", "group": "security", "label":
"security", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue":
false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration",
"configurationField": "configuration", "description": "Whether to reject an
inbound signed AS2 message that cann [...]
"signedReceiptMicAlgorithms": { "index": 45, "kind": "parameter",
"displayName": "Signed Receipt Mic Algorithms", "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": "The list of algorithms,
in order of preference, requested to generate a messa [...]
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 6ac5717c2cda..0f016f5783d9 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
@@ -54,3 +54,11 @@ producers, strip these internal headers at the trust
boundary -- for example wit
`removeHeaders("Camel*")` -- before the message reaches this component, so
that a sender cannot
override the API call. See xref:manual::security-model.adoc[the Camel security
model] for details.
====
+
+Several `as2://server/listen` consumers can share a `serverPortNumber`, each
with its own `requestUriPattern`. Each
+consumer applies its own keys and certificates and its own asynchronous MDN
settings (`mdnUserName`, `mdnPassword`,
+`mdnAccessToken` and `asyncMdnAllowedHosts`) to the messages received on its
own pattern.
+
+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.
diff --git
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
index 05b4061f9d8e..928dafb23196 100644
---
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
+++
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
@@ -89,10 +89,7 @@ public class AS2ServerConnection {
private final String as2Version;
private final String originServer;
private final String serverFqdn;
- private final String userName;
- private final String password;
- private final String accessToken;
- private final String asyncMdnAllowedHosts;
+ private final SSLContext sslContext;
/**
* Stores the configuration for each consumer endpoint path (e.g.,
"/consumerA"). Uses LinkedHashMap to preserve
@@ -130,8 +127,10 @@ public class AS2ServerConnection {
= ContextValue.newThreadLocal("AS2ConsumerConfig");
/**
- * Configuration data holding all necessary security material (signing
keys/certs and decryption keys/certs) for a
- * single AS2 consumer endpoint. This immutable object is looked up per
request URI.
+ * Configuration data holding all necessary security material (signing
keys/certs and decryption keys/certs) and the
+ * asynchronous MDN settings (the credentials attached to an asynchronous
MDN and the hosts it may be delivered to)
+ * for a single AS2 consumer endpoint. This immutable object is looked up
per request URI, so the consumers sharing
+ * a server connection each use their own values.
*/
public static class AS2ConsumerConfiguration {
private final Certificate[] signingCertificateChain;
@@ -139,18 +138,49 @@ public class AS2ServerConnection {
private final PrivateKey decryptingPrivateKey;
private final Certificate[] validateSigningCertificateChain;
private final AS2SignatureAlgorithm signingAlgorithm;
+ private final String mdnUserName;
+ private final String mdnPassword;
+ private final String mdnAccessToken;
+ private final String asyncMdnAllowedHosts;
+ /**
+ * Creates a configuration without asynchronous MDN settings: an
asynchronous MDN for a request matched by it is
+ * delivered without credentials, to the delivery address the sender
named.
+ */
public AS2ConsumerConfiguration(
AS2SignatureAlgorithm signingAlgorithm,
Certificate[] signingCertificateChain,
PrivateKey signingPrivateKey,
PrivateKey decryptingPrivateKey,
Certificate[]
validateSigningCertificateChain) {
+ this(signingAlgorithm, signingCertificateChain, signingPrivateKey,
decryptingPrivateKey,
+ validateSigningCertificateChain, null, null, null, null);
+ }
+
+ /**
+ * Creates a configuration with its own asynchronous MDN settings: an
asynchronous MDN for a request matched by
+ * it is delivered with these credentials (a user name and password,
or an access token), and only to a delivery
+ * address in {@code asyncMdnAllowedHosts} when that list is set.
+ */
+ public AS2ConsumerConfiguration(
+ AS2SignatureAlgorithm signingAlgorithm,
+ Certificate[] signingCertificateChain,
+ PrivateKey signingPrivateKey,
+ PrivateKey decryptingPrivateKey,
+ Certificate[]
validateSigningCertificateChain,
+ String mdnUserName,
+ String mdnPassword,
+ String mdnAccessToken,
+ String asyncMdnAllowedHosts) {
this.signingAlgorithm = signingAlgorithm;
this.signingCertificateChain = signingCertificateChain;
this.signingPrivateKey = signingPrivateKey;
this.decryptingPrivateKey = decryptingPrivateKey;
this.validateSigningCertificateChain =
validateSigningCertificateChain;
+ this.mdnUserName = mdnUserName;
+ this.mdnPassword = mdnPassword;
+ this.mdnAccessToken = mdnAccessToken;
+ this.asyncMdnAllowedHosts = asyncMdnAllowedHosts;
}
// Getters
@@ -173,6 +203,22 @@ public class AS2ServerConnection {
public PrivateKey getDecryptingPrivateKey() {
return decryptingPrivateKey;
}
+
+ public String getMdnUserName() {
+ return mdnUserName;
+ }
+
+ public String getMdnPassword() {
+ return mdnPassword;
+ }
+
+ public String getMdnAccessToken() {
+ return mdnAccessToken;
+ }
+
+ public String getAsyncMdnAllowedHosts() {
+ return asyncMdnAllowedHosts;
+ }
}
/**
@@ -510,55 +556,7 @@ public class AS2ServerConnection {
HttpCoreContext coreContext =
HttpCoreContext.castOrCreate(context);
try {
- // Safely retrieve the AS2 consumer configuration and
path from ThreadLocal storage.
- AS2ConsumerConfiguration config =
Optional.ofNullable(CURRENT_CONSUMER_CONFIG.get())
- .map(w -> w.config)
- .orElse(null);
-
- String recipientAddress =
coreContext.getAttribute(AS2AsynchronousMDNManager.RECIPIENT_ADDRESS,
- String.class);
-
- if (recipientAddress != null && config != null) {
- // Send the MDN asynchronously.
-
- DispositionNotificationMultipartReportEntity
multipartReportEntity = coreContext.getAttribute(
- AS2AsynchronousMDNManager.ASYNCHRONOUS_MDN,
-
DispositionNotificationMultipartReportEntity.class);
- AS2AsynchronousMDNManager asynchronousMDNManager =
new AS2AsynchronousMDNManager(
- AS2ServerConnection.this.as2Version,
- AS2ServerConnection.this.originServer,
- AS2ServerConnection.this.serverFqdn,
- config.getSigningCertificateChain(),
- config.getSigningPrivateKey(),
- AS2ServerConnection.this.userName,
- AS2ServerConnection.this.password,
- AS2ServerConnection.this.accessToken,
-
AS2ServerConnection.this.asyncMdnAllowedHosts);
-
- HttpRequest request = coreContext.getRequest();
- AS2SignedDataGenerator gen =
ResponseMDN.createSigningGenerator(
- request,
- config.getSigningAlgorithm(),
- config.getSigningCertificateChain(),
- config.getSigningPrivateKey());
- if (gen != null) {
- // send a signed MDN
- MultipartSignedEntity multipartSignedEntity =
null;
- try {
- multipartSignedEntity =
ResponseMDN.prepareSignedReceipt(gen, multipartReportEntity);
- } catch (Exception e) {
- LOG.warn("failed to sign MDN");
- }
- if (multipartSignedEntity != null) {
- asynchronousMDNManager.send(
- multipartSignedEntity,
multipartSignedEntity.getContentType(), recipientAddress);
- }
- } else {
- // send an unsigned MDN
-
asynchronousMDNManager.send(multipartReportEntity,
-
multipartReportEntity.getMainMessageContentType(), recipientAddress);
- }
- }
+ handleAsynchronousMDN(coreContext);
} finally {
// The context and the ThreadLocal are reused for
every request handled on this
// connection, and nothing else clears them. Without
this, a later request that does not
@@ -583,6 +581,60 @@ public class AS2ServerConnection {
}
}
+ private void handleAsynchronousMDN(HttpCoreContext coreContext) throws
HttpException, IOException {
+ // Safely retrieve the AS2 consumer configuration and path from
ThreadLocal storage.
+ AS2ConsumerConfiguration config =
Optional.ofNullable(CURRENT_CONSUMER_CONFIG.get())
+ .map(w -> w.config)
+ .orElse(null);
+
+ String recipientAddress =
coreContext.getAttribute(AS2AsynchronousMDNManager.RECIPIENT_ADDRESS,
+ String.class);
+
+ if (recipientAddress != null && config != null) {
+ // Send the MDN asynchronously.
+
+ DispositionNotificationMultipartReportEntity
multipartReportEntity = coreContext.getAttribute(
+ AS2AsynchronousMDNManager.ASYNCHRONOUS_MDN,
+ DispositionNotificationMultipartReportEntity.class);
+ // The MDN settings come from the matched path's
configuration, like the signing material: the
+ // consumers sharing this connection each deliver their
asynchronous MDNs with their own values.
+ AS2AsynchronousMDNManager asynchronousMDNManager = new
AS2AsynchronousMDNManager(
+ AS2ServerConnection.this.as2Version,
+ AS2ServerConnection.this.originServer,
+ AS2ServerConnection.this.serverFqdn,
+ config.getSigningCertificateChain(),
+ config.getSigningPrivateKey(),
+ config.getMdnUserName(),
+ config.getMdnPassword(),
+ config.getMdnAccessToken(),
+ config.getAsyncMdnAllowedHosts());
+
+ HttpRequest request = coreContext.getRequest();
+ AS2SignedDataGenerator gen =
ResponseMDN.createSigningGenerator(
+ request,
+ config.getSigningAlgorithm(),
+ config.getSigningCertificateChain(),
+ config.getSigningPrivateKey());
+ if (gen != null) {
+ // send a signed MDN
+ MultipartSignedEntity multipartSignedEntity = null;
+ try {
+ multipartSignedEntity =
ResponseMDN.prepareSignedReceipt(gen, multipartReportEntity);
+ } catch (Exception e) {
+ LOG.warn("failed to sign MDN", e);
+ }
+ if (multipartSignedEntity != null) {
+ asynchronousMDNManager.send(
+ multipartSignedEntity,
multipartSignedEntity.getContentType(), recipientAddress);
+ }
+ } else {
+ // send an unsigned MDN
+ asynchronousMDNManager.send(multipartReportEntity,
+ multipartReportEntity.getMainMessageContentType(),
recipientAddress);
+ }
+ }
+ }
+
}
public AS2ServerConnection(String as2Version,
@@ -621,24 +673,27 @@ public class AS2ServerConnection {
String accessToken,
String asyncMdnAllowedHosts)
throws IOException
{
- this.asyncMdnAllowedHosts = asyncMdnAllowedHosts;
+ this.sslContext = sslContext;
this.as2Version = ObjectHelper.notNull(as2Version, "as2Version");
this.originServer = ObjectHelper.notNull(originServer, "userAgent");
this.serverFqdn = ObjectHelper.notNull(serverFqdn, "serverFqdn");
final Integer parserServerPortNumber =
ObjectHelper.notNull(serverPortNumber, "serverPortNumber");
- this.userName = userName;
- this.password = password;
- this.accessToken = accessToken;
// Create and register a default consumer configuration for the root
path ('/').
- // This ensures that all incoming requests have a fallback
configuration for decryption
- // and MDN signing, even if they don't match a specific Camel route
path.
+ // This ensures that all incoming requests have a fallback
configuration for decryption,
+ // MDN signing and asynchronous MDN delivery, even if they don't match
a specific Camel route path.
+ // The MDN settings given to this constructor belong to that fallback
only: a consumer registers
+ // its own configuration, with its own MDN settings, for its own
request URI pattern.
AS2ServerConnection.AS2ConsumerConfiguration consumerConfig = new
AS2ServerConnection.AS2ConsumerConfiguration(
signingAlgorithm,
signingCertificateChain,
signingPrivateKey,
decryptingPrivateKey,
- validateSigningCertificateChain);
+ validateSigningCertificateChain,
+ userName,
+ password,
+ accessToken,
+ asyncMdnAllowedHosts);
registerConsumerConfiguration("/", consumerConfig);
listenerService = new RequestListenerService(
@@ -670,6 +725,13 @@ public class AS2ServerConnection {
.orElse(null);
}
+ /**
+ * Registers the configuration used for the requests matching a consumer's
request URI pattern: its signing and
+ * decrypting material and its asynchronous MDN settings. Each consumer
registers its own configuration for its own
+ * pattern, also when the pattern is the root path ({@code /}) for which
the constructor registered a fallback: the
+ * fallback and the configurations of the other consumers sharing this
connection are not its own. Registering a
+ * pattern again replaces the configuration registered for it.
+ */
public void registerConsumerConfiguration(String path,
AS2ConsumerConfiguration config) {
consumerConfigurations.put(path, config);
}
diff --git
a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ConsumerConfigurationMdnSettingsTest.java
b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ConsumerConfigurationMdnSettingsTest.java
new file mode 100644
index 000000000000..1a297f6a10b3
--- /dev/null
+++
b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ConsumerConfigurationMdnSettingsTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.camel.component.as2.api;
+
+import
org.apache.camel.component.as2.api.AS2ServerConnection.AS2ConsumerConfiguration;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * The asynchronous MDN settings (the credentials attached to an asynchronous
MDN and the hosts it may be delivered to)
+ * are part of the per-path {@link AS2ConsumerConfiguration}, so the consumers
sharing a server connection each keep
+ * their own. The fallback configuration of the root path carries the values
given to the connection's constructor.
+ */
+class AS2ConsumerConfigurationMdnSettingsTest {
+
+ @Test
+ void configurationCarriesTheMdnSettings() {
+ AS2ConsumerConfiguration config = new AS2ConsumerConfiguration(
+ AS2SignatureAlgorithm.SHA256WITHRSA, null, null, null, null,
+ "user", "password", "token", "receipts.example.com");
+
+ assertEquals(AS2SignatureAlgorithm.SHA256WITHRSA,
config.getSigningAlgorithm());
+ assertEquals("user", config.getMdnUserName());
+ assertEquals("password", config.getMdnPassword());
+ assertEquals("token", config.getMdnAccessToken());
+ assertEquals("receipts.example.com", config.getAsyncMdnAllowedHosts());
+ }
+
+ @Test
+ void configurationWithoutMdnSettingsHasNone() {
+ AS2ConsumerConfiguration config = new AS2ConsumerConfiguration(
+ AS2SignatureAlgorithm.SHA256WITHRSA, null, null, null, null);
+
+ assertNull(config.getMdnUserName());
+ assertNull(config.getMdnPassword());
+ assertNull(config.getMdnAccessToken());
+ assertNull(config.getAsyncMdnAllowedHosts());
+ }
+
+ @Test
+ void rootPathFallbackCarriesTheConnectionSettings() throws Exception {
+ AS2ServerConnection connection = new AS2ServerConnection(
+ "1.1", "AS2ConsumerConfigurationMdnSettingsTest Server",
"server.example.com", 0,
+ AS2SignatureAlgorithm.SHA256WITHRSA, null, null, null, "TBD",
null, null,
+ "user", "password", "token", "receipts.example.com");
+ try {
+ AS2ConsumerConfiguration fallback =
connection.getConfigurationForPath("/").orElseThrow();
+ assertEquals("user", fallback.getMdnUserName());
+ assertEquals("password", fallback.getMdnPassword());
+ assertEquals("token", fallback.getMdnAccessToken());
+ assertEquals("receipts.example.com",
fallback.getAsyncMdnAllowedHosts());
+
+ // a consumer registered for another path keeps its own settings,
not the fallback's
+ connection.registerConsumerConfiguration("/partner", new
AS2ConsumerConfiguration(
+ AS2SignatureAlgorithm.SHA256WITHRSA, null, null, null,
null,
+ null, null, "partner-token", "partner.example.com"));
+ AS2ConsumerConfiguration partner =
connection.getConfigurationForPath("/partner").orElseThrow();
+ assertNull(partner.getMdnUserName());
+ assertNull(partner.getMdnPassword());
+ assertEquals("partner-token", partner.getMdnAccessToken());
+ assertEquals("partner.example.com",
partner.getAsyncMdnAllowedHosts());
+ assertEquals("user",
connection.getConfigurationForPath("/").orElseThrow().getMdnUserName());
+
+ // registering the root path again replaces the fallback
+ connection.registerConsumerConfiguration("/", new
AS2ConsumerConfiguration(
+ AS2SignatureAlgorithm.SHA256WITHRSA, null, null, null,
null));
+ AS2ConsumerConfiguration replaced =
connection.getConfigurationForPath("/").orElseThrow();
+ assertNull(replaced.getMdnUserName());
+ assertNull(replaced.getMdnAccessToken());
+ assertNull(replaced.getAsyncMdnAllowedHosts());
+ } finally {
+ connection.close();
+ }
+ }
+}
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 e64448cd6565..dd6e71df9345 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,14 +68,14 @@
"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 host names an
asynchronous MDN may be delivered to. The delivery [...]
"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." },
"hostnameVerifier": { "index": 39, "kind": "parameter", "displayName":
"Hostname Verifier", "group": "security", "label": "security", "required":
false, "type": "object", "javaType": "javax.net.ssl.HostnameVerifier",
"deprecated": false, "autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.as2.AS2Configuration", "configurationField":
"configuration", "description": "Set hostname verifier for SSL session." },
- "mdnAccessToken": { "index": 40, "kind": "parameter", "displayName": "Mdn
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 server
when it sends an async MDN." },
- "mdnPassword": { "index": 41, "kind": "parameter", "displayName": "Mdn
Password", "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 password that is used by the server for
basic authentication when it sends an async [...]
- "mdnUserName": { "index": 42, "kind": "parameter", "displayName": "Mdn
User Name", "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 user-name that is used by the server for
basic authentication when it sends an asyn [...]
+ "mdnAccessToken": { "index": 40, "kind": "parameter", "displayName": "Mdn
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 server
when it sends an async MDN. The optio [...]
+ "mdnPassword": { "index": 41, "kind": "parameter", "displayName": "Mdn
Password", "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 password that is used by the server for
basic authentication when it sends an async [...]
+ "mdnUserName": { "index": 42, "kind": "parameter", "displayName": "Mdn
User Name", "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 user-name that is used by the server for
basic authentication when it sends an asyn [...]
"password": { "index": 43, "kind": "parameter", "displayName": "Password",
"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 password that is used by the client for
basic authentication." },
"signatureVerificationRequired": { "index": 44, "kind": "parameter",
"displayName": "Signature Verification Required", "group": "security", "label":
"security", "required": false, "type": "boolean", "javaType": "boolean",
"deprecated": false, "autowired": false, "secret": false, "defaultValue":
false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration",
"configurationField": "configuration", "description": "Whether to reject an
inbound signed AS2 message that cann [...]
"signedReceiptMicAlgorithms": { "index": 45, "kind": "parameter",
"displayName": "Signed Receipt Mic Algorithms", "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": "The list of algorithms,
in order of preference, requested to generate a messa [...]
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 6ac5717c2cda..0f016f5783d9 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
@@ -54,3 +54,11 @@ producers, strip these internal headers at the trust
boundary -- for example wit
`removeHeaders("Camel*")` -- before the message reaches this component, so
that a sender cannot
override the API call. See xref:manual::security-model.adoc[the Camel security
model] for details.
====
+
+Several `as2://server/listen` consumers can share a `serverPortNumber`, each
with its own `requestUriPattern`. Each
+consumer applies its own keys and certificates and its own asynchronous MDN
settings (`mdnUserName`, `mdnPassword`,
+`mdnAccessToken` and `asyncMdnAllowedHosts`) to the messages received on its
own pattern.
+
+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.
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 cc283631a27c..3bc6aec16704 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
@@ -661,7 +661,9 @@ public class AS2Configuration {
/**
* The user-name that is used by the server for basic authentication when
it sends an async MDN. If options for
- * basic authentication and bearer authentication are both set then basic
authentication takes precedence.
+ * basic authentication and bearer authentication are both set then basic
authentication takes precedence. The
+ * option applies to the consumer endpoint it is set on: consumers sharing
a serverPortNumber each send their async
+ * MDNs with their own values.
*/
public void setMdnUserName(String mdnUserName) {
this.mdnUserName = mdnUserName;
@@ -672,7 +674,8 @@ public class AS2Configuration {
}
/**
- * The password that is used by the server for basic authentication when
it sends an async MDN.
+ * The password that is used by the server for basic authentication when
it sends an async MDN. The option applies
+ * to the consumer endpoint it is set on, like mdnUserName.
*/
public void setMdnPassword(String mdnPassword) {
this.mdnPassword = mdnPassword;
@@ -683,7 +686,8 @@ public class AS2Configuration {
}
/**
- * The access token that is used by the server when it sends an async MDN.
+ * The access token that is used by the server when it sends an async MDN.
The option applies to the consumer
+ * endpoint it is set on, like mdnUserName.
*/
public void setMdnAccessToken(String accessToken) {
this.mdnAccessToken = accessToken;
@@ -700,6 +704,9 @@ public class AS2Configuration {
* 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.
+ *
+ * The option applies to the consumer endpoint it is set on: consumers
sharing a serverPortNumber each apply their
+ * own list.
*/
public void setAsyncMdnAllowedHosts(String asyncMdnAllowedHosts) {
this.asyncMdnAllowedHosts = asyncMdnAllowedHosts;
diff --git
a/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Consumer.java
b/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Consumer.java
index 9446fc6135e1..9a2e8854dba4 100644
---
a/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Consumer.java
+++
b/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Consumer.java
@@ -93,18 +93,22 @@ public class AS2Consumer extends
AbstractApiConsumer<AS2ApiName, AS2Configuratio
String uri = properties.computeIfAbsent("requestUriPattern", param ->
"/").toString();
- // Check if the configuration for this specific URI path has already
been registered
- // (e.g., by the default "/" fallback or another consumer).
- // If not, create and register it now using the endpoint's configured
keys/certs.
- if (as2ServerConnection.getConfigurationForPath(uri).isEmpty()) {
- AS2ServerConnection.AS2ConsumerConfiguration consumerConfig = new
AS2ServerConnection.AS2ConsumerConfiguration(
- getEndpoint().getSigningAlgorithm(),
- getEndpoint().getSigningCertificateChain(),
- getEndpoint().getSigningPrivateKey(),
- getEndpoint().getDecryptingPrivateKey(),
- getEndpoint().getValidateSigningCertificateChain());
- as2ServerConnection.registerConsumerConfiguration(uri,
consumerConfig);
- }
+ // Register this consumer's own configuration for its URI pattern: the
endpoint's keys/certs and its
+ // asynchronous MDN settings. The server connection is shared by every
consumer on the port and was
+ // built from the first endpoint created on it, so the "/" fallback
and the configurations of the
+ // other consumers are not this consumer's; a consumer whose pattern
is "/" replaces the fallback.
+ AS2Configuration configuration = getEndpoint().getConfiguration();
+ AS2ServerConnection.AS2ConsumerConfiguration consumerConfig = new
AS2ServerConnection.AS2ConsumerConfiguration(
+ getEndpoint().getSigningAlgorithm(),
+ getEndpoint().getSigningCertificateChain(),
+ getEndpoint().getSigningPrivateKey(),
+ getEndpoint().getDecryptingPrivateKey(),
+ getEndpoint().getValidateSigningCertificateChain(),
+ configuration.getMdnUserName(),
+ configuration.getMdnPassword(),
+ configuration.getMdnAccessToken(),
+ configuration.getAsyncMdnAllowedHosts());
+ as2ServerConnection.registerConsumerConfiguration(uri, consumerConfig);
as2ServerConnection.listen(uri, this);
}
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
new file mode 100644
index 000000000000..2420baafa10c
--- /dev/null
+++
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerTwoConsumerAsyncMdnSettingsTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.camel.component.as2;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.as2.api.AS2MediaType;
+import org.apache.camel.component.as2.api.AS2MessageStructure;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Several {@code as2://server/listen} consumers share one {@code
serverPortNumber}, each with its own
+ * {@code requestUriPattern}. Each of them delivers the asynchronous MDNs for
the messages received on its own pattern
+ * with its own {@code mdnUserName}, {@code mdnPassword}, {@code
mdnAccessToken} and {@code asyncMdnAllowedHosts}, not
+ * with the values of the consumer that created the shared server connection.
This also holds for a consumer on the root
+ * pattern, for which the connection registers a fallback configuration from
the endpoint that created it.
+ */
+class AS2ServerTwoConsumerAsyncMdnSettingsTest extends AS2ServerSecTestBase {
+
+ private static final String FIRST_PATTERN = "/receiver-one";
+ private static final String SECOND_PATTERN = "/receiver-two";
+ private static final String THIRD_PATTERN = "/receiver-three";
+ private static final String ROOT_PATTERN = "/";
+ private static final String FIRST_USER_NAME = "first-user";
+ private static final String FIRST_PASSWORD = "first-password";
+ private static final String SECOND_ACCESS_TOKEN = "second-token";
+ private static final String ROOT_ACCESS_TOKEN = "root-token";
+ private static final String RECEIPT_PATH = "/receipts";
+
+ // one receipt endpoint per host name, so that each delivery address
resolves to the endpoint bound to it
+ @RegisterExtension
+ AvailablePortFinder.Port localhostReceiptPort = AvailablePortFinder.find();
+ @RegisterExtension
+ AvailablePortFinder.Port loopbackReceiptPort = AvailablePortFinder.find();
+
+ @Test
+ void asyncMdnForTheSecondConsumerUsesItsOwnSettings() throws Exception {
+ String authorization = authorizationOfTheReceiptFor(SECOND_PATTERN,
loopbackReceiptAddress());
+
+ assertEquals("Bearer " + SECOND_ACCESS_TOKEN, authorization);
+ }
+
+ @Test
+ void asyncMdnForTheSecondConsumerAppliesItsOwnAllowedHosts() throws
Exception {
+ // localhost is on the first consumer's list only, so the second
consumer's MDN is not delivered to it
+ MockEndpoint receipts = getMockEndpoint("mock:receipts");
+ receipts.expectedMessageCount(0);
+
+ HttpCoreContext context = sendTo(SECOND_PATTERN,
localhostReceiptAddress());
+ verifyOkResponse(context);
+
+ receipts.assertIsSatisfied(2000);
+ }
+
+ @Test
+ void asyncMdnForAConsumerWithoutSettingsCarriesNoAuthorization() throws
Exception {
+ assertNull(authorizationOfTheReceiptFor(THIRD_PATTERN,
localhostReceiptAddress()));
+ }
+
+ @Test
+ void asyncMdnForTheConsumerOnTheRootPatternUsesItsOwnSettings() throws
Exception {
+ String authorization = authorizationOfTheReceiptFor(ROOT_PATTERN,
localhostReceiptAddress());
+
+ assertEquals("Bearer " + ROOT_ACCESS_TOKEN, authorization);
+ }
+
+ @Test
+ void asyncMdnForTheFirstConsumerIsUnchanged() throws Exception {
+ String authorization = authorizationOfTheReceiptFor(FIRST_PATTERN,
localhostReceiptAddress());
+
+ String encoded = Base64.getEncoder()
+ .encodeToString((FIRST_USER_NAME + ":" +
FIRST_PASSWORD).getBytes(StandardCharsets.UTF_8));
+ assertEquals("Basic " + encoded, authorization);
+ }
+
+ @Test
+ void asyncMdnAfterTheFirstConsumerStopsUsesTheRemainingConsumerSettings()
throws Exception {
+ context.getRouteController().stopRoute("first");
+
+ String authorization = authorizationOfTheReceiptFor(SECOND_PATTERN,
loopbackReceiptAddress());
+
+ assertEquals("Bearer " + SECOND_ACCESS_TOKEN, authorization);
+ }
+
+ private String authorizationOfTheReceiptFor(String requestUri, String
receiptDeliveryAddress) throws Exception {
+ MockEndpoint receipts = getMockEndpoint("mock:receipts");
+ receipts.expectedMessageCount(1);
+
+ HttpCoreContext context = sendTo(requestUri, receiptDeliveryAddress);
+ verifyOkResponse(context);
+
+ receipts.assertIsSatisfied();
+ return
receipts.getExchanges().get(0).getIn().getHeader("Authorization", String.class);
+ }
+
+ private HttpCoreContext sendTo(String requestUri, String
receiptDeliveryAddress) throws Exception {
+ return clientConnection().send(EDI_MESSAGE, requestUri, SUBJECT, FROM,
AS2_NAME, AS2_NAME,
+ AS2MessageStructure.PLAIN, AS2MediaType.APPLICATION_EDIFACT,
null, null,
+ null, null, null, null,
+ DISPOSITION_NOTIFICATION_TO, null, null, null,
+ null, receiptDeliveryAddress, null, null, null);
+ }
+
+ private String localhostReceiptAddress() {
+ return "http://localhost:" + localhostReceiptPort.getPort() +
RECEIPT_PATH;
+ }
+
+ private String loopbackReceiptAddress() {
+ return "http://127.0.0.1:" + loopbackReceiptPort.getPort() +
RECEIPT_PATH;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() {
+ // 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")
+ .routeId("first")
+ .to("mock:first");
+
+ from("as2://server/listen?requestUriPattern=" + SECOND_PATTERN
+ + "&mdnAccessToken=" + SECOND_ACCESS_TOKEN +
"&asyncMdnAllowedHosts=127.0.0.1")
+ .routeId("second")
+ .to("mock:second");
+
+ // no asynchronous MDN settings at all
+ from("as2://server/listen?requestUriPattern=" + THIRD_PATTERN)
+ .routeId("third")
+ .to("mock:third");
+
+ // the root pattern, on the connection another consumer created
+ from("as2://server/listen?requestUriPattern=" + ROOT_PATTERN
+ + "&mdnAccessToken=" + ROOT_ACCESS_TOKEN +
"&asyncMdnAllowedHosts=localhost")
+ .routeId("root")
+ .to("mock:root");
+
+ // where the asynchronous MDNs are delivered to
+ from("jetty:http://localhost:" +
localhostReceiptPort.getPort() + RECEIPT_PATH)
+ .to("mock:receipts");
+ from("jetty:http://127.0.0.1:" + loopbackReceiptPort.getPort()
+ RECEIPT_PATH)
+ .to("mock:receipts");
+ }
+ };
+ }
+}
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 b7916b27f96e..176cfe113a6e 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
@@ -112,6 +112,21 @@ Additionally:
- When `alwaysReauthenticate=true` (the default), no caller identity stays
bound to the processing thread after the
policy check completes.
+=== camel-as2 - consumers sharing a port use their own asynchronous MDN
settings
+
+`as2://server/listen` consumers that share a `serverPortNumber` now deliver
asynchronous MDNs with their own
+`mdnUserName`, `mdnPassword`, `mdnAccessToken` and `asyncMdnAllowedHosts`,
selected by the matched `requestUriPattern`
+in the same way as the signing material. Previously the values of the first
consumer configured on the port were used
+for every consumer on it, and the values set on the other consumers were
ignored. A consumer that relied on inheriting
+them now has to set them itself.
+
+This applies to `asyncMdnAllowedHosts` as well. A consumer without its own
list used to inherit the first consumer's
+list, so asynchronous MDNs to other hosts were refused; it now delivers to the
address the sender gives, without
+credentials. A deployment that counted on the inherited list as a restriction
has to set it on each consumer.
+
+A consumer whose `requestUriPattern` is `/` now also uses its own keys,
certificates and asynchronous MDN settings when
+another consumer created the shared server connection; before, it used the
first consumer's.
+
== Upgrading from 4.22.1 to 4.22.2
=== camel-management - Route group exchange counters count once per entry into
the group again
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 bdcfdec593df..97e5a5dc260e 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
@@ -630,7 +630,9 @@ public interface AS2EndpointBuilderFactory {
* 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.
+ * 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.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -762,7 +764,8 @@ public interface AS2EndpointBuilderFactory {
}
/**
* The access token that is used by the server when it sends an async
- * MDN.
+ * MDN. The option applies to the consumer endpoint it is set on, like
+ * mdnUserName.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -777,7 +780,8 @@ public interface AS2EndpointBuilderFactory {
}
/**
* The password that is used by the server for basic authentication
when
- * it sends an async MDN.
+ * it sends an async MDN. The option applies to the consumer endpoint
it
+ * is set on, like mdnUserName.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -794,7 +798,9 @@ public interface AS2EndpointBuilderFactory {
* The user-name that is used by the server for basic authentication
* when it sends an async MDN. If options for basic authentication and
* bearer authentication are both set then basic authentication takes
- * precedence.
+ * precedence. The option applies to the consumer endpoint it is set
on:
+ * consumers sharing a serverPortNumber each send their async MDNs with
+ * their own values.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -1766,7 +1772,9 @@ public interface AS2EndpointBuilderFactory {
* 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.
+ * 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.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -1898,7 +1906,8 @@ public interface AS2EndpointBuilderFactory {
}
/**
* The access token that is used by the server when it sends an async
- * MDN.
+ * MDN. The option applies to the consumer endpoint it is set on, like
+ * mdnUserName.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -1913,7 +1922,8 @@ public interface AS2EndpointBuilderFactory {
}
/**
* The password that is used by the server for basic authentication
when
- * it sends an async MDN.
+ * it sends an async MDN. The option applies to the consumer endpoint
it
+ * is set on, like mdnUserName.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -1930,7 +1940,9 @@ public interface AS2EndpointBuilderFactory {
* The user-name that is used by the server for basic authentication
* when it sends an async MDN. If options for basic authentication and
* bearer authentication are both set then basic authentication takes
- * precedence.
+ * precedence. The option applies to the consumer endpoint it is set
on:
+ * consumers sharing a serverPortNumber each send their async MDNs with
+ * their own values.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -2843,7 +2855,9 @@ public interface AS2EndpointBuilderFactory {
* 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.
+ * 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.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -2975,7 +2989,8 @@ public interface AS2EndpointBuilderFactory {
}
/**
* The access token that is used by the server when it sends an async
- * MDN.
+ * MDN. The option applies to the consumer endpoint it is set on, like
+ * mdnUserName.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -2990,7 +3005,8 @@ public interface AS2EndpointBuilderFactory {
}
/**
* The password that is used by the server for basic authentication
when
- * it sends an async MDN.
+ * it sends an async MDN. The option applies to the consumer endpoint
it
+ * is set on, like mdnUserName.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -3007,7 +3023,9 @@ public interface AS2EndpointBuilderFactory {
* The user-name that is used by the server for basic authentication
* when it sends an async MDN. If options for basic authentication and
* bearer authentication are both set then basic authentication takes
- * precedence.
+ * precedence. The option applies to the consumer endpoint it is set
on:
+ * consumers sharing a serverPortNumber each send their async MDNs with
+ * their own values.
*
* The option is a: <code>java.lang.String</code> type.
*