/*
* Copyright (c) 2008-2024 Jonathan Schleifer <js@nil.im>
*
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version 3.0 only,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* version 3.0 for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* version 3.0 along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <errno.h>
#import "OFAsyncIPSocketConnector.h"
#import "OFData.h"
#ifdef OF_HAVE_SCTP
# import "OFSCTPSocket.h"
#endif
#import "OFTCPSocket.h"
#import "OFThread.h"
#import "OFTimer.h"
#import "OFConnectIPSocketFailedException.h"
#import "OFInvalidFormatException.h"
@implementation OFAsyncIPSocketConnector
- (instancetype)initWithSocket: (id)sock
host: (OFString *)host
port: (uint16_t)port
delegate: (id)delegate
handler: (id)handler
{
self = [super init];
@try {
_socket = [sock retain];
_host = [host copy];
_port = port;
_delegate = [delegate retain];
_handler = [handler copy];
} @catch (id e) {
[self release];
@throw e;
}
return self;
}
- (void)dealloc
{
[_socket release];
[_host release];
[_delegate release];
[_handler release];
[_exception release];
[_socketAddresses release];
[super dealloc];
}
- (void)didConnect
{
if (_exception == nil)
[_socket setCanBlock: true];
#ifdef OF_HAVE_BLOCKS
if (_handler != NULL) {
if ([_socket isKindOfClass: [OFTCPSocket class]])
((OFTCPSocketConnectedHandler)_handler)(_socket, _host,
_port, _exception);
# ifdef OF_HAVE_SCTP
else if ([_socket isKindOfClass: [OFSCTPSocket class]])
((OFSCTPSocketConnectedHandler)_handler)(_socket, _host,
_port, _exception);
# endif
else
OFEnsure(0);
} else {
#endif
if ([_delegate respondsToSelector:
@selector(socket:didConnectToHost:port:exception:)])
[_delegate socket: _socket
didConnectToHost: _host
port: _port
exception: _exception];
#ifdef OF_HAVE_BLOCKS
}
#endif
}
- (void)of_socketDidConnect: (id)sock exception: (id)exception
{
if (exception != nil) {
/*
* self might be retained only by the pending async requests,
* which we're about to cancel.
*/
[[self retain] autorelease];
[sock cancelAsyncRequests];
[sock of_closeSocket];
if (_socketAddressesIndex >= _socketAddresses.count) {
_exception = [exception retain];
[self didConnect];
} else {
/*
* We must not call it before returning, as otherwise
* the new socket would be removed from the queue upon
* return.
*/
OFRunLoop *runLoop = [OFRunLoop currentRunLoop];
SEL selector =
@selector(tryNextAddressWithRunLoopMode:);
OFTimer *timer = [OFTimer
timerWithTimeInterval: 0
target: self
selector: selector
object: runLoop.currentMode
repeats: false];
[runLoop addTimer: timer forMode: runLoop.currentMode];
}
return;
}
[self didConnect];
}
- (id)of_connectionFailedExceptionForErrNo: (int)errNo
{
return [OFConnectIPSocketFailedException exceptionWithHost: _host
port: _port
socket: _socket
errNo: errNo];
}
- (void)tryNextAddressWithRunLoopMode: (OFRunLoopMode)runLoopMode
{
OFSocketAddress address = *(const OFSocketAddress *)
[_socketAddresses itemAtIndex: _socketAddressesIndex++];
int errNo;
OFSocketAddressSetIPPort(&address, _port);
if (![_socket of_createSocketForAddress: &address errNo: &errNo]) {
if (_socketAddressesIndex >= _socketAddresses.count) {
_exception = [[OFConnectIPSocketFailedException alloc]
initWithHost: _host
port: _port
socket: _socket
errNo: errNo];
[self didConnect];
return;
}
[self tryNextAddressWithRunLoopMode: runLoopMode];
return;
}
#if defined(OF_NINTENDO_3DS) || defined(OF_WII)
/*
* On Wii and 3DS, connect() fails if non-blocking is enabled.
*
* Additionally, on Wii, there is no getsockopt(), so it would not be
* possible to get the error (or success) after connecting anyway.
*
* So for now, connecting is blocking on Wii and 3DS.
*
* FIXME: Use a different thread as a work around.
*/
[_socket setCanBlock: true];
#else
[_socket setCanBlock: false];
#endif
if (![_socket of_connectSocketToAddress: &address errNo: &errNo]) {
#if !defined(OF_NINTENDO_3DS) && !defined(OF_WII)
# ifdef OF_WINDOWS
if (errNo == EINPROGRESS || errNo == EWOULDBLOCK) {
# else
if (errNo == EINPROGRESS) {
# endif
[OFRunLoop of_addAsyncConnectForSocket: _socket
mode: runLoopMode
delegate: self];
return;
} else {
#endif
[_socket of_closeSocket];
if (_socketAddressesIndex >= _socketAddresses.count) {
_exception = [[OFConnectIPSocketFailedException
alloc] initWithHost: _host
port: _port
socket: _socket
errNo: errNo];
[self didConnect];
return;
}
[self tryNextAddressWithRunLoopMode: runLoopMode];
return;
#if !defined(OF_NINTENDO_3DS) && !defined(OF_WII)
}
#endif
}
#if defined(OF_NINTENDO_3DS) || defined(OF_WII)
[_socket setCanBlock: false];
#endif
[self didConnect];
}
- (void)resolver: (OFDNSResolver *)resolver
didResolveHost: (OFString *)host
addresses: (OFData *)addresses
exception: (id)exception
{
if (exception != nil) {
_exception = [exception retain];
[self didConnect];
return;
}
_socketAddresses = [addresses copy];
[self tryNextAddressWithRunLoopMode:
[OFRunLoop currentRunLoop].currentMode];
}
- (void)startWithRunLoopMode: (OFRunLoopMode)runLoopMode
{
@try {
OFSocketAddress address = OFSocketAddressParseIP(_host, _port);
_socketAddresses = [[OFData alloc]
initWithItems: &address
count: 1
itemSize: sizeof(address)];
[self tryNextAddressWithRunLoopMode: runLoopMode];
return;
} @catch (OFInvalidFormatException *e) {
}
[[OFThread DNSResolver]
asyncResolveAddressesForHost: _host
addressFamily: OFSocketAddressFamilyAny
runLoopMode: runLoopMode
delegate: self];
}
@end