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 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 t) { + for (Map.Entry 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(); + } + }; + } + + } +}