@@ -1,7 +1,7 @@ /* - * Copyright (c) 2008-2021 Jonathan Schleifer + * Copyright (c) 2008-2022 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 @@ -26,13 +26,13 @@ #import "OFHTTPRequest.h" #import "OFHTTPResponse.h" #import "OFKernelEventObserver.h" #import "OFNumber.h" #import "OFRunLoop.h" -#import "OFSocket+Private.h" #import "OFString.h" #import "OFTCPSocket.h" +#import "OFTLSStream.h" #import "OFURL.h" #import "OFAlreadyConnectedException.h" #import "OFHTTPRequestFailedException.h" #import "OFInvalidArgumentException.h" @@ -49,11 +49,12 @@ #import "OFWriteFailedException.h" static const unsigned int defaultRedirects = 10; OF_DIRECT_MEMBERS -@interface OFHTTPClientRequestHandler: OFObject +@interface OFHTTPClientRequestHandler: OFObject { @public OFHTTPClient *_client; OFHTTPRequest *_request; unsigned int _redirects; @@ -72,32 +73,32 @@ OF_DIRECT_MEMBERS @interface OFHTTPClientRequestBodyStream: OFStream { OFHTTPClientRequestHandler *_handler; - OFTCPSocket *_socket; + OFStream *_stream; bool _chunked; unsigned long long _toWrite; bool _atEndOfStream; } - (instancetype)initWithHandler: (OFHTTPClientRequestHandler *)handler - socket: (OFTCPSocket *)sock; + stream: (OFStream *)stream; @end OF_DIRECT_MEMBERS @interface OFHTTPClientResponse: OFHTTPResponse { - OFTCPSocket *_socket; + OFStream *_stream; bool _hasContentLength, _chunked, _keepAlive; bool _atEndOfStream, _setAtEndOfStream; long long _toRead; } @property (nonatomic, setter=of_setKeepAlive:) bool of_keepAlive; -- (instancetype)initWithSocket: (OFTCPSocket *)sock; +- (instancetype)initWithStream: (OFStream *)stream; @end OF_DIRECT_MEMBERS @interface OFHTTPClientSyncPerformer: OFObject { @@ -293,20 +294,20 @@ didPerformRequest: _request response: nil exception: exception]; } -- (void)createResponseWithSocketOrThrow: (OFTCPSocket *)sock +- (void)createResponseWithStreamOrThrow: (OFStream *)stream { OFURL *URL = _request.URL; OFHTTPClientResponse *response; OFString *connectionHeader; bool keepAlive; OFString *location; id exception; - response = [[[OFHTTPClientResponse alloc] initWithSocket: sock] + response = [[[OFHTTPClientResponse alloc] initWithStream: stream] autorelease]; response.protocolVersionString = _version; response.statusCode = _status; response.headers = _serverHeaders; @@ -325,11 +326,11 @@ } if (keepAlive) { response.of_keepAlive = true; - _client->_socket = [sock retain]; + _client->_stream = [stream retain]; _client->_lastURL = [URL copy]; _client->_lastWasHEAD = (_request.method == OFHTTPRequestMethodHead); _client->_lastResponse = [response retain]; } @@ -429,14 +430,14 @@ withObject: response withObject: exception afterDelay: 0]; } -- (void)createResponseWithSocket: (OFTCPSocket *)sock +- (void)createResponseWithStream: (OFStream *)stream { @try { - [self createResponseWithSocketOrThrow: sock]; + [self createResponseWithStreamOrThrow: stream]; } @catch (id e) { [self raiseException: e]; } } @@ -471,11 +472,11 @@ _status = (short)status; return true; } -- (bool)handleServerHeader: (OFString *)line socket: (OFTCPSocket *)sock +- (bool)handleServerHeader: (OFString *)line stream: (OFStream *)stream { OFString *key, *value, *old; const char *lineC, *tmp; char *keyC; @@ -490,14 +491,14 @@ [_client->_delegate client: _client didReceiveHeaders: _serverHeaders statusCode: _status request: _request]; - sock.delegate = nil; + stream.delegate = nil; - [self performSelector: @selector(createResponseWithSocket:) - withObject: sock + [self performSelector: @selector(createResponseWithStream:) + withObject: stream afterDelay: 0]; return false; } @@ -532,11 +533,11 @@ [_serverHeaders setObject: value forKey: key]; return true; } -- (bool)stream: (OFStream *)sock +- (bool)stream: (OFStream *)stream didReadLine: (OFString *)line exception: (id)exception { bool ret; @@ -552,12 +553,11 @@ @try { if (_firstLine) { _firstLine = false; ret = [self handleFirstLine: line]; } else - ret = [self handleServerHeader: line - socket: (OFTCPSocket *)sock]; + ret = [self handleServerHeader: line stream: stream]; } @catch (id e) { [self raiseException: e]; ret = false; } @@ -594,12 +594,11 @@ if (chunked || [headers objectForKey: @"Content-Length"] != nil) { stream.delegate = nil; OFStream *requestBody = [[[OFHTTPClientRequestBodyStream alloc] - initWithHandler: self - socket: (OFTCPSocket *)stream] autorelease]; + initWithHandler: self stream: stream] autorelease]; if ([_client->_delegate respondsToSelector: @selector(client:wantsRequestBody:request:)]) [_client->_delegate client: _client wantsRequestBody: requestBody @@ -608,23 +607,23 @@ [stream asyncReadLine]; return nil; } -- (void)handleSocket: (OFTCPSocket *)sock +- (void)handleStream: (OFStream *)stream { /* * As a work around for a bug with split packets in lighttpd when using * HTTPS, we construct the complete request in a buffer string and then * send it all at once. * - * We do not use the socket's write buffer in case we need to resend + * We do not use the streams's write buffer in case we need to resend * the entire request (e.g. in case a keep-alive connection timed out). */ @try { - [sock asyncWriteString: constructRequestString(_request)]; + [stream asyncWriteString: constructRequestString(_request)]; } @catch (id e) { [self raiseException: e]; return; } } @@ -632,58 +631,95 @@ - (void)socket: (OFTCPSocket *)sock didConnectToHost: (OFString *)host port: (uint16_t)port exception: (id)exception { - sock.delegate = self; + if (exception != nil) { + [self raiseException: exception]; + return; + } + sock.canBlock = false; + + if ([_client->_delegate respondsToSelector: + @selector(client:didCreateTCPSocket:request:)]) + [_client->_delegate client: _client + didCreateTCPSocket: sock + request: _request]; + + if ([_request.URL.scheme caseInsensitiveCompare: @"https"] == + OFOrderedSame) { + OFTLSStream *stream; + @try { + stream = [OFTLSStream streamWithStream: sock]; + } @catch (OFNotImplementedException *e) { + [self raiseException: + [OFUnsupportedProtocolException + exceptionWithURL: _request.URL]]; + return; + } + + if ([_client->_delegate respondsToSelector: + @selector(client:didCreateTLSStream:request:)]) + [_client->_delegate client: _client + didCreateTLSStream: stream + request: _request]; + + stream.delegate = self; + [stream asyncPerformClientHandshakeWithHost: _request.URL.host]; + } else { + sock.delegate = self; + [self performSelector: @selector(handleStream:) + withObject: sock + afterDelay: 0]; + } +} + +- (void)stream: (OFTLSStream *)stream + didPerformClientHandshakeWithHost: (OFString *)host + exception: (id)exception +{ if (exception != nil) { [self raiseException: exception]; return; } - if ([_client->_delegate respondsToSelector: - @selector(client:didCreateSocket:request:)]) - [_client->_delegate client: _client - didCreateSocket: sock - request: _request]; - - [self performSelector: @selector(handleSocket:) - withObject: sock + [self performSelector: @selector(handleStream:) + withObject: stream afterDelay: 0]; } - (void)start { OFURL *URL = _request.URL; - OFTCPSocket *sock; + OFStream *stream; /* Can we reuse the last socket? */ - if (_client->_socket != nil && !_client->_socket.atEndOfStream && + if (_client->_stream != nil && !_client->_stream.atEndOfStream && [_client->_lastURL.scheme isEqual: URL.scheme] && [_client->_lastURL.host isEqual: URL.host] && (_client->_lastURL.port == URL.port || [_client->_lastURL.port isEqual: URL.port]) && (_client->_lastWasHEAD || _client->_lastResponse.atEndOfStream)) { /* - * Set _socket to nil, so that in case of an error it won't be - * reused. If everything is successful, we set _socket again + * Set _stream to nil, so that in case of an error it won't be + * reused. If everything is successful, we set _stream again * at the end. */ - sock = [_client->_socket autorelease]; - _client->_socket = nil; + stream = [_client->_stream autorelease]; + _client->_stream = nil; [_client->_lastURL release]; _client->_lastURL = nil; [_client->_lastResponse release]; _client->_lastResponse = nil; - sock.delegate = self; + stream.delegate = self; - [self performSelector: @selector(handleSocket:) - withObject: sock + [self performSelector: @selector(handleStream:) + withObject: stream afterDelay: 0]; } else [self closeAndReconnect]; } @@ -694,23 +730,18 @@ OFTCPSocket *sock; uint16_t port; OFNumber *URLPort; [_client close]; + + sock = [OFTCPSocket socket]; if ([URL.scheme caseInsensitiveCompare: @"https"] == - OFOrderedSame) { - if (OFTLSSocketClass == Nil) - @throw [OFUnsupportedProtocolException - exceptionWithURL: URL]; - - sock = [[[OFTLSSocketClass alloc] init] autorelease]; + OFOrderedSame) port = 443; - } else { - sock = [OFTCPSocket socket]; + else port = 80; - } URLPort = URL.port; if (URLPort != nil) port = URLPort.unsignedShortValue; @@ -722,20 +753,20 @@ } @end @implementation OFHTTPClientRequestBodyStream - (instancetype)initWithHandler: (OFHTTPClientRequestHandler *)handler - socket: (OFTCPSocket *)sock + stream: (OFStream *)stream { self = [super init]; @try { OFDictionary OF_GENERIC(OFString *, OFString *) *headers; OFString *transferEncoding, *contentLengthString; _handler = [handler retain]; - _socket = [sock retain]; + _stream = [stream retain]; headers = _handler->_request.headers; transferEncoding = [headers objectForKey: @"Transfer-Encoding"]; _chunked = [transferEncoding isEqual: @"chunked"]; @@ -757,25 +788,23 @@ return self; } - (void)dealloc { - if (_socket != nil) + if (_stream != nil) [self close]; [_handler release]; [super dealloc]; } -- (size_t)lowlevelWriteBuffer: (const void *)buffer - length: (size_t)length +- (size_t)lowlevelWriteBuffer: (const void *)buffer length: (size_t)length { - size_t requestedLength = length; - size_t ret; + /* TODO: Use non-blocking writes */ - if (_socket == nil) + if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; /* * We must not send a chunk of size 0, as that would end the body. We * always ignore writing 0 bytes to still allow writing 0 bytes after @@ -783,90 +812,80 @@ */ if (length == 0) return 0; if (_atEndOfStream) - @throw [OFWriteFailedException - exceptionWithObject: self - requestedLength: requestedLength - bytesWritten: 0 - errNo: 0]; + @throw [OFWriteFailedException exceptionWithObject: self + requestedLength: length + bytesWritten: 0 + errNo: ENOTCONN]; if (_chunked) - [_socket writeFormat: @"%zX\r\n", length]; + [_stream writeFormat: @"%zX\r\n", length]; else if (length > _toWrite) - length = (size_t)_toWrite; + @throw [OFOutOfRangeException exception]; - ret = [_socket writeBuffer: buffer length: length]; + [_stream writeBuffer: buffer length: length]; if (_chunked) - [_socket writeString: @"\r\n"]; - - if (ret > length) - @throw [OFOutOfRangeException exception]; + [_stream writeString: @"\r\n"]; if (!_chunked) { - _toWrite -= ret; + _toWrite -= length; if (_toWrite == 0) _atEndOfStream = true; } - if (requestedLength > length) - @throw [OFWriteFailedException - exceptionWithObject: self - requestedLength: requestedLength - bytesWritten: ret - errNo: 0]; - - return ret; + return length; } - (bool)lowlevelIsAtEndOfStream { return _atEndOfStream; } - (void)close { - if (_socket == nil) + if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (_chunked) - [_socket writeString: @"0\r\n\r\n"]; + [_stream writeString: @"0\r\n\r\n"]; else if (_toWrite > 0) @throw [OFTruncatedDataException exception]; - _socket.delegate = _handler; - [_socket asyncReadLine]; + _stream.delegate = _handler; + [_stream asyncReadLine]; - [_socket release]; - _socket = nil; + [_stream release]; + _stream = nil; [super close]; } - (int)fileDescriptorForWriting { - return _socket.fileDescriptorForWriting; + return ((OFStream *)_stream) + .fileDescriptorForWriting; } @end @implementation OFHTTPClientResponse @synthesize of_keepAlive = _keepAlive; -- (instancetype)initWithSocket: (OFTCPSocket *)sock +- (instancetype)initWithStream: (OFStream *)stream { self = [super init]; - _socket = [sock retain]; + _stream = [stream retain]; return self; } - (void)dealloc { - if (_socket != nil) + if (_stream != nil) [self close]; [super dealloc]; } @@ -900,30 +919,30 @@ } } - (size_t)lowlevelReadIntoBuffer: (void *)buffer length: (size_t)length { - if (_socket == nil) + if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (_atEndOfStream) return 0; if (!_hasContentLength && !_chunked) - return [_socket readIntoBuffer: buffer length: length]; + return [_stream readIntoBuffer: buffer length: length]; - if (_socket.atEndOfStream) + if (_stream.atEndOfStream) @throw [OFTruncatedDataException exception]; /* Content-Length */ if (!_chunked) { size_t ret; if (length > (unsigned long long)_toRead) length = (size_t)_toRead; - ret = [_socket readIntoBuffer: buffer length: length]; + ret = [_stream readIntoBuffer: buffer length: length]; if (ret > length) @throw [OFOutOfRangeException exception]; _toRead -= ret; @@ -935,11 +954,11 @@ /* Chunked */ if (_toRead == -2) { char tmp[2]; - switch ([_socket readIntoBuffer: tmp length: 2]) { + switch ([_stream readIntoBuffer: tmp length: 2]) { case 2: _toRead++; if (tmp[1] != '\n') @throw [OFInvalidServerReplyException exception]; @@ -955,11 +974,11 @@ return 0; } else if (_toRead == -1) { char tmp; - if ([_socket readIntoBuffer: &tmp length: 1] == 1) { + if ([_stream readIntoBuffer: &tmp length: 1] == 1) { _toRead++; if (tmp != '\n') @throw [OFInvalidServerReplyException exception]; } @@ -970,11 +989,11 @@ return 0; } else if (_toRead > 0) { if (length > (unsigned long long)_toRead) length = (size_t)_toRead; - length = [_socket readIntoBuffer: buffer length: length]; + length = [_stream readIntoBuffer: buffer length: length]; _toRead -= length; if (_toRead == 0) _toRead = -2; @@ -983,11 +1002,11 @@ void *pool = objc_autoreleasePoolPush(); OFString *line; size_t pos; @try { - line = [_socket tryReadLine]; + line = [_stream tryReadLine]; } @catch (OFInvalidEncodingException *e) { @throw [OFInvalidServerReplyException exception]; } if (line == nil) @@ -997,14 +1016,14 @@ if (pos != OFNotFound) line = [line substringToIndex: pos]; if (line.length < 1) { /* - * We have read the empty string because the socket is + * We have read the empty string because the stream is * at end of stream. */ - if (_socket.atEndOfStream && pos == OFNotFound) + if (_stream.atEndOfStream && pos == OFNotFound) @throw [OFTruncatedDataException exception]; else @throw [OFInvalidServerReplyException exception]; } @@ -1035,41 +1054,42 @@ - (bool)lowlevelIsAtEndOfStream { if (_atEndOfStream) return true; - if (_socket == nil) + if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (!_hasContentLength && !_chunked) - return _socket.atEndOfStream; + return _stream.atEndOfStream; return _atEndOfStream; } - (int)fileDescriptorForReading { - if (_socket == nil) + if (_stream == nil) return -1; - return _socket.fileDescriptorForReading; + return ((OFStream *)_stream) + .fileDescriptorForReading; } - (bool)hasDataInReadBuffer { - return (super.hasDataInReadBuffer || _socket.hasDataInReadBuffer); + return (super.hasDataInReadBuffer || _stream.hasDataInReadBuffer); } - (void)close { - if (_socket == nil) + if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; _atEndOfStream = false; - [_socket release]; - _socket = nil; + [_stream release]; + _stream = nil; [super close]; } @end @@ -1132,19 +1152,30 @@ didPerformRequest: request response: response exception: nil]; } -- (void)client: (OFHTTPClient *)client - didCreateSocket: (OFTCPSocket *)sock - request: (OFHTTPRequest *)request +- (void)client: (OFHTTPClient *)client + didCreateTCPSocket: (OFTCPSocket *)TCPSocket + request: (OFHTTPRequest *)request +{ + if ([_delegate respondsToSelector: + @selector(client:didCreateTCPSocket:request:)]) + [_delegate client: client + didCreateTCPSocket: TCPSocket + request: request]; +} + +- (void)client: (OFHTTPClient *)client + didCreateTLSStream: (OFTLSStream *)TLSStream + request: (OFHTTPRequest *)request { if ([_delegate respondsToSelector: - @selector(client:didCreateSocket:request:)]) - [_delegate client: client - didCreateSocket: sock - request: request]; + @selector(client:didCreateTLSStream:request:)]) + [_delegate client: client + didCreateTLSStream: TLSStream + request: request]; } - (void)client: (OFHTTPClient *)client wantsRequestBody: (OFStream *)body request: (OFHTTPRequest *)request @@ -1255,15 +1286,15 @@ objc_autoreleasePoolPop(pool); } - (void)close { - [_socket release]; - _socket = nil; + [_stream release]; + _stream = nil; [_lastURL release]; _lastURL = nil; [_lastResponse release]; _lastResponse = nil; } @end