/*
* 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]
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\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