eu.peppol.as2.InboundMessageReceiver.java Source code

Java tutorial

Introduction

Here is the source code for eu.peppol.as2.InboundMessageReceiver.java

Source

/*
 * Copyright (c) 2010 - 2015 Norwegian Agency for Pupblic Government and eGovernment (Difi)
 *
 * This file is part of Oxalis.
 *
 * Licensed under the EUPL, Version 1.1 or  as soon they will be approved by the European Commission
 * - subsequent versions of the EUPL (the "Licence"); You may not use this work except in compliance with the Licence.
 *
 * You may obtain a copy of the Licence at:
 *
 * https://joinup.ec.europa.eu/software/page/eupl5
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the Licence
 *  is distributed on an "AS IS" basis,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licence for the specific language governing permissions and limitations under the Licence.
 *
 */

package eu.peppol.as2;

import com.google.inject.Inject;
import eu.peppol.MessageDigestResult;
import eu.peppol.PeppolMessageMetaData;
import eu.peppol.PeppolStandardBusinessHeader;
import eu.peppol.as2.evidence.As2TransmissionEvidenceFactory;
import eu.peppol.as2.servlet.ResponseData;
import eu.peppol.document.PayloadDigestCalculator;
import eu.peppol.document.Sbdh2PeppolHeaderConverter;
import eu.peppol.document.SbdhFastParser;
import eu.peppol.evidence.TransmissionEvidence;
import eu.peppol.identifier.AccessPointIdentifier;
import eu.peppol.persistence.MessageRepository;
import eu.peppol.persistence.OxalisMessagePersistenceException;
import eu.peppol.security.OxalisCertificateValidator;
import eu.peppol.start.identifier.ChannelId;
import eu.peppol.statistics.RawStatistics;
import eu.peppol.statistics.RawStatisticsRepository;
import eu.peppol.util.OxalisConstant;
import eu.peppol.xsd.ticc.receipt._1.TransmissionRole;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.unece.cefact.namespaces.standardbusinessdocumentheader.StandardBusinessDocumentHeader;

import javax.mail.MessagingException;
import javax.mail.internet.InternetHeaders;
import javax.mail.internet.MimeMessage;
import javax.security.auth.x500.X500Principal;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;

/**
 * Main entry point for receiving AS2 messages.
 *
 * @author steinar
 * @author thore
 */
public class InboundMessageReceiver {

    public static final Logger log = LoggerFactory.getLogger(InboundMessageReceiver.class);

    private final As2MessageInspector as2MessageInspector;
    private final MdnMimeMessageFactory mdnMimeMessageFactory;
    private final SbdhFastParser sbdhFastParser;
    private final MessageRepository messageRepository;
    private final RawStatisticsRepository rawStatisticsRepository;
    private final AccessPointIdentifier ourAccessPointIdentifier;
    private final OxalisCertificateValidator oxalisCertificateValidator;
    private final As2TransmissionEvidenceFactory as2TransmissionEvidenceFactory;

    @Inject
    public InboundMessageReceiver(MdnMimeMessageFactory mdnMimeMessageFactory, SbdhFastParser sbdhFastParser,
            As2MessageInspector as2MessageInspector, MessageRepository messageRepository,
            RawStatisticsRepository rawStatisticsRepository, AccessPointIdentifier ourAccessPointIdentifier,
            OxalisCertificateValidator oxalisCertificateValidator,
            As2TransmissionEvidenceFactory as2TransmissionEvidenceFactory) {
        this.mdnMimeMessageFactory = mdnMimeMessageFactory;
        this.sbdhFastParser = sbdhFastParser;
        this.as2MessageInspector = as2MessageInspector;
        this.messageRepository = messageRepository;
        this.rawStatisticsRepository = rawStatisticsRepository;
        this.ourAccessPointIdentifier = ourAccessPointIdentifier;
        this.oxalisCertificateValidator = oxalisCertificateValidator;
        this.as2TransmissionEvidenceFactory = as2TransmissionEvidenceFactory;

        // Gives us access to BouncyCastle
        Security.addProvider(new BouncyCastleProvider());

        // Sanity checks
        if (messageRepository == null) {
            throw new IllegalArgumentException("messageRepository is a required argument in constructor");
        }
        if (mdnMimeMessageFactory == null) {
            throw new IllegalArgumentException("MdnMimeMessageFactory is required argument");
        }

    }

    /**
     * Receives an AS2 Message in the form of a map of headers together with the payload,
     * which is made available in an input stream
     * <p>
     * If persisting message to MessageRepository fails, we have to return negative MDN.
     *
     * @param httpHeaders the http headers received
     * @param inputStream supplies the actual data stream
     * @return MDN object to signal if everything is ok or if some error occurred while receiving
     * @throws ErrorWithMdnException if validation fails due to syntactic, semantic or other reasons.
     */
    public ResponseData receive(InternetHeaders httpHeaders, InputStream inputStream) {
        if (httpHeaders == null) {
            throw new IllegalArgumentException("httpHeaders required constructor argument");
        }
        if (inputStream == null) {
            throw new IllegalArgumentException("inputStream required constructor argument");
        }

        Mic mic = null;
        try {

            log.debug("Receiving message ..");

            // Inspects the eu.peppol.as2.As2Header.DISPOSITION_NOTIFICATION_OPTIONS

            // TODO: Move this tp As2MessageFactory.createAs2MessageFrom
            inspectDispositionNotificationOptions(httpHeaders);

            log.debug(
                    "Message contains valid AS2 Disposition-notification-options, now creating internal AS2 message...");

            MimeMessage mimeMessage = MimeMessageHelper.createMimeMessageAssistedByHeaders(inputStream,
                    httpHeaders);
            SignedMimeMessage signedMimeMessage = new SignedMimeMessage(mimeMessage);

            // Transforms the input data into a proper As2Message
            As2Message as2Message = As2MessageFactory.createAs2MessageFrom(httpHeaders, signedMimeMessage);

            // Validates the message headers according to the PEPPOL rules and performs semantic validation
            log.debug("Validating AS2 Message: " + as2Message);
            as2MessageInspector.validate(as2Message);

            // Extracts the SBDH from the message, the SBDH is required by OpenPEPPOL
            StandardBusinessDocumentHeader sbdh = sbdhFastParser
                    .parse(as2Message.getSignedMimeMessage().getPayload());
            if (sbdh == null) {
                throw new IllegalStateException(
                        "Payload does not contain Standard Business Document Header (SBDH)");
            }

            // Calculates the message digest of the payload, there are two alternatives:
            // a) if the payload consists of SBDH + XML document -> calculate digest over entire payload
            // b) if payload consists of SBDH + ASiC -> calculate digest of binary ASiC archive only. I.e. without the SBDH and
            //    base64 decoded.
            MessageDigestResult payloadDigestResult = PayloadDigestCalculator.calcDigest(
                    OxalisConstant.DEFAULT_DIGEST_ALGORITHM, sbdh, as2Message.getSignedMimeMessage().getPayload());
            log.debug("The MessageDigest of the payload is "
                    + new String(Base64.encode(payloadDigestResult.getDigest())));

            // Persists the payload
            PeppolMessageMetaData peppolMessageMetaData = persistPayload(sbdh, messageRepository, as2Message);

            // Creates the MDN data to be returned (not the actual MDN, which must be represented as an S/MIME message)
            // Calculates the MIC for the payload using the preferred mic algorithm
            String micAlgorithmName = as2Message.getDispositionNotificationOptions()
                    .getPreferredSignedReceiptMicAlgorithmName();
            mic = as2Message.getSignedMimeMessage().calculateMic(micAlgorithmName);
            log.debug("Calculated MIC : " + mic.toString());
            MdnData mdnData = createMdnData(httpHeaders, mic, payloadDigestResult);

            // Finally we persist the raw statistics data
            persistStatistics(rawStatisticsRepository, ourAccessPointIdentifier, peppolMessageMetaData);

            // Creates the S/MIME message to be returned to the sender
            MimeMessage signedMdn = mdnMimeMessageFactory.createSignedMdn(mdnData, httpHeaders);
            try {
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                signedMdn.writeTo(byteArrayOutputStream);
                messageRepository.saveNativeTransportReceipt(byteArrayOutputStream.toByteArray());
            } catch (IOException | MessagingException e) {
                log.error("Unable to write signed mdn to byte array:" + e.getMessage(), e);
            }

            // Creates the REM evidence and persists it
            TransmissionEvidence remWithMdnEvidence = as2TransmissionEvidenceFactory
                    .createRemWithMdnEvidence(mdnData, peppolMessageMetaData, signedMdn, TransmissionRole.C_3);
            messageRepository.saveTransportReceipt(remWithMdnEvidence, peppolMessageMetaData);

            // Returns the response to be emitted by whoever is calling us
            ResponseData responseData = new ResponseData(HttpServletResponse.SC_OK, signedMdn, mdnData);
            return responseData;

        } catch (InvalidAs2MessageException | MdnRequestException | OxalisMessagePersistenceException e) {
            log.error("Invalid AS2 message: " + e.getMessage(), e);

            MdnData mdnData = MdnData.Builder.buildFailureFromHeaders(httpHeaders, mic, e.getMessage());
            MimeMessage signedMdn = mdnMimeMessageFactory.createSignedMdn(mdnData, httpHeaders);

            ResponseData responseDataWithErrors = new ResponseData(HttpServletResponse.SC_BAD_REQUEST, signedMdn,
                    mdnData);
            return responseDataWithErrors;
        }
    }

    protected PeppolMessageMetaData persistPayload(StandardBusinessDocumentHeader sbdh,
            MessageRepository messageRepository, As2Message as2Message) throws OxalisMessagePersistenceException {

        log.debug("Persisting AS2 Message ....");

        PeppolMessageMetaData peppolMessageMetaData = collectTransmissionMetaData(as2Message, sbdh);

        // Performs the actual persistence by invoking whatever has been configured for persistence
        messageRepository.saveInboundMessage(peppolMessageMetaData, as2Message.getSignedMimeMessage().getPayload());

        return peppolMessageMetaData;
    }

    /**
     * Creates a message digest using our preferred algorithm
     *
     * @return
     */
    MessageDigest createMessageDigest() {
        MessageDigest messageDigest;
        try {
            messageDigest = MessageDigest.getInstance(OxalisConstant.DEFAULT_DIGEST_ALGORITHM,
                    new BouncyCastleProvider());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("Unable to create MessageDigest object for algortihm : ", e);
        }
        return messageDigest;
    }

    protected MdnData createMdnData(InternetHeaders internetHeaders, Mic mic,
            MessageDigestResult messageDigestResult) {
        MdnData mdnData = MdnData.Builder.buildProcessedOK(internetHeaders, mic, messageDigestResult);
        log.debug("Message received OK, MDN returned will be: " + mdnData);
        return mdnData;
    }

    protected void persistStatistics(RawStatisticsRepository rawStatisticsRepository,
            AccessPointIdentifier ourAccessPointIdentifier, PeppolMessageMetaData peppolMessageMetaData) {
        // Persists raw statistics when message was received (ignore if stats couldn't be persisted, just warn)
        try {
            RawStatistics rawStatistics = new RawStatistics.RawStatisticsBuilder()
                    .accessPointIdentifier(ourAccessPointIdentifier).inbound()
                    .documentType(peppolMessageMetaData.getDocumentTypeIdentifier())
                    .sender(peppolMessageMetaData.getSenderId()).receiver(peppolMessageMetaData.getRecipientId())
                    .profile(peppolMessageMetaData.getProfileTypeIdentifier()).channel(new ChannelId("AS2"))
                    .build();
            rawStatisticsRepository.persist(rawStatistics);
        } catch (Exception e) {
            log.error(
                    "Unable to persist statistics for " + peppolMessageMetaData.toString() + "; " + e.getMessage(),
                    e);
            log.error("Message has been persisted and confirmation sent, but you must investigate this error");
        }
    }

    /**
     * Extracts data from the SBDH received, which we need for handling the message received.
     *
     * @param as2Message
     * @param sbdh
     * @return
     */
    PeppolMessageMetaData collectTransmissionMetaData(As2Message as2Message, StandardBusinessDocumentHeader sbdh) {

        // Converts the SBDH into a PEPPOL header
        PeppolStandardBusinessHeader peppolStandardBusinessHeader = Sbdh2PeppolHeaderConverter
                .convertSbdh2PeppolHeader(sbdh);

        PeppolMessageMetaData peppolMessageMetaData = new PeppolMessageMetaData();
        peppolMessageMetaData.setTransmissionId(as2Message.getTransmissionId());
        peppolMessageMetaData.setMessageId(peppolStandardBusinessHeader.getMessageId().toString());
        peppolMessageMetaData.setSenderId(peppolStandardBusinessHeader.getSenderId());
        peppolMessageMetaData.setRecipientId(peppolStandardBusinessHeader.getRecipientId());
        peppolMessageMetaData.setDocumentTypeIdentifier(peppolStandardBusinessHeader.getDocumentTypeIdentifier());
        peppolMessageMetaData.setProfileTypeIdentifier(peppolStandardBusinessHeader.getProfileTypeIdentifier());
        peppolMessageMetaData
                .setSendingAccessPointId(new AccessPointIdentifier(as2Message.getAs2From().toString()));
        peppolMessageMetaData.setReceivingAccessPoint(new AccessPointIdentifier(as2Message.getAs2To().toString()));

        // Retrieves the Common Name of the X500Principal, which is used to construct the AccessPointIdentifier for the senders access point
        X500Principal subjectX500Principal = as2Message.getSignedMimeMessage().getSignersX509Certificate()
                .getSubjectX500Principal();
        peppolMessageMetaData.setSendingAccessPointPrincipal(subjectX500Principal);

        return peppolMessageMetaData;

    }

    private void inspectDispositionNotificationOptions(InternetHeaders internetHeaders) throws MdnRequestException {
        String[] headerValue = internetHeaders
                .getHeader(As2Header.DISPOSITION_NOTIFICATION_OPTIONS.getHttpHeaderName());
        if (headerValue == null || headerValue[0] == null) {
            throw new MdnRequestException("AS2 header '"
                    + As2Header.DISPOSITION_NOTIFICATION_OPTIONS.getHttpHeaderName() + "' not found in request");
        }
        // Attempts to parseMultipart the Disposition Notification Options
        String value = headerValue[0];
        As2DispositionNotificationOptions as2DispositionNotificationOptions = As2DispositionNotificationOptions
                .valueOf(value);
        String micAlgorithm = as2DispositionNotificationOptions.getPreferredSignedReceiptMicAlgorithmName();
        if (!"sha1".equalsIgnoreCase(micAlgorithm)) {
            throw new MdnRequestException("Invalid MIC algorithm, only SHA1 supported:" + micAlgorithm);
        }
    }

}