Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
348 changes: 45 additions & 303 deletions WebDriverAgent.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
* LICENSE file in the root directory of this source tree.
*/

// RoutingHTTPServer/CocoaHTTPServer need BSD sockets (via GCDAsyncSocket), which watchOS
// forbids - see FBTCPSocket.h/.m. This is a minimal HTTP/1.1 server on top of the watchOS
// FBTCPSocket, mirroring just enough of RoutingHTTPServer's API for FBWebServer.m to swap
// servers with a single #if TARGET_OS_WATCH.
// A minimal HTTP/1.1 server on top of FBTCPSocket (Network.framework-backed on every platform,
// since watchOS forbids BSD sockets outright - see FBTCPSocket.h/.m).
//
// No chunked encoding, range requests, or pipelining - just request line + headers +
// Content-Length body, and ":param" path matching like RoutingHTTPServer.m.
// Content-Length body, and ":param" path matching.

@import Foundation;

Expand All @@ -21,7 +19,7 @@

NS_ASSUME_NONNULL_BEGIN

@interface FBWatchHTTPServer : NSObject
@interface FBHTTPServer : NSObject

/*! The port the server is (or will be) listening on */
@property (nonatomic) uint16_t port;
Expand All @@ -40,9 +38,15 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)setDefaultHeader:(NSString *)field value:(NSString *)value;

/**
Sets the local IP address to bind the listener to. Must be called before -start:. Pass nil (the
default) to listen on all interfaces.
*/
- (void)setInterface:(nullable NSString *)interface;

/**
Registers a route handler for the given HTTP method and path pattern (":param" segments are
captured into the request's `params`, matching RoutingHTTPServer's convention).
captured into the request's `params`).
*/
- (void)handleMethod:(NSString *)method
withPath:(NSString *)path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/

#import "FBWatchHTTPServer.h"
#import "FBHTTPServer.h"

#import "FBConfiguration.h"
#import "FBTCPSocket.h"
Expand All @@ -28,29 +28,30 @@
return (NSData * _Nonnull)[string dataUsingEncoding:NSUTF8StringEncoding];
}

@interface FBWatchHTTPRoute : NSObject
@interface FBHTTPRoute : NSObject
@property (nonatomic, copy) NSString *verb;
@property (nonatomic, strong) NSRegularExpression *regex;
@property (nonatomic, copy, nullable) NSArray<NSString *> *keys;
@property (nonatomic, copy) void (^block)(RouteRequest *request, RouteResponse *response);
@end

@implementation FBWatchHTTPRoute
@implementation FBHTTPRoute
@end


@interface FBWatchHTTPServer () <FBTCPSocketDelegate>
@interface FBHTTPServer () <FBTCPSocketDelegate>

@property (nonatomic, nullable, strong) FBTCPSocket *socket;
@property (nonatomic, strong) NSMutableArray<FBWatchHTTPRoute *> *routes;
@property (nonatomic, strong) NSMutableArray<FBHTTPRoute *> *routes;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSString *> *defaultHeaders;
@property (nonatomic, nullable) dispatch_queue_t routeQueue;
@property (nonatomic, copy, nullable) NSString *interface;
// nw_connection_t isn't NSCopying, so it can't be an NSDictionary key - use NSMapTable instead.
@property (nonatomic, strong) NSMapTable<id, NSMutableData *> *connectionBuffers;

@end

@implementation FBWatchHTTPServer
@implementation FBHTTPServer

- (instancetype)init
{
Expand All @@ -73,15 +74,20 @@ - (void)setDefaultHeader:(NSString *)field value:(NSString *)value
self.defaultHeaders[field] = value;
}

- (void)setInterface:(nullable NSString *)interface
{
_interface = interface.copy;
}

#pragma mark - Route registration

- (FBWatchHTTPRoute *)compiledRouteWithPath:(NSString *)path
- (FBHTTPRoute *)compiledRouteWithPath:(NSString *)path
{
FBWatchHTTPRoute *route = [FBWatchHTTPRoute new];
FBHTTPRoute *route = [FBHTTPRoute new];
NSMutableArray<NSString *> *keys = [NSMutableArray array];

// Escape regex-significant characters before substituting :param placeholders, like
// RoutingHTTPServer.m does.
// RoutingHTTPServer.m used to.
NSRegularExpression *escapeRegex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]"
options:(NSRegularExpressionOptions)0
error:nil];
Expand Down Expand Up @@ -126,7 +132,7 @@ - (void)handleMethod:(NSString *)method
withPath:(NSString *)path
block:(void (^)(RouteRequest *request, RouteResponse *response))block
{
FBWatchHTTPRoute *route = [self compiledRouteWithPath:path];
FBHTTPRoute *route = [self compiledRouteWithPath:path];
route.verb = method.uppercaseString;
route.block = block;
[self.routes addObject:route];
Expand All @@ -142,6 +148,7 @@ - (void)get:(NSString *)path withBlock:(void (^)(RouteRequest *request, RouteRes
- (BOOL)start:(NSError **)error
{
FBTCPSocket *socket = [[FBTCPSocket alloc] initWithPort:self.port];
socket.interface = self.interface;
socket.delegate = self;
if (![socket startWithError:error]) {
return NO;
Expand Down Expand Up @@ -239,8 +246,8 @@ - (void)processBufferForClient:(nw_connection_t)client

NSUInteger contentLength = (NSUInteger)requestHeaders[@"content-length"].integerValue;
if (contentLength > FBConfiguration.sharedInstance.httpRequestBodySizeLimit) {
// Mirrors FBHTTPConnection's maxRequestBodySize enforcement on iOS/tvOS. Closes the
// connection after responding, since the rest of the oversized body is still incoming.
// Mirrors CocoaHTTPServer's maxRequestBodySize enforcement. Closes the connection after
// responding, since the rest of the oversized body is still incoming.
RouteResponse *tooLarge = [RouteResponse new];
tooLarge.statusCode = kHTTPStatusCodeRequestEntityTooLarge;
[tooLarge respondWithString:@"Request Entity Too Large"];
Expand Down Expand Up @@ -269,7 +276,7 @@ - (void)dispatchMethod:(NSString *)method pathAndQuery:(NSString *)pathAndQuery
NSURLComponents *requestTarget = [NSURLComponents componentsWithString:pathAndQuery];
NSString *path = requestTarget.path ?: pathAndQuery;

for (FBWatchHTTPRoute *route in self.routes) {
for (FBHTTPRoute *route in self.routes) {
if (![route.verb isEqualToString:method]) {
continue;
}
Expand Down Expand Up @@ -367,6 +374,12 @@ - (NSString *)reasonPhraseForStatusCode:(HTTPStatusCode)statusCode
return @"Bad Request";
} else if (kHTTPStatusCodeNotFound == statusCode) {
return @"Not Found";
} else if (kHTTPStatusCodeMethodNotAllowed == statusCode) {
return @"Method Not Allowed";
} else if (kHTTPStatusCodeRequestTimeout == statusCode) {
return @"Request Timeout";
} else if (kHTTPStatusCodeRequestEntityTooLarge == statusCode) {
return @"Request Entity Too Large";
} else if (kHTTPStatusCodeInternalServerError == statusCode) {
return @"Internal Server Error";
}
Expand Down
55 changes: 10 additions & 45 deletions WebDriverAgentLib/Routing/FBTCPSocket.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,18 @@
* LICENSE file in the root directory of this source tree.
*/

// TARGET_OS_WATCH must be defined before the #if below runs, or (on some older Xcode/SDK
// toolchains) it silently evaluates as undefined/false here despite being true for the rest of
// the translation unit - which desyncs this file's declarations from FBTCPSocket.m's own
// #if TARGET_OS_WATCH branch.
#import <TargetConditionals.h>

#if TARGET_OS_WATCH
@import Foundation;
// A textual import, not `@import Network;` - older Xcode/watchOS SDK combinations (verified:
// Xcode 15.4/watchOS 10.5) fail to expose nw_listener_t/nw_connection_t and friends through the
// Network module map on watchOS, even though the underlying API has existed since watchOS 5.0.
// Kept unconditional (rather than gated to watchOS) since it also builds cleanly on iOS/tvOS.
#import <Network/Network.h>
#else
#import "GCDAsyncSocket.h"
#endif

NS_ASSUME_NONNULL_BEGIN

#if TARGET_OS_WATCH

// watchOS forbids BSD sockets, so this is backed by Network.framework instead of GCDAsyncSocket -
// hence a differently-shaped, push-style delegate protocol.
// Backed by Network.framework rather than BSD sockets on every platform, since watchOS forbids
// BSD sockets outright and there is no reason to keep a second, socket-based implementation
// around just for iOS/tvOS.
@protocol FBTCPSocketDelegate <NSObject>

/**
Expand All @@ -54,35 +44,6 @@ NS_ASSUME_NONNULL_BEGIN

@end

#else

@protocol FBTCPSocketDelegate

/**
The callback which is fired on new TCP client connection

@param newClient The newly connected socket
*/
- (void)didClientConnect:(GCDAsyncSocket *)newClient;

/**
The callback which is fired when the TCP server receives a data from a connected client

@param client The client, which sent the data
*/
- (void)didClientSendData:(GCDAsyncSocket *)client;

/**
The callback which is fired when TCP client disconnects

@param client The actual diconnected client
*/
- (void)didClientDisconnect:(GCDAsyncSocket *)client;

@end

#endif


@interface FBTCPSocket : NSObject

Expand All @@ -99,6 +60,12 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, readonly) uint16_t port;

/**
The local IP address to bind the listener to, or nil to listen on all interfaces. Must be set
before -startWithError: is called.
*/
@property (nonatomic, copy, nullable) NSString *interface;

/**
Creates TCP socket isntance which is going to be started on the specified port

Expand All @@ -120,7 +87,6 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)stop;

#if TARGET_OS_WATCH
/**
Writes data to the given connected client

Expand All @@ -139,7 +105,6 @@ NS_ASSUME_NONNULL_BEGIN
@param completion Called once the send attempt finishes
*/
- (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(void))completion;
#endif

@end

Expand Down
106 changes: 12 additions & 94 deletions WebDriverAgentLib/Routing/FBTCPSocket.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

#import "FBTCPSocket.h"

#if TARGET_OS_WATCH

@interface FBTCPSocket()
@property (readonly, nonatomic) dispatch_queue_t socketQueue;
@property (nullable, nonatomic) nw_listener_t listener;
Expand Down Expand Up @@ -37,7 +35,18 @@ - (BOOL)startWithError:(NSError **)error
NSString *portString = [NSString stringWithFormat:@"%u", (unsigned int)self.port];
// portString is always valid UTF8; -UTF8String is just declared nullable in general.
const char * _Nonnull portCString = (const char * _Nonnull)portString.UTF8String;
nw_listener_t listener = nw_listener_create_with_port(portCString, parameters);

nw_listener_t listener;
if (nil != self.interface) {
const char * _Nonnull interfaceCString = (const char * _Nonnull)((NSString * _Nonnull)self.interface).UTF8String;
nw_endpoint_t localEndpoint = nw_endpoint_create_host(interfaceCString, portCString);
nw_parameters_set_local_endpoint(parameters, localEndpoint);
// The port is already encoded in localEndpoint above - do not also pass it to
// nw_listener_create_with_port, which would be ambiguous.
listener = nw_listener_create(parameters);
} else {
listener = nw_listener_create_with_port(portCString, parameters);
}
if (nil == listener) {
if (error) {
*error = [NSError errorWithDomain:@"FBTCPSocket"
Expand Down Expand Up @@ -206,94 +215,3 @@ - (void)stop
}

@end

#else

@interface FBTCPSocket()
@property (readonly, nonatomic) dispatch_queue_t socketQueue;
@property (readonly, nonatomic) GCDAsyncSocket *listeningSocket;
@property (readonly, nonatomic) NSMutableArray *connectedClients;
@end


@interface FBTCPSocket(AsyncSocket) <GCDAsyncSocketDelegate>

@end


@implementation FBTCPSocket

- (instancetype)initWithPort:(uint16_t)port
{
if ((self = [super init])) {
_socketQueue = dispatch_queue_create("socketQueue", NULL);
_listeningSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:_socketQueue];
_connectedClients = [[NSMutableArray alloc] initWithCapacity:1];
_port = port;
_delegate = nil;
}
return self;
}

- (BOOL)startWithError:(NSError **)error
{
if (![self.listeningSocket acceptOnPort:self.port error:error]) {
return NO;
}

_port = self.listeningSocket.localPort;
return YES;
}

- (void)stop
{
@synchronized(self.connectedClients) {
NSArray *clients = self.connectedClients.copy;
[self.connectedClients removeAllObjects];
for (GCDAsyncSocket *client in clients) {
[client disconnect];
}
}

self.delegate = nil;
[self.listeningSocket disconnect];
}

@end


@implementation FBTCPSocket(AsyncSocket)

- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
@synchronized(self.connectedClients) {
[self.connectedClients addObject:newSocket];
}
id<FBTCPSocketDelegate> delegate = self.delegate;
if (nil != delegate) {
[delegate didClientConnect:newSocket];
}
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
id<FBTCPSocketDelegate> delegate = self.delegate;
if (nil != delegate) {
[delegate didClientSendData:sock];
}
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
@synchronized(self.connectedClients) {
[self.connectedClients removeObject:sock];
}
id<FBTCPSocketDelegate> delegate = self.delegate;
if (nil != delegate) {
[delegate didClientDisconnect:sock];
}
}

@end

#endif
2 changes: 1 addition & 1 deletion WebDriverAgentLib/Routing/FBWebServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

#import <Foundation/Foundation.h>

@class RouteResponse, RoutingHTTPServer, FBExceptionHandler;
@class RouteResponse, FBExceptionHandler;
@protocol FBWebServerDelegate;

NS_ASSUME_NONNULL_BEGIN
Expand Down
Loading
Loading