package org.jboss.resteasy.plugins.providers.multipart; import java.util.HashSet; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.MimeIOException; import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.QuotedPrintableInputStream; import org.apache.james.mime4j.descriptor.BodyDescriptor; import org.apache.james.mime4j.field.ContentTypeField; import org.apache.james.mime4j.message.BinaryBody; import org.apache.james.mime4j.message.Body; import org.apache.james.mime4j.message.BodyFactory; import org.apache.james.mime4j.message.BodyPart; import org.apache.james.mime4j.message.Entity; import org.apache.james.mime4j.message.Message; import org.apache.james.mime4j.message.MessageBuilder; import org.apache.james.mime4j.message.Multipart; import org.apache.james.mime4j.message.TextBody; import org.apache.james.mime4j.parser.Field; import org.apache.james.mime4j.parser.MimeStreamParser; import org.apache.james.mime4j.storage.AbstractStorageProvider; import org.apache.james.mime4j.storage.DefaultStorageProvider; import org.apache.james.mime4j.storage.Storage; import org.apache.james.mime4j.storage.StorageOutputStream; import org.apache.james.mime4j.storage.StorageProvider; import org.apache.james.mime4j.storage.ThresholdStorageProvider; import org.apache.james.mime4j.util.MimeUtil; import org.jboss.resteasy.core.ProvidersContextRetainer; import org.jboss.resteasy.plugins.providers.multipart.i18n.Messages; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.util.CaseInsensitiveMap; import org.jboss.resteasy.resteasy_jaxrs.i18n.*; import javax.ws.rs.core.GenericType; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.Providers; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.SequenceInputStream; import java.io.UnsupportedEncodingException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; /** * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @version $Revision: 1 $ */ public class MultipartInputImpl implements MultipartInput, ProvidersContextRetainer { protected MediaType contentType; protected Providers workers; protected Message mimeMessage; protected List<InputPart> parts = new ArrayList<InputPart>(); protected static final Annotation[] empty = {}; protected MediaType defaultPartContentType = MultipartConstants.TEXT_PLAIN_WITH_CHARSET_US_ASCII_TYPE; protected String defaultPartCharset = null; protected Providers savedProviders; // We hack MIME4j so that it always returns a BinaryBody so we don't have to deal with Readers and their charset conversions private static class BinaryOnlyMessageBuilder extends MessageBuilder { private Method expectMethod; private java.lang.reflect.Field bodyFactoryField; private java.lang.reflect.Field stackField; private void init() { try { expectMethod = MessageBuilder.class.getDeclaredMethod("expect", Class.class); expectMethod.setAccessible(true); bodyFactoryField = MessageBuilder.class.getDeclaredField("bodyFactory"); bodyFactoryField.setAccessible(true); stackField = MessageBuilder.class.getDeclaredField("stack"); stackField.setAccessible(true); } catch (Exception e) { throw new RuntimeException(e); } } private BinaryOnlyMessageBuilder(Entity entity) { super(entity); init(); } private BinaryOnlyMessageBuilder(Entity entity, StorageProvider storageProvider) { super(entity, storageProvider); init(); } @SuppressWarnings(value = "unchecked") @Override public void body(BodyDescriptor bd, InputStream is) throws MimeException, IOException { // the only thing different from the superclass is that we just return a BinaryBody no matter what try { expectMethod.invoke(this, Entity.class); } catch (Exception e) { throw new RuntimeException(e); } final String enc = bd.getTransferEncoding(); final Body body; final InputStream decodedStream; if (MimeUtil.ENC_BASE64.equals(enc)) { decodedStream = new Base64InputStream(is); } else if (MimeUtil.ENC_QUOTED_PRINTABLE.equals(enc)) { decodedStream = new QuotedPrintableInputStream(is); } else { decodedStream = is; } BodyFactory factory; try { factory = (BodyFactory)bodyFactoryField.get(this); } catch (Exception e) { throw new RuntimeException(e); } body = factory.binaryBody(decodedStream); Stack<Object> st; try { st = (Stack<Object>)stackField.get(this); } catch (Exception e) { throw new RuntimeException(e); } Entity entity = ((Entity) st.peek()); entity.setBody(body); } } private static class BinaryMessage extends Message { private BinaryMessage(InputStream is) throws IOException, MimeIOException { try { MimeStreamParser parser = new MimeStreamParser(null); StorageProvider storageProvider; if (System.getProperty(DefaultStorageProvider.DEFAULT_STORAGE_PROVIDER_PROPERTY) != null) { storageProvider = DefaultStorageProvider.getInstance(); } else { StorageProvider backend = new CustomTempFileStorageProvider(); storageProvider = new ThresholdStorageProvider(backend, 1024); } parser.setContentHandler(new BinaryOnlyMessageBuilder(this, storageProvider)); parser.parse(is); } catch (MimeException e) { throw new MimeIOException(e); } } } public MultipartInputImpl(MediaType contentType, Providers workers) { this.contentType = contentType; this.workers = workers; HttpRequest httpRequest = ResteasyProviderFactory .getContextData(HttpRequest.class); if (httpRequest != null) { String defaultContentType = (String) httpRequest .getAttribute(InputPart.DEFAULT_CONTENT_TYPE_PROPERTY); if (defaultContentType != null) this.defaultPartContentType = MediaType .valueOf(defaultContentType); this.defaultPartCharset = (String) httpRequest.getAttribute(InputPart.DEFAULT_CHARSET_PROPERTY); if (defaultPartCharset != null) { this.defaultPartContentType = getMediaTypeWithDefaultCharset(this.defaultPartContentType); } } } public MultipartInputImpl(MediaType contentType, Providers workers, MediaType defaultPartContentType, String defaultPartCharset) { this.contentType = contentType; this.workers = workers; if (defaultPartContentType != null) this.defaultPartContentType = defaultPartContentType; this.defaultPartCharset = defaultPartCharset; if (defaultPartCharset != null) { this.defaultPartContentType = getMediaTypeWithDefaultCharset(this.defaultPartContentType); } } public MultipartInputImpl(Multipart multipart, Providers workers) throws IOException { for (BodyPart bodyPart : multipart.getBodyParts()) parts.add(extractPart(bodyPart)); this.workers = workers; } public void parse(InputStream is) throws IOException { mimeMessage = new BinaryMessage(addHeaderToHeadlessStream(is)); extractParts(); } protected InputStream addHeaderToHeadlessStream(InputStream is) throws UnsupportedEncodingException { return new SequenceInputStream(createHeaderInputStream(), is); } protected InputStream createHeaderInputStream() throws UnsupportedEncodingException { String header = HttpHeaders.CONTENT_TYPE + ": " + contentType + "\r\n\r\n"; return new ByteArrayInputStream(header.getBytes(StandardCharsets.UTF_8)); } public String getPreamble() { return ((Multipart) mimeMessage.getBody()).getPreamble(); } public List<InputPart> getParts() { return parts; } protected void extractParts() throws IOException { Multipart multipart = (Multipart) mimeMessage.getBody(); for (BodyPart bodyPart : multipart.getBodyParts()) parts.add(extractPart(bodyPart)); } protected InputPart extractPart(BodyPart bodyPart) throws IOException { return new PartImpl(bodyPart); } public class PartImpl implements InputPart { private BodyPart bodyPart; private MediaType contentType; private MultivaluedMap<String, String> headers = new CaseInsensitiveMap<String>(); private boolean contentTypeFromMessage; public PartImpl(BodyPart bodyPart) { this.bodyPart = bodyPart; for (Field field : bodyPart.getHeader()) { headers.add(field.getName(), field.getBody()); if (field instanceof ContentTypeField) { contentType = MediaType.valueOf(field.getBody()); contentTypeFromMessage = true; } } if (contentType == null) contentType = defaultPartContentType; if (getCharset(contentType) == null) { if (defaultPartCharset != null) { contentType = getMediaTypeWithDefaultCharset(contentType); } else if (contentType.getType().equalsIgnoreCase("text")) { contentType = getMediaTypeWithCharset(contentType, "us-ascii"); } } } @Override public void setMediaType(MediaType mediaType) { contentType = mediaType; contentTypeFromMessage = false; headers.putSingle("Content-Type", mediaType.toString()); } @SuppressWarnings("unchecked") public <T> T getBody(Class<T> type, Type genericType) throws IOException { if (MultipartInput.class.equals(type)) { if (bodyPart.getBody() instanceof Multipart) { return (T) new MultipartInputImpl(Multipart.class.cast(bodyPart.getBody()), workers); } } try { if (savedProviders != null) { ResteasyProviderFactory.pushContext(Providers.class, savedProviders); } MessageBodyReader<T> reader = workers.getMessageBodyReader(type, genericType, empty, contentType); if (reader == null) { throw new RuntimeException(Messages.MESSAGES.unableToFindMessageBodyReader(contentType, type.getName())); } LogMessages.LOGGER.debugf("MessageBodyReader: %s", reader.getClass().getName()); return reader.readFrom(type, genericType, empty, contentType, headers, getBody()); } finally { if (savedProviders != null) { ResteasyProviderFactory.popContextData(Providers.class); } } } @SuppressWarnings("unchecked") public <T> T getBody(GenericType<T> type) throws IOException { return getBody((Class<T>) type.getRawType(), type.getType()); } public InputStream getBody() throws IOException { Body body = bodyPart.getBody(); InputStream result = null; if (body instanceof TextBody) { throw new UnsupportedOperationException(); /* InputStreamReader reader = (InputStreamReader)((TextBody) body).getReader(); StringBuilder inputBuilder = new StringBuilder(); char[] buffer = new char[1024]; while (true) { int readCount = reader.read(buffer); if (readCount < 0) { break; } inputBuilder.append(buffer, 0, readCount); } String str = inputBuilder.toString(); return new ByteArrayInputStream(str.getBytes(reader.getEncoding())); */ } else if (body instanceof BinaryBody) { return ((BinaryBody)body).getInputStream(); } return result; } public String getBodyAsString() throws IOException { return getBody(String.class, null); } public MultivaluedMap<String, String> getHeaders() { return headers; } public MediaType getMediaType() { return contentType; } public boolean isContentTypeFromMessage() { return contentTypeFromMessage; } } public static void main(String[] args) throws Exception { String input = "URLSTR: file:/Users/billburke/jboss/resteasy-jaxrs/resteasy-jaxrs/src/test/test-data/data.txt\r\n" + "--B98hgCmKsQ-B5AUFnm2FnDRCgHPDE3\r\n" + "Content-Disposition: form-data; name=\"part1\"\r\n" + "Content-Type: text/plain; charset=US-ASCII\r\n" + "Content-Transfer-Encoding: 8bit\r\n" + "\r\n" + "This is Value 1\r\n" + "--B98hgCmKsQ-B5AUFnm2FnDRCgHPDE3\r\n" + "Content-Disposition: form-data; name=\"part2\"\r\n" + "Content-Type: text/plain; charset=US-ASCII\r\n" + "Content-Transfer-Encoding: 8bit\r\n" + "\r\n" + "This is Value 2\r\n" + "--B98hgCmKsQ-B5AUFnm2FnDRCgHPDE3\r\n" + "Content-Disposition: form-data; name=\"data.txt\"; filename=\"data.txt\"\r\n" + "Content-Type: application/octet-stream; charset=ISO-8859-1\r\n" + "Content-Transfer-Encoding: binary\r\n" + "\r\n" + "hello world\r\n" + "--B98hgCmKsQ-B5AUFnm2FnDRCgHPDE3--"; ByteArrayInputStream bais = new ByteArrayInputStream(input.getBytes()); Map<String, String> parameters = new LinkedHashMap<String, String>(); parameters.put("boundary", "B98hgCmKsQ-B5AUFnm2FnDRCgHPDE3"); MediaType contentType = new MediaType("multipart", "form-data", parameters); MultipartInputImpl multipart = new MultipartInputImpl(contentType, null); multipart.parse(bais); System.out.println(multipart.getPreamble()); System.out.println("**********"); for (InputPart part : multipart.getParts()) { System.out.println("--"); System.out.println("\"" + part.getBodyAsString() + "\""); } System.out.println("done"); } @Override public void close() { if (mimeMessage != null) { try { mimeMessage.dispose(); } catch (Exception e) { } } } protected void finalize() throws Throwable { close(); } protected String getCharset(MediaType mediaType) { for (Iterator<String> it = mediaType.getParameters().keySet().iterator(); it.hasNext(); ) { String key = it.next(); if ("charset".equalsIgnoreCase(key)) { return mediaType.getParameters().get(key); } } return null; } private MediaType getMediaTypeWithDefaultCharset(MediaType mediaType) { String charset = defaultPartCharset; return getMediaTypeWithCharset(mediaType, charset); } private MediaType getMediaTypeWithCharset(MediaType mediaType, String charset) { Map<String, String> params = mediaType.getParameters(); Map<String, String> newParams = new LinkedHashMap<String, String>(); newParams.put("charset", charset); for (Iterator<String> it = params.keySet().iterator(); it.hasNext(); ) { String key = it.next(); if (!"charset".equalsIgnoreCase(key)) { newParams.put(key, params.get(key)); } } return new MediaType(mediaType.getType(), mediaType.getSubtype(), newParams); } @Override public void setProviders(Providers providers) { savedProviders = providers; } /** * A custom TempFileStorageProvider that do no set deleteOnExit on temp files, * to avoid memory leaks (see https://issues.apache.org/jira/browse/MIME4J-251) * */ private static class CustomTempFileStorageProvider extends AbstractStorageProvider { private static final String DEFAULT_PREFIX = "m4j"; private final String prefix; private final String suffix; private final File directory; public CustomTempFileStorageProvider() { this(DEFAULT_PREFIX, null, null); } public CustomTempFileStorageProvider(String prefix, String suffix, File directory) { if (prefix == null || prefix.length() < 3) throw new IllegalArgumentException("invalid prefix"); if (directory != null && !directory.isDirectory() && !directory.mkdirs()) throw new IllegalArgumentException("invalid directory"); this.prefix = prefix; this.suffix = suffix; this.directory = directory; } public StorageOutputStream createStorageOutputStream() throws IOException { File file = File.createTempFile(prefix, suffix, directory); return new TempFileStorageOutputStream(file); } private static final class TempFileStorageOutputStream extends StorageOutputStream { private File file; private OutputStream out; public TempFileStorageOutputStream(File file) throws IOException { this.file = file; this.out = new FileOutputStream(file); } @Override public void close() throws IOException { super.close(); out.close(); } @Override protected void write0(byte[] buffer, int offset, int length) throws IOException { out.write(buffer, offset, length); } @Override protected Storage toStorage0() throws IOException { // out has already been closed because toStorage calls close return new TempFileStorage(file); } } private static final class TempFileStorage implements Storage { private File file; private static final Set<File> filesToDelete = new HashSet<File>(); public TempFileStorage(File file) { this.file = file; } public void delete() { // deleting a file might not immediately succeed if there are still // streams left open (especially under Windows). so we keep track of // the files that have to be deleted and try to delete all these // files each time this method gets invoked. // a better but more complicated solution would be to start a // separate thread that tries to delete the files periodically. synchronized (filesToDelete) { if (file != null) { filesToDelete.add(file); file = null; } for (Iterator<File> iterator = filesToDelete.iterator(); iterator.hasNext();) { File file = iterator.next(); if (file.delete()) { iterator.remove(); } } } } public InputStream getInputStream() throws IOException { if (file == null) throw new IllegalStateException("storage has been deleted"); return new BufferedInputStream(new FileInputStream(file)); } } } }