Index: src/OFDataArray.m ================================================================== --- src/OFDataArray.m +++ src/OFDataArray.m @@ -25,18 +25,20 @@ #import "OFFile.h" #import "OFURL.h" #import "OFHTTPClient.h" #import "OFHTTPRequest.h" #import "OFHTTPRequestReply.h" +#import "OFDictionary.h" #import "OFXMLElement.h" #import "OFSystemInfo.h" #import "OFHTTPRequestFailedException.h" #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" #import "OFOutOfMemoryException.h" #import "OFOutOfRangeException.h" +#import "OFTruncatedDataException.h" #import "autorelease.h" #import "base64.h" #import "macros.h" @@ -142,10 +144,12 @@ { void *pool; OFHTTPClient *client; OFHTTPRequest *request; OFHTTPRequestReply *reply; + OFDictionary *headers; + OFString *contentLength; Class c; c = [self class]; [self release]; @@ -165,12 +169,20 @@ @throw [OFHTTPRequestFailedException exceptionWithClass: [request class] request: request reply: reply]; - self = [[reply data] retain]; + self = [[reply readDataArrayTillEndOfStream] retain]; + + headers = [reply headers]; + if ((contentLength = [headers objectForKey: @"Content-Length"]) != nil) + if ([self count] != (size_t)[contentLength decimalValue]) + @throw [OFTruncatedDataException + exceptionWithClass: [self class]]; + objc_autoreleasePoolPop(pool); + return self; } - initWithStringRepresentation: (OFString*)string { Index: src/OFHTTPClient.h ================================================================== --- src/OFHTTPClient.h +++ src/OFHTTPClient.h @@ -59,25 +59,10 @@ - (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 @@ -103,17 +88,15 @@ * @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. @@ -148,27 +131,10 @@ * * @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 */ - (OFHTTPRequestReply*)performRequest: (OFHTTPRequest*)request; Index: src/OFHTTPClient.m ================================================================== --- src/OFHTTPClient.m +++ src/OFHTTPClient.m @@ -25,11 +25,10 @@ #import "OFString.h" #import "OFURL.h" #import "OFTCPSocket.h" #import "OFDictionary.h" #import "OFDataArray.h" -#import "OFSystemInfo.h" #import "OFHTTPRequestFailedException.h" #import "OFInvalidEncodingException.h" #import "OFInvalidFormatException.h" #import "OFInvalidServerReplyException.h" @@ -59,26 +58,134 @@ firstLetter = NO; str++; } } + +@interface OFHTTPClientReply: OFHTTPRequestReply +{ + OFTCPSocket *sock; + BOOL chunked, atEndOfStream; + size_t toRead; +} + +- initWithSocket: (OFTCPSocket*)sock; +@end + +@implementation OFHTTPClientReply +- initWithSocket: (OFTCPSocket*)sock_ +{ + self = [super init]; + + sock = [sock_ retain]; + + return self; +} + +- (void)dealloc +{ + [sock release]; + + [super dealloc]; +} + +- (void)setHeaders: (OFDictionary*)headers_ +{ + [super setHeaders: headers_]; + + chunked = [[headers_ objectForKey: @"Transfer-Encoding"] + isEqual: @"chunked"]; +} + +- (size_t)lowlevelReadIntoBuffer: (void*)buffer + length: (size_t)length +{ + if (!chunked) + return [sock readIntoBuffer: buffer + length: length]; + + if (toRead > 0) { + if (length > toRead) + length = toRead; + + length = [sock readIntoBuffer: buffer + length: length]; + + toRead -= length; + + if (toRead == 0) + if ([[sock readLine] length] > 0) + @throw [OFInvalidServerReplyException + exceptionWithClass: [self class]]; + + return length; + } else { + void *pool = objc_autoreleasePoolPush(); + OFString *line; + 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) { + [sock close]; + atEndOfStream = YES; + } + + objc_autoreleasePoolPop(pool); + + return 0; + } +} + +- (BOOL)lowlevelIsAtEndOfStream +{ + if (!chunked) + return [sock isAtEndOfStream]; + + return atEndOfStream; +} + +- (int)fileDescriptorForReading +{ + return [sock fileDescriptorForReading]; +} + +- (size_t)pendingBytes +{ + return [super pendingBytes] + [sock pendingBytes]; +} + +- (void)close +{ + [sock close]; +} +@end @implementation OFHTTPClient + (instancetype)client { return [[[self alloc] init] autorelease]; } -- init -{ - self = [super init]; - - storesData = YES; - - return self; -} - - (void)setDelegate: (id )delegate_ { delegate = delegate_; } @@ -95,20 +202,10 @@ - (BOOL)insecureRedirectsAllowed { return insecureRedirectsAllowed; } -- (void)setStoresData: (BOOL)storesData_ -{ - storesData = storesData_; -} - -- (BOOL)storesData -{ - return storesData; -} - - (OFHTTPRequestReply*)performRequest: (OFHTTPRequest*)request { return [self performRequest: request redirects: 10]; } @@ -121,23 +218,17 @@ OFString *scheme = [URL scheme]; of_http_request_type_t requestType = [request requestType]; OFDictionary *headers = [request headers]; OFDataArray *POSTData = [request POSTData]; OFTCPSocket *sock; - OFHTTPRequestReply *reply; + OFHTTPClientReply *reply; OFString *line, *path, *version; OFMutableDictionary *serverHeaders; - OFDataArray *data; OFEnumerator *keyEnumerator, *objectEnumerator; - OFString *key, *object, *contentLengthHeader; + OFString *key, *object; int status; const char *type = NULL; - size_t contentLength = 0; - BOOL chunked; - size_t pageSize; - char *buffer; - size_t bytesReceived; if (![scheme isEqual: @"http"] && ![scheme isEqual: @"https"]) @throw [OFUnsupportedProtocolException exceptionWithClass: [self class] URL: URL]; @@ -177,14 +268,15 @@ if ([(path = [URL path]) length] == 0) path = @"/"; if ([URL query] != nil) - [sock writeFormat: @"%s %@?%@ HTTP/1.1\r\n", - type, path, [URL query]]; + [sock writeFormat: @"%s %@?%@ HTTP/%@\r\n", + type, path, [URL query], [request protocolVersionString]]; else - [sock writeFormat: @"%s %@ HTTP/1.1\r\n", type, path]; + [sock writeFormat: @"%s %@ HTTP/%@\r\n", + type, path, [request protocolVersionString]]; if ([URL port] == 80) [sock writeFormat: @"Host: %@\r\n", [URL host]]; else [sock writeFormat: @"Host: %@:%d\r\n", [URL host], @@ -340,164 +432,33 @@ } [serverHeaders setObject: value forKey: key]; } + + [serverHeaders makeImmutable]; if ([delegate respondsToSelector: @selector(client:didReceiveHeaders:statusCode:request:)]) [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]]; - } - - pageSize = [OFSystemInfo pageSize]; - buffer = [self allocMemoryWithSize: 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 < pageSize - ? toRead : pageSize); - - length = [sock readIntoBuffer: buffer - length: length]; - - if ([delegate respondsToSelector: - @selector(client:didReceiveData: - length:request:)]) - [delegate client: self - didReceiveData: buffer - length: length - request: request]; - - objc_autoreleasePoolPop(pool2); - pool2 = objc_autoreleasePoolPush(); - - bytesReceived += length; - [data addItems: buffer - count: length]; - - toRead -= length; - } - - @try { - line = [sock readLine]; - } @catch (OFInvalidEncodingException *e) { - @throw [OFInvalidServerReplyException - exceptionWithClass: [self class]]; - } - - if ([line length] > 0) - @throw [OFInvalidServerReplyException - exceptionWithClass: [self class]]; - - objc_autoreleasePoolPop(pool2); - } - } else { - size_t length; - - while (![sock isAtEndOfStream]) { - void *pool2; - - length = [sock readIntoBuffer: buffer - length: pageSize]; - - pool2 = objc_autoreleasePoolPush(); - - if ([delegate respondsToSelector: - @selector(client:didReceiveData:length: - request:)]) - [delegate client: self - didReceiveData: buffer - length: length - request: request]; - - objc_autoreleasePoolPop(pool2); - - bytesReceived += length; - [data addItems: 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]; - - reply = [[OFHTTPRequestReply alloc] initWithStatusCode: status - headers: serverHeaders - data: data]; + reply = [[OFHTTPClientReply alloc] initWithSocket: sock]; + [reply setProtocolVersionFromString: version]; + [reply setStatusCode: status]; + [reply setHeaders: serverHeaders]; objc_autoreleasePoolPop(pool); [reply autorelease]; - if (status != 200) + if (status / 100 != 2) @throw [OFHTTPRequestFailedException exceptionWithClass: [self class] request: request reply: reply]; return reply; } @end Index: src/OFHTTPRequest.h ================================================================== --- src/OFHTTPRequest.h +++ src/OFHTTPRequest.h @@ -33,26 +33,38 @@ OF_HTTP_REQUEST_TYPE_POST, /*! HEAD */ OF_HTTP_REQUEST_TYPE_HEAD } of_http_request_type_t; +/*! + * @brief The HTTP version of the HTTP request. + */ +typedef struct of_http_request_protocol_version_t { + /*! The major of the HTTP version */ + uint8_t major; + /*! The minor of the HTTP version */ + uint8_t minor; +} of_http_request_protocol_version_t; + /*! * @brief A class for storing HTTP requests. */ @interface OFHTTPRequest: OFObject { OFURL *URL; of_http_request_type_t requestType; + of_http_request_protocol_version_t protocolVersion; OFDictionary *headers; OFDataArray *POSTData; OFString *MIMEType; OFString *remoteAddress; } #ifdef OF_HAVE_PROPERTIES @property (copy) OFURL *URL; @property of_http_request_type_t requestType; +@property of_http_request_protocol_version_t protocolVersion; @property (copy) OFDictionary *headers; @property (retain) OFDataArray *POSTData; @property (copy) OFString *MIMEType; @property (copy) OFString *remoteAddress; #endif @@ -106,10 +118,39 @@ * * @return The request type of the HTTP request */ - (of_http_request_type_t)requestType; +/*! + * @brief Sets the protocol version of the HTTP request. + * + * @param protocolVersion The protocol version of the HTTP request + */ +- (void)setProtocolVersion: (of_http_request_protocol_version_t)protocolVersion; + +/*! + * @brief Returns the protocol version of the HTTP request. + * + * @return The protocol version of the HTTP request + */ +- (of_http_request_protocol_version_t)protocolVersion; + +/*! + * @brief Sets the protocol version of the HTTP request to the version + * described by the specified string. + * + * @param string A string describing an HTTP version + */ +- (void)setProtocolVersionFromString: (OFString*)string; + +/*! + * @brief Returns the protocol version of the HTTP request as a string. + * + * @return The protocol version of the HTTP request as a string + */ +- (OFString*)protocolVersionString; + /*! * @brief Sets a dictionary with headers for the HTTP request. * * @param headers A dictionary with headers for the HTTP request */ Index: src/OFHTTPRequest.m ================================================================== --- src/OFHTTPRequest.m +++ src/OFHTTPRequest.m @@ -19,14 +19,19 @@ #import "OFHTTPRequest.h" #import "OFString.h" #import "OFURL.h" #import "OFDictionary.h" #import "OFDataArray.h" +#import "OFArray.h" #import "autorelease.h" #import "macros.h" +#import "OFInvalidFormatException.h" +#import "OFOutOfRangeException.h" +#import "OFUnsupportedVersionException.h" + @implementation OFHTTPRequest + (instancetype)request { return [[[self alloc] init] autorelease]; } @@ -39,10 +44,12 @@ - init { self = [super init]; requestType = OF_HTTP_REQUEST_TYPE_GET; + protocolVersion.major = 1; + protocolVersion.minor = 1; return self; } - initWithURL: (OFURL*)URL_ @@ -87,10 +94,58 @@ - (of_http_request_type_t)requestType { return requestType; } + +- (void)setProtocolVersion: (of_http_request_protocol_version_t)protocolVersion_ +{ + if (protocolVersion_.major != 1 || protocolVersion.minor > 1) + @throw [OFUnsupportedVersionException + exceptionWithClass: [self class] + version: [OFString stringWithFormat: @"%u.%u", + protocolVersion_.major, + protocolVersion_.minor]]; + + protocolVersion = protocolVersion_; +} + +- (of_http_request_protocol_version_t)protocolVersion +{ + return protocolVersion; +} + +- (void)setProtocolVersionFromString: (OFString*)string +{ + void *pool = objc_autoreleasePoolPush(); + OFArray *components = [string componentsSeparatedByString: @"."]; + intmax_t major, minor; + of_http_request_protocol_version_t protocolVersion_; + + if ([components count] != 2) + @throw [OFInvalidFormatException + exceptionWithClass: [self class]]; + + major = [[components firstObject] decimalValue]; + minor = [[components lastObject] decimalValue]; + + if (major < 0 || major > UINT8_MAX || minor < 0 || minor > UINT8_MAX) + @throw [OFOutOfRangeException exceptionWithClass: [self class]]; + + protocolVersion_.major = (uint8_t)major; + protocolVersion_.minor = (uint8_t)minor; + + [self setProtocolVersion: protocolVersion_]; + + objc_autoreleasePoolPop(pool); +} + +- (OFString*)protocolVersionString +{ + return [OFString stringWithFormat: @"%u.%u", protocolVersion.major, + protocolVersion.minor]; +} - (void)setHeaders: (OFDictionary*)headers_ { OF_SETTER(headers, headers_, YES, 1) } Index: src/OFHTTPRequestReply.h ================================================================== --- src/OFHTTPRequestReply.h +++ src/OFHTTPRequestReply.h @@ -12,71 +12,83 @@ * 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" +#import "OFStream.h" +#import "OFHTTPRequest.h" -@class OFDataArray; @class OFDictionary; /*! - * @brief A class for storing a reply to an HTTP request. + * @brief A class for representing an HTTP request reply as a stream. */ -@interface OFHTTPRequestReply: OFObject +@interface OFHTTPRequestReply: OFStream { + of_http_request_protocol_version_t protocolVersion; short statusCode; - OFDataArray *data; OFDictionary *headers; } #ifdef OF_HAVE_PROPERTIES -@property (readonly) short statusCode; -@property (readonly, copy) OFDictionary *headers; -@property (readonly, retain) OFDataArray *data; +@property of_http_request_protocol_version_t protocolVersion; +@property short statusCode; +@property (copy) OFDictionary *headers; #endif /*! - * @brief Creates a new OFHTTPRequestReply. - * - * @param status The HTTP status code replied to the request - * @param headers The headers replied to the request - * @param data The data replied to the request - * @return A new OFHTTPRequestReply - */ -+ replyWithStatusCode: (short)status - headers: (OFDictionary*)headers - data: (OFDataArray*)data; - -/*! - * @brief Initializes an already allocated OFHTTPRequestReply. - * - * @param status The HTTP status code replied to the request - * @param headers The headers replied to the request - * @param data The data replied to the request - * @return An initialized OFHTTPRequestReply - */ -- initWithStatusCode: (short)status - headers: (OFDictionary*)headers - data: (OFDataArray*)data; - -/*! - * @brief Returns the state code of the reply of the HTTP request. - * - * @return The status code of the reply of the HTTP request + * @brief Sets the protocol version of the HTTP request reply. + * + * @param protocolVersion The protocol version of the HTTP request reply + */ +- (void)setProtocolVersion: (of_http_request_protocol_version_t)protocolVersion; + +/*! + * @brief Returns the protocol version of the HTTP request reply. + * + * @return The protocol version of the HTTP request reply + */ +- (of_http_request_protocol_version_t)protocolVersion; + +/*! + * @brief Sets the protocol version of the HTTP request reply to the version + * described by the specified string. + * + * @param string A string describing an HTTP version + */ +- (void)setProtocolVersionFromString: (OFString*)string; + +/*! + * @brief Returns the protocol version of the HTTP request reply as a string. + * + * @return The protocol version of the HTTP request reply as a string + */ +- (OFString*)protocolVersionString; + +/*! + * @brief Returns the status code of the reply to the HTTP request. + * + * @return The status code of the reply to the HTTP request */ - (short)statusCode; /*! - * @brief Returns the headers of the reply of the HTTP request. + * @brief Sets the status code of the reply to the HTTP request. + * + * @param statusCode The status code of the reply to the HTTP request + */ +- (void)setStatusCode: (short)statusCode; + +/*! + * @brief Returns the headers of the reply to the HTTP request. * - * @return The headers of the reply of the HTTP request + * @return The headers of the reply to the HTTP request */ - (OFDictionary*)headers; /*! - * @brief Returns the data received for the HTTP request. + * @brief Returns the headers of the reply to the HTTP request. * - * @return The data received for the HTTP request + * @param headers The headers of the reply to the HTTP request */ -- (OFDataArray*)data; +- (void)setHeaders: (OFDictionary*)headers; @end Index: src/OFHTTPRequestReply.m ================================================================== --- src/OFHTTPRequestReply.m +++ src/OFHTTPRequestReply.m @@ -16,87 +16,122 @@ #include "config.h" #import "OFHTTPRequestReply.h" #import "OFString.h" -#import "OFDataArray.h" #import "OFDictionary.h" +#import "OFArray.h" #import "autorelease.h" #import "macros.h" +#import "OFInvalidFormatException.h" +#import "OFOutOfRangeException.h" +#import "OFUnsupportedVersionException.h" + @implementation OFHTTPRequestReply -+ replyWithStatusCode: (short)status - headers: (OFDictionary*)headers - data: (OFDataArray*)data -{ - return [[[self alloc] initWithStatusCode: status - headers: headers - data: data] autorelease]; -} - -- initWithStatusCode: (short)status - headers: (OFDictionary*)headers_ - data: (OFDataArray*)data_ +- init { self = [super init]; - @try { - statusCode = status; - headers = [headers_ copy]; - data = [data_ retain]; - } @catch (id e) { - [self release]; - @throw e; - } + protocolVersion.major = 1; + protocolVersion.minor = 1; return self; } - (void)dealloc { [headers release]; - [data release]; [super dealloc]; } + +- (void)setProtocolVersion: (of_http_request_protocol_version_t)protocolVersion_ +{ + if (protocolVersion_.major != 1 || protocolVersion.minor > 1) + @throw [OFUnsupportedVersionException + exceptionWithClass: [self class] + version: [OFString stringWithFormat: @"%u.%u", + protocolVersion_.major, + protocolVersion_.minor]]; + + protocolVersion = protocolVersion_; +} + +- (of_http_request_protocol_version_t)protocolVersion +{ + return protocolVersion; +} + +- (void)setProtocolVersionFromString: (OFString*)string +{ + void *pool = objc_autoreleasePoolPush(); + OFArray *components = [string componentsSeparatedByString: @"."]; + intmax_t major, minor; + of_http_request_protocol_version_t protocolVersion_; + + if ([components count] != 2) + @throw [OFInvalidFormatException + exceptionWithClass: [self class]]; + + major = [[components firstObject] decimalValue]; + minor = [[components lastObject] decimalValue]; + + if (major < 0 || major > UINT8_MAX || minor < 0 || minor > UINT8_MAX) + @throw [OFOutOfRangeException exceptionWithClass: [self class]]; + + protocolVersion_.major = (uint8_t)major; + protocolVersion_.minor = (uint8_t)minor; + + [self setProtocolVersion: protocolVersion_]; + + objc_autoreleasePoolPop(pool); +} + +- (OFString*)protocolVersionString +{ + return [OFString stringWithFormat: @"%u.%u", protocolVersion.major, + protocolVersion.minor]; +} - (short)statusCode { return statusCode; } + +- (void)setStatusCode: (short)statusCode_ +{ + statusCode = statusCode_; +} - (OFDictionary*)headers { OF_GETTER(headers, YES) } -- (OFDataArray*)data +- (void)setHeaders: (OFDictionary*)headers_ { - OF_GETTER(data, YES) + OF_SETTER(headers, headers_, YES, YES) } - (OFString*)description { void *pool = objc_autoreleasePoolPush(); - OFString *indentedHeaders, *indentedData, *ret; + OFString *indentedHeaders, *ret; indentedHeaders = [[headers description] - stringByReplacingOccurrencesOfString: @"\n" - withString: @"\n\t"]; - indentedData = [[data description] stringByReplacingOccurrencesOfString: @"\n" withString: @"\n\t"]; ret = [[OFString alloc] initWithFormat: @"<%@:\n" @"\tStatus code = %d\n" @"\tHeaders = %@\n" - @"\tData = %@\n" @">", - [self class], statusCode, indentedHeaders, indentedData]; + [self class], statusCode, indentedHeaders]; objc_autoreleasePoolPop(pool); return [ret autorelease]; } @end Index: src/OFHTTPServer.h ================================================================== --- src/OFHTTPServer.h +++ src/OFHTTPServer.h @@ -30,14 +30,15 @@ * @brief This method is called when the HTTP server received a request from a * client. * * @param server The HTTP server which received the request * @param request The request the HTTP server received - * @return The reply the HTTP server should send to the client + * @param reply The reply the server wants to send to the client */ -- (OFHTTPRequestReply*)server: (OFHTTPServer*)server - didReceiveRequest: (OFHTTPRequest*)request; +- (void)server: (OFHTTPServer*)server + didReceiveRequest: (OFHTTPRequest*)request + reply: (OFHTTPRequestReply*)reply; #ifdef OF_HAVE_OPTIONAL_PROTOCOLS @optional #endif /*! Index: src/OFHTTPServer.m ================================================================== --- src/OFHTTPServer.m +++ src/OFHTTPServer.m @@ -39,11 +39,10 @@ #import "macros.h" #define BUFFER_SIZE 1024 /* - * TODO: Add support for chunked transfer encoding. * FIXME: Key normalization replaces headers like "DNT" with "Dnt". * FIXME: Errors are not reported to the user. */ static const char* @@ -161,10 +160,120 @@ } return [OFString stringWithUTF8StringNoCopy: cString freeWhenDone: YES]; } + +@interface OFHTTPServerReply: OFHTTPRequestReply +{ + OFTCPSocket *sock; + OFHTTPServer *server; + BOOL chunked, headersSent, closed; +} + +- initWithSocket: (OFTCPSocket*)sock + server: (OFHTTPServer*)server; +@end + +@implementation OFHTTPServerReply +- initWithSocket: (OFTCPSocket*)sock_ + server: (OFHTTPServer*)server_ +{ + self = [super init]; + + statusCode = 500; + sock = [sock_ retain]; + server = [server_ retain]; + + return self; +} + +- (void)dealloc +{ + if (!closed) + [self close]; + + [sock release]; + [server release]; + + [super dealloc]; +} + +- (void)sendHeaders +{ + void *pool = objc_autoreleasePoolPush(); + OFString *date = [[OFDate date] + dateStringWithFormat: @"%a, %d %b %Y %H:%M:%S GMT"]; + OFEnumerator *keyEnumerator, *valueEnumerator; + OFString *key, *value; + + [sock writeFormat: @"HTTP/%@ %d %s\r\n" + @"Server: %@\r\n" + @"Date: %@\r\n", + [self protocolVersionString], statusCode, + status_code_to_string(statusCode), + [server name], date]; + + keyEnumerator = [headers keyEnumerator]; + valueEnumerator = [headers objectEnumerator]; + while ((key = [keyEnumerator nextObject]) != nil && + (value = [valueEnumerator nextObject]) != nil) + if (![key isEqual: @"Server"] && ![key isEqual: @"Date"]) + [sock writeFormat: @"%@: %@\r\n", key, value]; + + [sock writeString: @"\r\n"]; + + headersSent = YES; + chunked = [[headers objectForKey: @"Transfer-Encoding"] + isEqual: @"chunked"]; + + objc_autoreleasePoolPop(pool); +} + +- (void)lowlevelWriteBuffer: (const void*)buffer + length: (size_t)length +{ + void *pool; + + if (!headersSent) + [self sendHeaders]; + + if (!chunked) { + [sock writeBuffer: buffer + length: length]; + return; + } + + pool = objc_autoreleasePoolPush(); + [sock writeString: [OFString stringWithFormat: @"%zx\r\n", length]]; + objc_autoreleasePoolPop(pool); + + [sock writeBuffer: buffer + length: length]; + [sock writeBuffer: "\r\n" + length: 2]; +} + +- (void)close +{ + if (!headersSent) + [self sendHeaders]; + + if (chunked) + [sock writeBuffer: "0\r\n\r\n" + length: 5]; + + [sock close]; + + closed = YES; +} + +- (int)fileDescriptorForWriting +{ + return [sock fileDescriptorForWriting]; +} +@end @interface OFHTTPServer_Connection: OFObject { OFTCPSocket *sock; OFHTTPServer *server; @@ -193,11 +302,11 @@ - (BOOL)socket: (OFTCPSocket*)sock didReadIntoBuffer: (const char*)buffer length: (size_t)length exception: (OFException*)exception; - (BOOL)sendErrorAndClose: (short)statusCode; -- (void)sendReply; +- (void)createReply; @end @implementation OFHTTPServer_Connection - initWithSocket: (OFTCPSocket*)sock_ server: (OFHTTPServer*)server_ @@ -252,11 +361,11 @@ case PARSING_HEADERS: if (![self parseHeaders: line]) return NO; if (state == SEND_REPLY) { - [self sendReply]; + [self createReply]; return NO; } return YES; default: @@ -421,11 +530,11 @@ [POSTData addItems: buffer count: length]; if ([POSTData count] >= contentLength) { @try { - [self sendReply]; + [self createReply]; } @catch (OFWriteFailedException *e) { return NO; } return NO; @@ -450,20 +559,15 @@ [sock close]; return NO; } -- (void)sendReply +- (void)createReply { OFURL *URL; OFHTTPRequest *request; - OFHTTPRequestReply *reply; - OFDictionary *replyHeaders; - OFDataArray *replyData; - OFString *date; - OFEnumerator *keyEnumerator, *valueEnumerator; - OFString *key, *value; + OFHTTPServerReply *reply; size_t pos; [timer invalidate]; [timer release]; timer = nil; @@ -495,53 +599,23 @@ } else [URL setPath: path]; request = [OFHTTPRequest requestWithURL: URL]; [request setRequestType: requestType]; + [request setProtocolVersion: + (of_http_request_protocol_version_t){ 1, HTTPMinorVersion }]; [request setHeaders: headers]; [request setPOSTData: POSTData]; [request setRemoteAddress: [sock remoteAddress]]; - reply = [[server delegate] server: server - didReceiveRequest: request]; - - date = [[OFDate date] - dateStringWithFormat: @"%a, %d %b %Y %H:%M:%S GMT"]; - - if (reply == nil) { - [self sendErrorAndClose: 500]; - @throw [OFInvalidArgumentException - exceptionWithClass: [(id)[server delegate] class] - selector: @selector(server:didReceiveRequest:)]; - } - - replyHeaders = [reply headers]; - replyData = [reply data]; - - [sock writeFormat: @"HTTP/1.1 %d %s\r\n" - @"Server: %@\r\n" - @"Date: %@\r\n", - [reply statusCode], - status_code_to_string([reply statusCode]), - [server name], date]; - - if (requestType != OF_HTTP_REQUEST_TYPE_HEAD) - [sock writeFormat: @"Content-Length: %zu\r\n", - [replyData count] * [replyData itemSize]]; - - keyEnumerator = [replyHeaders keyEnumerator]; - valueEnumerator = [replyHeaders objectEnumerator]; - while ((key = [keyEnumerator nextObject]) != nil && - (value = [valueEnumerator nextObject]) != nil) - if (![key isEqual: @"Server"] && ![key isEqual: @"Date"]) - [sock writeFormat: @"%@: %@\r\n", key, value]; - - [sock writeString: @"\r\n"]; - - if (requestType != OF_HTTP_REQUEST_TYPE_HEAD) - [sock writeBuffer: [replyData items] - length: [replyData count] * [replyData itemSize]]; + reply = [[[OFHTTPServerReply alloc] + initWithSocket: sock + server: server] autorelease]; + + [[server delegate] server: server + didReceiveRequest: request + reply: reply]; } @end @implementation OFHTTPServer + (instancetype)server Index: src/OFString.m ================================================================== --- src/OFString.m +++ src/OFString.m @@ -41,10 +41,11 @@ #import "OFInvalidFormatException.h" #import "OFNotImplementedException.h" #import "OFOpenFileFailedException.h" #import "OFOutOfMemoryException.h" #import "OFOutOfRangeException.h" +#import "OFTruncatedDataException.h" #import "autorelease.h" #import "macros.h" #import "of_asprintf.h" #import "unicode.h" @@ -899,11 +900,13 @@ { void *pool; OFHTTPClient *client; OFHTTPRequest *request; OFHTTPRequestReply *reply; - OFString *contentType; + OFDictionary *headers; + OFString *contentType, *contentLength; + OFDataArray *data; Class c; c = [self class]; [self release]; @@ -926,13 +929,15 @@ if ([reply statusCode] != 200) @throw [OFHTTPRequestFailedException exceptionWithClass: [request class] request: request reply: reply]; + + headers = [reply headers]; if (encoding == OF_STRING_ENCODING_AUTODETECT && - (contentType = [[reply headers] objectForKey: @"Content-Type"])) { + (contentType = [headers objectForKey: @"Content-Type"]) != nil) { contentType = [contentType lowercaseString]; if ([contentType hasSuffix: @"charset=utf-8"]) encoding = OF_STRING_ENCODING_UTF_8; if ([contentType hasSuffix: @"charset=iso-8859-1"]) @@ -944,15 +949,23 @@ } if (encoding == OF_STRING_ENCODING_AUTODETECT) encoding = OF_STRING_ENCODING_UTF_8; - self = [[c alloc] initWithCString: (char*)[[reply data] items] + data = [reply readDataArrayTillEndOfStream]; + + if ((contentLength = [headers objectForKey: @"Content-Length"]) != nil) + if ([data count] != (size_t)[contentLength decimalValue]) + @throw [OFTruncatedDataException + exceptionWithClass: [self class]]; + + self = [[c alloc] initWithCString: (char*)[data items] encoding: encoding - length: [[reply data] count]]; + length: [data count]]; objc_autoreleasePoolPop(pool); + return self; } - initWithSerialization: (OFXMLElement*)element { Index: tests/OFHTTPClientTests.m ================================================================== --- tests/OFHTTPClientTests.m +++ tests/OFHTTPClientTests.m @@ -29,10 +29,11 @@ #import "OFTCPSocket.h" #import "OFThread.h" #import "OFCondition.h" #import "OFURL.h" #import "OFDictionary.h" +#import "OFDataArray.h" #import "OFAutoreleasePool.h" #import "TestsAppDelegate.h" static OFString *module = @"OFHTTPClient"; @@ -96,10 +97,11 @@ OFHTTPClientTestsServer *server; OFURL *url; OFHTTPClient *client; OFHTTPRequest *request; OFHTTPRequestReply *reply; + OFDataArray *data; cond = [OFCondition condition]; [cond lock]; server = [[[OFHTTPClientTestsServer alloc] init] autorelease]; @@ -116,12 +118,16 @@ R(client = [OFHTTPClient client]) && R(request = [OFHTTPRequest requestWithURL: url]) && R(reply = [client performRequest: request])) TEST(@"Normalization of server header keys", - ([[reply headers] objectForKey: @"Content-Length"] != nil)) + ([[reply headers] objectForKey: @"Content-Length"] != nil)) + + TEST(@"Correct parsing of data", + (data = [reply readDataArrayTillEndOfStream]) && + [data count] == 7 && !memcmp([data items], "foo\nbar", 7)) [server join]; [pool drain]; } @end