Index: generators/TableGenerator.m ================================================================== --- generators/TableGenerator.m +++ generators/TableGenerator.m @@ -88,11 +88,11 @@ [self parseCaseFolding: response]; } - (void)client: (OFHTTPClient *)client didEncounterException: (id)exception - forRequest: (OFHTTPRequest *)request + request: (OFHTTPRequest *)request context: (id)context { @throw exception; } Index: src/OFHTTPClient.h ================================================================== --- src/OFHTTPClient.h +++ src/OFHTTPClient.h @@ -25,10 +25,11 @@ @class OFDictionary OF_GENERIC(KeyType, ObjectType); @class OFHTTPClient; @class OFHTTPRequest; @class OFHTTPResponse; +@class OFStream; @class OFTCPSocket; @class OFURL; /*! * @protocol OFHTTPClientDelegate OFHTTPClient.h ObjFW/OFHTTPClient.h @@ -60,11 +61,11 @@ * @param context The context object that was passed to * @ref asyncPerformRequest:context: */ - (void)client: (OFHTTPClient *)client didEncounterException: (id)exception - forRequest: (OFHTTPRequest *)request + request: (OFHTTPRequest *)request context: (nullable id)context; @optional /*! * @brief A callback which is called when an OFHTTPClient creates a socket. @@ -80,13 +81,28 @@ * @param context The context object that was passed to * @ref asyncPerformRequest:context: */ - (void)client: (OFHTTPClient *)client didCreateSocket: (OF_KINDOF(OFTCPSocket *))socket - forRequest: (OFHTTPRequest *)request + request: (OFHTTPRequest *)request context: (nullable id)context; +/*! + * @brief A callback which is called when an OFHTTPClient wants to send the + * body for a request. + * + * @param client The OFHTTPClient that wants to send the body + * @param body A stream into which the body should be written + * @param request The request for which the OFHTTPClient wants to send the body + * @param context The context object that was passed to + * @ref asyncPerformRequest:context: + */ +- (void)client: (OFHTTPClient *)client + requestsBody: (OFStream *)body + request: (OFHTTPRequest *)request + context: (nullable id)context; + /*! * @brief A callback which is called when an OFHTTPClient received headers. * * @param client The OFHTTPClient which received the headers * @param headers The headers received Index: src/OFHTTPClient.m ================================================================== --- src/OFHTTPClient.m +++ src/OFHTTPClient.m @@ -33,10 +33,11 @@ #import "OFTCPSocket.h" #import "OFURL.h" #import "OFAlreadyConnectedException.h" #import "OFHTTPRequestFailedException.h" +#import "OFInvalidArgumentException.h" #import "OFInvalidEncodingException.h" #import "OFInvalidFormatException.h" #import "OFInvalidServerReplyException.h" #import "OFNotImplementedException.h" #import "OFNotOpenException.h" @@ -47,10 +48,11 @@ #import "OFUnsupportedVersionException.h" #import "OFWriteFailedException.h" @interface OFHTTPClientRequestHandler: OFObject { +@public OFHTTPClient *_client; OFHTTPRequest *_request; unsigned int _redirects; id _context; bool _firstLine; @@ -64,10 +66,22 @@ redirects: (unsigned int)redirects context: (id)context; - (void)start; - (void)closeAndReconnect; @end + +@interface OFHTTPClientRequestBodyStream: OFStream +{ + OFHTTPClientRequestHandler *_handler; + OFTCPSocket *_socket; + intmax_t _contentLength, _written; + bool _closed; +} + +- (instancetype)initWithHandler: (OFHTTPClientRequestHandler *)handler + socket: (OFTCPSocket *)sock; +@end @interface OFHTTPClientResponse: OFHTTPResponse { OFTCPSocket *_socket; bool _hasContentLength, _chunked, _keepAlive, _atEndOfStream; @@ -85,11 +99,10 @@ void *pool = objc_autoreleasePoolPush(); of_http_request_method_t method = [request method]; OFURL *URL = [request URL]; OFString *path; OFString *user = [URL user], *password = [URL password]; - OFData *body = [request body]; OFMutableString *requestString; OFMutableDictionary OF_GENERIC(OFString *, OFString *) *headers; OFEnumerator OF_GENERIC(OFString *) *keyEnumerator, *objectEnumerator; OFString *key, *object; @@ -149,30 +162,21 @@ if ([headers objectForKey: @"User-Agent"] == nil) [headers setObject: @"Something using ObjFW " @"" forKey: @"User-Agent"]; - if (body != nil) { - if ([headers objectForKey: @"Content-Length"] == nil) { - OFString *contentLength = [OFString stringWithFormat: - @"%zd", [body itemSize] * [body count]]; - - [headers setObject: contentLength - forKey: @"Content-Length"]; - } - - if ([headers objectForKey: @"Content-Type"] == nil) - [headers setObject: @"application/x-www-form-" - @"urlencoded; charset=UTF-8" - forKey: @"Content-Type"]; - } - if ([request protocolVersion].major == 1 && [request protocolVersion].minor == 0 && [headers objectForKey: @"Connection"] == nil) [headers setObject: @"keep-alive" forKey: @"Connection"]; + + if ([headers objectForKey: @"Content-Length"] != nil && + [headers objectForKey: @"Content-Type"] == nil) + [headers setObject: @"application/x-www-form-" + @"urlencoded; charset=UTF-8" + forKey: @"Content-Type"]; keyEnumerator = [headers keyEnumerator]; objectEnumerator = [headers objectEnumerator]; while ((key = [keyEnumerator nextObject]) != nil && @@ -349,17 +353,17 @@ objectEnumerator = [headers objectEnumerator]; while ((key = [keyEnumerator nextObject]) != nil && (object = [objectEnumerator nextObject]) != nil) - if ([key hasPrefix: @"Content-"]) + if ([key hasPrefix: @"Content-"] || + [key hasPrefix: @"Transfer-"]) [newHeaders removeObjectForKey: key]; [newRequest setMethod: OF_HTTP_REQUEST_METHOD_GET]; - [newRequest setBody: nil]; } [newRequest setURL: newURL]; [newRequest setHeaders: newHeaders]; @@ -393,11 +397,11 @@ @try { [self createResponseWithSocketOrThrow: sock]; } @catch (id e) { [_client->_delegate client: _client didEncounterException: e - forRequest: _request + request: _request context: _context]; } } - (bool)handleFirstLine: (OFString *)line @@ -503,11 +507,11 @@ [OFInvalidEncodingException class]]) exception = [OFInvalidServerReplyException exception]; [_client->_delegate client: _client didEncounterException: exception - forRequest: _request + request: _request context: _context]; return false; } @try { @@ -518,48 +522,24 @@ ret = [self handleServerHeader: line socket: sock]; } @catch (id e) { [_client->_delegate client: _client didEncounterException: e - forRequest: _request + request: _request context: _context]; ret = false; } return ret; } -- (size_t)socket: (OFTCPSocket *)sock - didWriteBody: (const void **)body - length: (size_t)length - context: (id)context - exception: (id)exception -{ - if (exception != nil) { - [_client->_delegate client: _client - didEncounterException: exception - forRequest: _request - context: _context]; - return 0; - } - - _firstLine = true; - [sock asyncReadLineWithTarget: self - selector: @selector(socket:didReadLine:context: - exception:) - context: nil]; - return 0; -} - - (size_t)socket: (OFTCPSocket *)sock didWriteRequest: (const void **)request length: (size_t)length context: (id)context exception: (id)exception { - OFData *body; - if (exception != nil) { if ([exception isKindOfClass: [OFWriteFailedException class]] && ([exception errNo] == ECONNRESET || [exception errNo] == EPIPE)) { /* In case a keep-alive connection timed out */ @@ -567,29 +547,36 @@ return 0; } [_client->_delegate client: _client didEncounterException: exception - forRequest: _request + request: _request context: _context]; return 0; } - if ((body = [_request body]) != nil) { - [sock asyncWriteBuffer: [body items] - length: [body count] * [body itemSize] - target: self - selector: @selector(socket:didWriteBody:length: - context:exception:) - context: nil]; - return 0; + _firstLine = true; + + if ([[_request headers] objectForKey: @"Content-Length"] != nil) { + OFStream *stream = [[[OFHTTPClientRequestBodyStream alloc] + initWithHandler: self + socket: sock] autorelease]; + + if ([_client->_delegate respondsToSelector: + @selector(client:requestsBody:request:context:)]) + [_client->_delegate client: _client + requestsBody: stream + request: _request + context: _context]; + } else - return [self socket: sock - didWriteBody: NULL - length: 0 - context: nil - exception: nil]; + [sock asyncReadLineWithTarget: self + selector: @selector(socket:didReadLine: + context:exception:) + context: nil]; + + return 0; } - (void)handleSocket: (OFTCPSocket *)sock { /* @@ -617,11 +604,11 @@ length:context:exception:) context: requestString]; } @catch (id e) { [_client->_delegate client: _client didEncounterException: e - forRequest: _request + request: _request context: _context]; return; } } @@ -630,20 +617,20 @@ exception: (id)exception { if (exception != nil) { [_client->_delegate client: _client didEncounterException: exception - forRequest: _request + request: _request context: _context]; return; } if ([_client->_delegate respondsToSelector: - @selector(client:didCreateSocket:forRequest:context:)]) + @selector(client:didCreateSocket:request:context:)]) [_client->_delegate client: _client didCreateSocket: sock - forRequest: _request + request: _request context: _context]; [self performSelector: @selector(handleSocket:) withObject: sock afterDelay: 0]; @@ -656,11 +643,11 @@ exception: (id)exception { if (exception != nil) { [_client->_delegate client: _client didEncounterException: exception - forRequest: _request + request: _request context: _context]; return false; } if ([response isAtEndOfStream]) { @@ -756,15 +743,108 @@ exception:) context: nil]; } @catch (id e) { [_client->_delegate client: _client didEncounterException: e - forRequest: _request + request: _request context: _context]; } } @end + +@implementation OFHTTPClientRequestBodyStream +- (instancetype)initWithHandler: (OFHTTPClientRequestHandler *)handler + socket: (OFTCPSocket *)sock +{ + self = [super init]; + + @try { + OFDictionary OF_GENERIC(OFString *, OFString *) *headers; + OFString *contentLengthString; + + _handler = [handler retain]; + _socket = [sock retain]; + + headers = [_handler->_request headers]; + + contentLengthString = [headers objectForKey: @"Content-Length"]; + if (contentLengthString == nil) + @throw [OFInvalidArgumentException exception]; + + _contentLength = [contentLengthString decimalValue]; + if (_contentLength < 0) + @throw [OFOutOfRangeException exception]; + + if ([headers objectForKey: @"Transfer-Encoding"] != nil) + @throw [OFInvalidArgumentException exception]; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + [self close]; + + [_handler release]; + [_socket release]; + + [super dealloc]; +} + +- (size_t)lowlevelWriteBuffer: (const void *)buffer + length: (size_t)length +{ + size_t written; + +#if SIZE_MAX >= INTMAX_MAX + if (length > INTMAX_MAX) + @throw [OFOutOfRangeException exception]; +#endif + + if (INTMAX_MAX - _written < (intmax_t)length || + _written + (intmax_t)length > _contentLength) + @throw [OFOutOfRangeException exception]; + + written = [_socket writeBuffer: buffer + length: length]; + +#if SIZE_MAX >= INTMAX_MAX + if (written > INTMAX_MAX) + @throw [OFOutOfRangeException exception]; +#endif + + if (INTMAX_MAX - _written < (intmax_t)written) + @throw [OFOutOfRangeException exception]; + + _written += written; + + return written; +} + +- (void)close +{ + if (_closed) + return; + + if (_written < _contentLength) + @throw [OFTruncatedDataException exception]; + + [_socket asyncReadLineWithTarget: _handler + selector: @selector(socket:didReadLine:context: + exception:) + context: nil]; +} + +- (int)fileDescriptorForWriting +{ + return [_socket fileDescriptorForWriting]; +} +@end @implementation OFHTTPClientResponse @synthesize of_keepAlive = _keepAlive; - (instancetype)initWithSocket: (OFTCPSocket *)sock Index: src/OFStream.h ================================================================== --- src/OFStream.h +++ src/OFStream.h @@ -820,13 +820,15 @@ * data has been written. The method should return the length for * the next write with the same callback or 0 if it should not * repeat. The buffer may be changed, so that every time a new * buffer and length can be specified while the callback stays * the same. - * @param selector The selector to call on the target. The signature must be - * `size_t (OFStream *stream, const void *buffer, - * size_t bytesWritten, id context, id exception)`. + * @param selector The selector to call on the target. It should return the + * length for the next write with the same callback or 0 if it + * should not repeat. The signature must be `size_t (OFStream + * *stream, const void *buffer, size_t bytesWritten, id + * context, id exception)`. * @param context A context object to pass along to the target */ - (void)asyncWriteBuffer: (const void *)buffer length: (size_t)length target: (id)target Index: tests/OFHTTPClientTests.m ================================================================== --- tests/OFHTTPClientTests.m +++ tests/OFHTTPClientTests.m @@ -52,10 +52,11 @@ @implementation HTTPClientTestsServer - (id)main { OFTCPSocket *listener, *client; + char buffer[5]; [cond lock]; listener = [OFTCPSocket socket]; _port = [listener bindToHost: @"127.0.0.1" @@ -70,17 +71,29 @@ if (![[client readLine] isEqual: @"GET /foo HTTP/1.1"]) OF_ENSURE(0); if (![[client readLine] hasPrefix: @"User-Agent:"]) OF_ENSURE(0); + + if (![[client readLine] isEqual: @"Content-Length: 5"]) + OF_ENSURE(0); + + if (![[client readLine] isEqual: + @"Content-Type: application/x-www-form-urlencoded; charset=UTF-8"]) + OF_ENSURE(0); if (![[client readLine] isEqual: [OFString stringWithFormat: @"Host: 127.0.0.1:%" @PRIu16, _port]]) OF_ENSURE(0); if (![[client readLine] isEqual: @""]) OF_ENSURE(0); + + [client readIntoBuffer: buffer + exactLength: 5]; + if (memcmp(buffer, "Hello", 5) != 0) + OF_ENSURE(0); [client writeString: @"HTTP/1.0 200 OK\r\n" @"cONTeNT-lENgTH: 7\r\n" @"\r\n" @"foo\n" @@ -90,10 +103,18 @@ return nil; } @end @implementation TestsAppDelegate (OFHTTPClientTests) +- (void)client: (OFHTTPClient *)client + requestsBody: (OFStream *)body + request: (OFHTTPRequest *)request + context: (id)context +{ + [body writeString: @"Hello"]; +} + - (void)client: (OFHTTPClient *)client didPerformRequest: (OFHTTPRequest *)request response: (OFHTTPResponse *)response_ context: (id)context { @@ -124,11 +145,14 @@ [OFString stringWithFormat: @"http://127.0.0.1:%" @PRIu16 "/foo", server->_port]]; TEST(@"-[asyncPerformRequest:]", (client = [OFHTTPClient client]) && R([client setDelegate: self]) && - R(request = [OFHTTPRequest requestWithURL: URL]) && + (request = [OFHTTPRequest requestWithURL: URL]) && + R([request setHeaders: + [OFDictionary dictionaryWithObject: @"5" + forKey: @"Content-Length"]]) && R([client asyncPerformRequest: request context: nil])) [[OFRunLoop mainRunLoop] runUntilDate: [OFDate dateWithTimeIntervalSinceNow: 2]]; Index: utils/ofhttp/OFHTTP.m ================================================================== --- utils/ofhttp/OFHTTP.m +++ utils/ofhttp/OFHTTP.m @@ -59,11 +59,11 @@ size_t _URLIndex; int _errorCode; OFString *_outputPath, *_currentFileName; bool _continue, _force, _detectFileName, _detectedFileName; bool _quiet, _verbose, _insecure; - OFData *_body; + OFStream *_body; of_http_request_method_t _method; OFMutableDictionary *_clientHeaders; OFHTTPClient *_HTTPClient; char *_buffer; OFStream *_output; @@ -283,22 +283,22 @@ [_clientHeaders setObject: value forKey: name]; } -- (void)setBody: (OFString *)file +- (void)setBody: (OFString *)path { + uintmax_t bodySize; + [_body release]; - - if ([file isEqual: @"-"]) { - void *pool = objc_autoreleasePoolPush(); - - _body = [[of_stdin readDataUntilEndOfStream] copy]; - - objc_autoreleasePoolPop(pool); - } else - _body = [[OFData alloc] initWithContentsOfFile: file]; + _body = [[OFFile alloc] initWithPath: path + mode: @"r"]; + + bodySize = [[[OFFileManager defaultManager] + attributesOfItemAtPath: path] fileSize]; + [_clientHeaders setObject: [OFString stringWithFormat: @"%ju", bodySize] + forKey: @"Content-Length"]; } - (void)setMethod: (OFString *)method { void *pool = objc_autoreleasePoolPush(); @@ -508,10 +508,27 @@ { if (_insecure && [sock respondsToSelector: @selector(setCertificateVerificationEnabled:)]) [sock setCertificateVerificationEnabled: false]; } + +- (void)client: (OFHTTPClient *)client + requestsBody: (OFStream *)body + request: (OFHTTPRequest *)request + context: (id)context +{ + /* TODO: Do asynchronously and print status */ + while (![_body isAtEndOfStream]) { + char buffer[4096]; + size_t length; + + length = [_body readIntoBuffer: buffer + length: 4096]; + [body writeBuffer: buffer + length: length]; + } +} - (bool)client: (OFHTTPClient *)client shouldFollowRedirect: (OFURL *)URL statusCode: (int)statusCode request: (OFHTTPRequest *)request @@ -544,11 +561,11 @@ return true; } - (void)client: (OFHTTPClient *)client didEncounterException: (id)e - forRequest: (OFHTTPRequest *)request + request: (OFHTTPRequest *)request context: (id)context { if ([e isKindOfClass: [OFAddressTranslationFailedException class]]) { if (!_quiet) [of_stdout writeString: @"\n"]; @@ -954,11 +971,10 @@ } request = [OFHTTPRequest requestWithURL: URL]; [request setHeaders: clientHeaders]; [request setMethod: _method]; - [request setBody: _body]; [_HTTPClient asyncPerformRequest: request context: nil]; return;