// // FLEXNetworkObserver.m // Derived from: // // PDAFNetworkDomainController.m // PonyDebugger // // Created by Mike Lewis on 2/27/12. // // Licensed to Square, Inc. under one or more contributor license agreements. // See the LICENSE file distributed with this work for the terms under // which Square, Inc. licenses this file to you. // // Heavily modified and added to by Tanner Bennett and various other contributors. // git blame details these modifications. // #import "FLEXNetworkObserver.h" #import "FLEXNetworkRecorder.h" #import "FLEXUtility.h" #import "NSUserDefaults+FLEX.h" #import "NSObject+FLEX_Reflection.h" #import "FLEXMethod.h" #import "Firestore.h" #import #import #import #include NSString *const kFLEXNetworkObserverEnabledStateChangedNotification = @"kFLEXNetworkObserverEnabledStateChangedNotification"; typedef void (^NSURLSessionAsyncCompletion)(id fileURLOrData, NSURLResponse *response, NSError *error); typedef NSURLSessionTask * (^NSURLSessionNewTaskMethod)(NSURLSession *, id, NSURLSessionAsyncCompletion); @interface FLEXInternalRequestState : NSObject @property (nonatomic, copy) NSURLRequest *request; @property (nonatomic) NSMutableData *dataAccumulator; @end @implementation FLEXInternalRequestState @end @interface FLEXNetworkObserver (NSURLConnectionHelpers) - (void)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response delegate:(id)delegate; - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response delegate:(id)delegate; - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data delegate:(id)delegate; - (void)connectionDidFinishLoading:(NSURLConnection *)connection delegate:(id)delegate; - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error delegate:(id)delegate; - (void)connectionWillCancel:(NSURLConnection *)connection; @end @interface FLEXNetworkObserver (NSURLSessionTaskHelpers) - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler delegate:(id)delegate; - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler delegate:(id)delegate; - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data delegate:(id)delegate; - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id)delegate; - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error delegate:(id)delegate; - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite delegate:(id)delegate; - (void)URLSession:(NSURLSession *)session task:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location data:(NSData *)data delegate:(id)delegate; - (void)URLSessionTaskWillResume:(NSURLSessionTask *)task; - (void)websocketTask:(NSURLSessionWebSocketTask *)task sendMessagage:(NSURLSessionWebSocketMessage *)message API_AVAILABLE(ios(13.0)); - (void)websocketTaskMessageSendCompletion:(NSURLSessionWebSocketMessage *)message error:(NSError *)error API_AVAILABLE(ios(13.0)); - (void)websocketTask:(NSURLSessionWebSocketTask *)task receiveMessagage:(NSURLSessionWebSocketMessage *)message error:(NSError *)error API_AVAILABLE(ios(13.0)); @end @interface FLEXNetworkObserver () @property (nonatomic) NSMutableDictionary *requestStatesForRequestIDs; @property (nonatomic) dispatch_queue_t queue; @end @implementation FLEXNetworkObserver #pragma mark - Public Methods + (void)setEnabled:(BOOL)enabled { BOOL previouslyEnabled = [self isEnabled]; NSUserDefaults.standardUserDefaults.flex_networkObserverEnabled = enabled; if (enabled) { // Inject if needed. This injection is protected with a dispatch_once, so we're ok calling it multiple times. // By doing the injection lazily, we keep the impact of the tool lower when this feature isn't enabled. [self setNetworkMonitorHooks]; } if (previouslyEnabled != enabled) { [NSNotificationCenter.defaultCenter postNotificationName:kFLEXNetworkObserverEnabledStateChangedNotification object:self]; } } + (BOOL)isEnabled { return NSUserDefaults.standardUserDefaults.flex_networkObserverEnabled; } + (void)load { // We don't want to do the swizzling from +load because not all the // delegate classes we want to hook may be loaded at this point. // However, Firebase classes will definitely be loaded by now, // so we can definitely hook those sooner if need be. dispatch_async(dispatch_get_main_queue(), ^{ if ([self isEnabled]) { [self setNetworkMonitorHooks]; } }); } #pragma mark - Statics + (instancetype)sharedObserver { static FLEXNetworkObserver *sharedObserver = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedObserver = [self new]; }); return sharedObserver; } + (NSString *)nextRequestID { return NSUUID.UUID.UUIDString; } #pragma mark Delegate Injection Convenience Methods /// All swizzled delegate methods should make use of this guard. /// This will prevent duplicated sniffing when the original implementation calls up to a superclass /// implementation which we've also swizzled. The superclass implementation (and implementations in /// classes above that) will be executed without interference if called from the original implementation. + (void)sniffWithoutDuplicationForObject:(NSObject *)object selector:(SEL)selector sniffingBlock:(void (^)(void))sniffingBlock originalImplementationBlock:(void (^)(void))originalImplementationBlock { // If we don't have an object to detect nested calls on, just run the original implementation and bail. // This case can happen if someone besides the URL loading system calls the delegate methods directly. // See https://github.com/Flipboard/FLEX/issues/61 for an example. if (!object) { originalImplementationBlock(); return; } const void *key = selector; // Don't run the sniffing block if we're inside a nested call if (!objc_getAssociatedObject(object, key)) { sniffingBlock(); } // Mark that we're calling through to the original so we can detect nested calls objc_setAssociatedObject(object, key, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); originalImplementationBlock(); objc_setAssociatedObject(object, key, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } #pragma mark - Hooking static void (*_logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$)( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, FIRDocumentSnapshotBlock); static void _logos_method$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, FIRDocumentSnapshotBlock); static void (*_logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$)( _LOGOS_SELF_TYPE_NORMAL FIRQuery * _LOGOS_SELF_CONST, SEL, FIRQuerySnapshotBlock); static void _logos_method$_ungrouped$FIRQuery$getDocumentsWithCompletion$( _LOGOS_SELF_TYPE_NORMAL FIRQuery * _LOGOS_SELF_CONST, SEL, FIRQuerySnapshotBlock); static void (*_logos_orig$_ungrouped$FIRDocumentReference$setData$merge$completion$)( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, NSDictionary *, BOOL, void (^)(NSError *)); static void (*_logos_orig$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$)( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, NSDictionary *, NSArray *, void (^)(NSError *)); static void (*_logos_orig$_ungrouped$FIRDocumentReference$updateData$completion$)( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, NSDictionary *, void (^)(NSError *)); static void (*_logos_orig$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$)( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, void (^)(NSError *)); static void _logos_register_hook(Class _class, SEL _cmd, IMP _new, IMP *_old) { unsigned int _count, _i; Class _searchedClass = _class; Method *_methods; while (_searchedClass) { _methods = class_copyMethodList(_searchedClass, &_count); for (_i = 0; _i < _count; _i++) { if (method_getName(_methods[_i]) == _cmd) { if (_class == _searchedClass) { *_old = method_getImplementation(_methods[_i]); *_old = class_replaceMethod(_class, _cmd, _new, method_getTypeEncoding(_methods[_i])); } else { class_addMethod(_class, _cmd, _new, method_getTypeEncoding(_methods[_i])); } free(_methods); return; } } free(_methods); _searchedClass = class_getSuperclass(_searchedClass); } } static Class _logos_superclass$_ungrouped$FIRDocumentReference; static void (*_logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$)( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST, SEL, FIRDocumentSnapshotBlock); static Class _logos_superclass$_ungrouped$FIRQuery; static void (*_logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$)( _LOGOS_SELF_TYPE_NORMAL FIRQuery * _LOGOS_SELF_CONST, SEL, FIRQuerySnapshotBlock); static Class _logos_superclass$_ungrouped$FIRCollectionReference; static FIRDocumentReference * (*_logos_orig$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$)( _LOGOS_SELF_TYPE_NORMAL FIRCollectionReference * _LOGOS_SELF_CONST, SEL, NSDictionary *, void (^)(NSError *error)); #pragma mark Firebase, Reading Data static void _logos_method$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST self, SEL _cmd, FIRDocumentSnapshotBlock completion) { // Generate transaction ID NSString *requestID = [FLEXNetworkObserver nextRequestID]; // Record transaction start [FLEXNetworkRecorder.defaultRecorder recordFIRDocumentWillFetch:self withTransactionID:requestID]; // Hook callback FIRDocumentSnapshotBlock orig = completion; completion = ^(FIRDocumentSnapshot *document, NSError *error) { [FLEXNetworkRecorder.defaultRecorder recordFIRDocumentDidFetch:document error:error transactionID:requestID]; orig(document, error); }; // Forward invocation (_logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$ ? _logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$ : (__typeof__(_logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRDocumentReference, @selector(getDocumentWithCompletion:)))(self, _cmd, completion); } static void _logos_method$_ungrouped$FIRQuery$getDocumentsWithCompletion$( _LOGOS_SELF_TYPE_NORMAL FIRQuery * _LOGOS_SELF_CONST self, SEL _cmd, FIRQuerySnapshotBlock completion) { // Generate transaction ID NSString *requestID = [FLEXNetworkObserver nextRequestID]; // Record transaction start [FLEXNetworkRecorder.defaultRecorder recordFIRQueryWillFetch:self withTransactionID:requestID]; // Hook callback FIRQuerySnapshotBlock orig = completion; completion = ^(FIRQuerySnapshot *query, NSError *error) { [FLEXNetworkRecorder.defaultRecorder recordFIRQueryDidFetch:query error:error transactionID:requestID]; orig(query, error); }; // Forward invocation (_logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$ ? _logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$ : (__typeof__(_logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRQuery, @selector(getDocumentsWithCompletion:)))(self, _cmd, completion); } #pragma mark Firebase, Writing Data static void _logos_method$_ungrouped$FIRDocumentReference$setData$merge$completion$( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST __unused self, SEL __unused _cmd, NSDictionary * documentData, BOOL merge, void (^completion)(NSError *)) { // Generate transaction ID NSString *requestID = [FLEXNetworkObserver nextRequestID]; // Record transaction start [FLEXNetworkRecorder.defaultRecorder recordFIRWillSetData:self data:documentData merge:@(merge) mergeFields:nil transactionID:requestID ]; // Hook callback void (^orig)(NSError *) = completion; completion = ^(NSError *error) { [FLEXNetworkRecorder.defaultRecorder recordFIRDidSetData:error transactionID:requestID]; orig(error); }; // Forward invocation (_logos_orig$_ungrouped$FIRDocumentReference$setData$merge$completion$ ? _logos_orig$_ungrouped$FIRDocumentReference$setData$merge$completion$ : (__typeof__(_logos_orig$_ungrouped$FIRDocumentReference$setData$merge$completion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRDocumentReference, @selector(setData:merge:completion:)))(self, _cmd, documentData, merge, completion); } static void _logos_method$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST __unused self, SEL __unused _cmd, NSDictionary * documentData, NSArray * mergeFields, void (^completion)(NSError *)) { // Generate transaction ID NSString *requestID = [FLEXNetworkObserver nextRequestID]; // Record transaction start [FLEXNetworkRecorder.defaultRecorder recordFIRWillSetData:self data:documentData merge:nil mergeFields:mergeFields transactionID:requestID ]; // Hook callback void (^orig)(NSError *) = completion; completion = ^(NSError *error) { [FLEXNetworkRecorder.defaultRecorder recordFIRDidSetData:error transactionID:requestID]; orig(error); }; // Forward invocation (_logos_orig$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$ ? _logos_orig$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$ : (__typeof__(_logos_orig$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRDocumentReference, @selector(setData:mergeFields:completion:)))(self, _cmd, documentData, mergeFields, completion); } static void _logos_method$_ungrouped$FIRDocumentReference$updateData$completion$( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST __unused self, SEL __unused _cmd, NSDictionary * fields, void (^completion)(NSError *)) { // Generate transaction ID NSString *requestID = [FLEXNetworkObserver nextRequestID]; // Record transaction start [FLEXNetworkRecorder.defaultRecorder recordFIRWillUpdateData:self fields:fields transactionID:requestID]; // Hook callback void (^orig)(NSError *) = completion; completion = ^(NSError *error) { [FLEXNetworkRecorder.defaultRecorder recordFIRDidUpdateData:error transactionID:requestID]; orig(error); }; // Forward invocation (_logos_orig$_ungrouped$FIRDocumentReference$updateData$completion$ ? _logos_orig$_ungrouped$FIRDocumentReference$updateData$completion$ : (__typeof__(_logos_orig$_ungrouped$FIRDocumentReference$updateData$completion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRDocumentReference, @selector(updateData:completion:)))(self, _cmd, fields, completion); } static void _logos_method$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$( _LOGOS_SELF_TYPE_NORMAL FIRDocumentReference * _LOGOS_SELF_CONST __unused self, SEL __unused _cmd, void (^completion)(NSError *)) { // Generate transaction ID NSString *requestID = [FLEXNetworkObserver nextRequestID]; // Record transaction start [FLEXNetworkRecorder.defaultRecorder recordFIRWillDeleteDocument:self transactionID:requestID]; // Hook callback void (^orig)(NSError *) = completion; completion = ^(NSError *error) { [FLEXNetworkRecorder.defaultRecorder recordFIRDidDeleteDocument:error transactionID:requestID]; orig(error); }; // Forward invocation (_logos_orig$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$ ? _logos_orig$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$ : (__typeof__(_logos_orig$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRDocumentReference, @selector(deleteDocumentWithCompletion:)))(self, _cmd, completion); } static FIRDocumentReference * _logos_method$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$( _LOGOS_SELF_TYPE_NORMAL FIRCollectionReference * _LOGOS_SELF_CONST __unused self, SEL __unused _cmd, NSDictionary * data, void (^completion)(NSError *error)) { // Generate transaction ID NSString *requestID = [FLEXNetworkObserver nextRequestID]; // Hook callback void (^orig)(NSError *) = completion; completion = ^(NSError *error) { [FLEXNetworkRecorder.defaultRecorder recordFIRDidAddDocument:error transactionID:requestID]; orig(error); }; // Forward invocation FIRDocumentReference *ret = (_logos_orig$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$ ? _logos_orig$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$ : (__typeof__(_logos_orig$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$))class_getMethodImplementation(_logos_superclass$_ungrouped$FIRCollectionReference, @selector(addDocumentWithData:completion:)))(self, _cmd, data, completion); // Record transaction start [FLEXNetworkRecorder.defaultRecorder recordFIRWillAddDocument:self document:ret transactionID:requestID]; // Return return ret; } + (void)setNetworkMonitorHooks { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self hookFirebaseThings]; [self injectIntoAllNSURLThings]; }); } + (void)hookFirebaseThings { Class _logos_class$_ungrouped$FIRDocumentReference = objc_getClass("FIRDocumentReference"); _logos_superclass$_ungrouped$FIRDocumentReference = class_getSuperclass(_logos_class$_ungrouped$FIRDocumentReference); Class _logos_class$_ungrouped$FIRQuery = objc_getClass("FIRQuery"); _logos_superclass$_ungrouped$FIRQuery = class_getSuperclass(_logos_class$_ungrouped$FIRQuery); Class _logos_class$_ungrouped$FIRCollectionReference = objc_getClass("FIRCollectionReference"); _logos_superclass$_ungrouped$FIRCollectionReference = class_getSuperclass(_logos_class$_ungrouped$FIRCollectionReference); // Reading // _logos_register_hook( _logos_class$_ungrouped$FIRDocumentReference, @selector(getDocumentWithCompletion:), (IMP)&_logos_method$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$, (IMP *)&_logos_orig$_ungrouped$FIRDocumentReference$getDocumentWithCompletion$ ); _logos_register_hook( _logos_class$_ungrouped$FIRQuery, @selector(getDocumentsWithCompletion:), (IMP)&_logos_method$_ungrouped$FIRQuery$getDocumentsWithCompletion$, (IMP *)&_logos_orig$_ungrouped$FIRQuery$getDocumentsWithCompletion$ ); // Writing // _logos_register_hook( _logos_class$_ungrouped$FIRDocumentReference, @selector(setData:merge:completion:), (IMP)&_logos_method$_ungrouped$FIRDocumentReference$setData$merge$completion$, (IMP *)&_logos_orig$_ungrouped$FIRDocumentReference$setData$merge$completion$ ); _logos_register_hook( _logos_class$_ungrouped$FIRDocumentReference, @selector(setData:mergeFields:completion:), (IMP)&_logos_method$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$, (IMP *)&_logos_orig$_ungrouped$FIRDocumentReference$setData$mergeFields$completion$ ); _logos_register_hook( _logos_class$_ungrouped$FIRDocumentReference, @selector(updateData:completion:), (IMP)&_logos_method$_ungrouped$FIRDocumentReference$updateData$completion$, (IMP *)&_logos_orig$_ungrouped$FIRDocumentReference$updateData$completion$ ); _logos_register_hook( _logos_class$_ungrouped$FIRDocumentReference, @selector(deleteDocumentWithCompletion:), (IMP)&_logos_method$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$, (IMP *)&_logos_orig$_ungrouped$FIRDocumentReference$deleteDocumentWithCompletion$ ); _logos_register_hook( _logos_class$_ungrouped$FIRCollectionReference, @selector(addDocumentWithData:completion:), (IMP)&_logos_method$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$, (IMP *)&_logos_orig$_ungrouped$FIRCollectionReference$addDocumentWithData$completion$ ); } + (void)injectIntoAllNSURLThings { // Only allow swizzling once. static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // Swizzle any classes that implement one of these selectors. const SEL selectors[] = { @selector(connectionDidFinishLoading:), @selector(connection:willSendRequest:redirectResponse:), @selector(connection:didReceiveResponse:), @selector(connection:didReceiveData:), @selector(connection:didFailWithError:), @selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:), @selector(URLSession:dataTask:didReceiveData:), @selector(URLSession:dataTask:didReceiveResponse:completionHandler:), @selector(URLSession:task:didCompleteWithError:), @selector(URLSession:dataTask:didBecomeDownloadTask:), @selector(URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:), @selector(URLSession:downloadTask:didFinishDownloadingToURL:) }; const int numSelectors = sizeof(selectors) / sizeof(SEL); Class *classes = NULL; int numClasses = objc_getClassList(NULL, 0); if (numClasses > 0) { classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses); numClasses = objc_getClassList(classes, numClasses); for (NSInteger classIndex = 0; classIndex < numClasses; ++classIndex) { Class class = classes[classIndex]; if (class == [FLEXNetworkObserver class]) { continue; } // Use the C API rather than NSObject methods to avoid sending messages // to classes we're not interested in swizzling, which could result // in us calling +initialize on potentially uninitialized classes. // NOTE: calling class_getInstanceMethod() DOES send +initialize // to the class. That's why we iterate through the method list. unsigned int methodCount = 0; Method *methods = class_copyMethodList(class, &methodCount); BOOL matchingSelectorFound = NO; for (unsigned int methodIndex = 0; methodIndex < methodCount; methodIndex++) { for (int selectorIndex = 0; selectorIndex < numSelectors; ++selectorIndex) { if (method_getName(methods[methodIndex]) == selectors[selectorIndex]) { [self injectIntoDelegateClass:class]; matchingSelectorFound = YES; break; } } if (matchingSelectorFound) { break; } } free(methods); } free(classes); } [self injectIntoNSURLConnectionCancel]; [self injectIntoNSURLSessionTaskResume]; [self injectIntoNSURLConnectionAsynchronousClassMethod]; [self injectIntoNSURLConnectionSynchronousClassMethod]; [self injectIntoNSURLSessionAsyncDataAndDownloadTaskMethods]; [self injectIntoNSURLSessionAsyncUploadTaskMethods]; if (@available(iOS 13.0, *)) { Class websocketTask = NSClassFromString(@"__NSURLSessionWebSocketTask"); [self injectWebsocketSendMessage:websocketTask]; [self injectWebsocketReceiveMessage:websocketTask]; websocketTask = [NSURLSessionWebSocketTask class]; [self injectWebsocketSendMessage:websocketTask]; [self injectWebsocketReceiveMessage:websocketTask]; } }); } + (void)injectIntoDelegateClass:(Class)cls { // Connections [self injectWillSendRequestIntoDelegateClass:cls]; [self injectDidReceiveDataIntoDelegateClass:cls]; [self injectDidReceiveResponseIntoDelegateClass:cls]; [self injectDidFinishLoadingIntoDelegateClass:cls]; [self injectDidFailWithErrorIntoDelegateClass:cls]; // Sessions [self injectTaskWillPerformHTTPRedirectionIntoDelegateClass:cls]; [self injectTaskDidReceiveDataIntoDelegateClass:cls]; [self injectTaskDidReceiveResponseIntoDelegateClass:cls]; [self injectTaskDidCompleteWithErrorIntoDelegateClass:cls]; [self injectRespondsToSelectorIntoDelegateClass:cls]; // Data tasks [self injectDataTaskDidBecomeDownloadTaskIntoDelegateClass:cls]; // Download tasks [self injectDownloadTaskDidWriteDataIntoDelegateClass:cls]; [self injectDownloadTaskDidFinishDownloadingIntoDelegateClass:cls]; } + (void)injectIntoNSURLConnectionCancel { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [NSURLConnection class]; SEL selector = @selector(cancel); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; Method originalCancel = class_getInstanceMethod(class, selector); void (^swizzleBlock)(NSURLConnection *) = ^(NSURLConnection *slf) { [FLEXNetworkObserver.sharedObserver connectionWillCancel:slf]; ((void(*)(id, SEL))objc_msgSend)( slf, swizzledSelector ); }; IMP implementation = imp_implementationWithBlock(swizzleBlock); class_addMethod(class, swizzledSelector, implementation, method_getTypeEncoding(originalCancel)); Method newCancel = class_getInstanceMethod(class, swizzledSelector); method_exchangeImplementations(originalCancel, newCancel); }); } + (void)injectIntoNSURLSessionTaskResume { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // In iOS 7 resume lives in __NSCFLocalSessionTask // In iOS 8 resume lives in NSURLSessionTask // In iOS 9 resume lives in __NSCFURLSessionTask // In iOS 14 resume lives in NSURLSessionTask Class baseResumeClass = Nil; if (![NSProcessInfo.processInfo respondsToSelector:@selector(operatingSystemVersion)]) { // iOS ... 7 baseResumeClass = NSClassFromString(@"__NSCFLocalSessionTask"); } else { NSInteger majorVersion = NSProcessInfo.processInfo.operatingSystemVersion.majorVersion; if (majorVersion < 9 || majorVersion >= 14) { // iOS 8 or iOS 14+ baseResumeClass = [NSURLSessionTask class]; } else { // iOS 9 ... 13 baseResumeClass = NSClassFromString(@"__NSCFURLSessionTask"); } } // Hook the base implementation of -resume IMP originalResume = [baseResumeClass instanceMethodForSelector:@selector(resume)]; [self swizzleResumeSelector:@selector(resume) forClass:baseResumeClass]; // *Sigh* // // So, multiple versions of AFNetworking 2.5.X swizzle -resume in various and // short-sighted ways. If you look through the version history from 2.5.0 upwards, // you'll see a variety of techniques were tried, including taking a private // subclass of NSURLSessionTask and calling class_addMethod with `originalResume` // below, so that a duplicate implementation of -resume exists in that class. // // This technique in particular is troublesome, because the implementation in // `baseResumeClass` is never called at all, which means our swizzle is never invoked. // // The only solution is a brute-force one: we must loop over the class tree // below `baseResumeClass` and check for all classes that implement `af_resume`. // if the IMP corresponding to that method is equal to `originalResume` then we // swizzle that in addition to swizzling `resume` on `baseResumeClass` above. // // However, we only go to the trouble at all if NSSelectorFromString // can even find an `"af_resume"` selector in the first place. SEL sel_af_resume = NSSelectorFromString(@"af_resume"); if (sel_af_resume) { NSMutableArray *classTree = FLEXGetAllSubclasses(baseResumeClass, NO).mutableCopy; for (NSInteger i = 0; i < classTree.count; i++) { [classTree addObjectsFromArray:FLEXGetAllSubclasses(classTree[i], NO)]; } for (Class current in classTree) { IMP af_resume = [current instanceMethodForSelector:sel_af_resume]; if (af_resume == originalResume) { [self swizzleResumeSelector:sel_af_resume forClass:current]; } } } }); } + (void)swizzleResumeSelector:(SEL)selector forClass:(Class)class { SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; Method originalResume = class_getInstanceMethod(class, selector); IMP implementation = imp_implementationWithBlock(^(NSURLSessionTask *slf) { // iOS's internal HTTP parser finalization code is mysteriously not thread safe, // invoking it asynchronously has a chance to cause a `double free` crash. // This line below will ask for HTTPBody synchronously, make the HTTPParser // parse the request, and cache them in advance. After that the HTTPParser // will be finalized. Make sure other threads inspecting the request // won't trigger a race to finalize the parser. [slf.currentRequest HTTPBody]; [FLEXNetworkObserver.sharedObserver URLSessionTaskWillResume:slf]; ((void(*)(id, SEL))objc_msgSend)( slf, swizzledSelector ); }); class_addMethod(class, swizzledSelector, implementation, method_getTypeEncoding(originalResume)); Method newResume = class_getInstanceMethod(class, swizzledSelector); method_exchangeImplementations(originalResume, newResume); } + (void)injectIntoNSURLConnectionAsynchronousClassMethod { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = objc_getMetaClass(class_getName([NSURLConnection class])); SEL selector = @selector(sendAsynchronousRequest:queue:completionHandler:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; typedef void (^AsyncCompletion)( NSURLResponse *response, NSData *data, NSError *error ); typedef void (^SendAsyncRequestBlock)( Class, NSURLRequest *, NSOperationQueue *, AsyncCompletion ); SendAsyncRequestBlock swizzleBlock = ^(Class slf, NSURLRequest *request, NSOperationQueue *queue, AsyncCompletion completion) { if (FLEXNetworkObserver.isEnabled) { NSString *requestID = [self nextRequestID]; [FLEXNetworkRecorder.defaultRecorder recordRequestWillBeSentWithRequestID:requestID request:request redirectResponse:nil ]; NSString *mechanism = [self mechanismFromClassMethod:selector onClass:class]; [FLEXNetworkRecorder.defaultRecorder recordMechanism:mechanism forRequestID:requestID]; AsyncCompletion wrapper = ^(NSURLResponse *response, NSData *data, NSError *error) { [FLEXNetworkRecorder.defaultRecorder recordResponseReceivedWithRequestID:requestID response:response ]; [FLEXNetworkRecorder.defaultRecorder recordDataReceivedWithRequestID:requestID dataLength:data.length ]; if (error) { [FLEXNetworkRecorder.defaultRecorder recordLoadingFailedWithRequestID:requestID error:error ]; } else { [FLEXNetworkRecorder.defaultRecorder recordLoadingFinishedWithRequestID:requestID responseBody:data ]; } // Call through to the original completion handler if (completion) { completion(response, data, error); } }; ((void(*)(id, SEL, id, id, id))objc_msgSend)( slf, swizzledSelector, request, queue, wrapper ); } else { ((void(*)(id, SEL, id, id, id))objc_msgSend)( slf, swizzledSelector, request, queue, completion ); } }; [FLEXUtility replaceImplementationOfKnownSelector:selector onClass:class withBlock:swizzleBlock swizzledSelector:swizzledSelector ]; }); } + (void)injectIntoNSURLConnectionSynchronousClassMethod { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = objc_getMetaClass(class_getName([NSURLConnection class])); SEL selector = @selector(sendSynchronousRequest:returningResponse:error:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; typedef NSData * (^AsyncCompletion)(Class, NSURLRequest *, NSURLResponse **, NSError **); AsyncCompletion swizzleBlock = ^NSData *(Class slf, NSURLRequest *request, NSURLResponse **response, NSError **error) { NSData *data = nil; if (FLEXNetworkObserver.isEnabled) { NSString *requestID = [self nextRequestID]; [FLEXNetworkRecorder.defaultRecorder recordRequestWillBeSentWithRequestID:requestID request:request redirectResponse:nil ]; NSString *mechanism = [self mechanismFromClassMethod:selector onClass:class]; [FLEXNetworkRecorder.defaultRecorder recordMechanism:mechanism forRequestID:requestID]; NSError *temporaryError = nil; NSURLResponse *temporaryResponse = nil; data = ((id(*)(id, SEL, id, NSURLResponse **, NSError **))objc_msgSend)( slf, swizzledSelector, request, &temporaryResponse, &temporaryError ); [FLEXNetworkRecorder.defaultRecorder recordResponseReceivedWithRequestID:requestID response:temporaryResponse ]; [FLEXNetworkRecorder.defaultRecorder recordDataReceivedWithRequestID:requestID dataLength:data.length ]; if (temporaryError) { [FLEXNetworkRecorder.defaultRecorder recordLoadingFailedWithRequestID:requestID error:temporaryError ]; } else { [FLEXNetworkRecorder.defaultRecorder recordLoadingFinishedWithRequestID:requestID responseBody:data ]; } if (error) { *error = temporaryError; } if (response) { *response = temporaryResponse; } } else { data = ((id(*)(id, SEL, id, NSURLResponse **, NSError **))objc_msgSend)( slf, swizzledSelector, request, response, error ); } return data; }; [FLEXUtility replaceImplementationOfKnownSelector:selector onClass:class withBlock:swizzleBlock swizzledSelector:swizzledSelector ]; }); } + (void)injectIntoNSURLSessionAsyncDataAndDownloadTaskMethods { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [NSURLSession class]; // The method signatures here are close enough that // we can use the same logic to inject into all of them. const SEL selectors[] = { @selector(dataTaskWithRequest:completionHandler:), @selector(dataTaskWithURL:completionHandler:), @selector(downloadTaskWithRequest:completionHandler:), @selector(downloadTaskWithResumeData:completionHandler:), @selector(downloadTaskWithURL:completionHandler:) }; const int numSelectors = sizeof(selectors) / sizeof(SEL); for (int selectorIndex = 0; selectorIndex < numSelectors; selectorIndex++) { SEL selector = selectors[selectorIndex]; SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; if ([FLEXUtility instanceRespondsButDoesNotImplementSelector:selector class:class]) { // iOS 7 does not implement these methods on NSURLSession. We actually want to // swizzle __NSCFURLSession, which we can get from the class of the shared session class = [NSURLSession.sharedSession class]; } typedef NSURLSessionTask * (^NSURLSessionNewTaskMethod)( NSURLSession *, id, NSURLSessionAsyncCompletion ); NSURLSessionNewTaskMethod swizzleBlock = ^NSURLSessionTask *(NSURLSession *slf, id argument, NSURLSessionAsyncCompletion completion) { NSURLSessionTask *task = nil; // Check if network observing is on and a callback was provided if (FLEXNetworkObserver.isEnabled && completion) { NSString *requestID = [self nextRequestID]; NSString *mechanism = [self mechanismFromClassMethod:selector onClass:class]; // "Hook" the completion block NSURLSessionAsyncCompletion completionWrapper = [self asyncCompletionWrapperForRequestID:requestID mechanism:mechanism completion:completion ]; // Call the original method task = ((id(*)(id, SEL, id, id))objc_msgSend)( slf, swizzledSelector, argument, completionWrapper ); [self setRequestID:requestID forConnectionOrTask:task]; } else { // Network observer disabled or no callback provided, // just pass through to the original method task = ((id(*)(id, SEL, id, id))objc_msgSend)( slf, swizzledSelector, argument, completion ); } return task; }; // Actually swizzle [FLEXUtility replaceImplementationOfKnownSelector:selector onClass:class withBlock:swizzleBlock swizzledSelector:swizzledSelector ]; } }); } + (void)injectIntoNSURLSessionAsyncUploadTaskMethods { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [NSURLSession class]; // The method signatures here are close enough that we can use the same logic to inject into both of them. // Note that they have 3 arguments, so we can't easily combine with the data and download method above. typedef NSURLSessionUploadTask *(^UploadTaskMethod)( NSURLSession *, NSURLRequest *, id, NSURLSessionAsyncCompletion ); const SEL selectors[] = { @selector(uploadTaskWithRequest:fromData:completionHandler:), @selector(uploadTaskWithRequest:fromFile:completionHandler:) }; const int numSelectors = sizeof(selectors) / sizeof(SEL); for (int selectorIndex = 0; selectorIndex < numSelectors; selectorIndex++) { SEL selector = selectors[selectorIndex]; SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; if ([FLEXUtility instanceRespondsButDoesNotImplementSelector:selector class:class]) { // iOS 7 does not implement these methods on NSURLSession. We actually want to // swizzle __NSCFURLSession, which we can get from the class of the shared session class = [NSURLSession.sharedSession class]; } UploadTaskMethod swizzleBlock = ^NSURLSessionUploadTask *(NSURLSession * slf, NSURLRequest *request, id argument, NSURLSessionAsyncCompletion completion) { NSURLSessionUploadTask *task = nil; if (FLEXNetworkObserver.isEnabled && completion) { NSString *requestID = [self nextRequestID]; NSString *mechanism = [self mechanismFromClassMethod:selector onClass:class]; NSURLSessionAsyncCompletion completionWrapper = [self asyncCompletionWrapperForRequestID:requestID mechanism:mechanism completion:completion ]; task = ((id(*)(id, SEL, id, id, id))objc_msgSend)( slf, swizzledSelector, request, argument, completionWrapper ); [self setRequestID:requestID forConnectionOrTask:task]; } else { task = ((id(*)(id, SEL, id, id, id))objc_msgSend)( slf, swizzledSelector, request, argument, completion ); } return task; }; [FLEXUtility replaceImplementationOfKnownSelector:selector onClass:class withBlock:swizzleBlock swizzledSelector:swizzledSelector ]; } }); } + (NSString *)mechanismFromClassMethod:(SEL)selector onClass:(Class)class { return [NSString stringWithFormat:@"+[%@ %@]", NSStringFromClass(class), NSStringFromSelector(selector)]; } + (NSURLSessionAsyncCompletion)asyncCompletionWrapperForRequestID:(NSString *)requestID mechanism:(NSString *)mechanism completion:(NSURLSessionAsyncCompletion)completion { NSURLSessionAsyncCompletion completionWrapper = ^(id fileURLOrData, NSURLResponse *response, NSError *error) { [FLEXNetworkRecorder.defaultRecorder recordMechanism:mechanism forRequestID:requestID]; [FLEXNetworkRecorder.defaultRecorder recordResponseReceivedWithRequestID:requestID response:response ]; NSData *data = nil; if ([fileURLOrData isKindOfClass:[NSURL class]]) { data = [NSData dataWithContentsOfURL:fileURLOrData]; } else if ([fileURLOrData isKindOfClass:[NSData class]]) { data = fileURLOrData; } [FLEXNetworkRecorder.defaultRecorder recordDataReceivedWithRequestID:requestID dataLength:data.length ]; if (error) { [FLEXNetworkRecorder.defaultRecorder recordLoadingFailedWithRequestID:requestID error:error ]; } else { [FLEXNetworkRecorder.defaultRecorder recordLoadingFinishedWithRequestID:requestID responseBody:data ]; } // Call through to the original completion handler if (completion) { completion(fileURLOrData, response, error); } }; return completionWrapper; } + (void)injectWillSendRequestIntoDelegateClass:(Class)cls { SEL selector = @selector(connection:willSendRequest:redirectResponse:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; Protocol *protocol = @protocol(NSURLConnectionDataDelegate); protocol = protocol ?: @protocol(NSURLConnectionDelegate); struct objc_method_description methodDescription = protocol_getMethodDescription( protocol, selector, NO, YES ); typedef NSURLRequest *(^WillSendRequestBlock)( id slf, NSURLConnection *connection, NSURLRequest *request, NSURLResponse *response ); WillSendRequestBlock undefinedBlock = ^NSURLRequest *(id slf, NSURLConnection *connection, NSURLRequest *request, NSURLResponse *response) { [FLEXNetworkObserver.sharedObserver connection:connection willSendRequest:request redirectResponse:response delegate:slf ]; return request; }; WillSendRequestBlock implementationBlock = ^NSURLRequest *(id slf, NSURLConnection *connection, NSURLRequest *request, NSURLResponse *response) { __block NSURLRequest *returnValue = nil; [self sniffWithoutDuplicationForObject:connection selector:selector sniffingBlock:^{ undefinedBlock(slf, connection, request, response); } originalImplementationBlock:^{ returnValue = ((id(*)(id, SEL, id, id, id))objc_msgSend)( slf, swizzledSelector, connection, request, response ); }]; return returnValue; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:methodDescription implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectDidReceiveResponseIntoDelegateClass:(Class)cls { SEL selector = @selector(connection:didReceiveResponse:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; Protocol *protocol = @protocol(NSURLConnectionDataDelegate); protocol = protocol ?: @protocol(NSURLConnectionDelegate); struct objc_method_description description = protocol_getMethodDescription( protocol, selector, NO, YES ); typedef void (^DidReceiveResponseBlock)( id slf, NSURLConnection *connection, NSURLResponse *response ); DidReceiveResponseBlock undefinedBlock = ^(id slf, NSURLConnection *connection, NSURLResponse *response) { [FLEXNetworkObserver.sharedObserver connection:connection didReceiveResponse:response delegate:slf ]; }; DidReceiveResponseBlock implementationBlock = ^(id slf, NSURLConnection *connection, NSURLResponse *response) { [self sniffWithoutDuplicationForObject:connection selector:selector sniffingBlock:^{ undefinedBlock(slf, connection, response); } originalImplementationBlock:^{ ((void(*)(id, SEL, id, id))objc_msgSend)( slf, swizzledSelector, connection, response ); }]; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:description implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectDidReceiveDataIntoDelegateClass:(Class)cls { SEL selector = @selector(connection:didReceiveData:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; Protocol *protocol = @protocol(NSURLConnectionDataDelegate); protocol = protocol ?: @protocol(NSURLConnectionDelegate); struct objc_method_description description = protocol_getMethodDescription( protocol, selector, NO, YES ); typedef void (^DidReceiveDataBlock)( id slf, NSURLConnection *connection, NSData *data ); DidReceiveDataBlock undefinedBlock = ^(id slf, NSURLConnection *connection, NSData *data) { [FLEXNetworkObserver.sharedObserver connection:connection didReceiveData:data delegate:slf ]; }; DidReceiveDataBlock implementationBlock = ^(id slf, NSURLConnection *connection, NSData *data) { [self sniffWithoutDuplicationForObject:connection selector:selector sniffingBlock:^{ undefinedBlock(slf, connection, data); } originalImplementationBlock:^{ ((void(*)(id, SEL, id, id))objc_msgSend)( slf, swizzledSelector, connection, data ); }]; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:description implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectDidFinishLoadingIntoDelegateClass:(Class)cls { SEL selector = @selector(connectionDidFinishLoading:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; Protocol *protocol = @protocol(NSURLConnectionDataDelegate); protocol = protocol ?: @protocol(NSURLConnectionDelegate); struct objc_method_description description = protocol_getMethodDescription( protocol, selector, NO, YES ); typedef void (^FinishLoadingBlock)(id slf, NSURLConnection *connection); FinishLoadingBlock undefinedBlock = ^(id slf, NSURLConnection *connection) { [FLEXNetworkObserver.sharedObserver connectionDidFinishLoading:connection delegate:slf]; }; FinishLoadingBlock implementationBlock = ^(id slf, NSURLConnection *connection) { [self sniffWithoutDuplicationForObject:connection selector:selector sniffingBlock:^{ undefinedBlock(slf, connection); } originalImplementationBlock:^{ ((void(*)(id, SEL, id))objc_msgSend)( slf, swizzledSelector, connection ); }]; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:description implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectDidFailWithErrorIntoDelegateClass:(Class)cls { SEL selector = @selector(connection:didFailWithError:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; struct objc_method_description description = protocol_getMethodDescription( @protocol(NSURLConnectionDelegate), selector, NO, YES ); typedef void (^DidFailWithErrorBlock)( id slf, NSURLConnection *connection, NSError *error ); DidFailWithErrorBlock undefinedBlock = ^(id slf, NSURLConnection *connection, NSError *error) { [FLEXNetworkObserver.sharedObserver connection:connection didFailWithError:error delegate:slf ]; }; DidFailWithErrorBlock implementationBlock = ^(id slf, NSURLConnection *connection, NSError *error) { [self sniffWithoutDuplicationForObject:connection selector:selector sniffingBlock:^{ undefinedBlock(slf, connection, error); } originalImplementationBlock:^{ ((void(*)(id, SEL, id, id))objc_msgSend)( slf, swizzledSelector, connection, error ); }]; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:description implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectTaskWillPerformHTTPRedirectionIntoDelegateClass:(Class)cls { SEL selector = @selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; struct objc_method_description description = protocol_getMethodDescription( @protocol(NSURLSessionTaskDelegate), selector, NO, YES ); typedef void (^HTTPRedirectionBlock)(id slf, NSURLSession *session, NSURLSessionTask *task, NSHTTPURLResponse *response, NSURLRequest *newRequest, void(^completionHandler)(NSURLRequest *)); HTTPRedirectionBlock undefinedBlock = ^(id slf, NSURLSession *session, NSURLSessionTask *task, NSHTTPURLResponse *response, NSURLRequest *newRequest, void(^completionHandler)(NSURLRequest *)) { [FLEXNetworkObserver.sharedObserver URLSession:session task:task willPerformHTTPRedirection:response newRequest:newRequest completionHandler:completionHandler delegate:slf ]; completionHandler(newRequest); }; HTTPRedirectionBlock implementationBlock = ^(id slf, NSURLSession *session, NSURLSessionTask *task, NSHTTPURLResponse *response, NSURLRequest *newRequest, void(^completionHandler)(NSURLRequest *)) { [self sniffWithoutDuplicationForObject:session selector:selector sniffingBlock:^{ [FLEXNetworkObserver.sharedObserver URLSession:session task:task willPerformHTTPRedirection:response newRequest:newRequest completionHandler:completionHandler delegate:slf ]; } originalImplementationBlock:^{ ((id(*)(id, SEL, id, id, id, id, void(^)(NSURLRequest *)))objc_msgSend)( slf, swizzledSelector, session, task, response, newRequest, completionHandler ); }]; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:description implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectTaskDidReceiveDataIntoDelegateClass:(Class)cls { SEL selector = @selector(URLSession:dataTask:didReceiveData:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; struct objc_method_description description = protocol_getMethodDescription( @protocol(NSURLSessionDataDelegate), selector, NO, YES ); typedef void (^DidReceiveDataBlock)(id slf, NSURLSession *session, NSURLSessionDataTask *dataTask, NSData *data); DidReceiveDataBlock undefinedBlock = ^(id slf, NSURLSession *session, NSURLSessionDataTask *dataTask, NSData *data) { [FLEXNetworkObserver.sharedObserver URLSession:session dataTask:dataTask didReceiveData:data delegate:slf ]; }; DidReceiveDataBlock implementationBlock = ^(id slf, NSURLSession *session, NSURLSessionDataTask *dataTask, NSData *data) { [self sniffWithoutDuplicationForObject:session selector:selector sniffingBlock:^{ undefinedBlock(slf, session, dataTask, data); } originalImplementationBlock:^{ ((void(*)(id, SEL, id, id, id))objc_msgSend)( slf, swizzledSelector, session, dataTask, data ); }]; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:description implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectDataTaskDidBecomeDownloadTaskIntoDelegateClass:(Class)cls { SEL selector = @selector(URLSession:dataTask:didBecomeDownloadTask:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; struct objc_method_description description = protocol_getMethodDescription( @protocol(NSURLSessionDataDelegate), selector, NO, YES ); typedef void (^DidBecomeDownloadTaskBlock)(id slf, NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLSessionDownloadTask *downloadTask); DidBecomeDownloadTaskBlock undefinedBlock = ^(id slf, NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLSessionDownloadTask *downloadTask) { [FLEXNetworkObserver.sharedObserver URLSession:session dataTask:dataTask didBecomeDownloadTask:downloadTask delegate:slf ]; }; DidBecomeDownloadTaskBlock implementationBlock = ^(id slf, NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLSessionDownloadTask *downloadTask) { [self sniffWithoutDuplicationForObject:session selector:selector sniffingBlock:^{ undefinedBlock(slf, session, dataTask, downloadTask); } originalImplementationBlock:^{ ((void(*)(id, SEL, id, id, id))objc_msgSend)( slf, swizzledSelector, session, dataTask, downloadTask ); }]; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:description implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectTaskDidReceiveResponseIntoDelegateClass:(Class)cls { SEL selector = @selector(URLSession:dataTask:didReceiveResponse:completionHandler:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; struct objc_method_description description = protocol_getMethodDescription( @protocol(NSURLSessionDataDelegate), selector, NO, YES ); typedef void (^DidReceiveResponseBlock)(id slf, NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLResponse *response, void(^completion)(NSURLSessionResponseDisposition)); DidReceiveResponseBlock undefinedBlock = ^(id slf, NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLResponse *response, void(^completion)(NSURLSessionResponseDisposition)) { [FLEXNetworkObserver.sharedObserver URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completion delegate:slf ]; completion(NSURLSessionResponseAllow); }; DidReceiveResponseBlock implementationBlock = ^(id slf, NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLResponse *response, void(^completion)(NSURLSessionResponseDisposition )) { [self sniffWithoutDuplicationForObject:session selector:selector sniffingBlock:^{ [FLEXNetworkObserver.sharedObserver URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completion delegate:slf ]; } originalImplementationBlock:^{ ((void(*)(id, SEL, id, id, id, void(^)(NSURLSessionResponseDisposition)))objc_msgSend)( slf, swizzledSelector, session, dataTask, response, completion ); }]; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:description implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectTaskDidCompleteWithErrorIntoDelegateClass:(Class)cls { SEL selector = @selector(URLSession:task:didCompleteWithError:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; struct objc_method_description description = protocol_getMethodDescription( @protocol(NSURLSessionDataDelegate), selector, NO, YES ); typedef void (^DidCompleteWithErrorBlock)(id slf, NSURLSession *session, NSURLSessionTask *task, NSError *error); DidCompleteWithErrorBlock undefinedBlock = ^(id slf, NSURLSession *session, NSURLSessionTask *task, NSError *error) { [FLEXNetworkObserver.sharedObserver URLSession:session task:task didCompleteWithError:error delegate:slf ]; }; DidCompleteWithErrorBlock implementationBlock = ^(id slf, NSURLSession *session, NSURLSessionTask *task, NSError *error) { [self sniffWithoutDuplicationForObject:session selector:selector sniffingBlock:^{ undefinedBlock(slf, session, task, error); } originalImplementationBlock:^{ ((void(*)(id, SEL, id, id, id))objc_msgSend)( slf, swizzledSelector, session, task, error ); }]; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:description implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } // Used for overriding AFNetworking behavior + (void)injectRespondsToSelectorIntoDelegateClass:(Class)cls { SEL selector = @selector(respondsToSelector:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; //Protocol *protocol = @protocol(NSURLSessionTaskDelegate); Method method = class_getInstanceMethod(cls, selector); struct objc_method_description methodDescription = *method_getDescription(method); typedef BOOL (^RespondsToSelectorImpl)(id self, SEL sel); RespondsToSelectorImpl undefinedBlock = ^(id slf, SEL sel) { return YES; }; RespondsToSelectorImpl implementationBlock = ^(id slf, SEL sel) { if (sel == @selector(URLSession:dataTask:didReceiveResponse:completionHandler:)) { return undefinedBlock(slf, sel); } return ((BOOL(*)(id, SEL, SEL))objc_msgSend)(slf, swizzledSelector, sel); }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:methodDescription implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectDownloadTaskDidFinishDownloadingIntoDelegateClass:(Class)cls { SEL selector = @selector(URLSession:downloadTask:didFinishDownloadingToURL:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; struct objc_method_description description = protocol_getMethodDescription( @protocol(NSURLSessionDownloadDelegate), selector, NO, YES ); typedef void (^DidFinishDownloadingBlock)(id slf, NSURLSession *session, NSURLSessionDownloadTask *task, NSURL *location); DidFinishDownloadingBlock undefinedBlock = ^(id slf, NSURLSession *session, NSURLSessionDownloadTask *task, NSURL *location) { NSData *data = [NSData dataWithContentsOfFile:location.relativePath]; [FLEXNetworkObserver.sharedObserver URLSession:session task:task didFinishDownloadingToURL:location data:data delegate:slf ]; }; DidFinishDownloadingBlock implementationBlock = ^(id slf, NSURLSession *session, NSURLSessionDownloadTask *task, NSURL *location) { [self sniffWithoutDuplicationForObject:session selector:selector sniffingBlock:^{ undefinedBlock(slf, session, task, location); } originalImplementationBlock:^{ ((void(*)(id, SEL, id, id, id))objc_msgSend)( slf, swizzledSelector, session, task, location ); }]; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:description implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectDownloadTaskDidWriteDataIntoDelegateClass:(Class)cls { SEL selector = @selector(URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; struct objc_method_description description = protocol_getMethodDescription( @protocol(NSURLSessionDownloadDelegate), selector, NO, YES ); typedef void (^DidWriteDataBlock)(id slf, NSURLSession *session, NSURLSessionDownloadTask *task, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite); DidWriteDataBlock undefinedBlock = ^(id slf, NSURLSession *session, NSURLSessionDownloadTask *task, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) { [FLEXNetworkObserver.sharedObserver URLSession:session downloadTask:task didWriteData:bytesWritten totalBytesWritten:totalBytesWritten totalBytesExpectedToWrite:totalBytesExpectedToWrite delegate:slf ]; }; DidWriteDataBlock implementationBlock = ^(id slf, NSURLSession *session, NSURLSessionDownloadTask *task, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) { [self sniffWithoutDuplicationForObject:session selector:selector sniffingBlock:^{ undefinedBlock( slf, session, task, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite ); } originalImplementationBlock:^{ ((void(*)(id, SEL, id, id, int64_t, int64_t, int64_t))objc_msgSend)( slf, swizzledSelector, session, task, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite ); }]; }; [FLEXUtility replaceImplementationOfSelector:selector withSelector:swizzledSelector forClass:cls withMethodDescription:description implementationBlock:implementationBlock undefinedBlock:undefinedBlock ]; } + (void)injectWebsocketSendMessage:(Class)cls API_AVAILABLE(ios(13.0)) { SEL selector = @selector(sendMessage:completionHandler:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; typedef void (^SendMessageBlock)( NSURLSessionWebSocketTask *slf, NSURLSessionWebSocketMessage *message, void (^completion)(NSError *error) ); SendMessageBlock implementationBlock = ^( NSURLSessionWebSocketTask *slf, NSURLSessionWebSocketMessage *message, void (^completion)(NSError *error) ) { [FLEXNetworkObserver.sharedObserver websocketTask:slf sendMessagage:message ]; completion = ^(NSError *error) { [FLEXNetworkObserver.sharedObserver websocketTaskMessageSendCompletion:message error:error ]; }; ((void(*)(id, SEL, id, id))objc_msgSend)( slf, swizzledSelector, message, completion ); }; [FLEXUtility replaceImplementationOfKnownSelector:selector onClass:cls withBlock:implementationBlock swizzledSelector:swizzledSelector ]; } + (void)injectWebsocketReceiveMessage:(Class)cls API_AVAILABLE(ios(13.0)) { SEL selector = @selector(receiveMessageWithCompletionHandler:); SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector]; typedef void (^SendMessageBlock)( NSURLSessionWebSocketTask *slf, void (^completion)(NSURLSessionWebSocketMessage *message, NSError *error) ); SendMessageBlock implementationBlock = ^( NSURLSessionWebSocketTask *slf, void (^completion)(NSURLSessionWebSocketMessage *message, NSError *error) ) { id completionHook = ^(NSURLSessionWebSocketMessage *message, NSError *error) { [FLEXNetworkObserver.sharedObserver websocketTask:slf receiveMessagage:message error:error ]; completion(message, error); }; ((void(*)(id, SEL, id))objc_msgSend)( slf, swizzledSelector, completionHook ); }; [FLEXUtility replaceImplementationOfKnownSelector:selector onClass:cls withBlock:implementationBlock swizzledSelector:swizzledSelector ]; } static char const * const kFLEXRequestIDKey = "kFLEXRequestIDKey"; + (NSString *)requestIDForConnectionOrTask:(id)connectionOrTask { NSString *requestID = objc_getAssociatedObject(connectionOrTask, kFLEXRequestIDKey); if (!requestID) { requestID = [self nextRequestID]; [self setRequestID:requestID forConnectionOrTask:connectionOrTask]; } return requestID; } + (void)setRequestID:(NSString *)requestID forConnectionOrTask:(id)connectionOrTask { objc_setAssociatedObject( connectionOrTask, kFLEXRequestIDKey, requestID, OBJC_ASSOCIATION_RETAIN_NONATOMIC ); } #pragma mark - Initialization - (id)init { self = [super init]; if (self) { self.requestStatesForRequestIDs = [NSMutableDictionary new]; self.queue = dispatch_queue_create( "com.flex.FLEXNetworkObserver", DISPATCH_QUEUE_SERIAL ); } return self; } #pragma mark - Private Methods - (void)performBlock:(dispatch_block_t)block { if ([[self class] isEnabled]) { dispatch_async(_queue, block); } } - (FLEXInternalRequestState *)requestStateForRequestID:(NSString *)requestID { FLEXInternalRequestState *requestState = self.requestStatesForRequestIDs[requestID]; if (!requestState) { requestState = [FLEXInternalRequestState new]; [self.requestStatesForRequestIDs setObject:requestState forKey:requestID]; } return requestState; } - (void)removeRequestStateForRequestID:(NSString *)requestID { [self.requestStatesForRequestIDs removeObjectForKey:requestID]; } @end @implementation FLEXNetworkObserver (NSURLConnectionHelpers) - (void)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response delegate:(id)delegate { [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:connection]; FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID]; requestState.request = request; [FLEXNetworkRecorder.defaultRecorder recordRequestWillBeSentWithRequestID:requestID request:request redirectResponse:response ]; NSString *mechanism = [NSString stringWithFormat: @"NSURLConnection (delegate: %@)", [delegate class] ]; [FLEXNetworkRecorder.defaultRecorder recordMechanism:mechanism forRequestID:requestID]; }]; } - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response delegate:(id)delegate { [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:connection]; FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID]; requestState.dataAccumulator = [NSMutableData new]; [FLEXNetworkRecorder.defaultRecorder recordResponseReceivedWithRequestID:requestID response:response ]; }]; } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data delegate:(id)delegate { // Just to be safe since we're doing this async data = [data copy]; [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:connection]; FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID]; [requestState.dataAccumulator appendData:data]; [FLEXNetworkRecorder.defaultRecorder recordDataReceivedWithRequestID:requestID dataLength:data.length ]; }]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection delegate:(id)delegate { [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:connection]; FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID]; [FLEXNetworkRecorder.defaultRecorder recordLoadingFinishedWithRequestID:requestID responseBody:requestState.dataAccumulator ]; [self removeRequestStateForRequestID:requestID]; }]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error delegate:(id)delegate { [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:connection]; FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID]; // Cancellations can occur prior to the willSendRequest:... // NSURLConnection delegate call. These are pretty common // and clutter up the logs. Only record the failure if the // recorder already knows about the request through willSendRequest:... if (requestState.request) { [FLEXNetworkRecorder.defaultRecorder recordLoadingFailedWithRequestID:requestID error:error ]; } [self removeRequestStateForRequestID:requestID]; }]; } - (void)connectionWillCancel:(NSURLConnection *)connection { [self performBlock:^{ // Mimic the behavior of NSURLSession which is to create an error on cancellation. NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : @"cancelled" }; NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo ]; [self connection:connection didFailWithError:error delegate:nil]; }]; } @end @implementation FLEXNetworkObserver (NSURLSessionTaskHelpers) - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler delegate:(id)delegate { [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:task]; [FLEXNetworkRecorder.defaultRecorder recordRequestWillBeSentWithRequestID:requestID request:request redirectResponse:response ]; }]; } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler delegate:(id)delegate { [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:dataTask]; FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID]; requestState.dataAccumulator = [NSMutableData new]; NSString *requestMechanism = [NSString stringWithFormat: @"NSURLSessionDataTask (delegate: %@)", [delegate class] ]; [FLEXNetworkRecorder.defaultRecorder recordMechanism:requestMechanism forRequestID:requestID ]; [FLEXNetworkRecorder.defaultRecorder recordResponseReceivedWithRequestID:requestID response:response ]; }]; } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id)delegate { [self performBlock:^{ // By setting the request ID of the download task to match the data task, // it can pick up where the data task left off. NSString *requestID = [[self class] requestIDForConnectionOrTask:dataTask]; [[self class] setRequestID:requestID forConnectionOrTask:downloadTask]; }]; } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data delegate:(id)delegate { // Just to be safe since we're doing this async data = [data copy]; [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:dataTask]; FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID]; [requestState.dataAccumulator appendData:data]; [FLEXNetworkRecorder.defaultRecorder recordDataReceivedWithRequestID:requestID dataLength:data.length ]; }]; } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error delegate:(id)delegate { [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:task]; FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID]; if (error) { [FLEXNetworkRecorder.defaultRecorder recordLoadingFailedWithRequestID:requestID error:error ]; } else { [FLEXNetworkRecorder.defaultRecorder recordLoadingFinishedWithRequestID:requestID responseBody:requestState.dataAccumulator ]; } [self removeRequestStateForRequestID:requestID]; }]; } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite delegate:(id)delegate { [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:downloadTask]; FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID]; if (!requestState.dataAccumulator) { requestState.dataAccumulator = [NSMutableData new]; [FLEXNetworkRecorder.defaultRecorder recordResponseReceivedWithRequestID:requestID response:downloadTask.response ]; NSString *requestMechanism = [NSString stringWithFormat: @"NSURLSessionDownloadTask (delegate: %@)", [delegate class] ]; [FLEXNetworkRecorder.defaultRecorder recordMechanism:requestMechanism forRequestID:requestID ]; } [FLEXNetworkRecorder.defaultRecorder recordDataReceivedWithRequestID:requestID dataLength:bytesWritten ]; }]; } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location data:(NSData *)data delegate:(id)delegate { data = [data copy]; [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:downloadTask]; FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID]; [requestState.dataAccumulator appendData:data]; }]; } - (void)URLSessionTaskWillResume:(NSURLSessionTask *)task { // Since resume can be called multiple times on the same task, only treat the first resume as // the equivalent to connection:willSendRequest:... [self performBlock:^{ NSString *requestID = [[self class] requestIDForConnectionOrTask:task]; FLEXInternalRequestState *requestState = [self requestStateForRequestID:requestID]; if (!requestState.request) { requestState.request = task.currentRequest; [FLEXNetworkRecorder.defaultRecorder recordRequestWillBeSentWithRequestID:requestID request:task.currentRequest redirectResponse:nil ]; } }]; } - (void)websocketTask:(NSURLSessionWebSocketTask *)task sendMessagage:(NSURLSessionWebSocketMessage *)message { [self performBlock:^{ // NSString *requestID = [[self class] requestIDForConnectionOrTask:task]; [FLEXNetworkRecorder.defaultRecorder recordWebsocketMessageSend:message task:task]; }]; } - (void)websocketTaskMessageSendCompletion:(NSURLSessionWebSocketMessage *)message error:(NSError *)error { [self performBlock:^{ [FLEXNetworkRecorder.defaultRecorder recordWebsocketMessageSendCompletion:message error:error ]; }]; } - (void)websocketTask:(NSURLSessionWebSocketTask *)task receiveMessagage:(NSURLSessionWebSocketMessage *)message error:(NSError *)error { [self performBlock:^{ if (!error && message) { [FLEXNetworkRecorder.defaultRecorder recordWebsocketMessageReceived:message task:task ]; } }]; } @end