Add implementation for Wi-Fi LAN server socket that use the Network framework

PiperOrigin-RevId: 508517597
This commit is contained in:
Nick Bourdakos
2023-02-09 17:21:26 -08:00
committed by Copybara-Service
parent 296c6dd2dc
commit a96760b977
8 changed files with 447 additions and 4 deletions
@@ -28,6 +28,9 @@ objc_library(
"WifiLan/GNCMBonjourConnection.m",
"WifiLan/GNCMBonjourService.m",
"WifiLan/GNCMBonjourUtils.m",
"WifiLanV2/GNCWiFiLANError.m",
"WifiLanV2/GNCWiFiLANServerSocket.m",
"WifiLanV2/GNCWiFiLANServerSocket+Internal.h",
"WifiLanV2/GNCWiFiLANSocket.m",
],
hdrs = [
@@ -41,6 +44,8 @@ objc_library(
"WifiLan/GNCMBonjourConnection.h",
"WifiLan/GNCMBonjourService.h",
"WifiLan/GNCMBonjourUtils.h",
"WifiLanV2/GNCWiFiLANError.h",
"WifiLanV2/GNCWiFiLANServerSocket.h",
"WifiLanV2/GNCWiFiLANSocket.h",
],
deps = [
@@ -0,0 +1,28 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <Foundation/Foundation.h>
/**
* The domain for @c NSErrors raised by the Wi-Fi LAN medium.
*/
extern NSErrorDomain const GNCWiFiLANErrorDomain;
/**
* Wi-Fi LAN medium error codes.
*/
typedef NS_ERROR_ENUM(GNCWiFiLANErrorDomain, GNCWiFiLANError){
GNCWiFiLANErrorUnknown,
GNCWiFiLANErrorTimedOut,
};
@@ -0,0 +1,17 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANError.h"
NSErrorDomain const GNCWiFiLANErrorDomain = @"com.google.nearby.wifilan.error";
@@ -0,0 +1,47 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <Foundation/Foundation.h>
#import "internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANServerSocket.h"
@interface GNCWiFiLANServerSocket (Internal)
/**
* Starts listening for inbound connections.
*
* Blocks execution until listening has started or failed.
*
* @param[out] error Error that will be populated on failure.
* @return Returns YES when listening has successfully started.
*/
- (BOOL)startListeningWithError:(NSError **_Nullable)error;
/**
* Creates a Bonjour service that advertises the listener on the local network.
*
* @param serviceName The Bonjour name of the service.
* @param serviceType The Bonjour type of the service.
* @param txtRecords The TXT record to advertise with the service.
*/
- (void)startAdvertisingServiceName:(nonnull NSString *)serviceName
serviceType:(nonnull NSString *)serviceType
txtRecords:(nonnull NSDictionary<NSString *, NSString *> *)txtRecords;
/**
* Removes the Bonjour service advertisement.
*/
- (void)stopAdvertising;
@end
@@ -0,0 +1,63 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <Foundation/Foundation.h>
@class GNCWiFiLANSocket;
@interface GNCWiFiLANServerSocket : NSObject
/**
* @remark init is not an available initializer.
*/
- (nonnull instancetype)init NS_UNAVAILABLE;
/**
* Creates a server socket for a given port.
*
* @param port The port of the server socket.
*/
- (nonnull instancetype)initWithPort:(nonnull NSNumber *)port NS_DESIGNATED_INITIALIZER;
/**
* The 4 byte binary representation for the IPv4 address of the physical network interface.
*/
@property(nonatomic, readonly, copy) NSString *ipAddress;
/**
* The port of the server socket.
*/
@property(nonatomic, readonly, copy) NSNumber *port;
/**
* Returns an arbitrary, connected socket, ready to exchange data.
*
* Blocks execution until at least one connection is ready, a permanent error occurs or the server
* socket was closed.
*
* Any error reported indicates a permanent failure and an automatic closure of the server socket.
*
* @param[out] error Error that will be populated on failure.
* @return Returns a connected socket or nil if an error has occured.
*/
- (nullable GNCWiFiLANSocket *)acceptWithError:(NSError **_Nullable)error;
/**
* Stops listening for inbound connections.
*
* Any calls to acceptWithError: will be unblocked with an error.
*/
- (void)close;
@end
@@ -0,0 +1,282 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANServerSocket.h"
#import <Foundation/Foundation.h>
#import <Network/Network.h>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <netdb.h>
#include <sys/socket.h>
#import "internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANError.h"
#import "internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANServerSocket+Internal.h"
#import "internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANSocket.h"
#import "GoogleToolboxForMac/GTMLogger.h"
@interface GNCWiFiLANServerSocket ()
@property(nonatomic, readonly) NSMutableArray<nw_connection_t> *pendingConnections;
@property(nonatomic, readonly) NSMutableArray<nw_connection_t> *readyConnections;
@end
@implementation GNCWiFiLANServerSocket {
NSCondition *_condition;
nw_listener_t _listener;
nw_listener_state_t _listenerState;
NSError *_listenerError;
}
- (instancetype)initWithPort:(NSNumber *)port {
self = [super init];
if (self) {
_port = [port copy];
_condition = [[NSCondition alloc] init];
_listenerState = nw_listener_state_invalid;
}
return self;
}
@synthesize pendingConnections = _pendingConnections;
- (NSMutableArray<nw_connection_t> *)pendingConnections {
if (!_pendingConnections) {
_pendingConnections = [[NSMutableArray alloc] init];
}
return _pendingConnections;
}
@synthesize readyConnections = _readyConnections;
- (NSMutableArray<nw_connection_t> *)readyConnections {
if (!_readyConnections) {
_readyConnections = [[NSMutableArray alloc] init];
}
return _readyConnections;
}
@synthesize ipAddress = _ipAddress;
- (NSString *)ipAddress {
if (!_ipAddress) {
_ipAddress = [GNCWiFiLANServerSocket lookupIpAddress];
}
return _ipAddress;
}
- (GNCWiFiLANSocket *)acceptWithError:(NSError **)error {
// Wait until we have a ready connection and listener is in a ready/invalid state.
[_condition lock];
nw_connection_t connection = [self.readyConnections lastObject];
while (connection == nil && (_listenerState == nw_listener_state_ready ||
_listenerState == nw_listener_state_invalid)) {
[_condition wait];
connection = [self.readyConnections lastObject];
}
if (connection != nil) {
[self.readyConnections removeObject:connection];
}
if (error != nil) {
*error = [_listenerError copy];
}
[_condition unlock];
if (connection == nil) {
return nil;
}
return [[GNCWiFiLANSocket alloc] initWithConnection:connection];
}
- (void)close {
nw_listener_cancel(_listener);
_listener = nil;
}
- (BOOL)startListeningWithError:(NSError **)error {
// Create a parameters object configured to support TCP. TLS MUST be disabled for Nearby to
// function properly. This also is set to include peer-to-peer, but unsure if it's required.
nw_parameters_t parameters =
nw_parameters_create_secure_tcp(/*tls*/ NW_PARAMETERS_DISABLE_PROTOCOL,
/*tcp*/ NW_PARAMETERS_DEFAULT_CONFIGURATION);
nw_parameters_set_include_peer_to_peer(parameters, true);
// If the server socket port is zero, a random port will be selected. The server socket port will
// be updated to reflect the port it's listening on, once the listener transitions to the ready
// state.
if ([self.port intValue] == 0) {
_listener = nw_listener_create(parameters);
} else {
_listener = nw_listener_create_with_port([[self.port stringValue] UTF8String], parameters);
}
// Register to listen for incoming connections.
nw_listener_set_queue(_listener, dispatch_get_main_queue());
nw_listener_set_new_connection_handler(_listener, ^(nw_connection_t connection) {
// Keep track of any incoming connections. We must keep a reference otherwise the connection
// will be dropped.
[self.pendingConnections addObject:connection];
// Register for state changes so we can clean up any failed/canceled connections and inform
// Nearby of any ready connections when asked.
nw_connection_set_queue(connection, dispatch_get_main_queue());
nw_connection_set_state_changed_handler(connection,
^(nw_connection_state_t state, nw_error_t error) {
[_condition lock];
switch (state) {
case nw_connection_state_cancelled:
case nw_connection_state_invalid:
case nw_connection_state_failed:
[self.pendingConnections removeObject:connection];
break;
case nw_connection_state_waiting:
// A connection couldn't be opened, but could be
// retried when conditions are more favorable. We
// choose to just drop the connection, but we
// could add retry logic here in the future.
[self.pendingConnections removeObject:connection];
break;
case nw_connection_state_preparing:
// ignore.
break;
case nw_connection_state_ready:
// Move the connection from "pending" to "ready".
[self.readyConnections addObject:connection];
[self.pendingConnections removeObject:connection];
[_condition signal];
break;
}
[_condition unlock];
});
nw_connection_start(connection);
});
// Register for state changes for the listener so we can block until we have an initial status.
[_condition lock];
nw_listener_set_state_changed_handler(_listener, ^(nw_listener_state_t state, nw_error_t error) {
[_condition lock];
_listenerState = state;
if (error != nil) {
_listenerError = (__bridge NSError *)nw_error_copy_cf_error(error);
}
[_condition signal];
[_condition unlock];
});
nw_listener_start(_listener);
// This doesn't start flaking until 0.0005 seconds, so 0.5 should be plenty of time.
BOOL didSignal = [_condition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];
if (error != nil) {
*error = [_listenerError copy];
}
[_condition unlock];
// We timed out waiting for the listener to transition into a state.
if (!didSignal) {
[self close];
_listenerError = [NSError errorWithDomain:GNCWiFiLANErrorDomain
code:GNCWiFiLANErrorTimedOut
userInfo:nil];
return NO;
}
switch (_listenerState) {
case nw_listener_state_invalid:
case nw_listener_state_waiting:
case nw_listener_state_failed:
case nw_listener_state_cancelled:
[self close];
return NO;
case nw_listener_state_ready:
_port = @(nw_listener_get_port(_listener));
return YES;
}
}
- (void)startAdvertisingServiceName:(NSString *)serviceName
serviceType:(NSString *)serviceType
txtRecords:(NSDictionary<NSString *, NSString *> *)txtRecords {
nw_advertise_descriptor_t advertiseDescriptor = nw_advertise_descriptor_create_bonjour_service(
[serviceName UTF8String], [serviceType UTF8String], /*domain=*/nil);
nw_txt_record_t txtRecord = nw_txt_record_create_dictionary();
for (NSString *key in txtRecords) {
NSString *recordValue = [txtRecords objectForKey:key];
nw_txt_record_set_key(txtRecord, [key UTF8String], [recordValue UTF8String],
[recordValue length]);
}
nw_advertise_descriptor_set_txt_record_object(advertiseDescriptor, txtRecord);
nw_listener_set_advertise_descriptor(_listener, advertiseDescriptor);
}
- (void)stopAdvertising {
nw_listener_set_advertise_descriptor(_listener, nil);
}
/**
* Attemps to find an IPv4 hostname for a Wi-Fi/Ethernet interface.
*
* Returns the IP address as a 4 byte string. If not available, this returns an empty string to
* align with the Windows implementation.
*/
+ (NSString *)lookupIpAddress {
struct ifaddrs *ifaddr;
// Note: The data returned by `getifaddrs()` is dynamically allocated and should be freed using
// `freeifaddrs()` when no longer needed.
//
// See: https://linux.die.net/man/3/getifaddrs
if (getifaddrs(&ifaddr) == -1) {
return @"";
}
// Walk through linked list, maintaining head pointer so we can free list later.
for (struct ifaddrs *cur = ifaddr; cur != nil; cur = cur->ifa_next) {
struct sockaddr *address = cur->ifa_addr;
if (address == nil) {
continue;
}
// Filter out any interfaces that aren't IPv4.
if (address->sa_family != AF_INET) {
continue;
}
// Filter out any interfaces that aren't en0 (Wi-Fi/Ethernet).
NSString *interfaceName = @(cur->ifa_name);
if (![interfaceName isEqualToString:@"en0"]) {
continue;
}
// Break the 4 byte binary representation of the hostname into 4 separate bytes.
uint32_t host = ((struct sockaddr_in *)address)->sin_addr.s_addr;
uint8_t byte1 = (host >> (8 * 0)) & 0xff;
uint8_t byte2 = (host >> (8 * 1)) & 0xff;
uint8_t byte3 = (host >> (8 * 2)) & 0xff;
uint8_t byte4 = (host >> (8 * 3)) & 0xff;
// Join the bytes into a 4 character string.
NSString *hostString = [NSString stringWithFormat:@"%c%c%c%c", byte1, byte2, byte3, byte4];
freeifaddrs(ifaddr);
return hostString;
}
freeifaddrs(ifaddr);
return @"";
}
@end
@@ -25,9 +25,9 @@
/**
* Creates a socket that allows reading/writing for a given connection.
*
* @param connection A bidirectional data connection between a local and remote endpoint. It is the
* responsibility of the caller to maintain a strong reference to the connection
* object.
* @param connection A bidirectional data connection between a local and remote endpoint. This class
* will take ownership of connection and manage its lifetime. The connection
* should not be shared or reused.
*/
- (nonnull instancetype)initWithConnection:(nonnull nw_connection_t)connection
NS_DESIGNATED_INITIALIZER;
@@ -21,7 +21,7 @@
@interface GNCWiFiLANSocket ()
@property(nonatomic, weak, readonly) nw_connection_t connection;
@property(nonatomic, readonly) nw_connection_t connection;
@end
@@ -119,6 +119,7 @@
return;
}
nw_connection_cancel(connection);
_connection = nil;
}
@end