diff --git a/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.h b/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.h index 246672a9..5cd968b6 100644 --- a/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.h +++ b/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.h @@ -35,6 +35,8 @@ typedef NS_ENUM(NSInteger, OCHTTPPipelineState) OCHTTPPipelineStateStopping }; +typedef NSString* OCHTTPPipelineLogFormat NS_TYPED_ENUM; + NS_ASSUME_NONNULL_BEGIN @protocol OCHTTPPipelinePolicyHandler @@ -165,5 +167,9 @@ NS_ASSUME_NONNULL_BEGIN extern OCClassSettingsIdentifier OCClassSettingsIdentifierHTTP; extern OCClassSettingsKey OCHTTPPipelineSettingUserAgent; +extern OCClassSettingsKey OCHTTPPipelineSettingTrafficLogFormat; + +extern OCHTTPPipelineLogFormat OCHTTPPipelineLogFormatPlainText; +extern OCHTTPPipelineLogFormat OCHTTPPipelineLogFormatJSON; NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.m b/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.m index 1cc517a9..3483b0e8 100644 --- a/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.m +++ b/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.m @@ -1175,13 +1175,78 @@ - (void)_scheduleTask:(OCHTTPPipelineTask *)task // Log request if (OCLogToggleEnabled(OCLogOptionLogRequestsAndResponses) && OCLoggingEnabled()) { - BOOL prefixedLogging = [[OCLogger classSettingForOCClassSettingsKey:OCClassSettingsKeyLogSingleLined] boolValue]; - NSString *infoPrefix = (prefixedLogging ? @"[info] " : @""); - NSString *errorDescription = (error != nil) ? (prefixedLogging ? [[error description] stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"\n"] withString:[NSString stringWithFormat:@"\n[info] "]] : [error description]) : @"-"; - NSArray *extraTags = [NSArray arrayWithObjects: @"HTTP", @"Request", request.method, OCLogTagTypedID(@"RequestID", request.identifier), OCLogTagTypedID(@"URLSessionTaskID", task.urlSessionTaskID), nil]; - OCTLogDebug([extraTags arrayByAddingObject:@"HTSum"], @"-> %@ %@", request.method, request.effectiveURL); - OCPFMLogDebug(OCLogOptionLogRequestsAndResponses, extraTags, @"Sending request:\n%@# REQUEST ---------------------------------------------------------\n%@URL: %@\n%@Error: %@\n%@Req Signals: %@\n%@- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n%@-----------------------------------------------------------------", infoPrefix, infoPrefix, request.effectiveURL, infoPrefix, errorDescription, infoPrefix, [request.requiredSignals.allObjects componentsJoinedByString:@", "], infoPrefix, [request requestDescriptionPrefixed:prefixedLogging]); + OCHTTPPipelineLogFormat httpLogFormat = [self classSettingForOCClassSettingsKey:OCHTTPPipelineSettingTrafficLogFormat]; + + if ([httpLogFormat isEqual:OCHTTPPipelineLogFormatJSON]) + { + // OCHTTPPipelineLogFormatJSON: JSON logging + NSMutableDictionary *infoDict = [NSMutableDictionary new]; + NSMutableDictionary *headerDict = [NSMutableDictionary new]; + NSMutableDictionary *bodyDict = [NSMutableDictionary new]; + + // ## Info + // IDs + infoDict[@"id"] = request.identifier; + if (![request.headerFields[OCHTTPHeaderFieldNameOriginalRequestID] isEqual:request.identifier]) + { + infoDict[@"original-id"] = request.headerFields[OCHTTPHeaderFieldNameOriginalRequestID]; + } + infoDict[@"url-session-task-id"] = task.urlSessionTaskID; + infoDict[@"required-signals"] = (request.requiredSignals.count > 0) ? request.requiredSignals.allObjects : nil; + + // Method + URL + infoDict[@"method"] = request.method; + infoDict[@"url"] = request.effectiveURL.absoluteString; + + // HTTP Error + if (error != nil) + { + infoDict[@"error"] = error.description; + } + + // ## Header + [OCHTTPRequest formatHeaders:request.headerFields withConsumer:^(NSString *headerField, NSString *value) { + headerDict[headerField] = value; + }]; + + // ## Body + NSNumber *bodyLength = nil; + NSString *readableContent = [OCHTTPRequest bodyDescriptionForURL:request.bodyURL data:request.bodyData headers:request.headerFields prefixed:NO bodyLength:&bodyLength altTextDescription:NULL]; + bodyDict[@"length"] = bodyLength; + bodyDict[@"data"] = readableContent; + bodyDict[@"local-data-url"] = request.bodyURL.absoluteString; + + // Compose JSON dict + NSDictionary *jsonDict = @{ + @"request" : @{ + @"info" : infoDict, + @"header" : headerDict, + @"body" : bodyDict + } + }; + + // Log JSON dict + NSError *jsonError = nil; + NSData *jsonData; + NSString *jsonString = nil; + + if ((jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:0 error:&jsonError]) != nil) + { + jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } + OCPFMLogDebug(OCLogOptionLogRequestsAndResponses, extraTags, @"REQUEST %@ %@", request.identifier, (jsonString != nil) ? jsonString : [NSString stringWithFormat:@"JSON log encoding error: %@", jsonError]); + } + else + { + // OCHTTPPipelineLogFormatPlainText + default: plain text logging + BOOL prefixedLogging = [[OCLogger classSettingForOCClassSettingsKey:OCClassSettingsKeyLogSingleLined] boolValue]; + NSString *infoPrefix = (prefixedLogging ? @"[info] " : @""); + NSString *errorDescription = (error != nil) ? (prefixedLogging ? [[error description] stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"\n"] withString:[NSString stringWithFormat:@"\n[info] "]] : [error description]) : @"-"; + + OCTLogDebug([extraTags arrayByAddingObject:@"HTSum"], @"-> %@ %@", request.method, request.effectiveURL); + OCPFMLogDebug(OCLogOptionLogRequestsAndResponses, extraTags, @"Sending request:\n%@# REQUEST ---------------------------------------------------------\n%@URL: %@\n%@Error: %@\n%@Req Signals: %@\n%@- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n%@-----------------------------------------------------------------", infoPrefix, infoPrefix, request.effectiveURL, infoPrefix, errorDescription, infoPrefix, [request.requiredSignals.allObjects componentsJoinedByString:@", "], infoPrefix, [request requestDescriptionPrefixed:prefixedLogging]); + } } // Update task @@ -1298,14 +1363,85 @@ - (void)_finishedTask:(OCHTTPPipelineTask *)task withResponse:(OCHTTPResponse *) // Log response if (OCLogToggleEnabled(OCLogOptionLogRequestsAndResponses) && OCLoggingEnabled()) { - BOOL prefixedLogging = [[OCLogger classSettingForOCClassSettingsKey:OCClassSettingsKeyLogSingleLined] boolValue]; - NSString *infoPrefix = (prefixedLogging ? @"[info] " : @""); - NSString *errorDescription = (task.response.httpError != nil) ? (prefixedLogging ? [[task.response.httpError description] stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"\n"] withString:[NSString stringWithFormat:@"\n[info] "]] : [task.response.httpError description]) : @"-"; - NSArray *extraTags = [NSArray arrayWithObjects: @"HTTP", @"Response", task.request.method, OCLogTagTypedID(@"RequestID", task.request.identifier), OCLogTagTypedID(@"URLSessionTaskID", task.urlSessionTaskID), nil]; - OCTLogDebug([extraTags arrayByAddingObject:@"HTSum"], @"<- %lu %@ (%@ %@)%@", (unsigned long)task.response.status.code, task.response.status.name, task.request.method, task.request.effectiveURL, ((task.response.redirectURL != nil) ? [NSString stringWithFormat:@" -> %@ ",task.response.redirectURL] : @"")); - OCPFMLogDebug(OCLogOptionLogRequestsAndResponses, extraTags, @"Received response:\n%@# RESPONSE --------------------------------------------------------\n%@Method: %@\n%@URL: %@\n%@Request-ID: %@%@\n%@Error: %@\n%@Req Signals: %@\n%@Metrics: %@\n%@- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n%@-----------------------------------------------------------------", infoPrefix, infoPrefix, task.request.method, infoPrefix, task.request.effectiveURL, infoPrefix, task.request.identifier, ((task.request.headerFields[OCHTTPHeaderFieldNameOriginalRequestID] != nil) ? (![task.request.headerFields[OCHTTPHeaderFieldNameOriginalRequestID] isEqual:task.request.identifier] ? [NSString stringWithFormat:@" (original: %@)", task.request.headerFields[OCHTTPHeaderFieldNameOriginalRequestID]] : @"") : @""), infoPrefix, errorDescription, infoPrefix, [task.request.requiredSignals.allObjects componentsJoinedByString:@", "], infoPrefix, task.metrics.compactSummary, infoPrefix, [task.response responseDescriptionPrefixed:prefixedLogging]); + OCHTTPPipelineLogFormat httpLogFormat = [self classSettingForOCClassSettingsKey:OCHTTPPipelineSettingTrafficLogFormat]; + + if ([httpLogFormat isEqual:OCHTTPPipelineLogFormatJSON]) + { + // OCHTTPPipelineLogFormatJSON: JSON logging + NSMutableDictionary *infoDict = [NSMutableDictionary new]; + NSMutableDictionary *replyDict = [NSMutableDictionary new]; + NSMutableDictionary *headerDict = [NSMutableDictionary new]; + NSMutableDictionary *bodyDict = [NSMutableDictionary new]; + + // ## Info + // IDs + infoDict[@"id"] = task.request.identifier; + if (![task.request.headerFields[OCHTTPHeaderFieldNameOriginalRequestID] isEqual:task.request.identifier]) + { + infoDict[@"original-id"] = task.request.headerFields[OCHTTPHeaderFieldNameOriginalRequestID]; + } + infoDict[@"url-session-task-id"] = task.urlSessionTaskID; + infoDict[@"required-signals"] = (task.request.requiredSignals.count > 0) ? task.request.requiredSignals.allObjects : nil; + + // Method + URL + infoDict[@"method"] = task.request.method; + infoDict[@"url"] = task.request.effectiveURL.absoluteString; + + // Reply status + HTTP Error + replyDict[@"status"] = @(task.response.status.code); + replyDict[@"status-name"] = task.response.status.name; + replyDict[@"metrics"] = task.metrics.compactSummary; + + if (task.response.httpError != nil) + { + replyDict[@"error"] = [task.response.httpError description]; + } + infoDict[@"reply"] = replyDict; + + // ## Header + [OCHTTPRequest formatHeaders:task.response.headerFields withConsumer:^(NSString *headerField, NSString *value) { + headerDict[headerField] = value; + }]; + + // ## Body + NSNumber *bodyLength = nil; + NSString *readableContent = [OCHTTPRequest bodyDescriptionForURL:task.response.bodyURL data:task.response.bodyData headers:task.response.headerFields prefixed:NO bodyLength:&bodyLength altTextDescription:NULL]; + bodyDict[@"length"] = bodyLength; + bodyDict[@"data"] = readableContent; + bodyDict[@"local-data-url"] = task.response.bodyURL.absoluteString; + + // Compose JSON dict + NSDictionary *jsonDict = @{ + @"response" : @{ + @"info" : infoDict, + @"header" : headerDict, + @"body" : bodyDict + } + }; + + // Log JSON dict + NSError *jsonError = nil; + NSData *jsonData; + NSString *jsonString = nil; + + if ((jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:0 error:&jsonError]) != nil) + { + jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } + OCPFMLogDebug(OCLogOptionLogRequestsAndResponses, extraTags, @"RESPONSE %@ %@", task.request.identifier, (jsonString != nil) ? jsonString : [NSString stringWithFormat:@"JSON log encoding error: %@", jsonError]); + } + else + { + // OCHTTPPipelineLogFormatPlainText + default: plain text logging + BOOL prefixedLogging = [[OCLogger classSettingForOCClassSettingsKey:OCClassSettingsKeyLogSingleLined] boolValue]; + NSString *infoPrefix = (prefixedLogging ? @"[info] " : @""); + NSString *errorDescription = (task.response.httpError != nil) ? (prefixedLogging ? [[task.response.httpError description] stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"\n"] withString:[NSString stringWithFormat:@"\n[info] "]] : [task.response.httpError description]) : @"-"; + + OCTLogDebug([extraTags arrayByAddingObject:@"HTSum"], @"<- %lu %@ (%@ %@)%@", (unsigned long)task.response.status.code, task.response.status.name, task.request.method, task.request.effectiveURL, ((task.response.redirectURL != nil) ? [NSString stringWithFormat:@" -> %@ ",task.response.redirectURL] : @"")); + OCPFMLogDebug(OCLogOptionLogRequestsAndResponses, extraTags, @"Received response:\n%@# RESPONSE --------------------------------------------------------\n%@Method: %@\n%@URL: %@\n%@Request-ID: %@%@\n%@Error: %@\n%@Req Signals: %@\n%@Metrics: %@\n%@- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n%@-----------------------------------------------------------------", infoPrefix, infoPrefix, task.request.method, infoPrefix, task.request.effectiveURL, infoPrefix, task.request.identifier, ((task.request.headerFields[OCHTTPHeaderFieldNameOriginalRequestID] != nil) ? (![task.request.headerFields[OCHTTPHeaderFieldNameOriginalRequestID] isEqual:task.request.identifier] ? [NSString stringWithFormat:@" (original: %@)", task.request.headerFields[OCHTTPHeaderFieldNameOriginalRequestID]] : @"") : @""), infoPrefix, errorDescription, infoPrefix, [task.request.requiredSignals.allObjects componentsJoinedByString:@", "], infoPrefix, task.metrics.compactSummary, infoPrefix, [task.response responseDescriptionPrefixed:prefixedLogging]); + } } // Attempt delivery @@ -2555,7 +2691,8 @@ + (OCClassSettingsIdentifier)classSettingsIdentifier + (NSDictionary *)defaultSettingsForIdentifier:(OCClassSettingsIdentifier)identifier { return (@{ - OCHTTPPipelineSettingUserAgent : @"ownCloudApp/{{app.version}} ({{app.part}}/{{app.build}}; {{os.name}}/{{os.version}}; {{device.model}})" + OCHTTPPipelineSettingUserAgent : @"ownCloudApp/{{app.version}} ({{app.part}}/{{app.build}}; {{os.name}}/{{os.version}}; {{device.model}})", + OCHTTPPipelineSettingTrafficLogFormat : OCHTTPPipelineLogFormatJSON }); } @@ -2569,6 +2706,17 @@ + (OCClassSettingsMetadataCollection)classSettingsMetadata OCClassSettingsMetadataKeyStatus : OCClassSettingsKeyStatusSupported, OCClassSettingsMetadataKeyCategory : @"Connection", }, + + OCHTTPPipelineSettingTrafficLogFormat : @{ + OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeString, + OCClassSettingsMetadataKeyDescription : @"If request and response logging is enabled, the format to use.", + OCClassSettingsMetadataKeyStatus : OCClassSettingsKeyStatusSupported, + OCClassSettingsMetadataKeyCategory : @"Connection", + OCClassSettingsMetadataKeyPossibleValues : @{ + OCHTTPPipelineLogFormatPlainText : @"Plain text", + OCHTTPPipelineLogFormatJSON : @"JSON" + } + } }); } @@ -2643,3 +2791,7 @@ - (void)queueBlock:(dispatch_block_t)block withBusy:(BOOL)withBusy OCClassSettingsIdentifier OCClassSettingsIdentifierHTTP = @"http"; OCClassSettingsKey OCHTTPPipelineSettingUserAgent = @"user-agent"; +OCClassSettingsKey OCHTTPPipelineSettingTrafficLogFormat = @"traffic-log-format"; + +OCHTTPPipelineLogFormat OCHTTPPipelineLogFormatPlainText = @"plain"; +OCHTTPPipelineLogFormat OCHTTPPipelineLogFormatJSON = @"json"; diff --git a/ownCloudSDK/HTTP/Request/OCHTTPRequest.h b/ownCloudSDK/HTTP/Request/OCHTTPRequest.h index 30a0dac7..4a3d91db 100644 --- a/ownCloudSDK/HTTP/Request/OCHTTPRequest.h +++ b/ownCloudSDK/HTTP/Request/OCHTTPRequest.h @@ -158,7 +158,9 @@ typedef NSDictionary* OCHTTPRequestResumeInfo; @property(strong) OCHTTPResponse *httpResponse; #pragma mark - Description ++ (NSString *)bodyDescriptionForURL:(NSURL *)url data:(NSData *)data headers:(NSDictionary *)headers prefixed:(BOOL)prefixed bodyLength:(NSNumber **)outBodyLengthNumber altTextDescription:(NSString **)outAltTextDescription; + (NSString *)bodyDescriptionForURL:(NSURL *)url data:(NSData *)data headers:(NSDictionary *)headers prefixed:(BOOL)prefixed; ++ (void)formatHeaders:(NSDictionary *)headers withConsumer:(void(^)(NSString *, NSString *))headerConsumer; + (NSString *)formattedHeaders:(NSDictionary *)headers withLinePrefix:(NSString *)linePrefix; - (NSString *)requestDescriptionPrefixed:(BOOL)prefixed; diff --git a/ownCloudSDK/HTTP/Request/OCHTTPRequest.m b/ownCloudSDK/HTTP/Request/OCHTTPRequest.m index 6969a2d6..04af1fc9 100644 --- a/ownCloudSDK/HTTP/Request/OCHTTPRequest.m +++ b/ownCloudSDK/HTTP/Request/OCHTTPRequest.m @@ -381,7 +381,7 @@ - (void)cancel } #pragma mark - Description -+ (NSString *)bodyDescriptionForURL:(NSURL *)url data:(NSData *)data headers:(NSDictionary *)headers prefixed:(BOOL)prefixed ++ (NSString *)bodyDescriptionForURL:(NSURL *)url data:(NSData *)data headers:(NSDictionary *)headers prefixed:(BOOL)prefixed bodyLength:(NSNumber **)outBodyLengthNumber altTextDescription:(NSString **)outAltTextDescription { NSString *contentType = [[headers[OCHTTPHeaderFieldNameContentType] componentsSeparatedByString:@"; "] firstObject]; BOOL readableContent = [contentType hasPrefix:@"text/"] || @@ -403,38 +403,71 @@ + (NSString *)bodyDescriptionForURL:(NSURL *)url data:(NSData *)data headers:(NS if (url != nil) { + NSNumber *fileSize = nil; + + if ([url getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL]) + { + if (outBodyLengthNumber != NULL) + { + *outBodyLengthNumber = fileSize; + } + } + if (readableContent) { return (FormatReadableData([NSData dataWithContentsOfURL:url])); } - NSNumber *fileSize = nil; - - if ([url getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL]) + if (url != nil) { - return ([NSString stringWithFormat:@"%@[Contents from %@ (%ld bytes)]", (prefixed ? @"[body] " : @""), url.path, fileSize.integerValue]); + if (outAltTextDescription != NULL) + { + *outAltTextDescription = [NSString stringWithFormat:@"%@[Contents from %@ (%ld bytes)]", (prefixed ? @"[body] " : @""), url.path, fileSize.integerValue]; + } + return (nil); } - return ([NSString stringWithFormat:@"%@[Contents from %@]", (prefixed ? @"[body] " : @""), url.path]); + if (outAltTextDescription != NULL) + { + *outAltTextDescription = [NSString stringWithFormat:@"%@[Contents from %@]", (prefixed ? @"[body] " : @""), url.path]; + } + return (nil); } if (data != nil) { + if (outBodyLengthNumber != NULL) + { + *outBodyLengthNumber = @(data.length); + } + if (readableContent) { return (FormatReadableData(data)); } - return ([NSString stringWithFormat:@"%@[%lu bytes of %@ data]", (prefixed ? @"[body] " : @""), (unsigned long)data.length, contentType]); + if (outAltTextDescription != NULL) + { + *outAltTextDescription = [NSString stringWithFormat:@"%@[%lu bytes of %@ data]", (prefixed ? @"[body] " : @""), (unsigned long)data.length, contentType]; + } + return (nil); } return (nil); } -+ (NSString *)formattedHeaders:(NSDictionary *)headers withLinePrefix:(NSString *)linePrefix ++ (NSString *)bodyDescriptionForURL:(NSURL *)url data:(NSData *)data headers:(NSDictionary *)headers prefixed:(BOOL)prefixed { - NSMutableString *formattedHeaders = [NSMutableString new]; + NSString *readableContent = nil; + NSString *altTextDescription = nil; + + readableContent = [self bodyDescriptionForURL:url data:data headers:headers prefixed:prefixed bodyLength:NULL altTextDescription:&altTextDescription]; + + return ((readableContent != nil) ? readableContent : altTextDescription); +} ++ (void)formatHeaders:(NSDictionary *)headers withConsumer:(void(^)(NSString *, NSString *))headerConsumer +{ static dispatch_once_t onceToken; static NSMutableArray *knownAuthorizationHeaders; @@ -471,6 +504,16 @@ + (NSString *)formattedHeaders:(NSDictionary *)headers w } } + headerConsumer(headerField, value); + }]; +} + ++ (NSString *)formattedHeaders:(NSDictionary *)headers withLinePrefix:(NSString *)linePrefix +{ + NSMutableString *formattedHeaders = [NSMutableString new]; + + [self formatHeaders:headers withConsumer:^(NSString *headerField, NSString *value) { + if (linePrefix != nil) { [formattedHeaders appendFormat:@"%@%@: %@\n", linePrefix, headerField, value];