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

com.helger.smpclient.httpclient.AbstractGenericSMPClient Maven / Gradle / Ivy

There is a newer version: 9.6.0
Show newest version
/*
 * Copyright (C) 2015-2024 Philip Helger
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.smpclient.httpclient;

import java.io.IOException;
import java.net.ConnectException;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.util.function.Consumer;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.HttpResponseException;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.annotation.ReturnsMutableObject;
import com.helger.commons.debug.GlobalDebug;
import com.helger.commons.mime.CMimeType;
import com.helger.commons.string.StringHelper;
import com.helger.commons.string.ToStringGenerator;
import com.helger.commons.traits.IGenericImplTrait;
import com.helger.httpclient.HttpClientManager;
import com.helger.jaxb.GenericJAXBMarshaller;
import com.helger.security.keystore.EKeyStoreType;
import com.helger.smpclient.config.SMPClientConfiguration;
import com.helger.smpclient.exception.SMPClientBadRequestException;
import com.helger.smpclient.exception.SMPClientException;
import com.helger.smpclient.exception.SMPClientNotFoundException;
import com.helger.smpclient.exception.SMPClientUnauthorizedException;
import com.helger.xsds.xmldsig.X509DataType;

import jakarta.xml.bind.JAXBElement;

/**
 * Abstract base class for SMP clients - wraps all the HTTP stuff
 * 

* Note: this class is also licensed under Apache 2 license, as it was not part * of the original implementation *

* * @author Philip Helger * @param * Real implementation class */ public abstract class AbstractGenericSMPClient > implements IGenericImplTrait { public static final boolean DEFAULT_FOLLOW_REDIRECTS = true; public static final boolean DEFAULT_XML_SCHEMA_VALIDATION = true; // The default text/xml content type uses iso-8859-1! public static final ContentType CONTENT_TYPE_TEXT_XML = ContentType.create (CMimeType.TEXT_XML.getAsString (), StandardCharsets.UTF_8); private static final Logger LOGGER = LoggerFactory.getLogger (AbstractGenericSMPClient.class); private static final KeyStore DEFAULT_TRUST_STORE; static { final EKeyStoreType eType = SMPClientConfiguration.getTrustStoreType (); final String sPath = SMPClientConfiguration.getTrustStorePath (); DEFAULT_TRUST_STORE = SMPClientConfiguration.loadTrustStore (); if (DEFAULT_TRUST_STORE != null) { if (LOGGER.isDebugEnabled ()) LOGGER.debug ("Successfully loaded configured SMP client trust store '" + sPath + "' of type " + eType); } else { if (StringHelper.hasNoText (sPath)) LOGGER.warn ("No SMP client trust store is configured"); else LOGGER.warn ("Failed to load the configured SMP client trust store '" + sPath + "' of type " + eType); } } /** * The string representation of the SMP host URL, always ending with a * trailing slash! */ private final String m_sSMPHost; private boolean m_bVerifySignature = SMPHttpResponseHandlerSigned.DEFAULT_VERIFY_SIGNATURE; private boolean m_bSecureValidation = SMPHttpResponseHandlerSigned.DEFAULT_SECURE_VALIDATION; private KeyStore m_aTrustStore = DEFAULT_TRUST_STORE; private boolean m_bFollowSMPRedirects = DEFAULT_FOLLOW_REDIRECTS; private boolean m_bXMLSchemaValidation = DEFAULT_XML_SCHEMA_VALIDATION; private final SMPHttpClientSettings m_aHttpClientSettings = new SMPHttpClientSettings (); private Consumer > m_aMarshallerConsumer; /** * Constructor with a direct SMP URL.
* Remember: must be HTTP and using port 80 only! * * @param aSMPHost * The address of the SMP service. Must be port 80 and basic http only * (no https!). Example: http://smpcompany.company.org * @param bPeppolLimitationsActive * true if the Peppol limitations (Port 80, http only, in * root context) should be complained about or not. */ protected AbstractGenericSMPClient (@Nonnull final URI aSMPHost, final boolean bPeppolLimitationsActive) { ValueEnforcer.notNull (aSMPHost, "SMPHost"); if (bPeppolLimitationsActive) { if (!"http".equals (aSMPHost.getScheme ())) LOGGER.warn ("SMP URI " + aSMPHost + " does not use the expected http scheme, which is required for Peppol!"); // getPort () returns -1 if none was explicitly specified if (aSMPHost.getPort () != 80 && aSMPHost.getPort () != -1) LOGGER.warn ("SMP URI " + aSMPHost + " is not running on port 80, which is required for Peppol!"); } // Build string and ensure it ends with a "/" final String sSMPHost = aSMPHost.toString (); m_sSMPHost = sSMPHost.endsWith ("/") ? sSMPHost : sSMPHost + '/'; } /** * @return The SMP host URI string we're operating on. Never null * . Always has a trailing "/". */ @Nonnull public final String getSMPHostURI () { return m_sSMPHost; } /** * @return The HTTP client settings to be configured. Never null. * @since 8.0.1 */ @Nonnull @ReturnsMutableObject public final SMPHttpClientSettings httpClientSettings () { return m_aHttpClientSettings; } /** * Special version to modify the httpClientSettings but with a chainable API. * * @param aConsumer * The consumer that deals with the SMPHttpClientSettings * @return this for chaining * @since 9.0.9 */ @Nonnull public final IMPLTYPE withHttpClientSettings (@Nonnull final Consumer aConsumer) { ValueEnforcer.notNull (aConsumer, "Consumer"); aConsumer.accept (m_aHttpClientSettings); return thisAsT (); } /** * @return true if SMP client response certificate checking is * enabled, false if it is disabled. By default this * check is enabled (see * {@link SMPHttpResponseHandlerSigned#DEFAULT_VERIFY_SIGNATURE}). * @since 8.0.3 */ public final boolean isVerifySignature () { return m_bVerifySignature; } /** * Check the certificate retrieved from a signed SMP response? This may be * helpful for debugging and testing of SMP client connections!
* Uses the trust store configured in the SMP client configuration. * * @param bVerifySignature * true to enable SMP response checking (on by default) or * false to disable it. * @return this for chaining * @since 8.0.3 */ @Nonnull public final IMPLTYPE setVerifySignature (final boolean bVerifySignature) { m_bVerifySignature = bVerifySignature; return thisAsT (); } /** * @return true if SMP client response certificate checking * should use secure validation, false if validation also * allows deprecated algorithms. By default this check is enabled (see * {@link SMPHttpResponseHandlerSigned#DEFAULT_SECURE_VALIDATION}). * @since 9.0.5 */ public final boolean isSecureValidation () { return m_bSecureValidation; } /** * Enable or disable the usage of secure XMLDsig validation. By default secure * validation is enabled. Java 17 disables the usage of SHA-1 in XMLDsig by * default, as documented in https://bugs.openjdk.org/browse/JDK-8261246. * Currently the Peppol SMP still uses SHA-1 so you might want to disable this * for the sake of sanity. * * @param bSecureValidation * true to enable SMP secure certificate validation * (enabled by default) or false to disable it. * @return this for chaining * @since 9.0.5 */ @Nonnull public final IMPLTYPE setSecureValidation (final boolean bSecureValidation) { m_bSecureValidation = bSecureValidation; return thisAsT (); } /** * @return The trust store to be used for verifying the signature. May be * null if an invalid trust store is configured. * @since 8.1.1 */ @Nullable public final KeyStore getTrustStore () { return m_aTrustStore; } /** * Set the trust store to be used. The trust store must be used, if signature * verification is enabled. * * @param aTrustStore * The trust store to be used. May be null. * @return this for chaining * @since 8.1.1 */ @Nonnull public final IMPLTYPE setTrustStore (@Nullable final KeyStore aTrustStore) { m_aTrustStore = aTrustStore; return thisAsT (); } /** * @return true if SMP redirects should be followed, * false if not. By default this check is enabled (see * {@link #DEFAULT_FOLLOW_REDIRECTS}). * @since 7.0.6 */ public final boolean isFollowSMPRedirects () { return m_bFollowSMPRedirects; } /** * Should the SMP client follow the SMP redirects that can be found in service * registrations. Enabled by default. * * @param bFollowSMPRedirects * true to follow SMP redirects (on by default) or * false to disable it. * @return this for chaining * @since 7.0.6 */ @Nonnull public final IMPLTYPE setFollowSMPRedirects (final boolean bFollowSMPRedirects) { m_bFollowSMPRedirects = bFollowSMPRedirects; return thisAsT (); } /** * @return true if responses should be checked against the XML * Schemas, false if not. By default this check is * enabled (see {@link #DEFAULT_XML_SCHEMA_VALIDATION}). * @since 8.0.5 */ public final boolean isXMLSchemaValidation () { return m_bXMLSchemaValidation; } /** * Should the SMP client perform XML Schema validation or not. Enabled by * default. * * @param bXMLSchemaValidation * true to perform XML Schema validation, * false to disable it. * @return this for chaining * @since 8.0.5 */ @Nonnull public final IMPLTYPE setXMLSchemaValidation (final boolean bXMLSchemaValidation) { m_bXMLSchemaValidation = bXMLSchemaValidation; return thisAsT (); } @Nonnull @OverrideOnDemand protected HttpClientContext createHttpContext () { return HttpClientContext.create (); } /** * Execute a generic request on the SMP. This is e.g. helpful for accessing * the PEPPOL Directory BusinessCard API. Compared to * {@link #executeGenericRequest(HttpUriRequestBase, HttpClientResponseHandler)} * this method does NOT convert the {@link IOException} from HTTP * communication problems to {@link IOException}. * * @param aRequest * The request to be executed. The proxy + connection and request * timeout are set in this method. * @param aResponseHandler * The response handler to be used. May not be null. * @return The return value of the response handler. * @throws IOException * On HTTP communication error * @see #executeGenericRequest(HttpUriRequestBase, HttpClientResponseHandler) * @param * Expected response type */ @Nonnull public T executeRequest (@Nonnull final HttpUriRequestBase aRequest, @Nonnull final HttpClientResponseHandler aResponseHandler) throws IOException { final HttpClientContext aHttpContext = createHttpContext (); try (final HttpClientManager aHttpClientMgr = HttpClientManager.create (m_aHttpClientSettings)) { aRequest.setAbsoluteRequestUri (true); LOGGER.info ("Performing SMP query at '" + aRequest.toString () + "'"); return aHttpClientMgr.execute (aRequest, aHttpContext, aResponseHandler); } catch (final RuntimeException | IOException ex) { if (GlobalDebug.isDebugMode ()) LOGGER.error ("Error performing SMP query", ex); else LOGGER.error ("Error performing SMP query: " + ex.getClass ().getName () + " - " + ex.getMessage ()); throw ex; } } /** * Execute a generic request on the SMP. This is e.g. helpful for accessing * the PEPPOL Directory BusinessCard API. This is equivalent to * {@link #executeRequest(HttpUriRequestBase, HttpClientResponseHandler)} but * includes the conversion of Exceptions to {@link SMPClientException} * objects. * * @param aRequest * The request to be executed. The proxy + connection and request * timeout are set in this method. * @param aResponseHandler * The response handler to be used. May not be null. * @return The return value of the response handler. * @throws SMPClientException * One of the converted exceptions * @param * Expected response type * @see #executeRequest(HttpUriRequestBase, HttpClientResponseHandler) * @see #getConvertedException(Exception) */ @Nonnull public T executeGenericRequest (@Nonnull final HttpUriRequestBase aRequest, @Nonnull final HttpClientResponseHandler aResponseHandler) throws SMPClientException { try { return executeRequest (aRequest, aResponseHandler); } catch (final Exception ex) { if (LOGGER.isDebugEnabled ()) LOGGER.debug ("Exception executing HTTP request " + aRequest, ex); throw getConvertedException (ex); } } /** * Convert the passed generic HTTP exception into a more specific exception. * * @param ex * The generic exception. May not be null. * @return A new SMP specific exception, using the passed exception as the * cause. */ @Nonnull public static SMPClientException getConvertedException (@Nonnull final Exception ex) { if (LOGGER.isDebugEnabled ()) LOGGER.debug ("Converting exception of class '" + ex.getClass ().getName () + "' to an SMP expception"); if (ex instanceof SMPClientException) return (SMPClientException) ex; if (ex instanceof HttpResponseException) { final HttpResponseException hex = (HttpResponseException) ex; final int nHttpStatus = hex.getStatusCode (); switch (nHttpStatus) { case HttpStatus.SC_BAD_REQUEST: return new SMPClientBadRequestException (hex); case HttpStatus.SC_FORBIDDEN: return new SMPClientUnauthorizedException (hex); case HttpStatus.SC_NOT_FOUND: return new SMPClientNotFoundException (hex); default: return new SMPClientException ("Error thrown with HTTP status code " + nHttpStatus, hex); } } // Special case if (ex instanceof UnknownHostException) return new SMPClientNotFoundException ((UnknownHostException) ex); if (ex instanceof ConnectException) return new SMPClientNotFoundException ((ConnectException) ex); // For new SMPClientBadResponseException if (ex instanceof ClientProtocolException && ex.getCause () instanceof SMPClientException) return (SMPClientException) ex.getCause (); // Generic version return new SMPClientException ("Unknown error thrown by SMP server (" + ex.getMessage () + ")", ex); } /** * Customize the JAXB marshaller, e.g. to add error handler etc. * * @param aMarshaller * Never null. * @since 8.6.3 * @see #getMarshallerCustomizer() * @see #setMarshallerCustomizer(Consumer) */ protected final void customizeMarshaller (@Nonnull final GenericJAXBMarshaller aMarshaller) { if (m_aMarshallerConsumer != null) m_aMarshallerConsumer.accept (aMarshaller); } /** * @return The JAXB Marshaller Customizer. May be null. * @since 8.6.3 */ @Nullable public final Consumer > getMarshallerCustomizer () { return m_aMarshallerConsumer; } /** * Set the JAXB Marshaller Customizer * * @param a * The customizer to be used. May be null. * @return this for chaining * @since 8.6.3 */ @Nonnull public final IMPLTYPE setMarshallerCustomizer (@Nullable final Consumer > a) { m_aMarshallerConsumer = a; return thisAsT (); } @Override public String toString () { return new ToStringGenerator (this).append ("SMPHost", m_sSMPHost) .append ("VerifySignature", m_bVerifySignature) .append ("TrustStore", m_aTrustStore) .append ("FollowSMPRedirects", m_bFollowSMPRedirects) .append ("XMLSchemaValidation", m_bXMLSchemaValidation) .append ("HttpClientSettings", m_aHttpClientSettings) .appendIfNotNull ("MarshallerConsumer", m_aMarshallerConsumer) .getToString (); } public static boolean containsRedirectSubject (@Nonnull final X509DataType aX509Data, @Nonnull final String sRedirectCertificateUID) throws SMPClientException { for (final Object aX509Obj : aX509Data.getX509IssuerSerialOrX509SKIOrX509SubjectName ()) if (aX509Obj instanceof JAXBElement ) { final JAXBElement aX509element = (JAXBElement ) aX509Obj; // Find the first subject (of type string) (element name // X509SubjectName) if (aX509element.getValue () instanceof String) { final String sSubject = (String) aX509element.getValue (); if (!sRedirectCertificateUID.equals (sSubject)) { throw new SMPClientException ("The certificate UID of the redirect did not match the certificate subject. Subject is '" + sSubject + "'. Required certificate UID is '" + sRedirectCertificateUID + "'"); } return true; } } return false; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy