Commit a54eb64e authored by akwizgran's avatar akwizgran

Merge branch '1468-reject-unsupported-images' into 'master'

Reject unsupported images

Closes #1468

See merge request !1038
parents 249e1e28 ad2d3e70
Pipeline #3583 passed with stage
in 8 minutes and 25 seconds
package org.briarproject.briar.android.conversation;
package org.briarproject.briar.android.attachment;
import android.content.res.AssetManager;
import android.support.test.InstrumentationRegistry;
......@@ -21,7 +21,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class AttachmentControllerIntegrationTest {
public class AttachmentRetrieverIntegrationTest {
private static final String smallKitten =
"https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Kitten_in_Rizal_Park%2C_Manila.jpg/160px-Kitten_in_Rizal_Park%2C_Manila.jpg";
......@@ -47,15 +47,15 @@ public class AttachmentControllerIntegrationTest {
);
private final MessageId msgId = new MessageId(getRandomId());
private final AttachmentController controller =
new AttachmentController(null, dimensions);
private final AttachmentRetriever retriever =
new AttachmentRetriever(null, dimensions);
@Test
public void testSmallJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(smallKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(160, item.getWidth());
assertEquals(240, item.getHeight());
......@@ -71,7 +71,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(originalKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(1728, item.getWidth());
assertEquals(2592, item.getHeight());
......@@ -87,7 +87,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/png");
InputStream is = getUrlInputStream(pngKitten);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(737, item.getWidth());
assertEquals(510, item.getHeight());
......@@ -103,7 +103,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(uberGif);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
......@@ -118,7 +118,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(lottaPixel);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(64250, item.getWidth());
assertEquals(64250, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
......@@ -133,7 +133,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(imageIoCrash);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1184, item.getWidth());
assertEquals(448, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
......@@ -148,7 +148,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(gimpCrash);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
......@@ -163,7 +163,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(optiPngAfl);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(32, item.getWidth());
assertEquals(32, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
......@@ -178,7 +178,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(librawError);
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertTrue(item.hasError());
}
......@@ -187,7 +187,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated.gif");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(65535, item.getWidth());
assertEquals(65535, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
......@@ -202,7 +202,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated2.gif");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(10000, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
......@@ -217,7 +217,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("error_large.gif");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(16384, item.getWidth());
assertEquals(16384, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
......@@ -232,7 +232,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_high.jpg");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.minWidth, item.getThumbnailWidth());
......@@ -247,7 +247,7 @@ public class AttachmentControllerIntegrationTest {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_wide.jpg");
Attachment a = new Attachment(is);
AttachmentItem item = controller.getAttachmentItem(h, a, true);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1920, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
......
package org.briarproject.briar.android.attachment;
import android.content.ContentResolver;
import android.net.Uri;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES;
@NotNullByDefault
class AttachmentCreationTask {
private static Logger LOG =
getLogger(AttachmentCreationTask.class.getName());
private final MessagingManager messagingManager;
private final ContentResolver contentResolver;
private final GroupId groupId;
private final Collection<Uri> uris;
private final boolean needsSize;
@Nullable
private volatile AttachmentCreator attachmentCreator;
private volatile boolean canceled = false;
AttachmentCreationTask(MessagingManager messagingManager,
ContentResolver contentResolver,
AttachmentCreator attachmentCreator, GroupId groupId,
Collection<Uri> uris, boolean needsSize) {
this.messagingManager = messagingManager;
this.contentResolver = contentResolver;
this.groupId = groupId;
this.uris = uris;
this.needsSize = needsSize;
this.attachmentCreator = attachmentCreator;
}
public void cancel() {
canceled = true;
attachmentCreator = null;
}
@IoExecutor
public void storeAttachments() {
for (Uri uri: uris) processUri(uri);
AttachmentCreator attachmentCreator = this.attachmentCreator;
if (!canceled && attachmentCreator != null)
attachmentCreator.onAttachmentCreationFinished();
this.attachmentCreator = null;
}
@IoExecutor
private void processUri(Uri uri) {
if (canceled) return;
try {
AttachmentHeader h = storeAttachment(uri);
AttachmentCreator attachmentCreator = this.attachmentCreator;
if (attachmentCreator != null) {
attachmentCreator.onAttachmentHeaderReceived(uri, h, needsSize);
}
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
AttachmentCreator attachmentCreator = this.attachmentCreator;
if (attachmentCreator != null) {
attachmentCreator.onAttachmentError(uri, e);
}
canceled = true;
}
}
@IoExecutor
private AttachmentHeader storeAttachment(Uri uri)
throws IOException, DbException {
long start = now();
String contentType = contentResolver.getType(uri);
if (contentType == null) throw new IOException("null content type");
if (!isValidMimeType(contentType)) {
String uriString = uri.toString();
throw new UnsupportedMimeTypeException("", contentType, uriString);
}
InputStream is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
long timestamp = System.currentTimeMillis();
AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is);
tryToClose(is, LOG, WARNING);
logDuration(LOG, "Storing attachment", start);
return h;
}
private boolean isValidMimeType(String mimeType) {
for (String supportedType : IMAGE_MIME_TYPES) {
if (supportedType.equals(mimeType)) return true;
}
return false;
}
}
package org.briarproject.briar.android.attachment;
import android.app.Application;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.FileTooBigException;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
public class AttachmentCreator {
private static Logger LOG = getLogger(AttachmentCreator.class.getName());
private final Application app;
@IoExecutor
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final AttachmentRetriever retriever;
private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
new CopyOnWriteArrayList<>();
private final MutableLiveData<AttachmentResult> result =
new MutableLiveData<>();
@Nullable
private AttachmentCreationTask task;
public AttachmentCreator(Application app, @IoExecutor Executor ioExecutor,
MessagingManager messagingManager, AttachmentRetriever retriever) {
this.app = app;
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.retriever = retriever;
}
@UiThread
public LiveData<AttachmentResult> storeAttachments(
LiveData<GroupId> groupId, Collection<Uri> newUris) {
if (task != null || !uris.isEmpty())
throw new IllegalStateException();
uris.addAll(newUris);
observeForeverOnce(groupId, id -> {
if (id == null) throw new IllegalStateException();
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(messagingManager,
app.getContentResolver(), this, id, uris, needsSize);
ioExecutor.execute(() -> task.storeAttachments());
});
return result;
}
/**
* This should be only called after configuration changes.
* In this case we should not create new attachments.
* They are already being created and returned by the existing LiveData.
*/
@UiThread
public LiveData<AttachmentResult> getLiveAttachments() {
if (task == null || uris.isEmpty())
throw new IllegalStateException();
// A task is already running. It will update the result LiveData.
// So nothing more to do here.
return result;
}
@IoExecutor
void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize) {
// get and cache AttachmentItem for ImagePreview
try {
Attachment a = retriever.getMessageAttachment(h);
AttachmentItem item = retriever.getAttachmentItem(h, a, needsSize);
if (item.hasError()) throw new IOException();
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, item);
itemResults.add(itemResult);
result.postValue(getResult(false));
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onAttachmentError(uri, e);
}
}
@IoExecutor
void onAttachmentError(Uri uri, Throwable t) {
// get error message
String errorMsg;
if (t instanceof UnsupportedMimeTypeException) {
String mimeType = ((UnsupportedMimeTypeException) t).getMimeType();
errorMsg = app.getString(
R.string.image_attach_error_invalid_mime_type, mimeType);
} else if (t instanceof FileTooBigException) {
int mb = MAX_IMAGE_SIZE / 1024 / 1024;
errorMsg = app.getString(R.string.image_attach_error_too_big, mb);
} else {
errorMsg = null; // generic error
}
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, errorMsg);
itemResults.add(itemResult);
result.postValue(getResult(false));
// expect to receive a cancel from the UI
}
@IoExecutor
void onAttachmentCreationFinished() {
result.postValue(getResult(true));
}
@UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() {
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
headers.add(itemResult.getItem().getHeader());
}
return headers;
}
/**
* Marks the attachments as sent and adds the items to the cache for display
*
* @param id The MessageId of the sent message.
*/
@UiThread
public void onAttachmentsSent(MessageId id) {
List<AttachmentItem> items = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
items.add(itemResult.getItem());
}
retriever.cachePut(id, items);
resetState();
}
/**
* Needs to be called when created attachments will not be sent anymore.
*/
@UiThread
public void cancel() {
if (task == null) throw new AssertionError();
task.cancel();
deleteUnsentAttachments();
resetState();
}
@UiThread
private void resetState() {
task = null;
uris.clear();
itemResults.clear();
result.setValue(null);
}
@UiThread
public void deleteUnsentAttachments() {
// Make a copy for the IoExecutor as we clear the itemResults soon
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() != null)
headers.add(itemResult.getItem().getHeader());
}
ioExecutor.execute(() -> {
for (AttachmentHeader header : headers) {
try {
messagingManager.removeAttachment(header);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
}
private AttachmentResult getResult(boolean finished) {
// Make a copy of the list,
// because our copy will continue to change in the background.
// (As it's a CopyOnWriteArrayList,
// the code that receives the result can safely do simple things
// like iterating over the list,
// but anything that involves calling more than one list method
// is still unsafe.)
Collection<AttachmentItemResult> items = new ArrayList<>(itemResults);
return new AttachmentResult(items, finished);
}
}
package org.briarproject.briar.android.conversation;
package org.briarproject.briar.android.attachment;
import android.content.res.Resources;
import android.support.annotation.VisibleForTesting;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
class AttachmentDimensions {
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class AttachmentDimensions {
final int defaultSize;
final int minWidth, maxWidth;
......@@ -21,7 +26,7 @@ class AttachmentDimensions {
this.maxHeight = maxHeight;
}
static AttachmentDimensions getAttachmentDimensions(Resources res) {
public static AttachmentDimensions getAttachmentDimensions(Resources res) {
int defaultSize =
res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
int minWidth = res.getDimensionPixelSize(
......@@ -33,7 +38,7 @@ class AttachmentDimensions {
int maxHeight = res.getDimensionPixelSize(
R.dimen.message_bubble_image_max_height);
return new AttachmentDimensions(defaultSize, minWidth, maxWidth,
minHeight, minHeight);
minHeight, maxHeight);
}
}
package org.briarproject.briar.android.conversation;
package org.briarproject.briar.android.attachment;
import android.os.Parcel;
import android.os.Parcelable;
......@@ -6,18 +6,21 @@ import android.support.annotation.Nullable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.concurrent.Immutable;
import static java.util.Objects.requireNonNull;
@Immutable
@NotNullByDefault
public class AttachmentItem implements Parcelable {
private final MessageId messageId;
private final AttachmentHeader header;
private final int width, height;
private final String mimeType, extension;
private final String extension;
private final int thumbnailWidth, thumbnailHeight;
private final boolean hasError;
private final long instanceId;
......@@ -37,13 +40,12 @@ public class AttachmentItem implements Parcelable {
private static final AtomicLong NEXT_INSTANCE_ID = new AtomicLong(0);
AttachmentItem(MessageId messageId, int width, int height, String mimeType,
AttachmentItem(AttachmentHeader header, int width, int height,
String extension, int thumbnailWidth, int thumbnailHeight,
boolean hasError) {
this.messageId = messageId;
this.header = header;
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.extension = extension;
this.thumbnailWidth = thumbnailWidth;
this.thumbnailHeight = thumbnailHeight;
......@@ -54,19 +56,24 @@ public class AttachmentItem implements Parcelable {
protected AttachmentItem(Parcel in) {
byte[] messageIdByte = new byte[MessageId.LENGTH];
in.readByteArray(messageIdByte);
messageId = new MessageId(messageIdByte);
MessageId messageId = new MessageId(messageIdByte);
width = in.readInt();
height = in.readInt();
mimeType = in.readString();
extension = in.readString();
String mimeType = requireNonNull(in.readString());
extension = requireNonNull(in.readString());
thumbnailWidth = in.readInt();
thumbnailHeight = in.readInt();
hasError = in.readByte() != 0;
instanceId = in.readLong();
header = new AttachmentHeader(messageId, mimeType);
}
AttachmentHeader getHeader() {
return header;
}
public MessageId getMessageId() {
return messageId;
return header.getMessageId();
}
int getWidth() {
......@@ -77,27 +84,27 @@ public class AttachmentItem implements Parcelable {
return height;
}
String getMimeType() {
return mimeType;
public String getMimeType() {
return header.getContentType();
}
String getExtension() {
public String getExtension() {
return extension;
}
int getThumbnailWidth() {
public int getThumbnailWidth() {
return thumbnailWidth;
}
int getThumbnailHeight() {
public int getThumbnailHeight() {
return thumbnailHeight;
}
boolean hasError() {
public boolean hasError() {
return hasError;
}
String getTransitionName() {
public String getTransitionName() {
return String.valueOf(instanceId);
}
......@@ -108,10 +115,10 @@ public class AttachmentItem implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByteArray(messageId.getBytes());
dest.writeByteArray(header.getMessageId().getBytes());
dest.writeInt(width);
dest.writeInt(height);
dest.writeString(mimeType);
dest.writeString(header.getContentType());
dest.writeString(extension);