appium-mac2-driver 4.2.0 → 4.3.1
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/WebDriverAgentMac/IntegrationTests/AMW3CActionsTests.m +7 -9
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMHTTPServer.h +84 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMHTTPServer.m +493 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMTCPSocket.h +109 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/AMTCPSocket.m +235 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBWebServer.h +1 -1
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBWebServer.m +9 -21
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteRequest.h +36 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteRequest.m +33 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteResponse.h +37 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/RouteResponse.m +62 -0
- package/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj +32 -176
- package/npm-shrinkwrap.json +107 -167
- package/package.json +3 -2
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h +0 -1226
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m +0 -8513
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.h +0 -1036
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m +0 -5632
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h +0 -12
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m +0 -88
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h +0 -56
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m +0 -106
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h +0 -107
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m +0 -2238
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h +0 -122
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h +0 -48
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m +0 -114
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h +0 -149
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h +0 -200
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m +0 -720
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h +0 -13
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m +0 -83
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h +0 -9
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m +0 -40
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h +0 -13
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m +0 -84
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h +0 -18
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m +0 -11
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h +0 -16
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m +0 -50
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h +0 -20
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m +0 -66
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h +0 -5
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m +0 -142
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h +0 -55
- package/WebDriverAgentMac/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m +0 -288
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [4.3.1](https://github.com/appium/appium-mac2-driver/compare/v4.3.0...v4.3.1) (2026-08-30)
|
|
2
|
+
|
|
3
|
+
### Miscellaneous Chores
|
|
4
|
+
|
|
5
|
+
* **deps-dev:** bump @appium/docutils from 2.5.2 to 3.0.0 ([#417](https://github.com/appium/appium-mac2-driver/issues/417)) ([277db80](https://github.com/appium/appium-mac2-driver/commit/277db80af726d55810a15202582a841c97f37da1))
|
|
6
|
+
|
|
7
|
+
## [4.3.0](https://github.com/appium/appium-mac2-driver/compare/v4.2.0...v4.3.0) (2026-08-26)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* port WebDriverAgentMac's HTTP server to Network.framework ([#415](https://github.com/appium/appium-mac2-driver/issues/415)) ([f4ff684](https://github.com/appium/appium-mac2-driver/commit/f4ff68477925a0839c004a280bdda447cf9a4e17))
|
|
12
|
+
|
|
1
13
|
## [4.2.0](https://github.com/appium/appium-mac2-driver/compare/v4.1.1...v4.2.0) (2026-08-10)
|
|
2
14
|
|
|
3
15
|
### Features
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
#import "AMIntegrationTestCase.h"
|
|
20
20
|
#import "XCUIApplication+FBW3CActions.h"
|
|
21
|
+
#import "XCUIElement+AMEditable.h"
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
@interface AMW3CActionsTests : AMIntegrationTestCase
|
|
@@ -49,15 +50,6 @@
|
|
|
49
50
|
},
|
|
50
51
|
],
|
|
51
52
|
|
|
52
|
-
// Chain element with empty 'actions'
|
|
53
|
-
@[@{
|
|
54
|
-
@"type": @"pointer",
|
|
55
|
-
@"id": @"finger1",
|
|
56
|
-
@"parameters": @{@"pointerType": @"mouse"},
|
|
57
|
-
@"actions": @[],
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
|
|
61
53
|
// Chain element without type
|
|
62
54
|
@[@{
|
|
63
55
|
@"id": @"finger1",
|
|
@@ -343,6 +335,9 @@
|
|
|
343
335
|
[self switchToEditsTab];
|
|
344
336
|
XCUIElement *edit = self.testedApplication.textFields.firstMatch;
|
|
345
337
|
[edit click];
|
|
338
|
+
NSError *clearError;
|
|
339
|
+
XCTAssertTrue([edit am_clearTextWithError:&clearError]);
|
|
340
|
+
XCTAssertNil(clearError);
|
|
346
341
|
|
|
347
342
|
NSArray<NSDictionary<NSString *, id> *> *gesture =
|
|
348
343
|
@[@{
|
|
@@ -375,6 +370,9 @@
|
|
|
375
370
|
[self switchToEditsTab];
|
|
376
371
|
XCUIElement *edit = self.testedApplication.textFields.firstMatch;
|
|
377
372
|
[edit click];
|
|
373
|
+
NSError *clearError;
|
|
374
|
+
XCTAssertTrue([edit am_clearTextWithError:&clearError]);
|
|
375
|
+
XCTAssertNil(clearError);
|
|
378
376
|
|
|
379
377
|
NSArray<NSDictionary<NSString *, id> *> *gesture =
|
|
380
378
|
@[@{
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
// A minimal HTTP/1.1 server on top of AMTCPSocket (Network.framework-backed).
|
|
18
|
+
//
|
|
19
|
+
// No chunked encoding, range requests, or pipelining - just request line + headers +
|
|
20
|
+
// Content-Length body, and ":param" path matching.
|
|
21
|
+
|
|
22
|
+
#import <Foundation/Foundation.h>
|
|
23
|
+
|
|
24
|
+
#import "RouteRequest.h"
|
|
25
|
+
#import "RouteResponse.h"
|
|
26
|
+
|
|
27
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
28
|
+
|
|
29
|
+
@interface AMHTTPServer : NSObject
|
|
30
|
+
|
|
31
|
+
/*! The port the server is (or will be) listening on */
|
|
32
|
+
@property (nonatomic) uint16_t port;
|
|
33
|
+
|
|
34
|
+
/*! Whether the server is currently listening for connections */
|
|
35
|
+
@property (nonatomic, readonly) BOOL isRunning;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
Sets the dispatch queue on which route blocks are invoked. Pass NULL to invoke them
|
|
39
|
+
synchronously on the socket's own queue.
|
|
40
|
+
*/
|
|
41
|
+
- (void)setRouteQueue:(nullable dispatch_queue_t)queue;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
Sets a header which is added to every response, unless overridden by the route itself.
|
|
45
|
+
*/
|
|
46
|
+
- (void)setDefaultHeader:(NSString *)field value:(NSString *)value;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
Sets the local IP address to bind the listener to. Must be called before -start:. Pass nil (the
|
|
50
|
+
default) to listen on all interfaces.
|
|
51
|
+
*/
|
|
52
|
+
- (void)setInterface:(nullable NSString *)interface;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
Registers a route handler for the given HTTP method and path pattern (":param" segments are
|
|
56
|
+
captured into the request's `params`).
|
|
57
|
+
*/
|
|
58
|
+
- (void)handleMethod:(NSString *)method
|
|
59
|
+
withPath:(NSString *)path
|
|
60
|
+
block:(void (^)(RouteRequest *request, RouteResponse *response))block;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
Convenience for -handleMethod:@"GET" withPath:path block:block.
|
|
64
|
+
*/
|
|
65
|
+
- (void)get:(NSString *)path withBlock:(void (^)(RouteRequest *request, RouteResponse *response))block;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
Convenience for -handleMethod:@"DELETE" withPath:path block:block.
|
|
69
|
+
*/
|
|
70
|
+
- (void)delete:(NSString *)path withBlock:(void (^)(RouteRequest *request, RouteResponse *response))block;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
Starts listening on `port`.
|
|
74
|
+
*/
|
|
75
|
+
- (BOOL)start:(NSError **)error;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
Stops listening and disconnects all clients.
|
|
79
|
+
*/
|
|
80
|
+
- (void)stop:(BOOL)immediately;
|
|
81
|
+
|
|
82
|
+
@end
|
|
83
|
+
|
|
84
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -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
|