1798 lines
82 KiB
Objective-C
1798 lines
82 KiB
Objective-C
//
|
|
// MLCall.m
|
|
// monalxmpp
|
|
//
|
|
// Created by admin on 30.12.22.
|
|
// Copyright © 2022 monal-im.org. All rights reserved.
|
|
//
|
|
|
|
#import <Foundation/Foundation.h>
|
|
#import "MLConstants.h"
|
|
#import "Monal-Swift.h"
|
|
#import "HelperTools.h"
|
|
#import "XMPPIQ.h"
|
|
#import "XMPPMessage.h"
|
|
#import "xmpp.h"
|
|
#import "MLXMPPManager.h"
|
|
#import "MLVoIPProcessor.h"
|
|
#import "MLCall.h"
|
|
#import "MonalAppDelegate.h"
|
|
#import "MLOMEMO.h"
|
|
|
|
@import CallKit;
|
|
@import WebRTC;
|
|
|
|
//this is our private interface only shared with MLVoIPProcessor
|
|
@interface MLCall() <WebRTCClientDelegate>
|
|
{
|
|
//these are not synthesized automatically because we have getters and setters
|
|
MLXMLNode* _jmiProceed;
|
|
CXAnswerCallAction* _providerAnswerAction;
|
|
WebRTCClient* _webRTCClient;
|
|
BOOL _muted;
|
|
BOOL _speaker;
|
|
BOOL _isConnected;
|
|
AVAudioSession* _audioSession;
|
|
}
|
|
@property (nonatomic, strong) NSUUID* uuid;
|
|
@property (nonatomic, strong) NSString* jmiid;
|
|
@property (nonatomic, strong) MLContact* contact;
|
|
@property (nonatomic) MLCallType callType;
|
|
@property (nonatomic) MLCallDirection direction;
|
|
@property (nonatomic) MLCallEncryptionState encryptionState;
|
|
|
|
@property (nonatomic, strong) MLXMLNode* _Nullable jmiPropose;
|
|
@property (nonatomic, strong) MLXMLNode* _Nullable jmiProceed;
|
|
@property (nonatomic, strong) NSString* _Nullable fullRemoteJid;
|
|
@property (nonatomic, strong) WebRTCClient* _Nullable webRTCClient;
|
|
@property (nonatomic, strong) CXAnswerCallAction* _Nullable providerAnswerAction;
|
|
@property (nonatomic, assign) BOOL isConnected;
|
|
@property (nonatomic, assign) BOOL wasConnectedOnce;
|
|
@property (nonatomic, assign) BOOL isReconnecting;
|
|
@property (nonatomic, assign) BOOL isFinished;
|
|
@property (nonatomic, assign) BOOL tieBreak;
|
|
@property (nonatomic, strong) AVAudioSession* _Nullable audioSession;
|
|
@property (nonatomic, assign) MLCallFinishReason finishReason;
|
|
@property (nonatomic, assign) uint32_t durationTime;
|
|
@property (nonatomic, strong) NSTimer* _Nullable callDurationTimer;
|
|
@property (nonatomic, strong) monal_void_block_t _Nullable cancelDiscoveringTimeout;
|
|
@property (nonatomic, strong) monal_void_block_t _Nullable cancelRingingTimeout;
|
|
@property (nonatomic, strong) monal_void_block_t _Nullable cancelConnectingTimeout;
|
|
@property (nonatomic, strong) monal_void_block_t _Nullable cancelWaitUntilIceRestart;
|
|
@property (nonatomic, strong) MLXMLNode* localSDP;
|
|
@property (nonatomic, strong) MLXMLNode* remoteSDP;
|
|
@property (nonatomic, strong) NSNumber* remoteOmemoDeviceId;
|
|
@property (nonatomic, strong) NSObject* candidateQueueLock;
|
|
@property (nonatomic, strong) NSMutableArray<XMPPIQ*>* incomingCandidateQueue;
|
|
@property (nonatomic, strong) NSMutableArray<XMPPIQ*>* outgoingCandidateQueue;
|
|
|
|
@property (nonatomic, readonly) xmpp* account;
|
|
@property (nonatomic, strong) MLVoIPProcessor* voipProcessor;
|
|
@end
|
|
|
|
//this is private and only shared to this class
|
|
@interface MLVoIPProcessor()
|
|
@property (nonatomic, strong) CXCallController* _Nullable callController;
|
|
@property (nonatomic, strong) CXProvider* _Nullable cxProvider;
|
|
-(void) removeCall:(MLCall*) call;
|
|
-(void) initWebRTCForPendingCall:(MLCall*) call;
|
|
-(void) handleIncomingJMIStanza:(MLXMLNode*) messageNode onAccount:(xmpp*) account;
|
|
@end
|
|
|
|
@implementation MLCall
|
|
|
|
+(instancetype) makeDummyCall:(int) type
|
|
{
|
|
NSUUID* uuid = [NSUUID UUID];
|
|
return [[self alloc] initWithUUID:uuid jmiid:uuid.UUIDString contact:[MLContact makeDummyContact:type] callType:MLCallTypeAudio andDirection:MLCallDirectionOutgoing];
|
|
}
|
|
|
|
-(instancetype) initWithUUID:(NSUUID*) uuid jmiid:(NSString*) jmiid contact:(MLContact*) contact callType:(MLCallType) callType andDirection:(MLCallDirection) direction
|
|
{
|
|
self = [super init];
|
|
MLAssert(uuid != nil, @"Call UUIDs must not be nil!");
|
|
MLAssert(jmiid != nil, @"Call jmiids must not be nil!");
|
|
self.uuid = uuid;
|
|
self.jmiid = jmiid;
|
|
self.contact = contact;
|
|
self.callType = callType;
|
|
self.direction = direction;
|
|
self.encryptionState = MLCallEncryptionStateUnknown;
|
|
self.isConnected = NO;
|
|
self.wasConnectedOnce = NO;
|
|
self.isReconnecting = NO;
|
|
self.durationTime = 0;
|
|
self.isFinished = NO;
|
|
self.finishReason = MLCallFinishReasonUnknown;
|
|
self.cancelDiscoveringTimeout = nil;
|
|
self.cancelRingingTimeout = nil;
|
|
self.cancelConnectingTimeout = nil;
|
|
self.localSDP = nil;
|
|
self.remoteSDP = nil;
|
|
self.remoteOmemoDeviceId = nil;
|
|
self.candidateQueueLock = [NSObject new];
|
|
self.incomingCandidateQueue = [NSMutableArray new];
|
|
self.outgoingCandidateQueue = [NSMutableArray new];
|
|
|
|
[HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:^{
|
|
MonalAppDelegate* appDelegate = (MonalAppDelegate*)[[UIApplication sharedApplication] delegate];
|
|
MLAssert(appDelegate.voipProcessor != nil, @"appDelegate.voipProcessor should never be nil!");
|
|
self.voipProcessor = appDelegate.voipProcessor;
|
|
}];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processIncomingSDP:) name:kMonalIncomingSDP object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processIncomingICECandidate:) name:kMonalIncomingICECandidate object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioRouteChangeNotification:) name:AVAudioSessionRouteChangeNotification object:nil];
|
|
//[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleConnectivityChange:) name:kMonalConnectivityChange object:nil];
|
|
|
|
return self;
|
|
}
|
|
|
|
-(void) dealloc
|
|
{
|
|
DDLogInfo(@"Called dealloc: %@", self);
|
|
[self.callDurationTimer invalidate];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
#pragma mark - public interface
|
|
|
|
-(void) startCaptureLocalVideoWithRenderer:(id<RTCVideoRenderer>) renderer andCameraPosition:(AVCaptureDevicePosition) position
|
|
{
|
|
MLAssert(self.callType == MLCallTypeVideo, @"startCaptureLocalVideoWithRenderer:andCameraPosition: can only be called for video calls!");
|
|
[self.webRTCClient startCaptureLocalVideoWithRenderer:renderer andCameraPosition:position];
|
|
}
|
|
|
|
-(void) stopCaptureLocalVideo
|
|
{
|
|
MLAssert(self.callType == MLCallTypeVideo, @"stopCaptureLocalVideo: can only be called for video calls!");
|
|
[self.webRTCClient stopCaptureLocalVideo];
|
|
}
|
|
|
|
-(void) renderRemoteVideoWithRenderer:(id<RTCVideoRenderer>) renderer
|
|
{
|
|
MLAssert(self.callType == MLCallTypeVideo, @"renderRemoteVideoWithRenderer: can only be called for video calls!");
|
|
[self.webRTCClient renderRemoteVideoTo:renderer];
|
|
}
|
|
|
|
-(void) hideVideo
|
|
{
|
|
MLAssert(self.callType == MLCallTypeVideo, @"hideVideo: can only be called for video calls!");
|
|
[self.webRTCClient hideVideo];
|
|
}
|
|
|
|
-(void) showVideo
|
|
{
|
|
MLAssert(self.callType == MLCallTypeVideo, @"showVideo: can only be called for video calls!");
|
|
[self.webRTCClient showVideo];
|
|
}
|
|
|
|
-(void) end
|
|
{
|
|
if(self.isFinished)
|
|
{
|
|
DDLogInfo(@"Not requesting end call action: call already in finished state...");
|
|
return;
|
|
}
|
|
DDLogVerbose(@"Requesting end call transaction for %@", [self short]);
|
|
CXEndCallAction* endCallAction = [[CXEndCallAction alloc] initWithCallUUID:self.uuid];
|
|
CXTransaction* transaction = [[CXTransaction alloc] initWithAction:endCallAction];
|
|
[self.voipProcessor.callController requestTransaction:transaction completion:^(NSError* error) {
|
|
if(error != nil)
|
|
{
|
|
//try to do this "manually" without looping through callkit
|
|
DDLogError(@"Error requesting end call transaction: %@", error);
|
|
[self internalHandleEndCallActionWithReason:MLCallFinishReasonUnknown];
|
|
return;
|
|
}
|
|
else
|
|
DDLogInfo(@"Successfully created end call transaction for CallKit..");
|
|
}];
|
|
}
|
|
|
|
-(void) delayedEnd:(double) delay withDisconnectedState:(BOOL) disconnected
|
|
{
|
|
createTimer(delay, (^{
|
|
//isConnected = NO will result in MLCallFinishReasonConnectivityError if wasConnectedOnce == YES
|
|
if(disconnected)
|
|
self.isConnected = NO;
|
|
[self end];
|
|
}));
|
|
}
|
|
|
|
-(void) setMuted:(BOOL) muted
|
|
{
|
|
@synchronized(self) {
|
|
if(self.webRTCClient == nil || self.audioSession == nil)
|
|
return;
|
|
_muted = muted;
|
|
if(_muted)
|
|
[self.webRTCClient muteAudio];
|
|
else
|
|
[self.webRTCClient unmuteAudio];
|
|
}
|
|
}
|
|
|
|
-(BOOL) muted
|
|
{
|
|
@synchronized(self) {
|
|
return _muted;
|
|
}
|
|
}
|
|
|
|
-(void) setSpeaker:(BOOL) speaker
|
|
{
|
|
@synchronized(self) {
|
|
if(self.webRTCClient == nil || self.audioSession == nil)
|
|
return;
|
|
if(_speaker == speaker)
|
|
return;
|
|
_speaker = speaker;
|
|
if(_speaker)
|
|
[self.webRTCClient speakerOn];
|
|
else
|
|
[self.webRTCClient speakerOff];
|
|
}
|
|
}
|
|
|
|
-(BOOL) speaker
|
|
{
|
|
@synchronized(self) {
|
|
return _speaker;
|
|
}
|
|
}
|
|
|
|
-(MLCallState) state
|
|
{
|
|
@synchronized(self) {
|
|
if(self.direction == MLCallDirectionOutgoing)
|
|
{
|
|
if(self.isFinished)
|
|
return MLCallStateFinished;
|
|
if(self.isConnected && self.webRTCClient != nil && self.audioSession != nil)
|
|
return MLCallStateConnected;
|
|
if(self.jmiProceed != nil && self.isReconnecting)
|
|
return MLCallStateReconnecting;
|
|
if(self.jmiProceed != nil)
|
|
return MLCallStateConnecting;
|
|
if(self.jmiProceed == nil && self.cancelRingingTimeout != nil)
|
|
return MLCallStateRinging;
|
|
if(self.jmiProceed == nil && self.cancelDiscoveringTimeout != nil)
|
|
return MLCallStateDiscovering;
|
|
return MLCallStateUnknown;
|
|
}
|
|
else
|
|
{
|
|
if(self.isFinished)
|
|
return MLCallStateFinished;
|
|
if(self.isConnected && self.webRTCClient != nil && self.audioSession != nil)
|
|
return MLCallStateConnected;
|
|
if(self.providerAnswerAction != nil && self.isReconnecting)
|
|
return MLCallStateReconnecting;
|
|
if(self.providerAnswerAction != nil)
|
|
return MLCallStateConnecting;
|
|
if(self.providerAnswerAction == nil)
|
|
return MLCallStateRinging;
|
|
return MLCallStateUnknown;
|
|
}
|
|
}
|
|
}
|
|
|
|
+(NSSet*) keyPathsForValuesAffectingState
|
|
{
|
|
return [NSSet setWithObjects:@"direction", @"isConnected", @"jmiProceed", @"webRTCClient", @"providerAnswerAction", @"audioSession", @"isFinished", @"cancelDiscoveringTimeout", @"cancelRingingTimeout", @"cancelConnectingTimeout", @"isReconnecting", nil];
|
|
}
|
|
|
|
#pragma mark - internals
|
|
|
|
-(xmpp*) account
|
|
{
|
|
@synchronized(self) {
|
|
xmpp* account = self.contact.account;
|
|
MLAssert(account != nil, @"Account of call must be listed in MLXMPPManager connected accounts!", (@{
|
|
@"contact": nilWrapper(self.contact),
|
|
@"call": nilWrapper(self),
|
|
}));
|
|
return account;
|
|
}
|
|
}
|
|
-(void) startCallDuartionTimer
|
|
{
|
|
//the timer needs a thread with runloop, see https://stackoverflow.com/a/18098396/3528174
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if(self.cancelDiscoveringTimeout != nil)
|
|
self.cancelDiscoveringTimeout();
|
|
self.cancelDiscoveringTimeout = nil;
|
|
if(self.cancelRingingTimeout != nil)
|
|
self.cancelRingingTimeout();
|
|
self.cancelRingingTimeout = nil;
|
|
if(self.cancelConnectingTimeout != nil)
|
|
self.cancelConnectingTimeout();
|
|
self.cancelConnectingTimeout = nil;
|
|
|
|
//don't restart our timer if we just reconnected
|
|
if(self.isReconnecting)
|
|
return;
|
|
if(self.callDurationTimer != nil)
|
|
[self.callDurationTimer invalidate];
|
|
DDLogInfo(@"%@: Starting call duration timer...", [self short]);
|
|
self.callDurationTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer* timer) {
|
|
DDLogVerbose(@"%@:Call duration timer triggered: %d", [self short], self.durationTime);
|
|
if(self.state == MLCallStateFinished)
|
|
{
|
|
DDLogInfo(@"%@: Stopping call duration timer...", [self short]);
|
|
[timer invalidate];
|
|
self.callDurationTimer = nil;
|
|
}
|
|
else
|
|
self.durationTime++;
|
|
}];
|
|
});
|
|
}
|
|
|
|
-(void) setJmiProceed:(MLXMLNode*) jmiProceed
|
|
{
|
|
@synchronized(self) {
|
|
_jmiProceed = jmiProceed;
|
|
if(self.direction == MLCallDirectionOutgoing)
|
|
{
|
|
//see https://gist.github.com/iNPUTmice/aa4fc0aeea6ce5fb0e0fe04baca842cd
|
|
self.remoteOmemoDeviceId = [jmiProceed findFirst:@"{urn:xmpp:jingle-message:0}proceed/{http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification}device@id|uint"];
|
|
DDLogInfo(@"Proceed set remote omemo deviceid to: %@", self.remoteOmemoDeviceId);
|
|
}
|
|
if(self.direction == MLCallDirectionOutgoing && self.webRTCClient != nil)
|
|
[self establishOutgoingConnection];
|
|
}
|
|
}
|
|
-(MLXMLNode*) jmiProceed
|
|
{
|
|
@synchronized(self) {
|
|
return _jmiProceed;
|
|
}
|
|
}
|
|
|
|
-(void) setProviderAnswerAction:(CXAnswerCallAction*) action
|
|
{
|
|
@synchronized(self) {
|
|
_providerAnswerAction = action;
|
|
if(self.direction == MLCallDirectionIncoming && self.webRTCClient != nil)
|
|
[self establishIncomingConnection];
|
|
}
|
|
}
|
|
-(CXAnswerCallAction*) providerAnswerAction
|
|
{
|
|
@synchronized(self) {
|
|
return _providerAnswerAction;
|
|
}
|
|
}
|
|
|
|
-(void) setWebRTCClient:(WebRTCClient*) webRTCClient
|
|
{
|
|
@synchronized(self) {
|
|
_webRTCClient = webRTCClient;
|
|
if(self.webRTCClient != nil && self.direction == MLCallDirectionIncoming && self.providerAnswerAction != nil)
|
|
[self establishIncomingConnection];
|
|
if(self.webRTCClient != nil && self.direction == MLCallDirectionOutgoing && self.jmiProceed != nil)
|
|
[self establishOutgoingConnection];
|
|
}
|
|
}
|
|
-(WebRTCClient*) webRTCClient
|
|
{
|
|
@synchronized(self) {
|
|
return _webRTCClient;
|
|
}
|
|
}
|
|
|
|
-(void) setIsConnected:(BOOL) isConnected
|
|
{
|
|
@synchronized(self) {
|
|
BOOL oldValue = _isConnected;
|
|
_isConnected = isConnected;
|
|
if(isConnected)
|
|
self.wasConnectedOnce = YES;
|
|
|
|
//if switching to connected state: check if we need to activate the already reported audio session now
|
|
if(oldValue == NO && self.isConnected == YES && self.audioSession != nil)
|
|
[self didActivateAudioSession:self.audioSession];
|
|
|
|
//start timer once we are fully connected
|
|
if(self.isConnected && self.audioSession != nil)
|
|
[self startCallDuartionTimer];
|
|
}
|
|
|
|
#ifdef IS_ALPHA
|
|
#if TARGET_OS_MACCATALYST
|
|
//set audio session to default one
|
|
self.audioSession = [AVAudioSession sharedInstance];
|
|
#endif
|
|
#endif
|
|
}
|
|
-(BOOL) isConnected
|
|
{
|
|
@synchronized(self) {
|
|
return _isConnected;
|
|
}
|
|
}
|
|
|
|
-(void) setAudioSession:(AVAudioSession*) audioSession
|
|
{
|
|
@synchronized(self) {
|
|
if(audioSession == _audioSession)
|
|
{
|
|
DDLogWarn(@"Trying to activate same audio session a second time, ignoring...");
|
|
return;
|
|
}
|
|
BOOL assertActivated = YES;
|
|
#ifdef IS_ALPHA
|
|
#if TARGET_OS_MACCATALYST
|
|
assertActivated = NO;
|
|
#endif
|
|
#endif
|
|
if(assertActivated && audioSession != nil)
|
|
MLAssert(_audioSession == nil, @"Audio session should never be activated without deactivating old audio session first!", (@{
|
|
@"oldAudioSession": nilWrapper(_audioSession),
|
|
@"newAudioSession": nilWrapper(audioSession),
|
|
@"call": self,
|
|
}));
|
|
AVAudioSession* oldSession = _audioSession;
|
|
_audioSession = audioSession;
|
|
|
|
//do nothing if not yet connected
|
|
if(self.isConnected == YES && oldSession == nil && self.audioSession != nil)
|
|
[self didActivateAudioSession:self.audioSession];
|
|
|
|
if(self.audioSession == nil && oldSession != nil)
|
|
[self didDeactivateAudioSession:oldSession];
|
|
|
|
//start timer once we are fully connected
|
|
if(self.isConnected && self.audioSession != nil)
|
|
[self startCallDuartionTimer];
|
|
}
|
|
}
|
|
-(AVAudioSession*) audioSession
|
|
{
|
|
@synchronized(self) {
|
|
return _audioSession;
|
|
}
|
|
}
|
|
|
|
-(void) didActivateAudioSession:(AVAudioSession*) audioSession
|
|
{
|
|
NSError* error = nil;
|
|
DDLogInfo(@"Activating audio session now: %@", audioSession);
|
|
[[RTCAudioSession sharedInstance] lockForConfiguration];
|
|
NSUInteger options = 0;
|
|
options |= AVAudioSessionCategoryOptionAllowBluetooth;
|
|
options |= AVAudioSessionCategoryOptionAllowBluetoothA2DP;
|
|
options |= AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers;
|
|
options |= AVAudioSessionCategoryOptionAllowAirPlay;
|
|
[[RTCAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:options error:&error];
|
|
if(error != nil)
|
|
DDLogError(@"Failed to configure AVAudioSession category: %@", error);
|
|
[[RTCAudioSession sharedInstance] setMode:AVAudioSessionModeVoiceChat error:&error];
|
|
if(error != nil)
|
|
DDLogError(@"Failed to configure AVAudioSession mode: %@", error);
|
|
[[RTCAudioSession sharedInstance] audioSessionDidActivate:audioSession];
|
|
[[RTCAudioSession sharedInstance] setIsAudioEnabled:YES];
|
|
[[RTCAudioSession sharedInstance] unlockForConfiguration];
|
|
}
|
|
|
|
-(void) didDeactivateAudioSession:(AVAudioSession*) audioSession
|
|
{
|
|
DDLogInfo(@"Deactivating audio session now: %@", audioSession);
|
|
[[RTCAudioSession sharedInstance] lockForConfiguration];
|
|
[[RTCAudioSession sharedInstance] audioSessionDidDeactivate:audioSession];
|
|
[[RTCAudioSession sharedInstance] setIsAudioEnabled:NO];
|
|
[[RTCAudioSession sharedInstance] unlockForConfiguration];
|
|
}
|
|
|
|
-(void) reportRinging
|
|
{
|
|
DDLogDebug(@"%@ was reported as ringing...", [self short]);
|
|
[self createRingingTimeoutTimer];
|
|
}
|
|
|
|
-(void) migrateTo:(MLCall*) otherCall
|
|
{
|
|
//send jmi finish with migration before chaning all ids etc.
|
|
DDLogDebug(@"Migrating call using JMI finish: %@", self);
|
|
XMPPMessage* jmiNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:self.fullRemoteJid];
|
|
[jmiNode addChildNode:[[MLXMLNode alloc] initWithElement:@"finish" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{
|
|
@"id": self.jmiid,
|
|
} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"reason" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"expired"]
|
|
] andData:nil],
|
|
[[MLXMLNode alloc] initWithElement:@"migrated" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{
|
|
@"to": otherCall.jmiid,
|
|
} andChildren:@[] andData:nil]
|
|
] andData:nil]];
|
|
[jmiNode setStoreHint];
|
|
[self.account send:jmiNode];
|
|
|
|
@synchronized(self) {
|
|
DDLogDebug(@"%@: Preparing this call for new webrtc connection...", [self short]);
|
|
self.jmiid = otherCall.jmiid;
|
|
self.fullRemoteJid = otherCall.fullRemoteJid;
|
|
self.callType = otherCall.callType;
|
|
self.isConnected = NO;
|
|
self.isReconnecting = YES;
|
|
self.finishReason = MLCallFinishReasonUnknown;
|
|
self.direction = otherCall.direction;
|
|
self.jmiPropose = otherCall.jmiPropose;
|
|
self.jmiProceed = nil;
|
|
[self.callDurationTimer invalidate];
|
|
self.callDurationTimer = nil;
|
|
self.localSDP = otherCall.localSDP; //should be nil
|
|
self.remoteSDP = otherCall.remoteSDP; //should be nil
|
|
self.incomingCandidateQueue = otherCall.incomingCandidateQueue; //should be empty
|
|
self.outgoingCandidateQueue = otherCall.outgoingCandidateQueue; //should be empty
|
|
self.remoteOmemoDeviceId = otherCall.remoteOmemoDeviceId; //depends on jmiProceed and should be empty
|
|
self.encryptionState = MLCallEncryptionStateUnknown; //depends on callstate >= connecting
|
|
otherCall = nil;
|
|
|
|
DDLogDebug(@"%@: Stopping all running timers...", [self short]);
|
|
if(self.cancelDiscoveringTimeout != nil)
|
|
self.cancelDiscoveringTimeout();
|
|
self.cancelDiscoveringTimeout = nil;
|
|
if(self.cancelRingingTimeout != nil)
|
|
self.cancelRingingTimeout();
|
|
self.cancelRingingTimeout = nil;
|
|
if(self.cancelConnectingTimeout != nil)
|
|
self.cancelConnectingTimeout();
|
|
self.cancelConnectingTimeout = nil;
|
|
|
|
if(self.webRTCClient != nil)
|
|
{
|
|
DDLogDebug(@"%@: Closing old webrtc connection...", [self short]);
|
|
__block WebRTCClient* client = self.webRTCClient;
|
|
self.webRTCClient = nil;
|
|
//do this async to not run into a deadlock with the signalling thread
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
[client.peerConnection close];
|
|
client = nil;
|
|
|
|
//report this migrated call as ringing
|
|
[self sendJmiRinging];
|
|
|
|
//now fake a cxprovider answer action (we do auto-answer this call, but ios does not even know we switched the underlying webrtc connection)
|
|
DDLogVerbose(@"%@: Faking CXAnswerCallAction...", [self short]);
|
|
self.providerAnswerAction = [[CXAnswerCallAction alloc] initWithCallUUID:self.uuid];
|
|
|
|
DDLogVerbose(@"%@: Initializing webrtc for our migrated call...", [self short]);
|
|
[self.voipProcessor initWebRTCForPendingCall:self];
|
|
|
|
DDLogDebug(@"%@: Migration done, waiting for new webrtc connection...", [self short]);
|
|
});
|
|
}
|
|
else
|
|
DDLogDebug(@"%@: No old webrtc connection to close...", [self short]);
|
|
}
|
|
}
|
|
|
|
-(void) handleEndCallActionWithReason:(MLCallFinishReason) reason
|
|
{
|
|
@synchronized(self) {
|
|
[self internalHandleEndCallActionWithReason:reason];
|
|
[self internalUpdateCallKitState];
|
|
}
|
|
}
|
|
|
|
-(void) internalHandleEndCallActionWithReason:(MLCallFinishReason) reason
|
|
{
|
|
@synchronized(self) {
|
|
//stop all running timers
|
|
if(self.cancelDiscoveringTimeout != nil)
|
|
self.cancelDiscoveringTimeout();
|
|
self.cancelDiscoveringTimeout = nil;
|
|
if(self.cancelRingingTimeout != nil)
|
|
self.cancelRingingTimeout();
|
|
self.cancelRingingTimeout = nil;
|
|
if(self.cancelConnectingTimeout != nil)
|
|
self.cancelConnectingTimeout();
|
|
self.cancelConnectingTimeout = nil;
|
|
|
|
//end webrtc call if already established or in the process of establishing
|
|
if(self.webRTCClient != nil)
|
|
{
|
|
WebRTCClient* client = self.webRTCClient;
|
|
self.webRTCClient = nil; //this will prevent the new webrtc state from being handled
|
|
//do this async to not run into a deadlock with the signalling thread
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
[client.peerConnection close];
|
|
});
|
|
}
|
|
|
|
//update state (this will automatically stop the call duration timer)
|
|
self.finishReason = reason;
|
|
self.isConnected = NO;
|
|
self.isFinished = YES;
|
|
|
|
//remove this call from pending calls
|
|
[self.voipProcessor removeCall:self];
|
|
}
|
|
}
|
|
|
|
-(void) internalUpdateCallKitState
|
|
{
|
|
@synchronized(self) {
|
|
//the CXEndCallAction means either the call was rejected (if not yet answered) or it was terminated normally (if the call was accepted)
|
|
//see https://developer.apple.com/documentation/callkit/cxcallendedreason?language=objc for end reasons
|
|
if(self.direction == MLCallDirectionIncoming)
|
|
{
|
|
[self.providerAnswerAction fail]; //fail will do nothing if already fulfilled or nil
|
|
if(self.jmiProceed == nil)
|
|
{
|
|
if(self.finishReason == MLCallFinishReasonAnsweredElsewhere)
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonAnsweredElsewhere];
|
|
else if(self.finishReason == MLCallFinishReasonUnanswered)
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonRemoteEnded];
|
|
else if(self.finishReason == MLCallFinishReasonRejected)
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonDeclinedElsewhere];
|
|
else if(self.finishReason == MLCallFinishReasonDeclined)
|
|
{
|
|
if(self.tieBreak)
|
|
[self sendJmiRejectWithTieBreak];
|
|
else
|
|
[self sendJmiReject];
|
|
}
|
|
else if(self.finishReason == MLCallFinishReasonError)
|
|
{
|
|
[self sendJmiFinishWithReason:@"application-error"];
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
else
|
|
{
|
|
DDLogError(@"Unexpected finish reason: %@", (@{@"call": self, @"finishReason": @(self.finishReason)}));
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(self.finishReason == MLCallFinishReasonNormal)
|
|
{
|
|
[self sendJmiFinishWithReason:@"success"];
|
|
//this is not needed because this case is always looped through cxprovider endCallAction
|
|
//[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonRemoteEnded];
|
|
}
|
|
else if(self.finishReason == MLCallFinishReasonConnectivityError)
|
|
{
|
|
[self sendJmiFinishWithReason:@"connectivity-error"];
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
else if(self.finishReason == MLCallFinishReasonSecurityError)
|
|
{
|
|
[self sendJmiFinishWithReason:@"security-error"];
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
else if(self.finishReason == MLCallFinishReasonError)
|
|
{
|
|
[self sendJmiFinishWithReason:@"application-error"];
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
else
|
|
{
|
|
DDLogError(@"Unexpected finish reason: %@", (@{@"call": self, @"finishReason": @(self.finishReason)}));
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(self.jmiPropose != nil)
|
|
{
|
|
if(self.jmiProceed == nil)
|
|
{
|
|
if(self.finishReason == MLCallFinishReasonRejected)
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonRemoteEnded];
|
|
else if(self.finishReason == MLCallFinishReasonAnsweredElsewhere)
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonAnsweredElsewhere];
|
|
else if(self.finishReason == MLCallFinishReasonRetracted)
|
|
{
|
|
if(self.tieBreak)
|
|
[self sendJmiRetractWithTieBreak];
|
|
else
|
|
[self sendJmiRetract];
|
|
}
|
|
else if(self.finishReason == MLCallFinishReasonError)
|
|
{
|
|
[self sendJmiFinishWithReason:@"application-error"];
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
else
|
|
{
|
|
DDLogError(@"Unexpected finish reason: %@", (@{@"call": self, @"finishReason": @(self.finishReason)}));
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(self.finishReason == MLCallFinishReasonNormal)
|
|
{
|
|
[self sendJmiFinishWithReason:@"success"];
|
|
//this is not needed because this case is always looped through cxprovider endCallAction
|
|
//[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonRemoteEnded];
|
|
}
|
|
else if(self.finishReason == MLCallFinishReasonConnectivityError)
|
|
{
|
|
[self sendJmiFinishWithReason:@"connectivity-error"];
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
else if(self.finishReason == MLCallFinishReasonSecurityError)
|
|
{
|
|
[self sendJmiFinishWithReason:@"security-error"];
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
else if(self.finishReason == MLCallFinishReasonError)
|
|
{
|
|
[self sendJmiFinishWithReason:@"application-error"];
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
else
|
|
{
|
|
DDLogError(@"Unexpected finish reason: %@", (@{@"call": self, @"finishReason": @(self.finishReason)}));
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//this case probably does never happen
|
|
//(the outgoing call transaction was started, but start call action not yet executed, and then the end call action arrives)
|
|
[self sendJmiFinishWithReason:@"connectivity-error"];
|
|
[self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonUnanswered];
|
|
self.finishReason = MLCallFinishReasonConnectivityError;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void) createConnectingTimeoutTimer
|
|
{
|
|
if(self.cancelDiscoveringTimeout != nil)
|
|
self.cancelDiscoveringTimeout();
|
|
self.cancelDiscoveringTimeout = nil;
|
|
if(self.cancelRingingTimeout != nil)
|
|
self.cancelRingingTimeout();
|
|
self.cancelRingingTimeout = nil;
|
|
if(self.cancelConnectingTimeout != nil)
|
|
self.cancelConnectingTimeout();
|
|
self.cancelConnectingTimeout = nil;
|
|
self.cancelConnectingTimeout = createTimer(15.0, (^{
|
|
DDLogError(@"Failed to connect call, aborting!");
|
|
[self end];
|
|
}));
|
|
}
|
|
|
|
-(void) createReconnectingTimeoutTimer
|
|
{
|
|
if(self.cancelDiscoveringTimeout != nil)
|
|
self.cancelDiscoveringTimeout();
|
|
self.cancelDiscoveringTimeout = nil;
|
|
if(self.cancelRingingTimeout != nil)
|
|
self.cancelRingingTimeout();
|
|
self.cancelRingingTimeout = nil;
|
|
if(self.cancelConnectingTimeout != nil)
|
|
self.cancelConnectingTimeout();
|
|
self.cancelConnectingTimeout = nil;
|
|
self.cancelConnectingTimeout = createTimer(45.0, (^{
|
|
DDLogError(@"Failed to connect call, aborting!");
|
|
[self end];
|
|
}));
|
|
}
|
|
|
|
-(void) createRingingTimeoutTimer
|
|
{
|
|
if(self.cancelDiscoveringTimeout != nil)
|
|
self.cancelDiscoveringTimeout();
|
|
self.cancelDiscoveringTimeout = nil;
|
|
if(self.cancelRingingTimeout != nil)
|
|
self.cancelRingingTimeout();
|
|
self.cancelRingingTimeout = nil;
|
|
self.cancelRingingTimeout = createTimer(45.0, (^{
|
|
DDLogError(@"Call not answered in time, aborting!");
|
|
[self end];
|
|
}));
|
|
}
|
|
|
|
-(void) createDiscoveringTimeoutTimer
|
|
{
|
|
if(self.cancelDiscoveringTimeout != nil)
|
|
self.cancelDiscoveringTimeout();
|
|
self.cancelDiscoveringTimeout = nil;
|
|
self.cancelDiscoveringTimeout = createTimer(30.0, (^{
|
|
DDLogError(@"Discovery not answered in time, aborting!");
|
|
[self end];
|
|
}));
|
|
}
|
|
|
|
-(void) establishIncomingConnection
|
|
{
|
|
DDLogInfo(@"Now connecting incoming VoIP call: %@", self);
|
|
[self.webRTCClient configureAudioSession];
|
|
[self createConnectingTimeoutTimer];
|
|
//the remote (e.g. "initiator") will send a jingle "session-initiate" as soon as it receives our jmi proceed
|
|
[self sendJmiProceed];
|
|
}
|
|
|
|
-(void) establishOutgoingConnection
|
|
{
|
|
DDLogInfo(@"Now connecting outgoing VoIP call: %@", self);
|
|
[self.webRTCClient configureAudioSession];
|
|
[self.voipProcessor.cxProvider reportOutgoingCallWithUUID:self.uuid startedConnectingAtDate:nil];
|
|
[self createConnectingTimeoutTimer];
|
|
[self offerSDP];
|
|
}
|
|
|
|
/*
|
|
-(void) restartIce
|
|
{
|
|
if(self.isReconnecting)
|
|
{
|
|
DDLogWarn(@"Not restarting ICE, already reconnecting!");
|
|
return;
|
|
}
|
|
DDLogInfo(@"Restarting ICE...");
|
|
@synchronized(self) {
|
|
self.isConnected = NO;
|
|
self.isReconnecting = YES;
|
|
[self.webRTCClient.peerConnection restartIce];
|
|
|
|
//we have to decide for a prefered direction because otherwise we'd get a webrtc error on incoming sdp:
|
|
//Failed to set remote offer sdp: Called in wrong state: have-local-offer
|
|
if(self.direction == MLCallDirectionOutgoing)
|
|
[self offerSDP];
|
|
|
|
//start connecting timeout if not already running (but leave it running if so, because we don't want to create endless reconnect loops
|
|
if(self.cancelConnectingTimeout == nil)
|
|
[self createReconnectingTimeoutTimer];
|
|
}
|
|
}
|
|
|
|
-(void) handleConnectivityChange:(NSNotification*) notification
|
|
{
|
|
//only handle connectivity change if we switched to unreachable
|
|
if(self.wasConnectedOnce && self.isConnected && !self.isReconnecting && [notification.userInfo[@"reachable"] boolValue] == NO)
|
|
{
|
|
DDLogDebug(@"Connectivity changed, restarting ICE...");
|
|
//this will reconnect and use the (possibly still working) old connection until
|
|
//the new connection is usable, then transparently switch over to the new one
|
|
[self restartIce];
|
|
}
|
|
else
|
|
DDLogDebug(@"Not restarting ICE because of connectivity change: was never connected");
|
|
}
|
|
*/
|
|
|
|
-(void) offerSDP
|
|
{
|
|
//see https://webrtc.googlesource.com/src/+/refs/heads/main/sdk/objc/api/peerconnection/RTCSessionDescription.h
|
|
[self.webRTCClient offerWithCompletion:^(RTCSessionDescription* sdp) {
|
|
DDLogDebug(@"WebRTC reported local SDP '%@', sending to '%@': %@", [RTCSessionDescription stringForType:sdp.type], self.fullRemoteJid, sdp.sdp);
|
|
|
|
NSArray<MLXMLNode*>* children = [HelperTools sdp2xml:sdp.sdp withInitiator:YES];
|
|
if(children.count == 0)
|
|
{
|
|
DDLogError(@"Could not serialize local SDP to XML!");
|
|
[self handleEndCallActionWithReason:MLCallFinishReasonError];
|
|
return;
|
|
}
|
|
|
|
//we don't encrypt anything if encryption is not enabled for this contact or if the remote did not send us their deviceid
|
|
if(self.contact.isEncrypted && self.remoteOmemoDeviceId != nil && [self encryptFingerprintsInChildren:children])
|
|
{
|
|
//we are encrypted now (if the remote can't decrypt this or answers with a cleartext fingerprint, we throw a security error later on)
|
|
self.encryptionState = [self encryptionTypeForDeviceid:self.remoteOmemoDeviceId];
|
|
}
|
|
else
|
|
self.encryptionState = MLCallEncryptionStateClear;
|
|
|
|
XMPPIQ* sdpIQ = [[XMPPIQ alloc] initWithType:kiqSetType to:self.fullRemoteJid];
|
|
[sdpIQ addChildNode:[[MLXMLNode alloc] initWithElement:@"jingle" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{
|
|
@"action": @"session-initiate",
|
|
@"sid": self.jmiid,
|
|
} andChildren:children andData:nil]];
|
|
@synchronized(self.candidateQueueLock) {
|
|
self.localSDP = sdpIQ;
|
|
}
|
|
[self.account sendIq:sdpIQ withResponseHandler:^(XMPPIQ* result) {
|
|
DDLogDebug(@"Received SDP response for offer: %@", result);
|
|
} andErrorHandler:^(XMPPIQ* error) {
|
|
DDLogError(@"Got error for SDP offer: %@", error);
|
|
}];
|
|
}];
|
|
}
|
|
|
|
-(void) sendJmiPropose
|
|
{
|
|
DDLogDebug(@"Proposing new call via JMI: %@", self);
|
|
NSMutableArray* descriptions = [NSMutableArray new];
|
|
[descriptions addObject:[[MLXMLNode alloc] initWithElement:@"description" andNamespace:@"urn:xmpp:jingle:apps:rtp:1" withAttributes:@{@"media": @"audio"} andChildren:@[] andData:nil]];
|
|
if(self.callType == MLCallTypeVideo)
|
|
[descriptions addObject:[[MLXMLNode alloc] initWithElement:@"description" andNamespace:@"urn:xmpp:jingle:apps:rtp:1" withAttributes:@{@"media": @"video"} andChildren:@[] andData:nil]];
|
|
XMPPMessage* jmiNode = [[XMPPMessage alloc] initToContact:self.contact];
|
|
[jmiNode addChildNode:[[MLXMLNode alloc] initWithElement:@"propose" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{
|
|
@"id": self.jmiid,
|
|
} andChildren:descriptions andData:nil]];
|
|
[jmiNode setStoreHint];
|
|
self.jmiPropose = jmiNode;
|
|
[self.account send:jmiNode];
|
|
|
|
//abort if no device responds with "ringing" in time
|
|
[self createDiscoveringTimeoutTimer];
|
|
}
|
|
|
|
-(void) sendJmiReject
|
|
{
|
|
DDLogDebug(@"Rejecting via JMI: %@", self);
|
|
XMPPMessage* jmiNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:self.fullRemoteJid];
|
|
[jmiNode addChildNode:[[MLXMLNode alloc] initWithElement:@"reject" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{
|
|
@"id": self.jmiid,
|
|
} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"reason" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"busy"]
|
|
] andData:nil]
|
|
] andData:nil]];
|
|
[jmiNode setStoreHint];
|
|
[self.account send:jmiNode];
|
|
}
|
|
|
|
-(void) sendJmiRejectWithTieBreak
|
|
{
|
|
DDLogDebug(@"Rejecting with tie-break via JMI: %@", self);
|
|
XMPPMessage* jmiNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:self.fullRemoteJid];
|
|
[jmiNode addChildNode:[[MLXMLNode alloc] initWithElement:@"reject" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{
|
|
@"id": self.jmiid,
|
|
} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"reason" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"expired"]
|
|
] andData:nil],
|
|
[[MLXMLNode alloc] initWithElement:@"tie-break"]
|
|
] andData:nil]];
|
|
[jmiNode setStoreHint];
|
|
[self.account send:jmiNode];
|
|
}
|
|
|
|
-(void) sendJmiRinging
|
|
{
|
|
DDLogDebug(@"Ringing via JMI: %@", self);
|
|
XMPPMessage* jmiNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:self.fullRemoteJid];
|
|
[jmiNode addChildNode:[[MLXMLNode alloc] initWithElement:@"ringing" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{
|
|
@"id": self.jmiid,
|
|
} andChildren:@[] andData:nil]];
|
|
[jmiNode setStoreHint];
|
|
[self.account send:jmiNode];
|
|
}
|
|
|
|
-(void) sendJmiProceed
|
|
{
|
|
DDLogDebug(@"Accepting via JMI: %@", self);
|
|
//xep 0353 mandates bare jid, but daniel will update it to mandate full jid
|
|
XMPPMessage* jmiNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:self.fullRemoteJid];
|
|
MLXMLNode* proceedElement = [[MLXMLNode alloc] initWithElement:@"proceed" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{
|
|
@"id": self.jmiid,
|
|
} andChildren:@[] andData:nil];
|
|
//only offer omemo deviceid for encryption if encryption is enabled for this contact
|
|
if(self.contact.isEncrypted)
|
|
{
|
|
//see https://gist.github.com/iNPUTmice/aa4fc0aeea6ce5fb0e0fe04baca842cd
|
|
[proceedElement addChildNode:[[MLXMLNode alloc] initWithElement:@"device" andNamespace:@"http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification" withAttributes:@{
|
|
@"id": [self.account.omemo getDeviceId],
|
|
} andChildren:@[] andData:nil]];
|
|
}
|
|
[jmiNode addChildNode:proceedElement];
|
|
[jmiNode setStoreHint];
|
|
self.jmiProceed = jmiNode;
|
|
[self.account send:jmiNode];
|
|
}
|
|
|
|
-(void) sendJmiFinishWithReason:(NSString*) reason
|
|
{
|
|
DDLogVerbose(@"Finishing via jingle: %@", self);
|
|
XMPPIQ* jingleIQ = [[XMPPIQ alloc] initWithType:kiqSetType to:self.fullRemoteJid];
|
|
[jingleIQ addChildNode:[[MLXMLNode alloc] initWithElement:@"jingle" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{
|
|
@"action": @"session-terminate",
|
|
@"sid": self.jmiid,
|
|
} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"reason" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:reason]
|
|
] andData:nil]
|
|
] andData:nil]];
|
|
[self.account send:jingleIQ];
|
|
|
|
DDLogDebug(@"Finishing via JMI: %@", self);
|
|
XMPPMessage* jmiNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:self.fullRemoteJid];
|
|
[jmiNode addChildNode:[[MLXMLNode alloc] initWithElement:@"finish" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{
|
|
@"id": self.jmiid,
|
|
} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"reason" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:reason]
|
|
] andData:nil]
|
|
] andData:nil]];
|
|
[jmiNode setStoreHint];
|
|
[self.account send:jmiNode];
|
|
}
|
|
|
|
-(void) sendJmiRetract
|
|
{
|
|
DDLogDebug(@"Retracting via JMI: %@", self);
|
|
XMPPMessage* jmiNode = [[XMPPMessage alloc] initToContact:self.contact];
|
|
[jmiNode addChildNode:[[MLXMLNode alloc] initWithElement:@"retract" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{
|
|
@"id": self.jmiid,
|
|
} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"reason" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"cancel"]
|
|
] andData:nil]
|
|
] andData:nil]];
|
|
[jmiNode setStoreHint];
|
|
[self.account send:jmiNode];
|
|
}
|
|
|
|
-(void) sendJmiRetractWithTieBreak
|
|
{
|
|
DDLogDebug(@"Retracting via JMI: %@", self);
|
|
XMPPMessage* jmiNode = [[XMPPMessage alloc] initToContact:self.contact];
|
|
[jmiNode addChildNode:[[MLXMLNode alloc] initWithElement:@"retract" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{
|
|
@"id": self.jmiid,
|
|
} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"reason" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"cancel"]
|
|
] andData:nil]
|
|
] andData:nil]];
|
|
[jmiNode setStoreHint];
|
|
[self.account send:jmiNode];
|
|
}
|
|
|
|
-(NSString*) description
|
|
{
|
|
NSString* state;
|
|
switch(self.state)
|
|
{
|
|
case MLCallStateDiscovering: state = @"discovering"; break;
|
|
case MLCallStateRinging: state = @"ringing"; break;
|
|
case MLCallStateConnecting: state = @"connecting"; break;
|
|
case MLCallStateReconnecting: state = @"reconnecting"; break;
|
|
case MLCallStateConnected: state = @"connected"; break;
|
|
case MLCallStateFinished: state = @"finished"; break;
|
|
case MLCallStateUnknown: state = @"unknown"; break;
|
|
default: state = @"undefined"; break;
|
|
}
|
|
return [NSString stringWithFormat:@"%@Call:%@",
|
|
self.direction == MLCallDirectionIncoming ? @"Incoming" : @"Outgoing",
|
|
@{
|
|
@"uuid": self.uuid,
|
|
@"jmiid": self.jmiid,
|
|
@"state": state,
|
|
@"finishReason": @(self.finishReason),
|
|
@"durationTime": @(self.durationTime),
|
|
@"contact": nilWrapper(self.contact),
|
|
@"fullRemoteJid": nilWrapper(self.fullRemoteJid),
|
|
@"jmiPropose": nilWrapper(self.jmiPropose),
|
|
@"jmiProceed": nilWrapper(self.jmiProceed),
|
|
@"webRTCClient": nilWrapper(self.webRTCClient),
|
|
@"providerAnswerAction": nilWrapper(self.providerAnswerAction),
|
|
@"wasConnectedOnce": bool2str(self.wasConnectedOnce),
|
|
@"isConnected": bool2str(self.isConnected),
|
|
@"isReconnecting": bool2str(self.isReconnecting),
|
|
@"hasLocalSDP": bool2str(self.localSDP != nil),
|
|
@"hasRemoteSDP": bool2str(self.remoteSDP != nil),
|
|
@"remoteOmemoDeviceId": nilWrapper(self.remoteOmemoDeviceId),
|
|
@"encryptionState": @(self.encryptionState),
|
|
}
|
|
];
|
|
}
|
|
|
|
-(NSString*) short
|
|
{
|
|
return [NSString stringWithFormat:@"%@Call:%@{%@}", self.direction == MLCallDirectionIncoming ? @"Incoming" : @"Outgoing", self.uuid, self.jmiid];
|
|
}
|
|
|
|
-(BOOL) isEqualToContact:(MLContact*) contact
|
|
{
|
|
return [self.contact isEqualToContact:contact];
|
|
}
|
|
|
|
-(BOOL) isEqualToCall:(MLCall*) call
|
|
{
|
|
return [self.uuid isEqual:call.uuid];
|
|
}
|
|
|
|
-(BOOL) isEqual:(id _Nullable) object
|
|
{
|
|
if(object == nil || self == object)
|
|
return YES;
|
|
else if([object isKindOfClass:[MLContact class]])
|
|
return [self isEqualToContact:(MLContact*)object];
|
|
else if([object isKindOfClass:[MLCall class]])
|
|
return [self isEqualToCall:(MLCall*)object];
|
|
else
|
|
return NO;
|
|
}
|
|
|
|
-(NSUInteger) hash
|
|
{
|
|
return [self.uuid hash];
|
|
}
|
|
|
|
#pragma mark - WebRTCClientDelegate
|
|
|
|
-(void) webRTCClient:(WebRTCClient*) webRTCClient didDiscoverLocalCandidate:(RTCIceCandidate*) candidate
|
|
{
|
|
@synchronized(self) {
|
|
if(webRTCClient != self.webRTCClient)
|
|
{
|
|
DDLogDebug(@"%@: Ignoring discovered local ICE candidate: %@ (call migrated)", [self short], candidate);
|
|
return;
|
|
}
|
|
DDLogDebug(@"%@: Discovered local ICE candidate destined for '%@': %@", [self short], self.fullRemoteJid, candidate);
|
|
|
|
//set ufrag to nil, it will be automatically filled via candidate.sdp
|
|
//extract the pwd from our outgoing offer using the sdpMid to identify the correct <content/> element
|
|
NSString* localPwd = [self.localSDP findFirst:@"{urn:xmpp:jingle:1}jingle/content<name=%@>/{urn:xmpp:jingle:transports:ice-udp:1}transport@pwd", candidate.sdpMid];
|
|
MLXMLNode* contentNode = [HelperTools candidate2xml:candidate.sdp withMid:candidate.sdpMid pwd:localPwd ufrag:nil andInitiator:self.direction==MLCallDirectionOutgoing];
|
|
if(contentNode == nil)
|
|
{
|
|
DDLogError(@"Failed to convert raw sdp candidate to jingle, ignoring this candidate: %@", candidate);
|
|
return;
|
|
}
|
|
#ifdef IS_ALPHA
|
|
if([contentNode check:@"{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate<protocol=tcp>"])
|
|
{
|
|
//add tcptype because that attribute is apparently not supported by our mozilla sdp lib
|
|
MLXMLNode* candidateNode = [contentNode findFirst:@"{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"];
|
|
if([candidate.sdp containsString:@"typ host tcptype active"])
|
|
candidateNode.attributes[@"tcptype"] = @"active";
|
|
else if([candidate.sdp containsString:@"typ host tcptype passive"])
|
|
candidateNode.attributes[@"tcptype"] = @"passive";
|
|
else
|
|
DDLogWarn(@"Unknown type-tcptype combination!");
|
|
}
|
|
#else
|
|
if([contentNode check:@"{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate<protocol=tcp>"])
|
|
{
|
|
DDLogError(@"Ignoring raw sdp candidate, because it's using tcp instead of udp: %@", candidate);
|
|
return;
|
|
}
|
|
#endif
|
|
//see https://webrtc.googlesource.com/src/+/refs/heads/main/sdk/objc/api/peerconnection/RTCIceCandidate.h
|
|
XMPPIQ* candidateIq = [[XMPPIQ alloc] initWithType:kiqSetType to:self.fullRemoteJid];
|
|
[candidateIq addChildNode:[[MLXMLNode alloc] initWithElement:@"jingle" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{
|
|
@"action": @"transport-info",
|
|
@"sid": self.jmiid,
|
|
} andChildren:@[contentNode] andData:nil]];
|
|
@synchronized(self.candidateQueueLock) {
|
|
//queue candidate if sdp offer or answer have not been processed yet
|
|
if(self.remoteSDP == nil || self.localSDP == nil)
|
|
{
|
|
DDLogDebug(@"Adding outgoing ICE candidate iq to candidate queue: %@", candidateIq);
|
|
[self.outgoingCandidateQueue addObject:candidateIq];
|
|
return;
|
|
}
|
|
}
|
|
[self.account sendIq:candidateIq withResponseHandler:^(XMPPIQ* result) {
|
|
DDLogDebug(@"%@: Received outgoing ICE candidate result: %@", [self short], result);
|
|
} andErrorHandler:^(XMPPIQ* error) {
|
|
DDLogError(@"%@: Got error for outgoing ICE candidate: %@", [self short], error);
|
|
}];
|
|
}
|
|
}
|
|
|
|
-(void) webRTCClient:(WebRTCClient*) webRTCClient didChangeConnectionState:(RTCIceConnectionState) state
|
|
{
|
|
@synchronized(self) {
|
|
if(webRTCClient != self.webRTCClient)
|
|
{
|
|
DDLogInfo(@"Ignoring new RTCIceConnectionState %ld for webRTCClient: %@ (call migrated)", (long)state, webRTCClient);
|
|
return;
|
|
}
|
|
if(self.isFinished)
|
|
{
|
|
DDLogInfo(@"Ignoring new RTCIceConnectionState %ld for webRTCClient: %@ (call already finished)", (long)state, webRTCClient);
|
|
return;
|
|
}
|
|
//state enums can be found over here: https://chromium.googlesource.com/external/webrtc/+/9eeb6240c93efe2219d4d6f4cf706030e00f64d7/webrtc/sdk/objc/Framework/Headers/WebRTC/RTCPeerConnection.h
|
|
DDLogDebug(@"New RTCIceConnectionState %ld for webRTCClient: %@", (long)state, webRTCClient);
|
|
//we *always* want to cancel the running iceRestart timer once the state changes
|
|
if(self.cancelWaitUntilIceRestart != nil)
|
|
{
|
|
self.cancelWaitUntilIceRestart();
|
|
self.cancelWaitUntilIceRestart = nil;
|
|
}
|
|
switch(state)
|
|
{
|
|
case RTCIceConnectionStateConnected:
|
|
DDLogInfo(@"New WebRTC ICE state: connected, falling through to completed...");
|
|
case RTCIceConnectionStateCompleted:
|
|
DDLogInfo(@"New WebRTC ICE state: completed: %@", self);
|
|
self.isConnected = YES;
|
|
self.isReconnecting = NO;
|
|
//at this stage this means the call is incoming (--> fulfill callkit answer action to update ui to reflect connected call)
|
|
if(self.direction == MLCallDirectionIncoming)
|
|
{
|
|
DDLogInfo(@"Informing CallKit of successful connection of incoming call...");
|
|
[self.providerAnswerAction fulfill];
|
|
}
|
|
//otherwise the call was outgoing (--> initialize callkit ui for outgoing call, we are connected now)
|
|
else
|
|
{
|
|
DDLogInfo(@"Informing CallKit of successful connection of outgoing call...");
|
|
[self.voipProcessor.cxProvider reportOutgoingCallWithUUID:self.uuid connectedAtDate:nil];
|
|
}
|
|
break;
|
|
case RTCIceConnectionStateDisconnected:
|
|
DDLogInfo(@"New WebRTC ICE state: disconnected: %@", self);
|
|
/*if(self.wasConnectedOnce)
|
|
{
|
|
//wait some time before restarting ice (maybe the connection can be reestablished without a new candidate exchange)
|
|
//see: https://groups.google.com/g/discuss-webrtc/c/I4K8NwN4Huw
|
|
//see: https://webrtccourse.com/course/webrtc-codelab/module/fiddle-of-the-month/lesson/ice-restarts/
|
|
//see: https://medium.com/@fippo/ice-restarts-5d759caceda6
|
|
self.cancelWaitUntilIceRestart = createTimer(2.0, (^{
|
|
[self restartIce];
|
|
}));
|
|
}
|
|
else
|
|
[self end];*/
|
|
//wait some time for other jmi and jingle stanzas to arrive (these may contain call end reasons we want to process)
|
|
[self delayedEnd:2.0 withDisconnectedState:YES];
|
|
break;
|
|
case RTCIceConnectionStateFailed:
|
|
DDLogInfo(@"New WebRTC ICE state: failed: %@", self);
|
|
/*if(self.wasConnectedOnce)
|
|
[self restartIce];
|
|
else
|
|
[self end];*/
|
|
self.isConnected = NO; //will result in MLCallFinishReasonConnectivityError if wasConnectedOnce == YES
|
|
[self end];
|
|
break;
|
|
//all following states can be ignored
|
|
case RTCIceConnectionStateClosed:
|
|
DDLogInfo(@"New WebRTC ICE state: closed: %@", self);
|
|
break;
|
|
case RTCIceConnectionStateNew:
|
|
DDLogInfo(@"New WebRTC ICE state: new: %@", self);
|
|
break;
|
|
case RTCIceConnectionStateChecking:
|
|
DDLogInfo(@"New WebRTC ICE state: checking: %@", self);
|
|
break;
|
|
case RTCIceConnectionStateCount:
|
|
DDLogInfo(@"New WebRTC ICE state: count: %@", self);
|
|
break;
|
|
default:
|
|
DDLogInfo(@"New WebRTC ICE state: UNKNOWN: %@", self);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void) webRTCClient:(WebRTCClient*) webRTCClient didReceiveData:(NSData*) data
|
|
{
|
|
if(webRTCClient != self.webRTCClient)
|
|
{
|
|
DDLogDebug(@"Ignoring received WebRTC data: %@ (call migrated)", data);
|
|
return;
|
|
}
|
|
DDLogDebug(@"Received WebRTC data: %@", data);
|
|
}
|
|
|
|
#pragma mark - ICE handling
|
|
|
|
-(void) processIncomingICECandidate:(NSNotification*) notification
|
|
{
|
|
DDLogInfo(@"Got new incoming ICE candidate...");
|
|
xmpp* account = notification.object;
|
|
NSDictionary* userInfo = notification.userInfo;
|
|
//ignore sdp for disabled accounts
|
|
if(account != [[MLXMPPManager sharedInstance] getEnabledAccountForID:account.accountID])
|
|
return;
|
|
//don't use self.account because that asserts on nil
|
|
if(self.contact.account == nil)
|
|
return;
|
|
|
|
XMPPIQ* iqNode = userInfo[@"iqNode"];
|
|
NSString* jmiid = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle@sid"];
|
|
if(![account.accountID isEqualToNumber:self.account.accountID] || ![self.jmiid isEqual:jmiid])
|
|
{
|
|
DDLogInfo(@"Incoming ICE candidate not matching %@, ignoring...", [self short]);
|
|
return;
|
|
}
|
|
|
|
@synchronized(self.candidateQueueLock) {
|
|
//queue candidate if sdp offer or answer have not been processed yet
|
|
if(self.remoteSDP == nil || self.localSDP == nil)
|
|
{
|
|
DDLogDebug(@"Adding incoming ICE candidate iq to candidate queue: %@", iqNode);
|
|
[self.incomingCandidateQueue addObject:iqNode];
|
|
return;
|
|
}
|
|
}
|
|
[self processRemoteICECandidate:iqNode];
|
|
}
|
|
|
|
-(void) processRemoteICECandidate:(XMPPIQ*) iqNode
|
|
{
|
|
RTCIceCandidate* incomingCandidate = nil;
|
|
NSString* rawSdp = [HelperTools xml2candidate:[iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"] withInitiator:self.direction==MLCallDirectionIncoming];
|
|
#ifdef IS_ALPHA
|
|
if([iqNode check:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate<protocol=tcp>"])
|
|
{
|
|
NSString* type = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate@type"];
|
|
NSString* tcptype = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate@tcptype"];
|
|
DDLogDebug(@"Patching raw sdp type=%@ to contain tcptype: %@", type, tcptype);
|
|
rawSdp = [rawSdp stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"typ %@", type] withString:[NSString stringWithFormat:@"typ %@ tcptype %@", type, tcptype]];
|
|
}
|
|
#else
|
|
if([iqNode check:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate<protocol=tcp>"])
|
|
{
|
|
DDLogWarn(@"Got tcp candidate, ignoring: %@", [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/candidate"]);
|
|
rawSdp = nil;
|
|
}
|
|
#endif
|
|
DDLogVerbose(@"Got raw remote sdp: %@", rawSdp);
|
|
if(rawSdp == nil)
|
|
{
|
|
DDLogError(@"Failed to convert jingle candidate to raw sdp!");
|
|
XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
[errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"bad-request" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
] andData:nil]];
|
|
[self.account send:errorIq];
|
|
|
|
//don't be too harsh and not end the call here
|
|
//[self handleEndCallActionWithReason:MLCallFinishReasonError];
|
|
return;
|
|
}
|
|
|
|
//calculate correct mLineIndex by searching for the corresponding mid (e.g. content@name) in the list of contents advertised in our offer
|
|
NSString* sdpMid = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content@name"];
|
|
NSArray<MLXMLNode*>* offeredMedia = [self.remoteSDP find:@"{urn:xmpp:jingle:1}jingle/content"];
|
|
NSUInteger mLineIndex = 0;
|
|
for(; mLineIndex < [offeredMedia count]; mLineIndex++)
|
|
if([sdpMid isEqualToString:offeredMedia[mLineIndex].attributes[@"name"]])
|
|
{
|
|
incomingCandidate = [[RTCIceCandidate alloc] initWithSdp:rawSdp sdpMLineIndex:(int)mLineIndex sdpMid:sdpMid];
|
|
break;
|
|
}
|
|
if(mLineIndex == [offeredMedia count])
|
|
DDLogError(@"Could not find content element with mid='%@' in remoteSDP!", sdpMid);
|
|
|
|
if(incomingCandidate == nil)
|
|
{
|
|
DDLogError(@"incomingCandidate is unexpectedly nil, ignoring!");
|
|
[self.account send:[[XMPPIQ alloc] initAsResponseTo:iqNode]];
|
|
return;
|
|
|
|
// XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
// [errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[
|
|
// [[MLXMLNode alloc] initWithElement:@"bad-request" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
// ] andData:nil]];
|
|
// [self.account send:errorIq];
|
|
//
|
|
// //don't be too harsh and not end the call here
|
|
// //[self handleEndCallActionWithReason:MLCallFinishReasonError];
|
|
// return;
|
|
}
|
|
DDLogInfo(@"%@: Got remote ICE candidate for call: %@", self, incomingCandidate);
|
|
NSString* remoteUfrag = [self.remoteSDP findFirst:@"{urn:xmpp:jingle:1}jingle/content<name=%@>/{urn:xmpp:jingle:transports:ice-udp:1}transport@ufrag", incomingCandidate.sdpMid];
|
|
NSString* remotePwd = [self.remoteSDP findFirst:@"{urn:xmpp:jingle:1}jingle/content<name=%@>/{urn:xmpp:jingle:transports:ice-udp:1}transport@pwd", incomingCandidate.sdpMid];
|
|
NSString* candidateUfrag = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content<name=%@>/{urn:xmpp:jingle:transports:ice-udp:1}transport@ufrag", incomingCandidate.sdpMid];
|
|
NSString* candidatePwd = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/content<name=%@>/{urn:xmpp:jingle:transports:ice-udp:1}transport@pwd", incomingCandidate.sdpMid];
|
|
if(remotePwd == nil || remoteUfrag == nil || ![remoteUfrag isEqualToString:candidateUfrag] || ![remotePwd isEqualToString:candidatePwd])
|
|
{
|
|
DDLogError(@"Jingle incoming candidate has wrong pwd or ufrag: incomingCandidate.ufrag='%@', incomingCandidate.pwd='%@', remoteSDP.ufrag='%@', remoteSDP.pwd='%@'", candidateUfrag, candidatePwd, remoteUfrag, remotePwd);
|
|
XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
[errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"auth"} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"not-authorized" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
] andData:nil]];
|
|
[self.account send:errorIq];
|
|
|
|
//don't be too harsh and not end the call here
|
|
//[self handleEndCallActionWithReason:MLCallFinishReasonError];
|
|
return;
|
|
}
|
|
|
|
weakify(self);
|
|
[self.webRTCClient setRemoteCandidate:incomingCandidate completion:^(id error) {
|
|
strongify(self);
|
|
DDLogDebug(@"Got setRemoteCandidate callback...");
|
|
if(error)
|
|
{
|
|
DDLogError(@"Got error while passing new remote ICE candidate to webRTCClient: %@", error);
|
|
XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
[errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"not-acceptable" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
] andData:nil]];
|
|
[self.account send:errorIq];
|
|
|
|
//don't be too harsh and not end the call here
|
|
//[self handleEndCallActionWithReason:MLCallFinishReasonError];
|
|
}
|
|
else
|
|
{
|
|
DDLogDebug(@"Successfully passed new remote ICE candidate to webRTCClient...");
|
|
[self.account send:[[XMPPIQ alloc] initAsResponseTo:iqNode]];
|
|
}
|
|
}];
|
|
DDLogDebug(@"Leaving method...");
|
|
}
|
|
|
|
-(void) processIncomingSDP:(NSNotification*) notification
|
|
{
|
|
DDLogInfo(@"Got new incoming SDP...");
|
|
xmpp* account = notification.object;
|
|
NSDictionary* userInfo = notification.userInfo;
|
|
//ignore sdp for disabled accounts
|
|
if(account != [[MLXMPPManager sharedInstance] getEnabledAccountForID:account.accountID])
|
|
return;
|
|
//don't use self.account because that asserts on nil
|
|
if(self.contact.account == nil)
|
|
return;
|
|
XMPPIQ* iqNode = userInfo[@"iqNode"];
|
|
|
|
NSString* jmiid = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle@sid"];
|
|
if(![account.accountID isEqualToNumber:self.account.accountID] || ![self.jmiid isEqual:jmiid])
|
|
{
|
|
DDLogInfo(@"Ignoring incoming SDP not matching: %@", self);
|
|
return;
|
|
}
|
|
|
|
//make sure we don't handle incoming sdp twice
|
|
if(self.remoteSDP != nil && [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle<action~^session-(initiate|accept)$>"])
|
|
{
|
|
DDLogWarn(@"Got new remote sdp but we already got one, ignoring! MITM/DDOS??");
|
|
XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
[errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"cancel"} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"conflict" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
] andData:nil]];
|
|
[self.account send:errorIq];
|
|
return;
|
|
}
|
|
|
|
NSString* rawSDP;
|
|
NSString* type;
|
|
if([iqNode check:@"{urn:xmpp:jingle:1}jingle<action~^session-(initiate|accept)$>"])
|
|
{
|
|
if(
|
|
([iqNode findFirst:@"{urn:xmpp:jingle:1}jingle<action=session-accept>"] && self.direction != MLCallDirectionOutgoing) ||
|
|
([iqNode findFirst:@"{urn:xmpp:jingle:1}jingle<action=session-initiate>"] && self.direction != MLCallDirectionIncoming)
|
|
) {
|
|
DDLogWarn(@"Unexpected incoming jingle data direction, ignoring: %@", iqNode);
|
|
return;
|
|
}
|
|
|
|
//don't change iqNode directly to not influence code outside of this method
|
|
iqNode = [iqNode copy];
|
|
//handle candidates in initial sdp (our webrtc lib does not like them --> fake transport-info iqs for these)
|
|
//(candidates in initial jingle are allowed by xep!)
|
|
@synchronized(self.candidateQueueLock) {
|
|
for(MLXMLNode* content in [iqNode find:@"{urn:xmpp:jingle:1}jingle/content"])
|
|
{
|
|
MLXMLNode* transport = [content findFirst:@"{urn:xmpp:jingle:transports:ice-udp:1}transport"];
|
|
for(MLXMLNode* candidate in [transport find:@"{urn:xmpp:jingle:transports:ice-udp:1}candidate"])
|
|
{
|
|
XMPPIQ* fakeCandidateIQ = [[XMPPIQ alloc] initWithType:kiqSetType];
|
|
fakeCandidateIQ.from = self.fullRemoteJid;
|
|
fakeCandidateIQ.to = self.account.connectionProperties.identity.fullJid;
|
|
MLXMLNode* shallowTransport = [transport shallowCopyWithData:YES];
|
|
[shallowTransport addChildNode:[transport removeChildNode:candidate]];
|
|
MLXMLNode* shallowContent = [content shallowCopyWithData:YES];
|
|
[shallowContent addChildNode:shallowTransport];
|
|
[fakeCandidateIQ addChildNode:[[MLXMLNode alloc] initWithElement:@"jingle" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{
|
|
@"action": @"transport-info",
|
|
@"sid": self.jmiid,
|
|
} andChildren:@[shallowContent] andData:nil]];
|
|
DDLogDebug(@"Adding fake candidate iq to candidate queue: %@", fakeCandidateIQ);
|
|
[self.incomingCandidateQueue addObject:fakeCandidateIQ];
|
|
}
|
|
}
|
|
}
|
|
//decrypt fingerprint, if needed (use iqNode copy created above to not influence code outside of this method)
|
|
//only decrypt if encryption is enabled for this contact
|
|
if(self.contact.isEncrypted)
|
|
{
|
|
//if this is a session-initiate and we can decrypt the fingerprint using the given deviceid, this call is encrypted now
|
|
//if we can NOT decrypt anything, but have a remote deviceid (e.g. the iq contains an omemo envelope), this is a security error
|
|
if([iqNode check:@"{urn:xmpp:jingle:1}jingle<action=session-initiate>"])
|
|
{
|
|
//save omemo deviceid if we got a session-initiate for this (incoming) call
|
|
self.remoteOmemoDeviceId = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle<action=session-initiate>/{urn:xmpp:jingle:1}content/{urn:xmpp:jingle:transports:ice-udp:1}transport/{http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification}fingerprint/{eu.siacs.conversations.axolotl}encrypted/header@sid|uint"];
|
|
if(self.remoteOmemoDeviceId != nil)
|
|
{
|
|
if([self decryptFingerprintsInIqNode:iqNode])
|
|
self.encryptionState = [self encryptionTypeForDeviceid:self.remoteOmemoDeviceId];
|
|
else
|
|
{
|
|
DDLogError(@"Could not decrypt remote SDP session-initiate fingerprint with OMEMO!");
|
|
XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
[errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"not-acceptable" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
[[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas" andData:@"Could not decrypt call with OMEMO!"],
|
|
] andData:nil]];
|
|
[self.account send:errorIq];
|
|
|
|
[self handleEndCallActionWithReason:MLCallFinishReasonSecurityError];
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
self.encryptionState = MLCallEncryptionStateClear;
|
|
}
|
|
|
|
//if this is a session-accept after sending an encrypted session-initiate and we can NOT decrypt the fingerprint,
|
|
//this call is a security error (if we can decrypt it, everything is fine and the call is secured)
|
|
if([iqNode check:@"{urn:xmpp:jingle:1}jingle<action=session-accept>"])
|
|
{
|
|
//we don't need to check self.remoteOmemoDeviceId, because self.encryptionState will only be different to
|
|
//MLCallEncryptionStateClear if the deviceid is not nil
|
|
if(self.encryptionState != MLCallEncryptionStateClear && ![self decryptFingerprintsInIqNode:iqNode])
|
|
{
|
|
DDLogError(@"Could not decrypt remote SDP session-accept fingerprint with OMEMO!");
|
|
XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
[errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"not-acceptable" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
[[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas" andData:@"Could not decrypt call with OMEMO!"],
|
|
] andData:nil]];
|
|
[self.account send:errorIq];
|
|
|
|
[self handleEndCallActionWithReason:MLCallFinishReasonSecurityError];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
self.encryptionState = MLCallEncryptionStateClear;
|
|
|
|
//check if the jingle offer/response contains only the media that got advertised in jmi and throw a security error otherwise
|
|
if(self.callType == MLCallTypeAudio && [iqNode check:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:apps:rtp:1}description<media=video>"])
|
|
{
|
|
DDLogError(@"Security: jingle advertises video while jmi only contained audio, aborting call!");
|
|
XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
[errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"not-acceptable" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
[[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas" andData:@"Sent video in jingle, but only advertised audio in jmi!"],
|
|
] andData:nil]];
|
|
[self.account send:errorIq];
|
|
|
|
[self handleEndCallActionWithReason:MLCallFinishReasonSecurityError];
|
|
return;
|
|
}
|
|
|
|
//now handle the jingle offer/response nodes and convert jingle xml to sdp
|
|
if([iqNode findFirst:@"{urn:xmpp:jingle:1}jingle<action=session-accept>"])
|
|
{
|
|
type = @"answer";
|
|
rawSDP = [HelperTools xml2sdp:[iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"] withInitiator:NO];
|
|
}
|
|
else if([iqNode findFirst:@"{urn:xmpp:jingle:1}jingle<action=session-initiate>"])
|
|
{
|
|
type = @"offer";
|
|
rawSDP = [HelperTools xml2sdp:[iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"] withInitiator:YES];
|
|
}
|
|
}
|
|
//handle session-terminate: fake jmi finish message and handle it
|
|
else if([iqNode findFirst:@"{urn:xmpp:jingle:1}jingle<action=session-terminate>"])
|
|
{
|
|
DDLogDebug(@"Got jingle session-terminate, faking incoming jmi:finish for Conversations compatibility...");
|
|
XMPPMessage* jmiNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:self.account.connectionProperties.identity.jid];
|
|
[jmiNode addChildNode:[[MLXMLNode alloc] initWithElement:@"finish" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{
|
|
@"id": self.jmiid,
|
|
} andChildren:[iqNode find:@"{urn:xmpp:jingle:1}jingle<action=session-terminate>/reason"] andData:nil]];
|
|
[jmiNode setStoreHint];
|
|
[self.voipProcessor handleIncomingJMIStanza:jmiNode onAccount:self.account];
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
DDLogWarn(@"Unexpected incoming jingle type, ignoring: %@", iqNode);
|
|
return;
|
|
}
|
|
|
|
DDLogVerbose(@"rawSDP(%@)=%@", type, rawSDP);
|
|
if(rawSDP == nil)
|
|
{
|
|
DDLogError(@"Failed to convert jingle to raw sdp!");
|
|
XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
[errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"bad-request" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
] andData:nil]];
|
|
[self.account send:errorIq];
|
|
|
|
[self handleEndCallActionWithReason:MLCallFinishReasonError];
|
|
return;
|
|
}
|
|
|
|
//convert raw sdp string to RTCSessionDescription object
|
|
RTCSessionDescription* resultSDP = [[RTCSessionDescription alloc] initWithType:[RTCSessionDescription typeForString:type] sdp:rawSDP];
|
|
if(resultSDP == nil)
|
|
{
|
|
DDLogError(@"resultSDP is unexpectedly nil!");
|
|
XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
[errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"bad-request" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
] andData:nil]];
|
|
[self.account send:errorIq];
|
|
|
|
[self handleEndCallActionWithReason:MLCallFinishReasonError];
|
|
return;
|
|
}
|
|
DDLogInfo(@"%@: Got remote SDP for call: %@", self, resultSDP);
|
|
@synchronized(self.candidateQueueLock) {
|
|
self.remoteSDP = iqNode;
|
|
}
|
|
|
|
//this is blocking (e.g. no need for an inner @synchronized)
|
|
weakify(self);
|
|
[self.webRTCClient setRemoteSdp:resultSDP completion:^(id error) {
|
|
strongify(self);
|
|
if(error)
|
|
{
|
|
DDLogError(@"Got error while passing remote SDP to webRTCClient: %@", error);
|
|
XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
[errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"not-acceptable" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
] andData:nil]];
|
|
[self.account send:errorIq];
|
|
|
|
[self handleEndCallActionWithReason:MLCallFinishReasonError];
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
DDLogDebug(@"Successfully passed SDP to webRTCClient...");
|
|
//only send a "session-accept" if the remote is the initiator (e.g. this is an incoming call)
|
|
if(self.direction == MLCallDirectionIncoming)
|
|
{
|
|
[self.webRTCClient answerWithCompletion:^(RTCSessionDescription* localSdp) {
|
|
DDLogDebug(@"Sending SDP answer back...");
|
|
NSArray<MLXMLNode*>* children = [HelperTools sdp2xml:localSdp.sdp withInitiator:NO];
|
|
//we got a session-initiate jingle iq
|
|
//--> self.encryptionState will NOT be MLCallEncryptionStateClear, if that iq contained an encrypted fingerprint,
|
|
//--> self.encryptionState WILL be MLCallEncryptionStateClear, if it did not contain such an encrypted fingerprint
|
|
//(in this case we just don't try to decrypt anything, the call will simply be unencrypted but continue)
|
|
//we don't need to check self.remoteOmemoDeviceId, because self.encryptionState will only be different to
|
|
//MLCallEncryptionStateClear if the deviceid is not nil
|
|
if(self.encryptionState != MLCallEncryptionStateClear && ![self encryptFingerprintsInChildren:children])
|
|
{
|
|
DDLogError(@"Could not encrypt local SDP response fingerprint with OMEMO!");
|
|
XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode];
|
|
[errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[
|
|
[[MLXMLNode alloc] initWithElement:@"not-acceptable" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"],
|
|
[[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas" andData:@"Could not encrypt call with OMEMO!"],
|
|
] andData:nil]];
|
|
[self.account send:errorIq];
|
|
|
|
[self handleEndCallActionWithReason:MLCallFinishReasonSecurityError];
|
|
return;
|
|
}
|
|
[self.account send:[[XMPPIQ alloc] initAsResponseTo:iqNode]];
|
|
|
|
XMPPIQ* sdpIQ = [[XMPPIQ alloc] initWithType:kiqSetType to:self.fullRemoteJid];
|
|
[sdpIQ addChildNode:[[MLXMLNode alloc] initWithElement:@"jingle" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{
|
|
@"action": @"session-accept",
|
|
@"sid": self.jmiid,
|
|
} andChildren:children andData:nil]];
|
|
[self.account send:sdpIQ];
|
|
|
|
@synchronized(self.candidateQueueLock) {
|
|
self.localSDP = sdpIQ;
|
|
|
|
DDLogDebug(@"Now handling queued incoming candidate iqs: %lu", (unsigned long)self.incomingCandidateQueue.count);
|
|
for(XMPPIQ* candidateIq in self.incomingCandidateQueue)
|
|
[self processRemoteICECandidate:candidateIq];
|
|
}
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
[self.account send:[[XMPPIQ alloc] initAsResponseTo:iqNode]];
|
|
@synchronized(self.candidateQueueLock) {
|
|
DDLogDebug(@"Now handling queued incoming candidate iqs: %lu", (unsigned long)self.incomingCandidateQueue.count);
|
|
for(XMPPIQ* candidateIq in self.incomingCandidateQueue)
|
|
[self processRemoteICECandidate:candidateIq];
|
|
}
|
|
}
|
|
@synchronized(self.candidateQueueLock) {
|
|
DDLogDebug(@"Now sending queued outgoing candidate iqs: %lu", (unsigned long)self.outgoingCandidateQueue.count);
|
|
for(XMPPIQ* candidateIq in self.outgoingCandidateQueue)
|
|
[self.account sendIq:candidateIq withResponseHandler:^(XMPPIQ* result) {
|
|
DDLogDebug(@"%@: Received outgoing ICE candidate result: %@", [self short], result);
|
|
} andErrorHandler:^(XMPPIQ* error) {
|
|
DDLogError(@"%@: Got error for outgoing ICE candidate: %@", [self short], error);
|
|
}];
|
|
}
|
|
}
|
|
}];
|
|
DDLogDebug(@"Leaving method...");
|
|
}
|
|
|
|
-(void) handleAudioRouteChangeNotification:(NSNotification*) notification
|
|
{
|
|
DDLogVerbose(@"Audio route changed: %@", notification);
|
|
DDLogVerbose(@"Current audio route: %@", self.audioSession.currentRoute);
|
|
BOOL speaker = NO;
|
|
for(AVAudioSessionPortDescription* port in self.audioSession.currentRoute.outputs)
|
|
if(port.portType == AVAudioSessionPortBuiltInSpeaker)
|
|
speaker = YES;
|
|
|
|
if(speaker)
|
|
self.speaker = YES;
|
|
else
|
|
self.speaker = NO;
|
|
}
|
|
|
|
-(MLCallEncryptionState) encryptionTypeForDeviceid:(NSNumber* _Nonnull) deviceid
|
|
{
|
|
NSNumber* trustLevel = [self.account.omemo getTrustLevelForJid:self.contact.contactJid andDeviceId:deviceid];
|
|
if(trustLevel == nil)
|
|
return MLCallEncryptionStateClear;
|
|
switch(trustLevel.intValue)
|
|
{
|
|
case MLOmemoTrusted: return MLCallEncryptionStateTrusted;
|
|
case MLOmemoToFU: return MLCallEncryptionStateToFU;
|
|
default: return MLCallEncryptionStateClear;
|
|
}
|
|
}
|
|
|
|
-(BOOL) encryptFingerprintsInChildren:(NSArray<MLXMLNode*>*) children
|
|
{
|
|
//don't try to encrypt if the remote deviceid is not trusted
|
|
if([self encryptionTypeForDeviceid:self.remoteOmemoDeviceId] == MLCallEncryptionStateClear)
|
|
return NO;
|
|
|
|
//see https://gist.github.com/iNPUTmice/aa4fc0aeea6ce5fb0e0fe04baca842cd
|
|
BOOL retval = NO;
|
|
for(MLXMLNode* child in children)
|
|
for(MLXMLNode* fingerprint in [child find:@"/{urn:xmpp:jingle:1}content/{urn:xmpp:jingle:transports:ice-udp:1}transport/{urn:xmpp:jingle:apps:dtls:0}fingerprint"])
|
|
{
|
|
MLXMLNode* envelope = [self.account.omemo encryptString:fingerprint.data toDeviceids:@{
|
|
self.contact.contactJid: [NSSet setWithArray:@[self.remoteOmemoDeviceId]],
|
|
}];
|
|
if(envelope == nil)
|
|
{
|
|
DDLogWarn(@"Could not encrypt fingerprint with OMEMO!");
|
|
return NO;
|
|
}
|
|
[fingerprint addChildNode:envelope];
|
|
[fingerprint setXMLNS:@"http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"];
|
|
fingerprint.data = nil;
|
|
retval = YES;
|
|
}
|
|
//this is only true if at least one fingerprint could be found and encrypted (this is normally true)
|
|
return retval;
|
|
}
|
|
|
|
-(BOOL) decryptFingerprintsInIqNode:(XMPPIQ*) iqNode
|
|
{
|
|
//don't try to decrypt if the remote deviceid is not trusted
|
|
if([self encryptionTypeForDeviceid:self.remoteOmemoDeviceId] == MLCallEncryptionStateClear)
|
|
return NO;
|
|
|
|
//see https://gist.github.com/iNPUTmice/aa4fc0aeea6ce5fb0e0fe04baca842cd
|
|
BOOL retval = NO;
|
|
for(MLXMLNode* fingerprintNode in [iqNode find:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/{http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification}fingerprint"])
|
|
{
|
|
//more than one omemo envelope means we are under attack
|
|
if([[fingerprintNode find:@"{eu.siacs.conversations.axolotl}encrypted"] count] > 1)
|
|
{
|
|
DDLogWarn(@"More than one OMEMO envelope found!");
|
|
return NO;
|
|
}
|
|
NSString* decryptedFingerprint = [self.account.omemo decryptOmemoEnvelope:[fingerprintNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted"] forSenderJid:self.contact.contactJid andReturnErrorString:NO];
|
|
if(decryptedFingerprint == nil)
|
|
{
|
|
DDLogWarn(@"Could not decrypt OMEMO encrypted fingerprint!");
|
|
return NO;
|
|
}
|
|
//remove omemo envelope, correct xmlns and add our decrypted fingerprint back in as text content
|
|
[fingerprintNode removeChildNode:[fingerprintNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted"]];
|
|
[fingerprintNode setXMLNS:@"urn:xmpp:jingle:apps:dtls:0"];
|
|
fingerprintNode.data = decryptedFingerprint;
|
|
retval = YES;
|
|
}
|
|
//this is only true if at least one fingerprint could be found and decrypted
|
|
//(that could be false, if the remote did something weird or a MITM changed something)
|
|
return retval;
|
|
}
|
|
|
|
@end
|