From f4c7d97f0e5b1e1513d74659d9bc642c5b10a6f5 Mon Sep 17 00:00:00 2001 From: Jiang Yichen Date: Wed, 7 Feb 2018 16:31:27 +0000 Subject: [PATCH] Support chunked data upload in iOS Cronet currently accumulates all the upload data from the stream and sends it on NSStreamEventEndEncountered when using NSMutableURLRequest to post data from a stream. This CL implements a chunked data uploader to support this functionality. Related discuss: https://groups.google.com/a/chromium.org/forum/#!topic/net-dev/I02k_dEiq1g R=kapishnikov@chromium.org Bug: 755463 Cq-Include-Trybots: master.tryserver.chromium.android:android_cronet_tester;master.tryserver.chromium.mac:ios-simulator-cronet;master.tryserver.chromium.mac:ios-simulator-full-configs Change-Id: I74aef03f4ffeee8847cd08c870e3d6825a99ccfd Reviewed-on: https://chromium-review.googlesource.com/813445 Commit-Queue: Andrei Kapishnikov Reviewed-by: Andrei Kapishnikov Reviewed-by: Mark Cogan Cr-Commit-Position: refs/heads/master@{#535023} --- AUTHORS | 1 + .../cronet/ios/test/cronet_http_test.mm | 181 ++++++++++++++ ios/net/BUILD.gn | 3 + ios/net/chunked_data_stream_uploader.cc | 99 ++++++++ ios/net/chunked_data_stream_uploader.h | 86 +++++++ .../chunked_data_stream_uploader_unittest.cc | 229 ++++++++++++++++++ ios/net/crn_http_protocol_handler.mm | 88 +++++-- 7 files changed, 673 insertions(+), 14 deletions(-) create mode 100644 ios/net/chunked_data_stream_uploader.cc create mode 100644 ios/net/chunked_data_stream_uploader.h create mode 100644 ios/net/chunked_data_stream_uploader_unittest.cc diff --git a/AUTHORS b/AUTHORS index 612a1987c42a68..16e0afed5289fe 100644 --- a/AUTHORS +++ b/AUTHORS @@ -876,6 +876,7 @@ Ye Liu Yeol Park Yi Shen Yi Sun +Yichen Jiang Yizhou Jiang Yoav Weiss Yoav Zilberberg diff --git a/components/cronet/ios/test/cronet_http_test.mm b/components/cronet/ios/test/cronet_http_test.mm index 3a8b8f254000ec..dafe20a4aafdfb 100644 --- a/components/cronet/ios/test/cronet_http_test.mm +++ b/components/cronet/ios/test/cronet_http_test.mm @@ -25,6 +25,78 @@ #include "url/gurl.h" +namespace { + +// The buffer size of the stream for HTTPBodyStream post test. +const NSUInteger kRequestBodyBufferLength = 1024; + +// The buffer size of the stream for HTTPBodyStream post test when +// testing the stream buffered data size larger than the net stack internal +// buffer size. +const NSUInteger kLargeRequestBodyBufferLength = 100 * kRequestBodyBufferLength; + +// The body data write times for HTTPBodyStream post test. +const NSInteger kRequestBodyWriteTimes = 16; +} + +@interface StreamBodyRequestDelegate : NSObject +- (void)setOutputStream:(NSOutputStream*)outputStream; +- (NSMutableString*)requestBody; +@end +@implementation StreamBodyRequestDelegate { + NSOutputStream* _stream; + NSInteger _count; + + NSMutableString* _requestBody; +} + +- (instancetype)init { + _requestBody = [NSMutableString string]; + return self; +} + +- (void)setOutputStream:(NSOutputStream*)outputStream { + _stream = outputStream; +} + +- (NSMutableString*)requestBody { + return _requestBody; +} + +- (void)stream:(NSStream*)stream handleEvent:(NSStreamEvent)event { + ASSERT_EQ(stream, _stream); + switch (event) { + case NSStreamEventHasSpaceAvailable: { + if (_count < kRequestBodyWriteTimes) { + uint8_t buffer[kRequestBodyBufferLength]; + memset(buffer, 'a' + _count, kRequestBodyBufferLength); + NSUInteger bytes_write = + [_stream write:buffer maxLength:kRequestBodyBufferLength]; + ASSERT_EQ(kRequestBodyBufferLength, bytes_write); + [_requestBody appendString:[[NSString alloc] + initWithBytes:buffer + length:kRequestBodyBufferLength + encoding:NSUTF8StringEncoding]]; + ++_count; + } else { + [_stream close]; + } + break; + } + case NSStreamEventErrorOccurred: + case NSStreamEventEndEncountered: { + [_stream close]; + [_stream setDelegate:nil]; + [_stream removeFromRunLoop:[NSRunLoop currentRunLoop] + forMode:NSDefaultRunLoopMode]; + break; + } + default: + break; + } +} +@end + namespace cronet { const char kUserAgent[] = "CronetTest/1.0.0.0"; @@ -424,6 +496,115 @@ void TearDown() override { ASSERT_TRUE(block_used); } +// Verify the chunked request body upload function. +TEST_F(HttpTest, PostRequestWithBodyStream) { + // Create request body stream. + CFReadStreamRef read_stream = NULL; + CFWriteStreamRef write_stream = NULL; + CFStreamCreateBoundPair(NULL, &read_stream, &write_stream, + kRequestBodyBufferLength); + + NSInputStream* input_stream = CFBridgingRelease(read_stream); + NSOutputStream* output_stream = CFBridgingRelease(write_stream); + + StreamBodyRequestDelegate* stream_delegate = + [[StreamBodyRequestDelegate alloc] init]; + output_stream.delegate = stream_delegate; + [stream_delegate setOutputStream:output_stream]; + + dispatch_queue_t queue = + dispatch_queue_create("data upload queue", DISPATCH_QUEUE_SERIAL); + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_async(queue, ^{ + [output_stream scheduleInRunLoop:[NSRunLoop currentRunLoop] + forMode:NSDefaultRunLoopMode]; + [output_stream open]; + + [[NSRunLoop currentRunLoop] + runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10.0]]; + + dispatch_semaphore_signal(semaphore); + }); + + // Prepare the request. + NSURL* url = net::NSURLWithGURL(GURL(TestServer::EchoRequestBodyURL())); + NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url]; + request.HTTPMethod = @"POST"; + request.HTTPBodyStream = input_stream; + + // Set the request filter to check that the request was handled by the Cronet + // stack. + __block BOOL block_used = NO; + [Cronet setRequestFilterBlock:^(NSURLRequest* req) { + block_used = YES; + EXPECT_EQ([req URL], url); + return YES; + }]; + + // Send the request and wait for the response. + NSURLSessionDataTask* data_task = [session_ dataTaskWithRequest:request]; + StartDataTaskAndWaitForCompletion(data_task); + + // Verify that the response from the server matches the request body. + ASSERT_EQ(nil, [delegate_ error]); + NSString* response_body = [delegate_ responseBody]; + NSMutableString* request_body = [stream_delegate requestBody]; + ASSERT_STREQ(base::SysNSStringToUTF8(request_body).c_str(), + base::SysNSStringToUTF8(response_body).c_str()); + ASSERT_TRUE(block_used); + + // Wait for the run loop of the child thread exits. Timeout is 5 seconds. + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC); + ASSERT_EQ(0, dispatch_semaphore_wait(semaphore, timeout)); +} + +// Verify that the chunked data uploader can correctly handle the request body +// if the stream contains data length exceed the internal upload buffer. +TEST_F(HttpTest, PostRequestWithLargeBodyStream) { + // Create request body stream. + CFReadStreamRef read_stream = NULL; + CFWriteStreamRef write_stream = NULL; + // 100KB data is written in one time. + CFStreamCreateBoundPair(NULL, &read_stream, &write_stream, + kLargeRequestBodyBufferLength); + + NSInputStream* input_stream = CFBridgingRelease(read_stream); + NSOutputStream* output_stream = CFBridgingRelease(write_stream); + [output_stream open]; + + uint8_t buffer[kLargeRequestBodyBufferLength]; + memset(buffer, 'a', kLargeRequestBodyBufferLength); + NSUInteger bytes_write = + [output_stream write:buffer maxLength:kLargeRequestBodyBufferLength]; + ASSERT_EQ(kLargeRequestBodyBufferLength, bytes_write); + [output_stream close]; + + // Prepare the request. + NSURL* url = net::NSURLWithGURL(GURL(TestServer::EchoRequestBodyURL())); + NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url]; + request.HTTPMethod = @"POST"; + request.HTTPBodyStream = input_stream; + + // Set the request filter to check that the request was handled by the Cronet + // stack. + __block BOOL block_used = NO; + [Cronet setRequestFilterBlock:^(NSURLRequest* req) { + block_used = YES; + EXPECT_EQ([req URL], url); + return YES; + }]; + + // Send the request and wait for the response. + NSURLSessionDataTask* data_task = [session_ dataTaskWithRequest:request]; + StartDataTaskAndWaitForCompletion(data_task); + + // Verify that the response from the server matches the request body. + ASSERT_EQ(nil, [delegate_ error]); + NSString* response_body = [delegate_ responseBody]; + ASSERT_EQ(kLargeRequestBodyBufferLength, [response_body length]); + ASSERT_TRUE(block_used); +} + // iOS Simulator doesn't support changing thread priorities. // Therefore, run these tests only on a physical device. #if TARGET_OS_SIMULATOR diff --git a/ios/net/BUILD.gn b/ios/net/BUILD.gn index 797bbeea5d64a5..28b88459784d03 100644 --- a/ios/net/BUILD.gn +++ b/ios/net/BUILD.gn @@ -31,6 +31,8 @@ source_set("net") { configs += [ "//build/config/compiler:enable_arc" ] sources = [ + "chunked_data_stream_uploader.cc", + "chunked_data_stream_uploader.h", "clients/crn_network_client_protocol.h", "cookies/cookie_cache.cc", "cookies/cookie_cache.h", @@ -109,6 +111,7 @@ test("ios_net_unittests") { ] sources = [ + "chunked_data_stream_uploader_unittest.cc", "cookies/cookie_cache_unittest.cc", "cookies/cookie_creation_time_manager_unittest.mm", "cookies/cookie_store_ios_persistent_unittest.mm", diff --git a/ios/net/chunked_data_stream_uploader.cc b/ios/net/chunked_data_stream_uploader.cc new file mode 100644 index 00000000000000..d8a3910c545709 --- /dev/null +++ b/ios/net/chunked_data_stream_uploader.cc @@ -0,0 +1,99 @@ +// Copyright (c) 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ios/net/chunked_data_stream_uploader.h" + +#include "base/logging.h" +#include "net/base/io_buffer.h" +#include "net/base/net_errors.h" + +namespace net { + +ChunkedDataStreamUploader::ChunkedDataStreamUploader(Delegate* delegate) + : UploadDataStream(true, 0), + delegate_(delegate), + pending_read_buffer_(nullptr), + pending_read_buffer_length_(0), + pending_internal_read_(false), + is_final_chunk_(false), + is_front_of_stream_(true), + weak_factory_(this) { + DCHECK(delegate_); +} + +ChunkedDataStreamUploader::~ChunkedDataStreamUploader() {} + +int ChunkedDataStreamUploader::InitInternal(const NetLogWithSource& net_log) { + if (is_front_of_stream_) + return OK; + else + return ERR_FAILED; +} + +int ChunkedDataStreamUploader::ReadInternal(net::IOBuffer* buffer, + int buffer_length) { + DCHECK(buffer); + DCHECK_GT(buffer_length, 0); + DCHECK(!pending_read_buffer_); + + pending_read_buffer_ = buffer; + pending_read_buffer_length_ = buffer_length; + + // Read the stream if input data comes first. + return Upload(); +} + +void ChunkedDataStreamUploader::ResetInternal() { + pending_read_buffer_ = nullptr; + pending_read_buffer_length_ = 0; + pending_internal_read_ = false; + // Internal reset will not affect the external stream data state. + is_final_chunk_ = false; +} + +void ChunkedDataStreamUploader::UploadWhenReady(bool is_final_chunk) { + is_final_chunk_ = is_final_chunk; + + // Put the data if internal read comes first. + if (pending_internal_read_) { + Upload(); + } +} + +int ChunkedDataStreamUploader::Upload() { + DCHECK(pending_read_buffer_); + + is_front_of_stream_ = false; + int bytes_read = 0; + + if (is_final_chunk_) { + SetIsFinalChunk(); + } else { + bytes_read = delegate_->OnRead(pending_read_buffer_->data(), + pending_read_buffer_length_); + + // NSInputStream can read 0 bytes when hasBytesAvailable is true, so ignore + // this piece and let this internal read remain pending. + // Still returns ERR_IO_PENDING for other errors because currently it is not + // supported to return failure in UploadDataStream::Read(). Handle the + // failure in the delegate level. + if (bytes_read <= 0) { + pending_internal_read_ = true; + return ERR_IO_PENDING; + } + } + + pending_read_buffer_ = nullptr; + pending_read_buffer_length_ = 0; + + // When there is a Read() pending, call OnReadCompleted to notify read + // completed. + if (pending_internal_read_) { + pending_internal_read_ = false; + OnReadCompleted(bytes_read); + } + return bytes_read; +} + +} // namespace net diff --git a/ios/net/chunked_data_stream_uploader.h b/ios/net/chunked_data_stream_uploader.h new file mode 100644 index 00000000000000..82d3be68f2d9e0 --- /dev/null +++ b/ios/net/chunked_data_stream_uploader.h @@ -0,0 +1,86 @@ +// Copyright (c) 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef IOS_NET_CHUNKED_DATA_STREAM_UPLOADER_H_ +#define IOS_NET_CHUNKED_DATA_STREAM_UPLOADER_H_ + +#include + +#include + +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "net/base/upload_data_stream.h" + +namespace net { +class IOBuffer; + +// The ChunkedDataStreamUploader is used to support chunked data post for iOS +// NSMutableURLRequest HTTPBodyStream. Called on the network thread. It's +// responsible to coordinate the internal callbacks from network layer with the +// NSInputStream data. Rewind is not supported. +class ChunkedDataStreamUploader : public net::UploadDataStream { + public: + class Delegate { + public: + Delegate() {} + virtual ~Delegate() {} + + // Called when the request is ready to read the data for request body. + // Data must be read in this function and put into |buf|. |buf_len| gives + // the length of the provided buffer, and the return values gives the actual + // bytes read. UploadDataStream::Read() currently does not support to return + // failure, so need to handle the stream errors in the callback. + virtual int OnRead(char* buffer, int buffer_length) = 0; + }; + + ChunkedDataStreamUploader(Delegate* delegate); + ~ChunkedDataStreamUploader() override; + + // Interface for iOS layer to try to upload data. If there already has a + // internal ReadInternal() callback ready from the network layer, data will be + // writen to buffer immediately. Otherwise, it will do nothing in order to + // wait internal callback. Once it is ready for the network layer to read + // data, the OnRead() callback will be called. + void UploadWhenReady(bool is_final_chunk); + + // The uploader interface for iOS layer to use. + base::WeakPtr GetWeakPtr() { + return weak_factory_.GetWeakPtr(); + } + + private: + // Internal function to implement data upload to network layer. + int Upload(); + + // net::UploadDataStream implementation: + int InitInternal(const NetLogWithSource& net_log) override; + int ReadInternal(IOBuffer* buffer, int buffer_length) override; + void ResetInternal() override; + + Delegate* const delegate_; + + // The pointer to the network layer buffer to send and the length of the + // buffer. + net::IOBuffer* pending_read_buffer_; + int pending_read_buffer_length_; + + // Flags indicating current upload process has network read callback pending. + bool pending_internal_read_; + + // Flags indicating if current block is the last. + bool is_final_chunk_; + + // Set to false when a read starts, does not support rewinding + // for stream upload. + bool is_front_of_stream_; + + base::WeakPtrFactory weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(ChunkedDataStreamUploader); +}; + +} // namespace net + +#endif // IOS_NET_CHUNKED_DATA_STREAM_UPLOADER_H_ diff --git a/ios/net/chunked_data_stream_uploader_unittest.cc b/ios/net/chunked_data_stream_uploader_unittest.cc new file mode 100644 index 00000000000000..48f62c812cd900 --- /dev/null +++ b/ios/net/chunked_data_stream_uploader_unittest.cc @@ -0,0 +1,229 @@ +// Copyright (c) 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ios/net/chunked_data_stream_uploader.h" + +#include +#include + +#include "net/base/io_buffer.h" +#include "net/base/net_errors.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +namespace { +const int kDefaultIOBufferSize = 1024; +} + +// Mock delegate to provide data from its internal buffer. +class MockChunkedDataStreamUploaderDelegate + : public ChunkedDataStreamUploader::Delegate { + public: + MockChunkedDataStreamUploaderDelegate() : data_length_(0) {} + ~MockChunkedDataStreamUploaderDelegate() override {} + + int OnRead(char* buffer, int buffer_length) override { + int bytes_read = 0; + if (data_length_ > 0) { + CHECK_GE(buffer_length, data_length_); + memcpy(buffer, data_, data_length_); + bytes_read = data_length_; + data_length_ = 0; + } + return bytes_read; + } + + void SetReadData(const char* data, int data_length) { + CHECK_GE(sizeof(data_), static_cast(data_length)); + memcpy(data_, data, data_length); + data_length_ = data_length; + CHECK(!memcmp(data_, data, data_length)); + } + + private: + char data_[kDefaultIOBufferSize]; + int data_length_; +}; + +class ChunkedDataStreamUploaderTest : public testing::Test { + public: + ChunkedDataStreamUploaderTest() : callback_count(0) { + delegate_ = std::make_unique(); + uploader_owner_ = + std::make_unique(delegate_.get()); + uploader_ = uploader_owner_->GetWeakPtr(); + + uploader_owner_->Init(base::BindRepeating([](int) {}), + net::NetLogWithSource()); + } + + void CompletionCallback(int result) { ++callback_count; } + + protected: + std::unique_ptr delegate_; + + std::unique_ptr uploader_owner_; + base::WeakPtr uploader_; + + // Completion callback counter for each case. + int callback_count; +}; + +// Tests that data from the application layer become ready before the network +// layer callback. +TEST_F(ChunkedDataStreamUploaderTest, ExternalDataReadyFirst) { + // Application layer data is ready. + const char kTestData[] = "Hello world!"; + delegate_->SetReadData(kTestData, sizeof(kTestData)); + uploader_->UploadWhenReady(false); + + // Network layer callback is called next, and the application data is expected + // to be read to the |buffer|. + scoped_refptr buffer = new net::IOBuffer(kDefaultIOBufferSize); + int bytes_read = uploader_->Read( + buffer.get(), kDefaultIOBufferSize, + base::BindRepeating(&ChunkedDataStreamUploaderTest::CompletionCallback, + base::Unretained(this))); + + EXPECT_EQ(sizeof(kTestData), static_cast(bytes_read)); + EXPECT_FALSE( + memcmp(kTestData, buffer->data(), static_cast(bytes_read))); + + // Application finishes data upload. Called after all data has been uploaded. + delegate_->SetReadData("", 0); + uploader_->UploadWhenReady(true); + bytes_read = uploader_->Read( + buffer.get(), kDefaultIOBufferSize, + base::BindRepeating(&ChunkedDataStreamUploaderTest::CompletionCallback, + base::Unretained(this))); + EXPECT_EQ(0, bytes_read); + EXPECT_TRUE(uploader_->IsEOF()); + + // No completion callback is called because Read() call should return + // directly. + EXPECT_EQ(0, callback_count); +} + +// Tests that callback from the network layer is called before the application +// layer data available. +TEST_F(ChunkedDataStreamUploaderTest, InternalReadReadyFirst) { + // Network layer callback is called and the request is pending. + scoped_refptr buffer = new net::IOBuffer(kDefaultIOBufferSize); + int ret = uploader_->Read( + buffer.get(), kDefaultIOBufferSize, + base::BindRepeating(&ChunkedDataStreamUploaderTest::CompletionCallback, + base::Unretained(this))); + EXPECT_EQ(ERR_IO_PENDING, ret); + + // The data is writen into |buffer| once the application layer data is ready. + const char kTestData[] = "Hello world!"; + delegate_->SetReadData(kTestData, sizeof(kTestData)); + uploader_->UploadWhenReady(false); + EXPECT_FALSE(memcmp(kTestData, buffer->data(), sizeof(kTestData))); + + // Callback is called again after a successful read. + ret = uploader_->Read( + buffer.get(), kDefaultIOBufferSize, + base::BindRepeating(&ChunkedDataStreamUploaderTest::CompletionCallback, + base::Unretained(this))); + EXPECT_EQ(ERR_IO_PENDING, ret); + + // No more data is available, and the upload will be finished. + delegate_->SetReadData("", 0); + uploader_->UploadWhenReady(true); + EXPECT_TRUE(uploader_->IsEOF()); + + EXPECT_EQ(2, callback_count); +} + +// Tests that null data is correctly handled when the callback comes first. +TEST_F(ChunkedDataStreamUploaderTest, NullContentWithReadFirst) { + scoped_refptr buffer = new net::IOBuffer(kDefaultIOBufferSize); + int ret = uploader_->Read( + buffer.get(), kDefaultIOBufferSize, + base::BindRepeating(&ChunkedDataStreamUploaderTest::CompletionCallback, + base::Unretained(this))); + EXPECT_EQ(ERR_IO_PENDING, ret); + + delegate_->SetReadData("", 0); + uploader_->UploadWhenReady(true); + EXPECT_TRUE(uploader_->IsEOF()); + + EXPECT_EQ(1, callback_count); +} + +// Tests that null data is correctly handled when data is available first. +TEST_F(ChunkedDataStreamUploaderTest, NullContentWithDataFirst) { + delegate_->SetReadData("", 0); + uploader_->UploadWhenReady(true); + + scoped_refptr buffer = new net::IOBuffer(kDefaultIOBufferSize); + int bytes_read = uploader_->Read( + buffer.get(), kDefaultIOBufferSize, + base::BindRepeating(&ChunkedDataStreamUploaderTest::CompletionCallback, + base::Unretained(this))); + EXPECT_EQ(0, bytes_read); + EXPECT_TRUE(uploader_->IsEOF()); + + EXPECT_EQ(0, callback_count); +} + +// A complex test case that the application layer data and network layer +// callback becomes ready first reciprocally. +TEST_F(ChunkedDataStreamUploaderTest, MixedScenarioTest) { + // Data comes first. + const char kTestData[] = "Hello world!"; + delegate_->SetReadData(kTestData, sizeof(kTestData)); + uploader_->UploadWhenReady(false); + + scoped_refptr buffer = new net::IOBuffer(kDefaultIOBufferSize); + int bytes_read = uploader_->Read( + buffer.get(), kDefaultIOBufferSize, + base::BindRepeating(&ChunkedDataStreamUploaderTest::CompletionCallback, + base::Unretained(this))); + EXPECT_EQ(sizeof(kTestData), static_cast(bytes_read)); + EXPECT_FALSE( + memcmp(kTestData, buffer->data(), static_cast(bytes_read))); + + // Callback comes first. + int ret = uploader_->Read( + buffer.get(), kDefaultIOBufferSize, + base::BindRepeating(&ChunkedDataStreamUploaderTest::CompletionCallback, + base::Unretained(this))); + EXPECT_EQ(ERR_IO_PENDING, ret); + + char test_data_long[kDefaultIOBufferSize]; + for (int i = 0; i < static_cast(sizeof(test_data_long)); ++i) { + test_data_long[i] = static_cast(i & 0xFF); + } + delegate_->SetReadData(test_data_long, sizeof(test_data_long)); + uploader_->UploadWhenReady(false); + EXPECT_FALSE(memcmp(test_data_long, buffer->data(), sizeof(test_data_long))); + + // Callback comes first again. + ret = uploader_->Read( + buffer.get(), kDefaultIOBufferSize, + base::BindRepeating(&ChunkedDataStreamUploaderTest::CompletionCallback, + base::Unretained(this))); + EXPECT_EQ(ERR_IO_PENDING, ret); + delegate_->SetReadData(kTestData, sizeof(kTestData)); + uploader_->UploadWhenReady(false); + EXPECT_FALSE(memcmp(kTestData, buffer->data(), sizeof(kTestData))); + + // Finish and data comes first. + delegate_->SetReadData("", 0); + uploader_->UploadWhenReady(true); + bytes_read = uploader_->Read( + buffer.get(), kDefaultIOBufferSize, + base::BindRepeating(&ChunkedDataStreamUploaderTest::CompletionCallback, + base::Unretained(this))); + EXPECT_EQ(0, bytes_read); + EXPECT_TRUE(uploader_->IsEOF()); + + // Completion callback is called only after Read() returns ERR_IO_PENDING; + EXPECT_EQ(2, callback_count); +} + +} // namespace net diff --git a/ios/net/crn_http_protocol_handler.mm b/ios/net/crn_http_protocol_handler.mm index f0f8ee16ba6796..6901c8a18db67c 100644 --- a/ios/net/crn_http_protocol_handler.mm +++ b/ios/net/crn_http_protocol_handler.mm @@ -20,6 +20,7 @@ #include "base/strings/string_util.h" #include "base/strings/sys_string_conversions.h" #include "base/strings/utf_string_conversions.h" +#include "ios/net/chunked_data_stream_uploader.h" #import "ios/net/clients/crn_network_client_protocol.h" #import "ios/net/crn_http_protocol_handler_proxy_with_client_thread.h" #import "ios/net/http_protocol_logging.h" @@ -145,7 +146,8 @@ - (void)stream:(NSStream*)theStream handleEvent:(NSStreamEvent)streamEvent; class HttpProtocolHandlerCore : public base::RefCountedThreadSafe, - public URLRequest::Delegate { + public URLRequest::Delegate, + public ChunkedDataStreamUploader::Delegate { public: explicit HttpProtocolHandlerCore(NSURLRequest* request); explicit HttpProtocolHandlerCore(NSURLSessionTask* task); @@ -171,6 +173,9 @@ void OnSSLCertificateError(URLRequest* request, void OnResponseStarted(URLRequest* request, int net_error) override; void OnReadCompleted(URLRequest* request, int bytes_read) override; + // ChunkedDataStreamUploader::Delegate method: + int OnRead(char* buffer, int buffer_length) override; + private: friend class base::RefCountedThreadSafe; @@ -219,6 +224,12 @@ void CompleteAuthentication(bool auth_ok, // thread. URLRequest* net_request_; + // It is a weak pointer because the owner of the uploader is the URLRequest. + base::WeakPtr chunked_uploader_; + + // The stream has data to upload. + NSInputStream* pending_stream_; + base::WeakPtr tracker_; DISALLOW_COPY_AND_ASSIGN(HttpProtocolHandlerCore); @@ -228,7 +239,8 @@ void CompleteAuthentication(bool auth_ok, : client_(nil), read_buffer_size_(kIOBufferMinSize), read_buffer_wrapper_(nullptr), - net_request_(nullptr) { + net_request_(nullptr), + pending_stream_(nil) { // The request will be accessed from another thread. It is safer to make a // copy to avoid conflicts. // The copy is mutable, because that request will be given to the client in @@ -262,6 +274,11 @@ void CompleteAuthentication(bool auth_ok, break; case NSStreamEventEndEncountered: StopListeningStream(stream); + if (chunked_uploader_) { + chunked_uploader_->UploadWhenReady(true); + break; + } + if (!post_data_readers_.empty()) { // NOTE: This call will result in |post_data_readers_| being cleared, // which is the desired behavior. @@ -274,6 +291,13 @@ void CompleteAuthentication(bool auth_ok, tracker_->StartRequest(net_request_); break; case NSStreamEventHasBytesAvailable: { + if (chunked_uploader_) { + DCHECK(pending_stream_ == nil || pending_stream_ == stream); + pending_stream_ = base::mac::ObjCCastStrict(stream); + chunked_uploader_->UploadWhenReady(false); + break; + } + NSInteger length; // TODO(crbug.com/738025): Dynamically change the size of the read buffer // to improve the read (POST) performance, see AllocateReadBuffer(), & @@ -708,19 +732,28 @@ void CompleteAuthentication(bool auth_ok, [input_stream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [input_stream open]; - // The request will be started when the stream is fully read. - return; - } - NSData* body = [request_ HTTPBody]; - const NSUInteger body_length = [body length]; - if (body_length > 0) { - const char* source_bytes = reinterpret_cast([body bytes]); - std::vector owned_data(source_bytes, source_bytes + body_length); - std::unique_ptr reader( - new UploadOwnedBytesElementReader(&owned_data)); - net_request_->set_upload( - ElementsUploadDataStream::CreateWithReader(std::move(reader), 0)); + if (net_request_->extra_request_headers().HasHeader( + HttpRequestHeaders::kContentLength)) { + // The request will be started when the stream is fully read. + return; + } + + std::unique_ptr uploader = + std::make_unique(this); + chunked_uploader_ = uploader->GetWeakPtr(); + net_request_->set_upload(std::move(uploader)); + } else if ([request_ HTTPBody]) { + NSData* body = [request_ HTTPBody]; + const NSUInteger body_length = [body length]; + if (body_length > 0) { + const char* source_bytes = reinterpret_cast([body bytes]); + std::vector owned_data(source_bytes, source_bytes + body_length); + std::unique_ptr reader( + new UploadOwnedBytesElementReader(&owned_data)); + net_request_->set_upload( + ElementsUploadDataStream::CreateWithReader(std::move(reader), 0)); + } } net_request_->Start(); @@ -769,6 +802,7 @@ void CompleteAuthentication(bool auth_ok, [stream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; stream_delegate_ = nil; + pending_stream_ = nil; // Close the stream if needed. switch ([stream streamStatus]) { case NSStreamStatusOpening: @@ -844,6 +878,32 @@ void CompleteAuthentication(bool auth_ok, HttpRequestHeaders::kOrigin)]; } +int HttpProtocolHandlerCore::OnRead(char* buffer, int buffer_length) { + int bytes_read = 0; + if (pending_stream_) { + // NSInputStream read() blocks the thread until there is at least one byte + // available, so check the status before call read(). + if (![pending_stream_ hasBytesAvailable]) + return ERR_IO_PENDING; + + bytes_read = [pending_stream_ read:reinterpret_cast(buffer) + maxLength:buffer_length]; + // NSInputStream can read 0 byte when hasBytesAvailable is true, so do not + // treat it as a failure. + if (bytes_read < 0) { + // If NSInputStream meets an error on read(), fail the request + // immediately. + DLOG(ERROR) << "Failed to read POST data: " + << base::SysNSStringToUTF8( + [[pending_stream_ streamError] description]); + StopListeningStream(pending_stream_); + StopRequestWithError(NSURLErrorUnknown, ERR_UNEXPECTED); + return ERR_UNEXPECTED; + } + } + return bytes_read; +} + } // namespace net #pragma mark -