/* * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 * Jonathan Schleifer <js@heap.zone> * * 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 <stdlib.h> #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 "OFHTTPResponse.h" #import "OFTCPSocket.h" #import "OFTimer.h" #import "OFAlreadyConnectedException.h" #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" #import "OFNotOpenException.h" #import "OFOutOfMemoryException.h" #import "OFOutOfRangeException.h" #import "OFWriteFailedException.h" #import "socket_helpers.h" #define BUFFER_SIZE 1024 /* * FIXME: Key normalization replaces headers like "DNT" with "Dnt". * FIXME: Errors are not reported to the user. */ @interface OFHTTPServer () - (bool)OF_socket: (OFTCPSocket*)socket didAcceptSocket: (OFTCPSocket*)clientSocket exception: (OFException*)exception; @end static const char* statusCodeToString(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* normalizedKey(OFString *key) { char *cString = of_strdup([key UTF8String]); uint8_t *tmp = (uint8_t*)cString; bool firstLetter = true; if (cString == NULL) @throw [OFOutOfMemoryException exceptionWithRequestedSize: strlen([key UTF8String])]; while (*tmp != '\0') { if (!isalnum(*tmp)) { firstLetter = true; tmp++; continue; } *tmp = (firstLetter ? toupper(*tmp) : tolower(*tmp)); firstLetter = false; tmp++; } return [OFString stringWithUTF8StringNoCopy: cString freeWhenDone: true]; } @interface OFHTTPServerResponse: OFHTTPResponse { OFTCPSocket *_socket; OFHTTPServer *_server; bool _chunked, _headersSent; } - initWithSocket: (OFTCPSocket*)socket server: (OFHTTPServer*)server; @end @implementation OFHTTPServerResponse - initWithSocket: (OFTCPSocket*)socket server: (OFHTTPServer*)server { self = [super init]; _statusCode = 500; _socket = [socket retain]; _server = [server retain]; return self; } - (void)dealloc { if (_socket != nil) [self close]; /* includes [_socket release] */ [_server release]; [super dealloc]; } - (void)OF_sendHeaders { void *pool = objc_autoreleasePoolPush(); OFString *date = [[OFDate date] dateStringWithFormat: @"%a, %d %b %Y %H:%M:%S GMT"]; OFEnumerator *keyEnumerator, *valueEnumerator; OFString *key, *value; [_socket writeFormat: @"HTTP/%@ %d %s\r\n" @"Server: %@\r\n" @"Date: %@\r\n", [self protocolVersionString], _statusCode, statusCodeToString(_statusCode), [_server name], date]; keyEnumerator = [_headers keyEnumerator]; valueEnumerator = [_headers objectEnumerator]; while ((key = [keyEnumerator nextObject]) != nil && (value = [valueEnumerator nextObject]) != nil) if (![key isEqual: @"Server"] && ![key isEqual: @"Date"]) [_socket writeFormat: @"%@: %@\r\n", key, value]; [_socket writeString: @"\r\n"]; _headersSent = true; _chunked = [[_headers objectForKey: @"Transfer-Encoding"] isEqual: @"chunked"]; objc_autoreleasePoolPop(pool); } - (void)lowlevelWriteBuffer: (const void*)buffer length: (size_t)length { void *pool; if (_socket == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (!_headersSent) [self OF_sendHeaders]; if (!_chunked) { [_socket writeBuffer: buffer length: length]; return; } pool = objc_autoreleasePoolPush(); [_socket writeString: [OFString stringWithFormat: @"%zx\r\n", length]]; objc_autoreleasePoolPop(pool); [_socket writeBuffer: buffer length: length]; [_socket writeBuffer: "\r\n" length: 2]; } - (void)close { if (_socket == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (!_headersSent) [self OF_sendHeaders]; if (_chunked) [_socket writeBuffer: "0\r\n\r\n" length: 5]; [_socket release]; _socket = nil; } - (int)fileDescriptorForWriting { if (_socket == nil) return -1; return [_socket fileDescriptorForWriting]; } @end @interface OFHTTPServer_Connection: OFObject { OFTCPSocket *_socket; OFHTTPServer *_server; OFTimer *_timer; enum { AWAITING_PROLOG, PARSING_HEADERS, SEND_RESPONSE } _state; uint8_t _HTTPMinorVersion; of_http_request_method_t _method; OFString *_host, *_path; uint16_t _port; OFMutableDictionary *_headers; size_t _contentLength; OFDataArray *_body; } - initWithSocket: (OFTCPSocket*)socket server: (OFHTTPServer*)server; - (bool)socket: (OFTCPSocket*)socket didReadLine: (OFString*)line exception: (OFException*)exception; - (bool)parseProlog: (OFString*)line; - (bool)parseHeaders: (OFString*)line; - (bool)socket: (OFTCPSocket*)socket didReadIntoBuffer: (char*)buffer length: (size_t)length exception: (OFException*)exception; - (bool)sendErrorAndClose: (short)statusCode; - (void)createResponse; @end @implementation OFHTTPServer_Connection - initWithSocket: (OFTCPSocket*)socket server: (OFHTTPServer*)server { self = [super init]; @try { _socket = [socket retain]; _server = [server retain]; _timer = [[OFTimer scheduledTimerWithTimeInterval: 10 target: socket selector: @selector( cancelAsyncRequests) repeats: false] retain]; _state = AWAITING_PROLOG; } @catch (id e) { [self release]; @throw e; } return self; } - (void)dealloc { [_socket release]; [_server release]; [_timer invalidate]; [_timer release]; [_host release]; [_path release]; [_headers release]; [_body release]; [super dealloc]; } - (bool)socket: (OFTCPSocket*)socket didReadLine: (OFString*)line exception: (OFException*)exception { if (line == nil || exception != nil) return false; @try { switch (_state) { case AWAITING_PROLOG: return [self parseProlog: line]; case PARSING_HEADERS: if (![self parseHeaders: line]) return false; if (_state == SEND_RESPONSE) { [self createResponse]; return false; } return true; default: return false; } } @catch (OFWriteFailedException *e) { return false; } OF_ENSURE(0); } - (bool)parseProlog: (OFString*)line { OFString *method; OFMutableString *path; 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]; method = [line substringWithRange: of_range(0, pos)]; @try { _method = of_http_request_method_from_string( [method UTF8String]); } @catch (OFInvalidFormatException *e) { return [self sendErrorAndClose: 405]; } @try { of_range_t range = of_range(pos + 1, [line length] - pos - 10); path = [[[line substringWithRange: range] mutableCopy] autorelease]; } @catch (OFOutOfRangeException *e) { return [self sendErrorAndClose: 400]; } [path deleteEnclosingWhitespaces]; if (![path hasPrefix: @"/"]) return [self sendErrorAndClose: 400]; [path deleteCharactersInRange: of_range(0, 1)]; [path makeImmutable]; _headers = [[OFMutableDictionary alloc] init]; _path = [path copy]; _state = PARSING_HEADERS; return true; } - (bool)parseHeaders: (OFString*)line { OFString *key, *value, *old; size_t pos; if ([line length] == 0) { intmax_t contentLength; @try { contentLength = [[_headers objectForKey: @"Content-Length"] decimalValue]; } @catch (OFInvalidFormatException *e) { return [self sendErrorAndClose: 400]; } if (contentLength > 0) { char *buffer; buffer = [self allocMemoryWithSize: BUFFER_SIZE]; _body = [[OFDataArray alloc] init]; [_socket asyncReadIntoBuffer: buffer length: BUFFER_SIZE target: self selector: @selector(socket: didReadIntoBuffer: length:exception:)]; [_timer setFireDate: [OFDate dateWithTimeIntervalSinceNow: 5]]; return false; } _state = SEND_RESPONSE; return true; } 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 = normalizedKey([key stringByDeletingTrailingWhitespaces]); value = [value stringByDeletingLeadingWhitespaces]; old = [_headers objectForKey: key]; if (old != nil) value = [old stringByAppendingFormat: @",%@", value]; [_headers setObject: value forKey: key]; if ([key isEqual: @"Host"]) { pos = [value rangeOfString: @":" options: OF_STRING_SEARCH_BACKWARDS].location; if (pos != OF_NOT_FOUND) { [_host release]; _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 release]; _host = [value retain]; _port = 80; } } return true; } - (bool)socket: (OFTCPSocket*)socket didReadIntoBuffer: (char*)buffer length: (size_t)length exception: (OFException*)exception { if ([socket isAtEndOfStream] || exception != nil) return false; [_body addItems: buffer count: length]; if ([_body count] >= _contentLength) { /* * Manually free the buffer here. While this is not required * now as the async read is the only thing referencing self and * the buffer is allocated on self, it is required once * Connection: keep-alive is implemented. */ [self freeMemory: buffer]; @try { [self createResponse]; } @catch (OFWriteFailedException *e) { return false; } return false; } [_timer setFireDate: [OFDate dateWithTimeIntervalSinceNow: 5]]; return true; } - (bool)sendErrorAndClose: (short)statusCode { OFString *date = [[OFDate date] dateStringWithFormat: @"%a, %d %b %Y %H:%M:%S GMT"]; [_socket writeFormat: @"HTTP/1.1 %d %s\r\n" @"Date: %@\r\n" @"Server: %@\r\n" @"\r\n", statusCode, statusCodeToString(statusCode), date, [_server name]]; return false; } - (void)createResponse { OFURL *URL; OFHTTPRequest *request; OFHTTPServerResponse *response; size_t pos; [_timer invalidate]; [_timer release]; _timer = nil; if (_host == nil || _port == 0) { if (_HTTPMinorVersion > 0) { [self sendErrorAndClose: 400]; return; } [_host release]; _host = [[_server host] retain]; _port = [_server port]; } URL = [OFURL URL]; [URL setScheme: @"http"]; [URL setHost: _host]; [URL setPort: _port]; if ((pos = [_path rangeOfString: @"?"].location) != OF_NOT_FOUND) { OFString *path, *query; path = [_path substringWithRange: of_range(0, pos)]; query = [_path substringWithRange: of_range(pos + 1, [_path length] - pos - 1)]; [URL setPath: path]; [URL setQuery: query]; } else [URL setPath: _path]; request = [OFHTTPRequest requestWithURL: URL]; [request setMethod: _method]; [request setProtocolVersion: (of_http_request_protocol_version_t){ 1, _HTTPMinorVersion }]; [request setHeaders: _headers]; [request setBody: _body]; [request setRemoteAddress: [_socket remoteAddress]]; response = [[[OFHTTPServerResponse alloc] initWithSocket: _socket server: _server] autorelease]; [[_server delegate] server: _server didReceiveRequest: request response: response]; } @end @implementation OFHTTPServer @synthesize host = _host, port = _port, delegate = _delegate, name = _name; + (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)start { if (_host == nil) @throw [OFInvalidArgumentException exception]; if (_listeningSocket != nil) @throw [OFAlreadyConnectedException exception]; _listeningSocket = [[OFTCPSocket alloc] init]; _port = [_listeningSocket bindToHost: _host port: _port]; [_listeningSocket listen]; [_listeningSocket asyncAcceptWithTarget: self selector: @selector(OF_socket: didAcceptSocket: exception:)]; } - (void)stop { [_listeningSocket cancelAsyncRequests]; [_listeningSocket release]; _listeningSocket = nil; } - (bool)OF_socket: (OFTCPSocket*)socket didAcceptSocket: (OFTCPSocket*)clientSocket exception: (OFException*)exception { OFHTTPServer_Connection *connection; if (exception != nil) { if ([_delegate respondsToSelector: @selector(server:didReceiveExceptionOnListeningSocket:)]) return [_delegate server: self didReceiveExceptionOnListeningSocket: exception]; return false; } connection = [[[OFHTTPServer_Connection alloc] initWithSocket: clientSocket server: self] autorelease]; [clientSocket asyncReadLineWithTarget: connection selector: @selector(socket:didReadLine: exception:)]; return true; } @end