1// Copyright (c) 2006, Google Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright
9// notice, this list of conditions and the following disclaimer.
10//     * Redistributions in binary form must reproduce the above
11// copyright notice, this list of conditions and the following disclaimer
12// in the documentation and/or other materials provided with the
13// distribution.
14//     * Neither the name of Google Inc. nor the names of its
15// contributors may be used to endorse or promote products derived from
16// this software without specific prior written permission.
17//
18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30#import "HTTPMultipartUpload.h"
31#import "GTMDefines.h"
32
33// As -[NSString stringByAddingPercentEscapesUsingEncoding:] has been
34// deprecated with iOS 9.0 / OS X 10.11 SDKs, this function re-implements it
35// using -[NSString stringByAddingPercentEncodingWithAllowedCharacters:] when
36// using those SDKs.
37static NSString *PercentEncodeNSString(NSString *key) {
38#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && defined(__IPHONE_9_0) &&     \
39     __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_9_0) ||                      \
40    (defined(MAC_OS_X_VERSION_MIN_REQUIRED) &&                                 \
41     defined(MAC_OS_X_VERSION_10_11) &&                                        \
42     MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_11)
43  return [key stringByAddingPercentEncodingWithAllowedCharacters:
44                  [NSCharacterSet URLQueryAllowedCharacterSet]];
45#else
46  return [key stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
47#endif
48}
49
50// As -[NSURLConnection sendSynchronousRequest:returningResponse:error:] has
51// been deprecated with iOS 9.0 / OS X 10.11 SDKs, this function re-implements
52// it using -[NSURLSession dataTaskWithRequest:completionHandler:] which is
53// available on iOS 7+.
54static NSData *SendSynchronousNSURLRequest(NSURLRequest *req,
55                                           NSURLResponse **out_response,
56                                           NSError **out_error) {
57#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && defined(__IPHONE_7_0) &&     \
58     __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0) ||                      \
59    (defined(MAC_OS_X_VERSION_MIN_REQUIRED) &&                                 \
60     defined(MAC_OS_X_VERSION_10_11) &&                                        \
61     MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_11)
62  __block NSData* result = nil;
63  __block NSError* error = nil;
64  __block NSURLResponse* response = nil;
65  dispatch_semaphore_t wait_semaphone = dispatch_semaphore_create(0);
66  [[[NSURLSession sharedSession]
67      dataTaskWithRequest:req
68        completionHandler:^(NSData *data,
69                            NSURLResponse *resp,
70                            NSError *err) {
71            if (out_error)
72              error = [err retain];
73            if (out_response)
74              response = [resp retain];
75            if (err == nil)
76              result = [data retain];
77            dispatch_semaphore_signal(wait_semaphone);
78  }] resume];
79  dispatch_semaphore_wait(wait_semaphone, DISPATCH_TIME_FOREVER);
80  dispatch_release(wait_semaphone);
81  if (out_error)
82    *out_error = [error autorelease];
83  if (out_response)
84    *out_response = [response autorelease];
85  return [result autorelease];
86#else
87  return [NSURLConnection sendSynchronousRequest:req
88                               returningResponse:out_response
89                                           error:out_error];
90#endif
91}
92@interface HTTPMultipartUpload(PrivateMethods)
93- (NSString *)multipartBoundary;
94// Each of the following methods will append the starting multipart boundary,
95// but not the ending one.
96- (NSData *)formDataForKey:(NSString *)key value:(NSString *)value;
97- (NSData *)formDataForFileContents:(NSData *)contents name:(NSString *)name;
98- (NSData *)formDataForFile:(NSString *)file name:(NSString *)name;
99@end
100
101@implementation HTTPMultipartUpload
102//=============================================================================
103#pragma mark -
104#pragma mark || Private ||
105//=============================================================================
106- (NSString *)multipartBoundary {
107  // The boundary has 27 '-' characters followed by 16 hex digits
108  return [NSString stringWithFormat:@"---------------------------%08X%08X",
109    rand(), rand()];
110}
111
112//=============================================================================
113- (NSData *)formDataForKey:(NSString *)key value:(NSString *)value {
114  NSString *escaped = PercentEncodeNSString(key);
115  NSString *fmt =
116    @"--%@\r\nContent-Disposition: form-data; name=\"%@\"\r\n\r\n%@\r\n";
117  NSString *form = [NSString stringWithFormat:fmt, boundary_, escaped, value];
118
119  return [form dataUsingEncoding:NSUTF8StringEncoding];
120}
121
122//=============================================================================
123- (NSData *)formDataForFileContents:(NSData *)contents name:(NSString *)name {
124  NSMutableData *data = [NSMutableData data];
125  NSString *escaped = PercentEncodeNSString(name);
126  NSString *fmt = @"--%@\r\nContent-Disposition: form-data; name=\"%@\"; "
127    "filename=\"minidump.dmp\"\r\nContent-Type: application/octet-stream\r\n\r\n";
128  NSString *pre = [NSString stringWithFormat:fmt, boundary_, escaped];
129
130  [data appendData:[pre dataUsingEncoding:NSUTF8StringEncoding]];
131  [data appendData:contents];
132
133  return data;
134}
135
136//=============================================================================
137- (NSData *)formDataForFile:(NSString *)file name:(NSString *)name {
138  NSData *contents = [NSData dataWithContentsOfFile:file];
139
140  return [self formDataForFileContents:contents name:name];
141}
142
143//=============================================================================
144#pragma mark -
145#pragma mark || Public ||
146//=============================================================================
147- (id)initWithURL:(NSURL *)url {
148  if ((self = [super init])) {
149    url_ = [url copy];
150    boundary_ = [[self multipartBoundary] retain];
151    files_ = [[NSMutableDictionary alloc] init];
152  }
153
154  return self;
155}
156
157//=============================================================================
158- (void)dealloc {
159  [url_ release];
160  [parameters_ release];
161  [files_ release];
162  [boundary_ release];
163  [response_ release];
164
165  [super dealloc];
166}
167
168//=============================================================================
169- (NSURL *)URL {
170  return url_;
171}
172
173//=============================================================================
174- (void)setParameters:(NSDictionary *)parameters {
175  if (parameters != parameters_) {
176    [parameters_ release];
177    parameters_ = [parameters copy];
178  }
179}
180
181//=============================================================================
182- (NSDictionary *)parameters {
183  return parameters_;
184}
185
186//=============================================================================
187- (void)addFileAtPath:(NSString *)path name:(NSString *)name {
188  [files_ setObject:path forKey:name];
189}
190
191//=============================================================================
192- (void)addFileContents:(NSData *)data name:(NSString *)name {
193  [files_ setObject:data forKey:name];
194}
195
196//=============================================================================
197- (NSDictionary *)files {
198  return files_;
199}
200
201//=============================================================================
202- (NSData *)send:(NSError **)error {
203  NSMutableURLRequest *req =
204    [[NSMutableURLRequest alloc]
205          initWithURL:url_ cachePolicy:NSURLRequestUseProtocolCachePolicy
206      timeoutInterval:60.0];
207
208  NSMutableData *postBody = [NSMutableData data];
209
210  [req setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",
211    boundary_] forHTTPHeaderField:@"Content-type"];
212
213  // Add any parameters to the message
214  NSArray *parameterKeys = [parameters_ allKeys];
215  NSString *key;
216
217  NSInteger count = [parameterKeys count];
218  for (NSInteger i = 0; i < count; ++i) {
219    key = [parameterKeys objectAtIndex:i];
220    [postBody appendData:[self formDataForKey:key
221                                        value:[parameters_ objectForKey:key]]];
222  }
223
224  // Add any files to the message
225  NSArray *fileNames = [files_ allKeys];
226  for (NSString *name in fileNames) {
227    id fileOrData = [files_ objectForKey:name];
228    NSData *fileData;
229
230    // The object can be either the path to a file (NSString) or the contents
231    // of the file (NSData).
232    if ([fileOrData isKindOfClass:[NSData class]])
233      fileData = [self formDataForFileContents:fileOrData name:name];
234    else
235      fileData = [self formDataForFile:fileOrData name:name];
236
237    [postBody appendData:fileData];
238  }
239
240  NSString *epilogue = [NSString stringWithFormat:@"\r\n--%@--\r\n", boundary_];
241  [postBody appendData:[epilogue dataUsingEncoding:NSUTF8StringEncoding]];
242
243  [req setHTTPBody:postBody];
244  [req setHTTPMethod:@"POST"];
245
246  [response_ release];
247  response_ = nil;
248
249  NSData *data = nil;
250  if ([[req URL] isFileURL]) {
251    [[req HTTPBody] writeToURL:[req URL] options:0 error:error];
252  } else {
253    NSURLResponse *response = nil;
254    data = SendSynchronousNSURLRequest(req, &response, error);
255    response_ = (NSHTTPURLResponse *)[response retain];
256  }
257  [req release];
258
259  return data;
260}
261
262//=============================================================================
263- (NSHTTPURLResponse *)response {
264  return response_;
265}
266
267@end
268