Skip to content

Commit dff2d7a

Browse files
authored
Merge pull request #152 from mjbenedict/gzipHTTPRequests
Gzip http requests
2 parents 038bfa5 + 5a1f336 commit dff2d7a

File tree

10 files changed

+261
-0
lines changed

10 files changed

+261
-0
lines changed

Classes/Networking/RequestBody/TMFormEncodedRequestBody.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ - (nullable NSString *)contentType {
3838
return @"application/x-www-form-urlencoded; charset=utf-8";
3939
}
4040

41+
- (nullable NSString *)contentEncoding {
42+
return nil;
43+
}
44+
4145
- (nonnull NSDictionary *)parameters {
4246
return self.body;
4347
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// TMGZIPEncodeRequestBody.h
3+
// TMTumblrSDK
4+
//
5+
// Created by Michael Benedict on 2/9/18.
6+
//
7+
8+
#import <Foundation/Foundation.h>
9+
#import "TMRequestBody.h"
10+
11+
/**
12+
* A request body that represents gzipped data.
13+
*/
14+
__attribute__((objc_subclassing_restricted))
15+
@interface TMGZIPEncodedRequestBody : NSObject <TMRequestBody>
16+
17+
/**
18+
* Initialized instance of @c TMGZIPEncodedRequestBody.
19+
*
20+
* @param body The uncompressed body that will be transformed into a gzipped request body.
21+
*
22+
* @return A newly initialized instance of @c TMGZIPEncodedRequestBody.
23+
*/
24+
- (nonnull instancetype)initWithRequestBody:(nonnull id<TMRequestBody>)body;
25+
26+
@end
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//
2+
// TMGZIPEncodedRequestBody.m
3+
// TMTumblrSDK
4+
//
5+
// Created by Michael Benedict on 2/9/18.
6+
//
7+
8+
#import "TMRequestBody.h"
9+
#import "TMGZIPEncodedRequestBody.h"
10+
#include <zlib.h>
11+
#define CHUNKSIZE (1024*4)
12+
13+
@interface TMGZIPEncodedRequestBody ()
14+
15+
@property (nonatomic, nonnull, readonly) id<TMRequestBody> originalBody;
16+
@property (nonatomic, nonnull, readonly) NSData *compressedBodyData;
17+
18+
@end
19+
20+
@implementation TMGZIPEncodedRequestBody
21+
22+
- (nonnull instancetype)initWithRequestBody:(nonnull id<TMRequestBody>)body {
23+
NSParameterAssert(body);
24+
self = [super init];
25+
26+
if (self) {
27+
_originalBody = body;
28+
_compressedBodyData = [self compressedBody:[body bodyData]];
29+
}
30+
31+
return self;
32+
}
33+
34+
#pragma mark - TMRequestBody
35+
36+
- (nullable NSString *)contentType {
37+
return [_originalBody contentType];
38+
}
39+
40+
- (nullable NSString *)contentEncoding {
41+
if (!_compressedBodyData ) {
42+
return nil;
43+
}
44+
else {
45+
return @"gzip";
46+
}
47+
}
48+
49+
50+
- (nullable NSData *)bodyData {
51+
if (!_compressedBodyData) {
52+
return [_originalBody bodyData];
53+
}
54+
else {
55+
return _compressedBodyData;
56+
}
57+
}
58+
59+
- (nonnull NSDictionary *)parameters {
60+
return [_originalBody parameters];
61+
}
62+
63+
- (BOOL)encodeParameters {
64+
return [_originalBody encodeParameters];
65+
}
66+
67+
- (NSData *)compressedBody:(NSData *)data {
68+
z_stream strm;
69+
strm.zalloc = Z_NULL;
70+
strm.zfree = Z_NULL;
71+
strm.opaque = Z_NULL;
72+
strm.total_out = 0;
73+
strm.next_in=(Bytef *)[data bytes];
74+
strm.avail_in = (unsigned int)[data length];
75+
76+
/* windowBits (15+16) - Base two logarithm of the maximum window size (the size of the history buffer). It should be in the range 8..15.
77+
Add 16 to windowBits to write a simple gzip header and trailer around the compressed data instead of a zlib wrapper. The gzip header
78+
will have no file name, no extra data, no comment, no modification time (set to zero), no header crc, and the
79+
operating system will be set to 255 (unknown).
80+
81+
15+16 - will give gzip compatible output
82+
83+
memLevel - The memLevel parameter specifies how much memory should be allocated for the internal compression state.
84+
memLevel=1 uses minimum memory but is slow and reduces compression ratio; memLevel=9 uses maximum memory for
85+
optimal speed. The default value is 8. See zconf.h for total memory usage as a function of windowBits and memLevel.
86+
*/
87+
88+
if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY) == Z_OK) {
89+
90+
// 16K chunks for expansion
91+
NSMutableData *compressed = [[NSMutableData alloc] initWithLength:CHUNKSIZE];
92+
93+
do {
94+
if (strm.total_out >= [compressed length]) {
95+
[compressed increaseLengthBy:CHUNKSIZE];
96+
}
97+
98+
strm.next_out = [compressed mutableBytes] + strm.total_out;
99+
strm.avail_out = (unsigned int)([compressed length] - strm.total_out);
100+
101+
if (Z_STREAM_ERROR == deflate(&strm, Z_FINISH)) {
102+
return nil;
103+
}
104+
105+
} while (strm.avail_out == 0);
106+
107+
if (deflateEnd(&strm) != Z_OK) {
108+
return nil;
109+
}
110+
111+
[compressed setLength: strm.total_out];
112+
113+
return compressed;
114+
}
115+
else {
116+
return nil;
117+
}
118+
}
119+
120+
@end

Classes/Networking/RequestBody/TMJSONEncodedRequestBody.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ - (nullable NSString *)contentType {
3333
return @"application/json";
3434
}
3535

36+
- (nullable NSString *)contentEncoding {
37+
return nil;
38+
}
39+
40+
3641
- (nullable NSData *)bodyData {
3742
return [NSJSONSerialization dataWithJSONObject:self.JSONDictionary options:0 error:nil];
3843
}

Classes/Networking/RequestBody/TMMultipartRequestBody.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ - (nullable NSString *)contentType {
6060
return [[NSString alloc] initWithFormat:@"multipart/form-data; charset=utf-8; boundary=%@", TMMultipartBoundary];
6161
}
6262

63+
- (nullable NSString *)contentEncoding {
64+
return nil;
65+
}
66+
6367
- (nullable NSData *)bodyData {
6468

6569
NSMutableArray *parts = [[NSMutableArray alloc] init];

Classes/Networking/RequestBody/TMQueryEncodedRequestBody.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ - (nullable NSString *)contentType {
3838
return nil;
3939
}
4040

41+
- (nullable NSString *)contentEncoding {
42+
return nil;
43+
}
44+
4145
- (nonnull NSDictionary *)parameters {
4246
return self.queryDictionary;
4347
}

Classes/Networking/RequestBody/TMRequestBody.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525
*/
2626
- (nullable NSString *)contentType;
2727

28+
/**
29+
* Gets the content encoding for the header of Content-Encoding on a request.
30+
*
31+
* @return The content encoding of the request body.
32+
*/
33+
- (nullable NSString *)contentEncoding;
34+
2835
/**
2936
* The parameters represented by the body data.
3037
*

Classes/TMRequestHelpers/TMRequestParamaterizer.m

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ - (nonnull NSURLRequest *)URLRequestWithRequest:(nonnull id <TMRequest>)request
7979
if (contentType) {
8080
[URLRequest addValue:contentType forHTTPHeaderField:@"Content-Type"];
8181
}
82+
83+
NSString *contentEncoding = [requestBody contentEncoding];
84+
85+
if (contentEncoding) {
86+
[URLRequest addValue:contentEncoding forHTTPHeaderField:@"Content-Encoding"];
87+
}
88+
8289

8390
URLRequest.HTTPMethod = [TMRequestMethodHelpers stringFromMethod:request.method];
8491

ExampleiOS/ExampleiOSTests/TMRequestBodiesTests.m

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
#import <TMTumblrSDK/TMQueryEncodedRequestBody.h>
1111
#import <TMTumblrSDK/TMFormEncodedRequestBody.h>
1212
#import <TMTumblrSDK/TMJSONEncodedRequestBody.h>
13+
#import <TMTumblrSDK/TMGZIPEncodedRequestBody.h>
14+
#import <zlib.h>
1315

1416
@interface TMRequestBodiesTests : XCTestCase
1517

@@ -43,6 +45,13 @@ - (void)testQueryContentTypeIsRightForJSON {
4345
XCTAssert([body.contentType isEqualToString:@"application/json"]);
4446
}
4547

48+
- (void)testQueryContentTypeIsRightForGzippedJSON {
49+
TMJSONEncodedRequestBody *jsonBody = [[TMJSONEncodedRequestBody alloc] initWithJSONDictionary:@{@"4" : @3}];
50+
TMGZIPEncodedRequestBody *body = [[TMGZIPEncodedRequestBody alloc] initWithRequestBody:jsonBody];
51+
52+
XCTAssert([body.contentType isEqualToString:@"application/json"]);
53+
}
54+
4655
- (void)testContentTypeIsNil {
4756
NSString *contentType = [[[TMQueryEncodedRequestBody alloc] initWithQueryDictionary:@{@"paul" : @"kenny", @"pool" : @"object"}] contentType];
4857
XCTAssertNil(contentType);
@@ -64,4 +73,48 @@ - (void)testJSONEncodedRequestBodysParamtersAreCorrect {
6473
XCTAssert([[body parameters] isEqual:@{@"4" : @3}]);
6574
}
6675

76+
- (void)testBodyIsRightForGzippedJSON {
77+
TMJSONEncodedRequestBody *jsonBody = [[TMJSONEncodedRequestBody alloc] initWithJSONDictionary:@{@"4" : @3}];
78+
TMGZIPEncodedRequestBody *body = [[TMGZIPEncodedRequestBody alloc] initWithRequestBody:jsonBody];
79+
80+
XCTAssert([[self unZipTinyData:body.bodyData] isEqualToString:@"{\"4\":3}"], @"Strings are not equal %@ %@", @"{\"4\":3}", [self unZipTinyData:body.bodyData]);
81+
}
82+
83+
- (void)testFormEncodedGzippedRequestBodysParamtersAreCorrect {
84+
TMFormEncodedRequestBody *formBody = [[TMFormEncodedRequestBody alloc] initWithBody:@{@"4" : @3}];
85+
TMGZIPEncodedRequestBody *body = [[TMGZIPEncodedRequestBody alloc] initWithRequestBody:formBody];
86+
87+
XCTAssert([[body parameters] isEqual:@{@"4" : @3}]);
88+
}
89+
90+
#pragma mark - Helpers
91+
// tiny unzip functionality that can return up to 1k of unzipped data which is good enough for unit tests
92+
- (NSString *)unZipTinyData:(NSData *)data {
93+
z_stream strm;
94+
strm.zalloc = Z_NULL;
95+
strm.zfree = Z_NULL;
96+
strm.opaque = Z_NULL;
97+
strm.avail_in = (unsigned int)[data length];
98+
strm.next_in = (Bytef *)[data bytes];
99+
NSMutableData *uncompressed = [[NSMutableData alloc] initWithLength:1024];
100+
strm.avail_out = 1024;
101+
strm.next_out = [uncompressed mutableBytes];
102+
103+
// https://stackoverflow.com/a/22311297/8838812 shows MAX_WBITS | 16 is
104+
// needed for decoding the gzip format
105+
if (inflateInit2(&strm, MAX_WBITS | 16) != Z_OK) {
106+
return nil;
107+
}
108+
109+
int z_error = inflate(&strm, Z_NO_FLUSH);
110+
111+
if (z_error == Z_STREAM_END) {
112+
[uncompressed setLength:strm.total_out];
113+
return [[NSString alloc] initWithData:uncompressed encoding:NSUTF8StringEncoding];
114+
}
115+
else {
116+
return nil;
117+
}
118+
}
119+
67120
@end

ExampleiOS/ExampleiOSTests/TMRequestBodyTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,37 @@ final class TMRequestBodyTests: XCTestCase {
3131
XCTAssertTrue(requestBody.encodeParameters(), "Query encoded request body should encode parameters.")
3232
}
3333

34+
func testJSONRequestContentType() {
35+
let dictionary = testDictionary()
36+
let requestBody = TMJSONEncodedRequestBody(jsonDictionary: dictionary)
37+
XCTAssertEqual(requestBody.contentType() as String?, "application/json", "JSON request should have Content-Type: application/json.")
38+
}
39+
40+
func testJSONRequestContentEncoding() {
41+
let dictionary = testDictionary()
42+
let requestBody = TMJSONEncodedRequestBody(jsonDictionary: dictionary)
43+
XCTAssertEqual(requestBody.contentEncoding() as String?, nil, "JSON request should NOT have a Content-Encoding.")
44+
}
45+
46+
func testGZIPJSONRequestContentType() {
47+
let dictionary = testDictionary()
48+
let requestBody = TMGZIPEncodedRequestBody(requestBody: TMJSONEncodedRequestBody(jsonDictionary: dictionary))
49+
XCTAssertEqual(requestBody.contentType() as String?, "application/json", "GZipped JSON request should have Content-Type: application/json.")
50+
}
51+
52+
func testGZIPJSONRequestContentEncoding() {
53+
let dictionary = testDictionary()
54+
let requestBody = TMGZIPEncodedRequestBody(requestBody: TMJSONEncodedRequestBody(jsonDictionary: dictionary))
55+
XCTAssertEqual(requestBody.contentEncoding() as String?, "gzip", "GZipped JSON request should have Content-Encoding: gzip.")
56+
}
57+
58+
func testGZIPFormEncodedBody() {
59+
let dictionary = testDictionary()
60+
let requestBody = TMGZIPEncodedRequestBody(requestBody: TMFormEncodedRequestBody(body: dictionary))
61+
XCTAssertEqual(requestBody.parameters() as NSDictionary, dictionary as NSDictionary, "Form encoded request parameters should equal the body dictionary passed in.")
62+
XCTAssertTrue(requestBody.encodeParameters(), "Form encoded request body should encode parameters.")
63+
}
64+
3465
// MARK: Private
3566

3667
private func testDictionary() -> [AnyHashable: Any] {

0 commit comments

Comments
 (0)