Index: src/Makefile ================================================================== --- src/Makefile +++ src/Makefile @@ -21,10 +21,11 @@ OFEnumerator.m \ OFFile.m \ OFHash.m \ OFHTTPClient.m \ OFHTTPRequest.m \ + OFHTTPServer.m \ OFIntrospection.m \ OFList.m \ OFMapTable.m \ OFMD5Hash.m \ OFMutableArray.m \ ADDED src/OFHTTPServer.h Index: src/OFHTTPServer.h ================================================================== --- src/OFHTTPServer.h +++ src/OFHTTPServer.h @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011, 2012 + * Jonathan Schleifer + * + * 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. + */ + +#import "OFObject.h" + +@class OFHTTPServer; +@class OFHTTPRequest; +@class OFHTTPRequestResult; +@class OFTCPSocket; +@class OFException; + +/*! + * @brief A delegate for OFHTTPServer. + */ +@protocol OFHTTPServerDelegate +/*! + * @brief This method is called when the HTTP server received a request from a + * client. + * + * @param server The HTTP server which received the request + * @param request The request the HTTP server received + * @return The result the HTTP server should send to the client + */ +- (OFHTTPRequestResult*)server: (OFHTTPServer*)server + didReceiveRequest: (OFHTTPRequest*)request; +@end + +/*! + * @brief A class for creating a simple HTTP server inside of applications. + */ +@interface OFHTTPServer: OFObject +{ + OFString *host; + uint16_t port; + id delegate; + OFTCPSocket *listeningSocket; +} + +#ifdef OF_HAVE_PROPERTIES +@property (copy) OFString *host; +@property uint16_t port; +@property (assign) id delegate; +#endif + +/*! + * @brief Creates a new HTTP server. + * + * @return A new HTTP server + */ ++ (instancetype)server; + +/*! + * @brief Sets the host on which the HTTP server will listen. + * + * @param host The host to listen on + */ +- (void)setHost: (OFString*)host; + +/*! + * @brief Returns the host on which the HTTP server will listen. + * + * @return The host on which the HTTP server will listen + */ +- (OFString*)host; + +/*! + * @brief Sets the port on which the HTTP server will listen. + * + * @param port The port to listen on + */ +- (void)setPort: (uint16_t)port; + +/*! + * @brief Returns the port on which the HTTP server will listen. + * + * @return The port on which the HTTP server will listen + */ +- (uint16_t)port; + +/*! + * @brief Sets the delegate for the HTTP server. + * + * @param delegate The delegate for the HTTP server + */ +- (void)setDelegate: (id )delegate; + +/*! + * @brief Returns the delegate for the HTTP server. + * + * @return The delegate for the HTTP server + */ +- (id )delegate; + +/*! + * @brief Starts the HTTP server in the current thread's runloop. + */ +- (void)start; + +- (BOOL)OF_socket: (OFTCPSocket*)socket + didAcceptSocket: (OFTCPSocket*)clientSocket + exception: (OFException*)exception; +@end + +@interface OFObject (OFHTTPServerDelegate) +@end ADDED src/OFHTTPServer.m Index: src/OFHTTPServer.m ================================================================== --- src/OFHTTPServer.m +++ src/OFHTTPServer.m @@ -0,0 +1,593 @@ +/* + * Copyright (c) 2008, 2009, 2010, 2011, 2012 + * Jonathan Schleifer + * + * 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 +#include + +#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]); + char *tmp = 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)] retain]; + } @catch (OFOutOfRangeException *e) { + return [self sendErrorAndClose: 400]; + } + + 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 addItemsFromCArray: 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: OFHTTPServer (ObjFW's HTTP server class, " + @")\r\n\r\n", + statusCode, status_code_to_string(statusCode), date]; + [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: [[server delegate] class] + selector: @selector(server:didReceiveRequest:)]; + } + + replyHeaders = [reply headers]; + replyData = [reply data]; + + [sock writeFormat: @"HTTP/1.1 %d %s\r\n" + @"Server: OFHTTPServer (ObjFW's HTTP server class, " + @")\r\n", + [reply statusCode], + status_code_to_string([reply statusCode])]; + + 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 cArray] + length: [replyData count] * [replyData itemSize]]; +} +@end + +@implementation OFHTTPServer ++ (instancetype)server +{ + return [[[self alloc] init] autorelease]; +} + +- (void)dealloc +{ + [host release]; + [listeningSocket 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 )delegate_ +{ + delegate = delegate_; +} + +- (id )delegate +{ + return delegate; +} + +- (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 Index: src/ObjFW.h ================================================================== --- src/ObjFW.h +++ src/ObjFW.h @@ -52,10 +52,11 @@ #import "OFProcess.h" #import "OFStreamObserver.h" #import "OFHTTPRequest.h" #import "OFHTTPClient.h" +#import "OFHTTPServer.h" #import "OFHash.h" #import "OFMD5Hash.h" #import "OFSHA1Hash.h"