eu.peppol.outbound.transmission.As2MessageSender.java Source code

Java tutorial

Introduction

Here is the source code for eu.peppol.outbound.transmission.As2MessageSender.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.outbound.transmission;

import com.google.inject.Inject;
import eu.peppol.as2.*;
import eu.peppol.as2.evidence.As2RemWithMdnTransmissionEvidenceImpl;
import eu.peppol.as2.evidence.As2TransmissionEvidenceFactory;
import eu.peppol.identifier.ParticipantId;
import eu.peppol.identifier.PeppolDocumentTypeId;
import eu.peppol.identifier.TransmissionId;
import eu.peppol.security.CommonName;
import eu.peppol.security.KeystoreManager;
import eu.peppol.smp.SmpLookupManager;
import eu.peppol.xsd.ticc.receipt._1.TransmissionRole;
import no.difi.vefa.peppol.common.model.DocumentTypeIdentifier;
import no.difi.vefa.peppol.common.model.ParticipantIdentifier;
import no.difi.vefa.peppol.evidence.rem.EventCode;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
import org.apache.http.util.EntityUtils;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.mail.MessagingException;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ProxySelector;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Date;
import java.util.Map;

/**
 * Thread safe implementation of a {@link MessageSender}, which sends messages using the AS2 protocol.
 * Stores the outbound MIC for verification against the mic received from the MDN later.
 *
 * @author steinar
 * @author thore
 */
class As2MessageSender implements MessageSender {

    public static final Logger log = LoggerFactory.getLogger(As2MessageSender.class);
    private final KeystoreManager keystoreManager;
    private final As2TransmissionEvidenceFactory as2TransmissionEvidenceFactory;

    private Mic mic;
    private boolean traceEnabled;

    @Inject
    public As2MessageSender(KeystoreManager keystoreManager,
            As2TransmissionEvidenceFactory as2TransmissionEvidenceFactory) {
        this.keystoreManager = keystoreManager;
        this.as2TransmissionEvidenceFactory = as2TransmissionEvidenceFactory;
    }

    @Override
    public TransmissionResponse send(TransmissionRequest transmissionRequest) {

        SmpLookupManager.PeppolEndpointData endpointAddress = transmissionRequest.getEndpointAddress();
        if (endpointAddress.getCommonName() == null) {
            throw new IllegalStateException(
                    "Must supply the X.509 common name (AS2 System Identifier) for AS2 protocol");
        }

        // did we enable additional tracing
        this.traceEnabled = transmissionRequest.isTraceEnabled();

        ByteArrayInputStream inputStream = new ByteArrayInputStream(transmissionRequest.getPayload());

        X509Certificate ourCertificate = keystoreManager.getOurCertificate();

        // Establishes our AS2 System Identifier based upon the contents of the CN= field of the certificate
        PeppolAs2SystemIdentifier as2SystemIdentifierOfSender = getAs2SystemIdentifierForSender(ourCertificate);

        SendResult sendResult = send(inputStream,
                transmissionRequest.getPeppolStandardBusinessHeader().getRecipientId(),
                transmissionRequest.getPeppolStandardBusinessHeader().getSenderId(),
                transmissionRequest.getPeppolStandardBusinessHeader().getDocumentTypeIdentifier(), endpointAddress,
                as2SystemIdentifierOfSender);

        return new As2TransmissionResponse(sendResult.transmissionId,
                transmissionRequest.getPeppolStandardBusinessHeader(), endpointAddress.getUrl(),
                endpointAddress.getBusDoxProtocol(), endpointAddress.getCommonName(), sendResult.evidenceBytes);
    }

    SendResult send(InputStream inputStream, ParticipantId recipient, ParticipantId sender,
            PeppolDocumentTypeId peppolDocumentTypeId, SmpLookupManager.PeppolEndpointData peppolEndpointData,
            PeppolAs2SystemIdentifier as2SystemIdentifierOfSender) {

        if (peppolEndpointData.getCommonName() == null) {
            throw new IllegalArgumentException("No common name in EndPoint object. " + peppolEndpointData);
        }
        X509Certificate ourCertificate = keystoreManager.getOurCertificate();

        SMimeMessageFactory sMimeMessageFactory = new SMimeMessageFactory(keystoreManager.getOurPrivateKey(),
                ourCertificate);
        MimeMessage signedMimeMessage = null;
        Mic mic = null;
        try {
            MimeBodyPart mimeBodyPart = MimeMessageHelper.createMimeBodyPart(inputStream,
                    new MimeType("application/xml"));
            mic = MimeMessageHelper.calculateMic(mimeBodyPart);
            log.debug("Outbound MIC is : " + mic.toString());
            signedMimeMessage = sMimeMessageFactory.createSignedMimeMessage(mimeBodyPart);
        } catch (MimeTypeParseException e) {
            throw new IllegalStateException("Problems with MIME types: " + e.getMessage(), e);
        }

        String endpointAddress = peppolEndpointData.getUrl().toExternalForm();
        HttpPost httpPost = new HttpPost(endpointAddress);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try {
            signedMimeMessage.writeTo(byteArrayOutputStream);

        } catch (Exception e) {
            throw new IllegalStateException("Unable to stream S/MIME message into byte array output stream");
        }

        httpPost.addHeader(As2Header.AS2_FROM.getHttpHeaderName(), as2SystemIdentifierOfSender.toString());
        try {
            httpPost.setHeader(As2Header.AS2_TO.getHttpHeaderName(),
                    PeppolAs2SystemIdentifier.valueOf(peppolEndpointData.getCommonName()).toString());
        } catch (InvalidAs2SystemIdentifierException e) {
            throw new IllegalArgumentException(
                    "Unable to create valid AS2 System Identifier for receiving end point: " + peppolEndpointData);
        }

        httpPost.addHeader(As2Header.DISPOSITION_NOTIFICATION_TO.getHttpHeaderName(), "not.in.use@difi.no");
        httpPost.addHeader(As2Header.DISPOSITION_NOTIFICATION_OPTIONS.getHttpHeaderName(),
                As2DispositionNotificationOptions.getDefault().toString());
        httpPost.addHeader(As2Header.AS2_VERSION.getHttpHeaderName(), As2Header.VERSION);
        httpPost.addHeader(As2Header.SUBJECT.getHttpHeaderName(), "AS2 message from OXALIS");

        TransmissionId transmissionId = new TransmissionId();
        httpPost.addHeader(As2Header.MESSAGE_ID.getHttpHeaderName(), transmissionId.toString());
        httpPost.addHeader(As2Header.DATE.getHttpHeaderName(), As2DateUtil.format(new Date()));

        // Inserts the S/MIME message to be posted.
        // Make sure we pass the same content type as the SignedMimeMessage, it'll end up as content-type HTTP header
        try {
            String contentType = signedMimeMessage.getContentType();
            ContentType ct = ContentType.create(contentType);
            httpPost.setEntity(new ByteArrayEntity(byteArrayOutputStream.toByteArray(), ct));
        } catch (Exception ex) {
            throw new IllegalStateException("Unable to set request header content type : " + ex.getMessage());
        }

        CloseableHttpResponse postResponse = null; // EXECUTE !!!!
        try {
            CloseableHttpClient httpClient = createCloseableHttpClient();
            log.debug("Sending AS2 from " + sender + " to " + recipient + " at " + endpointAddress + " type "
                    + peppolDocumentTypeId);
            postResponse = httpClient.execute(httpPost);
        } catch (HttpHostConnectException e) {
            throw new IllegalStateException("The Oxalis server does not seem to be running at " + endpointAddress);
        } catch (Exception e) {
            throw new IllegalStateException(
                    "Unexpected error during execution of http POST to " + endpointAddress + ": " + e.getMessage(),
                    e);
        }

        if (postResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
            log.error("AS2 HTTP POST expected HTTP OK, but got : " + postResponse.getStatusLine().getStatusCode()
                    + " from " + endpointAddress);
            throw handleFailedRequest(postResponse);
        }

        // handle normal HTTP OK response
        log.debug("AS2 transmission " + transmissionId + " to " + endpointAddress
                + " returned HTTP OK, verify MDN response");
        MimeMessage mimeMessage = handleTheHttpResponse(transmissionId, mic, postResponse, peppolEndpointData);

        // Transforms the signed MDN into a generic a As2RemWithMdnTransmissionEvidenceImpl
        MdnMimeMessageInspector mdnMimeMessageInspector = new MdnMimeMessageInspector(mimeMessage);
        Map<String, String> mdnFields = mdnMimeMessageInspector.getMdnFields();
        String messageDigestAsBase64 = mdnFields.get(MdnMimeMessageFactory.X_ORIGINAL_MESSAGE_DIGEST);
        if (messageDigestAsBase64 == null) {
            messageDigestAsBase64 = new String(Base64.getEncoder().encode("null".getBytes()));
        }
        String receptionTimeStampAsString = mdnFields.get(MdnMimeMessageFactory.X_PEPPOL_TIME_STAMP);
        Date receptionTimeStamp = null;
        if (receptionTimeStampAsString != null) {
            receptionTimeStamp = As2DateUtil.parseIso8601TimeStamp(receptionTimeStampAsString);
        } else {
            receptionTimeStamp = new Date();
        }

        // Converts the Oxalis DocumentTypeIdentifier into the corresponding type for peppol-evidence
        DocumentTypeIdentifier documentTypeIdentifier = new DocumentTypeIdentifier(peppolDocumentTypeId.toString());

        @NotNull
        As2RemWithMdnTransmissionEvidenceImpl evidence = as2TransmissionEvidenceFactory.createEvidence(
                EventCode.DELIVERY, TransmissionRole.C_2, mimeMessage,
                new ParticipantIdentifier(recipient.stringValue()), // peppol-evidence uses it's own types
                new ParticipantIdentifier(sender.stringValue()), // peppol-evidence uses it's own types
                documentTypeIdentifier, receptionTimeStamp, Base64.getDecoder().decode(messageDigestAsBase64),
                transmissionId);

        ByteArrayOutputStream evidenceBytes;
        try {
            evidenceBytes = new ByteArrayOutputStream();
            IOUtils.copy(evidence.getInputStream(), evidenceBytes);
        } catch (IOException e) {
            throw new IllegalStateException(
                    "Unable to transform transport evidence to byte array." + e.getMessage(), e);
        }

        return new SendResult(transmissionId, evidenceBytes.toByteArray());
    }

    /**
     * Handles the HTTP 200 POST response (the MDN with status indications)
     *
     * @param transmissionId the transmissionId (used in HTTP headers as Message-ID)
     * @param outboundMic    the calculated mic of the payload (should be verified against the one returned in MDN)
     * @param postResponse   the http response to be decoded as MDN
     * @return
     */
    MimeMessage handleTheHttpResponse(TransmissionId transmissionId, Mic outboundMic,
            CloseableHttpResponse postResponse, SmpLookupManager.PeppolEndpointData peppolEndpointData) {

        try {

            HttpEntity entity = postResponse.getEntity(); // Any textual results?
            if (entity == null) {
                throw new IllegalStateException(
                        "No contents in HTTP response with rc=" + postResponse.getStatusLine().getStatusCode());
            }

            String contents = EntityUtils.toString(entity);

            if (traceEnabled) {
                log.debug("HTTP-headers:");
                Header[] allHeaders = postResponse.getAllHeaders();
                for (Header header : allHeaders) {
                    log.debug("" + header.getName() + ": " + header.getValue());
                }
                log.debug("Contents:\n" + contents);
                log.debug("---------------------------");
            }

            Header contentTypeHeader = postResponse.getFirstHeader("Content-Type");
            if (contentTypeHeader == null) {
                throw new IllegalStateException("No Content-Type header in response, probably a server error");
            }
            String contentType = contentTypeHeader.getValue();

            MimeMessage mimeMessage = null;
            try {
                mimeMessage = MimeMessageHelper.parseMultipart(contents, new MimeType(contentType));

                try {
                    mimeMessage.writeTo(System.out);
                } catch (MessagingException e) {
                    throw new IllegalStateException("Unable to print mime message");
                }

            } catch (MimeTypeParseException e) {
                throw new IllegalStateException("Invalid Content-Type header");
            }

            // verify the signature of the MDN, we warn about dodgy signatures
            try {
                SignedMimeMessage signedMimeMessage = new SignedMimeMessage(mimeMessage);
                X509Certificate cert = signedMimeMessage.getSignersX509Certificate();
                cert.checkValidity();

                // Verify if the certificate used by the receiving Access Point in
                // the response message does not match its certificate published by the SMP
                if (peppolEndpointData.getCommonName() == null || !CommonName
                        .valueOf(cert.getSubjectX500Principal()).equals(peppolEndpointData.getCommonName())) {
                    throw new CertificateException(
                            "Common name in certificate from SMP does not match common name in AP certificate");
                }

                log.debug("MDN signature was verfied for : " + cert.getSubjectDN().toString());
            } catch (Exception ex) {
                log.warn("Exception when verifying MDN signature : " + ex.getMessage());
            }

            // Verifies the actual MDN
            MdnMimeMessageInspector mdnMimeMessageInspector = new MdnMimeMessageInspector(mimeMessage);
            String msg = mdnMimeMessageInspector.getPlainTextPartAsText();

            if (mdnMimeMessageInspector.isOkOrWarning(outboundMic)) {

                return mimeMessage;
            } else {
                log.error("AS2 transmission failed with some error message, msg :" + msg);
                log.error(contents);
                throw new IllegalStateException("AS2 transmission failed : " + msg);
            }

        } catch (IOException e) {
            throw new IllegalStateException("Unable to obtain the contents of the response: " + e.getMessage(), e);
        } finally {
            try {
                postResponse.close();
            } catch (IOException e) {
                throw new IllegalStateException("Unable to close http connection: " + e.getMessage(), e);
            }
        }

    }

    IllegalStateException handleFailedRequest(CloseableHttpResponse postResponse) {
        HttpEntity entity = postResponse.getEntity(); // Any results?
        try {
            if (entity == null) {
                // No content returned
                throw new IllegalStateException("Request failed with rc="
                        + postResponse.getStatusLine().getStatusCode() + ", no content returned in HTTP response");
            } else {
                String contents = EntityUtils.toString(entity);
                throw new IllegalStateException(
                        "Request failed with rc=" + postResponse.getStatusLine().getStatusCode()
                                + ", contents received (" + contents.trim().length() + " characters):" + contents);
            }
        } catch (IOException e) {
            throw new IllegalStateException("Request failed with rc=" + postResponse.getStatusLine().getStatusCode()
                    + ", ERROR while retrieving the contents of the response:" + e.getMessage(), e);
        }
    }

    private PeppolAs2SystemIdentifier getAs2SystemIdentifierForSender(X509Certificate ourCertificate) {
        PeppolAs2SystemIdentifier peppolAs2SystemIdentifier = null;
        try {
            peppolAs2SystemIdentifier = PeppolAs2SystemIdentifier
                    .valueOf(CommonName.valueOf(ourCertificate.getSubjectX500Principal()));
        } catch (InvalidAs2SystemIdentifierException e) {
            throw new IllegalStateException(
                    "AS2 System Identifier could not be obtained from " + ourCertificate.getSubjectX500Principal(),
                    e);
        }
        return peppolAs2SystemIdentifier;
    }

    CloseableHttpClient createCloseableHttpClient() {

        // "SSLv3" is disabled by default : http://www.apache.org/dist/httpcomponents/httpclient/RELEASE_NOTES-4.3.x.txt
        SystemDefaultRoutePlanner routePlanner = new SystemDefaultRoutePlanner(ProxySelector.getDefault());

        CloseableHttpClient httpclient = HttpClients.custom().setRoutePlanner(routePlanner).build();
        return httpclient;

    }

    private static class SendResult {
        final TransmissionId transmissionId;
        final byte[] evidenceBytes;

        public SendResult(TransmissionId transmissionId, byte[] evidenceBytes) {
            this.transmissionId = transmissionId;
            this.evidenceBytes = evidenceBytes;
        }
    }
}