This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch backport/28274-to-camel-4.22.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit b6050517143cc9854deb95b1860d0fe2e91b485d Author: Guillaume Nodet <[email protected]> AuthorDate: Fri Oct 23 11:05:58 2026 +0000 [backport camel-4.22.x] CAMEL-25918: camel-as2 - consumers sharing a port use their own asynchronous MDN settings --- .../org/apache/camel/catalog/components/as2.json | 8 +- .../apache/camel/catalog/docs/as2-component.adoc | 54 ++++++ .../component/as2/api/AS2ServerConnection.java | 187 ++++++++++++++------- .../AS2ConsumerConfigurationMdnSettingsTest.java | 91 ++++++++++ .../org/apache/camel/component/as2/as2.json | 8 +- .../src/main/docs/as2-component.adoc | 54 ++++++ .../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, 573 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..c35af1b32136 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,57 @@ 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. ==== + + +== Usage + +An `as2://server/listen` consumer receives AS2 messages on `serverPortNumber`, for the request URIs that match +`requestUriPattern`. An `as2://client/send` producer sends the message body (`inBody: ediMessage`) to +`targetHostname` and `targetPortNumber`. The following routes send a plain EDIFACT message from one to the other: + +[source,yaml] +---- +- route: + from: + uri: as2://server/listen + parameters: + serverPortNumber: 8888 + requestUriPattern: /receiver + steps: + - log: + message: "Received EDI message: ${body}" +- route: + from: + uri: timer:send + parameters: + repeatCount: 1 + steps: + - setBody: + expression: + constant: + expression: "UNB+UNOA:1+SENDER+RECEIVER+260101:1200+1'UNZ+1+1'" + - to: + uri: as2://client/send + parameters: + inBody: ediMessage + targetHostname: localhost + targetPortNumber: 8888 + requestUri: /receiver + as2From: sender + as2To: receiver + from: [email protected] + subject: Order + as2MessageStructure: PLAIN + ediMessageContentType: application/edifact +---- + +A `SIGNED`, `ENCRYPTED` or `SIGNED_ENCRYPTED` message structure also needs the keys and certificates of the signing +and encrypting options. + +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 and for the https delivery of asynchronous MDNs), +`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..e47d45595993 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,61 @@ 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(), + AS2ServerConnection.this.sslContext); + + 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); + } + } + } + } public AS2ServerConnection(String as2Version, @@ -621,24 +674,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 +726,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..c35af1b32136 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,57 @@ 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. ==== + + +== Usage + +An `as2://server/listen` consumer receives AS2 messages on `serverPortNumber`, for the request URIs that match +`requestUriPattern`. An `as2://client/send` producer sends the message body (`inBody: ediMessage`) to +`targetHostname` and `targetPortNumber`. The following routes send a plain EDIFACT message from one to the other: + +[source,yaml] +---- +- route: + from: + uri: as2://server/listen + parameters: + serverPortNumber: 8888 + requestUriPattern: /receiver + steps: + - log: + message: "Received EDI message: ${body}" +- route: + from: + uri: timer:send + parameters: + repeatCount: 1 + steps: + - setBody: + expression: + constant: + expression: "UNB+UNOA:1+SENDER+RECEIVER+260101:1200+1'UNZ+1+1'" + - to: + uri: as2://client/send + parameters: + inBody: ediMessage + targetHostname: localhost + targetPortNumber: 8888 + requestUri: /receiver + as2From: sender + as2To: receiver + from: [email protected] + subject: Order + as2MessageStructure: PLAIN + ediMessageContentType: application/edifact +---- + +A `SIGNED`, `ENCRYPTED` or `SIGNED_ENCRYPTED` message structure also needs the keys and certificates of the signing +and encrypting options. + +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 and for the https delivery of asynchronous MDNs), +`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 815dbc57e3dd..6667e2a2a792 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 @@ -2086,3 +2086,18 @@ acknowledged and the failure is left to the Camel error handler. A consumer that option leaves a failed QoS 1 message on a session that the broker keeps unacknowledged, as before; as the consumer does not connect that client, set the `cleanStart` and `sessionExpiryInterval` of the endpoint to match its connection options. + +=== 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. 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. *
