Index: src/OFData.m ================================================================== --- src/OFData.m +++ src/OFData.m @@ -26,22 +26,14 @@ #ifdef OF_HAVE_FILES # import "OFFile.h" # import "OFFileManager.h" #endif #import "OFURL.h" -#ifdef OF_HAVE_SOCKETS -# import "OFHTTPClient.h" -# import "OFHTTPRequest.h" -# import "OFHTTPResponse.h" -#endif #import "OFDictionary.h" #import "OFXMLElement.h" #import "OFSystemInfo.h" -#ifdef OF_HAVE_SOCKETS -# import "OFHTTPRequestFailedException.h" -#endif #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" #import "OFInvalidServerReplyException.h" #import "OFOutOfMemoryException.h" #import "OFOutOfRangeException.h" @@ -252,79 +244,10 @@ # ifdef OF_HAVE_FILES if ([scheme isEqual: @"file"]) self = [self initWithContentsOfFile: [URL path]]; else -# endif -# ifdef OF_HAVE_SOCKETS - if ([scheme isEqual: @"http"] || [scheme isEqual: @"https"]) { - bool mutable = [self isKindOfClass: [OFMutableData class]]; - - if (!mutable) { - [self release]; - self = [OFMutableData alloc]; - } - - self = [(OFMutableData *)self init]; - - @try { - OFHTTPClient *client = [OFHTTPClient client]; - OFHTTPRequest *request = [OFHTTPRequest - requestWithURL: URL]; - OFHTTPResponse *response = [client - performRequest: request]; - size_t pageSize; - char *buffer; - OFDictionary *headers; - OFString *contentLengthString; - - if ([response statusCode] != 200) - @throw [OFHTTPRequestFailedException - exceptionWithRequest: request - response: response]; - - pageSize = [OFSystemInfo pageSize]; - buffer = [self allocMemoryWithSize: pageSize]; - - @try { - while (![response isAtEndOfStream]) { - size_t length; - - length = [response - readIntoBuffer: buffer - length: pageSize]; - [(OFMutableData *)self - addItems: buffer - count: length]; - } - } @finally { - [self freeMemory: buffer]; - } - - headers = [response headers]; - if ((contentLengthString = - [headers objectForKey: @"Content-Length"]) != nil) { - intmax_t contentLength = - [contentLengthString decimalValue]; - - if (contentLength < 0) - @throw [OFInvalidServerReplyException - exception]; - - if ((uintmax_t)[self count] != - (uintmax_t)contentLength) - @throw [OFTruncatedDataException - exception]; - } - } @catch (id e) { - [self release]; - @throw e; - } - - if (!mutable) - [(OFMutableData *)self makeImmutable]; - } else # endif @throw [OFUnsupportedProtocolException exceptionWithURL: URL]; objc_autoreleasePoolPop(pool); Index: src/OFHTTPClient.h ================================================================== --- src/OFHTTPClient.h +++ src/OFHTTPClient.h @@ -20,23 +20,47 @@ # error No sockets available! #endif OF_ASSUME_NONNULL_BEGIN +@class OFDictionary OF_GENERIC(KeyType, ObjectType); +@class OFException; @class OFHTTPClient; @class OFHTTPRequest; @class OFHTTPResponse; -@class OFURL; @class OFTCPSocket; -@class OFDictionary OF_GENERIC(KeyType, ObjectType); +@class OFURL; /*! * @protocol OFHTTPClientDelegate OFHTTPClient.h ObjFW/OFHTTPClient.h * * @brief A delegate for OFHTTPClient. */ @protocol OFHTTPClientDelegate +/*! + * @brief A callback which is called when an OFHTTPClient performed a request. + * + * @param client The OFHTTPClient which performed the request + * @param request The request the OFHTTPClient performed + * @param response The response to the request performed + */ +- (void)client: (OFHTTPClient *)client + didPerformRequest: (OFHTTPRequest *)request + response: (OFHTTPResponse *)response; + +/*! + * @brief A callback which is called when an OFHTTPClient encountered an + * exception while performing a request. + * + * @param client The client which encountered an exception + * @param exception The exception the client encountered + * @param request The request during which the client encountered the exception + */ +- (void)client: (OFHTTPClient *)client + didEncounterException: (id)exception + forRequest: (OFHTTPRequest *)request; + @optional /*! * @brief A callback which is called when an OFHTTPClient creates a socket. * * This is useful if the connection is using HTTPS and the server requires a @@ -48,11 +72,11 @@ * @param socket The socket created by the OFHTTPClient * @param request The request for which the socket was created */ - (void)client: (OFHTTPClient *)client didCreateSocket: (OF_KINDOF(OFTCPSocket *))socket - request: (OFHTTPRequest *)request; + forRequest: (OFHTTPRequest *)request; /*! * @brief A callback which is called when an OFHTTPClient received headers. * * @param client The OFHTTPClient which received the headers @@ -93,32 +117,24 @@ - (bool)client: (OFHTTPClient *)client shouldFollowRedirect: (OFURL *)URL statusCode: (int)statusCode request: (OFHTTPRequest *)request response: (OFHTTPResponse *)response; - -/*! - * @brief A callback which is called when an OFHTTPClient performed a request. - * - * @param client The OFHTTPClient which performed the request - * @param request The request the OFHTTPClient performed - * @param response The response to the request performed - */ -- (void)client: (OFHTTPClient *)client - didPerformRequest: (OFHTTPRequest *)request - response: (OFHTTPResponse *)response; @end /*! * @class OFHTTPClient OFHTTPClient.h ObjFW/OFHTTPClient.h * * @brief A class for performing HTTP requests. */ @interface OFHTTPClient: OFObject { +#ifdef OF_HTTPCLIENT_M +@public +#endif id _delegate; - bool _insecureRedirectsAllowed; + bool _insecureRedirectsAllowed, _inProgress; OFTCPSocket *_socket; OFURL *_lastURL; bool _lastWasHEAD; OFHTTPResponse *_lastResponse; } @@ -140,30 +156,27 @@ * @return A new, autoreleased OFHTTPClient */ + (instancetype)client; /*! - * @brief Performs the specified HTTP request and returns an OFHTTPResponse. - * - * @return An OFHTTPResponse with the response for the HTTP request + * @brief Asynchronously performs the specified HTTP request. */ -- (OFHTTPResponse *)performRequest: (OFHTTPRequest *)request; +- (void)performRequest: (OFHTTPRequest *)request; /*! - * @brief Performs the HTTP request and returns an OFHTTPResponse. + * @brief Asynchronously performs the specified HTTP request. * * @param request The request to perform * @param redirects The maximum number of redirects after which no further * attempt is done to follow the redirect, but instead the - * redirect is returned as an OFHTTPResponse - * @return An OFHTTPResponse with the response for the HTTP request + * redirect is treated as an OFHTTPResponse */ -- (OFHTTPResponse *)performRequest: (OFHTTPRequest *)request - redirects: (size_t)redirects; +- (void)performRequest: (OFHTTPRequest *)request + redirects: (unsigned int)redirects; /*! * @brief Closes connections that are still open due to keep-alive. */ - (void)close; @end OF_ASSUME_NONNULL_END Index: src/OFHTTPClient.m ================================================================== --- src/OFHTTPClient.m +++ src/OFHTTPClient.m @@ -12,10 +12,12 @@ * 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. */ +#define OF_HTTPCLIENT_M + #include "config.h" #include #include @@ -26,10 +28,11 @@ #import "OFURL.h" #import "OFTCPSocket.h" #import "OFDictionary.h" #import "OFData.h" +#import "OFAlreadyConnectedException.h" #import "OFHTTPRequestFailedException.h" #import "OFInvalidEncodingException.h" #import "OFInvalidFormatException.h" #import "OFInvalidServerReplyException.h" #import "OFNotImplementedException.h" @@ -38,10 +41,39 @@ #import "OFOutOfRangeException.h" #import "OFTruncatedDataException.h" #import "OFUnsupportedProtocolException.h" #import "OFUnsupportedVersionException.h" #import "OFWriteFailedException.h" + +@interface OFHTTPClientRequestHandler: OFObject +{ + OFHTTPClient *_client; + OFHTTPRequest *_request; + unsigned int _redirects; + bool _firstLine; + OFString *_version; + int _status; + OFMutableDictionary OF_GENERIC(OFString *, OFString *) *_serverHeaders; +} + +- initWithClient: (OFHTTPClient *)client + request: (OFHTTPRequest *)request + redirects: (unsigned int)redirects; +- (void)start; +@end + +@interface OFHTTPClientResponse: OFHTTPResponse +{ + OFTCPSocket *_socket; + bool _hasContentLength, _chunked, _keepAlive, _atEndOfStream; + size_t _toRead; +} + +@property (nonatomic, setter=of_setKeepAlive:) bool of_keepAlive; + +- initWithSocket: (OFTCPSocket *)socket; +@end static OFString * constructRequestString(OFHTTPRequest *request) { void *pool = objc_autoreleasePoolPush(); @@ -167,24 +199,264 @@ firstLetter = false; str++; } } -static bool -parseServerHeader( - OFMutableDictionary OF_GENERIC(OFString *, OFString *) *serverHeaders, - OFString *line) +@implementation OFHTTPClientRequestHandler +- initWithClient: (OFHTTPClient *)client + request: (OFHTTPRequest *)request + redirects: (unsigned int)redirects +{ + self = [super init]; + + @try { + _client = [client retain]; + _request = [request retain]; + _redirects = redirects; + _firstLine = true; + _serverHeaders = [[OFMutableDictionary alloc] init]; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + [_client release]; + [_request release]; + [_version release]; + [_serverHeaders release]; + + [super dealloc]; +} + +- (void)closeAndReconnect +{ + OFURL *URL = [_request URL]; + OFTCPSocket *socket; + + [_client close]; + + if ([[URL scheme] isEqual: @"https"]) { + if (of_tls_socket_class == Nil) + @throw [OFUnsupportedProtocolException + exceptionWithURL: URL]; + + socket = [[[of_tls_socket_class alloc] init] + autorelease]; + } else + socket = [OFTCPSocket socket]; + + [socket asyncConnectToHost: [URL host] + port: [URL port] + target: self + selector: @selector(socketDidConnect:context: + exception:) + context: nil]; +} + +- (void)didCreateResponse: (OFHTTPResponse *)response +{ + [_client->_delegate client:_client + didPerformRequest:_request + response:response]; +} + +- (void)createResponseWithSocket: (OFTCPSocket *)socket +{ + OFURL *URL = [_request URL]; + OFHTTPClientResponse *response; + OFString *connectionHeader; + bool keepAlive; + OFString *location; + + response = [[[OFHTTPClientResponse alloc] initWithSocket: socket] + autorelease]; + [response setProtocolVersionFromString: _version]; + [response setStatusCode: _status]; + [response setHeaders: _serverHeaders]; + + connectionHeader = [_serverHeaders objectForKey: @"Connection"]; + if ([_version isEqual: @"1.1"]) { + if (connectionHeader != nil) + keepAlive = ([connectionHeader caseInsensitiveCompare: + @"close"] != OF_ORDERED_SAME); + else + keepAlive = true; + } else { + if (connectionHeader != nil) + keepAlive = ([connectionHeader caseInsensitiveCompare: + @"keep-alive"] == OF_ORDERED_SAME); + else + keepAlive = false; + } + + if (keepAlive) { + [response of_setKeepAlive: true]; + + _client->_socket = [socket retain]; + _client->_lastURL = [URL copy]; + _client->_lastWasHEAD = + ([_request method] == OF_HTTP_REQUEST_METHOD_HEAD); + _client->_lastResponse = [response retain]; + } + + /* FIXME: Case-insensitive check of redirect's scheme */ + if (_redirects > 0 && (_status == 301 || _status == 302 || + _status == 303 || _status == 307) && + (location = [_serverHeaders objectForKey: @"Location"]) != nil && + (_client->_insecureRedirectsAllowed || + [[URL scheme] isEqual: @"http"] || + [location hasPrefix: @"https://"])) { + OFURL *newURL; + bool follow; + + newURL = [OFURL URLWithString: location + relativeToURL: URL]; + + if ([_client->_delegate respondsToSelector: @selector( + client:shouldFollowRedirect:statusCode:request:response:)]) + follow = [_client->_delegate client: _client + shouldFollowRedirect: newURL + statusCode: _status + request: _request + response: response]; + else { + of_http_request_method_t method = [_request method]; + + /* + * 301, 302 and 307 should only redirect with user + * confirmation if the request method is not GET or + * HEAD. Asking the delegate and getting true returned + * is considered user confirmation. + */ + if (method == OF_HTTP_REQUEST_METHOD_GET || + method == OF_HTTP_REQUEST_METHOD_HEAD) + follow = true; + /* + * 303 should always be redirected and converted to a + * GET request. + */ + else if (_status == 303) + follow = true; + else + follow = false; + } + + if (follow) { + OFDictionary OF_GENERIC(OFString *, OFString *) + *headers = [_request headers]; + OFHTTPRequest *newRequest = + [[_request copy] autorelease]; + OFMutableDictionary *newHeaders = + [[headers mutableCopy] autorelease]; + + if (![[newURL host] isEqual: [URL host]]) + [newHeaders removeObjectForKey: @"Host"]; + + /* + * 303 means the request should be converted to a GET + * request before redirection. This also means stripping + * the entity of the request. + */ + if (_status == 303) { + OFEnumerator *keyEnumerator, *objectEnumerator; + id key, object; + + keyEnumerator = [headers keyEnumerator]; + objectEnumerator = [headers objectEnumerator]; + while ((key = [keyEnumerator nextObject]) != + nil && + (object = [objectEnumerator nextObject]) != + nil) + if ([key hasPrefix: @"Content-"]) + [newHeaders + removeObjectForKey: key]; + + [newRequest setMethod: + OF_HTTP_REQUEST_METHOD_GET]; + [newRequest setBody: nil]; + } + + [newRequest setURL: newURL]; + [newRequest setHeaders: newHeaders]; + + _client->_inProgress = false; + + [_client performRequest: newRequest + redirects: _redirects - 1]; + return; + } + } + + if (_status / 100 != 2) + @throw [OFHTTPRequestFailedException + exceptionWithRequest: _request + response: response]; + + _client->_inProgress = false; + + [self performSelector: @selector(didCreateResponse:) + withObject: response + afterDelay: 0]; +} + +- (bool)handleFirstLine: (OFString *)line +{ + /* + * It's possible that the write succeeds on a connection that is + * keep-alive, but the connection has already been closed by the remote + * end due to a timeout. In this case, we need to reconnect. + */ + if (line == nil) { + [self closeAndReconnect]; + return false; + } + + if (![line hasPrefix: @"HTTP/"] || [line length] < 9 || + [line characterAtIndex: 8] != ' ') + @throw [OFInvalidServerReplyException exception]; + + _version = [[line substringWithRange: of_range(5, 3)] copy]; + if (![_version isEqual: @"1.0"] && ![_version isEqual: @"1.1"]) + @throw [OFUnsupportedVersionException + exceptionWithVersion: _version]; + + _status = (int)[[line substringWithRange: of_range(9, 3)] decimalValue]; + + return true; +} + +- (bool)handleServerHeader: (OFString *)line + socket: (OFTCPSocket *)socket { OFString *key, *value, *old; const char *lineC, *tmp; char *keyC; if (line == nil) @throw [OFInvalidServerReplyException exception]; - if ([line length] == 0) + if ([line length] == 0) { + [_serverHeaders makeImmutable]; + + if ([_client->_delegate respondsToSelector: + @selector(client:didReceiveHeaders:statusCode:request:)]) + [_client->_delegate client: _client + didReceiveHeaders: _serverHeaders + statusCode: _status + request: _request]; + + [self performSelector: @selector(createResponseWithSocket:) + withObject: socket + afterDelay: 0]; + return false; + } lineC = [line UTF8String]; if ((tmp = strchr(lineC, ':')) == NULL) @throw [OFInvalidServerReplyException exception]; @@ -209,30 +481,182 @@ tmp++; } while (*tmp == ' '); value = [OFString stringWithUTF8String: tmp]; - old = [serverHeaders objectForKey: key]; + old = [_serverHeaders objectForKey: key]; if (old != nil) value = [old stringByAppendingFormat: @",%@", value]; - [serverHeaders setObject: value - forKey: key]; + [_serverHeaders setObject: value + forKey: key]; + + return true; +} + +- (bool)socket: (OFTCPSocket *)socket + didReadLine: (OFString *)line + context: (id)context + exception: (id)exception +{ + if (exception != nil) { + if ([exception isKindOfClass: + [OFInvalidEncodingException class]]) + exception = [OFInvalidServerReplyException exception]; + + [_client->_delegate client: _client + didEncounterException: exception + forRequest: _request]; + return false; + } + + @try { + if (_firstLine) { + _firstLine = false; + return [self handleFirstLine: line]; + } else + return [self handleServerHeader: line + socket: socket]; + } @catch (id e) { + [_client->_delegate client: _client + didEncounterException: e + forRequest: _request]; + return false; + } +} + +- (void)handleSocket: (OFTCPSocket *)socket +{ + /* + * 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 + * the entire request (e.g. in case a keep-alive connection timed out). + */ + + @try { + OFData *body; + + @try { + /* TODO: Do this asynchronously */ + [socket writeString: constructRequestString(_request)]; + } @catch (OFWriteFailedException *e) { + if ([e errNo] != ECONNRESET && [e errNo] == EPIPE) + @throw e; + + /* + * Reconnect in case a keep-alive connection timed out. + */ + [self closeAndReconnect]; + return; + } + + if ((body = [_request body]) != nil) + [socket writeBuffer: [body items] + length: [body count] * [body itemSize]]; + } @catch (id e) { + [_client->_delegate client: _client + didEncounterException: e + forRequest: _request]; + return; + } + + [socket asyncReadLineWithTarget: self + selector: @selector(socket:didReadLine:context: + exception:) + context: nil]; +} + +- (void)socketDidConnect: (OFTCPSocket *)socket + context: (id)context + exception: (OFException *)exception +{ + if ([_client->_delegate respondsToSelector: + @selector(client:didCreateSocket:forRequest:)]) + [_client->_delegate client: _client + didCreateSocket: socket + forRequest: _request]; + + [self performSelector: @selector(handleSocket:) + withObject: socket + afterDelay: 0]; +} + +- (bool)throwAwayContent: (OFHTTPClientResponse *)response + buffer: (char *)buffer + length: (size_t)length + context: (OFTCPSocket *)socket + exception: (id)exception +{ + if (exception != nil) { + [_client->_delegate client: _client + didEncounterException: exception + forRequest: _request]; + return false; + } + + if ([response isAtEndOfStream]) { + [self freeMemory: buffer]; + + [_client->_lastResponse release]; + _client->_lastResponse = nil; + + [self performSelector: @selector(handleSocket:) + withObject: socket + afterDelay: 0]; + return false; + } return true; } -@interface OFHTTPClientResponse: OFHTTPResponse -{ - OFTCPSocket *_socket; - bool _hasContentLength, _chunked, _keepAlive, _atEndOfStream; - size_t _toRead; -} - -@property (nonatomic, setter=of_setKeepAlive:) bool of_keepAlive; - -- initWithSocket: (OFTCPSocket *)socket; +- (void)start +{ + OFURL *URL = [_request URL]; + OFTCPSocket *socket; + + /* Can we reuse the last socket? */ + if (_client->_socket != nil && + [[_client->_lastURL scheme] isEqual: [URL scheme]] && + [[_client->_lastURL host] isEqual: [URL host]] && + [_client->_lastURL port] == [URL port]) { + /* + * Set _socket to nil, so that in case of an error it won't be + * reused. If everything is successful, we set _socket again + * at the end. + */ + socket = [_client->_socket autorelease]; + _client->_socket = nil; + + [_client->_lastURL release]; + _client->_lastURL = nil; + + if (!_client->_lastWasHEAD) { + /* Throw away content that has not been read yet */ + char *buffer = [self allocMemoryWithSize: 512]; + + [_client->_lastResponse + asyncReadIntoBuffer: buffer + length: 512 + target: self + selector: @selector(throwAwayContent: + buffer:length:context: + exception:) + context: socket]; + } else { + [_client->_lastResponse release]; + _client->_lastResponse = nil; + + [self performSelector: @selector(handleSocket:) + withObject: socket + afterDelay: 0]; + } + } else + [self closeAndReconnect]; +} @end @implementation OFHTTPClientResponse @synthesize of_keepAlive = _keepAlive; @@ -301,12 +725,14 @@ size_t ret; if (_toRead == 0) { _atEndOfStream = true; - if (!_keepAlive) - [_socket close]; + if (!_keepAlive) { + [_socket release]; + _socket = nil; + } return 0; } if (_toRead < length) @@ -376,12 +802,14 @@ } if ([line length] > 0) @throw [OFInvalidServerReplyException exception]; - } else - [_socket close]; + } else { + [_socket release]; + _socket = nil; + } } objc_autoreleasePoolPop(pool); return 0; @@ -388,10 +816,13 @@ } } - (bool)lowlevelIsAtEndOfStream { + if (_atEndOfStream) + return true; + if (_socket == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (!_hasContentLength && !_chunked) return [_socket isAtEndOfStream]; @@ -412,10 +843,12 @@ return ([super hasDataInReadBuffer] || [_socket hasDataInReadBuffer]); } - (void)close { + _atEndOfStream = false; + [_socket release]; _socket = nil; [super close]; } @@ -435,324 +868,42 @@ [self close]; [super dealloc]; } -- (OFHTTPResponse *)performRequest: (OFHTTPRequest *)request -{ - return [self performRequest: request - redirects: 10]; -} - -- (OFTCPSocket *)of_closeAndCreateSocketForRequest: (OFHTTPRequest *)request -{ - OFURL *URL = [request URL]; - OFTCPSocket *socket; - - [self close]; - - if ([[URL scheme] isEqual: @"https"]) { - if (of_tls_socket_class == Nil) - @throw [OFUnsupportedProtocolException - exceptionWithURL: URL]; - - socket = [[[of_tls_socket_class alloc] init] - autorelease]; - } else - socket = [OFTCPSocket socket]; - - if ([_delegate respondsToSelector: - @selector(client:didCreateSocket:request:)]) - [_delegate client: self - didCreateSocket: socket - request: request]; - - [socket connectToHost: [URL host] - port: [URL port]]; - - return socket; -} - -- (OFHTTPResponse *)performRequest: (OFHTTPRequest *)request - redirects: (size_t)redirects +- (void)performRequest: (OFHTTPRequest *)request +{ + [self performRequest: request + redirects: 10]; +} + +- (void)performRequest: (OFHTTPRequest *)request + redirects: (unsigned int)redirects { void *pool = objc_autoreleasePoolPush(); OFURL *URL = [request URL]; OFString *scheme = [URL scheme]; - of_http_request_method_t method = [request method]; - OFString *requestString; - OFData *body = [request body]; - OFTCPSocket *socket; - OFHTTPClientResponse *response; - OFString *line, *version, *redirect, *connectionHeader; - bool keepAlive; - OFMutableDictionary OF_GENERIC(OFString *, OFString *) *serverHeaders; - int status; if (![scheme isEqual: @"http"] && ![scheme isEqual: @"https"]) @throw [OFUnsupportedProtocolException exceptionWithURL: URL]; - /* Can we reuse the socket? */ - if (_socket != nil && [[_lastURL scheme] isEqual: scheme] && - [[_lastURL host] isEqual: [URL host]] && - [_lastURL port] == [URL port]) { - /* - * Set _socket to nil, so that in case of an error it won't be - * reused. If everything is successful, we set _socket again - * at the end. - */ - socket = [_socket autorelease]; - _socket = nil; - - [_lastURL release]; - _lastURL = nil; - - @try { - if (!_lastWasHEAD) { - /* - * Throw away content that has not been read - * yet. - */ - while (![_lastResponse isAtEndOfStream]) { - char buffer[512]; - - [_lastResponse readIntoBuffer: buffer - length: 512]; - } - } - } @finally { - [_lastResponse release]; - _lastResponse = nil; - } - } else - socket = [self of_closeAndCreateSocketForRequest: request]; - - /* - * 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 - * the entire request (e.g. in case a keep-alive connection timed out). - */ - - requestString = constructRequestString(request); - - @try { - [socket writeString: requestString]; - } @catch (OFWriteFailedException *e) { - if ([e errNo] != ECONNRESET && [e errNo] != EPIPE) - @throw e; - - /* Reconnect in case a keep-alive connection timed out */ - socket = [self of_closeAndCreateSocketForRequest: request]; - [socket writeString: requestString]; - } - - if (body != nil) - [socket writeBuffer: [body items] - length: [body count] * [body itemSize]]; - - @try { - line = [socket readLine]; - } @catch (OFInvalidEncodingException *e) { - @throw [OFInvalidServerReplyException exception]; - } - - /* - * It's possible that the write succeeds on a connection that is - * keep-alive, but the connection has already been closed by the remote - * end due to a timeout. In this case, we need to reconnect. - */ - if (line == nil) { - socket = [self of_closeAndCreateSocketForRequest: request]; - [socket writeString: requestString]; - - if (body != nil) - [socket writeBuffer: [body items] - length: [body count] * - [body itemSize]]; - - @try { - line = [socket readLine]; - } @catch (OFInvalidEncodingException *e) { - @throw [OFInvalidServerReplyException exception]; - } - } - - if (![line hasPrefix: @"HTTP/"] || [line length] < 9 || - [line characterAtIndex: 8] != ' ') - @throw [OFInvalidServerReplyException exception]; - - version = [line substringWithRange: of_range(5, 3)]; - if (![version isEqual: @"1.0"] && ![version isEqual: @"1.1"]) - @throw [OFUnsupportedVersionException - exceptionWithVersion: version]; - - status = (int)[[line substringWithRange: of_range(9, 3)] decimalValue]; - - serverHeaders = [OFMutableDictionary dictionary]; - - do { - @try { - line = [socket readLine]; - } @catch (OFInvalidEncodingException *e) { - @throw [OFInvalidServerReplyException exception]; - } - } while (parseServerHeader(serverHeaders, line)); - - [serverHeaders makeImmutable]; - - if ([_delegate respondsToSelector: - @selector(client:didReceiveHeaders:statusCode:request:)]) - [_delegate client: self - didReceiveHeaders: serverHeaders - statusCode: status - request: request]; - - response = [[[OFHTTPClientResponse alloc] initWithSocket: socket] - autorelease]; - [response setProtocolVersionFromString: version]; - [response setStatusCode: status]; - [response setHeaders: serverHeaders]; - - connectionHeader = [serverHeaders objectForKey: @"Connection"]; - if ([version isEqual: @"1.1"]) { - if (connectionHeader != nil) - keepAlive = ([connectionHeader caseInsensitiveCompare: - @"close"] != OF_ORDERED_SAME); - else - keepAlive = true; - } else { - if (connectionHeader != nil) - keepAlive = ([connectionHeader caseInsensitiveCompare: - @"keep-alive"] == OF_ORDERED_SAME); - else - keepAlive = false; - } - - if (keepAlive) { - [response of_setKeepAlive: true]; - - _socket = [socket retain]; - _lastURL = [URL copy]; - _lastWasHEAD = (method == OF_HTTP_REQUEST_METHOD_HEAD); - _lastResponse = [response retain]; - } - - /* FIXME: Case-insensitive check of redirect's scheme */ - if (redirects > 0 && (status == 301 || status == 302 || - status == 303 || status == 307) && - (redirect = [serverHeaders objectForKey: @"Location"]) != nil && - (_insecureRedirectsAllowed || [scheme isEqual: @"http"] || - [redirect hasPrefix: @"https://"])) { - OFURL *newURL; - bool follow; - - newURL = [OFURL URLWithString: redirect - relativeToURL: URL]; - - if ([_delegate respondsToSelector: @selector(client: - shouldFollowRedirect:statusCode:request:response:)]) - follow = [_delegate client: self - shouldFollowRedirect: newURL - statusCode: status - request: request - response: response]; - else { - /* - * 301, 302 and 307 should only redirect with user - * confirmation if the request method is not GET or - * HEAD. Asking the delegate and getting true returned - * is considered user confirmation. - */ - if (method == OF_HTTP_REQUEST_METHOD_GET || - method == OF_HTTP_REQUEST_METHOD_HEAD) - follow = true; - /* - * 303 should always be redirected and converted to a - * GET request. - */ - else if (status == 303) - follow = true; - else - follow = false; - } - - if (follow) { - OFDictionary OF_GENERIC(OFString *, OFString *) - *headers = [request headers]; - OFHTTPRequest *newRequest = - [[request copy] autorelease]; - OFMutableDictionary *newHeaders = - [[headers mutableCopy] autorelease]; - - if (![[newURL host] isEqual: [URL host]]) - [newHeaders removeObjectForKey: @"Host"]; - - /* - * 303 means the request should be converted to a GET - * request before redirection. This also means stripping - * the entity of the request. - */ - if (status == 303) { - OFEnumerator *keyEnumerator, *objectEnumerator; - id key, object; - - keyEnumerator = [headers keyEnumerator]; - objectEnumerator = [headers objectEnumerator]; - while ((key = [keyEnumerator nextObject]) != - nil && - (object = [objectEnumerator nextObject]) != - nil) - if ([key hasPrefix: @"Content-"]) - [newHeaders - removeObjectForKey: key]; - - [newRequest setMethod: - OF_HTTP_REQUEST_METHOD_GET]; - [newRequest setBody: nil]; - } - - [newRequest setURL: newURL]; - [newRequest setHeaders: newHeaders]; - - [newRequest retain]; - objc_autoreleasePoolPop(pool); - [newRequest autorelease]; - - return [self performRequest: newRequest - redirects: redirects - 1]; - } - } - - [response retain]; - objc_autoreleasePoolPop(pool); - [response autorelease]; - - if (status / 100 != 2) - @throw [OFHTTPRequestFailedException - exceptionWithRequest: request - response: response]; - - if ([_delegate respondsToSelector: @selector(client:didPerformRequest: - response:)]) { - pool = objc_autoreleasePoolPush(); - - [_delegate client: self - didPerformRequest: request - response: response]; - - objc_autoreleasePoolPop(pool); - } - - return response; + if (_inProgress) + /* TODO: Find a better exception */ + @throw [OFAlreadyConnectedException exception]; + + _inProgress = true; + + [[[[OFHTTPClientRequestHandler alloc] + initWithClient: self + request: request + redirects: redirects] autorelease] start]; + + objc_autoreleasePoolPop(pool); } - (void)close { - [_socket close]; [_socket release]; _socket = nil; [_lastURL release]; _lastURL = nil; Index: src/OFString.m ================================================================== --- src/OFString.m +++ src/OFString.m @@ -39,20 +39,12 @@ #ifdef OF_HAVE_FILES # import "OFFile.h" # import "OFFileManager.h" #endif #import "OFURL.h" -#ifdef OF_HAVE_SOCKETS -# import "OFHTTPClient.h" -# import "OFHTTPRequest.h" -# import "OFHTTPResponse.h" -#endif #import "OFXMLElement.h" -#ifdef OF_HAVE_SOCKETS -# import "OFHTTPRequestFailedException.h" -#endif #import "OFInitializationFailedException.h" #import "OFInvalidArgumentException.h" #import "OFInvalidEncodingException.h" #import "OFInvalidFormatException.h" #import "OFNotImplementedException.h" @@ -1050,30 +1042,10 @@ encoding = OF_STRING_ENCODING_UTF_8; self = [self initWithContentsOfFile: [URL path] encoding: encoding]; } else -# endif -# ifdef OF_HAVE_SOCKETS - if ([scheme isEqual: @"http"] || [scheme isEqual: @"https"]) { - bool mutable = [self isKindOfClass: [OFMutableString class]]; - OFHTTPClient *client = [OFHTTPClient client]; - OFHTTPRequest *request = [OFHTTPRequest requestWithURL: URL]; - OFHTTPResponse *response = [client performRequest: request]; - - if ([response statusCode] != 200) - @throw [OFHTTPRequestFailedException - exceptionWithRequest: request - response: response]; - - [self release]; - - if (mutable) - self = [[response string] mutableCopy]; - else - self = [[response string] copy]; - } else # endif @throw [OFUnsupportedProtocolException exceptionWithURL: URL]; objc_autoreleasePoolPop(pool); Index: tests/OFHTTPClientTests.m ================================================================== --- tests/OFHTTPClientTests.m +++ tests/OFHTTPClientTests.m @@ -18,25 +18,31 @@ #include #include #import "OFHTTPClient.h" +#import "OFCondition.h" +#import "OFData.h" +#import "OFDate.h" +#import "OFDictionary.h" #import "OFHTTPRequest.h" #import "OFHTTPResponse.h" +#import "OFRunLoop.h" #import "OFString.h" #import "OFTCPSocket.h" #import "OFThread.h" -#import "OFCondition.h" #import "OFURL.h" -#import "OFDictionary.h" -#import "OFData.h" #import "OFAutoreleasePool.h" #import "TestsAppDelegate.h" static OFString *module = @"OFHTTPClient"; static OFCondition *cond; +static OFHTTPResponse *response = nil; + +@interface TestsAppDelegate (HTTPClientTests) +@end @interface HTTPClientTestsServer: OFThread { @public uint16_t _port; @@ -83,18 +89,26 @@ return nil; } @end @implementation TestsAppDelegate (OFHTTPClientTests) +- (void)client: (OFHTTPClient *)client + didPerformRequest: (OFHTTPRequest *)request + response: (OFHTTPResponse *)response_ +{ + response = [response_ retain]; + + [[OFRunLoop mainRunLoop] stop]; +} + - (void)HTTPClientTests { OFAutoreleasePool *pool = [[OFAutoreleasePool alloc] init]; HTTPClientTestsServer *server; OFURL *URL; OFHTTPClient *client; OFHTTPRequest *request; - OFHTTPResponse *response = nil; OFData *data; cond = [OFCondition condition]; [cond lock]; @@ -107,13 +121,19 @@ URL = [OFURL URLWithString: [OFString stringWithFormat: @"http://127.0.0.1:%" @PRIu16 "/foo", server->_port]]; TEST(@"-[performRequest:]", - (client = [OFHTTPClient client]) && + (client = [OFHTTPClient client]) && R([client setDelegate: self]) && R(request = [OFHTTPRequest requestWithURL: URL]) && - R(response = [client performRequest: request])) + R([client performRequest: request])) + + [[OFRunLoop mainRunLoop] runUntilDate: + [OFDate dateWithTimeIntervalSinceNow: 2]]; + [response autorelease]; + + TEST(@"Asynchronous handling of requests", response != nil) TEST(@"Normalization of server header keys", [[response headers] objectForKey: @"Content-Length"] != nil) TEST(@"Correct parsing of data", Index: utils/ofhttp/OFHTTP.m ================================================================== --- utils/ofhttp/OFHTTP.m +++ utils/ofhttp/OFHTTP.m @@ -536,16 +536,15 @@ [of_stdout writeFormat: @"☇ %@", [URL string]]; return true; } -- (bool)performRequest: (OFHTTPRequest *)request +- (void)client: (OFHTTPClient *)client + didEncounterException: (id)e + forRequest: (OFHTTPRequest *)request { - @try { - [_HTTPClient performRequest: request]; - return true; - } @catch (OFAddressTranslationFailedException *e) { + if ([e isKindOfClass: [OFAddressTranslationFailedException class]]) { if (!_quiet) [of_stdout writeString: @"\n"]; [of_stderr writeLine: OF_LOCALIZED(@"download_failed_address_translation", @@ -552,11 +551,11 @@ @"%[prog]: Failed to download <%[url]>!\n" @" Address translation failed: %[exception]", @"prog", [OFApplication programName], @"url", [[request URL] string], @"exception", e)]; - } @catch (OFConnectionFailedException *e) { + } else if ([e isKindOfClass: [OFConnectionFailedException class]]) { if (!_quiet) [of_stdout writeString: @"\n"]; [of_stderr writeLine: OF_LOCALIZED(@"download_failed_connection_failed", @@ -563,31 +562,31 @@ @"%[prog]: Failed to download <%[url]>!\n" @" Connection failed: %[exception]", @"prog", [OFApplication programName], @"url", [[request URL] string], @"exception", e)]; - } @catch (OFInvalidServerReplyException *e) { + } else if ([e isKindOfClass: [OFInvalidServerReplyException class]]) { if (!_quiet) [of_stdout writeString: @"\n"]; [of_stderr writeLine: OF_LOCALIZED(@"download_failed_invalid_server_reply", @"%[prog]: Failed to download <%[url]>!\n" @" Invalid server reply!", @"prog", [OFApplication programName], @"url", [[request URL] string])]; - } @catch (OFUnsupportedProtocolException *e) { + } else if ([e isKindOfClass: [OFUnsupportedProtocolException class]]) { if (!_quiet) [of_stdout writeString: @"\n"]; [of_stderr writeLine: OF_LOCALIZED(@"no_ssl_library", @"%[prog]: No TLS library loaded!\n" @" In order to download via https, you need to preload an " @"TLS library for ObjFW\n" @" such as ObjOpenSSL!", @"prog", [OFApplication programName])]; - } @catch (OFReadOrWriteFailedException *e) { + } else if ([e isKindOfClass: [OFReadOrWriteFailedException class]]) { OFString *error = OF_LOCALIZED( @"download_failed_read_or_write_failed_any", @"Read or write failed"); if (!_quiet) @@ -608,22 +607,24 @@ @" %[error]: %[exception]", @"prog", [OFApplication programName], @"url", [[request URL] string], @"error", error, @"exception", e)]; - } @catch (OFHTTPRequestFailedException *e) { + } else if ([e isKindOfClass: [OFHTTPRequestFailedException class]]) { if (!_quiet) [of_stdout writeFormat: @" ➜ %d\n", [[e response] statusCode]]; [of_stderr writeLine: OF_LOCALIZED(@"download_failed", @"%[prog]: Failed to download <%[url]>!", @"prog", [OFApplication programName], @"url", [[request URL] string])]; - } + } else + @throw e; - return false; + [self performSelector: @selector(downloadNextURL) + afterDelay: 0]; } - (void)client: (OFHTTPClient *)client didPerformRequest: (OFHTTPRequest *)request response: (OFHTTPResponse *)response @@ -905,15 +906,11 @@ request = [OFHTTPRequest requestWithURL: URL]; [request setHeaders: clientHeaders]; [request setMethod: OF_HTTP_REQUEST_METHOD_HEAD]; - if (![self performRequest: request]) { - _errorCode = 1; - goto next; - } - + [_HTTPClient performRequest: request]; return; } _detectedFileName = false; @@ -950,17 +947,13 @@ request = [OFHTTPRequest requestWithURL: URL]; [request setHeaders: clientHeaders]; [request setMethod: _method]; [request setBody: _body]; - if (![self performRequest: request]) { - _errorCode = 1; - goto next; - } - + [_HTTPClient performRequest: request]; return; next: [self performSelector: @selector(downloadNextURL) afterDelay: 0]; } @end