diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPConnection.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPConnection.h new file mode 100644 index 00000000..71a0a108 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPConnection.h @@ -0,0 +1,52 @@ +// Copyright 2025 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/GNCMConnection.h" + +NS_ASSUME_NONNULL_BEGIN + +@class GNCBLEL2CAPStream; + +/** + * A GNCBLEL2CAPConnection implementation for BLE L2CAP. + * This class is thread-safe. + */ +@interface GNCBLEL2CAPConnection : NSObject +@property(nonatomic) GNCMConnectionHandlers *connectionHandlers; + +/* + * Creates a |GNCBLEL2CAPConnection|. + * + * @param stream A |GNCL2CAPStream| instance. + * @param serviceID A string that uniquely identifies the service. + * @param incomingConnection A flag to indicate the connection is incoming. + * @param callbackQueue The queue on which all callbacks are made. + */ ++ (instancetype)connectionWithStream:(GNCBLEL2CAPStream *)stream + serviceID:(nullable NSString *)serviceID + incomingConnection:(BOOL)incomingConnection + callbackQueue:(dispatch_queue_t)callbackQueue; + +/** + * Sends data to the remote device. + * + * @param payload The data to send. + * @param completion A block that is called when the data has been sent. The block takes a BOOL + * parameter indicating whether the data was sent successfully. + */ +- (void)sendData:(NSData *)payload completion:(void (^)(BOOL))completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPConnection.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPConnection.m new file mode 100644 index 00000000..1e88411e --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPConnection.m @@ -0,0 +1,158 @@ +// Copyright 2025 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/BLEv2/GNCBLEL2CAPConnection.h" + +#import + +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.h" +#import "internal/platform/implementation/apple/Mediums/Ble/GNCMBleUtils.h" +#import "internal/platform/implementation/apple/Mediums/GNCLeaks.h" +#import "internal/platform/implementation/apple/Mediums/GNCMConnection.h" +#import "GoogleToolboxForMac/GTMLogger.h" + +enum { kL2CAPPacketLength = 4 }; + +static char *const kGNCBLEL2CAPConnectionQueueLabel = "com.google.nearby.GNCBLEL2CAPConnection"; + +static NSData *PrefixDataWithServiceIDHash(NSData *serviceIDHash, NSData *data) { + NSMutableData *combinedData = [NSMutableData dataWithCapacity:serviceIDHash.length + data.length]; + + [combinedData appendData:serviceIDHash]; + [combinedData appendData:data]; + + return combinedData; +} + +static NSData *PrefixLengthData(NSData *data) { + uint32_t dataLength = (uint32_t)data.length; + uint32_t lengthBigEndian = CFSwapInt32HostToBig(dataLength); + + NSMutableData *packet = [NSMutableData dataWithCapacity:sizeof(uint32_t)]; + [packet appendBytes:&lengthBigEndian length:sizeof(uint32_t)]; + [packet appendData:data]; + + return packet; +} + +@interface GNCBLEL2CAPConnection () +@property(nonatomic) dispatch_queue_t selfQueue; +@property(nonatomic) GNCBLEL2CAPStream *stream; +@property(nonatomic) NSData *serviceIDHash; +@property(nonatomic) dispatch_queue_t callbackQueue; +@property(nonatomic) BOOL incomingConnection; +@end + +@implementation GNCBLEL2CAPConnection + ++ (instancetype)connectionWithStream:(GNCBLEL2CAPStream *)stream + serviceID:(NSString *)serviceID + incomingConnection:(BOOL)incomingConnection + callbackQueue:(dispatch_queue_t)callbackQueue { + GNCBLEL2CAPConnection *connection = [[GNCBLEL2CAPConnection alloc] init]; + [connection setStream:stream]; + stream.delegate = connection; + [connection setServiceIDHash:GNCMServiceIDHash(serviceID)]; + [connection setCallbackQueue:callbackQueue]; + [connection + setSelfQueue:dispatch_queue_create(kGNCBLEL2CAPConnectionQueueLabel, + dispatch_queue_attr_make_with_qos_class( + DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1))]; + [connection setIncomingConnection:incomingConnection]; + return connection; +} + +- (void)dealloc { + [_stream close]; + GNCVerifyDealloc(_stream, 2); // assert if not deallocated +} + +#pragma mark GNCBLEL2CAPConnection + +- (void)sendData:(NSData *)data completion:(void (^)(BOOL))completion { + dispatch_async(_selfQueue, ^{ + NSData *packet; + + // Prefix the service ID hash. + packet = PrefixLengthData(PrefixDataWithServiceIDHash(_serviceIDHash, data)); + GTMLoggerInfo(@"[NEARBY] GNCBLEL2CAPConnection data to be sent: %@", [packet description]); + [_stream sendData:packet + completionBlock:^(BOOL result) { + dispatch_async(_callbackQueue, ^{ + completion(result); + }); + }]; + }); +} + +#pragma mark GNCBLEL2CAPStreamDelegate + +- (void)stream:(GNCBLEL2CAPStream *)stream didReceiveData:(NSData *)data { + GTMLoggerDebug(@"[NEARBY] BLEL2CAPConnection didReceiveData, data: %@", [data description]); + + NSData *realData = [self extractRealDataFromData:data]; + if (realData == nil) { + return; + } + + // Validate the L2CAP packet. + // TODO: b/399815436 - Refactor the validation logic to connections layer. + + // Extract the service ID prefix from each data packet and validate it. + NSUInteger prefixLength = _serviceIDHash.length; + if (![[realData subdataWithRange:NSMakeRange(0, prefixLength)] isEqual:_serviceIDHash]) { + GTMLoggerInfo(@"[NEARBY]: Received wrong data packet and discarded"); + return; + } + + dispatch_async(_selfQueue, ^{ + if (_connectionHandlers.payloadHandler) { + dispatch_async(_callbackQueue, ^{ + _connectionHandlers.payloadHandler([NSData + dataWithData:[realData subdataWithRange:NSMakeRange(prefixLength, + realData.length - prefixLength)]]); + }); + } + }); +} + +- (void)stream:(GNCBLEL2CAPStream *)stream didDisconnectWithError:(NSError *_Nullable)error { + dispatch_async(_selfQueue, ^{ + if (_connectionHandlers.disconnectedHandler) { + dispatch_async(_callbackQueue, ^{ + _connectionHandlers.disconnectedHandler(); + }); + } + }); +} + +#pragma mark Private + +- (NSData *_Nullable)extractRealDataFromData:(NSData *)data { + if (data.length < kL2CAPPacketLength) { + GTMLoggerError(@"[NEARBY] Packet length mismatch. Expected: > %d, Actual: %lu", + kL2CAPPacketLength, data.length); + return nil; + } + int realDataLength = CFSwapInt32BigToHost( + *(int *)([[data subdataWithRange:NSMakeRange(0, kL2CAPPacketLength)] bytes])); + if (realDataLength != (data.length - kL2CAPPacketLength)) { + GTMLoggerError(@"[NEARBY] Data length mismatch. Expected: %d, Actual: %lu", + realDataLength, data.length - kL2CAPPacketLength); + return nil; + } + return [data subdataWithRange:NSMakeRange(kL2CAPPacketLength, data.length - kL2CAPPacketLength)]; +} + +@end diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.h b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.h index e4bc35cd..cc01fd9f 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.h +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.h @@ -14,6 +14,8 @@ #import +@class GNCBLEL2CAPStream; + NS_ASSUME_NONNULL_BEGIN /// Block invoked when the stream is closed. @@ -22,6 +24,23 @@ typedef void (^GNCBLEL2CAPStreamClosedBlock)(void); /// Block invoked when |data| is received from the remote device on the L2CAP connection. typedef void (^GNCBLEL2CAPControllerReceivedDataBlock)(NSData *data); +/** Delegate for @c GNCBLEL2CAPStream. */ +@protocol GNCBLEL2CAPStreamDelegate + +/** + * Called when data is received from the remote device on the L2CAP connection. + * + * @param data The data received from the remote device. + */ +- (void)stream:(GNCBLEL2CAPStream *)stream didReceiveData:(NSData *)data; + +/** + * Called when the stream is closed. + */ +- (void)stream:(GNCBLEL2CAPStream *)stream didDisconnectWithError:(NSError *_Nullable)error; + +@end + /** * Abstraction to take in two streams returned from L2CAP controller and provide a simplified * interface to send and receive data from the streams. @@ -29,6 +48,9 @@ typedef void (^GNCBLEL2CAPControllerReceivedDataBlock)(NSData *data); */ @interface GNCBLEL2CAPStream : NSObject +/// Delegate for @c GNCBLEL2CAPStream +@property(nonatomic, weak) id delegate; + /// Returns an instance of @c GNCBLEL2CAPStream which sends and receives data on |inputStream| /// and |outputStream|. /// Invokes |closedBlock| if stream closed signal is received when reading data. diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.m index 8eecabd8..8cc1e02a 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.m +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.m @@ -320,6 +320,8 @@ } dispatch_async(_receivedDataQueue, ^{ + [_delegate stream:self didReceiveData:data]; + // TODO: b/399815436 - Remove below once the delegate is implemented. self->_receivedDataBlock(data); }); } else if (bytesRead < 0) { @@ -328,6 +330,7 @@ GTMLoggerDebug(@"[NEARBY] End of stream reached. Disconnecting"); // This indicates the L2CAP socket is closed. Notifying the owner so that it can tear down this // stream and update its own state. + [_delegate stream:self didDisconnectWithError:nil]; _closedBlock(); } } diff --git a/internal/platform/implementation/apple/Mediums/BUILD b/internal/platform/implementation/apple/Mediums/BUILD index e84d2584..5ae83ced 100644 --- a/internal/platform/implementation/apple/Mediums/BUILD +++ b/internal/platform/implementation/apple/Mediums/BUILD @@ -27,6 +27,7 @@ objc_library( "BLEv2/GNCBLEGATTClient.m", "BLEv2/GNCBLEGATTServer.m", "BLEv2/GNCBLEL2CAPClient.m", + "BLEv2/GNCBLEL2CAPConnection.m", "BLEv2/GNCBLEL2CAPServer.m", "BLEv2/GNCBLEL2CAPStream.m", "BLEv2/GNCBLEMedium.m", @@ -55,6 +56,7 @@ objc_library( "BLEv2/GNCBLEGATTClient.h", "BLEv2/GNCBLEGATTServer.h", "BLEv2/GNCBLEL2CAPClient.h", + "BLEv2/GNCBLEL2CAPConnection.h", "BLEv2/GNCBLEL2CAPServer.h", "BLEv2/GNCBLEL2CAPStream.h", "BLEv2/GNCBLEMedium.h",