Project import
diff --git a/CFSockets/CFSocket.h b/CFSockets/CFSocket.h new file mode 100755 index 0000000..910f596 --- /dev/null +++ b/CFSockets/CFSocket.h
@@ -0,0 +1,181 @@ +// CFSockets CFSocket.h +// +// Copyright © 2009–2013, Roy Ratcliffe, Pioneering Software, United Kingdom +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the “Software”), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EITHER +// EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO +// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +//------------------------------------------------------------------------------ + +#import <Foundation/Foundation.h> + +@class CFSocket; +@class CFStreamPair; + +@protocol CFSocketDelegate<NSObject> +@optional + +- (void)socket:(CFSocket *)socket acceptNativeHandle:(NSSocketNativeHandle)nativeHandle; +- (void)socket:(CFSocket *)socket acceptStreamPair:(CFStreamPair *)streamPair; + +@end + +/** + * Wraps Core Foundation's sockets in Objective-C clothing. + * + * Handles ARC memory management issues for a socket and implements a + * delegation protocol. As an object, you can sub-class the socket. It papers + * over the toll-free bridging requirements necessary for interactions between + * Core Foundation and Foundation. Start by initialising a socket, either an + * explicit IPv6 or IPv4 TCP socket, or whichever version of TCP socket the + * system makes available to you. Note that IPv4 clients can also access IPv6 + * sockets. + * + * Note, you can have an object class called CFSocket; it does not clash with + * Apple's Core Foundation C-based socket functions, externals and constants + * because those exist in the C name space, while CFSocket here exists in the + * Objective-C name space. They do not collide. + */ +@interface CFSocket : NSObject +{ + CFSocketRef _socket; + CFRunLoopSourceRef _runLoopSource; +} + +@property(weak, NS_NONATOMIC_IOSONLY) id<CFSocketDelegate> delegate; + +/** + * Designated initialiser. + * + * Initialisers also create the underlying Core Foundation socket. You + * cannot have a partially initialised Objective-C socket. When socket creation + * fails, initialisation fails also. All socket initialisers follow this + * pattern. Hence, you cannot initialise a socket with a `NULL` socket + * reference. In such cases, the initialiser answers `nil`. + * + * This approach creates a slight quandary. Creating a Core Foundation socket + * requires a socket context. The context needs to retain a bridging reference + * to `self`, the Objective-C object encapsulating the socket. Otherwise, the + * socket call-back function cannot springboard from C to Objective-C when + * call-backs trigger. When the initialiser returns successfully however, the + * answer overwrites `self`. What if `self` changes? If it changes to `nil`, + * no problem. But what if it changes to some other pointer address? + * + * TODO: Add more initialisers; specifically, socket signature initialisers. + * + * @param socket Reference to a CFSocket object. + */ +- (id)initWithSocketRef:(CFSocketRef)socket; + +- (id)initWithProtocolFamily:(int)family socketType:(int)type protocol:(int)protocol; +- (id)initForTCPv6; +- (id)initForTCPv4; + +/** + * Instantiates and creates a TCP socket using Internet Protocol version + * 6 if available, else tries version 4. + * + * Starts by creating a socket for Internet Protocol version 6. This + * also supports IPv4 clients via IPv4-mapped IPv6 addresses. Falls back to IPv4 + * only when IPv6 fails. + * + * @result Answers a TCP socket, version 6 or 4; returns `nil` if the socket + * fails to create. + */ +- (id)initForTCP; + +/** + * Initialises using a native socket handle. + * @param nativeHandle Platform-specific native socket handle. + */ +- (id)initWithNativeHandle:(NSSocketNativeHandle)nativeHandle; + +/** + * Binds an address to a socket. + * + * Despite the innocuous-sounding method name, this method irreversibly + * binds the socket; assuming success. Do this early when constructing a + * socket. Be aware that accessing the address also binds the socket, if not + * already bound. You cannot therefore subsequently bind it. If you want to bind + * to a specific port, do so by setting the socket address _before_ asking for + * the address; that is, before using the getter method. + */ +- (BOOL)setAddress:(NSData *)addressData error:(NSError **)outError; + +- (BOOL)connectToAddress:(NSData *)addressData timeout:(NSTimeInterval)timeout error:(NSError **)outError; + +- (void)invalidate; +- (BOOL)isValid; +- (NSData *)address; +- (NSData *)peerAddress; +- (NSSocketNativeHandle)nativeHandle; +- (BOOL)setReuseAddressOption:(BOOL)flag; + +/** + * Answers the socket address family. + * + * For Internet-based sockets, answers either `AF_INET6` or + * `AF_INET`. The latter for IP version 4 addresses. Note, protocol and address + * family are one and the same for Internet addresses. Protocol families are + * defined in terms of their address family; the `PF_` equals its corresponding + * `AF_` manifest constant. + * + * You can use this for polymorphic behaviour. If behaviour depends on a + * particular kind of socket, you can ask this method for the underlying address + * family and respond accordingly. This method differs from -address which binds + * the address first. The implementation here obtains the address family + * non-destructively; socket state remains unchanged. + * + * @result Answers the socket address family, or `AF_MAX` when an error + * occurs. On error, standard library `errno` value indicates the problem. + */ +- (int)addressFamily; + +/** + * Answers the port number. + * + * The answer depends on the socket's address family: version 4 or + * 6. Handles the network-to-host byte ordering. The resulting integer has + * correct ordering for the host machine. + */ +- (int)port; + +- (void)addToCurrentRunLoopForCommonModes; +- (void)removeFromCurrentRunLoopForCommonModes; +- (void)disableAcceptCallBack; +- (void)enableAcceptCallBack; + +/** + * Handles the socket _accept_ call-back behaviour. + * + * Executes when Next Step meets Core Foundation. The argument specifies a + * native socket handle, an integer defining the Unix socket descriptor. + * + * Exists to allow for optional overriding. You do not need to deploy + * the delegate protocol if your sub-class handles "accept native handle" events + * directly; though delegation usually works best. + * + * @param nativeHandle Platform-specific native socket handle. + */ +- (void)acceptNativeHandle:(NSSocketNativeHandle)nativeHandle; + +@end + +extern NSString *const CFSocketErrorDomain; + +void __CFSocketCallOut(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info);
diff --git a/CFSockets/CFSocket.m b/CFSockets/CFSocket.m new file mode 100755 index 0000000..4638696 --- /dev/null +++ b/CFSockets/CFSocket.m
@@ -0,0 +1,270 @@ +// CFSockets CFSocket.m +// +// Copyright © 2009–2013, Roy Ratcliffe, Pioneering Software, United Kingdom +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the “Software”), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EITHER +// EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO +// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +//------------------------------------------------------------------------------ + +#import "CFSocket.h" +#import "CFStreamPair.h" + +// for setsockopt(2) +#import <sys/socket.h> + +// for IPPROTO_TCP +#import <netinet/in.h> + +@implementation CFSocket + +@synthesize delegate = _delegate; + +- (id)init +{ + return (self = nil); +} + +// designated initialiser +- (id)initWithSocketRef:(CFSocketRef)socket +{ + if ((self = [super init])) + { + if (socket) + { + _socket = socket; + } + else + { + self = nil; + } + } + return self; +} + +- (id)initWithProtocolFamily:(int)family socketType:(int)type protocol:(int)protocol +{ + CFSocketContext context = { .info = (__bridge void *)self }; + return [self initWithSocketRef:CFSocketCreate(kCFAllocatorDefault, family, type, protocol, kCFSocketAcceptCallBack, __CFSocketCallOut, &context)]; +} + +- (id)initForTCPv6 +{ + return [self initWithProtocolFamily:PF_INET6 socketType:SOCK_STREAM protocol:IPPROTO_TCP]; +} + +- (id)initForTCPv4 +{ + return [self initWithProtocolFamily:PF_INET socketType:SOCK_STREAM protocol:IPPROTO_TCP]; +} + +- (id)initForTCP +{ + self = [self initForTCPv6]; + if (self == nil) self = [self initForTCPv4]; + return self; +} + +- (id)initWithNativeHandle:(NSSocketNativeHandle)nativeHandle +{ + CFSocketContext context = { .info = (__bridge void *)self }; + return [self initWithSocketRef:CFSocketCreateWithNative(kCFAllocatorDefault, nativeHandle, kCFSocketAcceptCallBack, __CFSocketCallOut, &context)]; +} + +- (BOOL)setAddress:(NSData *)addressData error:(NSError **)outError +{ + CFSocketError error = CFSocketSetAddress(_socket, (__bridge CFDataRef)addressData); + BOOL success = (error == kCFSocketSuccess); + if (!success) + { + if (outError && *outError == nil) + { + *outError = [NSError errorWithDomain:CFSocketErrorDomain code:error userInfo:nil]; + } + } + return success; +} + +- (BOOL)connectToAddress:(NSData *)addressData timeout:(NSTimeInterval)timeout error:(NSError **)outError +{ + CFSocketError error = CFSocketConnectToAddress(_socket, (__bridge CFDataRef)addressData, timeout); + BOOL success = (error == kCFSocketSuccess); + if (!success) + { + if (outError && *outError == nil) + { + *outError = [NSError errorWithDomain:CFSocketErrorDomain code:error userInfo:nil]; + } + } + return success; +} + +- (void)invalidate +{ + // Never close the underlying native socket without first invalidating. + CFSocketInvalidate(_socket); +} + +- (BOOL)isValid +{ + return CFSocketIsValid(_socket) != false; +} + +- (NSData *)address +{ + return CFBridgingRelease(CFSocketCopyAddress(_socket)); +} + +- (NSData *)peerAddress +{ + return CFBridgingRelease(CFSocketCopyPeerAddress(_socket)); +} + +- (NSSocketNativeHandle)nativeHandle +{ + return CFSocketGetNative(_socket); +} + +- (BOOL)setReuseAddressOption:(BOOL)flag +{ + int option = (flag == NO) ? 0 : 1; + return 0 == setsockopt([self nativeHandle], SOL_SOCKET, SO_REUSEADDR, (void *)&option, sizeof(option)); +} + +- (int)addressFamily +{ + uint8_t sockaddr[SOCK_MAXADDRLEN]; + socklen_t len = sizeof(sockaddr); + return 0 == getsockname([self nativeHandle], (struct sockaddr *)sockaddr, &len) && len >= offsetof(struct sockaddr, sa_data) ? ((struct sockaddr *)sockaddr)->sa_family : AF_MAX; +} + +- (int)port +{ + int port; + switch ([self addressFamily]) + { + case AF_INET: + port = ntohs(((struct sockaddr_in *)[[self address] bytes])->sin_port); + break; + case AF_INET6: + port = ntohs(((struct sockaddr_in6 *)[[self address] bytes])->sin6_port); + break; + default: + port = 0; + } + return port; +} + +- (void)addToCurrentRunLoopForCommonModes +{ + // NSRunLoop is not toll-free bridged to CFRunLoop, even though their names + // might suggest that they are. + if (_runLoopSource == NULL) + { + _runLoopSource = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket, 0); + CFRunLoopAddSource(CFRunLoopGetCurrent(), _runLoopSource, kCFRunLoopCommonModes); + } +} + +- (void)removeFromCurrentRunLoopForCommonModes +{ + if (_runLoopSource) + { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), _runLoopSource, kCFRunLoopCommonModes); + CFRelease(_runLoopSource); + _runLoopSource = NULL; + } +} + +- (void)disableAcceptCallBack +{ + CFSocketDisableCallBacks(_socket, kCFSocketAcceptCallBack); +} + +- (void)enableAcceptCallBack +{ + // The Read, Accept and Data callbacks are mutually exclusive. + CFSocketEnableCallBacks(_socket, kCFSocketAcceptCallBack); +} + +- (void)acceptNativeHandle:(NSSocketNativeHandle)nativeHandle +{ + id<CFSocketDelegate> delegate = [self delegate]; + if (delegate) + { + if ([delegate respondsToSelector:@selector(socket:acceptNativeHandle:)]) + { + [delegate socket:self acceptNativeHandle:nativeHandle]; + } + else if ([delegate respondsToSelector:@selector(socket:acceptStreamPair:)]) + { + CFStreamPair *streamPair = [[CFStreamPair alloc] initWithSocketNativeHandle:nativeHandle]; + if (streamPair) + { + [delegate socket:self acceptStreamPair:streamPair]; + } + } + else + { + close(nativeHandle); + } + } + else + { + close(nativeHandle); + } +} + +- (void)dealloc +{ + // The de-allocator does not need to wonder if the underlying socket exists, + // or not. By contract, the socket must exist. This assumes, of course, that + // a failed initialisation sequence does not invoke the + // de-allocator. However, you cannot assume that. Assigning self to nil + // under ARC de-allocates the instance and invokes the -dealloc method. + [self removeFromCurrentRunLoopForCommonModes]; + if (_socket) + { + CFRelease(_socket); + _socket = NULL; + } +} + +@end + +NSString *const CFSocketErrorDomain = @"CFSocketErrorDomain"; + +void __CFSocketCallOut(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) +{ + switch (type) + { + case kCFSocketAcceptCallBack: + { + // Next Step meets Core Foundation socket native handle type in the + // next statement. You can use them interchangeably. Apple + // type-define both as int. They are really Unix socket + // descriptors. The external interface uses the Next Step + // definition, since the Next Step foundation framework is the most + // immediate dependency. + [(__bridge CFSocket *)info acceptNativeHandle:*(CFSocketNativeHandle *)data]; + break; + } + default: + ; + } +}
diff --git a/CFSockets/CFSocketAddressDataHelpers.h b/CFSockets/CFSocketAddressDataHelpers.h new file mode 100755 index 0000000..44369fc --- /dev/null +++ b/CFSockets/CFSocketAddressDataHelpers.h
@@ -0,0 +1,35 @@ +/* CFSockets CFSocketAddressDataHelpers.h + * + * Copyright © 2012, 2013, Roy Ratcliffe, Pioneering Software, United Kingdom + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the “Software”), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + ******************************************************************************/ + +#import <Foundation/Foundation.h> + +#import <netinet/in.h> + +NSData *CFSocketAddressDataFromIPv6AddressWithPort(const struct in6_addr *addr, in_port_t port); +NSData *CFSocketAddressDataFromAnyIPv6WithPort(in_port_t port); +NSData *CFSocketAddressDataFromLoopBackIPv6WithPort(in_port_t port); + +NSData *CFSocketAddressDataFromIPv4AddressWithPort(in_addr_t addr, in_port_t port); +NSData *CFSocketAddressDataFromAnyIPv4WithPort(in_port_t port); +NSData *CFSocketAddressDataFromLoopBackIPv4WithPort(in_port_t port);
diff --git a/CFSockets/CFSocketAddressDataHelpers.m b/CFSockets/CFSocketAddressDataHelpers.m new file mode 100644 index 0000000..a054c7d --- /dev/null +++ b/CFSockets/CFSocketAddressDataHelpers.m
@@ -0,0 +1,72 @@ +/* CFSockets CFSocketAddressDataHelpers.m + * + * Copyright © 2012, 2013, Roy Ratcliffe, Pioneering Software, United Kingdom + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the “Software”), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + ******************************************************************************/ + +#import "CFSocketAddressDataHelpers.h" +#import "arpa/inet.h" + +NSData *CFSocketAddressDataFromIPv6AddressWithPort(const struct in6_addr *addr, in_port_t port) +{ + struct sockaddr_in6 sockaddr; + memset(&sockaddr, 0, sizeof(sockaddr)); + + sockaddr.sin6_len = sizeof(sockaddr); + sockaddr.sin6_family = AF_INET6; + sockaddr.sin6_port = htons(port); + memcpy(&sockaddr.sin6_addr, addr, sizeof(sockaddr.sin6_addr)); + + return [NSData dataWithBytes:&sockaddr length:sizeof(sockaddr)]; +} + +NSData *CFSocketAddressDataFromAnyIPv6WithPort(in_port_t port) +{ + return CFSocketAddressDataFromIPv6AddressWithPort(&in6addr_any, port); +} + +NSData *CFSocketAddressDataFromLoopBackIPv6WithPort(in_port_t port) +{ + return CFSocketAddressDataFromIPv6AddressWithPort(&in6addr_loopback, port); +} + +NSData *CFSocketAddressDataFromIPv4AddressWithPort(in_addr_t addr, in_port_t port) +{ + struct sockaddr_in sockaddr; + memset(&sockaddr, 0, sizeof(sockaddr)); + + sockaddr.sin_len = sizeof(sockaddr); + sockaddr.sin_family = AF_INET; + sockaddr.sin_port = htons(port); + sockaddr.sin_addr.s_addr = htonl(addr); + + return [NSData dataWithBytes:&sockaddr length:sizeof(sockaddr)]; +} + +NSData *CFSocketAddressDataFromAnyIPv4WithPort(in_port_t port) +{ + return CFSocketAddressDataFromIPv4AddressWithPort(INADDR_ANY, port); +} + +NSData *CFSocketAddressDataFromLoopBackIPv4WithPort(in_port_t port) +{ + return CFSocketAddressDataFromIPv4AddressWithPort(INADDR_LOOPBACK, port); +}
diff --git a/CFSockets/CFSockets.h b/CFSockets/CFSockets.h new file mode 100755 index 0000000..58aa56b --- /dev/null +++ b/CFSockets/CFSockets.h
@@ -0,0 +1,28 @@ +/* CFSockets CFSockets.h + * + * Copyright © 2012, 2013, Roy Ratcliffe, Pioneering Software, United Kingdom + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the “Software”), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + ******************************************************************************/ + +#import <CFSockets/CFSocket.h> +#import <CFSockets/CFSocketAddressDataHelpers.h> +#import <CFSockets/CFStreamPair.h> +#import <CFSockets/Versioning.h>
diff --git a/CFSockets/CFStreamPair.h b/CFSockets/CFStreamPair.h new file mode 100755 index 0000000..7d6e5ea --- /dev/null +++ b/CFSockets/CFStreamPair.h
@@ -0,0 +1,100 @@ +// CFSockets CFStreamPair.h +// +// Copyright © 2012, 2013, Roy Ratcliffe, Pioneering Software, United Kingdom +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the “Software”), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EITHER +// EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO +// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +//------------------------------------------------------------------------------ + +#import <Foundation/Foundation.h> + +@class CFStreamPair; + +@protocol CFStreamPairDelegate<NSObject> +@optional + +- (void)streamPair:(CFStreamPair *)streamPair hasBytesAvailable:(NSUInteger)bytesAvailable; + +- (void)streamPair:(CFStreamPair *)streamPair handleRequestEvent:(NSStreamEvent)eventCode; +- (void)streamPair:(CFStreamPair *)streamPair handleResponseEvent:(NSStreamEvent)eventCode; + +@end + +/** + * Adds request-response semantics to a socket by encapsulating it with + * an input-output stream pair. + * + * For TCP connections, the request input stream corresponds to the + * request; _receive_ the request bytes from the request input, _send_ the + * response to the response output stream. + */ +@interface CFStreamPair : NSObject<NSStreamDelegate> + +@property(weak, NS_NONATOMIC_IOSONLY) id<CFStreamPairDelegate> delegate; +@property(strong, NS_NONATOMIC_IOSONLY) NSInputStream *requestStream; +@property(strong, NS_NONATOMIC_IOSONLY) NSOutputStream *responseStream; + +- (id)initWithRequestStream:(NSInputStream *)requestStream responseStream:(NSOutputStream *)responseStream; +- (id)initWithSocketNativeHandle:(NSSocketNativeHandle)socketNativeHandle; + +/** + * Opens the request and response streams, scheduling them for service within + * the current run loop. You cannot reopen the streams. + * + * This method assumes that you have not already delegated, scheduled + * or opened the underlying request-response stream pair. + */ +- (void)open; + +/** + * Reverses the opening. Closes the request and response streams, removes them + * from the current run loop. This assumes that you send `-close` from the same + * thread as you sent the original `-open`. + */ +- (void)close; + +/** + * Destructively receives bytes from the request buffer. + */ +- (NSData *)receiveAvailableBytes; + +/** + * Special convenience method for receiving lines of text from the + * request stream based on a given string encoding. + * + * The result includes any line termination characters. There could be + * more than one termination character at the end of the line since some line + * termination sequences span multiple characters. + * + * @result Answers `nil` if the request buffer does not yet contain a complete + * line. Try again later. + */ +- (NSString *)receiveLineUsingEncoding:(NSStringEncoding)encoding; + +- (void)sendBytes:(NSData *)outputBytes; + +//-------------------------------------------------------------------- Overrides + +- (void)hasBytesAvailable; +- (void)hasSpaceAvailable; +- (void)sendBytes; +- (void)handleRequestEvent:(NSStreamEvent)eventCode; +- (void)handleResponseEvent:(NSStreamEvent)eventCode; + +@end
diff --git a/CFSockets/CFStreamPair.m b/CFSockets/CFStreamPair.m new file mode 100644 index 0000000..2b6a122 --- /dev/null +++ b/CFSockets/CFStreamPair.m
@@ -0,0 +1,338 @@ +// CFSockets CFStreamPair.m +// +// Copyright © 2012, 2013, Roy Ratcliffe, Pioneering Software, United Kingdom +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the “Software”), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EITHER +// EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO +// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +//------------------------------------------------------------------------------ + +#import "CFStreamPair.h" + +static NSString *const NewLineCharacter = @"\n"; + +@interface CFStreamPair() + +// You would not normally access the buffers directly. The following exposes the +// buffer implementation, albeit away from the public header: just a pair of +// mutable data objects. +@property(strong, NS_NONATOMIC_IOSONLY) NSMutableData *requestBuffer; +@property(strong, NS_NONATOMIC_IOSONLY) NSMutableData *responseBuffer; + +@end + +@implementation CFStreamPair + +@synthesize delegate = _delegate; + +// streams +@synthesize requestStream = _requestStream; +@synthesize responseStream = _responseStream; + +// buffers +@synthesize requestBuffer = _requestBuffer; +@synthesize responseBuffer = _responseBuffer; + +// designated initialiser +- (id)init +{ + if ((self = [super init])) + { + // Sets up the request and response buffers at the outset. You can ask + // for available request bytes even before the request stream + // opens. Similarly, you can send response bytes even before the + // response opens. Hard to imagine exactly why however. Still, there is + // nothing to say that we can assume that the response stream will open + // before the request opens, or vice versa; indeed, the delegate may + // even respond by sending some bytes even before the response stream + // becomes ready. The buffer pair make such behaviour a valid pattern. + [self setRequestBuffer:[NSMutableData data]]; + [self setResponseBuffer:[NSMutableData data]]; + } + return self; +} + +- (void)dealloc +{ + [self close]; +} + +// convenience initialiser +- (id)initWithRequestStream:(NSInputStream *)requestStream responseStream:(NSOutputStream *)responseStream +{ + self = [self init]; + if (self) + { + [self setRequestStream:requestStream]; + [self setResponseStream:responseStream]; + } + return self; +} + +- (id)initWithSocketNativeHandle:(NSSocketNativeHandle)socketNativeHandle +{ + if ((self = [self init])) + { + CFReadStreamRef readStream = NULL; + CFWriteStreamRef writeStream = NULL; + CFStreamCreatePairWithSocket(kCFAllocatorDefault, socketNativeHandle, &readStream, &writeStream); + if (readStream && writeStream) + { + CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + [self setRequestStream:CFBridgingRelease(readStream)]; + [self setResponseStream:CFBridgingRelease(writeStream)]; + } + else + { + if (readStream) CFRelease(readStream); + if (writeStream) CFRelease(writeStream); + + // Something went wrong. Answer nil. Bear in mind however that this + // does not mean that the de-allocation method will not run: it + // will run. + self = nil; + } + } + return self; +} + +- (void)open +{ + for (NSStream *stream in [NSArray arrayWithObjects:[self requestStream], [self responseStream], nil]) + { + [stream setDelegate:self]; + + [stream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + [stream open]; + } +} + +- (void)close +{ + // Send -close first. Closing may trigger events. Let the stream emit all + // events until closing finishes. Might be wise to check the stream status + // first, before attempting to close the stream. The de-allocator invokes + // -close and therefore may send a double-close if the pair has already + // received an explicit -close message. + for (NSStream *stream in [NSArray arrayWithObjects:[self requestStream], [self responseStream], nil]) + { + [stream close]; + [stream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + } +} + +- (NSData *)receiveAvailableBytes +{ + NSData *bytes = [[self requestBuffer] copy]; + [[self requestBuffer] setLength:0]; + return bytes; +} + +- (NSString *)receiveLineUsingEncoding:(NSStringEncoding)encoding { + // The implementation first converts all the request bytes to a string. This + // could be risky for multi-byte characters. The implementation effectively + // assumes that multi-byte characters do not cross buffer boundaries. + // + // When the request range has length equal to zero, sending + // -lineRangeForRange: searches for the first line. The final bit is + // tricky. How to dissect the line from any remaining characters? Simply + // convert the remaining characters back to data using the given encoding. + NSString *result; + + NSArray *requestsArray = [self requestsArrayFromData:self.requestBuffer withEncoding:encoding]; + NSString *requestString = requestsArray.firstObject; + + NSRange lineRange = [requestString lineRangeForRange:NSMakeRange(0, 0)]; + + if (lineRange.length) { + NSMutableArray *mutableRequestsArray = [requestsArray mutableCopy]; + [mutableRequestsArray removeObjectAtIndex:0]; + + if (mutableRequestsArray.count) { + self.requestBuffer.data = [self dataFromRequestsArray:mutableRequestsArray withEncoding:encoding]; + } else { + self.requestBuffer.length = 0; + } + + result = [requestString substringToIndex:lineRange.length]; + } else { + result = nil; + } + + return result; +} + +- (NSArray *)requestsArrayFromData:(NSData *)data withEncoding:(NSStringEncoding)encoding { + NSString *requestsString = [[NSString alloc] initWithData:data encoding:encoding]; + NSMutableArray *requestsArray = [[requestsString componentsSeparatedByString:NewLineCharacter] mutableCopy]; + + for (NSInteger i = requestsArray.count - 1; i >= 0; i--) { + NSString *requestString = [((NSString *) requestsArray[i]) stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + if (requestString.length == 0) { + [requestsArray removeObjectAtIndex:i]; + } + } + + return requestsArray; +} + +- (NSData *)dataFromRequestsArray:(NSArray *)requestsArray withEncoding:(NSStringEncoding)encoding { + NSString *requestBufferString = [requestsArray componentsJoinedByString:NewLineCharacter]; + + return [requestBufferString dataUsingEncoding:encoding]; +} + +- (void)sendBytes:(NSData *)responseBytes +{ + [[self responseBuffer] appendData:responseBytes]; + + // Trigger a "has space available" event if the response stream reports + // available space at this point. + if ([[self responseStream] hasSpaceAvailable]) + { + [self hasSpaceAvailable]; + } +} + +#pragma mark - +#pragma mark Overrides + +- (void)hasBytesAvailable +{ + if ([[self requestBuffer] length]) + { + id delegate = [self delegate]; + if (delegate && [delegate respondsToSelector:@selector(streamPair:hasBytesAvailable:)]) + { + [delegate streamPair:self hasBytesAvailable:[[self requestBuffer] length]]; + } + } +} + +- (void)hasSpaceAvailable +{ + // Note that writing zero bytes to the response stream closes the + // connection. Therefore, avoid sending nothing unless you want to close. + if ([[self responseBuffer] length]) + { + [self sendBytes]; + } +} + +- (void)sendBytes +{ + NSMutableData *responseBuffer = [self responseBuffer]; + NSInteger bytesSent = [[self responseStream] write:[responseBuffer bytes] maxLength:[responseBuffer length]]; + if (bytesSent > 0) + { + [responseBuffer replaceBytesInRange:NSMakeRange(0, bytesSent) withBytes:NULL length:0]; + } +} + +- (void)handleRequestEvent:(NSStreamEvent)eventCode +{ + switch (eventCode) + { + case NSStreamEventHasBytesAvailable: + { + uint8_t bytes[4096]; + NSInteger bytesAvailable = [[self requestStream] read:bytes maxLength:sizeof(bytes)]; + // Do not send a -read:maxLength message unless the stream reports + // that it has bytes available. Always send this message at least + // once when the bytes-available event fires, i.e. right now. The + // stream event indicates that available bytes have already been + // sensed. Avoid asking again. + // + // What happens however if more bytes arrive while reading, or the + // available bytes overflow the stack-based temporary buffer? In + // these cases, after reading, ask if more bytes exist. Issue + // another read if they do, and repeat while they do. + while (bytesAvailable > 0) + { + [[self requestBuffer] appendBytes:bytes length:bytesAvailable]; + if ([[self requestStream] hasBytesAvailable]) + { + bytesAvailable = [[self requestStream] read:bytes maxLength:sizeof(bytes)]; + } + else + { + bytesAvailable = 0; + } + } + // Please note, the delegate can receive an has-bytes-available + // event immediately followed by an error event. + [self hasBytesAvailable]; + if (bytesAvailable < 0) + { + + } + break; + } + default: + ; + } + + id delegate = [self delegate]; + if (delegate && [delegate respondsToSelector:@selector(streamPair:handleRequestEvent:)]) + { + [delegate streamPair:self handleRequestEvent:eventCode]; + } +} + +- (void)handleResponseEvent:(NSStreamEvent)eventCode +{ + switch (eventCode) + { + case NSStreamEventHasSpaceAvailable: + { + [self hasSpaceAvailable]; + break; + } + default: + ; + } + + id delegate = [self delegate]; + if (delegate && [delegate respondsToSelector:@selector(streamPair:handleResponseEvent:)]) + { + [delegate streamPair:self handleResponseEvent:eventCode]; + } +} + +#pragma mark - +#pragma mark Stream Delegate + +- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode +{ + if (stream == [self requestStream]) + { + [self handleRequestEvent:eventCode]; + } + else if (stream == [self responseStream]) + { + [self handleResponseEvent:eventCode]; + } + else + { + ; + } +} + +@end
diff --git a/CFSockets/Versioning.h b/CFSockets/Versioning.h new file mode 100755 index 0000000..8d3e74e --- /dev/null +++ b/CFSockets/Versioning.h
@@ -0,0 +1,36 @@ +/* CFSockets Versioning.h + * + * Copyright © 2012, 2013, Roy Ratcliffe, Pioneering Software, United Kingdom + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the “Software”), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + ******************************************************************************/ + +#import <Foundation/Foundation.h> + +extern const unsigned char kCFSocketsVersionString[]; +extern const double kCFSocketsVersionNumber; + +/** + * Answers the current Apple-generic versioning-formatted version string. The + * version string has been trimmed. It has no leading or trailing whitespace or + * newlines. Note that the raw C-based version string contrastingly has a single + * terminating newline character. + */ +NSString *CFSocketsVersionString(void);
diff --git a/CFSockets/Versioning.m b/CFSockets/Versioning.m new file mode 100755 index 0000000..94846c8 --- /dev/null +++ b/CFSockets/Versioning.m
@@ -0,0 +1,38 @@ +/* CFSockets Versioning.m + * + * Copyright © 2012, 2013, Roy Ratcliffe, Pioneering Software, United Kingdom + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the “Software”), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO + * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + ******************************************************************************/ + +#import "Versioning.h" + +NSString *CFSocketsVersionString() +{ + // The implementation assumes that the raw C-language version string + // terminates with null. It also trims assuming that the very last character + // is a terminating line feed. Also assumes UTF-8 encoding. + static NSString *__strong versionString; + if (versionString == nil) + { + versionString = [[NSString stringWithCString:(const char *)kCFSocketsVersionString encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + } + return versionString; +}
diff --git a/README.md b/README.md new file mode 100755 index 0000000..1b59196 --- /dev/null +++ b/README.md
@@ -0,0 +1,14 @@ +# Core Foundation Sockets + +[](https://travis-ci.org/royratcliffe/CFSockets) + +The project has four targets: a Cocoa framework with its unit test bundle, an +iOS static library with its unit test bundle. The framework and the library +share the same source files. The sources use Automatic Reference Counting (ARC). + +Why the plural project name? The project goes by the name 'Core Foundation +Sockets' rather than 'Core Foundation Socket.' This avoids confusion with +`CFSocket` which represents just one component of the CFSockets project. The +project's monolithic header imports as `<CFSockets/CFSockets.h>`. The +additional _s_ for the project prevents a clash with header +`<CFSockets/CFSocket.h>` which only imports the `CFSocket` class.