diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index f8378b3f34..6df676ba2c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -62,7 +62,6 @@ import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob; import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob; import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob; import org.thoughtcrime.securesms.jobs.FcmRefreshJob; -import org.thoughtcrime.securesms.jobs.RetryPendingSendsJob; import org.thoughtcrime.securesms.jobs.FontDownloaderJob; import org.thoughtcrime.securesms.jobs.GroupRingCleanupJob; import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob; @@ -76,13 +75,13 @@ import org.thoughtcrime.securesms.jobs.RefreshSvrCredentialsJob; import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob; +import org.thoughtcrime.securesms.jobs.RetryPendingSendsJob; import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob; import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver; -import org.thoughtcrime.securesms.messages.GroupSendEndorsementInternalNotifier; import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.mms.SignalGlideComponents; import org.thoughtcrime.securesms.mms.SignalGlideModule; @@ -227,7 +226,6 @@ public class ApplicationContext extends Application implements AppForegroundObse .addPostRender(GroupRingCleanupJob::enqueue) .addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary) .addPostRender(() -> ActiveCallManager.clearNotifications(this)) - .addPostRender(() -> GroupSendEndorsementInternalNotifier.init()) .addPostRender(RestoreOptimizedMediaJob::enqueueIfNecessary) .addPostRender(RetryPendingSendsJob::enqueueForAll) .execute(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendEndorsementInternalNotifier.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendEndorsementInternalNotifier.kt index 4a91b05b4e..94b81c3592 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendEndorsementInternalNotifier.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendEndorsementInternalNotifier.kt @@ -14,12 +14,10 @@ import androidx.core.app.NotificationManagerCompat import org.signal.core.util.PendingIntentFlags import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.NotificationIds import org.thoughtcrime.securesms.util.RemoteConfig -import org.whispersystems.signalservice.api.crypto.SealedSenderAccess import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes @@ -27,7 +25,7 @@ import kotlin.time.Duration.Companion.minutes /** * Internal user only notifier when "bad" things happen with group send endorsement sends. */ -object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListener { +object GroupSendEndorsementInternalNotifier { private const val TAG = "GSENotifier" @@ -36,27 +34,14 @@ object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListene private var lastMissingNotify: Duration = 0.milliseconds - private var lastFallbackNotify: Duration = 0.milliseconds - @JvmStatic - fun init() { - if (RemoteConfig.internalUser) { - SealedSenderAccess.fallbackListener = this + fun maybePostGroupSendFallbackError(context: Context) { + if (!RemoteConfig.internalUser) { + return } - } - override fun onAccessToTokenFallback() { - Log.w(TAG, "Fallback from access key to token", Throwable()) - postFallbackError(AppDependencies.application) - } + Log.internal().w(TAG, "Group send with GSE failed, GSE was likely out of date or incorrect", Throwable()) - override fun onTokenToAccessFallback(hasAccessKeyFallback: Boolean) { - Log.w(TAG, "Fallback from token hasAccessKey=$hasAccessKeyFallback", Throwable()) - postFallbackError(AppDependencies.application) - } - - @JvmStatic - fun postGroupSendFallbackError(context: Context) { val now = System.currentTimeMillis().milliseconds if (lastGroupSendNotify + 5.minutes > now && skippedGroupSendNotifies < 5) { skippedGroupSendNotifies++ @@ -65,8 +50,8 @@ object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListene val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES) .setSmallIcon(R.drawable.ic_notification) - .setContentTitle("[Internal-only] GSE failed for group send") - .setContentText("Please tap to send a debug log") + .setContentTitle("[Internal-only] Group send failure (GSE)") + .setContentText("Please tap to get a debug log") .setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable())) .build() @@ -77,7 +62,13 @@ object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListene } @JvmStatic - fun postMissingGroupSendEndorsement(context: Context) { + fun maybePostMissingGroupSendEndorsement(context: Context) { + if (!RemoteConfig.internalUser) { + return + } + + Log.internal().w(TAG, "GSE missing for recipient", Throwable()) + val now = System.currentTimeMillis().milliseconds if (lastMissingNotify + 5.minutes > now) { return @@ -85,8 +76,8 @@ object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListene val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES) .setSmallIcon(R.drawable.ic_notification) - .setContentTitle("[Internal-only] GSE missing for recipient") - .setContentText("Please tap to send a debug log") + .setContentTitle("[Internal-only] Missing recipient (GSE)") + .setContentText("Please tap to get a debug log") .setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable())) .build() @@ -94,23 +85,4 @@ object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListene lastMissingNotify = now } - - @JvmStatic - fun postFallbackError(context: Context) { - val now = System.currentTimeMillis().milliseconds - if (lastFallbackNotify + 5.minutes > now) { - return - } - - val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle("[Internal-only] GSE fallback occurred!") - .setContentText("Please tap to send a debug log") - .setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable())) - .build() - - NotificationManagerCompat.from(context).notify(NotificationIds.INTERNAL_ERROR, notification) - - lastFallbackNotify = now - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java index 9b84b2e1f3..f063ae3369 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java @@ -14,8 +14,8 @@ import org.signal.libsignal.protocol.NoSessionException; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement; import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken; -import org.thoughtcrime.securesms.crypto.SenderKeyUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil; +import org.thoughtcrime.securesms.crypto.SenderKeyUtil; import org.thoughtcrime.securesms.database.MessageSendLogTables; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.DistributionListId; @@ -34,7 +34,6 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.util.RecipientAccessList; import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.SignalLocalMetrics; -import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.CancelationException; import org.whispersystems.signalservice.api.SignalServiceMessageSender; @@ -161,11 +160,11 @@ public final class GroupSendUtil { * Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of * {@link SendMessageResult}s just like we're used to. * - * @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group. + * @param groupId The groupId of the group you're sending to */ @WorkerThread public static List sendCallMessage(@NonNull Context context, - @Nullable GroupId.V2 groupId, + @NonNull GroupId.V2 groupId, @NonNull List allTargets, @NonNull SignalServiceCallMessage message) throws IOException, UntrustedIdentityException @@ -292,9 +291,11 @@ public final class GroupSendUtil { groupSendEndorsementRecords = SignalDatabase.groups().getGroupSendEndorsements(groupId); } catch (GroupChangeException | IOException e) { if (groupSendEndorsementExpiration == 0) { - Log.w(TAG, "Unable to update group send endorsements, falling back to access key", e); + Log.w(TAG, "Unable to update group send endorsements, falling back to legacy", e); useGroupSendEndorsements = false; groupSendEndorsementRecords = new GroupSendEndorsementRecords(Collections.emptyMap()); + + GroupSendEndorsementInternalNotifier.maybePostGroupSendFallbackError(context); } else { Log.w(TAG, "Unable to update group send endorsements, using what we have", e); } @@ -308,49 +309,38 @@ public final class GroupSendUtil { List senderKeyTargets = new LinkedList<>(); List legacyTargets = new LinkedList<>(); - for (Recipient recipient : registeredTargets) { - Optional access = recipients.getAccessPair(recipient.getId()); - boolean validMembership = groupId == null || (groupRecord.isPresent() && groupRecord.get().getMembers().contains(recipient.getId())); - - if (useGroupSendEndorsements) { + // Determine recipients that can be sent to via sender key vs must use legacy fan-out + if (distributionId == null) { + Log.i(TAG, "No DistributionId. Using legacy."); + legacyTargets.addAll(registeredTargets); + } else if (isStorySend) { + Log.i(TAG, "Sending a story. Using sender key for all " + allTargets.size() + " recipients."); + senderKeyTargets.addAll(registeredTargets); + } else if (!useGroupSendEndorsements) { + Log.i(TAG, "No group send endorsements, using legacy for all " + allTargets.size() + " recipients."); + legacyTargets.addAll(registeredTargets); + } else { + for (Recipient recipient : registeredTargets) { + boolean validMembership = groupRecord.get().getMembers().contains(recipient.getId()); GroupSendEndorsement groupSendEndorsement = groupSendEndorsementRecords.getEndorsement(recipient.getId()); + if (groupSendEndorsement != null && recipient.getHasAci() && validMembership) { senderKeyTargets.add(recipient); } else { legacyTargets.add(recipient); if (validMembership) { Log.w(TAG, "Should be using group send endorsement but not found for " + recipient.getId()); - if (RemoteConfig.internalUser()) { - GroupSendEndorsementInternalNotifier.postMissingGroupSendEndorsement(context); - } + GroupSendEndorsementInternalNotifier.maybePostMissingGroupSendEndorsement(context); } } - } else { - // Use sender key - if (recipient.getHasServiceId() && - access.isPresent() && - validMembership) - { - senderKeyTargets.add(recipient); - } else { - legacyTargets.add(recipient); - } } } - if (distributionId == null) { - Log.i(TAG, "No DistributionId. Using legacy."); - legacyTargets.addAll(senderKeyTargets); - senderKeyTargets.clear(); - } else if (isStorySend) { - Log.i(TAG, "Sending a story. Using sender key for all " + allTargets.size() + " recipients."); - senderKeyTargets.clear(); - senderKeyTargets.addAll(registeredTargets); - legacyTargets.clear(); - } else if (SignalStore.internal().getRemoveSenderKeyMinimum()) { + // Enforce minimum number of sender key destinations + if (SignalStore.internal().getRemoveSenderKeyMinimum()) { Log.i(TAG, "Sender key minimum removed. Using for " + senderKeyTargets.size() + " recipients."); - } else if (senderKeyTargets.size() < 2) { - Log.i(TAG, "Too few sender-key-capable users (" + senderKeyTargets.size() + "). Doing all legacy sends."); + } else if (senderKeyTargets.size() < 2 && !isStorySend) { + Log.i(TAG, "Too few sender-key-capable users (" + senderKeyTargets.size() + ") for non-story send. Doing all legacy sends."); legacyTargets.addAll(senderKeyTargets); senderKeyTargets.clear(); } else { @@ -431,8 +421,8 @@ public final class GroupSendUtil { Log.w(TAG, "Someone had a bad UD header. Falling back to legacy sends.", e); legacyTargets.addAll(senderKeyTargets); - if (useGroupSendEndorsements && RemoteConfig.internalUser()) { - GroupSendEndorsementInternalNotifier.postGroupSendFallbackError(context); + if (useGroupSendEndorsements) { + GroupSendEndorsementInternalNotifier.maybePostGroupSendFallbackError(context); } } catch (NoSessionException e) { Log.w(TAG, "No session. Falling back to legacy sends.", e); @@ -712,6 +702,8 @@ public final class GroupSendUtil { @Nullable PartialSendBatchCompleteListener partialListener) throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException { + Preconditions.checkNotNull(groupSendEndorsements, "GSEs must be non-null for non-story sender key send."); + messageSender.sendGroupTyping(distributionId, targets, access, groupSendEndorsements, message); List results = targets.stream().map(a -> SendMessageResult.success(a, Collections.emptyList(), true, false, -1, Optional.empty())).collect(Collectors.toList()); @@ -780,6 +772,8 @@ public final class GroupSendUtil { @Nullable PartialSendBatchCompleteListener partialSendListener) throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException { + Preconditions.checkNotNull(groupSendEndorsements, "GSEs must be non-null for non-story sender key send."); + return messageSender.sendCallMessage(distributionId, targets, access, groupSendEndorsements, message, partialSendListener); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index c122b6ebb2..0f174d50b0 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -6,6 +6,7 @@ package org.whispersystems.signalservice.api; import org.signal.core.util.Base64; +import org.signal.libsignal.metadata.certificate.SenderCertificate; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.InvalidKeyException; @@ -138,6 +139,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; @@ -286,7 +288,7 @@ public class SignalServiceMessageSender { public void sendGroupTyping(DistributionId distributionId, List recipients, List unidentifiedAccess, - @Nullable GroupSendEndorsements groupSendEndorsements, + @Nonnull GroupSendEndorsements groupSendEndorsements, SignalServiceTypingMessage message) throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException { @@ -388,7 +390,7 @@ public class SignalServiceMessageSender { public List sendCallMessage(DistributionId distributionId, List recipients, List unidentifiedAccess, - @Nullable GroupSendEndorsements groupSendEndorsements, + @Nonnull GroupSendEndorsements groupSendEndorsements, SignalServiceCallMessage message, PartialSendBatchCompleteListener partialListener) throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException @@ -2356,6 +2358,7 @@ public class SignalServiceMessageSender { return Collections.emptyList(); } + Preconditions.checkArgument(groupSendEndorsements != null || story, "[" + timestamp + "] GSE is null and not sending a story"); Preconditions.checkArgument(recipients.size() == unidentifiedAccess.size(), "[" + timestamp + "] Unidentified access mismatch!"); Map accessBySid = new HashMap<>(); @@ -2366,7 +2369,8 @@ public class SignalServiceMessageSender { accessBySid.put(addressIterator.next().getServiceId(), accessIterator.next()); } - SealedSenderAccess sealedSenderAccess = SealedSenderAccess.forGroupSend(groupSendEndorsements, unidentifiedAccess, story); + SenderCertificate senderCertificate = unidentifiedAccess.stream().filter(Objects::nonNull).findFirst().map(UnidentifiedAccess::getUnidentifiedCertificate).orElse(null); + SealedSenderAccess sealedSenderAccess = SealedSenderAccess.forGroupSend(senderCertificate, groupSendEndorsements, story); for (int i = 0; i < RETRY_COUNT; i++) { GroupTargetInfo targetInfo = buildGroupTargetInfo(recipients); @@ -2493,12 +2497,8 @@ public class SignalServiceMessageSender { handleStaleDevices(address, stale.getDevices()); } } catch (InvalidUnidentifiedAccessHeaderException e) { - sealedSenderAccess = sealedSenderAccess.switchToFallback(); - if (sealedSenderAccess != null) { - Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Handling invalid group send endorsements. (" + e.getMessage() + ")"); - } else { - throw e; - } + Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Invalid access header. (" + e.getMessage() + ")"); + throw e; } Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Attempt failed (i = " + i + ")"); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SealedSenderAccess.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SealedSenderAccess.kt index cfb765a0f9..564a592a47 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SealedSenderAccess.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SealedSenderAccess.kt @@ -9,7 +9,6 @@ import org.signal.core.util.Base64 import org.signal.libsignal.metadata.certificate.SenderCertificate import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken import org.whispersystems.signalservice.api.groupsv2.GroupSendEndorsements -import org.whispersystems.util.ByteArrayUtil /** * Provides single interface for the various ways to send via sealed sender. @@ -25,6 +24,8 @@ sealed class SealedSenderAccess { abstract fun switchToFallback(): SealedSenderAccess? + open fun applyHeader(): Boolean = true + /** * For sending to an single recipient using group send endorsement/token first and then fallback to * access key if available. @@ -39,7 +40,6 @@ sealed class SealedSenderAccess { override val headerValue: String by lazy { Base64.encodeWithPadding(groupSendToken.serialize()) } override fun switchToFallback(): SealedSenderAccess? { - fallbackListener?.onTokenToAccessFallback(unidentifiedAccess != null) return if (unidentifiedAccess != null) { IndividualUnidentifiedAccessFirst(unidentifiedAccess) } else { @@ -66,7 +66,6 @@ sealed class SealedSenderAccess { override fun switchToFallback(): SealedSenderAccess? { val groupSendToken = createGroupSendToken?.create() return if (groupSendToken != null) { - fallbackListener?.onAccessToTokenFallback() IndividualGroupSendTokenFirst(groupSendToken, senderCertificate) } else { null @@ -92,27 +91,15 @@ sealed class SealedSenderAccess { } } - /** - * For sending to a "group" of recipients using access keys. - */ - class GroupUnidentifiedAccess( - private val unidentifiedAccess: List, - override val senderCertificate: SenderCertificate = unidentifiedAccess.first().unidentifiedCertificate - ) : SealedSenderAccess() { - - override val headerName: String = "Unidentified-Access-Key" - override val headerValue: String by lazy { - var joinedUnidentifiedAccess = ByteArray(16) - for (access in unidentifiedAccess) { - joinedUnidentifiedAccess = ByteArrayUtil.xor(joinedUnidentifiedAccess, access.unidentifiedAccessKey) - } - - Base64.encodeWithPadding(joinedUnidentifiedAccess) - } + class StorySendNoop(override val senderCertificate: SenderCertificate) : SealedSenderAccess() { + override val headerName: String = "" + override val headerValue: String = "" override fun switchToFallback(): SealedSenderAccess? { return null } + + override fun applyHeader(): Boolean = false } /** @@ -122,14 +109,7 @@ sealed class SealedSenderAccess { fun create(): GroupSendFullToken? } - interface FallbackListener { - fun onAccessToTokenFallback() - fun onTokenToAccessFallback(hasAccessKeyFallback: Boolean) - } - companion object { - var fallbackListener: FallbackListener? = null - @JvmField val NONE: SealedSenderAccess? = null @@ -178,12 +158,12 @@ sealed class SealedSenderAccess { } @JvmStatic - fun forGroupSend(groupSendEndorsements: GroupSendEndorsements?, unidentifiedAccess: List, forStory: Boolean): SealedSenderAccess { - return if (groupSendEndorsements != null && !forStory) { - GroupGroupSendToken(groupSendEndorsements) - } else { - GroupUnidentifiedAccess(unidentifiedAccess) + fun forGroupSend(senderCertificate: SenderCertificate?, groupSendEndorsements: GroupSendEndorsements?, forStory: Boolean): SealedSenderAccess { + if (forStory) { + return StorySendNoop(senderCertificate!!) } + + return GroupGroupSendToken(groupSendEndorsements!!) } @JvmStatic diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/websocket/SignalWebSocket.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/websocket/SignalWebSocket.kt index f59de0eb3d..f80a9c01c1 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/websocket/SignalWebSocket.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/websocket/SignalWebSocket.kt @@ -287,7 +287,9 @@ sealed class SignalWebSocket( class UnauthenticatedWebSocket(connectionFactory: WebSocketFactory, canConnect: CanConnect, sleepTimer: SleepTimer, disconnectTimeoutMs: Long) : SignalWebSocket(connectionFactory, canConnect, sleepTimer, disconnectTimeoutMs.milliseconds) { fun request(requestMessage: WebSocketRequestMessage, sealedSenderAccess: SealedSenderAccess): Single { val headers: MutableList = requestMessage.headers.toMutableList() - headers.add(sealedSenderAccess.header) + if (sealedSenderAccess.applyHeader()) { + headers.add(sealedSenderAccess.header) + } val message = requestMessage .newBuilder()