/*
* 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_SERVER_M
#include <string.h>
#include <ctype.h>
#import "OFHTTPServer.h"
#import "OFDataArray.h"
#import "OFDate.h"
#import "OFDictionary.h"
#import "OFURL.h"
#import "OFHTTPRequest.h"
#import "OFTCPSocket.h"
#import "OFAlreadyConnectedException.h"
#import "OFInvalidArgumentException.h"
#import "OFInvalidFormatException.h"
#import "OFNotImplementedException.h"
#import "OFOutOfMemoryException.h"
#import "OFOutOfRangeException.h"
#import "OFWriteFailedException.h"
#import "macros.h"
#define BUFFER_SIZE 1024
/*
* FIXME: Currently, connections never time out, which means a DoS is possible.
* TODO: Add support for chunked transfer encoding.
* FIXME: Key normalization replaces headers like "DNT" with "Dnt".
* FIXME: It is currently not possible to stop the server.
* FIXME: Errors are not reported to the user.
*/
static const char*
status_code_to_string(short code)
{
switch (code) {
case 100:
return "Continue";
case 101:
return "Switching Protocols";
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 203:
return "Non-Authoritative Information";
case 204:
return "No Content";
case 205:
return "Reset Content";
case 206:
return "Partial Content";
case 300:
return "Multiple Choices";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 303:
return "See Other";
case 304:
return "Not Modified";
case 305:
return "Use Proxy";
case 307:
return "Temporary Redirect";
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 402:
return "Payment Required";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 406:
return "Not Acceptable";
case 407:
return "Proxy Authentication Required";
case 408:
return "Request Timeout";
case 409:
return "Conflict";
case 410:
return "Gone";
case 411:
return "Length Required";
case 412:
return "Precondition Failed";
case 413:
return "Request Entity Too Large";
case 414:
return "Request-URI Too Long";
case 415:
return "Unsupported Media Type";
case 416:
return "Requested Range Not Satisfiable";
case 417:
return "Expectation Failed";
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Timeout";
case 505:
return "HTTP Version Not Supported";
default:
return NULL;
}
}
static OF_INLINE OFString*
normalized_key(OFString *key)
{
char *cString = strdup([key UTF8String]);
uint8_t *tmp = (uint8_t*)cString;
BOOL firstLetter = YES;
if (cString == NULL)
@throw [OFOutOfMemoryException
exceptionWithClass: nil
requestedSize: strlen([key UTF8String])];
while (*tmp != '\0') {
if (!isalnum(*tmp)) {
firstLetter = YES;
tmp++;
continue;
}
*tmp = (firstLetter ? toupper(*tmp) : tolower(*tmp));
firstLetter = NO;
tmp++;
}
return [OFString stringWithUTF8StringNoCopy: cString
freeWhenDone: YES];
}
@interface OFHTTPServer_Connection: OFObject
{
OFTCPSocket *sock;
OFHTTPServer *server;
enum {
AWAITING_PROLOG,
PARSING_HEADERS,
SEND_REPLY
} state;
uint8_t HTTPMinorVersion;
of_http_request_type_t requestType;
OFString *host, *path;
uint16_t port;
OFMutableDictionary *headers;
size_t contentLength;
OFDataArray *POSTData;
}
- initWithSocket: (OFTCPSocket*)socket
server: (OFHTTPServer*)server;
- (BOOL)socket: (OFTCPSocket*)sock
didReadLine: (OFString*)line
exception: (OFException*)exception;
- (BOOL)parseProlog: (OFString*)line;
- (BOOL)parseHeaders: (OFString*)line;
- (BOOL)socket: (OFTCPSocket*)sock
didReadIntoBuffer: (const char*)buffer
length: (size_t)length
exception: (OFException*)exception;
- (BOOL)sendErrorAndClose: (short)statusCode;
- (void)sendReply;
@end
@implementation OFHTTPServer_Connection
- initWithSocket: (OFTCPSocket*)sock_
server: (OFHTTPServer*)server_
{
self = [super init];
sock = [sock_ retain];
server = server_;
state = AWAITING_PROLOG;
return self;
}
- (void)dealloc
{
[sock release];
[host release];
[path release];
[headers release];
[POSTData release];
[super dealloc];
}
- (BOOL)socket: (OFTCPSocket*)sock_
didReadLine: (OFString*)line
exception: (OFException*)exception
{
if (line == nil || exception != nil)
return NO;
@try {
switch (state) {
case AWAITING_PROLOG:
return [self parseProlog: line];
case PARSING_HEADERS:
if (![self parseHeaders: line])
return NO;
if (state == SEND_REPLY) {
[self sendReply];
return NO;
}
return YES;
default:
return NO;
}
} @catch (OFWriteFailedException *e) {
return NO;
}
}
- (BOOL)parseProlog: (OFString*)line
{
OFString *type;
size_t pos;
@try {
OFString *version = [line
substringWithRange: of_range([line length] - 9, 9)];
of_unichar_t tmp;
if (![version hasPrefix: @" HTTP/1."])
return [self sendErrorAndClose: 505];
tmp = [version characterAtIndex: 8];
if (tmp < '0' || tmp > '9')
return [self sendErrorAndClose: 400];
HTTPMinorVersion = (uint8_t)(tmp - '0');
} @catch (OFOutOfRangeException *e) {
return [self sendErrorAndClose: 400];
}
pos = [line rangeOfString: @" "].location;
if (pos == OF_NOT_FOUND)
return [self sendErrorAndClose: 400];
type = [line substringWithRange: of_range(0, pos)];
if ([type isEqual: @"GET"])
requestType = OF_HTTP_REQUEST_TYPE_GET;
else if ([type isEqual: @"POST"])
requestType = OF_HTTP_REQUEST_TYPE_POST;
else if ([type isEqual: @"HEAD"])
requestType = OF_HTTP_REQUEST_TYPE_HEAD;
else
return [self sendErrorAndClose: 501];
@try {
path = [line substringWithRange:
of_range(pos + 1, [line length] - pos - 10)];
} @catch (OFOutOfRangeException *e) {
return [self sendErrorAndClose: 400];
}
path = [[path stringByDeletingEnclosingWhitespaces] retain];
if (![path hasPrefix: @"/"])
return [self sendErrorAndClose: 400];
headers = [[OFMutableDictionary alloc] init];
state = PARSING_HEADERS;
return YES;
}
- (BOOL)parseHeaders: (OFString*)line
{
OFString *key, *value;
size_t pos;
if ([line isEqual: @""]) {
switch (requestType) {
case OF_HTTP_REQUEST_TYPE_GET:
case OF_HTTP_REQUEST_TYPE_HEAD:
state = SEND_REPLY;
break;
case OF_HTTP_REQUEST_TYPE_POST:;
OFString *tmp;
char *buffer;
tmp = [headers objectForKey: @"Content-Length"];
if (tmp == nil)
return [self sendErrorAndClose: 411];
@try {
contentLength = (size_t)[tmp decimalValue];
} @catch (OFInvalidFormatException *e) {
return [self sendErrorAndClose: 400];
}
buffer = [self allocMemoryWithSize: BUFFER_SIZE];
POSTData = [[OFDataArray alloc] init];
[sock asyncReadIntoBuffer: buffer
length: BUFFER_SIZE
target: self
selector: @selector(socket:
didReadIntoBuffer:
length:exception:)];
return NO;
}
return YES;
}
pos = [line rangeOfString: @":"].location;
if (pos == OF_NOT_FOUND)
return [self sendErrorAndClose: 400];
key = [line substringWithRange: of_range(0, pos)];
value = [line substringWithRange:
of_range(pos + 1, [line length] - pos - 1)];
key = normalized_key([key stringByDeletingTrailingWhitespaces]);
value = [value stringByDeletingLeadingWhitespaces];
[headers setObject: value
forKey: key];
if ([key isEqual: @"Host"]) {
pos = [value
rangeOfString: @":"
options: OF_STRING_SEARCH_BACKWARDS].location;
if (pos != OF_NOT_FOUND) {
host = [[value substringWithRange:
of_range(0, pos)] retain];
@try {
of_range_t range =
of_range(pos + 1, [value length] - pos - 1);
intmax_t portTmp = [[value
substringWithRange: range] decimalValue];
if (portTmp < 1 || portTmp > UINT16_MAX)
return [self sendErrorAndClose: 400];
port = (uint16_t)portTmp;
} @catch (OFInvalidFormatException *e) {
return [self sendErrorAndClose: 400];
}
} else {
host = [value retain];
port = 80;
}
}
return YES;
}
- (BOOL)socket: (OFTCPSocket*)sock_
didReadIntoBuffer: (const char*)buffer
length: (size_t)length
exception: (OFException*)exception
{
if ([sock_ isAtEndOfStream] || exception != nil)
return NO;
[POSTData addItems: buffer
count: length];
if ([POSTData count] >= contentLength) {
@try {
[self sendReply];
} @catch (OFWriteFailedException *e) {
return NO;
}
return NO;
}
return YES;
}
- (BOOL)sendErrorAndClose: (short)statusCode
{
OFString *date = [[OFDate date]
dateStringWithFormat: @"%a, %d %b %Y %H:%M:%S GMT"];
[sock writeFormat: @"HTTP/1.1 %d %s\r\n"
@"Date: %@\r\n"
@"Server: %@\r\n"
@"\r\n",
statusCode, status_code_to_string(statusCode), date,
[server name]];
[sock close];
return NO;
}
- (void)sendReply
{
OFURL *URL;
OFHTTPRequest *request;
OFHTTPRequestResult *reply;
OFDictionary *replyHeaders;
OFDataArray *replyData;
OFEnumerator *keyEnumerator, *valueEnumerator;
OFString *key, *value;
if (host == nil || port == 0) {
if (HTTPMinorVersion > 0) {
[self sendErrorAndClose: 400];
return;
}
host = [server host];
port = [server port];
}
URL = [OFURL URL];
[URL setScheme: @"http"];
[URL setHost: host];
[URL setPort: port];
[URL setPath: path];
request = [OFHTTPRequest requestWithURL: URL];
[request setRequestType: requestType];
[request setHeaders: headers];
[request setPOSTData: POSTData];
reply = [[server delegate] server: server
didReceiveRequest: request];
if (reply == nil) {
[self sendErrorAndClose: 500];
@throw [OFInvalidArgumentException
exceptionWithClass: [(id)[server delegate] class]
selector: @selector(server:didReceiveRequest:)];
}
replyHeaders = [reply headers];
replyData = [reply data];
[sock writeFormat: @"HTTP/1.1 %d %s\r\n"
@"Server: %@\r\n",
[reply statusCode],
status_code_to_string([reply statusCode]),
[server name]];
if ([replyHeaders objectForKey: @"Date"] == nil) {
OFString *date = [[OFDate date]
dateStringWithFormat: @"%a, %d %b %Y %H:%M:%S GMT"];
[sock writeFormat: @"Date: %@\r\n", date];
}
if (requestType != OF_HTTP_REQUEST_TYPE_HEAD)
[sock writeFormat: @"Content-Length: %zu\r\n",
[replyData count] * [replyData itemSize]];
keyEnumerator = [replyHeaders keyEnumerator];
valueEnumerator = [replyHeaders objectEnumerator];
while ((key = [keyEnumerator nextObject]) != nil &&
(value = [valueEnumerator nextObject]) != nil)
[sock writeFormat: @"%@: %@\r\n", key, value];
[sock writeString: @"\r\n"];
if (requestType != OF_HTTP_REQUEST_TYPE_HEAD)
[sock writeBuffer: [replyData items]
length: [replyData count] * [replyData itemSize]];
}
@end
@implementation OFHTTPServer
+ (instancetype)server
{
return [[[self alloc] init] autorelease];
}
- init
{
self = [super init];
name = @"OFHTTPServer (ObjFW's HTTP server class "
@"<https://webkeks.org/objfw/>)";
return self;
}
- (void)dealloc
{
[host release];
[listeningSocket release];
[name release];
[super dealloc];
}
- (void)setHost: (OFString*)host_
{
OF_SETTER(host, host_, YES, 1)
}
- (OFString*)host
{
OF_GETTER(host, YES)
}
- (void)setPort: (uint16_t)port_
{
port = port_;
}
- (uint16_t)port
{
return port;
}
- (void)setDelegate: (id <OFHTTPServerDelegate>)delegate_
{
delegate = delegate_;
}
- (id <OFHTTPServerDelegate>)delegate
{
return delegate;
}
- (void)setName: (OFString*)name_
{
OF_SETTER(name, name_, YES, 1)
}
- (OFString*)name
{
OF_GETTER(name, YES)
}
- (void)start
{
if (host == nil || port == 0)
@throw [OFInvalidArgumentException
exceptionWithClass: [self class]
selector: _cmd];
if (listeningSocket != nil)
@throw [OFAlreadyConnectedException
exceptionWithClass: [self class]
socket: listeningSocket];
listeningSocket = [[OFTCPSocket alloc] init];
[listeningSocket bindToHost: host
port: port];
[listeningSocket listen];
[listeningSocket asyncAcceptWithTarget: self
selector: @selector(OF_socket:
didAcceptSocket:
exception:)];
}
- (BOOL)OF_socket: (OFTCPSocket*)socket
didAcceptSocket: (OFTCPSocket*)clientSocket
exception: (OFException*)exception
{
OFHTTPServer_Connection *connection;
if (exception != nil)
return NO;
connection = [[[OFHTTPServer_Connection alloc]
initWithSocket: clientSocket
server: self] autorelease];
[clientSocket asyncReadLineWithTarget: connection
selector: @selector(socket:didReadLine:
exception:)];
return YES;
}
@end
@implementation OFObject (OFHTTPServerDelegate)
- (OFHTTPRequestResult*)server: (OFHTTPServer*)server
didReceiveRequest: (OFHTTPRequest*)request
{
@throw [OFNotImplementedException exceptionWithClass: [self class]
selector: _cmd];
}
@end