diff options
| author | mef <mef@chromium.org> | 2016-01-29 16:13:45 -0800 |
|---|---|---|
| committer | Commit bot <commit-bot@chromium.org> | 2016-01-30 00:14:47 +0000 |
| commit | 72e10340f0078cc85d10ae9654cec3e91b46b128 (patch) | |
| tree | 595959a6a5d54f5a8074f903c06144bd587394cc | |
| parent | b69e64de8c10dee033d42e8cd92dcca6f2f81411 (diff) | |
| download | chromium_src-72e10340f0078cc85d10ae9654cec3e91b46b128.zip chromium_src-72e10340f0078cc85d10ae9654cec3e91b46b128.tar.gz chromium_src-72e10340f0078cc85d10ae9654cec3e91b46b128.tar.bz2 | |
Initial implementation of CronetBidirectionalStream.
BUG=516342
Review URL: https://codereview.chromium.org/1412243012
Cr-Commit-Position: refs/heads/master@{#372486}
20 files changed, 3052 insertions, 86 deletions
diff --git a/components/cronet.gypi b/components/cronet.gypi index 7194ca4..1dd1a64 100644 --- a/components/cronet.gypi +++ b/components/cronet.gypi @@ -3,6 +3,9 @@ # found in the LICENSE file. { + 'variables': { + 'enable_bidirectional_stream%': 0, + }, 'conditions': [ ['OS=="android"', { 'targets': [ @@ -10,6 +13,7 @@ 'target_name': 'cronet_jni_headers', 'type': 'none', 'sources': [ + 'cronet/android/java/src/org/chromium/net/CronetBidirectionalStream.java', 'cronet/android/java/src/org/chromium/net/CronetLibraryLoader.java', 'cronet/android/java/src/org/chromium/net/CronetUploadDataStream.java', 'cronet/android/java/src/org/chromium/net/CronetUrlRequest.java', @@ -193,6 +197,17 @@ 'includes': [ 'cronet/cronet_static.gypi' ], }, { + # GN version: //cronet:features + 'target_name': 'cronet_features', + 'includes': [ '../build/buildflag_header.gypi' ], + 'variables': { + 'buildflag_header_path': 'components/cronet/cronet_features.h', + 'buildflag_flags': [ + 'ENABLE_BIDIRECTIONAL_STREAM=<(enable_bidirectional_stream)', + ], + }, + }, + { 'target_name': 'libcronet', 'type': 'shared_library', 'sources': [ @@ -242,6 +257,7 @@ '**/ChromiumUrlRequestError.java', '**/ChromiumUrlRequestFactory.java', '**/ChromiumUrlRequestPriority.java', + '**/CronetBidirectionalStream.java', '**/CronetLibraryLoader.java', '**/CronetUploadDataStream.java', '**/CronetUrlRequest.java', @@ -424,6 +440,16 @@ 'is_test_apk': 1, 'run_findbugs': 1, }, + 'conditions': [ + ['enable_bidirectional_stream==0', { + 'variables' : { + 'jar_excluded_classes': [ + '**/BidirectionalStreamTest*', + '**/TestBidirectionalStreamCallback*', + ], + }, + },], + ], 'includes': [ '../build/java_apk.gypi' ], }, { diff --git a/components/cronet/android/BUILD.gn b/components/cronet/android/BUILD.gn index 24af5be..ed917a7 100644 --- a/components/cronet/android/BUILD.gn +++ b/components/cronet/android/BUILD.gn @@ -2,6 +2,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import("//build/buildflag_header.gni") import("//build/config/android/config.gni") import("//build/config/android/rules.gni") import("//build/util/version.gni") @@ -12,12 +13,14 @@ assert(!is_component_build, "Cronet requires static library build.") declare_args() { cronet_enable_data_reduction_proxy_support = false + cronet_enable_bidirectional_stream = false } generate_jni("cronet_jni_headers") { sources = [ "java/src/org/chromium/net/ChromiumUrlRequest.java", "java/src/org/chromium/net/ChromiumUrlRequestContext.java", + "java/src/org/chromium/net/CronetBidirectionalStream.java", "java/src/org/chromium/net/CronetLibraryLoader.java", "java/src/org/chromium/net/CronetUploadDataStream.java", "java/src/org/chromium/net/CronetUrlRequest.java", @@ -101,6 +104,7 @@ template("cronet_static_tmpl") { ":cronet_jni_headers", ":cronet_url_request_java", ":cronet_version_header", + ":features", "//base", "//base:i18n", "//base:prefs", @@ -156,6 +160,13 @@ template("cronet_static_tmpl") { ] } + if (cronet_enable_bidirectional_stream) { + sources += [ + "//components/cronet/android/cronet_bidirectional_stream_adapter.cc", + "//components/cronet/android/cronet_bidirectional_stream_adapter.h", + ] + } + if (defined(invoker.defines)) { defines += invoker.defines } @@ -239,6 +250,7 @@ android_library("cronet_java") { "java/src/org/chromium/net/ChromiumUrlRequest.java", "java/src/org/chromium/net/ChromiumUrlRequestContext.java", "java/src/org/chromium/net/ChromiumUrlRequestFactory.java", + "java/src/org/chromium/net/CronetBidirectionalStream.java", "java/src/org/chromium/net/CronetLibraryLoader.java", "java/src/org/chromium/net/CronetUploadDataStream.java", "java/src/org/chromium/net/CronetUrlRequest.java", @@ -401,7 +413,20 @@ android_resources("cronet_test_apk_resources") { android_library("cronet_test_apk_java") { testonly = true - DEPRECATED_java_in_dir = "test/src" + + java_files = [ + "test/src/org/chromium/net/CronetTestApplication.java", + "test/src/org/chromium/net/MockCertVerifier.java", + "test/src/org/chromium/net/SdchObserver.java", + "test/src/org/chromium/net/CronetTestFramework.java", + "test/src/org/chromium/net/MockUrlRequestJobFactory.java", + "test/src/org/chromium/net/TestFilesInstaller.java", + "test/src/org/chromium/net/CronetTestUtil.java", + "test/src/org/chromium/net/NativeTestServer.java", + "test/src/org/chromium/net/TestUploadDataStreamHandler.java", + "test/src/org/chromium/net/NetworkChangeNotifierUtil.java", + "test/src/org/chromium/net/QuicTestServer.java", + ] deps = [ ":cronet_api", @@ -409,6 +434,18 @@ android_library("cronet_test_apk_java") { "//base:base_java", "//net/android:net_java_test_support", ] + + if (cronet_enable_bidirectional_stream) { + java_files += [ + "test/src/org/chromium/net/Http2TestHandler.java", + "test/src/org/chromium/net/Http2TestServer.java", + ] + + deps += [ + "//third_party/netty-tcnative:netty-tcnative", + "//third_party/netty4:netty_all", + ] + } } android_assets("cronet_test_apk_assets") { @@ -461,7 +498,6 @@ android_apk("cronet_test_apk") { ":cronet_tests", "//base:base_java", "//third_party/netty-tcnative:netty-tcnative_all", - "//third_party/netty4:netty_all", ] run_findbugs = true @@ -471,7 +507,38 @@ instrumentation_test_apk("cronet_test_instrumentation_apk") { apk_name = "CronetTestInstrumentation" apk_under_test = ":cronet_test_apk" android_manifest = "test/javatests/AndroidManifest.xml" - DEPRECATED_java_in_dir = "test/javatests/src" + + java_files = [ + "test/javatests/src/org/chromium/net/ChromiumUrlRequestTest.java", + "test/javatests/src/org/chromium/net/ChunkedWritableByteChannelTest.java", + "test/javatests/src/org/chromium/net/ContextInitTest.java", + "test/javatests/src/org/chromium/net/Criteria.java", + "test/javatests/src/org/chromium/net/CronetTestBase.java", + "test/javatests/src/org/chromium/net/CronetUploadTest.java", + "test/javatests/src/org/chromium/net/CronetUrlRequestContextTest.java", + "test/javatests/src/org/chromium/net/CronetUrlRequestTest.java", + "test/javatests/src/org/chromium/net/CronetUrlTest.java", + "test/javatests/src/org/chromium/net/GetStatusTest.java", + "test/javatests/src/org/chromium/net/HttpUrlRequestFactoryTest.java", + "test/javatests/src/org/chromium/net/NetworkChangeNotifierTest.java", + "test/javatests/src/org/chromium/net/PkpTest.java", + "test/javatests/src/org/chromium/net/QuicTest.java", + "test/javatests/src/org/chromium/net/SdchTest.java", + "test/javatests/src/org/chromium/net/TestDrivenDataProvider.java", + "test/javatests/src/org/chromium/net/TestHttpUrlRequestListener.java", + "test/javatests/src/org/chromium/net/TestUploadDataProvider.java", + "test/javatests/src/org/chromium/net/TestUrlRequestCallback.java", + "test/javatests/src/org/chromium/net/UploadTest.java", + "test/javatests/src/org/chromium/net/UrlResponseInfoTest.java", + "test/javatests/src/org/chromium/net/urlconnection/CronetBufferedOutputStreamTest.java", + "test/javatests/src/org/chromium/net/urlconnection/CronetChunkedOutputStreamTest.java", + "test/javatests/src/org/chromium/net/urlconnection/CronetFixedModeOutputStreamTest.java", + "test/javatests/src/org/chromium/net/urlconnection/CronetHttpURLConnectionTest.java", + "test/javatests/src/org/chromium/net/urlconnection/CronetHttpURLStreamHandlerTest.java", + "test/javatests/src/org/chromium/net/urlconnection/CronetURLStreamHandlerFactoryTest.java", + "test/javatests/src/org/chromium/net/urlconnection/MessageLoopTest.java", + "test/javatests/src/org/chromium/net/urlconnection/TestUtil.java", + ] deps = [ ":cronet_api", @@ -484,6 +551,13 @@ instrumentation_test_apk("cronet_test_instrumentation_apk") { ] run_findbugs = true + + if (cronet_enable_bidirectional_stream) { + java_files += [ + "test/javatests/src/org/chromium/net/BidirectionalStreamTest.java", + "test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java", + ] + } } test("cronet_unittests") { @@ -686,3 +760,9 @@ group("cronet_package") { ":repackage_extracted_jars", ] } + +buildflag_header("features") { + header = "../cronet_features.h" + + flags = [ "ENABLE_BIDIRECTIONAL_STREAM=$cronet_enable_bidirectional_stream" ] +} diff --git a/components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java b/components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java index af42976..3c5f19c 100644 --- a/components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java +++ b/components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java @@ -88,7 +88,7 @@ public abstract class BidirectionalStream { */ public Builder setHttpMethod(String method) { if (method == null) { - throw new NullPointerException("Invalid method name."); + throw new NullPointerException("Method is required."); } mHttpMethod = method; return this; diff --git a/components/cronet/android/api/src/org/chromium/net/CronetException.java b/components/cronet/android/api/src/org/chromium/net/CronetException.java index 9af6feee..9a9e3c4 100644 --- a/components/cronet/android/api/src/org/chromium/net/CronetException.java +++ b/components/cronet/android/api/src/org/chromium/net/CronetException.java @@ -14,6 +14,6 @@ public class CronetException extends UrlRequestException { } CronetException(String message, int netError) { - super(message, null); + super(message, netError); } } diff --git a/components/cronet/android/cronet_bidirectional_stream_adapter.cc b/components/cronet/android/cronet_bidirectional_stream_adapter.cc new file mode 100644 index 0000000..27221ee --- /dev/null +++ b/components/cronet/android/cronet_bidirectional_stream_adapter.cc @@ -0,0 +1,349 @@ +// Copyright 2015 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 "cronet_bidirectional_stream_adapter.h" + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "components/cronet/android/cronet_url_request_context_adapter.h" +#include "jni/CronetBidirectionalStream_jni.h" +#include "net/base/io_buffer.h" +#include "net/base/net_errors.h" +#include "net/base/request_priority.h" +#include "net/http/bidirectional_stream_request_info.h" +#include "net/http/http_network_session.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_status_code.h" +#include "net/http/http_transaction_factory.h" +#include "net/http/http_util.h" +#include "net/spdy/spdy_header_block.h" +#include "net/ssl/ssl_info.h" +#include "net/url_request/url_request_context.h" +#include "url/gurl.h" + +using base::android::ConvertUTF8ToJavaString; +using base::android::ConvertJavaStringToUTF8; + +namespace cronet { + +static jlong CreateBidirectionalStream( + JNIEnv* env, + const JavaParamRef<jobject>& jbidi_stream, + jlong jurl_request_context_adapter) { + CronetURLRequestContextAdapter* context_adapter = + reinterpret_cast<CronetURLRequestContextAdapter*>( + jurl_request_context_adapter); + DCHECK(context_adapter); + + CronetBidirectionalStreamAdapter* adapter = + new CronetBidirectionalStreamAdapter(context_adapter, env, jbidi_stream); + + return reinterpret_cast<jlong>(adapter); +} + +// TODO(mef): Extract this and its original from cronet_url_request_adapter.cc +// into separate module. +// net::WrappedIOBuffer subclass for a buffer owned by a Java ByteBuffer. Keeps +// the ByteBuffer alive until destroyed. Uses WrappedIOBuffer because data() is +// owned by the embedder. +class CronetBidirectionalStreamAdapter::IOBufferWithByteBuffer + : public net::WrappedIOBuffer { + public: + // Creates a buffer wrapping the Java ByteBuffer |jbyte_buffer|. + // |byte_buffer_data| points to the memory backed by the ByteBuffer, and + // |position| is the index of the first byte of data inside of the buffer. + // |limit| is the the index of the first element that should not be read or + // written, preserved to verify that buffer is not changed externally during + // networking operations. + IOBufferWithByteBuffer(JNIEnv* env, + const JavaParamRef<jobject>& jbyte_buffer, + void* byte_buffer_data, + int position, + int limit) + : net::WrappedIOBuffer(static_cast<char*>(byte_buffer_data) + position), + byte_buffer_(env, jbyte_buffer), + initial_position_(position), + initial_limit_(limit) { + DCHECK(byte_buffer_data); + DCHECK_EQ(env->GetDirectBufferAddress(jbyte_buffer), byte_buffer_data); + } + + int initial_position() const { return initial_position_; } + int initial_limit() const { return initial_limit_; } + + jobject byte_buffer() const { return byte_buffer_.obj(); } + + private: + ~IOBufferWithByteBuffer() override {} + + base::android::ScopedJavaGlobalRef<jobject> byte_buffer_; + + const int initial_position_; + const int initial_limit_; +}; + +// static +bool CronetBidirectionalStreamAdapter::RegisterJni(JNIEnv* env) { + return RegisterNativesImpl(env); +} + +CronetBidirectionalStreamAdapter::CronetBidirectionalStreamAdapter( + CronetURLRequestContextAdapter* context, + JNIEnv* env, + const JavaParamRef<jobject>& jbidi_stream) + : context_(context), owner_(env, jbidi_stream) {} + +CronetBidirectionalStreamAdapter::~CronetBidirectionalStreamAdapter() { + DCHECK(context_->IsOnNetworkThread()); +} + +jint CronetBidirectionalStreamAdapter::Start( + JNIEnv* env, + const JavaParamRef<jobject>& jcaller, + const JavaParamRef<jstring>& jurl, + jint jpriority, + const JavaParamRef<jstring>& jmethod, + const JavaParamRef<jobjectArray>& jheaders, + jboolean jend_of_stream) { + // Prepare request info here to be able to return the error. + scoped_ptr<net::BidirectionalStreamRequestInfo> request_info( + new net::BidirectionalStreamRequestInfo()); + request_info->url = GURL(ConvertJavaStringToUTF8(env, jurl)); + request_info->priority = static_cast<net::RequestPriority>(jpriority); + // Http method is a token, just as header name. + request_info->method = ConvertJavaStringToUTF8(env, jmethod); + if (!net::HttpUtil::IsValidHeaderName(request_info->method)) + return -1; + + std::vector<std::string> headers; + base::android::AppendJavaStringArrayToStringVector(env, jheaders, &headers); + for (size_t i = 0; i < headers.size(); i += 2) { + std::string name(headers[i]); + std::string value(headers[i + 1]); + if (!net::HttpUtil::IsValidHeaderName(name) || + !net::HttpUtil::IsValidHeaderValue(value)) { + return i + 1; + } + request_info->extra_headers.SetHeader(name, value); + } + request_info->end_stream_on_headers = jend_of_stream; + + context_->PostTaskToNetworkThread( + FROM_HERE, + base::Bind(&CronetBidirectionalStreamAdapter::StartOnNetworkThread, + base::Unretained(this), base::Passed(&request_info))); + return 0; +} + +jboolean CronetBidirectionalStreamAdapter::ReadData( + JNIEnv* env, + const JavaParamRef<jobject>& jcaller, + const JavaParamRef<jobject>& jbyte_buffer, + jint jposition, + jint jlimit) { + DCHECK_LT(jposition, jlimit); + + void* data = env->GetDirectBufferAddress(jbyte_buffer); + if (!data) + return JNI_FALSE; + + scoped_refptr<IOBufferWithByteBuffer> read_buffer( + new IOBufferWithByteBuffer(env, jbyte_buffer, data, jposition, jlimit)); + + int remaining_capacity = jlimit - jposition; + + context_->PostTaskToNetworkThread( + FROM_HERE, + base::Bind(&CronetBidirectionalStreamAdapter::ReadDataOnNetworkThread, + base::Unretained(this), read_buffer, remaining_capacity)); + return JNI_TRUE; +} + +jboolean CronetBidirectionalStreamAdapter::WriteData( + JNIEnv* env, + const JavaParamRef<jobject>& jcaller, + const JavaParamRef<jobject>& jbyte_buffer, + jint jposition, + jint jlimit, + jboolean jend_of_stream) { + DCHECK_LE(jposition, jlimit); + + void* data = env->GetDirectBufferAddress(jbyte_buffer); + if (!data) + return JNI_FALSE; + + scoped_refptr<IOBufferWithByteBuffer> write_buffer( + new IOBufferWithByteBuffer(env, jbyte_buffer, data, jposition, jlimit)); + + int remaining_capacity = jlimit - jposition; + + context_->PostTaskToNetworkThread( + FROM_HERE, + base::Bind(&CronetBidirectionalStreamAdapter::WriteDataOnNetworkThread, + base::Unretained(this), write_buffer, remaining_capacity, + jend_of_stream)); + return JNI_TRUE; +} + +void CronetBidirectionalStreamAdapter::Destroy( + JNIEnv* env, + const JavaParamRef<jobject>& jcaller, + jboolean jsend_on_canceled) { + // Destroy could be called from any thread, including network thread (if + // posting task to executor throws an exception), but is posted, so |this| + // is valid until calling task is complete. Destroy() is always called from + // within a synchronized java block that guarantees no future posts to the + // network thread with the adapter pointer. + context_->PostTaskToNetworkThread( + FROM_HERE, + base::Bind(&CronetBidirectionalStreamAdapter::DestroyOnNetworkThread, + base::Unretained(this), jsend_on_canceled)); +} + +void CronetBidirectionalStreamAdapter::OnHeadersSent() { + DCHECK(context_->IsOnNetworkThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetBidirectionalStream_onRequestHeadersSent(env, + owner_.obj()); +} + +void CronetBidirectionalStreamAdapter::OnHeadersReceived( + const net::SpdyHeaderBlock& response_headers) { + DCHECK(context_->IsOnNetworkThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + // Get http status code from response headers. + jint http_status_code = 0; + const auto http_status_header = response_headers.find(":status"); + if (http_status_header != response_headers.end()) + base::StringToInt(http_status_header->second, &http_status_code); + + std::string protocol; + switch (bidi_stream_->GetProtocol()) { + case net::kProtoHTTP2: + protocol = "h2"; + default: + break; + } + + cronet::Java_CronetBidirectionalStream_onResponseHeadersReceived( + env, owner_.obj(), http_status_code, + ConvertUTF8ToJavaString(env, protocol).obj(), + GetHeadersArray(env, response_headers).obj(), + bidi_stream_->GetTotalReceivedBytes()); +} + +void CronetBidirectionalStreamAdapter::OnDataRead(int bytes_read) { + DCHECK(context_->IsOnNetworkThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetBidirectionalStream_onReadCompleted( + env, owner_.obj(), read_buffer_->byte_buffer(), bytes_read, + read_buffer_->initial_position(), read_buffer_->initial_limit(), + bidi_stream_->GetTotalReceivedBytes()); + // Free the read buffer. This lets the Java ByteBuffer be freed, if the + // embedder releases it, too. + read_buffer_ = nullptr; +} + +void CronetBidirectionalStreamAdapter::OnDataSent() { + DCHECK(context_->IsOnNetworkThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetBidirectionalStream_onWriteCompleted( + env, owner_.obj(), write_buffer_->byte_buffer(), + write_buffer_->initial_position(), write_buffer_->initial_limit()); + // Free the write buffer. This lets the Java ByteBuffer be freed, if the + // embedder releases it, too. + write_buffer_ = nullptr; +} + +void CronetBidirectionalStreamAdapter::OnTrailersReceived( + const net::SpdyHeaderBlock& response_trailers) { + DCHECK(context_->IsOnNetworkThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetBidirectionalStream_onResponseTrailersReceived( + env, owner_.obj(), GetHeadersArray(env, response_trailers).obj()); +} + +void CronetBidirectionalStreamAdapter::OnFailed(int error) { + DCHECK(context_->IsOnNetworkThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetBidirectionalStream_onError( + env, owner_.obj(), error, + ConvertUTF8ToJavaString(env, net::ErrorToString(error)).obj(), + bidi_stream_->GetTotalReceivedBytes()); +} + +void CronetBidirectionalStreamAdapter::StartOnNetworkThread( + scoped_ptr<net::BidirectionalStreamRequestInfo> request_info) { + DCHECK(context_->IsOnNetworkThread()); + DCHECK(!bidi_stream_); + bidi_stream_.reset(new net::BidirectionalStream( + std::move(request_info), context_->GetURLRequestContext() + ->http_transaction_factory() + ->GetSession(), + this)); +} + +void CronetBidirectionalStreamAdapter::ReadDataOnNetworkThread( + scoped_refptr<IOBufferWithByteBuffer> read_buffer, + int buffer_size) { + DCHECK(context_->IsOnNetworkThread()); + DCHECK(read_buffer); + DCHECK(!read_buffer_); + + read_buffer_ = read_buffer; + + int bytes_read = bidi_stream_->ReadData(read_buffer_.get(), buffer_size); + // If IO is pending, wait for the BidirectionalStream to call OnDataRead. + if (bytes_read == net::ERR_IO_PENDING) + return; + + if (bytes_read < 0) { + OnFailed(bytes_read); + return; + } + OnDataRead(bytes_read); +} + +void CronetBidirectionalStreamAdapter::WriteDataOnNetworkThread( + scoped_refptr<IOBufferWithByteBuffer> write_buffer, + int buffer_size, + bool end_of_stream) { + DCHECK(context_->IsOnNetworkThread()); + DCHECK(write_buffer); + DCHECK(!write_buffer_); + + write_buffer_ = write_buffer; + bidi_stream_->SendData(write_buffer_.get(), buffer_size, end_of_stream); +} + +void CronetBidirectionalStreamAdapter::DestroyOnNetworkThread( + bool send_on_canceled) { + DCHECK(context_->IsOnNetworkThread()); + if (send_on_canceled) { + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetBidirectionalStream_onCanceled(env, owner_.obj()); + } + delete this; +} + +base::android::ScopedJavaLocalRef<jobjectArray> +CronetBidirectionalStreamAdapter::GetHeadersArray( + JNIEnv* env, + const net::SpdyHeaderBlock& header_block) { + DCHECK(context_->IsOnNetworkThread()); + + std::vector<std::string> headers; + for (const auto& header : header_block) { + headers.push_back(header.first.as_string()); + headers.push_back(header.second.as_string()); + } + return base::android::ToJavaArrayOfStrings(env, headers); +} + +} // namespace cronet diff --git a/components/cronet/android/cronet_bidirectional_stream_adapter.h b/components/cronet/android/cronet_bidirectional_stream_adapter.h new file mode 100644 index 0000000..14b403e --- /dev/null +++ b/components/cronet/android/cronet_bidirectional_stream_adapter.h @@ -0,0 +1,130 @@ +// Copyright 2015 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 COMPONENTS_CRONET_ANDROID_CRONET_BIDIRECTIONAL_STREAM_ADAPTER_H_ +#define COMPONENTS_CRONET_ANDROID_CRONET_BIDIRECTIONAL_STREAM_ADAPTER_H_ + +#include <jni.h> +#include <string> + +#include "base/android/jni_android.h" +#include "base/android/jni_array.h" +#include "base/android/jni_string.h" +#include "base/android/scoped_java_ref.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "net/http/bidirectional_stream.h" + +namespace net { +class BidirectionalStreamRequestInfo; +class SpdyHeaderBlock; +} // namespace net + +namespace cronet { + +class CronetURLRequestContextAdapter; + +// An adapter from Java BidirectionalStream object to net::BidirectionalStream. +// Created and configured from a Java thread. Start, ReadData, WriteData and +// Destroy can be called on any thread (including network thread), and post +// calls to corresponding {Start|ReadData|WriteData|Destroy}OnNetworkThread to +// the network thread. The object is always deleted on network thread. All +// callbacks into the Java BidirectionalStream are done on the network thread. +// Java BidirectionalStream is expected to initiate the next step like ReadData +// or Destroy. Public methods can be called on any thread. +class CronetBidirectionalStreamAdapter + : public net::BidirectionalStream::Delegate { + public: + static bool RegisterJni(JNIEnv* env); + + CronetBidirectionalStreamAdapter( + CronetURLRequestContextAdapter* context, + JNIEnv* env, + const base::android::JavaParamRef<jobject>& jbidi_stream); + ~CronetBidirectionalStreamAdapter() override; + + // Validates method and headers, initializes and starts the request. If + // |jend_of_stream| is true, then stream is half-closed after sending header + // frame and no data is expected to be written. + // Returns 0 if request is valid and started successfully, + // Returns -1 if |jmethod| is not valid HTTP method name. + // Returns position of invalid header value in |jheaders| if header name is + // not valid. + jint Start(JNIEnv* env, + const base::android::JavaParamRef<jobject>& jcaller, + const base::android::JavaParamRef<jstring>& jurl, + jint jpriority, + const base::android::JavaParamRef<jstring>& jmethod, + const base::android::JavaParamRef<jobjectArray>& jheaders, + jboolean jend_of_stream); + + // Reads more data into |jbyte_buffer| starting at |jposition| and not + // exceeding |jlimit|. Arguments are preserved to ensure that |jbyte_buffer| + // is not modified by the application during read. + jboolean ReadData(JNIEnv* env, + const base::android::JavaParamRef<jobject>& jcaller, + const base::android::JavaParamRef<jobject>& jbyte_buffer, + jint jposition, + jint jlimit); + + // Writes more data from |jbyte_buffer| starting at |jposition| and ending at + // |jlimit|-1. Arguments are preserved to ensure that |jbyte_buffer| + // is not modified by the application during write. The |jend_of_stream| is + // passed to remote to indicate end of stream. + jboolean WriteData(JNIEnv* env, + const base::android::JavaParamRef<jobject>& jcaller, + const base::android::JavaParamRef<jobject>& jbyte_buffer, + jint jposition, + jint jlimit, + jboolean jend_of_stream); + + // Releases all resources for the request and deletes the object itself. + // |jsend_on_canceled| indicates if Java onCanceled callback should be + // issued to indicate that no more callbacks will be issued. + void Destroy(JNIEnv* env, + const base::android::JavaParamRef<jobject>& jcaller, + jboolean jsend_on_canceled); + + private: + class IOBufferWithByteBuffer; + + // net::BidirectionalStream::Delegate implementations: + void OnHeadersSent() override; + void OnHeadersReceived(const net::SpdyHeaderBlock& response_headers) override; + void OnDataRead(int bytes_read) override; + void OnDataSent() override; + void OnTrailersReceived(const net::SpdyHeaderBlock& trailers) override; + void OnFailed(int error) override; + + void StartOnNetworkThread( + scoped_ptr<net::BidirectionalStreamRequestInfo> request_info); + void ReadDataOnNetworkThread( + scoped_refptr<IOBufferWithByteBuffer> read_buffer, + int buffer_size); + void WriteDataOnNetworkThread( + scoped_refptr<IOBufferWithByteBuffer> read_buffer, + int buffer_size, + bool end_of_stream); + void DestroyOnNetworkThread(bool send_on_canceled); + // Gets headers as a Java array. + base::android::ScopedJavaLocalRef<jobjectArray> GetHeadersArray( + JNIEnv* env, + const net::SpdyHeaderBlock& header_block); + + CronetURLRequestContextAdapter* const context_; + + // Java object that owns this CronetBidirectionalStreamAdapter. + base::android::ScopedJavaGlobalRef<jobject> owner_; + + scoped_refptr<IOBufferWithByteBuffer> read_buffer_; + scoped_refptr<IOBufferWithByteBuffer> write_buffer_; + scoped_ptr<net::BidirectionalStream> bidi_stream_; + + DISALLOW_COPY_AND_ASSIGN(CronetBidirectionalStreamAdapter); +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_CRONET_BIDIRECTIONAL_STREAM_ADAPTER_H_ diff --git a/components/cronet/android/cronet_library_loader.cc b/components/cronet/android/cronet_library_loader.cc index 018d000..85b8ebf 100644 --- a/components/cronet/android/cronet_library_loader.cc +++ b/components/cronet/android/cronet_library_loader.cc @@ -21,6 +21,7 @@ #include "components/cronet/android/cronet_upload_data_stream_adapter.h" #include "components/cronet/android/cronet_url_request_adapter.h" #include "components/cronet/android/cronet_url_request_context_adapter.h" +#include "components/cronet/cronet_features.h" #include "components/cronet/version.h" #include "jni/CronetLibraryLoader_jni.h" #include "net/android/net_jni_registrar.h" @@ -28,6 +29,10 @@ #include "net/base/network_change_notifier.h" #include "url/url_util.h" +#if BUILDFLAG(ENABLE_BIDIRECTIONAL_STREAM) +#include "components/cronet/android/cronet_bidirectional_stream_adapter.h" +#endif + #if defined(USE_ICU_ALTERNATIVES_ON_ANDROID) #include "url/android/url_jni_registrar.h" // nogncheck #else @@ -41,6 +46,10 @@ const base::android::RegistrationMethod kCronetRegisteredMethods[] = { {"BaseAndroid", base::android::RegisterJni}, {"ChromiumUrlRequest", ChromiumUrlRequestRegisterJni}, {"ChromiumUrlRequestContext", ChromiumUrlRequestContextRegisterJni}, +#if BUILDFLAG(ENABLE_BIDIRECTIONAL_STREAM) + {"CronetBidirectionalStreamAdapter", + CronetBidirectionalStreamAdapter::RegisterJni}, +#endif {"CronetLibraryLoader", RegisterNativesImpl}, {"CronetUploadDataStreamAdapter", CronetUploadDataStreamAdapterRegisterJni}, {"CronetUrlRequestAdapter", CronetUrlRequestAdapterRegisterJni}, diff --git a/components/cronet/android/java/src/org/chromium/net/CronetBidirectionalStream.java b/components/cronet/android/java/src/org/chromium/net/CronetBidirectionalStream.java new file mode 100644 index 0000000..53fdea1 --- /dev/null +++ b/components/cronet/android/java/src/org/chromium/net/CronetBidirectionalStream.java @@ -0,0 +1,644 @@ +// Copyright 2015 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. + +package org.chromium.net; + +import org.chromium.base.Log; +import org.chromium.base.VisibleForTesting; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeClassQualifiedName; + +import java.nio.ByteBuffer; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +import javax.annotation.concurrent.GuardedBy; + +/** + * {@link BidirectionalStream} implementation using Chromium network stack. + * All @CalledByNative methods are called on the native network thread + * and post tasks with callback calls onto Executor. Upon returning from callback, the native + * stream is called on Executor thread and posts native tasks to the native network thread. + */ +@JNINamespace("cronet") +class CronetBidirectionalStream extends BidirectionalStream { + /** + * States of BidirectionalStream are tracked in mReadState and mWriteState. + * The write state is separated out as it changes independently of the read state. + * There is one initial state: State.NOT_STARTED. There is one normal final state: + * State.SUCCESS, reached after State.READING_DONE and State.WRITING_DONE. There are two + * exceptional final states: State.CANCELED and State.ERROR, which can be reached from + * any other non-final state. + */ + private enum State { + /* Initial state, stream not started. */ + NOT_STARTED, + /* Stream started, request headers are being sent. */ + STARTED, + /* Waiting for {@code read()} to be called. */ + WAITING_FOR_READ, + /* Reading from the remote, {@code onReadCompleted()} callback will be called when done. */ + READING, + /* There is no more data to read and stream is half-closed by the remote side. */ + READING_DONE, + /* Stream is canceled. */ + CANCELED, + /* Error has occured, stream is closed. */ + ERROR, + /* Reading and writing are done, and the stream is closed successfully. */ + SUCCESS, + /* Waiting for {@code write()} to be called. */ + WAITING_FOR_WRITE, + /* Writing to the remote, {@code onWriteCompleted()} callback will be called when done. */ + WRITING, + /* There is no more data to write and stream is half-closed by the local side. */ + WRITING_DONE, + } + + private final CronetUrlRequestContext mRequestContext; + private final Executor mExecutor; + private final Callback mCallback; + private final String mInitialUrl; + private final int mInitialPriority; + private final String mInitialMethod; + private final String mRequestHeaders[]; + + /* + * Synchronizes access to mNativeStream, mReadState and mWriteState. + */ + private final Object mNativeStreamLock = new Object(); + + /* Native BidirectionalStream object, owned by CronetBidirectionalStream. */ + @GuardedBy("mNativeStreamLock") + private long mNativeStream; + + /** + * Read state is tracking reading flow. + * / <--- READING <--- \ + * | | + * \ / + * NOT_STARTED -> STARTED --> WAITING_FOR_READ -> READING_DONE -> SUCCESS + */ + @GuardedBy("mNativeStreamLock") + private State mReadState = State.NOT_STARTED; + + /** + * Write state is tracking writing flow. + * / <--- WRITING <--- \ + * | | + * \ / + * NOT_STARTED -> STARTED --> WAITING_FOR_WRITE -> WRITING_DONE -> SUCCESS + */ + @GuardedBy("mNativeStreamLock") + private State mWriteState = State.NOT_STARTED; + + private UrlResponseInfo mResponseInfo; + + /* + * OnReadCompleted callback is repeatedly invoked when each read is completed, so it + * is cached as a member variable. + */ + private OnReadCompletedRunnable mOnReadCompletedTask; + + /* + * OnWriteCompleted callback is repeatedly invoked when each write is completed, so it + * is cached as a member variable. + */ + private OnWriteCompletedRunnable mOnWriteCompletedTask; + + private Runnable mOnDestroyedCallbackForTesting; + + private final class OnReadCompletedRunnable implements Runnable { + // Buffer passed back from current invocation of onReadCompleted. + ByteBuffer mByteBuffer; + // End of stream flag from current invocation of onReadCompleted. + boolean mEndOfStream; + + @Override + public void run() { + try { + // Null out mByteBuffer, to pass buffer ownership to callback or release if done. + ByteBuffer buffer = mByteBuffer; + mByteBuffer = null; + synchronized (mNativeStreamLock) { + if (isDoneLocked()) { + return; + } + if (mEndOfStream) { + mReadState = State.READING_DONE; + if (maybeSucceedLocked()) { + return; + } + } else { + mReadState = State.WAITING_FOR_READ; + } + } + mCallback.onReadCompleted(CronetBidirectionalStream.this, mResponseInfo, buffer); + } catch (Exception e) { + onCallbackException(e); + } + } + } + + private final class OnWriteCompletedRunnable implements Runnable { + // Buffer passed back from current invocation of onWriteCompleted. + ByteBuffer mByteBuffer; + // End of stream flag from current call to write. + boolean mEndOfStream; + + @Override + public void run() { + try { + // Null out mByteBuffer, to pass buffer ownership to callback or release if done. + ByteBuffer buffer = mByteBuffer; + mByteBuffer = null; + synchronized (mNativeStreamLock) { + if (isDoneLocked()) { + return; + } + if (mEndOfStream) { + mWriteState = State.WRITING_DONE; + if (maybeSucceedLocked()) { + return; + } + } else { + mWriteState = State.WAITING_FOR_WRITE; + } + } + mCallback.onWriteCompleted(CronetBidirectionalStream.this, mResponseInfo, buffer); + } catch (Exception e) { + onCallbackException(e); + } + } + } + + CronetBidirectionalStream(CronetUrlRequestContext requestContext, String url, + @BidirectionalStream.Builder.StreamPriority int priority, Callback callback, + Executor executor, String httpMethod, List<Map.Entry<String, String>> requestHeaders) { + mRequestContext = requestContext; + mInitialUrl = url; + mInitialPriority = convertStreamPriority(priority); + mCallback = callback; + mExecutor = executor; + mInitialMethod = httpMethod; + mRequestHeaders = stringsFromHeaderList(requestHeaders); + } + + @Override + public void start() { + synchronized (mNativeStreamLock) { + if (mReadState != State.NOT_STARTED) { + throw new IllegalStateException("Stream is already started."); + } + try { + mNativeStream = nativeCreateBidirectionalStream( + mRequestContext.getUrlRequestContextAdapter()); + mRequestContext.onRequestStarted(); + // Non-zero startResult means an argument error. + int startResult = nativeStart(mNativeStream, mInitialUrl, mInitialPriority, + mInitialMethod, mRequestHeaders, !doesMethodAllowWriteData(mInitialMethod)); + if (startResult == -1) { + throw new IllegalArgumentException("Invalid http method " + mInitialMethod); + } + if (startResult > 0) { + int headerPos = startResult - 1; + throw new IllegalArgumentException("Invalid header " + + mRequestHeaders[headerPos] + "=" + mRequestHeaders[headerPos + 1]); + } + mReadState = mWriteState = State.STARTED; + } catch (RuntimeException e) { + // If there's an exception, clean up and then throw the + // exception to the caller. + destroyNativeStreamLocked(false); + throw e; + } + } + } + + @Override + public void read(ByteBuffer buffer) { + synchronized (mNativeStreamLock) { + Preconditions.checkHasRemaining(buffer); + Preconditions.checkDirect(buffer); + if (mReadState != State.WAITING_FOR_READ) { + throw new IllegalStateException("Unexpected read attempt."); + } + if (isDoneLocked()) { + return; + } + if (mOnReadCompletedTask == null) { + mOnReadCompletedTask = new OnReadCompletedRunnable(); + } + mReadState = State.READING; + if (!nativeReadData(mNativeStream, buffer, buffer.position(), buffer.limit())) { + // Still waiting on read. This is just to have consistent + // behavior with the other error cases. + mReadState = State.WAITING_FOR_READ; + throw new IllegalArgumentException("Unable to call native read"); + } + } + } + + @Override + public void write(ByteBuffer buffer, boolean endOfStream) { + synchronized (mNativeStreamLock) { + Preconditions.checkDirect(buffer); + if (!buffer.hasRemaining() && !endOfStream) { + throw new IllegalArgumentException("Empty buffer before end of stream."); + } + if (mWriteState != State.WAITING_FOR_WRITE) { + throw new IllegalStateException("Unexpected write attempt."); + } + if (isDoneLocked()) { + return; + } + if (mOnWriteCompletedTask == null) { + mOnWriteCompletedTask = new OnWriteCompletedRunnable(); + } + mOnWriteCompletedTask.mEndOfStream = endOfStream; + mWriteState = State.WRITING; + if (!nativeWriteData( + mNativeStream, buffer, buffer.position(), buffer.limit(), endOfStream)) { + // Still waiting on write. This is just to have consistent + // behavior with the other error cases. + mWriteState = State.WAITING_FOR_WRITE; + throw new IllegalArgumentException("Unable to call native write"); + } + } + } + + @Override + public void ping(PingCallback callback, Executor executor) { + // TODO(mef): May be last thing to be implemented on Android. + throw new UnsupportedOperationException("ping is not supported yet."); + } + + @Override + public void windowUpdate(int windowSizeIncrement) { + // TODO(mef): Understand the needs and semantics of this method. + throw new UnsupportedOperationException("windowUpdate is not supported yet."); + } + + @Override + public void cancel() { + synchronized (mNativeStreamLock) { + if (isDoneLocked() || mReadState == State.NOT_STARTED) { + return; + } + mReadState = mWriteState = State.CANCELED; + destroyNativeStreamLocked(true); + } + } + + @Override + public boolean isDone() { + synchronized (mNativeStreamLock) { + return isDoneLocked(); + } + } + + @GuardedBy("mNativeStreamLock") + private boolean isDoneLocked() { + return mReadState != State.NOT_STARTED && mNativeStream == 0; + } + + @SuppressWarnings("unused") + @CalledByNative + private void onRequestHeadersSent() { + postTaskToExecutor(new Runnable() { + public void run() { + synchronized (mNativeStreamLock) { + if (isDoneLocked()) { + return; + } + if (doesMethodAllowWriteData(mInitialMethod)) { + mWriteState = State.WAITING_FOR_WRITE; + } else { + mWriteState = State.WRITING_DONE; + } + } + + try { + mCallback.onRequestHeadersSent(CronetBidirectionalStream.this); + } catch (Exception e) { + onCallbackException(e); + } + } + }); + } + + /** + * Called when the final set of headers, after all redirects, + * is received. Can only be called once for each stream. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onResponseHeadersReceived(int httpStatusCode, String negotiatedProtocol, + String[] headers, long receivedBytesCount) { + try { + mResponseInfo = prepareResponseInfoOnNetworkThread( + httpStatusCode, negotiatedProtocol, headers, receivedBytesCount); + } catch (Exception e) { + failWithException(new CronetException("Cannot prepare ResponseInfo", null)); + return; + } + postTaskToExecutor(new Runnable() { + public void run() { + synchronized (mNativeStreamLock) { + if (isDoneLocked()) { + return; + } + mReadState = State.WAITING_FOR_READ; + } + + try { + mCallback.onResponseHeadersReceived( + CronetBidirectionalStream.this, mResponseInfo); + } catch (Exception e) { + onCallbackException(e); + } + } + }); + } + + @SuppressWarnings("unused") + @CalledByNative + private void onReadCompleted(final ByteBuffer byteBuffer, int bytesRead, int initialPosition, + int initialLimit, long receivedBytesCount) { + mResponseInfo.setReceivedBytesCount(receivedBytesCount); + if (byteBuffer.position() != initialPosition || byteBuffer.limit() != initialLimit) { + failWithException( + new CronetException("ByteBuffer modified externally during read", null)); + return; + } + if (bytesRead < 0 || initialPosition + bytesRead > initialLimit) { + failWithException(new CronetException("Invalid number of bytes read", null)); + return; + } + byteBuffer.position(initialPosition + bytesRead); + assert mOnReadCompletedTask.mByteBuffer == null; + mOnReadCompletedTask.mByteBuffer = byteBuffer; + mOnReadCompletedTask.mEndOfStream = (bytesRead == 0); + postTaskToExecutor(mOnReadCompletedTask); + } + + @SuppressWarnings("unused") + @CalledByNative + private void onWriteCompleted( + final ByteBuffer byteBuffer, int initialPosition, int initialLimit) { + if (byteBuffer.position() != initialPosition || byteBuffer.limit() != initialLimit) { + failWithException( + new CronetException("ByteBuffer modified externally during write", null)); + return; + } + // Current implementation always writes the complete buffer. + byteBuffer.position(byteBuffer.limit()); + assert mOnWriteCompletedTask.mByteBuffer == null; + mOnWriteCompletedTask.mByteBuffer = byteBuffer; + postTaskToExecutor(mOnWriteCompletedTask); + } + + @SuppressWarnings("unused") + @CalledByNative + private void onResponseTrailersReceived(String[] trailers) { + final UrlResponseInfo.HeaderBlock trailersBlock = + new UrlResponseInfo.HeaderBlock(headersListFromStrings(trailers)); + postTaskToExecutor(new Runnable() { + public void run() { + synchronized (mNativeStreamLock) { + if (isDoneLocked()) { + return; + } + } + try { + mCallback.onResponseTrailersReceived( + CronetBidirectionalStream.this, mResponseInfo, trailersBlock); + } catch (Exception e) { + onCallbackException(e); + } + } + }); + } + + @SuppressWarnings("unused") + @CalledByNative + private void onError(final int nativeError, final String errorString, long receivedBytesCount) { + if (mResponseInfo != null) { + mResponseInfo.setReceivedBytesCount(receivedBytesCount); + } + failWithException(new CronetException( + "Exception in BidirectionalStream: " + errorString, nativeError)); + } + + /** + * Called when request is canceled, no callbacks will be called afterwards. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onCanceled() { + postTaskToExecutor(new Runnable() { + public void run() { + try { + mCallback.onCanceled(CronetBidirectionalStream.this, mResponseInfo); + } catch (Exception e) { + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in onCanceled method", e); + } + } + }); + } + + @VisibleForTesting + public void setOnDestroyedCallbackForTesting(Runnable onDestroyedCallbackForTesting) { + mOnDestroyedCallbackForTesting = onDestroyedCallbackForTesting; + } + + private static boolean doesMethodAllowWriteData(String methodName) { + return !methodName.equals("GET") && !methodName.equals("HEAD"); + } + + private static ArrayList<Map.Entry<String, String>> headersListFromStrings(String[] headers) { + ArrayList<Map.Entry<String, String>> headersList = new ArrayList<>(headers.length / 2); + for (int i = 0; i < headers.length; i += 2) { + headersList.add(new AbstractMap.SimpleImmutableEntry<>(headers[i], headers[i + 1])); + } + return headersList; + } + + private static String[] stringsFromHeaderList(List<Map.Entry<String, String>> headersList) { + String headersArray[] = new String[headersList.size() * 2]; + int i = 0; + for (Map.Entry<String, String> requestHeader : headersList) { + headersArray[i++] = requestHeader.getKey(); + headersArray[i++] = requestHeader.getValue(); + } + return headersArray; + } + + private static int convertStreamPriority( + @BidirectionalStream.Builder.StreamPriority int priority) { + switch (priority) { + case Builder.STREAM_PRIORITY_IDLE: + return RequestPriority.IDLE; + case Builder.STREAM_PRIORITY_LOWEST: + return RequestPriority.LOWEST; + case Builder.STREAM_PRIORITY_LOW: + return RequestPriority.LOW; + case Builder.STREAM_PRIORITY_MEDIUM: + return RequestPriority.MEDIUM; + case Builder.STREAM_PRIORITY_HIGHEST: + return RequestPriority.HIGHEST; + default: + throw new IllegalArgumentException("Invalid stream priority."); + } + } + + /** + * Checks whether reading and writing are done. + * @return false if either reading or writing is not done. If both reading and writing + * are done, then posts cleanup task and returns true. + */ + @GuardedBy("mNativeStreamLock") + private boolean maybeSucceedLocked() { + if (mReadState != State.READING_DONE || mWriteState != State.WRITING_DONE) { + return false; + } + + mReadState = mWriteState = State.SUCCESS; + postTaskToExecutor(new Runnable() { + public void run() { + synchronized (mNativeStreamLock) { + if (isDoneLocked()) { + return; + } + // Destroy native stream first, so UrlRequestContext could be shut + // down from the listener. + destroyNativeStreamLocked(false); + } + try { + mCallback.onSucceeded(CronetBidirectionalStream.this, mResponseInfo); + } catch (Exception e) { + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in onSucceeded method", e); + } + } + }); + return true; + } + + /** + * Posts task to application Executor. Used for callbacks + * and other tasks that should not be executed on network thread. + */ + private void postTaskToExecutor(Runnable task) { + try { + mExecutor.execute(task); + } catch (RejectedExecutionException failException) { + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception posting task to executor", + failException); + // If posting a task throws an exception, then there is no choice + // but to destroy the stream without invoking the callback. + synchronized (mNativeStreamLock) { + mReadState = mWriteState = State.ERROR; + destroyNativeStreamLocked(false); + } + } + } + + private UrlResponseInfo prepareResponseInfoOnNetworkThread(int httpStatusCode, + String negotiatedProtocol, String[] headers, long receivedBytesCount) { + synchronized (mNativeStreamLock) { + if (mNativeStream == 0) { + return null; + } + } + + ArrayList<String> urlChain = new ArrayList<>(); + urlChain.add(mInitialUrl); + + UrlResponseInfo responseInfo = new UrlResponseInfo(urlChain, httpStatusCode, "", + headersListFromStrings(headers), false, negotiatedProtocol, null); + + responseInfo.setReceivedBytesCount(receivedBytesCount); + return responseInfo; + } + + @GuardedBy("mNativeStreamLock") + private void destroyNativeStreamLocked(boolean sendOnCanceled) { + Log.i(CronetUrlRequestContext.LOG_TAG, "destroyNativeStreamLocked " + this.toString()); + if (mNativeStream == 0) { + return; + } + nativeDestroy(mNativeStream, sendOnCanceled); + mNativeStream = 0; + mRequestContext.onRequestDestroyed(); + if (mOnDestroyedCallbackForTesting != null) { + mOnDestroyedCallbackForTesting.run(); + } + } + + /** + * Fails the stream with an exception. Only called on the Executor. + */ + private void failWithExceptionOnExecutor(CronetException e) { + // Do not call into listener if request is complete. + synchronized (mNativeStreamLock) { + if (isDoneLocked()) { + return; + } + mReadState = mWriteState = State.ERROR; + destroyNativeStreamLocked(false); + } + try { + mCallback.onFailed(this, mResponseInfo, e); + } catch (Exception failException) { + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception notifying of failed request", + failException); + } + } + + /** + * If callback method throws an exception, stream gets canceled + * and exception is reported via onFailed callback. + * Only called on the Executor. + */ + private void onCallbackException(Exception e) { + CronetException streamError = + new CronetException("CalledByNative method has thrown an exception", e); + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in CalledByNative method", e); + failWithExceptionOnExecutor(streamError); + } + + /** + * Fails the stream with an exception. Can be called on any thread. + */ + private void failWithException(final CronetException exception) { + postTaskToExecutor(new Runnable() { + public void run() { + failWithExceptionOnExecutor(exception); + } + }); + } + + // Native methods are implemented in cronet_bidirectional_stream_adapter.cc. + private native long nativeCreateBidirectionalStream(long urlRequestContextAdapter); + + @NativeClassQualifiedName("CronetBidirectionalStreamAdapter") + private native int nativeStart(long nativePtr, String url, int priority, String method, + String[] headers, boolean endOfStream); + + @NativeClassQualifiedName("CronetBidirectionalStreamAdapter") + private native boolean nativeReadData( + long nativePtr, ByteBuffer byteBuffer, int position, int limit); + + @NativeClassQualifiedName("CronetBidirectionalStreamAdapter") + private native boolean nativeWriteData( + long nativePtr, ByteBuffer byteBuffer, int position, int limit, boolean endOfStream); + + @NativeClassQualifiedName("CronetBidirectionalStreamAdapter") + private native void nativeDestroy(long nativePtr, boolean sendOnCanceled); +} diff --git a/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java b/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java index a618f1b..75c61a5 100644 --- a/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java +++ b/components/cronet/android/java/src/org/chromium/net/CronetUrlRequest.java @@ -194,7 +194,7 @@ final class CronetUrlRequest implements UrlRequest { try { mUrlRequestAdapter = nativeCreateRequestAdapter( mRequestContext.getUrlRequestContextAdapter(), mInitialUrl, mPriority); - mRequestContext.onRequestStarted(this); + mRequestContext.onRequestStarted(); if (mInitialMethod != null) { if (!nativeSetHttpMethod(mUrlRequestAdapter, mInitialMethod)) { throw new IllegalArgumentException("Invalid http method " + mInitialMethod); @@ -439,7 +439,7 @@ final class CronetUrlRequest implements UrlRequest { } nativeDestroy(mUrlRequestAdapter, sendOnCanceled); mRequestContext.reportFinished(this); - mRequestContext.onRequestDestroyed(this); + mRequestContext.onRequestDestroyed(); mUrlRequestAdapter = 0; if (mOnDestroyedCallbackForTesting != null) { mOnDestroyedCallbackForTesting.run(); diff --git a/components/cronet/android/java/src/org/chromium/net/CronetUrlRequestContext.java b/components/cronet/android/java/src/org/chromium/net/CronetUrlRequestContext.java index cac2e32..cf6dd24 100644 --- a/components/cronet/android/java/src/org/chromium/net/CronetUrlRequestContext.java +++ b/components/cronet/android/java/src/org/chromium/net/CronetUrlRequestContext.java @@ -143,7 +143,11 @@ class CronetUrlRequestContext extends CronetEngine { BidirectionalStream createBidirectionalStream(String url, BidirectionalStream.Callback callback, Executor executor, String httpMethod, List<Map.Entry<String, String>> requestHeaders, @BidirectionalStream.Builder.StreamPriority int priority) { - throw new UnsupportedOperationException(); + synchronized (mLock) { + checkHaveAdapter(); + return new CronetBidirectionalStream( + this, url, priority, callback, executor, httpMethod, requestHeaders); + } } @Override @@ -343,7 +347,7 @@ class CronetUrlRequestContext extends CronetEngine { * Mark request as started to prevent shutdown when there are active * requests. */ - void onRequestStarted(UrlRequest urlRequest) { + void onRequestStarted() { mActiveRequestCount.incrementAndGet(); } @@ -351,7 +355,7 @@ class CronetUrlRequestContext extends CronetEngine { * Mark request as finished to allow shutdown when there are no active * requests. */ - void onRequestDestroyed(UrlRequest urlRequest) { + void onRequestDestroyed() { mActiveRequestCount.decrementAndGet(); } diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java b/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java index 7e10f83..3c85b78 100644 --- a/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java +++ b/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java @@ -4,15 +4,22 @@ package org.chromium.net; +import android.os.ConditionVariable; import android.test.suitebuilder.annotation.SmallTest; import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestBase.OnlyRunNativeCronet; +import org.chromium.net.TestBidirectionalStreamCallback.FailureType; +import org.chromium.net.TestBidirectionalStreamCallback.ResponseStep; import java.nio.ByteBuffer; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Test functionality of BidirectionalStream interface. @@ -23,63 +30,56 @@ public class BidirectionalStreamTest extends CronetTestBase { @Override protected void setUp() throws Exception { super.setUp(); - mTestFramework = startCronetTestFramework(); - assertTrue(NativeTestServer.startNativeTestServer(getContext())); - // Add url interceptors after native application context is initialized. - MockUrlRequestJobFactory.setUp(); + // Load library first to create MockCertVerifier. + System.loadLibrary("cronet_tests"); + CronetEngine.Builder builder = new CronetEngine.Builder(getContext()); + builder.setMockCertVerifierForTesting(QuicTestServer.createMockCertVerifier()); + + mTestFramework = startCronetTestFrameworkWithUrlAndCronetEngineBuilder(null, builder); + assertTrue(Http2TestServer.startHttp2TestServer( + getContext(), QuicTestServer.getServerCert(), QuicTestServer.getServerCertKey())); } @Override protected void tearDown() throws Exception { - NativeTestServer.shutdownNativeTestServer(); - mTestFramework.mCronetEngine.shutdown(); + assertTrue(Http2TestServer.shutdownHttp2TestServer()); + if (mTestFramework.mCronetEngine != null) { + mTestFramework.mCronetEngine.shutdown(); + } super.tearDown(); } - private class TestBidirectionalStreamCallback extends BidirectionalStream.Callback { - // Executor Service for Cronet callbacks. - private final ExecutorService mExecutorService = - Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); - private Thread mExecutorThread; + private static void checkResponseInfo(UrlResponseInfo responseInfo, String expectedUrl, + int expectedHttpStatusCode, String expectedHttpStatusText) { + assertEquals(expectedUrl, responseInfo.getUrl()); + assertEquals( + expectedUrl, responseInfo.getUrlChain().get(responseInfo.getUrlChain().size() - 1)); + assertEquals(expectedHttpStatusCode, responseInfo.getHttpStatusCode()); + assertEquals(expectedHttpStatusText, responseInfo.getHttpStatusText()); + assertFalse(responseInfo.wasCached()); + assertTrue(responseInfo.toString().length() > 0); + } - private class ExecutorThreadFactory implements ThreadFactory { - public Thread newThread(Runnable r) { - mExecutorThread = new Thread(r); - return mExecutorThread; - } + private static String createLongString(String base, int repetition) { + StringBuilder builder = new StringBuilder(base.length() * repetition); + for (int i = 0; i < repetition; ++i) { + builder.append(i); + builder.append(base); } + return builder.toString(); + } - @Override - public void onRequestHeadersSent(BidirectionalStream stream) {} - - @Override - public void onResponseHeadersReceived(BidirectionalStream stream, UrlResponseInfo info) {} - - @Override - public void onReadCompleted( - BidirectionalStream stream, UrlResponseInfo info, ByteBuffer buffer) {} - - @Override - public void onWriteCompleted( - BidirectionalStream stream, UrlResponseInfo info, ByteBuffer buffer) {} - - @Override - public void onResponseTrailersReceived(BidirectionalStream stream, UrlResponseInfo info, - UrlResponseInfo.HeaderBlock trailers) {} - - @Override - public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info) {} - - @Override - public void onFailed( - BidirectionalStream stream, UrlResponseInfo info, CronetException error) {} - - @Override - public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) {} - - Executor getExecutor() { - return mExecutorService; + private static UrlResponseInfo createUrlResponseInfo( + String[] urls, String message, int statusCode, int receivedBytes, String... headers) { + ArrayList<Map.Entry<String, String>> headersList = new ArrayList<>(); + for (int i = 0; i < headers.length; i += 2) { + headersList.add(new AbstractMap.SimpleImmutableEntry<String, String>( + headers[i], headers[i + 1])); } + UrlResponseInfo urlResponseInfo = new UrlResponseInfo( + Arrays.asList(urls), statusCode, message, headersList, false, "h2", null); + urlResponseInfo.setReceivedBytesCount(receivedBytes); + return urlResponseInfo; } @SmallTest @@ -94,28 +94,918 @@ public class BidirectionalStreamTest extends CronetTestBase { assertEquals("URL is required.", e.getMessage()); } try { - new BidirectionalStream.Builder(NativeTestServer.getRedirectURL(), null, + new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), null, callback.getExecutor(), mTestFramework.mCronetEngine); fail("Callback not null-checked"); } catch (NullPointerException e) { assertEquals("Callback is required.", e.getMessage()); } try { - new BidirectionalStream.Builder(NativeTestServer.getRedirectURL(), callback, null, - mTestFramework.mCronetEngine); + new BidirectionalStream.Builder( + Http2TestServer.getServerUrl(), callback, null, mTestFramework.mCronetEngine); fail("Executor not null-checked"); } catch (NullPointerException e) { assertEquals("Executor is required.", e.getMessage()); } try { new BidirectionalStream.Builder( - NativeTestServer.getRedirectURL(), callback, callback.getExecutor(), null); + Http2TestServer.getServerUrl(), callback, callback.getExecutor(), null); fail("CronetEngine not null-checked"); } catch (NullPointerException e) { assertEquals("CronetEngine is required.", e.getMessage()); } // Verify successful creation doesn't throw. - new BidirectionalStream.Builder(NativeTestServer.getRedirectURL(), callback, - callback.getExecutor(), mTestFramework.mCronetEngine); + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + try { + builder.addHeader(null, "value"); + fail("Header name is not null-checked"); + } catch (NullPointerException e) { + assertEquals("Invalid header name.", e.getMessage()); + } + try { + builder.addHeader("name", null); + fail("Header value is not null-checked"); + } catch (NullPointerException e) { + assertEquals("Invalid header value.", e.getMessage()); + } + try { + builder.setHttpMethod(null); + fail("Method name is not null-checked"); + } catch (NullPointerException e) { + assertEquals("Method is required.", e.getMessage()); + } + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testFailPlainHttp() throws Exception { + String url = "http://example.com"; + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + // Create stream. + BidirectionalStream stream = new BidirectionalStream + .Builder(url, callback, callback.getExecutor(), + mTestFramework.mCronetEngine) + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals("Exception in BidirectionalStream: net::ERR_DISALLOWED_URL_SCHEME", + callback.mError.getMessage()); + assertEquals(-301, callback.mError.netError()); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimpleGet() throws Exception { + String url = Http2TestServer.getEchoMethodUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + // Create stream. + BidirectionalStream stream = new BidirectionalStream + .Builder(url, callback, callback.getExecutor(), + mTestFramework.mCronetEngine) + .setHttpMethod("GET") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + // Default method is 'GET'. + assertEquals("GET", callback.mResponseAsString); + UrlResponseInfo urlResponseInfo = + createUrlResponseInfo(new String[] {url}, "", 200, 27, ":status", "200"); + assertResponseEquals(urlResponseInfo, callback.mResponseInfo); + checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodUrl(), 200, ""); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimpleHead() throws Exception { + String url = Http2TestServer.getEchoMethodUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + // Create stream. + BidirectionalStream stream = new BidirectionalStream + .Builder(url, callback, callback.getExecutor(), + mTestFramework.mCronetEngine) + .setHttpMethod("HEAD") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("HEAD", callback.mResponseAsString); + UrlResponseInfo urlResponseInfo = + createUrlResponseInfo(new String[] {url}, "", 200, 28, ":status", "200"); + assertResponseEquals(urlResponseInfo, callback.mResponseInfo); + checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodUrl(), 200, ""); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimplePost() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.addWriteData("Test String".getBytes()); + callback.addWriteData("1234567890".getBytes()); + callback.addWriteData("woot!".getBytes()); + // Create stream. + BidirectionalStream stream = new BidirectionalStream + .Builder(url, callback, callback.getExecutor(), + mTestFramework.mCronetEngine) + .addHeader("foo", "bar") + .addHeader("empty", "") + .addHeader("Content-Type", "zebra") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("Test String1234567890woot!", callback.mResponseAsString); + assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0)); + assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty").get(0)); + assertEquals( + "zebra", callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0)); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimplePut() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.addWriteData("Put This Data!".getBytes()); + String methodName = "PUT"; + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + builder.setHttpMethod(methodName); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("Put This Data!", callback.mResponseAsString); + assertEquals(methodName, callback.mResponseInfo.getAllHeaders().get("echo-method").get(0)); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBadMethod() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + try { + builder.setHttpMethod("bad:method!"); + builder.build().start(); + fail("IllegalArgumentException not thrown."); + } catch (IllegalArgumentException e) { + assertEquals("Invalid http method bad:method!", e.getMessage()); + } + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBadHeaderName() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + try { + builder.addHeader("goodheader1", "headervalue"); + builder.addHeader("header:name", "headervalue"); + builder.addHeader("goodheader2", "headervalue"); + builder.build().start(); + fail("IllegalArgumentException not thrown."); + } catch (IllegalArgumentException e) { + assertEquals("Invalid header header:name=headervalue", e.getMessage()); + } + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBadHeaderValue() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getServerUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + try { + builder.addHeader("headername", "bad header\r\nvalue"); + builder.build().start(); + fail("IllegalArgumentException not thrown."); + } catch (IllegalArgumentException e) { + assertEquals("Invalid header headername=bad header\r\nvalue", e.getMessage()); + } + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testAddHeader() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + String headerName = "header-name"; + String headerValue = "header-value"; + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoHeaderUrl(headerName), + callback, callback.getExecutor(), mTestFramework.mCronetEngine); + builder.addHeader(headerName, headerValue); + builder.setHttpMethod("GET"); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(headerValue, callback.mResponseAsString); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testMultiRequestHeaders() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + String headerName = "header-name"; + String headerValue1 = "header-value1"; + String headerValue2 = "header-value2"; + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoAllHeadersUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + builder.addHeader(headerName, headerValue1); + builder.addHeader(headerName, headerValue2); + builder.setHttpMethod("GET"); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + String headers = callback.mResponseAsString; + Pattern pattern = Pattern.compile(headerName + ":\\s(.*)\\r\\n"); + Matcher matcher = pattern.matcher(headers); + List<String> actualValues = new ArrayList<String>(); + while (matcher.find()) { + actualValues.add(matcher.group(1)); + } + assertEquals(1, actualValues.size()); + assertEquals("header-value2", actualValues.get(0)); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testEchoTrailers() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + String headerName = "header-name"; + String headerValue = "header-value"; + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoTrailersUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + builder.addHeader(headerName, headerValue); + builder.setHttpMethod("GET"); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertNotNull(callback.mTrailers); + // Verify that header value is properly echoed in trailers. + assertEquals(headerValue, callback.mTrailers.getAsMap().get("echo-" + headerName).get(0)); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testCustomUserAgent() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + String userAgentName = "User-Agent"; + String userAgentValue = "User-Agent-Value"; + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoHeaderUrl(userAgentName), + callback, callback.getExecutor(), mTestFramework.mCronetEngine); + builder.setHttpMethod("GET"); + builder.addHeader(userAgentName, userAgentValue); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(userAgentValue, callback.mResponseAsString); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testEchoStream() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + String[] testData = {"Test String", createLongString("1234567890", 50000), "woot!"}; + StringBuilder stringData = new StringBuilder(); + for (String writeData : testData) { + callback.addWriteData(writeData.getBytes()); + stringData.append(writeData); + } + // Create stream. + BidirectionalStream stream = new BidirectionalStream + .Builder(url, callback, callback.getExecutor(), + mTestFramework.mCronetEngine) + .addHeader("foo", "Value with Spaces") + .addHeader("Content-Type", "zebra") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(stringData.toString(), callback.mResponseAsString); + assertEquals( + "Value with Spaces", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0)); + assertEquals( + "zebra", callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0)); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testEchoStreamEmptyWrite() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.addWriteData(new byte[0]); + // Create stream. + BidirectionalStream stream = new BidirectionalStream + .Builder(url, callback, callback.getExecutor(), + mTestFramework.mCronetEngine) + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("", callback.mResponseAsString); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testDoubleWrite() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() { + @Override + public void onRequestHeadersSent(BidirectionalStream stream) { + startNextWrite(stream); + try { + // Second write from callback invoked on single-threaded executor throws + // an exception because first write is still pending until its completion + // is handled on executor. + ByteBuffer writeBuffer = ByteBuffer.allocateDirect(5); + writeBuffer.put("abc".getBytes()); + writeBuffer.flip(); + stream.write(writeBuffer, false); + fail("Exception is not thrown."); + } catch (Exception e) { + assertEquals("Unexpected write attempt.", e.getMessage()); + } + } + }; + callback.addWriteData("1".getBytes()); + callback.addWriteData("2".getBytes()); + // Create stream. + BidirectionalStream stream = new BidirectionalStream + .Builder(url, callback, callback.getExecutor(), + mTestFramework.mCronetEngine) + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("12", callback.mResponseAsString); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testDoubleRead() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() { + @Override + public void onResponseHeadersReceived( + BidirectionalStream stream, UrlResponseInfo info) { + startNextRead(stream); + try { + // Second read from callback invoked on single-threaded executor throws + // an exception because previous read is still pending until its completion + // is handled on executor. + stream.read(ByteBuffer.allocateDirect(5)); + fail("Exception is not thrown."); + } catch (Exception e) { + assertEquals("Unexpected read attempt.", e.getMessage()); + } + } + }; + callback.addWriteData("1".getBytes()); + callback.addWriteData("2".getBytes()); + // Create stream. + BidirectionalStream stream = new BidirectionalStream + .Builder(url, callback, callback.getExecutor(), + mTestFramework.mCronetEngine) + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("12", callback.mResponseAsString); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testReadAndWrite() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() { + @Override + public void onResponseHeadersReceived( + BidirectionalStream stream, UrlResponseInfo info) { + // Start the write, that will not complete until callback completion. + startNextWrite(stream); + // Start the read. It is allowed with write in flight. + super.onResponseHeadersReceived(stream, info); + } + }; + callback.setAutoAdvance(false); + callback.addWriteData("1".getBytes()); + callback.addWriteData("2".getBytes()); + // Create stream. + BidirectionalStream stream = new BidirectionalStream + .Builder(url, callback, callback.getExecutor(), + mTestFramework.mCronetEngine) + .build(); + stream.start(); + callback.waitForNextWriteStep(); + callback.waitForNextReadStep(); + callback.startNextRead(stream); + callback.setAutoAdvance(true); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("12", callback.mResponseAsString); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testEchoStreamWriteFirst() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.setAutoAdvance(false); + String[] testData = {"a", "bb", "ccc", "Test String", "1234567890", "woot!"}; + StringBuilder stringData = new StringBuilder(); + for (String writeData : testData) { + callback.addWriteData(writeData.getBytes()); + stringData.append(writeData); + } + // Create stream. + BidirectionalStream stream = new BidirectionalStream + .Builder(url, callback, callback.getExecutor(), + mTestFramework.mCronetEngine) + .build(); + stream.start(); + // Write first. + callback.waitForNextWriteStep(); + for (String expected : testData) { + // Write next chunk of test data. + callback.startNextWrite(stream); + callback.waitForNextWriteStep(); + } + + // Wait for read step, but don't read yet. + callback.waitForNextReadStep(); + assertEquals("", callback.mResponseAsString); + // Read back. + callback.startNextRead(stream); + callback.waitForNextReadStep(); + // Verify that some part of proper response is read. + assertTrue(callback.mResponseAsString.startsWith(testData[0])); + assertTrue(stringData.toString().startsWith(callback.mResponseAsString)); + // Read the rest of the response. + callback.setAutoAdvance(true); + callback.startNextRead(stream); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(stringData.toString(), callback.mResponseAsString); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testEchoStreamStepByStep() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.setAutoAdvance(false); + String[] testData = {"a", "bb", "ccc", "Test String", "1234567890", "woot!"}; + StringBuilder stringData = new StringBuilder(); + for (String writeData : testData) { + callback.addWriteData(writeData.getBytes()); + stringData.append(writeData); + } + // Create stream. + BidirectionalStream stream = new BidirectionalStream + .Builder(url, callback, callback.getExecutor(), + mTestFramework.mCronetEngine) + .build(); + stream.start(); + callback.waitForNextWriteStep(); + callback.waitForNextReadStep(); + + for (String expected : testData) { + // Write next chunk of test data. + callback.startNextWrite(stream); + callback.waitForNextWriteStep(); + + // Read next chunk of test data. + ByteBuffer readBuffer = ByteBuffer.allocateDirect(100); + callback.startNextRead(stream, readBuffer); + callback.waitForNextReadStep(); + assertEquals(expected.length(), readBuffer.position()); + assertFalse(stream.isDone()); + } + + callback.setAutoAdvance(true); + callback.startNextRead(stream); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(stringData.toString(), callback.mResponseAsString); + } + + /** + * Checks that the buffer is updated correctly, when starting at an offset. + */ + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimpleGetBufferUpdates() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.setAutoAdvance(false); + // Since the method is "GET", the expected response body is also "GET". + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + BidirectionalStream stream = builder.setHttpMethod("GET").build(); + stream.start(); + callback.waitForNextReadStep(); + + assertEquals(null, callback.mError); + assertFalse(callback.isDone()); + assertEquals(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_STARTED, + callback.mResponseStep); + + ByteBuffer readBuffer = ByteBuffer.allocateDirect(5); + readBuffer.put("FOR".getBytes()); + assertEquals(3, readBuffer.position()); + + // Read first two characters of the response ("GE"). It's theoretically + // possible to need one read per character, though in practice, + // shouldn't happen. + while (callback.mResponseAsString.length() < 2) { + assertFalse(callback.isDone()); + callback.startNextRead(stream, readBuffer); + callback.waitForNextReadStep(); + } + + // Make sure the two characters were read. + assertEquals("GE", callback.mResponseAsString); + + // Check the contents of the entire buffer. The first 3 characters + // should not have been changed, and the last two should be the first + // two characters from the response. + assertEquals("FORGE", bufferContentsToString(readBuffer, 0, 5)); + // The limit and position should be 5. + assertEquals(5, readBuffer.limit()); + assertEquals(5, readBuffer.position()); + + assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep); + + // Start reading from position 3. Since the only remaining character + // from the response is a "T", when the read completes, the buffer + // should contain "FORTE", with a position() of 4 and a limit() of 5. + readBuffer.position(3); + callback.startNextRead(stream, readBuffer); + callback.waitForNextReadStep(); + + // Make sure all three characters of the response have now been read. + assertEquals("GET", callback.mResponseAsString); + + // Check the entire contents of the buffer. Only the third character + // should have been modified. + assertEquals("FORTE", bufferContentsToString(readBuffer, 0, 5)); + + // Make sure position and limit were updated correctly. + assertEquals(4, readBuffer.position()); + assertEquals(5, readBuffer.limit()); + + assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep); + + // One more read attempt. The request should complete. + readBuffer.position(1); + readBuffer.limit(5); + callback.startNextRead(stream, readBuffer); + callback.waitForNextReadStep(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("GET", callback.mResponseAsString); + checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodUrl(), 200, ""); + + // Check that buffer contents were not modified. + assertEquals("FORTE", bufferContentsToString(readBuffer, 0, 5)); + + // Position should not have been modified, since nothing was read. + assertEquals(1, readBuffer.position()); + // Limit should be unchanged as always. + assertEquals(5, readBuffer.limit()); + + assertEquals(ResponseStep.ON_SUCCEEDED, callback.mResponseStep); + + // Make sure there are no other pending messages, which would trigger + // asserts in TestBidirectionalCallback. + testSimpleGet(); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBadBuffers() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.setAutoAdvance(false); + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + BidirectionalStream stream = builder.setHttpMethod("GET").build(); + stream.start(); + callback.waitForNextReadStep(); + + assertEquals(null, callback.mError); + assertFalse(callback.isDone()); + assertEquals(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_STARTED, + callback.mResponseStep); + + // Try to read using a full buffer. + try { + ByteBuffer readBuffer = ByteBuffer.allocateDirect(4); + readBuffer.put("full".getBytes()); + stream.read(readBuffer); + fail("Exception not thrown"); + } catch (IllegalArgumentException e) { + assertEquals("ByteBuffer is already full.", e.getMessage()); + } + + // Try to read using a non-direct buffer. + try { + ByteBuffer readBuffer = ByteBuffer.allocate(5); + stream.read(readBuffer); + fail("Exception not thrown"); + } catch (Exception e) { + assertEquals("byteBuffer must be a direct ByteBuffer.", e.getMessage()); + } + + // Finish the stream with a direct ByteBuffer. + callback.setAutoAdvance(true); + ByteBuffer readBuffer = ByteBuffer.allocateDirect(5); + stream.read(readBuffer); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("GET", callback.mResponseAsString); + } + + private void throwOrCancel(FailureType failureType, ResponseStep failureStep, + boolean expectResponseInfo, boolean expectError) { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.setFailure(failureType, failureStep); + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + BidirectionalStream stream = builder.setHttpMethod("GET").build(); + stream.start(); + callback.blockForDone(); + // assertEquals(callback.mResponseStep, failureStep); + assertTrue(stream.isDone()); + assertEquals(expectResponseInfo, callback.mResponseInfo != null); + assertEquals(expectError, callback.mError != null); + assertEquals(expectError, callback.mOnErrorCalled); + assertEquals(failureType == FailureType.CANCEL_SYNC + || failureType == FailureType.CANCEL_ASYNC + || failureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, + callback.mOnCanceledCalled); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testFailures() throws Exception { + throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_REQUEST_HEADERS_SENT, false, false); + throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_REQUEST_HEADERS_SENT, false, false); + throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_REQUEST_HEADERS_SENT, + false, false); + throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_REQUEST_HEADERS_SENT, false, true); + + throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_RESPONSE_STARTED, true, false); + throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_RESPONSE_STARTED, true, false); + throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_RESPONSE_STARTED, + true, false); + throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_RESPONSE_STARTED, true, true); + + throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_READ_COMPLETED, true, false); + throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_READ_COMPLETED, true, false); + throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_READ_COMPLETED, true, + false); + throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED, true, true); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testThrowOnSucceeded() { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_SUCCEEDED); + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + BidirectionalStream stream = builder.setHttpMethod("GET").build(); + stream.start(); + callback.blockForDone(); + assertEquals(callback.mResponseStep, ResponseStep.ON_SUCCEEDED); + assertTrue(stream.isDone()); + assertNotNull(callback.mResponseInfo); + // Check that error thrown from 'onSucceeded' callback is not reported. + assertNull(callback.mError); + assertFalse(callback.mOnErrorCalled); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testExecutorShutdownBeforeStreamIsDone() { + // Test that stream is destroyed even if executor is shut down and rejects posting tasks. + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.setAutoAdvance(false); + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + CronetBidirectionalStream stream = + (CronetBidirectionalStream) builder.setHttpMethod("GET").build(); + stream.start(); + callback.waitForNextReadStep(); + assertFalse(callback.isDone()); + assertFalse(stream.isDone()); + + final ConditionVariable streamDestroyed = new ConditionVariable(false); + stream.setOnDestroyedCallbackForTesting(new Runnable() { + @Override + public void run() { + streamDestroyed.open(); + } + }); + + // Shut down the executor, so posting the task will throw an exception. + callback.shutdownExecutor(); + ByteBuffer readBuffer = ByteBuffer.allocateDirect(5); + stream.read(readBuffer); + // Callback will never be called again because executor is shut down, + // but stream will be destroyed from network thread. + streamDestroyed.block(); + + assertFalse(callback.isDone()); + assertTrue(stream.isDone()); + } + + /** + * Callback that shuts down the engine when the stream has succeeded + * or failed. + */ + private class ShutdownTestBidirectionalStreamCallback extends TestBidirectionalStreamCallback { + @Override + public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info) { + mTestFramework.mCronetEngine.shutdown(); + // Clear mCronetEngine so it doesn't get shut down second time in tearDown(). + mTestFramework.mCronetEngine = null; + super.onSucceeded(stream, info); + } + + @Override + public void onFailed( + BidirectionalStream stream, UrlResponseInfo info, CronetException error) { + mTestFramework.mCronetEngine.shutdown(); + // Clear mCronetEngine so it doesn't get shut down second time in tearDown(). + mTestFramework.mCronetEngine = null; + super.onFailed(stream, info, error); + } + + @Override + public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) { + mTestFramework.mCronetEngine.shutdown(); + // Clear mCronetEngine so it doesn't get shut down second time in tearDown(). + mTestFramework.mCronetEngine = null; + super.onCanceled(stream, info); + } + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testCronetEngineShutdown() throws Exception { + // Test that CronetEngine cannot be shut down if there are any active streams. + TestBidirectionalStreamCallback callback = new ShutdownTestBidirectionalStreamCallback(); + // Block callback when response starts to verify that shutdown fails + // if there are active streams. + callback.setAutoAdvance(false); + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + CronetBidirectionalStream stream = + (CronetBidirectionalStream) builder.setHttpMethod("GET").build(); + stream.start(); + try { + mTestFramework.mCronetEngine.shutdown(); + fail("Should throw an exception"); + } catch (Exception e) { + assertEquals("Cannot shutdown with active requests.", e.getMessage()); + } + + callback.waitForNextReadStep(); + assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep); + try { + mTestFramework.mCronetEngine.shutdown(); + fail("Should throw an exception"); + } catch (Exception e) { + assertEquals("Cannot shutdown with active requests.", e.getMessage()); + } + callback.startNextRead(stream); + + callback.waitForNextReadStep(); + assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep); + try { + mTestFramework.mCronetEngine.shutdown(); + fail("Should throw an exception"); + } catch (Exception e) { + assertEquals("Cannot shutdown with active requests.", e.getMessage()); + } + + // May not have read all the data, in theory. Just enable auto-advance + // and finish the request. + callback.setAutoAdvance(true); + callback.startNextRead(stream); + callback.blockForDone(); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testCronetEngineShutdownAfterStreamFailure() throws Exception { + // Test that CronetEngine can be shut down after stream reports a failure. + TestBidirectionalStreamCallback callback = new ShutdownTestBidirectionalStreamCallback(); + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + CronetBidirectionalStream stream = + (CronetBidirectionalStream) builder.setHttpMethod("GET").build(); + stream.start(); + callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED); + callback.blockForDone(); + assertTrue(callback.mOnErrorCalled); + assertNull(mTestFramework.mCronetEngine); + } + + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testCronetEngineShutdownAfterStreamCancel() throws Exception { + // Test that CronetEngine can be shut down after stream is canceled. + TestBidirectionalStreamCallback callback = new ShutdownTestBidirectionalStreamCallback(); + BidirectionalStream.Builder builder = + new BidirectionalStream.Builder(Http2TestServer.getEchoMethodUrl(), callback, + callback.getExecutor(), mTestFramework.mCronetEngine); + CronetBidirectionalStream stream = + (CronetBidirectionalStream) builder.setHttpMethod("GET").build(); + + // Block callback when response starts to verify that shutdown fails + // if there are active requests. + callback.setAutoAdvance(false); + stream.start(); + try { + mTestFramework.mCronetEngine.shutdown(); + fail("Should throw an exception"); + } catch (Exception e) { + assertEquals("Cannot shutdown with active requests.", e.getMessage()); + } + callback.waitForNextReadStep(); + assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep); + stream.cancel(); + callback.blockForDone(); + assertTrue(callback.mOnCanceledCalled); + assertNull(mTestFramework.mCronetEngine); + } + + // Returns the contents of byteBuffer, from its position() to its limit(), + // as a String. Does not modify byteBuffer's position(). + private static String bufferContentsToString(ByteBuffer byteBuffer, int start, int end) { + // Use a duplicate to avoid modifying byteBuffer. + ByteBuffer duplicate = byteBuffer.duplicate(); + duplicate.position(start); + duplicate.limit(end); + byte[] contents = new byte[duplicate.remaining()]; + duplicate.get(contents); + return new String(contents); } } diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestBase.java b/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestBase.java index 30c4856..28aafd3 100644 --- a/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestBase.java +++ b/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestBase.java @@ -175,6 +175,22 @@ public class CronetTestBase extends AndroidTestCase { NativeTestServer.registerHostResolverProc(urlRequestContextAdapter, isLegacyAPI); } + void assertResponseEquals(UrlResponseInfo expected, UrlResponseInfo actual) { + assertEquals(expected.getAllHeaders(), actual.getAllHeaders()); + assertEquals(expected.getAllHeadersAsList(), actual.getAllHeadersAsList()); + assertEquals(expected.getHttpStatusCode(), actual.getHttpStatusCode()); + assertEquals(expected.getHttpStatusText(), actual.getHttpStatusText()); + assertEquals(expected.getUrlChain(), actual.getUrlChain()); + assertEquals(expected.getUrl(), actual.getUrl()); + // Transferred bytes and proxy server are not supported in pure java + if (!(mCronetTestFramework.mCronetEngine instanceof JavaCronetEngine)) { + assertEquals(expected.getReceivedBytesCount(), actual.getReceivedBytesCount()); + assertEquals(expected.getProxyServer(), actual.getProxyServer()); + // This is a place where behavior intentionally differs between native and java + assertEquals(expected.getNegotiatedProtocol(), actual.getNegotiatedProtocol()); + } + } + @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CompareDefaultWithCronet { diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java b/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java index 2ac2bc3..23a5854 100644 --- a/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java +++ b/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java @@ -149,22 +149,6 @@ public class CronetUrlRequestTest extends CronetTestBase { return unknown; } - void assertResponseEquals(UrlResponseInfo expected, UrlResponseInfo actual) { - assertEquals(expected.getAllHeaders(), actual.getAllHeaders()); - assertEquals(expected.getAllHeadersAsList(), actual.getAllHeadersAsList()); - assertEquals(expected.getHttpStatusCode(), actual.getHttpStatusCode()); - assertEquals(expected.getHttpStatusText(), actual.getHttpStatusText()); - assertEquals(expected.getUrlChain(), actual.getUrlChain()); - assertEquals(expected.getUrl(), actual.getUrl()); - // Transferred bytes and proxy server are not supported in pure java - if (!(mTestFramework.mCronetEngine instanceof JavaCronetEngine)) { - assertEquals(expected.getReceivedBytesCount(), actual.getReceivedBytesCount()); - assertEquals(expected.getProxyServer(), actual.getProxyServer()); - // This is a place where behavior intentionally differs between native and java - assertEquals(expected.getNegotiatedProtocol(), actual.getNegotiatedProtocol()); - } - } - /** * Tests a redirect by running it step-by-step. Also tests that delaying a * request works as expected. To make sure there are no unexpected pending diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java b/components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java index fa9c5d0..5a74621 100644 --- a/components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java +++ b/components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java @@ -24,7 +24,6 @@ import java.util.HashMap; */ public class QuicTest extends CronetTestBase { private static final String TAG = "cr.QuicTest"; - private static final String[] CERTS_USED = {"quic_test.example.com.crt"}; private CronetTestFramework mTestFramework; private CronetEngine.Builder mBuilder; @@ -51,7 +50,7 @@ public class QuicTest extends CronetTestBase { JSONObject experimentalOptions = new JSONObject().put("QUIC", quicParams); mBuilder.setExperimentalOptions(experimentalOptions.toString()); - mBuilder.setMockCertVerifierForTesting(MockCertVerifier.createMockCertVerifier(CERTS_USED)); + mBuilder.setMockCertVerifierForTesting(QuicTestServer.createMockCertVerifier()); mBuilder.setStoragePath(CronetTestFramework.getTestStorage(getContext())); mBuilder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 1000 * 1024); } @@ -141,7 +140,7 @@ public class QuicTest extends CronetTestBase { JSONObject quicParams = new JSONObject().put("host_whitelist", "test.example.com"); JSONObject experimentalOptions = new JSONObject().put("QUIC", quicParams); builder.setExperimentalOptions(experimentalOptions.toString()); - builder.setMockCertVerifierForTesting(MockCertVerifier.createMockCertVerifier(CERTS_USED)); + builder.setMockCertVerifierForTesting(QuicTestServer.createMockCertVerifier()); mTestFramework = startCronetTestFrameworkWithUrlAndCronetEngineBuilder(null, builder); registerHostResolver(mTestFramework); TestUrlRequestCallback callback2 = new TestUrlRequestCallback(); diff --git a/components/cronet/android/test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java b/components/cronet/android/test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java new file mode 100644 index 0000000..c7a630c --- /dev/null +++ b/components/cronet/android/test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java @@ -0,0 +1,334 @@ +// Copyright 2015 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. + +package org.chromium.net; + +import android.os.ConditionVariable; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * Callback that tracks information from different callbacks and and has a + * method to block thread until the stream completes on another thread. + * Allows to cancel, block stream or throw an exception from an arbitrary step. + */ +public class TestBidirectionalStreamCallback extends BidirectionalStream.Callback { + public UrlResponseInfo mResponseInfo; + public CronetException mError; + + public ResponseStep mResponseStep = ResponseStep.NOTHING; + + public boolean mOnErrorCalled = false; + public boolean mOnCanceledCalled = false; + + public int mHttpResponseDataLength = 0; + public String mResponseAsString = ""; + + public UrlResponseInfo.HeaderBlock mTrailers; + + private static final int READ_BUFFER_SIZE = 32 * 1024; + + // When false, the consumer is responsible for all calls into the stream + // that advance it. + private boolean mAutoAdvance = true; + + // Conditionally fail on certain steps. + private FailureType mFailureType = FailureType.NONE; + private ResponseStep mFailureStep = ResponseStep.NOTHING; + + // Signals when the stream is done either successfully or not. + private final ConditionVariable mDone = new ConditionVariable(); + + // Signaled on each step when mAutoAdvance is false. + private final ConditionVariable mReadStepBlock = new ConditionVariable(); + private final ConditionVariable mWriteStepBlock = new ConditionVariable(); + + // Executor Service for Cronet callbacks. + private final ExecutorService mExecutorService = + Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); + private Thread mExecutorThread; + + // position() of ByteBuffer prior to read() call. + private int mBufferPositionBeforeRead; + + // Data to write. + private ArrayList<ByteBuffer> mWriteBuffers = new ArrayList<ByteBuffer>(); + + private class ExecutorThreadFactory implements ThreadFactory { + public Thread newThread(Runnable r) { + mExecutorThread = new Thread(r); + return mExecutorThread; + } + } + + public enum ResponseStep { + NOTHING, + ON_REQUEST_HEADERS_SENT, + ON_RESPONSE_STARTED, + ON_READ_COMPLETED, + ON_WRITE_COMPLETED, + ON_TRAILERS, + ON_CANCELED, + ON_FAILED, + ON_SUCCEEDED + } + + public enum FailureType { + NONE, + CANCEL_SYNC, + CANCEL_ASYNC, + // Same as above, but continues to advance the stream after posting + // the cancellation task. + CANCEL_ASYNC_WITHOUT_PAUSE, + THROW_SYNC + } + + public void setAutoAdvance(boolean autoAdvance) { + mAutoAdvance = autoAdvance; + } + + public void setFailure(FailureType failureType, ResponseStep failureStep) { + mFailureStep = failureStep; + mFailureType = failureType; + } + + public void blockForDone() { + mDone.block(); + } + + public void waitForNextReadStep() { + mReadStepBlock.block(); + mReadStepBlock.close(); + } + + public void waitForNextWriteStep() { + mWriteStepBlock.block(); + mWriteStepBlock.close(); + } + + public Executor getExecutor() { + return mExecutorService; + } + + public void shutdownExecutor() { + mExecutorService.shutdown(); + } + + public void addWriteData(byte[] data) { + ByteBuffer writeBuffer = ByteBuffer.allocateDirect(data.length); + writeBuffer.put(data); + writeBuffer.flip(); + mWriteBuffers.add(writeBuffer); + } + + @Override + public void onRequestHeadersSent(BidirectionalStream stream) { + assertEquals(mExecutorThread, Thread.currentThread()); + assertFalse(stream.isDone()); + assertEquals(ResponseStep.NOTHING, mResponseStep); + assertNull(mError); + + mResponseStep = ResponseStep.ON_REQUEST_HEADERS_SENT; + if (maybeThrowCancelOrPause(stream, mWriteStepBlock)) { + return; + } + startNextWrite(stream); + } + + @Override + public void onResponseHeadersReceived(BidirectionalStream stream, UrlResponseInfo info) { + assertEquals(mExecutorThread, Thread.currentThread()); + assertFalse(stream.isDone()); + assertTrue(mResponseStep == ResponseStep.NOTHING + || mResponseStep == ResponseStep.ON_REQUEST_HEADERS_SENT + || mResponseStep == ResponseStep.ON_WRITE_COMPLETED); + assertNull(mError); + + mResponseStep = ResponseStep.ON_RESPONSE_STARTED; + mResponseInfo = info; + if (maybeThrowCancelOrPause(stream, mReadStepBlock)) { + return; + } + startNextRead(stream); + } + + @Override + public void onReadCompleted( + BidirectionalStream stream, UrlResponseInfo info, ByteBuffer byteBuffer) { + assertEquals(mExecutorThread, Thread.currentThread()); + assertFalse(stream.isDone()); + assertTrue(mResponseStep == ResponseStep.ON_RESPONSE_STARTED + || mResponseStep == ResponseStep.ON_READ_COMPLETED + || mResponseStep == ResponseStep.ON_WRITE_COMPLETED + || mResponseStep == ResponseStep.ON_TRAILERS); + assertNull(mError); + + mResponseStep = ResponseStep.ON_READ_COMPLETED; + + final int bytesRead = byteBuffer.position() - mBufferPositionBeforeRead; + mHttpResponseDataLength += bytesRead; + final byte[] lastDataReceivedAsBytes = new byte[bytesRead]; + // Rewind byteBuffer.position() to pre-read() position. + byteBuffer.position(mBufferPositionBeforeRead); + // This restores byteBuffer.position() to its value on entrance to + // this function. + byteBuffer.get(lastDataReceivedAsBytes); + + mResponseAsString += new String(lastDataReceivedAsBytes); + + if (maybeThrowCancelOrPause(stream, mReadStepBlock)) { + return; + } + startNextRead(stream); + } + + @Override + public void onWriteCompleted( + BidirectionalStream stream, UrlResponseInfo info, ByteBuffer buffer) { + assertEquals(mExecutorThread, Thread.currentThread()); + assertFalse(stream.isDone()); + assertNull(mError); + mResponseStep = ResponseStep.ON_WRITE_COMPLETED; + if (!mWriteBuffers.isEmpty()) { + assertEquals(buffer, mWriteBuffers.get(0)); + mWriteBuffers.remove(0); + } + if (maybeThrowCancelOrPause(stream, mWriteStepBlock)) { + return; + } + startNextWrite(stream); + } + + @Override + public void onResponseTrailersReceived(BidirectionalStream stream, UrlResponseInfo info, + UrlResponseInfo.HeaderBlock trailers) { + assertEquals(mExecutorThread, Thread.currentThread()); + assertFalse(stream.isDone()); + assertNull(mError); + mResponseStep = ResponseStep.ON_TRAILERS; + mTrailers = trailers; + if (maybeThrowCancelOrPause(stream, mReadStepBlock)) { + return; + } + } + + @Override + public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info) { + assertEquals(mExecutorThread, Thread.currentThread()); + assertTrue(stream.isDone()); + assertTrue(mResponseStep == ResponseStep.ON_RESPONSE_STARTED + || mResponseStep == ResponseStep.ON_READ_COMPLETED + || mResponseStep == ResponseStep.ON_WRITE_COMPLETED + || mResponseStep == ResponseStep.ON_TRAILERS); + assertFalse(mOnErrorCalled); + assertFalse(mOnCanceledCalled); + assertNull(mError); + + mResponseStep = ResponseStep.ON_SUCCEEDED; + mResponseInfo = info; + openDone(); + maybeThrowCancelOrPause(stream, mReadStepBlock); + } + + @Override + public void onFailed(BidirectionalStream stream, UrlResponseInfo info, CronetException error) { + assertEquals(mExecutorThread, Thread.currentThread()); + assertTrue(stream.isDone()); + // Shouldn't happen after success. + assertTrue(mResponseStep != ResponseStep.ON_SUCCEEDED); + // Should happen at most once for a single stream. + assertFalse(mOnErrorCalled); + assertFalse(mOnCanceledCalled); + assertNull(mError); + mResponseStep = ResponseStep.ON_FAILED; + + mOnErrorCalled = true; + mError = error; + openDone(); + maybeThrowCancelOrPause(stream, mReadStepBlock); + } + + @Override + public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) { + assertEquals(mExecutorThread, Thread.currentThread()); + assertTrue(stream.isDone()); + // Should happen at most once for a single stream. + assertFalse(mOnCanceledCalled); + assertFalse(mOnErrorCalled); + assertNull(mError); + mResponseStep = ResponseStep.ON_CANCELED; + + mOnCanceledCalled = true; + openDone(); + maybeThrowCancelOrPause(stream, mReadStepBlock); + } + + public void startNextRead(BidirectionalStream stream) { + startNextRead(stream, ByteBuffer.allocateDirect(READ_BUFFER_SIZE)); + } + + public void startNextRead(BidirectionalStream stream, ByteBuffer buffer) { + mBufferPositionBeforeRead = buffer.position(); + stream.read(buffer); + } + + public void startNextWrite(BidirectionalStream stream) { + if (!mWriteBuffers.isEmpty()) { + boolean isLastBuffer = mWriteBuffers.size() == 1; + stream.write(mWriteBuffers.get(0), isLastBuffer); + } + } + + public boolean isDone() { + // It's not mentioned by the Android docs, but block(0) seems to block + // indefinitely, so have to block for one millisecond to get state + // without blocking. + return mDone.block(1); + } + + protected void openDone() { + mDone.open(); + } + + /** + * Returns {@code false} if the callback should continue to advance the + * stream. + */ + private boolean maybeThrowCancelOrPause( + final BidirectionalStream stream, ConditionVariable stepBlock) { + if (mResponseStep != mFailureStep || mFailureType == FailureType.NONE) { + if (!mAutoAdvance) { + stepBlock.open(); + return true; + } + return false; + } + + if (mFailureType == FailureType.THROW_SYNC) { + throw new IllegalStateException("Callback Exception."); + } + Runnable task = new Runnable() { + public void run() { + stream.cancel(); + } + }; + if (mFailureType == FailureType.CANCEL_ASYNC + || mFailureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE) { + getExecutor().execute(task); + } else { + task.run(); + } + return mFailureType != FailureType.CANCEL_ASYNC_WITHOUT_PAUSE; + } +} diff --git a/components/cronet/android/test/src/org/chromium/net/Http2TestHandler.java b/components/cronet/android/test/src/org/chromium/net/Http2TestHandler.java new file mode 100644 index 0000000..2b70a78 --- /dev/null +++ b/components/cronet/android/test/src/org/chromium/net/Http2TestHandler.java @@ -0,0 +1,287 @@ +// Copyright 2015 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. + +package org.chromium.net; + +import org.chromium.base.Log; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import static io.netty.buffer.Unpooled.copiedBuffer; +import static io.netty.buffer.Unpooled.unreleasableBuffer; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.logging.LogLevel.INFO; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http2.AbstractHttp2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.Http2ConnectionDecoder; +import io.netty.handler.codec.http2.Http2ConnectionEncoder; +import io.netty.handler.codec.http2.Http2ConnectionHandler; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2Flags; +import io.netty.handler.codec.http2.Http2FrameListener; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.util.CharsetUtil; + +/** + * HTTP/2 test handler for Cronet BidirectionalStream tests. + */ +public final class Http2TestHandler extends Http2ConnectionHandler implements Http2FrameListener { + // Some Url Paths that have special meaning. + public static final String ECHO_ALL_HEADERS_PATH = "/echoallheaders"; + public static final String ECHO_HEADER_PATH = "/echoheader"; + public static final String ECHO_METHOD_PATH = "/echomethod"; + public static final String ECHO_STREAM_PATH = "/echostream"; + public static final String ECHO_TRAILERS_PATH = "/echotrailers"; + + private static final String TAG = "cr_Http2TestHandler"; + private static final Http2FrameLogger sLogger = + new Http2FrameLogger(INFO, Http2TestHandler.class); + private static final ByteBuf RESPONSE_BYTES = + unreleasableBuffer(copiedBuffer("HTTP/2 Test Server", CharsetUtil.UTF_8)); + + private HashMap<Integer, RequestResponder> mResponderMap = new HashMap<>(); + + /** + * Builder for HTTP/2 test handler. + */ + public static final class Builder + extends AbstractHttp2ConnectionHandlerBuilder<Http2TestHandler, Builder> { + public Builder() { + frameLogger(sLogger); + } + + @Override + public Http2TestHandler build() { + return super.build(); + } + + @Override + protected Http2TestHandler build(Http2ConnectionDecoder decoder, + Http2ConnectionEncoder encoder, Http2Settings initialSettings) { + Http2TestHandler handler = new Http2TestHandler(decoder, encoder, initialSettings); + frameListener(handler); + return handler; + } + } + + private class RequestResponder { + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) { + encoder().writeHeaders(ctx, streamId, createResponseHeadersFromRequestHeaders(headers), + 0, false, ctx.newPromise()); + ctx.flush(); + } + + int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, + boolean endOfStream) { + int processed = data.readableBytes() + padding; + encoder().writeData(ctx, streamId, data.retain(), 0, true, ctx.newPromise()); + ctx.flush(); + return processed; + } + + void sendResponseString(ChannelHandlerContext ctx, int streamId, String responseString) { + ByteBuf content = ctx.alloc().buffer(); + ByteBufUtil.writeAscii(content, responseString); + encoder().writeHeaders( + ctx, streamId, createDefaultResponseHeaders(), 0, false, ctx.newPromise()); + encoder().writeData(ctx, streamId, content, 0, true, ctx.newPromise()); + ctx.flush(); + } + } + + private class EchoStreamResponder extends RequestResponder { + @Override + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) { + // Send a frame for the response headers. + encoder().writeHeaders(ctx, streamId, createResponseHeadersFromRequestHeaders(headers), + 0, endOfStream, ctx.newPromise()); + ctx.flush(); + } + + @Override + int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, + boolean endOfStream) { + int processed = data.readableBytes() + padding; + encoder().writeData(ctx, streamId, data.retain(), 0, endOfStream, ctx.newPromise()); + ctx.flush(); + return processed; + } + } + + private class EchoHeaderResponder extends RequestResponder { + @Override + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) { + String[] splitPath = headers.path().toString().split("\\?"); + if (splitPath.length <= 1) { + sendResponseString(ctx, streamId, "Header name not found."); + return; + } + + String headerName = splitPath[1].toLowerCase(Locale.US); + if (headers.get(headerName) == null) { + sendResponseString(ctx, streamId, "Header not found:" + headerName); + return; + } + + sendResponseString(ctx, streamId, headers.get(headerName).toString()); + } + } + + private class EchoAllHeadersResponder extends RequestResponder { + @Override + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) { + StringBuilder response = new StringBuilder(); + for (Map.Entry<CharSequence, CharSequence> header : headers) { + response.append(header.getKey() + ": " + header.getValue() + "\r\n"); + } + sendResponseString(ctx, streamId, response.toString()); + } + } + + private class EchoMethodResponder extends RequestResponder { + @Override + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) { + sendResponseString(ctx, streamId, headers.method().toString()); + } + } + + private class EchoTrailersResponder extends RequestResponder { + @Override + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) { + encoder().writeHeaders( + ctx, streamId, createDefaultResponseHeaders(), 0, false, ctx.newPromise()); + encoder().writeData( + ctx, streamId, RESPONSE_BYTES.duplicate(), 0, false, ctx.newPromise()); + Http2Headers responseTrailers = createResponseHeadersFromRequestHeaders(headers).add( + "trailer", "value1", "Value2"); + encoder().writeHeaders(ctx, streamId, responseTrailers, 0, true, ctx.newPromise()); + ctx.flush(); + } + } + + private static Http2Headers createDefaultResponseHeaders() { + return new DefaultHttp2Headers().status(OK.codeAsText()); + } + + private static Http2Headers createResponseHeadersFromRequestHeaders( + Http2Headers requestHeaders) { + // Create response headers by echoing request headers. + Http2Headers responseHeaders = new DefaultHttp2Headers().status(OK.codeAsText()); + for (Map.Entry<CharSequence, CharSequence> header : requestHeaders) { + if (!header.getKey().toString().startsWith(":")) { + responseHeaders.add("echo-" + header.getKey(), header.getValue()); + } + } + + responseHeaders.add("echo-method", requestHeaders.get(":method").toString()); + return responseHeaders; + } + + private Http2TestHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, + Http2Settings initialSettings) { + super(decoder, encoder, initialSettings); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + Log.e(TAG, "An exception was caught", cause); + ctx.close(); + throw new Exception("Exception Caught", cause); + } + + @Override + public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, + boolean endOfStream) throws Http2Exception { + RequestResponder responder = mResponderMap.get(streamId); + if (endOfStream) { + mResponderMap.remove(streamId); + } + return responder.onDataRead(ctx, streamId, data, padding, endOfStream); + } + + @Override + public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, + int padding, boolean endOfStream) throws Http2Exception { + String path = headers.path().toString(); + RequestResponder responder; + if (path.startsWith(ECHO_STREAM_PATH)) { + responder = new EchoStreamResponder(); + } else if (path.startsWith(ECHO_TRAILERS_PATH)) { + responder = new EchoTrailersResponder(); + } else if (path.startsWith(ECHO_ALL_HEADERS_PATH)) { + responder = new EchoAllHeadersResponder(); + } else if (path.startsWith(ECHO_HEADER_PATH)) { + responder = new EchoHeaderResponder(); + } else if (path.startsWith(ECHO_METHOD_PATH)) { + responder = new EchoMethodResponder(); + } else { + responder = new RequestResponder(); + } + + responder.onHeadersRead(ctx, streamId, endOfStream, headers); + + if (!endOfStream) { + mResponderMap.put(streamId, responder); + } + } + + @Override + public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, + int streamDependency, short weight, boolean exclusive, int padding, boolean endOfStream) + throws Http2Exception { + onHeadersRead(ctx, streamId, headers, padding, endOfStream); + } + + @Override + public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, + short weight, boolean exclusive) throws Http2Exception {} + + @Override + public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) + throws Http2Exception {} + + @Override + public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {} + + @Override + public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) + throws Http2Exception {} + + @Override + public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {} + + @Override + public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {} + + @Override + public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, + Http2Headers headers, int padding) throws Http2Exception {} + + @Override + public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, + ByteBuf debugData) throws Http2Exception {} + + @Override + public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) + throws Http2Exception {} + + @Override + public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, + Http2Flags flags, ByteBuf payload) throws Http2Exception {} +} diff --git a/components/cronet/android/test/src/org/chromium/net/Http2TestServer.java b/components/cronet/android/test/src/org/chromium/net/Http2TestServer.java new file mode 100644 index 0000000..1af837c --- /dev/null +++ b/components/cronet/android/test/src/org/chromium/net/Http2TestServer.java @@ -0,0 +1,179 @@ +// Copyright 2015 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. + +package org.chromium.net; + +import android.content.Context; +import android.os.ConditionVariable; + +import org.chromium.base.Log; +import org.chromium.net.test.util.CertTestUtil; + +import java.io.File; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; +import io.netty.handler.ssl.OpenSslServerContext; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; + +/** + * Wrapper class to start a HTTP/2 test server. + */ +public final class Http2TestServer { + private static final ConditionVariable sBlock = new ConditionVariable(); + private static Channel sServerChannel; + private static final String TAG = "Http2TestServer"; + + private static final String HOST = "127.0.0.1"; + // Server port. + private static final int PORT = 8443; + + public static boolean shutdownHttp2TestServer() throws Exception { + if (sServerChannel != null) { + sServerChannel.close(); + sServerChannel = null; + return true; + } + return false; + } + + public static String getServerHost() { + return HOST; + } + + public static int getServerPort() { + return PORT; + } + + public static String getServerUrl() { + return "https://" + HOST + ":" + PORT; + } + + public static String getEchoAllHeadersUrl() { + return getServerUrl() + Http2TestHandler.ECHO_ALL_HEADERS_PATH; + } + + public static String getEchoHeaderUrl(String headerName) { + return getServerUrl() + Http2TestHandler.ECHO_HEADER_PATH + "?" + headerName; + } + + public static String getEchoMethodUrl() { + return getServerUrl() + Http2TestHandler.ECHO_METHOD_PATH; + } + + /** + * @return url of the server resource which will echo every received stream data frame. + */ + public static String getEchoStreamUrl() { + return getServerUrl() + Http2TestHandler.ECHO_STREAM_PATH; + } + + /** + * @return url of the server resource which will echo request headers as response trailers. + */ + public static String getEchoTrailersUrl() { + return getServerUrl() + Http2TestHandler.ECHO_TRAILERS_PATH; + } + + public static boolean startHttp2TestServer( + Context context, String certFileName, String keyFileName) throws Exception { + new Thread( + new Http2TestServerRunnable(new File(CertTestUtil.CERTS_DIRECTORY + certFileName), + new File(CertTestUtil.CERTS_DIRECTORY + keyFileName))) + .start(); + sBlock.block(); + return true; + } + + private Http2TestServer() {} + + private static class Http2TestServerRunnable implements Runnable { + private final SslContext mSslCtx; + + Http2TestServerRunnable(File certFile, File keyFile) throws Exception { + ApplicationProtocolConfig applicationProtocolConfig = new ApplicationProtocolConfig( + Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE, + SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2); + + mSslCtx = new OpenSslServerContext(certFile, keyFile, null, null, + Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE, + applicationProtocolConfig, 0, 0); + } + + public void run() { + try { + // Configure the server. + EventLoopGroup group = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.option(ChannelOption.SO_BACKLOG, 1024); + b.group(group) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new Http2ServerInitializer(mSslCtx)); + + sServerChannel = b.bind(PORT).sync().channel(); + Log.i(TAG, "Netty HTTP/2 server started on " + getServerUrl()); + sBlock.open(); + sServerChannel.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + Log.i(TAG, "Stopped Http2TestServerRunnable!"); + } catch (Exception e) { + Log.e(TAG, e.toString()); + } + } + } + + /** + * Sets up the Netty pipeline for the test server. + */ + private static class Http2ServerInitializer extends ChannelInitializer<SocketChannel> { + private final SslContext mSslCtx; + + public Http2ServerInitializer(SslContext sslCtx) { + this.mSslCtx = sslCtx; + } + + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast(mSslCtx.newHandler(ch.alloc()), new Http2NegotiationHandler()); + } + } + + private static class Http2NegotiationHandler extends ApplicationProtocolNegotiationHandler { + protected Http2NegotiationHandler() { + super(ApplicationProtocolNames.HTTP_1_1); + } + + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) + throws Exception { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ctx.pipeline().addLast(new Http2TestHandler.Builder().build()); + return; + } + + throw new IllegalStateException("unknown protocol: " + protocol); + } + } +} diff --git a/components/cronet/android/test/src/org/chromium/net/QuicTestServer.java b/components/cronet/android/test/src/org/chromium/net/QuicTestServer.java index 44c414f..1344cc4 100644 --- a/components/cronet/android/test/src/org/chromium/net/QuicTestServer.java +++ b/components/cronet/android/test/src/org/chromium/net/QuicTestServer.java @@ -19,6 +19,10 @@ public final class QuicTestServer { private static final ConditionVariable sBlock = new ConditionVariable(); private static final String TAG = "cr.QuicTestServer"; + private static final String CERT_USED = "quic_test.example.com.crt"; + private static final String KEY_USED = "quic_test.example.com.key"; + private static final String[] CERTS_USED = {CERT_USED}; + public static void startQuicTestServer(Context context) { TestFilesInstaller.installIfNeeded(context); nativeStartQuicTestServer(TestFilesInstaller.getInstalledPath(context)); @@ -42,6 +46,18 @@ public final class QuicTestServer { return nativeGetServerPort(); } + public static final String getServerCert() { + return CERT_USED; + } + + public static final String getServerCertKey() { + return KEY_USED; + } + + public static long createMockCertVerifier() { + return MockCertVerifier.createMockCertVerifier(CERTS_USED); + } + @CalledByNative private static void onServerStarted() { Log.i(TAG, "Quic server started."); diff --git a/components/cronet/cronet_static.gypi b/components/cronet/cronet_static.gypi index 22d2a6f..1557f8a 100644 --- a/components/cronet/cronet_static.gypi +++ b/components/cronet/cronet_static.gypi @@ -7,6 +7,7 @@ 'dependencies': [ '../base/base.gyp:base', '../base/third_party/dynamic_annotations/dynamic_annotations.gyp:dynamic_annotations', + 'cronet_features', 'cronet_jni_headers', 'cronet_url_request_java', 'cronet_version', @@ -72,5 +73,15 @@ ], } ], + # If Bidirectional Stream support is enabled, add the following sources. + # Dependencies are target-specific and are not included here. + ['enable_bidirectional_stream==1', + { + 'sources': [ + 'android/cronet_bidirectional_stream_adapter.cc', + 'android/cronet_bidirectional_stream_adapter.h', + ], + } + ], ], } diff --git a/components/cronet/tools/cr_cronet.py b/components/cronet/tools/cr_cronet.py index 7d87b6c..6a14515 100755 --- a/components/cronet/tools/cr_cronet.py +++ b/components/cronet/tools/cr_cronet.py @@ -51,6 +51,7 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('command', choices=['gyp', + 'gn', 'sync', 'build', 'install', @@ -67,16 +68,23 @@ def main(): print options print extra_options_list gyp_defines = 'GYP_DEFINES="OS=android enable_websockets=0 '+ \ - 'disable_file_support=1 disable_ftp_support=1" ' + 'disable_file_support=1 disable_ftp_support=1 '+ \ + 'enable_bidirectional_stream=1"' + gn_args = 'target_os="android" enable_websockets=false '+ \ + 'disable_file_support=true disable_ftp_support=true '+ \ + 'enable_bidirectional_stream=false' out_dir = 'out/Debug' release_arg = '' extra_options = ' '.join(extra_options_list) if options.release: out_dir = 'out/Release' release_arg = ' --release' + gn_args += ' is_debug=false ' if (options.command=='gyp'): return run (gyp_defines + ' gclient runhooks') + if (options.command=='gn'): + return run ('gn gen ' + out_dir + ' --args=\'' + gn_args + '\'') if (options.command=='sync'): return run ('git pull --rebase && ' + gyp_defines + ' gclient sync') if (options.command=='build'): |
