You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2022/12/23 11:57:14 UTC

[camel] 01/02: [CAMEL-15111] Fixes bug in camel-as2 via not forcing ascii charset when not specified (#8944)

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

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

commit fb8eb8d9a1e8711ba2f0f78557ad95db6d763567
Author: Yasser Zamani <ya...@apache.org>
AuthorDate: Fri Dec 23 14:53:00 2022 +0330

    [CAMEL-15111] Fixes bug in camel-as2 via not forcing ascii charset when not specified (#8944)
    
    * add CAMEL-15111 reproducer test case
    
    * fix CAMEL-15111 via not forcing ascii charset when not specified
    
    otherwise you will get java.nio.charset.MalformedInputException: Input length = 1 when content transfer encoding is binary
    
    * fix maven checkstyle errors
    
    * maven formatter:format
    
    * do not use a static version inside the component pom
    
    this should be in a property placeholder in parent pom
---
 components/camel-as2/camel-as2-api/pom.xml         |  7 ++
 .../component/as2/api/entity/EntityParser.java     | 87 +++++++++++-----------
 .../as2/api/io/AS2SessionInputBuffer.java          |  9 +++
 .../camel/component/as2/api/AS2MessageTest.java    | 77 +++++++++++++++++++
 components/camel-as2/pom.xml                       |  4 +
 5 files changed, 141 insertions(+), 43 deletions(-)

diff --git a/components/camel-as2/camel-as2-api/pom.xml b/components/camel-as2/camel-as2-api/pom.xml
index 712fd8c6c09..28e2c325029 100644
--- a/components/camel-as2/camel-as2-api/pom.xml
+++ b/components/camel-as2/camel-as2-api/pom.xml
@@ -96,6 +96,13 @@
             <artifactId>camel-test-junit5</artifactId>
             <scope>test</scope>
         </dependency>
+        <!-- added for binary content transfer encoding test because Camel AS2 client doesn't support binary currently -->
+        <dependency>
+            <groupId>com.helger.as2</groupId>
+            <artifactId>as2-lib</artifactId>
+            <version>${as2-lib-version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
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 85bede10230..01b4c7398cc 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
@@ -29,6 +29,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 
+import org.apache.camel.CamelException;
 import org.apache.camel.component.as2.api.AS2Header;
 import org.apache.camel.component.as2.api.AS2MimeType;
 import org.apache.camel.component.as2.api.io.AS2SessionInputBuffer;
@@ -37,6 +38,7 @@ import org.apache.camel.component.as2.api.util.ContentTypeUtils;
 import org.apache.camel.component.as2.api.util.DispositionNotificationContentUtils;
 import org.apache.camel.component.as2.api.util.EntityUtils;
 import org.apache.camel.component.as2.api.util.HttpMessageUtils;
+import org.apache.commons.codec.DecoderException;
 import org.apache.http.Header;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpException;
@@ -50,6 +52,7 @@ import org.apache.http.message.BasicLineParser;
 import org.apache.http.message.BasicNameValuePair;
 import org.apache.http.message.LineParser;
 import org.apache.http.message.ParserCursor;
+import org.apache.http.protocol.HTTP;
 import org.apache.http.util.Args;
 import org.apache.http.util.CharArrayBuffer;
 import org.bouncycastle.cms.CMSCompressedData;
@@ -907,22 +910,14 @@ public final class EntityParser {
         CharsetDecoder previousDecoder = inbuffer.getCharsetDecoder();
 
         try {
+            byte[] signature = parseBodyPartBytes(inbuffer, boundary, contentType, contentTransferEncoding);
+
             Charset charset = contentType.getCharset();
             if (charset == null) {
                 charset = StandardCharsets.US_ASCII;
             }
-            CharsetDecoder charsetDecoder = charset.newDecoder();
-
-            inbuffer.setCharsetDecoder(charsetDecoder);
-
-            String pkcs7SignatureBodyContent = parseBodyPartText(inbuffer, boundary);
-
-            byte[] signature = EntityUtils.decode(pkcs7SignatureBodyContent.getBytes(charset), contentTransferEncoding);
-
             String charsetName = charset.toString();
-            ApplicationPkcs7SignatureEntity applicationPkcs7SignatureEntity = new ApplicationPkcs7SignatureEntity(
-                    signature, charsetName, contentTransferEncoding, false);
-            return applicationPkcs7SignatureEntity;
+            return new ApplicationPkcs7SignatureEntity(signature, charsetName, contentTransferEncoding, false);
         } catch (Exception e) {
             ParseException parseException = new ParseException("failed to parse PKCS7 Signature entity");
             parseException.initCause(e);
@@ -942,21 +937,8 @@ public final class EntityParser {
         CharsetDecoder previousDecoder = inbuffer.getCharsetDecoder();
 
         try {
-            Charset charset = contentType.getCharset();
-            if (charset == null) {
-                charset = StandardCharsets.US_ASCII;
-            }
-            CharsetDecoder charsetDecoder = charset.newDecoder();
-
-            inbuffer.setCharsetDecoder(charsetDecoder);
-
-            String pkcs7EncryptedBodyContent = parseBodyPartText(inbuffer, boundary);
-
-            byte[] encryptedContent = EntityUtils.decode(pkcs7EncryptedBodyContent.getBytes(charset), contentTransferEncoding);
-
-            ApplicationPkcs7MimeEnvelopedDataEntity applicationPkcs7MimeEntity = new ApplicationPkcs7MimeEnvelopedDataEntity(
-                    encryptedContent, contentTransferEncoding, false);
-            return applicationPkcs7MimeEntity;
+            byte[] encryptedContent = parseBodyPartBytes(inbuffer, boundary, contentType, contentTransferEncoding);
+            return new ApplicationPkcs7MimeEnvelopedDataEntity(encryptedContent, contentTransferEncoding, false);
         } catch (Exception e) {
             ParseException parseException = new ParseException("failed to parse PKCS7 Mime entity");
             parseException.initCause(e);
@@ -976,22 +958,8 @@ public final class EntityParser {
         CharsetDecoder previousDecoder = inbuffer.getCharsetDecoder();
 
         try {
-            Charset charset = contentType.getCharset();
-            if (charset == null) {
-                charset = StandardCharsets.US_ASCII;
-            }
-            CharsetDecoder charsetDecoder = charset.newDecoder();
-
-            inbuffer.setCharsetDecoder(charsetDecoder);
-
-            String pkcs7CompressedBodyContent = parseBodyPartText(inbuffer, boundary);
-
-            byte[] compressedContent = EntityUtils.decode(pkcs7CompressedBodyContent.getBytes(charset),
-                    contentTransferEncoding);
-
-            ApplicationPkcs7MimeCompressedDataEntity applicationPkcs7MimeEntity = new ApplicationPkcs7MimeCompressedDataEntity(
-                    compressedContent, contentTransferEncoding, false);
-            return applicationPkcs7MimeEntity;
+            byte[] compressedContent = parseBodyPartBytes(inbuffer, boundary, contentType, contentTransferEncoding);
+            return new ApplicationPkcs7MimeCompressedDataEntity(compressedContent, contentTransferEncoding, false);
         } catch (Exception e) {
             ParseException parseException = new ParseException("failed to parse PKCS7 Mime entity");
             parseException.initCause(e);
@@ -1001,6 +969,36 @@ public final class EntityParser {
         }
     }
 
+    public static byte[] parseBodyPartBytes(
+            final AS2SessionInputBuffer inbuffer,
+            final String boundary,
+            ContentType contentType,
+            String contentTransferEncoding)
+            throws IOException, CamelException, DecoderException {
+
+        Charset charset = contentType.getCharset();
+        if (charset != null) {
+            CharsetDecoder charsetDecoder = charset.newDecoder();
+            inbuffer.setCharsetDecoder(charsetDecoder);
+        } else {
+            inbuffer.setCharsetDecoder(null);
+        }
+
+        String bodyContent = parseBodyPartText(inbuffer, boundary);
+
+        byte[] bodyContentBytes;
+        if (charset != null) {
+            bodyContentBytes = bodyContent.getBytes(charset);
+        } else {
+            bodyContentBytes = new byte[bodyContent.length()];
+            for (int i = 0; i < bodyContent.length(); i++) {
+                bodyContentBytes[i] = (byte) bodyContent.charAt(i);
+            }
+        }
+
+        return EntityUtils.decode(bodyContentBytes, contentTransferEncoding);
+    }
+
     public static String parseBodyPartText(
             final AS2SessionInputBuffer inbuffer,
             final String boundary)
@@ -1021,8 +1019,11 @@ public final class EntityParser {
             }
 
             buffer.append(line);
+            if (inbuffer.isLastLineReadEnrichedByCarriageReturn()) {
+                buffer.append((char) HTTP.CR);
+            }
             if (inbuffer.isLastLineReadTerminatedByLineFeed()) {
-                buffer.append("\r\n");
+                buffer.append((char) HTTP.LF);
             }
             line.clear();
         }
diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/io/AS2SessionInputBuffer.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/io/AS2SessionInputBuffer.java
index d4fad28fdf5..2936cf0b158 100644
--- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/io/AS2SessionInputBuffer.java
+++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/io/AS2SessionInputBuffer.java
@@ -52,6 +52,7 @@ public class AS2SessionInputBuffer implements SessionInputBuffer, BufferInfo {
     private int bufferlen;
     private CharBuffer cbuf;
 
+    private boolean lastLineReadEnrichedByCarriageReturn;
     private boolean lastLineReadTerminatedByLineFeed;
 
     public AS2SessionInputBuffer(final HttpTransportMetricsImpl metrics,
@@ -190,6 +191,7 @@ public class AS2SessionInputBuffer implements SessionInputBuffer, BufferInfo {
         final int maxLineLen = this.constraints.getMaxLineLength();
         int noRead = 0;
         boolean retry = true;
+        this.lastLineReadEnrichedByCarriageReturn = false;
         this.lastLineReadTerminatedByLineFeed = false;
         while (retry) {
             // attempt to find end of line (LF)
@@ -198,6 +200,9 @@ public class AS2SessionInputBuffer implements SessionInputBuffer, BufferInfo {
                 if (this.buffer[i] == HTTP.LF) {
                     pos = i;
                     this.lastLineReadTerminatedByLineFeed = true;
+                    if (i > 0 && this.buffer[i - 1] == HTTP.CR) {
+                        this.lastLineReadEnrichedByCarriageReturn = true;
+                    }
                     break;
                 }
             }
@@ -252,6 +257,10 @@ public class AS2SessionInputBuffer implements SessionInputBuffer, BufferInfo {
         return lastLineReadTerminatedByLineFeed;
     }
 
+    public boolean isLastLineReadEnrichedByCarriageReturn() {
+        return lastLineReadEnrichedByCarriageReturn;
+    }
+
     @Override
     public boolean isDataAvailable(int timeout) throws IOException {
         return hasBufferedData();
diff --git a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2MessageTest.java b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2MessageTest.java
index 696c151d28f..022c9f04b1e 100644
--- a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2MessageTest.java
+++ b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2MessageTest.java
@@ -16,16 +16,28 @@
  */
 package org.apache.camel.component.as2.api;
 
+import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
+import java.security.KeyStore;
 import java.security.SecureRandom;
 import java.security.Security;
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
 import java.util.*;
 
+import com.helger.as2lib.client.AS2Client;
+import com.helger.as2lib.client.AS2ClientRequest;
+import com.helger.as2lib.client.AS2ClientResponse;
+import com.helger.as2lib.client.AS2ClientSettings;
+import com.helger.as2lib.crypto.ECompressionType;
+import com.helger.as2lib.crypto.ECryptoAlgorithmCrypt;
+import com.helger.as2lib.crypto.ECryptoAlgorithmSign;
+import com.helger.mail.cte.EContentTransferEncoding;
+import com.helger.security.keystore.EKeyStoreType;
 import org.apache.camel.component.as2.api.entity.AS2DispositionModifier;
 import org.apache.camel.component.as2.api.entity.AS2DispositionType;
 import org.apache.camel.component.as2.api.entity.AS2MessageDispositionNotificationEntity;
@@ -76,6 +88,8 @@ import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -84,6 +98,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
 
 public class AS2MessageTest {
 
@@ -145,6 +160,10 @@ public class AS2MessageTest {
     private static X509Certificate signingCert;
     private static List<X509Certificate> certList;
 
+    private static File keystoreFile;
+
+    private static ApplicationEDIEntity ediEntity;
+
     private AS2SignedDataGenerator gen;
 
     @BeforeAll
@@ -169,6 +188,17 @@ public class AS2MessageTest {
         signingKP = kpg.generateKeyPair();
         signingCert = Utils.makeCertificate(signingKP, signingDN, issueKP, issueDN);
 
+        //
+        // initialize as2-lib keystore file
+        //
+        KeyStore ks = KeyStore.getInstance(EKeyStoreType.PKCS12.getID());
+        ks.load(null, "test".toCharArray());
+        ks.setKeyEntry("openas2a_alias", issueKP.getPrivate(), "test".toCharArray(), new X509Certificate[] { issueCert });
+        ks.setKeyEntry("openas2b_alias", signingKP.getPrivate(), "test".toCharArray(), new X509Certificate[] { signingCert });
+        keystoreFile = File.createTempFile("camel-as2", "keystore-p12");
+        keystoreFile.deleteOnExit();
+        ks.store(new FileOutputStream(keystoreFile), "test".toCharArray());
+
         certList = new ArrayList<>();
 
         certList.add(signingCert);
@@ -188,6 +218,7 @@ public class AS2MessageTest {
                     context.setAttribute(AS2ServerManager.SUBJECT, SUBJECT);
                     context.setAttribute(AS2ServerManager.FROM, AS2_NAME);
                     LOG.debug(AS2Utils.printMessage(request));
+                    ediEntity = HttpMessageUtils.extractEdiPayload(request, testServer.getDecryptingPrivateKey());
                 } catch (Exception e) {
                     throw new HttpException("Failed to parse AS2 Message Entity", e);
                 }
@@ -282,6 +313,52 @@ public class AS2MessageTest {
         assertTrue(ediEntity.isMainBody(), "Entity not set as main body of request");
     }
 
+    @ParameterizedTest
+    @CsvSource({
+            "false,false,false", "false,false,true", "false,true,false", "false,true,true", "true,false,false",
+            "true,false,true", "true,true,false", "true,true,true" })
+    void binaryContentTransferEncodingTest(boolean encrypt, boolean sign, boolean compress) {
+        // test with as2-lib because Camel AS2 client doesn't support binary content transfer encoding at the moment
+        // inspired from https://github.com/phax/as2-lib/wiki/Submodule-as2%E2%80%90lib#as2-client
+
+        // Start client configuration
+        final AS2ClientSettings aSettings = new AS2ClientSettings();
+        aSettings.setKeyStore(EKeyStoreType.PKCS12, keystoreFile, "test");
+
+        // Fixed sender
+        aSettings.setSenderData(AS2_NAME, FROM, "openas2a_alias");
+
+        // Fixed receiver
+        aSettings.setReceiverData(AS2_NAME, "openas2b_alias", "http://" + TARGET_HOST + ":" + TARGET_PORT + "/");
+        aSettings.setReceiverCertificate(issueCert);
+
+        // AS2 stuff
+        aSettings.setPartnershipName(aSettings.getSenderAS2ID() + "_" + aSettings.getReceiverAS2ID());
+
+        // Build client request
+        final AS2ClientRequest aRequest = new AS2ClientRequest("AS2 test message from as2-lib");
+        aRequest.setData(EDI_MESSAGE, StandardCharsets.US_ASCII);
+        aRequest.setContentType(AS2MediaType.APPLICATION_EDIFACT);
+
+        // reproduce https://issues.apache.org/jira/projects/CAMEL/issues/CAMEL-15111
+        aSettings.setEncryptAndSign(encrypt ? ECryptoAlgorithmCrypt.CRYPT_AES128_GCM : null,
+                sign ? ECryptoAlgorithmSign.DIGEST_SHA_512 : null);
+        if (compress) {
+            aSettings.setCompress(ECompressionType.ZLIB, false);
+        }
+        aRequest.setContentTransferEncoding(EContentTransferEncoding.BINARY);
+
+        // Send message
+        ediEntity = null;
+        final AS2ClientResponse aResponse = new AS2Client().sendSynchronous(aSettings, aRequest);
+
+        // Assertions
+        if (aResponse.hasException()) {
+            fail(aResponse.getException());
+        }
+        assertEquals(EDI_MESSAGE, ediEntity.getEdiMessage().replaceAll("\r", ""));
+    }
+
     @Test
     public void multipartSignedMessageTest() throws Exception {
         AS2ClientConnection clientConnection = new AS2ClientConnection(
diff --git a/components/camel-as2/pom.xml b/components/camel-as2/pom.xml
index 5bc35d5345b..9d6a1770b22 100644
--- a/components/camel-as2/pom.xml
+++ b/components/camel-as2/pom.xml
@@ -29,6 +29,10 @@
     <artifactId>camel-as2-parent</artifactId>
     <packaging>pom</packaging>
 
+    <properties>
+        <as2-lib-version>4.11.0</as2-lib-version>
+    </properties>
+
     <name>Camel :: AS2 :: Parent</name>
     <description>Camel AS2 parent</description>