HttpUploadManager.java

  1package eu.siacs.conversations.xmpp.manager;
  2
  3import android.content.Context;
  4import android.util.Base64;
  5import com.google.common.collect.ImmutableMap;
  6import com.google.common.util.concurrent.Futures;
  7import com.google.common.util.concurrent.ListenableFuture;
  8import com.google.common.util.concurrent.MoreExecutors;
  9import eu.siacs.conversations.entities.DownloadableFile;
 10import eu.siacs.conversations.xml.Namespace;
 11import eu.siacs.conversations.xmpp.Jid;
 12import eu.siacs.conversations.xmpp.XmppConnection;
 13import im.conversations.android.xmpp.model.stanza.Iq;
 14import im.conversations.android.xmpp.model.upload.Request;
 15import java.nio.ByteBuffer;
 16import java.util.Map;
 17import java.util.UUID;
 18import okhttp3.Headers;
 19import okhttp3.HttpUrl;
 20
 21public class HttpUploadManager extends AbstractManager {
 22
 23    public HttpUploadManager(final Context context, final XmppConnection connection) {
 24        super(context, connection);
 25    }
 26
 27    public ListenableFuture<Slot> request(final DownloadableFile file, final String mime) {
 28        final var result =
 29                getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
 30        if (result == null) {
 31            return Futures.immediateFailedFuture(
 32                    new IllegalStateException("No HTTP upload host found"));
 33        }
 34        return requestHttpUpload(result.getKey(), file, mime);
 35    }
 36
 37    private ListenableFuture<Slot> requestHttpUpload(
 38            final Jid host, final DownloadableFile file, final String mime) {
 39        final Iq iq = new Iq(Iq.Type.GET);
 40        iq.setTo(host);
 41        final var request = iq.addExtension(new Request());
 42        request.setFilename(convertFilename(file.getName()));
 43        request.setSize(file.getExpectedSize());
 44        request.setContentType(mime);
 45        final var iqFuture = this.connection.sendIqPacket(iq);
 46        return Futures.transform(
 47                iqFuture,
 48                response -> {
 49                    final var slot =
 50                            response.getExtension(
 51                                    im.conversations.android.xmpp.model.upload.Slot.class);
 52                    if (slot == null) {
 53                        throw new IllegalStateException("Slot not found in IQ response");
 54                    }
 55                    final var getUrl = slot.getGetUrl();
 56                    final var put = slot.getPut();
 57                    if (getUrl == null || put == null) {
 58                        throw new IllegalStateException("Missing get or put in slot response");
 59                    }
 60                    final var putUrl = put.getUrl();
 61                    if (putUrl == null) {
 62                        throw new IllegalStateException("Missing put url");
 63                    }
 64                    final var contentType = mime == null ? "application/octet-stream" : mime;
 65                    final var headers =
 66                            new ImmutableMap.Builder<String, String>()
 67                                    .putAll(put.getHeadersAllowList())
 68                                    .put("Content-Type", contentType)
 69                                    .buildKeepingLast();
 70                    return new Slot(putUrl, getUrl, headers);
 71                },
 72                MoreExecutors.directExecutor());
 73    }
 74
 75    private static String convertFilename(final String name) {
 76        int pos = name.indexOf('.');
 77        if (pos < 0) {
 78            return name;
 79        }
 80        try {
 81            UUID uuid = UUID.fromString(name.substring(0, pos));
 82            ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
 83            bb.putLong(uuid.getMostSignificantBits());
 84            bb.putLong(uuid.getLeastSignificantBits());
 85            return Base64.encodeToString(
 86                            bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP)
 87                    + name.substring(pos);
 88        } catch (final Exception e) {
 89            return name;
 90        }
 91    }
 92
 93    public static class Slot {
 94        public final HttpUrl put;
 95        public final HttpUrl get;
 96        public final Headers headers;
 97
 98        private Slot(final HttpUrl put, final HttpUrl get, final Headers headers) {
 99            this.put = put;
100            this.get = get;
101            this.headers = headers;
102        }
103
104        private Slot(final HttpUrl put, final HttpUrl get, final Map<String, String> headers) {
105            this(put, get, Headers.of(headers));
106        }
107    }
108}