/* * Copyright (c) 2008, 2009, 2010, 2011, 2012 * 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 "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]; } + (instancetype)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]; [super dealloc]; } - (void)setURL: (OFURL*)URL_ { OF_SETTER(URL, URL_, YES, 1) } - (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, 1) } - (OFString*)queryString { OF_GETTER(queryString, YES) } - (void)setHeaders: (OFDictionary*)headers_ { OF_SETTER(headers, headers_, YES, 1) } - (OFDictionary*)headers { OF_GETTER(headers, YES) } - (void)setRedirectsFromHTTPSToHTTPAllowed: (BOOL)allowed { redirectsFromHTTPSToHTTPAllowed = allowed; } - (BOOL)redirectsFromHTTPSToHTTPAllowed { return redirectsFromHTTPSToHTTPAllowed; } - (void)setDelegate: (id <OFHTTPRequestDelegate>)delegate_ { delegate = delegate_; } - (id <OFHTTPRequestDelegate>)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 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; } 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]; } @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 { OF_GETTER(headers, YES) } - (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 willFollowRedirectTo: (OFURL*)url { return YES; } @end