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

network.oxalis.as4.inbound.As4InboundHandler Maven / Gradle / Ivy

package network.oxalis.as4.inbound;

import com.google.common.io.ByteStreams;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import network.oxalis.as4.lang.OxalisAs4Exception;
import network.oxalis.as4.lang.OxalisAs4TransmissionException;
import network.oxalis.as4.util.*;
import network.oxalis.api.header.HeaderParser;
import network.oxalis.api.inbound.InboundService;
import network.oxalis.api.lang.TimestampException;
import network.oxalis.api.lang.VerifierException;
import network.oxalis.api.model.Direction;
import network.oxalis.api.model.TransmissionIdentifier;
import network.oxalis.api.persist.PersisterHandler;
import network.oxalis.api.timestamp.Timestamp;
import network.oxalis.api.timestamp.TimestampProvider;
import network.oxalis.api.transmission.TransmissionVerifier;
import network.oxalis.as4.common.As4MessageProperties;
import network.oxalis.as4.common.As4MessageProperty;
import network.oxalis.commons.header.SbdhHeaderParser;
import network.oxalis.commons.io.UnclosableInputStream;
import network.oxalis.vefa.peppol.common.code.DigestMethod;
import network.oxalis.vefa.peppol.common.model.*;
import network.oxalis.vefa.peppol.sbdh.SbdReader;
import network.oxalis.vefa.peppol.sbdh.lang.SbdhException;
import org.apache.cxf.attachment.AttachmentUtil;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.message.Attachment;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.ws.policy.AssertionInfoMap;
import org.apache.neethi.Policy;
import org.oasis_open.docs.ebxml_msg.ebms.v3_0.ns.core._200704.*;
import org.w3.xmldsig.ReferenceType;

import javax.xml.soap.*;
import javax.xml.ws.handler.MessageContext;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipException;

@Slf4j
@Singleton
public class As4InboundHandler {

    private static final String REQUIRED_PAYLOAD_HREF_PREFIX = "cid:";
    public static final String COMPRESSION_TYPE = "CompressionType";

    private final TransmissionVerifier transmissionVerifier;
    private final PersisterHandler persisterHandler;
    private final TimestampProvider timestampProvider;
    private final HeaderParser headerParser;
    private final As4MessageFactory as4MessageFactory;
    private final PolicyService policyService;
    private final InboundService inboundService;

    @Inject
    public As4InboundHandler(TransmissionVerifier transmissionVerifier, PersisterHandler persisterHandler, TimestampProvider timestampProvider, HeaderParser headerParser, As4MessageFactory as4MessageFactory, PolicyService policyService, InboundService inboundService) {
        this.transmissionVerifier = transmissionVerifier;
        this.persisterHandler = persisterHandler;
        this.timestampProvider = timestampProvider;
        this.headerParser = headerParser;
        this.as4MessageFactory = as4MessageFactory;
        this.policyService = policyService;
        this.inboundService = inboundService;
    }

    public SOAPMessage handle(SOAPMessage request, MessageContext messageContext) throws OxalisAs4Exception {
        SOAPHeader soapHeader = getSoapHeader(request);
        Timestamp timestamp = getTimestamp(soapHeader);
        Iterator attachments = CastUtils.cast(request.getAttachments());

        // Organize input data
        UserMessage userMessage = SOAPHeaderParser.getUserMessage(soapHeader);

        As4EnvelopeHeader envelopeHeader = parseAs4EnvelopeHeader(userMessage);
        messageContext.put(AS4MessageContextKey.ENVELOPE_HEADER, envelopeHeader);

        TransmissionIdentifier messageId = TransmissionIdentifier.of(envelopeHeader.getMessageId());

        validateMessageId(messageId.getIdentifier()); // Validate UserMessage
        validatePayloads(userMessage.getPayloadInfo()); // Validate Payloads

        List referenceList = SOAPHeaderParser.getReferenceListFromSignedInfo(soapHeader);
        ProsessingContext prosessingContext = new ProsessingContext(timestamp, referenceList);

        // Prepare response
        SOAPMessage response = as4MessageFactory.createReceiptMessage(userMessage, prosessingContext);

        if (!isPingMessage(userMessage)) {
            // Inform Backend

            // Take a copy of the response so that we can persist it as metadata/proof
            byte[] copyOfReceipt = copyReceipt(response);

            // Handle payload
            LinkedHashMap payloads = parseAttachments(attachments, userMessage);

            List paths = new ArrayList<>();
            for (Map.Entry payload : payloads.entrySet()) {
                validateAttachmentHeader(payload.getValue());

                // Persist payload
                paths.add(persistPayload(payload.getKey(), payload.getValue(), messageId));
            }

            Path firstPayloadPath = paths.get(0);
            messageContext.put(AS4MessageContextKey.FIRST_PAYLOAD_PATH, firstPayloadPath);
            messageContext.put(AS4MessageContextKey.FIRST_PAYLOAD_HEADER, payloads.values().iterator().next());

            // Persist Metadata
            As4PayloadHeader firstHeader = payloads.entrySet().iterator().next().getValue();
            String firstAttachmentId = envelopeHeader.getPayloadCIDs().get(0);
            Digest firstAttachmentDigest = Digest.of(DigestMethod.SHA256, SOAPHeaderParser.getAttachmentDigest(firstAttachmentId, soapHeader));

            X509Certificate senderCertificate = getSenderCertificate(soapHeader);

            As4InboundMetadata as4InboundMetadata = new As4InboundMetadata(
                    messageId,
                    userMessage.getCollaborationInfo().getConversationId(),
                    firstHeader,
                    timestamp,
                    TransportProfile.AS4,
                    firstAttachmentDigest,
                    senderCertificate,
                    copyOfReceipt,
                    envelopeHeader);

            try {
                persisterHandler.persist(as4InboundMetadata, firstPayloadPath);
            } catch (IOException e) {
                throw new OxalisAs4Exception("Error persisting AS4 metadata", e, AS4ErrorCode.EBMS_0202);
            }

            messageContext.put(AS4MessageContextKey.PERSISTED, true);

            // Persist statistics
            inboundService.complete(as4InboundMetadata);
        }

        // Send response
        Policy policy = null;
        try {
            policy = policyService.getPolicy(userMessage.getCollaborationInfo());
        } catch (OxalisAs4TransmissionException e) {
            throw new OxalisAs4Exception("Could not get policy", e, AS4ErrorCode.EBMS_0202);
        }

        try {
            response.setProperty(AssertionInfoMap.class.getName(), new AssertionInfoMap(policy));
            response.saveChanges();
        } catch (SOAPException e) {
            throw new OxalisAs4Exception("Error persisting AS4 metadata", e, AS4ErrorCode.EBMS_0202);
        }

        return response;
    }

    private X509Certificate getSenderCertificate(SOAPHeader soapHeader) {
        try {
            return SOAPHeaderParser.getSenderCertificate(soapHeader);
        } catch (OxalisAs4Exception e) {
            return null;
        }
    }

    private boolean isPingMessage(UserMessage userMessage) {
        if (userMessage == null) {
            return false;
        }

        CollaborationInfo collaborationInfo = userMessage.getCollaborationInfo();

        if (collaborationInfo == null) {
            return false;
        }

        return Optional.ofNullable(collaborationInfo.getService())
                .map(Service::getValue)
                .map(service -> Optional.ofNullable(collaborationInfo.getAction())
                        .map(action ->
                                Constants.TEST_SERVICE.equals(service) && Constants.TEST_ACTION.equals(action)
                        ).orElse(false)
                ).orElse(false);
    }

    public static void validatePayloads(PayloadInfo payloadInfo) throws OxalisAs4Exception {
        List externalPayloads = payloadInfo.getPartInfo().stream()
                .map(PartInfo::getHref)
                .filter(href -> !href.startsWith(REQUIRED_PAYLOAD_HREF_PREFIX))
                .collect(Collectors.toList());

        if (!externalPayloads.isEmpty()) {
            String errorMessage = "Invalid PayloadInfo. Href(s) detected with \"external\" source: " + externalPayloads;
            log.debug(errorMessage);

            throw new OxalisAs4Exception(
                    errorMessage,
                    AS4ErrorCode.EBMS_0009
            );
        }


        List payloadsWithInvalidCharset = payloadInfo.getPartInfo().stream()
                .filter(As4InboundHandler::partInfoHasInvalidCharset)
                .map(PartInfo::getHref)
                .collect(Collectors.toList());

        if (!payloadsWithInvalidCharset.isEmpty()) {
            String errorMessage = "Invalid PayloadInfo. Part(s) detected invalid \"CharacterSet\" header: " + payloadsWithInvalidCharset;
            log.debug(errorMessage);

            throw new OxalisAs4Exception(
                    errorMessage,
                    AS4ErrorCode.EBMS_0009
            );
        }


        List payloadsMissingMimeTypeHeader = payloadInfo.getPartInfo().stream()
                .filter(As4InboundHandler::partInfoMissingMimeTypeHeader)
                .map(PartInfo::getHref)
                .collect(Collectors.toList());

        if (!payloadsMissingMimeTypeHeader.isEmpty()) {
            String errorMessage = "Invalid PayloadInfo. Part(s) detected without \"MimeType\" header: " + payloadsMissingMimeTypeHeader;
            log.debug(errorMessage);

            throw new OxalisAs4Exception(
                    errorMessage,
                    AS4ErrorCode.EBMS_0009
            );
        }

    }

    private static boolean partInfoHasInvalidCharset(PartInfo partInfo) {

        return Optional.ofNullable(partInfo)
                .map(PartInfo::getPartProperties)
                .map(PartProperties::getProperty)
                .map(Collection::stream).orElse(Stream.empty())
                .anyMatch(property ->
                        Optional.of(property)
                                .map(Property::getName)
                                .filter("CharacterSet"::equals)
                                .map(fieldName -> Optional.of(property)
                                        .map(Property::getValue)
                                        .map(charset -> {
                                            try {
                                                return null == Charset.forName(property.getValue());
                                            } catch (Exception e) {
                                                return true;
                                            }
                                        }).orElse(true)
                                ).orElse(false)
                );
    }

    public static boolean partInfoMissingMimeTypeHeader(PartInfo partInfo) {
        return Optional.ofNullable(partInfo)
                .map(PartInfo::getPartProperties)
                .map(PartProperties::getProperty)
                .map(Collection::stream).orElse(Stream.empty())
                .map(Property::getName)
                .noneMatch("MimeType"::equals);
    }

    public byte[] copyReceipt(SOAPMessage response) throws OxalisAs4Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        try {
            response.writeTo(bos);
        } catch (SOAPException | IOException e) {
            throw new OxalisAs4Exception("Could not write SOAP response", e, AS4ErrorCode.EBMS_0202);
        }

        return bos.toByteArray();
    }

    public void validateMessageId(String messageId) throws OxalisAs4Exception {

        if (!MessageIdUtil.verify(messageId)) {
            throw new OxalisAs4Exception(
                    "Invalid Message-ID '" + messageId + "' in inbound message.",
                    AS4ErrorCode.EBMS_0009
            );
        }

    }

    private LinkedHashMap parseAttachments(Iterator attachments, UserMessage userMessage) throws OxalisAs4Exception {

        if (!attachments.hasNext()) {
            throw new OxalisAs4Exception("No attachment(s) present");
        }

        // >
        Map> partInfoHeadersMap = userMessage.getPayloadInfo().getPartInfo().stream()
                .collect(
                        Collectors.toMap(
                                partInfo -> AttachmentUtil.cleanContentId(partInfo.getHref()),
                                partInfo -> partInfo.getPartProperties().getProperty().stream().collect(
                                        Collectors.toMap(
                                                Property::getName,
                                                property -> new MimeHeader(property.getName(), property.getValue())
                                        )
                                )
                        )
                );

        LinkedHashMap payloads = new LinkedHashMap<>();

        Collection s = PhaseInterceptorChain.getCurrentMessage().getAttachments();

        for (Attachment attachment : s) {
            try {
                InputStream is = attachment.getDataHandler().getInputStream();
                String contentId = AttachmentUtil.cleanContentId(attachment.getId());

                Map mimeHeaders = new HashMap<>();
                attachment.getHeaderNames()
                        .forEachRemaining(h -> mimeHeaders.put(h, new MimeHeader(h, attachment.getHeader(h))));

                Map partInfoHeaders = partInfoHeadersMap.get(contentId);

                if (isAttachmentCompressed(partInfoHeaders, mimeHeaders)) {
                    try {
                        is = new GZIPInputStream(new BufferedInputStream(is), 8192);
                    } catch (IOException e) {
                        log.info("PartInfo headers: {}", partInfoHeaders.values().stream()
                                .map(p -> p.getName() + "=" + p.getValue())
                                .collect(Collectors.joining(", ", "{", "}")));

                        log.info("MIME headers: {}", mimeHeaders.values().stream()
                                .map(p -> p.getName() + "=" + p.getValue())
                                .collect(Collectors.joining(", ", "{", "}")));

                        throw new OxalisAs4Exception(
                                "Unable to initiate decompression of payload with Content-ID: " + contentId,
                                e,
                                AS4ErrorCode.EBMS_0303,
                                AS4ErrorCode.Severity.FAILURE
                        );
                    }
                }

                BufferedInputStream bis = new BufferedInputStream(is, 65536);

                Header sbdh;
                if (headerParser instanceof SbdhHeaderParser) {
                    bis.mark(65536);
                    sbdh = readHeader(contentId, bis);
                    bis.reset();
                } else {
                    sbdh = new Header()
                            .sender(ParticipantIdentifier.of(userMessage.getPartyInfo().getFrom().getPartyId().get(0).getValue()))
                            .receiver(ParticipantIdentifier.of(userMessage.getPartyInfo().getTo().getPartyId().get(0).getValue()))
                            .documentType(DocumentTypeIdentifier.of(userMessage.getCollaborationInfo().getService().getValue(), Scheme.of(userMessage.getCollaborationInfo().getService().getType())))
                            .identifier(InstanceIdentifier.of(userMessage.getCollaborationInfo().getAction()));
                }

                // Get an "unexpected eof in prolog"
                As4PayloadHeader header = new As4PayloadHeader(sbdh, partInfoHeaders.values(), contentId, userMessage.getMessageInfo().getMessageId());

                // Extract "fresh" InputStream
                payloads.put(bis, header);

            } catch (IOException e) {
                throw new OxalisAs4Exception("Could not get attachment input stream", e);
            }
        }

        return payloads;
    }

    private Header readHeader(String contentId, InputStream is) throws OxalisAs4Exception {
        try (SbdReader sbdReader = SbdReader.newInstance(is)) {
            return sbdReader.getHeader();
        } catch (SbdhException | IOException e) {
            launderZipException(contentId, e);
            throw new OxalisAs4Exception("Could not extract SBDH from payload");
        }
    }

    private void launderZipException(String contentId, Exception e) throws OxalisAs4Exception {
        Throwable cause = e;
        for (int i = 0; i < 10 && cause != null; i++) {
            if (cause instanceof ZipException) {
                throw new OxalisAs4Exception(
                        "Unable to decompress of payload with Content-ID: " + contentId,
                        cause,
                        AS4ErrorCode.EBMS_0303,
                        AS4ErrorCode.Severity.FAILURE
                );
            }

            cause = e.getCause();
        }
    }

    private boolean isAttachmentCompressed(Map partInfoHeaders, Map mimeHeaders) {
        if (partInfoHeaders.containsKey(COMPRESSION_TYPE)) {
            String value = partInfoHeaders.get(COMPRESSION_TYPE).getValue();

            // Somehow this fails
            if ("application/gzip".equals(value)) {
                return true;
            }
        }

//        if (mimeHeaders.containsKey(COMPRESSION_TYPE) &&
//                "application/gzip".equals(mimeHeaders.get(COMPRESSION_TYPE).getValue())) {
//            return true;
//        }

        return false;
    }

    private As4EnvelopeHeader parseAs4EnvelopeHeader(UserMessage userMessage) {

        As4EnvelopeHeader as4EnvelopeHeader = new As4EnvelopeHeader();

        as4EnvelopeHeader.setMessageId(userMessage.getMessageInfo().getMessageId());
        as4EnvelopeHeader.setConversationId(userMessage.getCollaborationInfo().getConversationId());

        as4EnvelopeHeader.setFromPartyId(userMessage.getPartyInfo().getFrom().getPartyId().stream().map(PartyId::getValue).collect(Collectors.toList()));
        as4EnvelopeHeader.setFromPartyRole(userMessage.getPartyInfo().getFrom().getRole());

        as4EnvelopeHeader.setToPartyId(userMessage.getPartyInfo().getTo().getPartyId().stream().map(PartyId::getValue).collect(Collectors.toList()));
        as4EnvelopeHeader.setToPartyRole(userMessage.getPartyInfo().getTo().getRole());

        as4EnvelopeHeader.setAction(userMessage.getCollaborationInfo().getAction());
        as4EnvelopeHeader.setService(userMessage.getCollaborationInfo().getService().getValue());

        as4EnvelopeHeader.setMessageProperties(userMessage.getMessageProperties().getProperty()
                .stream()
                .map(p -> new As4MessageProperty(p.getName(), p.getType(), p.getValue()))
                .collect(Collectors.toCollection(As4MessageProperties::new)));

        as4EnvelopeHeader.setPayloadCIDs(userMessage.getPayloadInfo().getPartInfo().stream().map(PartInfo::getHref).collect(Collectors.toList()));

        return as4EnvelopeHeader;
    }

    private Timestamp getTimestamp(SOAPHeader header) throws OxalisAs4Exception {
        byte[] signature = SOAPHeaderParser.getSignature(header);
        try {
            return timestampProvider.generate(signature, Direction.IN);
        } catch (TimestampException e) {
            throw new OxalisAs4Exception("Error generating timestamp", e);
        }
    }

    private Path persistPayload(InputStream inputStream, As4PayloadHeader as4PayloadHeader, TransmissionIdentifier ti) throws OxalisAs4Exception {
        try (InputStream is = inputStream) {
            // Persist content
            Path payloadPath = persisterHandler.persist(ti, as4PayloadHeader, new UnclosableInputStream(is));

            // Exhaust InputStream
            ByteStreams.exhaust(is);
            inputStream.close();
            return payloadPath;
        } catch (IOException e) {
            launderZipException(as4PayloadHeader.getCid(), e);
            throw new OxalisAs4Exception("Error processing payload input stream", e);
        }
    }

    private void validateAttachmentHeader(Header attachmentHeader) throws OxalisAs4Exception {
        try {
            transmissionVerifier.verify(attachmentHeader, Direction.IN);
        } catch (VerifierException e) {
            throw new OxalisAs4Exception("Error verifying SBDH", e);
        }
    }

    private SOAPHeader getSoapHeader(SOAPMessage request) throws OxalisAs4Exception {
        try {
            return request.getSOAPHeader();
        } catch (SOAPException e) {
            throw new OxalisAs4Exception("Could not get SOAP header", e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy