All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.phase4.peppol.servlet.Phase4PeppolServletMessageProcessorSPI Maven / Gradle / Ivy

There is a newer version: 3.0.0
Show newest version
/*
 * Copyright (C) 2015-2024 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.phase4.peppol.servlet;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Locale;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.unece.cefact.namespaces.sbdh.StandardBusinessDocument;
import org.w3c.dom.Node;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.IsSPIImplementation;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.annotation.ReturnsMutableObject;
import com.helger.commons.annotation.UnsupportedOperation;
import com.helger.commons.annotation.UsedViaReflection;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.datetime.XMLOffsetDateTime;
import com.helger.commons.error.IError;
import com.helger.commons.error.list.ErrorList;
import com.helger.commons.http.HttpHeaderMap;
import com.helger.commons.io.stream.NonBlockingByteArrayOutputStream;
import com.helger.commons.io.stream.StreamHelper;
import com.helger.commons.lang.ServiceLoaderHelper;
import com.helger.commons.state.ETriState;
import com.helger.commons.string.StringHelper;
import com.helger.peppol.reporting.api.CPeppolReporting;
import com.helger.peppol.reporting.api.PeppolReportingItem;
import com.helger.peppol.sbdh.PeppolSBDHData;
import com.helger.peppol.sbdh.read.PeppolSBDHDocumentReadException;
import com.helger.peppol.sbdh.read.PeppolSBDHDocumentReader;
import com.helger.peppol.smp.ESMPTransportProfile;
import com.helger.peppol.smp.ISMPTransportProfile;
import com.helger.peppol.utils.EPeppolCertificateCheckResult;
import com.helger.peppol.utils.PeppolCertificateChecker;
import com.helger.peppol.utils.PeppolCertificateHelper;
import com.helger.peppolid.IDocumentTypeIdentifier;
import com.helger.peppolid.IParticipantIdentifier;
import com.helger.peppolid.IProcessIdentifier;
import com.helger.peppolid.factory.SimpleIdentifierFactory;
import com.helger.peppolid.peppol.PeppolIdentifierHelper;
import com.helger.phase4.attachment.AS4DecompressException;
import com.helger.phase4.attachment.EAS4CompressionMode;
import com.helger.phase4.attachment.IAS4Attachment;
import com.helger.phase4.attachment.WSS4JAttachment;
import com.helger.phase4.ebms3header.Ebms3Error;
import com.helger.phase4.ebms3header.Ebms3Property;
import com.helger.phase4.ebms3header.Ebms3SignalMessage;
import com.helger.phase4.ebms3header.Ebms3UserMessage;
import com.helger.phase4.error.EEbmsError;
import com.helger.phase4.messaging.IAS4IncomingMessageMetadata;
import com.helger.phase4.mgr.MetaAS4Manager;
import com.helger.phase4.model.pmode.IPMode;
import com.helger.phase4.servlet.IAS4MessageState;
import com.helger.phase4.servlet.spi.AS4MessageProcessorResult;
import com.helger.phase4.servlet.spi.AS4SignalMessageProcessorResult;
import com.helger.phase4.servlet.spi.IAS4ServletMessageProcessorSPI;
import com.helger.phase4.util.Phase4Exception;
import com.helger.sbdh.SBDMarshaller;
import com.helger.security.certificate.CertificateHelper;
import com.helger.smpclient.peppol.ISMPServiceMetadataProvider;
import com.helger.smpclient.peppol.PeppolWildcardSelector;
import com.helger.smpclient.peppol.SMPClientReadOnly;
import com.helger.xml.serialize.write.XMLWriter;
import com.helger.xsds.peppol.smp1.EndpointType;
import com.helger.xsds.peppol.smp1.SignedServiceMetadataType;

/**
 * This is the SPI implementation to handle generic incoming AS4 requests. The
 * main goal of this class is to implement the Peppol specific requirements of
 * packaging data in SBDH. Users of this package must implement
 * {@link IPhase4PeppolIncomingSBDHandlerSPI} instead which provides a more
 * Peppol-style SPI handler.
 *
 * @author Philip Helger
 */
@IsSPIImplementation
public class Phase4PeppolServletMessageProcessorSPI implements IAS4ServletMessageProcessorSPI
{
  private static final class ReadAttachment
  {
    private String m_sID;
    private String m_sMimeType;
    private String m_sUncompressedMimeType;
    private Charset m_aCharset;
    private EAS4CompressionMode m_eCompressionMode;
    private byte [] m_aPayloadBytes;
    private StandardBusinessDocument m_aSBDH;

    private ReadAttachment ()
    {}

    @Nullable
    public String getID ()
    {
      return m_sID;
    }

    @Nullable
    public String getMimeType ()
    {
      return m_sMimeType;
    }

    @Nullable
    public String getUncompressedMimeType ()
    {
      return m_sUncompressedMimeType;
    }

    @Nullable
    public Charset getCharset ()
    {
      return m_aCharset;
    }

    @Nullable
    public EAS4CompressionMode getCompressionMode ()
    {
      return m_eCompressionMode;
    }

    @Nonnull
    @ReturnsMutableObject
    public byte [] payloadBytes ()
    {
      return m_aPayloadBytes;
    }

    @Nonnull
    @ReturnsMutableObject
    public StandardBusinessDocument standardBusinessDocument ()
    {
      return m_aSBDH;
    }
  }

  public static final ESMPTransportProfile DEFAULT_TRANSPORT_PROFILE = ESMPTransportProfile.TRANSPORT_PROFILE_PEPPOL_AS4_V2;

  private static final Logger LOGGER = LoggerFactory.getLogger (Phase4PeppolServletMessageProcessorSPI.class);

  private ICommonsList  m_aHandlers;
  private ISMPTransportProfile m_aTransportProfile = DEFAULT_TRANSPORT_PROFILE;
  private Phase4PeppolReceiverCheckData m_aReceiverCheckData;
  private ETriState m_eCheckSigningCertificateRevocation = ETriState.UNDEFINED;

  /**
   * Constructor. Uses all SPI implementations of
   * {@link IPhase4PeppolIncomingSBDHandlerSPI} as the handlers.
   */
  @UsedViaReflection
  public Phase4PeppolServletMessageProcessorSPI ()
  {
    m_aHandlers = ServiceLoaderHelper.getAllSPIImplementations (IPhase4PeppolIncomingSBDHandlerSPI.class);
  }

  /**
   * @return A list of all contained Peppol specific SBD handlers. Never
   *         null but maybe empty.
   */
  @Nonnull
  @ReturnsMutableCopy
  public final ICommonsList  getAllHandler ()
  {
    return m_aHandlers.getClone ();
  }

  /**
   * Set all handler to be used. This is helpful, if this message processor is
   * not used as an SPI but as a manually configured handler.
   *
   * @param aHandlers
   *        The handler to be set. May not be null but maybe empty
   *        (in which case the message is basically discarded).
   * @return this for chaining
   */
  @Nonnull
  public final Phase4PeppolServletMessageProcessorSPI setAllHandler (@Nonnull final Iterable  aHandlers)
  {
    ValueEnforcer.notNull (aHandlers, "Handlers");
    m_aHandlers = new CommonsArrayList <> (aHandlers);
    if (m_aHandlers.isEmpty ())
      LOGGER.warn ("Phase4PeppolServletMessageProcessorSPI has an empty handler list - this means incoming messages are only checked and afterwards discarded");
    return this;
  }

  /**
   * @return the transport profile to be handled. Never null. By
   *         default it is "Peppol AS4 v2" (see
   *         {@link #DEFAULT_TRANSPORT_PROFILE}).
   */
  @Nonnull
  public final ISMPTransportProfile getTransportProfile ()
  {
    return m_aTransportProfile;
  }

  /**
   * Set the transport profile to be used. By default it is Peppol AS4 v2.
   *
   * @param aTransportProfile
   *        The transport profile to be used. May not be null.
   * @return this for chaining
   */
  @Nonnull
  public final Phase4PeppolServletMessageProcessorSPI setTransportProfile (@Nonnull final ISMPTransportProfile aTransportProfile)
  {
    ValueEnforcer.notNull (aTransportProfile, "TransportProfile");
    m_aTransportProfile = aTransportProfile;
    return this;
  }

  /**
   * @return The receiver check data to be used. null by default.
   * @since 0.9.13
   */
  @Nullable
  public final Phase4PeppolReceiverCheckData getReceiverCheckData ()
  {
    return m_aReceiverCheckData;
  }

  /**
   * Set the receiver check data to be used. If set, it overrides the global one
   * defined by {@link Phase4PeppolServletConfiguration}.
   *
   * @param aReceiverCheckData
   *        The customer receiver check data to use. May be null.
   * @return this for chaining
   * @since 0.9.13
   */
  @Nonnull
  public final Phase4PeppolServletMessageProcessorSPI setReceiverCheckData (@Nullable final Phase4PeppolReceiverCheckData aReceiverCheckData)
  {
    m_aReceiverCheckData = aReceiverCheckData;
    return this;
  }

  /**
   * @return Whether the X.509 certificate used for signing the message should
   *         be check for revocation or not. By default it is "UNDEFINED"
   *         meaning, that the global setting is used instead. May not be
   *         null.
   * @since 2.7.1
   * @see Phase4PeppolServletConfiguration#isCheckSigningCertificateRevocation()
   *      for the global setting
   */
  @Nonnull
  public final ETriState getCheckSigningCertificateRevocation ()
  {
    return m_eCheckSigningCertificateRevocation;
  }

  /**
   * Set whether the signing X.509 certificate should be checked for revocation
   * or not.
   *
   * @param eCheckSigningCertificateRevocation
   *        The signing certificate revocation state. May not be
   *        null.
   * @return this for chaining
   * @since 2.7.1
   * @see Phase4PeppolServletConfiguration#setCheckSigningCertificateRevocation(boolean)
   *      to set this globally
   */
  @Nonnull
  public final Phase4PeppolServletMessageProcessorSPI setCheckSigningCertificateRevocation (@Nonnull final ETriState eCheckSigningCertificateRevocation)
  {
    ValueEnforcer.notNull (eCheckSigningCertificateRevocation, "CheckSigningCertificateRevocation");
    m_eCheckSigningCertificateRevocation = eCheckSigningCertificateRevocation;
    return this;
  }

  @Nullable
  private EndpointType _getReceiverEndpoint (@Nonnull final String sLogPrefix,
                                             @Nonnull final ISMPServiceMetadataProvider aSMPClient,
                                             @Nullable final IParticipantIdentifier aRecipientID,
                                             @Nullable final IDocumentTypeIdentifier aDocTypeID,
                                             @Nullable final IProcessIdentifier aProcessID,
                                             @Nullable final PeppolWildcardSelector.EMode eWildcardSelectionMode) throws Phase4PeppolServletException
  {
    if (aRecipientID == null || aDocTypeID == null || aProcessID == null || eWildcardSelectionMode == null)
      return null;

    try
    {
      if (LOGGER.isDebugEnabled ())
      {
        LOGGER.debug (sLogPrefix +
                      "Looking up the endpoint of recipient " +
                      aRecipientID.getURIEncoded () +
                      " for " +
                      aRecipientID.getURIEncoded () +
                      " and " +
                      aDocTypeID.getURIEncoded () +
                      " and " +
                      aProcessID.getURIEncoded () +
                      " and " +
                      m_aTransportProfile.getID () +
                      "; wildcard-mode=" +
                      eWildcardSelectionMode);
      }

      final EndpointType aEndpoint;
      final boolean bWildcard = PeppolIdentifierHelper.DOCUMENT_TYPE_SCHEME_PEPPOL_DOCTYPE_WILDCARD.equals (aDocTypeID.getScheme ());
      if (bWildcard)
      {
        // Wildcard lookup
        final SignedServiceMetadataType aSSM = aSMPClient.getWildcardServiceMetadataOrNull (aRecipientID,
                                                                                            aDocTypeID,
                                                                                            eWildcardSelectionMode);
        aEndpoint = aSSM == null ? null : SMPClientReadOnly.getEndpoint (aSSM, aProcessID, m_aTransportProfile);
      }
      else
      {
        // Direct match
        aEndpoint = aSMPClient.getEndpoint (aRecipientID, aDocTypeID, aProcessID, m_aTransportProfile);
      }
      return aEndpoint;
    }
    catch (final Exception ex)
    {
      throw new Phase4PeppolServletException (sLogPrefix +
                                              "Failed to retrieve endpoint of (" +
                                              aRecipientID.getURIEncoded () +
                                              ", " +
                                              aDocTypeID.getURIEncoded () +
                                              ", " +
                                              aProcessID.getURIEncoded () +
                                              ", " +
                                              m_aTransportProfile.getID () +
                                              ")",
                                              ex);
    }
  }

  private static void _checkIfReceiverEndpointURLMatches (@Nonnull final String sLogPrefix,
                                                          @Nonnull @Nonempty final String sOwnAPUrl,
                                                          @Nonnull final EndpointType aRecipientEndpoint) throws Phase4PeppolServletException
  {
    if (LOGGER.isDebugEnabled ())
      LOGGER.debug (sLogPrefix + "Our AP URL is " + sOwnAPUrl);

    final String sRecipientAPUrl = SMPClientReadOnly.getEndpointAddress (aRecipientEndpoint);
    if (LOGGER.isDebugEnabled ())
      LOGGER.debug (sLogPrefix + "Recipient AP URL from SMP is " + sRecipientAPUrl);

    // Is it for us?
    if (sRecipientAPUrl == null || !sRecipientAPUrl.contains (sOwnAPUrl))
    {
      final String sErrorMsg = sLogPrefix +
                               "Internal error: The request is targeted for '" +
                               sRecipientAPUrl +
                               "' and is not for us (" +
                               sOwnAPUrl +
                               ")";
      LOGGER.error (sErrorMsg);
      throw new Phase4PeppolServletException (sErrorMsg);
    }
  }

  private static void _checkIfEndpointCertificateMatches (@Nonnull final String sLogPrefix,
                                                          @Nonnull final X509Certificate aOurCert,
                                                          @Nonnull final EndpointType aRecipientEndpoint) throws Phase4PeppolServletException
  {
    final String sRecipientCertString = aRecipientEndpoint.getCertificate ();
    X509Certificate aRecipientCert = null;
    try
    {
      aRecipientCert = CertificateHelper.convertStringToCertficate (sRecipientCertString);
    }
    catch (final CertificateException t)
    {
      throw new Phase4PeppolServletException (sLogPrefix +
                                              "Internal error: Failed to convert looked up endpoint certificate string '" +
                                              sRecipientCertString +
                                              "' to an X.509 certificate!",
                                              t);
    }

    if (aRecipientCert == null)
    {
      // No certificate found - most likely because of invalid SMP entry
      throw new Phase4PeppolServletException (sLogPrefix +
                                              "No certificate found in looked up endpoint! Is this AP maybe NOT contained in an SMP?");
    }

    // Certificate found
    if (LOGGER.isDebugEnabled ())
      LOGGER.debug (sLogPrefix + "Conformant recipient certificate present: " + aRecipientCert.toString ());

    // Compare serial numbers
    if (!aOurCert.getSerialNumber ().equals (aRecipientCert.getSerialNumber ()))
    {
      final String sErrorMsg = sLogPrefix +
                               "Certificate retrieved from SMP lookup (" +
                               aRecipientCert +
                               ") does not match this APs configured Certificate (" +
                               aOurCert +
                               ") - different serial numbers - ignoring document";
      LOGGER.error (sErrorMsg);
      throw new Phase4PeppolServletException (sErrorMsg);
    }

    if (LOGGER.isDebugEnabled ())
      LOGGER.debug (sLogPrefix + "The certificate of the SMP lookup matches our certificate");
  }

  /**
   * Method to create a new {@link PeppolReportingItem} for a received message.
   *
   * @param aUserMessage
   *        The current AS4 UserMessage. May not be null.
   * @param aPeppolSBD
   *        The parsed Peppol SBDH object. May not be null.
   * @param aState
   *        The processing state of the incoming message. May not be
   *        null.
   * @param sC3ID
   *        The Peppol Service Provider Seat ID (in the format PXX000000). May
   *        neither be null nor empty.
   * @param sC4CountryCode
   *        The country code of the End User that is the business receiver of
   *        the document. Must neither be null nor empty.
   * @param sEndUserID
   *        The internal (local) ID of the End User that is the business
   *        receiver of the document. This ID is NOT part of the reporting
   *        towards OpenPeppol, it is just for created aggregating counts. Must
   *        neither be null nor empty.
   * @return null if not all necessary elements are present. Check
   *         logs for details.
   * @since 2.2.2
   */
  @Nullable
  public static PeppolReportingItem createPeppolReportingItemForReceivedMessage (@Nonnull final Ebms3UserMessage aUserMessage,
                                                                                 @Nonnull final PeppolSBDHData aPeppolSBD,
                                                                                 @Nonnull final IAS4MessageState aState,
                                                                                 @Nonnull @Nonempty final String sC3ID,
                                                                                 @Nonnull @Nonempty final String sC4CountryCode,
                                                                                 @Nonnull @Nonempty final String sEndUserID)
  {
    ValueEnforcer.notNull (aUserMessage, "UserMessage");
    ValueEnforcer.notNull (aPeppolSBD, "PeppolSBD");
    ValueEnforcer.notNull (aState, "State");
    ValueEnforcer.notEmpty (sC3ID, "C3ID");
    ValueEnforcer.notEmpty (sC4CountryCode, "C4CountryCode");
    ValueEnforcer.notEmpty (sEndUserID, "EndUserID");

    // In case of success start building reporting item
    final XMLOffsetDateTime aUserMsgDT = aUserMessage.getMessageInfo ().getTimestamp ();
    final OffsetDateTime aExchangeDT;
    if (aUserMsgDT != null)
    {
      // Take AS4 sending date time
      if (aUserMsgDT.getOffset () != null)
        aExchangeDT = aUserMsgDT.toOffsetDateTime ();
      else
        aExchangeDT = OffsetDateTime.of (aUserMsgDT.toLocalDateTime (), ZoneOffset.UTC);
    }
    else
    {
      // Try SBDH
      final XMLOffsetDateTime aSbdhDT = aPeppolSBD.getCreationDateAndTime ();
      if (aSbdhDT != null)
      {
        // Take SBDH creation date time
        if (aSbdhDT.getOffset () != null)
          aExchangeDT = aSbdhDT.toOffsetDateTime ();
        else
          aExchangeDT = OffsetDateTime.of (aSbdhDT.toLocalDateTime (), ZoneOffset.UTC);
      }
      else
      {
        // Neither in AS4 nor in SBDH
        LOGGER.warn ("Incoming messages does not contain a UserMessage/MessageInfo/Timestamp value and no SBDH CreationDateTime. Using current date time");
        aExchangeDT = MetaAS4Manager.getTimestampMgr ().getCurrentDateTime ();
      }
    }

    // Incoming message signed by C2
    final String sC2ID = PeppolCertificateHelper.getSubjectCN (aState.getUsedCertificate ());

    try
    {
      String sC1CountryCode = aPeppolSBD.getCountryC1 ();
      if (StringHelper.hasNoText (sC1CountryCode))
      {
        // Fallback to ZZ to make sure the item can be created
        sC1CountryCode = CPeppolReporting.REPLACEMENT_COUNTRY_CODE;
      }

      return PeppolReportingItem.builder ()
                                .exchangeDateTime (aExchangeDT)
                                .directionReceiving ()
                                .c2ID (sC2ID)
                                .c3ID (sC3ID)
                                .docTypeID (aPeppolSBD.getDocumentTypeAsIdentifier ())
                                .processID (aPeppolSBD.getProcessAsIdentifier ())
                                .transportProtocolPeppolAS4v2 ()
                                .c1CountryCode (sC1CountryCode)
                                .c4CountryCode (sC4CountryCode)
                                .endUserID (sEndUserID)
                                .build ();
    }
    catch (final IllegalStateException ex)
    {
      LOGGER.error ("Not all mandatory fields are set. Cannot create Peppol Reporting Item", ex);
      return null;
    }
  }

  /**
   * Method that is invoked after the message was successfully processed with at
   * least one handler, and before a Receipt is returned. By default this method
   * does nothing. The idea was to override this method to allow for remembering
   * the created transaction for Peppol Reporting.
   *
   * @param aUserMessage
   *        The current AS4 UserMessage. Never null.
   * @param aPeppolSBD
   *        The parsed Peppol SBDH object. Never null.
   * @param aState
   *        The processing state of the incoming message. Never
   *        null.
   * @since 2.2.2
   * @see #createPeppolReportingItemForReceivedMessage(Ebms3UserMessage,
   *      PeppolSBDHData, IAS4MessageState, String, String, String)
   */
  @OverrideOnDemand
  protected void afterSuccessfulPeppolProcessing (@Nonnull final Ebms3UserMessage aUserMessage,
                                                  @Nonnull final PeppolSBDHData aPeppolSBD,
                                                  @Nonnull final IAS4MessageState aState)
  {}

  @Nonnull
  public AS4MessageProcessorResult processAS4UserMessage (@Nonnull final IAS4IncomingMessageMetadata aMessageMetadata,
                                                          @Nonnull final HttpHeaderMap aHttpHeaders,
                                                          @Nonnull final Ebms3UserMessage aUserMessage,
                                                          @Nonnull final IPMode aSrcPMode,
                                                          @Nullable final Node aPayload,
                                                          @Nullable final ICommonsList  aIncomingAttachments,
                                                          @Nonnull final IAS4MessageState aState,
                                                          @Nonnull final ICommonsList  aProcessingErrorMessages)
  {
    if (LOGGER.isDebugEnabled ())
      LOGGER.debug ("Invoking processAS4UserMessage");

    final String sMessageID = aUserMessage.getMessageInfo ().getMessageId ();
    final String sService = aUserMessage.getCollaborationInfo ().getServiceValue ();
    final String sAction = aUserMessage.getCollaborationInfo ().getAction ();
    final String sConversationID = aUserMessage.getCollaborationInfo ().getConversationId ();
    final Locale aDisplayLocale = aState.getLocale ();
    final String sLogPrefix = "[" + sMessageID + "] ";

    // Debug log
    if (LOGGER.isDebugEnabled ())
    {
      if (aSrcPMode == null)
        LOGGER.debug (sLogPrefix + "  No Source PMode present");
      else
        LOGGER.debug (sLogPrefix + "  Source PMode = " + aSrcPMode.getID ());
      LOGGER.debug (sLogPrefix + "  AS4 Message ID = '" + sMessageID + "'");
      LOGGER.debug (sLogPrefix + "  AS4 Service = '" + sService + "'");
      LOGGER.debug (sLogPrefix + "  AS4 Action = '" + sAction + "'");
      LOGGER.debug (sLogPrefix + "  AS4 ConversationId = '" + sConversationID + "'");

      // Log source properties
      if (aUserMessage.getMessageProperties () != null && aUserMessage.getMessageProperties ().hasPropertyEntries ())
      {
        LOGGER.debug (sLogPrefix + "  AS4 MessageProperties:");
        for (final Ebms3Property p : aUserMessage.getMessageProperties ().getProperty ())
          LOGGER.debug (sLogPrefix + "    [" + p.getName () + "] = [" + p.getValue () + "]");
      }
      else
        LOGGER.debug (sLogPrefix + "  No AS4 Mesage Properties present");

      if (aPayload == null)
        LOGGER.debug (sLogPrefix + "  No SOAP Body Payload present");
      else
        LOGGER.debug (sLogPrefix + "  SOAP Body Payload = " + XMLWriter.getNodeAsString (aPayload));
    }

    // Check preconditions
    if (!aState.isSoapDecrypted ())
    {
      final String sDetails = "The received Peppol message seems not to be encrypted (properly).";
      LOGGER.error (sLogPrefix + sDetails);
      aProcessingErrorMessages.add (EEbmsError.EBMS_FAILED_DECRYPTION.errorBuilder (aDisplayLocale)
                                                                     .refToMessageInError (sMessageID)
                                                                     .errorDetail (sDetails)
                                                                     .build ());
      return AS4MessageProcessorResult.createFailure ();
    }
    if (!aState.isSoapSignatureChecked ())
    {
      final String sDetails = "The received Peppol message seems not to be signed (properly).";
      LOGGER.error (sLogPrefix + sDetails);
      aProcessingErrorMessages.add (EEbmsError.EBMS_FAILED_AUTHENTICATION.errorBuilder (aDisplayLocale)
                                                                         .refToMessageInError (sMessageID)
                                                                         .errorDetail (sDetails)
                                                                         .build ());
      return AS4MessageProcessorResult.createFailure ();
    }

    if (getCheckSigningCertificateRevocation ().getAsBooleanValue (Phase4PeppolServletConfiguration.isCheckSigningCertificateRevocation ()))
    {
      final OffsetDateTime aNow = MetaAS4Manager.getTimestampMgr ().getCurrentDateTime ();
      final X509Certificate aSenderCert = aState.getUsedCertificate ();
      // Check if signing AP certificate is revoked
      // * Use global caching setting
      // * Use global certificate check mode
      final EPeppolCertificateCheckResult eCertCheckResult = PeppolCertificateChecker.checkPeppolAPCertificate (aSenderCert,
                                                                                                                aNow,
                                                                                                                ETriState.UNDEFINED,
                                                                                                                null);
      if (eCertCheckResult.isInvalid ())
      {
        final String sDetails = "The received Peppol message is signed with a Peppol AP certificate invalid at " +
                                aNow +
                                ". Rejecting incoming message. Reason: " +
                                eCertCheckResult.getReason ();
        LOGGER.error (sLogPrefix + sDetails);
        aProcessingErrorMessages.add (EEbmsError.EBMS_FAILED_AUTHENTICATION.errorBuilder (aDisplayLocale)
                                                                           .refToMessageInError (sMessageID)
                                                                           .errorDetail (sDetails)
                                                                           .build ());
        return AS4MessageProcessorResult.createFailure ();
      }
    }
    else
    {
      LOGGER.warn (sLogPrefix + "The revocation check of the received signing certificate is disabled.");
    }

    // Read all attachments
    final ICommonsList  aReadAttachments = new CommonsArrayList <> ();
    if (aIncomingAttachments != null)
    {
      int nAttachmentIndex = 0;
      for (final IAS4Attachment aIncomingAttachment : aIncomingAttachments)
      {
        final ReadAttachment a = new ReadAttachment ();
        a.m_sID = aIncomingAttachment.getId ();
        a.m_sMimeType = aIncomingAttachment.getMimeType ();
        a.m_sUncompressedMimeType = aIncomingAttachment.getUncompressedMimeType ();
        a.m_aCharset = aIncomingAttachment.getCharset ();
        a.m_eCompressionMode = aIncomingAttachment.getCompressionMode ();
        try (final InputStream aSIS = aIncomingAttachment.getSourceStream ())
        {
          final NonBlockingByteArrayOutputStream aBAOS = new NonBlockingByteArrayOutputStream ();
          if (StreamHelper.copyInputStreamToOutputStreamAndCloseOS (aSIS, aBAOS).isSuccess ())
          {
            a.m_aPayloadBytes = aBAOS.getBufferOrCopy ();
          }
        }
        catch (final IOException | AS4DecompressException ex)
        {
          // Fall through
        }
        if (a.m_aPayloadBytes == null)
        {
          final String sDetails = "Failed to decompress the payload of attachment #" + nAttachmentIndex;
          LOGGER.error (sLogPrefix + sDetails);
          aProcessingErrorMessages.add (EEbmsError.EBMS_DECOMPRESSION_FAILURE.errorBuilder (aDisplayLocale)
                                                                             .refToMessageInError (sMessageID)
                                                                             .errorDetail (sDetails)
                                                                             .build ());
          return AS4MessageProcessorResult.createFailure ();
        }

        // Read data as SBDH
        // Hint for production systems: this may take a huge amount of memory,
        // if the payload is large
        final ErrorList aSBDHErrors = new ErrorList ();
        a.m_aSBDH = new SBDMarshaller ().setCollectErrors (aSBDHErrors).read (a.m_aPayloadBytes);

        // Only fail if the first attachment is not an SBDH. The check for
        // exactly 1 attachment comes below
        if (nAttachmentIndex == 0 && a.m_aSBDH == null)
        {
          if (aSBDHErrors.isEmpty ())
          {
            final String sDetails = "Failed to read the provided SBDH document";
            LOGGER.error (sLogPrefix + sDetails);
            aProcessingErrorMessages.add (EEbmsError.EBMS_OTHER.errorBuilder (aDisplayLocale)
                                                               .refToMessageInError (sMessageID)
                                                               .errorDetail (sDetails)
                                                               .build ());
          }
          else
          {
            for (final IError aError : aSBDHErrors)
            {
              final String sDetails = "Peppol SBDH Issue: " + aError.getAsString (aDisplayLocale);
              LOGGER.error (sLogPrefix + sDetails);
              aProcessingErrorMessages.add (EEbmsError.EBMS_OTHER.errorBuilder (aDisplayLocale)
                                                                 .refToMessageInError (sMessageID)
                                                                 .errorDetail (sDetails)
                                                                 .build ());
            }
          }

          return AS4MessageProcessorResult.createFailure ();
        }

        aReadAttachments.add (a);

        if (LOGGER.isDebugEnabled ())
          LOGGER.debug (sLogPrefix +
                        "AS4 Attachment " +
                        nAttachmentIndex +
                        " with ID [" +
                        a.m_sID +
                        "] uses [" +
                        a.m_sMimeType +
                        (a.m_sUncompressedMimeType == null ? null : " - uncompressed " + a.m_sUncompressedMimeType) +
                        "] and [" +
                        StringHelper.getToString (a.m_aCharset, "no charset") +
                        "] and length is " +
                        (a.m_aPayloadBytes == null ? "" : Integer.toString (a.m_aPayloadBytes.length)) +
                        " bytes" +
                        (a.m_eCompressionMode == null ? "" : " of compressed payload"));
        nAttachmentIndex++;
      }
    }

    if (aReadAttachments.size () != 1)
    {
      // In Peppol there must be exactly one payload
      final String sDetails = "In Peppol exactly one payload attachment is expected. This request has " +
                              aReadAttachments.size () +
                              " attachments";
      LOGGER.error (sLogPrefix + sDetails);
      aProcessingErrorMessages.add (EEbmsError.EBMS_OTHER.errorBuilder (aDisplayLocale)
                                                         .refToMessageInError (aState.getMessageID ())
                                                         .errorDetail (sDetails)
                                                         .build ());
      return AS4MessageProcessorResult.createFailure ();
    }

    // The one and only
    final ReadAttachment aReadAttachment = aReadAttachments.getFirstOrNull ();

    // Extract Peppol values from SBD
    final PeppolSBDHData aPeppolSBD;
    try
    {
      if (LOGGER.isDebugEnabled ())
        LOGGER.debug (sLogPrefix + "Now evaluating the SBDH against Peppol rules");

      // Interpret as Peppol SBDH and eventually perform consistency checks
      final boolean bPerformValueChecks = Phase4PeppolServletConfiguration.isPerformSBDHValueChecks ();
      final boolean bCheckForCountryC1 = Phase4PeppolServletConfiguration.isCheckSBDHForMandatoryCountryC1 ();
      // Read with SimpleIdentifierFactory - accepts more the
      // PeppolIdentifierFactory
      final PeppolSBDHDocumentReader aReader = new PeppolSBDHDocumentReader (SimpleIdentifierFactory.INSTANCE).setPerformValueChecks (bPerformValueChecks)
                                                                                                              .setCheckForCountryC1 (bCheckForCountryC1);

      aPeppolSBD = aReader.extractData (aReadAttachment.standardBusinessDocument ());

      if (LOGGER.isDebugEnabled ())
        LOGGER.debug (sLogPrefix +
                      "The provided SBDH is valid according to Peppol rules, with value checks being " +
                      (bPerformValueChecks ? "enabled" : "disabled"));
    }
    catch (final PeppolSBDHDocumentReadException ex)
    {
      final String sMsg = "Failed to extract the Peppol data from SBDH.";
      LOGGER.error (sLogPrefix + sMsg, ex);
      aProcessingErrorMessages.add (EEbmsError.EBMS_OTHER.errorBuilder (aDisplayLocale)
                                                         .refToMessageInError (aState.getMessageID ())
                                                         .errorDetail (sMsg, ex)
                                                         .build ());
      return AS4MessageProcessorResult.createFailure ();
    }

    {
      // Start consistency checks if the receiver is supported or not
      final Phase4PeppolReceiverCheckData aReceiverCheckData = m_aReceiverCheckData != null ? m_aReceiverCheckData
                                                                                            : Phase4PeppolServletConfiguration.getAsReceiverCheckData ();
      if (aReceiverCheckData != null)
      {
        LOGGER.info (sLogPrefix + "Performing checks if the received data is registered in our SMP");

        try
        {
          // Get the endpoint information required from the recipient
          // Check if an endpoint is registered
          final IParticipantIdentifier aReceiverID = aPeppolSBD.getReceiverAsIdentifier ();
          final IDocumentTypeIdentifier aDocTypeID = aPeppolSBD.getDocumentTypeAsIdentifier ();
          final IProcessIdentifier aProcessID = aPeppolSBD.getProcessAsIdentifier ();
          final EndpointType aReceiverEndpoint = _getReceiverEndpoint (sLogPrefix,
                                                                       aReceiverCheckData.getSMPClient (),
                                                                       aReceiverID,
                                                                       aDocTypeID,
                                                                       aProcessID,
                                                                       aReceiverCheckData.getWildcardSelectionMode ());
          if (aReceiverEndpoint == null)
          {
            final String sMsg = "Failed to resolve SMP endpoint for provided receiver ID (" +
                                (aReceiverID == null ? "null" : aReceiverID.getURIEncoded ()) +
                                ")/documentType ID (" +
                                (aDocTypeID == null ? "null" : aDocTypeID.getURIEncoded ()) +
                                ")/process ID (" +
                                (aProcessID == null ? "null" : aProcessID.getURIEncoded ()) +
                                ")/transport profile (" +
                                m_aTransportProfile.getID () +
                                ") - not handling incoming AS4 document";
            LOGGER.error (sLogPrefix + sMsg);
            // the errorDetail MUST be set according to Peppol AS4 profile 2.2
            aProcessingErrorMessages.add (EEbmsError.EBMS_OTHER.errorBuilder (aDisplayLocale)
                                                               .refToMessageInError (aState.getMessageID ())
                                                               .description (sMsg, aDisplayLocale)
                                                               .errorDetail ("PEPPOL:NOT_SERVICED")
                                                               .build ());
            return AS4MessageProcessorResult.createFailure ();
          }

          // Check if the message is for us
          _checkIfReceiverEndpointURLMatches (sLogPrefix, aReceiverCheckData.getAS4EndpointURL (), aReceiverEndpoint);

          // Get the recipient certificate from the SMP
          _checkIfEndpointCertificateMatches (sLogPrefix, aReceiverCheckData.getAPCertificate (), aReceiverEndpoint);
        }
        catch (final Phase4Exception ex)
        {
          final String sMsg = "The addressing data contained in the SBDH could not be verified";
          LOGGER.error (sLogPrefix + sMsg, ex);
          aProcessingErrorMessages.add (EEbmsError.EBMS_OTHER.errorBuilder (aDisplayLocale)
                                                             .refToMessageInError (aState.getMessageID ())
                                                             .errorDetail (sMsg, ex)
                                                             .build ());
          return AS4MessageProcessorResult.createFailure ();
        }
      }
      else
      {
        LOGGER.info (sLogPrefix + "Endpoint checks for incoming AS4 messages are disabled");
      }

      // Receiving checks are positively done
    }

    if (m_aHandlers.isEmpty ())
    {
      // Oops - programming error
      LOGGER.error (sLogPrefix + "No SPI handler is present - the message is unhandled and discarded!");
    }
    else
    {
      // Now start invoking SPI handlers
      for (final IPhase4PeppolIncomingSBDHandlerSPI aHandler : m_aHandlers)
      {
        try
        {
          if (LOGGER.isDebugEnabled ())
            LOGGER.debug (sLogPrefix + "Invoking Peppol handler " + aHandler);
          aHandler.handleIncomingSBD (aMessageMetadata,
                                      aHttpHeaders.getClone (),
                                      aUserMessage.clone (),
                                      aReadAttachment.payloadBytes (),
                                      aReadAttachment.standardBusinessDocument (),
                                      aPeppolSBD,
                                      aState,
                                      aProcessingErrorMessages);
        }
        catch (final Phase4PeppolClientException ex)
        {
          final String sDetails = "Error invoking Peppol handler " + aHandler;
          LOGGER.error (sLogPrefix + sDetails, ex);
          aProcessingErrorMessages.add (EEbmsError.EBMS_OTHER.errorBuilder (aDisplayLocale)
                                                             .refToMessageInError (aState.getMessageID ())
                                                             .errorDetail (sDetails, ex)
                                                             .build ());

          // Returned AS4 Error without a custom prefix
          return AS4MessageProcessorResult.createFailure ();
        }
        catch (final Exception ex)
        {
          LOGGER.error (sLogPrefix + "Error invoking Peppol handler " + aHandler, ex);
          if (aHandler.exceptionTranslatesToAS4Error ())
          {
            final String sDetails = "The incoming Peppol message could not be processed.";
            LOGGER.error (sLogPrefix + sDetails, ex);
            aProcessingErrorMessages.add (EEbmsError.EBMS_OTHER.errorBuilder (aDisplayLocale)
                                                               .refToMessageInError (aState.getMessageID ())
                                                               .errorDetail (sDetails, ex)
                                                               .build ());
            return AS4MessageProcessorResult.createFailure ();
          }
        }
      }

      // Trigger post-processing, e.g. for reporting
      afterSuccessfulPeppolProcessing (aUserMessage, aPeppolSBD, aState);
    }

    return AS4MessageProcessorResult.createSuccess ();
  }

  @Nonnull
  @UnsupportedOperation
  public AS4SignalMessageProcessorResult processAS4SignalMessage (@Nonnull final IAS4IncomingMessageMetadata aMessageMetadata,
                                                                  @Nonnull final HttpHeaderMap aHttpHeaders,
                                                                  @Nonnull final Ebms3SignalMessage aSignalMessage,
                                                                  @Nullable final IPMode aPMode,
                                                                  @Nonnull final IAS4MessageState aState,
                                                                  @Nonnull final ICommonsList  aProcessingErrorMessages)
  {
    LOGGER.error ("Invoking processAS4SignalMessage is not supported");
    throw new UnsupportedOperationException ();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy