Index: configure.ac ================================================================== --- configure.ac +++ configure.ac @@ -1331,11 +1331,11 @@ AC_CHECK_HEADERS([arpa/inet.h netdb.h]) AC_CHECK_HEADER(netipx/ipx.h, [ AC_DEFINE(OF_HAVE_NETIPX_IPX_H, 1, [Whether we have netipx/ipx.h]) ]) - AC_CHECK_HEADERS(sys/un.h, [ + AC_CHECK_HEADER(sys/un.h, [ AC_DEFINE(OF_HAVE_SYS_UN_H, 1, [Whether we have sys/un.h]) ]) AC_CHECK_MEMBER([struct sockaddr_in6.sin6_addr], [ AC_EGREP_CPP(egrep_cpp_yes, [ @@ -1533,10 +1533,24 @@ AC_SUBST(OF_SELECT_KERNEL_EVENT_OBSERVER_M, "OFSelectKernelEventObserver.m") ]) ;; esac + + AC_CHECK_HEADERS(Security/SecureTransport.h, [ + old_LIBS="$LIBS" + LIBS="-framework Security -framework Foundation $LIBS" + + AC_CHECK_FUNC(SSLHandshake, [ + AC_DEFINE(HAVE_SECURE_TRANSPORT, 1, + [Whether we have Secure Transport]) + AC_SUBST(OF_SECURE_TRANSPORT_TLS_STREAM_M, + "OFSecureTransportTLSStream.m") + ], [ + LIBS="$old_LIBS" + ]) + ]) AS_IF([test x"$enable_threads" != x"no"], [ AC_SUBST(OF_HTTP_CLIENT_TESTS_M, "OFHTTPClientTests.m") ]) Index: extra.mk.in ================================================================== --- extra.mk.in +++ extra.mk.in @@ -46,10 +46,11 @@ OF_BLOCK_TESTS_M = @OF_BLOCK_TESTS_M@ OF_EPOLL_KERNEL_EVENT_OBSERVER_M = @OF_EPOLL_KERNEL_EVENT_OBSERVER_M@ OF_HTTP_CLIENT_TESTS_M = @OF_HTTP_CLIENT_TESTS_M@ OF_KQUEUE_KERNEL_EVENT_OBSERVER_M = @OF_KQUEUE_KERNEL_EVENT_OBSERVER_M@ OF_POLL_KERNEL_EVENT_OBSERVER_M = @OF_POLL_KERNEL_EVENT_OBSERVER_M@ +OF_SECURE_TRANSPORT_TLS_STREAM_M = @OF_SECURE_TRANSPORT_TLS_STREAM_M@ OF_SELECT_KERNEL_EVENT_OBSERVER_M = @OF_SELECT_KERNEL_EVENT_OBSERVER_M@ OF_SUBPROCESS_M = @OF_SUBPROCESS_M@ REEXPORT_RUNTIME = @REEXPORT_RUNTIME@ REEXPORT_RUNTIME_FRAMEWORK = @REEXPORT_RUNTIME_FRAMEWORK@ RUNTIME = @RUNTIME@ Index: src/Makefile ================================================================== --- src/Makefile +++ src/Makefile @@ -214,10 +214,11 @@ OFHostAddressResolver.m \ OFIPSocketAsyncConnector.m \ OFKernelEventObserver.m \ ${OF_KQUEUE_KERNEL_EVENT_OBSERVER_M} \ ${OF_POLL_KERNEL_EVENT_OBSERVER_M} \ + ${OF_SECURE_TRANSPORT_TLS_STREAM_M} \ ${OF_SELECT_KERNEL_EVENT_OBSERVER_M} \ OFTCPSocketSOCKS5Connector.m OBJS_EXTRA = exceptions/exceptions.a \ encodings/encodings.a \ Index: src/OFIPSocketAsyncConnector.m ================================================================== --- src/OFIPSocketAsyncConnector.m +++ src/OFIPSocketAsyncConnector.m @@ -74,11 +74,11 @@ OFEnsure(0); } else { #endif if ([_delegate respondsToSelector: @selector(socket:didConnectToHost:port:exception:)]) - [_delegate socket: _socket + [_delegate socket: _socket didConnectToHost: _host port: _port exception: _exception]; #ifdef OF_HAVE_BLOCKS } ADDED src/OFSecureTransportTLSStream.h Index: src/OFSecureTransportTLSStream.h ================================================================== --- src/OFSecureTransportTLSStream.h +++ src/OFSecureTransportTLSStream.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2008-2021 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#import "OFTLSStream.h" + +OF_ASSUME_NONNULL_BEGIN + +@interface OFSecureTransportTLSStream: OFTLSStream +{ + struct SSLContext *_context; + OFString *_host; +} +@end + +OF_ASSUME_NONNULL_END ADDED src/OFSecureTransportTLSStream.m Index: src/OFSecureTransportTLSStream.m ================================================================== --- src/OFSecureTransportTLSStream.m +++ src/OFSecureTransportTLSStream.m @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2008-2021 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#include "config.h" + +#import "OFSecureTransportTLSStream.h" + +#include + +#import "OFAlreadyConnectedException.h" +#import "OFNotOpenException.h" +#import "OFReadFailedException.h" +#import "OFTLSHandshakeFailedException.h" +#import "OFWriteFailedException.h" + +static OSStatus +readFunc(SSLConnectionRef connection, void *data, size_t *dataLength) +{ + bool incomplete; + size_t length = [((OFTLSStream *)connection).wrappedStream + readIntoBuffer: data + length: *dataLength]; + + incomplete = (length < *dataLength); + *dataLength = length; + + return (incomplete ? errSSLWouldBlock : noErr); +} + +static OSStatus +writeFunc(SSLConnectionRef connection, const void *data, size_t *dataLength) +{ + @try { + [((OFTLSStream *)connection).wrappedStream + writeBuffer: data + length: *dataLength]; + } @catch (OFWriteFailedException *e) { + *dataLength = e.bytesWritten; + + if (e.errNo == EWOULDBLOCK) + return errSSLWouldBlock; + + @throw e; + } + + return noErr; +} + +/* + * Apple deprecated Secure Transport without providing a replacement that can + * work with any socket. On top of that, their replacement, Network.framework, + * doesn't support STARTTLS at all. + */ +#pragma GCC diagnostic ignored "-Wdeprecated" + +@implementation OFSecureTransportTLSStream +- (instancetype)initWithStream: (OFStream *)stream +{ + self = [super initWithStream: stream]; + + @try { + _wrappedStream.delegate = self; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + if (_context != NULL) + [self close]; + + [_host release]; + + [super dealloc]; +} + +- (void)close +{ + if (_context == NULL) + @throw [OFNotOpenException exceptionWithObject: self]; + + SSLClose(_context); + CFRelease(_context); + _context = NULL; + + [super close]; +} + +- (size_t)lowlevelReadIntoBuffer: (void *)buffer length: (size_t)length +{ + OSStatus status; + size_t ret; + + if (_context == NULL) + @throw [OFNotOpenException exceptionWithObject: self]; + + status = SSLRead(_context, buffer, length, &ret); + if (status != noErr && status != errSSLWouldBlock) + /* FIXME: Translate status to errNo */ + @throw [OFReadFailedException exceptionWithObject: self + requestedLength: length + errNo: 0]; + + return ret; +} + +- (size_t)lowlevelWriteBuffer: (const void *)buffer length: (size_t)length +{ + OSStatus status; + size_t ret = 0; + + if (_context == NULL) + @throw [OFNotOpenException exceptionWithObject: self]; + + status = SSLWrite(_context, buffer, length, &ret); + if (status != noErr && status != errSSLWouldBlock) + /* FIXME: Translate status to errNo */ + @throw [OFWriteFailedException exceptionWithObject: self + requestedLength: length + bytesWritten: ret + errNo: 0]; + + return ret; +} + +- (void)asyncPerformClientHandshakeWithHost: (OFString *)host + runLoopMode: (OFRunLoopMode)runLoopMode +{ + static const OFTLSStreamErrorCode initFailedErrorCode = + OFTLSStreamErrorCodeInitializationFailed; + id exception = nil; + OSStatus status; + + if (_context != NULL) + @throw [OFAlreadyConnectedException exceptionWithSocket: self]; + + if ((_context = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, + kSSLStreamType)) == NULL) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + + if (SSLSetIOFuncs(_context, readFunc, writeFunc) != noErr || + SSLSetConnection(_context, self) != noErr) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + + if (_verifiesCertificates) + if (SSLSetPeerDomainName(_context, + host.UTF8String, host.UTF8StringLength) != noErr) + @throw [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: initFailedErrorCode]; + + status = SSLHandshake(_context); + + if (status == errSSLWouldBlock) { + /* + * Theoretically it is possible we block because Secure + * Transport cannot write without blocking. But unfortunately, + * Secure Transport does not tell us whether it's blocked on + * reading or writing. Waiting for the stream to be either + * readable or writable doesn't work either, as the stream is + * almost always at least ready for one of the two. + */ + [_wrappedStream asyncReadIntoBuffer: (void *)"" + length: 0 + runLoopMode: runLoopMode]; + [_delegate retain]; + _host = [host copy]; + return; + } + + if (status != noErr) + /* FIXME: Map to better errors */ + exception = [OFTLSHandshakeFailedException + exceptionWithStream: self + host: host + errorCode: OFTLSStreamErrorCodeUnknown]; + + if ([_delegate respondsToSelector: + @selector(stream:didPerformClientHandshakeWithHost:exception:)]) + [_delegate stream: self + didPerformClientHandshakeWithHost: host + exception: exception]; +} + +- (bool)stream: (OFStream *)stream + didReadIntoBuffer: (void *)buffer + length: (size_t)length + exception: (nullable id)exception +{ + if (exception == nil) { + OSStatus status = SSLHandshake(_context); + + if (status == errSSLWouldBlock) + return true; + + if (status != noErr) + exception = [OFTLSHandshakeFailedException + exceptionWithStream: self + host: _host + errorCode: OFTLSStreamErrorCodeUnknown]; + } + + if ([_delegate respondsToSelector: + @selector(stream:didPerformClientHandshakeWithHost:exception:)]) + [_delegate stream: self + didPerformClientHandshakeWithHost: _host + exception: exception]; + + [_host release]; + _host = nil; + + [_delegate release]; + + return false; +} +@end Index: src/OFTLSStream.h ================================================================== --- src/OFTLSStream.h +++ src/OFTLSStream.h @@ -20,10 +20,20 @@ /** @file */ @class OFTLSStream; +/** + * @brief An enum representing an error of an OFTLSStream. + */ +typedef enum { + /** @brief An unknown error. */ + OFTLSStreamErrorCodeUnknown, + /** @brief Initialization of the TLS context failed. */ + OFTLSStreamErrorCodeInitializationFailed +} OFTLSStreamErrorCode; + /** * @protocol OFTLSStreamDelegate OFTLSStream.h ObjFW/OFTLSStream.h * * A delegate for OFTLSStream. */ @@ -108,11 +118,12 @@ * @param stream The stream to use as underlying stream. Must not be closed * before the TLS stream is closed. * @return An initialized TLS stream */ - (instancetype)initWithStream: (OFStream *)stream; + OFReadyForWritingObserving> *)stream + OF_DESIGNATED_INITIALIZER; /** * @brief Asynchronously performs the TLS client handshake for the specified * host and calls the delegate afterwards. * @@ -147,10 +158,19 @@ * This can be set to a class that is always used for OFTLSStream. This is * useful to either force a specific implementation or use one that ObjFW does * not know about. */ extern Class OFTLSStreamImplementation; + +/** + * @brief Returns a string description for the TLS stream error code. + * + * @param errorCode The error code to return the description for + * @return A string description for the TLS stream error code + */ +extern OFString *OFTLSStreamErrorCodeDescription( + OFTLSStreamErrorCode errorCode); #ifdef __cplusplus } #endif OF_ASSUME_NONNULL_END Index: src/OFTLSStream.m ================================================================== --- src/OFTLSStream.m +++ src/OFTLSStream.m @@ -15,24 +15,38 @@ #include "config.h" #import "OFTLSStream.h" #import "OFDate.h" +#ifdef HAVE_SECURE_TRANSPORT +# import "OFSecureTransportTLSStream.h" +#endif #import "OFNotImplementedException.h" -Class OFTLSStreamImplementation = Nil; -static const OFRunLoopMode handshakeRunLoopMode = - @"OFTLSStreamHandshakeRunLoopMode"; - @interface OFTLSStreamHandshakeDelegate: OFObject { @public bool _done; id _exception; } @end + +Class OFTLSStreamImplementation = Nil; +static const OFRunLoopMode handshakeRunLoopMode = + @"OFTLSStreamHandshakeRunLoopMode"; + +OFString * +OFTLSStreamErrorCodeDescription(OFTLSStreamErrorCode errorCode) +{ + switch (errorCode) { + case OFTLSStreamErrorCodeInitializationFailed: + return @"Initialization of TLS context failed"; + default: + return @"Unknown error"; + } +} @implementation OFTLSStreamHandshakeDelegate - (void)dealloc { [_exception release]; @@ -58,12 +72,16 @@ { if (self == [OFTLSStream class]) { if (OFTLSStreamImplementation != Nil) return [OFTLSStreamImplementation alloc]; +#ifdef HAVE_SECURE_TRANSPORT + return [OFSecureTransportTLSStream alloc]; +#else @throw [OFNotImplementedException exceptionWithSelector: _cmd object: self]; +#endif } return [super alloc]; } Index: src/ObjFW.h ================================================================== --- src/ObjFW.h +++ src/ObjFW.h @@ -229,10 +229,13 @@ #ifdef OF_HAVE_THREADS # import "OFThreadJoinFailedException.h" # import "OFThreadStartFailedException.h" # import "OFThreadStillRunningException.h" #endif +#ifdef OF_HAVE_SOCKETS +# import "OFTLSHandshakeFailedException.h" +#endif #import "OFTruncatedDataException.h" #import "OFUnboundNamespaceException.h" #import "OFUnboundPrefixException.h" #import "OFUndefinedKeyException.h" #import "OFUnknownXMLEntityException.h" Index: src/exceptions/Makefile ================================================================== --- src/exceptions/Makefile +++ src/exceptions/Makefile @@ -59,11 +59,12 @@ OFConnectionFailedException.m \ OFDNSQueryFailedException.m \ OFHTTPRequestFailedException.m \ OFListenFailedException.m \ OFObserveFailedException.m \ - OFResolveHostFailedException.m + OFResolveHostFailedException.m \ + OFTLSHandshakeFailedException.m SRCS_THREADS = OFConditionBroadcastFailedException.m \ OFConditionSignalFailedException.m \ OFConditionStillWaitingException.m \ OFConditionWaitFailedException.m \ OFThreadJoinFailedException.m \ Index: src/exceptions/OFListenFailedException.h ================================================================== --- src/exceptions/OFListenFailedException.h +++ src/exceptions/OFListenFailedException.h @@ -63,11 +63,11 @@ errNo: (int)errNo; - (instancetype)init OF_UNAVAILABLE; /** - * @brief Initializes an already allocated listen failed exception + * @brief Initializes an already allocated listen failed exception. * * @param socket The socket which failed to listen * @param backlog The requested size of the back log * @param errNo The errno of the error that occurred * @return An initialized listen failed exception Index: src/exceptions/OFReadFailedException.m ================================================================== --- src/exceptions/OFReadFailedException.m +++ src/exceptions/OFReadFailedException.m @@ -19,10 +19,15 @@ #import "OFString.h" @implementation OFReadFailedException - (OFString *)description { - return [OFString stringWithFormat: - @"Failed to read %zu bytes from an object of type %@: %@", - _requestedLength, [_object class], OFStrError(_errNo)]; + if (_errNo != 0) + return [OFString stringWithFormat: + @"Failed to read %zu bytes from an object of type %@: %@", + _requestedLength, [_object class], OFStrError(_errNo)]; + else + return [OFString stringWithFormat: + @"Failed to read %zu bytes from an object of type %@", + _requestedLength, [_object class]]; } @end Index: src/exceptions/OFReadOrWriteFailedException.m ================================================================== --- src/exceptions/OFReadOrWriteFailedException.m +++ src/exceptions/OFReadOrWriteFailedException.m @@ -61,11 +61,17 @@ [super dealloc]; } - (OFString *)description { - return [OFString stringWithFormat: - @"Failed to read or write %zu bytes from / to an object of type " - @"%@: %@", - _requestedLength, [_object class], OFStrError(_errNo)]; + if (_errNo != 0) + return [OFString stringWithFormat: + @"Failed to read or write %zu bytes from / to an object of " + @"type %@: %@", + _requestedLength, [_object class], OFStrError(_errNo)]; + else + return [OFString stringWithFormat: + @"Failed to read or write %zu bytes from / to an object of " + @"type %@", + _requestedLength, [_object class]]; } @end ADDED src/exceptions/OFTLSHandshakeFailedException.h Index: src/exceptions/OFTLSHandshakeFailedException.h ================================================================== --- src/exceptions/OFTLSHandshakeFailedException.h +++ src/exceptions/OFTLSHandshakeFailedException.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2008-2021 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#import "OFException.h" + +#ifndef OF_HAVE_SOCKETS +# error No sockets available! +#endif + +#import "OFTLSStream.h" + +OF_ASSUME_NONNULL_BEGIN + +/** + * @class OFTLSHandshakeFailedException \ + * OFTLSHandshakeFailedException.h ObjFW/OFTLSHandshakeFailedException.h + * + * @brief An exception indicating that a TLS handshake. + */ +@interface OFTLSHandshakeFailedException: OFException +{ + OFTLSStream *_stream; + OFString *_Nullable _host; + OFTLSStreamErrorCode _errorCode; +} + +/** + * @brief The TLS stream which failed the handshake. + */ +@property (readonly, nonatomic) OFTLSStream *stream; + +/** + * @brief The host for the handshake. + */ +@property OF_NULLABLE_PROPERTY (readonly, nonatomic) OFString *host; + +/** + * @brief The error code from the TLS stream. + */ +@property (readonly, nonatomic) OFTLSStreamErrorCode errorCode; + ++ (instancetype)exception OF_UNAVAILABLE; + +/** + * @brief Creates a new, autoreleased TLS handshake failed exception. + * + * @param stream The TLS stream which failed the handshake + * @param host The host for the handshake + * @param errorCode The error code from the TLS stream + * @return A new, autoreleased TLS handshake failed exception + */ ++ (instancetype)exceptionWithStream: (OFTLSStream *)stream + host: (nullable OFString *)host + errorCode: (OFTLSStreamErrorCode)errorCode; + +- (instancetype)init OF_UNAVAILABLE; + +/** + * @brief Initializes an already allocated TLS handshake failed exception. + * + * @param stream The TLS stream which failed the handshake + * @param host The host for the handshake + * @param errorCode The error code from the TLS stream + * @return An initialized TLS handshake failed exception + */ +- (instancetype)initWithStream: (OFTLSStream *)stream + host: (nullable OFString *)host + errorCode: (OFTLSStreamErrorCode)errorCode + OF_DESIGNATED_INITIALIZER; +@end + +OF_ASSUME_NONNULL_END ADDED src/exceptions/OFTLSHandshakeFailedException.m Index: src/exceptions/OFTLSHandshakeFailedException.m ================================================================== --- src/exceptions/OFTLSHandshakeFailedException.m +++ src/exceptions/OFTLSHandshakeFailedException.m @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2008-2021 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#include "config.h" + +#import "OFTLSHandshakeFailedException.h" +#import "OFString.h" + +@implementation OFTLSHandshakeFailedException +@synthesize stream = _stream, host = _host, errorCode = _errorCode; + ++ (instancetype)exception +{ + OF_UNRECOGNIZED_SELECTOR +} + ++ (instancetype)exceptionWithStream: (OFTLSStream *)stream + host: (OFString *)host + errorCode: (OFTLSStreamErrorCode)errorCode +{ + return [[[self alloc] initWithStream: stream + host: host + errorCode: errorCode] autorelease]; +} + +- (instancetype)init +{ + OF_INVALID_INIT_METHOD +} + +- (instancetype)initWithStream: (OFTLSStream *)stream + host: (OFString *)host + errorCode: (OFTLSStreamErrorCode)errorCode +{ + self = [super init]; + + @try { + _stream = [stream retain]; + _host = [host copy]; + _errorCode = errorCode; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + [_stream release]; + [_host release]; + + [super dealloc]; +} + +- (OFString *)description +{ + if (_host != nil) + return [OFString stringWithFormat: + @"TLS handshake in class %@ with host %@ failed: %@", + _stream.class, _host, + OFTLSStreamErrorCodeDescription(_errorCode)]; + else + return [OFString stringWithFormat: + @"TLS handshake in class %@ failed: %@", + _stream.class, OFTLSStreamErrorCodeDescription(_errorCode)]; +} +@end Index: src/exceptions/OFWriteFailedException.m ================================================================== --- src/exceptions/OFWriteFailedException.m +++ src/exceptions/OFWriteFailedException.m @@ -62,12 +62,18 @@ return self; } - (OFString *)description { - return [OFString stringWithFormat: - @"Failed to write %zu bytes (after %zu bytes written) to an " - @"object of type %@: %@", - _requestedLength, _bytesWritten, [_object class], - OFStrError(_errNo)]; + if (_errNo != 0) + return [OFString stringWithFormat: + @"Failed to write %zu bytes (after %zu bytes written) to " + @"an object of type %@: %@", + _requestedLength, _bytesWritten, [_object class], + OFStrError(_errNo)]; + else + return [OFString stringWithFormat: + @"Failed to write %zu bytes (after %zu bytes written) to " + @"an object of type %@", + _requestedLength, _bytesWritten, [_object class]]; } @end