1 |
efrain |
1 |
// Copyright 2019 Google
|
|
|
2 |
//
|
|
|
3 |
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
4 |
// you may not use this file except in compliance with the License.
|
|
|
5 |
// You may obtain a copy of the License at
|
|
|
6 |
//
|
|
|
7 |
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
8 |
//
|
|
|
9 |
// Unless required by applicable law or agreed to in writing, software
|
|
|
10 |
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
11 |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
12 |
// See the License for the specific language governing permissions and
|
|
|
13 |
// limitations under the License.
|
|
|
14 |
|
|
|
15 |
#import "Crashlytics/Shared/FIRCLSNetworking/FIRCLSMultipartMimeStreamEncoder.h"
|
|
|
16 |
|
|
|
17 |
#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
|
|
|
18 |
#import "Crashlytics/Shared/FIRCLSByteUtility.h"
|
|
|
19 |
#import "Crashlytics/Shared/FIRCLSUUID.h"
|
|
|
20 |
|
|
|
21 |
@interface FIRCLSMultipartMimeStreamEncoder () <NSStreamDelegate>
|
|
|
22 |
|
|
|
23 |
@property(nonatomic) NSUInteger length;
|
|
|
24 |
@property(nonatomic, copy) NSString *boundary;
|
|
|
25 |
@property(nonatomic, copy, readonly) NSData *headerData;
|
|
|
26 |
@property(nonatomic, copy, readonly) NSData *footerData;
|
|
|
27 |
@property(nonatomic, strong) NSOutputStream *outputStream;
|
|
|
28 |
|
|
|
29 |
@end
|
|
|
30 |
|
|
|
31 |
@implementation FIRCLSMultipartMimeStreamEncoder
|
|
|
32 |
|
|
|
33 |
+ (void)populateRequest:(NSMutableURLRequest *)request
|
|
|
34 |
withDataFromEncoder:(void (^)(FIRCLSMultipartMimeStreamEncoder *encoder))block {
|
|
|
35 |
NSString *boundary = [self generateBoundary];
|
|
|
36 |
|
|
|
37 |
NSOutputStream *stream = [NSOutputStream outputStreamToMemory];
|
|
|
38 |
|
|
|
39 |
FIRCLSMultipartMimeStreamEncoder *encoder =
|
|
|
40 |
[[FIRCLSMultipartMimeStreamEncoder alloc] initWithStream:stream andBoundary:boundary];
|
|
|
41 |
|
|
|
42 |
[encoder encode:^{
|
|
|
43 |
block(encoder);
|
|
|
44 |
}];
|
|
|
45 |
|
|
|
46 |
[request setValue:encoder.contentTypeHTTPHeaderValue forHTTPHeaderField:@"Content-Type"];
|
|
|
47 |
[request setValue:encoder.contentLengthHTTPHeaderValue forHTTPHeaderField:@"Content-Length"];
|
|
|
48 |
|
|
|
49 |
NSData *data = [stream propertyForKey:NSStreamDataWrittenToMemoryStreamKey];
|
|
|
50 |
request.HTTPBody = data;
|
|
|
51 |
}
|
|
|
52 |
|
|
|
53 |
+ (NSString *)contentTypeHTTPHeaderValueWithBoundary:(NSString *)boundary {
|
|
|
54 |
return [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
|
|
|
55 |
}
|
|
|
56 |
|
|
|
57 |
+ (instancetype)encoderWithStream:(NSOutputStream *)stream andBoundary:(NSString *)boundary {
|
|
|
58 |
return [[self alloc] initWithStream:stream andBoundary:boundary];
|
|
|
59 |
}
|
|
|
60 |
|
|
|
61 |
+ (NSString *)generateBoundary {
|
|
|
62 |
return FIRCLSGenerateUUID();
|
|
|
63 |
}
|
|
|
64 |
|
|
|
65 |
- (instancetype)initWithStream:(NSOutputStream *)stream andBoundary:(NSString *)boundary {
|
|
|
66 |
self = [super init];
|
|
|
67 |
if (!self) {
|
|
|
68 |
return nil;
|
|
|
69 |
}
|
|
|
70 |
|
|
|
71 |
self.outputStream = stream;
|
|
|
72 |
|
|
|
73 |
if (!boundary) {
|
|
|
74 |
boundary = [FIRCLSMultipartMimeStreamEncoder generateBoundary];
|
|
|
75 |
}
|
|
|
76 |
|
|
|
77 |
_boundary = boundary;
|
|
|
78 |
|
|
|
79 |
return self;
|
|
|
80 |
}
|
|
|
81 |
|
|
|
82 |
- (void)encode:(void (^)(void))block {
|
|
|
83 |
[self beginEncoding];
|
|
|
84 |
|
|
|
85 |
block();
|
|
|
86 |
|
|
|
87 |
[self endEncoding];
|
|
|
88 |
}
|
|
|
89 |
|
|
|
90 |
- (NSString *)contentTypeHTTPHeaderValue {
|
|
|
91 |
return [[self class] contentTypeHTTPHeaderValueWithBoundary:self.boundary];
|
|
|
92 |
}
|
|
|
93 |
|
|
|
94 |
- (NSString *)contentLengthHTTPHeaderValue {
|
|
|
95 |
return [NSString stringWithFormat:@"%lu", (unsigned long)_length];
|
|
|
96 |
}
|
|
|
97 |
|
|
|
98 |
#pragma - mark MIME part API
|
|
|
99 |
- (void)beginEncoding {
|
|
|
100 |
_length = 0;
|
|
|
101 |
|
|
|
102 |
[self.outputStream open];
|
|
|
103 |
|
|
|
104 |
[self writeData:self.headerData];
|
|
|
105 |
}
|
|
|
106 |
|
|
|
107 |
- (void)endEncoding {
|
|
|
108 |
[self writeData:self.footerData];
|
|
|
109 |
|
|
|
110 |
[self.outputStream close];
|
|
|
111 |
}
|
|
|
112 |
|
|
|
113 |
- (NSData *)headerData {
|
|
|
114 |
return [@"MIME-Version: 1.0\r\n" dataUsingEncoding:NSUTF8StringEncoding];
|
|
|
115 |
}
|
|
|
116 |
|
|
|
117 |
- (NSData *)footerData {
|
|
|
118 |
return [[NSString stringWithFormat:@"--%@--\r\n", self.boundary]
|
|
|
119 |
dataUsingEncoding:NSUTF8StringEncoding];
|
|
|
120 |
}
|
|
|
121 |
|
|
|
122 |
- (void)addFileData:(NSData *)data
|
|
|
123 |
fileName:(NSString *)fileName
|
|
|
124 |
mimeType:(NSString *)mimeType
|
|
|
125 |
fieldName:(NSString *)name {
|
|
|
126 |
if ([data length] == 0) {
|
|
|
127 |
FIRCLSErrorLog(@"Unable to MIME encode data with zero length (%@)", name);
|
|
|
128 |
return;
|
|
|
129 |
}
|
|
|
130 |
|
|
|
131 |
if ([name length] == 0 || [fileName length] == 0) {
|
|
|
132 |
FIRCLSErrorLog(@"name (%@) or fieldname (%@) is invalid", name, fileName);
|
|
|
133 |
return;
|
|
|
134 |
}
|
|
|
135 |
|
|
|
136 |
NSMutableString *string;
|
|
|
137 |
|
|
|
138 |
string = [NSMutableString
|
|
|
139 |
stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n",
|
|
|
140 |
self.boundary, name, fileName];
|
|
|
141 |
|
|
|
142 |
if (mimeType) {
|
|
|
143 |
[string appendFormat:@"Content-Type: %@\r\n", mimeType];
|
|
|
144 |
[string appendString:@"Content-Transfer-Encoding: binary\r\n\r\n"];
|
|
|
145 |
} else {
|
|
|
146 |
[string appendString:@"Content-Type: application/octet-stream\r\n\r\n"];
|
|
|
147 |
}
|
|
|
148 |
|
|
|
149 |
[self writeData:[string dataUsingEncoding:NSUTF8StringEncoding]];
|
|
|
150 |
|
|
|
151 |
[self writeData:data];
|
|
|
152 |
|
|
|
153 |
[self writeData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
|
|
154 |
}
|
|
|
155 |
|
|
|
156 |
- (void)addValue:(id)value fieldName:(NSString *)name {
|
|
|
157 |
if ([name length] == 0 || !value || value == NSNull.null) {
|
|
|
158 |
FIRCLSErrorLog(@"name (%@) or value (%@) is invalid", name, value);
|
|
|
159 |
return;
|
|
|
160 |
}
|
|
|
161 |
|
|
|
162 |
NSMutableString *string;
|
|
|
163 |
|
|
|
164 |
string =
|
|
|
165 |
[NSMutableString stringWithFormat:@"--%@\r\nContent-Disposition: form-data; name=\"%@\"\r\n",
|
|
|
166 |
self.boundary, name];
|
|
|
167 |
[string appendString:@"Content-Type: text/plain\r\n\r\n"];
|
|
|
168 |
[string appendFormat:@"%@\r\n", value];
|
|
|
169 |
|
|
|
170 |
[self writeData:[string dataUsingEncoding:NSUTF8StringEncoding]];
|
|
|
171 |
}
|
|
|
172 |
|
|
|
173 |
- (void)addFile:(NSURL *)fileURL
|
|
|
174 |
fileName:(NSString *)fileName
|
|
|
175 |
mimeType:(NSString *)mimeType
|
|
|
176 |
fieldName:(NSString *)name {
|
|
|
177 |
NSData *data = [NSData dataWithContentsOfURL:fileURL];
|
|
|
178 |
|
|
|
179 |
[self addFileData:data fileName:fileName mimeType:mimeType fieldName:name];
|
|
|
180 |
}
|
|
|
181 |
|
|
|
182 |
- (BOOL)writeBytes:(const void *)bytes ofLength:(NSUInteger)length {
|
|
|
183 |
if ([self.outputStream write:bytes maxLength:length] != length) {
|
|
|
184 |
FIRCLSErrorLog(@"Failed to write bytes to stream");
|
|
|
185 |
return NO;
|
|
|
186 |
}
|
|
|
187 |
|
|
|
188 |
_length += length;
|
|
|
189 |
|
|
|
190 |
return YES;
|
|
|
191 |
}
|
|
|
192 |
|
|
|
193 |
- (void)writeData:(NSData *)data {
|
|
|
194 |
FIRCLSEnumerateByteRangesOfNSDataUsingBlock(
|
|
|
195 |
data, ^(const void *bytes, NSRange byteRange, BOOL *stop) {
|
|
|
196 |
NSUInteger length = byteRange.length;
|
|
|
197 |
|
|
|
198 |
if ([self.outputStream write:bytes maxLength:length] != length) {
|
|
|
199 |
FIRCLSErrorLog(@"Failed to write data to stream");
|
|
|
200 |
*stop = YES;
|
|
|
201 |
return;
|
|
|
202 |
}
|
|
|
203 |
|
|
|
204 |
self->_length += length;
|
|
|
205 |
});
|
|
|
206 |
}
|
|
|
207 |
|
|
|
208 |
@end
|