Skip to content

Instantly share code, notes, and snippets.

@echoz
Created May 31, 2012 02:10
Show Gist options
  • Save echoz/2840408 to your computer and use it in GitHub Desktop.
Save echoz/2840408 to your computer and use it in GitHub Desktop.
Class to generate HTTP Post Body
//
// LBCHTTPPostBody.h
// LBCCore
//
// Created by Jeremy Foo on 22/5/12.
// Copyright (c) 2012 BOB FTW PTE LTD. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import <Foundation/Foundation.h>
// result is either a stream file NSURL or a NSData
typedef void (^LBCHTTPPostBodyGenerationCompletionHandler)(BOOL isStreamFile, NSString *contentType, NSString *contentLength, id result, NSError *error);
typedef enum {
LBCHTTPPostBodyNoSuchStreamFileError,
LBCHTTPPostBodyWrongURLPathSpecifiedError
} LBCHTTPPostBodyErrorStatus;
extern NSString *const LBCHTTPPostBodyErrorDomain;
@interface LBCHTTPPostBody : NSObject <NSStreamDelegate, NSCoding>
@property (readonly) NSMutableDictionary *parameters;
+(void)performStreamGenerationOfParamters:(NSDictionary *)params toPath:(NSURL *)path completion:(LBCHTTPPostBodyGenerationCompletionHandler)completion;
+(void)performAutomaticHTTPBodyGenerationOfParameters:(NSDictionary *)params completion:(LBCHTTPPostBodyGenerationCompletionHandler)completion;
-(id)initWithHTTPParameters:(NSDictionary *)params;
-(void)performAutomatic:(BOOL)automatic HTTPBodyGenerationWithCompletion:(LBCHTTPPostBodyGenerationCompletionHandler)completion;
-(void)performStreamGenerationToPath:(NSURL *)path completion:(LBCHTTPPostBodyGenerationCompletionHandler)completion;
// HTTPBody Specific Methods
+(BOOL)isExistingFileURLForObject:(id)obj;
+(BOOL)needMultiPartForParameters:(NSDictionary *)params hasExternalFile:(BOOL *)externalFile;
// NSData Convinence Methods
+(NSData *)octetStreamHeaderForName:(NSString *)name filename:(NSString *)filename;
+(NSData *)octetStreamFooter;
+(void)appendData:(NSMutableData *)data key:(NSString *)key value:(NSString *)value;
+(void)appendData:(NSMutableData *)data key:(NSString *)key data:(NSData *)dataValue;
+(NSInteger)appendOutputStream:(NSOutputStream *)stream key:(NSString *)key value:(NSString *)value;
+(NSInteger)appendOutputStream:(NSOutputStream *)stream key:(NSString *)key data:(NSData *)dataValue;
// String Utilities
+(NSString *)hashForData:(NSData *)data;
+(NSString *)urlEncodedStringFromParams:(NSDictionary *)params;
+(NSString *)escapeString:(NSString *)str;
+(NSString *)escapeString:(NSString *)str withEscapees:(NSString *)escapees;
+(NSString *)multipartBoundary;
+(NSString *)contentTypeForMultipart;
+(NSURL *)fileURLForTemporaryStreamFile;
@end
//
// LBCHTTPPostBody.m
// LBCCore
//
// Created by Jeremy Foo on 22/5/12.
// Copyright (c) 2012 BOB FTW PTE LTD. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "LBCHTTPPostBody.h"
#import <CommonCrypto/CommonDigest.h>
#define HTTP_MULTIPART_BOUNDARY @"h3110th1s1slbcur1r3qu35tsh-t-t-p-b0und4r7"
#define FILE_STREAM_FORMNAME_KEY @"FORMNAME"
#define FILE_STREAM_FILEPATH_KEY @"FILEPATH"
NSString *const LBCHTTPPostBodyErrorDomain = @"LBCHTTPPostBodyErrorDomain";
@interface LBCHTTPPostBody (/* Private Method */)
@property (nonatomic, copy) LBCHTTPPostBodyGenerationCompletionHandler _completion;
@property (nonatomic, retain) NSString *_pathToMultipartBody;
@property (nonatomic, retain) NSInputStream *_fileStream;
@property (nonatomic, retain) NSOutputStream *_multipartBodyOutputStream;
@property (nonatomic, retain) NSMutableArray *_filesToStream;
-(void)_processFileStream;
-(void)_prepareHTTPBodyForStreamToPath:(NSURL *)path;
-(void)_completeStreamConstruction;
@end
@implementation LBCHTTPPostBody
@synthesize parameters;
@synthesize _completion;
@synthesize _fileStream = __fileStream, _multipartBodyOutputStream = __multipartBodyOutputStream, _pathToMultipartBody = __pathToMultipartBody, _filesToStream = __filesToStream;
#pragma mark - NSCoding
-(void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.parameters forKey:@"paramters"];
}
-(id)initWithCoder:(NSCoder *)aDecoder {
return [self initWithHTTPParameters:[aDecoder decodeObjectForKey:@"parameters"]];
}
#pragma mark - Object Life Cycle
+(void)performAutomaticHTTPBodyGenerationOfParameters:(NSDictionary *)params completion:(LBCHTTPPostBodyGenerationCompletionHandler)completion {
NSAssert1((completion != nil), @"Completion cannot be nil if this is a static method call", nil);
LBCHTTPPostBody *postbody = [[LBCHTTPPostBody alloc] initWithHTTPParameters:params];
[postbody performAutomatic:YES HTTPBodyGenerationWithCompletion:^(BOOL isStreamFile, NSString *contentType, NSString *contentLength, id result, NSError *error) {
completion(isStreamFile, contentType, contentLength, result, error);
}];
[postbody release];
}
+(void)performStreamGenerationOfParamters:(NSDictionary *)params toPath:(NSURL *)path completion:(LBCHTTPPostBodyGenerationCompletionHandler)completion {
NSAssert1((completion != nil), @"Completion cannot be nil if this is a static method call", nil);
LBCHTTPPostBody *postbody = [[LBCHTTPPostBody alloc] initWithHTTPParameters:params];
[postbody performStreamGenerationToPath:path completion:^(BOOL isStreamFile, NSString *contentType, NSString *contentLength, id result, NSError *error) {
completion(isStreamFile, contentType, contentLength, result, error);
}];
[postbody release];
}
-(id)initWithHTTPParameters:(NSDictionary *)params {
if ((self = [super init])) {
parameters = [params mutableCopy];
}
return self;
}
-(void)dealloc {
[__pathToMultipartBody release], __pathToMultipartBody = nil;
[__multipartBodyOutputStream release], __multipartBodyOutputStream = nil;
[__fileStream release], __fileStream = nil;
[__filesToStream release], __filesToStream = nil;
[_completion release], _completion = nil;
[parameters release], parameters = nil;
[super dealloc];
}
#pragma mark - Body Generation
-(void)performStreamGenerationToPath:(NSURL *)path completion:(LBCHTTPPostBodyGenerationCompletionHandler)completion {
if (![path isFileURL]) {
if (completion)
completion(NO, nil, nil, nil, [NSError errorWithDomain:LBCHTTPPostBodyErrorDomain code:LBCHTTPPostBodyWrongURLPathSpecifiedError userInfo:nil]);
return;
}
self._completion = completion;
[self _prepareHTTPBodyForStreamToPath:path];
}
-(void)performAutomatic:(BOOL)automatic HTTPBodyGenerationWithCompletion:(LBCHTTPPostBodyGenerationCompletionHandler)completion {
NSAssert1((completion != nil), @"Completion block cannot be nil", nil);
// do check to decide if you want to URLEncode or not
// if a URL exists as a param and is a fileURL to a valid file
// do no URL encode, generate a stream instead.
BOOL hasExternalFile = NO;
if ([[self class] needMultiPartForParameters:parameters hasExternalFile:&hasExternalFile]) {
// lets do bit wise values to decide if we should use in memory generation
// the only condition we should do contrary is when auto is YES and hasExternal is YES
int extFile = 0;
if (hasExternalFile)
extFile = 1;
int atmtic = 0;
if (automatic)
atmtic = 1;
if (~(extFile & atmtic)) {
// can generate in memory in place
NSMutableData *postData = [NSMutableData data];
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", [[self class] multipartBoundary]];
@autoreleasepool {
for (NSString *key in parameters) {
[postData appendData:[[NSString stringWithFormat:@"--%@\r\n", [[self class] multipartBoundary]] dataUsingEncoding:NSUTF8StringEncoding]];
if ([[parameters objectForKey:key] isKindOfClass:[NSData class]]) {
[[self class] appendData:postData key:key data:[parameters objectForKey:key]];
} else if ([[parameters objectForKey:key] isKindOfClass:[NSURL class]]) {
if (([[parameters objectForKey:key] filePathURL]) &&
([[NSFileManager defaultManager] fileExistsAtPath:[[[parameters objectForKey:key] filePathURL] path]])) {
NSData *fileData = [NSData dataWithContentsOfFile:[[parameters objectForKey:key] path]];
[[self class] appendData:postData key:key data:fileData];
} else {
[[self class] appendData:postData key:key value:[[parameters objectForKey:key] absoluteString]];
}
} else {
[[self class] appendData:postData key:key value:[parameters objectForKey:key]];
}
}
[postData appendData:[[NSString stringWithFormat:@"--%@--\r\n", [[self class] multipartBoundary]] dataUsingEncoding:NSUTF8StringEncoding]];
}
if (completion)
completion(NO, contentType, [NSString stringWithFormat:@"%ld", [postData length]], postData, nil);
} else {
// need to do generation
self._completion = completion;
[self _prepareHTTPBodyForStreamToPath:[[self class] fileURLForTemporaryStreamFile]];
}
} else {
NSString *contentType = @"application/x-www-form-urlencoded";
NSData *postData = [[[self class] urlEncodedStringFromParams:parameters] dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
if (completion)
completion(NO, contentType, [NSString stringWithFormat:@"%ld", [postData length]], postData, nil);
}
}
#pragma mark - Stream Construction
-(void)_completeStreamConstruction {
if ([[NSFileManager defaultManager] fileExistsAtPath:self._pathToMultipartBody]) {
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", [[self class] multipartBoundary]];
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:self._pathToMultipartBody error:nil];
NSString *contentLength = [NSString stringWithFormat:@"%lld", [[fileAttributes objectForKey:@"NSFileSize"] unsignedLongLongValue]];
if (_completion)
_completion(YES, contentType, contentLength, self._pathToMultipartBody, nil);
} else {
if (_completion) {
NSError *error = [NSError errorWithDomain:LBCHTTPPostBodyErrorDomain code:LBCHTTPPostBodyNoSuchStreamFileError userInfo:[NSDictionary dictionaryWithObject:self._pathToMultipartBody forKey:@"NoSuchFile"]];
_completion(NO, nil, nil, nil, error);
}
}
[self release];
}
-(void)_prepareHTTPBodyForStreamToPath:(NSURL *)path {
// generate unique file to insert shit into
if (![path isFileURL])
return;
self._pathToMultipartBody = [path path];
self._multipartBodyOutputStream = [NSOutputStream outputStreamToFileAtPath:self._pathToMultipartBody append:YES];
[self._multipartBodyOutputStream open];
// sort keys into normal http post params and files
NSMutableArray *sortedKeys = [NSMutableArray arrayWithCapacity:[parameters count]];
NSMutableArray *urlKeys = [NSMutableArray arrayWithCapacity:1];
[parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if ([[self class] isExistingFileURLForObject:obj]) {
[urlKeys addObject:key];
} else {
[sortedKeys addObject:[NSDictionary dictionaryWithObjectsAndKeys:key, FILE_STREAM_FORMNAME_KEY, obj, FILE_STREAM_FILEPATH_KEY, nil]];
}
}];
// deal with normal HTTP POST params
NSMutableData *data = [NSMutableData data];
for (NSString *key in sortedKeys) {
[[self class] appendData:data key:key data:[parameters objectForKey:key]];
}
[self._multipartBodyOutputStream write:[data bytes] maxLength:[data length]];
// deal with files.
[self retain];
if ([urlKeys count] > 0) {
self._filesToStream = urlKeys;
[self _processFileStream];
} else {
[self _completeStreamConstruction];
}
}
-(void)_processFileStream {
if (self._fileStream) {
[self._fileStream close];
[self._fileStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
self._fileStream = nil;
}
if ([self._filesToStream count] > 0) {
NSDictionary *fileDict = [self._filesToStream lastObject];
NSData *header = [[self class] octetStreamHeaderForName:[fileDict objectForKey:FILE_STREAM_FORMNAME_KEY] filename:[[fileDict objectForKey:FILE_STREAM_FILEPATH_KEY] lastPathComponent]];
[self._multipartBodyOutputStream write:[header bytes] maxLength:[header length]];
self._fileStream = [[[NSInputStream alloc] initWithFileAtPath:[[fileDict objectForKey:FILE_STREAM_FILEPATH_KEY] path]] autorelease];
self._fileStream.delegate = self;
[self._fileStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
NSData *HTTPBoundary = [[NSString stringWithFormat:@"--%@\r\n", [[self class] multipartBoundary]] dataUsingEncoding:NSUTF8StringEncoding];
[self._multipartBodyOutputStream write:[HTTPBoundary bytes] maxLength:[HTTPBoundary length]];
[self._fileStream open];
[self._filesToStream removeLastObject];
} else {
// complete and call handler
NSData *closeHTTPBoundary = [[NSString stringWithFormat:@"--%@--\r\n", [[self class] multipartBoundary]] dataUsingEncoding:NSUTF8StringEncoding];
[self._multipartBodyOutputStream write:[closeHTTPBoundary bytes] maxLength:[closeHTTPBoundary length]];
[self _completeStreamConstruction];
}
}
-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
if (aStream == self._fileStream) {
uint8_t buf[1024*100];
NSUInteger len = 0;
switch(eventCode) {
case NSStreamEventOpenCompleted:
NSLog(@"Media file opened");
break;
case NSStreamEventHasBytesAvailable:
len = [self._fileStream read:buf maxLength:1024];
if (len) {
[self._multipartBodyOutputStream write:buf maxLength:len];
} else {
NSLog(@"Buffer finished; wrote to %@", self._pathToMultipartBody);
NSData *footer = [[self class] octetStreamFooter];
[self._multipartBodyOutputStream write:[footer bytes] maxLength:[footer length]];
[self _processFileStream];
}
break;
case NSStreamEventErrorOccurred:
NSLog(@"ERROR piping image to body file %@", [aStream streamError]);
if (_completion)
_completion(NO, nil, nil, nil, [aStream streamError]);
[self release];
break;
default:
NSLog(@"Unhandled stream event (%ld)", eventCode);
break;
}
}
}
#pragma mark - String utility functions
+(NSString *)escapeString:(NSString *)str withEscapees:(NSString *)escapees {
return [(NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)str, NULL, (CFStringRef)escapees, kCFStringEncodingUTF8) autorelease];
}
+(NSString *)escapeString:(NSString *)str {
//return [[self class] escapeString:str withEscapees:@" ()<>#%{}|\\^~[]`;/?:@=&$"];
return [[self class] escapeString:str withEscapees:@" ()<>#{}|\\^~[]`;$"];
}
+(NSString *)hashForData:(NSData *)data {
// calculate MD5 of nsdata for filename
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5([data bytes], (uint)[data length], result);
NSMutableString *hash = [NSMutableString string];
for (int i = 0; i < 16; i++)
[hash appendFormat:@"%02X", result[i]];
return hash;
}
+(NSString *)urlEncodedStringFromParams:(NSDictionary *)params {
NSMutableString *post = [NSMutableString string];
id param = nil;
for (NSString *key in params) {
if ([post length] > 0) {
[post appendString:@"&"];
}
param = [params objectForKey:key];
if ([param isKindOfClass:[NSNumber class]]) {
param = [param stringValue];
} else {
param = [NSString stringWithFormat:@"%@", param];
}
[post appendFormat:@"%@=%@",[[self class] escapeString:key], [[self class] escapeString:param]];
}
return post;
}
+(NSString *)multipartBoundary {
return HTTP_MULTIPART_BOUNDARY;
}
+(NSString *)contentTypeForMultipart {
return [NSString stringWithFormat:@"multipart/form-data; boundary=%@", [[self class] multipartBoundary]];
}
+(NSURL *)fileURLForTemporaryStreamFile {
CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
CFStringRef uuidStr = CFUUIDCreateString(kCFAllocatorDefault, uuid);
NSString *extension = @"multipartbody";
NSString *bodyFileName = [(NSString *)uuidStr
stringByAppendingPathExtension:extension];
CFRelease(uuidStr);
CFRelease(uuid);
return [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:bodyFileName]];
}
#pragma mark - Generation of URL Request
+(void)appendData:(NSMutableData *)data key:(NSString *)key value:(NSString *)value {
// write content disposition
[data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]];
// write value
[data appendData:[[NSString stringWithFormat:@"\r\n%@\r\n", value] dataUsingEncoding:NSUTF8StringEncoding]];
}
+(NSData *)octetStreamHeaderForName:(NSString *)name filename:(NSString *)filename {
NSMutableData *data = [NSMutableData data];
[data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", name, filename] dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:[[NSString stringWithFormat:@"Content-Type: application/octet-stream\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:[[NSString stringWithFormat:@"Content-Transfer-Encoding: base64\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
return data;
}
+(NSData *)octetStreamFooter {
return [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding];
}
+(void)appendData:(NSMutableData *)data key:(NSString *)key data:(NSData *)dataValue {
[data appendData:[[self class] octetStreamHeaderForName:key filename:[[self class] hashForData:dataValue]]];
[data appendData:[NSData dataWithData:dataValue]];
[data appendData:[[self class] octetStreamFooter]];
}
+(NSInteger)appendOutputStream:(NSOutputStream *)stream key:(NSString *)key value:(NSString *)value {
NSMutableData *data = [NSMutableData data];
[[self class] appendData:data key:key value:value];
return [stream write:[data bytes] maxLength:[data length]];
}
+(NSInteger)appendOutputStream:(NSOutputStream *)stream key:(NSString *)key data:(NSData *)dataValue {
NSMutableData *data = [NSMutableData data];
[[self class] appendData:data key:key data:dataValue];
return [stream write:[data bytes] maxLength:[data length]];
}
+(BOOL)isExistingFileURLForObject:(id)obj {
if ([obj isKindOfClass:[NSURL class]]) {
if (([((NSURL *)obj) filePathURL]) && ([[NSFileManager defaultManager] fileExistsAtPath:[[((NSURL *)obj) filePathURL] path]])) {
return YES;
}
}
return NO;
}
+(BOOL)needMultiPartForParameters:(NSDictionary *)params hasExternalFile:(BOOL *)externalFile {
BOOL needMultiPart = NO;
id testobj = nil;
if (externalFile)
*externalFile = NO;
for (NSString *key in params) {
testobj = [params objectForKey:key];
if ([[self class] isExistingFileURLForObject:testobj]) {
needMultiPart = YES;
if (externalFile)
*externalFile = YES;
break;
} else if ([testobj isKindOfClass:[NSData class]]) {
needMultiPart = YES;
break;
}
}
return needMultiPart;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment