From 34dd66cc024b88a4db70e0c1ca133f855fed8b08 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 17 Aug 2022 13:50:07 -0500 Subject: [PATCH] Preliminary support for bits-of-binary https://xmpp.org/extensions/xep-0231.html When a CID URI is received as part of an OOB, use bob to fetch from the sender. This does not verify the hash at this time, nor does it "cache" for a future send since our storage is not content-addressable yet. --- .../com/cheogram/android/BobTransfer.java | 129 ++++++++++++++++++ .../siacs/conversations/entities/Message.java | 2 +- .../java/eu/siacs/conversations/http/URL.java | 2 +- .../conversations/parser/MessageParser.java | 15 +- .../ui/ConversationFragment.java | 19 ++- .../conversations/utils/MessageUtils.java | 3 +- 6 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 src/cheogram/java/com/cheogram/android/BobTransfer.java diff --git a/src/cheogram/java/com/cheogram/android/BobTransfer.java b/src/cheogram/java/com/cheogram/android/BobTransfer.java new file mode 100644 index 000000000..dc8aefc0c --- /dev/null +++ b/src/cheogram/java/com/cheogram/android/BobTransfer.java @@ -0,0 +1,129 @@ +package com.cheogram.android; + +import android.util.Base64; +import android.util.Log; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.http.AesGcmURL; +import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.MimeUtils; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class BobTransfer implements Transferable { + protected int status = Transferable.STATUS_OFFER; + protected Message message; + protected URI uri; + protected XmppConnectionService xmppConnectionService; + protected DownloadableFile file; + + public BobTransfer(Message message, XmppConnectionService xmppConnectionService) throws URISyntaxException { + this.message = message; + this.xmppConnectionService = xmppConnectionService; + this.uri = new URI(message.getFileParams().url); + setupFile(); + } + + private void setupFile() { + final String reference = uri.getFragment(); + if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) { + this.file = new DownloadableFile(xmppConnectionService.getCacheDir(), message.getUuid()); + this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference)); + Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")"); + } else { + this.file = xmppConnectionService.getFileBackend().getFile(message, false); + } + } + + @Override + public boolean start() { + if (status == Transferable.STATUS_DOWNLOADING) return true; + + if (xmppConnectionService.hasInternetConnection()) { + changeStatus(Transferable.STATUS_DOWNLOADING); + + IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(message.getCounterpart()); + final Element dataq = request.addChild("data", "urn:xmpp:bob"); + dataq.setAttribute("cid", uri.getSchemeSpecificPart()); + xmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (acct, packet) -> { + final Element data = packet.findChild("data", "urn:xmpp:bob"); + if (packet.getType() == IqPacket.TYPE.ERROR || data == null) { + Log.d(Config.LOGTAG, "BobTransfer failed: " + packet); + xmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); + } else { + final String contentType = data.getAttribute("type"); + if (contentType != null) { + final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType); + if (fileExtension != null) { + xmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), fileExtension), contentType); + Log.d(Config.LOGTAG, "rewriting name for bob based on content type"); + setupFile(); + } + } + + try { + file.getParentFile().mkdirs(); + if (!file.exists() && !file.createNewFile()) { + throw new IOException(file.getAbsolutePath()); + } + final OutputStream outputStream = AbstractConnectionManager.createOutputStream(file, false, false); + outputStream.write(Base64.decode(data.getContent(), Base64.DEFAULT)); + outputStream.flush(); + outputStream.close(); + + final boolean privateMessage = message.isPrivateMessage(); + message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE); + xmppConnectionService.getFileBackend().updateFileParams(message, uri.toString()); + xmppConnectionService.updateMessage(message); + } catch (IOException e) { + xmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file); + } + } + message.setTransferable(null); + xmppConnectionService.updateConversationUi(); + }); + return true; + } else { + return false; + } + } + + @Override + public int getStatus() { + return status; + } + + @Override + public int getProgress() { + return 0; + } + + @Override + public Long getFileSize() { + return null; + } + + @Override + public void cancel() { + // No real way to cancel an iq in process... + changeStatus(Transferable.STATUS_CANCELLED); + message.setTransferable(null); + } + + protected void changeStatus(int newStatus) { + status = newStatus; + xmppConnectionService.updateConversationUi(); + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 016f101da..db7229afa 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -930,7 +930,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable fileParams.size = this.transferable.getFileSize(); } - if (oobUri != null && ("http".equalsIgnoreCase(oobUri.getScheme()) || "https".equalsIgnoreCase(oobUri.getScheme()))) { + if (oobUri != null && ("http".equalsIgnoreCase(oobUri.getScheme()) || "https".equalsIgnoreCase(oobUri.getScheme()) || "cid".equalsIgnoreCase(oobUri.getScheme()))) { fileParams.url = oobUri.toString(); } } diff --git a/src/main/java/eu/siacs/conversations/http/URL.java b/src/main/java/eu/siacs/conversations/http/URL.java index e294ed8a0..9dd3c33ba 100644 --- a/src/main/java/eu/siacs/conversations/http/URL.java +++ b/src/main/java/eu/siacs/conversations/http/URL.java @@ -9,7 +9,7 @@ import okhttp3.HttpUrl; public class URL { - public static final List WELL_KNOWN_SCHEMES = Arrays.asList("http", "https", AesGcmURL.PROTOCOL_NAME); + public static final List WELL_KNOWN_SCHEMES = Arrays.asList("http", "https", AesGcmURL.PROTOCOL_NAME, "cid"); public static String tryParse(String url) { final URI uri; diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index fac226603..238519d24 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -3,6 +3,9 @@ package eu.siacs.conversations.parser; import android.util.Log; import android.util.Pair; +import com.cheogram.android.BobTransfer; + +import java.net.URISyntaxException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -746,7 +749,17 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece mXmppConnectionService.databaseBackend.createMessage(message); final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager(); if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) { - manager.createNewDownloadConnection(message); + if (message.getOob() != null && message.getOob().getScheme().equalsIgnoreCase("cid")) { + try { + BobTransfer transfer = new BobTransfer(message, mXmppConnectionService); + message.setTransferable(transfer); + transfer.start(); + } catch (URISyntaxException e) { + Log.d(Config.LOGTAG, "BobTransfer failed to parse URI"); + } + } else { + manager.createNewDownloadConnection(message); + } } else if (notify) { if (query != null && query.isCatchup()) { mXmppConnectionService.getNotificationService().pushFromBacklog(message); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 684f438ef..2cbee4e71 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -64,11 +64,14 @@ import androidx.databinding.DataBindingUtil; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; +import com.cheogram.android.BobTransfer; + import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import org.jetbrains.annotations.NotNull; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -1925,9 +1928,19 @@ public class ConversationFragment extends XmppFragment .show(); return; } - activity.xmppConnectionService - .getHttpConnectionManager() - .createNewDownloadConnection(message, true); + if (message.getOob() != null && message.getOob().getScheme().equalsIgnoreCase("cid")) { + try { + BobTransfer transfer = new BobTransfer(message, activity.xmppConnectionService); + message.setTransferable(transfer); + transfer.start(); + } catch (URISyntaxException e) { + Log.d(Config.LOGTAG, "BobTransfer failed to parse URI"); + } + } else { + activity.xmppConnectionService + .getHttpConnectionManager() + .createNewDownloadConnection(message, true); + } } @SuppressLint("InflateParams") diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index 0f1636308..207e07155 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -117,6 +117,7 @@ public class MessageUtils { } public static boolean unInitiatedButKnownSize(Message message) { - return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size != null && message.getFileParams().url != null; + return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().url != null && + (message.getFileParams().size != null || (message.getOob() != null && message.getOob().getScheme().equalsIgnoreCase("cid"))); } } -- 2.34.5