This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch fix/as2-issues in repository https://gitbox.apache.org/repos/asf/camel.git
commit 43d5068b57c2f31ac2c0f6177a824ed87b3849a0 Author: Guillaume Nodet <[email protected]> AuthorDate: Mon Mar 9 11:04:16 2026 +0100 CAMEL-23064: Support non-standard content types in AS2 EDI payload Add GenericApplicationEntity to handle EDI payloads with content types not explicitly mapped (e.g. text/plain, application/octet-stream). Previously, unknown content types threw a CamelException. Also fix missing return statement in HttpMessageUtils.getEntity() for response branch. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../component/as2/api/entity/EntityParser.java | 3 + .../as2/api/entity/GenericApplicationEntity.java | 39 ++++++++ .../camel/component/as2/api/util/EntityUtils.java | 5 +- .../component/as2/api/util/HttpMessageUtils.java | 59 +++++++++--- .../api/entity/GenericApplicationEntityTest.java | 100 +++++++++++++++++++++ 5 files changed, 192 insertions(+), 14 deletions(-) diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/EntityParser.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/EntityParser.java index d6b4f84efeaa..7d2b25b6de84 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/EntityParser.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/EntityParser.java @@ -523,6 +523,9 @@ public final class EntityParser { contentTransferEncoding); break; default: + // Accept non-standard content types (e.g., text/plain, application/octet-stream) + // that real-world AS2 partners may use to wrap EDI payloads + parseApplicationEDIEntity(message, entity.getContent(), inBuffer, contentType, contentTransferEncoding); break; } } diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/GenericApplicationEntity.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/GenericApplicationEntity.java new file mode 100644 index 000000000000..6bc3d2e7a49f --- /dev/null +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/GenericApplicationEntity.java @@ -0,0 +1,39 @@ +/* + * 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.entity; + +import java.io.IOException; + +import org.apache.hc.core5.http.ContentType; + +/** + * A generic application entity that wraps EDI payloads with non-standard content types (e.g., text/plain, + * application/octet-stream). Real-world AS2 partners frequently send EDI payloads wrapped in content types other than + * the standard application/edifact, application/edi-x12, or application/edi-consent types. + */ +public class GenericApplicationEntity extends ApplicationEntity { + + public GenericApplicationEntity(byte[] content, ContentType contentType, String contentTransferEncoding, + boolean isMainBody, String filename) { + super(content, contentType, contentTransferEncoding, isMainBody, filename); + } + + @Override + public void close() throws IOException { + // do nothing + } +} diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/EntityUtils.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/EntityUtils.java index 3de97bda41dc..3bd4404ff917 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/EntityUtils.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/EntityUtils.java @@ -33,6 +33,7 @@ import org.apache.camel.component.as2.api.entity.ApplicationEDIFACTEntity; import org.apache.camel.component.as2.api.entity.ApplicationEDIX12Entity; import org.apache.camel.component.as2.api.entity.ApplicationEntity; import org.apache.camel.component.as2.api.entity.ApplicationXMLEntity; +import org.apache.camel.component.as2.api.entity.GenericApplicationEntity; import org.apache.camel.component.as2.api.entity.MimeEntity; import org.apache.camel.util.ObjectHelper; import org.apache.commons.codec.DecoderException; @@ -203,7 +204,9 @@ public final class EntityUtils { case AS2MediaType.APPLICATION_XML: return new ApplicationXMLEntity(ediMessage, charset, contentTransferEncoding, isMainBody, filename); default: - throw new CamelException("Invalid EDI entity mime type: " + ediMessageContentType.getMimeType()); + return new GenericApplicationEntity( + ediMessage, ediMessageContentType, contentTransferEncoding, + isMainBody, filename); } } diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java index d6a03c17dec9..7acc58adee41 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java @@ -75,7 +75,7 @@ public final class HttpMessageUtils { } else if (message instanceof BasicClassicHttpResponse httpResponse) { HttpEntity entity = httpResponse.getEntity(); if (entity != null && type.isInstance(entity)) { - type.cast(entity); + return type.cast(entity); } } return null; @@ -152,10 +152,23 @@ public final class HttpMessageUtils { } break; } - default: - throw new HttpException( - "Failed to extract EDI message: invalid content type '" + contentType.getMimeType() - + "' for AS2 request message"); + default: { + // Accept non-standard content types (e.g., text/plain, application/octet-stream) + // that real-world AS2 partners may use to wrap EDI payloads + if (decrpytingAndSigningInfo.getValidateSigningCertificateChain() != null) { + throw new AS2InsufficientSecurityException("Failed to validate the signature"); + } + if (decrpytingAndSigningInfo.getDecryptingPrivateKey() != null) { + throw new AS2InsufficientSecurityException("Expected to be encrypted"); + } + ediEntity = getEntity(message, ApplicationEntity.class); + if (ediEntity == null) { + throw new HttpException( + "Failed to extract EDI message: could not parse entity with content type '" + + contentType.getMimeType() + "'"); + } + break; + } } return ediEntity; @@ -274,10 +287,20 @@ public final class HttpMessageUtils { ediEntity = extractEdiPayloadFromCompressedEntity(compressedDataEntity, decrpytingAndSigningInfo, false); break; } - default: - throw new HttpException( - "Failed to extract EDI payload: invalid content type '" + contentType.getMimeType() - + "' for AS2 enveloped entity"); + default: { + // Accept non-standard content types within enveloped entities + if (decrpytingAndSigningInfo.getValidateSigningCertificateChain() != null) { + throw new AS2InsufficientSecurityException("Failed to validate the signature"); + } + if (entity instanceof ApplicationEntity) { + ediEntity = (ApplicationEntity) entity; + } else { + throw new HttpException( + "Failed to extract EDI payload: invalid content type '" + contentType.getMimeType() + + "' for AS2 enveloped entity"); + } + break; + } } return ediEntity; @@ -325,10 +348,20 @@ public final class HttpMessageUtils { } break; } - default: - throw new HttpException( - "Failed to extract EDI payload: invalid content type '" + contentType.getMimeType() - + "' for AS2 compressed entity"); + default: { + // Accept non-standard content types within compressed entities + if (!hasValidSignature && decrpytingAndSigningInfo.getValidateSigningCertificateChain() != null) { + throw new AS2InsufficientSecurityException("Failed to validate the signature"); + } + if (entity instanceof ApplicationEntity) { + ediEntity = (ApplicationEntity) entity; + } else { + throw new HttpException( + "Failed to extract EDI payload: invalid content type '" + contentType.getMimeType() + + "' for AS2 compressed entity"); + } + break; + } } return ediEntity; diff --git a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/entity/GenericApplicationEntityTest.java b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/entity/GenericApplicationEntityTest.java new file mode 100644 index 000000000000..ab00bfbe705b --- /dev/null +++ b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/entity/GenericApplicationEntityTest.java @@ -0,0 +1,100 @@ +/* + * 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.entity; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.Security; + +import org.apache.camel.component.as2.api.AS2Header; +import org.apache.camel.component.as2.api.AS2TransferEncoding; +import org.apache.camel.component.as2.api.util.EntityUtils; +import org.apache.camel.component.as2.api.util.HttpMessageUtils; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.BasicHttpEntity; +import org.apache.hc.core5.http.message.BasicClassicHttpRequest; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class GenericApplicationEntityTest { + + private static final String EDI_MESSAGE = "UNB+UNOA:1+005435656:1+006415160:1+060515:1434+00000000000778'"; + + @BeforeAll + static void setUp() { + Security.addProvider(new BouncyCastleProvider()); + } + + @Test + void genericApplicationEntityPreservesContent() throws Exception { + byte[] content = EDI_MESSAGE.getBytes(StandardCharsets.US_ASCII); + ContentType contentType = ContentType.create("text/plain", StandardCharsets.US_ASCII); + GenericApplicationEntity entity = new GenericApplicationEntity(content, contentType, null, true, null); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + entity.writeTo(out); + assertArrayEquals(content, out.toByteArray()); + } + + @Test + void genericApplicationEntityGetEdiMessage() { + byte[] content = EDI_MESSAGE.getBytes(StandardCharsets.US_ASCII); + ContentType contentType = ContentType.create("text/plain", StandardCharsets.US_ASCII); + GenericApplicationEntity entity = new GenericApplicationEntity(content, contentType, null, true, null); + + Object ediMessage = entity.getEdiMessage(); + assertInstanceOf(String.class, ediMessage); + assertEquals(EDI_MESSAGE, ediMessage); + } + + @ParameterizedTest + @ValueSource(strings = { "text/plain", "application/octet-stream" }) + void extractEdiPayloadAcceptsNonStandardContentTypes(String mimeType) throws Exception { + byte[] content = EDI_MESSAGE.getBytes(StandardCharsets.US_ASCII); + ContentType contentType = ContentType.create(mimeType, StandardCharsets.US_ASCII); + + BasicClassicHttpRequest request = new BasicClassicHttpRequest("POST", "/"); + request.addHeader(AS2Header.CONTENT_TYPE, contentType.toString()); + InputStream is = new ByteArrayInputStream(content); + request.setEntity(new BasicHttpEntity(is, content.length, contentType)); + + ApplicationEntity ediEntity = HttpMessageUtils.extractEdiPayload(request, + new HttpMessageUtils.DecrpytingAndSigningInfo(null, null)); + + assertNotNull(ediEntity, "EDI entity should not be null for content type: " + mimeType); + assertInstanceOf(GenericApplicationEntity.class, ediEntity); + assertEquals(EDI_MESSAGE, ediEntity.getEdiMessage().toString()); + } + + @Test + void createEDIEntityReturnsGenericForUnknownType() throws Exception { + byte[] content = EDI_MESSAGE.getBytes(StandardCharsets.US_ASCII); + ContentType contentType = ContentType.create("text/plain", StandardCharsets.US_ASCII); + + ApplicationEntity entity = EntityUtils.createEDIEntity(content, contentType, + AS2TransferEncoding.NONE, false, "test.txt"); + + assertInstanceOf(GenericApplicationEntity.class, entity); + } +}
