Artifact 66f489833ff35b0ca70d735918c7164bc1cb7a596ac03f305807db17f0eb705a:
- File
src/OFHTTPRequest.m
— part of check-in
[e1e7ffa903]
at
2011-09-22 23:25:42
on branch trunk
— Exceptions are now autoreleased.
This is safe as an "exception loop" can't happen, since if allocating
an exception fails, it throws an OFAllocFailedException which is
preallocated and can always be thrown.So, the worst case would be that an autorelease of an exception fails,
triggering an OFOutOfMemoryException for which there is no memory,
resulting in an OFAllocFailedException to be thrown. (user: js, size: 10086) [annotate] [blame] [check-ins using]
/* * Copyright (c) 2008, 2009, 2010, 2011 * Jonathan Schleifer <js@webkeks.org> * * 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 <string.h> #include <ctype.h> #import "OFHTTPRequest.h" #import "OFString.h" #import "OFURL.h" #import "OFTCPSocket.h" #import "OFDictionary.h" #import "OFDataArray.h" #import "OFAutoreleasePool.h" #import "OFHTTPRequestFailedException.h" #import "OFInvalidServerReplyException.h" #import "OFOutOfRangeException.h" #import "OFTruncatedDataException.h" #import "OFUnsupportedProtocolException.h" #import "macros.h" Class of_http_request_tls_socket_class = Nil; static OF_INLINE void normalize_key(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 + request { return [[[self alloc] init] autorelease]; } + requestWithURL: (OFURL*)URL { return [[[self alloc] initWithURL: URL] autorelease]; } - init { self = [super init]; requestType = OF_HTTP_REQUEST_TYPE_GET; headers = [[OFDictionary alloc] initWithObject: @"Something using ObjFW " @"<https://webkeks.org/objfw/>" forKey: @"User-Agent"]; storesData = YES; return self; } - initWithURL: (OFURL*)URL_ { self = [self init]; @try { [self setURL: URL_]; } @catch (id e) { [self release]; @throw e; } return self; } - (void)dealloc { [URL release]; [queryString release]; [headers release]; [(id)delegate release]; [super dealloc]; } - (void)setURL: (OFURL*)URL_ { OF_SETTER(URL, URL_, YES, YES) } - (OFURL*)URL { OF_GETTER(URL, YES) } - (void)setRequestType: (of_http_request_type_t)requestType_ { requestType = requestType_; } - (of_http_request_type_t)requestType { return requestType; } - (void)setQueryString: (OFString*)queryString_ { OF_SETTER(queryString, queryString_, YES, YES) } - (OFString*)queryString { OF_GETTER(queryString, YES) } - (void)setHeaders: (OFDictionary*)headers_ { OF_SETTER(headers, headers_, YES, YES) } - (OFDictionary*)headers { OF_GETTER(headers, YES) } - (void)setRedirectsFromHTTPSToHTTPAllowed: (BOOL)allowed { redirectsFromHTTPSToHTTPAllowed = allowed; } - (BOOL)redirectsFromHTTPSToHTTPAllowed { return redirectsFromHTTPSToHTTPAllowed; } - (void)setDelegate: (id <OFHTTPRequestDelegate>)delegate_ { OF_SETTER(delegate, delegate_, YES, NO) } - (id <OFHTTPRequestDelegate>)delegate { OF_GETTER(delegate, YES) } - (void)setStoresData: (BOOL)storesData_ { storesData = storesData_; } - (BOOL)storesData { return storesData; } - (OFHTTPRequestResult*)perform { return [self performWithRedirects: 10]; } - (OFHTTPRequestResult*)performWithRedirects: (size_t)redirects { OFAutoreleasePool *pool = [[OFAutoreleasePool alloc] init]; OFString *scheme = [URL scheme]; OFTCPSocket *sock; OFHTTPRequestResult *result; if (![scheme isEqual: @"http"] && ![scheme isEqual: @"https"]) @throw [OFUnsupportedProtocolException exceptionWithClass: isa URL: URL]; if ([scheme isEqual: @"http"]) sock = [OFTCPSocket socket]; else { if (of_http_request_tls_socket_class == Nil) @throw [OFUnsupportedProtocolException exceptionWithClass: isa URL: URL]; sock = [[[of_http_request_tls_socket_class alloc] init] autorelease]; } @try { OFString *line, *path; OFMutableDictionary *serverHeaders; OFDataArray *data; OFEnumerator *keyEnumerator, *objectEnumerator; OFString *key, *object, *contentLengthHeader; int status; const char *type = NULL; char *buffer; size_t bytesReceived; [sock connectToHost: [URL host] port: [URL port]]; /* * Work around a bug with packet bisection in lighttpd when * using HTTPS. */ [sock setBuffersWrites: 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.0\r\n", type, path, [URL query]]; else [sock writeFormat: @"%s %@ HTTP/1.0\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]]; 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 setBuffersWrites: NO]; 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 exceptionWithClass: isa]; status = (int)[[line substringWithRange: of_range(9, 3)] decimalValue]; serverHeaders = [OFMutableDictionary dictionary]; while ((line = [sock readLine]) != nil) { OFString *key, *value; const char *line_c = [line UTF8String], *tmp; if ([line isEqual: @""]) break; if ((tmp = strchr(line_c, ':')) == NULL) @throw [OFInvalidServerReplyException exceptionWithClass: isa]; key = [OFString stringWithUTF8String: line_c length: tmp - line_c]; normalize_key(key); do { tmp++; } while (*tmp == ' '); value = [OFString stringWithUTF8String: tmp]; if ((redirects > 0 && (status == 301 || status == 302 || status == 303) && [key isEqual: @"Location"]) && (redirectsFromHTTPSToHTTPAllowed || [scheme isEqual: @"http"] || ![value hasPrefix: @"http://"])) { OFURL *new; BOOL follow; new = [OFURL URLWithString: value relativeToURL: URL]; follow = [delegate request: self willFollowRedirectTo: 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; } [pool release]; pool = nil; return [self performWithRedirects: redirects - 1]; } [serverHeaders setObject: value forKey: key]; } [delegate request: self didReceiveHeaders: serverHeaders withStatusCode: status]; if (storesData) data = [OFDataArray dataArray]; else data = nil; buffer = [self allocMemoryWithSize: of_pagesize]; bytesReceived = 0; @try { size_t len; while ((len = [sock readNBytes: of_pagesize intoBuffer: buffer]) > 0) { [delegate request: self didReceiveData: buffer withLength: len]; bytesReceived += len; [data addNItems: len fromCArray: buffer]; } } @finally { [self freeMemory: buffer]; } if ((contentLengthHeader = [serverHeaders objectForKey: @"Content-Length"]) != nil) { intmax_t cl = [contentLengthHeader decimalValue]; if (cl > SIZE_MAX) @throw [OFOutOfRangeException exceptionWithClass: isa]; /* * We only want to throw on these status codes as we * will throw an OFHTTPRequestFailedException for all * other status codes later. */ if (cl != bytesReceived && (status == 200 || status == 301 || status == 302 || status == 303)) @throw [OFTruncatedDataException exceptionWithClass: isa]; } [serverHeaders makeImmutable]; result = [[OFHTTPRequestResult alloc] initWithStatusCode: status headers: serverHeaders data: data]; if (status != 200 && status != 301 && status != 302 && status != 303) @throw [OFHTTPRequestFailedException exceptionWithClass: isa HTTPRequest: self result: result]; } @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 @implementation OFObject (OFHTTPRequestDelegate) - (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 willFollowRedirectTo: (OFURL*)url { return YES; } @end