/ src / common / mac / HTTPRequest.m
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