/** * 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.receiver.net; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.Socket; import java.security.cert.X509Certificate; import javax.annotation.Nonnull; import javax.mail.MessagingException; import javax.mail.internet.ContentType; import javax.mail.internet.MimeBodyPart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.helger.as2lib.cert.ECertificatePartnershipType; import com.helger.as2lib.cert.ICertificateFactory; import com.helger.as2lib.disposition.DispositionException; 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.IMessageMDN; import com.helger.as2lib.processor.NoModuleException; import com.helger.as2lib.processor.receiver.AS2MDNReceiverModule; import com.helger.as2lib.processor.receiver.AbstractActiveNetModule; 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.IOHelper; import com.helger.as2lib.util.http.AS2HttpResponseHandlerSocket; import com.helger.as2lib.util.http.AS2InputStreamProviderSocket; import com.helger.as2lib.util.http.HTTPHelper; import com.helger.as2lib.util.http.IAS2HttpResponseHandler; import com.helger.commons.ValueEnforcer; import com.helger.commons.io.stream.NonBlockingBufferedReader; import com.helger.commons.io.stream.NonBlockingByteArrayOutputStream; import com.helger.commons.io.stream.StreamHelper; import com.helger.commons.state.ETriState; import com.helger.commons.string.StringParser; import com.helger.mail.datasource.ByteArrayDataSource; public class AS2MDNReceiverHandler extends AbstractReceiverHandler { private static final String ATTR_PENDINGMDNINFO = "pendingmdninfo"; private static final String ATTR_PENDINGMDN = "pendingmdn"; private static final Logger s_aLogger = LoggerFactory.getLogger (AS2MDNReceiverHandler.class); private final AS2MDNReceiverModule m_aModule; public AS2MDNReceiverHandler (@Nonnull final AS2MDNReceiverModule aModule) { m_aModule = ValueEnforcer.notNull (aModule, "Module"); } @Nonnull public AS2MDNReceiverModule getModule () { return m_aModule; } public void handle (@Nonnull final AbstractActiveNetModule aOwner, @Nonnull final Socket aSocket) { final String sClientInfo = getClientInfo (aSocket); s_aLogger.info ("incoming connection [" + sClientInfo + "]"); final AS2Message aMsg = new AS2Message (); final IAS2HttpResponseHandler aResponseHandler = new AS2HttpResponseHandlerSocket (aSocket); byte [] aData = null; // Read in the message request, headers, and data try { aData = readAndDecodeHttpRequest (new AS2InputStreamProviderSocket (aSocket), aResponseHandler, aMsg); // Asynch MDN 2007-03-12 // check if the requested URL is defined in attribute "as2_receipt_option" // in one of partnerships, if yes, then process incoming AsyncMDN s_aLogger.info ("incoming connection for receiving AsyncMDN" + " [" + sClientInfo + "]" + aMsg.getLoggingText ()); final ContentType aReceivedContentType = new ContentType (aMsg.getHeader (CAS2Header.HEADER_CONTENT_TYPE)); final String sReceivedContentType = aReceivedContentType.toString (); final MimeBodyPart aReceivedPart = new MimeBodyPart (aMsg.getHeaders (), aData); aMsg.setData (aReceivedPart); // MimeBodyPart receivedPart = new MimeBodyPart(); aReceivedPart.setDataHandler (new ByteArrayDataSource (aData, sReceivedContentType, null).getAsDataHandler ()); // Must be set AFTER the DataHandler! aReceivedPart.setHeader (CAS2Header.HEADER_CONTENT_TYPE, sReceivedContentType); aMsg.setData (aReceivedPart); receiveMDN (aMsg, aData, aResponseHandler); } catch (final Exception ex) { final NetException ne = new NetException (aSocket.getInetAddress (), aSocket.getPort (), ex); ne.terminate (); } } // Asynch MDN 2007-03-12 /** * method for receiving and processing Async MDN sent from receiver. * * @param aMsg * The MDN message * @param aData * The MDN content * @param aResponseHandler * The HTTP response handler for setting the correct HTTP response code * @throws OpenAS2Exception * In case of error * @throws IOException * In case of IO error */ protected final void receiveMDN (@Nonnull final AS2Message aMsg, final byte [] aData, @Nonnull final IAS2HttpResponseHandler aResponseHandler) throws OpenAS2Exception, IOException { try { // Create a MessageMDN and copy HTTP headers final IMessageMDN aMDN = new AS2MessageMDN (aMsg); // copy headers from msg to MDN from msg aMDN.setHeaders (aMsg.getHeaders ()); final MimeBodyPart aPart = new MimeBodyPart (aMDN.getHeaders (), aData); 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 getModule ().getSession ().getPartnershipFactory ().updatePartnership (aMDN, false); final ICertificateFactory aCertFactory = getModule ().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 = getModule ().getSession ().isCryptoVerifyUseCertificateInBodyPart (); } AS2Helper.parseMDN (aMsg, aSenderCert, bUseCertificateInBodyPart); // in order to name & save the mdn with the original AS2-From + AS2-To + // Message id., // the 3 msg attributes have to be reset before calling MDNFileModule aMsg.getPartnership ().setSenderAS2ID (aMDN.getHeader (CAS2Header.HEADER_AS2_TO)); aMsg.getPartnership ().setReceiverAS2ID (aMDN.getHeader (CAS2Header.HEADER_AS2_FROM)); getModule ().getSession ().getPartnershipFactory ().updatePartnership (aMsg, false); aMsg.setMessageID (aMsg.getMDN ().getAttribute (AS2MessageMDN.MDNA_ORIG_MESSAGEID)); try { getModule ().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 } // check if the mic (message integrity check) is correct if (checkAsyncMDN (aMsg)) HTTPHelper.sendSimpleHTTPResponse (aResponseHandler, HttpURLConnection.HTTP_OK); else HTTPHelper.sendSimpleHTTPResponse (aResponseHandler, HttpURLConnection.HTTP_NOT_FOUND); final String sDisposition = aMsg.getMDN ().getAttribute (AS2MessageMDN.MDNA_DISPOSITION); 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) { HTTPHelper.sendSimpleHTTPResponse (aResponseHandler, HttpURLConnection.HTTP_BAD_REQUEST); throw ex; } catch (final Exception ex) { HTTPHelper.sendSimpleHTTPResponse (aResponseHandler, HttpURLConnection.HTTP_BAD_REQUEST); final OpenAS2Exception we = WrappedOpenAS2Exception.wrap (ex); we.addSource (OpenAS2Exception.SOURCE_MESSAGE, aMsg); throw we; } } // Asynch MDN 2007-03-12 /** * verify if the mic is matched. * * @param aMsg * Message * @return true if mdn processed */ public boolean checkAsyncMDN (final AS2Message aMsg) { try { // get the returned mic from mdn object final String sReturnMIC = aMsg.getMDN ().getAttribute (AS2MessageMDN.MDNA_MIC); // use original message id. to open the pending information file // from pendinginfo folder. final String sOrigMessageID = aMsg.getMDN ().getAttribute (AS2MessageMDN.MDNA_ORIG_MESSAGEID); final String sPendingInfoFile = getModule ().getSession () .getMessageProcessor () .getAttributeAsString (ATTR_PENDINGMDNINFO) + "/" + IOHelper.getFilenameFromMessageID (sOrigMessageID); final NonBlockingBufferedReader aPendingInfoReader = new NonBlockingBufferedReader (new FileReader (sPendingInfoFile)); String sOriginalMIC; File aPendingFile; try { // Get the original mic from the first line of pending information // file sOriginalMIC = aPendingInfoReader.readLine (); // Get the original pending file from the second line of pending // information file aPendingFile = new File (aPendingInfoReader.readLine ()); } finally { StreamHelper.close (aPendingInfoReader); } final String sDisposition = aMsg.getMDN ().getAttribute (AS2MessageMDN.MDNA_DISPOSITION); s_aLogger.info ("received MDN [" + sDisposition + "]" + aMsg.getLoggingText ()); /* * original code just did string compare - returnmic.equals(originalmic). * Sadly this is not good enough as the mic fields are * "base64string, algorithm" taken from a rfc822 style * Returned-Content-MIC header and rfc822 headers can contain spaces all * over the place. (not to mention comments!). Simple fix - delete all * spaces. */ if (sOriginalMIC == null || !sReturnMIC.replaceAll ("\\s+", "").equals (sOriginalMIC.replaceAll ("\\s+", ""))) { s_aLogger.info ("MIC IS NOT MATCHED, original mic: " + sOriginalMIC + " return mic: " + sReturnMIC + aMsg.getLoggingText ()); return false; } // delete the pendinginfo & pending file if mic is matched s_aLogger.info ("mic is matched, mic: " + sReturnMIC + aMsg.getLoggingText ()); final File aPendingInfoFile = new File (sPendingInfoFile); s_aLogger.info ("delete pendinginfo file : " + aPendingInfoFile.getName () + " from pending folder : " + getModule ().getSession ().getMessageProcessor ().getAttributeAsString (ATTR_PENDINGMDN) + aMsg.getLoggingText ()); aPendingInfoFile.delete (); s_aLogger.info ("delete pending file : " + aPendingFile.getName () + " from pending folder : " + aPendingFile.getParent () + aMsg.getLoggingText ()); aPendingFile.delete (); } catch (final Exception ex) { s_aLogger.error ("Error checking async MDN", ex); return false; } return true; } public void reparse (@Nonnull final AS2Message aMsg, final HttpURLConnection aConn) { // Create a MessageMDN and copy HTTP headers final IMessageMDN aMDN = new AS2MessageMDN (aMsg); HTTPHelper.copyHttpHeaders (aConn, aMDN.getHeaders ()); // Receive the MDN data NonBlockingByteArrayOutputStream aMDNStream = null; try { final InputStream aIS = aConn.getInputStream (); aMDNStream = new NonBlockingByteArrayOutputStream (); // Retrieve the message content final long nContentLength = StringParser.parseLong (aMDN.getHeader (CAS2Header.HEADER_CONTENT_LENGTH), -1); if (nContentLength >= 0) StreamHelper.copyInputStreamToOutputStreamWithLimit (aIS, aMDNStream, nContentLength); else StreamHelper.copyInputStreamToOutputStream (aIS, aMDNStream); } catch (final IOException ex) { s_aLogger.error ("Error reparsing", ex); } finally { StreamHelper.close (aMDNStream); } if (HTTPHelper.isHTTPIncomingDumpEnabled ()) HTTPHelper.dumpIncomingHttpRequest (HTTPHelper.getAllHTTPHeaderLines (aMDN.getHeaders ()), aMDNStream.toByteArray (), aMDN); MimeBodyPart aPart = null; if (aMDNStream != null) try { aPart = new MimeBodyPart (aMDN.getHeaders (), aMDNStream.toByteArray ()); } catch (final MessagingException ex) { s_aLogger.error ("Error creating MimeBodyPart", ex); } 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)); } }