HTTPRequest.m
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+. 58 static 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