/*
* Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013
* 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"
#include <string.h>
#include <ctype.h>
#import "OFHTTPClient.h"
#import "OFHTTPRequest.h"
#import "OFHTTPRequestReply.h"
#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"
#import "OFOutOfMemoryException.h"
#import "OFOutOfRangeException.h"
#import "OFTruncatedDataException.h"
#import "OFUnsupportedProtocolException.h"
#import "OFUnsupportedVersionException.h"
#import "autorelease.h"
#import "macros.h"
static OF_INLINE void
normalize_key(char *str_)
{
uint8_t *str = (uint8_t*)str_;
BOOL firstLetter = YES;
while (*str != '\0') {
if (!isalnum(*str)) {
firstLetter = YES;
str++;
continue;
}
*str = (firstLetter ? toupper(*str) : tolower(*str));
firstLetter = NO;
str++;
}
}
@implementation OFHTTPClient
+ (instancetype)client
{
return [[[self alloc] init] autorelease];
}
- init
{
self = [super init];
storesData = YES;
return self;
}
- (void)setDelegate: (id <OFHTTPClientDelegate>)delegate_
{
delegate = delegate_;
}
- (id <OFHTTPClientDelegate>)delegate
{
return delegate;
}
- (void)setInsecureRedirectsAllowed: (BOOL)allowed
{
insecureRedirectsAllowed = allowed;
}
- (BOOL)insecureRedirectsAllowed
{
return insecureRedirectsAllowed;
}
- (void)setStoresData: (BOOL)storesData_
{
storesData = storesData_;
}
- (BOOL)storesData
{
return storesData;
}
- (OFHTTPRequestReply*)performRequest: (OFHTTPRequest*)request
{
return [self performRequest: request
redirects: 10];
}
- (OFHTTPRequestReply*)performRequest: (OFHTTPRequest*)request
redirects: (size_t)redirects
{
void *pool = objc_autoreleasePoolPush();
OFURL *URL = [request URL];
OFString *scheme = [URL scheme];
of_http_request_type_t requestType = [request requestType];
OFDictionary *headers = [request headers];
OFDataArray *POSTData = [request POSTData];
OFTCPSocket *sock;
OFHTTPRequestReply *reply;
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;
size_t pageSize;
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_tls_socket_class == Nil)
@throw [OFUnsupportedProtocolException
exceptionWithClass: [self class]
URL: URL];
sock = [[[of_tls_socket_class alloc] init] autorelease];
}
if ([delegate respondsToSelector:
@selector(client:didCreateSocket:request:)])
[delegate client: self
didCreateSocket: sock
request: request];
[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"];
if ([headers objectForKey: @"User-Agent"] == nil)
[sock writeString: @"User-Agent: Something using ObjFW "
@"<https://webkeks.org/objfw>\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) {
OFString *contentType = [request MIMEType];
if (contentType == nil)
contentType = @"application/x-www-form-urlencoded; "
@"charset=UTF-8\r\n";
[sock writeFormat: @"Content-Type: %@\r\n", contentType];
[sock writeFormat: @"Content-Length: %d\r\n",
[POSTData count] * [POSTData itemSize]];
}
[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 writeBuffer: [POSTData items]
length: [POSTData count] * [POSTData itemSize]];
@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 *lineC, *tmp;
char *keyC;
@try {
line = [sock readLine];
} @catch (OFInvalidEncodingException *e) {
@throw [OFInvalidServerReplyException
exceptionWithClass: [self class]];
}
if (line == nil)
@throw [OFInvalidServerReplyException
exceptionWithClass: [self class]];
if ([line isEqual: @""])
break;
lineC = [line UTF8String];
if ((tmp = strchr(lineC, ':')) == NULL)
@throw [OFInvalidServerReplyException
exceptionWithClass: [self class]];
if ((keyC = malloc(tmp - lineC + 1)) == NULL)
@throw [OFOutOfMemoryException
exceptionWithClass: [self class]
requestedSize: tmp - lineC + 1];
memcpy(keyC, lineC, tmp - lineC);
keyC[tmp - lineC] = '\0';
normalize_key(keyC);
@try {
key = [OFString stringWithUTF8StringNoCopy: keyC
freeWhenDone: YES];
} @catch (id e) {
free(keyC);
@throw e;
}
do {
tmp++;
} while (*tmp == ' ');
value = [OFString stringWithUTF8String: tmp];
if ((redirects > 0 && (status == 301 || status == 302 ||
status == 303 || status == 307) &&
[key isEqual: @"Location"]) && (insecureRedirectsAllowed ||
[scheme isEqual: @"http"] ||
![value hasPrefix: @"http://"])) {
OFURL *newURL;
OFHTTPRequest *newRequest;
BOOL follow = YES;
newURL = [OFURL URLWithString: value
relativeToURL: URL];
if ([delegate respondsToSelector:
@selector(client:shouldFollowRedirect:request:)])
follow = [delegate client: self
shouldFollowRedirect: newURL
request: request];
if (!follow) {
[serverHeaders setObject: value
forKey: key];
continue;
}
newRequest = [OFHTTPRequest requestWithURL: newURL];
[newRequest setRequestType: requestType];
[newRequest setHeaders: headers];
[newRequest setPOSTData: POSTData];
[newRequest setMIMEType: [request MIMEType]];
if (status == 303) {
[newRequest
setRequestType: OF_HTTP_REQUEST_TYPE_GET];
[newRequest setPOSTData: nil];
[newRequest setMIMEType: nil];
}
[newRequest retain];
objc_autoreleasePoolPop(pool);
[newRequest autorelease];
return [self performRequest: newRequest
redirects: redirects - 1];
}
[serverHeaders setObject: value
forKey: key];
}
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 isEqual: @""])
@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];
objc_autoreleasePoolPop(pool);
[reply autorelease];
if (status != 200)
@throw [OFHTTPRequestFailedException
exceptionWithClass: [self class]
request: request
reply: reply];
return reply;
}
@end