Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/ThreemaFramework/Business/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 82 kB image not shown  

SSL ServerConnector.m  Sprache: unbekannt

 
Spracherkennung für: .m vermutete Sprache: Lisp {Lisp[51] Masm[51] ABAP[44]} [Methode: maximale Elemente, drei Dimensionen]

#include <CommonCrypto/CommonDigest.h>

#import "ThreemaFramework/ThreemaFramework-Swift.h"

#import "ServerConnector.h"
#import "NaClCrypto.h"
#import "ChatTcpSocket.h"
#import "NSString+Hex.h"
#import "BoxedMessage.h"
#import "MyIdentityStore.h"
#import "ProtocolDefines.h"
#import "ThreemaUtilityObjC.h"
#import "ContactStore.h"
#import "UserSettings.h"
#import "BundleUtil.h"
#import "AppGroup.h"
#import "LicenseStore.h"
@import Keychain;

#ifdef DEBUG
  static const DDLogLevel ddLogLevel = DDLogLevelAll;
#else
  static const DDLogLevel ddLogLevel = DDLogLevelNotice;
#endif
#define LOG_KEY_INFO 0

static const int MAX_BYTES_TO_DECRYPT_NO_LIMIT = 0;
static const int MAX_BYTES_TO_DECRYPT_NOTIFICATION_EXTENSION = 500000;

@implementation ServerConnector {
    NSData *clientTempKeyPub;
    NSData *clientTempKeySec;
    NSData *clientCookie;
    
    NSData *serverCookie;
    NSData *serverTempKeyPub;
    
    NSData *chosenServerKeyPub;
    NSData *serverKeyPub;
    NSData *serverAltKeyPub;
    NSData *tempKeyHash;
    
    dispatch_queue_t sendPushTokenQueue;
    dispatch_queue_t removeVoIPPushTokenQueue;
    BOOL isRemovedVoIPPushToken;
    
    uint64_t serverNonce;
    uint64_t clientNonce;
    dispatch_queue_t socketQueue;
    dispatch_queue_t sendMessageQueue;
    dispatch_source_t keepalive_timer;
    NSCondition *disconnectCondition;
    id<SocketProtocol> socket;
    int reconnectAttempts;

    NSMutableArray *connectionInitiators;

    ProcessCoordinator *processCoordinator;
    ServerConnectorConnectionState *serverConnectorConnectionState;

    BOOL autoReconnect;
    CFTimeInterval lastRead;
    NSDate *lastErrorDisplay;

    CFTimeInterval lastEchoSendTime;
    uint64_t lastSentEchoSeq;
    uint64_t lastRcvdEchoSeq;

    ReachabilityWrapper *reachabilityWrapper;
    
    NSMutableSet *displayedServerAlerts;
    int anotherConnectionCount;
    BOOL chatServerInInitialQueueSend;
    BOOL mediatorServerInInitialQueueSend;
    BOOL doUnblockIncomingMessages;
    BOOL isWaitingForReconnect;

    dispatch_queue_t queueConnectionStateDelegate;
    NSMutableSet *clientConnectionStateDelegates;

    dispatch_queue_t queueMessageListenerDelegate;
    NSMutableSet *clientMessageListenerDelegates;

    dispatch_queue_t queueMessageProcessorDelegate;
    id<MessageProcessorDelegate> clientMessageProcessorDelegate;
    
    dispatch_queue_t queueTaskExecutionTransactionDelegate;
    id<TaskExecutionTransactionDelegate> clientTaskExecutionTransactionDelegate;
}

@synthesize backgroundEntityManagerForMessageProcessing;
@synthesize lastRtt;
@synthesize deviceGroupKeys;
@synthesize deviceID;
@synthesize maximumNumberOfDeviceSlots;
@synthesize isAppInBackground;
@synthesize isRolePromotedToLeader;

#pragma pack(push, 1)
#pragma pack(1)

struct pktClientHello {
    unsigned char client_tempkey_pub[kNaClCryptoPubKeySize];
    unsigned char client_cookie[kCookieLen];
};

struct pktServerHelloBox {
    unsigned char server_tempkey_pub[kNaClCryptoPubKeySize];
    unsigned char client_cookie[kCookieLen];
};

struct pktServerHello {
    unsigned char server_cookie[kCookieLen];
    char box[sizeof(struct pktServerHelloBox) + kNaClBoxOverhead];
};

struct pktLogin {
    char identity[kIdentityLen];
    char client_version[kClientVersionLen];
    unsigned char server_cookie[kCookieLen];
    unsigned char reserved1[24]; // all-zero
    unsigned char vouch[kVouchLen];
    char reserved2[16]; // all-zero
};

struct pktLoginAck {
    char reserved[kLoginAckReservedLen];
};

struct pktPayload {
    uint8_t type;
    uint8_t reserved[3];
    char data[];
};

struct pktExtension {
    uint8_t type;
    uint16_t length;
    char data[];
};

#pragma pack(pop)

#define TAG_CLIENT_HELLO_SENT 1
#define TAG_SERVER_HELLO_READ 2
#define TAG_LOGIN_SENT 3
#define TAG_LOGIN_ACK_READ 4
#define TAG_PAYLOAD_SENT 5
#define TAG_PAYLOAD_LENGTH_READ 6
#define TAG_PAYLOAD_READ 7
#define TAG_PAYLOAD_MEDIATOR_TRIGGERED 8

#define EXTENSION_TYPE_CLIENT_INFO 0x00
#define EXTENSION_TYPE_DEVICE_ID 0x01
#define EXTENSION_TYPE_MESSAGE_PAYLOAD_VERSION 0x02
#define EXTENSION_TYPE_DEVICE_COOKIE 0x03

#define DUPLICATE_CONNECTION_RECONNECT_ALLOWED @"duplicateConnectionReconnectAllowed"

+ (ServerConnector*)sharedServerConnector {
    static ServerConnector *instance;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [ServerConnector new];
    });

    return instance;
}

- (id)init
{
    self = [super init];
    if (self) {
        sendPushTokenQueue = dispatch_queue_create("ch.threema.ServerConnector.sendPushTokenQueue", NULL);
        removeVoIPPushTokenQueue = dispatch_queue_create("ch.threema.ServerConnector.removeVoIPPushTokenQueue", NULL);
        isRemovedVoIPPushToken = NO;
        
        connectionInitiators = [[NSMutableArray alloc] init];

        socketQueue = dispatch_queue_create("ch.threema.ServerConnector.socketQueue", NULL);
        sendMessageQueue = dispatch_queue_create("ch.threema.ServerConnector.sendMessageQueue", NULL);
        disconnectCondition = [[NSCondition alloc] init];

        serverConnectorConnectionState = [[ServerConnectorConnectionState alloc]  initWithUserSettings:[UserSettings sharedUserSettings] connectionStateDelegate:self];
        reconnectAttempts = 0;
        lastSentEchoSeq = 0;
        lastRcvdEchoSeq = 0;

        displayedServerAlerts = [NSMutableSet set];
        
        /* register with reachability API */
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkStatusDidChange:) name:@"reachabilityChanged" object:nil];
        
        reachabilityWrapper = [[ReachabilityWrapper alloc] init];

        doUnblockIncomingMessages = YES;
        isWaitingForReconnect = NO;
        [self setIsRolePromotedToLeader:NO];

        // Listen for identity changes
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(identityDestroyed:) name:kNotificationDestroyedIdentity object:nil];

        queueConnectionStateDelegate = dispatch_queue_create("ch.threema.ServerConnector.queueConnectionStateDelegate", NULL);
        queueMessageListenerDelegate = dispatch_queue_create("ch.threema.ServerConnector.queueMessageListenerDelegate", NULL);
        queueMessageProcessorDelegate = dispatch_queue_create("ch.threema.ServerConnector.queueMessageProcessorDelegate", NULL);
        queueTaskExecutionTransactionDelegate = dispatch_queue_create("ch.threema.ServerConnector.queueTaskExecutionTransactionDelegate", NULL);

        if ([[UserSettings sharedUserSettings] ipcCommunicationEnabled]) {
            processCoordinator = [[ProcessCoordinator alloc] initWithServerConnector:self userSettings:[UserSettings sharedUserSettings]];
        }
    }
    return self;
}

#pragma mark - Register/unregister delegates for Message Listener, Message Processor and Task Manager transaction

- (void)registerConnectionStateDelegate:(id<ConnectionStateDelegate>)delegate {
    dispatch_sync(queueConnectionStateDelegate, ^{
        if (clientConnectionStateDelegates == nil) {
            clientConnectionStateDelegates = [NSMutableSet new];
        }
        if ([clientConnectionStateDelegates containsObject:delegate] == NO) {
            [clientConnectionStateDelegates addObject:delegate];
        }
    });
}

- (void)unregisterConnectionStateDelegate:(id<ConnectionStateDelegate>)delegate {
    dispatch_sync(queueConnectionStateDelegate, ^{
        if (clientConnectionStateDelegates != nil && [clientConnectionStateDelegates containsObject:delegate] == YES) {
            [clientConnectionStateDelegates removeObject:delegate];
        }
    });
}

- (void)registerMessageListenerDelegate:(id<MessageListenerDelegate>)delegate {
    dispatch_sync(queueMessageListenerDelegate, ^{
        if (clientMessageListenerDelegates == nil) {
            clientMessageListenerDelegates = [NSMutableSet new];
        }
        if ([clientMessageListenerDelegates containsObject:delegate] == NO) {
            [clientMessageListenerDelegates addObject:delegate];
        }
    });
}

- (void)unregisterMessageListenerDelegate:(id<MessageListenerDelegate>)delegate {
    dispatch_sync(queueMessageListenerDelegate, ^{
        if (clientMessageListenerDelegates != nil && [clientMessageListenerDelegates containsObject:delegate] == YES) {
            [clientMessageListenerDelegates removeObject:delegate];
        }
    });
}

- (void)registerMessageProcessorDelegate:(id<MessageProcessorDelegate>)delegate {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        if (delegate != nil) {
            clientMessageProcessorDelegate = delegate;
        }
    });
}

- (void)unregisterMessageProcessorDelegate:(id<MessageProcessorDelegate>)delegate {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        if ([delegate isEqual:clientMessageProcessorDelegate]) {
            clientMessageProcessorDelegate = nil;
        }
    });
}

- (void)registerTaskExecutionTransactionDelegate:(id<TaskExecutionTransactionDelegate>)delegate {
    dispatch_sync(queueTaskExecutionTransactionDelegate, ^{
        if (delegate != nil) {
            clientTaskExecutionTransactionDelegate = delegate;
        }
    });
}

- (void)unregisterTaskExecutionTransactionDelegate:(id<TaskExecutionTransactionDelegate>)delegate {
    dispatch_sync(queueTaskExecutionTransactionDelegate, ^{
        if ([delegate isEqual:clientTaskExecutionTransactionDelegate]) {
            clientTaskExecutionTransactionDelegate = nil;
        }
    });
}

#pragma mark - Chat (Mediator) Server connection handling

/// If another process is active then the timeout will be set to 4900 ms otherwise to 700 ms.
///
/// @return The timeout in milliseconds
- (int)processCoordinatorPollingTimeout {
    if (AppGroup.areOthersActive) {
        return 4900;
    }
    return 700;
}

/// Check of duplicate connection error of the share or notification extension.
/// Show duplicate connection alert is it called by the app.
///
/// @return Is reconnection allowed, is always true is it called by the extension
- (BOOL)checkDuplicateConnectionInExtension {
    BOOL isReconnectAllowed = YES;

    if (AppGroup.getCurrentType != AppGroupTypeApp) {
        return isReconnectAllowed;
    }

    NSUserDefaults *userDefaults = AppGroup.userDefaults;
    id duplicateConnectionReconnectAllowed = [userDefaults valueForKey:DUPLICATE_CONNECTION_RECONNECT_ALLOWED];
    if (duplicateConnectionReconnectAllowed) {
        [userDefaults removeObjectForKey:DUPLICATE_CONNECTION_RECONNECT_ALLOWED];

        isReconnectAllowed = ((NSNumber *)duplicateConnectionReconnectAllowed).boolValue;
        [self showAnotherConnectionAlertWithReconnectAllowed:isReconnectAllowed];
    }

    return isReconnectAllowed;
}

- (void)connect:(ConnectionInitiator)initiator onCompletion:(void(^ _Nullable)(BOOL))onCompletion {
    if (![self checkDuplicateConnectionInExtension]) {
        return;
    }

    if ([[UserSettings sharedUserSettings] ipcCommunicationEnabled]) {
        int timeout = [self processCoordinatorPollingTimeout];

        DDLogNotice(@"[Darwin] Connect (timeout: %i)", timeout);
        [processCoordinator requestAccessWithPollingTimeout:timeout completionHandler:^(enum AccessState state) {
            switch (state) {
                case AccessStateUnused:
                    if ([AppGroup getCurrentType] == AppGroupTypeApp) {
                        DDLogNotice(@"[Darwin] Resource is unused (App), request access again");
                        [self connect:initiator onCompletion:onCompletion];
                    }
                    else {
                        DDLogNotice(@"[Darwin] Resource is unused (NOT App), disconnecting from server");
                        [self _disconnect];
                        if (onCompletion) {
                            onCompletion(NO);
                        }
                    }
                    break;
                case AccessStateRequested:
                    DDLogNotice(@"[Darwin] Resource is requested, request access again");
                    [self connect:initiator onCompletion:onCompletion];
                    break;
                case AccessStateUsing:
                    DDLogNotice(@"[Darwin] Resource is using, connecting to server");

                    [self _connectWithConnectionInitiator:initiator];
                    if (onCompletion) {
                        onCompletion(YES);
                    }
                    break;
                case AccessStateWillRelease:
                    DDLogNotice(@"[Darwin] Resource will release, disconnecting from server");
                    if (AppGroup.getCurrentType == AppGroupTypeApp) {
                        // This is happen when the Share Extension and the App is active at the same time,
                        // e.g. when sharing Debug_log via Share Extension.
                        // In this case disconnect immediately and wait a little time to reconnect.
                        [self _disconnectAndReconnectAfterDelay];
                    }
                    else {
                        [self _disconnect];
                    }
                    if (onCompletion) {
                        onCompletion(NO);
                    }
                    break;
                default:
                    DDLogError(@"[Darwin] Unknown access state %@, do not connect", [ProcessCoordinator nameForAccessState:state]);
                    if (onCompletion) {
                        onCompletion(NO);
                    }
                    break;
            }
        }];
    }
    else {
        [self _connectWithConnectionInitiator:initiator];
        if (onCompletion) {
            onCompletion(YES);
        }
    }
}

- (void)_connectWithConnectionInitiator:(ConnectionInitiator)initiator {
    if (!doUnblockIncomingMessages) {
        DDLogNotice(@"Cannot connect - incoming messages are blocked");
        return;
    }
    
    dispatch_async(socketQueue, ^{
        // TODO: Remove comment IOS-3558
        DDLogNotice(@"Entered socket queue in connect.");
        [self connectBy:initiator];
        // TODO: Remove comment IOS-3558
        DDLogNotice(@"ConnectBy finished.");

        lastErrorDisplay = nil;
        [self _connect];
        // TODO: Remove comment IOS-3558
        DDLogNotice(@"Left socket queue in connect.");
    });
}

- (void)connectWait:(ConnectionInitiator)initiator {
    if (![self checkDuplicateConnectionInExtension]) {
        return;
    }

    if ([[UserSettings sharedUserSettings] ipcCommunicationEnabled]) {
        int timeout = [self processCoordinatorPollingTimeout];

        DDLogNotice(@"[Darwin] Connect wait (timeout: %i)", timeout);
        [processCoordinator requestAccessWithPollingTimeout:timeout completionHandler:^(enum AccessState state) {
            switch (state) {
                case AccessStateUnused:
                    if ([AppGroup getCurrentType] == AppGroupTypeApp) {
                        DDLogNotice(@"[Darwin] Resource is unused (App), request access again");
                        [self connectWait:initiator];
                    }
                    else {
                        DDLogNotice(@"[Darwin] Resource is unused (NOT App), disconnecting from server");
                        [self _disconnect];
                    }
                    break;
                case AccessStateRequested:
                    DDLogNotice(@"[Darwin] Resource is requested, request access again");
                    [self connectWait:initiator];
                    break;
                case AccessStateUsing:
                    DDLogNotice(@"[Darwin] Resource is using, connecting to server");

                    [self _connectWaitWithConnectionInitiator:initiator];
                    break;
                case AccessStateWillRelease:
                    DDLogNotice(@"[Darwin] Resource will release, disconnecting from server");
                    if (AppGroup.getCurrentType == AppGroupTypeApp) {
                        // This is happen when the Share Extension and the App is active at the same time,
                        // e.g. when sharing Debug_log via Share Extension.
                        // In this case disconnect immediately and wait a little time to reconnect.
                        [self _disconnectAndReconnectAfterDelay];
                    }
                    else {
                        [self _disconnect];
                    }
                    break;
                default:
                    DDLogError(@"[Darwin] Unknown access state %@, do not connect", [ProcessCoordinator nameForAccessState:state]);
                    break;
            }
        }];
    }
    else {
        [self _connectWaitWithConnectionInitiator:initiator];
    }
}

- (void)_connectWaitWithConnectionInitiator:(ConnectionInitiator)initiator {
    if (!doUnblockIncomingMessages) {
        DDLogNotice(@"Cannot connect - incoming messages are blocked");
        return;
    }
    
    dispatch_sync(socketQueue, ^{
        // TODO: Remove comment IOS-3558
        DDLogNotice(@"Entered socket queue in connectWait.");
        [self connectBy:initiator];
        // TODO: Remove comment IOS-3558
        DDLogNotice(@"ConnectBy finished.");

        lastErrorDisplay = nil;
        [self _connect];
        // TODO: Remove comment IOS-3558
        DDLogNotice(@"Left socket queue in connectWait.");
    });
}

- (void)connectWaitDoNotUnblockIncomingMessages:(ConnectionInitiator)initiator {
    doUnblockIncomingMessages = NO;

    dispatch_sync(socketQueue, ^{
        [self connectBy:initiator];

        lastErrorDisplay = nil;
        [self _connect];
    });
}

- (void)unblockIncomingMessages {
    BOOL blockedBefore = (doUnblockIncomingMessages == NO);
    doUnblockIncomingMessages = YES;
    
    dispatch_sync(socketQueue, ^{
        if (blockedBefore && self.connectionState == ConnectionStateLoggedIn) {
            [self _unblockIncomingMessagesIfAllowed];
        }
    });
}

- (void)connectWithCurrentAppGroupType {
    ConnectionInitiator initiator;

    switch ([AppGroup getCurrentType]) {
        case AppGroupTypeApp:
        case AppGroupTypeNone:
            initiator = ConnectionInitiatorApp;
            break;
        case AppGroupTypeShareExtension:
            initiator = ConnectionInitiatorShareExtension;
            break;
        case AppGroupTypeNotificationExtension:
            initiator = ConnectionInitiatorNotificationExtension;
            break;
    }

    [self connect:initiator onCompletion:nil];
}

- (void)_connect {
    // TODO: Remove comment IOS-3558
    DDLogNotice(@"Connect began.");
    if (ProcessInfoHelper.isRunningForScreenshots)  {
        return;
    }

    if (!AppSetup.isIdentityProvisioned) {
        DDLogNotice(@"Cannot connect - identity not provisioned");
        return;
    }

    if (self.connectionState == ConnectionStateDisconnecting) {
        // The socket does not always report its disconnect and the state then remains on `disconnecting` forever.
        // Assume the callback is lost when the state is expired and connect anyway.
        if ([serverConnectorConnectionState isStateExpired]) {
            DDLogWarn(@"Connect: No disconnect reported, assume the socket is disconnected");
        }
        else {
            DDLogNotice(@"Connect: State was disconnecting.");

            if ([AppGroup getCurrentType] != AppGroupTypeNotificationExtension) {
                // The socketDidDisconnect callback has not been called yet; ensure that we reconnect
                // as soon as the previous disconnect has finished.
                autoReconnect = YES;
                [self reconnectAfterDelay];
                return;
            }
        }
    } else if (self.connectionState != ConnectionStateDisconnected) {
        if (![serverConnectorConnectionState isStateExpired]) {
            DDLogNotice(@"Abort connect, because connection state is %@", [self nameForConnectionState:self.connectionState]);
            return;
        }

        DDLogWarn(@"Connection state is %@ for too long, force disconnect", [self nameForConnectionState:self.connectionState]);

        // The state `disconnected` is set after the successful disconnect, otherwise another process will connect
        // too early and it could end in a duplicate connection.
        [serverConnectorConnectionState disconnecting];
        [socket disconnect];

        if ([AppGroup getCurrentType] != AppGroupTypeNotificationExtension) {
            // Reconnect as soon as the disconnect has finished
            autoReconnect = YES;
            [self reconnectAfterDelay];
            return;
        }
    }

    if ([[UserSettings sharedUserSettings] ipcCommunicationEnabled] == NO && [AppGroup amIActive] == NO) {
        if ([AppGroup areOthersActive] == NO) {
            DDLogNotice(@"Not active -> there are no other processes active, set me as active");
            [AppGroup setMeActive];
        }
        else {
            if ([AppGroup getCurrentType] != AppGroupTypeNotificationExtension) {
                DDLogNotice(@"Not active -> don't connect now, retry later");
                // keep delay at constant rate to avoid too long waits when becoming active again
                reconnectAttempts = 1;
                autoReconnect = YES;
                [self reconnectAfterDelay];
            }
            else {
                DDLogNotice(@"Not active -> disconnect now");
                [self _disconnect];
            }
            return;
        }
    }

    LicenseStore *licenseStore = [LicenseStore sharedLicenseStore];
    if ([licenseStore isValid] == NO) {
        // TODO: Remove comment IOS-3558
        DDLogNotice(@"LicenseStore was invalid.");
        [licenseStore performLicenseCheckWithCompletion:^(BOOL success) {
            // TODO: Remove comment IOS-3558
            DDLogNotice(@"LicenseStore perform check.");
            if (success) {
                [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationLicenseCheckSuccess object:nil];
                [self _connect];
            } else {
                // don't show license warning for connection errors
                DDLogNotice(@"License check failed: %@", licenseStore.error);
                if ([licenseStore.error.domain hasPrefix:@"NSURL"] == NO && licenseStore.error.code != 256) {                    
                    // License check failed permanently; need to inform user and ask for new license username/password
                    [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationLicenseMissing object:nil];
                }
                else if ([licenseStore.error.domain hasPrefix:@"NSURL"] == YES && licenseStore.error.code == -1009 && ![[LicenseStore sharedLicenseStore] isWithinOfflineInterval]) {
                    // License check failed because we don't have network connection. WithinCheckInterval for license failed, show license screen
                    [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationLicenseMissing object:nil];
                }
                else {
                    // License check failed due to connection error – try again later
                    // TODO: Remove comment IOS-3558
                    DDLogNotice(@"LicenseStore failed, reconnecting after delay.");
                    autoReconnect = YES;
                    [self reconnectAfterDelay];
                }
            }
        }];
        
        return;
    }

    [licenseStore performUpdateWorkInfo];

    [serverConnectorConnectionState connecting];
    autoReconnect = YES;
    self.lastRtt = -1;
    lastRead = CACurrentMediaTime();
    chatServerInInitialQueueSend = YES;
    mediatorServerInInitialQueueSend = YES;
    
    /* Reset nonces for new connection */
    clientNonce = 1;
    serverNonce = 1;
    
    /* Generate a new ephemeral key pair for the server connection. */
    NSData *publicKey, *secretKey;
    [[NaClCrypto sharedCrypto] generateKeyPairPublicKey:&publicKey secretKey:&secretKey];
    clientTempKeyPub = publicKey;
    clientTempKeySec = secretKey;
#if LOG_KEY_INFO
    DDLogVerbose(@"Client tempkey_pub = %@, tempkey_sec = %@", clientTempKeyPub, clientTempKeySec);
#endif

    DeviceGroupKeyManager *deviceGroupKeyManager = [DeviceGroupKeyManager new];
    NSData *dgk = deviceGroupKeyManager.dgk;
    if (!dgk) {
        DDLogNotice(@"Connect direct.");
        // TODO: Remove comment IOS-3558
        [self _connectDirect];
    } else {
        DDLogNotice(@"Connect mediator.");
        // TODO: Remove comment IOS-3558
        [self _connectViaMediator:dgk];
    }
}

- (void)_connectDirect {
    // TODO: Remove comment IOS-3558
    DDLogNotice(@"Connect direct started.");
    // Multi device is not activated, reset device group keys and device ID
    @synchronized (deviceGroupKeys) {
        deviceGroupKeys = nil;
        deviceID = nil;
    };

    // Obtain chat server host/ports/keys from ServerInfoProvider
    [[ServerInfoProviderFactory makeServerInfoProvider] chatServerWithIpv6:[UserSettings sharedUserSettings].enableIPv6 completionHandler:^(ChatServerInfo * _Nullable chatServerInfo, NSError *error) {
        // TODO: Remove comment IOS-3558
        DDLogNotice(@"ChatServerWithIpv6 started.");
        dispatch_async(socketQueue, ^{
            // TODO: Remove comment IOS-3558
            DDLogNotice(@"Connect direct socket queue entered.");
            if (chatServerInfo == nil) {
                // Could not get the info at the moment; try again later
                [serverConnectorConnectionState disconnected];
                [self reconnectAfterDelay];
                return;
            }
            
            NSString *serverHost;
            if (chatServerInfo.useServerGroups) {
                serverHost = [NSString stringWithFormat:@"%@%@%@", chatServerInfo.serverNamePrefix, [MyIdentityStore sharedMyIdentityStore].serverGroup, chatServerInfo.serverNameSuffix];
            } else {
                serverHost = chatServerInfo.serverNameSuffix;
            }
            
            serverKeyPub = chatServerInfo.publicKey;
            serverAltKeyPub = chatServerInfo.publicKeyAlt;
            
            UserSettings *settings = [UserSettings sharedUserSettings];

            NSError *socketError;
            socket = [[ChatTcpSocket alloc] initWithServer:serverHost ports:chatServerInfo.serverPorts preferIPv6:settings.enableIPv6 delegate:self queue:socketQueue error:&socketError]='color:green'>;
            
            if (socketError != nil || ![socket connect]) {
                // TODO: Remove comment IOS-3558
                DDLogNotice(@"Connect direct error.");
                [serverConnectorConnectionState disconnected];
                [self reconnectAfterDelay];
                return;
            }
            // TODO: Remove comment IOS-3558
            DDLogNotice(@"Connect direct socket queue left.");
        });
    }];
}

- (void)_connectViaMediator:(nonnull NSData *)dgk {
    // Derive multi device keys
    NSError *deriveKeyError;
    DeviceGroupDerivedKey *deviceGroupDerivedKey = [[DeviceGroupDerivedKey alloc] initWithDgk:dgk error:&deriveKeyError];

    if (deriveKeyError) {
        DDLogError(@"Device Group Keys could not be derived");
        [serverConnectorConnectionState disconnected];
        [self reconnectAfterDelay];
        return;
    }

    // Multi device is activated, check device ID
    NSData *deviceGroupID;

    @synchronized (deviceGroupKeys) {
        deviceGroupID = [[NaClCrypto sharedCrypto] derivePublicKeyFromSecretKey:deviceGroupDerivedKey.dgpk];

        const unsigned char *deviceGroupIDBytes = [deviceGroupID bytes];
        NSString *deviceGroupIDFirstByteHex = [NSString stringWithFormat:@"%02lx", (unsigned long)deviceGroupIDBytes[0]];

        NSLog(@"%@", [NSString stringWithHexData:deviceGroupID]);
        deviceGroupKeys = [[DeviceGroupKeys alloc] initWithDgpk:deviceGroupDerivedKey.dgpk dgrk:deviceGroupDerivedKey.dgrk dgdik:deviceGroupDerivedKey.dgdik dgsddk:deviceGroupDerivedKey.dgsddk dgtsk:deviceGroupDerivedKey.dgtsk deviceGroupIDFirstByteHex:deviceGroupIDFirstByteHex];

        KeychainManager *keychainManager = [[BusinessInjector ui] keychainManagerObjC];
        deviceID = [keychainManager loadMultiDeviceIDObjC];

        if (deviceID == nil || [deviceID length] != kDeviceIdLen) {
            deviceID = [NSData dataWithBytes:[[NaClCrypto sharedCrypto] randomBytes:kDeviceIdLen].bytes length:kDeviceIdLen];

            NSError *error = nil;
            [keychainManager storeMultiDeviceIDWithId:deviceID error:&error];
            if (error) {
                NSString *errorMessage = [NSString stringWithFormat:@"Couldn't store device ID in Keychain: %@", error.localizedDescription];
                DDLogError(@"%@", errorMessage);
                [NSException raise:error.domain.description format:@"%@", errorMessage];
            }
        }
    };

    NSAssert([deviceID length] == kDeviceIdLen, @"Device ID has wrong length");

    NSString *clientUrlInfo = [MediatorMessageProtocol encodeClientURLInfoWithDgpkPublicKey:deviceGroupID serverGroup:[MyIdentityStore sharedMyIdentityStore].serverGroup];

    id<ServerInfoProvider> serverInfoProvider = [ServerInfoProviderFactory makeServerInfoProvider];
    [serverInfoProvider mediatorServerWithDeviceGroupIDFirstByteHex: deviceGroupKeys.deviceGroupIDFirstByteHex completionHandler:^(MediatorServerInfo * _Nullable mediatorServerInfo, NSError * _Nullable mediatorServerError) {
        // Obtain chat server info too (for public keys)
        [serverInfoProvider chatServerWithIpv6:[UserSettings sharedUserSettings].enableIPv6 completionHandler:^(ChatServerInfo * _Nullable chatServerInfo, NSError * _Nullable chatServerError) {
            if (mediatorServerInfo == nil || chatServerInfo == nil) {
                // Could not get the info at the moment; try again later
                [serverConnectorConnectionState disconnected];
                [self reconnectAfterDelay];
                return;
            }
            
            NSString *server = [NSString stringWithFormat:@"%@/%@", mediatorServerInfo.url, clientUrlInfo];
            serverKeyPub = chatServerInfo.publicKey;
            serverAltKeyPub = chatServerInfo.publicKeyAlt;

            UserSettings *settings = [UserSettings sharedUserSettings];
            NSError *socketError;
            socket = [[MediatorWebSocket alloc] initWithServer:server ports:@[] preferIPv6:settings.enableIPv6 delegate:self queue:socketQueue error:&socketError];
            
            if (socketError != nil || ![socket connect]) {
                [serverConnectorConnectionState disconnected];
                [self reconnectAfterDelay];
                return;
            }
        }];
    }];
}

- (void)_disconnect {
    if ([serverConnectorConnectionState connectionState] == ConnectionStateDisconnected) {
        return;
    }
    
    /* disconnect socket and make sure we don't reconnect */
    autoReconnect = NO;
    [serverConnectorConnectionState disconnecting];
    [socket disconnect];
}

/**
 Disconnect anyway and reconnect after a little delay
 */

- (void)_disconnectAndReconnectAfterDelay {
    [self _disconnect];

    // Keep delay at constant rate to avoid too long waits when becoming active again
    reconnectAttempts = 1;
    autoReconnect = YES;
    [self reconnectAfterDelay];
}

- (void)disconnect:(ConnectionInitiator)initiator {
    [self disconnect:initiator force:NO];
}

- (void)disconnect:(ConnectionInitiator)initiator force:(BOOL)force {
    dispatch_async(socketQueue, ^{
        if ([self isConnectionStillRequestedAfterDisconnectBy:initiator] == NO || force == YES) {
            [self _disconnect];
        }
    });
}

- (BOOL)disconnectWait:(ConnectionInitiator)initiator {
    __block BOOL isDisconnected = NO;

    dispatch_sync(socketQueue, ^{
        if ([self isConnectionStillRequestedAfterDisconnectBy:initiator] == NO) {
            [self _disconnect];
            isDisconnected = YES;
        }
    });

    if (isDisconnected) {
        [serverConnectorConnectionState waitForStateDisconnected];
    }

    return isDisconnected;
}

- (void)reconnect {
    dispatch_async(socketQueue, ^{
        if ([serverConnectorConnectionState connectionState] == ConnectionStateDisconnected) {
            [self connectWithCurrentAppGroupType];
        } else if ([serverConnectorConnectionState connectionState] == ConnectionStateConnecting) {
            DDLogVerbose(@"Connection already in progress, not reconnecting");
        } else {
            autoReconnect = YES;
            [serverConnectorConnectionState disconnecting];
            [socket disconnect];
        }
    });
}

- (ConnectionState)connectionState {
    return [serverConnectorConnectionState connectionState];
}

#pragma mark - Chat (Mediator) Server connection initiator handling

- (void)connectBy:(ConnectionInitiator)initiator {
    if (![connectionInitiators containsObject:[NSNumber numberWithInteger:initiator]]) {
        [connectionInitiators addObject:[NSNumber numberWithInteger:initiator]];
    }

    DDLogNotice(@"[ConnectionInitiator] Connect requested by %@, connection is requested by: %@", [self nameForConnectionInitiator:initiator], [self namesForConnectionInitiators]);
}

- (BOOL)isConnectionStillRequestedAfterDisconnectBy:(ConnectionInitiator)initiator {
    [connectionInitiators removeObject:[NSNumber numberWithInteger:initiator]];

    if ([connectionInitiators count] != 0) {
        DDLogNotice(@"[ConnectionInitiator] Disconnect requested by %@, but connection is still requested by: %@", [self nameForConnectionInitiator:initiator], [self namesForConnectionInitiators]);
        return YES;
    }

    DDLogNotice(@"[ConnectionInitiator] Disconnect requested by %@, connection is requested by nobody anymore, disconnect now", [self nameForConnectionInitiator:initiator]);
    return NO;
}

- (NSString *)namesForConnectionInitiators {
    NSMutableString *names = [NSMutableString new];

    for (NSNumber *item in connectionInitiators) {
        if ([names length] > 0) {
            [names appendString:@", "];
        }
        [names appendString:[self nameForConnectionInitiator:(ConnectionInitiator)[item intValue]]];
    }

    return names;
}

- (NSString *)nameForConnectionInitiator:(ConnectionInitiator)initiator {
    switch (initiator) {
        case ConnectionInitiatorApp:
            return @"App";
        case ConnectionInitiatorNotificationExtension:
            return @"NotificationExtension";
        case ConnectionInitiatorNotificationHandler:
            return @"NotificationHandler";
        case ConnectionInitiatorShareExtension:
            return @"ShareExtension";
        case ConnectionInitiatorThreemaCall:
            return @"ThreemaCall";
        case ConnectionInitiatorThreemaWeb:
            return @"ThreemaWeb";
        default:
            return nil;
    }
}

#pragma mark - Processing incoming payloads

- (void)processPayload:(struct pktPayload*)pl datalen:(int)datalen {
    
    switch (pl->type) {
        case PLTYPE_ECHO_RESPONSE: {
            self.lastRtt = CACurrentMediaTime() - lastEchoSendTime;
            if (datalen == sizeof(lastRcvdEchoSeq)) {
                memcpy(&lastRcvdEchoSeq, pl->data, sizeof(lastRcvdEchoSeq));
            } else {
                DDLogError(@"Bad echo reply datalen %d", datalen);
                [socket disconnect];
                break;
            }
            DDLogInfo(@"Received echo reply (seq %llu, RTT %.1f ms)", lastRcvdEchoSeq, self.lastRtt * 1000);
            break;
        }
        case PLTYPE_ERROR: {
            if (datalen < sizeof(struct plError)) {
                DDLogError(@"Bad error payload datalen %d", datalen);
                [socket disconnect];
                break;
            }
            struct plError *plerr = (struct plError*)pl->data;
            NSData *errorMessageData = [NSData dataWithBytes:plerr->err_message length:datalen - sizeof(struct plError)];
            NSString *errorMessage = [[NSString alloc] initWithData:errorMessageData encoding:NSUTF8StringEncoding];
            DDLogError(@"Received error message from server: %@", errorMessage);

            if ([errorMessage rangeOfString:@"Another connection"].location != NSNotFound) {
                [self showAnotherConnectionAlertWithReconnectAllowed:plerr->reconnect_allowed ? YES : NO];
            }
            else {
                if (!plerr->reconnect_allowed) {
                    autoReconnect = NO;
                }

                NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys: errorMessage, kKeyMessage, nil];
                [self showConnectionAlertWithInfo:info];
            }

            break;
        }
        case PLTYPE_ALERT: {
            NSData *alertData = [NSData dataWithBytes:pl->data length:datalen];
            NSString *alertText = [[NSString alloc] initWithData:alertData encoding:NSUTF8StringEncoding];
            [self displayServerAlert:alertText];
            break;
        }
        case PLTYPE_OUTGOING_MESSAGE_ACK: {
            if (datalen != sizeof(struct plMessageAck)) {
                DDLogError(@"Bad ACK payload datalen %d", datalen);
                [socket disconnect];
                break;
            }
            
            struct plOutgoingMessageAck *ack = (struct plOutgoingMessageAck*)pl->data;
            NSData *messageId = [NSData dataWithBytes:ack->message_id length:kMessageIdLen];
            NSString *toIdentity = [[NSString alloc] initWithData:[NSData dataWithBytes:ack->to_identity length:kIdentityLen] encoding:NSASCIIStringEncoding];
            [[NSNotificationCenter defaultCenter] postNotificationName:[TaskManager chatMessageAckObserverNameWithMessageID:messageId toIdentity:toIdentity] object:nil];
            break;
        }
        case PLTYPE_INCOMING_MESSAGE: {
            if (datalen <= sizeof(struct plMessage)) {
                DDLogError(@"Bad message payload datalen %d", datalen);
                [socket disconnect];
                break;
            }
            
            if (((![[UserSettings sharedUserSettings] ipcCommunicationEnabled] && [AppGroup amIActive])
                  || [[UserSettings sharedUserSettings] ipcCommunicationEnabled])
                 && [AppGroup getCurrentType] != AppGroupTypeShareExtension) {
                struct plMessage *plmsg = (struct plMessage*)pl->data;
                int minlen = (sizeof(struct plMessage) + kNonceLen + plmsg->metadata_len + kNaClBoxOverhead + 1);
                if (datalen <= minlen || (plmsg->metadata_len > 0 && plmsg->metadata_len <= kNaClBoxOverhead)) {
                    DDLogError(@"Bad message payload datalen %d, metadata_len %d", datalen, plmsg->metadata_len);
                    [socket disconnect];
                    break;
                }
                
                BoxedMessage *boxmsg = [[BoxedMessage alloc] init];
                boxmsg.fromIdentity = [[NSString alloc] initWithData:[NSData dataWithBytes:plmsg->from_identity length:kIdentityLen] encoding:NSASCIIStringEncoding];
                boxmsg.toIdentity = [[NSString alloc] initWithData:[NSData dataWithBytes:plmsg->to_identity length:kIdentityLen] encoding:NSASCIIStringEncoding];
                boxmsg.messageId = [NSData dataWithBytes:plmsg->message_id length:kMessageIdLen];
                boxmsg.date = [NSDate dateWithTimeIntervalSince1970:plmsg->date];
                boxmsg.flags = plmsg->flags;
                char pushFromNameT[kPushFromNameLen+1];
                memcpy(pushFromNameT, plmsg->push_from_name, kPushFromNameLen);
                pushFromNameT[kPushFromNameLen] = 0;
                boxmsg.pushFromName = [NSString stringWithCString:pushFromNameT encoding:NSUTF8StringEncoding];
                if (plmsg->metadata_len > 0) {
                    boxmsg.metadataBox = [NSData dataWithBytes:&plmsg->metadata_nonce_box length:plmsg->metadata_len];
                }
                boxmsg.nonce = [NSData dataWithBytes:&plmsg->metadata_nonce_box[plmsg->metadata_len] length:kNonceLen];
                boxmsg.box = [NSData dataWithBytes:&plmsg->metadata_nonce_box[plmsg->metadata_len + kNonceLen] length:(datalen - sizeof(struct plMessage) - kNonceLen - plmsg->metadata_len)];

                // Set time out for downloading thumbnail to 5s, if the app in background or notification extension is running
                int timeoutDownloadThumbnail = isAppInBackground || [AppGroup getCurrentType] == AppGroupTypeNotificationExtension ? 5 : 0;

                TaskDefinitionReceiveMessage *task = [[TaskDefinitionReceiveMessage alloc] initWithMessage:boxmsg receivedAfterInitialQueueSend:!chatServerInInitialQueueSend maxBytesToDecrypt:[AppGroup getCurrentType] != AppGroupTypeNotificationExtension ? MAX_BYTES_TO_DECRYPT_NO_LIMIT : MAX_BYTES_TO_DECRYPT_NOTIFICATION_EXTENSION timeoutDownloadThumbnail:timeoutDownloadThumbnail];

                // Use `[self backgroundEntityManagerForMessageProcessing]` if is not nil (properly setted from Notification Extension), otherwise nil (means will be created within TaskManager) for in App processing
                TaskManager *tm = [[TaskManager alloc] initWithBackgroundEntityManager:[self backgroundEntityManagerForMessageProcessing] serverConnector:self];
                [tm addObjcWithTaskDefinition:task];
            }
            break;
        }
        case PLTYPE_QUEUE_SEND_COMPLETE:
            DDLogInfo(@"Queue send complete");
            chatServerInInitialQueueSend = NO;
            
            [self chatQueueDry];
            break;
        case PLTYPE_DEVICE_COOKIE_CHANGE_INDICATION: {
            DDLogWarn(@"Got device cookie change indication");
            if ([DeviceCookieManager changeIndicationReceived]) {
                [self clearDeviceCookieChangedIndicator];
            }
            break;
        }
        default:
            DDLogWarn(@"Unsupported payload type %d", pl->type);
            break;
    }
}

- (void)completedProcessingAbstractMessage:(AbstractMessage *)msg {
    if (!(msg.flags.intValue & MESSAGE_FLAG_DONT_ACK)) {
        /* send ACK to server */
        [self ackMessage:msg.messageId fromIdentity:msg.fromIdentity];
    }
}

- (BOOL)completedProcessingMessage:(BoxedMessage *)boxmsg {
    if (!(boxmsg.flags & MESSAGE_FLAG_DONT_ACK)) {
        /* send ACK to server */
        return [self ackMessage:boxmsg.messageId fromIdentity:boxmsg.fromIdentity];
    }
    return YES;
}

- (void)reconnectAfterDelay {
    // Never reconnect for the notification extension
    if (!autoReconnect || [AppGroup getCurrentType] == AppGroupTypeNotificationExtension) {
        return;
    }
    
    /* calculate delay using bound exponential backoff */
    float reconnectDelay = powf(kReconnectBaseInterval, MIN(reconnectAttempts - 1, 10));
    if (reconnectDelay > kReconnectMaxInterval) {
        reconnectDelay = kReconnectMaxInterval;
    }
    
    if (!isWaitingForReconnect) {
        isWaitingForReconnect = true;
        reconnectAttempts++;
        DDLogNotice(@"Waiting %f seconds before reconnecting", reconnectDelay);
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, reconnectDelay * NSEC_PER_SEC);
        dispatch_after(popTime, socketQueue, ^(void){
            isWaitingForReconnect = false;
            [self connectWithCurrentAppGroupType];
        });
    }
}

- (BOOL)sendPayloadWithType:(uint8_t)type data:(NSData*)data {
    if ([serverConnectorConnectionState connectionState] != ConnectionStateLoggedIn) {
        DDLogVerbose(@"Cannot send payload - not logged in");
        return NO;
    }
    
    /* Make encrypted box */
    unsigned long pllen = sizeof(struct pktPayload) + data.length;
    struct pktPayload *pl = malloc(pllen);
    if (!pl) {
        return NO;
    }
    
    memset(pl, 0, pllen);
    
    pl->type = type;
    memcpy(pl->data, data.bytes, data.length);
    
    NSData *plData = [NSData dataWithBytesNoCopy:pl length:pllen];
    
    __block BOOL isSent = NO;
    
    // Gets next client nonce not before the message was sent
    dispatch_barrier_sync(sendMessageQueue, ^{
        NSData *nextClientNonce = [self nextClientNonce];
        NSData *plBox = [[NaClCrypto sharedCrypto] encryptData:plData withPublicKey:serverTempKeyPub signKey:clientTempKeySec nonce:nextClientNonce];
        if (plBox == nil) {
            DDLogError(@"Payload encryption failed!");
            isSent = NO;
        }
        else {
            /* prepend length - make one NSData object to pass to socket to ensure it is sent
               in a single TCP segment */

            uint16_t pktlen = plBox.length;
            
            if (pktlen > kMaxPktLen) {
                DDLogError(@"Packet is too big (%d) - cannot send", pktlen);
                isSent = NO;
            }
            else {
                NSMutableData *sendData = [NSMutableData dataWithCapacity:plBox.length + sizeof(uint16_t)];
                [sendData appendBytes:&pktlen length:sizeof(uint16_t)];
                [sendData appendData:plBox];
                
                [socket writeWithData:sendData tag:TAG_PAYLOAD_SENT];
                
                isSent = YES;
            }
        }
    });
    
    return isSent;
}

- (BOOL)sendMessage:(BoxedMessage*)message {
    unsigned long msglen = sizeof(struct plMessage) + message.metadataBox.length + message.nonce.length + message.box.length;
    struct plMessage *plmsg = malloc(msglen);
    if (!plmsg) {
        return NO;
    }
    
    DDLogInfo(@"Sending message from %@ to %@ (ID %@), metadata box length %lu, box length %lu", message.fromIdentity,
          message.toIdentity, message.messageId, (unsigned long)message.metadataBox.length, (unsigned long)message.box.length);
    
    memcpy(plmsg->from_identity, [message.fromIdentity dataUsingEncoding:NSASCIIStringEncoding].bytes, kIdentityLen);
    memcpy(plmsg->to_identity, [message.toIdentity dataUsingEncoding:NSASCIIStringEncoding].bytes, kIdentityLen);
    memcpy(plmsg->message_id, message.messageId.bytes, kMessageIdLen);
    // Timestamp is now in encrypted metadata, so we always send 0. This will then be set by the server.
    plmsg->date = 0;
    plmsg->flags = message.flags;
    plmsg->reserved = 0;
    plmsg->metadata_len = message.metadataBox.length;
    memset(plmsg->push_from_name, 0, kPushFromNameLen);
    if (message.pushFromName != nil) {
        NSData *encodedPushFromName = [ThreemaUtilityObjC truncatedUTF8String:message.pushFromName maxLength:kPushFromNameLen];
        strncpy(plmsg->push_from_name, encodedPushFromName.bytes, encodedPushFromName.length);
    }
    
    size_t offset = 0;
    if (message.metadataBox != nil) {
        memcpy(&plmsg->metadata_nonce_box[offset], message.metadataBox.bytes, message.metadataBox.length);
        offset += message.metadataBox.length;
    }
    memcpy(&plmsg->metadata_nonce_box[offset], message.nonce.bytes, message.nonce.length);
    offset += message.nonce.length;
    memcpy(&plmsg->metadata_nonce_box[offset], message.box.bytes, message.box.length);
    
    return [self sendPayloadWithType:PLTYPE_OUTGOING_MESSAGE data:[NSData dataWithBytesNoCopy:plmsg length:msglen]];
}

- (NSError * _Nullable)reflectMessage:(NSData *)message {

    if (message == nil) {
        return [ThreemaError threemaError:[NSString stringWithFormat:@"Bad message"] withCode:ThreemaProtocolErrorBadMessage];
    }
    
    if ([(NSObject*)socket isKindOfClass:[MediatorWebSocket class]] == NO) {
        return [ThreemaError threemaError:[NSString stringWithFormat:@"Not connected to mediator"] withCode:ThreemaProtocolErrorNotConnectedToMediator];
    }

    if ([serverConnectorConnectionState connectionState] != ConnectionStateLoggedIn) {
        return [ThreemaError threemaError:[NSString stringWithFormat:@"Not logged in"] withCode:ThreemaProtocolErrorNotLoggedIn];
    }

    [socket writeWithData:message];
    return nil;
}

- (BOOL)ackMessage:(NSData*)messageId fromIdentity:(NSString*)fromIdentity {
    int msglen = sizeof(struct plMessageAck);
    struct plMessageAck *plmsgack = malloc(msglen);
    if (!plmsgack)
        return NO;
    
    DDLogInfo(@"Sending ack for message ID %@ from %@", messageId, fromIdentity);
    
    memcpy(plmsgack->from_identity, [fromIdentity dataUsingEncoding:NSASCIIStringEncoding].bytes, kIdentityLen);
    memcpy(plmsgack->message_id, messageId.bytes, kMessageIdLen);
    
    return [self sendPayloadWithType:PLTYPE_INCOMING_MESSAGE_ACK data:[NSData dataWithBytesNoCopy:plmsgack length:msglen]];
}

- (void)ping {
    dispatch_async(socketQueue, ^{
        [self sendEchoRequest];
    });
}

- (void)sendEchoRequest {
    [self sendEchoRequestWithTimeout:kEchoRequestTimeout];
}

- (void)sendEchoRequestWithTimeout:(NSTimeInterval)timeout {
    if ([serverConnectorConnectionState connectionState] != ConnectionStateLoggedIn)
        return;
    
    lastSentEchoSeq++;
    DDLogInfo(@"Sending echo request (seq %llu)", lastSentEchoSeq);
    
    lastEchoSendTime = CACurrentMediaTime();
    [self sendPayloadWithType:PLTYPE_ECHO_REQUEST data:[NSData dataWithBytes:&lastSentEchoSeq length:sizeof(lastSentEchoSeq)]];
    
    id<SocketProtocol> curSocket = socket;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^(void){
        if (curSocket == socket && lastRcvdEchoSeq < lastSentEchoSeq) {
            DDLogInfo(@"No reply to echo payload; disconnecting");
            [socket disconnect];
        }
    });
}

- (void)sendConnectionIdleTimeout {
    if ([serverConnectorConnectionState connectionState] != ConnectionStateLoggedIn)
        return;
    
    uint16_t connectionIdleTimeout = [UserSettings sharedUserSettings].enableMultiDevice ? kConnectionIdleMDTimeout : kConnectionIdleTimeout;
    if (connectionIdleTimeout < 30) {
        connectionIdleTimeout = 30;
        DDLogError(@"[ServerConnector] Set connection idle timeout can be min 30 seconds!");
    }
    else if (connectionIdleTimeout > 600) {
        connectionIdleTimeout = 600;
        DDLogError(@"[ServerConnector] Set connection idle timeout can be max 600 seconds!");
    }
    
    DDLogInfo(@"Sending connection idle timeout");
    [self sendPayloadWithType:PLTYPE_SET_CONNECTION_IDLE_TIMEOUT data: [NSData dataWithBytes:&connectionIdleTimeout length:sizeof(UInt16)]];
}

#pragma mark - Unblocking

- (void)_unblockIncomingMessagesIfAllowed {
    /* Unblock incoming messages if not running multi-device or already promoted to leader */
    if (doUnblockIncomingMessages && (deviceID == nil || [deviceID length] != kDeviceIdLen || isRolePromotedToLeader)) {
        [self sendPayloadWithType:PLTYPE_UNBLOCK_INCOMING_MESSAGES data:[NSData data]];
    }
}

#pragma mark - Multi-Device

- (BOOL)isMultiDeviceActivated {
    @synchronized (deviceGroupKeys) {
        return deviceGroupKeys != nil && deviceID != nil;
    }
}

- (void)deactivateMultiDevice {
    @synchronized (deviceGroupKeys) {
        [DeviceGroupKeyManager destroy];
        [UserSettings sharedUserSettings].enableMultiDevice = NO;
        // Ensure that the change is observed by SettingStores
        [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationSettingStoreSynchronization object:nil];
        deviceGroupKeys = nil;
        deviceID = nil;
        doUnblockIncomingMessages = YES;
    };
}

- (void)setIsRolePromotedToLeader:(BOOL)newIsRolePromotedToLeader {
    if (isRolePromotedToLeader != newIsRolePromotedToLeader) {
        isRolePromotedToLeader = newIsRolePromotedToLeader;
    }

    if ([[UserSettings sharedUserSettings] enableMultiDevice] == YES) {
        DDLogNotice(@"[Config] Multi-Device: Am I promoted to leader? %@", isRolePromotedToLeader ? @"YES" : @"NO");
    }
}

#pragma mark - Push Notification

- (BOOL)shouldRegisterPush {
    return [serverConnectorConnectionState connectionState] == ConnectionStateLoggedIn;
}

- (void)setPushToken:(NSData *)pushToken {
    [[AppGroup userDefaults] setObject:pushToken forKey:kPushNotificationDeviceToken];
    [[AppGroup userDefaults] synchronize];
    [self sendPushToken];
}

- (void)sendPushToken {
    dispatch_sync(sendPushTokenQueue, ^{
        NSData *pushToken = [[AppGroup userDefaults] objectForKey:kPushNotificationDeviceToken];
        
        if ([self shouldRegisterPush] == NO || pushToken == nil) {
            return;
        }

        NSData *pushEncryptionKey = [KeychainManager loadPushNotificationEncryptionKeyObjC];
        if (pushEncryptionKey == nil) {
            DDLogError(@"Cannot send push notification token (apple mc), because the push encryption key is missing");
            return;
        }

        DDLogInfo(@"Sending push notification token (apple mc)");
        
#ifdef DEBUG
        uint8_t pushTokenType = PUSHTOKEN_TYPE_APPLE_SANDBOX_MC;
#else
        uint8_t pushTokenType = PUSHTOKEN_TYPE_APPLE_PROD_MC;
#endif
        
        NSMutableData *payloadData = [NSMutableData dataWithBytes:&pushTokenType length:1];
        [payloadData appendData:pushToken];
        [payloadData appendData:[@"|" dataUsingEncoding:NSUTF8StringEncoding]];
        [payloadData appendData:[[BundleUtil threemaAppIdentifier] dataUsingEncoding:NSASCIIStringEncoding]];
        [payloadData appendData:[@"|" dataUsingEncoding:NSUTF8StringEncoding]];
        [payloadData appendData:pushEncryptionKey];
        [self sendPayloadWithType:PLTYPE_PUSH_NOTIFICATION_TOKEN data:payloadData];
    });
}

- (void)removePushToken {
    dispatch_sync(sendPushTokenQueue, ^{
        if ([[AppGroup userDefaults] objectForKey:kPushNotificationDeviceToken] != nil) {
            [[AppGroup userDefaults] setObject:nil forKey:kPushNotificationDeviceToken];
            [[AppGroup userDefaults] synchronize];
        }
        else {
            DDLogInfo(@"Already removed push notification token (apple mc)");
            return;
        }
        
        if ([self shouldRegisterPush] == NO) {
            return;
        }

        DDLogInfo(@"Clearing push notification token (apple mc)");

        uint8_t pushTokenType = PUSHTOKEN_TYPE_NONE;
        NSData *payloadData = [NSData dataWithBytes:&pushTokenType length:1];
        [self sendPayloadWithType:PLTYPE_PUSH_NOTIFICATION_TOKEN data:payloadData];
    });
}

- (void)removeVoIPPushToken {
    dispatch_sync(removeVoIPPushTokenQueue, ^{
        
        if (isRemovedVoIPPushToken == YES) {
            DDLogInfo(@"Already removed VoIP push token (apple)");
            return;
        }

        if([[AppGroup userDefaults] objectForKey:kVoIPPushNotificationDeviceToken] != nil) {
            [[AppGroup userDefaults] setObject:nil forKey:kVoIPPushNotificationDeviceToken];
            [[AppGroup userDefaults] synchronize];
        }
 
        if ([self shouldRegisterPush] == NO) {
            return;
        }
        
        DDLogInfo(@"Clearing VoIP push token (apple)");
        
        uint8_t voIPPushTokenType = PUSHTOKEN_TYPE_NONE;
        NSData *payloadData = [NSData dataWithBytes:&voIPPushTokenType length:1];
        [self sendPayloadWithType:PLTYPE_VOIP_PUSH_NOTIFICATION_TOKEN data:payloadData];
        
        isRemovedVoIPPushToken = YES;
    });
}

- (void)clearDeviceCookieChangedIndicator {
    [self sendPayloadWithType:PLTYPE_CLEAR_DEVICE_COOKIE_CHANGE_INDICATION data:[NSData data]];
}

#pragma mark - Nonces and encryption

- (NSData*)nextClientNonce {
    char nonce[kNaClCryptoNonceSize];
    memcpy(nonce, clientCookie.bytes, kCookieLen);
    memcpy(&nonce[kCookieLen], &clientNonce, sizeof(clientNonce));
    clientNonce++;
    return [NSData dataWithBytes:nonce length:kNaClCryptoNonceSize];
}

- (NSData*)nextServerNonce {
    char nonce[kNaClCryptoNonceSize];
    memcpy(nonce, serverCookie.bytes, kCookieLen);
    memcpy(&nonce[kCookieLen], &serverNonce, sizeof(serverNonce));
    serverNonce++;
    return [NSData dataWithBytes:nonce length:kNaClCryptoNonceSize];
}

- (NSData *)encryptData:(NSData *)message key:(NSData *)key {
    if (message == nil) {
        return nil;
    }
    
    NSData *nonce = [[NaClCrypto sharedCrypto] randomBytes:kNonceLen];
    NSData *encryptedMessage = [[NaClCrypto sharedCrypto] symmetricEncryptData:message withKey:key nonce:nonce];

    NSMutableData *encryptedData = [[NSMutableData alloc] initWithData:nonce];
    [encryptedData appendData:encryptedMessage];
    
    return encryptedData;
}

#pragma mark - Connection state

- (NSString *)nameForConnectionState:(ConnectionState)state {
    return [serverConnectorConnectionState nameForConnectionState:state];
}

- (BOOL)isIPv6Connection {
    return [socket isIPv6];
}

- (BOOL)isProxyConnection {
    return [socket isProxyConnection];
}

- (void)networkStatusDidChange:(NSNotification *)notice
{
    if (![reachabilityWrapper didLastConnectionTypeChange]) {
        return;
    }

    ConnectionState state = [serverConnectorConnectionState connectionState];
    if (state == ConnectionStateLoggedIn && ![reachabilityWrapper isReachabilityUnavailable]) {
        // The connection may still use the previous network, check it now instead of waiting for the next echo.
        // Slow connections get more time (`lastRtt` is -1 if unknown), at most the regular timeout.
        NSTimeInterval timeout = MIN(MAX(kNetworkChangeEchoRequestTimeout, self.lastRtt * 4), kEchoRequestTimeout);
        DDLogNotice(@"Network type changed - checking connection (timeout %.1f s)", timeout);
        dispatch_async(socketQueue, ^{
            [self sendEchoRequestWithTimeout:timeout];
        });
        return;
    }

    if ([reachabilityWrapper isReachabilityUnavailable] || state == ConnectionStateDisconnected || state == ConnectionStateDisconnecting) {
        if ([AppGroup getCurrentType] != AppGroupTypeNotificationExtension) {
            DDLogNotice(@"Internet status is or was unavailable - forcing reconnect");
            [self reconnect];
        }
        else {
            DDLogNotice(@"Internet status is or was unavailable - disconnect Notification Extension");
            [serverConnectorConnectionState disconnecting];
            [socket disconnect];
        }
    }
}

#pragma mark - Display server / connection alert

- (void)displayServerAlert:(NSString*)alertText {

    if ([displayedServerAlerts containsObject:alertText]) {
        return;
    }

    /* not shown before */
    NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:
                          alertText, kKeyMessage,
                          nil];

    [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationServerMessage object:nil userInfo:info];

    [displayedServerAlerts addObject:alertText];
}

- (void)showAnotherConnectionAlertWithReconnectAllowed:(BOOL)isReconnectAllowed {
    // If I am not active than an extension took over the connection
    if ([[UserSettings sharedUserSettings] ipcCommunicationEnabled] == NO && [AppGroup amIActive] == NO) {
        return;
    }

    // Ignore first few occurrences of "Another connection" messages to gracefully handle network switches
    if ([[UserSettings sharedUserSettings] ipcCommunicationEnabled] == NO && anotherConnectionCount < 5) {
        anotherConnectionCount++;
        return;
    }

    if (!isReconnectAllowed) {
        autoReconnect = NO;
    }

    // Do not show the alert if it's the share or notification extension, but set a flag to display it next time in the app
    if (AppGroup.getCurrentType == AppGroupTypeShareExtension || AppGroup.getCurrentType == AppGroupTypeNotificationExtension) {
        NSUserDefaults *userDefaults = AppGroup.userDefaults;
        [userDefaults setValue:[NSNumber numberWithBool:isReconnectAllowed] forKey:DUPLICATE_CONNECTION_RECONNECT_ALLOWED];
        return;
    }

    NSString *localizedErrorMessage = [BundleUtil localizedStringForKey:@"error_other_connection_for_same_identity_message"];
    NSString *errorMessage = [NSString stringWithFormat:localizedErrorMessage, TargetManagerObjC.localizedAppName, TargetManagerObjC.appName, TargetManagerObjC.appName];
    NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys: [BundleUtil localizedStringForKey:@"error_other_connection_for_same_identity_title"], kKeyTitle, errorMessage, kKeyMessage, nil];

    [self showConnectionAlertWithInfo:info];
}

- (void)showConnectionAlertWithInfo:(NSDictionary *)info {
    if (lastErrorDisplay == nil || ((-[lastErrorDisplay timeIntervalSinceNow]) > kErrorDisplayInterval)) {
        lastErrorDisplay = [NSDate date];

        [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationErrorConnectionFailed object:nil userInfo:info];
    }
}

#pragma mark - SocketProtocolDelegate

- (void)didConnect {
    [serverConnectorConnectionState connected];

    /* Send client hello packet with temporary public key and client cookie */
    clientCookie = [[NaClCrypto sharedCrypto] randomBytes:kCookieLen];
    DDLogVerbose(@"Client cookie = %@", clientCookie);
    
    /* Make sure to pass everything in one writeData call, or we will get two separate TCP segments */
    struct pktClientHello clientHello;
    memcpy(clientHello.client_tempkey_pub, clientTempKeyPub.bytes, sizeof(clientHello.client_tempkey_pub));
    memcpy(clientHello.client_cookie, clientCookie.bytes, sizeof(clientHello.client_cookie));
    [socket writeWithData:[NSData dataWithBytes:&clientHello length:sizeof(clientHello)] tag:TAG_CLIENT_HELLO_SENT];
    
    /* Prepare to receive server hello packet */
    [socket readWithLength:sizeof(struct pktServerHello) timeout:kReadTimeout tag:TAG_SERVER_HELLO_READ];
}

- (void)didDisconnectWithErrorCode:(NSInteger)code {
    [serverConnectorConnectionState disconnected];

    DDLogNotice(@"Interrupt queue on Task Manager");
    [TaskManager interrupt];

    [self setIsRolePromotedToLeader:NO];
    doUnblockIncomingMessages = YES;
    maximumNumberOfDeviceSlots = nil;

    if (keepalive_timer != nil) {
        dispatch_source_cancel(keepalive_timer);
        keepalive_timer = nil;
    }

    if (code == ServerConnectionCloseCodeUnsupportedProtocolVersion) {
        [self displayServerAlert:[NSString stringWithFormat:[BundleUtil localizedStringForKey:@"multi_device_unsupported_protocol_version_alert"], TargetManagerObjC.appName, TargetManagerObjC.appName]];
    }
    else if (code == ServerConnectionCloseCodeDeviceSlotStateMismatch) {
        [self displayServerAlert:[BundleUtil localizedStringForKey:@"multi_device_slot_state_mismatch_alert"]];

        // Device slot state mismatch -> this device must relink
        [self deactivateMultiDevice];
    }
    else if (code == ServerConnectionCloseCodeDuplicateConnection) {
        [self showAnotherConnectionAlertWithReconnectAllowed:NO];
    }
    else {
        [self reconnectAfterDelay];
    }
}

- (void)didReadData:(NSData * _Nonnull)data tag:(int16_t)tag {
    switch (tag) {
        case TAG_SERVER_HELLO_READ: {
            DDLogVerbose(@"Got server hello!");
            const struct pktServerHello* serverHello = data.bytes;
            
            serverCookie = [NSData dataWithBytes:serverHello->server_cookie length:sizeof(serverHello->server_cookie)];
            DDLogVerbose(@"Server cookie = %@", serverCookie);
            
            /* decrypt server hello box */
            chosenServerKeyPub = serverKeyPub;
            NSData *serverHelloBox = [NSData dataWithBytes:serverHello->box length:sizeof(serverHello->box)];
            NSData *nonce = [self nextServerNonce];
            NSData *serverHelloBoxOpen = [[NaClCrypto sharedCrypto] decryptData:serverHelloBox withSecretKey:clientTempKeySec signKey:chosenServerKeyPub nonce:nonce];
            if (serverHelloBoxOpen == nil) {
                /* try alternate key */
                chosenServerKeyPub = serverAltKeyPub;
                serverHelloBoxOpen = [[NaClCrypto sharedCrypto] decryptData:serverHelloBox withSecretKey:clientTempKeySec signKey:chosenServerKeyPub nonce:nonce];
                if (serverHelloBoxOpen == nil) {
                    DDLogError(@"Decryption of server hello box failed");
                    [socket disconnect];
                    return;
                } else {
                    DDLogWarn(@"Using alternate server key!");
                }
            }
            
            const struct pktServerHelloBox *serverHelloBoxU = (struct pktServerHelloBox*)serverHelloBoxOpen.bytes;
            
            /* verify client cookie */
            NSData *clientCookieFromServer = [NSData dataWithBytes:serverHelloBoxU->client_cookie length:sizeof(serverHelloBoxU->client_cookie)];
            if (![clientCookieFromServer isEqualToData:clientCookie]) {
                DDLogError(@"Client cookie mismatch (mine: %@, server: %@)", clientCookie, clientCookieFromServer);
                [socket disconnect];
                return;
            }
            
            /* copy temporary server key */
            serverTempKeyPub = [NSData dataWithBytes:serverHelloBoxU->server_tempkey_pub length:sizeof(serverHelloBoxU->server_tempkey_pub)];
            
            DDLogInfo(@"Server hello successful, tempkey_pub = %@", serverTempKeyPub);
            
            /* prepare extension packet */
            NSMutableData *extensionsData = [NSMutableData data];
            
            /* client info (0x00) extension payload */
            NSData *clientVersion = [ThreemaUtility.clientVersion dataUsingEncoding:NSASCIIStringEncoding];
            [extensionsData appendData:[self makeExtensionWithType:EXTENSION_TYPE_CLIENT_INFO data:clientVersion]];
            
            /* message payload version (0x02) extension payload */
            uint8_t plv = 0x01;
            [extensionsData appendData:[self makeExtensionWithType:EXTENSION_TYPE_MESSAGE_PAYLOAD_VERSION data:[NSData dataWithBytes:&plv length:1]]];
            
            // Adding Device ID extension if is Multi Device activated
            if (deviceID != nil && [deviceID length] == kDeviceIdLen) {
                /* CSP device ID (0x01) extension payload */
                [extensionsData appendData:[self makeExtensionWithType:EXTENSION_TYPE_DEVICE_ID data:deviceID]];
            }
            
            /* device cookie (0x03) extension payload */
            NSData *deviceCookie = [DeviceCookieManager obtainDeviceCookie];
            if (deviceCookie == nil) {
                DDLogError(@"Could not obtain device cookie");
                [socket disconnect];
                return;
            }
            [extensionsData appendData:[self makeExtensionWithType:EXTENSION_TYPE_DEVICE_COOKIE data:deviceCookie]];

            NSData *loginNonce = [self nextClientNonce];
            NSData *extensionsNonce = [self nextClientNonce];
            NSData *extensionsBox = [[NaClCrypto sharedCrypto] encryptData:extensionsData withPublicKey:serverTempKeyPub signKey:clientTempKeySec nonce:extensionsNonce];
            
            /* now prepare login packet */
            struct pktLogin login;
            memset(&login, 0, sizeof(struct pktLogin));
            
            memcpy(login.identity, [[MyIdentityStore sharedMyIdentityStore].identity dataUsingEncoding:NSASCIIStringEncoding].bytes, kIdentityLen);
            
            memcpy(login.client_version, "threema-clever-extension-field", 30);
            uint16_t extLen = extensionsBox.length;
            memcpy(&login.client_version[30], &extLen, sizeof(uint16_t));
            
            memcpy(login.server_cookie, serverCookie.bytes, kCookieLen);
            
            /* vouch calculation */
            NSMutableData *sharedSecrets = [NSMutableData dataWithData:[[MyIdentityStore sharedMyIdentityStore] sharedSecretWithPublicKey:chosenServerKeyPub]];
            [sharedSecrets appendData:[[MyIdentityStore sharedMyIdentityStore] sharedSecretWithPublicKey:serverTempKeyPub]];
            NSMutableData *vouchInput = [NSMutableData dataWithData:serverCookie];
            [vouchInput appendData:clientTempKeyPub];
            ThreemaKDF *kdf = [[ThreemaKDF alloc] initWithPersonal:@"3ma-csp"];
            NSData *vouchKey = [kdf deriveKeyWithSalt:@"v2" key:sharedSecrets];
            NSData *vouch = [ThreemaKDF calculateMacWithKey:vouchKey input:vouchInput];
            memcpy(login.vouch, vouch.bytes, kVouchLen);
                        
            /* encrypt login packet */
            NSData *loginBox = [[NaClCrypto sharedCrypto] encryptData:[NSData dataWithBytes:&login length:sizeof(login)] withPublicKey:serverTempKeyPub signKey:clientTempKeySec nonce:loginNonce];
            
            /* send it! */
            [socket writeWithData:loginBox tag:0];
            [socket writeWithData:extensionsBox tag:TAG_LOGIN_SENT];
            
            /* Prepare to receive login ack packet */
            [socket readWithLength:sizeof(struct pktLoginAck) + kNaClBoxOverhead timeout:kReadTimeout tag:TAG_LOGIN_ACK_READ];
            
            break;
        }
            
        case TAG_LOGIN_ACK_READ: {
            DDLogNotice(@"Login ack received");
            lastRead = CACurrentMediaTime();
            
            /* decrypt server hello box */
            NSData *loginAckBox = data;
            loginAckBox = [[NaClCrypto sharedCrypto] decryptData:loginAckBox withSecretKey:clientTempKeySec signKey:serverTempKeyPub nonce:[self nextServerNonce]];
            if (loginAckBox == nil) {
                DDLogError(@"Decryption of login ack failed");
                [socket disconnect];
                return;
            }
            
            /* Don't care about the contents of the login ACK for now; it only needs to decrypt correctly */
            
            reconnectAttempts = 0;
            DDLogNotice(@"Got chat server login ack; client connected");
            [serverConnectorConnectionState loggedInChatServer];

            [self sendPushToken];
            
            // Remove VoIP push token (since min OS version is iOS 15 or above)
            [self removeVoIPPushToken];
            
            /* set connection idle timeout */
            [self sendConnectionIdleTimeout];

            /* Schedule task for keepalive */
            keepalive_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue);
            dispatch_source_set_event_handler(keepalive_timer, ^{
                [self sendEchoRequest];
            });
            
            int64_t echoRequestInterval = [UserSettings sharedUserSettings].enableMultiDevice ? kEchoRequestMDInterval : kEchoRequestInterval;
            dispatch_source_set_timer(keepalive_timer, dispatch_time(DISPATCH_TIME_NOW, echoRequestInterval * NSEC_PER_SEC),
                                      echoRequestInterval * NSEC_PER_SEC, NSEC_PER_SEC);
            dispatch_resume(keepalive_timer);
            
            [self _unblockIncomingMessagesIfAllowed];
            
            /* Receive next payload header */
            [socket readWithLength:sizeof(uint16_t) timeout:-1 tag:TAG_PAYLOAD_LENGTH_READ];
            
            // Process all tasks.
            //
            // Call the spool function 2s delayed. Because when is running in the Notification Extension (NE)
            // it can happen the task queue is empty, right after the login (because the task for the incoming
            // message is not added at this time) and then the NE will be terminated. But it needs this call,
            // because it can happen also, that is receiving a reflected message before logged in and the
            // task will not be executed (because the connection state is not logged in).
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2000 * NSEC_PER_MSEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                TaskManager *tm = [[TaskManager alloc] init];
                [tm spool];
            });

            break;
        }
            
        case TAG_PAYLOAD_LENGTH_READ: {
            uint16_t msglen = *((uint16_t*)data.bytes);
            [socket readWithLength:msglen timeout:-1 tag:TAG_PAYLOAD_READ];
            break;
        }
            
        case TAG_PAYLOAD_READ: {
            DDLogVerbose(@"Payload (%lu bytes) received", (unsigned long)data.length);
            
            lastRead = CACurrentMediaTime();
            
            int64_t echoRequestInterval = [UserSettings sharedUserSettings].enableMultiDevice ? kEchoRequestMDInterval : kEchoRequestInterval;
            dispatch_source_set_timer(keepalive_timer, dispatch_time(DISPATCH_TIME_NOW, echoRequestInterval * NSEC_PER_SEC),
                                      echoRequestInterval * NSEC_PER_SEC, NSEC_PER_SEC);
            
            /* Decrypt payload */
            NSData *plData = [[NaClCrypto sharedCrypto] decryptData:data withSecretKey:clientTempKeySec signKey:serverTempKeyPub nonce:[self nextServerNonce]];
            if (plData == nil) {
                DDLogError(@"Payload decryption failed");
                [socket disconnect];
                return;
            }
            
            struct pktPayload *pl = (struct pktPayload*)plData.bytes;
            int datalen = (int)plData.length - (int)sizeof(struct pktPayload);
            DDLogInfo(@"Decrypted payload (type %02x, data %@)", pl->type, [NSData dataWithBytes:pl->data length:datalen]);
            
            [self processPayload:pl datalen:datalen];
            
            /* Receive next payload header */
            [socket readWithLength:sizeof(uint16_t) timeout:-1 tag:TAG_PAYLOAD_LENGTH_READ];
            
            break;
        }
            
        case TAG_PAYLOAD_MEDIATOR_TRIGGERED: {
            int timeoutDownloadThumbnail = isAppInBackground || [AppGroup getCurrentType] == AppGroupTypeNotificationExtension ? 20 : 0;

            TaskManager *taskManager = [[TaskManager alloc] init];

            MediatorMessageProcessor *processor = [[MediatorMessageProcessor alloc]
                                                   initWithDeviceGroupKeys:deviceGroupKeys
                                                   deviceID:deviceID
                                                   maxBytesToDecrypt:[AppGroup getCurrentType] != AppGroupTypeNotificationExtension ? MAX_BYTES_TO_DECRYPT_NO_LIMIT : MAX_BYTES_TO_DECRYPT_NOTIFICATION_EXTENSION
                                                   timeoutDownloadThumbnail:timeoutDownloadThumbnail
                                                   mediatorMessageProtocol:[[MediatorMessageProtocol alloc] initWithDeviceGroupKeys:deviceGroupKeys]
                                                   userSettings:[UserSettings sharedUserSettings]
                                                   taskManager:taskManager
                                                   socketProtocolDelegate:self
                                                   messageProcessorDelegate:self];

            uint8_t type;
            NSNumber *maxDeviceSlots;
            NSData *result = [processor processWithMessage:data messageType:&type receivedAfterInitialQueueSend:!mediatorServerInInitialQueueSend maxDeviceSlots:&maxDeviceSlots];

            if (result != nil && (int)type == MediatorMessageProtocol.MEDIATOR_MESSAGE_TYPE_REFLECT_ACK) {
                [[NSNotificationCenter defaultCenter] postNotificationName:[TaskManager mediatorMessageAckObserverNameWithReflectID:result] object:result];
            }
            else if ((int)type == MediatorMessageProtocol.MEDIATOR_MESSAGE_TYPE_LOCK_ACK || (int)type == MediatorMessageProtocol.MEDIATOR_MESSAGE_TYPE_UNLOCK_ACK || (int)type == MediatorMessageProtocol.MEDIATOR_MESSAGE_TYPE_TRANSACTION_REJECT) {
                [self transactionResponse:type reason:result];
            }
            else if ((int)type == MediatorMessageProtocol.MEDIATOR_MESSAGE_TYPE_TRANSACTION_ENDED) {
                [taskManager spool];
            }
            else if (result != nil && (int)type == MediatorMessageProtocol.MEDIATOR_MESSAGE_TYPE_SERVER_HELLO) {
                DDLogInfo(@"Send server hello to mediator");
                [socket writeWithData:result];
            }
            else if ((int)type == MediatorMessageProtocol.MEDIATOR_MESSAGE_TYPE_SERVER_INFO) {
                DDLogNotice(@"Got mediator server info; client connected");
                [serverConnectorConnectionState loggedInMediatorServer];
                maximumNumberOfDeviceSlots = maxDeviceSlots;
            }
            else if ((int)type == MediatorMessageProtocol.MEDIATOR_MESSAGE_TYPE_REFLECTION_QUEUE_DRY) {
                mediatorServerInInitialQueueSend = NO;
            }
            else if ((int)type == MediatorMessageProtocol.MEDIATOR_MESSAGE_TYPE_ROLE_PROMOTED_TO_LEADER) {
                DDLogVerbose(@"Promoted to leader -> unblock incoming chat messages, if allowed");

                [self setIsRolePromotedToLeader:YES];

                if ([serverConnectorConnectionState connectionState] == ConnectionStateLoggedIn) {
                    /* Unblock incoming messages */
                    [self _unblockIncomingMessagesIfAllowed];

                    /* Receive next payload header */
                    [socket readWithLength:sizeof(uint16_t) timeout:-1 tag:TAG_PAYLOAD_LENGTH_READ];
                }
            }
            else if (result != nil) {
                [self messageReceived:self type:type data:result];
            }

            break;
        }
    }
}

- (NSData*)makeExtensionWithType:(uint8_t)type data:(NSData*)data {
    struct pktExtension *extension = malloc(sizeof(struct pktExtension) + data.length);
    if (!extension) {
        return nil;
    }
    extension->type = type;
    extension->length = data.length;
    memcpy(extension->data, data.bytes, data.length);
    return [NSData dataWithBytesNoCopy:extension length:(sizeof(struct pktExtension) + data.length) freeWhenDone:YES];
}

#pragma mark - ConnectionStateDelegate

- (void)connectionStateChanged:(ConnectionState)state {
    if (UserSettings.sharedUserSettings.ipcCommunicationEnabled) {
        if (state == ConnectionStateDisconnected) {
            [AppGroup setMeInactive];
        }
        else if (![AppGroup amIActive]) {
                [AppGroup setMeActive];
        }
    }
    else if (state == ConnectionStateDisconnected && isAppInBackground && [AppGroup getCurrentType] == AppGroupTypeApp && connectionInitiators.count == 0) {
        // Hand over to the extensions, e.g. after a call ended in the background
        [AppGroup setActive:NO forType:AppGroupTypeApp];
    }

    dispatch_sync(queueConnectionStateDelegate, ^{
        if (clientConnectionStateDelegates != nil && [clientConnectionStateDelegates count] > 0) {
            for (id<ConnectionStateDelegate> delegate in clientConnectionStateDelegates) {
                [delegate connectionStateChanged:state];
            }
        }
    });
}

#pragma mark - MessageListenerDelegate

- (void)messageReceived:(_Nonnull id<MessageListenerDelegate>)listener type:(uint8_t)type data:(NSData * _Nonnull)data {
    dispatch_sync(queueMessageListenerDelegate, ^{
        if (clientMessageListenerDelegates != nil && [clientMessageListenerDelegates count] > 0) {
            for (id<MessageListenerDelegate> clientListener in clientMessageListenerDelegates) {
                [clientListener messageReceived:clientListener type:type data:data];
            }
        }
    });
}


#pragma mark - MessageProcessorDelegate

- (void)beforeDecode {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate beforeDecode];
    });
}

- (void)changedManagedObjectID:(NSManagedObjectID *)objectID {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate changedManagedObjectID:objectID];
    });
}

- (void)incomingMessageStarted:(AbstractMessage * _Nonnull)message {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate incomingMessageStarted:message];
    });
}

- (void)incomingMessageChanged:(AbstractMessage * _Nonnull)message baseMessageEntity:(NSObject * _Nonnull)baseMessageEntityObject {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate incomingMessageChanged:message baseMessageEntity:baseMessageEntityObject];
    });
}

- (void)incomingMessageFinished:(AbstractMessage * _Nonnull)message {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate incomingMessageFinished:message];
    });
}

- (void)incomingMessageFailed:(BoxedMessage *)message {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate incomingMessageFailed:message];
    });
}

- (void)incomingAbstractMessageFailed:(AbstractMessage *)message {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate incomingAbstractMessageFailed:message];
    });
}

- (void)incomingForwardSecurityMessageWithNoResultFinished:(AbstractMessage *)message {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate incomingForwardSecurityMessageWithNoResultFinished:message];
    });
}

- (void)readMessage:(NSSet *)inConversations {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate readMessage:inConversations];
    });
}

- (void)taskQueueEmpty {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate taskQueueEmpty];
    });
}

- (void)chatQueueDry {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate chatQueueDry];
    });
}

- (void)reflectionQueueDry {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate reflectionQueueDry];
    });
}

- (void)processTypingIndicator:(TypingIndicatorMessage * _Nonnull)message {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate processTypingIndicator:message];
    });
}

- (void)processVoIPCall:(NSObject * _Nonnull)message identity:(NSString * _Nonnull)identity onCompletion:(void (^_Nonnull)(id<MessageProcessorDelegate> _Nonnull))onCompletion onError:(void(^ _Nonnull)(NSError * _Nonnull, id<MessageProcessorDelegate> _Nonnull))onError {
    dispatch_sync(queueMessageProcessorDelegate, ^{
        [clientMessageProcessorDelegate processVoIPCall:message identity:identity onCompletion:onCompletion onError:onError];
    });
}

#pragma mark - TaskExecutionTransactionDelegate

- (void)transactionResponse:(uint8_t)messageType reason:(NSData * _Nullable)reason {
    dispatch_sync(queueTaskExecutionTransactionDelegate, ^{
        [clientTaskExecutionTransactionDelegate transactionResponse:messageType reason:reason];
    });
}

#pragma mark - Notifications

- (void)identityDestroyed:(NSNotification*)notification {
    /* when the identity is destroyed, we must disconnect */
    if ([serverConnectorConnectionState connectionState] != ConnectionStateDisconnected) {
        DDLogInfo(@"Disconnecting because identity destroyed");
        
        /* Clear push token on server now to reduce occurrence of push messages being
           delivered to devices that don't use that particular identity anymore */

        DDLogInfo(@"Clearing push notification token");
        uint8_t pushTokenType;
#ifdef DEBUG
        pushTokenType = PUSHTOKEN_TYPE_APPLE_SANDBOX_MC;
#else
        pushTokenType = PUSHTOKEN_TYPE_APPLE_PROD_MC;
#endif
        
        NSMutableData *payloadData = [NSMutableData dataWithBytes:&pushTokenType length:1];
        NSData *pushToken = [[NaClCrypto sharedCrypto] zeroBytes:32];
        [payloadData appendData:pushToken];
        [self sendPayloadWithType:PLTYPE_PUSH_NOTIFICATION_TOKEN data:payloadData];
        
        DDLogInfo(@"Sending VoIP push notification token");
        
        uint8_t voIPPushTokenType;
#ifdef DEBUG
        voIPPushTokenType = PUSHTOKEN_TYPE_APPLE_SANDBOX;
#else
        voIPPushTokenType = PUSHTOKEN_TYPE_APPLE_PROD;
#endif
        
        NSMutableData *voipPayloadData = [NSMutableData dataWithBytes:&voIPPushTokenType length:1];
        NSData *voipPushToken = [[NaClCrypto sharedCrypto] zeroBytes:32];
        [voipPayloadData appendData:voipPushToken];
        [self sendPayloadWithType:PLTYPE_VOIP_PUSH_NOTIFICATION_TOKEN data:voipPayloadData];
        
        [self _disconnect];
    }

    /* also clear the queue so that messages stuck in it don't later cause problems
       because they have the wrong from identity */

    DDLogWarn(@"Clear queue on Task Manager");
    [TaskManager removeAllTasks];
}

@end

[Verzeichnis aufwärts0.78unsichere Verbindung]