appium-webdriveragent 16.9.2 → 16.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/WebDriverAgent.xcodeproj/project.pbxproj +4 -0
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBHTTPServer.m +311 -64
- package/WebDriverAgentLib/Routing/FBTCPSocket.h +4 -2
- package/WebDriverAgentLib/Routing/FBTCPSocket.m +5 -2
- package/WebDriverAgentTests/UnitTests/FBHTTPServerTests.m +264 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [16.9.3](https://github.com/appium/WebDriverAgent/compare/v16.9.2...v16.9.3) (2026-08-29)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* reject malformed Content-Length values and bound request header buffering ([#1226](https://github.com/appium/WebDriverAgent/issues/1226)) ([60c5fc4](https://github.com/appium/WebDriverAgent/commit/60c5fc461d91611778b8bb0745a21c80451d6ccf))
|
|
6
|
+
|
|
1
7
|
## [16.9.2](https://github.com/appium/WebDriverAgent/compare/v16.9.1...v16.9.2) (2026-08-28)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
|
@@ -1190,6 +1190,7 @@
|
|
|
1190
1190
|
EE8DDD7F20C5733C004D4925 /* XCUIElement+FBForceTouch.h in Headers */ = {isa = PBXBuildFile; fileRef = EE8DDD7D20C5733C004D4925 /* XCUIElement+FBForceTouch.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
|
1191
1191
|
EE9AB8011CAEE048008C271F /* UITestingUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */; };
|
|
1192
1192
|
EE9B76591CF7987800275851 /* FBRouteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76571CF7987300275851 /* FBRouteTests.m */; };
|
|
1193
|
+
DF82F4A8758B79DD91FA20CC /* FBHTTPServerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 76B7399DDF52C85A21433C1D /* FBHTTPServerTests.m */; };
|
|
1193
1194
|
EE9B768E1CF7997600275851 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76831CF7997600275851 /* AppDelegate.m */; };
|
|
1194
1195
|
EE9B768F1CF7997600275851 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76851CF7997600275851 /* ViewController.m */; };
|
|
1195
1196
|
EE9B76911CF7997600275851 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76871CF7997600275851 /* main.m */; };
|
|
@@ -1905,6 +1906,7 @@
|
|
|
1905
1906
|
EE9B75D41CF7956C00275851 /* IntegrationApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IntegrationApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
1906
1907
|
EE9B75EC1CF7956C00275851 /* IntegrationTests_1.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests_1.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
1907
1908
|
EE9B76571CF7987300275851 /* FBRouteTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBRouteTests.m; sourceTree = "<group>"; };
|
|
1909
|
+
76B7399DDF52C85A21433C1D /* FBHTTPServerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBHTTPServerTests.m; sourceTree = "<group>"; };
|
|
1908
1910
|
EE9B76581CF7987300275851 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
|
1909
1911
|
EE9B76821CF7997600275851 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
|
|
1910
1912
|
EE9B76831CF7997600275851 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
|
@@ -2675,6 +2677,7 @@
|
|
|
2675
2677
|
713352FC26CEF31D00523CBC /* FBLRUCacheTests.m */,
|
|
2676
2678
|
EE18883C1DA663EB00307AA8 /* FBMathUtilsTests.m */,
|
|
2677
2679
|
718F49C7230844330045FE8B /* FBProtocolHelpersTests.m */,
|
|
2680
|
+
76B7399DDF52C85A21433C1D /* FBHTTPServerTests.m */,
|
|
2678
2681
|
EE9B76571CF7987300275851 /* FBRouteTests.m */,
|
|
2679
2682
|
EE3F8CFD1D08AA17006F02CE /* FBRunLoopSpinnerTests.m */,
|
|
2680
2683
|
ADEF63AE1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m */,
|
|
@@ -4809,6 +4812,7 @@
|
|
|
4809
4812
|
719FF5B91DAD21F5008E0099 /* FBElementUtilitiesTests.m in Sources */,
|
|
4810
4813
|
716E0BD11E917F260087A825 /* FBXMLSafeStringTests.m in Sources */,
|
|
4811
4814
|
ADEF63AF1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m in Sources */,
|
|
4815
|
+
DF82F4A8758B79DD91FA20CC /* FBHTTPServerTests.m in Sources */,
|
|
4812
4816
|
EE9B76591CF7987800275851 /* FBRouteTests.m in Sources */,
|
|
4813
4817
|
7139145C1DF01A12005896C2 /* NSExpressionFBFormatTests.m in Sources */,
|
|
4814
4818
|
71A224E81DE326C500844D55 /* NSPredicateFBFormatTests.m in Sources */,
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>FMWK</string>
|
|
17
17
|
<key>CFBundleShortVersionString</key>
|
|
18
|
-
<string>16.9.
|
|
18
|
+
<string>16.9.3</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.9.
|
|
22
|
+
<string>16.9.3</string>
|
|
23
23
|
<key>NSPrincipalClass</key>
|
|
24
24
|
<string/>
|
|
25
25
|
</dict>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
#import "FBCommandStatus.h"
|
|
12
12
|
#import "FBConfiguration.h"
|
|
13
|
+
#import "FBLogger.h"
|
|
13
14
|
#import "FBResponsePayload.h"
|
|
14
15
|
#import "FBTCPSocket.h"
|
|
15
16
|
|
|
@@ -30,6 +31,35 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
30
31
|
return (NSData * _Nonnull)[string dataUsingEncoding:NSUTF8StringEncoding];
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
// Caps a request's header block, so a connection that never completes one cannot grow its buffer
|
|
35
|
+
// without limit. Matches node's default --max-http-header-size.
|
|
36
|
+
static const NSUInteger FBMaxRequestHeaderSize = 16 * 1024;
|
|
37
|
+
|
|
38
|
+
// ASCII decimal digits only. -integerValue must not be used here: it maps garbage silently
|
|
39
|
+
// ("bogus" -> 0, "12abc" -> 12), desyncing the framing of every later request on the connection.
|
|
40
|
+
static BOOL FBParseContentLength(NSString *value, NSUInteger *outLength)
|
|
41
|
+
{
|
|
42
|
+
if (value.length < 1) {
|
|
43
|
+
return NO;
|
|
44
|
+
}
|
|
45
|
+
NSUInteger result = 0;
|
|
46
|
+
for (NSUInteger i = 0; i < value.length; i++) {
|
|
47
|
+
unichar c = [value characterAtIndex:i];
|
|
48
|
+
if (c < '0' || c > '9') {
|
|
49
|
+
return NO;
|
|
50
|
+
}
|
|
51
|
+
NSUInteger digit = (NSUInteger)(c - '0');
|
|
52
|
+
// NSUInteger is 32-bit on watchOS (arm64_32), so this bounds truncation as well as overflow.
|
|
53
|
+
// Anything smaller is left to the caller's httpRequestBodySizeLimit check.
|
|
54
|
+
if (result > (NSUIntegerMax - digit) / 10) {
|
|
55
|
+
return NO;
|
|
56
|
+
}
|
|
57
|
+
result = result * 10 + digit;
|
|
58
|
+
}
|
|
59
|
+
*outLength = result;
|
|
60
|
+
return YES;
|
|
61
|
+
}
|
|
62
|
+
|
|
33
63
|
@interface FBHTTPRoute : NSObject
|
|
34
64
|
@property (nonatomic, copy) NSString *verb;
|
|
35
65
|
@property (nonatomic, strong) NSRegularExpression *regex;
|
|
@@ -101,9 +131,19 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
101
131
|
// standalone or not (except DELETE /session itself - see -dispatchMethod:). See
|
|
102
132
|
// -abandonPendingRequestsForSessionID:. Guarded by @synchronized(self.pendingSessionRequests).
|
|
103
133
|
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableSet<FBPendingRequest *> *> *pendingSessionRequests;
|
|
134
|
+
// When each connection started waiting for its current request. The reaper closes connections
|
|
135
|
+
// whose entry outlives FBIncompleteRequestTimeout; idle keep-alive connections have no entry and
|
|
136
|
+
// are exempt. Guarded by @synchronized(self.connectionBuffers).
|
|
137
|
+
@property (nonatomic, strong) NSMapTable<id, NSDate *> *incompleteRequestStarts;
|
|
138
|
+
@property (nonatomic, nullable) dispatch_source_t staleConnectionReaper;
|
|
104
139
|
|
|
105
140
|
@end
|
|
106
141
|
|
|
142
|
+
// How long a connection may take to deliver a complete request, matching the header read timeout
|
|
143
|
+
// the previous CocoaHTTPServer stack enforced.
|
|
144
|
+
static const NSTimeInterval FBIncompleteRequestTimeout = 30.0;
|
|
145
|
+
static const int64_t FBStaleConnectionSweepIntervalSec = 10;
|
|
146
|
+
|
|
107
147
|
@implementation FBHTTPServer
|
|
108
148
|
|
|
109
149
|
- (instancetype)init
|
|
@@ -119,6 +159,8 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
119
159
|
_connectionsAwaitingResponse = [NSMutableSet set];
|
|
120
160
|
_standaloneWaiters = [NSMutableDictionary dictionary];
|
|
121
161
|
_pendingSessionRequests = [NSMutableDictionary dictionary];
|
|
162
|
+
_incompleteRequestStarts = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
|
|
163
|
+
valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
|
|
122
164
|
}
|
|
123
165
|
return self;
|
|
124
166
|
}
|
|
@@ -226,18 +268,56 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
226
268
|
return NO;
|
|
227
269
|
}
|
|
228
270
|
self.socket = socket;
|
|
271
|
+
dispatch_source_t reaper = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.bufferProcessingQueue);
|
|
272
|
+
dispatch_source_set_timer(reaper,
|
|
273
|
+
dispatch_time(DISPATCH_TIME_NOW, FBStaleConnectionSweepIntervalSec * NSEC_PER_SEC),
|
|
274
|
+
(uint64_t)FBStaleConnectionSweepIntervalSec * NSEC_PER_SEC,
|
|
275
|
+
NSEC_PER_SEC);
|
|
276
|
+
__weak typeof(self) weakSelf = self;
|
|
277
|
+
dispatch_source_set_event_handler(reaper, ^{
|
|
278
|
+
[weakSelf reapStaleConnections];
|
|
279
|
+
});
|
|
280
|
+
dispatch_resume(reaper);
|
|
281
|
+
self.staleConnectionReaper = reaper;
|
|
229
282
|
_isRunning = YES;
|
|
230
283
|
return YES;
|
|
231
284
|
}
|
|
232
285
|
|
|
286
|
+
- (void)reapStaleConnections
|
|
287
|
+
{
|
|
288
|
+
NSMutableArray *staleConnections = [NSMutableArray array];
|
|
289
|
+
@synchronized (self.connectionBuffers) {
|
|
290
|
+
for (id connection in self.incompleteRequestStarts) {
|
|
291
|
+
// Waiting on the handler, not the peer - never reap, however long the handler takes.
|
|
292
|
+
if ([self.connectionsAwaitingResponse containsObject:connection]) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
NSDate *start = [self.incompleteRequestStarts objectForKey:connection];
|
|
296
|
+
if (nil != start && -start.timeIntervalSinceNow > FBIncompleteRequestTimeout) {
|
|
297
|
+
[staleConnections addObject:connection];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (id connection in staleConnections) {
|
|
302
|
+
[FBLogger logFmt:@"Closing a connection that did not deliver a complete request within %@ seconds", @(FBIncompleteRequestTimeout)];
|
|
303
|
+
[self closeClient:(nw_connection_t)connection];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
233
307
|
- (void)stop:(BOOL)immediately
|
|
234
308
|
{
|
|
309
|
+
dispatch_source_t reaper = self.staleConnectionReaper;
|
|
310
|
+
if (nil != reaper) {
|
|
311
|
+
dispatch_source_cancel(reaper);
|
|
312
|
+
self.staleConnectionReaper = nil;
|
|
313
|
+
}
|
|
235
314
|
[self.socket stop];
|
|
236
315
|
self.socket = nil;
|
|
237
316
|
@synchronized (self.connectionBuffers) {
|
|
238
317
|
[self.connectionBuffers removeAllObjects];
|
|
239
318
|
[self.pendingRequestHeaders removeAllObjects];
|
|
240
319
|
[self.connectionsAwaitingResponse removeAllObjects];
|
|
320
|
+
[self.incompleteRequestStarts removeAllObjects];
|
|
241
321
|
}
|
|
242
322
|
_isRunning = NO;
|
|
243
323
|
}
|
|
@@ -248,6 +328,8 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
248
328
|
{
|
|
249
329
|
@synchronized (self.connectionBuffers) {
|
|
250
330
|
[self.connectionBuffers setObject:[NSMutableData data] forKey:newClient];
|
|
331
|
+
// Starts at connect, so a peer that connects and then sends nothing is reaped too.
|
|
332
|
+
[self.incompleteRequestStarts setObject:[NSDate date] forKey:newClient];
|
|
251
333
|
}
|
|
252
334
|
}
|
|
253
335
|
|
|
@@ -257,6 +339,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
257
339
|
[self.connectionBuffers removeObjectForKey:client];
|
|
258
340
|
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
259
341
|
[self.connectionsAwaitingResponse removeObject:client];
|
|
342
|
+
[self.incompleteRequestStarts removeObjectForKey:client];
|
|
260
343
|
}
|
|
261
344
|
}
|
|
262
345
|
|
|
@@ -271,12 +354,34 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
271
354
|
if (nil == strongSelf) {
|
|
272
355
|
return;
|
|
273
356
|
}
|
|
357
|
+
BOOL isOverBufferCap = NO;
|
|
274
358
|
@synchronized (strongSelf.connectionBuffers) {
|
|
275
359
|
NSMutableData *buffer = [strongSelf.connectionBuffers objectForKey:client];
|
|
276
360
|
if (nil == buffer) {
|
|
277
361
|
return;
|
|
278
362
|
}
|
|
279
363
|
[buffer appendData:data];
|
|
364
|
+
// One maximal header block plus one maximal body, plus headroom for a pipelined follow-up.
|
|
365
|
+
// The per-request checks don't run while a request is executing, so without this cap a
|
|
366
|
+
// client could pump data unboundedly for as long as its previous request takes.
|
|
367
|
+
uint64_t bufferCap = FBConfiguration.sharedInstance.httpRequestBodySizeLimit + 2 * (uint64_t)FBMaxRequestHeaderSize;
|
|
368
|
+
if (bufferCap < FBConfiguration.sharedInstance.httpRequestBodySizeLimit) {
|
|
369
|
+
bufferCap = UINT64_MAX;
|
|
370
|
+
}
|
|
371
|
+
isOverBufferCap = buffer.length > bufferCap;
|
|
372
|
+
// In the body phase the timeout is an idle bound, refreshed on progress: a declared body
|
|
373
|
+
// may legitimately be slow and its size is already capped by Content-Length. In the header
|
|
374
|
+
// phase the clock is only started, never refreshed, so drip-fed headers cannot outlive it.
|
|
375
|
+
BOOL isBodyPhase = nil != [strongSelf.pendingRequestHeaders objectForKey:client];
|
|
376
|
+
if (isBodyPhase || nil == [strongSelf.incompleteRequestStarts objectForKey:client]) {
|
|
377
|
+
[strongSelf.incompleteRequestStarts setObject:[NSDate date] forKey:client];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (isOverBufferCap) {
|
|
381
|
+
// No response owed: a peer this far past any legitimate size is not reading anyway.
|
|
382
|
+
[FBLogger log:@"Closing a connection that overflowed its request buffer"];
|
|
383
|
+
[strongSelf closeClient:client];
|
|
384
|
+
return;
|
|
280
385
|
}
|
|
281
386
|
[strongSelf processBufferForClient:client];
|
|
282
387
|
});
|
|
@@ -302,78 +407,183 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
302
407
|
}
|
|
303
408
|
|
|
304
409
|
if (nil == pending) {
|
|
305
|
-
|
|
306
|
-
if (
|
|
307
|
-
//
|
|
410
|
+
pending = [self parsedRequestHeaderFromBuffer:buffer forClient:client];
|
|
411
|
+
if (nil == pending) {
|
|
412
|
+
// Either the header block is still incomplete, or it was rejected and answered already.
|
|
308
413
|
return;
|
|
309
414
|
}
|
|
415
|
+
@synchronized (self.connectionBuffers) {
|
|
416
|
+
[self.pendingRequestHeaders setObject:pending forKey:client];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
[self dispatchBufferedRequestWithHeader:pending fromBuffer:buffer forClient:client];
|
|
421
|
+
}
|
|
310
422
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
423
|
+
// Locates the CRLFCRLF that ends the buffered header block and bounds the block's size. Returns
|
|
424
|
+
// NO when nothing can be parsed yet - either because more bytes are needed or because the block
|
|
425
|
+
// was rejected, in which case the 400 has already been written.
|
|
426
|
+
- (BOOL)findHeaderBlockEnd:(out NSRange *)outHeaderEndRange
|
|
427
|
+
inBuffer:(NSMutableData *)buffer
|
|
428
|
+
forClient:(nw_connection_t)client
|
|
429
|
+
{
|
|
430
|
+
NSRange headerEndRange = [buffer rangeOfData:FBCRLFCRLFData() options:(NSDataSearchOptions)0 range:NSMakeRange(0, buffer.length)];
|
|
431
|
+
if (NSNotFound == headerEndRange.location) {
|
|
432
|
+
if (buffer.length > FBMaxRequestHeaderSize) {
|
|
433
|
+
// Past any legitimate header block and still unterminated - stop buffering.
|
|
315
434
|
[self respondBadRequestToClient:client];
|
|
316
|
-
return;
|
|
317
435
|
}
|
|
436
|
+
// Otherwise wait for the rest of the header block to arrive.
|
|
437
|
+
return NO;
|
|
438
|
+
}
|
|
439
|
+
if (headerEndRange.location > FBMaxRequestHeaderSize) {
|
|
440
|
+
// The check above only fires while the terminator is missing; one large receive can deliver
|
|
441
|
+
// an oversized block with it, so bound the completed block too before parsing it.
|
|
442
|
+
[self respondBadRequestToClient:client];
|
|
443
|
+
return NO;
|
|
444
|
+
}
|
|
445
|
+
*outHeaderEndRange = headerEndRange;
|
|
446
|
+
return YES;
|
|
447
|
+
}
|
|
318
448
|
|
|
319
|
-
|
|
320
|
-
|
|
449
|
+
// Turns the header lines that follow the request line into a lowercase-keyed dictionary.
|
|
450
|
+
// Returns nil for the malformed and ambiguous shapes, having written the 400 already.
|
|
451
|
+
- (nullable NSDictionary<NSString *, NSString *> *)parsedHeaderFieldsFromLines:(NSArray<NSString *> *)lines
|
|
452
|
+
forClient:(nw_connection_t)client
|
|
453
|
+
{
|
|
454
|
+
NSMutableDictionary<NSString *, NSString *> *requestHeaders = [NSMutableDictionary dictionary];
|
|
455
|
+
for (NSUInteger i = 1; i < lines.count; i++) {
|
|
456
|
+
NSString *line = lines[i];
|
|
457
|
+
NSRange colonRange = [line rangeOfString:@":"];
|
|
458
|
+
if (0 == line.length) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (NSNotFound == colonRange.location) {
|
|
462
|
+
// Malformed. Skipping it would drop what it meant to say: "Content-Length 5" would
|
|
463
|
+
// dispatch with an empty body, leaving its bytes to be parsed as another request.
|
|
321
464
|
[self respondBadRequestToClient:client];
|
|
322
|
-
return;
|
|
465
|
+
return nil;
|
|
323
466
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
NSString *name = [line substringToIndex:colonRange.location];
|
|
333
|
-
NSString *value = [[line substringFromIndex:colonRange.location + 1]
|
|
334
|
-
stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
|
335
|
-
requestHeaders[name.lowercaseString] = value;
|
|
467
|
+
NSString *name = [line substringToIndex:colonRange.location];
|
|
468
|
+
// RFC 7230 (3.2.4): whitespace before the colon MUST be rejected. Storing "content-length "
|
|
469
|
+
// as its own key would drop the real header and desync the framing.
|
|
470
|
+
if (0 == name.length
|
|
471
|
+
|| NSNotFound != [name rangeOfCharacterFromSet:NSCharacterSet.whitespaceAndNewlineCharacterSet].location) {
|
|
472
|
+
[self respondBadRequestToClient:client];
|
|
473
|
+
return nil;
|
|
336
474
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
traceback:nil]);
|
|
347
|
-
[notImplementedPayload dispatchWithResponse:notImplemented];
|
|
348
|
-
[self failClient:client withResponse:notImplemented];
|
|
349
|
-
return;
|
|
475
|
+
NSString *value = [[line substringFromIndex:colonRange.location + 1]
|
|
476
|
+
stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
|
477
|
+
NSString *normalizedName = name.lowercaseString;
|
|
478
|
+
// RFC 7230 (3.3.3): repeated framing fields are unrecoverable. Last-wins would let an empty
|
|
479
|
+
// "Transfer-Encoding:" mask an earlier "chunked", and the last Content-Length drive parsing.
|
|
480
|
+
if (([normalizedName isEqualToString:@"content-length"] || [normalizedName isEqualToString:@"transfer-encoding"])
|
|
481
|
+
&& nil != requestHeaders[normalizedName]) {
|
|
482
|
+
[self respondBadRequestToClient:client];
|
|
483
|
+
return nil;
|
|
350
484
|
}
|
|
485
|
+
requestHeaders[normalizedName] = value;
|
|
486
|
+
}
|
|
487
|
+
return requestHeaders;
|
|
488
|
+
}
|
|
351
489
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
490
|
+
// Rejects framing this server cannot honour and resolves the declared body length from the
|
|
491
|
+
// remaining framing headers. Returns NO having written the closing error response already.
|
|
492
|
+
- (BOOL)resolveBodyLength:(out NSUInteger *)outBodyLength
|
|
493
|
+
fromHeaderFields:(NSDictionary<NSString *, NSString *> *)requestHeaders
|
|
494
|
+
forClient:(nw_connection_t)client
|
|
495
|
+
{
|
|
496
|
+
NSString *transferEncoding = requestHeaders[@"transfer-encoding"];
|
|
497
|
+
if (nil != transferEncoding) {
|
|
498
|
+
// No transfer decoder exists, so mere presence is rejected - including an empty value,
|
|
499
|
+
// which is not a valid encoding list and would let the body be misread as empty.
|
|
500
|
+
RouteResponse *notImplemented = [RouteResponse new];
|
|
501
|
+
id<FBResponsePayload> notImplementedPayload = FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Transfer-Encoding is not supported"
|
|
502
|
+
traceback:nil]);
|
|
503
|
+
[notImplementedPayload dispatchWithResponse:notImplemented];
|
|
504
|
+
[self failClient:client withResponse:notImplemented];
|
|
505
|
+
return NO;
|
|
506
|
+
}
|
|
362
507
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
508
|
+
NSString *contentLengthValue = requestHeaders[@"content-length"];
|
|
509
|
+
NSUInteger contentLength = 0;
|
|
510
|
+
if (nil != contentLengthValue && !FBParseContentLength(contentLengthValue, &contentLength)) {
|
|
511
|
+
// The body's extent is unknowable, so the connection cannot be resynced - reject and close.
|
|
512
|
+
[self respondBadRequestToClient:client];
|
|
513
|
+
return NO;
|
|
514
|
+
}
|
|
515
|
+
if (contentLength > FBConfiguration.sharedInstance.httpRequestBodySizeLimit) {
|
|
516
|
+
// Closes the connection after responding, since the rest of the oversized body is still incoming.
|
|
517
|
+
RouteResponse *tooLarge = [RouteResponse new];
|
|
518
|
+
id<FBResponsePayload> tooLargePayload = FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"The request body exceeds the configured size limit"
|
|
519
|
+
traceback:nil]);
|
|
520
|
+
[tooLargePayload dispatchWithResponse:tooLarge];
|
|
521
|
+
[self failClient:client withResponse:tooLarge];
|
|
522
|
+
return NO;
|
|
523
|
+
}
|
|
524
|
+
*outBodyLength = contentLength;
|
|
525
|
+
return YES;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Parses the request line and headers of the request at the head of the buffer. Returns nil
|
|
529
|
+
// while the header block is still incomplete, and for a rejected one, which is answered here.
|
|
530
|
+
- (nullable FBPendingHTTPRequestHeader *)parsedRequestHeaderFromBuffer:(NSMutableData *)buffer
|
|
531
|
+
forClient:(nw_connection_t)client
|
|
532
|
+
{
|
|
533
|
+
NSRange headerEndRange;
|
|
534
|
+
if (![self findHeaderBlockEnd:&headerEndRange inBuffer:buffer forClient:client]) {
|
|
535
|
+
return nil;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
NSData *headerData = [buffer subdataWithRange:NSMakeRange(0, headerEndRange.location)];
|
|
539
|
+
NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding];
|
|
540
|
+
NSArray<NSString *> *lines = [headerString componentsSeparatedByString:@"\r\n"];
|
|
541
|
+
if (lines.count < 1) {
|
|
542
|
+
[self respondBadRequestToClient:client];
|
|
543
|
+
return nil;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
NSArray<NSString *> *requestLineParts = [lines.firstObject componentsSeparatedByString:@" "];
|
|
547
|
+
if (requestLineParts.count < 2) {
|
|
548
|
+
[self respondBadRequestToClient:client];
|
|
549
|
+
return nil;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
NSDictionary<NSString *, NSString *> *requestHeaders = [self parsedHeaderFieldsFromLines:lines forClient:client];
|
|
553
|
+
if (nil == requestHeaders) {
|
|
554
|
+
return nil;
|
|
555
|
+
}
|
|
556
|
+
NSUInteger contentLength = 0;
|
|
557
|
+
if (![self resolveBodyLength:&contentLength fromHeaderFields:requestHeaders forClient:client]) {
|
|
558
|
+
return nil;
|
|
371
559
|
}
|
|
372
560
|
|
|
561
|
+
FBPendingHTTPRequestHeader *pending = [FBPendingHTTPRequestHeader new];
|
|
562
|
+
pending.method = requestLineParts[0].uppercaseString;
|
|
563
|
+
pending.pathAndQuery = requestLineParts[1];
|
|
564
|
+
pending.bodyStart = headerEndRange.location + headerEndRange.length;
|
|
565
|
+
pending.contentLength = contentLength;
|
|
566
|
+
return pending;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Consumes the already-parsed request from the head of the buffer and dispatches it, once its
|
|
570
|
+
// whole body has arrived. Returns with the cached header left in place while it hasn't.
|
|
571
|
+
- (void)dispatchBufferedRequestWithHeader:(FBPendingHTTPRequestHeader *)pending
|
|
572
|
+
fromBuffer:(NSMutableData *)buffer
|
|
573
|
+
forClient:(nw_connection_t)client
|
|
574
|
+
{
|
|
373
575
|
NSUInteger totalRequestLength = pending.bodyStart + pending.contentLength;
|
|
374
576
|
if (buffer.length < totalRequestLength) {
|
|
375
|
-
// Wait for the rest of the body to arrive - the parsed header stays cached
|
|
577
|
+
// Wait for the rest of the body to arrive - the parsed header stays cached, so this
|
|
376
578
|
// doesn't re-scan/re-parse the header block on every subsequently arriving chunk.
|
|
579
|
+
@synchronized (self.connectionBuffers) {
|
|
580
|
+
// The request is now in its body phase, which is idle-bounded rather than hard-bounded.
|
|
581
|
+
// -client:didReceiveData: samples that phase before this parse runs, so the receive that
|
|
582
|
+
// completed a slowly-delivered header (and carried the first body bytes) would otherwise
|
|
583
|
+
// leave the connection on its header-phase timestamp and let the sweep close it despite
|
|
584
|
+
// the body having just made progress.
|
|
585
|
+
[self.incompleteRequestStarts setObject:[NSDate date] forKey:client];
|
|
586
|
+
}
|
|
377
587
|
return;
|
|
378
588
|
}
|
|
379
589
|
|
|
@@ -383,6 +593,14 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
383
593
|
[buffer replaceBytesInRange:NSMakeRange(0, totalRequestLength) withBytes:NULL length:0];
|
|
384
594
|
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
385
595
|
[self.connectionsAwaitingResponse addObject:client];
|
|
596
|
+
if (0 == buffer.length) {
|
|
597
|
+
// A complete request was delivered and nothing further is buffered: the connection is a
|
|
598
|
+
// healthy keep-alive and must not be reaped while idle.
|
|
599
|
+
[self.incompleteRequestStarts removeObjectForKey:client];
|
|
600
|
+
} else {
|
|
601
|
+
// Pipelined bytes of the next request are already buffered - restart its clock.
|
|
602
|
+
[self.incompleteRequestStarts setObject:[NSDate date] forKey:client];
|
|
603
|
+
}
|
|
386
604
|
}
|
|
387
605
|
|
|
388
606
|
[self dispatchMethod:pending.method pathAndQuery:pending.pathAndQuery body:body client:client];
|
|
@@ -612,19 +830,46 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
612
830
|
|
|
613
831
|
if (shouldClose) {
|
|
614
832
|
__weak typeof(self) weakSelf = self;
|
|
615
|
-
[self.socket writeData:payload toClient:client completion:^{
|
|
833
|
+
[self.socket writeData:payload toClient:client completion:^(BOOL didSucceed) {
|
|
616
834
|
[weakSelf closeClient:client];
|
|
617
835
|
}];
|
|
618
836
|
} else {
|
|
619
|
-
//
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
[self.connectionsAwaitingResponse removeObject:client];
|
|
623
|
-
}
|
|
837
|
+
// Unblocked from the send's completion, not before it: ordering is preserved either way
|
|
838
|
+
// (nw_connection_send is FIFO per connection), but unblocking early lets a client that
|
|
839
|
+
// pipelines without reading responses pile up rendered responses inside Network.framework.
|
|
624
840
|
__weak typeof(self) weakSelf = self;
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
841
|
+
[self.socket writeData:payload toClient:client completion:^(BOOL didSucceed) {
|
|
842
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
843
|
+
if (nil == strongSelf) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (!didSucceed) {
|
|
847
|
+
// The response never reached the peer, so running its next pipelined request - possibly
|
|
848
|
+
// a mutating one - would change device state for a client that can no longer be answered.
|
|
849
|
+
[FBLogger log:@"Failed to write a response; dropping the connection and its pending requests"];
|
|
850
|
+
[strongSelf closeClient:client];
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
// Lifting the exemption and resuming parsing happen in one step on bufferProcessingQueue,
|
|
854
|
+
// the queue the reaper also runs on: doing it out here exposes the connection to a sweep
|
|
855
|
+
// queued ahead of the parse, which would judge a buffered request by the previous one's
|
|
856
|
+
// timestamp.
|
|
857
|
+
dispatch_async(strongSelf.bufferProcessingQueue, ^{
|
|
858
|
+
__strong typeof(weakSelf) queuedSelf = weakSelf;
|
|
859
|
+
if (nil == queuedSelf) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
@synchronized (queuedSelf.connectionBuffers) {
|
|
863
|
+
[queuedSelf.connectionsAwaitingResponse removeObject:client];
|
|
864
|
+
// Mid-request connections get their window from when parsing could resume, not from the
|
|
865
|
+
// previous request. Absent entries stay absent, so idle keep-alives remain exempt.
|
|
866
|
+
if (nil != [queuedSelf.incompleteRequestStarts objectForKey:client]) {
|
|
867
|
+
[queuedSelf.incompleteRequestStarts setObject:[NSDate date] forKey:client];
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
[queuedSelf processBufferForClient:client];
|
|
871
|
+
});
|
|
872
|
+
}];
|
|
628
873
|
}
|
|
629
874
|
}
|
|
630
875
|
|
|
@@ -632,7 +877,9 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
632
877
|
{
|
|
633
878
|
@synchronized (self.connectionBuffers) {
|
|
634
879
|
[self.connectionBuffers removeObjectForKey:client];
|
|
880
|
+
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
635
881
|
[self.connectionsAwaitingResponse removeObject:client];
|
|
882
|
+
[self.incompleteRequestStarts removeObjectForKey:client];
|
|
636
883
|
}
|
|
637
884
|
nw_connection_cancel(client);
|
|
638
885
|
}
|
|
@@ -102,9 +102,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
102
102
|
|
|
103
103
|
@param data The data to send
|
|
104
104
|
@param client The destination client
|
|
105
|
-
@param completion Called once the send attempt finishes
|
|
105
|
+
@param completion Called once the send attempt finishes. `didSucceed` is NO if the send failed
|
|
106
|
+
(e.g. the peer went away mid-write), in which case nothing was delivered and the caller
|
|
107
|
+
must not treat the connection as usable.
|
|
106
108
|
*/
|
|
107
|
-
- (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(
|
|
109
|
+
- (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(BOOL didSucceed))completion;
|
|
108
110
|
|
|
109
111
|
@end
|
|
110
112
|
|
|
@@ -195,12 +195,15 @@
|
|
|
195
195
|
[self writeData:data toClient:client completion:nil];
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
- (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(
|
|
198
|
+
- (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(BOOL didSucceed))completion
|
|
199
199
|
{
|
|
200
200
|
dispatch_data_t dispatchData = dispatch_data_create(data.bytes, data.length, self.socketQueue, DISPATCH_DATA_DESTRUCTOR_DEFAULT);
|
|
201
201
|
nw_connection_send(client, dispatchData, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t _Nullable sendError) {
|
|
202
202
|
if (completion) {
|
|
203
|
-
|
|
203
|
+
// The send error must reach the caller: a failed write means the response never reached
|
|
204
|
+
// the peer, and treating that as success would e.g. let the next pipelined request run
|
|
205
|
+
// against a connection that can no longer answer it.
|
|
206
|
+
completion(nil == sendError);
|
|
204
207
|
}
|
|
205
208
|
});
|
|
206
209
|
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#import <XCTest/XCTest.h>
|
|
10
|
+
|
|
11
|
+
#import <arpa/inet.h>
|
|
12
|
+
#import <stdatomic.h>
|
|
13
|
+
#import <sys/socket.h>
|
|
14
|
+
#import <unistd.h>
|
|
15
|
+
|
|
16
|
+
#import "FBHTTPServer.h"
|
|
17
|
+
|
|
18
|
+
static atomic_int gFramingProbeHits;
|
|
19
|
+
|
|
20
|
+
// Exercises FBHTTPServer's HTTP framing defenses with raw socket data that URL-loading APIs
|
|
21
|
+
// cannot produce: malformed Content-Length values and header blocks that never terminate.
|
|
22
|
+
@interface FBHTTPServerTests : XCTestCase
|
|
23
|
+
@property (nonatomic, strong) FBHTTPServer *server;
|
|
24
|
+
@property (nonatomic, assign) uint16_t port;
|
|
25
|
+
@end
|
|
26
|
+
|
|
27
|
+
@implementation FBHTTPServerTests
|
|
28
|
+
|
|
29
|
+
- (void)setUp
|
|
30
|
+
{
|
|
31
|
+
[super setUp];
|
|
32
|
+
atomic_store(&gFramingProbeHits, 0);
|
|
33
|
+
self.server = [FBHTTPServer new];
|
|
34
|
+
[self.server handleMethod:@"POST" withPath:@"/framing/probe" block:^(RouteRequest *request, RouteResponse *response) {
|
|
35
|
+
atomic_fetch_add(&gFramingProbeHits, 1);
|
|
36
|
+
[response respondWithString:@"probe-ok"];
|
|
37
|
+
}];
|
|
38
|
+
[self.server get:@"/framing/ping" withBlock:^(RouteRequest *request, RouteResponse *response) {
|
|
39
|
+
[response respondWithString:@"pong"];
|
|
40
|
+
}];
|
|
41
|
+
self.server.port = 0;
|
|
42
|
+
NSError *error;
|
|
43
|
+
XCTAssertTrue([self.server start:&error], @"%@", error);
|
|
44
|
+
self.port = [[self.server valueForKeyPath:@"socket.port"] unsignedShortValue];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
- (void)tearDown
|
|
48
|
+
{
|
|
49
|
+
[self.server stop:NO];
|
|
50
|
+
self.server = nil;
|
|
51
|
+
[super tearDown];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Sends `payload` as-is and reads until the server closes the connection or `timeout` elapses.
|
|
55
|
+
// Returns everything received (nil on connect failure); *didClose reports whether EOF was seen.
|
|
56
|
+
- (NSString *)responseForRawPayload:(NSData *)payload timeout:(NSTimeInterval)timeout didClose:(BOOL *)didClose
|
|
57
|
+
{
|
|
58
|
+
*didClose = NO;
|
|
59
|
+
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
60
|
+
if (fd < 0) {
|
|
61
|
+
return nil;
|
|
62
|
+
}
|
|
63
|
+
int noSigpipe = 1;
|
|
64
|
+
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &noSigpipe, sizeof(noSigpipe));
|
|
65
|
+
struct timeval tv = { .tv_sec = (long)timeout, .tv_usec = 0 };
|
|
66
|
+
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
|
67
|
+
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(self.port) };
|
|
68
|
+
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
69
|
+
if (0 != connect(fd, (struct sockaddr *)&addr, sizeof(addr))) {
|
|
70
|
+
close(fd);
|
|
71
|
+
return nil;
|
|
72
|
+
}
|
|
73
|
+
// send(2) may write only part of the payload, which would truncate the multi-KiB flood
|
|
74
|
+
// payloads into something the server answers differently. Errors stay ignored on purpose:
|
|
75
|
+
// those same tests expect the server to close the connection mid-send.
|
|
76
|
+
const uint8_t *bytes = payload.bytes;
|
|
77
|
+
size_t remaining = payload.length;
|
|
78
|
+
while (remaining > 0) {
|
|
79
|
+
ssize_t sent = send(fd, bytes, remaining, 0);
|
|
80
|
+
if (sent <= 0) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
bytes += sent;
|
|
84
|
+
remaining -= (size_t)sent;
|
|
85
|
+
}
|
|
86
|
+
NSMutableData *received = [NSMutableData data];
|
|
87
|
+
char chunk[4096];
|
|
88
|
+
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:timeout];
|
|
89
|
+
while (deadline.timeIntervalSinceNow > 0) {
|
|
90
|
+
ssize_t n = recv(fd, chunk, sizeof(chunk), 0);
|
|
91
|
+
if (n == 0) {
|
|
92
|
+
*didClose = YES;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
if (n < 0) {
|
|
96
|
+
// A read timeout. Only stop waiting once the response is a keep-alive success, where no
|
|
97
|
+
// EOF is ever coming; every other response precedes a close, and giving up here would
|
|
98
|
+
// report didClose = NO for a connection the server is about to drop.
|
|
99
|
+
NSString *soFar = [[NSString alloc] initWithData:received encoding:NSUTF8StringEncoding] ?: @"";
|
|
100
|
+
if ([soFar containsString:@"HTTP/1.1 200"]) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
[received appendBytes:chunk length:(NSUInteger)n];
|
|
106
|
+
// The response has started arriving; poll in short slices from here so a keep-alive success
|
|
107
|
+
// doesn't sit out the whole timeout waiting for an EOF that never comes.
|
|
108
|
+
struct timeval drainTv = { .tv_sec = 0, .tv_usec = 200000 };
|
|
109
|
+
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &drainTv, sizeof(drainTv));
|
|
110
|
+
}
|
|
111
|
+
close(fd);
|
|
112
|
+
return [[NSString alloc] initWithData:received encoding:NSUTF8StringEncoding] ?: @"";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
- (void)testWellFormedRequestStillSucceeds
|
|
116
|
+
{
|
|
117
|
+
BOOL didClose;
|
|
118
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[@"GET /framing/ping HTTP/1.1\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]
|
|
119
|
+
timeout:5.0
|
|
120
|
+
didClose:&didClose];
|
|
121
|
+
XCTAssertTrue([response containsString:@"200"], @"%@", response);
|
|
122
|
+
XCTAssertTrue([response containsString:@"pong"], @"%@", response);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
- (void)testNonNumericContentLengthIsRejected
|
|
126
|
+
{
|
|
127
|
+
// Under -integerValue's lenient parsing "bogus" became 0: the probe route would run with an
|
|
128
|
+
// empty body and the smuggled GET below would be answered as a second pipelined request.
|
|
129
|
+
NSString *payload = @"POST /framing/probe HTTP/1.1\r\nContent-Length: bogus\r\n\r\nGET /framing/ping HTTP/1.1\r\n\r\n";
|
|
130
|
+
BOOL didClose;
|
|
131
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[payload dataUsingEncoding:NSUTF8StringEncoding]
|
|
132
|
+
timeout:5.0
|
|
133
|
+
didClose:&didClose];
|
|
134
|
+
XCTAssertTrue([response containsString:@"400"], @"%@", response);
|
|
135
|
+
XCTAssertFalse([response containsString:@"pong"], @"the smuggled request must not be answered: %@", response);
|
|
136
|
+
XCTAssertTrue(didClose, @"the connection must be closed after unparseable framing");
|
|
137
|
+
XCTAssertEqual(atomic_load(&gFramingProbeHits), 0, @"the route must not be dispatched with unknown body extent");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
- (void)testWhitespaceBeforeHeaderColonIsRejected
|
|
141
|
+
{
|
|
142
|
+
// RFC 7230 (3.2.4): whitespace between a field name and its colon MUST be rejected with a 400.
|
|
143
|
+
// Tolerating it stores "content-length " as a distinct key, dispatches the request with a
|
|
144
|
+
// zero-length body, and re-parses the declared body as a smuggled pipelined request.
|
|
145
|
+
NSString *payload = @"POST /framing/probe HTTP/1.1\r\nContent-Length : 5\r\n\r\nhello";
|
|
146
|
+
BOOL didClose;
|
|
147
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[payload dataUsingEncoding:NSUTF8StringEncoding]
|
|
148
|
+
timeout:5.0
|
|
149
|
+
didClose:&didClose];
|
|
150
|
+
XCTAssertTrue([response containsString:@"400"], @"%@", response);
|
|
151
|
+
XCTAssertTrue(didClose);
|
|
152
|
+
XCTAssertEqual(atomic_load(&gFramingProbeHits), 0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
- (void)testHeaderLineWithoutColonIsRejected
|
|
156
|
+
{
|
|
157
|
+
// Silently skipping the malformed line made this dispatch with an empty body while "hello"
|
|
158
|
+
// stayed in the buffer to be parsed as the next request.
|
|
159
|
+
NSString *payload = @"POST /framing/probe HTTP/1.1\r\nContent-Length 5\r\n\r\nhello";
|
|
160
|
+
BOOL didClose;
|
|
161
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[payload dataUsingEncoding:NSUTF8StringEncoding]
|
|
162
|
+
timeout:5.0
|
|
163
|
+
didClose:&didClose];
|
|
164
|
+
XCTAssertTrue([response containsString:@"400"], @"%@", response);
|
|
165
|
+
XCTAssertTrue(didClose);
|
|
166
|
+
XCTAssertEqual(atomic_load(&gFramingProbeHits), 0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
- (void)testDuplicateContentLengthIsRejected
|
|
170
|
+
{
|
|
171
|
+
// RFC 7230 (3.3.3): repeated framing fields are unrecoverable. Last-wins assignment would let
|
|
172
|
+
// the second value drive parsing while an intermediary used the first - a smuggling primitive.
|
|
173
|
+
NSString *payload = @"POST /framing/probe HTTP/1.1\r\nContent-Length: 5\r\nContent-Length: 0\r\n\r\nhello";
|
|
174
|
+
BOOL didClose;
|
|
175
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[payload dataUsingEncoding:NSUTF8StringEncoding]
|
|
176
|
+
timeout:5.0
|
|
177
|
+
didClose:&didClose];
|
|
178
|
+
XCTAssertTrue([response containsString:@"400"], @"%@", response);
|
|
179
|
+
XCTAssertTrue(didClose);
|
|
180
|
+
XCTAssertEqual(atomic_load(&gFramingProbeHits), 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
- (void)testEmptyTransferEncodingIsRejected
|
|
184
|
+
{
|
|
185
|
+
// "chunked" followed by an empty value: with last-wins assignment plus a non-empty presence
|
|
186
|
+
// check, the empty value used to make the header look absent, so the chunked body was parsed
|
|
187
|
+
// as a zero-length body and its bytes re-read as smuggled requests.
|
|
188
|
+
NSString *payload = @"POST /framing/probe HTTP/1.1\r\nTransfer-Encoding: chunked\r\nTransfer-Encoding: \r\n\r\n0\r\n\r\n";
|
|
189
|
+
BOOL didClose;
|
|
190
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[payload dataUsingEncoding:NSUTF8StringEncoding]
|
|
191
|
+
timeout:5.0
|
|
192
|
+
didClose:&didClose];
|
|
193
|
+
XCTAssertTrue([response containsString:@"400"] || [response containsString:@"501"], @"%@", response);
|
|
194
|
+
XCTAssertTrue(didClose);
|
|
195
|
+
XCTAssertEqual(atomic_load(&gFramingProbeHits), 0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
- (void)testPipelinedRequestsAreServedInOrder
|
|
199
|
+
{
|
|
200
|
+
// Two requests in one payload: both must be answered on the same connection. Guards the
|
|
201
|
+
// response backpressure logic - the next pipelined request is only processed once the
|
|
202
|
+
// previous response's send completed, which must not stall or reorder the pipeline.
|
|
203
|
+
NSString *payload = @"GET /framing/ping HTTP/1.1\r\n\r\nGET /framing/ping HTTP/1.1\r\n\r\n";
|
|
204
|
+
BOOL didClose;
|
|
205
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[payload dataUsingEncoding:NSUTF8StringEncoding]
|
|
206
|
+
timeout:5.0
|
|
207
|
+
didClose:&didClose];
|
|
208
|
+
NSUInteger pongCount = [response componentsSeparatedByString:@"pong"].count - 1;
|
|
209
|
+
XCTAssertEqual(pongCount, 2, @"both pipelined requests must be answered: %@", response);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
- (void)testPartiallyNumericContentLengthIsRejected
|
|
213
|
+
{
|
|
214
|
+
NSString *payload = @"POST /framing/probe HTTP/1.1\r\nContent-Length: 5abc\r\n\r\nhello";
|
|
215
|
+
BOOL didClose;
|
|
216
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[payload dataUsingEncoding:NSUTF8StringEncoding]
|
|
217
|
+
timeout:5.0
|
|
218
|
+
didClose:&didClose];
|
|
219
|
+
XCTAssertTrue([response containsString:@"400"], @"%@", response);
|
|
220
|
+
XCTAssertTrue(didClose);
|
|
221
|
+
XCTAssertEqual(atomic_load(&gFramingProbeHits), 0);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
- (void)testOversizedHeaderBlockIsRejected
|
|
225
|
+
{
|
|
226
|
+
// A header block that never terminates: 96 KiB of header lines with no \r\n\r\n. The server
|
|
227
|
+
// must stop buffering and close the connection instead of growing the buffer indefinitely.
|
|
228
|
+
NSMutableString *payload = [NSMutableString stringWithString:@"GET /framing/ping HTTP/1.1\r\n"];
|
|
229
|
+
NSString *filler = [@"X-Filler: " stringByAppendingString:[@"" stringByPaddingToLength:1013 withString:@"a" startingAtIndex:0]];
|
|
230
|
+
while (payload.length < 96 * 1024) {
|
|
231
|
+
[payload appendString:filler];
|
|
232
|
+
[payload appendString:@"\r\n"];
|
|
233
|
+
}
|
|
234
|
+
BOOL didClose;
|
|
235
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[payload dataUsingEncoding:NSUTF8StringEncoding]
|
|
236
|
+
timeout:10.0
|
|
237
|
+
didClose:&didClose];
|
|
238
|
+
XCTAssertTrue([response containsString:@"400"], @"%@", response);
|
|
239
|
+
XCTAssertTrue(didClose, @"the connection must be closed rather than left buffering");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
- (void)testOversizedCompletedHeaderBlockIsRejected
|
|
243
|
+
{
|
|
244
|
+
// Same flood, but properly terminated with \r\n\r\n. Depending on how the bytes coalesce, the
|
|
245
|
+
// terminator can arrive in the same receive callback as the bulk of the block, in which case
|
|
246
|
+
// the incomplete-header cap never fires - the completed block must be rejected too instead of
|
|
247
|
+
// being copied and parsed.
|
|
248
|
+
NSMutableString *payload = [NSMutableString stringWithString:@"GET /framing/ping HTTP/1.1\r\n"];
|
|
249
|
+
NSString *filler = [@"X-Filler: " stringByAppendingString:[@"" stringByPaddingToLength:1013 withString:@"a" startingAtIndex:0]];
|
|
250
|
+
while (payload.length < 96 * 1024) {
|
|
251
|
+
[payload appendString:filler];
|
|
252
|
+
[payload appendString:@"\r\n"];
|
|
253
|
+
}
|
|
254
|
+
[payload appendString:@"\r\n"];
|
|
255
|
+
BOOL didClose;
|
|
256
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[payload dataUsingEncoding:NSUTF8StringEncoding]
|
|
257
|
+
timeout:10.0
|
|
258
|
+
didClose:&didClose];
|
|
259
|
+
XCTAssertTrue([response containsString:@"400"], @"%@", response);
|
|
260
|
+
XCTAssertFalse([response containsString:@"pong"], @"the oversized request must not be served: %@", response);
|
|
261
|
+
XCTAssertTrue(didClose, @"the connection must be closed rather than left buffering");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@end
|