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

eu.peppol.smp.SmpLookupManagerImpl Maven / Gradle / Ivy

Go to download

Holds the stuff which is common for both inbound and outbound messages and should be shared between them.

There is a newer version: 4.1.2
Show newest version
/*
 * 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.smp;

import com.google.inject.Inject;
import eu.peppol.BusDoxProtocol;
import eu.peppol.identifier.ParticipantId;
import eu.peppol.identifier.PeppolDocumentTypeId;
import eu.peppol.identifier.PeppolDocumentTypeIdAcronym;
import eu.peppol.identifier.PeppolProcessTypeId;
import eu.peppol.security.CommonName;
import eu.peppol.security.SmpResponseValidator;
import eu.peppol.util.*;
import org.busdox.smp.EndpointType;
import org.busdox.smp.ProcessIdentifierType;
import org.busdox.smp.SignedServiceMetadataType;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Thread safe implementation of {@link SmpLookupManager}
 * 

* User: nigel * Date: Oct 25, 2011 * Time: 9:01:53 AM * * @author Nigel Parker * @author Steinar O. Cook * @author Thore Johnsen */ public class SmpLookupManagerImpl implements SmpLookupManager { private static final Logger log = LoggerFactory.getLogger(SmpLookupManagerImpl.class); private final OperationalMode operationalMode; private final SmlHost configuredSmlHost; private JAXBContext jaxbContext; private final DNSLookupHelper dnsLookupHelper; private final SmlHost smlHost; // Keeping the SMP content retriever in a separate class allows for unit testing private final SmpContentRetriever smpContentRetriever; private final BusDoxProtocolSelectionStrategy busDoxProtocolSelectionStrategy; @Inject public SmpLookupManagerImpl(SmpContentRetriever smpContentRetriever, BusDoxProtocolSelectionStrategy busDoxProtocolSelectionStrategy, OperationalMode operationalMode, @Nullable SmlHost configuredSmlHost) { this.operationalMode = operationalMode; this.configuredSmlHost = configuredSmlHost; this.smlHost = discoverSmlHost(); this.smpContentRetriever = smpContentRetriever; this.busDoxProtocolSelectionStrategy = busDoxProtocolSelectionStrategy; this.dnsLookupHelper = new DNSLookupHelper(); try { jaxbContext = JaxbContextCache.getInstance(SignedServiceMetadataType.class); } catch (JAXBException e) { throw new IllegalStateException("Unable to create Jaxb context cache: " + e, e); } } /** * Discovers which SML host should be used. * * @return the SML host instance to be used */ SmlHost discoverSmlHost() { SmlHost computedSmlHostName; switch (operationalMode) { case TEST: log.warn("Mode of operation is TEST"); computedSmlHostName = SmlHost.TEST_SML; break; default: computedSmlHostName = SmlHost.PRODUCTION_SML; break; } // Finally we check to see if the SML hostname has been overridden in the configuration file computedSmlHostName = checkForSmlHostnameOverride(computedSmlHostName); log.debug("SML hostname: " + computedSmlHostName); return computedSmlHostName; } SmlHost checkForSmlHostnameOverride(SmlHost computedSmlHost) { SmlHost result; if (configuredSmlHost != null) { log.debug("SML hostname has been overridden: [" + configuredSmlHost + "]"); result = configuredSmlHost; } else { result = computedSmlHost; } return result; } /** * Produces the endpoint URL for the supplied participant and document type identifier. * * @param participant identifies the participant for which we are performing a lookup * @param documentTypeIdentifier the document type identifier, which constitutes the second half of the lookup key. * @return The endpoint address for the participant and DocumentId * @throws RuntimeException If the end point address cannot be resolved for the participant. This is caused by a {@link java.net.UnknownHostException} */ @Override public URL getEndpointAddress(ParticipantId participant, PeppolDocumentTypeId documentTypeIdentifier) { EndpointType endpointType = getEndpointType(participant, documentTypeIdentifier); String address = getEndPointUrl(endpointType); log.info("Found endpoint address for " + participant.stringValue() + " from SMP: " + address); try { return new URL(address); } catch (Exception e) { throw new RuntimeException("SMP returned invalid URL", e); } } /** * Provides the end point data required for transmission of a message. */ @Override public PeppolEndpointData getEndpointTransmissionData(ParticipantId participantId, PeppolDocumentTypeId documentTypeIdentifier) { EndpointType endpointType = getEndpointType(participantId, documentTypeIdentifier); String transportProfile = endpointType.getTransportProfile(); String address = getEndPointUrl(endpointType); X509Certificate x509Certificate = getX509CertificateFromEndpointType(endpointType); try { return new PeppolEndpointData(new URL(address), BusDoxProtocol.instanceFrom(transportProfile), CommonName.valueOf(x509Certificate.getSubjectX500Principal())); } catch (Exception e) { throw new IllegalStateException("Unable to provide end point data for " + participantId + " for " + documentTypeIdentifier.toString()); } } /** * Retrieves the end point certificate for the given combination of receiving participant id and document type identifier. * * @param participant receiving participant * @param documentTypeIdentifier document type to be sent * @return The X509Certificate for the given ParticipantId and DocumentId * @throws RuntimeException If the end point address cannot be resolved for the participant. This is caused by a {@link java.net.UnknownHostException} */ @Override public X509Certificate getEndpointCertificate(ParticipantId participant, PeppolDocumentTypeId documentTypeIdentifier) { EndpointType endpointType = getEndpointType(participant, documentTypeIdentifier); return getX509CertificateFromEndpointType(endpointType); } /** * Retrieves a group of URLs representing the documents accepted by the given participant id * * @param participantId participant id to look up * @return list of URLs representing each document type accepted */ @Override public List getServiceGroups(ParticipantId participantId) throws SmpLookupException, ParticipantNotRegisteredException { // Creates the URL for the service meta data for the supplied participant URL serviceGroupURL = constructServiceGroupURL(participantId); if (!isParticipantRegistered(serviceGroupURL)) { throw new ParticipantNotRegisteredException(participantId); } NodeList nodes; List result = new ArrayList(); InputSource smpContents; /* When looking up ParticipantId("9908:976098897") we expected the SML not to resolve, but it actually did and we got a not found (HTTP 404) response from SMP instead (smp-basware.publisher.sml.peppolcentral.org). */ try { smpContents = smpContentRetriever.getUrlContent(serviceGroupURL); } catch (ConnectionException ex) { if (404 == ex.getCode()) { // signal that we got a NOT FOUND for that participant in the SMP throw new ParticipantNotRegisteredException(participantId); } else { throw ex; // re-throw exception } } // Parses the XML response from the SMP try { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); // Prevents XML entity expansion attacks documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING,true); documentBuilderFactory.setNamespaceAware(true); DocumentBuilder documentBuilder; Document document; documentBuilder = documentBuilderFactory.newDocumentBuilder(); document = documentBuilder.parse(smpContents); // Locates the namespace URI of the root element String nameSpaceURI = document.getDocumentElement().getNamespaceURI(); nodes = document.getElementsByTagNameNS(nameSpaceURI, "ServiceMetadataReference"); } catch (Exception e) { throw new SmpLookupException(participantId, serviceGroupURL, e); } // Loop the SMR elements, if any, and populate the result list if (nodes != null) { for (int i = 0; i < nodes.getLength(); i++) { String docTypeAsString = null; try { // Fetch href attribute Element element = (Element) nodes.item(i); String hrefAsString = element.getAttribute("href"); // Gets rid of all the funny %3A's... hrefAsString = URLDecoder.decode(hrefAsString, "UTF-8"); // Grabs the entire text string after "busdox-docid-qns::" docTypeAsString = hrefAsString.substring(hrefAsString.indexOf("busdox-docid-qns::") + "busdox-docid-qns::".length()); // Parses and creates the document type id PeppolDocumentTypeId peppolDocumentTypeId = PeppolDocumentTypeId.valueOf(docTypeAsString); result.add(peppolDocumentTypeId); } catch (Exception e) { /* ignore unparseable document types at runtime */ log.warn("Unable to create PeppolDocumentTypeId from " + docTypeAsString + ", got exception " + e.getMessage()); } } } return result; } public PeppolProcessTypeId getProcessIdentifierForDocumentType(ParticipantId participantId, PeppolDocumentTypeId documentTypeIdentifier) throws SmpSignedServiceMetaDataException { SignedServiceMetadataType serviceMetaData = getServiceMetaData(participantId, documentTypeIdentifier); // SOAP generated type... ProcessIdentifierType processIdentifier = serviceMetaData.getServiceMetadata().getServiceInformation().getProcessList().getProcess().get(0).getProcessIdentifier(); // Converts SOAP generated type into something nicer return PeppolProcessTypeId.valueOf(processIdentifier.getValue()); } /** * Retrieves the complete signed service meta data for a given participant and document type identifier. * @throws SmpSignedServiceMetaDataException */ @Override public SignedServiceMetadataType getServiceMetaData(ParticipantId participant, PeppolDocumentTypeId documentTypeIdentifier) throws SmpSignedServiceMetaDataException { // Constructs the URL for looking up the participant and document type identifier URL smpUrl = constructDocumentTypeURL(participant, documentTypeIdentifier); // Retrieves the contents of the URL InputSource smpContents = fetchContentsOfSmpUrl(participant, documentTypeIdentifier, smpUrl); // Transforms the response received into an XML document Document document = null; try { document = createXmlDocument(smpContents); } catch (Exception e) { throw new SmpSignedServiceMetaDataException(participant, documentTypeIdentifier, smpUrl, e); } // Validates the signature SmpResponseValidator smpResponseValidator = new SmpResponseValidator(document); if (!smpResponseValidator.isSmpSignatureValid()) { throw new IllegalStateException("SMP response contained invalid signature"); } // TODO Uncomment code below if PEPPOL decides we need to follow the chain of trust for the SMP certificate. // Validates the certificate supplied with the signature // if (!OxalisCertificateValidator.getInstance().validateUsingCache(smpResponseValidator.getCertificate())) { // throw new IllegalStateException("SMP Certificate not valid for " + smpUrl); // } try { // Finally parse the response into a properly typed object return parseSmpResponseIntoSignedServiceMetadataType(document); } catch (Exception e) { throw new SmpSignedServiceMetaDataException(participant, documentTypeIdentifier, smpUrl, e); } } /** * Helper method, which extracts the URL of the end point. */ private String getEndPointUrl(EndpointType endpointType) { return endpointType.getEndpointReference().getAddress().getValue(); } /** * Constructs the URL used to look up all the service groups for a given participant identifier. * @param participantId participant for which we should perform the lookup * @return URL instance, which can be used to obtain service groups * @throws SmpLookupException */ URL constructServiceGroupURL(ParticipantId participantId) throws SmpLookupException { String scheme = ParticipantId.getScheme(); String value = participantId.stringValue(); try { String hostname = "B-" + Util.calculateMD5(value.toLowerCase()) + "." + scheme + "." + smlHost; // Example: iso6523-actorid-upis%3A%3A9908:810017902 String encodedParticipant = URLEncoder.encode(scheme + "::", "UTF-8") + value; return new URL("http://" + hostname + "/" + encodedParticipant); } catch (Exception e) { throw new SmpLookupException(participantId, e); } } /** * Constructs the URL used to obtain meta data for a given document type identifier of a given participant identifier. */ URL constructDocumentTypeURL(ParticipantId participantId, PeppolDocumentTypeId documentTypeIdentifier) { String scheme = ParticipantId.getScheme(); String value = participantId.stringValue(); String hostname = null; String urlString = null; try { hostname = "B-" + Util.calculateMD5(value.toLowerCase()) + "." + scheme + "." + smlHost; String encodedParticipant = URLEncoder.encode(scheme + "::" + value, "UTF-8"); String encodedDocumentId = URLEncoder.encode(PeppolDocumentTypeIdAcronym.getScheme() + "::" + documentTypeIdentifier.toString(), "UTF-8"); urlString = "http://" + hostname + "/" + encodedParticipant + "/services/" + encodedDocumentId; return new URL(urlString); } catch (MessageDigestException e) { throw new IllegalStateException("Unable to calculate message digest for: " + participantId + ", doc.type:" + documentTypeIdentifier, e); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("Unable to encode _" + scheme + "::" + value, e); } catch (MalformedURLException e) { throw new IllegalArgumentException("Unable to create URL from string:" + urlString,e); } } SignedServiceMetadataType parseSmpResponseIntoSignedServiceMetadataType(Document document) throws ParserConfigurationException, SAXException, IOException, JAXBException { Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); return unmarshaller.unmarshal(document, SignedServiceMetadataType.class).getValue(); } // Parses the XML response from the SMP Document createXmlDocument(InputSource smpContents) throws ParserConfigurationException, SAXException, IOException { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); // Prevents XML entity expansion attacks documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); return documentBuilder.parse(smpContents); } /** * Helper method, which extracts a valid X509 certificate for an end point. */ private X509Certificate getX509CertificateFromEndpointType(EndpointType endpointType) { try { String body = endpointType.getCertificate(); String endpointCertificate = "-----BEGIN CERTIFICATE-----\n" + body + "\n-----END CERTIFICATE-----"; CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate)certificateFactory.generateCertificate(new ByteArrayInputStream(endpointCertificate.getBytes())); cert.checkValidity(); return cert; } catch (CertificateException e) { throw new RuntimeException("Failed to get valid certificate from Endpoint data", e); } } /** * Each participant has its own sub-domain in peppolcentral, therefore if one does not * exist it means participant is not registered. */ private boolean isParticipantRegistered(URL serviceGroupURL) { return dnsLookupHelper.domainExists(serviceGroupURL); } private InputSource fetchContentsOfSmpUrl(ParticipantId participant, PeppolDocumentTypeId documentTypeIdentifier, URL smpUrl) throws SmpSignedServiceMetaDataException { InputSource smpContents; try { log.debug("Constructed SMP url: " + smpUrl.toExternalForm()); smpContents = smpContentRetriever.getUrlContent(smpUrl); } catch (Exception e) { throw new SmpSignedServiceMetaDataException(participant, documentTypeIdentifier, smpUrl, e); } return smpContents; } /** * Retrieves an instance of the complete EndpointType for a given participant and the given document type identifier. * Given by the following XPath: *

     *     //ServiceMetadata/ServiceInformation/ProcessList/Process[0]/ServiceEndpointList/Endpoint[0]
     * 
* * @param participant the participant identifier * @param documentTypeIdentifier the document type identifier * @return the JAXB generated EndpointType object */ private EndpointType getEndpointType(ParticipantId participant, PeppolDocumentTypeId documentTypeIdentifier) { try { SignedServiceMetadataType serviceMetadata = getServiceMetaData(participant, documentTypeIdentifier); return selectOptimalEndpoint(serviceMetadata); } catch (Exception e) { String pid = (participant == null) ? "participant missing" : "for participant " + participant.toString(); String did = (documentTypeIdentifier == null) ? "document type missing" : "document type " + documentTypeIdentifier.toString(); throw new RuntimeException("Problem with SMP lookup " + pid + " and " + did, e); } } EndpointType selectOptimalEndpoint(SignedServiceMetadataType serviceMetadata) { // List of end points contained in the signed service meta data type List endPointsForDocumentTypeIdentifier = serviceMetadata .getServiceMetadata() .getServiceInformation() .getProcessList() .getProcess() .get(0) .getServiceEndpointList() .getEndpoint(); Map protocolsAndEndpointType = new HashMap(); for (EndpointType endpointType : endPointsForDocumentTypeIdentifier) { BusDoxProtocol busDoxProtocol = BusDoxProtocol.instanceFrom(endpointType.getTransportProfile()); protocolsAndEndpointType.put(busDoxProtocol, endpointType); } BusDoxProtocol preferredProtocol = busDoxProtocolSelectionStrategy.selectOptimalProtocol(new ArrayList(protocolsAndEndpointType.keySet())); return protocolsAndEndpointType.get(preferredProtocol); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy