From 0e494a0a644e1b503f8cce1d37923312c46e6621 Mon Sep 17 00:00:00 2001 From: Philip Helger Date: Sun, 1 Mar 2020 17:29:23 +0100 Subject: [PATCH] Added automatic resource cleansing; #103 --- .../helger/as2lib/crypto/BCCryptoHelper.java | 36 ++- .../helger/as2lib/crypto/ICryptoHelper.java | 9 +- .../receiver/net/AS2MDNReceiverHandler.java | 22 +- .../receiver/net/AS2ReceiverHandler.java | 17 +- .../processor/sender/AS2SenderModule.java | 21 +- .../com/helger/as2lib/util/AS2Helper.java | 11 +- .../helger/as2lib/util/AS2ResourceHelper.java | 253 ++++++++++++++++++ .../processor/sender/ReadMDNFuncTest.java | 18 +- 8 files changed, 336 insertions(+), 51 deletions(-) create mode 100644 as2-lib/src/main/java/com/helger/as2lib/util/AS2ResourceHelper.java diff --git a/as2-lib/src/main/java/com/helger/as2lib/crypto/BCCryptoHelper.java b/as2-lib/src/main/java/com/helger/as2lib/crypto/BCCryptoHelper.java index 39a1d71c..ebbacbe7 100644 --- a/as2-lib/src/main/java/com/helger/as2lib/crypto/BCCryptoHelper.java +++ b/as2-lib/src/main/java/com/helger/as2lib/crypto/BCCryptoHelper.java @@ -87,6 +87,7 @@ import org.bouncycastle.mail.smime.SMIMESignedGenerator; import org.bouncycastle.mail.smime.SMIMESignedParser; import org.bouncycastle.mail.smime.SMIMEUtil; +import org.bouncycastle.mail.smime.util.FileBackedMimeBodyPart; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.OutputEncryptor; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; @@ -96,6 +97,7 @@ import com.helger.as2lib.exception.AS2Exception; import com.helger.as2lib.util.AS2HttpHelper; import com.helger.as2lib.util.AS2IOHelper; +import com.helger.as2lib.util.AS2ResourceHelper; import com.helger.bc.PBCProvider; import com.helger.commons.ValueEnforcer; import com.helger.commons.annotation.Nonempty; @@ -395,15 +397,17 @@ private static void _dumpDecrypted (@Nonnull final byte [] aPayload) public MimeBodyPart decrypt (@Nonnull final MimeBodyPart aPart, @Nonnull final X509Certificate aX509Cert, @Nonnull final PrivateKey aPrivateKey, - final boolean bForceDecrypt) throws GeneralSecurityException, - MessagingException, - CMSException, - SMIMEException, - IOException + final boolean bForceDecrypt, + @Nonnull final AS2ResourceHelper aResHelper) throws GeneralSecurityException, + MessagingException, + CMSException, + SMIMEException, + IOException { ValueEnforcer.notNull (aPart, "MimeBodyPart"); ValueEnforcer.notNull (aX509Cert, "X509Cert"); ValueEnforcer.notNull (aPrivateKey, "PrivateKey"); + ValueEnforcer.notNull (aResHelper, "ResHelper"); if (LOGGER.isDebugEnabled ()) LOGGER.debug ("BCCryptoHelper.decrypt; X509 subject=" + @@ -436,7 +440,9 @@ public MimeBodyPart decrypt (@Nonnull final MimeBodyPart aPart, throw new GeneralSecurityException ("Certificate does not match part signature"); // try to decrypt the data - final MimeBodyPart aDecryptedDataBodyPart = SMIMEUtil.toMimeBodyPart (aRecipient.getContentStream (new JceKeyTransEnvelopedRecipient (aPrivateKey).setProvider (m_sSecurityProviderName))); + // Custom file: see #103 + final FileBackedMimeBodyPart aDecryptedDataBodyPart = SMIMEUtil.toMimeBodyPart (aRecipient.getContentStream (new JceKeyTransEnvelopedRecipient (aPrivateKey).setProvider (m_sSecurityProviderName)), + aResHelper.createTempFile ()); if (s_aDumpDecryptedDirectory != null) { @@ -624,11 +630,12 @@ public MimeBodyPart verify (@Nonnull final MimeBodyPart aPart, @Nullable final X509Certificate aX509Cert, final boolean bUseCertificateInBodyPart, final boolean bForceVerify, - @Nullable final Consumer aEffectiveCertificateConsumer) throws GeneralSecurityException, - IOException, - MessagingException, - CMSException, - OperatorCreationException + @Nullable final Consumer aEffectiveCertificateConsumer, + @Nonnull final AS2ResourceHelper aResHelper) throws GeneralSecurityException, + IOException, + MessagingException, + CMSException, + OperatorCreationException { if (LOGGER.isDebugEnabled ()) LOGGER.debug ("BCCryptoHelper.verify; X509 subject=" + @@ -649,11 +656,13 @@ public MimeBodyPart verify (@Nonnull final MimeBodyPart aPart, throw new IllegalStateException ("Expected Part content to be MimeMultipart but it isn't. It is " + ClassHelper.getClassName (aContent)); final MimeMultipart aMainPart = (MimeMultipart) aContent; + // SMIMESignedParser uses "7bit" as the default - AS2 wants "binary" final SMIMESignedParser aSignedParser = new SMIMESignedParser (new JcaDigestCalculatorProviderBuilder ().setProvider (m_sSecurityProviderName) .build (), aMainPart, - EContentTransferEncoding.AS2_DEFAULT.getID ()); + EContentTransferEncoding.AS2_DEFAULT.getID (), + aResHelper.createTempFile ()); final X509Certificate aRealX509Cert = _verifyFindCertificate (aX509Cert, bUseCertificateInBodyPart, aSignedParser); @@ -662,7 +671,8 @@ public MimeBodyPart verify (@Nonnull final MimeBodyPart aPart, aX509Cert) ? "Verifying signature using the provided certificate (partnership)" : "Verifying signature using the certificate contained in the MIME body part"); - // Call before validity check to retrieve the information about the details + // Call before validity check to retrieve the information about the + // details // outside if (aEffectiveCertificateConsumer != null) aEffectiveCertificateConsumer.accept (aRealX509Cert); diff --git a/as2-lib/src/main/java/com/helger/as2lib/crypto/ICryptoHelper.java b/as2-lib/src/main/java/com/helger/as2lib/crypto/ICryptoHelper.java index bdf23ea5..3e370ebc 100644 --- a/as2-lib/src/main/java/com/helger/as2lib/crypto/ICryptoHelper.java +++ b/as2-lib/src/main/java/com/helger/as2lib/crypto/ICryptoHelper.java @@ -45,6 +45,7 @@ import javax.mail.internet.MimeBodyPart; import com.helger.as2lib.exception.AS2Exception; +import com.helger.as2lib.util.AS2ResourceHelper; import com.helger.mail.cte.EContentTransferEncoding; import com.helger.security.keystore.IKeyStoreType; @@ -152,7 +153,8 @@ MimeBodyPart encrypt (@Nonnull MimeBodyPart aPart, MimeBodyPart decrypt (@Nonnull MimeBodyPart aPart, @Nonnull X509Certificate aCert, @Nonnull PrivateKey aKey, - boolean bForceDecrypt) throws Exception; + boolean bForceDecrypt, + @Nonnull AS2ResourceHelper aResHelper) throws Exception; /** * Sign a MIME body part. @@ -208,6 +210,8 @@ MimeBodyPart sign (@Nonnull MimeBodyPart aPart, * @param aEffectiveCertificateConsumer * An optional consumer that takes the effective certificate that was * used for verification. May be null. + * @param aResHelper + * The resource helper to use. May not be null. * @return The signed content. Never null. * @throws Exception * In case something goes wrong. @@ -218,5 +222,6 @@ MimeBodyPart verify (@Nonnull MimeBodyPart aPart, @Nullable X509Certificate aCert, boolean bUseCertificateInBodyPart, boolean bForceVerify, - @Nullable Consumer aEffectiveCertificateConsumer) throws Exception; + @Nullable Consumer aEffectiveCertificateConsumer, + @Nonnull AS2ResourceHelper aResHelper) throws Exception; } diff --git a/as2-lib/src/main/java/com/helger/as2lib/processor/receiver/net/AS2MDNReceiverHandler.java b/as2-lib/src/main/java/com/helger/as2lib/processor/receiver/net/AS2MDNReceiverHandler.java index 0af1c59a..b688ffd9 100644 --- a/as2-lib/src/main/java/com/helger/as2lib/processor/receiver/net/AS2MDNReceiverHandler.java +++ b/as2-lib/src/main/java/com/helger/as2lib/processor/receiver/net/AS2MDNReceiverHandler.java @@ -69,6 +69,7 @@ import com.helger.as2lib.util.AS2Helper; import com.helger.as2lib.util.AS2HttpHelper; import com.helger.as2lib.util.AS2IOHelper; +import com.helger.as2lib.util.AS2ResourceHelper; import com.helger.as2lib.util.dump.IHTTPIncomingDumper; import com.helger.as2lib.util.http.AS2HttpClient; import com.helger.as2lib.util.http.AS2HttpResponseHandlerSocket; @@ -153,7 +154,7 @@ public void handle (@Nonnull final AbstractActiveNetModule aOwner, @Nonnull fina byte [] aData = null; // Read in the message request, headers, and data - try + try (final AS2ResourceHelper aResHelper = new AS2ResourceHelper ()) { final IHTTPIncomingDumper aIncomingDumper = getEffectiveHttpIncomingDumper (); final DataSource aDataSourceBody = readAndDecodeHttpRequest (new AS2InputStreamProviderSocket (aSocket), @@ -180,7 +181,7 @@ public void handle (@Nonnull final AbstractActiveNetModule aOwner, @Nonnull fina aMsg.setData (aReceivedPart); - receiveMDN (aMsg, aData, aResponseHandler); + receiveMDN (aMsg, aData, aResponseHandler, aResHelper); } catch (final Exception ex) { @@ -199,6 +200,8 @@ public void handle (@Nonnull final AbstractActiveNetModule aOwner, @Nonnull fina * The MDN content * @param aResponseHandler * The HTTP response handler for setting the correct HTTP response code + * @param aResHelper + * Resource helper * @throws AS2Exception * In case of error * @throws IOException @@ -206,8 +209,8 @@ public void handle (@Nonnull final AbstractActiveNetModule aOwner, @Nonnull fina */ protected final void receiveMDN (@Nonnull final AS2Message aMsg, final byte [] aData, - @Nonnull final IAS2HttpResponseHandler aResponseHandler) throws AS2Exception, - IOException + @Nonnull final IAS2HttpResponseHandler aResponseHandler, + @Nonnull final AS2ResourceHelper aResHelper) throws AS2Exception, IOException { try { @@ -244,7 +247,11 @@ protected final void receiveMDN (@Nonnull final AS2Message aMsg, bUseCertificateInBodyPart = getModule ().getSession ().isCryptoVerifyUseCertificateInBodyPart (); } - AS2Helper.parseMDN (aMsg, aSenderCert, bUseCertificateInBodyPart, getVerificationCertificateConsumer ()); + AS2Helper.parseMDN (aMsg, + aSenderCert, + bUseCertificateInBodyPart, + getVerificationCertificateConsumer (), + aResHelper); // in order to name & save the mdn with the original AS2-From + AS2-To + // Message id., @@ -334,9 +341,8 @@ public boolean checkAsyncMDN (@Nonnull final AS2Message aMsg) throws AS2Exceptio final String sOriginalMIC; final MIC aOriginalMIC; final File aPendingFile; - try ( - final NonBlockingBufferedReader aPendingInfoReader = FileHelper.getBufferedReader (new File (sPendingInfoFile), - StandardCharsets.ISO_8859_1)) + try (final NonBlockingBufferedReader aPendingInfoReader = FileHelper.getBufferedReader (new File (sPendingInfoFile), + StandardCharsets.ISO_8859_1)) { // Get the original mic from the first line of pending information // file diff --git a/as2-lib/src/main/java/com/helger/as2lib/processor/receiver/net/AS2ReceiverHandler.java b/as2-lib/src/main/java/com/helger/as2lib/processor/receiver/net/AS2ReceiverHandler.java index 789beca9..e6999536 100644 --- a/as2-lib/src/main/java/com/helger/as2lib/processor/receiver/net/AS2ReceiverHandler.java +++ b/as2-lib/src/main/java/com/helger/as2lib/processor/receiver/net/AS2ReceiverHandler.java @@ -76,6 +76,7 @@ import com.helger.as2lib.util.AS2Helper; import com.helger.as2lib.util.AS2HttpHelper; import com.helger.as2lib.util.AS2IOHelper; +import com.helger.as2lib.util.AS2ResourceHelper; import com.helger.as2lib.util.dump.IHTTPIncomingDumper; import com.helger.as2lib.util.http.AS2HttpResponseHandlerSocket; import com.helger.as2lib.util.http.AS2InputStreamProviderSocket; @@ -208,7 +209,7 @@ protected AS2Message createMessage (@Nonnull final Socket aSocket) return aMsg; } - protected void decrypt (@Nonnull final IMessage aMsg) throws AS2Exception + protected void decrypt (@Nonnull final IMessage aMsg, @Nonnull final AS2ResourceHelper aResHelper) throws AS2Exception { final ICertificateFactory aCertFactory = m_aReceiverModule.getSession ().getCertificateFactory (); final ICryptoHelper aCryptoHelper = AS2Helper.getCryptoHelper (); @@ -242,7 +243,8 @@ protected void decrypt (@Nonnull final IMessage aMsg) throws AS2Exception final MimeBodyPart aDecryptedData = aCryptoHelper.decrypt (aMsg.getData (), aReceiverCert, aReceiverKey, - bForceDecrypt); + bForceDecrypt, + aResHelper); aMsg.setData (aDecryptedData); // Remember that message was encrypted aMsg.attrs ().putIn (AS2Message.ATTRIBUTE_RECEIVED_ENCRYPTED, true); @@ -262,7 +264,7 @@ protected void decrypt (@Nonnull final IMessage aMsg) throws AS2Exception } } - protected void verify (@Nonnull final IMessage aMsg) throws AS2Exception + protected void verify (@Nonnull final IMessage aMsg, @Nonnull final AS2ResourceHelper aResHelper) throws AS2Exception { final ICertificateFactory aCertFactory = m_aReceiverModule.getSession ().getCertificateFactory (); final ICryptoHelper aCryptoHelper = AS2Helper.getCryptoHelper (); @@ -309,7 +311,8 @@ protected void verify (@Nonnull final IMessage aMsg) throws AS2Exception aSenderCert, bUseCertificateInBodyPart, bForceVerify, - aCertHolder::set); + aCertHolder::set, + aResHelper); final IConsumer aExternalConsumer = getVerificationCertificateConsumer (); if (aExternalConsumer != null) aExternalConsumer.accept (aCertHolder.get ()); @@ -520,7 +523,7 @@ public void handleIncomingMessage (@Nonnull final String sClientInfo, @Nonnull final AS2Message aMsg, @Nonnull final IAS2HttpResponseHandler aResponseHandler) { - try + try (final AS2ResourceHelper aResHelper = new AS2ResourceHelper ()) { final IAS2Session aSession = m_aReceiverModule.getSession (); @@ -570,7 +573,7 @@ public void handleIncomingMessage (@Nonnull final String sClientInfo, // Decrypt and verify signature of the data, and attach data to the // message - decrypt (aMsg); + decrypt (aMsg, aResHelper); if (aCryptoHelper.isCompressed (aMsg.getContentType ())) { @@ -580,7 +583,7 @@ public void handleIncomingMessage (@Nonnull final String sClientInfo, bIsDecompressed = true; } - verify (aMsg); + verify (aMsg, aResHelper); if (aCryptoHelper.isCompressed (aMsg.getContentType ())) { diff --git a/as2-lib/src/main/java/com/helger/as2lib/processor/sender/AS2SenderModule.java b/as2-lib/src/main/java/com/helger/as2lib/processor/sender/AS2SenderModule.java index b2677b1e..0b0169e2 100644 --- a/as2-lib/src/main/java/com/helger/as2lib/processor/sender/AS2SenderModule.java +++ b/as2-lib/src/main/java/com/helger/as2lib/processor/sender/AS2SenderModule.java @@ -81,6 +81,7 @@ import com.helger.as2lib.util.AS2Helper; import com.helger.as2lib.util.AS2HttpHelper; import com.helger.as2lib.util.AS2IOHelper; +import com.helger.as2lib.util.AS2ResourceHelper; import com.helger.as2lib.util.CAS2Header; import com.helger.as2lib.util.dump.IHTTPIncomingDumper; import com.helger.as2lib.util.dump.IHTTPOutgoingDumper; @@ -649,6 +650,8 @@ protected void updateHttpHeaders (@Nonnull final AS2HttpHeaderSetter aHeaderSett * mic value from original msg * @param aIncomingDumper * Incoming dumper. May be null. + * @param aResHelper + * Resource helper * @throws AS2Exception * in case of an error * @throws IOException @@ -657,7 +660,8 @@ protected void updateHttpHeaders (@Nonnull final AS2HttpHeaderSetter aHeaderSett protected void receiveSyncMDN (@Nonnull final AS2Message aMsg, @Nonnull final AS2HttpClient aHttpClient, @Nonnull final MIC aOriginalMIC, - @Nullable final IHTTPIncomingDumper aIncomingDumper) throws AS2Exception, IOException + @Nullable final IHTTPIncomingDumper aIncomingDumper, + @Nonnull final AS2ResourceHelper aResHelper) throws AS2Exception, IOException { if (LOGGER.isDebugEnabled ()) LOGGER.debug ("Receiving synchronous MDN for message" + aMsg.getLoggingText ()); @@ -725,7 +729,7 @@ protected void receiveSyncMDN (@Nonnull final AS2Message aMsg, bUseCertificateInBodyPart = getSession ().isCryptoVerifyUseCertificateInBodyPart (); } - AS2Helper.parseMDN (aMsg, aSenderCert, bUseCertificateInBodyPart, m_aVerificationCertificateConsumer); + AS2Helper.parseMDN (aMsg, aSenderCert, bUseCertificateInBodyPart, m_aVerificationCertificateConsumer, aResHelper); try { @@ -811,9 +815,10 @@ private void _sendViaHTTP (@Nonnull final AS2Message aMsg, @Nullable final MIC aMIC, @Nullable final EContentTransferEncoding eCTE, @Nullable final IHTTPOutgoingDumper aOutgoingDumper, - @Nullable final IHTTPIncomingDumper aIncomingDumper) throws AS2Exception, - IOException, - MessagingException + @Nullable final IHTTPIncomingDumper aIncomingDumper, + @Nonnull final AS2ResourceHelper aResHelper) throws AS2Exception, + IOException, + MessagingException { final Partnership aPartnership = aMsg.partnership (); @@ -876,7 +881,7 @@ private void _sendViaHTTP (@Nonnull final AS2Message aMsg, // go ahead to receive sync MDN // Note: If an MDN is requested, a MIC is present assert aMIC != null; - receiveSyncMDN (aMsg, aConn, aMIC, aIncomingDumper); + receiveSyncMDN (aMsg, aConn, aMIC, aIncomingDumper, aResHelper); if (LOGGER.isInfoEnabled ()) LOGGER.info ("message sent" + aMsg.getLoggingText ()); @@ -916,7 +921,7 @@ public void handle (@Nonnull final String sAction, final int nRetries = getRetryCount (aMsg.partnership (), aOptions); - try + try (final AS2ResourceHelper aResHelper = new AS2ResourceHelper ()) { // Get Content-Transfer-Encoding to use final String sContentTransferEncoding = aMsg.partnership () @@ -943,7 +948,7 @@ public void handle (@Nonnull final String sAction, { final IHTTPIncomingDumper aIncomingDumper = getEffectiveHttpIncomingDumper (); // Use no CTE, because it was set on all MIME parts - _sendViaHTTP (aMsg, aSecuredData, aMIC, true ? null : eCTE, aOutgoingDumper, aIncomingDumper); + _sendViaHTTP (aMsg, aSecuredData, aMIC, true ? null : eCTE, aOutgoingDumper, aIncomingDumper, aResHelper); } } catch (final AS2HttpResponseException ex) diff --git a/as2-lib/src/main/java/com/helger/as2lib/util/AS2Helper.java b/as2-lib/src/main/java/com/helger/as2lib/util/AS2Helper.java index 43f679f9..2a7d6e05 100644 --- a/as2-lib/src/main/java/com/helger/as2lib/util/AS2Helper.java +++ b/as2-lib/src/main/java/com/helger/as2lib/util/AS2Helper.java @@ -377,7 +377,8 @@ public static IMessageMDN createMDN (@Nonnull final IAS2Session aSession, public static void parseMDN (@Nonnull final IMessage aMsg, @Nullable final X509Certificate aReceiverCert, final boolean bUseCertificateInBodyPart, - @Nullable final Consumer aEffectiveCertificateConsumer) throws Exception + @Nullable final Consumer aEffectiveCertificateConsumer, + @Nonnull final AS2ResourceHelper aResHelper) throws Exception { final String sLoggingText = aMsg.getLoggingText (); LOGGER.info ("Start parsing MDN of" + sLoggingText); @@ -407,7 +408,8 @@ public static void parseMDN (@Nonnull final IMessage aMsg, aReceiverCert, bUseCertificateInBodyPart, bForceVerify, - x -> aCertHolder.set (x)); + aCertHolder::set, + aResHelper); if (aEffectiveCertificateConsumer != null) aEffectiveCertificateConsumer.accept (aCertHolder.get ()); @@ -445,9 +447,8 @@ public static void parseMDN (@Nonnull final IMessage aMsg, { // https://github.com/phax/as2-lib/issues/100 final String sCTE = aReportPart.getHeader (CHttpHeader.CONTENT_TRANSFER_ENCODING, null); - try ( - final InputStream aRealIS = AS2IOHelper.getContentTransferEncodingAwareInputStream (aReportPart.getInputStream (), - sCTE)) + try (final InputStream aRealIS = AS2IOHelper.getContentTransferEncodingAwareInputStream (aReportPart.getInputStream (), + sCTE)) { final InternetHeaders aDispositionHeaders = new InternetHeaders (aRealIS); aMdn.attrs () diff --git a/as2-lib/src/main/java/com/helger/as2lib/util/AS2ResourceHelper.java b/as2-lib/src/main/java/com/helger/as2lib/util/AS2ResourceHelper.java new file mode 100644 index 00000000..b0bedc29 --- /dev/null +++ b/as2-lib/src/main/java/com/helger/as2lib/util/AS2ResourceHelper.java @@ -0,0 +1,253 @@ +/** + * Copyright (C) 2015-2020 Philip Helger (www.helger.com) + * philip[at]helger[dot]com + * + * Licensed 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 com.helger.as2lib.util; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; + +import org.apache.http.HttpEntity; +import org.apache.http.entity.FileEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.helger.as2lib.CAS2Info; +import com.helger.commons.ValueEnforcer; +import com.helger.commons.annotation.ReturnsMutableCopy; +import com.helger.commons.collection.impl.CommonsArrayList; +import com.helger.commons.collection.impl.ICommonsList; +import com.helger.commons.concurrent.SimpleReadWriteLock; +import com.helger.commons.io.file.FileHelper; +import com.helger.commons.io.file.FileIOError; +import com.helger.commons.io.stream.StreamHelper; + +/** + * A resource manager that keeps track of temporary files and other closables + * that will be closed when this manager is closed. When calling + * {@link #createTempFile()} a new filename is created and added to the list. + * When using {@link #addCloseable(Closeable)} the Closable is added for + * postponed closing. + * + * @author Philip Helger + * @since 4.5.3 + */ +public class AS2ResourceHelper implements Closeable +{ + private static final Logger LOGGER = LoggerFactory.getLogger (AS2ResourceHelper.class); + private static File s_aTempDir; + + /** + * @return The temp file directory to use, or null for the system + * default. + */ + @Nullable + public static File getTempDir () + { + return s_aTempDir; + } + + /** + * Set a temporary directory to use. + * + * @param aTempDir + * The directory to use. It must be an existing directory. May be + * null to use the system default. + * @throws IllegalArgumentException + * If the directory does not exist + */ + public static void setTempDir (@Nullable final File aTempDir) + { + if (aTempDir != null) + if (!aTempDir.isDirectory ()) + throw new IllegalArgumentException ("Temporary directory '" + + aTempDir.getAbsolutePath () + + "' is not a directory"); + s_aTempDir = aTempDir; + } + + private final SimpleReadWriteLock m_aRWLock = new SimpleReadWriteLock (); + private final AtomicBoolean m_aInClose = new AtomicBoolean (false); + @GuardedBy ("m_aRWLock") + private final ICommonsList m_aTempFiles = new CommonsArrayList <> (); + @GuardedBy ("m_aRWLock") + private final ICommonsList m_aCloseables = new CommonsArrayList <> (); + + public AS2ResourceHelper () + {} + + /** + * @return A new temporary {@link File} that will be deleted when + * {@link #close()} is called. + * @throws IOException + * When temp file creation fails. + * @throws IllegalStateException + * If {@link #close()} was already called before + */ + @Nonnull + public File createTempFile () throws IOException + { + if (m_aInClose.get ()) + throw new IllegalStateException ("ResourceManager is already closing/closed!"); + + // Create + final File ret = File.createTempFile ("as2-lib-res-", ".tmp", s_aTempDir); + // And remember + m_aRWLock.writeLocked ( () -> m_aTempFiles.add (ret)); + return ret; + } + + /** + * @return A list of all known temp files. Never null but maybe + * empty. + */ + @Nonnull + @ReturnsMutableCopy + public ICommonsList getAllTempFiles () + { + return m_aRWLock.readLocked (m_aTempFiles::getClone); + } + + /** + * Add a new closable for later closing. + * + * @param aCloseable + * The closable to be closed later. May not be null. + * @throws IllegalStateException + * If {@link #close()} was already called before + */ + public void addCloseable (@Nonnull final Closeable aCloseable) + { + ValueEnforcer.notNull (aCloseable, "Closeable"); + + if (m_aInClose.get ()) + throw new IllegalStateException ("AS4ResourceHelper is already closing/closed!"); + + m_aCloseables.add (aCloseable); + } + + /** + * @return A list of all known closables. Never null but maybe + * empty. + */ + @Nonnull + @ReturnsMutableCopy + public ICommonsList getAllCloseables () + { + return m_aRWLock.readLocked (m_aCloseables::getClone); + } + + public void close () + { + // Avoid taking new objects + // close only once + if (!m_aInClose.getAndSet (true)) + { + // Close all closeables before deleting files, because the closables might + // be the files to be deleted :) + final ICommonsList aCloseables = m_aRWLock.writeLocked ( () -> { + final ICommonsList ret = m_aCloseables.getClone (); + m_aCloseables.clear (); + return ret; + }); + if (aCloseables.isNotEmpty ()) + { + if (LOGGER.isDebugEnabled ()) + LOGGER.debug ("Closing " + aCloseables.size () + " " + CAS2Info.NAME_VERSION + " stream handles"); + + for (final Closeable aCloseable : aCloseables) + StreamHelper.close (aCloseable); + } + + // Get and delete all temp files + final ICommonsList aFiles = m_aRWLock.writeLocked ( () -> { + final ICommonsList ret = m_aTempFiles.getClone (); + m_aTempFiles.clear (); + return ret; + }); + if (aFiles.isNotEmpty ()) + { + if (LOGGER.isDebugEnabled ()) + LOGGER.debug ("Deleting " + aFiles.size () + " temporary " + CAS2Info.NAME_VERSION + " files"); + + for (final File aFile : aFiles) + { + if (LOGGER.isDebugEnabled ()) + LOGGER.debug ("Deleting temporary file '" + aFile.getAbsolutePath () + "'"); + + final FileIOError aError = AS2IOHelper.getFileOperationManager ().deleteFileIfExisting (aFile); + if (aError.isFailure ()) + LOGGER.warn (" Failed to delete temporary " + + CAS2Info.NAME_VERSION + + " file " + + aFile.getAbsolutePath () + + ": " + + aError.toString ()); + } + } + } + } + + /** + * Ensure the provided {@link HttpEntity} can be read more than once. If the + * provided entity is not repeatable a temporary file is created and a new + * file-based Http Entity is created. + * + * @param aSrcEntity + * The source Http entity. May not be null. + * @return A non-null Http entity that can be read more than + * once. + * @throws IOException + * on IO error + */ + @Nonnull + public HttpEntity createRepeatableHttpEntity (@Nonnull final HttpEntity aSrcEntity) throws IOException + { + ValueEnforcer.notNull (aSrcEntity, "SrcEntity"); + + // Do we need to do anything? + if (aSrcEntity.isRepeatable ()) + return aSrcEntity; + + // First serialize the content once to a file, so that a repeatable entity + // can be created + final File aTempFile = createTempFile (); + + LOGGER.info ("Converting " + + aSrcEntity + + " to a repeatable HTTP entity using file " + + aTempFile.getAbsolutePath ()); + + try (final OutputStream aOS = FileHelper.getBufferedOutputStream (aTempFile)) + { + aSrcEntity.writeTo (aOS); + } + + // Than use the FileEntity as the basis + final FileEntity aRepeatableEntity = new FileEntity (aTempFile); + aRepeatableEntity.setContentType (aSrcEntity.getContentType ()); + aRepeatableEntity.setContentEncoding (aSrcEntity.getContentEncoding ()); + aRepeatableEntity.setChunked (aSrcEntity.isChunked ()); + return aRepeatableEntity; + } + +} diff --git a/as2-lib/src/test/java/com/helger/as2lib/processor/sender/ReadMDNFuncTest.java b/as2-lib/src/test/java/com/helger/as2lib/processor/sender/ReadMDNFuncTest.java index 6b03c5a7..572ed0ac 100644 --- a/as2-lib/src/test/java/com/helger/as2lib/processor/sender/ReadMDNFuncTest.java +++ b/as2-lib/src/test/java/com/helger/as2lib/processor/sender/ReadMDNFuncTest.java @@ -54,6 +54,7 @@ import com.helger.as2lib.message.IMessageMDN; import com.helger.as2lib.util.AS2Helper; import com.helger.as2lib.util.AS2HttpHelper; +import com.helger.as2lib.util.AS2ResourceHelper; import com.helger.commons.http.HttpHeaderMap; import com.helger.commons.io.resource.ClassPathResource; import com.helger.commons.io.resource.IReadableResource; @@ -76,8 +77,7 @@ public void testReadMDN02 () throws Exception assertTrue (aCertRes.exists ()); final HttpHeaderMap aHeaders = new HttpHeaderMap (); - try ( - NonBlockingBufferedReader aBR = new NonBlockingBufferedReader (aHeaderRes.getReader (StandardCharsets.ISO_8859_1))) + try (NonBlockingBufferedReader aBR = new NonBlockingBufferedReader (aHeaderRes.getReader (StandardCharsets.ISO_8859_1))) { String s; while ((s = aBR.readLine ()) != null) @@ -114,9 +114,9 @@ public void testReadMDN02 () throws Exception assertFalse (aCryptoHelper.isCompressed (aPart.getContentType ())); final Consumer aCertHolder = null; - try + try (final AS2ResourceHelper aResHelper = new AS2ResourceHelper ()) { - AS2Helper.parseMDN (aMsg, aCert, true, aCertHolder); + AS2Helper.parseMDN (aMsg, aCert, true, aCertHolder, aResHelper); fail (); } catch (final CMSException ex) @@ -142,8 +142,7 @@ public void testReadMDNIssue97 () throws Exception } final HttpHeaderMap aHeaders = new HttpHeaderMap (); - try ( - NonBlockingBufferedReader aBR = new NonBlockingBufferedReader (aHeaderRes.getReader (StandardCharsets.ISO_8859_1))) + try (NonBlockingBufferedReader aBR = new NonBlockingBufferedReader (aHeaderRes.getReader (StandardCharsets.ISO_8859_1))) { String s; while ((s = aBR.readLine ()) != null) @@ -181,7 +180,10 @@ public void testReadMDNIssue97 () throws Exception assertFalse (aCryptoHelper.isEncrypted (aPart)); assertFalse (aCryptoHelper.isCompressed (aPart.getContentType ())); - final Consumer aCertHolder = null; - AS2Helper.parseMDN (aMsg, null, true, aCertHolder); + try (final AS2ResourceHelper aResHelper = new AS2ResourceHelper ()) + { + final Consumer aCertHolder = null; + AS2Helper.parseMDN (aMsg, null, true, aCertHolder, aResHelper); + } } }