blob: 3f6246b853dd8ea71d893b19dd8c36a9ab31900e [file] [log] [blame] [edit]
//
// Copyright 2018 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
//
// http://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 "Channel/Sources/EDOSocket.h"
#import "Channel/Sources/EDOListenSocket.h"
#import "Channel/Sources/EDOSocketPort.h"
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/tcp.h>
#include <sys/un.h>
// The 'nil' completion block that does nothing.
static EDOSocketConnectedBlock gNoOpHandlerBlock = ^(EDOSocket *socket, NSError *error) {
};
#pragma mark - Socket help functions
/**
* Create a non-block socket.
*
* @param errNo the out parameter when it errors.
*
* @return -1, if fails to create and @c errNo contains @c errno value;
* the socket file descriptor, otherwise.
*/
static dispatch_fd_t edo_CreateSocket(int *errNo) {
NSCAssert(errNo, @"errNo cannot be nil");
dispatch_fd_t fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
*errNo = errno;
return -1;
}
if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) {
*errNo = errno;
close(fd);
return -1;
}
return fd;
}
/** Util function to run block on the error code. */
static void edo_RunHandlerWithErrorInQueueWithBlock(int code, dispatch_queue_t queue,
EDOSocketConnectedBlock block) {
if (block) {
dispatch_async(queue, ^{
block(nil, [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:nil]);
});
}
}
#pragma mark - EDOSocket implementation
@interface EDOSocket ()
/** The global serial queue for handling dispatch I/O's cleanup handler and closing socket. */
@property(class, atomic, readonly) dispatch_queue_t closeSocketQueue;
@end
@implementation EDOSocket {
dispatch_fd_t _socket;
}
@dynamic valid;
+ (dispatch_queue_t)closeSocketQueue {
static dispatch_queue_t closeSocketQueue;
static dispatch_once_t once_token;
dispatch_once(&once_token, ^{
closeSocketQueue = dispatch_queue_create("com.google.edo.CloseSocket", DISPATCH_QUEUE_SERIAL);
});
return closeSocketQueue;
}
+ (nullable instancetype)socketWithTCPPort:(UInt16)port
queue:(dispatch_queue_t _Nullable)queue
error:(NSError *_Nullable *_Nullable)error {
__block EDOSocket *connectedSocket;
__block NSError *connectionError;
dispatch_semaphore_t waitLock = dispatch_semaphore_create(0);
[self connectWithTCPPort:port
queue:queue
connectedBlock:^(EDOSocket *socket, NSError *socketError) {
connectedSocket = socket;
connectionError = socketError;
dispatch_semaphore_signal(waitLock);
}];
dispatch_semaphore_wait(waitLock, DISPATCH_TIME_FOREVER);
if (error) {
*error = connectionError;
}
return connectedSocket;
}
- (instancetype)initWithSocket:(dispatch_fd_t)socket {
self = [super init];
if (self) {
_socket = socket;
_socketPort = [[EDOSocketPort alloc] initWithSocket:socket];
}
return self;
}
- (void)dealloc {
// Make sure the socket is released and closed.
[self invalidate];
}
+ (instancetype)socketWithSocket:(dispatch_fd_t)socket {
return [[self alloc] initWithSocket:socket];
}
- (dispatch_fd_t)releaseSocket {
@synchronized(self) {
dispatch_fd_t socketFD = _socket;
_socket = -1;
return socketFD;
}
}
- (nullable dispatch_io_t)releaseAsDispatchIO {
dispatch_fd_t socket = [self releaseSocket];
if (socket == -1) {
return NULL;
}
dispatch_queue_t queue = [self class].closeSocketQueue;
dispatch_io_t channel = dispatch_io_create(DISPATCH_IO_STREAM, socket, queue, ^(int error) {
if (error) {
NSLog(@"Error (%d) when closing dispatch channel.", error);
}
if (error == 0) {
close(socket);
}
});
// Clean up the socket if it fails to create the channel here.
if (!channel) {
close(socket);
}
return channel;
}
- (void)invalidate {
dispatch_fd_t socketFD = [self releaseSocket];
if (socketFD != -1) {
close(socketFD);
}
}
- (BOOL)valid {
@synchronized(self) {
return _socket >= 0;
}
}
+ (void)connectWithTCPPort:(UInt16)port
queue:(dispatch_queue_t)queue
connectedBlock:(EDOSocketConnectedBlock)block {
block = block ?: gNoOpHandlerBlock;
queue = queue ?: dispatch_queue_create("com.google.edo.connectSocket", DISPATCH_QUEUE_SERIAL);
int socketErr = 0;
dispatch_fd_t socketFD = edo_CreateSocket(&socketErr);
if (socketFD == -1) {
edo_RunHandlerWithErrorInQueueWithBlock(socketErr, queue, block);
return;
}
// The dispatch source to wait on the connection.
dispatch_source_t source =
dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, (uintptr_t)socketFD, 0, queue);
dispatch_source_set_event_handler(source, ^{
int connectError = 0;
socklen_t errorlen = sizeof(connectError);
// If there is an error, the connection fails.
if (getsockopt(socketFD, SOL_SOCKET, SO_ERROR, &connectError, &errorlen) != 0 ||
connectError != 0) {
edo_RunHandlerWithErrorInQueueWithBlock(connectError, queue, block);
} else {
// Prevent SIGPIPE, suggested by Apple.
// https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/CommonPitfalls/CommonPitfalls.html
int on = 1;
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on));
block([EDOSocket socketWithSocket:socketFD], nil);
}
// Once connected, we don't need this source any more; so we don't track this internally.
// This will also effectively release the strong reference of the source so it breaks the
// retain cycle as after the source is cancelled, the event handler will get released.
dispatch_source_cancel(source);
});
dispatch_resume(source);
// Setup a sockaddr with the default local loopback address 127.0.0.1 to connect to.
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
int ret = connect(socketFD, (struct sockaddr const *)&addr, sizeof(addr));
socketErr = errno;
if (ret != 0 && socketErr != EINPROGRESS) {
edo_RunHandlerWithErrorInQueueWithBlock(socketErr, queue, block);
close(socketFD);
}
}
+ (EDOSocket *)listenWithTCPPort:(UInt16)port
queue:(dispatch_queue_t)queue
connectedBlock:(EDOSocketConnectedBlock)block {
block = block ?: gNoOpHandlerBlock;
queue = queue ?: dispatch_queue_create("com.google.edo.listenSocket", DISPATCH_QUEUE_CONCURRENT);
int socketErr = 0;
dispatch_fd_t socketFD = edo_CreateSocket(&socketErr);
if (socketFD == -1) {
edo_RunHandlerWithErrorInQueueWithBlock(socketErr, queue, block);
return nil;
}
int on = 1;
if (setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
edo_RunHandlerWithErrorInQueueWithBlock(errno, queue, block);
close(socketFD);
return nil;
}
// Setup a sockaddr with the default local loopback address 127.0.0.1 and bind it to listen on.
// Currently only listen on the default addresss.
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_len = sizeof(addr);
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (bind(socketFD, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
close(socketFD);
return nil;
}
return [EDOListenSocket listenSocketWithSocket:socketFD
connectedBlock:^(EDOSocket *socket, NSError *error) {
// dispatch the block to the user's queue
dispatch_async(queue, ^{
block(socket, nil);
});
}];
}
@end