Index: ObjFW.xcodeproj/project.pbxproj ================================================================== --- ObjFW.xcodeproj/project.pbxproj +++ ObjFW.xcodeproj/project.pbxproj @@ -194,10 +194,12 @@ 4B6EF6811235358D0076B512 /* TestsAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TestsAppDelegate.m; path = tests/TestsAppDelegate.m; sourceTree = SOURCE_ROOT; }; 4B6EF684123535B60076B512 /* TestPlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TestPlugin.m; path = tests/plugin/TestPlugin.m; sourceTree = SOURCE_ROOT; }; 4B6EF685123535C80076B512 /* test.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = test.m; path = tests/objc_sync/test.m; sourceTree = SOURCE_ROOT; }; 4B981CDE116F71DD00294DB7 /* OFSeekableStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OFSeekableStream.h; path = src/OFSeekableStream.h; sourceTree = ""; }; 4B981CDF116F71DD00294DB7 /* OFSeekableStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OFSeekableStream.m; path = src/OFSeekableStream.m; sourceTree = ""; }; + 4B99250F12E0780000215DBE /* OFHTTPRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OFHTTPRequest.h; path = src/OFHTTPRequest.h; sourceTree = ""; }; + 4B99251012E0780000215DBE /* OFHTTPRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OFHTTPRequest.m; path = src/OFHTTPRequest.m; sourceTree = ""; }; 4BAF5F46123460C900F4E111 /* OFCollection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OFCollection.h; path = src/OFCollection.h; sourceTree = ""; }; 4BAF5F47123460C900F4E111 /* OFStreamObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OFStreamObserver.h; path = src/OFStreamObserver.h; sourceTree = ""; }; 4BAF5F48123460C900F4E111 /* OFStreamObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OFStreamObserver.m; path = src/OFStreamObserver.m; sourceTree = ""; }; 4BAF5F49123460C900F4E111 /* OFStreamSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OFStreamSocket.h; path = src/OFStreamSocket.h; sourceTree = ""; }; 4BAF5F4A123460C900F4E111 /* OFStreamSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OFStreamSocket.m; path = src/OFStreamSocket.m; sourceTree = ""; }; @@ -275,10 +277,12 @@ 4B6799651099E7C50041064A /* OFExceptions.m */, 4B6799661099E7C50041064A /* OFFile.h */, 4B6799671099E7C50041064A /* OFFile.m */, 4BF1BCC011C9663F0025511F /* OFHash.h */, 4BF1BCC111C9663F0025511F /* OFHash.m */, + 4B99250F12E0780000215DBE /* OFHTTPRequest.h */, + 4B99251012E0780000215DBE /* OFHTTPRequest.m */, 4B67996C1099E7C50041064A /* OFList.h */, 4B67996D1099E7C50041064A /* OFList.m */, 4BF1BCC211C9663F0025511F /* OFMD5Hash.h */, 4BF1BCC311C9663F0025511F /* OFMD5Hash.m */, 4B67996F1099E7C50041064A /* OFMutableArray.h */, Index: src/Makefile ================================================================== --- src/Makefile +++ src/Makefile @@ -15,10 +15,11 @@ OFDate.m \ OFDictionary.m \ OFExceptions.m \ OFFile.m \ OFHash.m \ + OFHTTPRequest.m \ OFEnumerator.m \ OFList.m \ OFMD5Hash.m \ OFMutableArray.m \ OFMutableDictionary.m \ Index: src/OFExceptions.h ================================================================== --- src/OFExceptions.h +++ src/OFExceptions.h @@ -18,10 +18,11 @@ #import "OFObject.h" @class OFString; @class OFURL; +@class OFHTTPRequest; /** * \brief An exception indicating an object could not be allocated. * * This exception is preallocated, as if there's no memory, no exception can @@ -1283,5 +1284,58 @@ /** * \return The URL whose protocol is unsupported */ - (OFURL*)URL; @end + +/** + * \brief An exception indicating that the server sent an invalid reply. + */ +@interface OFInvalidServerReplyException: OFException +@end + +/** + * \brief An exception indicating that a HTTP request failed. + */ +@interface OFHTTPRequestFailedException: OFException +{ + OFHTTPRequest *HTTPRequest; + short statusCode; +} + +#ifdef OF_HAVE_PROPERTIES +@property (readonly, nonatomic) OFHTTPRequest *HTTPRequest; +@property (readonly) short statusCode; +#endif + +/** + * \param class_ The class of the object which caused the exception + * \param request The HTTP request which failed + * \param code The status code of the fialed HTTP request + * \return A new HTTP request failed exception + */ ++ newWithClass: (Class)class_ + HTTPRequest: (OFHTTPRequest*)request + statusCode: (short)code; + +/** + * Initializes an already allocated HTTP request failed exception + * + * \param class_ The class of the object which caused the exception + * \param request The HTTP request which failed + * \param code The status code of the fialed HTTP request + * \return A new HTTP request failed exception + */ +- initWithClass: (Class)class_ + HTTPRequest: (OFHTTPRequest*)request + statusCode: (short)code; + +/** + * \return The HTTP request which failed + */ +- (OFHTTPRequest*)HTTPRequest; + +/** + * \return The status code of the HTTP request + */ +- (short)statusCode; +@end Index: src/OFExceptions.m ================================================================== --- src/OFExceptions.m +++ src/OFExceptions.m @@ -32,10 +32,12 @@ #endif #import "OFExceptions.h" #import "OFString.h" #import "OFTCPSocket.h" +#import "OFHTTPRequest.h" +#import "OFAutoreleasePool.h" #ifndef _WIN32 # include # define GET_ERRNO errno # ifndef HAVE_THREADSAFE_GETADDRINFO @@ -1885,5 +1887,103 @@ - (OFURL*)URL { return URL; } @end + +@implementation OFInvalidServerReplyException +- (OFString*)description +{ + if (description != nil) + return description; + + description = [[OFString alloc] initWithFormat: + @"Got an invalid reply from the server in class %s", + class_getName(inClass)]; + + return description; +} +@end + +@implementation OFHTTPRequestFailedException ++ newWithClass: (Class)class_ + HTTPRequest: (OFHTTPRequest*)request + statusCode: (short)code +{ + return [[self alloc] initWithClass: class_ + HTTPRequest: request + statusCode: code]; +} + +- initWithClass: (Class)class_ +{ + Class c = isa; + [self release]; + @throw [OFNotImplementedException newWithClass: c + selector: _cmd]; +} + +- initWithClass: (Class)class_ + HTTPRequest: (OFHTTPRequest*)request + statusCode: (short)code +{ + self = [super initWithClass: class_]; + + @try { + HTTPRequest = [request retain]; + statusCode = code; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + [HTTPRequest release]; + + [super dealloc]; +} + +- (OFString*)description +{ + OFAutoreleasePool *pool; + const char *type = "(unknown)"; + + if (description != nil) + return description; + + switch ([HTTPRequest requestType]) { + case OF_HTTP_REQUEST_TYPE_GET: + type = "GET"; + break; + case OF_HTTP_REQUEST_TYPE_HEAD: + type = "HEAD"; + break; + case OF_HTTP_REQUEST_TYPE_POST: + type = "POST"; + break; + } + + pool = [[OFAutoreleasePool alloc] init]; + + description = [[OFString alloc] initWithFormat: + @"A HTTP %s request of class %s with URL %@ failed with code %d", + type, class_getName(inClass), [HTTPRequest URL], statusCode]; + + [pool release]; + + return description; +} + +- (OFHTTPRequest*)HTTPRequest +{ + return HTTPRequest; +} + +- (short)statusCode +{ + return statusCode; +} +@end ADDED src/OFHTTPRequest.h Index: src/OFHTTPRequest.h ================================================================== --- src/OFHTTPRequest.h +++ src/OFHTTPRequest.h @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011 + * 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 OFString; +@class OFDictionary; +@class OFURL; +@class OFHTTPRequestResult; +@class OFDataArray; + +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 class for storing and performing HTTP requests. + */ +@interface OFHTTPRequest: OFObject +{ + OFURL *URL; + of_http_request_type_t requestType; + OFString *queryString; + OFDictionary *headers; +} + +#ifdef OF_HAVE_PROPERTIES +@property (copy) OFURL *URL; +@property (assign) of_http_request_type_t requestType; +@property (copy) OFString *queryString; +@property (copy) OFDictionary *headers; +#endif + +/** + * \return A new, autoreleased OFHTTPRequest + */ ++ request; + +/** + * Sets the URL for the HTTP request. + * + * \param URL The URL for the HTTP request + */ +- (void)setURL: (OFURL*)url; + +/** + * \return The URL for the HTTP request + */ +- (OFURL*)URL; + +/** + * Sets the request type for the HTTP request. + * + * \param type The request type for the HTTP request + */ +- (void)setRequestType: (of_http_request_type_t)type; + +/** + * \return The request type for the HTTP request + */ +- (of_http_request_type_t)requestType; + +/** + * Sets the query string for the HTTP request. + * Only used for GET and HEAD requests! + * + * \param qs The query string for the HTTP request + */ +- (void)setQueryString: (OFString*)qs; + +/** + * \return The query string for the HTTP request + */ +- (OFString*)queryString; + +/** + * Sets a dictionary with headers for the HTTP request. + * + * \param headers A dictionary with headers for the HTTP request + */ +- (void)setHeaders: (OFDictionary*)headers; + +/** + * \return A dictionary with headers for the HTTP request. + */ +- (OFDictionary*)headers; + +/** + * Performs the HTTP request and returns an OFHTTPRequestResult. + * + * \return An OFHTTPRequestResult with the result of the HTTP request + */ +- (OFHTTPRequestResult*)result; + +/** + * 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*)resultWithRedirects: (size_t)redirects; +@end + +/** + * \brief A class for storing the result of an HTTP request. + */ +@interface OFHTTPRequestResult: OFObject +{ + short statusCode; + OFDataArray *data; + OFDictionary *headers; +} + +#ifdef OF_HAVE_PROPERTIES +@property (readonly) short statusCode; +@property (readonly, copy) OFDictionary *headers; +@property (readonly, retain) OFDataArray *data; +#endif + +/// \cond internal +- initWithStatusCode: (short)status + headers: (OFDictionary*)headers + data: (OFDataArray*)data; +/// \endcond + +/** + * \return The status code of the result of the HTTP request + */ +- (short)statusCode; + +/** + * \return The HTTP headers of the result of the HTTP request + */ +- (OFDictionary*)headers; + +/** + * \return The data returned for the HTTP request + */ +- (OFDataArray*)data; +@end ADDED src/OFHTTPRequest.m Index: src/OFHTTPRequest.m ================================================================== --- src/OFHTTPRequest.m +++ src/OFHTTPRequest.m @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011 + * 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 + +#import "OFHTTPRequest.h" +#import "OFString.h" +#import "OFURL.h" +#import "OFTCPSocket.h" +#import "OFDictionary.h" +#import "OFAutoreleasePool.h" +#import "OFExceptions.h" + +@implementation OFHTTPRequest ++ request +{ + return [[[self alloc] init] autorelease]; +} + +- init +{ + self = [super init]; + + requestType = OF_HTTP_REQUEST_TYPE_GET; + headers = [OFDictionary + dictionaryWithObject: @"Something using ObjFW " + @"" + forKey: @"User-Agent"]; + + return self; +} + +- (void)dealloc +{ + [URL release]; + [queryString release]; + [headers release]; + + [super dealloc]; +} + +- (void)setURL: (OFURL*)url +{ + OFURL *old = URL; + URL = [url copy]; + [old release]; +} + +- (OFURL*)URL +{ + return [[URL copy] autorelease]; +} + +- (void)setRequestType: (of_http_request_type_t)type +{ + requestType = type; +} + +- (of_http_request_type_t)requestType +{ + return requestType; +} + +- (void)setQueryString: (OFString*)qs +{ + OFString *old = queryString; + queryString = [qs copy]; + [old release]; +} + +- (OFString*)queryString +{ + return [[queryString copy] autorelease]; +} + +- (void)setHeaders: (OFDictionary*)headers_ +{ + OFDictionary *old = headers; + headers = [headers_ copy]; + [old release]; +} + +- (OFDictionary*)headers +{ + return [[headers copy] autorelease]; +} + +- (OFHTTPRequestResult*)result +{ + return [self resultWithRedirects: 10]; +} + +- (OFHTTPRequestResult*)resultWithRedirects: (size_t)redirects +{ + OFAutoreleasePool *pool; + OFTCPSocket *sock; + OFHTTPRequestResult *result; + + if (![[URL scheme] isEqual: @"http"]) + @throw [OFUnsupportedProtocolException newWithClass: isa + URL: URL]; + pool = [[OFAutoreleasePool alloc] init]; + sock = [OFTCPSocket socket]; + + [sock connectToService: [OFString stringWithFormat: @"%d", [URL port]] + onNode: [URL host]]; + + @try { + OFString *line; + OFMutableDictionary *s_headers; + OFDataArray *data; + OFEnumerator *enumerator; + OFString *key; + int status; + char *t; + + if (requestType == OF_HTTP_REQUEST_TYPE_GET) + t = "GET"; + if (requestType == OF_HTTP_REQUEST_TYPE_HEAD) + t = "HEAD"; + if (requestType == OF_HTTP_REQUEST_TYPE_POST) + t = "POST"; + + if ([URL query] != nil) + [sock writeFormat: @"%s /%@?%@ HTTP/1.0\r\n", + t, [URL path], [URL query]]; + else + [sock writeFormat: @"%s /%@ HTTP/1.0\r\n", + t, [URL path]]; + + if ([URL port] == 80) + [sock writeFormat: @"Host: %@\r\n", [URL host]]; + else + [sock writeFormat: @"Host: %@:%d\r\n", [URL host], + [URL port]]; + + enumerator = [headers keyEnumerator]; + + while ((key = [enumerator nextObject]) != nil) + [sock writeFormat: @"%@: %@\r\n", + key, [headers objectForKey: key]]; + + if (requestType == OF_HTTP_REQUEST_TYPE_POST) { + if ([headers objectForKey: @"Content-Type"] == nil) + [sock writeString: @"Content-Type: " + @"application/x-www-form-urlencoded\r\n"]; + + if ([headers objectForKey: @"Content-Length"] == nil) + [sock writeFormat: @"Content-Length: %d\r\n", + [queryString cStringLength]]; + } + + [sock writeString: @"\r\n"]; + + if (requestType == OF_HTTP_REQUEST_TYPE_POST) + [sock writeString: queryString]; + + /* + * We also need to check for HTTP/1.1 since Apache always + * declares the reply to be HTTP/1.1. + */ + line = [sock readLine]; + if (![line hasPrefix: @"HTTP/1.0 "] && + ![line hasPrefix: @"HTTP/1.1 "]) + @throw [OFInvalidServerReplyException + newWithClass: isa]; + + status = [[line substringFromIndex: 9 + toIndex: 12] decimalValue]; + + if (status != 200 && status != 301 && status != 302 && + status != 303) + @throw [OFHTTPRequestFailedException + newWithClass: isa + HTTPRequest: self + statusCode: status]; + + s_headers = [OFMutableDictionary dictionary]; + + while ((line = [sock readLine]) != nil) { + OFString *key, *value; + const char *line_c = [line cString], *tmp; + + if ([line isEqual: @""]) + break; + + if ((tmp = strchr(line_c, ':')) == NULL) + @throw [OFInvalidServerReplyException + newWithClass: isa]; + + key = [OFString stringWithCString: line_c + length: tmp - line_c]; + + do { + tmp++; + } while (*tmp == ' '); + + value = [OFString stringWithCString: tmp]; + + if (redirects > 0 && (status == 301 || status == 302 || + status == 303) && [key caseInsensitiveCompare: + @"Location"] == OF_ORDERED_SAME) { + OFURL *new; + + new = [[OFURL alloc] initWithString: value + relativeToURL: URL]; + [URL release]; + URL = new; + + if (status == 303) { + requestType = OF_HTTP_REQUEST_TYPE_GET; + [queryString release]; + queryString = nil; + } + + [pool release]; + pool = nil; + + return [self resultWithRedirects: + redirects - 1]; + } + + [s_headers setObject: value + forKey: key]; + } + + data = [[sock readDataArrayTillEndOfStream] retain]; + + result = [[OFHTTPRequestResult alloc] + initWithStatusCode: status + headers: s_headers + data: data]; + } @finally { + [pool release]; + } + + return [result autorelease]; +} +@end + +@implementation OFHTTPRequestResult +- initWithStatusCode: (short)status + headers: (OFDictionary*)headers_ + data: (OFDataArray*)data_ +{ + self = [super init]; + + statusCode = status; + data = [data_ retain]; + headers = [headers_ copy]; + + return self; +} + +- (void)dealloc +{ + [data release]; + [headers release]; + + [super dealloc]; +} + +- (short)statusCode +{ + return statusCode; +} + +- (OFDictionary*)headers +{ + return [[headers copy] autorelease]; +} + +- (OFDataArray*)data +{ + return [[data retain] autorelease]; +} +@end Index: src/ObjFW.h ================================================================== --- src/ObjFW.h +++ src/ObjFW.h @@ -38,10 +38,12 @@ #import "OFStream.h" #import "OFFile.h" #import "OFStreamSocket.h" #import "OFTCPSocket.h" #import "OFStreamObserver.h" + +#import "OFHTTPRequest.h" #import "OFHash.h" #import "OFMD5Hash.h" #import "OFSHA1Hash.h"