appium-webdriveragent 16.7.0 → 16.7.2
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 +12 -0
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBHTTPServer.h +3 -2
- package/WebDriverAgentLib/Routing/FBHTTPServer.m +141 -53
- package/WebDriverAgentLib/Routing/FBTCPSocket.m +25 -11
- package/WebDriverAgentLib/Routing/FBWebServer.m +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [16.7.2](https://github.com/appium/WebDriverAgent/compare/v16.7.1...v16.7.2) (2026-08-24)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* harden FBHTTPServer/FBTCPSocket against races and protocol gaps ([#1224](https://github.com/appium/WebDriverAgent/issues/1224)) ([cf4bb2b](https://github.com/appium/WebDriverAgent/commit/cf4bb2b57b4d3a55ee4bbe8f860b00b0e7f5a326))
|
|
6
|
+
|
|
7
|
+
## [16.7.1](https://github.com/appium/WebDriverAgent/compare/v16.7.0...v16.7.1) (2026-08-23)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* return W3C-compliant JSON error for unmatched routes ([#1223](https://github.com/appium/WebDriverAgent/issues/1223)) ([c951a91](https://github.com/appium/WebDriverAgent/commit/c951a91c8202d3c875db04dd633d37b409c80e43))
|
|
12
|
+
|
|
1
13
|
## [16.7.0](https://github.com/appium/WebDriverAgent/compare/v16.6.0...v16.7.0) (2026-08-22)
|
|
2
14
|
|
|
3
15
|
### Features
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>FMWK</string>
|
|
17
17
|
<key>CFBundleShortVersionString</key>
|
|
18
|
-
<string>16.7.
|
|
18
|
+
<string>16.7.2</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.7.
|
|
22
|
+
<string>16.7.2</string>
|
|
23
23
|
<key>NSPrincipalClass</key>
|
|
24
24
|
<string/>
|
|
25
25
|
</dict>
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// A minimal HTTP/1.1 server on top of FBTCPSocket (Network.framework-backed on every platform,
|
|
10
10
|
// since watchOS forbids BSD sockets outright - see FBTCPSocket.h/.m).
|
|
11
11
|
//
|
|
12
|
-
// No
|
|
13
|
-
//
|
|
12
|
+
// No range requests or request pipelining - just request line + headers + Content-Length body,
|
|
13
|
+
// and ":param" path matching. Any Transfer-Encoding is rejected outright (501) rather than
|
|
14
|
+
// silently mishandled, since no decoder is implemented.
|
|
14
15
|
|
|
15
16
|
@import Foundation;
|
|
16
17
|
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
#import "FBHTTPServer.h"
|
|
10
10
|
|
|
11
|
+
#import "FBCommandStatus.h"
|
|
11
12
|
#import "FBConfiguration.h"
|
|
13
|
+
#import "FBResponsePayload.h"
|
|
12
14
|
#import "FBTCPSocket.h"
|
|
13
15
|
|
|
14
16
|
static NSData *FBCRLFCRLFData(void)
|
|
@@ -39,6 +41,20 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
39
41
|
@end
|
|
40
42
|
|
|
41
43
|
|
|
44
|
+
// Cached result of parsing a connection's request line + headers, kept around while its body is
|
|
45
|
+
// still streaming in so a slow body doesn't cause the header block to be re-found and re-parsed
|
|
46
|
+
// on every single incoming TCP segment.
|
|
47
|
+
@interface FBPendingHTTPRequestHeader : NSObject
|
|
48
|
+
@property (nonatomic, copy) NSString *method;
|
|
49
|
+
@property (nonatomic, copy) NSString *pathAndQuery;
|
|
50
|
+
@property (nonatomic) NSUInteger bodyStart;
|
|
51
|
+
@property (nonatomic) NSUInteger contentLength;
|
|
52
|
+
@end
|
|
53
|
+
|
|
54
|
+
@implementation FBPendingHTTPRequestHeader
|
|
55
|
+
@end
|
|
56
|
+
|
|
57
|
+
|
|
42
58
|
@interface FBHTTPServer () <FBTCPSocketDelegate>
|
|
43
59
|
|
|
44
60
|
@property (nonatomic, nullable, strong) FBTCPSocket *socket;
|
|
@@ -48,6 +64,9 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
48
64
|
@property (nonatomic, copy, nullable) NSString *interface;
|
|
49
65
|
// nw_connection_t isn't NSCopying, so it can't be an NSDictionary key - use NSMapTable instead.
|
|
50
66
|
@property (nonatomic, strong) NSMapTable<id, NSMutableData *> *connectionBuffers;
|
|
67
|
+
// Per-client cache of the already-parsed request line + headers while its body is still
|
|
68
|
+
// arriving; nil while a client's next unread bytes start with an unparsed header block.
|
|
69
|
+
@property (nonatomic, strong) NSMapTable<id, FBPendingHTTPRequestHeader *> *pendingRequestHeaders;
|
|
51
70
|
|
|
52
71
|
@end
|
|
53
72
|
|
|
@@ -60,6 +79,8 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
60
79
|
_defaultHeaders = [NSMutableDictionary dictionary];
|
|
61
80
|
_connectionBuffers = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
|
|
62
81
|
valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
|
|
82
|
+
_pendingRequestHeaders = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
|
|
83
|
+
valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
|
|
63
84
|
}
|
|
64
85
|
return self;
|
|
65
86
|
}
|
|
@@ -101,6 +122,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
101
122
|
error:nil];
|
|
102
123
|
NSMutableString *regexPath = [NSMutableString stringWithString:escapedPath];
|
|
103
124
|
__block NSInteger diff = 0;
|
|
125
|
+
__block NSUInteger wildcardIndex = 0;
|
|
104
126
|
[paramRegex enumerateMatchesInString:escapedPath
|
|
105
127
|
options:(NSMatchingOptions)0
|
|
106
128
|
range:NSMakeRange(0, escapedPath.length)
|
|
@@ -109,7 +131,11 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
109
131
|
NSString *capturedString = [escapedPath substringWithRange:result.range];
|
|
110
132
|
NSString *replacementString;
|
|
111
133
|
if ([capturedString isEqualToString:@"*"]) {
|
|
112
|
-
|
|
134
|
+
// Only the first wildcard keeps the plain "wildcards" name - later ones get an index
|
|
135
|
+
// suffix so multiple "*" segments in one path don't overwrite each other's capture.
|
|
136
|
+
NSString *wildcardKey = 0 == wildcardIndex ? @"wildcards" : [NSString stringWithFormat:@"wildcards%lu", (unsigned long)wildcardIndex];
|
|
137
|
+
wildcardIndex++;
|
|
138
|
+
[keys addObject:wildcardKey];
|
|
113
139
|
replacementString = @"(.*?)";
|
|
114
140
|
} else {
|
|
115
141
|
NSString *keyString = [escapedPath substringWithRange:[result rangeAtIndex:2]];
|
|
@@ -164,6 +190,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
164
190
|
self.socket = nil;
|
|
165
191
|
@synchronized (self.connectionBuffers) {
|
|
166
192
|
[self.connectionBuffers removeAllObjects];
|
|
193
|
+
[self.pendingRequestHeaders removeAllObjects];
|
|
167
194
|
}
|
|
168
195
|
_isRunning = NO;
|
|
169
196
|
}
|
|
@@ -181,6 +208,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
181
208
|
{
|
|
182
209
|
@synchronized (self.connectionBuffers) {
|
|
183
210
|
[self.connectionBuffers removeObjectForKey:client];
|
|
211
|
+
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
184
212
|
}
|
|
185
213
|
}
|
|
186
214
|
|
|
@@ -203,72 +231,130 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
203
231
|
{
|
|
204
232
|
while (YES) {
|
|
205
233
|
NSMutableData *buffer;
|
|
234
|
+
FBPendingHTTPRequestHeader *pending;
|
|
206
235
|
@synchronized (self.connectionBuffers) {
|
|
207
236
|
buffer = [self.connectionBuffers objectForKey:client];
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
237
|
+
if (nil == buffer) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
pending = [self.pendingRequestHeaders objectForKey:client];
|
|
211
241
|
}
|
|
212
242
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
243
|
+
if (nil == pending) {
|
|
244
|
+
NSRange headerEndRange = [buffer rangeOfData:FBCRLFCRLFData() options:(NSDataSearchOptions)0 range:NSMakeRange(0, buffer.length)];
|
|
245
|
+
if (NSNotFound == headerEndRange.location) {
|
|
246
|
+
// Wait for the rest of the header block to arrive.
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
217
249
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
250
|
+
NSData *headerData = [buffer subdataWithRange:NSMakeRange(0, headerEndRange.location)];
|
|
251
|
+
NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding];
|
|
252
|
+
NSArray<NSString *> *lines = [headerString componentsSeparatedByString:@"\r\n"];
|
|
253
|
+
if (lines.count < 1) {
|
|
254
|
+
[self respondBadRequestToClient:client];
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
225
257
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
NSString *method = requestLineParts[0].uppercaseString;
|
|
232
|
-
NSString *pathAndQuery = requestLineParts[1];
|
|
233
|
-
|
|
234
|
-
NSMutableDictionary<NSString *, NSString *> *requestHeaders = [NSMutableDictionary dictionary];
|
|
235
|
-
for (NSUInteger i = 1; i < lines.count; i++) {
|
|
236
|
-
NSString *line = lines[i];
|
|
237
|
-
NSRange colonRange = [line rangeOfString:@":"];
|
|
238
|
-
if (NSNotFound == colonRange.location) {
|
|
239
|
-
continue;
|
|
258
|
+
NSArray<NSString *> *requestLineParts = [lines.firstObject componentsSeparatedByString:@" "];
|
|
259
|
+
if (requestLineParts.count < 2) {
|
|
260
|
+
[self respondBadRequestToClient:client];
|
|
261
|
+
return;
|
|
240
262
|
}
|
|
241
|
-
NSString *name = [line substringToIndex:colonRange.location];
|
|
242
|
-
NSString *value = [[line substringFromIndex:colonRange.location + 1]
|
|
243
|
-
stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
|
244
|
-
requestHeaders[name.lowercaseString] = value;
|
|
245
|
-
}
|
|
246
263
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
264
|
+
NSMutableDictionary<NSString *, NSString *> *requestHeaders = [NSMutableDictionary dictionary];
|
|
265
|
+
for (NSUInteger i = 1; i < lines.count; i++) {
|
|
266
|
+
NSString *line = lines[i];
|
|
267
|
+
NSRange colonRange = [line rangeOfString:@":"];
|
|
268
|
+
if (NSNotFound == colonRange.location) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
NSString *name = [line substringToIndex:colonRange.location];
|
|
272
|
+
NSString *value = [[line substringFromIndex:colonRange.location + 1]
|
|
273
|
+
stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
|
274
|
+
requestHeaders[name.lowercaseString] = value;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
NSString *transferEncoding = requestHeaders[@"transfer-encoding"];
|
|
278
|
+
if (transferEncoding.length > 0) {
|
|
279
|
+
// No transfer decoder is implemented at all, so any encoding (chunked or otherwise -
|
|
280
|
+
// including a value only introduced by a duplicate header overwriting "chunked" above)
|
|
281
|
+
// is rejected rather than risking the body being misread as empty and desyncing the rest
|
|
282
|
+
// of the connection's request stream.
|
|
283
|
+
RouteResponse *notImplemented = [RouteResponse new];
|
|
284
|
+
id<FBResponsePayload> notImplementedPayload = FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:@"Transfer-Encoding is not supported"
|
|
285
|
+
traceback:nil]);
|
|
286
|
+
[notImplementedPayload dispatchWithResponse:notImplemented];
|
|
287
|
+
[self failClient:client withResponse:notImplemented];
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
NSUInteger contentLength = (NSUInteger)requestHeaders[@"content-length"].integerValue;
|
|
292
|
+
if (contentLength > FBConfiguration.sharedInstance.httpRequestBodySizeLimit) {
|
|
293
|
+
// Mirrors CocoaHTTPServer's maxRequestBodySize enforcement. Closes the connection after
|
|
294
|
+
// responding, since the rest of the oversized body is still incoming.
|
|
295
|
+
RouteResponse *tooLarge = [RouteResponse new];
|
|
296
|
+
id<FBResponsePayload> tooLargePayload = FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:@"Request Entity Too Large"
|
|
297
|
+
traceback:nil]);
|
|
298
|
+
[tooLargePayload dispatchWithResponse:tooLarge];
|
|
299
|
+
[self failClient:client withResponse:tooLarge];
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
pending = [FBPendingHTTPRequestHeader new];
|
|
304
|
+
pending.method = requestLineParts[0].uppercaseString;
|
|
305
|
+
pending.pathAndQuery = requestLineParts[1];
|
|
306
|
+
pending.bodyStart = headerEndRange.location + headerEndRange.length;
|
|
307
|
+
pending.contentLength = contentLength;
|
|
308
|
+
@synchronized (self.connectionBuffers) {
|
|
309
|
+
[self.pendingRequestHeaders setObject:pending forKey:client];
|
|
310
|
+
}
|
|
256
311
|
}
|
|
257
|
-
|
|
258
|
-
NSUInteger totalRequestLength = bodyStart + contentLength;
|
|
312
|
+
|
|
313
|
+
NSUInteger totalRequestLength = pending.bodyStart + pending.contentLength;
|
|
259
314
|
if (buffer.length < totalRequestLength) {
|
|
260
|
-
// Wait for the rest of the body to arrive
|
|
315
|
+
// Wait for the rest of the body to arrive - the parsed header stays cached above, so this
|
|
316
|
+
// doesn't re-scan/re-parse the header block on every subsequently arriving chunk.
|
|
261
317
|
return;
|
|
262
318
|
}
|
|
263
319
|
|
|
264
|
-
NSData *body = contentLength > 0 ? [buffer subdataWithRange:NSMakeRange(bodyStart, contentLength)] : [NSData data];
|
|
320
|
+
NSData *body = pending.contentLength > 0 ? [buffer subdataWithRange:NSMakeRange(pending.bodyStart, pending.contentLength)] : [NSData data];
|
|
265
321
|
|
|
266
322
|
@synchronized (self.connectionBuffers) {
|
|
267
323
|
[buffer replaceBytesInRange:NSMakeRange(0, totalRequestLength) withBytes:NULL length:0];
|
|
324
|
+
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
268
325
|
}
|
|
269
326
|
|
|
270
|
-
[self dispatchMethod:method pathAndQuery:pathAndQuery body:body client:client];
|
|
327
|
+
[self dispatchMethod:pending.method pathAndQuery:pending.pathAndQuery body:body client:client];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Removes the client's buffered state and responds with a closing error response. Removing the
|
|
332
|
+
// buffer synchronously ensures any request bytes still streaming in for this connection are
|
|
333
|
+
// dropped rather than being re-parsed and re-triggering this same response.
|
|
334
|
+
- (void)failClient:(nw_connection_t)client withResponse:(RouteResponse *)response
|
|
335
|
+
{
|
|
336
|
+
@synchronized (self.connectionBuffers) {
|
|
337
|
+
[self.connectionBuffers removeObjectForKey:client];
|
|
338
|
+
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
271
339
|
}
|
|
340
|
+
[self applyDefaultHeadersToResponse:response];
|
|
341
|
+
[self writeResponse:response toClient:client thenCloseConnection:YES];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
- (void)respondBadRequestToClient:(nw_connection_t)client
|
|
345
|
+
{
|
|
346
|
+
RouteResponse *badRequest = [RouteResponse new];
|
|
347
|
+
id<FBResponsePayload> payload = FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:@"The request could not be parsed as valid HTTP"
|
|
348
|
+
traceback:nil]);
|
|
349
|
+
[payload dispatchWithResponse:badRequest];
|
|
350
|
+
[self failClient:client withResponse:badRequest];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
- (void)applyDefaultHeadersToResponse:(RouteResponse *)response
|
|
354
|
+
{
|
|
355
|
+
[self.defaultHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) {
|
|
356
|
+
[response setHeader:field value:value];
|
|
357
|
+
}];
|
|
272
358
|
}
|
|
273
359
|
|
|
274
360
|
- (void)dispatchMethod:(NSString *)method pathAndQuery:(NSString *)pathAndQuery body:(NSData *)body client:(nw_connection_t)client
|
|
@@ -300,9 +386,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
300
386
|
NSURL *url = [NSURL URLWithString:path] ?: [NSURL URLWithString:@"/"];
|
|
301
387
|
RouteRequest *request = [[RouteRequest alloc] initWithURL:url params:params.copy body:body];
|
|
302
388
|
RouteResponse *response = [RouteResponse new];
|
|
303
|
-
[self
|
|
304
|
-
[response setHeader:field value:value];
|
|
305
|
-
}];
|
|
389
|
+
[self applyDefaultHeadersToResponse:response];
|
|
306
390
|
|
|
307
391
|
void (^invoke)(void) = ^{
|
|
308
392
|
route.block(request, response);
|
|
@@ -318,8 +402,10 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
318
402
|
}
|
|
319
403
|
|
|
320
404
|
RouteResponse *notFound = [RouteResponse new];
|
|
321
|
-
|
|
322
|
-
|
|
405
|
+
FBCommandStatus *status = [FBCommandStatus unknownCommandErrorWithMessage:nil
|
|
406
|
+
traceback:nil];
|
|
407
|
+
[FBResponseWithStatus(status) dispatchWithResponse:notFound];
|
|
408
|
+
[self applyDefaultHeadersToResponse:notFound];
|
|
323
409
|
[self writeResponse:notFound toClient:client];
|
|
324
410
|
}
|
|
325
411
|
|
|
@@ -380,6 +466,8 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
380
466
|
return @"Request Timeout";
|
|
381
467
|
} else if (kHTTPStatusCodeRequestEntityTooLarge == statusCode) {
|
|
382
468
|
return @"Request Entity Too Large";
|
|
469
|
+
} else if (kHTTPStatusCodeNotImplemented == statusCode) {
|
|
470
|
+
return @"Not Implemented";
|
|
383
471
|
} else if (kHTTPStatusCodeInternalServerError == statusCode) {
|
|
384
472
|
return @"Internal Server Error";
|
|
385
473
|
}
|
|
@@ -78,12 +78,18 @@
|
|
|
78
78
|
// NSLocalizedDescriptionKey must be a string, not the underlying NSError itself, or
|
|
79
79
|
// -[NSError localizedDescription] crashes trying to treat it as one.
|
|
80
80
|
NSError *underlyingError = nwError ? (NSError *)CFBridgingRelease(nw_error_copy_cf_error(nwError)) : nil;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
if ([underlyingError.domain isEqualToString:NSPOSIXErrorDomain]) {
|
|
82
|
+
// Surface POSIX errors (e.g. EADDRINUSE) directly, since callers like FBWebServer check
|
|
83
|
+
// for them by domain/code on the top-level error to decide whether to retry another port.
|
|
84
|
+
startupError = underlyingError;
|
|
85
|
+
} else {
|
|
86
|
+
NSMutableDictionary<NSString *, id> *userInfo = [NSMutableDictionary dictionary];
|
|
87
|
+
userInfo[NSLocalizedDescriptionKey] = underlyingError.localizedDescription ?: @"The TCP listener failed to start";
|
|
88
|
+
if (underlyingError) {
|
|
89
|
+
userInfo[NSUnderlyingErrorKey] = underlyingError;
|
|
90
|
+
}
|
|
91
|
+
startupError = [NSError errorWithDomain:@"FBTCPSocket" code:2 userInfo:userInfo];
|
|
85
92
|
}
|
|
86
|
-
startupError = [NSError errorWithDomain:@"FBTCPSocket" code:2 userInfo:userInfo];
|
|
87
93
|
dispatch_semaphore_signal(startupSemaphore);
|
|
88
94
|
}
|
|
89
95
|
});
|
|
@@ -98,6 +104,10 @@
|
|
|
98
104
|
if (error) {
|
|
99
105
|
*error = startupError;
|
|
100
106
|
}
|
|
107
|
+
// Cancel rather than just dropping our reference - otherwise a late ready/failed callback
|
|
108
|
+
// can still fire and the port stays bound at the OS level even though the caller was told
|
|
109
|
+
// startup failed.
|
|
110
|
+
nw_listener_cancel(listener);
|
|
101
111
|
self.listener = nil;
|
|
102
112
|
return NO;
|
|
103
113
|
}
|
|
@@ -144,11 +154,9 @@
|
|
|
144
154
|
}
|
|
145
155
|
if (nil != content) {
|
|
146
156
|
dispatch_data_t nonnullContent = (dispatch_data_t _Nonnull)content;
|
|
147
|
-
|
|
157
|
+
NSMutableData *data = [NSMutableData data];
|
|
148
158
|
dispatch_data_apply(nonnullContent, ^bool(dispatch_data_t _Nonnull region, size_t offset, const void * _Nonnull buffer, size_t size) {
|
|
149
|
-
|
|
150
|
-
[accumulated appendBytes:buffer length:size];
|
|
151
|
-
data = accumulated.copy;
|
|
159
|
+
[data appendBytes:buffer length:size];
|
|
152
160
|
return true;
|
|
153
161
|
});
|
|
154
162
|
if (data.length > 0) {
|
|
@@ -198,13 +206,19 @@
|
|
|
198
206
|
|
|
199
207
|
- (void)stop
|
|
200
208
|
{
|
|
209
|
+
NSArray<nw_connection_t> *clients;
|
|
201
210
|
@synchronized (self.connectedClients) {
|
|
202
|
-
|
|
211
|
+
clients = self.connectedClients.copy;
|
|
203
212
|
[self.connectedClients removeAllObjects];
|
|
213
|
+
}
|
|
214
|
+
// Cancel on socketQueue, the same queue every connection's send/receive is bound to (see
|
|
215
|
+
// -acceptConnection:), so a write already issued just before -stop (e.g. a shutdown route's
|
|
216
|
+
// response) is processed before the cancellation rather than racing it.
|
|
217
|
+
dispatch_async(self.socketQueue, ^{
|
|
204
218
|
for (nw_connection_t client in clients) {
|
|
205
219
|
nw_connection_cancel(client);
|
|
206
220
|
}
|
|
207
|
-
}
|
|
221
|
+
});
|
|
208
222
|
|
|
209
223
|
self.delegate = nil;
|
|
210
224
|
nw_listener_t listener = self.listener;
|
|
@@ -261,7 +261,11 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
[response respondWithString:@"Shutting down"];
|
|
264
|
-
|
|
264
|
+
// Deferred so the "Shutting down" response is written to the client before
|
|
265
|
+
// webServerDidRequestShutdown: tears down the server's socket out from under it.
|
|
266
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
267
|
+
[strongSelf.delegate webServerDidRequestShutdown:strongSelf];
|
|
268
|
+
});
|
|
265
269
|
}];
|
|
266
270
|
|
|
267
271
|
[self registerRouteHandlers:@[FBUnknownCommands.class]];
|