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

no.difi.oxalis.as2.outbound.As2MessageSender Maven / Gradle / Ivy

/*
 * Copyright 2010-2017 Norwegian Agency for Public Management and eGovernment (Difi)
 *
 * 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/community/eupl/og_page/eupl
 *
 * 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 no.difi.oxalis.as2.outbound;

import brave.Span;
import brave.Tracer;
import com.google.inject.Inject;
import com.google.inject.Provider;
import no.difi.oxalis.api.lang.OxalisTransmissionException;
import no.difi.oxalis.api.lang.TimestampException;
import no.difi.oxalis.api.model.Direction;
import no.difi.oxalis.api.model.TransmissionIdentifier;
import no.difi.oxalis.api.outbound.TransmissionRequest;
import no.difi.oxalis.api.outbound.TransmissionResponse;
import no.difi.oxalis.api.timestamp.Timestamp;
import no.difi.oxalis.api.timestamp.TimestampProvider;
import no.difi.oxalis.as2.code.As2Header;
import no.difi.oxalis.as2.code.MdnHeader;
import no.difi.oxalis.as2.model.As2DispositionNotificationOptions;
import no.difi.oxalis.as2.model.Mic;
import no.difi.oxalis.as2.util.*;
import no.difi.oxalis.commons.bouncycastle.BCHelper;
import no.difi.oxalis.commons.security.CertificateUtils;
import no.difi.oxalis.commons.tracing.Traceable;
import no.difi.vefa.peppol.common.model.Digest;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
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.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.mail.MessagingException;
import javax.mail.internet.InternetHeaders;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.util.ByteArrayDataSource;
import javax.net.ssl.SSLHandshakeException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Not thread safe implementation of sender, 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
 * @author erlend
 */
class As2MessageSender extends Traceable {

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

    /**
     * Provider of HTTP clients.
     */
    private final Provider httpClientProvider;

    private final SMimeMessageFactory sMimeMessageFactory;

    /**
     * Timestamp provider used to create timestamp "t3" (time of reception of transport specific receipt, MDN).
     */
    private final TimestampProvider timestampProvider;

    /**
     * Identifier from sender's certificate used during transmission in "AS2-From" header.
     */
    private final String fromIdentifier;

    private TransmissionRequest transmissionRequest;

    private TransmissionIdentifier transmissionIdentifier;

    private Span root;

    private Digest outboundMic;

    /**
     * Constructor expecting resources needed to perform transmission using AS2. All task required to be done once for
     * all requests using this instance is done here.
     *
     * @param httpClientProvider  Provider of HTTP clients.
     * @param certificate         Certificate of sender.
     * @param sMimeMessageFactory Factory prepared to create S/MIME messages using our private key.
     * @param timestampProvider   Provider used to fetch timestamps.
     * @param tracer              Tracing tool.
     */
    @Inject
    public As2MessageSender(Provider httpClientProvider, X509Certificate certificate,
                            SMimeMessageFactory sMimeMessageFactory, TimestampProvider timestampProvider,
                            Tracer tracer) {
        super(tracer);
        this.httpClientProvider = httpClientProvider;
        this.sMimeMessageFactory = sMimeMessageFactory;
        this.timestampProvider = timestampProvider;

        // Establishes our AS2 System Identifier based upon the contents of the CN= field of the certificate
        this.fromIdentifier = CertificateUtils.extractCommonName(certificate);
    }

    public TransmissionResponse send(TransmissionRequest transmissionRequest, Span root)
            throws OxalisTransmissionException {
        this.transmissionRequest = transmissionRequest;

        this.root = tracer.newChild(root.context()).name("Send AS2 message").start();
        try {
            return sendHttpRequest(prepareHttpRequest());
        } catch (OxalisTransmissionException e) {
            this.root.tag("exception", e.getMessage());
            throw e;
        } finally {
            root.finish();
        }
    }

    @SuppressWarnings("unchecked")
    protected HttpPost prepareHttpRequest() throws OxalisTransmissionException {
        Span span = tracer.newChild(root.context()).name("request").start();
        try {
            final HttpPost httpPost;

            // Create the body part of the MIME message containing our content to be transmitted.
            MimeBodyPart mimeBodyPart = MimeMessageHelper
                    .createMimeBodyPart(transmissionRequest.getPayload(), "application/xml");

            // Digest method to use.
            SMimeDigestMethod digestMethod = SMimeDigestMethod.findByTransportProfile(
                    transmissionRequest.getEndpoint().getTransportProfile());

            outboundMic = MimeMessageHelper.calculateMic(mimeBodyPart, digestMethod);
            span.tag("mic", outboundMic.toString());
            span.tag("endpoint url", transmissionRequest.getEndpoint().getAddress().toString());

            // Create a complete S/MIME message using the body part containing our content as the
            // signed part of the S/MIME message.
            MimeMessage signedMimeMessage = sMimeMessageFactory
                    .createSignedMimeMessage(mimeBodyPart, digestMethod);
            // .createSignedMimeMessageNew(mimeBodyPart, outboundMic, digestMethod);

            // Initiate POST request
            httpPost = new HttpPost(transmissionRequest.getEndpoint().getAddress());

            // Get all headers in S/MIME message.
            List headers = Collections.list(signedMimeMessage.getAllHeaders());

            List headerNames = headers.stream()
                    // Tag for tracing.
                    .peek(h -> span.tag(h.getName(), h.getValue()))
                    // Add headers to httpPost object (remove new lines according to HTTP 1.1).
                    .peek(h -> httpPost.addHeader(h.getName(), h.getValue().replace("\r\n\t", "")))
                    // Collect header names....
                    .map(javax.mail.Header::getName)
                    // ... in a list.
                    .collect(Collectors.toList());

            // Write content to OutputStream without headers.
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            signedMimeMessage.writeTo(byteArrayOutputStream, headerNames.toArray(new String[headerNames.size()]));

            transmissionIdentifier = TransmissionIdentifier.fromHeader(
                    httpPost.getFirstHeader(As2Header.MESSAGE_ID).getValue());

            // 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
            httpPost.setEntity(new ByteArrayEntity(byteArrayOutputStream.toByteArray()));

            // Set all headers specific to AS2 (not MIME).
            httpPost.addHeader(As2Header.AS2_FROM, fromIdentifier);
            httpPost.setHeader(As2Header.AS2_TO, CertificateUtils.extractCommonName(
                    transmissionRequest.getEndpoint().getCertificate()));
            httpPost.addHeader(As2Header.DISPOSITION_NOTIFICATION_TO, "[email protected]");
            httpPost.addHeader(As2Header.DISPOSITION_NOTIFICATION_OPTIONS,
                    As2DispositionNotificationOptions.getDefault(digestMethod).toString());
            httpPost.addHeader(As2Header.AS2_VERSION, As2Header.VERSION);
            httpPost.addHeader(As2Header.SUBJECT, "AS2 message from OXALIS");
            httpPost.addHeader(As2Header.DATE, As2DateUtil.RFC822.format(new Date()));

            return httpPost;
        } catch (MessagingException | IOException e) {
            throw new OxalisTransmissionException("Unable to stream S/MIME message into byte array output stream");
        } finally {
            span.finish();
        }
    }

    protected TransmissionResponse sendHttpRequest(HttpPost httpPost) throws OxalisTransmissionException {
        Span span = tracer.newChild(root.context()).name("execute").start();
        try (CloseableHttpClient httpClient = httpClientProvider.get()) {

            CloseableHttpResponse response = httpClient.execute(httpPost);

            span.finish();

            return handleResponse(response);
        } catch (HttpHostConnectException e) {
            span.tag("exception", e.getMessage());
            throw new OxalisTransmissionException("Receiving server does not seem to be running.",
                    transmissionRequest.getEndpoint().getAddress(), e);
        } catch (SSLHandshakeException e) {
            span.tag("exception", e.getMessage());
            throw new OxalisTransmissionException("Possible invalid SSL Certificate at the other end.",
                    transmissionRequest.getEndpoint().getAddress(), e);
        } catch (IOException e) {
            span.tag("exception", String.valueOf(e.getMessage()));
            throw new OxalisTransmissionException(transmissionRequest.getEndpoint().getAddress(), e);
        } finally {
            span.finish();
        }
    }

    protected TransmissionResponse handleResponse(CloseableHttpResponse closeableHttpResponse)
            throws OxalisTransmissionException {
        Span span = tracer.newChild(root.context()).name("response").start();
        try (CloseableHttpResponse response = closeableHttpResponse) {
            span.tag("code", String.valueOf(response.getStatusLine().getStatusCode()));

            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                LOGGER.error("AS2 HTTP POST expected HTTP OK, but got : {} from {}",
                        response.getStatusLine().getStatusCode(), transmissionRequest.getEndpoint().getAddress());

                // Throws exception
                handleFailedRequest(response);
            }

            // handle normal HTTP OK response
            LOGGER.debug("AS2 transmission to {} returned HTTP OK, verify MDN response",
                    transmissionRequest.getEndpoint().getAddress());

            Header contentTypeHeader = response.getFirstHeader("Content-Type");
            if (contentTypeHeader == null)
                throw new OxalisTransmissionException("No Content-Type header in response, probably a server error.");

            // Read MIME Message
            MimeMessage mimeMessage = MimeMessageHelper.parseMultipart(
                    response.getEntity().getContent(), contentTypeHeader.getValue());

            // Add headers to MIME Message
            for (Header header : response.getAllHeaders())
                mimeMessage.addHeader(header.getName(), header.getValue());

            SMimeReader sMimeReader = new SMimeReader(mimeMessage);

            // Timestamp of reception of MDN
            Timestamp t3 = timestampProvider.generate(sMimeReader.getSignature(), Direction.OUT);

            // Extract signed digest and digest algorithm
            SMimeDigestMethod digestMethod = sMimeReader.getDigestMethod();

            // Preparing calculation of digest
            MessageDigest messageDigest = BCHelper.getMessageDigest(digestMethod.getIdentifier());
            InputStream digestInputStream = new DigestInputStream(sMimeReader.getBodyInputStream(), messageDigest);

            // Reading report
            MimeMultipart mimeMultipart = new MimeMultipart(
                    new ByteArrayDataSource(digestInputStream, mimeMessage.getContentType()));

            // Create digest object
            Digest digest = Digest.of(digestMethod.getDigestMethod(), messageDigest.digest());

            // Verify signature
            /*
            X509Certificate certificate = SMimeBC.verifySignature(
                    ImmutableMap.of(digestMethod.getOid(), digest.getValue()),
                    sMimeReader.getSignature()
            );
            */

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

            // Verify if the certificate used by the receiving Access Point in
            // the response message does not match its certificate published by the SMP
            if (!transmissionRequest.getEndpoint().getCertificate().equals(certificate))
                throw new OxalisTransmissionException(String.format(
                        "Certificate in MDN ('%s') does not match certificate from SMP ('%s').",
                        certificate.getSubjectX500Principal().getName(),
                        transmissionRequest.getEndpoint().getCertificate().getSubjectX500Principal().getName()));

            LOGGER.debug("MDN signature was verified for : " + certificate.getSubjectDN().toString());

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

            if (!mdnMimeMessageInspector.isOkOrWarning(new Mic(outboundMic))) {
                LOGGER.error("AS2 transmission failed with some error message '{}'.", msg);
                throw new OxalisTransmissionException(String.format("AS2 transmission failed : %s", msg));
            }

            // Read structured content
            MimeBodyPart mimeBodyPart = (MimeBodyPart) mdnMimeMessageInspector.getMessageDispositionNotificationPart();
            InternetHeaders internetHeaders = new InternetHeaders((InputStream) mimeBodyPart.getContent());

            // Fetch timestamp if set
            Date date = t3.getDate();
            if (internetHeaders.getHeader(MdnHeader.DATE) != null)
                date = As2DateUtil.RFC822.parse(internetHeaders.getHeader(MdnHeader.DATE)[0]);

            // Return TransmissionResponse
            return new As2TransmissionResponse(transmissionIdentifier, transmissionRequest,
                    outboundMic, MimeMessageHelper.toBytes(mimeMessage), t3, date);
        } catch (TimestampException | IOException e) {
            throw new OxalisTransmissionException(e.getMessage(), e);
        } catch (NoSuchAlgorithmException | MessagingException e) {
            throw new OxalisTransmissionException("Unable to parse received content.", e);
        } finally {
            span.finish();
        }
    }

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy