/** * The FreeBSD Copyright * Copyright 1994-2008 The FreeBSD Project. All rights reserved. * Copyright (C) 2013-2017 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 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.CertificateExistsException; import com.helger.as2lib.cert.PKCS12CertificateFactory; import com.helger.as2lib.exception.OpenAS2Exception; 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.AbstractHttpSenderModule; import com.helger.as2lib.processor.sender.IProcessorSenderModule; import com.helger.as2lib.session.AS2Session; import com.helger.as2lib.util.StringMap; import com.helger.commons.ValueEnforcer; import com.helger.commons.annotation.OverrideOnDemand; import com.helger.commons.collection.ext.CommonsHashMap; import com.helger.commons.collection.ext.ICommonsMap; import com.helger.commons.factory.FactoryNewInstance; import com.helger.commons.factory.IFactory; import com.helger.commons.timing.StopWatch; /** * A simple client that allows for sending AS2 Messages and retrieving of * synchronous MDNs. * * @author Philip Helger */ public class AS2Client { private static final Logger s_aLogger = LoggerFactory.getLogger (AS2Client.class); private IFactory <AS2SenderModule> 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 <code>null</code>. */ public void setAS2SenderModuleFactory (@Nonnull final IFactory <AS2SenderModule> aAS2SenderModuleFactory) { m_aAS2SenderModuleFactory = ValueEnforcer.notNull (aAS2SenderModuleFactory, "AS2SenderModuleFactory"); } /** * @return The current HTTP proxy used. Defaults to <code>null</code>. */ @Nullable public Proxy getHttpProxy () { return m_aHttpProxy; } /** * Set the proxy server for transmission. * * @param aHttpProxy * The proxy to use. May be <code>null</code> to indicate no proxy. * @return this */ @Nonnull public AS2Client setHttpProxy (@Nullable final Proxy aHttpProxy) { m_aHttpProxy = aHttpProxy; return this; } @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 ()); // We want a sync MDN: aPartnership.setAS2MDNOptions (aSettings.getMDNOptions ()); if (false) aPartnership.setAS2MDNTo ("http://localhost:10080"); // We don't want an async MDN: aPartnership.setAS2ReceiptOption (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.setAttribute (CPartnershipIDs.PA_AS2_URL, aPartnership.getAS2URL ()); aMsg.setAttribute (CPartnershipIDs.PID_AS2, aPartnership.getReceiverAS2ID ()); aMsg.setAttribute (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 * <code>null</code>. */ @Nonnull @OverrideOnDemand protected PKCS12CertificateFactory createCertificateFactory () { return new PKCS12CertificateFactory (); } @OverrideOnDemand protected void initCertificateFactory (@Nonnull final AS2ClientSettings aSettings, @Nonnull final AS2Session aSession) throws OpenAS2Exception { // Dynamically add certificate factory final StringMap aParams = new StringMap (); aParams.setAttribute (PKCS12CertificateFactory.ATTR_FILENAME, aSettings.getKeyStoreFile ().getAbsolutePath ()); aParams.setAttribute (PKCS12CertificateFactory.ATTR_PASSWORD, aSettings.getKeyStorePassword ()); aParams.setAttribute (PKCS12CertificateFactory.ATTR_SAVE_CHANGES_TO_FILE, aSettings.isSaveKeyStoreChangesToFile ()); final PKCS12CertificateFactory aCertFactory = createCertificateFactory (); aCertFactory.initDynamicComponent (aSession, aParams); if (aSettings.getReceiverCertificate () != null) { // Dynamically add recipient certificate if provided try { aCertFactory.addCertificate (aSettings.getReceiverKeyAlias (), aSettings.getReceiverCertificate (), false); } catch (final CertificateExistsException ex) { // ignore } } aSession.setCertificateFactory (aCertFactory); } @OverrideOnDemand protected void initPartnershipFactory (@Nonnull final AS2Session aSession) throws OpenAS2Exception { // 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 OpenAS2Exception { 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 <code>null</code>. */ @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 <code>null</code>. */ @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 <code>null</code>. * @param aSession * Current session. Never <code>null</code>. * @param aMsg * Current message. Never <code>null</code>. */ @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 <code>null</code>. * @param aRequest * The request data to be send. May not be <code>null</code>. * @return The response object. Never <code>null</code>. */ @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 (s_aLogger.isDebugEnabled ()) s_aLogger.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 <String, Object> aHandleOptions = new CommonsHashMap<> (); if (bHasRetries) aHandleOptions.put (IProcessorResenderModule.OPTION_RETRIES, Integer.toString (aSettings.getRetryCount ())); // 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.setAttribute (AbstractHttpSenderModule.ATTR_CONNECT_TIMEOUT, Integer.toString (aSettings.getConnectTimeoutMS ())); aSender.setAttribute (AbstractHttpSenderModule.ATTR_READ_TIMEOUT, Integer.toString (aSettings.getReadTimeoutMS ())); aSession.getMessageProcessor ().addModule (aSender); aSender.handle (IProcessorSenderModule.DO_SEND, aMsg, aHandleOptions); } finally { aSession.getMessageProcessor ().stopActiveModules (); } } catch (final Throwable t) { s_aLogger.error ("Error sending AS2 message", t); aResponse.setException (t); } finally { if (aMsg != null && aMsg.getMDN () != null) { // May be present, even in case of an exception aResponse.setMDN (aMsg.getMDN ()); } } if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Response retrieved: " + aResponse.getAsString ()); aResponse.setExecutionDuration (aSW.stopAndGetDuration ()); return aResponse; } }