move slot requesting to HttpUploadManager

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/generator/IqGenerator.java                            |  33 
src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java                        |  16 
src/main/java/eu/siacs/conversations/http/SlotRequester.java                               | 126 
src/main/java/eu/siacs/conversations/xmpp/Managers.java                                    |   2 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                              |  29 
src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java |  11 
src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java                        |  11 
src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java                   | 108 
src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java                        |   6 
src/main/java/im/conversations/android/xmpp/model/upload/Put.java                          |  22 
10 files changed, 168 insertions(+), 196 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -7,7 +7,6 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
@@ -16,14 +15,11 @@ import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.forms.Data;
 import eu.siacs.conversations.xmpp.pep.Avatar;
 import im.conversations.android.xmpp.model.stanza.Iq;
-import im.conversations.android.xmpp.model.upload.Request;
-import java.nio.ByteBuffer;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
-import java.util.UUID;
 import org.whispersystems.libsignal.IdentityKey;
 import org.whispersystems.libsignal.ecc.ECPublicKey;
 import org.whispersystems.libsignal.state.PreKeyRecord;
@@ -262,35 +258,6 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public Iq requestHttpUploadSlot(
-            final Jid host, final DownloadableFile file, final String mime) {
-        final Iq packet = new Iq(Iq.Type.GET);
-        packet.setTo(host);
-        final var request = packet.addExtension(new Request());
-        request.setFilename(convertFilename(file.getName()));
-        request.setSize(file.getExpectedSize());
-        return packet;
-    }
-
-    private static String convertFilename(String name) {
-        int pos = name.indexOf('.');
-        if (pos != -1) {
-            try {
-                UUID uuid = UUID.fromString(name.substring(0, pos));
-                ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
-                bb.putLong(uuid.getMostSignificantBits());
-                bb.putLong(uuid.getLeastSignificantBits());
-                return Base64.encodeToString(
-                                bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP)
-                        + name.substring(pos);
-            } catch (Exception e) {
-                return name;
-            }
-        } else {
-            return name;
-        }
-    }
-
     public static Iq generateCreateAccountWithCaptcha(
             final Account account, final String id, final Data data) {
         final Iq register = new Iq(Iq.Type.SET);

src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java 🔗

@@ -17,6 +17,7 @@ import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.services.AbstractConnectionManager;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
@@ -39,12 +40,12 @@ public class HttpUploadConnection
     private boolean delayed = false;
     private DownloadableFile file;
     private final Message message;
-    private SlotRequester.Slot slot;
+    private HttpUploadManager.Slot slot;
     private byte[] key = null;
 
     private long transmitted = 0;
     private Call mostRecentCall;
-    private ListenableFuture<SlotRequester.Slot> slotFuture;
+    private ListenableFuture<HttpUploadManager.Slot> slotFuture;
 
     public HttpUploadConnection(
             final Message message, final HttpConnectionManager httpConnectionManager) {
@@ -78,7 +79,7 @@ public class HttpUploadConnection
 
     @Override
     public void cancel() {
-        final ListenableFuture<SlotRequester.Slot> slotFuture = this.slotFuture;
+        final ListenableFuture<HttpUploadManager.Slot> slotFuture = this.slotFuture;
         if (slotFuture != null && !slotFuture.isDone()) {
             if (slotFuture.cancel(true)) {
                 Log.d(Config.LOGTAG, "cancelled slot requester");
@@ -94,7 +95,7 @@ public class HttpUploadConnection
     private void fail(String errorMessage) {
         finish();
         final Call call = this.mostRecentCall;
-        final Future<SlotRequester.Slot> slotFuture = this.slotFuture;
+        final Future<HttpUploadManager.Slot> slotFuture = this.slotFuture;
         final boolean cancelled =
                 (call != null && call.isCanceled())
                         || (slotFuture != null && slotFuture.isCancelled());
@@ -109,8 +110,9 @@ public class HttpUploadConnection
         message.setTransferable(null);
     }
 
-    public void init(boolean delay) {
+    public void init(final boolean delay) {
         final Account account = message.getConversation().getAccount();
+        final var connection = account.getXmppConnection();
         this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
         final String mime;
         if (message.getEncryption() == Message.ENCRYPTION_PGP
@@ -129,12 +131,12 @@ public class HttpUploadConnection
         }
         this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
         message.resetFileParams();
-        this.slotFuture = new SlotRequester(mXmppConnectionService).request(account, file, mime);
+        this.slotFuture = connection.getManager(HttpUploadManager.class).request(file, mime);
         Futures.addCallback(
                 this.slotFuture,
                 new FutureCallback<>() {
                     @Override
-                    public void onSuccess(@Nullable SlotRequester.Slot result) {
+                    public void onSuccess(@Nullable HttpUploadManager.Slot result) {
                         HttpUploadConnection.this.slot = result;
                         try {
                             HttpUploadConnection.this.upload();

src/main/java/eu/siacs/conversations/http/SlotRequester.java 🔗

@@ -1,126 +0,0 @@
-/*
- * Copyright (c) 2018, Daniel Gultsch All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation and/or
- * other materials provided with the distribution.
- *
- * 3. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package eu.siacs.conversations.http;
-
-import android.util.Log;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.MoreExecutors;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.Jid;
-import im.conversations.android.xmpp.model.stanza.Iq;
-import im.conversations.android.xmpp.model.upload.Header;
-import im.conversations.android.xmpp.model.upload.Slot;
-import java.util.Map;
-import okhttp3.Headers;
-import okhttp3.HttpUrl;
-
-public class SlotRequester {
-
-    private final XmppConnectionService service;
-
-    public SlotRequester(XmppConnectionService service) {
-        this.service = service;
-    }
-
-    public ListenableFuture<Slot> request(
-            final Account account, final DownloadableFile file, final String mime) {
-        final var result =
-                account.getXmppConnection()
-                        .getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
-        if (result == null) {
-            return Futures.immediateFailedFuture(
-                    new IllegalStateException("No HTTP upload host found"));
-        }
-        return requestHttpUpload(account, result.getKey(), file, mime);
-    }
-
-    private ListenableFuture<Slot> requestHttpUpload(
-            final Account account, final Jid host, final DownloadableFile file, final String mime) {
-        final Iq request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
-        final var iqFuture = service.sendIqPacket(account, request);
-        return Futures.transform(
-                iqFuture,
-                response -> {
-                    final var slot =
-                            response.getExtension(
-                                    im.conversations.android.xmpp.model.upload.Slot.class);
-                    if (slot == null) {
-                        Log.d(Config.LOGTAG, "-->" + response);
-                        throw new IllegalStateException("Slot not found in IQ response");
-                    }
-                    final var getUrl = slot.getGetUrl();
-                    final var put = slot.getPut();
-                    if (getUrl == null || put == null) {
-                        throw new IllegalStateException("Missing get or put in slot response");
-                    }
-                    final var putUrl = put.getUrl();
-                    if (putUrl == null) {
-                        throw new IllegalStateException("Missing put url");
-                    }
-                    final var headers = new ImmutableMap.Builder<String, String>();
-                    for (final Header header : put.getHeaders()) {
-                        final String name = header.getHeaderName();
-                        final String value = header.getContent();
-                        if (Strings.isNullOrEmpty(value) || value.contains("\n")) {
-                            continue;
-                        }
-                        headers.put(name, value.trim());
-                    }
-                    headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
-                    return new Slot(putUrl, getUrl, headers.buildKeepingLast());
-                },
-                MoreExecutors.directExecutor());
-    }
-
-    public static class Slot {
-        public final HttpUrl put;
-        public final HttpUrl get;
-        public final Headers headers;
-
-        private Slot(HttpUrl put, HttpUrl get, Headers headers) {
-            this.put = put;
-            this.get = get;
-            this.headers = headers;
-        }
-
-        private Slot(HttpUrl put, HttpUrl getUrl, Map<String, String> headers) {
-            this.put = put;
-            this.get = getUrl;
-            this.headers = Headers.of(headers);
-        }
-    }
-}

src/main/java/eu/siacs/conversations/xmpp/Managers.java 🔗

@@ -11,6 +11,7 @@ import eu.siacs.conversations.xmpp.manager.BookmarkManager;
 import eu.siacs.conversations.xmpp.manager.CarbonsManager;
 import eu.siacs.conversations.xmpp.manager.DiscoManager;
 import eu.siacs.conversations.xmpp.manager.EntityTimeManager;
+import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
 import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager;
 import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager;
 import eu.siacs.conversations.xmpp.manager.NickManager;
@@ -39,6 +40,7 @@ public class Managers {
                 .put(CarbonsManager.class, new CarbonsManager(context, connection))
                 .put(DiscoManager.class, new DiscoManager(context, connection))
                 .put(EntityTimeManager.class, new EntityTimeManager(context, connection))
+                .put(HttpUploadManager.class, new HttpUploadManager(context, connection))
                 .put(LegacyBookmarkManager.class, new LegacyBookmarkManager(context, connection))
                 .put(
                         MessageDisplayedSynchronizationManager.class,

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -2730,29 +2730,6 @@ public class XmppConnection implements Runnable {
         return this.managers.getInstance(clazz);
     }
 
-    private List<Entry<Jid, InfoQuery>> findDiscoItemsByFeature(final String feature) {
-        final List<Entry<Jid, InfoQuery>> items = new ArrayList<>();
-        for (final Entry<Jid, InfoQuery> cursor :
-                getManager(DiscoManager.class).getServerItems().entrySet()) {
-            if (cursor.getValue().getFeatureStrings().contains(feature)) {
-                items.add(cursor);
-            }
-        }
-        return items;
-    }
-
-    public Entry<Jid, InfoQuery> getServiceDiscoveryResultByFeature(final String feature) {
-        return Iterables.getFirst(findDiscoItemsByFeature(feature), null);
-    }
-
-    public Jid findDiscoItemByFeature(final String feature) {
-        final var items = findDiscoItemsByFeature(feature);
-        if (items.isEmpty()) {
-            return null;
-        }
-        return Iterables.getFirst(items, null).getKey();
-    }
-
     public List<String> getMucServersWithholdAccount() {
         final List<String> servers = getMucServers();
         servers.remove(account.getDomain().toString());
@@ -3207,7 +3184,8 @@ public class XmppConnection implements Runnable {
             if (Config.DISABLE_HTTP_UPLOAD) {
                 return false;
             }
-            final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
+            final var result =
+                    getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
             if (result == null) {
                 return false;
             }
@@ -3238,7 +3216,8 @@ public class XmppConnection implements Runnable {
         }
 
         public long getMaxHttpUploadSize() {
-            final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
+            final var result =
+                    getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
             if (result == null) {
                 return -1;
             }

src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java 🔗

@@ -29,6 +29,7 @@ import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
 import eu.siacs.conversations.xmpp.jingle.DirectConnectionUtils;
 import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
 import im.conversations.android.xmpp.model.stanza.Iq;
 import java.io.IOException;
 import java.io.InputStream;
@@ -306,14 +307,18 @@ public class SocksByteStreamsTransport implements Transport {
             return Futures.immediateFailedFuture(
                     new IllegalStateException("Proxy look up is disabled"));
         }
-        final Jid streamer = xmppConnection.findDiscoItemByFeature(Namespace.BYTE_STREAMS);
+        final var streamer =
+                xmppConnection
+                        .getManager(DiscoManager.class)
+                        .findDiscoItemByFeature(Namespace.BYTE_STREAMS);
         if (streamer == null) {
             return Futures.immediateFailedFuture(
                     new IllegalStateException("No proxy/streamer found"));
         }
         final Iq iqRequest = new Iq(Iq.Type.GET);
-        iqRequest.setTo(streamer);
+        iqRequest.setTo(streamer.getKey());
         // TODO urgent refactor to extension
+        // TODO and maybe move to Manager
         iqRequest.query(Namespace.BYTE_STREAMS);
         final SettableFuture<Candidate> candidateFuture = SettableFuture.create();
         xmppConnection.sendIqPacket(
@@ -342,7 +347,7 @@ public class SocksByteStreamsTransport implements Transport {
                                 new Candidate(
                                         UUID.randomUUID().toString(),
                                         host,
-                                        streamer,
+                                        streamer.getKey(),
                                         port,
                                         655360 + (initiator ? 0 : 15),
                                         CandidateType.PROXY));

src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java 🔗

@@ -7,6 +7,8 @@ import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
 import com.google.common.io.BaseEncoding;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -467,6 +469,15 @@ public class DiscoManager extends AbstractManager {
         }
     }
 
+    public Map<Jid, InfoQuery> findDiscoItemsByFeature(final String feature) {
+        return Maps.filterValues(getServerItems(), v -> v.hasFeature(feature));
+    }
+
+    public Map.Entry<Jid, InfoQuery> findDiscoItemByFeature(final String feature) {
+        final var items = findDiscoItemsByFeature(feature);
+        return Iterables.getFirst(items.entrySet(), null);
+    }
+
     public static final class CapsHashMismatchException extends IllegalStateException {
         public CapsHashMismatchException(final String message) {
             super(message);

src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java 🔗

@@ -0,0 +1,108 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import android.util.Base64;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.upload.Request;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.UUID;
+import okhttp3.Headers;
+import okhttp3.HttpUrl;
+
+public class HttpUploadManager extends AbstractManager {
+
+    public HttpUploadManager(final Context context, final XmppConnection connection) {
+        super(context, connection);
+    }
+
+    public ListenableFuture<Slot> request(final DownloadableFile file, final String mime) {
+        final var result =
+                getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
+        if (result == null) {
+            return Futures.immediateFailedFuture(
+                    new IllegalStateException("No HTTP upload host found"));
+        }
+        return requestHttpUpload(result.getKey(), file, mime);
+    }
+
+    private ListenableFuture<Slot> requestHttpUpload(
+            final Jid host, final DownloadableFile file, final String mime) {
+        final Iq iq = new Iq(Iq.Type.GET);
+        iq.setTo(host);
+        final var request = iq.addExtension(new Request());
+        request.setFilename(convertFilename(file.getName()));
+        request.setSize(file.getExpectedSize());
+        request.setContentType(mime);
+        final var iqFuture = this.connection.sendIqPacket(iq);
+        return Futures.transform(
+                iqFuture,
+                response -> {
+                    final var slot =
+                            response.getExtension(
+                                    im.conversations.android.xmpp.model.upload.Slot.class);
+                    if (slot == null) {
+                        throw new IllegalStateException("Slot not found in IQ response");
+                    }
+                    final var getUrl = slot.getGetUrl();
+                    final var put = slot.getPut();
+                    if (getUrl == null || put == null) {
+                        throw new IllegalStateException("Missing get or put in slot response");
+                    }
+                    final var putUrl = put.getUrl();
+                    if (putUrl == null) {
+                        throw new IllegalStateException("Missing put url");
+                    }
+                    final var contentType = mime == null ? "application/octet-stream" : mime;
+                    final var headers =
+                            new ImmutableMap.Builder<String, String>()
+                                    .putAll(put.getHeadersAllowList())
+                                    .put("Content-Type", contentType)
+                                    .buildKeepingLast();
+                    return new Slot(putUrl, getUrl, headers);
+                },
+                MoreExecutors.directExecutor());
+    }
+
+    private static String convertFilename(final String name) {
+        int pos = name.indexOf('.');
+        if (pos < 0) {
+            return name;
+        }
+        try {
+            UUID uuid = UUID.fromString(name.substring(0, pos));
+            ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
+            bb.putLong(uuid.getMostSignificantBits());
+            bb.putLong(uuid.getLeastSignificantBits());
+            return Base64.encodeToString(
+                            bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP)
+                    + name.substring(pos);
+        } catch (final Exception e) {
+            return name;
+        }
+    }
+
+    public static class Slot {
+        public final HttpUrl put;
+        public final HttpUrl get;
+        public final Headers headers;
+
+        private Slot(final HttpUrl put, final HttpUrl get, final Headers headers) {
+            this.put = put;
+            this.get = get;
+            this.headers = headers;
+        }
+
+        private Slot(final HttpUrl put, final HttpUrl get, final Map<String, String> headers) {
+            this(put, get, Headers.of(headers));
+        }
+    }
+}

src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java 🔗

@@ -37,11 +37,13 @@ public class VCardManager extends AbstractManager {
                 vCard -> {
                     final var photo = vCard.getPhoto();
                     if (photo == null) {
-                        throw new IllegalStateException("No photo in vCard");
+                        throw new IllegalStateException(
+                                String.format("No photo in vCard of %s", address));
                     }
                     final var binaryValue = photo.getBinaryValue();
                     if (binaryValue == null) {
-                        throw new IllegalStateException("Photo has no binary value");
+                        throw new IllegalStateException(
+                                String.format("Photo has no binary value in vCard of %s", address));
                     }
                     return binaryValue.asBytes();
                 },

src/main/java/im/conversations/android/xmpp/model/upload/Put.java 🔗

@@ -1,14 +1,21 @@
 package im.conversations.android.xmpp.model.upload;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import im.conversations.android.annotation.XmlElement;
 import im.conversations.android.xmpp.model.Extension;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
+import java.util.Map;
 import okhttp3.HttpUrl;
 
 @XmlElement
 public class Put extends Extension {
 
+    private static final List<String> HEADER_ALLOW_LIST =
+            Arrays.asList("Authorization", "Cookie", "Expires");
+
     public Put() {
         super(Put.class);
     }
@@ -24,4 +31,19 @@ public class Put extends Extension {
     public Collection<Header> getHeaders() {
         return this.getExtensions(Header.class);
     }
+
+    public Map<String, String> getHeadersAllowList() {
+        final var headers = new ImmutableMap.Builder<String, String>();
+        for (final Header header : this.getHeaders()) {
+            final String name = header.getHeaderName();
+            final String value = Strings.nullToEmpty(header.getContent()).trim();
+            if (Strings.isNullOrEmpty(value) || value.contains("\n")) {
+                continue;
+            }
+            if (HEADER_ALLOW_LIST.contains(name)) {
+                headers.put(name, value);
+            }
+        }
+        return headers.buildKeepingLast();
+    }
 }