Skip to content

Commit fd4a146

Browse files
committed
Added possibility to explicitly access compressed streams; #361
1 parent 3ac5900 commit fd4a146

9 files changed

Lines changed: 264 additions & 11 deletions

File tree

phase4-lib/src/main/java/com/helger/phase4/attachment/IAS4Attachment.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,27 @@ public interface IAS4Attachment
8888
@Nullable
8989
IHasInputStream getInputStreamProvider ();
9090

91+
/**
92+
* Get the input stream provider for the attachment data in compressed form - that is the form
93+
* over which the digital signature digests are calculated. For incoming messages this is the data
94+
* as it was received (after eventual decryption), before the decompression took place. For
95+
* outgoing messages this is the data as it is transmitted (before eventual encryption). This is
96+
* primarily meant to preserve the compressed payload data for non-repudiation purposes, because
97+
* e.g. GZIP compression is not deterministic, and the compressed data cannot be re-created from
98+
* the decompressed data. The returned provider is backed by resources (usually temporary files)
99+
* that are deleted when the message processing is finished - so the data must be consumed while
100+
* the message is processed.
101+
*
102+
* @return The input stream provider for the compressed attachment data. May be <code>null</code>
103+
* if the attachment is not compressed.
104+
* @since 4.5.4
105+
*/
106+
@Nullable
107+
default IHasInputStream getCompressedSourceStreamProvider ()
108+
{
109+
return null;
110+
}
111+
91112
/**
92113
* @return <code>true</code> if the input stream backing this attachment can be read multiple
93114
* times, <code>false</code> if not.

phase4-lib/src/main/java/com/helger/phase4/attachment/WSS4JAttachment.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public class WSS4JAttachment extends Attachment implements IAS4Attachment
8080

8181
private final AS4ResourceHelper m_aResHelper;
8282
private IHasInputStream m_aISP;
83+
private IHasInputStream m_aCompressedISP;
8384
private EContentTransferEncoding m_eCTE = EContentTransferEncoding.BINARY;
8485
private EAS4CompressionMode m_eCompressionMode;
8586
private Charset m_aCharset;
@@ -186,6 +187,26 @@ public void setSourceStreamProvider (@NonNull final IHasInputStream aISP)
186187
m_aISP = aISP;
187188
}
188189

190+
@Override
191+
@Nullable
192+
public IHasInputStream getCompressedSourceStreamProvider ()
193+
{
194+
return m_aCompressedISP;
195+
}
196+
197+
/**
198+
* Set the input stream provider for the attachment data in compressed form. See
199+
* {@link #getCompressedSourceStreamProvider()} for the semantics.
200+
*
201+
* @param aISP
202+
* The input stream provider to use. May be <code>null</code>.
203+
* @since 4.5.4
204+
*/
205+
public void setCompressedSourceStreamProvider (@Nullable final IHasInputStream aISP)
206+
{
207+
m_aCompressedISP = aISP;
208+
}
209+
189210
@NonNull
190211
public final EContentTransferEncoding getContentTransferEncoding ()
191212
{
@@ -305,6 +326,7 @@ public String toString ()
305326
.append ("Headers", getHeaders ())
306327
.append ("ResourceManager", m_aResHelper)
307328
.append ("ISP", m_aISP)
329+
.append ("CompressedISP", m_aCompressedISP)
308330
.append ("CTE", m_eCTE)
309331
.append ("CM", m_eCompressionMode)
310332
.append ("Charset", m_aCharset)
@@ -433,7 +455,14 @@ public static WSS4JAttachment createOutgoingFileAttachment (@NonNull final File
433455

434456
// Set a stream provider that can be read multiple times (opens a new
435457
// FileInputStream internally)
436-
ret.setSourceStreamProvider (HasInputStream.multiple ( () -> FileHelper.getBufferedInputStream (aRealFile)));
458+
final IHasInputStream aISP = HasInputStream.multiple ( () -> FileHelper.getBufferedInputStream (aRealFile));
459+
ret.setSourceStreamProvider (aISP);
460+
if (eCompressionMode != null)
461+
{
462+
// Preserve the compressed data for non-repudiation purposes - the
463+
// signature digests are calculated over the compressed data
464+
ret.setCompressedSourceStreamProvider (aISP);
465+
}
437466
return ret;
438467
}
439468

@@ -494,7 +523,11 @@ public static WSS4JAttachment createOutgoingFileAttachment (final byte @NonNull
494523
aOS.write (aSrcData);
495524
}
496525
}
497-
ret.setSourceStreamProvider (HasInputStream.multiple ( () -> FileHelper.getBufferedInputStream (aRealFile)));
526+
final IHasInputStream aISP = HasInputStream.multiple ( () -> FileHelper.getBufferedInputStream (aRealFile));
527+
ret.setSourceStreamProvider (aISP);
528+
// Preserve the compressed data for non-repudiation purposes - the
529+
// signature digests are calculated over the compressed data
530+
ret.setCompressedSourceStreamProvider (aISP);
498531
}
499532
else
500533
{

phase4-lib/src/main/java/com/helger/phase4/client/AS4ClientUserMessage.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,8 @@ public AS4ClientBuiltMessage buildMessage (@NonNull @Nonempty final String sMess
716716

717717
// 1. compress
718718
// Is done when the attachments are added
719+
if (aCallback != null && bAttachmentsPresent)
720+
aCallback.onBuiltAttachments (m_aAttachments);
719721

720722
// 2. sign and/or encrypt
721723
Document aResultSoapDoc = aPureSoapDoc;

phase4-lib/src/main/java/com/helger/phase4/client/IAS4ClientBuildMessageCallback.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import org.jspecify.annotations.NonNull;
2020
import org.w3c.dom.Document;
2121

22+
import com.helger.annotation.Nonempty;
23+
import com.helger.collection.commons.ICommonsList;
24+
import com.helger.phase4.attachment.WSS4JAttachment;
2225
import com.helger.phase4.messaging.mime.AS4MimeMessage;
2326
import com.helger.phase4.model.message.AbstractAS4Message;
2427

@@ -39,6 +42,24 @@ public interface IAS4ClientBuildMessageCallback
3942
default void onAS4Message (@NonNull final AbstractAS4Message <?> aMsg)
4043
{}
4144

45+
/**
46+
* Called for all attachments of a User Message, after eventual compression was applied, but
47+
* before signing and encryption took place. This is the way to access the compressed attachment
48+
* data over which the digital signature digests are calculated - e.g. to preserve it for
49+
* non-repudiation purposes, because GZIP compression is not deterministic, and the compressed
50+
* data cannot be re-created from the uncompressed data. Use
51+
* {@link com.helger.phase4.attachment.IAS4Attachment#getCompressedSourceStreamProvider()} to
52+
* access the compressed data of compressed attachments. The attachment data is backed by
53+
* temporary files that are deleted when the sending process is finished, so the data must be
54+
* consumed inside this method. Only called for User Messages with at least one attachment.
55+
*
56+
* @param aAttachments
57+
* The outgoing attachments. Never <code>null</code> nor empty. Must not be modified.
58+
* @since 4.5.4
59+
*/
60+
default void onBuiltAttachments (@NonNull @Nonempty final ICommonsList <WSS4JAttachment> aAttachments)
61+
{}
62+
4263
/**
4364
* Called for the unsigned and unencrypted SOAP document. This method is called for all types.
4465
*

phase4-lib/src/main/java/com/helger/phase4/incoming/AS4IncomingHandler.java

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package com.helger.phase4.incoming;
1818

19+
import java.io.File;
1920
import java.io.IOException;
2021
import java.io.InputStream;
2122
import java.io.OutputStream;
@@ -43,6 +44,7 @@
4344
import com.helger.base.io.iface.IHasInputStream;
4445
import com.helger.base.io.nonblocking.NonBlockingByteArrayInputStream;
4546
import com.helger.base.io.stream.HasInputStream;
47+
import com.helger.base.io.stream.StreamHelper;
4648
import com.helger.base.spi.ServiceLoaderHelper;
4749
import com.helger.base.state.ESuccess;
4850
import com.helger.base.string.StringHelper;
@@ -56,6 +58,7 @@
5658
import com.helger.http.CHttp;
5759
import com.helger.http.CHttpHeader;
5860
import com.helger.http.header.HttpHeaderMap;
61+
import com.helger.io.file.FileHelper;
5962
import com.helger.mime.IMimeType;
6063
import com.helger.mime.parse.MimeTypeParser;
6164
import com.helger.phase4.attachment.AS4DecompressException;
@@ -577,6 +580,47 @@ private static void _processSoapHeaderElements (@NonNull final SoapHeaderElement
577580
}
578581
}
579582

583+
/**
584+
* Create an input stream provider that can be read multiple times, by lazily copying the data of
585+
* the provided single-read input stream provider to a temporary file on first access.
586+
*
587+
* @param aResHelper
588+
* The resource helper to create the temporary file with. May not be <code>null</code>.
589+
* @param aSrcISP
590+
* The single-read source input stream provider. May not be <code>null</code>.
591+
* @return A non-<code>null</code> input stream provider that can be read multiple times.
592+
*/
593+
@NonNull
594+
private static IHasInputStream _createReadMultipleISP (@NonNull final AS4ResourceHelper aResHelper,
595+
@NonNull final IHasInputStream aSrcISP)
596+
{
597+
final Wrapper <File> aTempFileWrapper = new Wrapper <> ();
598+
return new HasInputStream ( () -> {
599+
try
600+
{
601+
File aTempFile = aTempFileWrapper.get ();
602+
if (aTempFile == null)
603+
{
604+
final InputStream aSrcIS = aSrcISP.getInputStream ();
605+
if (aSrcIS == null)
606+
throw new IllegalStateException ("Failed to create InputStream from " + aSrcISP);
607+
608+
aTempFile = aResHelper.createTempFile ();
609+
try (final OutputStream aOS = FileHelper.getBufferedOutputStream (aTempFile))
610+
{
611+
StreamHelper.copyInputStreamToOutputStream (aSrcIS, aOS);
612+
}
613+
aTempFileWrapper.set (aTempFile);
614+
}
615+
return FileHelper.getBufferedInputStream (aTempFile);
616+
}
617+
catch (final IOException ex)
618+
{
619+
throw new AS4DecompressException (ex);
620+
}
621+
}, true);
622+
}
623+
580624
private static void _decompressAttachments (@NonNull final ICommonsList <WSS4JAttachment> aIncomingDecryptedAttachments,
581625
@NonNull final Ebms3UserMessage aUserMessage,
582626
@NonNull final IAS4IncomingMessageState aIncomingState)
@@ -587,7 +631,21 @@ private static void _decompressAttachments (@NonNull final ICommonsList <WSS4JAt
587631
final EAS4CompressionMode eCompressionMode = aIncomingState.getAttachmentCompressionMode (aIncomingAttachment.getId ());
588632
if (eCompressionMode != null)
589633
{
590-
final IHasInputStream aOldISP = aIncomingAttachment.getInputStreamProvider ();
634+
IHasInputStream aCompressedISP = aIncomingAttachment.getInputStreamProvider ();
635+
if (!aCompressedISP.isReadMultiple ())
636+
{
637+
// E.g. decrypted attachments can be read only once - make sure the
638+
// compressed data can be read multiple times: once for the
639+
// decompression and e.g. once for non-repudiation evidence storage
640+
aCompressedISP = _createReadMultipleISP (aIncomingAttachment.getResHelper (), aCompressedISP);
641+
}
642+
643+
// Preserve the compressed data - the signature digests are calculated
644+
// over the compressed data, and e.g. GZIP compression is not
645+
// reproducible (#361)
646+
aIncomingAttachment.setCompressedSourceStreamProvider (aCompressedISP);
647+
648+
final IHasInputStream aOldISP = aCompressedISP;
591649
aIncomingAttachment.setSourceStreamProvider (new HasInputStream ( () -> {
592650
try
593651
{

phase4-lib/src/test/java/com/helger/phase4/attachment/WSS4JAttachmentTest.java

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package com.helger.phase4.attachment;
1818

19+
import static org.junit.Assert.assertArrayEquals;
1920
import static org.junit.Assert.assertEquals;
2021
import static org.junit.Assert.assertNotEquals;
2122
import static org.junit.Assert.assertNotNull;
@@ -31,6 +32,7 @@
3132
import org.junit.Test;
3233
import org.junit.rules.TemporaryFolder;
3334

35+
import com.helger.base.io.iface.IHasInputStream;
3436
import com.helger.base.io.stream.StreamHelper;
3537
import com.helger.io.file.SimpleFileIO;
3638
import com.helger.mime.CMimeType;
@@ -83,7 +85,7 @@ public void testBasic ()
8385
@Test
8486
public void testOutgoingBytes () throws IOException
8587
{
86-
final byte [] aBytes = "<?xml version='1.0'?>".getBytes (StandardCharsets.ISO_8859_1);
88+
final byte [] aBytes = "<?xml version='1.0'?>".getBytes (StandardCharsets.UTF_8);
8789

8890
try (final AS4ResourceHelper aResHelper = new AS4ResourceHelper ())
8991
{
@@ -96,9 +98,12 @@ public void testOutgoingBytes () throws IOException
9698
assertEquals (CMimeType.APPLICATION_XML.getAsString (), a.getMimeType ());
9799
assertEquals (CMimeType.APPLICATION_XML.getAsString (), a.getUncompressedMimeType ());
98100

101+
// Not compressed - no compressed data present
102+
assertNull (a.getCompressedSourceStreamProvider ());
103+
99104
// Read content
100105
final byte [] aRead = StreamHelper.getAllBytes (a.getSourceStream ());
101-
assertEquals (new String (aBytes, StandardCharsets.ISO_8859_1), new String (aRead, StandardCharsets.ISO_8859_1));
106+
assertEquals (new String (aBytes, StandardCharsets.UTF_8), new String (aRead, StandardCharsets.UTF_8));
102107
}
103108
}
104109

@@ -107,7 +112,7 @@ public void testOutgoingFile () throws IOException
107112
{
108113
final File f = m_aRule.newFile ("test.xml");
109114
final String sXMLContent = "<?xml version='1.0'?>";
110-
SimpleFileIO.writeFile (f, sXMLContent.getBytes (StandardCharsets.ISO_8859_1));
115+
SimpleFileIO.writeFile (f, sXMLContent.getBytes (StandardCharsets.UTF_8));
111116

112117
try (final AS4ResourceHelper aResHelper = new AS4ResourceHelper ())
113118
{
@@ -122,16 +127,19 @@ public void testOutgoingFile () throws IOException
122127
assertEquals (CMimeType.APPLICATION_XML.getAsString (), a.getMimeType ());
123128
assertEquals (CMimeType.APPLICATION_XML.getAsString (), a.getUncompressedMimeType ());
124129

130+
// Not compressed - no compressed data present
131+
assertNull (a.getCompressedSourceStreamProvider ());
132+
125133
// Read content
126134
final byte [] aRead = StreamHelper.getAllBytes (a.getSourceStream ());
127-
assertEquals (sXMLContent, new String (aRead, StandardCharsets.ISO_8859_1));
135+
assertEquals (sXMLContent, new String (aRead, StandardCharsets.UTF_8));
128136
}
129137
}
130138

131139
@Test
132140
public void testOutgoingCompression () throws IOException
133141
{
134-
final byte [] aXmlBytes = "<?xml version='1.0'?>".getBytes (StandardCharsets.ISO_8859_1);
142+
final byte [] aXmlBytes = "<?xml version='1.0'?>".getBytes (StandardCharsets.UTF_8);
135143

136144
try (final AS4ResourceHelper aResHelper = new AS4ResourceHelper ())
137145
{
@@ -153,8 +161,19 @@ public void testOutgoingCompression () throws IOException
153161
assertTrue (aRead.length > 0);
154162

155163
// It is definitely not the XML
156-
assertNotEquals (new String (aRead, StandardCharsets.ISO_8859_1),
157-
new String (aXmlBytes, StandardCharsets.ISO_8859_1));
164+
assertNotEquals (new String (aRead, StandardCharsets.UTF_8), new String (aXmlBytes, StandardCharsets.UTF_8));
165+
166+
// The compressed data must be preserved (issue #361)
167+
final IHasInputStream aCompressedISP = a.getCompressedSourceStreamProvider ();
168+
assertNotNull (aCompressedISP);
169+
170+
// It must be identical to the source stream content
171+
assertArrayEquals (aRead, StreamHelper.getAllBytes (aCompressedISP.getInputStream ()));
172+
173+
// Decompressing the preserved compressed data must yield the original
174+
// payload
175+
final byte [] aDecompressed = StreamHelper.getAllBytes (EAS4CompressionMode.GZIP.getDecompressStream (aCompressedISP.getInputStream ()));
176+
assertArrayEquals (aXmlBytes, aDecompressed);
158177
}
159178
}
160179
}

0 commit comments

Comments
 (0)