1// Copyright 2020 Google LLC 2// 3// Redistribution and use in source and binary forms, with or without 4// modification, are permitted provided that the following conditions are 5// met: 6// 7// * Redistributions of source code must retain the above copyright 8// notice, this list of conditions and the following disclaimer. 9// * Redistributions in binary form must reproduce the above 10// copyright notice, this list of conditions and the following disclaimer 11// in the documentation and/or other materials provided with the 12// distribution. 13// * Neither the name of Google LLC nor the names of its 14// contributors may be used to endorse or promote products derived from 15// this software without specific prior written permission. 16// 17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29#import "HTTPRequest.h" 30 31#include <Availability.h> 32#include <AvailabilityMacros.h> 33 34#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && defined(__IPHONE_7_0) && \ 35 __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0) 36#import <UIKit/UIKit.h> 37#define HAS_BACKGROUND_TASK_API 1 38#else 39#define HAS_BACKGROUND_TASK_API 0 40#endif 41 42#import "encoding_util.h" 43 44#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && defined(__IPHONE_7_0) && \ 45 __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0) || \ 46 (defined(MAC_OS_X_VERSION_MIN_REQUIRED) && \ 47 defined(MAC_OS_X_VERSION_10_11) && \ 48 MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_11) 49#define USE_NSURLSESSION 1 50#else 51#define USE_NSURLSESSION 0 52#endif 53 54// As -[NSURLConnection sendSynchronousRequest:returningResponse:error:] has 55// been deprecated with iOS 9.0 / OS X 10.11 SDKs, this function re-implements 56// it using -[NSURLSession dataTaskWithRequest:completionHandler:] which is 57// available on iOS 7+. 58static NSData* SendSynchronousNSURLRequest(NSURLRequest* req, 59 NSURLResponse** outResponse, 60 NSError** outError) { 61#if USE_NSURLSESSION 62 __block NSData* result = nil; 63 __block NSError* error = nil; 64 __block NSURLResponse* response = nil; 65 dispatch_semaphore_t waitSemaphone = dispatch_semaphore_create(0); 66 67 NSURLSessionConfiguration* config = 68 [NSURLSessionConfiguration defaultSessionConfiguration]; 69 [config setTimeoutIntervalForRequest:240.0]; 70 NSURLSession* session = [NSURLSession sessionWithConfiguration:config]; 71 NSURLSessionDataTask *task = [session 72 dataTaskWithRequest:req 73 completionHandler:^(NSData* data, NSURLResponse* resp, NSError* err) { 74 if (outError) 75 error = [err retain]; 76 if (outResponse) 77 response = [resp retain]; 78 if (err == nil) 79 result = [data retain]; 80 dispatch_semaphore_signal(waitSemaphone); 81 }]; 82 [task resume]; 83 84#if HAS_BACKGROUND_TASK_API 85 // Used to guard against ending the background task twice, which UIKit 86 // considers to be an error. 87 __block BOOL isBackgroundTaskActive = YES; 88 __block UIBackgroundTaskIdentifier backgroundTaskIdentifier = 89 UIBackgroundTaskInvalid; 90 backgroundTaskIdentifier = [UIApplication.sharedApplication 91 beginBackgroundTaskWithName:@"Breakpad Upload" 92 expirationHandler:^{ 93 if (!isBackgroundTaskActive) { 94 return; 95 } 96 isBackgroundTaskActive = NO; 97 98 [task cancel]; 99 [UIApplication.sharedApplication 100 endBackgroundTask:backgroundTaskIdentifier]; 101 }]; 102#endif // HAS_BACKGROUND_TASK_API 103 104 dispatch_semaphore_wait(waitSemaphone, DISPATCH_TIME_FOREVER); 105 dispatch_release(waitSemaphone); 106 107#if HAS_BACKGROUND_TASK_API 108 if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) { 109 // Dispatch to main queue in order to synchronize access to 110 // `isBackgroundTaskActive` with the background task expiration handler, 111 // which is always run on the main thread. 112 dispatch_async(dispatch_get_main_queue(), ^{ 113 if (!isBackgroundTaskActive) { 114 return; 115 } 116 isBackgroundTaskActive = NO; 117 118 [UIApplication.sharedApplication 119 endBackgroundTask:backgroundTaskIdentifier]; 120 }); 121 } 122#endif // HAS_BACKGROUND_TASK_API 123 124 if (outError) 125 *outError = [error autorelease]; 126 if (outResponse) 127 *outResponse = [response autorelease]; 128 return [result autorelease]; 129#else // USE_NSURLSESSION 130 return [NSURLConnection sendSynchronousRequest:req 131 returningResponse:outResponse 132 error:outError]; 133#endif // USE_NSURLSESSION 134} 135 136@implementation HTTPRequest 137 138//============================================================================= 139- (id)initWithURL:(NSURL*)URL { 140 if ((self = [super init])) { 141 URL_ = [URL copy]; 142 } 143 144 return self; 145} 146 147//============================================================================= 148- (void)dealloc { 149 [URL_ release]; 150 [response_ release]; 151 152 [super dealloc]; 153} 154 155//============================================================================= 156- (NSURL*)URL { 157 return URL_; 158} 159 160//============================================================================= 161- (NSHTTPURLResponse*)response { 162 return response_; 163} 164 165//============================================================================= 166- (NSString*)HTTPMethod { 167 @throw [NSException 168 exceptionWithName:NSInternalInconsistencyException 169 reason:[NSString stringWithFormat:@"You must" 170 "override %@ in a subclass", 171 NSStringFromSelector(_cmd)] 172 userInfo:nil]; 173} 174 175//============================================================================= 176- (NSString*)contentType { 177 return nil; 178} 179 180//============================================================================= 181- (NSData*)bodyData { 182 return nil; 183} 184 185//============================================================================= 186- (NSData*)send:(NSError**)withError { 187 NSMutableURLRequest* req = [[NSMutableURLRequest alloc] 188 initWithURL:URL_ 189 cachePolicy:NSURLRequestUseProtocolCachePolicy 190 timeoutInterval:60.0]; 191 192 NSString* contentType = [self contentType]; 193 if ([contentType length] > 0) { 194 [req setValue:contentType forHTTPHeaderField:@"Content-type"]; 195 } 196 197 NSData* bodyData = [self bodyData]; 198 if ([bodyData length] > 0) { 199 [req setHTTPBody:bodyData]; 200 } 201 202 [req setHTTPMethod:[self HTTPMethod]]; 203 204 [response_ release]; 205 response_ = nil; 206 207 NSData* data = nil; 208 if ([[req URL] isFileURL]) { 209 [[req HTTPBody] writeToURL:[req URL] options:0 error:withError]; 210 } else { 211 NSURLResponse* response = nil; 212 data = SendSynchronousNSURLRequest(req, &response, withError); 213 response_ = (NSHTTPURLResponse*)[response retain]; 214 } 215 [req release]; 216 217 return data; 218} 219 220//============================================================================= 221+ (NSData*)formDataForFileContents:(NSData*)contents withName:(NSString*)name { 222 NSMutableData* data = [NSMutableData data]; 223 NSString* escaped = PercentEncodeNSString(name); 224 NSString* fmt = @"Content-Disposition: form-data; name=\"%@\"; " 225 "filename=\"minidump.dmp\"\r\nContent-Type: " 226 "application/octet-stream\r\n\r\n"; 227 NSString* pre = [NSString stringWithFormat:fmt, escaped]; 228 229 [data appendData:[pre dataUsingEncoding:NSUTF8StringEncoding]]; 230 [data appendData:contents]; 231 232 return data; 233} 234 235//============================================================================= 236+ (NSData*)formDataForFile:(NSString*)file withName:(NSString*)name { 237 NSData* contents = [NSData dataWithContentsOfFile:file]; 238 239 return [HTTPRequest formDataForFileContents:contents withName:name]; 240} 241 242//============================================================================= 243+ (NSData*)formDataForKey:(NSString*)key value:(NSString*)value { 244 NSString* escaped = PercentEncodeNSString(key); 245 NSString* fmt = @"Content-Disposition: form-data; name=\"%@\"\r\n\r\n%@\r\n"; 246 NSString* form = [NSString stringWithFormat:fmt, escaped, value]; 247 248 return [form dataUsingEncoding:NSUTF8StringEncoding]; 249} 250 251//============================================================================= 252+ (void)appendFileToBodyData:(NSMutableData*)data 253 withName:(NSString*)name 254 withFileOrData:(id)fileOrData { 255 NSData* fileData; 256 257 // The object can be either the path to a file (NSString) or the contents 258 // of the file (NSData). 259 if ([fileOrData isKindOfClass:[NSData class]]) 260 fileData = [self formDataForFileContents:fileOrData withName:name]; 261 else 262 fileData = [HTTPRequest formDataForFile:fileOrData withName:name]; 263 264 [data appendData:fileData]; 265} 266 267@end 268