Artifact fa59054e174255cbde0938b6db1b351ca847b55db3b78899fd1e09df0a2c7782:
- File
src/OFHTTPClient.m
— part of check-in
[3d16a30f41]
at
2013-06-22 12:12:36
on branch trunk
— Rework exceptions.
This mostly removes the argument for the class in which the exception
occurred. As backtraces were recently added for all platforms, the
passed class does not give any extra information on where the exception
occurred anymore.This also removes a few other arguments which were not too helpful. In
the past, the idea was to pass as many arguments as possible so that it
is easier to find the origin of the exception. However, as backtraces
are a much better way to find the origin, those are not useful anymore
and just make the exception more cumbersome to use. The rule is now to
only pass arguments that might help in recovering from the exception or
provide information that is otherwise not easily accessible. (user: js, size: 14753) [annotate] [blame] [check-ins using]
/* * 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> #include <errno.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 "OFHTTPRequestFailedException.h" #import "OFInvalidEncodingException.h" #import "OFInvalidFormatException.h" #import "OFInvalidServerReplyException.h" #import "OFNotConnectedException.h" #import "OFOutOfMemoryException.h" #import "OFOutOfRangeException.h" #import "OFReadFailedException.h" #import "OFTruncatedDataException.h" #import "OFUnsupportedProtocolException.h" #import "OFUnsupportedVersionException.h" #import "OFWriteFailedException.h" #import "autorelease.h" #import "macros.h" static OF_INLINE void normalize_key(char *str_) { uint8_t *str = (uint8_t*)str_; bool firstLetter = true; while (*str != '\0') { if (!isalnum(*str)) { firstLetter = true; str++; continue; } *str = (firstLetter ? toupper(*str) : tolower(*str)); firstLetter = false; str++; } } @interface OFHTTPClientReply: OFHTTPRequestReply { OFTCPSocket *_socket; bool _hasContentLength, _chunked, _keepAlive, _atEndOfStream; size_t _toRead; } - initWithSocket: (OFTCPSocket*)socket; - (void)setKeepAlive: (bool)keepAlive; @end @implementation OFHTTPClientReply - initWithSocket: (OFTCPSocket*)socket { self = [super init]; _socket = [socket retain]; return self; } - (void)setKeepAlive: (bool)keepAlive { _keepAlive = keepAlive; } - (void)dealloc { [_socket release]; [super dealloc]; } - (void)setHeaders: (OFDictionary*)headers { OFString *contentLength; [super setHeaders: headers]; _chunked = [[headers objectForKey: @"Transfer-Encoding"] isEqual: @"chunked"]; contentLength = [headers objectForKey: @"Content-Length"]; if (contentLength != nil) { _hasContentLength = true; @try { _toRead = (size_t)[contentLength decimalValue]; } @catch (OFInvalidFormatException *e) { @throw [OFInvalidServerReplyException exception]; } } } - (size_t)lowlevelReadIntoBuffer: (void*)buffer length: (size_t)length { if (_atEndOfStream) { OFReadFailedException *e; e = [OFReadFailedException exceptionWithStream: self requestedLength: length]; #ifndef _WIN32 e->_errNo = ENOTCONN; #else e->_errNo = WSAENOTCONN; #endif @throw e; } if (!_hasContentLength && !_chunked) return [_socket readIntoBuffer: buffer length: length]; /* Content-Length */ if (!_chunked) { size_t ret; if (_toRead == 0) { _atEndOfStream = true; if (!_keepAlive) [_socket close]; return 0; } if (_toRead < length) ret = [_socket readIntoBuffer: buffer length: _toRead]; else ret = [_socket readIntoBuffer: buffer length: length]; _toRead -= ret; return ret; } /* Chunked */ if (_toRead > 0) { if (length > _toRead) length = _toRead; length = [_socket readIntoBuffer: buffer length: length]; _toRead -= length; if (_toRead == 0) if ([[_socket readLine] length] > 0) @throw [OFInvalidServerReplyException exception]; return length; } else { void *pool = objc_autoreleasePoolPush(); OFString *line; of_range_t range; @try { line = [_socket readLine]; } @catch (OFInvalidEncodingException *e) { @throw [OFInvalidServerReplyException exception]; } 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 exception]; } if (_toRead == 0) { _atEndOfStream = true; if (_keepAlive) { @try { line = [_socket readLine]; } @catch (OFInvalidEncodingException *e) { @throw [OFInvalidServerReplyException exception]; } if ([line length] > 0) @throw [OFInvalidServerReplyException exception]; } else [_socket close]; } objc_autoreleasePoolPop(pool); return 0; } } - (bool)lowlevelIsAtEndOfStream { if (!_hasContentLength && !_chunked) return [_socket isAtEndOfStream]; return _atEndOfStream; } - (int)fileDescriptorForReading { return [_socket fileDescriptorForReading]; } - (size_t)numberOfBytesInReadBuffer { return [super numberOfBytesInReadBuffer] + [_socket numberOfBytesInReadBuffer]; } - (void)close { [_socket close]; } @end @implementation OFHTTPClient + (instancetype)client { return [[[self alloc] init] autorelease]; } - (void)dealloc { [self close]; [super dealloc]; } - (void)setDelegate: (id <OFHTTPClientDelegate>)delegate { _delegate = delegate; } - (id <OFHTTPClientDelegate>)delegate { return _delegate; } - (void)setInsecureRedirectsAllowed: (bool)allowed { _insecureRedirectsAllowed = allowed; } - (bool)insecureRedirectsAllowed { return _insecureRedirectsAllowed; } - (OFHTTPRequestReply*)performRequest: (OFHTTPRequest*)request { return [self performRequest: request redirects: 10]; } - (OFTCPSocket*)OF_createSocketForRequest: (OFHTTPRequest*)request { OFURL *URL = [request URL]; OFTCPSocket *socket; [self close]; if ([[URL scheme] isEqual: @"https"]) { if (of_tls_socket_class == Nil) @throw [OFUnsupportedProtocolException exceptionWithURL: URL]; socket = [[[of_tls_socket_class alloc] init] autorelease]; } else socket = [OFTCPSocket socket]; if ([_delegate respondsToSelector: @selector(client:didCreateSocket:request:)]) [_delegate client: self didCreateSocket: socket request: request]; [socket connectToHost: [URL host] port: [URL port]]; return socket; } - (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]; OFMutableString *requestString; OFDictionary *headers = [request headers]; OFDataArray *POSTData = [request POSTData]; OFTCPSocket *socket; OFHTTPClientReply *reply; OFString *line, *path, *version, *redirect, *keepAlive; OFMutableDictionary *serverHeaders; OFEnumerator *keyEnumerator, *objectEnumerator; OFString *key, *object; int status; const char *type = NULL; if (![scheme isEqual: @"http"] && ![scheme isEqual: @"https"]) @throw [OFUnsupportedProtocolException exceptionWithURL: URL]; /* Can we reuse the socket? */ if (_socket != nil && [[_lastURL scheme] isEqual: [URL scheme]] && [[_lastURL host] isEqual: [URL host]] && [_lastURL port] == [URL port]) { /* * Set _socket to nil, so that in case of an error it won't be * reused. If everything is successfull, we set _socket again * at the end. */ socket = [_socket autorelease]; _socket = nil; [_lastURL release]; _lastURL = nil; /* Throw away content that has not been read yet */ while (![_lastReply isAtEndOfStream]) { char buffer[512]; [_lastReply readIntoBuffer: buffer length: 512]; } [_lastReply release]; _lastReply = nil; } else socket = [self OF_createSocketForRequest: request]; 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]) length] == 0) path = @"/"; /* * As a work around for a bug with split packets in lighttpd when using * HTTPS, we construct the complete request in a buffer string and then * send it all at once. */ if ([URL query] != nil) requestString = [OFMutableString stringWithFormat: @"%s %@?%@ HTTP/%@\r\n", type, path, [URL query], [request protocolVersionString]]; else requestString = [OFMutableString stringWithFormat: @"%s %@ HTTP/%@\r\n", type, path, [request protocolVersionString]]; if ([URL port] == 80) [requestString appendFormat: @"Host: %@\r\n", [URL host]]; else [requestString appendFormat: @"Host: %@:%d\r\n", [URL host], [URL port]]; if ([headers objectForKey: @"User-Agent"] == nil) [requestString appendString: @"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) [requestString appendFormat: @"%@: %@\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"; [requestString appendFormat: @"Content-Type: %@\r\n" @"Content-Length: %d\r\n", contentType, [POSTData count] * [POSTData itemSize]]; } if ([request protocolVersion].major == 1 && [request protocolVersion].minor == 0) [requestString appendString: @"Connection: keep-alive\r\n"]; [requestString appendString: @"\r\n"]; @try { [socket writeString: requestString]; } @catch (OFWriteFailedException *e) { /* Reconnect in case a keep-alive connection timed out */ socket = [self OF_createSocketForRequest: request]; [socket writeString: requestString]; } @catch (OFNotConnectedException *e) { /* Reconnect in case a keep-alive connection timed out */ socket = [self OF_createSocketForRequest: request]; [socket writeString: requestString]; } if (requestType == OF_HTTP_REQUEST_TYPE_POST) [socket writeBuffer: [POSTData items] length: [POSTData count] * [POSTData itemSize]]; @try { line = [socket readLine]; } @catch (OFInvalidEncodingException *e) { @throw [OFInvalidServerReplyException exception]; } /* * It's possible that the write succeeds on a connection that is * keep-alive, but the connection has already been closed by the remote * end due to a timeout. In this case, we need to reconnect. */ if (line == nil) { socket = [self OF_createSocketForRequest: request]; [socket writeString: requestString]; if (requestType == OF_HTTP_REQUEST_TYPE_POST) [socket writeBuffer: [POSTData items] length: [POSTData count] * [POSTData itemSize]]; @try { line = [socket readLine]; } @catch (OFInvalidEncodingException *e) { @throw [OFInvalidServerReplyException exception]; } } if (![line hasPrefix: @"HTTP/"] || [line characterAtIndex: 8] != ' ') @throw [OFInvalidServerReplyException exception]; version = [line substringWithRange: of_range(5, 3)]; if (![version isEqual: @"1.0"] && ![version isEqual: @"1.1"]) @throw [OFUnsupportedVersionException exceptionWithVersion: 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 = [socket readLine]; } @catch (OFInvalidEncodingException *e) { @throw [OFInvalidServerReplyException exception]; } if (line == nil) @throw [OFInvalidServerReplyException exception]; if ([line length] == 0) break; lineC = [line UTF8String]; if ((tmp = strchr(lineC, ':')) == NULL) @throw [OFInvalidServerReplyException exception]; if ((keyC = malloc(tmp - lineC + 1)) == NULL) @throw [OFOutOfMemoryException exceptionWithRequestedSize: tmp - lineC + 1]; memcpy(keyC, lineC, tmp - lineC); keyC[tmp - lineC] = '\0'; normalize_key(keyC); @try { key = [OFString stringWithUTF8StringNoCopy: keyC freeWhenDone: true]; } @catch (id e) { free(keyC); @throw e; } do { tmp++; } while (*tmp == ' '); value = [OFString stringWithUTF8String: tmp]; [serverHeaders setObject: value forKey: key]; } [serverHeaders makeImmutable]; if ([_delegate respondsToSelector: @selector(client:didReceiveHeaders:statusCode:request:)]) [_delegate client: self didReceiveHeaders: serverHeaders statusCode: status request: request]; reply = [[[OFHTTPClientReply alloc] initWithSocket: socket] autorelease]; [reply setProtocolVersionFromString: version]; [reply setStatusCode: status]; [reply setHeaders: serverHeaders]; keepAlive = [serverHeaders objectForKey: @"Connection"]; if ([version isEqual: @"1.1"] || (keepAlive != nil && [keepAlive isEqual: @"keep-alive"])) { [reply setKeepAlive: true]; _socket = [socket retain]; _lastURL = [URL copy]; _lastReply = [reply retain]; } if (redirects > 0 && (status == 301 || status == 302 || status == 303 || status == 307) && (redirect = [serverHeaders objectForKey: @"Location"]) != nil && (_insecureRedirectsAllowed || [scheme isEqual: @"http"] || ![redirect hasPrefix: @"http://"])) { OFURL *newURL; bool follow = true; newURL = [OFURL URLWithString: redirect relativeToURL: URL]; if ([_delegate respondsToSelector: @selector(client:shouldFollowRedirect:request:)]) follow = [_delegate client: self shouldFollowRedirect: newURL request: request]; if (follow) { OFHTTPRequest *newRequest; 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]; } } [reply retain]; objc_autoreleasePoolPop(pool); [reply autorelease]; if (status / 100 != 2) @throw [OFHTTPRequestFailedException exceptionWithRequest: request reply: reply]; return reply; } - (void)close { [_socket release]; _socket = nil; [_lastURL release]; _lastURL = nil; [_lastReply release]; _lastReply = nil; } @end