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

com.helger.as2lib.client.AS2Client Maven / Gradle / Ivy

/**
 * The FreeBSD Copyright
 * Copyright 1994-2008 The FreeBSD Project. All rights reserved.
 * Copyright (C) 2013-2020 Philip Helger philip[at]helger[dot]com
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD PROJECT OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation
 * are those of the authors and should not be interpreted as representing
 * official policies, either expressed or implied, of the FreeBSD Project.
 */
package com.helger.as2lib.client;

import java.net.Proxy;
import java.security.cert.X509Certificate;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import javax.mail.MessagingException;
import javax.mail.internet.MimeBodyPart;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.as2lib.cert.AS2CertificateExistsException;
import com.helger.as2lib.cert.CertificateFactory;
import com.helger.as2lib.exception.AS2Exception;
import com.helger.as2lib.message.AS2Message;
import com.helger.as2lib.message.IMessage;
import com.helger.as2lib.partner.CPartnershipIDs;
import com.helger.as2lib.partner.Partnership;
import com.helger.as2lib.partner.SelfFillingPartnershipFactory;
import com.helger.as2lib.processor.DefaultMessageProcessor;
import com.helger.as2lib.processor.IMessageProcessor;
import com.helger.as2lib.processor.resender.IProcessorResenderModule;
import com.helger.as2lib.processor.resender.ImmediateResenderModule;
import com.helger.as2lib.processor.sender.AS2SenderModule;
import com.helger.as2lib.processor.sender.IProcessorSenderModule;
import com.helger.as2lib.session.AS2Session;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.collection.attr.StringMap;
import com.helger.commons.collection.impl.CommonsHashMap;
import com.helger.commons.collection.impl.ICommonsMap;
import com.helger.commons.factory.FactoryNewInstance;
import com.helger.commons.functional.ISupplier;
import com.helger.commons.io.stream.NonBlockingByteArrayInputStream;
import com.helger.commons.timing.StopWatch;
import com.helger.security.certificate.CertificateHelper;

/**
 * A simple client that allows for sending AS2 Messages and retrieving of
 * synchronous MDNs.
 *
 * @author Philip Helger
 */
public class AS2Client
{
  private static final Logger LOGGER = LoggerFactory.getLogger (AS2Client.class);
  private ISupplier  m_aAS2SenderModuleFactory = FactoryNewInstance.create (AS2SenderModule.class,
                                                                                             true);
  // proxy is not serializable
  private Proxy m_aHttpProxy;

  public AS2Client ()
  {}

  /**
   * Set the factory to create {@link AS2SenderModule} objects internally. By
   * default a new instance of {@link AS2SenderModule} is created so you don't
   * need to call this method.
   *
   * @param aAS2SenderModuleFactory
   *        The factory to be used. May not be null.
   * @return this for chaining
   */
  @Nonnull
  public AS2Client setAS2SenderModuleFactory (@Nonnull final ISupplier  aAS2SenderModuleFactory)
  {
    m_aAS2SenderModuleFactory = ValueEnforcer.notNull (aAS2SenderModuleFactory, "AS2SenderModuleFactory");
    return this;
  }

  /**
   * @return The current HTTP proxy used. Defaults to null.
   */
  @Nullable
  public Proxy getHttpProxy ()
  {
    return m_aHttpProxy;
  }

  /**
   * Set the proxy server for transmission.
   *
   * @param aHttpProxy
   *        The proxy to use. May be null to indicate no proxy.
   * @return this for chaining
   */
  @Nonnull
  public AS2Client setHttpProxy (@Nullable final Proxy aHttpProxy)
  {
    m_aHttpProxy = aHttpProxy;
    return this;
  }

  /**
   * Create a new {@link Partnership} object that is later used for message
   * creation. If you override this method, please ensure to call this class'
   * version of the method first.
   *
   * @param aSettings
   *        The current client settings. Never null.
   * @return Non-null Partnership.
   */
  @Nonnull
  @OverrideOnDemand
  @OverridingMethodsMustInvokeSuper
  protected Partnership buildPartnership (@Nonnull final AS2ClientSettings aSettings)
  {
    final Partnership aPartnership = new Partnership (aSettings.getPartnershipName ());

    aPartnership.setSenderAS2ID (aSettings.getSenderAS2ID ());
    aPartnership.setSenderX509Alias (aSettings.getSenderKeyAlias ());
    aPartnership.setSenderEmail (aSettings.getSenderEmailAddress ());

    aPartnership.setReceiverAS2ID (aSettings.getReceiverAS2ID ());
    aPartnership.setReceiverX509Alias (aSettings.getReceiverKeyAlias ());

    aPartnership.setAS2URL (aSettings.getDestinationAS2URL ());
    aPartnership.setEncryptAlgorithm (aSettings.getCryptAlgo ());
    aPartnership.setSigningAlgorithm (aSettings.getSignAlgo ());
    aPartnership.setProtocol (AS2Message.PROTOCOL_AS2);
    aPartnership.setMessageIDFormat (aSettings.getMessageIDFormat ());

    if (aSettings.isMDNRequested ())
    {
      // We want an MDN

      // This field must be set to a valid email address
      aPartnership.setAS2MDNTo (aSettings.getSenderEmailAddress ());

      // signed MDN requested?
      aPartnership.setAS2MDNOptions (aSettings.getMDNOptions ());

      if (aSettings.isAsyncMDNRequested ())
      {
        // We want an async MDN
        aPartnership.setAS2ReceiptDeliveryOption (aSettings.getAsyncMDNUrl ());
      }
      else
      {
        // We want an sync MDN
        // This field must be null - otherwise async MDN!
        aPartnership.setAS2ReceiptDeliveryOption (null);
      }
    }
    else
    {
      // We don't want an MDN
      aPartnership.setAS2MDNTo (null);
      aPartnership.setAS2ReceiptDeliveryOption (null);
      aPartnership.setAS2MDNOptions (null);
    }

    if (aSettings.getCompressionType () != null)
    {
      aPartnership.setCompressionType (aSettings.getCompressionType ());
      aPartnership.setCompressionMode (aSettings.isCompressBeforeSigning () ? CPartnershipIDs.COMPRESS_BEFORE_SIGNING
                                                                            : CPartnershipIDs.COMPRESS_AFTER_SIGNING);
    }

    return aPartnership;
  }

  @Nonnull
  @OverrideOnDemand
  protected AS2Message createAS2MessageObj ()
  {
    return new AS2Message ();
  }

  @Nonnull
  @OverrideOnDemand
  @OverridingMethodsMustInvokeSuper
  protected AS2Message createMessage (@Nonnull final Partnership aPartnership,
                                      @Nonnull final AS2ClientRequest aRequest) throws MessagingException
  {
    final AS2Message aMsg = createAS2MessageObj ();
    aMsg.setContentType (aRequest.getContentType ());
    aMsg.setSubject (aRequest.getSubject ());
    aMsg.setPartnership (aPartnership);
    aMsg.setMessageID (aMsg.generateMessageID ());

    aMsg.attrs ().putIn (CPartnershipIDs.PA_AS2_URL, aPartnership.getAS2URL ());
    aMsg.attrs ().putIn (CPartnershipIDs.PID_AS2, aPartnership.getReceiverAS2ID ());
    aMsg.attrs ().putIn (CPartnershipIDs.PID_EMAIL, aPartnership.getSenderEmail ());

    // Build message content
    final MimeBodyPart aPart = new MimeBodyPart ();
    aRequest.applyDataOntoMimeBodyPart (aPart);
    aMsg.setData (aPart);

    return aMsg;
  }

  /**
   * @return The certificate factory instance to be used. May not be
   *         null.
   */
  @Nonnull
  @OverrideOnDemand
  protected CertificateFactory createCertificateFactory ()
  {
    return new CertificateFactory ();
  }

  @OverrideOnDemand
  protected void initCertificateFactory (@Nonnull final AS2ClientSettings aSettings,
                                         @Nonnull final AS2Session aSession) throws AS2Exception
  {
    final StringMap aParams = new StringMap ();
    // TYPE is the only parameter that must be present in initDynamicComponents
    aParams.putIn (CertificateFactory.ATTR_TYPE, aSettings.getKeyStoreType ().getID ());

    final CertificateFactory aCertFactory = createCertificateFactory ();
    aCertFactory.initDynamicComponent (aSession, aParams);

    if (aSettings.getKeyStoreFile () != null)
    {
      if (LOGGER.isInfoEnabled ())
        LOGGER.info ("Loading AS2 client keystore from file " + aSettings.getKeyStoreFile ());

      aCertFactory.setFilename (aSettings.getKeyStoreFile ().getAbsolutePath ());
      aCertFactory.setPassword (aSettings.getKeyStorePassword ());
      aCertFactory.setSaveChangesToFile (aSettings.isSaveKeyStoreChangesToFile ());
      aCertFactory.load ();
    }
    else
      if (aSettings.getKeyStoreBytes () != null && aSettings.getKeyStorePassword () != null)
      {
        if (LOGGER.isInfoEnabled ())
          LOGGER.info ("Loading AS2 client keystore from byte array. No changes will be saved.");

        aCertFactory.setPassword (aSettings.getKeyStorePassword ());
        aCertFactory.setSaveChangesToFile (false);
        try (
            final NonBlockingByteArrayInputStream aBAIS = new NonBlockingByteArrayInputStream (aSettings.getKeyStoreBytes ()))
        {
          aCertFactory.load (aBAIS, aSettings.getKeyStorePassword ().toCharArray ());
        }
      }
      else
      {
        if (LOGGER.isInfoEnabled ())
          LOGGER.warn ("No AS2 client keystore data was provided. Signing and encryption/decryption will most likely fail.");

        // No file provided - no storage
        aCertFactory.setSaveChangesToFile (false);
      }

    if (aSettings.getReceiverCertificate () != null)
    {
      // Dynamically add recipient certificate if provided
      try
      {
        aCertFactory.addCertificate (aSettings.getReceiverKeyAlias (), aSettings.getReceiverCertificate (), false);
      }
      catch (final AS2CertificateExistsException ex)
      {
        // ignore
      }
    }
    aSession.setCertificateFactory (aCertFactory);
  }

  @OverrideOnDemand
  protected void initPartnershipFactory (@Nonnull final AS2Session aSession) throws AS2Exception
  {
    // Use a self-filling in-memory partnership factory
    final SelfFillingPartnershipFactory aPartnershipFactory = new SelfFillingPartnershipFactory ();
    aSession.setPartnershipFactory (aPartnershipFactory);
  }

  @OverrideOnDemand
  protected void initMessageProcessor (@Nonnull final AS2Session aSession) throws AS2Exception
  {
    final IMessageProcessor aMessageProcessor = new DefaultMessageProcessor ();
    aSession.setMessageProcessor (aMessageProcessor);
  }

  /**
   * Create an empty response object that is to be filled.
   *
   * @return The empty response object and never null.
   */
  @Nonnull
  @OverrideOnDemand
  protected AS2ClientResponse createResponse ()
  {
    return new AS2ClientResponse ();
  }

  /**
   * Create the AS2 session to be used. This method must ensure an eventually
   * needed proxy is set.
   *
   * @return The new AS2 session and never null.
   */
  @Nonnull
  @OverrideOnDemand
  protected AS2Session createSession ()
  {
    final AS2Session ret = new AS2Session ();
    ret.setHttpProxy (m_aHttpProxy);
    return ret;
  }

  /**
   * Callback method that is invoked before the main sending. This may be used
   * to customize the message.
   *
   * @param aSettings
   *        Client settings. Never null.
   * @param aSession
   *        Current session. Never null.
   * @param aMsg
   *        Current message. Never null.
   */
  @OverrideOnDemand
  protected void beforeSend (@Nonnull final AS2ClientSettings aSettings,
                             @Nonnull final AS2Session aSession,
                             @Nonnull final IMessage aMsg)
  {}

  /**
   * Send the AS2 message synchronously
   *
   * @param aSettings
   *        The settings to be used. May not be null.
   * @param aRequest
   *        The request data to be send. May not be null.
   * @return The response object. Never null.
   */
  @Nonnull
  public AS2ClientResponse sendSynchronous (@Nonnull final AS2ClientSettings aSettings,
                                            @Nonnull final AS2ClientRequest aRequest)
  {
    ValueEnforcer.notNull (aSettings, "ClientSettings");
    ValueEnforcer.notNull (aRequest, "ClientRequest");

    final AS2ClientResponse aResponse = createResponse ();
    IMessage aMsg = null;
    final StopWatch aSW = StopWatch.createdStarted ();
    try
    {
      final Partnership aPartnership = buildPartnership (aSettings);

      aMsg = createMessage (aPartnership, aRequest);
      aResponse.setOriginalMessageID (aMsg.getMessageID ());

      if (LOGGER.isDebugEnabled ())
        LOGGER.debug ("MessageID to send: " + aMsg.getMessageID ());

      final boolean bHasRetries = aSettings.getRetryCount () > 0;

      // Start a new session
      final AS2Session aSession = createSession ();

      initCertificateFactory (aSettings, aSession);
      initPartnershipFactory (aSession);
      initMessageProcessor (aSession);

      if (bHasRetries)
      {
        // Use synchronous no-delay resender
        final IProcessorResenderModule aResender = new ImmediateResenderModule ();
        aResender.initDynamicComponent (aSession, null);
        aSession.getMessageProcessor ().addModule (aResender);
      }

      aSession.getMessageProcessor ().startActiveModules ();
      try
      {
        // Invoke callback
        beforeSend (aSettings, aSession, aMsg);

        // Build options map for "handle"
        final ICommonsMap  aHandleOptions = new CommonsHashMap <> ();
        if (bHasRetries)
          aHandleOptions.put (IProcessorResenderModule.OPTION_RETRIES, Integer.toString (aSettings.getRetryCount ()));

        // Content-Transfer-Encoding is a partnership property
        aPartnership.setContentTransferEncodingSend (aRequest.getContentTransferEncoding ());
        aPartnership.setContentTransferEncodingReceive (aRequest.getContentTransferEncoding ());

        // And create a sender module that directly sends the message
        // The message processor registration is required for the resending
        // feature
        final AS2SenderModule aSender = m_aAS2SenderModuleFactory.get ();
        aSender.initDynamicComponent (aSession, null);
        // Set connect and read timeout
        aSender.setConnectionTimeoutMilliseconds (aSettings.getConnectTimeoutMS ());
        aSender.setReadTimeoutMilliseconds (aSettings.getReadTimeoutMS ());
        aSender.setQuoteHeaderValues (aSettings.isQuoteHeaderValues ());
        aSender.setHttpOutgoingDumperFactory (aSettings.getHttpOutgoingDumperFactory ());
        aSender.setHttpIncomingDumper (aSettings.getHttpIncomingDumper ());
        if (aSettings.getMICMatchingHandler () != null)
          aSender.setMICMatchingHandler (aSettings.getMICMatchingHandler ());
        aSender.setVerificationCertificateConsumer (aSettings.getVerificationCertificateConsumer ());

        // Add all custom headers
        aMsg.headers ().setAllHeaders (aSettings.customHeaders ());

        // Added sender as processor
        aSession.getMessageProcessor ().addModule (aSender);

        // Main sending
        aSender.handle (IProcessorSenderModule.DO_SEND, aMsg, aHandleOptions);
      }
      finally
      {
        aSession.getMessageProcessor ().stopActiveModules ();
      }
    }
    catch (final Exception ex)
    {
      LOGGER.error ("Error sending AS2 message", ex);
      aResponse.setException (ex);
    }
    finally
    {
      if (aMsg != null && aMsg.getMDN () != null)
      {
        // May be present, even in case of an exception
        aResponse.setMDN (aMsg.getMDN ());

        // Remember the certificate that was used to verify the MDN
        final String sReceivedCert = aMsg.attrs ().getAsString (AS2Message.ATTRIBUTE_RECEIVED_SIGNATURE_CERTIFICATE);
        if (sReceivedCert != null)
        {
          final X509Certificate aReceivedCert = CertificateHelper.convertStringToCertficateOrNull (sReceivedCert);
          aResponse.setMDNVerificationCertificate (aReceivedCert);
        }
      }
    }

    if (LOGGER.isDebugEnabled ())
      LOGGER.debug ("Response retrieved: " + aResponse.getAsString ());

    aResponse.setExecutionDuration (aSW.stopAndGetDuration ());
    return aResponse;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy