diff --git a/Classes/Network/FLEXNetworkRecorder.h b/Classes/Network/FLEXNetworkRecorder.h index dd139285cf..704e1c6b1c 100644 --- a/Classes/Network/FLEXNetworkRecorder.h +++ b/Classes/Network/FLEXNetworkRecorder.h @@ -14,7 +14,7 @@ extern NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification; extern NSString *const kFLEXNetworkRecorderUserInfoTransactionKey; extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification; -@class FLEXNetworkTransaction; +@class FLEXNetworkTransaction, FLEXHTTPTransaction, FLEXWebsocketTransaction; @interface FLEXNetworkRecorder : NSObject @@ -37,19 +37,21 @@ extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification; - (void)synchronizeDenylist; -// Accessing recorded network activity +#pragma mark Accessing recorded network activity -/// Array of FLEXNetworkTransaction objects ordered by start time with the newest first. -- (NSArray *)networkTransactions; +/// Array of FLEXHTTPTransaction objects ordered by start time with the newest first. +@property (nonatomic, readonly) NSArray *HTTPTransactions; +/// Array of FLEXWebsocketTransaction objects ordered by start time with the newest first. +@property (nonatomic, readonly) NSArray *websocketTransactions API_AVAILABLE(ios(13.0)); /// The full response data IFF it hasn't been purged due to memory pressure. -- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction; +- (NSData *)cachedResponseBodyForTransaction:(FLEXHTTPTransaction *)transaction; /// Dumps all network transactions and cached response bodies. - (void)clearRecordedActivity; -// Recording network activity +#pragma mark Recording network activity /// Call when app is about to send HTTP request. - (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID @@ -72,4 +74,12 @@ extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification; /// This string can be set to anything useful about the API used to make the request. - (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID; +- (void)recordWebsocketMessageSend:(NSURLSessionWebSocketMessage *)message + task:(NSURLSessionWebSocketTask *)task API_AVAILABLE(ios(13.0)); +- (void)recordWebsocketMessageSendCompletion:(NSURLSessionWebSocketMessage *)message + error:(NSError *)error API_AVAILABLE(ios(13.0)); + +- (void)recordWebsocketMessageReceived:(NSURLSessionWebSocketMessage *)message + task:(NSURLSessionWebSocketTask *)task API_AVAILABLE(ios(13.0)); + @end diff --git a/Classes/Network/FLEXNetworkRecorder.m b/Classes/Network/FLEXNetworkRecorder.m index c5af497642..2a59e2aa30 100644 --- a/Classes/Network/FLEXNetworkRecorder.m +++ b/Classes/Network/FLEXNetworkRecorder.m @@ -12,6 +12,7 @@ #import "FLEXUtility.h" #import "FLEXResources.h" #import "NSUserDefaults+FLEX.h" +#import "OSCache.h" NSString *const kFLEXNetworkRecorderNewTransactionNotification = @"kFLEXNetworkRecorderNewTransactionNotification"; NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification = @"kFLEXNetworkRecorderTransactionUpdatedNotification"; @@ -22,9 +23,10 @@ @interface FLEXNetworkRecorder () -@property (nonatomic) NSCache *responseCache; -@property (nonatomic) NSMutableArray *orderedTransactions; -@property (nonatomic) NSMutableDictionary *requestIDsToTransactions; +@property (nonatomic) OSCache *restCache; +@property (nonatomic) NSMutableArray *orderedHTTPTransactions; +@property (nonatomic) NSMutableArray *orderedWSTransactions; +@property (nonatomic) NSMutableDictionary *requestIDsToHTTPTransactions; @property (nonatomic) dispatch_queue_t queue; @end @@ -34,17 +36,18 @@ @implementation FLEXNetworkRecorder - (instancetype)init { self = [super init]; if (self) { - self.responseCache = [NSCache new]; + self.restCache = [OSCache new]; NSUInteger responseCacheLimit = [[NSUserDefaults.standardUserDefaults objectForKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey] unsignedIntegerValue ]; // Default to 25 MB max. The cache will purge earlier if there is memory pressure. - self.responseCache.totalCostLimit = responseCacheLimit ?: 25 * 1024 * 1024; - [self.responseCache setTotalCostLimit:responseCacheLimit]; + self.restCache.totalCostLimit = responseCacheLimit ?: 25 * 1024 * 1024; + [self.restCache setTotalCostLimit:responseCacheLimit]; - self.orderedTransactions = [NSMutableArray new]; - self.requestIDsToTransactions = [NSMutableDictionary new]; + self.orderedWSTransactions = [NSMutableArray new]; + self.orderedHTTPTransactions = [NSMutableArray new]; + self.requestIDsToHTTPTransactions = [NSMutableDictionary new]; self.hostDenylist = NSUserDefaults.standardUserDefaults.flex_networkHostDenylist.mutableCopy; // Serial queue used because we use mutable objects that are not thread safe @@ -67,34 +70,34 @@ + (instancetype)defaultRecorder { #pragma mark - Public Data Access - (NSUInteger)responseCacheByteLimit { - return self.responseCache.totalCostLimit; + return self.restCache.totalCostLimit; } - (void)setResponseCacheByteLimit:(NSUInteger)responseCacheByteLimit { - self.responseCache.totalCostLimit = responseCacheByteLimit; + self.restCache.totalCostLimit = responseCacheByteLimit; [NSUserDefaults.standardUserDefaults setObject:@(responseCacheByteLimit) forKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey ]; } -- (NSArray *)networkTransactions { - __block NSArray *transactions = nil; - dispatch_sync(self.queue, ^{ - transactions = self.orderedTransactions.copy; - }); - return transactions; +- (NSArray *)HTTPTransactions { + return self.orderedHTTPTransactions.copy; +} + +- (NSArray *)websocketTransactions { + return self.orderedWSTransactions.copy; } -- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction { - return [self.responseCache objectForKey:transaction.requestID]; +- (NSData *)cachedResponseBodyForTransaction:(FLEXHTTPTransaction *)transaction { + return [self.restCache objectForKey:transaction.requestID]; } - (void)clearRecordedActivity { dispatch_async(self.queue, ^{ - [self.responseCache removeAllObjects]; - [self.orderedTransactions removeAllObjects]; - [self.requestIDsToTransactions removeAllObjects]; + [self.restCache removeAllObjects]; + [self.orderedHTTPTransactions removeAllObjects]; + [self.requestIDsToHTTPTransactions removeAllObjects]; [self notify:kFLEXNetworkRecorderTransactionsClearedNotification transaction:nil]; }); @@ -102,8 +105,8 @@ - (void)clearRecordedActivity { - (void)clearExcludedTransactions { dispatch_sync(self.queue, ^{ - self.orderedTransactions = ({ - [self.orderedTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *ta, NSUInteger idx) { + self.orderedHTTPTransactions = ({ + [self.orderedHTTPTransactions flex_filtered:^BOOL(FLEXHTTPTransaction *ta, NSUInteger idx) { NSString *host = ta.request.URL.host; for (NSString *excluded in self.hostDenylist) { if ([host hasSuffix:excluded]) { @@ -132,22 +135,17 @@ - (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID } } - // Before async block to stay accurate - NSDate *startDate = [NSDate date]; + FLEXHTTPTransaction *transaction = [FLEXHTTPTransaction request:request identifier:requestID]; + // Before async block to keep times accurate if (redirectResponse) { [self recordResponseReceivedWithRequestID:requestID response:redirectResponse]; [self recordLoadingFinishedWithRequestID:requestID responseBody:nil]; } dispatch_async(self.queue, ^{ - FLEXNetworkTransaction *transaction = [FLEXNetworkTransaction new]; - transaction.requestID = requestID; - transaction.request = request; - transaction.startTime = startDate; - - [self.orderedTransactions insertObject:transaction atIndex:0]; - [self.requestIDsToTransactions setObject:transaction forKey:requestID]; + [self.orderedHTTPTransactions insertObject:transaction atIndex:0]; + [self.requestIDsToHTTPTransactions setObject:transaction forKey:requestID]; transaction.transactionState = FLEXNetworkTransactionStateAwaitingResponse; [self postNewTransactionNotificationWithTransaction:transaction]; @@ -159,7 +157,7 @@ - (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSUR NSDate *responseDate = [NSDate date]; dispatch_async(self.queue, ^{ - FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID]; + FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID]; if (!transaction) { return; } @@ -174,7 +172,7 @@ - (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSUR - (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength { dispatch_async(self.queue, ^{ - FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID]; + FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID]; if (!transaction) { return; } @@ -188,7 +186,7 @@ - (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(N NSDate *finishedDate = [NSDate date]; dispatch_async(self.queue, ^{ - FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID]; + FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID]; if (!transaction) { return; } @@ -205,7 +203,7 @@ - (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(N } if (shouldCache) { - [self.responseCache setObject:responseBody forKey:requestID cost:responseBody.length]; + [self.restCache setObject:responseBody forKey:requestID cost:responseBody.length]; } NSString *mimeType = transaction.response.MIMEType; @@ -213,32 +211,32 @@ - (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(N // Thumbnail image previews on a separate background queue dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSInteger maxPixelDimension = UIScreen.mainScreen.scale * 32.0; - transaction.responseThumbnail = [FLEXUtility + transaction.thumbnail = [FLEXUtility thumbnailedImageWithMaxPixelDimension:maxPixelDimension fromImageData:responseBody ]; [self postUpdateNotificationForTransaction:transaction]; }); } else if ([mimeType isEqual:@"application/json"]) { - transaction.responseThumbnail = FLEXResources.jsonIcon; + transaction.thumbnail = FLEXResources.jsonIcon; } else if ([mimeType isEqual:@"text/plain"]){ - transaction.responseThumbnail = FLEXResources.textPlainIcon; + transaction.thumbnail = FLEXResources.textPlainIcon; } else if ([mimeType isEqual:@"text/html"]) { - transaction.responseThumbnail = FLEXResources.htmlIcon; + transaction.thumbnail = FLEXResources.htmlIcon; } else if ([mimeType isEqual:@"application/x-plist"]) { - transaction.responseThumbnail = FLEXResources.plistIcon; + transaction.thumbnail = FLEXResources.plistIcon; } else if ([mimeType isEqual:@"application/octet-stream"] || [mimeType isEqual:@"application/binary"]) { - transaction.responseThumbnail = FLEXResources.binaryIcon; + transaction.thumbnail = FLEXResources.binaryIcon; } else if ([mimeType containsString:@"javascript"]) { - transaction.responseThumbnail = FLEXResources.jsIcon; + transaction.thumbnail = FLEXResources.jsIcon; } else if ([mimeType containsString:@"xml"]) { - transaction.responseThumbnail = FLEXResources.xmlIcon; + transaction.thumbnail = FLEXResources.xmlIcon; } else if ([mimeType hasPrefix:@"audio"]) { - transaction.responseThumbnail = FLEXResources.audioIcon; + transaction.thumbnail = FLEXResources.audioIcon; } else if ([mimeType hasPrefix:@"video"]) { - transaction.responseThumbnail = FLEXResources.videoIcon; + transaction.thumbnail = FLEXResources.videoIcon; } else if ([mimeType hasPrefix:@"text"]) { - transaction.responseThumbnail = FLEXResources.textIcon; + transaction.thumbnail = FLEXResources.textIcon; } [self postUpdateNotificationForTransaction:transaction]; @@ -247,7 +245,7 @@ - (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(N - (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error { dispatch_async(self.queue, ^{ - FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID]; + FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID]; if (!transaction) { return; } @@ -262,7 +260,7 @@ - (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *) - (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID { dispatch_async(self.queue, ^{ - FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID]; + FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID]; if (!transaction) { return; } @@ -272,6 +270,42 @@ - (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID }); } +#pragma mark - Websocket Events + +- (void)recordWebsocketMessageSend:(NSURLSessionWebSocketMessage *)message task:(NSURLSessionWebSocketTask *)task { + dispatch_async(self.queue, ^{ + FLEXWebsocketTransaction *send = [FLEXWebsocketTransaction + withMessage:message task:task direction:FLEXWebsocketOutgoing + ]; + + [self.orderedWSTransactions addObject:send]; + [self postNewTransactionNotificationWithTransaction:send]; + }); +} + +- (void)recordWebsocketMessageSendCompletion:(NSURLSessionWebSocketMessage *)message error:(NSError *)error { + dispatch_async(self.queue, ^{ + FLEXWebsocketTransaction *send = [self.orderedWSTransactions flex_firstWhere:^BOOL(FLEXWebsocketTransaction *t) { + return t.message == message; + }]; + send.error = error; + send.transactionState = error ? FLEXNetworkTransactionStateFailed : FLEXNetworkTransactionStateFinished; + + [self postUpdateNotificationForTransaction:send]; + }); +} + +- (void)recordWebsocketMessageReceived:(NSURLSessionWebSocketMessage *)message task:(NSURLSessionWebSocketTask *)task { + dispatch_async(self.queue, ^{ + FLEXWebsocketTransaction *receive = [FLEXWebsocketTransaction + withMessage:message task:task direction:FLEXWebsocketIncoming + ]; + + [self.orderedWSTransactions addObject:receive]; + [self postNewTransactionNotificationWithTransaction:receive]; + }); +} + #pragma mark Notification Posting - (void)postNewTransactionNotificationWithTransaction:(FLEXNetworkTransaction *)transaction { diff --git a/Classes/Network/PonyDebugger/FLEXNetworkObserver.m b/Classes/Network/PonyDebugger/FLEXNetworkObserver.m index 0a31da23d9..4d196eb072 100644 --- a/Classes/Network/PonyDebugger/FLEXNetworkObserver.m +++ b/Classes/Network/PonyDebugger/FLEXNetworkObserver.m @@ -68,6 +68,15 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionDownloadTask *)down - (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 () @@ -89,7 +98,7 @@ + (void)setEnabled:(BOOL)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 injectIntoAllNSURLConnectionDelegateClasses]; + [self injectIntoAllNSURLThings]; } if (previouslyEnabled != enabled) { @@ -105,7 +114,7 @@ + (void)load { // We don't want to do the swizzling from +load because not all the classes may be loaded at this point. dispatch_async(dispatch_get_main_queue(), ^{ if ([self isEnabled]) { - [self injectIntoAllNSURLConnectionDelegateClasses]; + [self injectIntoAllNSURLThings]; } }); } @@ -154,7 +163,7 @@ + (void)sniffWithoutDuplicationForObject:(NSObject *)object selector:(SEL)select #pragma mark - Delegate Injection -+ (void)injectIntoAllNSURLConnectionDelegateClasses { ++ (void)injectIntoAllNSURLThings { // Only allow swizzling once. static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -224,6 +233,15 @@ + (void)injectIntoAllNSURLConnectionDelegateClasses { [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]; + } }); } @@ -1266,7 +1284,76 @@ typedef void (^DidWriteDataBlock)(id slf, 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"; @@ -1589,4 +1676,35 @@ - (void)URLSessionTaskWillResume:(NSURLSessionTask *)task { }]; } +- (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