/** * The FreeBSD Copyright * Copyright 1994-2008 The FreeBSD Project. All rights reserved. * Copyright (C) 2013-2017 Philip Helger philip[at]helger[dot]com * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD PROJECT OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation * are those of the authors and should not be interpreted as representing * official policies, either expressed or implied, of the FreeBSD Project. */ package com.helger.as2lib.processor.sender; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Writer; import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.mail.MessagingException; import javax.mail.internet.MimeBodyPart; import org.bouncycastle.mail.smime.SMIMECompressedGenerator; import org.bouncycastle.mail.smime.SMIMEException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.helger.as2lib.cert.ECertificatePartnershipType; import com.helger.as2lib.cert.ICertificateFactory; import com.helger.as2lib.crypto.ECompressionType; import com.helger.as2lib.crypto.ECryptoAlgorithmCrypt; import com.helger.as2lib.crypto.ECryptoAlgorithmSign; import com.helger.as2lib.disposition.DispositionException; import com.helger.as2lib.disposition.DispositionOptions; import com.helger.as2lib.disposition.DispositionType; import com.helger.as2lib.exception.OpenAS2Exception; import com.helger.as2lib.exception.WrappedOpenAS2Exception; import com.helger.as2lib.message.AS2Message; import com.helger.as2lib.message.AS2MessageMDN; import com.helger.as2lib.message.IMessage; import com.helger.as2lib.message.IMessageMDN; import com.helger.as2lib.params.InvalidParameterException; import com.helger.as2lib.partner.CPartnershipIDs; import com.helger.as2lib.partner.Partnership; import com.helger.as2lib.processor.CFileAttribute; import com.helger.as2lib.processor.CNetAttribute; import com.helger.as2lib.processor.NoModuleException; import com.helger.as2lib.processor.storage.IProcessorStorageModule; import com.helger.as2lib.session.ComponentNotFoundException; import com.helger.as2lib.util.AS2Helper; import com.helger.as2lib.util.CAS2Header; import com.helger.as2lib.util.DateHelper; import com.helger.as2lib.util.IOHelper; import com.helger.as2lib.util.http.AS2HttpHeaderWrapperHttpURLConnection; import com.helger.as2lib.util.http.HTTPHelper; import com.helger.as2lib.util.http.IAS2HttpHeaderWrapper; import com.helger.as2lib.util.http.IHTTPOutgoingDumper; import com.helger.commons.annotation.Nonempty; import com.helger.commons.annotation.OverrideOnDemand; import com.helger.commons.io.file.FileHelper; import com.helger.commons.io.file.FilenameHelper; import com.helger.commons.io.stream.NonBlockingByteArrayOutputStream; import com.helger.commons.io.stream.StreamHelper; import com.helger.commons.io.stream.WrappedOutputStream; import com.helger.commons.state.ETriState; import com.helger.commons.string.StringParser; import com.helger.commons.timing.StopWatch; import com.helger.http.EHTTPMethod; import com.helger.mail.cte.EContentTransferEncoding; /** * AS2 sender module to send AS2 messages out. * * @author Philip Helger */ public class AS2SenderModule extends AbstractHttpSenderModule { private static final String ATTR_PENDINGMDNINFO = "pendingmdninfo"; private static final String ATTR_PENDINGMDN = "pendingmdn"; private static final Logger s_aLogger = LoggerFactory.getLogger (AS2SenderModule.class); public AS2SenderModule () {} public boolean canHandle (@Nonnull final String sAction, @Nonnull final IMessage aMsg, @Nullable final Map <String, Object> aOptions) { return IProcessorSenderModule.DO_SEND.equals (sAction) && aMsg instanceof AS2Message; } protected void checkRequired (@Nonnull final AS2Message aMsg) throws InvalidParameterException { final Partnership aPartnership = aMsg.getPartnership (); try { InvalidParameterException.checkValue (aMsg, "ContentType", aMsg.getContentType ()); InvalidParameterException.checkValue (aMsg, "Attribute: " + CPartnershipIDs.PA_AS2_URL, aPartnership.getAS2URL ()); InvalidParameterException.checkValue (aMsg, "Receiver: " + CPartnershipIDs.PID_AS2, aPartnership.getReceiverAS2ID ()); InvalidParameterException.checkValue (aMsg, "Sender: " + CPartnershipIDs.PID_AS2, aPartnership.getSenderAS2ID ()); InvalidParameterException.checkValue (aMsg, "Subject", aMsg.getSubject ()); InvalidParameterException.checkValue (aMsg, "Sender: " + CPartnershipIDs.PID_EMAIL, aPartnership.getSenderEmail ()); InvalidParameterException.checkValue (aMsg, "Message Data", aMsg.getData ()); } catch (final InvalidParameterException ex) { ex.addSource (OpenAS2Exception.SOURCE_MESSAGE, aMsg); throw ex; } } // Asynch MDN 2007-03-12 /** * for storing original mic and outgoing file into pending information file * * @param aMsg * AS2Message * @param sMIC * MIC value * @throws OpenAS2Exception * In case of an error */ protected void storePendingInfo (@Nonnull final AS2Message aMsg, @Nonnull final String sMIC) throws OpenAS2Exception { try { if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Original MIC is '" + sMIC + "'" + aMsg.getLoggingText ()); final String sPendingFolder = FilenameHelper.getAsSecureValidASCIIFilename (getSession ().getMessageProcessor () .getAttributeAsString (ATTR_PENDINGMDNINFO)); final String sMsgFilename = IOHelper.getFilenameFromMessageID (aMsg.getMessageID ()); final String sPendingFilename = FilenameHelper.getAsSecureValidASCIIFilename (getSession ().getMessageProcessor () .getAttributeAsString (ATTR_PENDINGMDN)) + "/" + sMsgFilename; s_aLogger.info ("Save Original MIC & message id information into folder '" + sPendingFolder + "'" + aMsg.getLoggingText ()); // input pending folder & original outgoing file name to get and // unique file name in order to avoid file overwriting. try (final Writer aWriter = FileHelper.getWriter (new File (sPendingFolder + "/" + sMsgFilename), StandardCharsets.ISO_8859_1)) { aWriter.write (sMIC + "\n" + sPendingFilename); } // remember aMsg.setAttribute (CFileAttribute.MA_PENDING_FILENAME, sPendingFilename); aMsg.setAttribute (CFileAttribute.MA_STATUS, CFileAttribute.MA_STATUS_PENDING); } catch (final Exception ex) { final OpenAS2Exception we = WrappedOpenAS2Exception.wrap (ex); we.addSource (OpenAS2Exception.SOURCE_MESSAGE, aMsg); throw we; } } /** * From RFC 4130 section 7.3.1: * <ul> * <li>For any signed messages, the MIC to be returned is calculated on the * RFC1767/RFC3023 MIME header and content. Canonicalization on the MIME * headers MUST be performed before the MIC is calculated, since the sender * requesting the signed receipt was also REQUIRED to canonicalize.</li> * <li>For encrypted, unsigned messages, the MIC to be returned is calculated * on the decrypted RFC 1767/RFC3023 MIME header and content. The content * after decryption MUST be canonicalized before the MIC is calculated.</li> * <li>For unsigned, unencrypted messages, the MIC MUST be calculated over the * message contents without the MIME or any other RFC 2822 headers, since * these are sometimes altered or reordered by Mail Transport Agents (MTAs). * </li> * </ul> * So headers must be included if signing or crypting is enabled.<br> * <br> * From RFC 5402 section 4.1: * <ul> * <li>MIC Calculation for Signed Message: For any signed message, the MIC to * be returned is calculated over the same data that was signed in the * original message as per [AS1]. The signed content will be a MIME bodypart * that contains either compressed or uncompressed data.</li> * <li>MIC Calculation for Encrypted, Unsigned Message: For encrypted, * unsigned messages, the MIC to be returned is calculated over the * uncompressed data content including all MIME header fields and any applied * Content-Transfer-Encoding.</li> * <li>MIC Calculation for Unencrypted, Unsigned Message: For unsigned, * unencrypted messages, the MIC is calculated over the uncompressed data * content including all MIME header fields and any applied * Content-Transfer-Encoding</li> * </ul> * So headers must always be included if compression is enabled. * * @param aMsg * Source message * @return MIC value. Neither <code>null</code> nor empty. * @throws Exception * On security or AS2 issues */ @Nonnull @Nonempty protected String calculateAndStoreMIC (@Nonnull final AS2Message aMsg) throws Exception { final Partnership aPartnership = aMsg.getPartnership (); final String sDispositionOptions = aPartnership.getAS2MDNOptions (); final DispositionOptions aDispositionOptions = DispositionOptions.createFromString (sDispositionOptions); if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("DispositionOptions=" + aDispositionOptions); // Calculate and get the original mic final boolean bIncludeHeadersInMIC = aPartnership.getSigningAlgorithm () != null || aPartnership.getEncryptAlgorithm () != null || aPartnership.getCompressionType () != null; final String sMIC = AS2Helper.getCryptoHelper ().calculateMIC (aMsg.getData (), aDispositionOptions.getFirstMICAlg (), bIncludeHeadersInMIC); if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Calculated MIC: '" + sMIC + "'"); if (aPartnership.getAS2ReceiptOption () != null) { // if yes : PA_AS2_RECEIPT_OPTION != null // then keep the original mic & message id. // then wait for the another HTTP call by receivers storePendingInfo (aMsg, sMIC); } return sMIC; } @Nonnull protected MimeBodyPart compress (@Nonnull final IMessage aMsg, @Nonnull final MimeBodyPart aData, @Nonnull final ECompressionType eCompressionType) throws SMIMEException, MessagingException { final SMIMECompressedGenerator aCompressedGenerator = new SMIMECompressedGenerator (); final String sTransferEncoding = aMsg.getPartnership () .getContentTransferEncoding (EContentTransferEncoding.AS2_DEFAULT.getID ()); aCompressedGenerator.setContentTransferEncoding (sTransferEncoding); final MimeBodyPart aCompressedBodyPart = aCompressedGenerator.generate (aData, eCompressionType.createOutputCompressor ()); aMsg.addHeader (CAS2Header.HEADER_CONTENT_TRANSFER_ENCODING, sTransferEncoding); if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Compressed data with " + eCompressionType + " to " + aCompressedBodyPart.getContentType () + ":" + aMsg.getLoggingText ()); return aCompressedBodyPart; } @Nonnull protected MimeBodyPart secure (@Nonnull final IMessage aMsg) throws Exception { // Set up encrypt/sign variables MimeBodyPart aDataBP = aMsg.getData (); final Partnership aPartnership = aMsg.getPartnership (); final ICertificateFactory aCertFactory = getSession ().getCertificateFactory (); // Check compression parameters // If compression is enabled, by default is is compressed before signing final String sCompressionType = aPartnership.getCompressionType (); ECompressionType eCompressionType = null; boolean bCompressBeforeSign = true; if (sCompressionType != null) { eCompressionType = ECompressionType.getFromIDCaseInsensitiveOrNull (sCompressionType); if (eCompressionType == null) throw new OpenAS2Exception ("The compression type '" + sCompressionType + "' is not supported!"); bCompressBeforeSign = aPartnership.isCompressBeforeSign (); } if (eCompressionType != null && bCompressBeforeSign) { // Compress before sign if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Compressing outbound message before signing..."); aDataBP = compress (aMsg, aDataBP, eCompressionType); // Replace the message data, because it is the basis for the MIC aMsg.setData (aDataBP); } // Sign the data if requested final String sSignAlgorithm = aPartnership.getSigningAlgorithm (); if (sSignAlgorithm != null) { final X509Certificate aSenderCert = aCertFactory.getCertificate (aMsg, ECertificatePartnershipType.SENDER); final PrivateKey aSenderKey = aCertFactory.getPrivateKey (aMsg, aSenderCert); final ECryptoAlgorithmSign eSignAlgorithm = ECryptoAlgorithmSign.getFromIDOrNull (sSignAlgorithm); if (eSignAlgorithm == null) throw new OpenAS2Exception ("The signing algorithm '" + sSignAlgorithm + "' is not supported!"); // Include certificate in signed content? boolean bIncludeCertificateInSignedContent; final ETriState eIncludeCertificateInSignedContent = aMsg.getPartnership () .getIncludeCertificateInSignedContent (); if (eIncludeCertificateInSignedContent.isDefined ()) { // Use per partnership bIncludeCertificateInSignedContent = eIncludeCertificateInSignedContent.getAsBooleanValue (); } else { // Use global value bIncludeCertificateInSignedContent = getSession ().isCryptoSignIncludeCertificateInBodyPart (); } // Use old MIC algorithms? final boolean bUseRFC3851MICAlg = aPartnership.isRFC3851MICAlgs (); // Main signing aDataBP = AS2Helper.getCryptoHelper () .sign (aDataBP, aSenderCert, aSenderKey, eSignAlgorithm, bIncludeCertificateInSignedContent, bUseRFC3851MICAlg); if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Signed data with " + eSignAlgorithm + " to " + aDataBP.getContentType () + ":" + aMsg.getLoggingText ()); } if (eCompressionType != null && !bCompressBeforeSign) { // Compress after sign if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Compressing outbound message after signing..."); aDataBP = compress (aMsg, aDataBP, eCompressionType); } // Encrypt the data if requested final String sCryptAlgorithm = aPartnership.getEncryptAlgorithm (); if (sCryptAlgorithm != null) { final X509Certificate aReceiverCert = aCertFactory.getCertificate (aMsg, ECertificatePartnershipType.RECEIVER); final ECryptoAlgorithmCrypt eCryptAlgorithm = ECryptoAlgorithmCrypt.getFromIDOrNull (sCryptAlgorithm); if (eCryptAlgorithm == null) throw new OpenAS2Exception ("The crypting algorithm '" + sCryptAlgorithm + "' is not supported!"); aDataBP = AS2Helper.getCryptoHelper ().encrypt (aDataBP, aReceiverCert, eCryptAlgorithm); if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Encrypted data with " + eCryptAlgorithm + " to " + aDataBP.getContentType () + ":" + aMsg.getLoggingText ()); } return aDataBP; } protected void updateHttpHeaders (@Nonnull final IAS2HttpHeaderWrapper aConn, @Nonnull final IMessage aMsg) { final Partnership aPartnership = aMsg.getPartnership (); aConn.setHttpHeader (CAS2Header.HEADER_CONNECTION, CAS2Header.DEFAULT_CONNECTION); aConn.setHttpHeader (CAS2Header.HEADER_USER_AGENT, CAS2Header.DEFAULT_USER_AGENT); aConn.setHttpHeader (CAS2Header.HEADER_DATE, DateHelper.getFormattedDateNow (CAS2Header.DEFAULT_DATE_FORMAT)); aConn.setHttpHeader (CAS2Header.HEADER_MESSAGE_ID, aMsg.getMessageID ()); // make sure this is the encoding used in the msg, run TBF1 aConn.setHttpHeader (CAS2Header.HEADER_MIME_VERSION, CAS2Header.DEFAULT_MIME_VERSION); aConn.setHttpHeader (CAS2Header.HEADER_CONTENT_TYPE, aMsg.getContentType ()); aConn.setHttpHeader (CAS2Header.HEADER_AS2_VERSION, CAS2Header.DEFAULT_AS2_VERSION); aConn.setHttpHeader (CAS2Header.HEADER_RECIPIENT_ADDRESS, aPartnership.getAS2URL ()); aConn.setHttpHeader (CAS2Header.HEADER_AS2_FROM, aPartnership.getSenderAS2ID ()); aConn.setHttpHeader (CAS2Header.HEADER_AS2_TO, aPartnership.getReceiverAS2ID ()); aConn.setHttpHeader (CAS2Header.HEADER_SUBJECT, aMsg.getSubject ()); aConn.setHttpHeader (CAS2Header.HEADER_FROM, aPartnership.getSenderEmail ()); // Set when compression is enabled aConn.setHttpHeader (CAS2Header.HEADER_CONTENT_TRANSFER_ENCODING, aMsg.getHeader (CAS2Header.HEADER_CONTENT_TRANSFER_ENCODING)); // Determine where to send the MDN to final String sDispTo = aPartnership.getAS2MDNTo (); if (sDispTo != null) aConn.setHttpHeader (CAS2Header.HEADER_DISPOSITION_NOTIFICATION_TO, sDispTo); final String sDispositionOptions = aPartnership.getAS2MDNOptions (); if (sDispositionOptions != null) aConn.setHttpHeader (CAS2Header.HEADER_DISPOSITION_NOTIFICATION_OPTIONS, sDispositionOptions); // Asynch MDN 2007-03-12 final String sReceiptOption = aPartnership.getAS2ReceiptOption (); if (sReceiptOption != null) aConn.setHttpHeader (CAS2Header.HEADER_RECEIPT_DELIVERY_OPTION, sReceiptOption); // As of 2007-06-01 final String sContententDisposition = aMsg.getContentDisposition (); if (sContententDisposition != null) aConn.setHttpHeader (CAS2Header.HEADER_CONTENT_DISPOSITION, sContententDisposition); } // Asynch MDN 2007-03-12 // added originalmic /** * @param aMsg * AS2Message * @param aConn * URLConnection * @param sOriginalMIC * mic value from original msg * @throws OpenAS2Exception * in case of an error * @throws IOException * in case of an IO error */ protected void receiveSyncMDN (@Nonnull final AS2Message aMsg, @Nonnull final HttpURLConnection aConn, @Nonnull final String sOriginalMIC) throws OpenAS2Exception, IOException { if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Receiving synchronous MDN for message" + aMsg.getLoggingText ()); try { // Create a MessageMDN and copy HTTP headers final IMessageMDN aMDN = new AS2MessageMDN (aMsg); HTTPHelper.copyHttpHeaders (aConn, aMDN.getHeaders ()); // Receive the MDN data final InputStream aConnIS = aConn.getInputStream (); final NonBlockingByteArrayOutputStream aMDNStream = new NonBlockingByteArrayOutputStream (); try { // Retrieve the whole MDN content final long nContentLength = StringParser.parseLong (aMDN.getHeader (CAS2Header.HEADER_CONTENT_LENGTH), -1); if (nContentLength >= 0) StreamHelper.copyInputStreamToOutputStreamWithLimit (aConnIS, aMDNStream, nContentLength); else StreamHelper.copyInputStreamToOutputStream (aConnIS, aMDNStream); } finally { StreamHelper.close (aMDNStream); } if (HTTPHelper.isHTTPIncomingDumpEnabled ()) HTTPHelper.dumpIncomingHttpRequest (HTTPHelper.getAllHTTPHeaderLines (aMDN.getHeaders ()), aMDNStream.toByteArray (), aMDN); if (s_aLogger.isTraceEnabled ()) { // Debug print the whole MDN stream s_aLogger.trace ("Retrieved MDN stream data:\n" + aMDNStream.getAsString (StandardCharsets.ISO_8859_1)); } final MimeBodyPart aPart = new MimeBodyPart (aMDN.getHeaders (), aMDNStream.toByteArray ()); aMsg.getMDN ().setData (aPart); // get the MDN partnership info aMDN.getPartnership ().setSenderAS2ID (aMDN.getHeader (CAS2Header.HEADER_AS2_FROM)); aMDN.getPartnership ().setReceiverAS2ID (aMDN.getHeader (CAS2Header.HEADER_AS2_TO)); // Set the appropriate keystore aliases aMDN.getPartnership ().setSenderX509Alias (aMsg.getPartnership ().getReceiverX509Alias ()); aMDN.getPartnership ().setReceiverX509Alias (aMsg.getPartnership ().getSenderX509Alias ()); // Update the partnership getSession ().getPartnershipFactory ().updatePartnership (aMDN, false); final ICertificateFactory aCertFactory = getSession ().getCertificateFactory (); final X509Certificate aSenderCert = aCertFactory.getCertificate (aMDN, ECertificatePartnershipType.SENDER); boolean bUseCertificateInBodyPart; final ETriState eUseCertificateInBodyPart = aMsg.getPartnership ().getVerifyUseCertificateInBodyPart (); if (eUseCertificateInBodyPart.isDefined ()) { // Use per partnership bUseCertificateInBodyPart = eUseCertificateInBodyPart.getAsBooleanValue (); } else { // Use global value bUseCertificateInBodyPart = getSession ().isCryptoVerifyUseCertificateInBodyPart (); } AS2Helper.parseMDN (aMsg, aSenderCert, bUseCertificateInBodyPart); try { getSession ().getMessageProcessor ().handle (IProcessorStorageModule.DO_STOREMDN, aMsg, null); } catch (final ComponentNotFoundException ex) { // No message processor found } catch (final NoModuleException ex) { // No module found in message processor } final String sDisposition = aMsg.getMDN ().getAttribute (AS2MessageMDN.MDNA_DISPOSITION); s_aLogger.info ("received MDN [" + sDisposition + "]" + aMsg.getLoggingText ()); // Asynch MDN 2007-03-12 // Verify if the original mic is equal to the mic in returned MDN final String sReturnMIC = aMsg.getMDN ().getAttribute (AS2MessageMDN.MDNA_MIC); // Catch ReturnMIC == null in case the attribute is simply missing if (sReturnMIC == null || !sReturnMIC.replaceAll ("\\s+", "").equals (sOriginalMIC.replaceAll ("\\s+", ""))) { // file was sent completely but the returned mic was not matched, // don't know it needs or needs not to be resent ? it's depended on // what! // anyway, just log the warning message here. s_aLogger.info ("MIC IS NOT MATCHED, original mic: '" + sOriginalMIC + "' return mic: '" + sReturnMIC + "'" + aMsg.getLoggingText ()); } else { s_aLogger.info ("mic is matched, mic: " + sReturnMIC + aMsg.getLoggingText ()); } try { DispositionType.createFromString (sDisposition).validate (); } catch (final DispositionException ex) { ex.setText (aMsg.getMDN ().getText ()); if (ex.getDisposition ().isWarning ()) { ex.addSource (OpenAS2Exception.SOURCE_MESSAGE, aMsg); ex.terminate (); } else { throw ex; } } } catch (final IOException ex) { throw ex; } catch (final Exception ex) { final OpenAS2Exception we = WrappedOpenAS2Exception.wrap (ex); we.addSource (OpenAS2Exception.SOURCE_MESSAGE, aMsg); throw we; } } @OverrideOnDemand protected void onReceivedMDNError (@Nonnull final AS2Message aMsg, @Nonnull final OpenAS2Exception ex) { final OpenAS2Exception oae2 = new OpenAS2Exception ("Message was sent but an error occured while receiving the MDN", ex); oae2.addSource (OpenAS2Exception.SOURCE_MESSAGE, aMsg); oae2.terminate (); } private void _sendViaHTTP (@Nonnull final AS2Message aMsg, @Nonnull final MimeBodyPart aSecuredMimePart, @Nonnull final String sMIC) throws OpenAS2Exception, IOException, MessagingException { final Partnership aPartnership = aMsg.getPartnership (); // Create the HTTP connection final String sUrl = aPartnership.getAS2URL (); final boolean bOutput = true; final boolean bInput = true; final boolean bUseCaches = false; final EHTTPMethod eRequestMethod = EHTTPMethod.POST; final HttpURLConnection aConn = getConnection (sUrl, bOutput, bInput, bUseCaches, eRequestMethod, getSession ().getHttpProxy ()); try { s_aLogger.info ("Connecting to " + sUrl + aMsg.getLoggingText ()); updateHttpHeaders (new AS2HttpHeaderWrapperHttpURLConnection (aConn), aMsg); aMsg.setAttribute (CNetAttribute.MA_DESTINATION_IP, aConn.getURL ().getHost ()); aMsg.setAttribute (CNetAttribute.MA_DESTINATION_PORT, Integer.toString (aConn.getURL ().getPort ())); // Note: closing this stream causes connection abort errors on some AS2 // servers OutputStream aMsgOS = aConn.getOutputStream (); // This stream dumps the HTTP OutputStream aDebugOS = null; final IHTTPOutgoingDumper aHttpDumper = HTTPHelper.getHTTPOutgoingDumper (); if (aHttpDumper != null) { aDebugOS = aHttpDumper.dumpOutgoingRequest (aMsg); if (aDebugOS != null) { // Overwrite the used OutputStream to additionally log to the debug // OutputStream final OutputStream aFinalDebugOS = aDebugOS; aMsgOS = new WrappedOutputStream (aMsgOS) { @Override public final void write (final int b) throws IOException { super.write (b); aFinalDebugOS.write (b); } }; } } // Transfer the data final InputStream aMsgIS = aSecuredMimePart.getInputStream (); final StopWatch aSW = StopWatch.createdStarted (); // Main transmission - closes InputStream final long nBytes = IOHelper.copy (aMsgIS, aMsgOS); aSW.stop (); s_aLogger.info ("transferred " + IOHelper.getTransferRate (nBytes, aSW) + aMsg.getLoggingText ()); // Close debug OS (if used) StreamHelper.close (aDebugOS); // Check the HTTP Response code final int nResponseCode = aConn.getResponseCode (); // Accept most of 2xx HTTP response codes if (nResponseCode != HttpURLConnection.HTTP_OK && nResponseCode != HttpURLConnection.HTTP_CREATED && nResponseCode != HttpURLConnection.HTTP_ACCEPTED && nResponseCode != HttpURLConnection.HTTP_NO_CONTENT && nResponseCode != HttpURLConnection.HTTP_PARTIAL) { s_aLogger.error ("Error URL '" + sUrl + "' - HTTP " + nResponseCode + " " + aConn.getResponseMessage ()); throw new HttpResponseException (sUrl, nResponseCode, aConn.getResponseMessage ()); } // Asynch MDN 2007-03-12 // Receive an MDN try { // Receive an MDN if (aMsg.isRequestingMDN ()) { // Check if the AsyncMDN is required if (aPartnership.getAS2ReceiptOption () == null) { // go ahead to receive sync MDN receiveSyncMDN (aMsg, aConn, sMIC); s_aLogger.info ("message sent" + aMsg.getLoggingText ()); } } } catch (final DispositionException ex) { // If a disposition error hasn't been handled, the message transfer // was not successful throw ex; } catch (final OpenAS2Exception ex) { // Don't re-send or fail, just log an error if one occurs while // receiving the MDN onReceivedMDNError (aMsg, ex); } } finally { aConn.disconnect (); } } public void handle (@Nonnull final String sAction, @Nonnull final IMessage aBaseMsg, @Nullable final Map <String, Object> aOptions) throws OpenAS2Exception { final AS2Message aMsg = (AS2Message) aBaseMsg; s_aLogger.info ("Submitting message" + aMsg.getLoggingText ()); // verify all required information is present for sending checkRequired (aMsg); final int nRetries = getRetryCount (aMsg.getPartnership (), aOptions); try { // compress and/or sign and/or encrypt the message if needed final MimeBodyPart aSecuredData = secure (aMsg); // Calculate MIC after compress/sign/crypt was handled, because the // message data might change if compression before signing is active. final String sMIC = calculateAndStoreMIC (aMsg); if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Setting message content type to '" + aSecuredData.getContentType () + "'"); aMsg.setContentType (aSecuredData.getContentType ()); _sendViaHTTP (aMsg, aSecuredData, sMIC); } catch (final HttpResponseException ex) { s_aLogger.error ("Http Response Error " + ex.getMessage ()); ex.terminate (); if (!doResend (IProcessorSenderModule.DO_SEND, aMsg, ex, nRetries)) throw ex; } catch (final IOException ex) { // Re-send if a network error occurs during transmission final OpenAS2Exception wioe = WrappedOpenAS2Exception.wrap (ex); wioe.addSource (OpenAS2Exception.SOURCE_MESSAGE, aMsg); wioe.terminate (); if (!doResend (IProcessorSenderModule.DO_SEND, aMsg, wioe, nRetries)) throw wioe; } catch (final Exception ex) { // Propagate error if it can't be handled by a re-send throw WrappedOpenAS2Exception.wrap (ex); } } }