diff --git a/pom.xml b/pom.xml
index eaa1873..ff4bdcb 100755
--- a/pom.xml
+++ b/pom.xml
@@ -131,11 +131,6 @@
peppol-sbdh
provided
-
- no.difi.vefa
- peppol-security
- provided
-
no.difi.vefa
peppol-mode
@@ -302,6 +297,12 @@
org.apache.neethi
neethi
${neethi.version}
+
+
+ woodstox-core-asl
+ org.codehaus.woodstox
+
+
@@ -313,7 +314,7 @@
org.projectlombok
lombok
- 1.18.8
+ 1.18.12
provided
diff --git a/src/main/java/no/difi/oxalis/as4/inbound/As4EndpointsPublisherImpl.java b/src/main/java/no/difi/oxalis/as4/inbound/As4EndpointsPublisherImpl.java
index 7dc09e9..2dc687f 100644
--- a/src/main/java/no/difi/oxalis/as4/inbound/As4EndpointsPublisherImpl.java
+++ b/src/main/java/no/difi/oxalis/as4/inbound/As4EndpointsPublisherImpl.java
@@ -1,6 +1,7 @@
package no.difi.oxalis.as4.inbound;
import com.google.inject.Inject;
+import org.apache.cxf.attachment.As4AttachmentInInterceptor;
import org.apache.cxf.Bus;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.binding.soap.SoapVersion;
@@ -8,7 +9,7 @@
import org.apache.cxf.binding.soap.interceptor.ReadHeadersInterceptor;
import org.apache.cxf.binding.soap.interceptor.StartBodyInterceptor;
import org.apache.cxf.ext.logging.LoggingFeature;
-import org.apache.cxf.interceptor.AttachmentInInterceptor;
+import org.apache.cxf.interceptor.StaxInEndingInterceptor;
import org.apache.cxf.interceptor.StaxInInterceptor;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.jaxws.handler.soap.SOAPHandlerFaultInInterceptor;
@@ -20,6 +21,8 @@
import javax.xml.ws.Endpoint;
import java.util.Arrays;
+import static org.apache.cxf.ws.security.SecurityConstants.ENABLE_STREAMING_SECURITY;
+
public class As4EndpointsPublisherImpl implements As4EndpointsPublisher {
@Inject
@@ -47,12 +50,15 @@ public EndpointImpl publish(Bus bus) {
new WSPolicyFeature());
endpoint.getServer().getEndpoint().put("allow-multiplex-endpoint", Boolean.TRUE);
+ endpoint.getServer().getEndpoint().put(ENABLE_STREAMING_SECURITY, false);
endpoint.getServer().getEndpoint()
.put(As4EndpointSelector.ENDPOINT_NAME, As4EndpointSelector.OXALIS_AS4_ENDPOINT_NAME);
endpoint.getBinding().setHandlerChain(Arrays.asList(as4FaultInHandler, new MessagingHandler()));
endpoint.getInInterceptors().add(oxalisAs4Interceptor);
endpoint.getInInterceptors().add(setPolicyInInterceptor);
+ endpoint.getInInterceptors().add(new AttachmentCleanupInterceptor());
+
endpoint.getOutInterceptors().add(setPolicyOutInterceptor);
endpoint.getInFaultInterceptors().add(setPolicyInInterceptor);
endpoint.getOutFaultInterceptors().add(setPolicyOutInterceptor);
@@ -64,13 +70,15 @@ protected Message createMessage(Message message) {
}
};
- newMO.getBindingInterceptors().add(new AttachmentInInterceptor());
+ newMO.getBindingInterceptors().add(new As4AttachmentInInterceptor());
newMO.getBindingInterceptors().add(new StaxInInterceptor());
+ newMO.getBindingInterceptors().add(new StaxInEndingInterceptor());
newMO.getBindingInterceptors().add(new SOAPHandlerFaultInInterceptor(endpoint.getBinding()));
newMO.getBindingInterceptors().add(new ReadHeadersInterceptor(bus, (SoapVersion) null));
newMO.getBindingInterceptors().add(new StartBodyInterceptor());
newMO.getBindingInterceptors().add(new CheckFaultInterceptor());
+
// Add in a default selection interceptor
newMO.getRoutingInterceptors().add(endpointSelector);
newMO.getEndpoints().add(endpoint.getServer().getEndpoint());
diff --git a/src/main/java/no/difi/oxalis/as4/inbound/As4InboundHandler.java b/src/main/java/no/difi/oxalis/as4/inbound/As4InboundHandler.java
index 3b1e57e..1708314 100755
--- a/src/main/java/no/difi/oxalis/as4/inbound/As4InboundHandler.java
+++ b/src/main/java/no/difi/oxalis/as4/inbound/As4InboundHandler.java
@@ -35,6 +35,7 @@
import javax.xml.soap.*;
import javax.xml.ws.handler.MessageContext;
+import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -334,7 +335,7 @@ private LinkedHashMap parseAttachments(Iterator p.getName() + "=" + p.getValue())
@@ -353,27 +354,18 @@ private LinkedHashMap parseAttachments(Iterator parseAttachments(Iterator parseAttachments(Iterator {
+
+ public AttachmentCleanupInterceptor() {
+ super(Phase.POST_INVOKE);
+ }
+
+ public void handleMessage(Message message) throws Fault {
+ Exchange exchange = message.getExchange();
+ cleanRequestAttachment(exchange);
+ }
+
+ private void cleanRequestAttachment(Exchange exchange) {
+ As4AttachmentDeserializer ad = exchange.getInMessage().get(As4AttachmentDeserializer.class);
+ ad.getRemoved().forEach(this::close);
+ }
+
+ @SneakyThrows
+ private void close(Attachment attachment) {
+ DataSource dataSource = attachment.getDataHandler().getDataSource();
+
+ if (dataSource instanceof As4AttachmentDataSource) {
+ As4AttachmentDataSource ads = (As4AttachmentDataSource) dataSource;
+ ads.closeAll();
+ }
+ }
+}
diff --git a/src/main/java/no/difi/oxalis/as4/inbound/CertificateValidatorSignatureTrustValidator.java b/src/main/java/no/difi/oxalis/as4/inbound/CertificateValidatorSignatureTrustValidator.java
deleted file mode 100644
index 52fc279..0000000
--- a/src/main/java/no/difi/oxalis/as4/inbound/CertificateValidatorSignatureTrustValidator.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package no.difi.oxalis.as4.inbound;
-
-import no.difi.vefa.peppol.security.api.CertificateValidator;
-import org.apache.wss4j.common.crypto.Crypto;
-import org.apache.wss4j.common.ext.WSSecurityException;
-import org.apache.wss4j.dom.handler.RequestData;
-import org.apache.wss4j.dom.validate.SignatureTrustValidator;
-
-import java.security.PublicKey;
-import java.security.cert.X509Certificate;
-
-public class CertificateValidatorSignatureTrustValidator extends SignatureTrustValidator {
-
-
- private final CertificateValidator certificateValidator;
-
- public CertificateValidatorSignatureTrustValidator(CertificateValidator certificateValidator) {
- this.certificateValidator = certificateValidator;
- }
-
- @Override
- protected void validateCertificates(X509Certificate[] certificates) throws WSSecurityException {
- super.validateCertificates(certificates);
- }
-
- @Override
- protected Crypto getCrypto(RequestData data) {
- return super.getCrypto(data);
- }
-
- @Override
- protected void verifyTrustInCerts(X509Certificate[] certificates, Crypto crypto, RequestData data, boolean enableRevocation) throws WSSecurityException {
- super.verifyTrustInCerts(certificates, crypto, data, enableRevocation);
- }
-
- @Override
- protected void validatePublicKey(PublicKey publicKey, Crypto crypto) throws WSSecurityException {
- super.validatePublicKey(publicKey, crypto);
- }
-}
-
-
diff --git a/src/main/java/no/difi/oxalis/as4/inbound/OxalisAS4WsInInterceptor.java b/src/main/java/no/difi/oxalis/as4/inbound/OxalisAS4WsInInterceptor.java
deleted file mode 100644
index d102787..0000000
--- a/src/main/java/no/difi/oxalis/as4/inbound/OxalisAS4WsInInterceptor.java
+++ /dev/null
@@ -1,116 +0,0 @@
-package no.difi.oxalis.as4.inbound;
-
-import no.difi.oxalis.as4.lang.OxalisAs4Exception;
-import no.difi.oxalis.as4.util.Constants;
-import org.apache.cxf.binding.soap.SoapMessage;
-import org.apache.cxf.helpers.CastUtils;
-import org.apache.cxf.interceptor.Fault;
-import org.apache.cxf.message.Attachment;
-import org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor;
-import org.apache.wss4j.common.crypto.Crypto;
-
-import javax.activation.DataHandler;
-import javax.xml.namespace.QName;
-import javax.xml.soap.AttachmentPart;
-import javax.xml.soap.SOAPMessage;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.*;
-import java.util.stream.Stream;
-import java.util.zip.GZIPInputStream;
-
-import static org.apache.cxf.rt.security.SecurityConstants.*;
-import static org.apache.cxf.ws.security.SecurityConstants.RETURN_SECURITY_ERROR;
-import static org.apache.wss4j.common.ConfigurationConstants.*;
-
-public class OxalisAS4WsInInterceptor extends WSS4JInInterceptor {
-
- private Crypto crypto;
- private String alias;
-
- OxalisAS4WsInInterceptor(Map props, Crypto crypto, String alias) {
- super(props);
- this.crypto = crypto;
- this.alias = alias;
- }
-
- @Override
- public void handleMessage(SoapMessage msg) throws Fault {
- msg.put(ENCRYPT_CRYPTO, crypto);
- msg.put(SIGNATURE_CRYPTO, crypto);
- msg.put(ENCRYPT_USERNAME, alias);
- msg.put(SIGNATURE_USERNAME, alias);
- msg.put(USERNAME, alias);
- msg.put(RETURN_SECURITY_ERROR, Boolean.TRUE);
-
- msg.putIfAbsent(ACTION, SIGNATURE + " " + ENCRYPT);
-
- Collection attachments = new ArrayList<>();
- if (msg.getAttachments() != null) {
- attachments.addAll(msg.getAttachments());
- }
-
-
- try {
- super.handleMessage(msg);
- } catch (Exception t) {
- if (attachmentsIsCompressed(attachments)) {
- msg.put("oxalis.as4.compressionErrorDetected", true);
- }
- throw t;
- }
-
-
- SOAPMessage soapMessage = msg.getContent(SOAPMessage.class);
-
- if (soapMessage != null && soapMessage.countAttachments() > 0) {
- Iterator it = CastUtils.cast(soapMessage.getAttachments());
- while (it.hasNext()) {
- AttachmentPart part = it.next();
- Attachment first = msg.getAttachments().stream()
- .filter(a -> a.getId().equals(part.getContentId().replaceAll("[<>]", "")))
- .findFirst()
- .orElseThrow(() -> new Fault(new OxalisAs4Exception("Unable to find attachment")));
- part.setDataHandler(first.getDataHandler());
- }
- }
- }
-
- @Override
- public Set getUnderstoodHeaders() {
- Set understoodHeaders = super.getUnderstoodHeaders();
- understoodHeaders.add(Constants.MESSAGING_QNAME);
- return understoodHeaders;
- }
-
- private boolean attachmentsIsCompressed(Collection attachments) {
-
- return Optional.of(attachments)
- .map(Collection::stream).orElseGet(Stream::empty)
- .map(Attachment::getDataHandler)
- .filter(Objects::nonNull)
- .anyMatch(this::isInputStreamZipped);
- }
-
- private boolean isInputStreamZipped(DataHandler dataHandler) {
- try {
- return canExtractZipEntry(dataHandler.getInputStream());
- } catch (IOException e) {
- return false;
- }
- }
-
- private boolean canExtractZipEntry(InputStream is) {
- try {
-
- byte[] testExtraction = new byte[20];
- GZIPInputStream zis = new GZIPInputStream(is);
- zis.read(testExtraction);
-
- } catch (IOException e) {
- return false;
- }
-
- return true;
- }
-}
diff --git a/src/main/java/no/difi/oxalis/as4/inbound/OxalisAs4WsOutInterceptor.java b/src/main/java/no/difi/oxalis/as4/inbound/OxalisAs4WsOutInterceptor.java
deleted file mode 100644
index 9d785c7..0000000
--- a/src/main/java/no/difi/oxalis/as4/inbound/OxalisAs4WsOutInterceptor.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package no.difi.oxalis.as4.inbound;
-
-import no.difi.oxalis.as4.util.PeppolConfiguration;
-import org.apache.cxf.binding.soap.SoapMessage;
-import org.apache.cxf.interceptor.Fault;
-import org.apache.cxf.ws.security.SecurityConstants;
-import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
-import org.apache.wss4j.common.crypto.Crypto;
-import org.apache.wss4j.dom.handler.WSHandlerConstants;
-
-import java.util.*;
-
-public class OxalisAs4WsOutInterceptor extends WSS4JOutInterceptor {
- private Crypto crypto;
- private String alias;
-
- OxalisAs4WsOutInterceptor(Map props, Crypto crypto, String alias) {
- super(props);
- this.crypto = crypto;
- this.alias = alias;
- }
-
- @Override
- public void handleMessage(SoapMessage msg) throws Fault {
- msg.put(SecurityConstants.ENCRYPT_CRYPTO, crypto);
- msg.put(SecurityConstants.SIGNATURE_CRYPTO, crypto);
- msg.put(SecurityConstants.SIGNATURE_USERNAME, alias);
- msg.put(SecurityConstants.USERNAME, alias);
-
-
- Optional.ofNullable(msg.get("oxalis.tag"))
- .filter(PeppolConfiguration.class::isInstance)
- .map(PeppolConfiguration.class::cast)
- .map(PeppolConfiguration::getActions)
- .ifPresent(actionlist -> {
- StringJoiner actions = new StringJoiner(" ");
- for(String action : actionlist ){
- actions.add(action);
- }
-
- msg.put(WSHandlerConstants.ACTION, actions.toString());
- });
-
- super.handleMessage(msg);
- }
-}
diff --git a/src/main/java/no/difi/oxalis/as4/inbound/PasswordCallbackHandler.java b/src/main/java/no/difi/oxalis/as4/inbound/PasswordCallbackHandler.java
deleted file mode 100644
index c920deb..0000000
--- a/src/main/java/no/difi/oxalis/as4/inbound/PasswordCallbackHandler.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package no.difi.oxalis.as4.inbound;
-
-import org.apache.wss4j.common.ext.WSPasswordCallback;
-
-import javax.security.auth.callback.Callback;
-import javax.security.auth.callback.CallbackHandler;
-import javax.security.auth.callback.PasswordCallback;
-import javax.security.auth.callback.UnsupportedCallbackException;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-
-public class PasswordCallbackHandler implements CallbackHandler {
-
- private String encryptPassword;
-
- PasswordCallbackHandler(String encryptPassword) {
- this.encryptPassword = encryptPassword;
- }
-
- @Override
- public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
-
- for (Callback callback : callbacks) {
-
- if (callback instanceof WSPasswordCallback) {
-
- WSPasswordCallback cb = (WSPasswordCallback) callback;
- cb.setPassword(encryptPassword);
-
- } else if (callback instanceof PasswordCallback) {
-
- PasswordCallback cb = (PasswordCallback) callback;
- if (encryptPassword != null) {
-
- cb.setPassword(encryptPassword.toCharArray());
- } else {
-
- cb.setPassword(new char[0]);
- }
-
- } else {
-
- throw new UnsupportedEncodingException("Unable to process callback of type " + callback.getClass().getSimpleName());
- }
- }
- }
-}
diff --git a/src/main/java/org/apache/cxf/attachment/As4AttachmentDataSource.java b/src/main/java/org/apache/cxf/attachment/As4AttachmentDataSource.java
new file mode 100644
index 0000000..c24e5cb
--- /dev/null
+++ b/src/main/java/org/apache/cxf/attachment/As4AttachmentDataSource.java
@@ -0,0 +1,35 @@
+package org.apache.cxf.attachment;
+
+import lombok.Getter;
+import lombok.SneakyThrows;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+@Getter
+public class As4AttachmentDataSource extends AttachmentDataSource {
+
+ List inputStreams = new ArrayList<>();
+
+ public As4AttachmentDataSource(String ctParam, InputStream inParam) throws IOException {
+ super(ctParam, inParam);
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ InputStream inputStream = super.getInputStream();
+ inputStreams.add(inputStream);
+ return inputStream;
+ }
+
+ public void closeAll() {
+ inputStreams.forEach(this::close);
+ }
+
+ @SneakyThrows
+ private void close(InputStream inputStream) {
+ inputStream.close();
+ }
+}
diff --git a/src/main/java/org/apache/cxf/attachment/As4AttachmentDeserializer.java b/src/main/java/org/apache/cxf/attachment/As4AttachmentDeserializer.java
new file mode 100644
index 0000000..4edb735
--- /dev/null
+++ b/src/main/java/org/apache/cxf/attachment/As4AttachmentDeserializer.java
@@ -0,0 +1,418 @@
+package org.apache.cxf.attachment;
+
+import lombok.Getter;
+import org.apache.cxf.common.logging.LogUtils;
+import org.apache.cxf.common.util.StringUtils;
+import org.apache.cxf.helpers.HttpHeaderHelper;
+import org.apache.cxf.helpers.IOUtils;
+import org.apache.cxf.io.CachedOutputStream;
+import org.apache.cxf.message.Attachment;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.message.MessageUtils;
+
+import javax.activation.DataSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+import java.util.*;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.apache.cxf.attachment.AttachmentDeserializer.*;
+
+public class As4AttachmentDeserializer {
+
+ private static final Pattern CONTENT_TYPE_BOUNDARY_PATTERN = Pattern.compile("boundary=\"?([^\";]*)");
+
+ private static final Pattern INPUT_STREAM_BOUNDARY_PATTERN =
+ Pattern.compile("^--(\\S*)$", Pattern.MULTILINE);
+
+ private static final Logger LOG = LogUtils.getL7dLogger(As4AttachmentDeserializer.class);
+
+ private static final int PUSHBACK_AMOUNT = 2048;
+
+ private boolean lazyLoading = true;
+
+ private PushbackInputStream stream;
+ private int createCount;
+ private int closedCount;
+ private boolean closed;
+
+ private byte[] boundary;
+
+ private String contentType;
+
+ @Getter
+ private As4LazyAttachmentCollection attachments;
+
+ private Message message;
+
+ private InputStream body;
+
+ private Set loaded = new HashSet<>();
+ private List supportedTypes;
+
+ private int maxHeaderLength = DEFAULT_MAX_HEADER_SIZE;
+
+ @Getter
+ private List removed = new ArrayList<>();
+
+ public As4AttachmentDeserializer(Message message) {
+ this(message, Collections.singletonList("multipart/related"));
+ }
+
+ public As4AttachmentDeserializer(Message message, List supportedTypes) {
+ this.message = message;
+ this.supportedTypes = supportedTypes;
+
+ // Get the maximum Header length from configuration
+ maxHeaderLength = MessageUtils.getContextualInteger(message, ATTACHMENT_MAX_HEADER_SIZE,
+ DEFAULT_MAX_HEADER_SIZE);
+ }
+
+ public void initializeAttachments() throws IOException {
+ initializeRootMessage();
+
+ Object maxCountProperty = message.getContextualProperty(AttachmentDeserializer.ATTACHMENT_MAX_COUNT);
+ int maxCount = 50;
+ if (maxCountProperty != null) {
+ if (maxCountProperty instanceof Integer) {
+ maxCount = (Integer) maxCountProperty;
+ } else {
+ maxCount = Integer.parseInt((String) maxCountProperty);
+ }
+ }
+
+ attachments = new As4LazyAttachmentCollection(this, maxCount);
+ message.setAttachments(attachments);
+ }
+
+ protected void initializeRootMessage() throws IOException {
+ contentType = (String) message.get(Message.CONTENT_TYPE);
+
+ if (contentType == null) {
+ throw new IllegalStateException("Content-Type can not be empty!");
+ }
+
+ if (message.getContent(InputStream.class) == null) {
+ throw new IllegalStateException("An InputStream must be provided!");
+ }
+
+ if (AttachmentUtil.isTypeSupported(contentType.toLowerCase(), supportedTypes)) {
+ String boundaryString = findBoundaryFromContentType(contentType);
+ if (null == boundaryString) {
+ boundaryString = findBoundaryFromInputStream();
+ }
+ // If a boundary still wasn't found, throw an exception
+ if (null == boundaryString) {
+ throw new IOException("Couldn't determine the boundary from the message!");
+ }
+ boundary = boundaryString.getBytes("utf-8");
+
+ stream = new PushbackInputStream(message.getContent(InputStream.class), PUSHBACK_AMOUNT);
+ if (!readTillFirstBoundary(stream, boundary)) {
+ throw new IOException("Couldn't find MIME boundary: " + boundaryString);
+ }
+
+ Map> ih = loadPartHeaders(stream);
+ message.put(ATTACHMENT_PART_HEADERS, ih);
+ String val = As4AttachmentUtil.getHeader(ih, "Content-Type", "; ");
+ if (!StringUtils.isEmpty(val)) {
+ String cs = HttpHeaderHelper.findCharset(val);
+ if (!StringUtils.isEmpty(cs)) {
+ message.put(Message.ENCODING, HttpHeaderHelper.mapCharset(cs));
+ }
+ }
+ val = As4AttachmentUtil.getHeader(ih, "Content-Transfer-Encoding");
+
+ MimeBodyPartInputStream mmps = new MimeBodyPartInputStream(stream, boundary, PUSHBACK_AMOUNT);
+ InputStream ins = AttachmentUtil.decode(mmps, val);
+ if (ins != mmps) {
+ ih.remove("Content-Transfer-Encoding");
+ }
+ body = new As4DelegatingInputStream(ins, this);
+ createCount++;
+ message.setContent(InputStream.class, body);
+ }
+ }
+
+ private String findBoundaryFromContentType(String ct) throws IOException {
+ // Use regex to get the boundary and return null if it's not found
+ Matcher m = CONTENT_TYPE_BOUNDARY_PATTERN.matcher(ct);
+ return m.find() ? "--" + m.group(1) : null;
+ }
+
+ private String findBoundaryFromInputStream() throws IOException {
+ InputStream is = message.getContent(InputStream.class);
+ //boundary should definitely be in the first 2K;
+ PushbackInputStream in = new PushbackInputStream(is, 4096);
+ byte[] buf = new byte[2048];
+ int i = in.read(buf);
+ int len = i;
+ while (i > 0 && len < buf.length) {
+ i = in.read(buf, len, buf.length - len);
+ if (i > 0) {
+ len += i;
+ }
+ }
+ String msg = IOUtils.newStringFromBytes(buf, 0, len);
+ in.unread(buf, 0, len);
+
+ // Reset the input stream since we'll need it again later
+ message.setContent(InputStream.class, in);
+
+ // Use regex to get the boundary and return null if it's not found
+ Matcher m = INPUT_STREAM_BOUNDARY_PATTERN.matcher(msg);
+ return m.find() ? "--" + m.group(1) : null;
+ }
+
+ public As4AttachmentImpl readNext() throws IOException {
+ // Cache any mime parts that are currently being streamed
+ cacheStreamedAttachments();
+ if (closed) {
+ return null;
+ }
+
+ int v = stream.read();
+ if (v == -1) {
+ return null;
+ }
+ stream.unread(v);
+
+ Map> headers = loadPartHeaders(stream);
+ return (As4AttachmentImpl) createAttachment(headers);
+ }
+
+ private void cacheStreamedAttachments() throws IOException {
+ if (body instanceof As4DelegatingInputStream
+ && !((As4DelegatingInputStream) body).isClosed()) {
+
+ cache((As4DelegatingInputStream) body);
+ }
+
+ List atts = new ArrayList<>(attachments.getLoadedAttachments());
+ for (Attachment a : atts) {
+ DataSource s = a.getDataHandler().getDataSource();
+ if (s instanceof As4AttachmentDataSource) {
+ As4AttachmentDataSource ads = (As4AttachmentDataSource) s;
+ if (!ads.isCached()) {
+ ads.cache(message);
+ }
+ } else if (s.getInputStream() instanceof As4DelegatingInputStream) {
+ cache((As4DelegatingInputStream) s.getInputStream());
+ } else {
+ //assume a normal stream that is already cached
+ }
+ }
+ }
+
+ private void cache(As4DelegatingInputStream input) throws IOException {
+ if (loaded.contains(input)) {
+ return;
+ }
+ loaded.add(input);
+ InputStream origIn = input.getInputStream();
+ try (CachedOutputStream out = new CachedOutputStream()) {
+ AttachmentUtil.setStreamedAttachmentProperties(message, out);
+ IOUtils.copy(input, out);
+ input.setInputStream(out.getInputStream());
+ origIn.close();
+ }
+ }
+
+ /**
+ * Move the read pointer to the begining of the first part read till the end
+ * of first boundary
+ *
+ * @param pushbackInStream
+ * @param boundary
+ * @throws IOException
+ */
+ private static boolean readTillFirstBoundary(PushbackInputStream pushbackInStream,
+ byte[] boundary) throws IOException {
+
+ // work around a bug in PushBackInputStream where the buffer isn't
+ // initialized
+ // and available always returns 0.
+ int value = pushbackInStream.read();
+ pushbackInStream.unread(value);
+ while (value != -1) {
+ value = pushbackInStream.read();
+ if ((byte) value == boundary[0]) {
+ int boundaryIndex = 0;
+ while (value != -1 && (boundaryIndex < boundary.length) && ((byte) value == boundary[boundaryIndex])) {
+
+ value = pushbackInStream.read();
+ if (value == -1) {
+ throw new IOException("Unexpected End while searching for first Mime Boundary");
+ }
+ boundaryIndex++;
+ }
+ if (boundaryIndex == boundary.length) {
+ // boundary found, read the newline
+ if (value == 13) {
+ pushbackInStream.read();
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Create an Attachment from the MIME stream. If there is a previous attachment
+ * that is not read, cache that attachment.
+ *
+ * @throws IOException
+ */
+ private Attachment createAttachment(Map> headers) throws IOException {
+ InputStream partStream =
+ new As4DelegatingInputStream(new MimeBodyPartInputStream(stream, boundary, PUSHBACK_AMOUNT),
+ this);
+ createCount++;
+
+ return As4AttachmentUtil.createAttachment(partStream, headers);
+ }
+
+ public boolean isLazyLoading() {
+ return lazyLoading;
+ }
+
+ public void setLazyLoading(boolean lazyLoading) {
+ this.lazyLoading = lazyLoading;
+ }
+
+ public void markClosed(As4DelegatingInputStream delegatingInputStream) throws IOException {
+ closedCount++;
+ if (closedCount == createCount && !attachments.hasNext(false)) {
+ int x = stream.read();
+ while (x != -1) {
+ x = stream.read();
+ }
+ stream.close();
+ closed = true;
+ }
+ }
+
+ /**
+ * Check for more attachment.
+ *
+ * @return whether there is more attachment or not. It will not deserialize the next attachment.
+ * @throws IOException
+ */
+ public boolean hasNext() throws IOException {
+ cacheStreamedAttachments();
+ if (closed) {
+ return false;
+ }
+
+ int v = stream.read();
+ if (v == -1) {
+ return false;
+ }
+ stream.unread(v);
+ return true;
+ }
+
+
+ private Map> loadPartHeaders(InputStream in) throws IOException {
+ StringBuilder buffer = new StringBuilder(128);
+ StringBuilder b = new StringBuilder(128);
+ Map> heads = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+ // loop until we hit the end or a null line
+ while (readLine(in, b)) {
+ // lines beginning with white space get special handling
+ char c = b.charAt(0);
+ if (c == ' ' || c == '\t') {
+ if (buffer.length() != 0) {
+ // preserve the line break and append the continuation
+ buffer.append("\r\n");
+ buffer.append(b);
+ }
+ } else {
+ // if we have a line pending in the buffer, flush it
+ if (buffer.length() > 0) {
+ addHeaderLine(heads, buffer);
+ buffer.setLength(0);
+ }
+ // add this to the accumulator
+ buffer.append(b);
+ }
+ }
+
+ // if we have a line pending in the buffer, flush it
+ if (buffer.length() > 0) {
+ addHeaderLine(heads, buffer);
+ }
+ return heads;
+ }
+
+ private boolean readLine(InputStream in, StringBuilder buffer) throws IOException {
+ if (buffer.length() != 0) {
+ buffer.setLength(0);
+ }
+ int c;
+
+ while ((c = in.read()) != -1) {
+ // a linefeed is a terminator, always.
+ if (c == '\n') {
+ break;
+ } else if (c == '\r') {
+ //just ignore the CR. The next character SHOULD be an NL. If not, we're
+ //just going to discard this
+ continue;
+ } else {
+ // just add to the buffer
+ buffer.append((char) c);
+ }
+
+ if (buffer.length() > maxHeaderLength) {
+ LOG.fine("The attachment header size has exceeded the configured parameter: " + maxHeaderLength);
+ throw new HeaderSizeExceededException();
+ }
+ }
+
+ // no characters found...this was either an eof or a null line.
+ return buffer.length() != 0;
+ }
+
+ private void addHeaderLine(Map> heads, StringBuilder line) {
+ // null lines are a nop
+ final int size = line.length();
+ if (size == 0) {
+ return;
+ }
+ int separator = line.indexOf(":");
+ final String name;
+ String value = "";
+ if (separator == -1) {
+ name = line.toString().trim();
+ } else {
+ name = line.substring(0, separator);
+ // step past the separator. Now we need to remove any leading white space characters.
+ separator++;
+
+ while (separator < size) {
+ char ch = line.charAt(separator);
+ if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n') {
+ break;
+ }
+ separator++;
+ }
+ value = line.substring(separator);
+ }
+ List v = heads.get(name);
+ if (v == null) {
+ v = new ArrayList<>(1);
+ heads.put(name, v);
+ }
+ v.add(value);
+ }
+
+ public void addRemoved(Attachment remove) {
+ this.removed.add(remove);
+ }
+}
diff --git a/src/main/java/org/apache/cxf/attachment/As4AttachmentImpl.java b/src/main/java/org/apache/cxf/attachment/As4AttachmentImpl.java
new file mode 100644
index 0000000..8391e71
--- /dev/null
+++ b/src/main/java/org/apache/cxf/attachment/As4AttachmentImpl.java
@@ -0,0 +1,9 @@
+package org.apache.cxf.attachment;
+
+import org.apache.cxf.attachment.AttachmentImpl;
+
+public class As4AttachmentImpl extends AttachmentImpl {
+ public As4AttachmentImpl(String idParam) {
+ super(idParam);
+ }
+}
diff --git a/src/main/java/org/apache/cxf/attachment/As4AttachmentInInterceptor.java b/src/main/java/org/apache/cxf/attachment/As4AttachmentInInterceptor.java
new file mode 100644
index 0000000..a8b092f
--- /dev/null
+++ b/src/main/java/org/apache/cxf/attachment/As4AttachmentInInterceptor.java
@@ -0,0 +1,55 @@
+package org.apache.cxf.attachment;
+
+import org.apache.cxf.common.logging.LogUtils;
+import org.apache.cxf.interceptor.AttachmentInInterceptor;
+import org.apache.cxf.interceptor.Fault;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.phase.AbstractPhaseInterceptor;
+import org.apache.cxf.phase.Phase;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+public class As4AttachmentInInterceptor extends AbstractPhaseInterceptor {
+
+ private static final Logger LOG = LogUtils.getL7dLogger(AttachmentInInterceptor.class);
+
+ private static final List TYPES = Collections.singletonList("multipart/related");
+
+ public As4AttachmentInInterceptor() {
+ super(Phase.RECEIVE);
+ }
+
+ public void handleMessage(Message message) {
+ if (isGET(message)) {
+ LOG.fine("As4AttachmentInInterceptor skipped in HTTP GET method");
+ return;
+ }
+ if (message.getContent(InputStream.class) == null) {
+ return;
+ }
+
+ String contentType = (String) message.get(Message.CONTENT_TYPE);
+ if (AttachmentUtil.isTypeSupported(contentType, getSupportedTypes())) {
+ As4AttachmentDeserializer ad = new As4AttachmentDeserializer(message, getSupportedTypes());
+ try {
+ ad.initializeAttachments();
+ } catch (IOException e) {
+ throw new Fault(e);
+ }
+
+ message.put(As4AttachmentDeserializer.class, ad);
+ }
+ }
+
+ public void handleFault(Message messageParam) {
+ }
+
+ protected List getSupportedTypes() {
+ return TYPES;
+ }
+}
+
diff --git a/src/main/java/org/apache/cxf/attachment/As4AttachmentUtil.java b/src/main/java/org/apache/cxf/attachment/As4AttachmentUtil.java
new file mode 100644
index 0000000..e063b2c
--- /dev/null
+++ b/src/main/java/org/apache/cxf/attachment/As4AttachmentUtil.java
@@ -0,0 +1,92 @@
+package org.apache.cxf.attachment;
+
+import org.apache.cxf.common.util.StringUtils;
+import org.apache.cxf.helpers.FileUtils;
+import org.apache.cxf.message.Attachment;
+
+import javax.activation.DataHandler;
+import javax.activation.DataSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+public final class As4AttachmentUtil {
+
+ private As4AttachmentUtil() {
+
+ }
+
+ static String getHeaderValue(List v) {
+ if (v != null && !v.isEmpty()) {
+ return v.get(0);
+ }
+ return null;
+ }
+
+ static String getHeaderValue(List v, String delim) {
+ if (v != null && !v.isEmpty()) {
+ return String.join(delim, v);
+ }
+ return null;
+ }
+
+ static String getHeader(Map> headers, String h) {
+ return getHeaderValue(headers.get(h));
+ }
+
+ static String getHeader(Map> headers, String h, String delim) {
+ return getHeaderValue(headers.get(h), delim);
+ }
+
+ public static Attachment createAttachment(InputStream stream, Map> headers)
+ throws IOException {
+
+ String id = AttachmentUtil.cleanContentId(getHeader(headers, "Content-ID"));
+
+ As4AttachmentImpl att = new As4AttachmentImpl(id);
+
+ final String ct = getHeader(headers, "Content-Type");
+ String cd = getHeader(headers, "Content-Disposition");
+ String fileName = getContentDispositionFileName(cd);
+
+ String encoding = null;
+
+ for (Map.Entry> e : headers.entrySet()) {
+ String name = e.getKey();
+ if ("Content-Transfer-Encoding".equalsIgnoreCase(name)) {
+ encoding = getHeader(headers, name);
+ if ("binary".equalsIgnoreCase(encoding)) {
+ att.setXOP(true);
+ }
+ }
+ att.setHeader(name, getHeaderValue(e.getValue()));
+ }
+ if (encoding == null) {
+ encoding = "binary";
+ }
+ InputStream ins = AttachmentUtil.decode(stream, encoding);
+ if (ins != stream) {
+ headers.remove("Content-Transfer-Encoding");
+ }
+ DataSource source = new As4AttachmentDataSource(ct, ins);
+ if (!StringUtils.isEmpty(fileName)) {
+ ((As4AttachmentDataSource) source).setName(FileUtils.stripPath(fileName));
+ }
+ att.setDataHandler(new DataHandler(source));
+ return att;
+ }
+
+ static String getContentDispositionFileName(String cd) {
+ if (StringUtils.isEmpty(cd)) {
+ return null;
+ }
+ ContentDisposition c = new ContentDisposition(cd);
+ String s = c.getParameter("filename");
+ if (s == null) {
+ s = c.getParameter("name");
+ }
+ return s;
+ }
+}
+
diff --git a/src/main/java/org/apache/cxf/attachment/As4DelegatingInputStream.java b/src/main/java/org/apache/cxf/attachment/As4DelegatingInputStream.java
new file mode 100644
index 0000000..b03f5d8
--- /dev/null
+++ b/src/main/java/org/apache/cxf/attachment/As4DelegatingInputStream.java
@@ -0,0 +1,92 @@
+package org.apache.cxf.attachment;
+
+import org.apache.cxf.helpers.IOUtils;
+import org.apache.cxf.io.Transferable;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class As4DelegatingInputStream extends InputStream implements Transferable {
+ private InputStream is;
+ private As4AttachmentDeserializer deserializer;
+ private boolean isClosed;
+
+ As4DelegatingInputStream(InputStream is, As4AttachmentDeserializer ads) {
+ this.is = is;
+ deserializer = ads;
+ }
+
+ @Override
+ public void close() throws IOException {
+ IOUtils.consume(is);
+ is.close();
+ if (!isClosed && deserializer != null) {
+ deserializer.markClosed(this);
+ }
+ isClosed = true;
+ }
+
+ public void transferTo(File destinationFile) throws IOException {
+ if (isClosed) {
+ throw new IOException("Stream is closed");
+ }
+ IOUtils.transferTo(is, destinationFile);
+ }
+
+ public boolean isClosed() {
+ return isClosed;
+ }
+
+ public void setClosed(boolean closed) {
+ this.isClosed = closed;
+ }
+
+ public int read() throws IOException {
+ return this.is.read();
+ }
+
+ @Override
+ public int available() throws IOException {
+ return this.is.available();
+ }
+
+ @Override
+ public synchronized void mark(int arg0) {
+ this.is.mark(arg0);
+ }
+
+ @Override
+ public boolean markSupported() {
+ return this.is.markSupported();
+ }
+
+ @Override
+ public int read(byte[] bytes, int arg1, int arg2) throws IOException {
+ return this.is.read(bytes, arg1, arg2);
+ }
+
+ @Override
+ public int read(byte[] bytes) throws IOException {
+ return this.is.read(bytes);
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ this.is.reset();
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ return this.is.skip(n);
+ }
+
+ public void setInputStream(InputStream inputStream) {
+ this.is = inputStream;
+ }
+
+
+ public InputStream getInputStream() {
+ return is;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/cxf/attachment/As4LazyAttachmentCollection.java b/src/main/java/org/apache/cxf/attachment/As4LazyAttachmentCollection.java
new file mode 100644
index 0000000..e79bfd2
--- /dev/null
+++ b/src/main/java/org/apache/cxf/attachment/As4LazyAttachmentCollection.java
@@ -0,0 +1,343 @@
+package org.apache.cxf.attachment;
+
+import org.apache.cxf.message.Attachment;
+
+import javax.activation.DataHandler;
+import java.io.IOException;
+import java.util.*;
+
+public class As4LazyAttachmentCollection implements Collection {
+
+ private As4AttachmentDeserializer deserializer;
+ private final List attachments = new ArrayList<>();
+ private final int maxAttachmentCount;
+
+ public As4LazyAttachmentCollection(As4AttachmentDeserializer deserializer, int maxAttachmentCount) {
+ super();
+ this.deserializer = deserializer;
+ this.maxAttachmentCount = maxAttachmentCount;
+ }
+
+ public List getLoadedAttachments() {
+ return attachments;
+ }
+
+ private void loadAll() {
+ try {
+ Attachment a = deserializer.readNext();
+ int count = 0;
+ while (a != null) {
+ attachments.add(a);
+ count++;
+ if (count > maxAttachmentCount) {
+ throw new IOException("The message contains more attachments than are permitted");
+ }
+ a = deserializer.readNext();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Check for more attachments by attempting to deserialize the next attachment.
+ *
+ * @param shouldLoadNew if false, the "loaded attachments" List will not be changed.
+ * @return there is more attachment or not
+ * @throws IOException
+ */
+ public boolean hasNext(boolean shouldLoadNew) throws IOException {
+ if (shouldLoadNew) {
+ Attachment a = deserializer.readNext();
+ if (a != null) {
+ attachments.add(a);
+ return true;
+ }
+ return false;
+ }
+ return deserializer.hasNext();
+ }
+
+ public boolean hasNext() throws IOException {
+ return hasNext(true);
+ }
+
+ public Iterator iterator() {
+ return new Iterator() {
+ int current;
+ boolean removed;
+
+ public boolean hasNext() {
+ if (attachments.size() > current) {
+ return true;
+ }
+
+ // check if there is another attachment
+ try {
+ Attachment a = deserializer.readNext();
+ if (a == null) {
+ return false;
+ }
+ attachments.add(a);
+ return true;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Attachment next() {
+ Attachment a = attachments.get(current);
+ current++;
+ removed = false;
+ return a;
+ }
+
+ public void remove() {
+ if (removed) {
+ throw new IllegalStateException();
+ }
+ deserializer.addRemoved(attachments.remove(--current));
+ removed = true;
+ }
+
+ };
+ }
+
+ public int size() {
+ loadAll();
+
+ return attachments.size();
+ }
+
+ public boolean add(Attachment arg0) {
+ return attachments.add(arg0);
+ }
+
+ public boolean addAll(Collection extends Attachment> arg0) {
+ return attachments.addAll(arg0);
+ }
+
+ public void clear() {
+ attachments.clear();
+ }
+
+ public boolean contains(Object arg0) {
+ return attachments.contains(arg0);
+ }
+
+ public boolean containsAll(Collection> arg0) {
+ return attachments.containsAll(arg0);
+ }
+
+ public boolean isEmpty() {
+ if (attachments.isEmpty()) {
+ return !iterator().hasNext();
+ }
+ return attachments.isEmpty();
+ }
+
+ public boolean remove(Object arg0) {
+ return attachments.remove(arg0);
+ }
+
+ public boolean removeAll(Collection> arg0) {
+ return attachments.removeAll(arg0);
+ }
+
+ public boolean retainAll(Collection> arg0) {
+ return attachments.retainAll(arg0);
+ }
+
+ public Object[] toArray() {
+ loadAll();
+
+ return attachments.toArray();
+ }
+
+ public T[] toArray(T[] arg0) {
+ loadAll();
+
+ return attachments.toArray(arg0);
+ }
+
+ public Map createDataHandlerMap() {
+ return new As4LazyAttachmentCollection.LazyAttachmentMap(this);
+ }
+
+ private static class LazyAttachmentMap implements Map {
+ As4LazyAttachmentCollection collection;
+
+ LazyAttachmentMap(As4LazyAttachmentCollection c) {
+ collection = c;
+ }
+
+ public void clear() {
+ collection.clear();
+ }
+
+ public boolean containsKey(Object key) {
+ Iterator it = collection.iterator();
+ while (it.hasNext()) {
+ Attachment at = it.next();
+ if (key.equals(at.getId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean containsValue(Object value) {
+ Iterator it = collection.iterator();
+ while (it.hasNext()) {
+ Attachment at = it.next();
+ if (value.equals(at.getDataHandler())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public DataHandler get(Object key) {
+ Iterator it = collection.iterator();
+ while (it.hasNext()) {
+ Attachment at = it.next();
+ if (key.equals(at.getId())) {
+ return at.getDataHandler();
+ }
+ }
+ return null;
+ }
+
+ public boolean isEmpty() {
+ return collection.isEmpty();
+ }
+
+ public int size() {
+ return collection.size();
+ }
+
+ public DataHandler remove(Object key) {
+ Iterator it = collection.iterator();
+ while (it.hasNext()) {
+ Attachment at = it.next();
+ if (key.equals(at.getId())) {
+ collection.remove(at);
+ return at.getDataHandler();
+ }
+ }
+ return null;
+ }
+
+ public DataHandler put(String key, DataHandler value) {
+ Attachment at = new AttachmentImpl(key, value);
+ collection.add(at);
+ return value;
+ }
+
+ public void putAll(Map extends String, ? extends DataHandler> t) {
+ for (Map.Entry extends String, ? extends DataHandler> ent : t.entrySet()) {
+ put(ent.getKey(), ent.getValue());
+ }
+ }
+
+
+ public Set> entrySet() {
+ return new AbstractSet>() {
+ public Iterator> iterator() {
+ return new Iterator>() {
+ Iterator it = collection.iterator();
+
+ public boolean hasNext() {
+ return it.hasNext();
+ }
+
+ public Map.Entry next() {
+ return new Map.Entry() {
+ Attachment at = it.next();
+
+ public String getKey() {
+ return at.getId();
+ }
+
+ public DataHandler getValue() {
+ return at.getDataHandler();
+ }
+
+ public DataHandler setValue(DataHandler value) {
+ if (at instanceof AttachmentImpl) {
+ DataHandler h = at.getDataHandler();
+ ((AttachmentImpl) at).setDataHandler(value);
+ return h;
+ }
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ public void remove() {
+ it.remove();
+ }
+ };
+ }
+
+ public int size() {
+ return collection.size();
+ }
+ };
+ }
+
+ public Set keySet() {
+ return new AbstractSet() {
+ public Iterator iterator() {
+ return new Iterator() {
+ Iterator it = collection.iterator();
+
+ public boolean hasNext() {
+ return it.hasNext();
+ }
+
+ public String next() {
+ return it.next().getId();
+ }
+
+ public void remove() {
+ it.remove();
+ }
+ };
+ }
+
+ public int size() {
+ return collection.size();
+ }
+ };
+ }
+
+
+ public Collection values() {
+ return new AbstractCollection() {
+ public Iterator iterator() {
+ return new Iterator() {
+ Iterator it = collection.iterator();
+
+ public boolean hasNext() {
+ return it.hasNext();
+ }
+
+ public DataHandler next() {
+ return it.next().getDataHandler();
+ }
+
+ public void remove() {
+ it.remove();
+ }
+ };
+ }
+
+ public int size() {
+ return collection.size();
+ }
+ };
+ }
+
+ }
+}