appium-mac2-driver 4.1.1 → 4.3.0

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/WebDriverAgentMac/IntegrationTests/AMFindElementTests.m +165 -2
  3. package/WebDriverAgentMac/IntegrationTests/AMIntegrationTestCase.h +11 -0
  4. package/WebDriverAgentMac/IntegrationTests/AMIntegrationTestCase.m +33 -0
  5. package/WebDriverAgentMac/IntegrationTests/AMW3CActionsTests.m +7 -9
  6. package/WebDriverAgentMac/WebDriverAgent/Base.lproj/Main.storyboard +16 -1
  7. package/WebDriverAgentMac/WebDriverAgent/WebViewController.h +21 -0
  8. package/WebDriverAgentMac/WebDriverAgent/WebViewController.m +49 -0
  9. package/WebDriverAgentMac/WebDriverAgentLib/Categories/NSPredicate+FBFormat.m +80 -0
  10. package/WebDriverAgentMac/WebDriverAgentLib/Categories/XCUIElement+AMAttributes.h +29 -1
  11. package/WebDriverAgentMac/WebDriverAgentLib/Categories/XCUIElement+AMAttributes.m +66 -15
  12. package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMHTTPServer.h +84 -0
  13. package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMHTTPServer.m +493 -0
  14. package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMTCPSocket.h +109 -0
  15. package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMTCPSocket.m +235 -0
  16. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBWebServer.h +1 -1
  17. package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBWebServer.m +9 -21
  18. package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteRequest.h +36 -0
  19. package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteRequest.m +33 -0
  20. package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteResponse.h +37 -0
  21. package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteResponse.m +62 -0
  22. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.h +22 -0
  23. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.m +5 -11
  24. package/WebDriverAgentMac/WebDriverAgentLib/Utilities/FBElementUtils.m +7 -1
  25. package/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj +46 -180
  26. package/npm-shrinkwrap.json +75 -146
  27. package/package.json +2 -1
  28. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h +0 -1226
  29. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m +0 -8513
  30. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.h +0 -1036
  31. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m +0 -5632
  32. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h +0 -12
  33. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m +0 -88
  34. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h +0 -56
  35. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m +0 -106
  36. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h +0 -107
  37. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m +0 -2238
  38. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h +0 -122
  39. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h +0 -48
  40. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m +0 -114
  41. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h +0 -149
  42. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h +0 -200
  43. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m +0 -720
  44. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h +0 -13
  45. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m +0 -83
  46. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h +0 -9
  47. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m +0 -40
  48. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h +0 -13
  49. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m +0 -84
  50. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h +0 -18
  51. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m +0 -11
  52. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h +0 -16
  53. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m +0 -50
  54. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h +0 -20
  55. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m +0 -66
  56. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h +0 -5
  57. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m +0 -142
  58. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h +0 -55
  59. package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m +0 -288
@@ -0,0 +1,493 @@
1
+ /*
2
+ * Licensed under the Apache License, Version 2.0 (the "License");
3
+ * you may not use this file except in compliance with the License.
4
+ * See the NOTICE file distributed with this work for additional
5
+ * information regarding copyright ownership.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ #import "AMHTTPServer.h"
18
+
19
+ #import "AMTCPSocket.h"
20
+ #import "FBHTTPStatusCodes.h"
21
+ #import "FBLogger.h"
22
+ #import "FBResponsePayload.h"
23
+
24
+ // Mirrors the safety net the previous CocoaHTTPServer-based stack provided: reject request
25
+ // bodies above this size instead of buffering them fully in memory.
26
+ static const NSUInteger kMaxHTTPRequestBodySize = 64 * 1024 * 1024;
27
+
28
+ static NSData *AMCRLFCRLFData(void)
29
+ {
30
+ static NSData *data;
31
+ static dispatch_once_t onceToken;
32
+ dispatch_once(&onceToken, ^{
33
+ data = [@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
34
+ });
35
+ return data;
36
+ }
37
+
38
+ // -dataUsingEncoding:NSUTF8StringEncoding never actually returns nil; this just keeps the cast
39
+ // out of every call site below.
40
+ static NSData * _Nonnull AMUTF8Data(NSString *string)
41
+ {
42
+ return (NSData * _Nonnull)[string dataUsingEncoding:NSUTF8StringEncoding];
43
+ }
44
+
45
+ @interface AMHTTPRoute : NSObject
46
+ @property (nonatomic, copy) NSString *verb;
47
+ @property (nonatomic, strong) NSRegularExpression *regex;
48
+ @property (nonatomic, copy, nullable) NSArray<NSString *> *keys;
49
+ @property (nonatomic, copy) void (^block)(RouteRequest *request, RouteResponse *response);
50
+ @end
51
+
52
+ @implementation AMHTTPRoute
53
+ @end
54
+
55
+
56
+ // Cached result of parsing a connection's request line + headers, kept around while its body is
57
+ // still streaming in so a slow/chunked-over-the-wire body doesn't cause the header block to be
58
+ // re-found and re-parsed on every single incoming TCP segment.
59
+ @interface AMPendingHTTPRequestHeader : NSObject
60
+ @property (nonatomic, copy) NSString *method;
61
+ @property (nonatomic, copy) NSString *pathAndQuery;
62
+ @property (nonatomic) NSUInteger bodyStart;
63
+ @property (nonatomic) NSUInteger contentLength;
64
+ @end
65
+
66
+ @implementation AMPendingHTTPRequestHeader
67
+ @end
68
+
69
+
70
+ @interface AMHTTPServer () <AMTCPSocketDelegate>
71
+
72
+ @property (nonatomic, nullable, strong) AMTCPSocket *socket;
73
+ @property (nonatomic, strong) NSMutableArray<AMHTTPRoute *> *routes;
74
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, NSString *> *defaultHeaders;
75
+ @property (nonatomic, nullable) dispatch_queue_t routeQueue;
76
+ @property (nonatomic, copy, nullable) NSString *interface;
77
+ // nw_connection_t isn't NSCopying, so it can't be an NSDictionary key - use NSMapTable instead.
78
+ @property (nonatomic, strong) NSMapTable<id, NSMutableData *> *connectionBuffers;
79
+ // Per-client cache of the already-parsed request line + headers while its body is still
80
+ // arriving; nil while a client's next unread bytes start with an unparsed header block.
81
+ @property (nonatomic, strong) NSMapTable<id, AMPendingHTTPRequestHeader *> *pendingRequestHeaders;
82
+
83
+ @end
84
+
85
+ @implementation AMHTTPServer
86
+
87
+ - (instancetype)init
88
+ {
89
+ if ((self = [super init])) {
90
+ _routes = [NSMutableArray array];
91
+ _defaultHeaders = [NSMutableDictionary dictionary];
92
+ _connectionBuffers = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
93
+ valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
94
+ _pendingRequestHeaders = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
95
+ valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
96
+ }
97
+ return self;
98
+ }
99
+
100
+ - (void)setRouteQueue:(nullable dispatch_queue_t)queue
101
+ {
102
+ _routeQueue = queue;
103
+ }
104
+
105
+ - (void)setDefaultHeader:(NSString *)field value:(NSString *)value
106
+ {
107
+ self.defaultHeaders[field] = value;
108
+ }
109
+
110
+ - (void)setInterface:(nullable NSString *)interface
111
+ {
112
+ _interface = interface.copy;
113
+ }
114
+
115
+ #pragma mark - Route registration
116
+
117
+ - (AMHTTPRoute *)compiledRouteWithPath:(NSString *)path
118
+ {
119
+ AMHTTPRoute *route = [AMHTTPRoute new];
120
+ NSMutableArray<NSString *> *keys = [NSMutableArray array];
121
+
122
+ // Escape regex-significant characters before substituting :param placeholders, like
123
+ // RoutingHTTPServer.m used to.
124
+ NSRegularExpression *escapeRegex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]"
125
+ options:(NSRegularExpressionOptions)0
126
+ error:nil];
127
+ NSString *escapedPath = [escapeRegex stringByReplacingMatchesInString:path
128
+ options:(NSMatchingOptions)0
129
+ range:NSMakeRange(0, path.length)
130
+ withTemplate:@"\\\\$0"];
131
+
132
+ NSRegularExpression *paramRegex = [NSRegularExpression regularExpressionWithPattern:@"(:(\\w+)|\\*)"
133
+ options:(NSRegularExpressionOptions)0
134
+ error:nil];
135
+ NSMutableString *regexPath = [NSMutableString stringWithString:escapedPath];
136
+ __block NSInteger diff = 0;
137
+ __block NSUInteger wildcardIndex = 0;
138
+ [paramRegex enumerateMatchesInString:escapedPath
139
+ options:(NSMatchingOptions)0
140
+ range:NSMakeRange(0, escapedPath.length)
141
+ usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
142
+ NSRange replacementRange = NSMakeRange(diff + result.range.location, result.range.length);
143
+ NSString *capturedString = [escapedPath substringWithRange:result.range];
144
+ NSString *replacementString;
145
+ if ([capturedString isEqualToString:@"*"]) {
146
+ // Only the first wildcard keeps the plain "wildcards" name - later ones get an index
147
+ // suffix so multiple "*" segments in one path don't overwrite each other's capture.
148
+ NSString *wildcardKey = 0 == wildcardIndex ? @"wildcards" : [NSString stringWithFormat:@"wildcards%lu", (unsigned long)wildcardIndex];
149
+ wildcardIndex++;
150
+ [keys addObject:wildcardKey];
151
+ replacementString = @"(.*?)";
152
+ } else {
153
+ NSString *keyString = [escapedPath substringWithRange:[result rangeAtIndex:2]];
154
+ [keys addObject:keyString];
155
+ replacementString = @"([^/]+)";
156
+ }
157
+ [regexPath replaceCharactersInRange:replacementRange withString:replacementString];
158
+ diff += replacementString.length - result.range.length;
159
+ }];
160
+
161
+ NSString *anchoredPattern = [NSString stringWithFormat:@"^%@$", regexPath];
162
+ route.regex = [NSRegularExpression regularExpressionWithPattern:anchoredPattern
163
+ options:NSRegularExpressionCaseInsensitive
164
+ error:nil];
165
+ route.keys = keys.count > 0 ? keys.copy : nil;
166
+ return route;
167
+ }
168
+
169
+ - (void)handleMethod:(NSString *)method
170
+ withPath:(NSString *)path
171
+ block:(void (^)(RouteRequest *request, RouteResponse *response))block
172
+ {
173
+ AMHTTPRoute *route = [self compiledRouteWithPath:path];
174
+ route.verb = method.uppercaseString;
175
+ route.block = block;
176
+ [self.routes addObject:route];
177
+ }
178
+
179
+ - (void)get:(NSString *)path withBlock:(void (^)(RouteRequest *request, RouteResponse *response))block
180
+ {
181
+ [self handleMethod:@"GET" withPath:path block:block];
182
+ }
183
+
184
+ - (void)delete:(NSString *)path withBlock:(void (^)(RouteRequest *request, RouteResponse *response))block
185
+ {
186
+ [self handleMethod:@"DELETE" withPath:path block:block];
187
+ }
188
+
189
+ #pragma mark - Lifecycle
190
+
191
+ - (BOOL)start:(NSError **)error
192
+ {
193
+ AMTCPSocket *socket = [[AMTCPSocket alloc] initWithPort:self.port];
194
+ socket.interface = self.interface;
195
+ socket.delegate = self;
196
+ if (![socket startWithError:error]) {
197
+ return NO;
198
+ }
199
+ self.socket = socket;
200
+ _isRunning = YES;
201
+ return YES;
202
+ }
203
+
204
+ - (void)stop:(BOOL)immediately
205
+ {
206
+ [self.socket stop];
207
+ self.socket = nil;
208
+ @synchronized (self.connectionBuffers) {
209
+ [self.connectionBuffers removeAllObjects];
210
+ [self.pendingRequestHeaders removeAllObjects];
211
+ }
212
+ _isRunning = NO;
213
+ }
214
+
215
+ #pragma mark - AMTCPSocketDelegate
216
+
217
+ - (void)didClientConnect:(nw_connection_t)newClient
218
+ {
219
+ @synchronized (self.connectionBuffers) {
220
+ [self.connectionBuffers setObject:[NSMutableData data] forKey:newClient];
221
+ }
222
+ }
223
+
224
+ - (void)didClientDisconnect:(nw_connection_t)client
225
+ {
226
+ @synchronized (self.connectionBuffers) {
227
+ [self.connectionBuffers removeObjectForKey:client];
228
+ [self.pendingRequestHeaders removeObjectForKey:client];
229
+ }
230
+ }
231
+
232
+ - (void)client:(nw_connection_t)client didReceiveData:(NSData *)data
233
+ {
234
+ NSMutableData *buffer;
235
+ @synchronized (self.connectionBuffers) {
236
+ buffer = [self.connectionBuffers objectForKey:client];
237
+ if (nil == buffer) {
238
+ return;
239
+ }
240
+ [buffer appendData:data];
241
+ }
242
+ [self processBufferForClient:client];
243
+ }
244
+
245
+ #pragma mark - HTTP parsing
246
+
247
+ - (void)processBufferForClient:(nw_connection_t)client
248
+ {
249
+ while (YES) {
250
+ NSMutableData *buffer;
251
+ AMPendingHTTPRequestHeader *pending;
252
+ @synchronized (self.connectionBuffers) {
253
+ buffer = [self.connectionBuffers objectForKey:client];
254
+ if (nil == buffer) {
255
+ return;
256
+ }
257
+ pending = [self.pendingRequestHeaders objectForKey:client];
258
+ }
259
+
260
+ if (nil == pending) {
261
+ NSRange headerEndRange = [buffer rangeOfData:AMCRLFCRLFData() options:(NSDataSearchOptions)0 range:NSMakeRange(0, buffer.length)];
262
+ if (NSNotFound == headerEndRange.location) {
263
+ // Wait for the rest of the header block to arrive.
264
+ return;
265
+ }
266
+
267
+ NSData *headerData = [buffer subdataWithRange:NSMakeRange(0, headerEndRange.location)];
268
+ NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding];
269
+ NSArray<NSString *> *lines = [headerString componentsSeparatedByString:@"\r\n"];
270
+ if (lines.count < 1) {
271
+ [self respondBadRequestToClient:client];
272
+ return;
273
+ }
274
+
275
+ NSArray<NSString *> *requestLineParts = [lines.firstObject componentsSeparatedByString:@" "];
276
+ if (requestLineParts.count < 2) {
277
+ [self respondBadRequestToClient:client];
278
+ return;
279
+ }
280
+
281
+ NSMutableDictionary<NSString *, NSString *> *requestHeaders = [NSMutableDictionary dictionary];
282
+ for (NSUInteger i = 1; i < lines.count; i++) {
283
+ NSString *line = lines[i];
284
+ NSRange colonRange = [line rangeOfString:@":"];
285
+ if (NSNotFound == colonRange.location) {
286
+ continue;
287
+ }
288
+ NSString *name = [line substringToIndex:colonRange.location];
289
+ NSString *value = [[line substringFromIndex:colonRange.location + 1]
290
+ stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
291
+ requestHeaders[name.lowercaseString] = value;
292
+ }
293
+
294
+ NSString *transferEncoding = requestHeaders[@"transfer-encoding"].lowercaseString;
295
+ if (nil != transferEncoding && NSNotFound != [transferEncoding rangeOfString:@"chunked"].location) {
296
+ // De-chunking isn't implemented - fail loudly instead of silently misreading the body as
297
+ // empty and desyncing the rest of the connection's request stream.
298
+ RouteResponse *notImplemented = [RouteResponse new];
299
+ id<FBResponsePayload> notImplementedPayload = FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:@"Chunked Transfer-Encoding is not supported"
300
+ traceback:nil]);
301
+ [notImplementedPayload dispatchWithResponse:notImplemented];
302
+ [self failClient:client withResponse:notImplemented];
303
+ return;
304
+ }
305
+
306
+ NSUInteger contentLength = (NSUInteger)requestHeaders[@"content-length"].integerValue;
307
+ if (contentLength > kMaxHTTPRequestBodySize) {
308
+ // Closes the connection after responding, since the rest of the oversized body is still
309
+ // incoming.
310
+ RouteResponse *tooLarge = [RouteResponse new];
311
+ id<FBResponsePayload> tooLargePayload = FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:@"Request Entity Too Large"
312
+ traceback:nil]);
313
+ [tooLargePayload dispatchWithResponse:tooLarge];
314
+ [self failClient:client withResponse:tooLarge];
315
+ return;
316
+ }
317
+
318
+ pending = [AMPendingHTTPRequestHeader new];
319
+ pending.method = requestLineParts[0].uppercaseString;
320
+ pending.pathAndQuery = requestLineParts[1];
321
+ pending.bodyStart = headerEndRange.location + headerEndRange.length;
322
+ pending.contentLength = contentLength;
323
+ @synchronized (self.connectionBuffers) {
324
+ [self.pendingRequestHeaders setObject:pending forKey:client];
325
+ }
326
+ }
327
+
328
+ NSUInteger totalRequestLength = pending.bodyStart + pending.contentLength;
329
+ if (buffer.length < totalRequestLength) {
330
+ // Wait for the rest of the body to arrive - the parsed header stays cached above, so this
331
+ // doesn't re-scan/re-parse the header block on every subsequently arriving chunk.
332
+ return;
333
+ }
334
+
335
+ NSData *body = pending.contentLength > 0 ? [buffer subdataWithRange:NSMakeRange(pending.bodyStart, pending.contentLength)] : [NSData data];
336
+
337
+ @synchronized (self.connectionBuffers) {
338
+ [buffer replaceBytesInRange:NSMakeRange(0, totalRequestLength) withBytes:NULL length:0];
339
+ [self.pendingRequestHeaders removeObjectForKey:client];
340
+ }
341
+
342
+ [self dispatchMethod:pending.method pathAndQuery:pending.pathAndQuery body:body client:client];
343
+ }
344
+ }
345
+
346
+ // Removes the client's buffered state and responds with a closing error response. Removing the
347
+ // buffer synchronously ensures any request bytes still streaming in for this connection are
348
+ // dropped rather than being re-parsed and re-triggering this same response.
349
+ - (void)failClient:(nw_connection_t)client withResponse:(RouteResponse *)response
350
+ {
351
+ @synchronized (self.connectionBuffers) {
352
+ [self.connectionBuffers removeObjectForKey:client];
353
+ [self.pendingRequestHeaders removeObjectForKey:client];
354
+ }
355
+ [self applyDefaultHeadersToResponse:response];
356
+ [self writeResponse:response toClient:client thenCloseConnection:YES];
357
+ }
358
+
359
+ - (void)respondBadRequestToClient:(nw_connection_t)client
360
+ {
361
+ RouteResponse *badRequest = [RouteResponse new];
362
+ id<FBResponsePayload> payload = FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:@"The request could not be parsed as valid HTTP"
363
+ traceback:nil]);
364
+ [payload dispatchWithResponse:badRequest];
365
+ [self failClient:client withResponse:badRequest];
366
+ }
367
+
368
+ - (void)applyDefaultHeadersToResponse:(RouteResponse *)response
369
+ {
370
+ [self.defaultHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) {
371
+ [response setHeader:field value:value];
372
+ }];
373
+ }
374
+
375
+ - (void)dispatchMethod:(NSString *)method pathAndQuery:(NSString *)pathAndQuery body:(NSData *)body client:(nw_connection_t)client
376
+ {
377
+ NSURLComponents *requestTarget = [NSURLComponents componentsWithString:pathAndQuery];
378
+ NSString *path = requestTarget.path ?: pathAndQuery;
379
+
380
+ for (AMHTTPRoute *route in self.routes) {
381
+ if (![route.verb isEqualToString:method]) {
382
+ continue;
383
+ }
384
+ NSTextCheckingResult *result = [route.regex firstMatchInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length)];
385
+ if (nil == result) {
386
+ continue;
387
+ }
388
+
389
+ NSMutableDictionary<NSString *, NSString *> *params = [NSMutableDictionary dictionary];
390
+ for (NSURLQueryItem *queryItem in requestTarget.queryItems) {
391
+ params[queryItem.name] = queryItem.value ?: @"";
392
+ }
393
+ if (route.keys.count > 0 && result.numberOfRanges == route.keys.count + 1) {
394
+ NSUInteger index = 1;
395
+ for (NSString *key in route.keys) {
396
+ params[key] = [path substringWithRange:[result rangeAtIndex:index]];
397
+ index++;
398
+ }
399
+ }
400
+
401
+ NSURL *url = [NSURL URLWithString:path] ?: [NSURL URLWithString:@"/"];
402
+ RouteRequest *request = [[RouteRequest alloc] initWithURL:url params:params.copy body:body];
403
+ RouteResponse *response = [RouteResponse new];
404
+ [self applyDefaultHeadersToResponse:response];
405
+
406
+ void (^invoke)(void) = ^{
407
+ route.block(request, response);
408
+ [self writeResponse:response toClient:client];
409
+ };
410
+ dispatch_queue_t routeQueue = self.routeQueue;
411
+ if (routeQueue) {
412
+ dispatch_async((dispatch_queue_t _Nonnull)routeQueue, invoke);
413
+ } else {
414
+ invoke();
415
+ }
416
+ return;
417
+ }
418
+
419
+ [FBLogger logFmt:@"Received request for %@ which we do not handle", path];
420
+ RouteResponse *notFound = [RouteResponse new];
421
+ id<FBResponsePayload> payload = FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:nil
422
+ traceback:nil]);
423
+ [payload dispatchWithResponse:notFound];
424
+ [self applyDefaultHeadersToResponse:notFound];
425
+ [self writeResponse:notFound toClient:client];
426
+ }
427
+
428
+ - (void)writeResponse:(RouteResponse *)response toClient:(nw_connection_t)client
429
+ {
430
+ [self writeResponse:response toClient:client thenCloseConnection:NO];
431
+ }
432
+
433
+ - (void)writeResponse:(RouteResponse *)response toClient:(nw_connection_t)client thenCloseConnection:(BOOL)shouldClose
434
+ {
435
+ NSMutableData *payload = [NSMutableData data];
436
+ NSString *statusLine = [NSString stringWithFormat:@"HTTP/1.1 %ld %@\r\n",
437
+ (long)response.statusCode, [self reasonPhraseForStatusCode:response.statusCode]];
438
+ [payload appendData:AMUTF8Data(statusLine)];
439
+
440
+ NSData *body = response.responseData ?: [NSData data];
441
+ NSMutableDictionary<NSString *, NSString *> *headers = response.headers.mutableCopy;
442
+ if (nil == headers[@"Content-Length"]) {
443
+ headers[@"Content-Length"] = [NSString stringWithFormat:@"%lu", (unsigned long)body.length];
444
+ }
445
+ [headers enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) {
446
+ NSString *headerLine = [NSString stringWithFormat:@"%@: %@\r\n", field, value];
447
+ [payload appendData:AMUTF8Data(headerLine)];
448
+ }];
449
+ [payload appendData:AMUTF8Data(@"\r\n")];
450
+ [payload appendData:body];
451
+
452
+ if (shouldClose) {
453
+ __weak typeof(self) weakSelf = self;
454
+ [self.socket writeData:payload toClient:client completion:^{
455
+ [weakSelf closeClient:client];
456
+ }];
457
+ } else {
458
+ [self.socket writeData:payload toClient:client];
459
+ }
460
+ }
461
+
462
+ - (void)closeClient:(nw_connection_t)client
463
+ {
464
+ @synchronized (self.connectionBuffers) {
465
+ [self.connectionBuffers removeObjectForKey:client];
466
+ }
467
+ nw_connection_cancel(client);
468
+ }
469
+
470
+ - (NSString *)reasonPhraseForStatusCode:(NSInteger)statusCode
471
+ {
472
+ // if/else, not switch, to avoid having to list all ~90 HTTPStatusCode cases for -Wswitch-enum.
473
+ if (kHTTPStatusCodeOK == statusCode) {
474
+ return @"OK";
475
+ } else if (kHTTPStatusCodeBadRequest == statusCode) {
476
+ return @"Bad Request";
477
+ } else if (kHTTPStatusCodeNotFound == statusCode) {
478
+ return @"Not Found";
479
+ } else if (kHTTPStatusCodeMethodNotAllowed == statusCode) {
480
+ return @"Method Not Allowed";
481
+ } else if (kHTTPStatusCodeRequestTimeout == statusCode) {
482
+ return @"Request Timeout";
483
+ } else if (kHTTPStatusCodeRequestEntityTooLarge == statusCode) {
484
+ return @"Request Entity Too Large";
485
+ } else if (kHTTPStatusCodeNotImplemented == statusCode) {
486
+ return @"Not Implemented";
487
+ } else if (kHTTPStatusCodeInternalServerError == statusCode) {
488
+ return @"Internal Server Error";
489
+ }
490
+ return @"Status";
491
+ }
492
+
493
+ @end
@@ -0,0 +1,109 @@
1
+ /*
2
+ * Licensed under the Apache License, Version 2.0 (the "License");
3
+ * you may not use this file except in compliance with the License.
4
+ * See the NOTICE file distributed with this work for additional
5
+ * information regarding copyright ownership.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ #import <Foundation/Foundation.h>
18
+ #import <Network/Network.h>
19
+
20
+ NS_ASSUME_NONNULL_BEGIN
21
+
22
+ // A TCP listener/connection wrapper backed by Network.framework rather than BSD sockets.
23
+ @protocol AMTCPSocketDelegate <NSObject>
24
+
25
+ /**
26
+ The callback which is fired on new TCP client connection
27
+
28
+ @param newClient The newly connected client
29
+ */
30
+ - (void)didClientConnect:(nw_connection_t)newClient;
31
+
32
+ /**
33
+ The callback which is fired when the TCP server receives data from a connected client
34
+
35
+ @param client The client, which sent the data
36
+ @param data The received data
37
+ */
38
+ - (void)client:(nw_connection_t)client didReceiveData:(NSData *)data;
39
+
40
+ /**
41
+ The callback which is fired when TCP client disconnects
42
+
43
+ @param client The actual disconnected client
44
+ */
45
+ - (void)didClientDisconnect:(nw_connection_t)client;
46
+
47
+ @end
48
+
49
+
50
+ @interface AMTCPSocket : NSObject
51
+
52
+ @property (nullable, nonatomic, weak) id<AMTCPSocketDelegate> delegate;
53
+
54
+ /**
55
+ The port this socket is listening on. Equal to the port passed to -initWithPort: unless that was
56
+ 0 ("let the system assign a port"), in which case this reflects the actually assigned port once
57
+ -startWithError: has returned successfully.
58
+ */
59
+ @property (nonatomic, readonly) uint16_t port;
60
+
61
+ /**
62
+ The local IP address to bind the listener to, or nil to listen on all interfaces. Must be set
63
+ before -startWithError: is called.
64
+ */
65
+ @property (nonatomic, copy, nullable) NSString *interface;
66
+
67
+ /**
68
+ Creates TCP socket isntance which is going to be started on the specified port
69
+
70
+ @param port The actual port number
71
+ @return self instance
72
+ */
73
+ - (instancetype)initWithPort:(uint16_t)port;
74
+
75
+ /**
76
+ Starts TCP socket listener on the specified port
77
+
78
+ @param error The alias to the actual startup error descirption or nil if the socket has started and is listening
79
+ @return NO If there was an error
80
+ */
81
+ - (BOOL)startWithError:(NSError **)error;
82
+
83
+ /**
84
+ Stops the socket if it is running
85
+ */
86
+ - (void)stop;
87
+
88
+ /**
89
+ Writes data to the given connected client
90
+
91
+ @param data The data to send
92
+ @param client The destination client, as received via -didClientConnect: or -client:didReceiveData:
93
+ */
94
+ - (void)writeData:(NSData *)data toClient:(nw_connection_t)client;
95
+
96
+ /**
97
+ Like -writeData:toClient:, but invokes completion once the send actually goes out. Use this
98
+ before cancelling the connection - nw_connection_send is async, so cancelling right away can
99
+ drop the write before it's sent.
100
+
101
+ @param data The data to send
102
+ @param client The destination client
103
+ @param completion Called once the send attempt finishes
104
+ */
105
+ - (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(void))completion;
106
+
107
+ @end
108
+
109
+ NS_ASSUME_NONNULL_END