eu.peppol.outbound.transmission.As2MessageSender Maven / Gradle / Ivy
/*
* 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.outbound.transmission;
import com.google.inject.Inject;
import eu.peppol.as2.*;
import eu.peppol.as2.evidence.As2RemWithMdnTransmissionEvidenceImpl;
import eu.peppol.as2.evidence.As2TransmissionEvidenceFactory;
import eu.peppol.identifier.ParticipantId;
import eu.peppol.identifier.PeppolDocumentTypeId;
import eu.peppol.identifier.TransmissionId;
import eu.peppol.security.CommonName;
import eu.peppol.security.KeystoreManager;
import eu.peppol.smp.SmpLookupManager;
import eu.peppol.xsd.ticc.receipt._1.TransmissionRole;
import no.difi.vefa.peppol.common.model.DocumentTypeIdentifier;
import no.difi.vefa.peppol.common.model.ParticipantIdentifier;
import no.difi.vefa.peppol.evidence.rem.EventCode;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
import org.apache.http.util.EntityUtils;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.mail.MessagingException;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ProxySelector;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Date;
import java.util.Map;
/**
* Thread safe implementation of a {@link MessageSender}, which sends messages using the AS2 protocol.
* Stores the outbound MIC for verification against the mic received from the MDN later.
*
* @author steinar
* @author thore
*/
class As2MessageSender implements MessageSender {
public static final Logger log = LoggerFactory.getLogger(As2MessageSender.class);
private final KeystoreManager keystoreManager;
private final As2TransmissionEvidenceFactory as2TransmissionEvidenceFactory;
private Mic mic;
private boolean traceEnabled;
@Inject
public As2MessageSender(KeystoreManager keystoreManager, As2TransmissionEvidenceFactory as2TransmissionEvidenceFactory) {
this.keystoreManager = keystoreManager;
this.as2TransmissionEvidenceFactory = as2TransmissionEvidenceFactory;
}
@Override
public TransmissionResponse send(TransmissionRequest transmissionRequest) {
SmpLookupManager.PeppolEndpointData endpointAddress = transmissionRequest.getEndpointAddress();
if (endpointAddress.getCommonName() == null) {
throw new IllegalStateException("Must supply the X.509 common name (AS2 System Identifier) for AS2 protocol");
}
// did we enable additional tracing
this.traceEnabled = transmissionRequest.isTraceEnabled();
ByteArrayInputStream inputStream = new ByteArrayInputStream(transmissionRequest.getPayload());
X509Certificate ourCertificate = keystoreManager.getOurCertificate();
// Establishes our AS2 System Identifier based upon the contents of the CN= field of the certificate
PeppolAs2SystemIdentifier as2SystemIdentifierOfSender = getAs2SystemIdentifierForSender(ourCertificate);
SendResult sendResult = send(inputStream,
transmissionRequest.getPeppolStandardBusinessHeader().getRecipientId(),
transmissionRequest.getPeppolStandardBusinessHeader().getSenderId(),
transmissionRequest.getPeppolStandardBusinessHeader().getDocumentTypeIdentifier(),
endpointAddress,
as2SystemIdentifierOfSender);
return new As2TransmissionResponse(sendResult.transmissionId, transmissionRequest.getPeppolStandardBusinessHeader(), endpointAddress.getUrl(), endpointAddress.getBusDoxProtocol(), endpointAddress.getCommonName(), sendResult.evidenceBytes);
}
SendResult send(InputStream inputStream,
ParticipantId recipient,
ParticipantId sender,
PeppolDocumentTypeId peppolDocumentTypeId,
SmpLookupManager.PeppolEndpointData peppolEndpointData,
PeppolAs2SystemIdentifier as2SystemIdentifierOfSender) {
if (peppolEndpointData.getCommonName() == null) {
throw new IllegalArgumentException("No common name in EndPoint object. " + peppolEndpointData);
}
X509Certificate ourCertificate = keystoreManager.getOurCertificate();
SMimeMessageFactory sMimeMessageFactory = new SMimeMessageFactory(keystoreManager.getOurPrivateKey(), ourCertificate);
MimeMessage signedMimeMessage = null;
Mic mic = null;
try {
MimeBodyPart mimeBodyPart = MimeMessageHelper.createMimeBodyPart(inputStream, new MimeType("application/xml"));
mic = MimeMessageHelper.calculateMic(mimeBodyPart);
log.debug("Outbound MIC is : " + mic.toString());
signedMimeMessage = sMimeMessageFactory.createSignedMimeMessage(mimeBodyPart);
} catch (MimeTypeParseException e) {
throw new IllegalStateException("Problems with MIME types: " + e.getMessage(), e);
}
String endpointAddress = peppolEndpointData.getUrl().toExternalForm();
HttpPost httpPost = new HttpPost(endpointAddress);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
signedMimeMessage.writeTo(byteArrayOutputStream);
} catch (Exception e) {
throw new IllegalStateException("Unable to stream S/MIME message into byte array output stream");
}
httpPost.addHeader(As2Header.AS2_FROM.getHttpHeaderName(), as2SystemIdentifierOfSender.toString());
try {
httpPost.setHeader(As2Header.AS2_TO.getHttpHeaderName(), PeppolAs2SystemIdentifier.valueOf(peppolEndpointData.getCommonName()).toString());
} catch (InvalidAs2SystemIdentifierException e) {
throw new IllegalArgumentException("Unable to create valid AS2 System Identifier for receiving end point: " + peppolEndpointData);
}
httpPost.addHeader(As2Header.DISPOSITION_NOTIFICATION_TO.getHttpHeaderName(), "[email protected]");
httpPost.addHeader(As2Header.DISPOSITION_NOTIFICATION_OPTIONS.getHttpHeaderName(), As2DispositionNotificationOptions.getDefault().toString());
httpPost.addHeader(As2Header.AS2_VERSION.getHttpHeaderName(), As2Header.VERSION);
httpPost.addHeader(As2Header.SUBJECT.getHttpHeaderName(), "AS2 message from OXALIS");
TransmissionId transmissionId = new TransmissionId();
httpPost.addHeader(As2Header.MESSAGE_ID.getHttpHeaderName(), transmissionId.toString());
httpPost.addHeader(As2Header.DATE.getHttpHeaderName(), As2DateUtil.format(new Date()));
// Inserts the S/MIME message to be posted.
// Make sure we pass the same content type as the SignedMimeMessage, it'll end up as content-type HTTP header
try {
String contentType = signedMimeMessage.getContentType();
ContentType ct = ContentType.create(contentType);
httpPost.setEntity(new ByteArrayEntity(byteArrayOutputStream.toByteArray(), ct));
} catch (Exception ex) {
throw new IllegalStateException("Unable to set request header content type : " + ex.getMessage());
}
CloseableHttpResponse postResponse = null; // EXECUTE !!!!
try {
CloseableHttpClient httpClient = createCloseableHttpClient();
log.debug("Sending AS2 from " + sender + " to " + recipient + " at " + endpointAddress + " type " + peppolDocumentTypeId);
postResponse = httpClient.execute(httpPost);
} catch (HttpHostConnectException e) {
throw new IllegalStateException("The Oxalis server does not seem to be running at " + endpointAddress);
} catch (Exception e) {
throw new IllegalStateException("Unexpected error during execution of http POST to " + endpointAddress + ": " + e.getMessage(), e);
}
if (postResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
log.error("AS2 HTTP POST expected HTTP OK, but got : " + postResponse.getStatusLine().getStatusCode() + " from " + endpointAddress);
throw handleFailedRequest(postResponse);
}
// handle normal HTTP OK response
log.debug("AS2 transmission " + transmissionId + " to " + endpointAddress + " returned HTTP OK, verify MDN response");
MimeMessage mimeMessage = handleTheHttpResponse(transmissionId, mic, postResponse, peppolEndpointData);
// Transforms the signed MDN into a generic a As2RemWithMdnTransmissionEvidenceImpl
MdnMimeMessageInspector mdnMimeMessageInspector = new MdnMimeMessageInspector(mimeMessage);
Map mdnFields = mdnMimeMessageInspector.getMdnFields();
String messageDigestAsBase64 = mdnFields.get(MdnMimeMessageFactory.X_ORIGINAL_MESSAGE_DIGEST);
if (messageDigestAsBase64 == null) {
messageDigestAsBase64 = new String(Base64.getEncoder().encode("null".getBytes()));
}
String receptionTimeStampAsString = mdnFields.get(MdnMimeMessageFactory.X_PEPPOL_TIME_STAMP);
Date receptionTimeStamp = null;
if (receptionTimeStampAsString != null) {
receptionTimeStamp = As2DateUtil.parseIso8601TimeStamp(receptionTimeStampAsString);
} else {
receptionTimeStamp = new Date();
}
// Converts the Oxalis DocumentTypeIdentifier into the corresponding type for peppol-evidence
DocumentTypeIdentifier documentTypeIdentifier = new DocumentTypeIdentifier(peppolDocumentTypeId.toString());
@NotNull As2RemWithMdnTransmissionEvidenceImpl evidence = as2TransmissionEvidenceFactory.createEvidence(EventCode.DELIVERY,
TransmissionRole.C_2, mimeMessage,
new ParticipantIdentifier(recipient.stringValue()), // peppol-evidence uses it's own types
new ParticipantIdentifier(sender.stringValue()), // peppol-evidence uses it's own types
documentTypeIdentifier,
receptionTimeStamp,
Base64.getDecoder().decode(messageDigestAsBase64),
transmissionId);
ByteArrayOutputStream evidenceBytes;
try {
evidenceBytes = new ByteArrayOutputStream();
IOUtils.copy(evidence.getInputStream(), evidenceBytes);
} catch (IOException e) {
throw new IllegalStateException("Unable to transform transport evidence to byte array." + e.getMessage(), e);
}
return new SendResult(transmissionId, evidenceBytes.toByteArray());
}
/**
* Handles the HTTP 200 POST response (the MDN with status indications)
*
* @param transmissionId the transmissionId (used in HTTP headers as Message-ID)
* @param outboundMic the calculated mic of the payload (should be verified against the one returned in MDN)
* @param postResponse the http response to be decoded as MDN
* @return
*/
MimeMessage handleTheHttpResponse(TransmissionId transmissionId, Mic outboundMic, CloseableHttpResponse postResponse, SmpLookupManager.PeppolEndpointData peppolEndpointData) {
try {
HttpEntity entity = postResponse.getEntity(); // Any textual results?
if (entity == null) {
throw new IllegalStateException("No contents in HTTP response with rc=" + postResponse.getStatusLine().getStatusCode());
}
String contents = EntityUtils.toString(entity);
if (traceEnabled) {
log.debug("HTTP-headers:");
Header[] allHeaders = postResponse.getAllHeaders();
for (Header header : allHeaders) {
log.debug("" + header.getName() + ": " + header.getValue());
}
log.debug("Contents:\n" + contents);
log.debug("---------------------------");
}
Header contentTypeHeader = postResponse.getFirstHeader("Content-Type");
if (contentTypeHeader == null) {
throw new IllegalStateException("No Content-Type header in response, probably a server error");
}
String contentType = contentTypeHeader.getValue();
MimeMessage mimeMessage = null;
try {
mimeMessage = MimeMessageHelper.parseMultipart(contents, new MimeType(contentType));
try {
mimeMessage.writeTo(System.out);
} catch (MessagingException e) {
throw new IllegalStateException("Unable to print mime message");
}
} catch (MimeTypeParseException e) {
throw new IllegalStateException("Invalid Content-Type header");
}
// verify the signature of the MDN, we warn about dodgy signatures
try {
SignedMimeMessage signedMimeMessage = new SignedMimeMessage(mimeMessage);
X509Certificate cert = signedMimeMessage.getSignersX509Certificate();
cert.checkValidity();
// Verify if the certificate used by the receiving Access Point in
// the response message does not match its certificate published by the SMP
if (peppolEndpointData.getCommonName() == null || !CommonName.valueOf(cert.getSubjectX500Principal()).equals(peppolEndpointData.getCommonName())) {
throw new CertificateException("Common name in certificate from SMP does not match common name in AP certificate");
}
log.debug("MDN signature was verfied for : " + cert.getSubjectDN().toString());
} catch (Exception ex) {
log.warn("Exception when verifying MDN signature : " + ex.getMessage());
}
// Verifies the actual MDN
MdnMimeMessageInspector mdnMimeMessageInspector = new MdnMimeMessageInspector(mimeMessage);
String msg = mdnMimeMessageInspector.getPlainTextPartAsText();
if (mdnMimeMessageInspector.isOkOrWarning(outboundMic)) {
return mimeMessage;
} else {
log.error("AS2 transmission failed with some error message, msg :" + msg);
log.error(contents);
throw new IllegalStateException("AS2 transmission failed : " + msg);
}
} catch (IOException e) {
throw new IllegalStateException("Unable to obtain the contents of the response: " + e.getMessage(), e);
} finally {
try {
postResponse.close();
} catch (IOException e) {
throw new IllegalStateException("Unable to close http connection: " + e.getMessage(), e);
}
}
}
IllegalStateException handleFailedRequest(CloseableHttpResponse postResponse) {
HttpEntity entity = postResponse.getEntity(); // Any results?
try {
if (entity == null) {
// No content returned
throw new IllegalStateException("Request failed with rc=" + postResponse.getStatusLine().getStatusCode() + ", no content returned in HTTP response");
} else {
String contents = EntityUtils.toString(entity);
throw new IllegalStateException("Request failed with rc=" + postResponse.getStatusLine().getStatusCode() + ", contents received (" + contents.trim().length() + " characters):" + contents);
}
} catch (IOException e) {
throw new IllegalStateException("Request failed with rc=" + postResponse.getStatusLine().getStatusCode()
+ ", ERROR while retrieving the contents of the response:" + e.getMessage(), e);
}
}
private PeppolAs2SystemIdentifier getAs2SystemIdentifierForSender(X509Certificate ourCertificate) {
PeppolAs2SystemIdentifier peppolAs2SystemIdentifier = null;
try {
peppolAs2SystemIdentifier = PeppolAs2SystemIdentifier.valueOf(CommonName.valueOf(ourCertificate.getSubjectX500Principal()));
} catch (InvalidAs2SystemIdentifierException e) {
throw new IllegalStateException("AS2 System Identifier could not be obtained from " + ourCertificate.getSubjectX500Principal(), e);
}
return peppolAs2SystemIdentifier;
}
CloseableHttpClient createCloseableHttpClient() {
// "SSLv3" is disabled by default : http://www.apache.org/dist/httpcomponents/httpclient/RELEASE_NOTES-4.3.x.txt
SystemDefaultRoutePlanner routePlanner = new SystemDefaultRoutePlanner(ProxySelector.getDefault());
CloseableHttpClient httpclient = HttpClients.custom()
.setRoutePlanner(routePlanner)
.build();
return httpclient;
}
private static class SendResult {
final TransmissionId transmissionId;
final byte[] evidenceBytes;
public SendResult(TransmissionId transmissionId, byte[] evidenceBytes) {
this.transmissionId = transmissionId;
this.evidenceBytes = evidenceBytes;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy