Index: configure.ac ================================================================== --- configure.ac +++ configure.ac @@ -460,11 +460,11 @@ OFMutex.m \ OFRecursiveMutex.m \ OFCondition.m \ ") AC_SUBST(OFTHREADTESTS_M, "OFThreadTests.m") - AC_SUBST(OFHTTPREQUESTTESTS_M, "OFHTTPRequestTests.m") + AC_SUBST(OFHTTPCLIENTTESTS_M, "OFHTTPClientTests.m") AC_SUBST(THREADING_H, "threading.h") AC_MSG_CHECKING(whether __thread works) AC_TRY_LINK([ /* It seems __thread is buggy with GCC 4.1 */ Index: extra.mk.in ================================================================== --- extra.mk.in +++ extra.mk.in @@ -22,11 +22,11 @@ INSTANCE_M = @INSTANCE_M@ LOOKUP_S = @LOOKUP_S@ OFBLOCKTESTS_M = @OFBLOCKTESTS_M@ OBJC_PROPERTIES_M = @OBJC_PROPERTIES_M@ OBJC_SYNC_M = @OBJC_SYNC_M@ -OFHTTPREQUESTTESTS_M = @OFHTTPREQUESTTESTS_M@ +OFHTTPCLIENTTESTS_M = @OFHTTPCLIENTTESTS_M@ OFPLUGIN_M = @OFPLUGIN_M@ OFPLUGINTESTS_M = @OFPLUGINTESTS_M@ OFSTREAMOBSERVER_KQUEUE_M = @OFSTREAMOBSERVER_KQUEUE_M@ OFSTREAMOBSERVER_POLL_M = @OFSTREAMOBSERVER_POLL_M@ OFSTREAMOBSERVER_SELECT_M = @OFSTREAMOBSERVER_SELECT_M@ Index: src/Makefile ================================================================== --- src/Makefile +++ src/Makefile @@ -19,10 +19,11 @@ OFDate.m \ OFDictionary.m \ OFEnumerator.m \ OFFile.m \ OFHash.m \ + OFHTTPClient.m \ OFHTTPRequest.m \ OFIntrospection.m \ OFList.m \ OFMapTable.m \ OFMD5Hash.m \ Index: src/OFDataArray.m ================================================================== --- src/OFDataArray.m +++ src/OFDataArray.m @@ -22,10 +22,11 @@ #import "OFDataArray.h" #import "OFString.h" #import "OFFile.h" #import "OFURL.h" +#import "OFHTTPClient.h" #import "OFHTTPRequest.h" #import "OFXMLElement.h" #import "OFHTTPRequestFailedException.h" #import "OFInvalidArgumentException.h" @@ -136,10 +137,11 @@ } - initWithContentsOfURL: (OFURL*)URL { void *pool; + OFHTTPClient *client; OFHTTPRequest *request; OFHTTPRequestResult *result; Class c; c = [self class]; @@ -151,12 +153,13 @@ self = [[c alloc] initWithContentsOfFile: [URL path]]; objc_autoreleasePoolPop(pool); return self; } + client = [OFHTTPClient client]; request = [OFHTTPRequest requestWithURL: URL]; - result = [request perform]; + result = [client performRequest: request]; if ([result statusCode] != 200) @throw [OFHTTPRequestFailedException exceptionWithClass: [request class] request: request ADDED src/OFHTTPClient.h Index: src/OFHTTPClient.h ================================================================== --- src/OFHTTPClient.h +++ src/OFHTTPClient.h @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011, 2012 + * 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 "OFObject.h" + +@class OFHTTPClient; +@class OFHTTPRequest; +@class OFHTTPRequestResult; +@class OFURL; +@class OFTCPSocket; +@class OFDictionary; +@class OFDataArray; + +/*! + * @brief A delegate for OFHTTPClient. + */ +#ifndef OF_HTTP_CLIENT_M +@protocol OFHTTPClientDelegate +#else +@protocol OFHTTPClientDelegate +#endif +#ifdef OF_HAVE_OPTIONAL_PROTOCOLS +@optional +#endif +/*! + * @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 + * client certificate. This callback can then be used to tell the TLS socket + * about the certificate. Another use case is to tell the socket about a SOCKS5 + * proxy it should use for this connection. + * + * @param client The OFHTTPClient that created a socket + * @param socket The socket created by the OFHTTPClient + * @param request The request for which the socket was created + */ +- (void)client: (OFHTTPClient*)client + didCreateSocket: (OFTCPSocket*)socket + request: (OFHTTPRequest*)request; + +/*! + * @brief A callback which is called when an OFHTTPClient received headers. + * + * @param client The OFHTTPClient which received the headers + * @param headers The headers received + * @param statusCode The status code received + * @param request The request for which the headers and status code have been + * received + */ +- (void)client: (OFHTTPClient*)client + didReceiveHeaders: (OFDictionary*)headers + statusCode: (int)statusCode + request: (OFHTTPRequest*)request; + +/*! + * @brief A callback which is called when an OFHTTPClient received data. + * + * This is useful for example if you want to update a status display. + * + * @param client The OFHTTPClient which received data + * @param data The data the OFHTTPClient received + * @param length The length of the data received, in bytes + * @param request The request for which data has been received + */ +- (void)client: (OFHTTPClient*)client + didReceiveData: (const char*)data + length: (size_t)length + request: (OFHTTPRequest*)request; + +/*! + * @brief A callback which is called when an OFHTTPClient will follow a + * redirect. + * + * If you want to get the headers and data for each redirect, set the number of + * redirects to 0 and perform a new OFHTTPClient for each redirect. However, + * this callback will not be called then and you have to look at the status code + * to detect a redirect. + * + * This callback will only be called if the OFHTTPClient will follow a + * redirect. If the maximum number of redirects has been reached already, this + * callback will not be called. + * + * @param client The OFHTTPClient which wants to follow a redirect + * @param URL The URL to which it will follow a redirect + * @param request The request for which the OFHTTPClient wants to redirect + * @return A boolean whether the OFHTTPClient should follow the redirect + */ +- (BOOL)client: (OFHTTPClient*)client + shouldFollowRedirect: (OFURL*)URL + request: (OFHTTPRequest*)request; +@end + +/*! + * @brief A class for performing HTTP requests. + */ +@interface OFHTTPClient: OFObject +{ + id delegate; + BOOL storesData; + BOOL insecureRedirectsAllowed; +} + +#ifdef OF_HAVE_PROPERTIES +@property (assign) id delegate; +@property BOOL storesData; +@property BOOL insecureRedirectsAllowed; +#endif + +/*! + * @brief Creates a new OFHTTPClient. + * + * @return A new, autoreleased OFHTTPClient + */ ++ (instancetype)client; + +/*! + * @brief Sets the delegate of the HTTP request. + * + * @param delegate The delegate of the HTTP request + */ +- (void)setDelegate: (id )delegate; + +/*! + * @brief Returns the delegate of the HTTP reqeust. + * + * @return The delegate of the HTTP request + */ +- (id )delegate; + +/*! + * @brief Sets whether redirects from HTTPS to HTTP are allowed. + * + * @param allowed Whether redirects from HTTPS to HTTP are allowed + */ +- (void)setInsecureRedirectsAllowed: (BOOL)allowed; + +/*! + * @brief Returns whether redirects from HTTPS to HTTP will be allowed + * + * @return Whether redirects from HTTPS to HTTP will be allowed + */ +- (BOOL)insecureRedirectsAllowed; + +/*! + * @brief Sets whether an OFDataArray with the data should be created. + * + * Setting this to NO is only useful if you are using the delegate to handle the + * data. + * + * @param enabled Whether to store the data in an OFDataArray + */ +- (void)setStoresData: (BOOL)enabled; + +/*! + * @brief Returns whether an OFDataArray with the date should be created. + * + * @return Whether an OFDataArray with the data should be created + */ +- (BOOL)storesData; + +/*! + * @brief Performs the specified HTTP request + */ +- (OFHTTPRequestResult*)performRequest: (OFHTTPRequest*)request; + +/*! + * @brief Performs the HTTP request and returns an OFHTTPRequestResult. + * + * @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 OFHTTPRequestResult + * @return An OFHTTPRequestResult with the result of the HTTP request + */ +- (OFHTTPRequestResult*)performRequest: (OFHTTPRequest*)request + redirects: (size_t)redirects; +@end + +@interface OFObject (OFHTTPClientDelegate) +@end + +extern Class of_http_client_tls_socket_class; ADDED src/OFHTTPClient.m Index: src/OFHTTPClient.m ================================================================== --- src/OFHTTPClient.m +++ src/OFHTTPClient.m @@ -0,0 +1,507 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011, 2012 + * 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" + +#define OF_HTTP_REQUEST_M + +#include +#include + +#import "OFHTTPClient.h" +#import "OFHTTPRequest.h" +#import "OFString.h" +#import "OFURL.h" +#import "OFTCPSocket.h" +#import "OFDictionary.h" +#import "OFDataArray.h" + +#import "OFHTTPRequestFailedException.h" +#import "OFInvalidEncodingException.h" +#import "OFInvalidFormatException.h" +#import "OFInvalidServerReplyException.h" +#import "OFOutOfRangeException.h" +#import "OFTruncatedDataException.h" +#import "OFUnsupportedProtocolException.h" +#import "OFUnsupportedVersionException.h" + +#import "autorelease.h" +#import "macros.h" + +Class of_http_client_tls_socket_class = Nil; + +static OF_INLINE void +normalizeKey(OFString *key) +{ + uint8_t *str = (uint8_t*)[key UTF8String]; + BOOL firstLetter = YES; + + while (*str != '\0') { + if (!isalnum(*str)) { + firstLetter = YES; + str++; + continue; + } + + *str = (firstLetter ? toupper(*str) : tolower(*str)); + + firstLetter = NO; + str++; + } +} + +@implementation OFHTTPClient ++ (instancetype)client +{ + return [[[self alloc] init] autorelease]; +} + +- init +{ + self = [super init]; + + storesData = YES; + + return self; +} + +- (void)setDelegate: (id )delegate_ +{ + delegate = delegate_; +} + +- (id )delegate +{ + return delegate; +} + +- (void)setInsecureRedirectsAllowed: (BOOL)allowed +{ + insecureRedirectsAllowed = allowed; +} + +- (BOOL)insecureRedirectsAllowed +{ + return insecureRedirectsAllowed; +} + +- (void)setStoresData: (BOOL)storesData_ +{ + storesData = storesData_; +} + +- (BOOL)storesData +{ + return storesData; +} + +- (OFHTTPRequestResult*)performRequest: (OFHTTPRequest*)request +{ + return [self performRequest: request + redirects: 10]; +} + +- (OFHTTPRequestResult*)performRequest: (OFHTTPRequest*)request + redirects: (size_t)redirects +{ + void *pool = objc_autoreleasePoolPush(); + OFURL *URL = [request URL]; + OFString *scheme = [URL scheme]; + of_http_request_type_t requestType = [request requestType]; + OFDictionary *headers = [request headers]; + OFDataArray *postData = [request postData]; + OFTCPSocket *sock; + OFHTTPRequestResult *result; + OFString *line, *path, *version; + OFMutableDictionary *serverHeaders; + OFDataArray *data; + OFEnumerator *keyEnumerator, *objectEnumerator; + OFString *key, *object, *contentLengthHeader; + int status; + const char *type = NULL; + size_t contentLength = 0; + BOOL chunked; + char *buffer; + size_t bytesReceived; + + if (![scheme isEqual: @"http"] && ![scheme isEqual: @"https"]) + @throw [OFUnsupportedProtocolException + exceptionWithClass: [self class] + URL: URL]; + + if ([scheme isEqual: @"http"]) + sock = [OFTCPSocket socket]; + else { + if (of_http_client_tls_socket_class == Nil) + @throw [OFUnsupportedProtocolException + exceptionWithClass: [self class] + URL: URL]; + + sock = [[[of_http_client_tls_socket_class alloc] init] + autorelease]; + } + + [delegate client: self + didCreateSocket: sock + request: request]; + + [sock connectToHost: [URL host] + port: [URL port]]; + + /* + * Work around a bug with packet bisection in lighttpd when using + * HTTPS. + */ + [sock setWriteBufferEnabled: YES]; + + if (requestType == OF_HTTP_REQUEST_TYPE_GET) + type = "GET"; + if (requestType == OF_HTTP_REQUEST_TYPE_HEAD) + type = "HEAD"; + if (requestType == OF_HTTP_REQUEST_TYPE_POST) + type = "POST"; + + if ([(path = [URL path]) isEqual: @""]) + path = @"/"; + + if ([URL query] != nil) + [sock writeFormat: @"%s %@?%@ HTTP/1.1\r\n", + type, path, [URL query]]; + else + [sock writeFormat: @"%s %@ HTTP/1.1\r\n", type, path]; + + if ([URL port] == 80) + [sock writeFormat: @"Host: %@\r\n", [URL host]]; + else + [sock writeFormat: @"Host: %@:%d\r\n", [URL host], + [URL port]]; + + [sock writeString: @"Connection: close\r\n"]; + + if ([headers objectForKey: @"User-Agent"] == nil) + [sock writeString: @"User-Agent: Something using ObjFW " + @"\r\n"]; + + keyEnumerator = [headers keyEnumerator]; + objectEnumerator = [headers objectEnumerator]; + + while ((key = [keyEnumerator nextObject]) != nil && + (object = [objectEnumerator nextObject]) != nil) + [sock writeFormat: @"%@: %@\r\n", key, object]; + + if (requestType == OF_HTTP_REQUEST_TYPE_POST) { + OFString *contentType = [request MIMEType]; + + if (contentType == nil) + contentType = @"application/x-www-form-urlencoded; " + @"charset=UTF-8\r\n"; + + [sock writeFormat: @"Content-Type: %@\r\n", contentType]; + [sock writeFormat: @"Content-Length: %d\r\n", + [postData count] * [postData itemSize]]; + } + + [sock writeString: @"\r\n"]; + + /* Work around a bug in lighttpd, see above */ + [sock flushWriteBuffer]; + [sock setWriteBufferEnabled: NO]; + + if (requestType == OF_HTTP_REQUEST_TYPE_POST) + [sock writeBuffer: [postData cArray] + length: [postData count] * [postData itemSize]]; + + @try { + line = [sock readLine]; + } @catch (OFInvalidEncodingException *e) { + @throw [OFInvalidServerReplyException + exceptionWithClass: [self class]]; + } + + if (![line hasPrefix: @"HTTP/"] || [line characterAtIndex: 8] != ' ') + @throw [OFInvalidServerReplyException + exceptionWithClass: [self class]]; + + version = [line substringWithRange: of_range(5, 3)]; + if (![version isEqual: @"1.0"] && ![version isEqual: @"1.1"]) + @throw [OFUnsupportedVersionException + exceptionWithClass: [self class] + version: version]; + + status = (int)[[line substringWithRange: of_range(9, 3)] decimalValue]; + + serverHeaders = [OFMutableDictionary dictionary]; + + for (;;) { + OFString *key, *value; + const char *line_c, *tmp; + + @try { + line = [sock readLine]; + } @catch (OFInvalidEncodingException *e) { + @throw [OFInvalidServerReplyException + exceptionWithClass: [self class]]; + } + + if (line == nil) + @throw [OFInvalidServerReplyException + exceptionWithClass: [self class]]; + + if ([line isEqual: @""]) + break; + + line_c = [line UTF8String]; + + if ((tmp = strchr(line_c, ':')) == NULL) + @throw [OFInvalidServerReplyException + exceptionWithClass: [self class]]; + + key = [OFString stringWithUTF8String: line_c + length: tmp - line_c]; + normalizeKey(key); + + do { + tmp++; + } while (*tmp == ' '); + + value = [OFString stringWithUTF8String: tmp]; + + if ((redirects > 0 && (status == 301 || status == 302 || + status == 303 || status == 307) && + [key isEqual: @"Location"]) && (insecureRedirectsAllowed || + [scheme isEqual: @"http"] || + ![value hasPrefix: @"http://"])) { + OFURL *newURL; + OFHTTPRequest *newRequest; + BOOL follow; + + newURL = [OFURL URLWithString: value + relativeToURL: URL]; + + follow = [delegate client: self + shouldFollowRedirect: newURL + request: request]; + + if (!follow && delegate != nil) { + [serverHeaders setObject: value + forKey: key]; + continue; + } + + newRequest = [OFHTTPRequest requestWithURL: newURL]; + [newRequest setRequestType: requestType]; + [newRequest setHeaders: headers]; + [newRequest setPostData: postData]; + [newRequest setMIMEType: [request MIMEType]]; + + if (status == 303) { + [newRequest + setRequestType: OF_HTTP_REQUEST_TYPE_GET]; + [newRequest setPostData: nil]; + [newRequest setMIMEType: nil]; + } + + [newRequest retain]; + objc_autoreleasePoolPop(pool); + [newRequest autorelease]; + + return [self performRequest: newRequest + redirects: redirects - 1]; + } + + [serverHeaders setObject: value + forKey: key]; + } + + [delegate client: self + didReceiveHeaders: serverHeaders + statusCode: status + request: request]; + + data = (storesData ? [OFDataArray dataArray] : nil); + chunked = [[serverHeaders objectForKey: @"Transfer-Encoding"] + isEqual: @"chunked"]; + + contentLengthHeader = [serverHeaders objectForKey: @"Content-Length"]; + + if (contentLengthHeader != nil) { + contentLength = (size_t)[contentLengthHeader decimalValue]; + + if (contentLength > SIZE_MAX) + @throw [OFOutOfRangeException + exceptionWithClass: [self class]]; + } + + buffer = [self allocMemoryWithSize: of_pagesize]; + bytesReceived = 0; + @try { + if (chunked) { + for (;;) { + void *pool2 = objc_autoreleasePoolPush(); + size_t toRead; + of_range_t range; + + @try { + line = [sock readLine]; + } @catch (OFInvalidEncodingException *e) { + @throw [OFInvalidServerReplyException + exceptionWithClass: [self class]]; + } + + range = [line rangeOfString: @";"]; + if (range.location != OF_NOT_FOUND) + line = [line substringWithRange: + of_range(0, range.location)]; + + @try { + toRead = + (size_t)[line hexadecimalValue]; + } @catch (OFInvalidFormatException *e) { + @throw [OFInvalidServerReplyException + exceptionWithClass: [self class]]; + } + + if (toRead == 0 || + (contentLengthHeader != nil && + contentLength >= bytesReceived)) + break; + + while (toRead > 0) { + size_t length = (toRead < of_pagesize + ? toRead : of_pagesize); + + length = [sock readIntoBuffer: buffer + length: length]; + + [delegate client: self + didReceiveData: buffer + length: length + request: request]; + + objc_autoreleasePoolPop(pool2); + pool2 = objc_autoreleasePoolPush(); + + bytesReceived += length; + [data addItemsFromCArray: buffer + count: length]; + + toRead -= length; + } + + @try { + line = [sock readLine]; + } @catch (OFInvalidEncodingException *e) { + @throw [OFInvalidServerReplyException + exceptionWithClass: [self class]]; + } + + if (![line isEqual: @""]) + @throw [OFInvalidServerReplyException + exceptionWithClass: [self class]]; + + objc_autoreleasePoolPop(pool2); + } + } else { + size_t length; + + while (![sock isAtEndOfStream]) { + void *pool2; + + length = [sock readIntoBuffer: buffer + length: of_pagesize]; + + pool2 = objc_autoreleasePoolPush(); + + [delegate client: self + didReceiveData: buffer + length: length + request: request]; + + objc_autoreleasePoolPop(pool2); + + bytesReceived += length; + [data addItemsFromCArray: buffer + count: length]; + + if (contentLengthHeader != nil && + bytesReceived >= contentLength) + break; + } + } + } @finally { + [self freeMemory: buffer]; + } + + [sock close]; + + /* + * We only want to throw on status code 200 as we will throw an + * OFHTTPRequestFailedException for all other status codes later. + */ + if (status == 200 && contentLengthHeader != nil && + contentLength != bytesReceived) + @throw [OFTruncatedDataException + exceptionWithClass: [self class]]; + + [serverHeaders makeImmutable]; + + result = [[OFHTTPRequestResult alloc] + OF_initWithStatusCode: status + headers: serverHeaders + data: data]; + + objc_autoreleasePoolPop(pool); + + [result autorelease]; + + if (status != 200) + @throw [OFHTTPRequestFailedException + exceptionWithClass: [self class] + request: request + result: result]; + + return result; +} +@end + +@implementation OFObject (OFHTTPClientDelegate) +- (void)client: (OFHTTPClient*)client + didCreateSocket: (OFTCPSocket*)socket + request: (OFHTTPRequest*)request +{ +} + +- (void)client: (OFHTTPClient*)client + didReceiveHeaders: (OFDictionary*)headers + statusCode: (int)statusCode + request: (OFHTTPRequest*)request +{ +} + +- (void)client: (OFHTTPClient*)client + didReceiveData: (const char*)data + length: (size_t)length + request: (OFHTTPRequest*)request +{ +} + +- (BOOL)client: (OFHTTPClient*)client + shouldFollowRedirect: (OFURL*)URL + request: (OFHTTPRequest*)request +{ + return YES; +} +@end Index: src/OFHTTPRequest.h ================================================================== --- src/OFHTTPRequest.h +++ src/OFHTTPRequest.h @@ -14,116 +14,39 @@ * file. */ #import "OFObject.h" -@class OFString; +@class OFURL; @class OFDictionary; -@class OFURL; -@class OFHTTPRequest; -@class OFHTTPRequestResult; -@class OFTCPSocket; @class OFDataArray; +@class OFString; typedef enum of_http_request_type_t { OF_HTTP_REQUEST_TYPE_GET, OF_HTTP_REQUEST_TYPE_POST, OF_HTTP_REQUEST_TYPE_HEAD } of_http_request_type_t; /*! - * @brief A delegate for OFHTTPRequests. - */ -#ifndef OF_HTTP_REQUEST_M -@protocol OFHTTPRequestDelegate -#else -@protocol OFHTTPRequestDelegate -#endif -#ifdef OF_HAVE_OPTIONAL_PROTOCOLS -@optional -#endif -/*! - * @brief A callback which is called when an OFHTTPRequest creates a socket. - * - * This is useful if the connection is using HTTPS and the server requires a - * client certificate. This callback can then be used to tell the TLS socket - * about the certificate. Another use case is to tell the socket about a SOCKS5 - * proxy it should use for this connection. - * - * @param request The OFHTTPRequest that created a socket - * @param socket The socket created by the OFHTTPRequest - */ -- (void)request: (OFHTTPRequest*)request - didCreateSocket: (OFTCPSocket*)socket; - -/*! - * @brief A callback which is called when an OFHTTPRequest received headers. - * - * @param request The OFHTTPRequest which received the headers - * @param headers The headers received - * @param statusCode The status code received - */ -- (void)request: (OFHTTPRequest*)request - didReceiveHeaders: (OFDictionary*)headers - withStatusCode: (int)statusCode; - -/*! - * @brief A callback which is called when an OFHTTPRequest received data. - * - * This is useful for example if you want to update a status display. - * - * @param request The OFHTTPRequest which received data - * @param data The data the OFHTTPRequest received - * @param length The length of the data received, in bytes - */ -- (void)request: (OFHTTPRequest*)request - didReceiveData: (const char*)data - withLength: (size_t)length; - -/*! - * @brief A callback which is called when an OFHTTPRequest will follow a - * redirect. - * - * If you want to get the headers and data for each redirect, set the number of - * redirects to 0 and perform a new OFHTTPRequest for each redirect. However, - * this callback will not be called then and you have to look at the status code - * to detect a redirect. - * - * This callback will only be called if the OFHTTPRequest will follow a - * redirect. If the maximum number of redirects has been reached already, this - * callback will not be called. - * - * @param request The OFHTTPRequest which will follow a redirect - * @param URL The URL to which it will follow a redirect - * @return A boolean whether the OFHTTPRequest should follow the redirect - */ -- (BOOL)request: (OFHTTPRequest*)request - shouldFollowRedirectTo: (OFURL*)URL; -@end - -/*! - * @brief A class for storing and performing HTTP requests. + * @brief A class for storing HTTP requests. */ @interface OFHTTPRequest: OFObject { OFURL *URL; of_http_request_type_t requestType; - OFString *queryString; OFDictionary *headers; - BOOL redirectsFromHTTPSToHTTPAllowed; - id delegate; - BOOL storesData; + OFDataArray *postData; + OFString *MIMEType; } #ifdef OF_HAVE_PROPERTIES @property (copy) OFURL *URL; @property of_http_request_type_t requestType; -@property (copy) OFString *queryString; @property (copy) OFDictionary *headers; -@property BOOL redirectsFromHTTPSToHTTPAllowed; -@property (assign) id delegate; -@property BOOL storesData; +@property (retain) OFDataArray *postData; +@property (copy) OFString *MIMEType; #endif /*! * @brief Creates a new OFHTTPRequest. * @@ -173,24 +96,10 @@ * * @return The request type of the HTTP request */ - (of_http_request_type_t)requestType; -/*! - * @brief Sets the query string of the HTTP request. - * - * @param queryString The query string of the HTTP request - */ -- (void)setQueryString: (OFString*)queryString; - -/*! - * @brief Returns the query string of the HTTP request. - * - * @return The query string of the HTTP request - */ -- (OFString*)queryString; - /*! * @brief Sets a dictionary with headers for the HTTP request. * * @param headers A dictionary with headers for the HTTP request */ @@ -202,70 +111,36 @@ * @return A dictionary with headers for the HTTP request. */ - (OFDictionary*)headers; /*! - * @brief Sets whether redirects from HTTPS to HTTP are allowed. - * - * @param allowed Whether redirects from HTTPS to HTTP are allowed - */ -- (void)setRedirectsFromHTTPSToHTTPAllowed: (BOOL)allowed; - -/*! - * @brief Returns whether redirects from HTTPS to HTTP will be allowed - * - * @return Whether redirects from HTTPS to HTTP will be allowed - */ -- (BOOL)redirectsFromHTTPSToHTTPAllowed; - -/*! - * @brief Sets the delegate of the HTTP request. - * - * @param delegate The delegate of the HTTP request - */ -- (void)setDelegate: (id )delegate; - -/*! - * @brief Returns the delegate of the HTTP reqeust. - * - * @return The delegate of the HTTP request - */ -- (id )delegate; - -/*! - * @brief Sets whether an OFDataArray with the data should be created. - * - * Setting this to NO is only useful if you are using the delegate to handle the - * data. - * - * @param storesData Whether to store the data in an OFDataArray - */ -- (void)setStoresData: (BOOL)storesData; - -/*! - * @brief Returns whether an OFDataArray with the date should be created. - * - * @return Whether an OFDataArray with the data should be created - */ -- (BOOL)storesData; - -/*! - * @brief Performs the HTTP request and returns an OFHTTPRequestResult. - * - * @return An OFHTTPRequestResult with the result of the HTTP request - */ -- (OFHTTPRequestResult*)perform; - -/*! - * @brief Performs the HTTP request and returns an OFHTTPRequestResult. - * - * @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 OFHTTPRequest - * @return An OFHTTPRequestResult with the result of the HTTP request - */ -- (OFHTTPRequestResult*)performWithRedirects: (size_t)redirects; + * @brief Sets the POST data of the HTTP request. + * + * @param postData The POST data of the HTTP request + */ +- (void)setPostData: (OFDataArray*)postData; + +/*! + * @brief Returns the POST data of the HTTP request. + * + * @return The POST data of the HTTP request + */ +- (OFDataArray*)postData; + +/*! + * @brief Sets the MIME type for the POST data. + * + * @param MIMEType The MIME type for the POST data + */ +- (void)setMIMEType: (OFString*)MIMEType; + +/*! + * @brief Returns the MIME type for the POST data. + * + * @return The MIME type for the POST data + */ +- (OFString*)MIMEType; @end /*! * @brief A class for storing the result of an HTTP request. */ @@ -280,13 +155,13 @@ @property (readonly) short statusCode; @property (readonly, copy) OFDictionary *headers; @property (readonly, retain) OFDataArray *data; #endif -- initWithStatusCode: (short)status - headers: (OFDictionary*)headers - data: (OFDataArray*)data; +- OF_initWithStatusCode: (short)status + headers: (OFDictionary*)headers + data: (OFDataArray*)data; /*! * @brief Returns the state code of the result of the HTTP request. * * @return The status code of the result of the HTTP request @@ -301,16 +176,9 @@ - (OFDictionary*)headers; /*! * @brief Returns the data received for the HTTP request. * - * Returns nil if storesData was set to NO. - * * @return The data received for the HTTP request */ - (OFDataArray*)data; @end - -@interface OFObject (OFHTTPRequestDelegate) -@end - -extern Class of_http_request_tls_socket_class; Index: src/OFHTTPRequest.m ================================================================== --- src/OFHTTPRequest.m +++ src/OFHTTPRequest.m @@ -14,56 +14,18 @@ * file. */ #include "config.h" -#define OF_HTTP_REQUEST_M - -#include -#include - #import "OFHTTPRequest.h" #import "OFString.h" #import "OFURL.h" -#import "OFTCPSocket.h" #import "OFDictionary.h" #import "OFDataArray.h" -#import "OFHTTPRequestFailedException.h" -#import "OFInvalidEncodingException.h" -#import "OFInvalidFormatException.h" -#import "OFInvalidServerReplyException.h" -#import "OFOutOfRangeException.h" -#import "OFTruncatedDataException.h" -#import "OFUnsupportedProtocolException.h" -#import "OFUnsupportedVersionException.h" - -#import "autorelease.h" #import "macros.h" -Class of_http_request_tls_socket_class = Nil; - -static OF_INLINE void -normalizeKey(OFString *key) -{ - uint8_t *str = (uint8_t*)[key UTF8String]; - BOOL firstLetter = YES; - - while (*str != '\0') { - if (!isalnum(*str)) { - firstLetter = YES; - str++; - continue; - } - - *str = (firstLetter ? toupper(*str) : tolower(*str)); - - firstLetter = NO; - str++; - } -} - @implementation OFHTTPRequest + (instancetype)request { return [[[self alloc] init] autorelease]; } @@ -76,15 +38,10 @@ - init { self = [super init]; requestType = OF_HTTP_REQUEST_TYPE_GET; - headers = [[OFDictionary alloc] - initWithObject: @"Something using ObjFW " - @"" - forKey: @"User-Agent"]; - storesData = YES; return self; } - initWithURL: (OFURL*)URL_ @@ -102,12 +59,13 @@ } - (void)dealloc { [URL release]; - [queryString release]; [headers release]; + [postData release]; + [MIMEType release]; [super dealloc]; } - (void)setURL: (OFURL*)URL_ @@ -128,20 +86,10 @@ - (of_http_request_type_t)requestType { return requestType; } -- (void)setQueryString: (OFString*)queryString_ -{ - OF_SETTER(queryString, queryString_, YES, 1) -} - -- (OFString*)queryString -{ - OF_GETTER(queryString, YES) -} - - (void)setHeaders: (OFDictionary*)headers_ { OF_SETTER(headers, headers_, YES, 1) } @@ -148,397 +96,35 @@ - (OFDictionary*)headers { OF_GETTER(headers, YES) } -- (void)setRedirectsFromHTTPSToHTTPAllowed: (BOOL)allowed -{ - redirectsFromHTTPSToHTTPAllowed = allowed; -} - -- (BOOL)redirectsFromHTTPSToHTTPAllowed -{ - return redirectsFromHTTPSToHTTPAllowed; -} - -- (void)setDelegate: (id )delegate_ -{ - delegate = delegate_; -} - -- (id )delegate -{ - return delegate; -} - -- (void)setStoresData: (BOOL)storesData_ -{ - storesData = storesData_; -} - -- (BOOL)storesData -{ - return storesData; -} - -- (OFHTTPRequestResult*)perform -{ - return [self performWithRedirects: 10]; -} - -- (OFHTTPRequestResult*)performWithRedirects: (size_t)redirects -{ - void *pool = objc_autoreleasePoolPush(); - OFString *scheme = [URL scheme]; - OFTCPSocket *sock; - OFHTTPRequestResult *result; - OFString *line, *path, *version; - OFMutableDictionary *serverHeaders; - OFDataArray *data; - OFEnumerator *keyEnumerator, *objectEnumerator; - OFString *key, *object, *contentLengthHeader; - int status; - const char *type = NULL; - size_t contentLength = 0; - BOOL chunked; - char *buffer; - size_t bytesReceived; - - if (![scheme isEqual: @"http"] && ![scheme isEqual: @"https"]) - @throw [OFUnsupportedProtocolException - exceptionWithClass: [self class] - URL: URL]; - - if ([scheme isEqual: @"http"]) - sock = [OFTCPSocket socket]; - else { - if (of_http_request_tls_socket_class == Nil) - @throw [OFUnsupportedProtocolException - exceptionWithClass: [self class] - URL: URL]; - - sock = [[[of_http_request_tls_socket_class alloc] init] - autorelease]; - } - - [delegate request: self - didCreateSocket: sock]; - - [sock connectToHost: [URL host] - port: [URL port]]; - - /* - * Work around a bug with packet bisection in lighttpd when using - * HTTPS. - */ - [sock setWriteBufferEnabled: YES]; - - if (requestType == OF_HTTP_REQUEST_TYPE_GET) - type = "GET"; - if (requestType == OF_HTTP_REQUEST_TYPE_HEAD) - type = "HEAD"; - if (requestType == OF_HTTP_REQUEST_TYPE_POST) - type = "POST"; - - if ([(path = [URL path]) isEqual: @""]) - path = @"/"; - - if ([URL query] != nil) - [sock writeFormat: @"%s %@?%@ HTTP/1.1\r\n", - type, path, [URL query]]; - else - [sock writeFormat: @"%s %@ HTTP/1.1\r\n", type, path]; - - if ([URL port] == 80) - [sock writeFormat: @"Host: %@\r\n", [URL host]]; - else - [sock writeFormat: @"Host: %@:%d\r\n", [URL host], - [URL port]]; - - [sock writeString: @"Connection: close\r\n"]; - - keyEnumerator = [headers keyEnumerator]; - objectEnumerator = [headers objectEnumerator]; - - while ((key = [keyEnumerator nextObject]) != nil && - (object = [objectEnumerator nextObject]) != nil) - [sock writeFormat: @"%@: %@\r\n", key, object]; - - if (requestType == OF_HTTP_REQUEST_TYPE_POST) { - if ([headers objectForKey: @"Content-Type"] == nil) - [sock writeString: @"Content-Type: " - @"application/x-www-form-urlencoded; " - @"charset=UTF-8\r\n"]; - - if ([headers objectForKey: @"Content-Length"] == nil) - [sock writeFormat: @"Content-Length: %d\r\n", - [queryString UTF8StringLength]]; - } - - [sock writeString: @"\r\n"]; - - /* Work around a bug in lighttpd, see above */ - [sock flushWriteBuffer]; - [sock setWriteBufferEnabled: NO]; - - if (requestType == OF_HTTP_REQUEST_TYPE_POST) - [sock writeString: queryString]; - - @try { - line = [sock readLine]; - } @catch (OFInvalidEncodingException *e) { - @throw [OFInvalidServerReplyException - exceptionWithClass: [self class]]; - } - - if (![line hasPrefix: @"HTTP/"] || [line characterAtIndex: 8] != ' ') - @throw [OFInvalidServerReplyException - exceptionWithClass: [self class]]; - - version = [line substringWithRange: of_range(5, 3)]; - if (![version isEqual: @"1.0"] && ![version isEqual: @"1.1"]) - @throw [OFUnsupportedVersionException - exceptionWithClass: [self class] - version: version]; - - status = (int)[[line substringWithRange: of_range(9, 3)] decimalValue]; - - serverHeaders = [OFMutableDictionary dictionary]; - - for (;;) { - OFString *key, *value; - const char *line_c, *tmp; - - @try { - line = [sock readLine]; - } @catch (OFInvalidEncodingException *e) { - @throw [OFInvalidServerReplyException - exceptionWithClass: [self class]]; - } - - if (line == nil) - @throw [OFInvalidServerReplyException - exceptionWithClass: [self class]]; - - if ([line isEqual: @""]) - break; - - line_c = [line UTF8String]; - - if ((tmp = strchr(line_c, ':')) == NULL) - @throw [OFInvalidServerReplyException - exceptionWithClass: [self class]]; - - key = [OFString stringWithUTF8String: line_c - length: tmp - line_c]; - normalizeKey(key); - - do { - tmp++; - } while (*tmp == ' '); - - value = [OFString stringWithUTF8String: tmp]; - - if ((redirects > 0 && (status == 301 || status == 302 || - status == 303 || status == 307) && - [key isEqual: @"Location"]) && - (redirectsFromHTTPSToHTTPAllowed || - [scheme isEqual: @"http"] || - ![value hasPrefix: @"http://"])) { - OFURL *new; - BOOL follow; - - new = [OFURL URLWithString: value - relativeToURL: URL]; - - follow = [delegate request: self - shouldFollowRedirectTo: new]; - - if (!follow && delegate != nil) { - [serverHeaders setObject: value - forKey: key]; - continue; - } - - new = [new retain]; - [URL release]; - URL = new; - - if (status == 303) { - requestType = OF_HTTP_REQUEST_TYPE_GET; - [queryString release]; - queryString = nil; - } - - objc_autoreleasePoolPop(pool); - - return [self performWithRedirects: redirects - 1]; - } - - [serverHeaders setObject: value - forKey: key]; - } - - [delegate request: self - didReceiveHeaders: serverHeaders - withStatusCode: status]; - - data = (storesData ? [OFDataArray dataArray] : nil); - chunked = [[serverHeaders objectForKey: @"Transfer-Encoding"] - isEqual: @"chunked"]; - - contentLengthHeader = [serverHeaders objectForKey: @"Content-Length"]; - - if (contentLengthHeader != nil) { - contentLength = (size_t)[contentLengthHeader decimalValue]; - - if (contentLength > SIZE_MAX) - @throw [OFOutOfRangeException - exceptionWithClass: [self class]]; - } - - buffer = [self allocMemoryWithSize: of_pagesize]; - bytesReceived = 0; - @try { - if (chunked) { - for (;;) { - void *pool2 = objc_autoreleasePoolPush(); - size_t toRead; - of_range_t range; - - @try { - line = [sock readLine]; - } @catch (OFInvalidEncodingException *e) { - @throw [OFInvalidServerReplyException - exceptionWithClass: [self class]]; - } - - range = [line rangeOfString: @";"]; - if (range.location != OF_NOT_FOUND) - line = [line substringWithRange: - of_range(0, range.location)]; - - @try { - toRead = - (size_t)[line hexadecimalValue]; - } @catch (OFInvalidFormatException *e) { - @throw [OFInvalidServerReplyException - exceptionWithClass: [self class]]; - } - - if (toRead == 0 || - (contentLengthHeader != nil && - contentLength >= bytesReceived)) - break; - - while (toRead > 0) { - size_t length = (toRead < of_pagesize - ? toRead : of_pagesize); - - length = [sock readIntoBuffer: buffer - length: length]; - - [delegate request: self - didReceiveData: buffer - withLength: length]; - - objc_autoreleasePoolPop(pool2); - pool2 = objc_autoreleasePoolPush(); - - bytesReceived += length; - [data addItemsFromCArray: buffer - count: length]; - - toRead -= length; - } - - @try { - line = [sock readLine]; - } @catch (OFInvalidEncodingException *e) { - @throw [OFInvalidServerReplyException - exceptionWithClass: [self class]]; - } - - if (![line isEqual: @""]) - @throw [OFInvalidServerReplyException - exceptionWithClass: [self class]]; - - objc_autoreleasePoolPop(pool2); - } - } else { - size_t length; - - while (![sock isAtEndOfStream]) { - void *pool2; - - length = [sock readIntoBuffer: buffer - length: of_pagesize]; - - pool2 = objc_autoreleasePoolPush(); - - [delegate request: self - didReceiveData: buffer - withLength: length]; - - objc_autoreleasePoolPop(pool2); - - bytesReceived += length; - [data addItemsFromCArray: buffer - count: length]; - - if (contentLengthHeader != nil && - bytesReceived >= contentLength) - break; - } - } - } @finally { - [self freeMemory: buffer]; - } - - [sock close]; - - /* - * We only want to throw on these status codes as we will throw an - * OFHTTPRequestFailedException for all other status codes later. - */ - if (contentLengthHeader != nil && contentLength != bytesReceived && - (status == 200 || status == 301 || status == 302 || status == 303 || - status == 307)) - @throw [OFTruncatedDataException - exceptionWithClass: [self class]]; - - [serverHeaders makeImmutable]; - - result = [[OFHTTPRequestResult alloc] initWithStatusCode: status - headers: serverHeaders - data: data]; - - switch (status) { - case 200: - case 301: - case 302: - case 303: - case 307: - break; - default: - [result autorelease]; - @throw [OFHTTPRequestFailedException - exceptionWithClass: [self class] - request: self - result: result]; - } - - objc_autoreleasePoolPop(pool); - - return [result autorelease]; +- (void)setPostData: (OFDataArray*)postData_ +{ + OF_SETTER(postData, postData_, YES, 0) +} + +- (OFDataArray*)postData +{ + OF_GETTER(postData, YES) +} + +- (void)setMIMEType: (OFString*)MIMEType_ +{ + OF_SETTER(MIMEType, MIMEType_, YES, 1) +} + +- (OFString*)MIMEType +{ + OF_GETTER(MIMEType, YES) } @end @implementation OFHTTPRequestResult -- initWithStatusCode: (short)status - headers: (OFDictionary*)headers_ - data: (OFDataArray*)data_ +- OF_initWithStatusCode: (short)status + headers: (OFDictionary*)headers_ + data: (OFDataArray*)data_ { self = [super init]; statusCode = status; data = [data_ retain]; @@ -567,31 +153,6 @@ - (OFDataArray*)data { OF_GETTER(data, YES) } -@end - -@implementation OFObject (OFHTTPRequestDelegate) -- (void)request: (OFHTTPRequest*)request - didCreateSocket: (OFTCPSocket*)socket -{ -} - -- (void)request: (OFHTTPRequest*)request - didReceiveHeaders: (OFDictionary*)headers - withStatusCode: (int)statusCode -{ -} - -- (void)request: (OFHTTPRequest*)request - didReceiveData: (const char*)data - withLength: (size_t)len -{ -} - -- (BOOL)request: (OFHTTPRequest*)request - shouldFollowRedirectTo: (OFURL*)url -{ - return YES; -} @end Index: src/OFMutex.m ================================================================== --- src/OFMutex.m +++ src/OFMutex.m @@ -66,11 +66,11 @@ lock: self]; } - (void)setName: (OFString*)name_ { - OF_SETTER(name, name_, YES, YES) + OF_SETTER(name, name_, YES, 1) } - (OFString*)name { OF_GETTER(name, YES) Index: src/OFRecursiveMutex.m ================================================================== --- src/OFRecursiveMutex.m +++ src/OFRecursiveMutex.m @@ -66,11 +66,11 @@ lock: self]; } - (void)setName: (OFString*)name_ { - OF_SETTER(name, name_, YES, YES) + OF_SETTER(name, name_, YES, 1) } - (OFString*)name { OF_GETTER(name, YES) Index: src/OFString.m ================================================================== --- src/OFString.m +++ src/OFString.m @@ -26,10 +26,11 @@ #import "OFString_UTF8.h" #import "OFArray.h" #import "OFDictionary.h" #import "OFFile.h" #import "OFURL.h" +#import "OFHTTPClient.h" #import "OFHTTPRequest.h" #import "OFDataArray.h" #import "OFXMLElement.h" #import "OFHTTPRequestFailedException.h" @@ -724,10 +725,11 @@ - initWithContentsOfURL: (OFURL*)URL encoding: (of_string_encoding_t)encoding { void *pool; + OFHTTPClient *client; OFHTTPRequest *request; OFHTTPRequestResult *result; OFString *contentType; Class c; @@ -744,12 +746,13 @@ encoding: encoding]; objc_autoreleasePoolPop(pool); return self; } + client = [OFHTTPClient client]; request = [OFHTTPRequest requestWithURL: URL]; - result = [request perform]; + result = [client performRequest: request]; if ([result statusCode] != 200) @throw [OFHTTPRequestFailedException exceptionWithClass: [request class] request: request Index: src/OFThread.m ================================================================== --- src/OFThread.m +++ src/OFThread.m @@ -343,11 +343,11 @@ OF_GETTER(name, YES) } - (void)setName: (OFString*)name_ { - OF_SETTER(name, name_, YES, YES) + OF_SETTER(name, name_, YES, 1) if (running == OF_THREAD_RUNNING) set_thread_name(self); } Index: src/ObjFW.h ================================================================== --- src/ObjFW.h +++ src/ObjFW.h @@ -51,10 +51,11 @@ #import "OFTLSSocket.h" #import "OFProcess.h" #import "OFStreamObserver.h" #import "OFHTTPRequest.h" +#import "OFHTTPClient.h" #import "OFHash.h" #import "OFMD5Hash.h" #import "OFSHA1Hash.h" Index: tests/Makefile ================================================================== --- tests/Makefile +++ tests/Makefile @@ -7,11 +7,11 @@ OFArrayTests.m \ ${OFBLOCKTESTS_M} \ OFDataArrayTests.m \ OFDateTests.m \ OFDictionaryTests.m \ - ${OFHTTPREQUESTTESTS_M} \ + ${OFHTTPCLIENTTESTS_M} \ OFJSONTests.m \ OFListTests.m \ OFMD5HashTests.m \ OFNumberTests.m \ OFObjectTests.m \ ADDED tests/OFHTTPClientTests.m Index: tests/OFHTTPClientTests.m ================================================================== --- tests/OFHTTPClientTests.m +++ tests/OFHTTPClientTests.m @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011, 2012 + * 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" + +#include +#include +#include + +#include + +#import "OFHTTPClient.h" +#import "OFHTTPRequest.h" +#import "OFString.h" +#import "OFTCPSocket.h" +#import "OFThread.h" +#import "OFCondition.h" +#import "OFURL.h" +#import "OFDictionary.h" +#import "OFAutoreleasePool.h" + +#import "TestsAppDelegate.h" + +static OFString *module = @"OFHTTPClient"; +static OFCondition *cond; + +@interface OFHTTPClientTestsServer: OFThread +{ +@public + uint16_t port; +} +@end + +@implementation OFHTTPClientTestsServer +- main +{ + OFTCPSocket *listener, *client; + + [cond lock]; + + listener = [OFTCPSocket socket]; + port = [listener bindToHost: @"127.0.0.1" + port: 0]; + [listener listen]; + + [cond signal]; + [cond unlock]; + + client = [listener accept]; + + if (![[client readLine] isEqual: @"GET /foo HTTP/1.1"]) + assert(0); + + if (![[client readLine] isEqual: + [OFString stringWithFormat: @"Host: 127.0.0.1:%" @PRIu16, port]]) + assert(0); + + if (![[client readLine] isEqual: @"Connection: close"]) + assert(0); + + if (![[client readLine] hasPrefix: @"User-Agent:"]) + assert(0); + + if (![[client readLine] isEqual: @""]) + assert(0); + + [client writeString: @"HTTP/1.1 200 OK\r\n" + @"cONTeNT-lENgTH: 7\r\n" + @"\r\n" + @"foo\n" + @"bar"]; + [client close]; + + return nil; +} +@end + +@implementation TestsAppDelegate (OFHTTPClientests) +- (void)HTTPClientTests +{ + OFAutoreleasePool *pool = [[OFAutoreleasePool alloc] init]; + OFHTTPClientTestsServer *server; + OFURL *url; + OFHTTPClient *client; + OFHTTPRequest *req; + OFHTTPRequestResult *res; + + cond = [OFCondition condition]; + [cond lock]; + + server = [[[OFHTTPClientTestsServer alloc] init] autorelease]; + [server start]; + + [cond wait]; + [cond unlock]; + + url = [OFURL URLWithString: + [OFString stringWithFormat: @"http://127.0.0.1:%" @PRIu16 "/foo", + server->port]]; + + TEST(@"-[performRequest:]", + R(client = [OFHTTPClient client]) && + R(req = [OFHTTPRequest requestWithURL: url]) && + R(res = [client performRequest: req])) + + TEST(@"Normalization of server header keys", + ([[res headers] objectForKey: @"Content-Length"] != nil)) + + [server join]; + + [pool drain]; +} +@end DELETED tests/OFHTTPRequestTests.m Index: tests/OFHTTPRequestTests.m ================================================================== --- tests/OFHTTPRequestTests.m +++ tests/OFHTTPRequestTests.m @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2008, 2009, 2010, 2011, 2012 - * 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" - -#include -#include -#include - -#include - -#import "OFHTTPRequest.h" -#import "OFString.h" -#import "OFTCPSocket.h" -#import "OFThread.h" -#import "OFCondition.h" -#import "OFURL.h" -#import "OFDictionary.h" -#import "OFAutoreleasePool.h" - -#import "TestsAppDelegate.h" - -static OFString *module = @"OFHTTPRequest"; -static OFCondition *cond; - -@interface OFHTTPRequestTestsServer: OFThread -{ -@public - uint16_t port; -} -@end - -@implementation OFHTTPRequestTestsServer -- main -{ - OFTCPSocket *listener, *client; - - [cond lock]; - - listener = [OFTCPSocket socket]; - port = [listener bindToHost: @"127.0.0.1" - port: 0]; - [listener listen]; - - [cond signal]; - [cond unlock]; - - client = [listener accept]; - - if (![[client readLine] isEqual: @"GET /foo HTTP/1.1"]) - assert(0); - - if (![[client readLine] isEqual: - [OFString stringWithFormat: @"Host: 127.0.0.1:%" @PRIu16, port]]) - assert(0); - - if (![[client readLine] isEqual: @"Connection: close"]) - assert(0); - - if (![[client readLine] hasPrefix: @"User-Agent:"]) - assert(0); - - if (![[client readLine] isEqual: @""]) - assert(0); - - [client writeString: @"HTTP/1.1 200 OK\r\n" - @"cONTeNT-lENgTH: 7\r\n" - @"\r\n" - @"foo\n" - @"bar"]; - [client close]; - - return nil; -} -@end - -@implementation TestsAppDelegate (OFHTTPRequesTests) -- (void)HTTPRequestTests -{ - OFAutoreleasePool *pool = [[OFAutoreleasePool alloc] init]; - OFHTTPRequestTestsServer *server; - OFURL *url; - OFHTTPRequest *req; - OFHTTPRequestResult *res; - - cond = [OFCondition condition]; - [cond lock]; - - server = [[[OFHTTPRequestTestsServer alloc] init] autorelease]; - [server start]; - - [cond wait]; - [cond unlock]; - - url = [OFURL URLWithString: - [OFString stringWithFormat: @"http://127.0.0.1:%" @PRIu16 "/foo", - server->port]]; - - TEST(@"+[requestWithURL]", (req = [OFHTTPRequest requestWithURL: url])) - - TEST(@"-[perform]", (res = [req perform])) - - TEST(@"Normalization of server header keys", - ([[res headers] objectForKey: @"Content-Length"] != nil)) - - [server join]; - - [pool drain]; -} -@end Index: tests/TestsAppDelegate.h ================================================================== --- tests/TestsAppDelegate.h +++ tests/TestsAppDelegate.h @@ -94,12 +94,12 @@ @interface TestsAppDelegate (ForwardingTests) - (void)forwardingTests; @end -@interface TestsAppDelegate (OFHTTPRequestTests) -- (void)HTTPRequestTests; +@interface TestsAppDelegate (OFHTTPClientTests) +- (void)HTTPClientTests; @end @interface TestsAppDelegate (OFJSONTests) - (void)JSONTests; @end Index: tests/TestsAppDelegate.m ================================================================== --- tests/TestsAppDelegate.m +++ tests/TestsAppDelegate.m @@ -139,11 +139,11 @@ #ifdef OF_THREADS [self threadTests]; #endif [self URLTests]; #ifdef OF_THREADS - [self HTTPRequestTests]; + [self HTTPClientTests]; #endif [self XMLParserTests]; [self XMLNodeTests]; [self XMLElementBuilderTests]; [self serializationTests];