295 lines
10 KiB
Objective-C
295 lines
10 KiB
Objective-C
//
|
|
// MLUDPLogger.m
|
|
// monalxmpp
|
|
//
|
|
// Created by Thilo Molitor on 17.08.20.
|
|
// Copyright © 2020 Monal.im. All rights reserved.
|
|
// Based on this gist: https://gist.github.com/ratulSharker/3b6bce0debe77fd96344e14566b23e06
|
|
//
|
|
|
|
#import <Foundation/Foundation.h>
|
|
#import <CoreFoundation/CoreFoundation.h>
|
|
#import <CommonCrypto/CommonDigest.h>
|
|
#import <Network/Network.h>
|
|
#import <zlib.h>
|
|
#import "MLUDPLogger.h"
|
|
#import "HelperTools.h"
|
|
#import "AESGcm.h"
|
|
#import "MLXMPPManager.h"
|
|
#import "MLContact.h"
|
|
#import "xmpp.h"
|
|
|
|
static NSData* _key;
|
|
static volatile MLUDPLogger* _self;
|
|
|
|
@interface MLUDPLogger ()
|
|
{
|
|
volatile nw_connection_t _connection;
|
|
volatile dispatch_queue_t _send_queue;
|
|
volatile NSCondition* _send_condition;
|
|
volatile nw_error_t _last_error;
|
|
volatile u_int64_t _counter;
|
|
}
|
|
+(void) logError:(NSString*) format, ... NS_FORMAT_FUNCTION(1, 2);
|
|
@end
|
|
|
|
|
|
@implementation MLUDPLogger
|
|
|
|
+(void) initialize
|
|
{
|
|
//hash raw key string with sha256 to get the correct 256 bit length needed for AES-256
|
|
//WARNING: THIS DOES NOT ENHANCE ENTROPY!! PLEASE MAKE SURE TO USE A KEY WITH PROPER ENTROPY!!
|
|
NSData* rawKey = [[[HelperTools defaultsDB] stringForKey:@"udpLoggerKey"] dataUsingEncoding:NSUTF8StringEncoding];
|
|
NSMutableData* key = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
|
|
CC_SHA256(rawKey.bytes, (unsigned int)rawKey.length, key.mutableBytes);
|
|
_key = [key copy];
|
|
}
|
|
|
|
+(void) flushWithTimeout:(double) timeout
|
|
{
|
|
if(_self != nil)
|
|
{
|
|
NSCondition* condition = [NSCondition new];
|
|
//this timeout will trigger if the flush could not be finished in time (leeway of 10ms)
|
|
//use dispatch_source_set_timer() directly instead of createTimer() because we don't want to log anything in here
|
|
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
|
|
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)), DISPATCH_TIME_FOREVER, (uint64_t)(0.010 * NSEC_PER_SEC));
|
|
dispatch_source_set_event_handler(timer, ^{
|
|
[[self class] logError:@"flush timer triggered!"];
|
|
dispatch_source_cancel(timer);
|
|
[condition lock];
|
|
[condition signal];
|
|
[condition unlock];
|
|
});
|
|
dispatch_resume(timer);
|
|
//this block will be executed if all prior blocks managed to send out their messages (e.g. queue is flushed)
|
|
dispatch_async(_self->_send_queue, ^{
|
|
[[self class] logError:@"flush succeeded in time"];
|
|
dispatch_source_cancel(timer); //stop timer
|
|
[condition lock];
|
|
[condition signal];
|
|
[condition unlock];
|
|
});
|
|
//wait for either timeout or flush to trigger
|
|
[condition lock];
|
|
[condition wait];
|
|
[condition unlock];
|
|
}
|
|
}
|
|
|
|
-(void) dealloc
|
|
{
|
|
_self = nil;
|
|
}
|
|
|
|
-(void) didAddLogger
|
|
{
|
|
_self = self;
|
|
_send_condition = [NSCondition new];
|
|
_send_queue = dispatch_queue_create("MLUDPLoggerSendQueue", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
|
|
}
|
|
|
|
-(void) willRemoveLogger
|
|
{
|
|
_self = nil;
|
|
}
|
|
|
|
+(void) logError:(NSString*) format, ... NS_FORMAT_FUNCTION(1, 2)
|
|
{
|
|
#ifdef IS_ALPHA
|
|
va_list args;
|
|
va_start(args, format);
|
|
NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
|
|
va_end(args);
|
|
|
|
NSLog(@"MLUDPLogger: %@", message);
|
|
|
|
/*
|
|
//log error in 250ms
|
|
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
|
|
dispatch_source_set_timer(timer,
|
|
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.250*NSEC_PER_SEC)),
|
|
DISPATCH_TIME_FOREVER,
|
|
(uint64_t)0);
|
|
dispatch_source_set_event_handler(timer, ^{
|
|
DDLogError(@"%@", message);
|
|
});
|
|
*/
|
|
#endif
|
|
}
|
|
|
|
//code taken from here: https://stackoverflow.com/a/11389847/3528174
|
|
-(NSData*) gzipDeflate:(NSData*) data
|
|
{
|
|
if([data length] == 0)
|
|
return data;
|
|
|
|
z_stream strm;
|
|
|
|
strm.zalloc = Z_NULL;
|
|
strm.zfree = Z_NULL;
|
|
strm.opaque = Z_NULL;
|
|
strm.total_out = 0;
|
|
strm.next_in = (Bytef*)[data bytes];
|
|
strm.avail_in = (unsigned int)[data length];
|
|
|
|
// Compresssion Levels:
|
|
// Z_NO_COMPRESSION
|
|
// Z_BEST_SPEED
|
|
// Z_BEST_COMPRESSION
|
|
// Z_DEFAULT_COMPRESSION
|
|
if(deflateInit2(&strm, Z_BEST_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY) != Z_OK)
|
|
{
|
|
[[self class] logError:@"gzipDeflate error"];
|
|
return nil;
|
|
}
|
|
|
|
NSMutableData* compressed = [NSMutableData dataWithLength:16384]; // 16K chunks for expansion
|
|
do {
|
|
if(strm.total_out >= [compressed length])
|
|
[compressed increaseLengthBy:16384];
|
|
strm.next_out = [compressed mutableBytes] + strm.total_out;
|
|
strm.avail_out = (unsigned int)([compressed length] - strm.total_out);
|
|
deflate(&strm, Z_FINISH);
|
|
} while(strm.avail_out == 0);
|
|
deflateEnd(&strm);
|
|
|
|
[compressed setLength:strm.total_out];
|
|
return compressed;
|
|
}
|
|
|
|
-(void) disconnect
|
|
{
|
|
if(_connection != NULL)
|
|
nw_connection_force_cancel(_connection);
|
|
_connection = NULL;
|
|
[_send_condition lock];
|
|
[_send_condition signal];
|
|
[_send_condition unlock];
|
|
}
|
|
|
|
-(void) createConnectionIfNeeded
|
|
{
|
|
if(_connection == NULL)
|
|
{
|
|
__block NSCondition* condition = [NSCondition new];
|
|
|
|
nw_endpoint_t endpoint = nw_endpoint_create_host([[[HelperTools defaultsDB] stringForKey:@"udpLoggerHostname"] cStringUsingEncoding:NSUTF8StringEncoding], [[[HelperTools defaultsDB] stringForKey:@"udpLoggerPort"] cStringUsingEncoding:NSUTF8StringEncoding]);
|
|
nw_parameters_t parameters = nw_parameters_create_secure_udp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION);
|
|
|
|
_connection = nw_connection_create(endpoint, parameters);
|
|
nw_connection_set_queue(_connection, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0));
|
|
nw_connection_set_state_changed_handler(_connection, ^(nw_connection_state_t state, nw_error_t error) {
|
|
if(state == nw_connection_state_ready)
|
|
{
|
|
[condition lock];
|
|
[condition signal];
|
|
[condition unlock];
|
|
}
|
|
//udp connections should be "established" in way less than 100ms, so unlock this (dispatch) queue after 100ms
|
|
//the connection blocking longer mostly happens if the device has no connectivity (state waiting)
|
|
else if(state == nw_connection_state_waiting || state == nw_connection_state_preparing)
|
|
{
|
|
usleep(100000);
|
|
[condition lock];
|
|
[condition signal];
|
|
[condition unlock];
|
|
}
|
|
//retry in all error cases
|
|
else if(state == nw_connection_state_failed || state == nw_connection_state_cancelled || state == nw_connection_state_invalid)
|
|
{
|
|
self->_last_error = error;
|
|
[[self class] logError:@"connect error: %@", error];
|
|
self->_connection = NULL;
|
|
[condition lock];
|
|
[condition signal];
|
|
[condition unlock];
|
|
[self->_send_condition lock];
|
|
[self->_send_condition signal];
|
|
[self->_send_condition unlock];
|
|
}
|
|
});
|
|
[condition lock];
|
|
nw_connection_start(_connection);
|
|
[condition wait];
|
|
[condition unlock];
|
|
|
|
//try again if we did not succeed
|
|
if(_connection == NULL)
|
|
{
|
|
[[self class] logError:@"retrying connection start..."];
|
|
[self createConnectionIfNeeded];
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void) logMessage:(DDLogMessage*) logMessage
|
|
{
|
|
static uint64_t counter = 0;
|
|
|
|
//early return if deactivated
|
|
if(![[HelperTools defaultsDB] boolForKey: @"udpLoggerEnabled"])
|
|
return;
|
|
|
|
NSError* error = nil;
|
|
NSData* rawData = [HelperTools convertLogmessageToJsonData:logMessage counter:&counter andError:&error];
|
|
if(error != nil || rawData == nil)
|
|
{
|
|
[[self class] logError:@"json encode error: %@", error];
|
|
return;
|
|
}
|
|
|
|
//compress data to account for udp size limits
|
|
rawData = [self gzipDeflate:rawData];
|
|
|
|
//encrypt rawData using the "derived" key (see warning above!)
|
|
MLEncryptedPayload* payload = [AESGcm encrypt:rawData withKey:_key];
|
|
NSMutableData* data = [NSMutableData dataWithData:payload.iv];
|
|
[data appendData:payload.authTag];
|
|
[data appendData:payload.body];
|
|
|
|
if(data.length > 65000)
|
|
[[self class] logError:@"not sending message, too big: %lu", (unsigned long)data.length];
|
|
else
|
|
dispatch_async(_send_queue, ^{
|
|
[self sendData:data withOriginalMessage:logMessage->_message];
|
|
});
|
|
}
|
|
|
|
-(void) sendData:(NSData*) data withOriginalMessage:(NSString*) msg
|
|
{
|
|
[self createConnectionIfNeeded];
|
|
|
|
//the call to dispatch_get_main_queue() is a dummy because we are using DISPATCH_DATA_DESTRUCTOR_DEFAULT which is performed inline
|
|
[_send_condition lock];
|
|
nw_connection_send(_connection, dispatch_data_create(data.bytes, data.length, dispatch_get_main_queue(), DISPATCH_DATA_DESTRUCTOR_DEFAULT), NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, ^(nw_error_t _Nullable error) {
|
|
self->_last_error = error;
|
|
if(error != NULL)
|
|
{
|
|
//NSError* st_error = (NSError*)CFBridgingRelease(nw_error_copy_cf_error(error));
|
|
[[self class] logError:@"send error: %@\n%@", error, msg];
|
|
|
|
}
|
|
//[[self class] logError:@"unlocking send condition (%@)...", [NSNumber numberWithUnsignedLongLong:self->_counter]];
|
|
[self->_send_condition lock];
|
|
[self->_send_condition signal];
|
|
[self->_send_condition unlock];
|
|
});
|
|
//block this queue until our udp message was sent or an error occured
|
|
[_send_condition wait];
|
|
[_send_condition unlock];
|
|
if(_last_error != NULL)
|
|
{
|
|
//don't retry if message was too long
|
|
if([@"Message too long" isEqualToString:[NSString stringWithFormat:@"%@", _last_error]])
|
|
return;
|
|
//retry
|
|
//[self disconnect];
|
|
[[self class] logError:@"retrying sendData with error: %@", _last_error];
|
|
[self sendData:data withOriginalMessage:msg];
|
|
}
|
|
}
|
|
|
|
@end
|