/* * 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 cString]; 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 newWithClass: isa URL: URL]; if ([scheme isEqual: @"http"]) sock = [OFTCPSocket socket]; else { if (of_http_request_tls_socket_class == Nil) @throw [OFUnsupportedProtocolException newWithClass: 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] onPort: [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\r\n"]; if ([headers objectForKey: @"Content-Length"] == nil) [sock writeFormat: @"Content-Length: %d\r\n", [queryString cStringLength]]; } [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 newWithClass: isa]; status = (int)[[line substringFromIndex: 9 toIndex: 12] decimalValue]; serverHeaders = [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]; normalize_key(key); do { tmp++; } while (*tmp == ' '); value = [OFString stringWithCString: 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 newWithClass: 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 newWithClass: isa]; } /* * Class swizzle the dictionary to be immutable. We pass it as * OFDictionary*, so it can't be modified anyway. But not * swizzling it would create a real copy each time -[copy] is * called. */ serverHeaders->isa = [OFDictionary class]; result = [[OFHTTPRequestResult alloc] initWithStatusCode: status headers: serverHeaders data: data]; if (status != 200 && status != 301 && status != 302 && status != 303) @throw [OFHTTPRequestFailedException newWithClass: 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