diff --git a/src/components/cronet/BUILD.gn b/src/components/cronet/BUILD.gn new file mode 100644 index 0000000000..2db3d1fd4a --- /dev/null +++ b/src/components/cronet/BUILD.gn @@ -0,0 +1,271 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//build/buildflag_header.gni") +import("//build/toolchain/toolchain.gni") +import("//build/util/lastchange.gni") +import("//build/util/process_version.gni") +import("//build/util/version.gni") +import("//components/cronet/native/include/headers.gni") +import("//components/grpc_support/include/headers.gni") +import("//testing/test.gni") + +declare_args() { + # If set to true, this will remove histogram manager to reduce binary size. + disable_histogram_support = is_mac || is_win +} + +# Disable histogram support is not allowed on Android. +assert(!disable_histogram_support || !is_android) + +buildflag_header("cronet_buildflags") { + header = "cronet_buildflags.h" + flags = [ "DISABLE_HISTOGRAM_SUPPORT=$disable_histogram_support" ] +} + +process_version("cronet_version_header") { + template_file = "//components/cronet/version.h.in" + sources = [ "//chrome/VERSION" ] + output = "$target_gen_dir/version.h" + extra_args = [ + "-e", + "VERSION_FULL=\"%s.%s.%s.%s\" % (MAJOR,MINOR,BUILD,PATCH)", + ] +} + +# Cronet common implementation. +source_set("cronet_common") { + sources = [ + "cronet_global_state.h", + "cronet_prefs_manager.cc", + "cronet_prefs_manager.h", + "cronet_upload_data_stream.cc", + "cronet_upload_data_stream.h", + "cronet_url_request.cc", + "cronet_url_request.h", + "cronet_url_request_context.cc", + "cronet_url_request_context.h", + "host_cache_persistence_manager.cc", + "host_cache_persistence_manager.h", + "stale_host_resolver.cc", + "stale_host_resolver.h", + "url_request_context_config.cc", + "url_request_context_config.h", + ] + deps = [ + ":cronet_buildflags", + ":cronet_version_header", + "//base", + "//components/prefs:prefs", + "//net", + "//third_party/metrics_proto", + ] + + if (!disable_histogram_support) { + public_deps = [ "//components/metrics:library_support" ] + } +} + +source_set("metrics_util") { + sources = [ + "metrics_util.cc", + "metrics_util.h", + ] + deps = [ "//base" ] +} + +# Unit tests for Cronet common implementation. +source_set("cronet_common_unittests") { + testonly = true + + deps = [ + ":cronet_common", + "//components/prefs:test_support", + "//net:test_support", + ] + + sources = [ + "host_cache_persistence_manager_unittest.cc", + "stale_host_resolver_unittest.cc", + "url_request_context_config_unittest.cc", + ] +} + +# For platforms on which the native Cronet library is used, build the library, +# a cronet_tests binary that exercises it, and a unit-tests binary. +# Android and iOS have their own platform-specific rules to build Cronet. +if (is_android) { + group("cronet_package") { + testonly = true + deps = [ "//components/cronet/android:cronet_package_android" ] + } +} else if (is_ios) { + group("cronet_package") { + deps = [ "//components/cronet/ios:cronet_package_ios" ] + } +} else { + config("shared_library_public_config") { + if (is_mac && !is_component_build) { + # Executable targets that depend on the shared libraries below need to have + # the rpath setup in non-component build configurations. + ldflags = [ "-Wl,-rpath,@executable_path/" ] + } + } + + _cronet_shared_lib_name = "cronet.$chrome_version_full" + _cronet_shared_lib_file_name = + "$shlib_prefix$_cronet_shared_lib_name$shlib_extension" + + shared_library("cronet") { + output_name = _cronet_shared_lib_name + + deps = [ + "//base", + "//components/cronet:cronet_common", + "//components/cronet/native:cronet_native_impl", + "//net", + ] + + sources = [ "cronet_global_state_stubs.cc" ] + + if (is_mac && !is_component_build) { + ldflags = [ + "-install_name", + "@executable_path/$_cronet_shared_lib_file_name", + ] + public_configs = [ ":shared_library_public_config" ] + } + } + + test("cronet_tests") { + deps = [ + ":cronet_common", + "//base", + "//base/test:test_support", + "//components/cronet/native:cronet_native_impl", + "//components/cronet/native/test:cronet_native_tests", + "//net", + ] + + sources = [ + "cronet_global_state_stubs.cc", + "run_all_unittests.cc", + ] + + defines = [ "CRONET_TESTS_IMPLEMENTATION" ] + + if ((is_linux || is_chromeos) && !is_component_build) { + public_configs = [ "//build/config/gcc:rpath_for_built_shared_libraries" ] + } + + if (is_fuchsia) { + use_cfv2 = false + additional_manifest_fragments = + [ "//build/config/fuchsia/test/network_capabilities.test-cmx" ] + } + } + + test("cronet_unittests") { + deps = [ + ":cronet_common", + ":cronet_common_unittests", + "//base", + "//base/test:test_support", + "//components/cronet/native:cronet_native_unittests", + "//net", + ] + + sources = [ + "cronet_global_state_stubs.cc", + "run_all_unittests.cc", + ] + } + + _package_dir = "$root_out_dir/cronet" + + # Generate LICENSE file by recursively joining all dependent licenses. + action("generate_license") { + _license_path = "$_package_dir/LICENSE" + + script = "//tools/licenses.py" + inputs = [ lastchange_file ] + outputs = [ _license_path ] + args = [ + "license_file", + rebase_path(_license_path, root_build_dir), + "--gn-target", + "//components/cronet:cronet", + "--gn-out-dir", + ".", + ] + } + + # Copy boiler-plate files into the package. + copy("cronet_package_copy") { + sources = [ + "${root_out_dir}${shlib_subdir}/${_cronet_shared_lib_file_name}", + "//AUTHORS", + "//chrome/VERSION", + ] + deps = [ ":cronet" ] + outputs = [ "$_package_dir/{{source_file_part}}" ] + } + + # Copy headers. + copy("cronet_package_headers") { + sources = cronet_native_public_headers + grpc_public_headers + + outputs = [ "$_package_dir/include/{{source_file_part}}" ] + } + + group("cronet_package") { + deps = [ + ":cronet_package_copy", + ":cronet_package_headers", + ":generate_license", + ] + } + + executable("cronet_native_perf_test") { + testonly = true + sources = [ + "native/perftest/main.cc", + "native/perftest/perf_test.cc", + ] + deps = [ + "//base", + "//components/cronet", + "//components/cronet/native:cronet_native_headers", + "//components/cronet/native/test:cronet_native_tests", + "//components/cronet/native/test:cronet_native_testutil", + "//net:test_support", + ] + } + + executable("cronet_sample") { + testonly = true + sources = [ + "native/sample/main.cc", + "native/sample/sample_executor.cc", + "native/sample/sample_executor.h", + "native/sample/sample_url_request_callback.cc", + "native/sample/sample_url_request_callback.h", + ] + deps = [ + "//components/cronet", + "//components/cronet/native:cronet_native_headers", + ] + if ((is_linux || is_chromeos) && !is_component_build) { + public_configs = [ "//build/config/gcc:rpath_for_built_shared_libraries" ] + } + } + + test("cronet_sample_test") { + sources = [ "native/sample/test/sample_test.cc" ] + deps = [ + ":cronet_sample", + "//testing/gtest:gtest", + ] + } +} diff --git a/src/components/cronet/DEPS b/src/components/cronet/DEPS new file mode 100644 index 0000000000..9b584a17ac --- /dev/null +++ b/src/components/cronet/DEPS @@ -0,0 +1,8 @@ +include_rules = [ + "+components/grpc_support", + "+components/metrics", + "+components/prefs", + "+net", + "+third_party/metrics_proto", + "+third_party/zlib", +] diff --git a/src/components/cronet/DIR_METADATA b/src/components/cronet/DIR_METADATA new file mode 100644 index 0000000000..9e5f77e85f --- /dev/null +++ b/src/components/cronet/DIR_METADATA @@ -0,0 +1,5 @@ +monorail { + component: "Internals>Network>Library" +} + +team_email: "net-dev@chromium.org" diff --git a/src/components/cronet/OWNERS b/src/components/cronet/OWNERS new file mode 100644 index 0000000000..ace4d683a7 --- /dev/null +++ b/src/components/cronet/OWNERS @@ -0,0 +1,5 @@ +cleborgne@google.com +danstahr@google.com +sporeba@google.com +torne@chromium.org +file://net/OWNERS diff --git a/src/components/cronet/PRESUBMIT.py b/src/components/cronet/PRESUBMIT.py new file mode 100644 index 0000000000..62f1fc4722 --- /dev/null +++ b/src/components/cronet/PRESUBMIT.py @@ -0,0 +1,106 @@ +# 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. + +"""Top-level presubmit script for src/components/cronet. + +See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts +for more details about the presubmit API built into depot_tools. +""" + +import os + +USE_PYTHON3 = True + + +def _PyLintChecks(input_api, output_api): + pylint_checks = input_api.canned_checks.GetPylint(input_api, output_api, + extra_paths_list=_GetPathsToPrepend(input_api), pylintrc='pylintrc') + return input_api.RunTests(pylint_checks) + + +def _GetPathsToPrepend(input_api): + current_dir = input_api.PresubmitLocalPath() + chromium_src_dir = input_api.os_path.join(current_dir, '..', '..') + return [ + input_api.os_path.join(chromium_src_dir, 'components'), + input_api.os_path.join(chromium_src_dir, 'tools', 'perf'), + input_api.os_path.join(chromium_src_dir, 'build', 'android'), + input_api.os_path.join(chromium_src_dir, 'build', 'android', 'gyp'), + input_api.os_path.join(chromium_src_dir, + 'mojo', 'public', 'tools', 'bindings', 'pylib'), + input_api.os_path.join(chromium_src_dir, 'net', 'tools', 'net_docs'), + input_api.os_path.join(chromium_src_dir, 'tools'), + input_api.os_path.join(chromium_src_dir, 'third_party'), + input_api.os_path.join(chromium_src_dir, + 'third_party', 'catapult', 'telemetry'), + input_api.os_path.join(chromium_src_dir, + 'third_party', 'catapult', 'devil'), + input_api.os_path.join(chromium_src_dir, + 'third_party', 'catapult', 'common', 'py_utils'), + ] + + +def _PackageChecks(input_api, output_api): + """Verify API classes are in org.chromium.net package, and implementation + classes are not in org.chromium.net package.""" + api_file_pattern = input_api.re.compile( + r'^components/cronet/android/api/.*\.(java|template)$') + impl_file_pattern = input_api.re.compile( + r'^components/cronet/android/java/.*\.(java|template)$') + api_package_pattern = input_api.re.compile(r'^package (?!org.chromium.net;)') + impl_package_pattern = input_api.re.compile(r'^package org.chromium.net;') + + source_filter = lambda path: input_api.FilterSourceFile(path, + files_to_check=[r'^components/cronet/android/.*\.(java|template)$']) + + problems = [] + for f in input_api.AffectedSourceFiles(source_filter): + local_path = f.LocalPath() + for line_number, line in f.ChangedContents(): + if (api_file_pattern.search(local_path)): + if (api_package_pattern.search(line)): + problems.append( + '%s:%d\n %s' % (local_path, line_number, line.strip())) + elif (impl_file_pattern.search(local_path)): + if (impl_package_pattern.search(line)): + problems.append( + '%s:%d\n %s' % (local_path, line_number, line.strip())) + + if problems: + return [output_api.PresubmitError( + 'API classes must be in org.chromium.net package, and implementation\n' + 'classes must not be in org.chromium.net package.', + problems)] + else: + return [] + + +def _RunToolsUnittests(input_api, output_api): + return input_api.canned_checks.RunUnitTestsInDirectory( + input_api, output_api, + '.', + [ r'^tools_unittest\.py$'], + run_on_python3=True) + + +def _ChangeAffectsCronetTools(change): + """ Returns |true| if the change may affect Cronet tools. """ + + for path in change.LocalPaths(): + if path.startswith(os.path.join('components', 'cronet', 'tools')): + return True + return False + + +def CheckChangeOnUpload(input_api, output_api): + results = [] + results.extend(_PyLintChecks(input_api, output_api)) + results.extend(_PackageChecks(input_api, output_api)) + if _ChangeAffectsCronetTools(input_api.change): + results.extend(_RunToolsUnittests(input_api, output_api)) + return results + + +def CheckChangeOnCommit(input_api, output_api): + return _RunToolsUnittests(input_api, output_api) diff --git a/src/components/cronet/README.md b/src/components/cronet/README.md new file mode 100644 index 0000000000..dfd428389d --- /dev/null +++ b/src/components/cronet/README.md @@ -0,0 +1,176 @@ +# Quick Start Guide to Using Cronet +Cronet is the networking stack of Chromium put into a library for use on +mobile. This is the same networking stack that is used in the Chrome browser +by over a billion people. It offers an easy-to-use, high performance, +standards-compliant, and secure way to perform HTTP requests. Cronet has support +for both Android and iOS. On Android, Cronet offers its own Java asynchronous +API as well as support for the [java.net.HttpURLConnection] API. +This document gives a brief introduction to using these two Java APIs. + +For instructions on checking out and building Cronet see +[Cronet build instructions](build_instructions.md). + +Testing information is available on the [native +API](native/test_instructions.md) and [Android +API](android/test_instructions.md) pages. + +### Basics +First you will need to extend `UrlRequest.Callback` to handle +events during the lifetime of a request. For example: + + class MyCallback extends UrlRequest.Callback { + @Override + public void onRedirectReceived(UrlRequest request, + UrlResponseInfo responseInfo, String newLocationUrl) { + if (followRedirect) { + // Let's tell Cronet to follow the redirect! + request.followRedirect(); + } else { + // Not worth following the redirect? Abandon the request. + request.cancel(); + } + } + + @Override + public void onResponseStarted(UrlRequest request, + UrlResponseInfo responseInfo) { + // Now we have response headers! + int httpStatusCode = responseInfo.getHttpStatusCode(); + if (httpStatusCode == 200) { + // Success! Let's tell Cronet to read the response body. + request.read(myBuffer); + } else if (httpStatusCode == 503) { + // Do something. Note that 4XX and 5XX are not considered + // errors from Cronet's perspective since the response is + // successfully read. + } + mResponseHeaders = responseInfo.getAllHeaders(); + } + + @Override + public void onReadCompleted(UrlRequest request, + UrlResponseInfo responseInfo, ByteBuffer byteBuffer) { + // Response body is available. + doSomethingWithResponseData(byteBuffer); + // Let's tell Cronet to continue reading the response body or + // inform us that the response is complete! + request.read(mBuffer); + } + + @Override + public void onSucceeded(UrlRequest request, + UrlResponseInfo responseInfo) { + // Request has completed successfully! + } + + @Override + public void onFailed(UrlRequest request, + UrlResponseInfo responseInfo, CronetException error) { + // Request has failed. responseInfo might be null. + Log.e("MyCallback", "Request failed. " + error.getMessage()); + // Maybe handle error here. Typical errors include hostname + // not resolved, connection to server refused, etc. + } + } + +Make a request like this: + + CronetEngine.Builder engineBuilder = new CronetEngine.Builder(getContext()); + CronetEngine engine = engineBuilder.build(); + Executor executor = Executors.newSingleThreadExecutor(); + MyCallback callback = new MyCallback(); + UrlRequest.Builder requestBuilder = engine.newUrlRequestBuilder( + "https://www.example.com", callback, executor); + UrlRequest request = requestBuilder.build(); + request.start(); + +In the above example, `MyCallback` extends `UrlRequest.Callback`. The request +is started asynchronously. When the response is ready (fully or partially), and +in the event of failures or redirects, `callback`'s methods will be invoked on +`executor`'s thread to inform the client of the request state and/or response +information. + +### Downloading Data +When Cronet fetches response headers from the server or gets them from the +cache, `UrlRequest.Callback.onResponseStarted` will be invoked. To read the +response body, the client should call `UrlRequest.read` and supply a +[ByteBuffer] for Cronet to fill. Once a portion or all of +the response body is read, `UrlRequest.Callback.onReadCompleted` will be invoked. +The client may then read and consume the data within `byteBuffer`. +Once the client is ready to consume more data, the client should call +`UrlRequest.read` again. The process continues until +`UrlRequest.Callback.onSucceeded` or `UrlRequest.Callback.onFailed` is invoked, +which signals the completion of the request. + +### Uploading Data + MyUploadDataProvider myUploadDataProvider = new MyUploadDataProvider(); + requestBuilder.setHttpMethod("POST"); + requestBuilder.setUploadDataProvider(myUploadDataProvider, executor); + +In the above example, `MyUploadDataProvider` extends `UploadDataProvider`. +When Cronet is ready to send the request body, +`myUploadDataProvider.read(UploadDataSink uploadDataSink, +ByteBuffer byteBuffer)` will be invoked. The client will need to write the +request body into `byteBuffer`. Once the client is done writing into +`byteBuffer`, the client can let Cronet know by calling +`uploadDataSink.onReadSucceeded`. If the request body doesn't fit into +`byteBuffer`, the client can continue writing when `UploadDataProvider.read` is +invoked again. For more details, please see the API reference. + +### Configuring Cronet +Various configuration options are available via the `CronetEngine.Builder` +object. + +Enabling HTTP/2 and QUIC: + +- For Example: + + engineBuilder.enableHttp2(true).enableQuic(true); + +Controlling the cache: + +- Use a 100KiB in-memory cache: + + engineBuilder.enableHttpCache( + CronetEngine.Builder.HttpCache.IN_MEMORY, 100 * 1024); + +- or use a 1MiB disk cache: + + engineBuilder.setStoragePath(storagePathString); + engineBuilder.enableHttpCache(CronetEngine.Builder.HttpCache.DISK, + 1024 * 1024); + +### Debugging +To get more information about how Cronet is processing network +requests, you can start and stop **NetLog** logging by calling +`CronetEngine.startNetLogToFile` and `CronetEngine.stopNetLog`. +Bear in mind that logs may contain sensitive data. You may analyze the +generated log by navigating to [chrome://net-internals#import] using a +Chrome browser. + +# Using the java.net.HttpURLConnection API +Cronet offers an implementation of the [java.net.HttpURLConnection] API to make +it easier for apps which rely on this API to use Cronet. +To open individual connections using Cronet's implementation, do the following: + + HttpURLConnection connection = + (HttpURLConnection)engine.openConnection(url); + +To use Cronet's implementation instead of the system's default implementation +for all connections established using `URL.openConnection()` do the following: + + URL.setURLStreamHandlerFactory(engine.createURLStreamHandlerFactory()); + +Cronet's +HttpURLConnection implementation has some limitations as compared to the system +implementation, including not utilizing the default system HTTP cache (Please +see {@link org.chromium.net.CronetEngine#createURLStreamHandlerFactory} for +more information). +You can configure Cronet and control caching through the +`CronetEngine.Builder` instance, `engineBuilder` +(See [Configuring Cronet](#configuring-cronet) section), before you build the +`CronetEngine` and then call `CronetEngine.createURLStreamHandlerFactory()`. + +[ByteBuffer]: https://developer.android.com/reference/java/nio/ByteBuffer.html +[chrome://net-internals#import]: chrome://net-internals#import +[java.net.HttpURLConnection]: https://developer.android.com/reference/java/net/HttpURLConnection.html diff --git a/src/components/cronet/__init__.py b/src/components/cronet/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/cronet/android/BUILD.gn b/src/components/cronet/android/BUILD.gn new file mode 100644 index 0000000000..a60928d5d2 --- /dev/null +++ b/src/components/cronet/android/BUILD.gn @@ -0,0 +1,1582 @@ +# 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. + +import("//build/android/resource_sizes.gni") +import("//build/buildflag_header.gni") +import("//build/config/android/config.gni") +import("//build/config/android/rules.gni") +import("//build/config/zip.gni") +import("//build/util/lastchange.gni") +import("//build/util/process_version.gni") +import("//build/util/version.gni") +import("//components/cronet/native/include/headers.gni") +import("//components/grpc_support/include/headers.gni") +import("//testing/test.gni") +import("//third_party/netty4/netty4.gni") +import("//third_party/protobuf/proto_library.gni") +import("//tools/binary_size/sizes.gni") +import("//url/features.gni") + +_jni_registration_header = "$target_gen_dir/cronet_jni_registration.h" +_templates_dir = "$target_gen_dir/templates" + +declare_args() { + # In integrated mode, CronetEngine will use the shared network task runner by + # other Chromium-based clients like webview without self-initialization. + # Besides, the native library would be compiled and loaded together with the + # native library of host. This mode is only for Android. + integrated_mode = false +} + +buildflag_header("buildflags") { + header = "buildflags.h" + flags = [ "INTEGRATED_MODE=$integrated_mode" ] +} + +generate_jni("cronet_jni_headers") { + sources = [ + "java/src/org/chromium/net/impl/CronetBidirectionalStream.java", + "java/src/org/chromium/net/impl/CronetLibraryLoader.java", + "java/src/org/chromium/net/impl/CronetUploadDataStream.java", + "java/src/org/chromium/net/impl/CronetUrlRequest.java", + "java/src/org/chromium/net/impl/CronetUrlRequestContext.java", + ] +} + +generate_jni_registration("cronet_jni_registration") { + targets = [ ":cronet_impl_native_base_java" ] + header_output = _jni_registration_header + sources_exclusions = [ + "//base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java", + "//base/android/java/src/org/chromium/base/library_loader/LibraryPrefetcher.java", + "//base/android/java/src/org/chromium/base/process_launcher/ChildProcessService.java", + "//base/android/java/src/org/chromium/base/SysUtils.java", + ] +} + +android_library("cronet_jni_registration_java") { + srcjar_deps = [ ":cronet_jni_registration" ] +} + +java_cpp_enum("rtt_throughput_values_java") { + sources = [ "//net/nqe/network_quality.h" ] +} + +java_cpp_enum("net_request_priority_java") { + sources = [ "//net/base/request_priority.h" ] +} + +java_cpp_enum("net_idempotency_java") { + sources = [ "//net/base/idempotency.h" ] +} + +java_cpp_enum("network_quality_observation_source_java") { + sources = [ "//net/nqe/network_quality_observation_source.h" ] +} + +java_cpp_enum("url_request_error_java") { + sources = [ "url_request_error.h" ] +} + +java_cpp_enum("http_cache_type_java") { + sources = [ "//components/cronet/url_request_context_config.h" ] +} + +java_cpp_template("load_states_list") { + sources = [ "java/src/org/chromium/net/impl/LoadState.template" ] + inputs = [ "//net/base/load_states_list.h" ] +} + +java_cpp_template("integrated_mode_state") { + sources = [ "java/src/org/chromium/net/impl/IntegratedModeState.template" ] + if (integrated_mode) { + defines = [ "INTEGRATED_MODE" ] + } +} + +_generated_api_version_java = "$_templates_dir/org/chromium/net/ApiVersion.java" +_api_level = read_file("api_version.txt", "value") + +process_version("api_version") { + process_only = true + template_file = "api/src/org/chromium/net/ApiVersion.template" + sources = [ + "//chrome/VERSION", + lastchange_file, + ] + extra_args = [ + "-e", + "API_LEVEL=$_api_level", + ] + output = _generated_api_version_java +} + +_generated_impl_version_java = + "$_templates_dir/org/chromium/net/impl/ImplVersion.java" + +process_version("impl_version") { + process_only = true + template_file = "java/src/org/chromium/net/impl/ImplVersion.template" + sources = [ + "//chrome/VERSION", + lastchange_file, + ] + extra_args = [ + "-e", + "API_LEVEL=$_api_level", + ] + output = _generated_impl_version_java +} + +_cronet_version_header_include_dir = "$target_gen_dir/cronet_version_header" + +source_set("cronet_static") { + deps = [ + ":buildflags", + ":cronet_jni_headers", + ":cronet_jni_registration", + "//base", + "//base/third_party/dynamic_annotations", + "//components/cronet:cronet_common", + "//components/cronet:cronet_version_header", + "//components/cronet:metrics_util", + "//components/cronet/native:cronet_native_impl", + "//components/metrics", + "//components/prefs", + "//net", + "//third_party/zlib:zlib", + "//url", + "//url:buildflags", + ] + sources = [ + "//components/cronet/android/cronet_bidirectional_stream_adapter.cc", + "//components/cronet/android/cronet_bidirectional_stream_adapter.h", + "//components/cronet/android/cronet_library_loader.cc", + "//components/cronet/android/cronet_upload_data_stream_adapter.cc", + "//components/cronet/android/cronet_upload_data_stream_adapter.h", + "//components/cronet/android/cronet_url_request_adapter.cc", + "//components/cronet/android/cronet_url_request_adapter.h", + "//components/cronet/android/cronet_url_request_context_adapter.cc", + "//components/cronet/android/cronet_url_request_context_adapter.h", + "//components/cronet/android/io_buffer_with_byte_buffer.cc", + "//components/cronet/android/io_buffer_with_byte_buffer.h", + "//components/cronet/android/url_request_error.cc", + "//components/cronet/android/url_request_error.h", + _jni_registration_header, + ] + + if (integrated_mode) { + sources += [ + "//components/cronet/android/cronet_integrated_mode_state.cc", + "//components/cronet/android/cronet_integrated_mode_state.h", + ] + } else { + sources += [ "//components/cronet/android/cronet_library_loader.h" ] + } + + include_dirs = [ _cronet_version_header_include_dir ] + + cflags = [ + "-DLOGGING=1", + "-Wno-sign-promo", + ] + + libs = [ + "android", + "log", + ] + + if (!use_platform_icu_alternatives) { + deps += [ "//base:i18n" ] + } +} + +config("hide_all_but_jni_onload_and_cronet") { + ldflags = [ "-Wl,--version-script=" + + rebase_path("android_only_jni_onload_and_cronet_exports.lst", + root_out_dir) ] +} + +_cronet_shared_lib_name = "cronet.$chrome_version_full" + +shared_library("cronet") { + output_name = _cronet_shared_lib_name + sources = [ "cronet_jni.cc" ] + deps = [ + ":cronet_static", + "//base", + "//net:net", + ] + configs -= [ "//build/config/android:hide_all_but_jni_onload" ] + configs += [ ":hide_all_but_jni_onload_and_cronet" ] +} + +sizes_test("cronet_sizes") { + data_deps = [ ":cronet" ] + data = [ "${root_out_dir}/lib${_cronet_shared_lib_name}.so" ] + executable_args = [ + "--platform", + "android-cronet", + ] +} + +# cronet_api_java.jar defines Cronet API. +android_library("cronet_api_java") { + sources = [ + "api/src/org/chromium/net/BidirectionalStream.java", + "api/src/org/chromium/net/CallbackException.java", + "api/src/org/chromium/net/CronetEngine.java", + "api/src/org/chromium/net/CronetException.java", + "api/src/org/chromium/net/CronetProvider.java", + "api/src/org/chromium/net/ExperimentalBidirectionalStream.java", + "api/src/org/chromium/net/ExperimentalCronetEngine.java", + "api/src/org/chromium/net/ExperimentalUrlRequest.java", + "api/src/org/chromium/net/ICronetEngineBuilder.java", + "api/src/org/chromium/net/InlineExecutionProhibitedException.java", + "api/src/org/chromium/net/NetworkException.java", + "api/src/org/chromium/net/NetworkQualityRttListener.java", + "api/src/org/chromium/net/NetworkQualityThroughputListener.java", + "api/src/org/chromium/net/QuicException.java", + "api/src/org/chromium/net/RequestFinishedInfo.java", + "api/src/org/chromium/net/UploadDataProvider.java", + "api/src/org/chromium/net/UploadDataProviders.java", + "api/src/org/chromium/net/UploadDataSink.java", + "api/src/org/chromium/net/UrlRequest.java", + "api/src/org/chromium/net/UrlResponseInfo.java", + _generated_api_version_java, + ] + + deps = [ + ":api_version", + "//third_party/androidx:androidx_annotation_annotation_java", + ] +} + +cronet_impl_common_java_srcjar_deps = [ + ":http_cache_type_java", + ":integrated_mode_state", + ":load_states_list", + ":rtt_throughput_values_java", + "//net:effective_connection_type_java", +] + +cronet_impl_common_java_deps_to_package = + [ "//net/android:net_thread_stats_uid_java" ] + +# cronet_impl_common_base_java.jar - common Cronet code that is shared +# by all Cronet engine implementations. +android_library("cronet_impl_common_base_java") { + sources = [ + "java/src/org/chromium/net/impl/CallbackExceptionImpl.java", + "java/src/org/chromium/net/impl/CronetEngineBase.java", + "java/src/org/chromium/net/impl/CronetEngineBuilderImpl.java", + "java/src/org/chromium/net/impl/CronetExceptionImpl.java", + "java/src/org/chromium/net/impl/NetworkExceptionImpl.java", + "java/src/org/chromium/net/impl/Preconditions.java", + "java/src/org/chromium/net/impl/QuicExceptionImpl.java", + "java/src/org/chromium/net/impl/RequestFinishedInfoImpl.java", + "java/src/org/chromium/net/impl/UrlRequestBase.java", + "java/src/org/chromium/net/impl/UrlRequestBuilderImpl.java", + "java/src/org/chromium/net/impl/UrlResponseInfoImpl.java", + "java/src/org/chromium/net/impl/UserAgent.java", + "java/src/org/chromium/net/impl/VersionSafeCallbacks.java", + _generated_impl_version_java, + ] + + # Adding deps here won't include those deps in the cronet_impl_common_java.jar. + # Please add to cronet_impl_common_java_deps_to_package instead. + deps = [ + ":cronet_api_java", + ":impl_version", + "//third_party/androidx:androidx_annotation_annotation_java", + ] + deps += cronet_impl_common_java_deps_to_package + + srcjar_deps = cronet_impl_common_java_srcjar_deps +} + +# cronet_impl_java_util_java.jar - Classes shared between Java Cronet implementations. +android_library("cronet_impl_java_util_java") { + sources = [ + "java/src/org/chromium/net/impl/JavaUploadDataSinkBase.java", + "java/src/org/chromium/net/impl/JavaUrlRequestUtils.java", + ] + + deps = [ + ":cronet_api_java", + "//third_party/androidx:androidx_annotation_annotation_java", + ] +} + +# cronet_impl_platform_base_java.jar - Java platform based implementation of the Cronet engine. +android_library("cronet_impl_platform_base_java") { + sources = [ + "java/src/org/chromium/net/impl/InputStreamChannel.java", + "java/src/org/chromium/net/impl/JavaCronetEngine.java", + "java/src/org/chromium/net/impl/JavaCronetEngineBuilderImpl.java", + "java/src/org/chromium/net/impl/JavaCronetProvider.java", + "java/src/org/chromium/net/impl/JavaUrlRequest.java", + ] + + deps = [ + ":cronet_api_java", + ":cronet_impl_common_base_java", + ":cronet_impl_java_util_java", + "//net/android:net_thread_stats_uid_java", + "//third_party/android_deps:com_google_code_findbugs_jsr305_java", + "//third_party/androidx:androidx_annotation_annotation_java", + ] +} + +# cronet_impl_fake_base_java.jar - Fake implementation of Cronet. +android_library("cronet_impl_fake_base_java") { + sources = [ + "fake/java/org/chromium/net/test/FakeCronetController.java", + "fake/java/org/chromium/net/test/FakeCronetEngine.java", + "fake/java/org/chromium/net/test/FakeCronetProvider.java", + "fake/java/org/chromium/net/test/FakeUrlRequest.java", + "fake/java/org/chromium/net/test/FakeUrlResponse.java", + "fake/java/org/chromium/net/test/ResponseMatcher.java", + "fake/java/org/chromium/net/test/UrlResponseMatcher.java", + ] + + deps = [ + ":cronet_api_java", + ":cronet_impl_common_base_java", + ":cronet_impl_java_util_java", + "//third_party/androidx:androidx_annotation_annotation_java", + ] +} + +cronet_impl_native_java_srcjar_deps = [ + ":net_idempotency_java", + ":net_request_priority_java", + ":network_quality_observation_source_java", + ":url_request_error_java", +] + +cronet_impl_native_java_deps_to_package = [ + ":cronet_urlconnection_impl_java", + "//base:base_java", + "//base:jni_java", + "//build/android:build_config_java", + "//net/android:net_java", + "//url:url_java", +] + +android_library("cronet_urlconnection_impl_java") { + sources = [ + "java/src/org/chromium/net/urlconnection/CronetBufferedOutputStream.java", + "java/src/org/chromium/net/urlconnection/CronetChunkedOutputStream.java", + "java/src/org/chromium/net/urlconnection/CronetFixedModeOutputStream.java", + "java/src/org/chromium/net/urlconnection/CronetHttpURLConnection.java", + "java/src/org/chromium/net/urlconnection/CronetHttpURLStreamHandler.java", + "java/src/org/chromium/net/urlconnection/CronetInputStream.java", + "java/src/org/chromium/net/urlconnection/CronetOutputStream.java", + "java/src/org/chromium/net/urlconnection/CronetURLStreamHandlerFactory.java", + "java/src/org/chromium/net/urlconnection/MessageLoop.java", + ] + deps = [ + ":cronet_api_java", + "//third_party/androidx:androidx_annotation_annotation_java", + ] +} + +# cronet_impl_native_base_java.jar - native implementation of the Cronet engine. +android_library("cronet_impl_native_base_java") { + sources = [ + "java/src/org/chromium/net/impl/BidirectionalStreamBuilderImpl.java", + "java/src/org/chromium/net/impl/BidirectionalStreamNetworkException.java", + "java/src/org/chromium/net/impl/CronetBidirectionalStream.java", + "java/src/org/chromium/net/impl/CronetLibraryLoader.java", + "java/src/org/chromium/net/impl/CronetMetrics.java", + "java/src/org/chromium/net/impl/CronetUploadDataStream.java", + "java/src/org/chromium/net/impl/CronetUrlRequest.java", + "java/src/org/chromium/net/impl/CronetUrlRequestContext.java", + "java/src/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java", + "java/src/org/chromium/net/impl/NativeCronetEngineBuilderWithLibraryLoaderImpl.java", + "java/src/org/chromium/net/impl/NativeCronetProvider.java", + ] + + # Adding deps here won't include those deps in the cronet_impl_native_java.jar. + # Please add to cronet_impl_native_java_deps_to_package instead. + deps = [ + ":cronet_api_java", + ":cronet_impl_common_base_java", + "//base:jni_java", + "//build/android:build_config_java", + "//third_party/android_deps:com_google_code_findbugs_jsr305_java", + "//third_party/androidx:androidx_annotation_annotation_java", + ] + deps += cronet_impl_native_java_deps_to_package + annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] + + srcjar_deps = cronet_impl_native_java_srcjar_deps +} + +# Groups all Cronet implementations and the common code into a single Java dependency. +java_group("cronet_impl_all_java") { + deps = [ + ":cronet_impl_common_base_java", + ":cronet_impl_native_base_java", + ":cronet_impl_platform_base_java", + ] +} + +android_resources("cronet_sample_apk_resources") { + sources = [ + "sample/res/layout/activity_main.xml", + "sample/res/layout/dialog_url.xml", + "sample/res/values/dimens.xml", + "sample/res/values/strings.xml", + ] + android_manifest = "sample/AndroidManifest.xml" + deps = [ "//third_party/android_deps:android_support_v7_appcompat_java" ] +} + +android_library("cronet_sample_apk_java") { + sources = [ + "sample/src/org/chromium/cronet_sample_apk/CronetSampleActivity.java", + "sample/src/org/chromium/cronet_sample_apk/CronetSampleApplication.java", + ] + + resources_package = "org.chromium.cronet_sample_apk" + deps = [ + ":cronet_sample_apk_resources", + ":package_api_java", + ":package_impl_native_java", + "//third_party/android_deps:android_support_v7_appcompat_java", + "//third_party/androidx:androidx_annotation_annotation_java", + ] +} + +android_apk("cronet_sample_apk") { + apk_name = "CronetSample" + android_manifest = "sample/AndroidManifest.xml" + shared_libraries = [ ":cronet" ] + + deps = [ + ":cronet_combine_proguard_flags", + ":cronet_sample_apk_java", + ":cronet_sample_apk_resources", + ] + + # Cronet jars will include this, so don't duplicate. + generate_buildconfig_java = false + + enable_multidex = false + if (!is_java_debug) { + proguard_enabled = true + proguard_configs = [ + "$target_gen_dir/cronet_impl_native_proguard.cfg", + "cronet_impl_common_proguard.cfg", + "//base/android/proguard/chromium_apk.flags", + ] + } +} + +android_resource_sizes_test("resource_sizes_cronet_sample_apk") { + apk_name = "CronetSample" + data_deps = [ ":cronet_sample_apk" ] +} + +action("cronet_combine_proguard_flags") { + script = "//components/cronet/tools/generate_proguard_file.py" + sources = [ + "//base/android/proguard/chromium_code.flags", + + # Massage the proguard to avoid incompatibilities when building internally. + "//components/cronet/android/cronet_impl_native_proguard.cfg", + ] + outputs = [ "$target_gen_dir/cronet_impl_native_proguard.cfg" ] + args = [ "--output-file" ] + rebase_path(outputs, root_build_dir) + + rebase_path(sources, root_build_dir) +} + +_package_dir = "$root_out_dir/cronet" + +# These package_* targets represent how Cronet is used in production code. +# Using these targets is preferred to using the underlying targets like +# :cronet_api_java or :cronet_impl_all_java, as it better tests production +# usage. +android_java_prebuilt("package_api_java") { + jar_path = "$_package_dir/cronet_api.jar" + deps = [ ":repackage_api" ] +} + +android_java_prebuilt("package_impl_common_java") { + jar_path = "$_package_dir/cronet_impl_common_java.jar" + deps = [ + ":package_api_java", + ":repackage_common", + ] +} + +java_prebuilt("package_impl_native_java") { + # This target is a java_prebuilt instead of a android_java_prebuilt so we + # don't filter out GEN_JNI which is part of jar_excluded_patterns in + # android_java_prebuilt. + jar_path = "$_package_dir/cronet_impl_native_java.jar" + supports_android = true + requires_android = true + deps = [ + ":package_api_java", + ":package_impl_common_java", + ":repackage_native", + "//third_party/android_deps:android_support_v4_java", + "//third_party/android_deps:com_google_code_findbugs_jsr305_java", + ] + jar_excluded_patterns = [ "androidx/*/R*" ] +} + +android_java_prebuilt("package_impl_util_java") { + jar_path = "$_package_dir/cronet_impl_util_java.jar" + deps = [ + ":package_api_java", + ":repackage_util", + ] +} + +android_java_prebuilt("package_impl_platform_java") { + jar_path = "$_package_dir/cronet_impl_platform_java.jar" + deps = [ + ":package_api_java", + ":package_impl_common_java", + ":repackage_platform", + ] +} + +android_java_prebuilt("package_impl_fake_java") { + jar_path = "$_package_dir/cronet_impl_fake_java.jar" + deps = [ + ":package_api_java", + ":package_impl_common_java", + ":repackage_fake", + ] +} + +template("jar_src") { + action_with_pydeps(target_name) { + _rebased_src_search_dirs = + rebase_path(invoker.src_search_dirs, root_build_dir) + + script = "//components/cronet/tools/jar_src.py" + depfile = "$target_gen_dir/$target_name.d" + outputs = [ invoker.jar_path ] + args = [ + "--src-search-dirs=${_rebased_src_search_dirs}", + "--jar-path", + rebase_path(invoker.jar_path, root_build_dir), + "--depfile", + rebase_path(depfile, root_build_dir), + ] + + deps = [] + if (defined(invoker.deps)) { + deps += invoker.deps + } + + _excluded_patterns = [] + if (defined(invoker.excluded_patterns)) { + _excluded_patterns = invoker.excluded_patterns + } + _src_jars = [] + + # Add src-jar files that are listed in "src_jars". + if (defined(invoker.src_jars)) { + _rebased_src_jars = rebase_path(invoker.src_jars, root_build_dir) + _src_jars += _rebased_src_jars + } + + # Add src-jar files that are generated by dependencies in "srcjar_deps". + if (defined(invoker.srcjar_deps)) { + foreach(_srcjar_dep, invoker.srcjar_deps) { + _dep_gen_dir = get_label_info(_srcjar_dep, "target_gen_dir") + _dep_name = get_label_info(_srcjar_dep, "name") + _src_jars += rebase_path([ "$_dep_gen_dir/$_dep_name.srcjar" ]) + deps += [ _srcjar_dep ] + } + } + + # Create the list of all source files that are given in "src_files". + _src_files = [] + if (defined(invoker.src_files)) { + _src_files += invoker.src_files + } + + # Handle "source_deps". + _src_list_files = [] + if (defined(invoker.source_deps)) { + foreach(_source_dep, invoker.source_deps) { + _dep_gen_dir = get_label_info(_source_dep, "target_gen_dir") + _dep_name = get_label_info(_source_dep, "name") + _src_list_files += rebase_path([ "$_dep_gen_dir/$_dep_name.sources" ]) + deps += [ _source_dep ] + } + } + args += [ "--src-jar=${_src_jars}" ] + args += [ "--src-files=${_src_files}" ] + args += [ "--src-list-files=${_src_list_files}" ] + args += [ "--excluded-classes=$_excluded_patterns" ] + + inputs = _src_jars + inputs += _src_files + inputs += _src_list_files + } +} + +jar_src("jar_cronet_api_source") { + src_search_dirs = [ + "api/src", + _templates_dir, + ] + source_deps = [ ":cronet_api_java" ] + jar_path = "$_package_dir/cronet_api-src.jar" +} + +jar_src("jar_cronet_impl_common_java_source") { + src_search_dirs = [ + "java/src", + _templates_dir, + ] + source_deps = [ ":cronet_impl_common_base_java" ] + srcjar_deps = cronet_impl_common_java_srcjar_deps + jar_path = "$_package_dir/cronet_impl_common_java-src.jar" +} + +jar_src("jar_cronet_impl_platform_java_source") { + src_search_dirs = [ "java/src" ] + source_deps = [ ":cronet_impl_platform_base_java" ] + jar_path = "$_package_dir/cronet_impl_platform_java-src.jar" +} + +jar_src("jar_cronet_impl_fake_java_source") { + src_search_dirs = [ "fake/java" ] + source_deps = [ ":cronet_impl_fake_base_java" ] + jar_path = "$_package_dir/cronet_impl_fake_java-src.jar" +} + +jar_src("jar_cronet_impl_util_java_source") { + src_search_dirs = [ "java/src" ] + source_deps = [ ":cronet_impl_java_util_java" ] + jar_path = "$_package_dir/cronet_impl_util_java-src.jar" +} + +# List of patterns of .class files to exclude from the jar. +_jar_excluded_patterns = [ + # Excludes Android support libraries crbug.com/832770. + "android/*", + "androidx/*", + "*/library_loader/*.class", + "*/multidex/*.class", + "*/process_launcher/*.class", + "*/SysUtils*.class", + "org/chromium/base/FeatureList*.class", + "org/chromium/base/jank_tracker/*.class", + "org/chromium/base/memory/MemoryPressureMonitor*.class", +] + +template("repackage_jars") { + dist_jar(target_name) { + requires_android = true + direct_deps_only = true + use_unprocessed_jars = true + no_build_hooks = true + forward_variables_from(invoker, "*") + } +} + +repackage_jars("repackage_api") { + output = "$_package_dir/cronet_api.jar" + deps = [ ":cronet_api_java" ] +} + +repackage_jars("repackage_platform") { + output = "$_package_dir/cronet_impl_platform_java.jar" + deps = [ ":cronet_impl_platform_base_java" ] +} + +repackage_jars("repackage_fake") { + output = "$_package_dir/cronet_impl_fake_java.jar" + deps = [ ":cronet_impl_fake_base_java" ] +} + +repackage_jars("repackage_util") { + output = "$_package_dir/cronet_impl_util_java.jar" + deps = [ ":cronet_impl_java_util_java" ] +} + +# See crbug.com/1005836 for more info on why repackage_native requires 2 extra +# targets. These 3 targets exist to ensure the correct version of GEN_JNI +# (a generated class containing native method definitions) is included. +repackage_jars("repackage_native") { + output = "$_package_dir/cronet_impl_native_java.jar" + deps = [ + ":cronet_jni_registration_java", + ":repackage_native_java", + ] + jar_excluded_patterns = _jar_excluded_patterns +} + +_native_intermediate_jar_path = "$target_out_dir/repackage_native_impl.jar" + +# Do not depend on this target directly. Use :repackage_native. +repackage_jars("repackage_native_impl") { + output = _native_intermediate_jar_path + deps = cronet_impl_native_java_deps_to_package + + [ ":cronet_impl_native_base_java" ] + jar_excluded_patterns = _jar_excluded_patterns +} + +# Do not depend on this target directly. Use :repackage_native. +# This target exists to provide :repackage_native with a suitable target to +# depend on (since dist_aar only pulls in deps of type "java_library"). +android_java_prebuilt("repackage_native_java") { + jar_path = _native_intermediate_jar_path + + # Since only the unprocessed jar is used, no need to complete the bytecode + # processing step. + enable_bytecode_checks = false + deps = [ ":repackage_native_impl" ] +} + +repackage_jars("repackage_common") { + output = "$_package_dir/cronet_impl_common_java.jar" + deps = cronet_impl_common_java_deps_to_package + [ + ":cronet_impl_common_base_java", + ":cronet_impl_java_util_java", + ] +} + +if (!is_component_build) { + _cronet_shared_lib_file_name = "lib" + _cronet_shared_lib_name + ".so" + + # cronet_sample_test_apk_resources is identical to + # cronet_sample_apk_resources. The two have to be different targets because + # targets which are common between the "instrumentation test apk" and the + # "tested apk" are removed from the "instrumentation test apk". + android_resources("cronet_sample_test_apk_resources") { + sources = [ + "sample/res/layout/activity_main.xml", + "sample/res/layout/dialog_url.xml", + "sample/res/values/dimens.xml", + "sample/res/values/strings.xml", + ] + android_manifest = "sample/javatests/AndroidManifest.xml" + } + + instrumentation_test_apk("cronet_sample_test_apk") { + apk_name = "CronetSampleTest" + apk_under_test = ":cronet_sample_apk" + android_manifest = "sample/javatests/AndroidManifest.xml" + sources = [ "sample/javatests/src/org/chromium/cronet_sample_apk/CronetSampleTest.java" ] + deps = [ + ":cronet_sample_apk_java", + "//third_party/android_deps:espresso_java", + "//third_party/android_support_test_runner:rules_java", + "//third_party/android_support_test_runner:runner_java", + "//third_party/androidx:androidx_test_runner_java", + "//third_party/junit", + ] + + enable_multidex = false + if (!is_java_debug) { + proguard_enabled = true + proguard_configs = [ "sample/javatests/proguard.cfg" ] + } + } + + generate_jni("cronet_tests_jni_headers") { + testonly = true + sources = [ + "test/javatests/src/org/chromium/net/CronetUrlRequestContextTest.java", + "test/javatests/src/org/chromium/net/CronetUrlRequestTest.java", + "test/javatests/src/org/chromium/net/ExperimentalOptionsTest.java", + "test/src/org/chromium/net/CronetTestUtil.java", + "test/src/org/chromium/net/MockCertVerifier.java", + "test/src/org/chromium/net/MockUrlRequestJobFactory.java", + "test/src/org/chromium/net/NativeTestServer.java", + "test/src/org/chromium/net/QuicTestServer.java", + "test/src/org/chromium/net/TestUploadDataStreamHandler.java", + ] + } + + shared_library("cronet_tests") { + testonly = true + sources = [ + # While "cronet_tests" cannot depend on "cronet_static", and hence cannot + # call any Cronet functions, it can access fields of Cronet objects, so add + # Cronet header files to facilitate accessing these fields. + "//components/cronet/android/cronet_url_request_adapter.h", + "//components/cronet/android/cronet_url_request_context_adapter.h", + "//components/cronet/cronet_url_request.h", + "//components/cronet/cronet_url_request_context.h", + "//components/cronet/url_request_context_config.h", + "test/cronet_test_jni.cc", + "test/cronet_test_util.cc", + "test/cronet_test_util.h", + "test/cronet_url_request_context_config_test.cc", + "test/cronet_url_request_context_config_test.h", + "test/cronet_url_request_test.cc", + "test/experimental_options_test.cc", + "test/mock_cert_verifier.cc", + "test/mock_url_request_job_factory.cc", + "test/native_test_server.cc", + "test/quic_test_server.cc", + "test/test_upload_data_stream_handler.cc", + "test/test_upload_data_stream_handler.h", + "test/url_request_intercepting_job_factory.cc", + "test/url_request_intercepting_job_factory.h", + ] + + deps = [ + ":cronet", + ":cronet_tests_jni_headers", + "//base", + "//base:i18n", + "//base/test:test_support", + "//components/cronet:cronet_version_header", + "//components/cronet/testing:test_support", + "//components/prefs", + "//net", + "//net:simple_quic_tools", + "//net:test_support", + "//third_party/icu", + ] + + include_dirs = [ _cronet_version_header_include_dir ] + + configs -= [ "//build/config/android:hide_all_but_jni_onload" ] + configs += [ "//build/config/android:hide_all_but_jni" ] + } + + android_resources("cronet_test_apk_resources") { + testonly = true + sources = [ + "test/res/values/strings.xml", + "test/res/xml/network_security_config.xml", + "test/smoketests/res/native/values/strings.xml", + ] + } + + android_library("cronet_test_apk_java") { + testonly = true + + sources = [ + "test/src/org/chromium/net/CronetTestApplication.java", + "test/src/org/chromium/net/CronetTestUtil.java", + "test/src/org/chromium/net/Http2TestHandler.java", + "test/src/org/chromium/net/Http2TestServer.java", + "test/src/org/chromium/net/MockCertVerifier.java", + "test/src/org/chromium/net/MockUrlRequestJobFactory.java", + "test/src/org/chromium/net/NativeTestServer.java", + "test/src/org/chromium/net/QuicTestServer.java", + "test/src/org/chromium/net/ReportingCollector.java", + "test/src/org/chromium/net/TestFilesInstaller.java", + "test/src/org/chromium/net/TestUploadDataStreamHandler.java", + ] + + deps = [ + ":cronet_api_java", + ":cronet_impl_all_java", + "//base:base_java", + "//base:base_java_test_support", + "//net/android:net_java_test_support", + "//third_party/android_sdk:android_test_base_java", + "//third_party/junit", + "//third_party/netty4:netty_all_java", + ] + } + + cronet_smoketests_platform_only_common_srcs = [ + "test/smoketests/src/org/chromium/net/smoke/ChromiumPlatformOnlyTestSupport.java", + "test/smoketests/src/org/chromium/net/smoke/CronetSmokeTestRule.java", + "test/smoketests/src/org/chromium/net/smoke/HttpTestServer.java", + "test/smoketests/src/org/chromium/net/smoke/SmokeTestRequestCallback.java", + "test/smoketests/src/org/chromium/net/smoke/TestSupport.java", + ] + + cronet_smoketests_native_common_srcs = cronet_smoketests_platform_only_common_srcs + [ + "test/smoketests/src/org/chromium/net/smoke/ChromiumNativeTestSupport.java", + "test/smoketests/src/org/chromium/net/smoke/NativeCronetTestRule.java", + ] + + # cronet_common_javatests.jar - Cronet Java test common files. + android_library("cronet_common_javatests") { + testonly = true + sources = [ + "test/javatests/src/org/chromium/net/CronetTestRule.java", + "test/javatests/src/org/chromium/net/TestUploadDataProvider.java", + "test/javatests/src/org/chromium/net/TestUrlRequestCallback.java", + ] + deps = [ + ":cronet_api_java", + ":cronet_impl_all_java", + "//base:base_java", + "//third_party/android_sdk:android_test_base_java", + "//third_party/android_support_test_runner:runner_java", + "//third_party/junit", + ] + } + + # cronet_fake_javatests.jar - Java tests for the fake implementation of Cronet. + android_library("cronet_fake_javatests") { + testonly = true + sources = [ + "fake/javatests/org/chromium/net/test/FakeCronetControllerTest.java", + "fake/javatests/org/chromium/net/test/FakeCronetEngineTest.java", + "fake/javatests/org/chromium/net/test/FakeCronetProviderTest.java", + "fake/javatests/org/chromium/net/test/FakeUrlRequestTest.java", + "fake/javatests/org/chromium/net/test/FakeUrlResponseTest.java", + "fake/javatests/org/chromium/net/test/UrlResponseMatcherTest.java", + ] + + deps = [ + ":cronet_api_java", + ":cronet_common_javatests", + ":cronet_impl_common_base_java", + ":cronet_impl_fake_base_java", + ":cronet_impl_platform_base_java", + "//base:base_java_test_support", + "//third_party/android_sdk:android_test_base_java", + "//third_party/android_support_test_runner:runner_java", + "//third_party/androidx:androidx_test_runner_java", + "//third_party/junit", + "//third_party/mockito:mockito_java", + ] + } + + cronet_javatests_deps_to_package = [ + ":cronet_common_javatests", + ":cronet_fake_javatests", + ":cronet_test_apk_java", + "//base:base_java", + "//base:base_java_test_support", + + "//net/android:embedded_test_server_aidl_java", + "//net/android:net_java", + "//net/android:net_java_test_support", + "//url:url_java", + ] + + android_library("cronet_javatests") { + testonly = true + + sources = [ + "test/javatests/src/org/chromium/net/BidirectionalStreamQuicTest.java", + "test/javatests/src/org/chromium/net/BidirectionalStreamTest.java", + "test/javatests/src/org/chromium/net/BrotliTest.java", + "test/javatests/src/org/chromium/net/Criteria.java", + "test/javatests/src/org/chromium/net/CronetEngineBuilderTest.java", + "test/javatests/src/org/chromium/net/CronetStressTest.java", + "test/javatests/src/org/chromium/net/CronetTestRuleTest.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/DiskStorageTest.java", + "test/javatests/src/org/chromium/net/ExperimentalOptionsTest.java", + "test/javatests/src/org/chromium/net/GetStatusTest.java", + "test/javatests/src/org/chromium/net/MetricsTestUtil.java", + "test/javatests/src/org/chromium/net/NQETest.java", + "test/javatests/src/org/chromium/net/NetworkChangeNotifierTest.java", + "test/javatests/src/org/chromium/net/NetworkErrorLoggingTest.java", + "test/javatests/src/org/chromium/net/PkpTest.java", + "test/javatests/src/org/chromium/net/QuicTest.java", + "test/javatests/src/org/chromium/net/RequestFinishedInfoTest.java", + "test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java", + "test/javatests/src/org/chromium/net/TestDrivenDataProvider.java", + "test/javatests/src/org/chromium/net/TestNetworkQualityRttListener.java", + "test/javatests/src/org/chromium/net/TestNetworkQualityThroughputListener.java", + "test/javatests/src/org/chromium/net/UploadDataProvidersTest.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/CronetInputStreamTest.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/QuicUploadTest.java", + "test/javatests/src/org/chromium/net/urlconnection/TestUtil.java", + ] + + # Adding deps here won't include those deps in the cronet_tests_java.jar. + # Please add to cronet_javatests_deps_to_package instead. + deps = [ + ":cronet_api_java", + ":cronet_impl_all_java", + ":cronet_urlconnection_impl_java", + "//third_party/android_sdk:android_test_base_java", + "//third_party/android_support_test_runner:runner_java", + "//third_party/androidx:androidx_test_runner_java", + "//third_party/google-truth:google_truth_java", + "//third_party/hamcrest:hamcrest_core_java", + "//third_party/junit", + "//third_party/mockito:mockito_java", + ] + deps += cronet_javatests_deps_to_package + data = [ "//components/cronet/testing/test_server/data/" ] + } + + instrumentation_test_apk("cronet_test_instrumentation_apk") { + # This is the only Cronet APK with lint enabled. This one was chosen because + # it depends on basically all source files. + enable_lint = true + lint_suppressions_file = "lint-suppressions.xml" + lint_baseline_file = "lint-baseline.xml" + + # Still needs to support KitKat. See crbug.com/1042122. + lint_min_sdk_version = 19 + + apk_name = "CronetTestInstrumentation" + android_manifest = "test/javatests/AndroidManifest.xml" + + shared_libraries = [ + ":cronet", + ":cronet_tests", + ] + loadable_modules = [ "$root_out_dir/libnetty-tcnative.so" ] + + sources = cronet_smoketests_native_common_srcs + [ + "test/smoketests/src/org/chromium/net/smoke/Http2Test.java", + "test/smoketests/src/org/chromium/net/smoke/QuicTest.java", + ] + + deps = [ + ":cronet_api_java", + ":cronet_combine_proguard_flags", + ":cronet_impl_all_java", + ":cronet_javatests", + ":cronet_test_apk_java", + ":cronet_test_apk_resources", + "//base:base_java", + "//base:base_java_test_support", + "//net/android:net_java", + "//net/android:net_java_test_support", + "//third_party/android_sdk:android_test_base_java", + "//third_party/android_sdk:android_test_mock_java", + "//third_party/android_support_test_runner:runner_java", + "//third_party/androidx:androidx_test_runner_java", + "//third_party/hamcrest:hamcrest_core_java", + "//third_party/junit", + "//third_party/netty-tcnative:netty-tcnative-so", + "//third_party/netty4:netty_all_java", + ] + additional_apks = [ "//net/android:net_test_support_apk" ] + + data_deps = [ "//net:test_support" ] + + enable_multidex = true + if (!is_java_debug) { + proguard_enabled = true + + proguard_configs = [ + "$target_gen_dir/cronet_impl_native_proguard.cfg", + "cronet_impl_common_proguard.cfg", + "cronet_impl_platform_proguard.cfg", + "test/proguard.cfg", + ] + } + } + + android_resources("cronet_smoketests_platform_only_apk_resources") { + testonly = true + sources = [ + "test/smoketests/res/platform_only/values/strings.xml", + "test/smoketests/res/platform_only/xml/network_security_config.xml", + ] + } + + instrumentation_test_apk( + "cronet_smoketests_platform_only_instrumentation_apk") { + apk_name = "PlatformOnlyEngineSmokeTestInstrumentation" + android_manifest = "test/javatests/AndroidManifest.xml" + + sources = cronet_smoketests_platform_only_common_srcs + [ + "test/src/org/chromium/net/CronetTestApplication.java", + "test/smoketests/src/org/chromium/net/smoke/PlatformOnlyEngineTest.java", + ] + + deps = [ + ":cronet_api_java", + ":cronet_impl_common_base_java", + ":cronet_impl_platform_base_java", + ":cronet_smoketests_platform_only_apk_resources", + "//base:base_java_test_support", + "//third_party/android_sdk:android_test_base_java", + "//third_party/android_sdk:android_test_mock_java", + "//third_party/android_support_test_runner:runner_java", + "//third_party/androidx:androidx_test_runner_java", + "//third_party/junit", + "//third_party/netty4:netty_all_java", + ] + + if (!is_java_debug) { + proguard_enabled = true + + proguard_configs = [ + "cronet_impl_common_proguard.cfg", + "cronet_impl_platform_proguard.cfg", + "test/proguard.cfg", + ] + } + } + + instrumentation_test_apk( + "cronet_smoketests_missing_native_library_instrumentation_apk") { + apk_name = "MissingNativeLibrarySmokeTestInstrumentation" + android_manifest = "test/javatests/AndroidManifest.xml" + + sources = cronet_smoketests_native_common_srcs + [ "test/smoketests/src/org/chromium/net/smoke/MissingNativeLibraryTest.java" ] + deps = [ + ":cronet_api_java", + ":cronet_combine_proguard_flags", + ":cronet_impl_common_base_java", + ":cronet_impl_platform_base_java", + ":cronet_test_apk_java", + ":cronet_test_apk_resources", + "//base:base_java", + "//base:base_java_test_support", + "//third_party/android_sdk:android_test_base_java", + "//third_party/android_sdk:android_test_mock_java", + "//third_party/android_support_test_runner:runner_java", + "//third_party/androidx:androidx_test_runner_java", + "//third_party/junit", + "//third_party/netty4:netty_all_java", + ] + + enable_multidex = true + if (!is_java_debug) { + proguard_enabled = true + proguard_configs = [ + "$target_gen_dir/cronet_impl_native_proguard.cfg", + "cronet_impl_common_proguard.cfg", + "cronet_impl_platform_proguard.cfg", + "test/proguard.cfg", + ] + } + } + + android_apk("cronet_perf_test_apk") { + testonly = true + apk_name = "CronetPerfTest" + android_manifest = "test/javaperftests/AndroidManifest.xml" + shared_libraries = [ + ":cronet", + ":cronet_tests", + ] + + sources = [ + "test/javaperftests/src/org/chromium/net/CronetPerfTestActivity.java", + ] + + deps = [ + ":cronet_api_java", + ":cronet_combine_proguard_flags", + ":cronet_impl_all_java", + ":cronet_javatests", + ":cronet_test_apk_java", + "//base:base_java", + "//third_party/android_sdk:android_test_mock_java", + "//third_party/junit", + ] + + enable_multidex = true + if (!is_java_debug) { + proguard_enabled = true + proguard_configs = [ + "$target_gen_dir/cronet_impl_native_proguard.cfg", + "cronet_impl_common_proguard.cfg", + "test/proguard.cfg", + "//base/android/proguard/chromium_apk.flags", + "//testing/android/proguard_for_test.flags", + ] + enable_proguard_checks = false + } + } + + test("cronet_unittests_android") { + deps = [ + ":cronet_impl_native_base_java", + ":cronet_static", + "//base", + "//base/test:test_support", + "//components/cronet:cronet_common_unittests", + "//components/cronet/native:cronet_native_unittests", + "//components/metrics", + "//components/prefs:test_support", + "//net", + "//net:test_support", + "//net/android:net_java", + "//testing/gtest", + ] + + sources = [ "../run_all_unittests.cc" ] + + data = [ "//components/cronet/testing/test_server/data/" ] + + if (is_android) { + shard_timeout = 180 + } + } + + test("cronet_tests_android") { + deps = [ + ":cronet_impl_native_base_java", + ":cronet_static", + "//base", + "//base/test:test_support", + "//components/cronet/native/test:cronet_native_tests", + "//components/metrics", + "//components/prefs:test_support", + "//net", + "//net:test_support", + "//net/android:net_java", + "//testing/gtest", + ] + + allow_cleartext_traffic = true + + sources = [ "../run_all_unittests.cc" ] + + defines = [ "CRONET_TESTS_IMPLEMENTATION" ] + + data = [ "//components/cronet/testing/test_server/data/" ] + + if (is_android) { + shard_timeout = 180 + } + } + + _test_package_dir = "$root_out_dir/cronet/test" + + repackage_jars("repackage_test_jars") { + output = "$_test_package_dir/cronet_tests_java.jar" + testonly = true + deps = cronet_javatests_deps_to_package + [ + ":cronet_javatests", + "//third_party/netty4:netty_all_java", + ] + } + + zip("jar_cronet_sample_source") { + inputs = [ + "sample/AndroidManifest.xml", + "sample/javatests/AndroidManifest.xml", + "sample/javatests/proguard.cfg", + "sample/javatests/src/org/chromium/cronet_sample_apk/CronetSampleTest.java", + "sample/README", + "sample/res/layout/activity_main.xml", + "sample/res/layout/dialog_url.xml", + "sample/res/values/dimens.xml", + "sample/res/values/strings.xml", + "sample/src/org/chromium/cronet_sample_apk/CronetSampleActivity.java", + "sample/src/org/chromium/cronet_sample_apk/CronetSampleApplication.java", + ] + output = "$_package_dir/cronet-sample-src.jar" + base_dir = "sample" + } + + jar_src("jar_cronet_impl_native_java_source") { + src_search_dirs = [ + "//base/android/java/src", + "//components/cronet/android/java/src", + "//net/android/java/src", + "//url/android/java/src", + ] + source_deps = [ + ":cronet_impl_native_base_java", + "//base:base_java", + "//net/android:net_java", + "//url:url_java", + ] + srcjar_deps = cronet_impl_native_java_srcjar_deps + [ + ":cronet_jni_registration", + "//base:base_android_java_enums_srcjar", + "//net/android:net_android_java_enums_srcjar", + "//net/android:net_errors_java", + ] + excluded_patterns = _jar_excluded_patterns + jar_path = "$_package_dir/cronet_impl_native_java-src.jar" + } + + action("generate_licenses") { + _license_path = "$_package_dir/LICENSE" + + script = "//tools/licenses.py" + outputs = [ _license_path ] + args = [ + "license_file", + rebase_path(_license_path, root_build_dir), + "--gn-target", + "//components/cronet/android:cronet", + "--gn-out-dir", + ".", + ] + } + + action_with_pydeps("generate_javadoc") { + script = "//components/cronet/tools/generate_javadoc.py" + depfile = "$target_gen_dir/$target_name.d" + _zip_file = "$target_gen_dir/$target_name.zip" + outputs = [ _zip_file ] + _annotations_jar = "$root_out_dir/lib.java/third_party/androidx/androidx_annotation_annotation.jar" + _src_jar = "$_package_dir/cronet_api-src.jar" + inputs = [ + _annotations_jar, + _src_jar, + android_sdk_jar, + ] + + args = [ + "--output-dir", + rebase_path(_package_dir, root_build_dir), + "--input-dir", + rebase_path("//components/cronet", root_build_dir), + "--overview-file", + rebase_path("$_package_dir/README.md.html", root_build_dir), + "--readme-file", + rebase_path("//components/cronet/README.md", root_build_dir), + "--depfile", + rebase_path(depfile, root_build_dir), + "--zip-file", + rebase_path(_zip_file, root_build_dir), + "--android-sdk-jar", + rebase_path(android_sdk_jar, root_build_dir), + "--support-annotations-jar", + rebase_path(_annotations_jar, root_build_dir), + + # JavaDoc is generated from Cronet's API source jar. + "--input-src-jar", + rebase_path(_src_jar, root_build_dir), + ] + deps = [ + ":jar_cronet_api_source", + "//third_party/androidx:androidx_annotation_annotation_java", + ] + } + + copy("cronet_package_copy") { + sources = [ + "$target_gen_dir/cronet_impl_native_proguard.cfg", + "//AUTHORS", + "//chrome/VERSION", + "api_version.txt", + "cronet_impl_common_proguard.cfg", + "cronet_impl_fake_proguard.cfg", + "cronet_impl_platform_proguard.cfg", + ] + outputs = [ "$_package_dir/{{source_file_part}}" ] + + deps = [ + ":cronet_api_java", + ":cronet_combine_proguard_flags", + ":cronet_impl_common_base_java", + ":cronet_impl_fake_base_java", + ":cronet_impl_platform_base_java", + ] + } + + copy("cronet_package_copy_native_headers") { + sources = cronet_native_public_headers + grpc_public_headers + + outputs = [ "$_package_dir/include/{{source_file_part}}" ] + } + + copy("cronet_package_copy_native_lib") { + sources = [ "$root_out_dir/" + _cronet_shared_lib_file_name ] + outputs = [ "$_package_dir/libs/${android_app_abi}/" + + _cronet_shared_lib_file_name ] + deps = [ ":cronet" ] + } + + copy("cronet_package_copy_native_lib_unstripped") { + sources = [ "$root_out_dir/lib.unstripped/" + _cronet_shared_lib_file_name ] + outputs = [ "$_package_dir/symbols/${android_app_abi}/" + + _cronet_shared_lib_file_name ] + deps = [ ":cronet" ] + } + + copy("cronet_package_copy_native_test_lib") { + testonly = true + sources = [ + "$root_out_dir/libcronet_tests.so", + "$root_out_dir/libnetty-tcnative.so", + ] + outputs = + [ "$_test_package_dir/libs/${android_app_abi}/{{source_file_part}}" ] + deps = [ + ":cronet_tests", + "//third_party/netty-tcnative:netty-tcnative-so", + ] + } + + copy("cronet_package_copy_native_test_lib_unstripped") { + testonly = true + sources = [ + "$root_out_dir/lib.unstripped/libcronet_tests.so", + "$root_out_dir/lib.unstripped/libnetty-tcnative.so", + ] + outputs = + [ "$_test_package_dir/symbols/${android_app_abi}/{{source_file_part}}" ] + deps = [ + ":cronet_tests", + "//third_party/netty-tcnative:netty-tcnative-so", + ] + } + + copy("cronet_package_copy_test_assets") { + testonly = true + sources = [ "//components/cronet/testing/test_server/data" ] + outputs = [ "$_test_package_dir/assets/test" ] + } + + copy("cronet_package_copy_test_support_apks") { + testonly = true + sources = [ + # Provides EmbeddedTestServer. + "$root_out_dir/apks/ChromiumNetTestSupport.apk", + ] + outputs = + [ "$_test_package_dir/apks/${android_app_abi}/{{source_file_part}}" ] + deps = [ "//net/android:net_test_support_apk" ] + } + + copy("cronet_package_copy_test_files") { + testonly = true + sources = [ + "//net/data/ssl/certificates/expired_cert.pem", + "//net/data/ssl/certificates/quic-chain.pem", + "//net/data/ssl/certificates/quic-leaf-cert.key", + "//net/data/ssl/certificates/quic-leaf-cert.key.pkcs8.pem", + "//net/data/ssl/certificates/root_ca_cert.pem", + ] + outputs = [ "$_test_package_dir/assets/test_files/net/data/ssl/certificates/{{source_file_part}}" ] + deps = [ + # Not really dependent, but builds can fail if these two targets attempt + # to create the "assets" subdirectory simultaneously. + ":cronet_package_copy_test_assets", + ] + } + + copy("cronet_package_copy_resources") { + sources = [ "api/res/raw/keep_cronet_api.xml" ] + outputs = [ "$_package_dir/res/raw/{{source_file_part}}" ] + } + + # Enforce that ARM Neon is not used when building for ARMv7 + if (target_cpu == "arm" && arm_version == 7 && !arm_use_neon) { + action("enforce_no_neon") { + script = "//components/cronet/tools/check_no_neon.py" + outputs = [ "$target_gen_dir/$target_name.stamp" ] + args = [ + rebase_path("${android_tool_prefix}objdump", root_build_dir), + + # libcronet.so may contain ARM Neon instructions from BoringSSL, but these + # are only used after checking whether the CPU supports NEON at runtime, + # so instead check base/ as it represents a large swath of code that only + # contains Neon instructions when Neon is enabled by default. + rebase_path("$root_out_dir/obj/base/base/*.o", root_build_dir), + "--stamp", + rebase_path(outputs[0], root_build_dir), + ] + deps = [ "//base:base" ] + } + } + + # Enforce restrictions for API<->impl boundary. + action("api_static_checks") { + script = "//components/cronet/tools/api_static_checks.py" + outputs = [ "$target_gen_dir/$target_name.stamp" ] + _api_jar = + "$root_build_dir/lib.java/components/cronet/android/cronet_api_java.jar" + _common_jar = "$root_build_dir/lib.java/components/cronet/android/cronet_impl_common_base_java.jar" + _platform_jar = "$root_build_dir/lib.java/components/cronet/android/cronet_impl_platform_base_java.jar" + _native_jar = "$root_build_dir/lib.java/components/cronet/android/cronet_impl_native_base_java.jar" + args = [ + "--api_jar", + rebase_path(_api_jar, root_build_dir), + "--impl_jar", + rebase_path(_common_jar, root_build_dir), + "--impl_jar", + rebase_path(_platform_jar, root_build_dir), + "--impl_jar", + rebase_path(_native_jar, root_build_dir), + "--stamp", + rebase_path(outputs[0], root_build_dir), + ] + deps = [ + ":cronet_api_java", + ":cronet_impl_common_base_java", + ":cronet_impl_native_base_java", + ":cronet_impl_platform_base_java", + ] + inputs = [ + _api_jar, + _common_jar, + _platform_jar, + _native_jar, + "//components/cronet/tools/update_api.py", + ] + sources = [ + "//components/cronet/android/api.txt", + "//components/cronet/android/api_version.txt", + ] + } + + group("cronet_package_android") { + # Marked as testonly as it contains test-only targets too. + testonly = true + + # Enforce building with ICU alternatives, crbug.com/611621. + # Enforce that arm_use_neon==false when building for ARMv7 by + # not including any deps in cronet_package target otherwise. + if (use_platform_icu_alternatives && + (!(target_cpu == "arm" && arm_version == 7) || !arm_use_neon)) { + deps = [ + ":api_static_checks", + ":cronet_package_copy", + ":cronet_package_copy_native_headers", + ":cronet_package_copy_native_lib", + ":cronet_package_copy_native_lib_unstripped", + ":cronet_package_copy_resources", + ":cronet_sizes", + ":cronet_test_package", + ":generate_javadoc", + ":generate_licenses", + ":jar_cronet_api_source", + ":jar_cronet_impl_common_java_source", + ":jar_cronet_impl_fake_java_source", + ":jar_cronet_impl_native_java_source", + ":jar_cronet_impl_platform_java_source", + ":jar_cronet_sample_source", + ":repackage_api", + ":repackage_common", + ":repackage_fake", + ":repackage_native", + ":repackage_platform", + ] + if (current_cpu == "arm" && arm_version == 7) { + deps += [ ":enforce_no_neon" ] + } + } + } + + group("cronet_test_package") { + testonly = true + + # Don't build for MIPS where tests aren't run. + if (current_cpu != "mipsel" && current_cpu != "mips64el") { + deps = [ + ":cronet_package_copy_native_test_lib", + ":cronet_package_copy_native_test_lib_unstripped", + ":cronet_package_copy_test_assets", + ":cronet_package_copy_test_files", + ":cronet_package_copy_test_support_apks", + ":repackage_test_jars", + ] + } + } +} diff --git a/src/components/cronet/android/DEPS b/src/components/cronet/android/DEPS new file mode 100644 index 0000000000..d6643eb64b --- /dev/null +++ b/src/components/cronet/android/DEPS @@ -0,0 +1,5 @@ +include_rules = [ + "+components/metrics", + "+crypto", + "+jni", +] diff --git a/src/components/cronet/android/OWNERS b/src/components/cronet/android/OWNERS new file mode 100644 index 0000000000..22fd905f29 --- /dev/null +++ b/src/components/cronet/android/OWNERS @@ -0,0 +1 @@ +per-file lint-*.xml=* diff --git a/src/components/cronet/android/android_only_jni_onload_and_cronet_exports.lst b/src/components/cronet/android/android_only_jni_onload_and_cronet_exports.lst new file mode 100644 index 0000000000..363860d0dc --- /dev/null +++ b/src/components/cronet/android/android_only_jni_onload_and_cronet_exports.lst @@ -0,0 +1,13 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Linker script that exports only symbols required for JNI and Cronet Native +# API to work. +{ + global: + JNI_OnLoad; + Cronet_*; + local: + *; +}; diff --git a/src/components/cronet/android/api.txt b/src/components/cronet/android/api.txt new file mode 100644 index 0000000000..ba238fa0b3 --- /dev/null +++ b/src/components/cronet/android/api.txt @@ -0,0 +1,401 @@ +DO NOT EDIT THIS FILE, USE update_api.py TO UPDATE IT + +public class org.chromium.net.ApiVersion { + public static java.lang.String getCronetVersionWithLastChange(); + public static int getMaximumAvailableApiLevel(); + public static int getApiLevel(); + public static java.lang.String getCronetVersion(); + public static java.lang.String getLastChange(); +} +public abstract class org.chromium.net.BidirectionalStream$Builder { + public static final int STREAM_PRIORITY_IDLE; + public static final int STREAM_PRIORITY_LOWEST; + public static final int STREAM_PRIORITY_LOW; + public static final int STREAM_PRIORITY_MEDIUM; + public static final int STREAM_PRIORITY_HIGHEST; + public org.chromium.net.BidirectionalStream$Builder(); + public abstract org.chromium.net.BidirectionalStream$Builder setHttpMethod(java.lang.String); + public abstract org.chromium.net.BidirectionalStream$Builder addHeader(java.lang.String, java.lang.String); + public abstract org.chromium.net.BidirectionalStream$Builder setPriority(int); + public abstract org.chromium.net.BidirectionalStream$Builder delayRequestHeadersUntilFirstFlush(boolean); + public abstract org.chromium.net.BidirectionalStream build(); +} +public abstract class org.chromium.net.BidirectionalStream$Callback { + public org.chromium.net.BidirectionalStream$Callback(); + public abstract void onStreamReady(org.chromium.net.BidirectionalStream); + public abstract void onResponseHeadersReceived(org.chromium.net.BidirectionalStream, org.chromium.net.UrlResponseInfo); + public abstract void onReadCompleted(org.chromium.net.BidirectionalStream, org.chromium.net.UrlResponseInfo, java.nio.ByteBuffer, boolean); + public abstract void onWriteCompleted(org.chromium.net.BidirectionalStream, org.chromium.net.UrlResponseInfo, java.nio.ByteBuffer, boolean); + public void onResponseTrailersReceived(org.chromium.net.BidirectionalStream, org.chromium.net.UrlResponseInfo, org.chromium.net.UrlResponseInfo$HeaderBlock); + public abstract void onSucceeded(org.chromium.net.BidirectionalStream, org.chromium.net.UrlResponseInfo); + public abstract void onFailed(org.chromium.net.BidirectionalStream, org.chromium.net.UrlResponseInfo, org.chromium.net.CronetException); + public void onCanceled(org.chromium.net.BidirectionalStream, org.chromium.net.UrlResponseInfo); +} +public abstract class org.chromium.net.BidirectionalStream { + public org.chromium.net.BidirectionalStream(); + public abstract void start(); + public abstract void read(java.nio.ByteBuffer); + public abstract void write(java.nio.ByteBuffer, boolean); + public abstract void flush(); + public abstract void cancel(); + public abstract boolean isDone(); +} +public abstract class org.chromium.net.CallbackException extends org.chromium.net.CronetException { + protected org.chromium.net.CallbackException(java.lang.String, java.lang.Throwable); +} +public abstract class org.chromium.net.CronetEngine$Builder$LibraryLoader { + public org.chromium.net.CronetEngine$Builder$LibraryLoader(); + public abstract void loadLibrary(java.lang.String); +} +public class org.chromium.net.CronetEngine$Builder { + protected final org.chromium.net.ICronetEngineBuilder mBuilderDelegate; + public static final int HTTP_CACHE_DISABLED; + public static final int HTTP_CACHE_IN_MEMORY; + public static final int HTTP_CACHE_DISK_NO_HTTP; + public static final int HTTP_CACHE_DISK; + public org.chromium.net.CronetEngine$Builder(android.content.Context); + public org.chromium.net.CronetEngine$Builder(org.chromium.net.ICronetEngineBuilder); + public java.lang.String getDefaultUserAgent(); + public org.chromium.net.CronetEngine$Builder setUserAgent(java.lang.String); + public org.chromium.net.CronetEngine$Builder setStoragePath(java.lang.String); + public org.chromium.net.CronetEngine$Builder setLibraryLoader(org.chromium.net.CronetEngine$Builder$LibraryLoader); + public org.chromium.net.CronetEngine$Builder enableQuic(boolean); + public org.chromium.net.CronetEngine$Builder enableHttp2(boolean); + public org.chromium.net.CronetEngine$Builder enableSdch(boolean); + public org.chromium.net.CronetEngine$Builder enableBrotli(boolean); + public org.chromium.net.CronetEngine$Builder enableHttpCache(int, long); + public org.chromium.net.CronetEngine$Builder addQuicHint(java.lang.String, int, int); + public org.chromium.net.CronetEngine$Builder addPublicKeyPins(java.lang.String, java.util.Set, boolean, java.util.Date); + public org.chromium.net.CronetEngine$Builder enablePublicKeyPinningBypassForLocalTrustAnchors(boolean); + public org.chromium.net.CronetEngine build(); +} +public abstract class org.chromium.net.CronetEngine { + public org.chromium.net.CronetEngine(); + public abstract java.lang.String getVersionString(); + public abstract void shutdown(); + public abstract void startNetLogToFile(java.lang.String, boolean); + public abstract void stopNetLog(); + public abstract byte[] getGlobalMetricsDeltas(); + public abstract java.net.URLConnection openConnection(java.net.URL) throws java.io.IOException; + public abstract java.net.URLStreamHandlerFactory createURLStreamHandlerFactory(); + public abstract org.chromium.net.UrlRequest$Builder newUrlRequestBuilder(java.lang.String, org.chromium.net.UrlRequest$Callback, java.util.concurrent.Executor); +} +public abstract class org.chromium.net.CronetException extends java.io.IOException { + protected org.chromium.net.CronetException(java.lang.String, java.lang.Throwable); +} +public abstract class org.chromium.net.CronetProvider { + public static final java.lang.String PROVIDER_NAME_APP_PACKAGED; + public static final java.lang.String PROVIDER_NAME_FALLBACK; + protected final android.content.Context mContext; + protected org.chromium.net.CronetProvider(android.content.Context); + public abstract org.chromium.net.CronetEngine$Builder createBuilder(); + public abstract java.lang.String getName(); + public abstract java.lang.String getVersion(); + public abstract boolean isEnabled(); + public java.lang.String toString(); + public static java.util.List getAllProviders(android.content.Context); +} +public abstract class org.chromium.net.ExperimentalBidirectionalStream$Builder extends org.chromium.net.BidirectionalStream$Builder { + public org.chromium.net.ExperimentalBidirectionalStream$Builder(); + public org.chromium.net.ExperimentalBidirectionalStream$Builder addRequestAnnotation(java.lang.Object); + public org.chromium.net.ExperimentalBidirectionalStream$Builder setTrafficStatsTag(int); + public org.chromium.net.ExperimentalBidirectionalStream$Builder setTrafficStatsUid(int); + public abstract org.chromium.net.ExperimentalBidirectionalStream$Builder setHttpMethod(java.lang.String); + public abstract org.chromium.net.ExperimentalBidirectionalStream$Builder addHeader(java.lang.String, java.lang.String); + public abstract org.chromium.net.ExperimentalBidirectionalStream$Builder setPriority(int); + public abstract org.chromium.net.ExperimentalBidirectionalStream$Builder delayRequestHeadersUntilFirstFlush(boolean); + public abstract org.chromium.net.ExperimentalBidirectionalStream build(); + public org.chromium.net.BidirectionalStream build(); + public org.chromium.net.BidirectionalStream$Builder delayRequestHeadersUntilFirstFlush(boolean); + public org.chromium.net.BidirectionalStream$Builder setPriority(int); + public org.chromium.net.BidirectionalStream$Builder addHeader(java.lang.String, java.lang.String); + public org.chromium.net.BidirectionalStream$Builder setHttpMethod(java.lang.String); +} +public abstract class org.chromium.net.ExperimentalBidirectionalStream extends org.chromium.net.BidirectionalStream { + public org.chromium.net.ExperimentalBidirectionalStream(); +} +public class org.chromium.net.ExperimentalCronetEngine$Builder extends org.chromium.net.CronetEngine$Builder { + public org.chromium.net.ExperimentalCronetEngine$Builder(android.content.Context); + public org.chromium.net.ExperimentalCronetEngine$Builder(org.chromium.net.ICronetEngineBuilder); + public org.chromium.net.ExperimentalCronetEngine$Builder enableNetworkQualityEstimator(boolean); + public org.chromium.net.ExperimentalCronetEngine$Builder setExperimentalOptions(java.lang.String); + public org.chromium.net.ExperimentalCronetEngine$Builder setThreadPriority(int); + public org.chromium.net.ICronetEngineBuilder getBuilderDelegate(); + public org.chromium.net.ExperimentalCronetEngine$Builder setUserAgent(java.lang.String); + public org.chromium.net.ExperimentalCronetEngine$Builder setStoragePath(java.lang.String); + public org.chromium.net.ExperimentalCronetEngine$Builder setLibraryLoader(org.chromium.net.CronetEngine$Builder$LibraryLoader); + public org.chromium.net.ExperimentalCronetEngine$Builder enableQuic(boolean); + public org.chromium.net.ExperimentalCronetEngine$Builder enableHttp2(boolean); + public org.chromium.net.ExperimentalCronetEngine$Builder enableSdch(boolean); + public org.chromium.net.ExperimentalCronetEngine$Builder enableHttpCache(int, long); + public org.chromium.net.ExperimentalCronetEngine$Builder addQuicHint(java.lang.String, int, int); + public org.chromium.net.ExperimentalCronetEngine$Builder addPublicKeyPins(java.lang.String, java.util.Set, boolean, java.util.Date); + public org.chromium.net.ExperimentalCronetEngine$Builder enablePublicKeyPinningBypassForLocalTrustAnchors(boolean); + public org.chromium.net.ExperimentalCronetEngine build(); + public org.chromium.net.CronetEngine build(); + public org.chromium.net.CronetEngine$Builder enablePublicKeyPinningBypassForLocalTrustAnchors(boolean); + public org.chromium.net.CronetEngine$Builder addPublicKeyPins(java.lang.String, java.util.Set, boolean, java.util.Date); + public org.chromium.net.CronetEngine$Builder addQuicHint(java.lang.String, int, int); + public org.chromium.net.CronetEngine$Builder enableHttpCache(int, long); + public org.chromium.net.CronetEngine$Builder enableSdch(boolean); + public org.chromium.net.CronetEngine$Builder enableHttp2(boolean); + public org.chromium.net.CronetEngine$Builder enableQuic(boolean); + public org.chromium.net.CronetEngine$Builder setLibraryLoader(org.chromium.net.CronetEngine$Builder$LibraryLoader); + public org.chromium.net.CronetEngine$Builder setStoragePath(java.lang.String); + public org.chromium.net.CronetEngine$Builder setUserAgent(java.lang.String); +} +public abstract class org.chromium.net.ExperimentalCronetEngine extends org.chromium.net.CronetEngine { + public static final int CONNECTION_METRIC_UNKNOWN; + public static final int EFFECTIVE_CONNECTION_TYPE_UNKNOWN; + public static final int EFFECTIVE_CONNECTION_TYPE_OFFLINE; + public static final int EFFECTIVE_CONNECTION_TYPE_SLOW_2G; + public static final int EFFECTIVE_CONNECTION_TYPE_2G; + public static final int EFFECTIVE_CONNECTION_TYPE_3G; + public static final int EFFECTIVE_CONNECTION_TYPE_4G; + public org.chromium.net.ExperimentalCronetEngine(); + public abstract org.chromium.net.ExperimentalBidirectionalStream$Builder newBidirectionalStreamBuilder(java.lang.String, org.chromium.net.BidirectionalStream$Callback, java.util.concurrent.Executor); + public abstract org.chromium.net.ExperimentalUrlRequest$Builder newUrlRequestBuilder(java.lang.String, org.chromium.net.UrlRequest$Callback, java.util.concurrent.Executor); + public void startNetLogToDisk(java.lang.String, boolean, int); + public int getEffectiveConnectionType(); + public void configureNetworkQualityEstimatorForTesting(boolean, boolean, boolean); + public void addRttListener(org.chromium.net.NetworkQualityRttListener); + public void removeRttListener(org.chromium.net.NetworkQualityRttListener); + public void addThroughputListener(org.chromium.net.NetworkQualityThroughputListener); + public void removeThroughputListener(org.chromium.net.NetworkQualityThroughputListener); + public java.net.URLConnection openConnection(java.net.URL, java.net.Proxy) throws java.io.IOException; + public void addRequestFinishedListener(org.chromium.net.RequestFinishedInfo$Listener); + public void removeRequestFinishedListener(org.chromium.net.RequestFinishedInfo$Listener); + public int getHttpRttMs(); + public int getTransportRttMs(); + public int getDownstreamThroughputKbps(); + public org.chromium.net.UrlRequest$Builder newUrlRequestBuilder(java.lang.String, org.chromium.net.UrlRequest$Callback, java.util.concurrent.Executor); +} +public abstract class org.chromium.net.ExperimentalUrlRequest$Builder extends org.chromium.net.UrlRequest$Builder { + public static final int DEFAULT_IDEMPOTENCY; + public static final int IDEMPOTENT; + public static final int NOT_IDEMPOTENT; + public org.chromium.net.ExperimentalUrlRequest$Builder(); + public org.chromium.net.ExperimentalUrlRequest$Builder disableConnectionMigration(); + public org.chromium.net.ExperimentalUrlRequest$Builder addRequestAnnotation(java.lang.Object); + public org.chromium.net.ExperimentalUrlRequest$Builder setTrafficStatsTag(int); + public org.chromium.net.ExperimentalUrlRequest$Builder setTrafficStatsUid(int); + public org.chromium.net.ExperimentalUrlRequest$Builder setRequestFinishedListener(org.chromium.net.RequestFinishedInfo$Listener); + public org.chromium.net.ExperimentalUrlRequest$Builder setIdempotency(int); + public abstract org.chromium.net.ExperimentalUrlRequest$Builder setHttpMethod(java.lang.String); + public abstract org.chromium.net.ExperimentalUrlRequest$Builder addHeader(java.lang.String, java.lang.String); + public abstract org.chromium.net.ExperimentalUrlRequest$Builder disableCache(); + public abstract org.chromium.net.ExperimentalUrlRequest$Builder setPriority(int); + public abstract org.chromium.net.ExperimentalUrlRequest$Builder setUploadDataProvider(org.chromium.net.UploadDataProvider, java.util.concurrent.Executor); + public abstract org.chromium.net.ExperimentalUrlRequest$Builder allowDirectExecutor(); + public abstract org.chromium.net.ExperimentalUrlRequest build(); + public org.chromium.net.UrlRequest build(); + public org.chromium.net.UrlRequest$Builder allowDirectExecutor(); + public org.chromium.net.UrlRequest$Builder setUploadDataProvider(org.chromium.net.UploadDataProvider, java.util.concurrent.Executor); + public org.chromium.net.UrlRequest$Builder setPriority(int); + public org.chromium.net.UrlRequest$Builder disableCache(); + public org.chromium.net.UrlRequest$Builder addHeader(java.lang.String, java.lang.String); + public org.chromium.net.UrlRequest$Builder setHttpMethod(java.lang.String); +} +public abstract class org.chromium.net.ExperimentalUrlRequest extends org.chromium.net.UrlRequest { + public org.chromium.net.ExperimentalUrlRequest(); +} +public abstract class org.chromium.net.ICronetEngineBuilder { + public org.chromium.net.ICronetEngineBuilder(); + public abstract org.chromium.net.ICronetEngineBuilder addPublicKeyPins(java.lang.String, java.util.Set, boolean, java.util.Date); + public abstract org.chromium.net.ICronetEngineBuilder addQuicHint(java.lang.String, int, int); + public abstract org.chromium.net.ICronetEngineBuilder enableHttp2(boolean); + public abstract org.chromium.net.ICronetEngineBuilder enableHttpCache(int, long); + public abstract org.chromium.net.ICronetEngineBuilder enablePublicKeyPinningBypassForLocalTrustAnchors(boolean); + public abstract org.chromium.net.ICronetEngineBuilder enableQuic(boolean); + public abstract org.chromium.net.ICronetEngineBuilder enableSdch(boolean); + public org.chromium.net.ICronetEngineBuilder enableBrotli(boolean); + public abstract org.chromium.net.ICronetEngineBuilder setExperimentalOptions(java.lang.String); + public abstract org.chromium.net.ICronetEngineBuilder setLibraryLoader(org.chromium.net.CronetEngine$Builder$LibraryLoader); + public abstract org.chromium.net.ICronetEngineBuilder setStoragePath(java.lang.String); + public abstract org.chromium.net.ICronetEngineBuilder setUserAgent(java.lang.String); + public abstract java.lang.String getDefaultUserAgent(); + public abstract org.chromium.net.ExperimentalCronetEngine build(); + public org.chromium.net.ICronetEngineBuilder enableNetworkQualityEstimator(boolean); + public org.chromium.net.ICronetEngineBuilder setThreadPriority(int); +} +public final class org.chromium.net.InlineExecutionProhibitedException extends java.util.concurrent.RejectedExecutionException { + public org.chromium.net.InlineExecutionProhibitedException(); +} +public abstract class org.chromium.net.NetworkException extends org.chromium.net.CronetException { + public static final int ERROR_HOSTNAME_NOT_RESOLVED; + public static final int ERROR_INTERNET_DISCONNECTED; + public static final int ERROR_NETWORK_CHANGED; + public static final int ERROR_TIMED_OUT; + public static final int ERROR_CONNECTION_CLOSED; + public static final int ERROR_CONNECTION_TIMED_OUT; + public static final int ERROR_CONNECTION_REFUSED; + public static final int ERROR_CONNECTION_RESET; + public static final int ERROR_ADDRESS_UNREACHABLE; + public static final int ERROR_QUIC_PROTOCOL_FAILED; + public static final int ERROR_OTHER; + protected org.chromium.net.NetworkException(java.lang.String, java.lang.Throwable); + public abstract int getErrorCode(); + public abstract int getCronetInternalErrorCode(); + public abstract boolean immediatelyRetryable(); +} +public abstract class org.chromium.net.NetworkQualityRttListener { + public org.chromium.net.NetworkQualityRttListener(java.util.concurrent.Executor); + public java.util.concurrent.Executor getExecutor(); + public abstract void onRttObservation(int, long, int); +} +public abstract class org.chromium.net.NetworkQualityThroughputListener { + public org.chromium.net.NetworkQualityThroughputListener(java.util.concurrent.Executor); + public java.util.concurrent.Executor getExecutor(); + public abstract void onThroughputObservation(int, long, int); +} +public abstract class org.chromium.net.QuicException extends org.chromium.net.NetworkException { + protected org.chromium.net.QuicException(java.lang.String, java.lang.Throwable); + public abstract int getQuicDetailedErrorCode(); +} +public abstract class org.chromium.net.RequestFinishedInfo$Listener { + public org.chromium.net.RequestFinishedInfo$Listener(java.util.concurrent.Executor); + public abstract void onRequestFinished(org.chromium.net.RequestFinishedInfo); + public java.util.concurrent.Executor getExecutor(); +} +public abstract class org.chromium.net.RequestFinishedInfo$Metrics { + public org.chromium.net.RequestFinishedInfo$Metrics(); + public abstract java.util.Date getRequestStart(); + public abstract java.util.Date getDnsStart(); + public abstract java.util.Date getDnsEnd(); + public abstract java.util.Date getConnectStart(); + public abstract java.util.Date getConnectEnd(); + public abstract java.util.Date getSslStart(); + public abstract java.util.Date getSslEnd(); + public abstract java.util.Date getSendingStart(); + public abstract java.util.Date getSendingEnd(); + public abstract java.util.Date getPushStart(); + public abstract java.util.Date getPushEnd(); + public abstract java.util.Date getResponseStart(); + public abstract java.util.Date getRequestEnd(); + public abstract boolean getSocketReused(); + public abstract java.lang.Long getTtfbMs(); + public abstract java.lang.Long getTotalTimeMs(); + public abstract java.lang.Long getSentByteCount(); + public abstract java.lang.Long getReceivedByteCount(); +} +public abstract class org.chromium.net.RequestFinishedInfo { + public static final int SUCCEEDED; + public static final int FAILED; + public static final int CANCELED; + public org.chromium.net.RequestFinishedInfo(); + public abstract java.lang.String getUrl(); + public abstract java.util.Collection getAnnotations(); + public abstract org.chromium.net.RequestFinishedInfo$Metrics getMetrics(); + public abstract int getFinishedReason(); + public abstract org.chromium.net.UrlResponseInfo getResponseInfo(); + public abstract org.chromium.net.CronetException getException(); +} +public abstract class org.chromium.net.UploadDataProvider implements java.io.Closeable { + public org.chromium.net.UploadDataProvider(); + public abstract long getLength() throws java.io.IOException; + public abstract void read(org.chromium.net.UploadDataSink, java.nio.ByteBuffer) throws java.io.IOException; + public abstract void rewind(org.chromium.net.UploadDataSink) throws java.io.IOException; + public void close() throws java.io.IOException; +} +final class org.chromium.net.UploadDataProviders$ByteBufferUploadProvider extends org.chromium.net.UploadDataProvider { + public long getLength(); + public void read(org.chromium.net.UploadDataSink, java.nio.ByteBuffer); + public void rewind(org.chromium.net.UploadDataSink); +} +interface org.chromium.net.UploadDataProviders$FileChannelProvider { + public abstract java.nio.channels.FileChannel getChannel() throws java.io.IOException; +} +final class org.chromium.net.UploadDataProviders$FileUploadProvider extends org.chromium.net.UploadDataProvider { + public long getLength() throws java.io.IOException; + public void read(org.chromium.net.UploadDataSink, java.nio.ByteBuffer) throws java.io.IOException; + public void rewind(org.chromium.net.UploadDataSink) throws java.io.IOException; + public void close() throws java.io.IOException; +} +public final class org.chromium.net.UploadDataProviders { + public static org.chromium.net.UploadDataProvider create(java.io.File); + public static org.chromium.net.UploadDataProvider create(android.os.ParcelFileDescriptor); + public static org.chromium.net.UploadDataProvider create(java.nio.ByteBuffer); + public static org.chromium.net.UploadDataProvider create(byte[], int, int); + public static org.chromium.net.UploadDataProvider create(byte[]); +} +public abstract class org.chromium.net.UploadDataSink { + public org.chromium.net.UploadDataSink(); + public abstract void onReadSucceeded(boolean); + public abstract void onReadError(java.lang.Exception); + public abstract void onRewindSucceeded(); + public abstract void onRewindError(java.lang.Exception); +} +public abstract class org.chromium.net.UrlRequest$Builder { + public static final int REQUEST_PRIORITY_IDLE; + public static final int REQUEST_PRIORITY_LOWEST; + public static final int REQUEST_PRIORITY_LOW; + public static final int REQUEST_PRIORITY_MEDIUM; + public static final int REQUEST_PRIORITY_HIGHEST; + public org.chromium.net.UrlRequest$Builder(); + public abstract org.chromium.net.UrlRequest$Builder setHttpMethod(java.lang.String); + public abstract org.chromium.net.UrlRequest$Builder addHeader(java.lang.String, java.lang.String); + public abstract org.chromium.net.UrlRequest$Builder disableCache(); + public abstract org.chromium.net.UrlRequest$Builder setPriority(int); + public abstract org.chromium.net.UrlRequest$Builder setUploadDataProvider(org.chromium.net.UploadDataProvider, java.util.concurrent.Executor); + public abstract org.chromium.net.UrlRequest$Builder allowDirectExecutor(); + public abstract org.chromium.net.UrlRequest build(); +} +public abstract class org.chromium.net.UrlRequest$Callback { + public org.chromium.net.UrlRequest$Callback(); + public abstract void onRedirectReceived(org.chromium.net.UrlRequest, org.chromium.net.UrlResponseInfo, java.lang.String) throws java.lang.Exception; + public abstract void onResponseStarted(org.chromium.net.UrlRequest, org.chromium.net.UrlResponseInfo) throws java.lang.Exception; + public abstract void onReadCompleted(org.chromium.net.UrlRequest, org.chromium.net.UrlResponseInfo, java.nio.ByteBuffer) throws java.lang.Exception; + public abstract void onSucceeded(org.chromium.net.UrlRequest, org.chromium.net.UrlResponseInfo); + public abstract void onFailed(org.chromium.net.UrlRequest, org.chromium.net.UrlResponseInfo, org.chromium.net.CronetException); + public void onCanceled(org.chromium.net.UrlRequest, org.chromium.net.UrlResponseInfo); +} +public class org.chromium.net.UrlRequest$Status { + public static final int INVALID; + public static final int IDLE; + public static final int WAITING_FOR_STALLED_SOCKET_POOL; + public static final int WAITING_FOR_AVAILABLE_SOCKET; + public static final int WAITING_FOR_DELEGATE; + public static final int WAITING_FOR_CACHE; + public static final int DOWNLOADING_PAC_FILE; + public static final int RESOLVING_PROXY_FOR_URL; + public static final int RESOLVING_HOST_IN_PAC_FILE; + public static final int ESTABLISHING_PROXY_TUNNEL; + public static final int RESOLVING_HOST; + public static final int CONNECTING; + public static final int SSL_HANDSHAKE; + public static final int SENDING_REQUEST; + public static final int WAITING_FOR_RESPONSE; + public static final int READING_RESPONSE; +} +public abstract class org.chromium.net.UrlRequest$StatusListener { + public org.chromium.net.UrlRequest$StatusListener(); + public abstract void onStatus(int); +} +public abstract class org.chromium.net.UrlRequest { + public org.chromium.net.UrlRequest(); + public abstract void start(); + public abstract void followRedirect(); + public abstract void read(java.nio.ByteBuffer); + public abstract void cancel(); + public abstract boolean isDone(); + public abstract void getStatus(org.chromium.net.UrlRequest$StatusListener); +} +public abstract class org.chromium.net.UrlResponseInfo$HeaderBlock { + public org.chromium.net.UrlResponseInfo$HeaderBlock(); + public abstract java.util.List> getAsList(); + public abstract java.util.Map> getAsMap(); +} +public abstract class org.chromium.net.UrlResponseInfo { + public org.chromium.net.UrlResponseInfo(); + public abstract java.lang.String getUrl(); + public abstract java.util.List getUrlChain(); + public abstract int getHttpStatusCode(); + public abstract java.lang.String getHttpStatusText(); + public abstract java.util.List> getAllHeadersAsList(); + public abstract java.util.Map> getAllHeaders(); + public abstract boolean wasCached(); + public abstract java.lang.String getNegotiatedProtocol(); + public abstract java.lang.String getProxyServer(); + public abstract long getReceivedByteCount(); +} +Stamp: 1c548f381e3715d983e967e830d675d8 diff --git a/src/components/cronet/android/api/res/raw/keep_cronet_api.xml b/src/components/cronet/android/api/res/raw/keep_cronet_api.xml new file mode 100644 index 0000000000..b87be37516 --- /dev/null +++ b/src/components/cronet/android/api/res/raw/keep_cronet_api.xml @@ -0,0 +1,3 @@ + + diff --git a/src/components/cronet/android/api/src/org/chromium/net/ApiVersion.template b/src/components/cronet/android/api/src/org/chromium/net/ApiVersion.template new file mode 100644 index 0000000000..4c32f80cc3 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/ApiVersion.template @@ -0,0 +1,63 @@ +// Copyright 2014 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; + +/** + * Version based on chrome/VERSION. + * {@hide as it's only used internally} + */ +public class ApiVersion { + private static final String CRONET_VERSION = "@MAJOR@.@MINOR@.@BUILD@.@PATCH@"; + private static final int API_LEVEL = @API_LEVEL@; + /** + * The minimum API level of implementations that are compatible with this API. + * The last API level which broke backwards API compatibility. In other words, the + * Cronet API that this class is part of won't work with Cronet implementations that implement + * API levels less than this value. That is if + * ImplVersion.getApiLevel() < ApiVersion.getApiLevel(), then the Cronet implementation + * providing ImplVersion cannot be used with the Cronet API providing ApiVersion; if they are + * used together various unexpected Errors, like AbstractMethodError, may result. + */ + private static final int MIN_COMPATIBLE_API_LEVEL = 3; + private static final String LAST_CHANGE = "@LASTCHANGE@"; + + /** + * Private constructor. All members of this class should be static. + */ + private ApiVersion() {} + + public static String getCronetVersionWithLastChange() { + return CRONET_VERSION + "@" + LAST_CHANGE.substring(0, 8); + } + + /** + * Returns API level of the API linked into the application. This is the maximum API + * level the application can use, even if the application is run with a newer implementation. + */ + public static int getMaximumAvailableApiLevel() { + return API_LEVEL; + } + + /** + * The minimum API level of implementations that are compatible with this API. + * Returns the last API level which broke backwards API compatibility. In other words, the + * Cronet API that this class is part of won't work with Cronet implementations that implement + * API levels less than this value. That is if + * ImplVersion.getApiLevel() < ApiVersion.getApiLevel(), then the Cronet implementation + * providing ImplVersion cannot be used with the Cronet API providing ApiVersion; if they are + * used together various unexpected Errors, like AbstractMethodError, may result. + */ + public static int getApiLevel() { + return MIN_COMPATIBLE_API_LEVEL; + } + + public static String getCronetVersion() { + return CRONET_VERSION; + } + + public static String getLastChange() { + return LAST_CHANGE; + } +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java b/src/components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java new file mode 100644 index 0000000000..e21a8fc28c --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/BidirectionalStream.java @@ -0,0 +1,303 @@ +// 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.annotation.SuppressLint; + +import java.nio.ByteBuffer; +import java.util.concurrent.Executor; + +/** + * Class for bidirectional sending and receiving of data over HTTP/2 or QUIC connections. + * Created by {@link Builder}. + * + * Note: There are ordering restrictions on methods of {@link BidirectionalStream}; + * please see individual methods for description of restrictions. + * + * {@hide experimental} + */ +public abstract class BidirectionalStream { + /** + * Builder for {@link BidirectionalStream}s. Allows configuring stream before constructing + * it via {@link Builder#build}. Created by + * {@link ExperimentalCronetEngine#newBidirectionalStreamBuilder}. + */ + public abstract static class Builder { + /** + * Sets the HTTP method for the request. Returns builder to facilitate chaining. + * + * @param method the method to use for request. Default is 'POST' + * @return the builder to facilitate chaining + */ + public abstract Builder setHttpMethod(String method); + + /** + * Adds a request header. Returns builder to facilitate chaining. + * + * @param header the header name + * @param value the header value + * @return the builder to facilitate chaining + */ + public abstract Builder addHeader(String header, String value); + + /** + * Lowest stream priority. Passed to {@link #setPriority}. + */ + public static final int STREAM_PRIORITY_IDLE = 0; + /** + * Very low stream priority. Passed to {@link #setPriority}. + */ + public static final int STREAM_PRIORITY_LOWEST = 1; + /** + * Low stream priority. Passed to {@link #setPriority}. + */ + public static final int STREAM_PRIORITY_LOW = 2; + /** + * Medium stream priority. Passed to {@link #setPriority}. This is the + * default priority given to the stream. + */ + public static final int STREAM_PRIORITY_MEDIUM = 3; + /** + * Highest stream priority. Passed to {@link #setPriority}. + */ + public static final int STREAM_PRIORITY_HIGHEST = 4; + + /** + * Sets priority of the stream which should be one of the + * {@link #STREAM_PRIORITY_IDLE STREAM_PRIORITY_*} values. + * The stream is given {@link #STREAM_PRIORITY_MEDIUM} priority if + * this method is not called. + * + * @param priority priority of the stream which should be one of the + * {@link #STREAM_PRIORITY_IDLE STREAM_PRIORITY_*} values. + * @return the builder to facilitate chaining. + */ + public abstract Builder setPriority(int priority); + + /** + * Delays sending request headers until {@link BidirectionalStream#flush()} + * is called. This flag is currently only respected when QUIC is negotiated. + * When true, QUIC will send request header frame along with data frame(s) + * as a single packet when possible. + * + * @param delayRequestHeadersUntilFirstFlush if true, sending request headers will + * be delayed until flush() is called. + * @return the builder to facilitate chaining. + */ + public abstract Builder delayRequestHeadersUntilFirstFlush( + boolean delayRequestHeadersUntilFirstFlush); + + /** + * Creates a {@link BidirectionalStream} using configuration from this + * {@link Builder}. The returned {@code BidirectionalStream} can then be started + * by calling {@link BidirectionalStream#start}. + * + * @return constructed {@link BidirectionalStream} using configuration from + * this {@link Builder} + */ + @SuppressLint("WrongConstant") // TODO(jbudorick): Remove this after rolling to the N SDK. + public abstract BidirectionalStream build(); + } + + /** + * Callback class used to receive callbacks from a {@link BidirectionalStream}. + */ + public abstract static class Callback { + /** + * Invoked when the stream is ready for reading and writing. + * Consumer may call {@link BidirectionalStream#read read()} to start reading data. + * Consumer may call {@link BidirectionalStream#write write()} to start writing data. + * + * @param stream the stream that is ready. + */ + public abstract void onStreamReady(BidirectionalStream stream); + + /** + * Invoked when initial response headers are received. Headers are available from + * {@code info.}{@link UrlResponseInfo#getAllHeaders getAllHeaders()}. + * Consumer may call {@link BidirectionalStream#read read()} to start reading. + * Consumer may call {@link BidirectionalStream#write write()} to start writing or close the + * stream. + * + * @param stream the stream on which response headers were received. + * @param info the response information. + */ + public abstract void onResponseHeadersReceived( + BidirectionalStream stream, UrlResponseInfo info); + + /** + * Invoked when data is read into the buffer passed to {@link BidirectionalStream#read + * read()}. Only part of the buffer may be populated. To continue reading, call {@link + * BidirectionalStream#read read()}. It may be invoked after {@code + * onResponseTrailersReceived()}, if there was pending read data before trailers were + * received. + * + * @param stream the stream on which the read completed + * @param info the response information + * @param buffer the buffer that was passed to {@link BidirectionalStream#read read()}, + * now containing the received data. The buffer's limit is not changed. + * The buffer's position is set to the end of the received data. If position is not + * updated, it means the remote side has signaled that it will send no more data. + * @param endOfStream if true, this is the last read data, remote will not send more data, + * and the read side is closed. + * + */ + public abstract void onReadCompleted(BidirectionalStream stream, UrlResponseInfo info, + ByteBuffer buffer, boolean endOfStream); + + /** + * Invoked when the entire ByteBuffer passed to {@link BidirectionalStream#write write()} + * is sent. The buffer's position is updated to be the same as the buffer's limit. + * The buffer's limit is not changed. To continue writing, call + * {@link BidirectionalStream#write write()}. + * + * @param stream the stream on which the write completed + * @param info the response information + * @param buffer the buffer that was passed to {@link BidirectionalStream#write write()}. + * The buffer's position is set to the buffer's limit. The buffer's limit + * is not changed. + * @param endOfStream the endOfStream flag that was passed to the corresponding + * {@link BidirectionalStream#write write()}. If true, the write side is closed. + */ + public abstract void onWriteCompleted(BidirectionalStream stream, UrlResponseInfo info, + ByteBuffer buffer, boolean endOfStream); + + /** + * Invoked when trailers are received before closing the stream. Only invoked + * when server sends trailers, which it may not. May be invoked while there is read data + * remaining in local buffer. + * + * Default implementation takes no action. + * + * @param stream the stream on which response trailers were received + * @param info the response information + * @param trailers the trailers received + */ + public void onResponseTrailersReceived(BidirectionalStream stream, UrlResponseInfo info, + UrlResponseInfo.HeaderBlock trailers) {} + + /** + * Invoked when there is no data to be read or written and the stream is closed successfully + * remotely and locally. Once invoked, no further {@link BidirectionalStream.Callback} + * methods will be invoked. + * + * @param stream the stream which is closed successfully + * @param info the response information + */ + public abstract void onSucceeded(BidirectionalStream stream, UrlResponseInfo info); + + /** + * Invoked if the stream failed for any reason after {@link BidirectionalStream#start}. + * HTTP/2 error codes are + * mapped to {@link UrlRequestException#getCronetInternalErrorCode} codes. Once invoked, + * no further {@link BidirectionalStream.Callback} methods will be invoked. + * + * @param stream the stream which has failed + * @param info the response information. May be {@code null} if no response was + * received. + * @param error information about the failure + */ + public abstract void onFailed( + BidirectionalStream stream, UrlResponseInfo info, CronetException error); + + /** + * Invoked if the stream was canceled via {@link BidirectionalStream#cancel}. Once + * invoked, no further {@link BidirectionalStream.Callback} methods will be invoked. + * Default implementation takes no action. + * + * @param stream the stream that was canceled + * @param info the response information. May be {@code null} if no response was + * received. + */ + public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) {} + } + + /** + * Starts the stream, all callbacks go to the {@code callback} argument passed to {@link + * BidirectionalStream.Builder}'s constructor. Should only be called once. + */ + public abstract void start(); + + /** + * Reads data from the stream into the provided buffer. + * Can only be called at most once in response to each invocation of the + * {@link Callback#onStreamReady onStreamReady()}/ + * {@link Callback#onResponseHeadersReceived onResponseHeadersReceived()} and {@link + * Callback#onReadCompleted onReadCompleted()} methods of the {@link + * Callback}. Each call will result in an invocation of one of the + * {@link Callback Callback}'s {@link Callback#onReadCompleted onReadCompleted()} + * method if data is read, or its {@link Callback#onFailed onFailed()} method if + * there's an error. + * + * An attempt to read data into {@code buffer} starting at {@code + * buffer.position()} is begun. At most {@code buffer.remaining()} bytes are + * read. {@code buffer.position()} is updated upon invocation of {@link + * Callback#onReadCompleted onReadCompleted()} to indicate how much data was read. + * + * @param buffer the {@link ByteBuffer} to read data into. Must be a + * direct ByteBuffer. The embedder must not read or modify buffer's + * position, limit, or data between its position and limit until + * {@link Callback#onReadCompleted onReadCompleted()}, {@link Callback#onCanceled + * onCanceled()}, or {@link Callback#onFailed onFailed()} are invoked. + */ + public abstract void read(ByteBuffer buffer); + + /** + * Attempts to write data from the provided buffer into the stream. + * If auto flush is disabled, data will be sent only after {@link #flush flush()} is called. + * Each call will result in an invocation of one of the + * {@link Callback Callback}'s {@link Callback#onWriteCompleted onWriteCompleted()} + * method if data is sent, or its {@link Callback#onFailed onFailed()} method if + * there's an error. + * + * An attempt to write data from {@code buffer} starting at {@code buffer.position()} + * is begun. {@code buffer.remaining()} bytes will be written. + * {@link Callback#onWriteCompleted onWriteCompleted()} will be invoked only when the + * full ByteBuffer is written. + * + * @param buffer the {@link ByteBuffer} to write data from. Must be a + * direct ByteBuffer. The embedder must not read or modify buffer's + * position, limit, or data between its position and limit until + * {@link Callback#onWriteCompleted onWriteCompleted()}, {@link Callback#onCanceled + * onCanceled()}, or {@link Callback#onFailed onFailed()} are invoked. Can be empty + * when {@code endOfStream} is {@code true}. + * @param endOfStream if {@code true}, then {@code buffer} is the last buffer to be written, + * and once written, stream is closed from the client side, resulting in half-closed + * stream or a fully closed stream if the remote side has already closed. + */ + public abstract void write(ByteBuffer buffer, boolean endOfStream); + + /** + * Flushes pending writes. This method should not be invoked before {@link + * Callback#onStreamReady onStreamReady()}. For previously delayed {@link + * #write write()}s, a corresponding {@link Callback#onWriteCompleted onWriteCompleted()} + * will be invoked when the buffer is sent. + */ + public abstract void flush(); + + /** + * Cancels the stream. Can be called at any time after {@link #start}. + * {@link Callback#onCanceled onCanceled()} will be invoked when cancelation + * is complete and no further callback methods will be invoked. If the + * stream has completed or has not started, calling {@code cancel()} has no + * effect and {@code onCanceled()} will not be invoked. If the + * {@link Executor} passed in during {@code BidirectionalStream} construction runs + * tasks on a single thread, and {@code cancel()} is called on that thread, + * no listener methods (besides {@code onCanceled()}) will be invoked after + * {@code cancel()} is called. Otherwise, at most one callback method may be + * invoked after {@code cancel()} has completed. + */ + public abstract void cancel(); + + /** + * Returns {@code true} if the stream was successfully started and is now + * done (succeeded, canceled, or failed). + * + * @return {@code true} if the stream was successfully started and is now + * done (completed, canceled, or failed), otherwise returns {@code false} + * to indicate stream is not yet started or is in progress. + */ + public abstract boolean isDone(); +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/CallbackException.java b/src/components/cronet/android/api/src/org/chromium/net/CallbackException.java new file mode 100644 index 0000000000..0e1fa61ea9 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/CallbackException.java @@ -0,0 +1,24 @@ +// Copyright 2016 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; + +/** + * Exception passed to {@link UrlRequest.Callback#onFailed UrlRequest.Callback.onFailed()} when + * {@link UrlRequest.Callback} or {@link UploadDataProvider} method throws an exception. In this + * case {@link java.io.IOException#getCause getCause()} can be used to find the thrown + * exception. + */ +public abstract class CallbackException extends CronetException { + /** + * Constructs an exception that wraps {@code cause} thrown by a {@link UrlRequest.Callback}. + * + * @param message explanation of failure. + * @param cause exception thrown by {@link UrlRequest.Callback} that's being wrapped. It is + * saved for later retrieval by the {@link java.io.IOException#getCause getCause()}. + */ + protected CallbackException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/CronetEngine.java b/src/components/cronet/android/api/src/org/chromium/net/CronetEngine.java new file mode 100644 index 0000000000..b772052456 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/CronetEngine.java @@ -0,0 +1,545 @@ +// 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.net.http.HttpResponseCache; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandlerFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; + +import javax.net.ssl.HttpsURLConnection; +/** + * An engine to process {@link UrlRequest}s, which uses the best HTTP stack + * available on the current platform. An instance of this class can be created + * using {@link Builder}. + */ +public abstract class CronetEngine { + private static final String TAG = CronetEngine.class.getSimpleName(); + + /** + * A builder for {@link CronetEngine}s, which allows runtime configuration of + * {@code CronetEngine}. Configuration options are set on the builder and + * then {@link #build} is called to create the {@code CronetEngine}. + */ + // NOTE(kapishnikov): In order to avoid breaking the existing API clients, all future methods + // added to this class and other API classes must have default implementation. + public static class Builder { + /** + * A class which provides a method for loading the cronet native library. Apps needing to + * implement custom library loading logic can inherit from this class and pass an instance + * to {@link CronetEngine.Builder#setLibraryLoader}. For example, this might be required + * to work around {@code UnsatisfiedLinkError}s caused by flaky installation on certain + * older devices. + */ + public abstract static class LibraryLoader { + /** + * Loads the native library. + * @param libName name of the library to load + */ + public abstract void loadLibrary(String libName); + } + + /** + * Reference to the actual builder implementation. + * {@hide exclude from JavaDoc}. + */ + protected final ICronetEngineBuilder mBuilderDelegate; + + /** + * Constructs a {@link Builder} object that facilitates creating a + * {@link CronetEngine}. The default configuration enables HTTP/2 and + * QUIC, but disables the HTTP cache. + * + * @param context Android {@link Context}, which is used by + * {@link Builder} to retrieve the application + * context. A reference to only the application + * context will be kept, so as to avoid extending + * the lifetime of {@code context} unnecessarily. + */ + public Builder(Context context) { + this(createBuilderDelegate(context)); + } + + /** + * Constructs {@link Builder} with a given delegate that provides the actual implementation + * of the {@code Builder} methods. This constructor is used only by the internal + * implementation. + * + * @param builderDelegate delegate that provides the actual implementation. + * + * {@hide} + */ + public Builder(ICronetEngineBuilder builderDelegate) { + mBuilderDelegate = builderDelegate; + } + + /** + * Constructs a User-Agent string including application name and version, + * system build version, model and id, and Cronet version. + * + * @return User-Agent string. + */ + public String getDefaultUserAgent() { + return mBuilderDelegate.getDefaultUserAgent(); + } + + /** + * Overrides the User-Agent header for all requests. An explicitly + * set User-Agent header (set using + * {@link UrlRequest.Builder#addHeader}) will override a value set + * using this function. + * + * @param userAgent the User-Agent string to use for all requests. + * @return the builder to facilitate chaining. + */ + public Builder setUserAgent(String userAgent) { + mBuilderDelegate.setUserAgent(userAgent); + return this; + } + + /** + * Sets directory for HTTP Cache and Cookie Storage. The directory must + * exist. + *

+ * NOTE: Do not use the same storage directory with more than one + * {@code CronetEngine} at a time. Access to the storage directory does + * not support concurrent access by multiple {@code CronetEngine}s. + * + * @param value path to existing directory. + * @return the builder to facilitate chaining. + */ + public Builder setStoragePath(String value) { + mBuilderDelegate.setStoragePath(value); + return this; + } + + /** + * Sets a {@link LibraryLoader} to be used to load the native library. + * If not set, the library will be loaded using {@link System#loadLibrary}. + * @param loader {@code LibraryLoader} to be used to load the native library. + * @return the builder to facilitate chaining. + */ + public Builder setLibraryLoader(LibraryLoader loader) { + mBuilderDelegate.setLibraryLoader(loader); + return this; + } + + /** + * Sets whether QUIC protocol + * is enabled. Defaults to enabled. If QUIC is enabled, then QUIC User Agent Id + * containing application name and Cronet version is sent to the server. + * @param value {@code true} to enable QUIC, {@code false} to disable. + * @return the builder to facilitate chaining. + */ + public Builder enableQuic(boolean value) { + mBuilderDelegate.enableQuic(value); + return this; + } + + /** + * Sets whether HTTP/2 + * protocol is enabled. Defaults to enabled. + * @param value {@code true} to enable HTTP/2, {@code false} to disable. + * @return the builder to facilitate chaining. + */ + public Builder enableHttp2(boolean value) { + mBuilderDelegate.enableHttp2(value); + return this; + } + + /** + * @deprecated SDCH is deprecated in Cronet M63. This method is a no-op. + * {@hide exclude from JavaDoc}. + */ + @Deprecated + public Builder enableSdch(boolean value) { + return this; + } + + /** + * Sets whether Brotli compression is + * enabled. If enabled, Brotli will be advertised in Accept-Encoding request headers. + * Defaults to disabled. + * @param value {@code true} to enable Brotli, {@code false} to disable. + * @return the builder to facilitate chaining. + */ + public Builder enableBrotli(boolean value) { + mBuilderDelegate.enableBrotli(value); + return this; + } + + /** + * Setting to disable HTTP cache. Some data may still be temporarily stored in memory. + * Passed to {@link #enableHttpCache}. + */ + public static final int HTTP_CACHE_DISABLED = 0; + + /** + * Setting to enable in-memory HTTP cache, including HTTP data. + * Passed to {@link #enableHttpCache}. + */ + public static final int HTTP_CACHE_IN_MEMORY = 1; + + /** + * Setting to enable on-disk cache, excluding HTTP data. + * {@link #setStoragePath} must be called prior to passing this constant to + * {@link #enableHttpCache}. + */ + public static final int HTTP_CACHE_DISK_NO_HTTP = 2; + + /** + * Setting to enable on-disk cache, including HTTP data. + * {@link #setStoragePath} must be called prior to passing this constant to + * {@link #enableHttpCache}. + */ + public static final int HTTP_CACHE_DISK = 3; + + /** + * Enables or disables caching of HTTP data and other information like QUIC + * server information. + * @param cacheMode control location and type of cached data. Must be one of + * {@link #HTTP_CACHE_DISABLED HTTP_CACHE_*}. + * @param maxSize maximum size in bytes used to cache data (advisory and maybe + * exceeded at times). + * @return the builder to facilitate chaining. + */ + public Builder enableHttpCache(int cacheMode, long maxSize) { + mBuilderDelegate.enableHttpCache(cacheMode, maxSize); + return this; + } + + /** + * Adds hint that {@code host} supports QUIC. + * Note that {@link #enableHttpCache enableHttpCache} + * ({@link #HTTP_CACHE_DISK}) is needed to take advantage of 0-RTT + * connection establishment between sessions. + * + * @param host hostname of the server that supports QUIC. + * @param port host of the server that supports QUIC. + * @param alternatePort alternate port to use for QUIC. + * @return the builder to facilitate chaining. + */ + public Builder addQuicHint(String host, int port, int alternatePort) { + mBuilderDelegate.addQuicHint(host, port, alternatePort); + return this; + } + + /** + *

+ * Pins a set of public keys for a given host. By pinning a set of public keys, + * {@code pinsSha256}, communication with {@code hostName} is required to + * authenticate with a certificate with a public key from the set of pinned ones. + * An app can pin the public key of the root certificate, any of the intermediate + * certificates or the end-entry certificate. Authentication will fail and secure + * communication will not be established if none of the public keys is present in the + * host's certificate chain, even if the host attempts to authenticate with a + * certificate allowed by the device's trusted store of certificates. + *

+ *

+ * Calling this method multiple times with the same host name overrides the previously + * set pins for the host. + *

+ *

+ * More information about the public key pinning can be found in + * RFC 7469. + *

+ * + * @param hostName name of the host to which the public keys should be pinned. A host that + * consists only of digits and the dot character is treated as invalid. + * @param pinsSha256 a set of pins. Each pin is the SHA-256 cryptographic + * hash of the DER-encoded ASN.1 representation of the Subject Public + * Key Info (SPKI) of the host's X.509 certificate. Use + * {@link java.security.cert.Certificate#getPublicKey() + * Certificate.getPublicKey()} and + * {@link java.security.Key#getEncoded() Key.getEncoded()} + * to obtain DER-encoded ASN.1 representation of the SPKI. + * Although, the method does not mandate the presence of the backup pin + * that can be used if the control of the primary private key has been + * lost, it is highly recommended to supply one. + * @param includeSubdomains indicates whether the pinning policy should be applied to + * subdomains of {@code hostName}. + * @param expirationDate specifies the expiration date for the pins. + * @return the builder to facilitate chaining. + * @throws NullPointerException if any of the input parameters are {@code null}. + * @throws IllegalArgumentException if the given host name is invalid or {@code pinsSha256} + * contains a byte array that does not represent a valid + * SHA-256 hash. + */ + public Builder addPublicKeyPins(String hostName, Set pinsSha256, + boolean includeSubdomains, Date expirationDate) { + mBuilderDelegate.addPublicKeyPins( + hostName, pinsSha256, includeSubdomains, expirationDate); + return this; + } + + /** + * Enables or disables public key pinning bypass for local trust anchors. Disabling the + * bypass for local trust anchors is highly discouraged since it may prohibit the app + * from communicating with the pinned hosts. E.g., a user may want to send all traffic + * through an SSL enabled proxy by changing the device proxy settings and adding the + * proxy certificate to the list of local trust anchor. Disabling the bypass will most + * likly prevent the app from sending any traffic to the pinned hosts. For more + * information see 'How does key pinning interact with local proxies and filters?' at + * https://www.chromium.org/Home/chromium-security/security-faq + * + * @param value {@code true} to enable the bypass, {@code false} to disable. + * @return the builder to facilitate chaining. + */ + public Builder enablePublicKeyPinningBypassForLocalTrustAnchors(boolean value) { + mBuilderDelegate.enablePublicKeyPinningBypassForLocalTrustAnchors(value); + return this; + } + + /** + * Build a {@link CronetEngine} using this builder's configuration. + * @return constructed {@link CronetEngine}. + */ + public CronetEngine build() { + return mBuilderDelegate.build(); + } + + /** + * Creates an implementation of {@link ICronetEngineBuilder} that can be used + * to delegate the builder calls to. The method uses {@link CronetProvider} + * to obtain the list of available providers. + * + * @param context Android Context to use. + * @return the created {@code ICronetEngineBuilder}. + */ + private static ICronetEngineBuilder createBuilderDelegate(Context context) { + List providers = + new ArrayList<>(CronetProvider.getAllProviders(context)); + CronetProvider provider = getEnabledCronetProviders(context, providers).get(0); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, + String.format("Using '%s' provider for creating CronetEngine.Builder.", + provider)); + } + return provider.createBuilder().mBuilderDelegate; + } + + /** + * Returns the list of available and enabled {@link CronetProvider}. The returned list + * is sorted based on the provider versions and types. + * + * @param context Android Context to use. + * @param providers the list of enabled and disabled providers to filter out and sort. + * @return the sorted list of enabled providers. The list contains at least one provider. + * @throws RuntimeException is the list of providers is empty or all of the providers + * are disabled. + */ + @VisibleForTesting + static List getEnabledCronetProviders( + Context context, List providers) { + // Check that there is at least one available provider. + if (providers.size() == 0) { + throw new RuntimeException("Unable to find any Cronet provider." + + " Have you included all necessary jars?"); + } + + // Exclude disabled providers from the list. + for (Iterator i = providers.iterator(); i.hasNext();) { + CronetProvider provider = i.next(); + if (!provider.isEnabled()) { + i.remove(); + } + } + + // Check that there is at least one enabled provider. + if (providers.size() == 0) { + throw new RuntimeException("All available Cronet providers are disabled." + + " A provider should be enabled before it can be used."); + } + + // Sort providers based on version and type. + Collections.sort(providers, new Comparator() { + @Override + public int compare(CronetProvider p1, CronetProvider p2) { + // The fallback provider should always be at the end of the list. + if (CronetProvider.PROVIDER_NAME_FALLBACK.equals(p1.getName())) { + return 1; + } + if (CronetProvider.PROVIDER_NAME_FALLBACK.equals(p2.getName())) { + return -1; + } + // A provider with higher version should go first. + return -compareVersions(p1.getVersion(), p2.getVersion()); + } + }); + return providers; + } + + /** + * Compares two strings that contain versions. The string should only contain + * dot-separated segments that contain an arbitrary number of digits digits [0-9]. + * + * @param s1 the first string. + * @param s2 the second string. + * @return -1 if s1s2 and 0 if s1=s2. If two versions are equal, the + * version with the higher number of segments is considered to be higher. + * + * @throws IllegalArgumentException if any of the strings contains an illegal + * version number. + */ + @VisibleForTesting + static int compareVersions(String s1, String s2) { + if (s1 == null || s2 == null) { + throw new IllegalArgumentException("The input values cannot be null"); + } + String[] s1segments = s1.split("\\."); + String[] s2segments = s2.split("\\."); + for (int i = 0; i < s1segments.length && i < s2segments.length; i++) { + try { + int s1segment = Integer.parseInt(s1segments[i]); + int s2segment = Integer.parseInt(s2segments[i]); + if (s1segment != s2segment) { + return Integer.signum(s1segment - s2segment); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Unable to convert version segments into" + + " integers: " + s1segments[i] + " & " + s2segments[i], + e); + } + } + return Integer.signum(s1segments.length - s2segments.length); + } + } + + /** + * @return a human-readable version string of the engine. + */ + public abstract String getVersionString(); + + /** + * Shuts down the {@link CronetEngine} if there are no active requests, + * otherwise throws an exception. + * + * Cannot be called on network thread - the thread Cronet calls into + * Executor on (which is different from the thread the Executor invokes + * callbacks on). May block until all the {@code CronetEngine}'s + * resources have been cleaned up. + */ + public abstract void shutdown(); + + /** + * Starts NetLog logging to a file. The NetLog will contain events emitted + * by all live CronetEngines. The NetLog is useful for debugging. + * The file can be viewed using a Chrome browser navigated to + * chrome://net-internals/#import + * @param fileName the complete file path. It must not be empty. If the file + * exists, it is truncated before starting. If actively logging, + * this method is ignored. + * @param logAll {@code true} to include basic events, user cookies, + * credentials and all transferred bytes in the log. This option presents + * a privacy risk, since it exposes the user's credentials, and should + * only be used with the user's consent and in situations where the log + * won't be public. + * {@code false} to just include basic events. + */ + public abstract void startNetLogToFile(String fileName, boolean logAll); + + /** + * Stops NetLog logging and flushes file to disk. If a logging session is + * not in progress, this call is ignored. + */ + public abstract void stopNetLog(); + + /** + * Returns differences in metrics collected by Cronet since the last call to + * this method. + *

+ * Cronet collects these metrics globally. This means deltas returned by + * {@code getGlobalMetricsDeltas()} will include measurements of requests + * processed by other {@link CronetEngine} instances. Since this function + * returns differences in metrics collected since the last call, and these + * metrics are collected globally, a call to any {@code CronetEngine} + * instance's {@code getGlobalMetricsDeltas()} method will affect the deltas + * returned by any other {@code CronetEngine} instance's + * {@code getGlobalMetricsDeltas()}. + *

+ * Cronet starts collecting these metrics after the first call to + * {@code getGlobalMetricsDeltras()}, so the first call returns no + * useful data as no metrics have yet been collected. + * + * @return differences in metrics collected by Cronet, since the last call + * to {@code getGlobalMetricsDeltas()}, serialized as a + * protobuf + * . + */ + public abstract byte[] getGlobalMetricsDeltas(); + + /** + * Establishes a new connection to the resource specified by the {@link URL} {@code url}. + *

+ * Note: Cronet's {@link java.net.HttpURLConnection} implementation is subject to certain + * limitations, see {@link #createURLStreamHandlerFactory} for details. + * + * @param url URL of resource to connect to. + * @return an {@link java.net.HttpURLConnection} instance implemented by this CronetEngine. + * @throws IOException if an error occurs while opening the connection. + */ + public abstract URLConnection openConnection(URL url) throws IOException; + + /** + * Creates a {@link URLStreamHandlerFactory} to handle HTTP and HTTPS + * traffic. An instance of this class can be installed via + * {@link URL#setURLStreamHandlerFactory} thus using this CronetEngine by default for + * all requests created via {@link URL#openConnection}. + *

+ * Cronet does not use certain HTTP features provided via the system: + *

    + *
  • the HTTP cache installed via + * {@link HttpResponseCache#install(java.io.File, long) HttpResponseCache.install()}
  • + *
  • the HTTP authentication method installed via + * {@link java.net.Authenticator#setDefault}
  • + *
  • the HTTP cookie storage installed via {@link java.net.CookieHandler#setDefault}
  • + *
+ *

+ * While Cronet supports and encourages requests using the HTTPS protocol, + * Cronet does not provide support for the + * {@link HttpsURLConnection} API. This lack of support also + * includes not using certain HTTPS features provided via the system: + *

    + *
  • the HTTPS hostname verifier installed via {@link + * HttpsURLConnection#setDefaultHostnameVerifier(javax.net.ssl.HostnameVerifier) + * HttpsURLConnection.setDefaultHostnameVerifier()}
  • + *
  • the HTTPS socket factory installed via {@link + * HttpsURLConnection#setDefaultSSLSocketFactory(javax.net.ssl.SSLSocketFactory) + * HttpsURLConnection.setDefaultSSLSocketFactory()}
  • + *
+ * + * @return an {@link URLStreamHandlerFactory} instance implemented by this + * CronetEngine. + */ + public abstract URLStreamHandlerFactory createURLStreamHandlerFactory(); + + /** + * Creates a builder for {@link UrlRequest}. All callbacks for + * generated {@link UrlRequest} objects will be invoked on + * {@code executor}'s threads. {@code executor} must not run tasks on the + * thread calling {@link Executor#execute} to prevent blocking networking + * operations and causing exceptions during shutdown. + * + * @param url URL for the generated requests. + * @param callback callback object that gets invoked on different events. + * @param executor {@link Executor} on which all callbacks will be invoked. + */ + public abstract UrlRequest.Builder newUrlRequestBuilder( + String url, UrlRequest.Callback callback, Executor executor); +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/CronetException.java b/src/components/cronet/android/api/src/org/chromium/net/CronetException.java new file mode 100644 index 0000000000..d21a424ac0 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/CronetException.java @@ -0,0 +1,24 @@ +// 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 java.io.IOException; + +/** + * Base exception passed to {@link UrlRequest.Callback#onFailed UrlRequest.Callback.onFailed()}. + */ +public abstract class CronetException extends IOException { + /** + * Constructs an exception that is caused by {@code cause}. + * + * @param message explanation of failure. + * @param cause the cause (which is saved for later retrieval by the {@link + * java.io.IOException#getCause getCause()} method). A null value is permitted, and + * indicates that the cause is nonexistent or unknown. + */ + protected CronetException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/CronetProvider.java b/src/components/cronet/android/api/src/org/chromium/net/CronetProvider.java new file mode 100644 index 0000000000..c302e647b6 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/CronetProvider.java @@ -0,0 +1,246 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net; + +import android.content.Context; +import android.util.Log; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Provides a factory method to create {@link CronetEngine.Builder} instances. + * A {@code CronetEngine.Builder} instance can be used to create a specific {@link CronetEngine} + * implementation. To get the list of available {@link CronetProvider}s call + * {@link #getAllProviders(Context)}. + *

+ * NOTE: This class is for advanced users that want to select a particular + * Cronet implementation. Most users should simply use {@code new} {@link + * CronetEngine.Builder#CronetEngine.Builder(android.content.Context)}. + * + * {@hide} + */ +public abstract class CronetProvider { + /** + * String returned by {@link CronetProvider#getName} for {@link CronetProvider} + * that provides native Cronet implementation packaged inside an application. + * This implementation offers significantly higher performance relative to the + * fallback Cronet implementations (see {@link #PROVIDER_NAME_FALLBACK}). + */ + public static final String PROVIDER_NAME_APP_PACKAGED = "App-Packaged-Cronet-Provider"; + + /** + * String returned by {@link CronetProvider#getName} for {@link CronetProvider} + * that provides Cronet implementation based on the system's + * {@link java.net.HttpURLConnection} implementation. This implementation + * offers significantly degraded performance relative to native Cronet + * implementations (see {@link #PROVIDER_NAME_APP_PACKAGED}). + */ + public static final String PROVIDER_NAME_FALLBACK = "Fallback-Cronet-Provider"; + + /** + * The name of an optional key in the app string resource file that contains the class name of + * an alternative {@code CronetProvider} implementation. + */ + private static final String RES_KEY_CRONET_IMPL_CLASS = "CronetProviderClassName"; + + private static final String TAG = CronetProvider.class.getSimpleName(); + + protected final Context mContext; + + protected CronetProvider(Context context) { + if (context == null) { + throw new IllegalArgumentException("Context must not be null"); + } + mContext = context; + } + + /** + * Creates and returns an instance of {@link CronetEngine.Builder}. + *

+ * NOTE: This class is for advanced users that want to select a particular + * Cronet implementation. Most users should simply use {@code new} {@link + * CronetEngine.Builder#CronetEngine.Builder(android.content.Context)}. + * + * @return {@code CronetEngine.Builder}. + * @throws IllegalStateException if the provider is not enabled (see {@link #isEnabled}. + */ + public abstract CronetEngine.Builder createBuilder(); + + /** + * Returns the provider name. The well-know provider names include: + *

    + *
  • {@link #PROVIDER_NAME_APP_PACKAGED}
  • + *
  • {@link #PROVIDER_NAME_FALLBACK}
  • + *
+ * + * @return provider name. + */ + public abstract String getName(); + + /** + * Returns the provider version. The version can be used to select the newest + * available provider if multiple providers are available. + * + * @return provider version. + */ + public abstract String getVersion(); + + /** + * Returns whether the provider is enabled and can be used to instantiate the Cronet engine. + * A provider being out-of-date (older than the API) and needing updating is one potential + * reason it could be disabled. Please read the provider documentation for + * enablement procedure. + * + * @return {@code true} if the provider is enabled. + */ + public abstract boolean isEnabled(); + + @Override + public String toString() { + return "[" + + "class=" + getClass().getName() + ", " + + "name=" + getName() + ", " + + "version=" + getVersion() + ", " + + "enabled=" + isEnabled() + "]"; + } + + /** + * Name of the Java {@link CronetProvider} class. + */ + private static final String JAVA_CRONET_PROVIDER_CLASS = + "org.chromium.net.impl.JavaCronetProvider"; + + /** + * Name of the native {@link CronetProvider} class. + */ + private static final String NATIVE_CRONET_PROVIDER_CLASS = + "org.chromium.net.impl.NativeCronetProvider"; + + /** + * {@link CronetProvider} class that is packaged with Google Play Services. + */ + private static final String PLAY_SERVICES_CRONET_PROVIDER_CLASS = + "com.google.android.gms.net.PlayServicesCronetProvider"; + + /** + * {@link CronetProvider} a deprecated class that may be packaged with + * some old versions of Google Play Services. + */ + private static final String GMS_CORE_CRONET_PROVIDER_CLASS = + "com.google.android.gms.net.GmsCoreCronetProvider"; + + /** + * Returns an unmodifiable list of all available {@link CronetProvider}s. + * The providers are returned in no particular order. Some of the returned + * providers may be in a disabled state and should be enabled by the invoker. + * See {@link CronetProvider#isEnabled()}. + * + * @return the list of available providers. + */ + public static List getAllProviders(Context context) { + // Use LinkedHashSet to preserve the order and eliminate duplicate providers. + Set providers = new LinkedHashSet<>(); + addCronetProviderFromResourceFile(context, providers); + addCronetProviderImplByClassName( + context, PLAY_SERVICES_CRONET_PROVIDER_CLASS, providers, false); + addCronetProviderImplByClassName(context, GMS_CORE_CRONET_PROVIDER_CLASS, providers, false); + addCronetProviderImplByClassName(context, NATIVE_CRONET_PROVIDER_CLASS, providers, false); + addCronetProviderImplByClassName(context, JAVA_CRONET_PROVIDER_CLASS, providers, false); + return Collections.unmodifiableList(new ArrayList<>(providers)); + } + + /** + * Attempts to add a new provider referenced by the class name to a set. + * + * @param className the class name of the provider that should be instantiated. + * @param providers the set of providers to add the new provider to. + * @return {@code true} if the provider was added to the set; {@code false} + * if the provider couldn't be instantiated. + */ + private static boolean addCronetProviderImplByClassName( + Context context, String className, Set providers, boolean logError) { + ClassLoader loader = context.getClassLoader(); + try { + Class providerClass = + loader.loadClass(className).asSubclass(CronetProvider.class); + Constructor ctor = + providerClass.getConstructor(Context.class); + providers.add(ctor.newInstance(context)); + return true; + } catch (InstantiationException e) { + logReflectiveOperationException(className, logError, e); + } catch (InvocationTargetException e) { + logReflectiveOperationException(className, logError, e); + } catch (NoSuchMethodException e) { + logReflectiveOperationException(className, logError, e); + } catch (IllegalAccessException e) { + logReflectiveOperationException(className, logError, e); + } catch (ClassNotFoundException e) { + logReflectiveOperationException(className, logError, e); + } + return false; + } + + /** + * De-duplicates exception handling logic in {@link #addCronetProviderImplByClassName}. + * It should be removed when support of API Levels lower than 19 is deprecated. + */ + private static void logReflectiveOperationException( + String className, boolean logError, Exception e) { + if (logError) { + Log.e(TAG, "Unable to load provider class: " + className, e); + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, + "Tried to load " + className + " provider class but it wasn't" + + " included in the app classpath"); + } + } + } + + /** + * Attempts to add a provider specified in the app resource file to a set. + * + * @param providers the set of providers to add the new provider to. + * @return {@code true} if the provider was added to the set; {@code false} + * if the app resources do not include the string with + * {@link #RES_KEY_CRONET_IMPL_CLASS} key. + * @throws RuntimeException if the provider cannot be found or instantiated. + */ + private static boolean addCronetProviderFromResourceFile( + Context context, Set providers) { + int resId = context.getResources().getIdentifier( + RES_KEY_CRONET_IMPL_CLASS, "string", context.getPackageName()); + // Resource not found + if (resId == 0) { + // The resource wasn't included in the app; therefore, there is nothing to add. + return false; + } + String className = context.getResources().getString(resId); + + // If the resource specifies a well known provider, don't load it because + // there will be an attempt to load it anyways. + if (className == null || className.equals(PLAY_SERVICES_CRONET_PROVIDER_CLASS) + || className.equals(GMS_CORE_CRONET_PROVIDER_CLASS) + || className.equals(JAVA_CRONET_PROVIDER_CLASS) + || className.equals(NATIVE_CRONET_PROVIDER_CLASS)) { + return false; + } + + if (!addCronetProviderImplByClassName(context, className, providers, true)) { + Log.e(TAG, + "Unable to instantiate Cronet implementation class " + className + + " that is listed as in the app string resource file under " + + RES_KEY_CRONET_IMPL_CLASS + " key"); + } + return true; + } +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/ExperimentalBidirectionalStream.java b/src/components/cronet/android/api/src/org/chromium/net/ExperimentalBidirectionalStream.java new file mode 100644 index 0000000000..d2c450dfed --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/ExperimentalBidirectionalStream.java @@ -0,0 +1,101 @@ +// Copyright 2016 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; + +/** + * {@link BidirectionalStream} that exposes experimental features. To obtain an + * instance of this class, cast a {@code BidirectionalStream} to this type. Every + * instance of {@code BidirectionalStream} can be cast to an instance of this class, + * as they are backed by the same implementation and hence perform identically. + * Instances of this class are not meant for general use, but instead only + * to access experimental features. Experimental features may be deprecated in the + * future. Use at your own risk. + * + * {@hide prototype} + */ +public abstract class ExperimentalBidirectionalStream extends BidirectionalStream { + /** + * {@link BidirectionalStream#Builder} that exposes experimental features. To obtain an + * instance of this class, cast a {@code BidirectionalStream.Builder} to this type. Every + * instance of {@code BidirectionalStream.Builder} can be cast to an instance of this class, + * as they are backed by the same implementation and hence perform identically. + * Instances of this class are not meant for general use, but instead only + * to access experimental features. Experimental features may be deprecated in the + * future. Use at your own risk. + */ + public abstract static class Builder extends BidirectionalStream.Builder { + /** + * Associates the annotation object with this request. May add more than one. + * Passed through to a {@link RequestFinishedInfo.Listener}, + * see {@link RequestFinishedInfo#getAnnotations}. + * + * @param annotation an object to pass on to the {@link RequestFinishedInfo.Listener} with a + * {@link RequestFinishedInfo}. + * @return the builder to facilitate chaining. + */ + public Builder addRequestAnnotation(Object annotation) { + return this; + } + + /** + * Sets {@link android.net.TrafficStats} tag to use when accounting socket traffic caused by + * this request. See {@link android.net.TrafficStats} for more information. If no tag is + * set (e.g. this method isn't called), then Android accounts for the socket traffic caused + * by this request as if the tag value were set to 0. + *

+ * NOTE:Setting a tag disallows sharing of sockets with requests + * with other tags, which may adversely effect performance by prohibiting + * connection sharing. In other words use of multiplexed sockets (e.g. HTTP/2 + * and QUIC) will only be allowed if all requests have the same socket tag. + * + * @param tag the tag value used to when accounting for socket traffic caused by this + * request. Tags between 0xFFFFFF00 and 0xFFFFFFFF are reserved and used + * internally by system services like {@link android.app.DownloadManager} when + * performing traffic on behalf of an application. + * @return the builder to facilitate chaining. + */ + public Builder setTrafficStatsTag(int tag) { + return this; + } + + /** + * Sets specific UID to use when accounting socket traffic caused by this request. See + * {@link android.net.TrafficStats} for more information. Designed for use when performing + * an operation on behalf of another application. Caller must hold + * {@link android.Manifest.permission#MODIFY_NETWORK_ACCOUNTING} permission. By default + * traffic is attributed to UID of caller. + *

+ * NOTE:Setting a UID disallows sharing of sockets with requests + * with other UIDs, which may adversely effect performance by prohibiting + * connection sharing. In other words use of multiplexed sockets (e.g. HTTP/2 + * and QUIC) will only be allowed if all requests have the same UID set. + * + * @param uid the UID to attribute socket traffic caused by this request. + * @return the builder to facilitate chaining. + */ + public Builder setTrafficStatsUid(int uid) { + return this; + } + + // To support method chaining, override superclass methods to return an + // instance of this class instead of the parent. + + @Override + public abstract Builder setHttpMethod(String method); + + @Override + public abstract Builder addHeader(String header, String value); + + @Override + public abstract Builder setPriority(int priority); + + @Override + public abstract Builder delayRequestHeadersUntilFirstFlush( + boolean delayRequestHeadersUntilFirstFlush); + + @Override + public abstract ExperimentalBidirectionalStream build(); + } +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/ExperimentalCronetEngine.java b/src/components/cronet/android/api/src/org/chromium/net/ExperimentalCronetEngine.java new file mode 100644 index 0000000000..3505e0cf49 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/ExperimentalCronetEngine.java @@ -0,0 +1,413 @@ +// Copyright 2016 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 androidx.annotation.VisibleForTesting; + +import java.io.IOException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.util.Date; +import java.util.Set; +import java.util.concurrent.Executor; + +/** + * {@link CronetEngine} that exposes experimental features. To obtain an + * instance of this class, cast a {@code CronetEngine} to this type. Every + * instance of {@code CronetEngine} can be cast to an instance of this class, + * as they are backed by the same implementation and hence perform identically. + * Instances of this class are not meant for general use, but instead only + * to access experimental features. Experimental features may be deprecated in the + * future. Use at your own risk. + * + * {@hide since this class exposes experimental features that should be hidden.} + */ +public abstract class ExperimentalCronetEngine extends CronetEngine { + /** + * The value of a connection metric is unknown. + */ + public static final int CONNECTION_METRIC_UNKNOWN = -1; + + /** + * The estimate of the effective connection type is unknown. + * + * @see #getEffectiveConnectionType + */ + public static final int EFFECTIVE_CONNECTION_TYPE_UNKNOWN = 0; + + /** + * The device is offline. + * + * @see #getEffectiveConnectionType + */ + public static final int EFFECTIVE_CONNECTION_TYPE_OFFLINE = 1; + + /** + * The estimate of the effective connection type is slow 2G. + * + * @see #getEffectiveConnectionType + */ + public static final int EFFECTIVE_CONNECTION_TYPE_SLOW_2G = 2; + + /** + * The estimate of the effective connection type is 2G. + * + * @see #getEffectiveConnectionType + */ + public static final int EFFECTIVE_CONNECTION_TYPE_2G = 3; + + /** + * The estimate of the effective connection type is 3G. + * + * @see #getEffectiveConnectionType + */ + public static final int EFFECTIVE_CONNECTION_TYPE_3G = 4; + + /** + * The estimate of the effective connection type is 4G. + * + * @see #getEffectiveConnectionType + */ + public static final int EFFECTIVE_CONNECTION_TYPE_4G = 5; + + /** + * A version of {@link CronetEngine.Builder} that exposes experimental + * features. Instances of this class are not meant for general use, but + * instead only to access experimental features. Experimental features + * may be deprecated in the future. Use at your own risk. + */ + public static class Builder extends CronetEngine.Builder { + /** + * Constructs a {@link Builder} object that facilitates creating a + * {@link CronetEngine}. The default configuration enables HTTP/2 and + * disables QUIC, SDCH and the HTTP cache. + * + * @param context Android {@link Context}, which is used by + * {@link Builder} to retrieve the application + * context. A reference to only the application + * context will be kept, so as to avoid extending + * the lifetime of {@code context} unnecessarily. + */ + public Builder(Context context) { + super(context); + } + + /** + * Constructs {@link Builder} with a given delegate that provides the actual implementation + * of the {@code Builder} methods. This constructor is used only by the internal + * implementation. + * + * @param builderDelegate delegate that provides the actual implementation. + * + * {@hide} + */ + public Builder(ICronetEngineBuilder builderDelegate) { + super(builderDelegate); + } + + /** + * Enables the network quality estimator, which collects and reports + * measurements of round trip time (RTT) and downstream throughput at + * various layers of the network stack. After enabling the estimator, + * listeners of RTT and throughput can be added with + * {@link #addRttListener} and {@link #addThroughputListener} and + * removed with {@link #removeRttListener} and + * {@link #removeThroughputListener}. The estimator uses memory and CPU + * only when enabled. + * @param value {@code true} to enable network quality estimator, + * {@code false} to disable. + * @return the builder to facilitate chaining. + */ + public Builder enableNetworkQualityEstimator(boolean value) { + mBuilderDelegate.enableNetworkQualityEstimator(value); + return this; + } + + /** + * Sets experimental options to be used in Cronet. + * + * @param options JSON formatted experimental options. + * @return the builder to facilitate chaining. + */ + public Builder setExperimentalOptions(String options) { + mBuilderDelegate.setExperimentalOptions(options); + return this; + } + + /** + * Sets the thread priority of Cronet's internal thread. + * + * @param priority the thread priority of Cronet's internal thread. + * A Linux priority level, from -20 for highest scheduling + * priority to 19 for lowest scheduling priority. For more + * information on values, see + * {@link android.os.Process#setThreadPriority(int, int)} and + * {@link android.os.Process#THREAD_PRIORITY_DEFAULT + * THREAD_PRIORITY_*} values. + * @return the builder to facilitate chaining. + */ + public Builder setThreadPriority(int priority) { + mBuilderDelegate.setThreadPriority(priority); + return this; + } + + /** + * Returns delegate, only for testing. + * @hide + */ + @VisibleForTesting + public ICronetEngineBuilder getBuilderDelegate() { + return mBuilderDelegate; + } + + // To support method chaining, override superclass methods to return an + // instance of this class instead of the parent. + + @Override + public Builder setUserAgent(String userAgent) { + super.setUserAgent(userAgent); + return this; + } + + @Override + public Builder setStoragePath(String value) { + super.setStoragePath(value); + return this; + } + + @Override + public Builder setLibraryLoader(LibraryLoader loader) { + super.setLibraryLoader(loader); + return this; + } + + @Override + public Builder enableQuic(boolean value) { + super.enableQuic(value); + return this; + } + + @Override + public Builder enableHttp2(boolean value) { + super.enableHttp2(value); + return this; + } + + @Override + public Builder enableSdch(boolean value) { + return this; + } + + @Override + public Builder enableHttpCache(int cacheMode, long maxSize) { + super.enableHttpCache(cacheMode, maxSize); + return this; + } + + @Override + public Builder addQuicHint(String host, int port, int alternatePort) { + super.addQuicHint(host, port, alternatePort); + return this; + } + + @Override + public Builder addPublicKeyPins(String hostName, Set pinsSha256, + boolean includeSubdomains, Date expirationDate) { + super.addPublicKeyPins(hostName, pinsSha256, includeSubdomains, expirationDate); + return this; + } + + @Override + public Builder enablePublicKeyPinningBypassForLocalTrustAnchors(boolean value) { + super.enablePublicKeyPinningBypassForLocalTrustAnchors(value); + return this; + } + + @Override + public ExperimentalCronetEngine build() { + return mBuilderDelegate.build(); + } + } + + /** + * Creates a builder for {@link BidirectionalStream} objects. All callbacks for + * generated {@code BidirectionalStream} objects will be invoked on + * {@code executor}. {@code executor} must not run tasks on the + * current thread, otherwise the networking operations may block and exceptions + * may be thrown at shutdown time. + * + * @param url URL for the generated streams. + * @param callback the {@link BidirectionalStream.Callback} object that gets invoked upon + * different events occurring. + * @param executor the {@link Executor} on which {@code callback} methods will be invoked. + * + * @return the created builder. + */ + public abstract ExperimentalBidirectionalStream.Builder newBidirectionalStreamBuilder( + String url, BidirectionalStream.Callback callback, Executor executor); + + @Override + public abstract ExperimentalUrlRequest.Builder newUrlRequestBuilder( + String url, UrlRequest.Callback callback, Executor executor); + + /** + * Starts NetLog logging to a specified directory with a bounded size. The NetLog will contain + * events emitted by all live CronetEngines. The NetLog is useful for debugging. + * Once logging has stopped {@link #stopNetLog}, the data will be written + * to netlog.json in {@code dirPath}. If logging is interrupted, you can + * stitch the files found in .inprogress subdirectory manually using: + * https://chromium.googlesource.com/chromium/src/+/main/net/tools/stitch_net_log_files.py. + * The log can be viewed using a Chrome browser navigated to chrome://net-internals/#import. + * @param dirPath the directory where the netlog.json file will be created. dirPath must + * already exist. NetLog files must not exist in the directory. If actively + * logging, this method is ignored. + * @param logAll {@code true} to include basic events, user cookies, + * credentials and all transferred bytes in the log. This option presents a + * privacy risk, since it exposes the user's credentials, and should only be + * used with the user's consent and in situations where the log won't be public. + * {@code false} to just include basic events. + * @param maxSize the maximum total disk space in bytes that should be used by NetLog. Actual + * disk space usage may exceed this limit slightly. + */ + public void startNetLogToDisk(String dirPath, boolean logAll, int maxSize) {} + + /** + * Returns an estimate of the effective connection type computed by the network quality + * estimator. Call {@link Builder#enableNetworkQualityEstimator} to begin computing this + * value. + * + * @return the estimated connection type. The returned value is one of + * {@link #EFFECTIVE_CONNECTION_TYPE_UNKNOWN EFFECTIVE_CONNECTION_TYPE_* }. + */ + public int getEffectiveConnectionType() { + return EFFECTIVE_CONNECTION_TYPE_UNKNOWN; + } + + /** + * Configures the network quality estimator for testing. This must be called + * before round trip time and throughput listeners are added, and after the + * network quality estimator has been enabled. + * @param useLocalHostRequests include requests to localhost in estimates. + * @param useSmallerResponses include small responses in throughput estimates. + * @param disableOfflineCheck when set to true, disables the device offline checks when + * computing the effective connection type or when writing the prefs. + */ + public void configureNetworkQualityEstimatorForTesting(boolean useLocalHostRequests, + boolean useSmallerResponses, boolean disableOfflineCheck) {} + + /** + * Registers a listener that gets called whenever the network quality + * estimator witnesses a sample round trip time. This must be called + * after {@link Builder#enableNetworkQualityEstimator}, and with throw an + * exception otherwise. Round trip times may be recorded at various layers + * of the network stack, including TCP, QUIC, and at the URL request layer. + * The listener is called on the {@link java.util.concurrent.Executor} that + * is passed to {@link Builder#enableNetworkQualityEstimator}. + * @param listener the listener of round trip times. + */ + public void addRttListener(NetworkQualityRttListener listener) {} + + /** + * Removes a listener of round trip times if previously registered with + * {@link #addRttListener}. This should be called after a + * {@link NetworkQualityRttListener} is added in order to stop receiving + * observations. + * @param listener the listener of round trip times. + */ + public void removeRttListener(NetworkQualityRttListener listener) {} + + /** + * Registers a listener that gets called whenever the network quality + * estimator witnesses a sample throughput measurement. This must be called + * after {@link Builder#enableNetworkQualityEstimator}. Throughput observations + * are computed by measuring bytes read over the active network interface + * at times when at least one URL response is being received. The listener + * is called on the {@link java.util.concurrent.Executor} that is passed to + * {@link Builder#enableNetworkQualityEstimator}. + * @param listener the listener of throughput. + */ + public void addThroughputListener(NetworkQualityThroughputListener listener) {} + + /** + * Removes a listener of throughput. This should be called after a + * {@link NetworkQualityThroughputListener} is added with + * {@link #addThroughputListener} in order to stop receiving observations. + * @param listener the listener of throughput. + */ + public void removeThroughputListener(NetworkQualityThroughputListener listener) {} + + /** + * Establishes a new connection to the resource specified by the {@link URL} {@code url} + * using the given proxy. + *

+ * Note: Cronet's {@link java.net.HttpURLConnection} implementation is subject to certain + * limitations, see {@link #createURLStreamHandlerFactory} for details. + * + * @param url URL of resource to connect to. + * @param proxy proxy to use when establishing connection. + * @return an {@link java.net.HttpURLConnection} instance implemented by this CronetEngine. + * @throws IOException if an error occurs while opening the connection. + */ + // TODO(pauljensen): Expose once implemented, http://crbug.com/418111 + public URLConnection openConnection(URL url, Proxy proxy) throws IOException { + return url.openConnection(proxy); + } + + /** + * Registers a listener that gets called after the end of each request with the request info. + * + *

The listener is called on an {@link java.util.concurrent.Executor} provided by the + * listener. + * + * @param listener the listener for finished requests. + */ + public void addRequestFinishedListener(RequestFinishedInfo.Listener listener) {} + + /** + * Removes a finished request listener. + * + * @param listener the listener to remove. + */ + public void removeRequestFinishedListener(RequestFinishedInfo.Listener listener) {} + + /** + * Returns the HTTP RTT estimate (in milliseconds) computed by the network + * quality estimator. Set to {@link #CONNECTION_METRIC_UNKNOWN} if the value + * is unavailable. This must be called after + * {@link Builder#enableNetworkQualityEstimator}, and will throw an + * exception otherwise. + * @return Estimate of the HTTP RTT in milliseconds. + */ + public int getHttpRttMs() { + return CONNECTION_METRIC_UNKNOWN; + } + + /** + * Returns the transport RTT estimate (in milliseconds) computed by the + * network quality estimator. Set to {@link #CONNECTION_METRIC_UNKNOWN} if + * the value is unavailable. This must be called after + * {@link Builder#enableNetworkQualityEstimator}, and will throw an + * exception otherwise. + * @return Estimate of the transport RTT in milliseconds. + */ + public int getTransportRttMs() { + return CONNECTION_METRIC_UNKNOWN; + } + + /** + * Returns the downstream throughput estimate (in kilobits per second) + * computed by the network quality estimator. Set to + * {@link #CONNECTION_METRIC_UNKNOWN} if the value is + * unavailable. This must be called after + * {@link Builder#enableNetworkQualityEstimator}, and will + * throw an exception otherwise. + * @return Estimate of the downstream throughput in kilobits per second. + */ + public int getDownstreamThroughputKbps() { + return CONNECTION_METRIC_UNKNOWN; + } +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/ExperimentalUrlRequest.java b/src/components/cronet/android/api/src/org/chromium/net/ExperimentalUrlRequest.java new file mode 100644 index 0000000000..220986bdea --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/ExperimentalUrlRequest.java @@ -0,0 +1,162 @@ +// Copyright 2016 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 java.util.concurrent.Executor; + +/** + * {@link UrlRequest} that exposes experimental features. To obtain an + * instance of this class, cast a {@code UrlRequest} to this type. Every + * instance of {@code UrlRequest} can be cast to an instance of this class, + * as they are backed by the same implementation and hence perform identically. + * Instances of this class are not meant for general use, but instead only + * to access experimental features. Experimental features may be deprecated in the + * future. Use at your own risk. + * + * {@hide since this class exposes experimental features that should be hidden}. + */ +public abstract class ExperimentalUrlRequest extends UrlRequest { + /** + * {@link UrlRequest#Builder} that exposes experimental features. To obtain an + * instance of this class, cast a {@code UrlRequest.Builder} to this type. Every + * instance of {@code UrlRequest.Builder} can be cast to an instance of this class, + * as they are backed by the same implementation and hence perform identically. + * Instances of this class are not meant for general use, but instead only + * to access experimental features. Experimental features may be deprecated in the + * future. Use at your own risk. + */ + public abstract static class Builder extends UrlRequest.Builder { + /** + * Disables connection migration for the request if enabled for + * the session. + * @return the builder to facilitate chaining. + */ + public Builder disableConnectionMigration() { + return this; + } + + /** + * Associates the annotation object with this request. May add more than one. + * Passed through to a {@link RequestFinishedInfo.Listener}, + * see {@link RequestFinishedInfo#getAnnotations}. + * + * @param annotation an object to pass on to the {@link RequestFinishedInfo.Listener} with a + * {@link RequestFinishedInfo}. + * @return the builder to facilitate chaining. + */ + public Builder addRequestAnnotation(Object annotation) { + return this; + } + + /** + * Sets {@link android.net.TrafficStats} tag to use when accounting socket traffic caused by + * this request. See {@link android.net.TrafficStats} for more information. If no tag is + * set (e.g. this method isn't called), then Android accounts for the socket traffic caused + * by this request as if the tag value were set to 0. + *

+ * NOTE:Setting a tag disallows sharing of sockets with requests + * with other tags, which may adversely effect performance by prohibiting + * connection sharing. In other words use of multiplexed sockets (e.g. HTTP/2 + * and QUIC) will only be allowed if all requests have the same socket tag. + * + * @param tag the tag value used to when accounting for socket traffic caused by this + * request. Tags between 0xFFFFFF00 and 0xFFFFFFFF are reserved and used + * internally by system services like {@link android.app.DownloadManager} when + * performing traffic on behalf of an application. + * @return the builder to facilitate chaining. + */ + public Builder setTrafficStatsTag(int tag) { + return this; + } + + /** + * Sets specific UID to use when accounting socket traffic caused by this request. See + * {@link android.net.TrafficStats} for more information. Designed for use when performing + * an operation on behalf of another application. Caller must hold + * {@link android.Manifest.permission#MODIFY_NETWORK_ACCOUNTING} permission. By default + * traffic is attributed to UID of caller. + *

+ * NOTE:Setting a UID disallows sharing of sockets with requests + * with other UIDs, which may adversely effect performance by prohibiting + * connection sharing. In other words use of multiplexed sockets (e.g. HTTP/2 + * and QUIC) will only be allowed if all requests have the same UID set. + * + * @param uid the UID to attribute socket traffic caused by this request. + * @return the builder to facilitate chaining. + */ + public Builder setTrafficStatsUid(int uid) { + return this; + } + + /** + * Sets a listener that gets invoked after {@link Callback#onCanceled onCanceled()}, + * {@link Callback#onFailed onFailed()} or {@link Callback#onSucceeded onSucceeded()} + * return. + * + *

The listener is invoked with the request finished info on an + * {@link java.util.concurrent.Executor} provided by + * {@link RequestFinishedInfo.Listener#getExecutor getExecutor()}. + * + * @param listener the listener for finished requests. + * @return the builder to facilitate chaining. + */ + public Builder setRequestFinishedListener(RequestFinishedInfo.Listener listener) { + return this; + } + + /** + * Default request idempotency, only enable 0-RTT for safe HTTP methods. Passed to {@link + * #setIdempotency}. + */ + public static final int DEFAULT_IDEMPOTENCY = 0; + + /** + * Request is idempotent. Passed to {@link #setIdempotency}. + */ + public static final int IDEMPOTENT = 1; + + /** + * Request is not idempotent. Passed to {@link #setIdempotency}. + */ + public static final int NOT_IDEMPOTENT = 2; + + /** + * Sets idempotency of the request which should be one of the {@link #DEFAULT_IDEMPOTENCY + * IDEMPOTENT NOT_IDEMPOTENT} values. The default idempotency indicates that 0-RTT is only + * enabled for safe HTTP methods (GET, HEAD, OPTIONS, and TRACE). + * + * @param idempotency idempotency of the request which should be one of the {@link + * #DEFAULT_IDEMPOTENCY IDEMPOTENT NOT_IDEMPOTENT} values. + * @return the builder to facilitate chaining. + */ + public Builder setIdempotency(int idempotency) { + return this; + } + + // To support method chaining, override superclass methods to return an + // instance of this class instead of the parent. + + @Override + public abstract Builder setHttpMethod(String method); + + @Override + public abstract Builder addHeader(String header, String value); + + @Override + public abstract Builder disableCache(); + + @Override + public abstract Builder setPriority(int priority); + + @Override + public abstract Builder setUploadDataProvider( + UploadDataProvider uploadDataProvider, Executor executor); + + @Override + public abstract Builder allowDirectExecutor(); + + @Override + public abstract ExperimentalUrlRequest build(); + } +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/ICronetEngineBuilder.java b/src/components/cronet/android/api/src/org/chromium/net/ICronetEngineBuilder.java new file mode 100644 index 0000000000..1f6694d4cc --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/ICronetEngineBuilder.java @@ -0,0 +1,54 @@ +// Copyright 2016 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 java.util.Date; +import java.util.Set; + +/** + * Defines methods that the actual implementation of {@link CronetEngine.Builder} has to implement. + * {@code CronetEngine.Builder} uses this interface to delegate the calls. + * For the documentation of individual methods, please see the identically named methods in + * {@link org.chromium.net.CronetEngine.Builder} and + * {@link org.chromium.net.ExperimentalCronetEngine.Builder}. + * + * {@hide internal class} + */ +public abstract class ICronetEngineBuilder { + // Public API methods. + public abstract ICronetEngineBuilder addPublicKeyPins(String hostName, Set pinsSha256, + boolean includeSubdomains, Date expirationDate); + public abstract ICronetEngineBuilder addQuicHint(String host, int port, int alternatePort); + public abstract ICronetEngineBuilder enableHttp2(boolean value); + public abstract ICronetEngineBuilder enableHttpCache(int cacheMode, long maxSize); + public abstract ICronetEngineBuilder enablePublicKeyPinningBypassForLocalTrustAnchors( + boolean value); + public abstract ICronetEngineBuilder enableQuic(boolean value); + public abstract ICronetEngineBuilder enableSdch(boolean value); + public ICronetEngineBuilder enableBrotli(boolean value) { + // Do nothing for older implementations. + return this; + } + public abstract ICronetEngineBuilder setExperimentalOptions(String options); + public abstract ICronetEngineBuilder setLibraryLoader( + CronetEngine.Builder.LibraryLoader loader); + public abstract ICronetEngineBuilder setStoragePath(String value); + public abstract ICronetEngineBuilder setUserAgent(String userAgent); + public abstract String getDefaultUserAgent(); + public abstract ExperimentalCronetEngine build(); + + // Experimental API methods. + // + // Note: all experimental API methods should have default implementation. This will allow + // removing the experimental methods from the implementation layer without breaking + // the client. + + public ICronetEngineBuilder enableNetworkQualityEstimator(boolean value) { + return this; + } + + public ICronetEngineBuilder setThreadPriority(int priority) { + return this; + } +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/InlineExecutionProhibitedException.java b/src/components/cronet/android/api/src/org/chromium/net/InlineExecutionProhibitedException.java new file mode 100644 index 0000000000..ec67202350 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/InlineExecutionProhibitedException.java @@ -0,0 +1,18 @@ +// Copyright 2016 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 java.util.concurrent.RejectedExecutionException; + +/** + * Thrown when an executor runs a submitted runnable inline in {@link + * java.util.concurrent.Executor#execute(Runnable)} and {@link + * UrlRequest.Builder#allowDirectExecutor} was not called. + */ +public final class InlineExecutionProhibitedException extends RejectedExecutionException { + public InlineExecutionProhibitedException() { + super("Inline execution is prohibited for this request"); + } +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/NetworkException.java b/src/components/cronet/android/api/src/org/chromium/net/NetworkException.java new file mode 100644 index 0000000000..f93a17aa36 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/NetworkException.java @@ -0,0 +1,111 @@ +// Copyright 2016 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; + +/** + * Exception passed to {@link UrlRequest.Callback#onFailed UrlRequest.Callback.onFailed()} when + * Cronet fails to process a network request. In this case {@link #getErrorCode} and + * {@link #getCronetInternalErrorCode} can be used to get more information about the specific + * type of failure. If {@link #getErrorCode} returns {@link #ERROR_QUIC_PROTOCOL_FAILED}, + * this exception can be cast to a {@link QuicException} which can provide further details. + */ +public abstract class NetworkException extends CronetException { + /** + * Error code indicating the host being sent the request could not be resolved to an IP address. + */ + public static final int ERROR_HOSTNAME_NOT_RESOLVED = 1; + /** + * Error code indicating the device was not connected to any network. + */ + public static final int ERROR_INTERNET_DISCONNECTED = 2; + /** + * Error code indicating that as the request was processed the network configuration changed. + * When {@link #getErrorCode} returns this code, this exception may be cast to + * {@link QuicException} for more information if + * QUIC protocol is used. + */ + public static final int ERROR_NETWORK_CHANGED = 3; + /** + * Error code indicating a timeout expired. Timeouts expiring while attempting to connect will + * be reported as the more specific {@link #ERROR_CONNECTION_TIMED_OUT}. + */ + public static final int ERROR_TIMED_OUT = 4; + /** + * Error code indicating the connection was closed unexpectedly. + */ + public static final int ERROR_CONNECTION_CLOSED = 5; + /** + * Error code indicating the connection attempt timed out. + */ + public static final int ERROR_CONNECTION_TIMED_OUT = 6; + /** + * Error code indicating the connection attempt was refused. + */ + public static final int ERROR_CONNECTION_REFUSED = 7; + /** + * Error code indicating the connection was unexpectedly reset. + */ + public static final int ERROR_CONNECTION_RESET = 8; + /** + * Error code indicating the IP address being contacted is unreachable, meaning there is no + * route to the specified host or network. + */ + public static final int ERROR_ADDRESS_UNREACHABLE = 9; + /** + * Error code indicating an error related to the + * QUIC protocol. When {@link #getErrorCode} returns this code, this exception can be cast + * to {@link QuicException} for more information. + */ + public static final int ERROR_QUIC_PROTOCOL_FAILED = 10; + /** + * Error code indicating another type of error was encountered. + * {@link #getCronetInternalErrorCode} can be consulted to get a more specific cause. + */ + public static final int ERROR_OTHER = 11; + + /** + * Constructs an exception that is caused by a network error. + * + * @param message explanation of failure. + * @param cause the cause (which is saved for later retrieval by the {@link + * java.io.IOException#getCause getCause()} method). A null value is permitted, and + * indicates that the cause is nonexistent or unknown. + */ + protected NetworkException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Returns error code, one of {@link #ERROR_HOSTNAME_NOT_RESOLVED ERROR_*}. + * + * @return error code, one of {@link #ERROR_HOSTNAME_NOT_RESOLVED ERROR_*}. + */ + public abstract int getErrorCode(); + + /** + * Returns a Cronet internal error code. This may provide more specific error + * diagnosis than {@link #getErrorCode}, but the constant values are not exposed to Java and + * may change over time. See + * + * here for the lastest list of values. + * + * @return Cronet internal error code. + */ + public abstract int getCronetInternalErrorCode(); + + /** + * Returns {@code true} if retrying this request right away might succeed, {@code false} + * otherwise. For example returns {@code true} when {@link #getErrorCode} returns + * {@link #ERROR_NETWORK_CHANGED} because trying the request might succeed using the new + * network configuration, but {@code false} when {@code getErrorCode()} returns + * {@link #ERROR_INTERNET_DISCONNECTED} because retrying the request right away will + * encounter the same failure (instead retrying should be delayed until device regains + * network connectivity). + * + * @return {@code true} if retrying this request right away might succeed, {@code false} + * otherwise. + */ + public abstract boolean immediatelyRetryable(); +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/NetworkQualityRttListener.java b/src/components/cronet/android/api/src/org/chromium/net/NetworkQualityRttListener.java new file mode 100644 index 0000000000..c6705a92b1 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/NetworkQualityRttListener.java @@ -0,0 +1,44 @@ +// 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 java.util.concurrent.Executor; + +/** + * Watches observations of various round trip times (RTTs) at various layers of + * the network stack. These include RTT estimates by QUIC and TCP, as well as + * the time between when a URL request is sent and when the first byte of the + * response is received. + * {@hide} as it's a prototype. + */ +public abstract class NetworkQualityRttListener { + /** + * The executor on which this listener will be notified. Set as a final + * field, so it can be safely accessed across threads. + */ + private final Executor mExecutor; + + /** + * @param executor The executor on which the observations are reported. + */ + public NetworkQualityRttListener(Executor executor) { + if (executor == null) { + throw new IllegalStateException("Executor must not be null"); + } + mExecutor = executor; + } + + public Executor getExecutor() { + return mExecutor; + } + + /** + * Reports a new round trip time observation. + * @param rttMs the round trip time in milliseconds. + * @param whenMs milliseconds since the Epoch (January 1st 1970, 00:00:00.000). + * @param source the observation source from {@link NetworkQualityObservationSource}. + */ + public abstract void onRttObservation(int rttMs, long whenMs, int source); +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/NetworkQualityThroughputListener.java b/src/components/cronet/android/api/src/org/chromium/net/NetworkQualityThroughputListener.java new file mode 100644 index 0000000000..44caac8987 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/NetworkQualityThroughputListener.java @@ -0,0 +1,42 @@ +// 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 java.util.concurrent.Executor; + +/** + * Listener that is notified of throughput observations from the network quality + * estimator. + * {@hide} as it's a prototype. + */ +public abstract class NetworkQualityThroughputListener { + /** + * The executor on which this listener will be notified. Set as a final + * field, so it can be safely accessed across threads. + */ + private final Executor mExecutor; + + /** + * @param executor The executor on which the observations are reported. + */ + public NetworkQualityThroughputListener(Executor executor) { + if (executor == null) { + throw new IllegalStateException("Executor must not be null"); + } + mExecutor = executor; + } + + public Executor getExecutor() { + return mExecutor; + } + + /** + * Reports a new throughput observation. + * @param throughputKbps the downstream throughput in kilobits per second. + * @param whenMs milliseconds since the Epoch (January 1st 1970, 00:00:00.000). + * @param source the observation source from {@link NetworkQualityObservationSource}. + */ + public abstract void onThroughputObservation(int throughputKbps, long whenMs, int source); +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/QuicException.java b/src/components/cronet/android/api/src/org/chromium/net/QuicException.java new file mode 100644 index 0000000000..ccebb7cf76 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/QuicException.java @@ -0,0 +1,35 @@ +// Copyright 2016 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; + +/** + * Subclass of {@link NetworkException} which contains a detailed + * QUIC error code from + * QuicErrorCode. An instance of {@code QuicException} is passed to {@code onFailed} callbacks + * when the error code is {@link NetworkException#ERROR_QUIC_PROTOCOL_FAILED + * NetworkException.ERROR_QUIC_PROTOCOL_FAILED}. + */ +public abstract class QuicException extends NetworkException { + /** + * Constructs an exception that is caused by a QUIC protocol error. + * + * @param message explanation of failure. + * @param cause the cause (which is saved for later retrieval by the {@link + * java.io.IOException#getCause getCause()} method). A null value is permitted, and + * indicates that the cause is nonexistent or unknown. + */ + protected QuicException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Returns the QUIC error code, which is a value + * from + * QuicErrorCode. + */ + public abstract int getQuicDetailedErrorCode(); +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/RequestFinishedInfo.java b/src/components/cronet/android/api/src/org/chromium/net/RequestFinishedInfo.java new file mode 100644 index 0000000000..7cceb76806 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/RequestFinishedInfo.java @@ -0,0 +1,320 @@ +// Copyright 2016 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 androidx.annotation.Nullable; + +import java.util.Collection; +import java.util.Date; +import java.util.concurrent.Executor; + +/** + * Information about a finished request. Passed to {@link RequestFinishedInfo.Listener}. + * + * To associate the data with the original request, use + * {@link ExperimentalUrlRequest.Builder#addRequestAnnotation} or + * {@link ExperimentalBidirectionalStream.Builder#addRequestAnnotation} to add a unique identifier + * when creating the request, and call {@link #getAnnotations} when the {@link RequestFinishedInfo} + * is received to retrieve the identifier. + * + * {@hide} as it's a prototype. + */ +public abstract class RequestFinishedInfo { + /** + * Listens for finished requests for the purpose of collecting metrics. + * + * {@hide} as it's a prototype. + */ + public abstract static class Listener { + private final Executor mExecutor; + + public Listener(Executor executor) { + if (executor == null) { + throw new IllegalStateException("Executor must not be null"); + } + mExecutor = executor; + } + + /** + * Invoked with request info. Will be called in a task submitted to the + * {@link java.util.concurrent.Executor} returned by {@link #getExecutor}. + * @param requestInfo {@link RequestFinishedInfo} for finished request. + */ + public abstract void onRequestFinished(RequestFinishedInfo requestInfo); + + /** + * Returns this listener's executor. Can be called on any thread. + * @return this listener's {@link java.util.concurrent.Executor} + */ + public Executor getExecutor() { + return mExecutor; + } + } + + /** + * Metrics collected for a single request. Most of these metrics are timestamps for events + * during the lifetime of the request, which can be used to build a detailed timeline for + * investigating performance. + * + * Events happen in this order: + *

    + *
  1. {@link #getRequestStart request start}
  2. + *
  3. {@link #getDnsStart DNS start}
  4. + *
  5. {@link #getDnsEnd DNS end}
  6. + *
  7. {@link #getConnectStart connect start}
  8. + *
  9. {@link #getSslStart SSL start}
  10. + *
  11. {@link #getSslEnd SSL end}
  12. + *
  13. {@link #getConnectEnd connect end}
  14. + *
  15. {@link #getSendingStart sending start}
  16. + *
  17. {@link #getSendingEnd sending end}
  18. + *
  19. {@link #getResponseStart response start}
  20. + *
  21. {@link #getRequestEnd request end}
  22. + *
+ * + * Start times are reported as the time when a request started blocking on event, not when the + * event actually occurred, with the exception of push start and end. If a metric is not + * meaningful or not available, including cases when a request finished before reaching that + * stage, start and end times will be {@code null}. If no time was spent blocking on an event, + * start and end will be the same time. + * + * If the system clock is adjusted during the request, some of the {@link java.util.Date} values + * might not match it. Timestamps are recorded using a clock that is guaranteed not to run + * backwards. All timestamps are correct relative to the system clock at the time of request + * start, and taking the difference between two timestamps will give the correct difference + * between the events. In order to preserve this property, timestamps for events other than + * request start are not guaranteed to match the system clock at the times they represent. + * + * Most timing metrics are taken from + * LoadTimingInfo, + * which holds the information for and + * . + * + * {@hide} as it's a prototype. + */ + public abstract static class Metrics { + /** + * Returns time when the request started. + * @return {@link java.util.Date} representing when the native request actually started. + * This timestamp will match the system clock at the time it represents. + */ + @Nullable + public abstract Date getRequestStart(); + + /** + * Returns time when DNS lookup started. This and {@link #getDnsEnd} will return non-null + * values regardless of whether the result came from a DNS server or the local cache. + * @return {@link java.util.Date} representing when DNS lookup started. {@code null} if the + * socket was reused (see {@link #getSocketReused}). + */ + @Nullable + public abstract Date getDnsStart(); + + /** + * Returns time when DNS lookup finished. This and {@link #getDnsStart} will return non-null + * values regardless of whether the result came from a DNS server or the local cache. + * @return {@link java.util.Date} representing when DNS lookup finished. {@code null} if the + * socket was reused (see {@link #getSocketReused}). + */ + @Nullable + public abstract Date getDnsEnd(); + + /** + * Returns time when connection establishment started. + * @return {@link java.util.Date} representing when connection establishment started, + * typically when DNS resolution finishes. {@code null} if the socket was reused (see + * {@link #getSocketReused}). + */ + @Nullable + public abstract Date getConnectStart(); + + /** + * Returns time when connection establishment finished. + * @return {@link java.util.Date} representing when connection establishment finished, + * after TCP connection is established and, if using HTTPS, SSL handshake is completed. + * For QUIC 0-RTT, this represents the time of handshake confirmation and might happen + * later than {@link #getSendingStart}. + * {@code null} if the socket was reused (see {@link #getSocketReused}). + */ + @Nullable + public abstract Date getConnectEnd(); + + /** + * Returns time when SSL handshake started. For QUIC, this will be the same time as + * {@link #getConnectStart}. + * @return {@link java.util.Date} representing when SSL handshake started. {@code null} if + * SSL is not used or if the socket was reused (see {@link #getSocketReused}). + */ + @Nullable + public abstract Date getSslStart(); + + /** + * Returns time when SSL handshake finished. For QUIC, this will be the same time as + * {@link #getConnectEnd}. + * @return {@link java.util.Date} representing when SSL handshake finished. {@code null} if + * SSL is not used or if the socket was reused (see {@link #getSocketReused}). + */ + @Nullable + public abstract Date getSslEnd(); + + /** + * Returns time when sending the request started. + * @return {@link java.util.Date} representing when sending HTTP request headers started. + */ + @Nullable + public abstract Date getSendingStart(); + + /** + * Returns time when sending the request finished. + * @return {@link java.util.Date} representing when sending HTTP request body finished. + * (Sending request body happens after sending request headers.) + */ + @Nullable + public abstract Date getSendingEnd(); + + /** + * Returns time when first byte of HTTP/2 server push was received. + * @return {@link java.util.Date} representing when the first byte of an HTTP/2 server push + * was received. {@code null} if server push is not used. + */ + @Nullable + public abstract Date getPushStart(); + + /** + * Returns time when last byte of HTTP/2 server push was received. + * @return {@link java.util.Date} representing when the last byte of an HTTP/2 server push + * was received. {@code null} if server push is not used. + */ + @Nullable + public abstract Date getPushEnd(); + + /** + * Returns time when the end of the response headers was received. + * @return {@link java.util.Date} representing when the end of the response headers was + * received. + */ + @Nullable + public abstract Date getResponseStart(); + + /** + * Returns time when the request finished. + * @return {@link java.util.Date} representing when the request finished. + */ + @Nullable + public abstract Date getRequestEnd(); + + /** + * Returns whether the socket was reused from a previous request. In HTTP/2 or QUIC, if + * streams are multiplexed in a single connection, returns {@code true} for all streams + * after the first. + * @return whether this request reused a socket from a previous request. When {@code true}, + * DNS, connection, and SSL times will be {@code null}. + */ + public abstract boolean getSocketReused(); + + /** + * Returns milliseconds between request initiation and first byte of response headers, + * or {@code null} if not collected. + * TODO(mgersh): Remove once new API works http://crbug.com/629194 + * {@hide} + */ + @Nullable + public abstract Long getTtfbMs(); + + /** + * Returns milliseconds between request initiation and finish, + * including a failure or cancellation, or {@code null} if not collected. + * TODO(mgersh): Remove once new API works http://crbug.com/629194 + * {@hide} + */ + @Nullable + public abstract Long getTotalTimeMs(); + + /** + * Returns total bytes sent over the network transport layer, or {@code null} if not + * collected. + */ + @Nullable + public abstract Long getSentByteCount(); + + /** + * Returns total bytes received over the network transport layer, or {@code null} if not + * collected. Number of bytes does not include any previous redirects. + */ + @Nullable + public abstract Long getReceivedByteCount(); + } + + /** + * Reason value indicating that the request succeeded. Returned from {@link #getFinishedReason}. + */ + public static final int SUCCEEDED = 0; + /** + * Reason value indicating that the request failed or returned an error. Returned from + * {@link #getFinishedReason}. + */ + public static final int FAILED = 1; + /** + * Reason value indicating that the request was canceled. Returned from + * {@link #getFinishedReason}. + */ + public static final int CANCELED = 2; + + /** + * Returns the request's original URL. + * + * @return the request's original URL + */ + public abstract String getUrl(); + + /** + * Returns the objects that the caller has supplied when initiating the request, using + * {@link ExperimentalUrlRequest.Builder#addRequestAnnotation} or + * {@link ExperimentalBidirectionalStream.Builder#addRequestAnnotation}. + * Annotations can be used to associate a {@link RequestFinishedInfo} with the original request + * or type of request. + * + * @return annotations supplied when creating the request + */ + public abstract Collection getAnnotations(); + + // TODO(klm): Collect and return a chain of Metrics objects for redirect responses. + // TODO(mgersh): Update this javadoc when new metrics are fully implemented + /** + * Returns metrics collected for this request. + * + *

The reported times and bytes account for all redirects, i.e. + * the TTFB is from the start of the original request to the ultimate response headers, + * the TTLB is from the start of the original request to the end of the ultimate response, + * the received byte count is for all redirects and the ultimate response combined. + * These cumulative metric definitions are debatable, but are chosen to make sense + * for user-facing latency analysis. + * + * @return metrics collected for this request. + */ + public abstract Metrics getMetrics(); + + /** + * Returns the reason why the request finished. + * @return one of {@link #SUCCEEDED}, {@link #FAILED}, or {@link #CANCELED} + */ + public abstract int getFinishedReason(); + + /** + * Returns a {@link UrlResponseInfo} for the request, if its response had started. + * @return {@link UrlResponseInfo} for the request, if its response had started. + */ + @Nullable + public abstract UrlResponseInfo getResponseInfo(); + + /** + * If the request failed, returns the same {@link CronetException} provided to + * {@link UrlRequest.Callback#onFailed}. + * + * @return the request's {@link CronetException}, if the request failed + */ + @Nullable + public abstract CronetException getException(); +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/UploadDataProvider.java b/src/components/cronet/android/api/src/org/chromium/net/UploadDataProvider.java new file mode 100644 index 0000000000..37ed263aac --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/UploadDataProvider.java @@ -0,0 +1,89 @@ +// 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 java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Abstract class allowing the embedder to provide an upload body to + * {@link UrlRequest}. It supports both non-chunked (size known in advanced) and + * chunked (size not known in advance) uploads. Be aware that not all servers + * support chunked uploads. + * + *

An upload is either always chunked, across multiple uploads if the data + * ends up being sent more than once, or never chunked. + */ +public abstract class UploadDataProvider implements Closeable { + /** + * If this is a non-chunked upload, returns the length of the upload. Must + * always return -1 if this is a chunked upload. + * + * @return the length of the upload for non-chunked uploads, -1 otherwise. + * @throws IOException if any IOException occurred during the process. + */ + public abstract long getLength() throws IOException; + + /** + * Reads upload data into {@code byteBuffer}. Upon completion, the buffer's + * position is updated to the end of the bytes that were read. The buffer's + * limit is not changed. Each call of this method must be followed be a + * single call, either synchronous or asynchronous, to + * {@code uploadDataSink}: {@link UploadDataSink#onReadSucceeded} on success + * or {@link UploadDataSink#onReadError} on failure. Neither read nor rewind + * will be called until one of those methods or the other is called. Even if + * the associated {@link UrlRequest} is canceled, one or the other must + * still be called before resources can be safely freed. Throwing an + * exception will also result in resources being freed and the request being + * errored out. + * + * @param uploadDataSink The object to notify when the read has completed, + * successfully or otherwise. + * @param byteBuffer The buffer to copy the read bytes into. Do not change + * byteBuffer's limit. + * @throws IOException if any IOException occurred during the process. + * {@link UrlRequest.Callback#onFailed} will be called with the + * thrown exception set as the cause of the + * {@link CallbackException}. + */ + public abstract void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) + throws IOException; + + /** + * Rewinds upload data. Each call must be followed be a single + * call, either synchronous or asynchronous, to {@code uploadDataSink}: + * {@link UploadDataSink#onRewindSucceeded} on success or + * {@link UploadDataSink#onRewindError} on failure. Neither read nor rewind + * will be called until one of those methods or the other is called. + * Even if the associated {@link UrlRequest} is canceled, one or the other + * must still be called before resources can be safely freed. Throwing an + * exception will also result in resources being freed and the request being + * errored out. + * + *

If rewinding is not supported, this should call + * {@link UploadDataSink#onRewindError}. Note that rewinding is required to + * follow redirects that preserve the upload body, and for retrying when the + * server times out stale sockets. + * + * @param uploadDataSink The object to notify when the rewind operation has + * completed, successfully or otherwise. + * @throws IOException if any IOException occurred during the process. + * {@link UrlRequest.Callback#onFailed} will be called with the + * thrown exception set as the cause of the + * {@link CallbackException}. + */ + public abstract void rewind(UploadDataSink uploadDataSink) throws IOException; + + /** + * Called when this UploadDataProvider is no longer needed by a request, so that resources + * (like a file) can be explicitly released. + * + * @throws IOException if any IOException occurred during the process. This will cause the + * request to fail if it is not yet complete; otherwise it will be logged. + */ + @Override + public void close() throws IOException {} +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/UploadDataProviders.java b/src/components/cronet/android/api/src/org/chromium/net/UploadDataProviders.java new file mode 100644 index 0000000000..5dc0e767ea --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/UploadDataProviders.java @@ -0,0 +1,186 @@ +// Copyright 2016 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.ParcelFileDescriptor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * Provides implementations of {@link UploadDataProvider} for common use cases. + */ +public final class UploadDataProviders { + /** + * Uploads an entire file. + * + * @param file The file to upload + * @return A new UploadDataProvider for the given file + */ + public static UploadDataProvider create(final File file) { + return new FileUploadProvider(new FileChannelProvider() { + @Override + public FileChannel getChannel() throws IOException { + return new FileInputStream(file).getChannel(); + } + }); + } + + /** + * Uploads an entire file, closing the descriptor when it is no longer needed. + * + * @param fd The file descriptor to upload + * @throws IllegalArgumentException if {@code fd} is not a file. + * @return A new UploadDataProvider for the given file descriptor + */ + public static UploadDataProvider create(final ParcelFileDescriptor fd) { + return new FileUploadProvider(new FileChannelProvider() { + @Override + public FileChannel getChannel() throws IOException { + if (fd.getStatSize() != -1) { + return new ParcelFileDescriptor.AutoCloseInputStream(fd).getChannel(); + } else { + fd.close(); + throw new IllegalArgumentException("Not a file: " + fd); + } + } + }); + } + + /** + * Uploads a ByteBuffer, from the current {@code buffer.position()} to {@code buffer.limit()} + * @param buffer The data to upload + * @return A new UploadDataProvider for the given buffer + */ + public static UploadDataProvider create(ByteBuffer buffer) { + return new ByteBufferUploadProvider(buffer.slice()); + } + + /** + * Uploads {@code length} bytes from {@code data}, starting from {@code offset} + * @param data Array containing data to upload + * @param offset Offset within data to start with + * @param length Number of bytes to upload + * @return A new UploadDataProvider for the given data + */ + public static UploadDataProvider create(byte[] data, int offset, int length) { + return new ByteBufferUploadProvider(ByteBuffer.wrap(data, offset, length).slice()); + } + + /** + * Uploads the contents of {@code data} + * @param data Array containing data to upload + * @return A new UploadDataProvider for the given data + */ + public static UploadDataProvider create(byte[] data) { + return create(data, 0, data.length); + } + + private interface FileChannelProvider { FileChannel getChannel() throws IOException; } + + private static final class FileUploadProvider extends UploadDataProvider { + private volatile FileChannel mChannel; + private final FileChannelProvider mProvider; + /** Guards initalization of {@code mChannel} */ + private final Object mLock = new Object(); + + private FileUploadProvider(FileChannelProvider provider) { + this.mProvider = provider; + } + + @Override + public long getLength() throws IOException { + return getChannel().size(); + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) throws IOException { + if (!byteBuffer.hasRemaining()) { + throw new IllegalStateException("Cronet passed a buffer with no bytes remaining"); + } + FileChannel channel = getChannel(); + int bytesRead = 0; + while (bytesRead == 0) { + int read = channel.read(byteBuffer); + if (read == -1) { + break; + } else { + bytesRead += read; + } + } + uploadDataSink.onReadSucceeded(false); + } + + @Override + public void rewind(UploadDataSink uploadDataSink) throws IOException { + getChannel().position(0); + uploadDataSink.onRewindSucceeded(); + } + + /** + * Lazily initializes the channel so that a blocking operation isn't performed on + * a non-executor thread. + */ + private FileChannel getChannel() throws IOException { + if (mChannel == null) { + synchronized (mLock) { + if (mChannel == null) { + mChannel = mProvider.getChannel(); + } + } + } + return mChannel; + } + + @Override + public void close() throws IOException { + FileChannel channel = mChannel; + if (channel != null) { + channel.close(); + } + } + } + + private static final class ByteBufferUploadProvider extends UploadDataProvider { + private final ByteBuffer mUploadBuffer; + + private ByteBufferUploadProvider(ByteBuffer uploadBuffer) { + this.mUploadBuffer = uploadBuffer; + } + + @Override + public long getLength() { + return mUploadBuffer.limit(); + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) { + if (!byteBuffer.hasRemaining()) { + throw new IllegalStateException("Cronet passed a buffer with no bytes remaining"); + } + if (byteBuffer.remaining() >= mUploadBuffer.remaining()) { + byteBuffer.put(mUploadBuffer); + } else { + int oldLimit = mUploadBuffer.limit(); + mUploadBuffer.limit(mUploadBuffer.position() + byteBuffer.remaining()); + byteBuffer.put(mUploadBuffer); + mUploadBuffer.limit(oldLimit); + } + uploadDataSink.onReadSucceeded(false); + } + + @Override + public void rewind(UploadDataSink uploadDataSink) { + mUploadBuffer.position(0); + uploadDataSink.onRewindSucceeded(); + } + } + + // Prevent instantiation + private UploadDataProviders() {} +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/UploadDataSink.java b/src/components/cronet/android/api/src/org/chromium/net/UploadDataSink.java new file mode 100644 index 0000000000..5474559635 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/UploadDataSink.java @@ -0,0 +1,36 @@ +// 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; + +/** + * Defines callbacks methods for {@link UploadDataProvider}. All methods + * may be called synchronously or asynchronously, on any thread. + */ +public abstract class UploadDataSink { + /** + * Called by {@link UploadDataProvider} when a read succeeds. + * @param finalChunk For chunked uploads, {@code true} if this is the final + * read. It must be {@code false} for non-chunked uploads. + */ + public abstract void onReadSucceeded(boolean finalChunk); + + /** + * Called by {@link UploadDataProvider} when a read fails. + * @param exception Exception passed on to the embedder. + */ + public abstract void onReadError(Exception exception); + + /** + * Called by {@link UploadDataProvider} when a rewind succeeds. + */ + public abstract void onRewindSucceeded(); + + /** + * Called by {@link UploadDataProvider} when a rewind fails, or if rewinding + * uploads is not supported. + * @param exception Exception passed on to the embedder. + */ + public abstract void onRewindError(Exception exception); +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/UrlRequest.java b/src/components/cronet/android/api/src/org/chromium/net/UrlRequest.java new file mode 100644 index 0000000000..d809677bd2 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/UrlRequest.java @@ -0,0 +1,422 @@ +// Copyright 2014 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 java.nio.ByteBuffer; +import java.util.concurrent.Executor; + +/** + * Controls an HTTP request (GET, PUT, POST etc). + * Created by {@link UrlRequest.Builder}, which can be obtained by calling + * {@link CronetEngine#newUrlRequestBuilder}. + * Note: All methods must be called on the {@link Executor} passed to + * {@link CronetEngine#newUrlRequestBuilder}. + */ +public abstract class UrlRequest { + /** + * Builder for {@link UrlRequest}s. Allows configuring requests before constructing them + * with {@link Builder#build}. The builder can be created by calling + * {@link CronetEngine#newUrlRequestBuilder}. + */ + public abstract static class Builder { + /** + * Sets the HTTP method verb to use for this request. + * + *

The default when this method is not called is "GET" if the request has + * no body or "POST" if it does. + * + * @param method "GET", "HEAD", "DELETE", "POST" or "PUT". + * @return the builder to facilitate chaining. + */ + public abstract Builder setHttpMethod(String method); + + /** + * Adds a request header. + * + * @param header header name. + * @param value header value. + * @return the builder to facilitate chaining. + */ + public abstract Builder addHeader(String header, String value); + + /** + * Disables cache for the request. If context is not set up to use cache, + * this call has no effect. + * @return the builder to facilitate chaining. + */ + public abstract Builder disableCache(); + + /** + * Lowest request priority. Passed to {@link #setPriority}. + */ + public static final int REQUEST_PRIORITY_IDLE = 0; + /** + * Very low request priority. Passed to {@link #setPriority}. + */ + public static final int REQUEST_PRIORITY_LOWEST = 1; + /** + * Low request priority. Passed to {@link #setPriority}. + */ + public static final int REQUEST_PRIORITY_LOW = 2; + /** + * Medium request priority. Passed to {@link #setPriority}. This is the + * default priority given to the request. + */ + public static final int REQUEST_PRIORITY_MEDIUM = 3; + /** + * Highest request priority. Passed to {@link #setPriority}. + */ + public static final int REQUEST_PRIORITY_HIGHEST = 4; + + /** + * Sets priority of the request which should be one of the + * {@link #REQUEST_PRIORITY_IDLE REQUEST_PRIORITY_*} values. + * The request is given {@link #REQUEST_PRIORITY_MEDIUM} priority if + * this method is not called. + * + * @param priority priority of the request which should be one of the + * {@link #REQUEST_PRIORITY_IDLE REQUEST_PRIORITY_*} values. + * @return the builder to facilitate chaining. + */ + public abstract Builder setPriority(int priority); + + /** + * Sets upload data provider. Switches method to "POST" if not + * explicitly set. Starting the request will throw an exception if a + * Content-Type header is not set. + * + * @param uploadDataProvider responsible for providing the upload data. + * @param executor All {@code uploadDataProvider} methods will be invoked + * using this {@code Executor}. May optionally be the same + * {@code Executor} the request itself is using. + * @return the builder to facilitate chaining. + */ + public abstract Builder setUploadDataProvider( + UploadDataProvider uploadDataProvider, Executor executor); + + /** + * Marks that the executors this request will use to notify callbacks (for + * {@code UploadDataProvider}s and {@code UrlRequest.Callback}s) is intentionally performing + * inline execution, like Guava's directExecutor or + * {@link java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy}. + * + *

Warning: This option makes it easy to accidentally block the network thread. + * It should not be used if your callbacks perform disk I/O, acquire locks, or call into + * other code you don't carefully control and audit. + */ + public abstract Builder allowDirectExecutor(); + + /** + * Creates a {@link UrlRequest} using configuration within this + * {@link Builder}. The returned {@code UrlRequest} can then be started + * by calling {@link UrlRequest#start}. + * + * @return constructed {@link UrlRequest} using configuration within + * this {@link Builder}. + */ + public abstract UrlRequest build(); + } + + /** + * Users of Cronet extend this class to receive callbacks indicating the + * progress of a {@link UrlRequest} being processed. An instance of this class + * is passed in to {@link UrlRequest.Builder}'s constructor when + * constructing the {@code UrlRequest}. + *

+ * Note: All methods will be invoked on the thread of the + * {@link java.util.concurrent.Executor} used during construction of the + * {@code UrlRequest}. + */ + public abstract static class Callback { + /** + * Invoked whenever a redirect is encountered. This will only be invoked + * between the call to {@link UrlRequest#start} and + * {@link Callback#onResponseStarted onResponseStarted()}. + * The body of the redirect response, if it has one, will be ignored. + * + * The redirect will not be followed until the URLRequest's + * {@link UrlRequest#followRedirect} method is called, either + * synchronously or asynchronously. + * + * @param request Request being redirected. + * @param info Response information. + * @param newLocationUrl Location where request is redirected. + * @throws Exception if an error occurs while processing a redirect. {@link #onFailed} + * will be called with the thrown exception set as the cause of the + * {@link CallbackException}. + */ + public abstract void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) throws Exception; + + /** + * Invoked when the final set of headers, after all redirects, is received. + * Will only be invoked once for each request. + * + * With the exception of {@link Callback#onCanceled onCanceled()}, + * no other {@link Callback} method will be invoked for the request, + * including {@link Callback#onSucceeded onSucceeded()} and {@link + * Callback#onFailed onFailed()}, until {@link UrlRequest#read + * UrlRequest.read()} is called to attempt to start reading the response + * body. + * + * @param request Request that started to get response. + * @param info Response information. + * @throws Exception if an error occurs while processing response start. {@link #onFailed} + * will be called with the thrown exception set as the cause of the + * {@link CallbackException}. + */ + public abstract void onResponseStarted(UrlRequest request, UrlResponseInfo info) + throws Exception; + + /** + * Invoked whenever part of the response body has been read. Only part of + * the buffer may be populated, even if the entire response body has not yet + * been consumed. + * + * With the exception of {@link Callback#onCanceled onCanceled()}, + * no other {@link Callback} method will be invoked for the request, + * including {@link Callback#onSucceeded onSucceeded()} and {@link + * Callback#onFailed onFailed()}, until {@link + * UrlRequest#read UrlRequest.read()} is called to attempt to continue + * reading the response body. + * + * @param request Request that received data. + * @param info Response information. + * @param byteBuffer The buffer that was passed in to + * {@link UrlRequest#read UrlRequest.read()}, now containing the + * received data. The buffer's position is updated to the end of + * the received data. The buffer's limit is not changed. + * @throws Exception if an error occurs while processing a read completion. + * {@link #onFailed} will be called with the thrown exception set as the cause of + * the {@link CallbackException}. + */ + public abstract void onReadCompleted( + UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) throws Exception; + + /** + * Invoked when request is completed successfully. Once invoked, no other + * {@link Callback} methods will be invoked. + * + * @param request Request that succeeded. + * @param info Response information. + */ + public abstract void onSucceeded(UrlRequest request, UrlResponseInfo info); + + /** + * Invoked if request failed for any reason after {@link UrlRequest#start}. + * Once invoked, no other {@link Callback} methods will be invoked. + * {@code error} provides information about the failure. + * + * @param request Request that failed. + * @param info Response information. May be {@code null} if no response was + * received. + * @param error information about error. + */ + public abstract void onFailed( + UrlRequest request, UrlResponseInfo info, CronetException error); + + /** + * Invoked if request was canceled via {@link UrlRequest#cancel}. Once + * invoked, no other {@link Callback} methods will be invoked. + * Default implementation takes no action. + * + * @param request Request that was canceled. + * @param info Response information. May be {@code null} if no response was + * received. + */ + public void onCanceled(UrlRequest request, UrlResponseInfo info) {} + } + + /** + * Request status values returned by {@link #getStatus}. + */ + public static class Status { + + /** + * This state indicates that the request is completed, canceled, or is not + * started. + */ + public static final int INVALID = -1; + /** + * This state corresponds to a resource load that has either not yet begun + * or is idle waiting for the consumer to do something to move things along + * (e.g. when the consumer of a {@link UrlRequest} has not called + * {@link UrlRequest#read read()} yet). + */ + public static final int IDLE = 0; + /** + * When a socket pool group is below the maximum number of sockets allowed + * per group, but a new socket cannot be created due to the per-pool socket + * limit, this state is returned by all requests for the group waiting on an + * idle connection, except those that may be serviced by a pending new + * connection. + */ + public static final int WAITING_FOR_STALLED_SOCKET_POOL = 1; + /** + * When a socket pool group has reached the maximum number of sockets + * allowed per group, this state is returned for all requests that don't + * have a socket, except those that correspond to a pending new connection. + */ + public static final int WAITING_FOR_AVAILABLE_SOCKET = 2; + /** + * This state indicates that the URLRequest delegate has chosen to block + * this request before it was sent over the network. + */ + public static final int WAITING_FOR_DELEGATE = 3; + /** + * This state corresponds to a resource load that is blocked waiting for + * access to a resource in the cache. If multiple requests are made for the + * same resource, the first request will be responsible for writing (or + * updating) the cache entry and the second request will be deferred until + * the first completes. This may be done to optimize for cache reuse. + */ + public static final int WAITING_FOR_CACHE = 4; + /** + * This state corresponds to a resource being blocked waiting for the + * PAC script to be downloaded. + */ + public static final int DOWNLOADING_PAC_FILE = 5; + /** + * This state corresponds to a resource load that is blocked waiting for a + * proxy autoconfig script to return a proxy server to use. + */ + public static final int RESOLVING_PROXY_FOR_URL = 6; + /** + * This state corresponds to a resource load that is blocked waiting for a + * proxy autoconfig script to return a proxy server to use, but that proxy + * script is busy resolving the IP address of a host. + */ + public static final int RESOLVING_HOST_IN_PAC_FILE = 7; + /** + * This state indicates that we're in the process of establishing a tunnel + * through the proxy server. + */ + public static final int ESTABLISHING_PROXY_TUNNEL = 8; + /** + * This state corresponds to a resource load that is blocked waiting for a + * host name to be resolved. This could either indicate resolution of the + * origin server corresponding to the resource or to the host name of a + * proxy server used to fetch the resource. + */ + public static final int RESOLVING_HOST = 9; + /** + * This state corresponds to a resource load that is blocked waiting for a + * TCP connection (or other network connection) to be established. HTTP + * requests that reuse a keep-alive connection skip this state. + */ + public static final int CONNECTING = 10; + /** + * This state corresponds to a resource load that is blocked waiting for the + * SSL handshake to complete. + */ + public static final int SSL_HANDSHAKE = 11; + /** + * This state corresponds to a resource load that is blocked waiting to + * completely upload a request to a server. In the case of a HTTP POST + * request, this state includes the period of time during which the message + * body is being uploaded. + */ + public static final int SENDING_REQUEST = 12; + /** + * This state corresponds to a resource load that is blocked waiting for the + * response to a network request. In the case of a HTTP transaction, this + * corresponds to the period after the request is sent and before all of the + * response headers have been received. + */ + public static final int WAITING_FOR_RESPONSE = 13; + /** + * This state corresponds to a resource load that is blocked waiting for a + * read to complete. In the case of a HTTP transaction, this corresponds to + * the period after the response headers have been received and before all + * of the response body has been downloaded. (NOTE: This state only applies + * for an {@link UrlRequest} while there is an outstanding + * {@link UrlRequest#read read()} operation.) + */ + public static final int READING_RESPONSE = 14; + + private Status() {} + } + + /** + * Listener class used with {@link #getStatus} to receive the status of a + * {@link UrlRequest}. + */ + public abstract static class StatusListener { + /** + * Invoked on {@link UrlRequest}'s {@link Executor}'s thread when request + * status is obtained. + * @param status integer representing the status of the request. It is + * one of the values defined in {@link Status}. + */ + public abstract void onStatus(int status); + } + + /** + * Starts the request, all callbacks go to {@link Callback}. May only be called + * once. May not be called if {@link #cancel} has been called. + */ + public abstract void start(); + + /** + * Follows a pending redirect. Must only be called at most once for each + * invocation of {@link Callback#onRedirectReceived + * onRedirectReceived()}. + */ + public abstract void followRedirect(); + + /** + * Attempts to read part of the response body into the provided buffer. + * Must only be called at most once in response to each invocation of the + * {@link Callback#onResponseStarted onResponseStarted()} and {@link + * Callback#onReadCompleted onReadCompleted()} methods of the {@link + * Callback}. Each call will result in an asynchronous call to + * either the {@link Callback Callback's} + * {@link Callback#onReadCompleted onReadCompleted()} method if data + * is read, its {@link Callback#onSucceeded onSucceeded()} method if + * there's no more data to read, or its {@link Callback#onFailed + * onFailed()} method if there's an error. + * + * @param buffer {@link ByteBuffer} to write response body to. Must be a + * direct ByteBuffer. The embedder must not read or modify buffer's + * position, limit, or data between its position and limit until the + * request calls back into the {@link Callback}. + */ + public abstract void read(ByteBuffer buffer); + + /** + * Cancels the request. Can be called at any time. + * {@link Callback#onCanceled onCanceled()} will be invoked when cancellation + * is complete and no further callback methods will be invoked. If the + * request has completed or has not started, calling {@code cancel()} has no + * effect and {@code onCanceled()} will not be invoked. If the + * {@link Executor} passed in during {@code UrlRequest} construction runs + * tasks on a single thread, and {@code cancel()} is called on that thread, + * no callback methods (besides {@code onCanceled()}) will be invoked after + * {@code cancel()} is called. Otherwise, at most one callback method may be + * invoked after {@code cancel()} has completed. + */ + public abstract void cancel(); + + /** + * Returns {@code true} if the request was successfully started and is now + * finished (completed, canceled, or failed). + * @return {@code true} if the request was successfully started and is now + * finished (completed, canceled, or failed). + */ + public abstract boolean isDone(); + + /** + * Queries the status of the request. + * @param listener a {@link StatusListener} that will be invoked with + * the request's current status. {@code listener} will be invoked + * back on the {@link Executor} passed in when the request was + * created. + */ + public abstract void getStatus(final StatusListener listener); + + // Note: There are deliberately no accessors for the results of the request + // here. Having none removes any ambiguity over when they are populated, + // particularly in the redirect case. +} diff --git a/src/components/cronet/android/api/src/org/chromium/net/UrlResponseInfo.java b/src/components/cronet/android/api/src/org/chromium/net/UrlResponseInfo.java new file mode 100644 index 0000000000..5c59950b96 --- /dev/null +++ b/src/components/cronet/android/api/src/org/chromium/net/UrlResponseInfo.java @@ -0,0 +1,121 @@ +// 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 java.util.List; +import java.util.Map; + +/** + * Basic information about a response. Included in {@link UrlRequest.Callback} callbacks. + * Each {@link UrlRequest.Callback#onRedirectReceived onRedirectReceived()} + * callback gets a different copy of {@code UrlResponseInfo} describing a particular redirect + * response. + */ +public abstract class UrlResponseInfo { + /** + * Unmodifiable container of response headers or trailers. + * {@hide}. + */ + public abstract static class HeaderBlock { + /** + * Returns an unmodifiable list of the response header field and value pairs. + * The headers are in the same order they are received over the wire. + * + * @return an unmodifiable list of response header field and value pairs + */ + public abstract List> getAsList(); + + /** + * Returns an unmodifiable map from response-header field names to lists of values. + * Each list of values for a single header field is in the same order they + * were received over the wire. + * + * @return an unmodifiable map from response-header field names to lists of values + */ + public abstract Map> getAsMap(); + } + + /** + * Returns the URL the response is for. This is the URL after following + * redirects, so it may not be the originally requested URL. + * @return the URL the response is for. + */ + public abstract String getUrl(); + + /** + * Returns the URL chain. The first entry is the originally requested URL; + * the following entries are redirects followed. + * @return the URL chain. + */ + public abstract List getUrlChain(); + + /** + * Returns the HTTP status code. When a resource is retrieved from the cache, + * whether it was revalidated or not, the original status code is returned. + * @return the HTTP status code. + */ + public abstract int getHttpStatusCode(); + + /** + * Returns the HTTP status text of the status line. For example, if the + * request received a "HTTP/1.1 200 OK" response, this method returns "OK". + * @return the HTTP status text of the status line. + */ + public abstract String getHttpStatusText(); + + /** + * Returns an unmodifiable list of response header field and value pairs. + * The headers are in the same order they are received over the wire. + * @return an unmodifiable list of response header field and value pairs. + */ + public abstract List> getAllHeadersAsList(); + + /** + * Returns an unmodifiable map of the response-header fields and values. + * Each list of values for a single header field is in the same order they + * were received over the wire. + * @return an unmodifiable map of the response-header fields and values. + */ + public abstract Map> getAllHeaders(); + + /** + * Returns {@code true} if the response came from the cache, including + * requests that were revalidated over the network before being retrieved + * from the cache. + * @return {@code true} if the response came from the cache, {@code false} + * otherwise. + */ + public abstract boolean wasCached(); + + /** + * Returns the protocol (for example 'quic/1+spdy/3') negotiated with the server. + * Returns an empty string if no protocol was negotiated, the protocol is + * not known, or when using plain HTTP or HTTPS. + * @return the protocol negotiated with the server. + */ + // TODO(mef): Figure out what this returns in the cached case, both with + // and without a revalidation request. + public abstract String getNegotiatedProtocol(); + + /** + * Returns the proxy server that was used for the request. + * @return the proxy server that was used for the request. + */ + public abstract String getProxyServer(); + + /** + * Returns a minimum count of bytes received from the network to process this + * request. This count may ignore certain overheads (for example IP and TCP/UDP framing, + * SSL handshake and framing, proxy handling). This count is taken prior to decompression + * (for example GZIP) and includes headers and data from all redirects. + * + * This value may change (even for one {@link UrlResponseInfo} instance) as the request + * progresses until completion, when {@link UrlRequest.Callback#onSucceeded onSucceeded()}, + * {@link UrlRequest.Callback#onFailed onFailed()}, or + * {@link UrlRequest.Callback#onCanceled onCanceled()} is called. + * @return a minimum count of bytes received from the network to process this request. + */ + public abstract long getReceivedByteCount(); +} diff --git a/src/components/cronet/android/api_version.txt b/src/components/cronet/android/api_version.txt new file mode 100644 index 0000000000..8351c19397 --- /dev/null +++ b/src/components/cronet/android/api_version.txt @@ -0,0 +1 @@ +14 diff --git a/src/components/cronet/android/cronet_bidirectional_stream_adapter.cc b/src/components/cronet/android/cronet_bidirectional_stream_adapter.cc new file mode 100644 index 0000000000..08bfa576a6 --- /dev/null +++ b/src/components/cronet/android/cronet_bidirectional_stream_adapter.cc @@ -0,0 +1,514 @@ +// 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 +#include +#include + +#include "base/bind.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/strings/abseil_string_conversions.h" +#include "base/strings/string_number_conversions.h" +#include "components/cronet/android/cronet_jni_headers/CronetBidirectionalStream_jni.h" +#include "components/cronet/android/cronet_url_request_context_adapter.h" +#include "components/cronet/android/io_buffer_with_byte_buffer.h" +#include "components/cronet/android/url_request_error.h" +#include "components/cronet/metrics_util.h" +#include "net/base/http_user_agent_settings.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/ssl/ssl_info.h" +#include "net/third_party/quiche/src/quic/core/quic_packets.h" +#include "net/third_party/quiche/src/spdy/core/spdy_header_block.h" +#include "net/url_request/url_request_context.h" +#include "url/gurl.h" + +using base::android::ConvertUTF8ToJavaString; +using base::android::ConvertJavaStringToUTF8; +using base::android::JavaRef; +using base::android::ScopedJavaLocalRef; + +namespace cronet { + +namespace { + +// As |GetArrayLength| makes no guarantees about the returned value (e.g., it +// may be -1 if |array| is not a valid Java array), provide a safe wrapper +// that always returns a valid, non-negative size. +template +size_t SafeGetArrayLength(JNIEnv* env, JavaArrayType jarray) { + DCHECK(jarray); + jsize length = env->GetArrayLength(jarray); + DCHECK_GE(length, 0) << "Invalid array length: " << length; + return static_cast(std::max(0, length)); +} + +} // namespace + +PendingWriteData::PendingWriteData( + JNIEnv* env, + const JavaRef& jwrite_buffer_list, + const JavaRef& jwrite_buffer_pos_list, + const JavaRef& jwrite_buffer_limit_list, + jboolean jwrite_end_of_stream) { + this->jwrite_buffer_list.Reset(jwrite_buffer_list); + this->jwrite_buffer_pos_list.Reset(jwrite_buffer_pos_list); + this->jwrite_buffer_limit_list.Reset(jwrite_buffer_limit_list); + this->jwrite_end_of_stream = jwrite_end_of_stream; +} + +PendingWriteData::~PendingWriteData() { + // Reset global references. + jwrite_buffer_list.Reset(); + jwrite_buffer_pos_list.Reset(); + jwrite_buffer_limit_list.Reset(); +} + +static jlong JNI_CronetBidirectionalStream_CreateBidirectionalStream( + JNIEnv* env, + const base::android::JavaParamRef& jbidi_stream, + jlong jurl_request_context_adapter, + jboolean jsend_request_headers_automatically, + jboolean jenable_metrics, + jboolean jtraffic_stats_tag_set, + jint jtraffic_stats_tag, + jboolean jtraffic_stats_uid_set, + jint jtraffic_stats_uid) { + CronetURLRequestContextAdapter* context_adapter = + reinterpret_cast( + jurl_request_context_adapter); + DCHECK(context_adapter); + + CronetBidirectionalStreamAdapter* adapter = + new CronetBidirectionalStreamAdapter( + context_adapter, env, jbidi_stream, + jsend_request_headers_automatically, jenable_metrics, + jtraffic_stats_tag_set, jtraffic_stats_tag, jtraffic_stats_uid_set, + jtraffic_stats_uid); + + return reinterpret_cast(adapter); +} + +CronetBidirectionalStreamAdapter::CronetBidirectionalStreamAdapter( + CronetURLRequestContextAdapter* context, + JNIEnv* env, + const base::android::JavaParamRef& jbidi_stream, + bool send_request_headers_automatically, + bool enable_metrics, + bool traffic_stats_tag_set, + int32_t traffic_stats_tag, + bool traffic_stats_uid_set, + int32_t traffic_stats_uid) + : context_(context), + owner_(env, jbidi_stream), + send_request_headers_automatically_(send_request_headers_automatically), + enable_metrics_(enable_metrics), + traffic_stats_tag_set_(traffic_stats_tag_set), + traffic_stats_tag_(traffic_stats_tag), + traffic_stats_uid_set_(traffic_stats_uid_set), + traffic_stats_uid_(traffic_stats_uid), + stream_failed_(false) {} + +CronetBidirectionalStreamAdapter::~CronetBidirectionalStreamAdapter() { + DCHECK(context_->IsOnNetworkThread()); +} + +void CronetBidirectionalStreamAdapter::SendRequestHeaders( + JNIEnv* env, + const base::android::JavaParamRef& jcaller) { + context_->PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce( + &CronetBidirectionalStreamAdapter::SendRequestHeadersOnNetworkThread, + base::Unretained(this))); +} + +jint CronetBidirectionalStreamAdapter::Start( + JNIEnv* env, + const base::android::JavaParamRef& jcaller, + const base::android::JavaParamRef& jurl, + jint jpriority, + const base::android::JavaParamRef& jmethod, + const base::android::JavaParamRef& jheaders, + jboolean jend_of_stream) { + // Prepare request info here to be able to return the error. + std::unique_ptr request_info( + new net::BidirectionalStreamRequestInfo()); + request_info->url = GURL(ConvertJavaStringToUTF8(env, jurl)); + request_info->priority = static_cast(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 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; + if (traffic_stats_tag_set_ || traffic_stats_uid_set_) { + request_info->socket_tag = net::SocketTag( + traffic_stats_uid_set_ ? traffic_stats_uid_ : net::SocketTag::UNSET_UID, + traffic_stats_tag_set_ ? traffic_stats_tag_ + : net::SocketTag::UNSET_TAG); + } + + context_->PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce(&CronetBidirectionalStreamAdapter::StartOnNetworkThread, + base::Unretained(this), std::move(request_info))); + return 0; +} + +jboolean CronetBidirectionalStreamAdapter::ReadData( + JNIEnv* env, + const base::android::JavaParamRef& jcaller, + const base::android::JavaParamRef& jbyte_buffer, + jint jposition, + jint jlimit) { + DCHECK_LT(jposition, jlimit); + + void* data = env->GetDirectBufferAddress(jbyte_buffer); + if (!data) + return JNI_FALSE; + + scoped_refptr read_buffer( + new IOBufferWithByteBuffer(env, jbyte_buffer, data, jposition, jlimit)); + + int remaining_capacity = jlimit - jposition; + + context_->PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce(&CronetBidirectionalStreamAdapter::ReadDataOnNetworkThread, + base::Unretained(this), read_buffer, remaining_capacity)); + return JNI_TRUE; +} + +jboolean CronetBidirectionalStreamAdapter::WritevData( + JNIEnv* env, + const base::android::JavaParamRef& jcaller, + const base::android::JavaParamRef& jbyte_buffers, + const base::android::JavaParamRef& jbyte_buffers_pos, + const base::android::JavaParamRef& jbyte_buffers_limit, + jboolean jend_of_stream) { + size_t buffers_array_size = SafeGetArrayLength(env, jbyte_buffers.obj()); + size_t pos_array_size = SafeGetArrayLength(env, jbyte_buffers.obj()); + size_t limit_array_size = SafeGetArrayLength(env, jbyte_buffers.obj()); + if (buffers_array_size != pos_array_size || + buffers_array_size != limit_array_size) { + DLOG(ERROR) << "Illegal arguments."; + return JNI_FALSE; + } + + std::unique_ptr pending_write_data; + pending_write_data.reset( + new PendingWriteData(env, jbyte_buffers, jbyte_buffers_pos, + jbyte_buffers_limit, jend_of_stream)); + for (size_t i = 0; i < buffers_array_size; ++i) { + ScopedJavaLocalRef jbuffer( + env, env->GetObjectArrayElement( + pending_write_data->jwrite_buffer_list.obj(), i)); + void* data = env->GetDirectBufferAddress(jbuffer.obj()); + if (!data) + return JNI_FALSE; + jint pos; + env->GetIntArrayRegion(pending_write_data->jwrite_buffer_pos_list.obj(), i, + 1, &pos); + jint limit; + env->GetIntArrayRegion(pending_write_data->jwrite_buffer_limit_list.obj(), + i, 1, &limit); + DCHECK_LE(pos, limit); + scoped_refptr write_buffer = + base::MakeRefCounted(static_cast(data) + + pos); + pending_write_data->write_buffer_list.push_back(write_buffer); + pending_write_data->write_buffer_len_list.push_back(limit - pos); + } + + context_->PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce( + &CronetBidirectionalStreamAdapter::WritevDataOnNetworkThread, + base::Unretained(this), std::move(pending_write_data))); + return JNI_TRUE; +} + +void CronetBidirectionalStreamAdapter::Destroy( + JNIEnv* env, + const base::android::JavaParamRef& 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::BindOnce(&CronetBidirectionalStreamAdapter::DestroyOnNetworkThread, + base::Unretained(this), jsend_on_canceled)); +} + +void CronetBidirectionalStreamAdapter::OnStreamReady( + bool request_headers_sent) { + DCHECK(context_->IsOnNetworkThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetBidirectionalStream_onStreamReady( + env, owner_, request_headers_sent ? JNI_TRUE : JNI_FALSE); +} + +void CronetBidirectionalStreamAdapter::OnHeadersReceived( + const spdy::Http2HeaderBlock& 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(base::StringViewToStringPiece(http_status_header->second), + &http_status_code); + + std::string protocol; + switch (bidi_stream_->GetProtocol()) { + case net::kProtoHTTP2: + protocol = "h2"; + break; + case net::kProtoQUIC: + protocol = "quic/1+spdy/3"; + break; + default: + break; + } + + cronet::Java_CronetBidirectionalStream_onResponseHeadersReceived( + env, owner_, http_status_code, ConvertUTF8ToJavaString(env, protocol), + GetHeadersArray(env, response_headers), + bidi_stream_->GetTotalReceivedBytes()); +} + +void CronetBidirectionalStreamAdapter::OnDataRead(int bytes_read) { + DCHECK(context_->IsOnNetworkThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetBidirectionalStream_onReadCompleted( + env, owner_, 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()); + DCHECK(pending_write_data_); + + JNIEnv* env = base::android::AttachCurrentThread(); + // Call into Java. + cronet::Java_CronetBidirectionalStream_onWritevCompleted( + env, owner_, pending_write_data_->jwrite_buffer_list, + pending_write_data_->jwrite_buffer_pos_list, + pending_write_data_->jwrite_buffer_limit_list, + pending_write_data_->jwrite_end_of_stream); + // Free the java objects. This lets the Java ByteBuffers be freed, if the + // embedder releases it, too. + pending_write_data_.reset(); +} + +void CronetBidirectionalStreamAdapter::OnTrailersReceived( + const spdy::Http2HeaderBlock& response_trailers) { + DCHECK(context_->IsOnNetworkThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetBidirectionalStream_onResponseTrailersReceived( + env, owner_, GetHeadersArray(env, response_trailers)); +} + +void CronetBidirectionalStreamAdapter::OnFailed(int error) { + DCHECK(context_->IsOnNetworkThread()); + stream_failed_ = true; + JNIEnv* env = base::android::AttachCurrentThread(); + net::NetErrorDetails net_error_details; + bidi_stream_->PopulateNetErrorDetails(&net_error_details); + cronet::Java_CronetBidirectionalStream_onError( + env, owner_, NetErrorToUrlRequestError(error), error, + net_error_details.quic_connection_error, + ConvertUTF8ToJavaString(env, net::ErrorToString(error)), + bidi_stream_->GetTotalReceivedBytes()); +} + +void CronetBidirectionalStreamAdapter::StartOnNetworkThread( + std::unique_ptr request_info) { + DCHECK(context_->IsOnNetworkThread()); + DCHECK(!bidi_stream_); + + request_info->detect_broken_connection = + context_->cronet_url_request_context() + ->bidi_stream_detect_broken_connection(); + request_info->heartbeat_interval = + context_->cronet_url_request_context()->heartbeat_interval(); + request_info->extra_headers.SetHeaderIfMissing( + net::HttpRequestHeaders::kUserAgent, context_->GetURLRequestContext() + ->http_user_agent_settings() + ->GetUserAgent()); + bidi_stream_.reset(new net::BidirectionalStream( + std::move(request_info), context_->GetURLRequestContext() + ->http_transaction_factory() + ->GetSession(), + send_request_headers_automatically_, this)); +} + +void CronetBidirectionalStreamAdapter::SendRequestHeadersOnNetworkThread() { + DCHECK(context_->IsOnNetworkThread()); + DCHECK(!send_request_headers_automatically_); + + if (stream_failed_) { + // If stream failed between the time when SendRequestHeaders is invoked and + // SendRequestHeadersOnNetworkThread is executed, do not call into + // |bidi_stream_| since the underlying stream might have been destroyed. + // Do not invoke Java callback either, since onError is posted when + // |stream_failed_| is set to true. + return; + } + bidi_stream_->SendRequestHeaders(); +} + +void CronetBidirectionalStreamAdapter::ReadDataOnNetworkThread( + scoped_refptr 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::WritevDataOnNetworkThread( + std::unique_ptr pending_write_data) { + DCHECK(context_->IsOnNetworkThread()); + DCHECK(pending_write_data); + DCHECK(!pending_write_data_); + + if (stream_failed_) { + // If stream failed between the time when WritevData is invoked and + // WritevDataOnNetworkThread is executed, do not call into |bidi_stream_| + // since the underlying stream might have been destroyed. Do not invoke + // Java callback either, since onError is posted when |stream_failed_| is + // set to true. + return; + } + + pending_write_data_ = std::move(pending_write_data); + bool end_of_stream = pending_write_data_->jwrite_end_of_stream == JNI_TRUE; + bidi_stream_->SendvData(pending_write_data_->write_buffer_list, + pending_write_data_->write_buffer_len_list, + 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_); + } + MaybeReportMetrics(); + delete this; +} + +base::android::ScopedJavaLocalRef +CronetBidirectionalStreamAdapter::GetHeadersArray( + JNIEnv* env, + const spdy::Http2HeaderBlock& header_block) { + DCHECK(context_->IsOnNetworkThread()); + + std::vector headers; + for (const auto& header : header_block) { + auto value = std::string(header.second); + size_t start = 0; + size_t end = 0; + // The do loop will split headers by '\0' so that applications can skip it. + do { + end = value.find('\0', start); + std::string split_value; + if (end != value.npos) { + split_value = value.substr(start, end - start); + } else { + split_value = value.substr(start); + } + headers.push_back(std::string(header.first)); + headers.push_back(split_value); + start = end + 1; + } while (end != value.npos); + } + return base::android::ToJavaArrayOfStrings(env, headers); +} + +void CronetBidirectionalStreamAdapter::MaybeReportMetrics() { + if (!enable_metrics_) + return; + + if (!bidi_stream_) + return; + net::LoadTimingInfo load_timing_info; + bidi_stream_->GetLoadTimingInfo(&load_timing_info); + JNIEnv* env = base::android::AttachCurrentThread(); + base::Time start_time = load_timing_info.request_start_time; + base::TimeTicks start_ticks = load_timing_info.request_start; + cronet::Java_CronetBidirectionalStream_onMetricsCollected( + env, owner_, + metrics_util::ConvertTime(start_ticks, start_ticks, start_time), + metrics_util::ConvertTime(load_timing_info.connect_timing.dns_start, + start_ticks, start_time), + metrics_util::ConvertTime(load_timing_info.connect_timing.dns_end, + start_ticks, start_time), + metrics_util::ConvertTime(load_timing_info.connect_timing.connect_start, + start_ticks, start_time), + metrics_util::ConvertTime(load_timing_info.connect_timing.connect_end, + start_ticks, start_time), + metrics_util::ConvertTime(load_timing_info.connect_timing.ssl_start, + start_ticks, start_time), + metrics_util::ConvertTime(load_timing_info.connect_timing.ssl_end, + start_ticks, start_time), + metrics_util::ConvertTime(load_timing_info.send_start, start_ticks, + start_time), + metrics_util::ConvertTime(load_timing_info.send_end, start_ticks, + start_time), + metrics_util::ConvertTime(load_timing_info.push_start, start_ticks, + start_time), + metrics_util::ConvertTime(load_timing_info.push_end, start_ticks, + start_time), + metrics_util::ConvertTime(load_timing_info.receive_headers_end, + start_ticks, start_time), + metrics_util::ConvertTime(base::TimeTicks::Now(), start_ticks, + start_time), + load_timing_info.socket_reused, bidi_stream_->GetTotalSentBytes(), + bidi_stream_->GetTotalReceivedBytes()); +} + +} // namespace cronet diff --git a/src/components/cronet/android/cronet_bidirectional_stream_adapter.h b/src/components/cronet/android/cronet_bidirectional_stream_adapter.h new file mode 100644 index 0000000000..70aee9a33f --- /dev/null +++ b/src/components/cronet/android/cronet_bidirectional_stream_adapter.h @@ -0,0 +1,197 @@ +// 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 + +#include + +#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/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "net/http/bidirectional_stream.h" +#include "net/third_party/quiche/src/spdy/core/spdy_header_block.h" + +namespace net { +struct BidirectionalStreamRequestInfo; +} // namespace net + +namespace cronet { + +class CronetURLRequestContextAdapter; +class IOBufferWithByteBuffer; + +// Convenient wrapper to hold Java references and data to represent the pending +// data to be written. +struct PendingWriteData { + PendingWriteData( + JNIEnv* env, + const base::android::JavaRef& jwrite_buffer_list, + const base::android::JavaRef& jwrite_buffer_pos_list, + const base::android::JavaRef& jwrite_buffer_limit_list, + jboolean jwrite_end_of_stream); + + PendingWriteData(const PendingWriteData&) = delete; + PendingWriteData& operator=(const PendingWriteData&) = delete; + + ~PendingWriteData(); + + // Arguments passed in from Java. Retain a global ref so they won't get GC-ed + // until the corresponding onWriteCompleted is invoked. + base::android::ScopedJavaGlobalRef jwrite_buffer_list; + base::android::ScopedJavaGlobalRef jwrite_buffer_pos_list; + base::android::ScopedJavaGlobalRef jwrite_buffer_limit_list; + // A copy of the end of stream flag passed in from Java. + jboolean jwrite_end_of_stream; + // Every IOBuffer in |write_buffer_list| points to the memory owned by the + // corresponding Java ByteBuffer in |jwrite_buffer_list|. + std::vector> write_buffer_list; + // A list of the length of each IOBuffer in |write_buffer_list|. + std::vector write_buffer_len_list; +}; + +// An adapter from Java BidirectionalStream object to net::BidirectionalStream. +// Created and configured from a Java thread. Start, ReadData, WritevData and +// Destroy can be called on any thread (including network thread), and post +// calls to corresponding {Start|ReadData|WritevData|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: + CronetBidirectionalStreamAdapter( + CronetURLRequestContextAdapter* context, + JNIEnv* env, + const base::android::JavaParamRef& jbidi_stream, + bool jsend_request_headers_automatically, + bool enable_metrics, + bool traffic_stats_tag_set, + int32_t traffic_stats_tag, + bool traffic_stats_uid_set, + int32_t traffic_stats_uid); + + CronetBidirectionalStreamAdapter(const CronetBidirectionalStreamAdapter&) = + delete; + CronetBidirectionalStreamAdapter& operator=( + const CronetBidirectionalStreamAdapter&) = delete; + + ~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& jcaller, + const base::android::JavaParamRef& jurl, + jint jpriority, + const base::android::JavaParamRef& jmethod, + const base::android::JavaParamRef& jheaders, + jboolean jend_of_stream); + + // Sends request headers to server. + // When |send_request_headers_automatically_| is + // false and OnStreamReady() is invoked with request_headers_sent = false, + // headers will be combined with next WriteData/WritevData unless this + // method is called first, in which case headers will be sent separately + // without delay. + // (This method cannot be called when |send_request_headers_automatically_| is + // true nor when OnStreamReady() is invoked with request_headers_sent = true, + // since headers have been sent by the stream when stream is negotiated + // successfully.) + void SendRequestHeaders(JNIEnv* env, + const base::android::JavaParamRef& jcaller); + + // 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& jcaller, + const base::android::JavaParamRef& jbyte_buffer, + jint jposition, + jint jlimit); + + // Writes more data from |jbyte_buffers|. For the i_th buffer in + // |jbyte_buffers|, bytes to write start from i_th position in |jpositions| + // and end at i_th limit in |jlimits|. + // 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 WritevData( + JNIEnv* env, + const base::android::JavaParamRef& jcaller, + const base::android::JavaParamRef& jbyte_buffers, + const base::android::JavaParamRef& jpositions, + const base::android::JavaParamRef& jlimits, + 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& jcaller, + jboolean jsend_on_canceled); + + private: + // net::BidirectionalStream::Delegate implementations: + void OnStreamReady(bool request_headers_sent) override; + void OnHeadersReceived( + const spdy::Http2HeaderBlock& response_headers) override; + void OnDataRead(int bytes_read) override; + void OnDataSent() override; + void OnTrailersReceived(const spdy::Http2HeaderBlock& trailers) override; + void OnFailed(int error) override; + + void StartOnNetworkThread( + std::unique_ptr request_info); + void SendRequestHeadersOnNetworkThread(); + void ReadDataOnNetworkThread( + scoped_refptr read_buffer, + int buffer_size); + void WritevDataOnNetworkThread( + std::unique_ptr pending_write_data); + void DestroyOnNetworkThread(bool send_on_canceled); + // Gets headers as a Java array. + base::android::ScopedJavaLocalRef GetHeadersArray( + JNIEnv* env, + const spdy::Http2HeaderBlock& header_block); + // Helper method to report metrics to the Java layer. + void MaybeReportMetrics(); + const raw_ptr context_; + + // Java object that owns this CronetBidirectionalStreamAdapter. + base::android::ScopedJavaGlobalRef owner_; + const bool send_request_headers_automatically_; + // Whether metrics collection is enabled when |this| is created. + const bool enable_metrics_; + // Whether |traffic_stats_tag_| should be applied. + const bool traffic_stats_tag_set_; + // TrafficStats tag to apply to URLRequest. + const int32_t traffic_stats_tag_; + // Whether |traffic_stats_uid_| should be applied. + const bool traffic_stats_uid_set_; + // UID to be applied to URLRequest. + const int32_t traffic_stats_uid_; + + scoped_refptr read_buffer_; + std::unique_ptr pending_write_data_; + std::unique_ptr bidi_stream_; + + // Whether BidirectionalStream::Delegate::OnFailed callback is invoked. + bool stream_failed_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_CRONET_BIDIRECTIONAL_STREAM_ADAPTER_H_ diff --git a/src/components/cronet/android/cronet_impl_common_proguard.cfg b/src/components/cronet/android/cronet_impl_common_proguard.cfg new file mode 100644 index 0000000000..1ea091b7c6 --- /dev/null +++ b/src/components/cronet/android/cronet_impl_common_proguard.cfg @@ -0,0 +1 @@ +# Proguard config for apps that depend on cronet_impl_common_java.jar. diff --git a/src/components/cronet/android/cronet_impl_fake_proguard.cfg b/src/components/cronet/android/cronet_impl_fake_proguard.cfg new file mode 100644 index 0000000000..52dc93ac7c --- /dev/null +++ b/src/components/cronet/android/cronet_impl_fake_proguard.cfg @@ -0,0 +1,6 @@ +# Proguard config for apps that depend on cronet_impl_fake_java.jar. + +# This constructor is called using the reflection from Cronet API (cronet_api.jar). +-keep class org.chromium.net.test.FakeCronetProvider { + public (android.content.Context); +} diff --git a/src/components/cronet/android/cronet_impl_native_proguard.cfg b/src/components/cronet/android/cronet_impl_native_proguard.cfg new file mode 100644 index 0000000000..cb368e7675 --- /dev/null +++ b/src/components/cronet/android/cronet_impl_native_proguard.cfg @@ -0,0 +1,32 @@ +# Proguard config for apps that depend on cronet_impl_native_java.jar. + +# This constructor is called using the reflection from Cronet API (cronet_api.jar). +-keep class org.chromium.net.impl.NativeCronetProvider { + public (android.content.Context); +} + +# Suppress unnecessary warnings. +-dontnote org.chromium.net.ProxyChangeListener$ProxyReceiver +-dontnote org.chromium.net.AndroidKeyStore +# Needs 'void setTextAppearance(int)' (API level 23). +-dontwarn org.chromium.base.ApiCompatibilityUtils +# Needs 'boolean onSearchRequested(android.view.SearchEvent)' (API level 23). +-dontwarn org.chromium.base.WindowCallbackWrapper + +# Generated for chrome apk and not included into cronet. +-dontwarn org.chromium.base.multidex.ChromiumMultiDexInstaller +-dontwarn org.chromium.base.library_loader.LibraryLoader +-dontwarn org.chromium.base.SysUtils +-dontwarn org.chromium.build.NativeLibraries + +# Objects of this type are passed around by native code, but the class +# is never used directly by native code. Since the class is not loaded, it does +# not need to be preserved as an entry point. +-dontnote org.chromium.net.UrlRequest$ResponseHeadersMap +# https://android.googlesource.com/platform/sdk/+/marshmallow-mr1-release/files/proguard-android.txt#54 +-dontwarn android.support.** + +# This class should be explicitly kept to avoid failure if +# class/merging/horizontal proguard optimization is enabled. +-keep class org.chromium.base.CollectionUtil + diff --git a/src/components/cronet/android/cronet_impl_platform_proguard.cfg b/src/components/cronet/android/cronet_impl_platform_proguard.cfg new file mode 100644 index 0000000000..8f9a61abf6 --- /dev/null +++ b/src/components/cronet/android/cronet_impl_platform_proguard.cfg @@ -0,0 +1,6 @@ +# Proguard config for apps that depend on cronet_impl_platform_java.jar. + +# This constructor is called using the reflection from Cronet API (cronet_api.jar). +-keep class org.chromium.net.impl.JavaCronetProvider { + public (android.content.Context); +} diff --git a/src/components/cronet/android/cronet_integrated_mode_state.cc b/src/components/cronet/android/cronet_integrated_mode_state.cc new file mode 100644 index 0000000000..9b688730bb --- /dev/null +++ b/src/components/cronet/android/cronet_integrated_mode_state.cc @@ -0,0 +1,31 @@ +// Copyright 2018 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 "components/cronet/android/cronet_integrated_mode_state.h" + +#include "base/atomicops.h" + +namespace cronet { +namespace { + +base::subtle::AtomicWord g_integrated_mode_network_task_runner = 0; + +} // namespace + +void SetIntegratedModeNetworkTaskRunner( + base::SingleThreadTaskRunner* network_task_runner) { + CHECK_EQ(base::subtle::Release_CompareAndSwap( + &g_integrated_mode_network_task_runner, 0, + reinterpret_cast(network_task_runner)), + 0); +} + +base::SingleThreadTaskRunner* GetIntegratedModeNetworkTaskRunner() { + base::subtle::AtomicWord task_runner = + base::subtle::Acquire_Load(&g_integrated_mode_network_task_runner); + CHECK(task_runner); + return reinterpret_cast(task_runner); +} + +} // namespace cronet diff --git a/src/components/cronet/android/cronet_integrated_mode_state.h b/src/components/cronet/android/cronet_integrated_mode_state.h new file mode 100644 index 0000000000..320a9966d9 --- /dev/null +++ b/src/components/cronet/android/cronet_integrated_mode_state.h @@ -0,0 +1,29 @@ +// Copyright 2018 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_INTEGRATED_MODE_STATE_H_ +#define COMPONENTS_CRONET_ANDROID_CRONET_INTEGRATED_MODE_STATE_H_ + +#include "base/task/thread_pool/thread_pool_instance.h" + +namespace cronet { + +/** + * Set a shared network task runner into Cronet in integrated mode. All the + * Cronet network tasks would be running in this task runner. This method should + * be invoked in native side before creating Cronet instance. + */ +void SetIntegratedModeNetworkTaskRunner( + base::SingleThreadTaskRunner* network_task_runner); + +/** + * Get the task runner for Cronet integrated mode. It would be invoked in the + * initialization of CronetURLRequestContext. This method must be invoked after + * SetIntegratedModeNetworkTaskRunner. + */ +base::SingleThreadTaskRunner* GetIntegratedModeNetworkTaskRunner(); + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_CRONET_INTEGRATED_MODE_STATE_H_ diff --git a/src/components/cronet/android/cronet_jni.cc b/src/components/cronet/android/cronet_jni.cc new file mode 100644 index 0000000000..c081f6e994 --- /dev/null +++ b/src/components/cronet/android/cronet_jni.cc @@ -0,0 +1,15 @@ +// Copyright 2014 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 "components/cronet/android/cronet_library_loader.h" + +// This is called by the VM when the shared library is first loaded. +extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) { + return cronet::CronetOnLoad(vm, reserved); +} + +extern "C" void JNI_OnUnLoad(JavaVM* vm, void* reserved) { + cronet::CronetOnUnLoad(vm, reserved); +} + diff --git a/src/components/cronet/android/cronet_library_loader.cc b/src/components/cronet/android/cronet_library_loader.cc new file mode 100644 index 0000000000..54b4a12a4e --- /dev/null +++ b/src/components/cronet/android/cronet_library_loader.cc @@ -0,0 +1,221 @@ +// Copyright 2014 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 +#include +#include +#include + +#include "base/android/base_jni_onload.h" +#include "base/android/build_info.h" +#include "base/android/jni_android.h" +#include "base/android/jni_registrar.h" +#include "base/android/jni_string.h" +#include "base/android/jni_utils.h" +#include "base/android/library_loader/library_loader_hooks.h" +#include "base/check_op.h" +#include "base/feature_list.h" +#include "base/message_loop/message_pump_type.h" +#include "base/synchronization/waitable_event.h" +#include "base/task/current_thread.h" +#include "base/task/single_thread_task_executor.h" +#include "base/task/thread_pool/thread_pool_instance.h" +#include "build/build_config.h" +#include "components/cronet/android/buildflags.h" +#include "components/cronet/android/cronet_jni_headers/CronetLibraryLoader_jni.h" +#include "components/cronet/cronet_global_state.h" +#include "components/cronet/version.h" +#include "net/android/network_change_notifier_factory_android.h" +#include "net/base/network_change_notifier.h" +#include "net/proxy_resolution/configured_proxy_resolution_service.h" +#include "net/proxy_resolution/proxy_config_service_android.h" +#include "third_party/zlib/zlib.h" +#include "url/buildflags.h" + +#if !BUILDFLAG(USE_PLATFORM_ICU_ALTERNATIVES) +#include "base/i18n/icu_util.h" // nogncheck +#endif + +#if !BUILDFLAG(INTEGRATED_MODE) +#include "components/cronet/android/cronet_jni_registration.h" +#include "components/cronet/android/cronet_library_loader.h" +#endif + +using base::android::JavaParamRef; +using base::android::ScopedJavaLocalRef; + +namespace cronet { +namespace { + +// SingleThreadTaskExecutor on the init thread, which is where objects that +// receive Java notifications generally live. +base::SingleThreadTaskExecutor* g_init_task_executor = nullptr; + +#if !BUILDFLAG(INTEGRATED_MODE) +std::unique_ptr g_network_change_notifier; +#endif + +base::WaitableEvent g_init_thread_init_done( + base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED); + +void NativeInit() { +// In integrated mode, ICU and FeatureList has been initialized by the host. +#if !BUILDFLAG(INTEGRATED_MODE) +#if !BUILDFLAG(USE_PLATFORM_ICU_ALTERNATIVES) + base::i18n::InitializeICU(); +#endif + base::FeatureList::InitializeInstance(std::string(), std::string()); +#endif + + if (!base::ThreadPoolInstance::Get()) + base::ThreadPoolInstance::CreateAndStartWithDefaultParams("Cronet"); +} + +} // namespace + +bool OnInitThread() { + DCHECK(g_init_task_executor); + return g_init_task_executor->task_runner()->RunsTasksInCurrentSequence(); +} + +// In integrated mode, Cronet native library is built and loaded together with +// the native library of the host app. +#if !BUILDFLAG(INTEGRATED_MODE) +// Checks the available version of JNI. Also, caches Java reflection artifacts. +jint CronetOnLoad(JavaVM* vm, void* reserved) { + base::android::InitVM(vm); + JNIEnv* env = base::android::AttachCurrentThread(); + if (!RegisterMainDexNatives(env) || !RegisterNonMainDexNatives(env)) { + return -1; + } + if (!base::android::OnJNIOnLoadInit()) + return -1; + NativeInit(); + return JNI_VERSION_1_6; +} + +void CronetOnUnLoad(JavaVM* jvm, void* reserved) { + if (base::ThreadPoolInstance::Get()) + base::ThreadPoolInstance::Get()->Shutdown(); + + base::android::LibraryLoaderExitHook(); +} +#endif + +void JNI_CronetLibraryLoader_CronetInitOnInitThread(JNIEnv* env) { + // Initialize SingleThreadTaskExecutor for init thread. + DCHECK(!base::CurrentThread::IsSet()); + DCHECK(!g_init_task_executor); + g_init_task_executor = + new base::SingleThreadTaskExecutor(base::MessagePumpType::JAVA); + +// In integrated mode, NetworkChangeNotifier has been initialized by the host. +#if BUILDFLAG(INTEGRATED_MODE) + CHECK(!net::NetworkChangeNotifier::CreateIfNeeded()); +#else + DCHECK(!g_network_change_notifier); + if (!net::NetworkChangeNotifier::GetFactory()) { + net::NetworkChangeNotifier::SetFactory( + new net::NetworkChangeNotifierFactoryAndroid()); + } + g_network_change_notifier = net::NetworkChangeNotifier::CreateIfNeeded(); + DCHECK(g_network_change_notifier); +#endif + + g_init_thread_init_done.Signal(); +} + +ScopedJavaLocalRef JNI_CronetLibraryLoader_GetCronetVersion( + JNIEnv* env) { +#if defined(ARCH_CPU_ARM64) + // Attempt to avoid crashes on some ARM64 Marshmallow devices by + // prompting zlib ARM feature detection early on. https://crbug.com/853725 + if (base::android::BuildInfo::GetInstance()->sdk_int() == + base::android::SDK_VERSION_MARSHMALLOW) { + crc32(0, Z_NULL, 0); + } +#endif + return base::android::ConvertUTF8ToJavaString(env, CRONET_VERSION); +} + +void PostTaskToInitThread(const base::Location& posted_from, + base::OnceClosure task) { + g_init_thread_init_done.Wait(); + g_init_task_executor->task_runner()->PostTask(posted_from, std::move(task)); +} + +void EnsureInitialized() { + if (g_init_task_executor) { + // Ensure that init is done on the init thread. + g_init_thread_init_done.Wait(); + return; + } + + // The initialization can only be done once, so static |s_run_once| variable + // is used to do it in the constructor. + static class RunOnce { + public: + RunOnce() { + NativeInit(); + JNIEnv* env = base::android::AttachCurrentThread(); + // Ensure initialized from Java side to properly create Init thread. + cronet::Java_CronetLibraryLoader_ensureInitializedFromNative(env); + } + } s_run_once; +} + +std::unique_ptr CreateProxyConfigService( + const scoped_refptr& io_task_runner) { + std::unique_ptr service = + net::ConfiguredProxyResolutionService::CreateSystemProxyConfigService( + io_task_runner); + // If a PAC URL is present, ignore it and use the address and port of + // Android system's local HTTP proxy server. See: crbug.com/432539. + // TODO(csharrison) Architect the wrapper better so we don't need to cast for + // android ProxyConfigServices. + net::ProxyConfigServiceAndroid* android_proxy_config_service = + static_cast(service.get()); + android_proxy_config_service->set_exclude_pac_url(true); + return service; +} + +// Creates a proxy resolution service appropriate for this platform. +std::unique_ptr CreateProxyResolutionService( + std::unique_ptr proxy_config_service, + net::NetLog* net_log) { + // Android provides a local HTTP proxy server that handles proxying when a PAC + // URL is present. Create a proxy service without a resolver and rely on this + // local HTTP proxy. See: crbug.com/432539. + return net::ConfiguredProxyResolutionService::CreateWithoutProxyResolver( + std::move(proxy_config_service), net_log); +} + +// Creates default User-Agent request value, combining optional +// |partial_user_agent| with system-dependent values. +std::string CreateDefaultUserAgent(const std::string& partial_user_agent) { + // Cronet global state must be initialized to include application info + // into default user agent + cronet::EnsureInitialized(); + + JNIEnv* env = base::android::AttachCurrentThread(); + std::string user_agent = base::android::ConvertJavaStringToUTF8( + cronet::Java_CronetLibraryLoader_getDefaultUserAgent(env)); + if (!partial_user_agent.empty()) + user_agent.insert(user_agent.size() - 1, "; " + partial_user_agent); + return user_agent; +} + +void SetNetworkThreadPriorityOnNetworkThread(double priority) { + int priority_int = priority; + DCHECK_LE(priority_int, 19); + DCHECK_GE(priority_int, -20); + if (priority_int >= -20 && priority_int <= 19) { + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetLibraryLoader_setNetworkThreadPriorityOnNetworkThread( + env, priority_int); + } +} + +} // namespace cronet diff --git a/src/components/cronet/android/cronet_library_loader.h b/src/components/cronet/android/cronet_library_loader.h new file mode 100644 index 0000000000..7e3ab3dff2 --- /dev/null +++ b/src/components/cronet/android/cronet_library_loader.h @@ -0,0 +1,17 @@ +// Copyright 2014 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_LIBRARY_LOADER_H_ +#define COMPONENTS_CRONET_ANDROID_CRONET_LIBRARY_LOADER_H_ + +#include + +namespace cronet { + +jint CronetOnLoad(JavaVM* vm, void* reserved); +void CronetOnUnLoad(JavaVM* jvm, void* reserved); + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_CRONET_LIBRARY_LOADER_H_ diff --git a/src/components/cronet/android/cronet_upload_data_stream_adapter.cc b/src/components/cronet/android/cronet_upload_data_stream_adapter.cc new file mode 100644 index 0000000000..70bda62289 --- /dev/null +++ b/src/components/cronet/android/cronet_upload_data_stream_adapter.cc @@ -0,0 +1,147 @@ +// 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 "components/cronet/android/cronet_upload_data_stream_adapter.h" + +#include +#include +#include + +#include "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/bind.h" +#include "base/check_op.h" +#include "base/task/single_thread_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/cronet/android/cronet_jni_headers/CronetUploadDataStream_jni.h" +#include "components/cronet/android/cronet_url_request_adapter.h" +#include "components/cronet/android/io_buffer_with_byte_buffer.h" + +using base::android::JavaParamRef; + +namespace cronet { + +CronetUploadDataStreamAdapter::CronetUploadDataStreamAdapter( + JNIEnv* env, + jobject jupload_data_stream) { + jupload_data_stream_.Reset(env, jupload_data_stream); +} + +CronetUploadDataStreamAdapter::~CronetUploadDataStreamAdapter() { +} + +void CronetUploadDataStreamAdapter::InitializeOnNetworkThread( + base::WeakPtr upload_data_stream) { + DCHECK(!upload_data_stream_); + DCHECK(!network_task_runner_.get()); + + upload_data_stream_ = upload_data_stream; + network_task_runner_ = base::ThreadTaskRunnerHandle::Get(); + DCHECK(network_task_runner_); +} + +void CronetUploadDataStreamAdapter::Read(scoped_refptr buffer, + int buf_len) { + DCHECK(upload_data_stream_); + DCHECK(network_task_runner_); + DCHECK(network_task_runner_->BelongsToCurrentThread()); + DCHECK_GT(buf_len, 0); + + JNIEnv* env = base::android::AttachCurrentThread(); + // Allow buffer reuse if |buffer| and |buf_len| are exactly the same as the + // ones used last time. + if (!(buffer_ && buffer_->io_buffer()->data() == buffer->data() && + buffer_->io_buffer_len() == buf_len)) { + buffer_ = std::make_unique(env, std::move(buffer), + buf_len); + } + Java_CronetUploadDataStream_readData(env, jupload_data_stream_, + buffer_->byte_buffer()); +} + +void CronetUploadDataStreamAdapter::Rewind() { + DCHECK(upload_data_stream_); + DCHECK(network_task_runner_->BelongsToCurrentThread()); + + JNIEnv* env = base::android::AttachCurrentThread(); + Java_CronetUploadDataStream_rewind(env, jupload_data_stream_); +} + +void CronetUploadDataStreamAdapter::OnUploadDataStreamDestroyed() { + // If CronetUploadDataStream::InitInternal was never called, + // |upload_data_stream_| and |network_task_runner_| will be NULL. + DCHECK(!network_task_runner_ || + network_task_runner_->BelongsToCurrentThread()); + + JNIEnv* env = base::android::AttachCurrentThread(); + Java_CronetUploadDataStream_onUploadDataStreamDestroyed(env, + jupload_data_stream_); + // |this| is invalid here since the Java call above effectively destroys it. +} + +void CronetUploadDataStreamAdapter::OnReadSucceeded( + JNIEnv* env, + const JavaParamRef& jcaller, + int bytes_read, + bool final_chunk) { + DCHECK(bytes_read > 0 || (final_chunk && bytes_read == 0)); + + network_task_runner_->PostTask( + FROM_HERE, base::BindOnce(&CronetUploadDataStream::OnReadSuccess, + upload_data_stream_, bytes_read, final_chunk)); +} + +void CronetUploadDataStreamAdapter::OnRewindSucceeded( + JNIEnv* env, + const JavaParamRef& jcaller) { + network_task_runner_->PostTask( + FROM_HERE, base::BindOnce(&CronetUploadDataStream::OnRewindSuccess, + upload_data_stream_)); +} + +void CronetUploadDataStreamAdapter::Destroy(JNIEnv* env) { + delete this; +} + +static jlong JNI_CronetUploadDataStream_AttachUploadDataToRequest( + JNIEnv* env, + const JavaParamRef& jupload_data_stream, + jlong jcronet_url_request_adapter, + jlong jlength) { + CronetURLRequestAdapter* request_adapter = + reinterpret_cast(jcronet_url_request_adapter); + DCHECK(request_adapter != nullptr); + + CronetUploadDataStreamAdapter* adapter = + new CronetUploadDataStreamAdapter(env, jupload_data_stream); + + std::unique_ptr upload_data_stream( + new CronetUploadDataStream(adapter, jlength)); + + request_adapter->SetUpload(std::move(upload_data_stream)); + + return reinterpret_cast(adapter); +} + +static jlong JNI_CronetUploadDataStream_CreateAdapterForTesting( + JNIEnv* env, + const JavaParamRef& jupload_data_stream) { + CronetUploadDataStreamAdapter* adapter = + new CronetUploadDataStreamAdapter(env, jupload_data_stream); + return reinterpret_cast(adapter); +} + +static jlong JNI_CronetUploadDataStream_CreateUploadDataStreamForTesting( + JNIEnv* env, + const JavaParamRef& jupload_data_stream, + jlong jlength, + jlong jadapter) { + CronetUploadDataStreamAdapter* adapter = + reinterpret_cast(jadapter); + CronetUploadDataStream* upload_data_stream = + new CronetUploadDataStream(adapter, jlength); + return reinterpret_cast(upload_data_stream); +} + +} // namespace cronet diff --git a/src/components/cronet/android/cronet_upload_data_stream_adapter.h b/src/components/cronet/android/cronet_upload_data_stream_adapter.h new file mode 100644 index 0000000000..81b21d2728 --- /dev/null +++ b/src/components/cronet/android/cronet_upload_data_stream_adapter.h @@ -0,0 +1,79 @@ +// 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_UPLOAD_DATA_STREAM_ADAPTER_H_ +#define COMPONENTS_CRONET_ANDROID_CRONET_UPLOAD_DATA_STREAM_ADAPTER_H_ + +#include + +#include "base/android/scoped_java_ref.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "components/cronet/cronet_upload_data_stream.h" +#include "net/base/io_buffer.h" + +namespace base { +class SingleThreadTaskRunner; +} // namespace base + +namespace cronet { +class ByteBufferWithIOBuffer; + +// The Adapter holds onto a reference to the IOBuffer that is currently being +// written to in Java, so may not be deleted until any read operation in Java +// has completed. +// +// The Adapter is owned by the Java CronetUploadDataStream, and also owns a +// reference to it. The Adapter is only destroyed after the net::URLRequest +// destroys the C++ CronetUploadDataStream and the Java CronetUploadDataStream +// has no read operation pending, at which point it also releases its reference +// to the Java CronetUploadDataStream. +// +// Failures don't go back through the Adapter, but directly to the Java request +// object, since normally reads aren't allowed to fail during an upload. +class CronetUploadDataStreamAdapter : public CronetUploadDataStream::Delegate { + public: + CronetUploadDataStreamAdapter(JNIEnv* env, jobject jupload_data_stream); + + CronetUploadDataStreamAdapter(const CronetUploadDataStreamAdapter&) = delete; + CronetUploadDataStreamAdapter& operator=( + const CronetUploadDataStreamAdapter&) = delete; + + ~CronetUploadDataStreamAdapter() override; + + // CronetUploadDataStream::Delegate implementation. Called on network thread. + void InitializeOnNetworkThread( + base::WeakPtr upload_data_stream) override; + void Read(scoped_refptr buffer, int buf_len) override; + void Rewind() override; + void OnUploadDataStreamDestroyed() override; + + // Callbacks from Java, called on some Java thread. + void OnReadSucceeded(JNIEnv* env, + const base::android::JavaParamRef& obj, + int bytes_read, + bool final_chunk); + void OnRewindSucceeded(JNIEnv* env, + const base::android::JavaParamRef& obj); + + // Destroys |this|. Can be called from any thread, but needs to be protected + // by the adapter lock. + void Destroy(JNIEnv* env); + + private: + // Initialized on construction, effectively constant. + base::android::ScopedJavaGlobalRef jupload_data_stream_; + + // These are initialized in InitializeOnNetworkThread, so are safe to access + // during Java callbacks, which all happen after initialization. + scoped_refptr network_task_runner_; + base::WeakPtr upload_data_stream_; + + // Keeps the net::IOBuffer and Java ByteBuffer alive until the next Read(). + std::unique_ptr buffer_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_CRONET_UPLOAD_DATA_STREAM_ADAPTER_H_ diff --git a/src/components/cronet/android/cronet_url_request_adapter.cc b/src/components/cronet/android/cronet_url_request_adapter.cc new file mode 100644 index 0000000000..d65ed58b6c --- /dev/null +++ b/src/components/cronet/android/cronet_url_request_adapter.cc @@ -0,0 +1,326 @@ +// Copyright 2014 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 "components/cronet/android/cronet_url_request_adapter.h" + +#include +#include +#include + +#include "base/bind.h" +#include "base/location.h" +#include "base/logging.h" +#include "components/cronet/android/cronet_jni_headers/CronetUrlRequest_jni.h" +#include "components/cronet/android/cronet_url_request_context_adapter.h" +#include "components/cronet/android/io_buffer_with_byte_buffer.h" +#include "components/cronet/android/url_request_error.h" +#include "components/cronet/metrics_util.h" +#include "net/base/idempotency.h" +#include "net/base/load_flags.h" +#include "net/base/load_states.h" +#include "net/base/net_errors.h" +#include "net/base/proxy_server.h" +#include "net/base/request_priority.h" +#include "net/base/upload_data_stream.h" +#include "net/cert/cert_status_flags.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_status_code.h" +#include "net/http/http_util.h" +#include "net/ssl/ssl_info.h" +#include "net/third_party/quiche/src/quic/core/quic_packets.h" +#include "net/url_request/redirect_info.h" +#include "net/url_request/url_request_context.h" + +using base::android::ConvertUTF8ToJavaString; +using base::android::JavaParamRef; + +namespace { + +base::android::ScopedJavaLocalRef ConvertResponseHeadersToJava( + JNIEnv* env, + const net::HttpResponseHeaders* headers) { + std::vector response_headers; + // Returns an empty array if |headers| is nullptr. + if (headers != nullptr) { + size_t iter = 0; + std::string header_name; + std::string header_value; + while (headers->EnumerateHeaderLines(&iter, &header_name, &header_value)) { + response_headers.push_back(header_name); + response_headers.push_back(header_value); + } + } + return base::android::ToJavaArrayOfStrings(env, response_headers); +} + +} // namespace + +namespace cronet { + +static jlong JNI_CronetUrlRequest_CreateRequestAdapter( + JNIEnv* env, + const JavaParamRef& jurl_request, + jlong jurl_request_context_adapter, + const JavaParamRef& jurl_string, + jint jpriority, + jboolean jdisable_cache, + jboolean jdisable_connection_migration, + jboolean jenable_metrics, + jboolean jtraffic_stats_tag_set, + jint jtraffic_stats_tag, + jboolean jtraffic_stats_uid_set, + jint jtraffic_stats_uid, + jint jidempotency) { + CronetURLRequestContextAdapter* context_adapter = + reinterpret_cast( + jurl_request_context_adapter); + DCHECK(context_adapter); + + GURL url(base::android::ConvertJavaStringToUTF8(env, jurl_string)); + + VLOG(1) << "New chromium network request_adapter: " + << url.possibly_invalid_spec(); + + CronetURLRequestAdapter* adapter = new CronetURLRequestAdapter( + context_adapter, env, jurl_request, url, + static_cast(jpriority), jdisable_cache, + jdisable_connection_migration, jenable_metrics, jtraffic_stats_tag_set, + jtraffic_stats_tag, jtraffic_stats_uid_set, jtraffic_stats_uid, + static_cast(jidempotency)); + + return reinterpret_cast(adapter); +} + +CronetURLRequestAdapter::CronetURLRequestAdapter( + CronetURLRequestContextAdapter* context, + JNIEnv* env, + jobject jurl_request, + const GURL& url, + net::RequestPriority priority, + jboolean jdisable_cache, + jboolean jdisable_connection_migration, + jboolean jenable_metrics, + jboolean jtraffic_stats_tag_set, + jint jtraffic_stats_tag, + jboolean jtraffic_stats_uid_set, + jint jtraffic_stats_uid, + net::Idempotency idempotency) + : request_( + new CronetURLRequest(context->cronet_url_request_context(), + std::unique_ptr(this), + url, + priority, + jdisable_cache == JNI_TRUE, + jdisable_connection_migration == JNI_TRUE, + jenable_metrics == JNI_TRUE, + jtraffic_stats_tag_set == JNI_TRUE, + jtraffic_stats_tag, + jtraffic_stats_uid_set == JNI_TRUE, + jtraffic_stats_uid, + idempotency)) { + owner_.Reset(env, jurl_request); +} + +CronetURLRequestAdapter::~CronetURLRequestAdapter() { +} + +jboolean CronetURLRequestAdapter::SetHttpMethod( + JNIEnv* env, + const JavaParamRef& jcaller, + const JavaParamRef& jmethod) { + std::string method(base::android::ConvertJavaStringToUTF8(env, jmethod)); + return request_->SetHttpMethod(method) ? JNI_TRUE : JNI_FALSE; +} + +jboolean CronetURLRequestAdapter::AddRequestHeader( + JNIEnv* env, + const JavaParamRef& jcaller, + const JavaParamRef& jname, + const JavaParamRef& jvalue) { + std::string name(base::android::ConvertJavaStringToUTF8(env, jname)); + std::string value(base::android::ConvertJavaStringToUTF8(env, jvalue)); + return request_->AddRequestHeader(name, value) ? JNI_TRUE : JNI_FALSE; +} + +void CronetURLRequestAdapter::SetUpload( + std::unique_ptr upload) { + request_->SetUpload(std::move(upload)); +} + +void CronetURLRequestAdapter::Start(JNIEnv* env, + const JavaParamRef& jcaller) { + request_->Start(); +} + +void CronetURLRequestAdapter::GetStatus( + JNIEnv* env, + const JavaParamRef& jcaller, + const JavaParamRef& jstatus_listener) { + base::android::ScopedJavaGlobalRef status_listener_ref; + status_listener_ref.Reset(env, jstatus_listener); + request_->GetStatus(base::BindOnce(&CronetURLRequestAdapter::OnStatus, + base::Unretained(this), + status_listener_ref)); +} + +void CronetURLRequestAdapter::FollowDeferredRedirect( + JNIEnv* env, + const JavaParamRef& jcaller) { + request_->FollowDeferredRedirect(); +} + +jboolean CronetURLRequestAdapter::ReadData( + JNIEnv* env, + const JavaParamRef& jcaller, + const JavaParamRef& jbyte_buffer, + jint jposition, + jint jlimit) { + DCHECK_LT(jposition, jlimit); + + void* data = env->GetDirectBufferAddress(jbyte_buffer); + if (!data) + return JNI_FALSE; + + IOBufferWithByteBuffer* read_buffer = + new IOBufferWithByteBuffer(env, jbyte_buffer, data, jposition, jlimit); + + int remaining_capacity = jlimit - jposition; + request_->ReadData(read_buffer, remaining_capacity); + return JNI_TRUE; +} + +void CronetURLRequestAdapter::Destroy(JNIEnv* env, + const JavaParamRef& 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. + request_->Destroy(jsend_on_canceled == JNI_TRUE); +} + +void CronetURLRequestAdapter::OnReceivedRedirect( + const std::string& new_location, + int http_status_code, + const std::string& http_status_text, + const net::HttpResponseHeaders* headers, + bool was_cached, + const std::string& negotiated_protocol, + const std::string& proxy_server, + int64_t received_byte_count) { + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetUrlRequest_onRedirectReceived( + env, owner_, ConvertUTF8ToJavaString(env, new_location), http_status_code, + ConvertUTF8ToJavaString(env, http_status_text), + ConvertResponseHeadersToJava(env, headers), + was_cached ? JNI_TRUE : JNI_FALSE, + ConvertUTF8ToJavaString(env, negotiated_protocol), + ConvertUTF8ToJavaString(env, proxy_server), received_byte_count); +} + +void CronetURLRequestAdapter::OnResponseStarted( + int http_status_code, + const std::string& http_status_text, + const net::HttpResponseHeaders* headers, + bool was_cached, + const std::string& negotiated_protocol, + const std::string& proxy_server, + int64_t received_byte_count) { + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetUrlRequest_onResponseStarted( + env, owner_, http_status_code, + ConvertUTF8ToJavaString(env, http_status_text), + ConvertResponseHeadersToJava(env, headers), + was_cached ? JNI_TRUE : JNI_FALSE, + ConvertUTF8ToJavaString(env, negotiated_protocol), + ConvertUTF8ToJavaString(env, proxy_server), received_byte_count); +} + +void CronetURLRequestAdapter::OnReadCompleted( + scoped_refptr buffer, + int bytes_read, + int64_t received_byte_count) { + IOBufferWithByteBuffer* read_buffer = + reinterpret_cast(buffer.get()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetUrlRequest_onReadCompleted( + env, owner_, read_buffer->byte_buffer(), bytes_read, + read_buffer->initial_position(), read_buffer->initial_limit(), + received_byte_count); +} + +void CronetURLRequestAdapter::OnSucceeded(int64_t received_byte_count) { + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetUrlRequest_onSucceeded(env, owner_, received_byte_count); +} + +void CronetURLRequestAdapter::OnError(int net_error, + int quic_error, + const std::string& error_string, + int64_t received_byte_count) { + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetUrlRequest_onError( + env, owner_, NetErrorToUrlRequestError(net_error), net_error, quic_error, + ConvertUTF8ToJavaString(env, error_string), received_byte_count); +} + +void CronetURLRequestAdapter::OnCanceled() { + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetUrlRequest_onCanceled(env, owner_); +} + +void CronetURLRequestAdapter::OnDestroyed() { + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetUrlRequest_onNativeAdapterDestroyed(env, owner_); + // |this| adapter will be destroyed by the owner after return from this call. +} + +void CronetURLRequestAdapter::OnStatus( + const base::android::ScopedJavaGlobalRef& status_listener_ref, + net::LoadState load_status) { + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_CronetUrlRequest_onStatus(env, owner_, status_listener_ref, + load_status); +} + +void CronetURLRequestAdapter::OnMetricsCollected( + const base::Time& start_time, + const base::TimeTicks& start_ticks, + const base::TimeTicks& dns_start, + const base::TimeTicks& dns_end, + const base::TimeTicks& connect_start, + const base::TimeTicks& connect_end, + const base::TimeTicks& ssl_start, + const base::TimeTicks& ssl_end, + const base::TimeTicks& send_start, + const base::TimeTicks& send_end, + const base::TimeTicks& push_start, + const base::TimeTicks& push_end, + const base::TimeTicks& receive_headers_end, + const base::TimeTicks& request_end, + bool socket_reused, + int64_t sent_bytes_count, + int64_t received_bytes_count) { + JNIEnv* env = base::android::AttachCurrentThread(); + Java_CronetUrlRequest_onMetricsCollected( + env, owner_, + metrics_util::ConvertTime(start_ticks, start_ticks, start_time), + metrics_util::ConvertTime(dns_start, start_ticks, start_time), + metrics_util::ConvertTime(dns_end, start_ticks, start_time), + metrics_util::ConvertTime(connect_start, start_ticks, start_time), + metrics_util::ConvertTime(connect_end, start_ticks, start_time), + metrics_util::ConvertTime(ssl_start, start_ticks, start_time), + metrics_util::ConvertTime(ssl_end, start_ticks, start_time), + metrics_util::ConvertTime(send_start, start_ticks, start_time), + metrics_util::ConvertTime(send_end, start_ticks, start_time), + metrics_util::ConvertTime(push_start, start_ticks, start_time), + metrics_util::ConvertTime(push_end, start_ticks, start_time), + metrics_util::ConvertTime(receive_headers_end, start_ticks, start_time), + metrics_util::ConvertTime(request_end, start_ticks, start_time), + socket_reused ? JNI_TRUE : JNI_FALSE, sent_bytes_count, + received_bytes_count); +} + +} // namespace cronet diff --git a/src/components/cronet/android/cronet_url_request_adapter.h b/src/components/cronet/android/cronet_url_request_adapter.h new file mode 100644 index 0000000000..a5141b5089 --- /dev/null +++ b/src/components/cronet/android/cronet_url_request_adapter.h @@ -0,0 +1,167 @@ +// Copyright 2014 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_URL_REQUEST_ADAPTER_H_ +#define COMPONENTS_CRONET_ANDROID_CRONET_URL_REQUEST_ADAPTER_H_ + +#include + +#include +#include + +#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/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/time/time.h" +#include "components/cronet/cronet_url_request.h" +#include "url/gurl.h" + +namespace net { +enum LoadState; +class UploadDataStream; +} // namespace net + +namespace cronet { + +class CronetURLRequestContextAdapter; +class TestUtil; + +// An adapter from Java CronetUrlRequest object to native CronetURLRequest. +// Created and configured from a Java thread. Start, ReadData, and Destroy are +// posted to network thread and all callbacks into the Java CronetUrlRequest are +// done on the network thread. Java CronetUrlRequest is expected to initiate the +// next step like FollowDeferredRedirect, ReadData or Destroy. Public methods +// can be called on any thread. +class CronetURLRequestAdapter : public CronetURLRequest::Callback { + public: + // Bypasses cache if |jdisable_cache| is true. If context is not set up to + // use cache, |jdisable_cache| has no effect. |jdisable_connection_migration| + // causes connection migration to be disabled for this request if true. If + // global connection migration flag is not enabled, + // |jdisable_connection_migration| has no effect. + CronetURLRequestAdapter(CronetURLRequestContextAdapter* context, + JNIEnv* env, + jobject jurl_request, + const GURL& url, + net::RequestPriority priority, + jboolean jdisable_cache, + jboolean jdisable_connection_migration, + jboolean jenable_metrics, + jboolean jtraffic_stats_tag_set, + jint jtraffic_stats_tag, + jboolean jtraffic_stats_uid_set, + jint jtraffic_stats_uid, + net::Idempotency idempotency); + + CronetURLRequestAdapter(const CronetURLRequestAdapter&) = delete; + CronetURLRequestAdapter& operator=(const CronetURLRequestAdapter&) = delete; + + ~CronetURLRequestAdapter() override; + + // Methods called prior to Start are never called on network thread. + + // Sets the request method GET, POST etc. + jboolean SetHttpMethod(JNIEnv* env, + const base::android::JavaParamRef& jcaller, + const base::android::JavaParamRef& jmethod); + + // Adds a header to the request before it starts. + jboolean AddRequestHeader(JNIEnv* env, + const base::android::JavaParamRef& jcaller, + const base::android::JavaParamRef& jname, + const base::android::JavaParamRef& jvalue); + + // Adds a request body to the request before it starts. + void SetUpload(std::unique_ptr upload); + + // Starts the request. + void Start(JNIEnv* env, const base::android::JavaParamRef& jcaller); + + void GetStatus(JNIEnv* env, + const base::android::JavaParamRef& jcaller, + const base::android::JavaParamRef& jstatus_listener); + + // Follows redirect. + void FollowDeferredRedirect( + JNIEnv* env, + const base::android::JavaParamRef& jcaller); + + // Reads more data. + jboolean ReadData(JNIEnv* env, + const base::android::JavaParamRef& jcaller, + const base::android::JavaParamRef& jbyte_buffer, + jint jposition, + jint jcapacity); + + // Releases all resources for the request and deletes the object itself. + // |jsend_on_canceled| indicates if Java onCanceled callback should be + // issued to indicate when no more callbacks will be issued. + void Destroy(JNIEnv* env, + const base::android::JavaParamRef& jcaller, + jboolean jsend_on_canceled); + + // CronetURLRequest::Callback implementations: + void OnReceivedRedirect(const std::string& new_location, + int http_status_code, + const std::string& http_status_text, + const net::HttpResponseHeaders* headers, + bool was_cached, + const std::string& negotiated_protocol, + const std::string& proxy_server, + int64_t received_byte_count) override; + void OnResponseStarted(int http_status_code, + const std::string& http_status_text, + const net::HttpResponseHeaders* headers, + bool was_cached, + const std::string& negotiated_protocol, + const std::string& proxy_server, + int64_t received_byte_count) override; + void OnReadCompleted(scoped_refptr buffer, + int bytes_read, + int64_t received_byte_count) override; + void OnSucceeded(int64_t received_byte_count) override; + void OnError(int net_error, + int quic_error, + const std::string& error_string, + int64_t received_byte_count) override; + void OnCanceled() override; + void OnDestroyed() override; + void OnMetricsCollected(const base::Time& request_start_time, + const base::TimeTicks& request_start, + const base::TimeTicks& dns_start, + const base::TimeTicks& dns_end, + const base::TimeTicks& connect_start, + const base::TimeTicks& connect_end, + const base::TimeTicks& ssl_start, + const base::TimeTicks& ssl_end, + const base::TimeTicks& send_start, + const base::TimeTicks& send_end, + const base::TimeTicks& push_start, + const base::TimeTicks& push_end, + const base::TimeTicks& receive_headers_end, + const base::TimeTicks& request_end, + bool socket_reused, + int64_t sent_bytes_count, + int64_t received_bytes_count) override; + + void OnStatus( + const base::android::ScopedJavaGlobalRef& status_listener_ref, + net::LoadState load_status); + + private: + friend class TestUtil; + + // Native Cronet URL Request that owns |this|. + raw_ptr request_; + + // Java object that owns this CronetURLRequestContextAdapter. + base::android::ScopedJavaGlobalRef owner_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_CRONET_URL_REQUEST_ADAPTER_H_ diff --git a/src/components/cronet/android/cronet_url_request_context_adapter.cc b/src/components/cronet/android/cronet_url_request_context_adapter.cc new file mode 100644 index 0000000000..6be1dac5c1 --- /dev/null +++ b/src/components/cronet/android/cronet_url_request_context_adapter.cc @@ -0,0 +1,357 @@ +// Copyright 2014 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 "components/cronet/android/cronet_url_request_context_adapter.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "base/android/jni_android.h" +#include "base/android/jni_array.h" +#include "base/android/jni_string.h" +#include "base/base64.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_file.h" +#include "base/lazy_instance.h" +#include "base/logging.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_macros.h" +#include "base/task/single_thread_task_runner.h" +#include "base/threading/thread_restrictions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "base/values.h" +#include "components/cronet/android/buildflags.h" +#include "components/cronet/android/cronet_jni_headers/CronetUrlRequestContext_jni.h" +#include "components/cronet/android/cronet_library_loader.h" +#include "components/cronet/cronet_prefs_manager.h" +#include "components/cronet/host_cache_persistence_manager.h" +#include "components/cronet/url_request_context_config.h" +#include "components/metrics/library_support/histogram_manager.h" +#include "net/base/load_flags.h" +#include "net/base/logging_network_change_observer.h" +#include "net/base/net_errors.h" +#include "net/base/network_delegate_impl.h" +#include "net/base/url_util.h" +#include "net/cert/caching_cert_verifier.h" +#include "net/cert/cert_verifier.h" +#include "net/cookies/cookie_monster.h" +#include "net/http/http_auth_handler_factory.h" +#include "net/log/file_net_log_observer.h" +#include "net/log/net_log_util.h" +#include "net/nqe/network_quality_estimator_params.h" +#include "net/proxy_resolution/proxy_config_service_android.h" +#include "net/proxy_resolution/proxy_resolution_service.h" +#include "net/third_party/quiche/src/quic/core/quic_versions.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_context_builder.h" +#include "net/url_request/url_request_interceptor.h" + +#if BUILDFLAG(INTEGRATED_MODE) +#include "components/cronet/android/cronet_integrated_mode_state.h" +#endif + +using base::android::JavaParamRef; +using base::android::ScopedJavaLocalRef; + +namespace { + +// Helper method that takes a Java string that can be null, in which case it +// will get converted to an empty string. +std::string ConvertNullableJavaStringToUTF8(JNIEnv* env, + const JavaParamRef& jstr) { + std::string str; + if (!jstr.is_null()) + base::android::ConvertJavaStringToUTF8(env, jstr, &str); + return str; +} + +} // namespace + +namespace cronet { + +CronetURLRequestContextAdapter::CronetURLRequestContextAdapter( + std::unique_ptr context_config) { + // Create context and pass ownership of |this| (self) to the context. + std::unique_ptr self(this); +#if BUILDFLAG(INTEGRATED_MODE) + // Create CronetURLRequestContext running in integrated network task runner. + context_ = + new CronetURLRequestContext(std::move(context_config), std::move(self), + GetIntegratedModeNetworkTaskRunner()); +#else + context_ = + new CronetURLRequestContext(std::move(context_config), std::move(self)); +#endif +} + +CronetURLRequestContextAdapter::~CronetURLRequestContextAdapter() = default; + +void CronetURLRequestContextAdapter::InitRequestContextOnInitThread( + JNIEnv* env, + const JavaParamRef& jcaller) { + jcronet_url_request_context_.Reset(env, jcaller); + context_->InitRequestContextOnInitThread(); +} + +void CronetURLRequestContextAdapter::ConfigureNetworkQualityEstimatorForTesting( + JNIEnv* env, + const JavaParamRef& jcaller, + jboolean use_local_host_requests, + jboolean use_smaller_responses, + jboolean disable_offline_check) { + context_->ConfigureNetworkQualityEstimatorForTesting( + use_local_host_requests == JNI_TRUE, use_smaller_responses == JNI_TRUE, + disable_offline_check == JNI_TRUE); +} + +void CronetURLRequestContextAdapter::ProvideRTTObservations( + JNIEnv* env, + const JavaParamRef& jcaller, + bool should) { + context_->ProvideRTTObservations(should == JNI_TRUE); +} + +void CronetURLRequestContextAdapter::ProvideThroughputObservations( + JNIEnv* env, + const JavaParamRef& jcaller, + bool should) { + context_->ProvideThroughputObservations(should == JNI_TRUE); +} + +void CronetURLRequestContextAdapter::OnInitNetworkThread() { + JNIEnv* env = base::android::AttachCurrentThread(); + Java_CronetUrlRequestContext_initNetworkThread(env, + jcronet_url_request_context_); +} + +void CronetURLRequestContextAdapter::OnDestroyNetworkThread() { + // The |context_| is destroyed. + context_ = nullptr; +} + +void CronetURLRequestContextAdapter::OnEffectiveConnectionTypeChanged( + net::EffectiveConnectionType effective_connection_type) { + Java_CronetUrlRequestContext_onEffectiveConnectionTypeChanged( + base::android::AttachCurrentThread(), jcronet_url_request_context_, + effective_connection_type); +} + +void CronetURLRequestContextAdapter::OnRTTOrThroughputEstimatesComputed( + int32_t http_rtt_ms, + int32_t transport_rtt_ms, + int32_t downstream_throughput_kbps) { + Java_CronetUrlRequestContext_onRTTOrThroughputEstimatesComputed( + base::android::AttachCurrentThread(), jcronet_url_request_context_, + http_rtt_ms, transport_rtt_ms, downstream_throughput_kbps); +} + +void CronetURLRequestContextAdapter::OnRTTObservation( + int32_t rtt_ms, + int32_t timestamp_ms, + net::NetworkQualityObservationSource source) { + Java_CronetUrlRequestContext_onRttObservation( + base::android::AttachCurrentThread(), jcronet_url_request_context_, + rtt_ms, timestamp_ms, source); +} + +void CronetURLRequestContextAdapter::OnThroughputObservation( + int32_t throughput_kbps, + int32_t timestamp_ms, + net::NetworkQualityObservationSource source) { + Java_CronetUrlRequestContext_onThroughputObservation( + base::android::AttachCurrentThread(), jcronet_url_request_context_, + throughput_kbps, timestamp_ms, source); +} + +void CronetURLRequestContextAdapter::OnStopNetLogCompleted() { + Java_CronetUrlRequestContext_stopNetLogCompleted( + base::android::AttachCurrentThread(), jcronet_url_request_context_); +} + +void CronetURLRequestContextAdapter::Destroy( + JNIEnv* env, + const JavaParamRef& jcaller) { + // Deleting |context_| on client thread will post cleanup onto network thread, + // which will in turn delete |this| on network thread. + delete context_; +} + +net::URLRequestContext* CronetURLRequestContextAdapter::GetURLRequestContext() { + return context_->GetURLRequestContext(); +} + +void CronetURLRequestContextAdapter::PostTaskToNetworkThread( + const base::Location& posted_from, + base::OnceClosure callback) { + context_->PostTaskToNetworkThread(posted_from, std::move(callback)); +} + +bool CronetURLRequestContextAdapter::IsOnNetworkThread() const { + return context_->IsOnNetworkThread(); +} + +bool CronetURLRequestContextAdapter::StartNetLogToFile( + JNIEnv* env, + const JavaParamRef& jcaller, + const JavaParamRef& jfile_name, + jboolean jlog_all) { + std::string file_name( + base::android::ConvertJavaStringToUTF8(env, jfile_name)); + return context_->StartNetLogToFile(file_name, jlog_all == JNI_TRUE); +} + +void CronetURLRequestContextAdapter::StartNetLogToDisk( + JNIEnv* env, + const JavaParamRef& jcaller, + const JavaParamRef& jdir_name, + jboolean jlog_all, + jint jmax_size) { + std::string dir_name(base::android::ConvertJavaStringToUTF8(env, jdir_name)); + context_->StartNetLogToDisk(dir_name, jlog_all == JNI_TRUE, jmax_size); +} + +void CronetURLRequestContextAdapter::StopNetLog( + JNIEnv* env, + const JavaParamRef& jcaller) { + context_->StopNetLog(); +} + +int CronetURLRequestContextAdapter::default_load_flags() const { + return context_->default_load_flags(); +} + +// Create a URLRequestContextConfig from the given parameters. +static jlong JNI_CronetUrlRequestContext_CreateRequestContextConfig( + JNIEnv* env, + const JavaParamRef& juser_agent, + const JavaParamRef& jstorage_path, + jboolean jquic_enabled, + const JavaParamRef& jquic_default_user_agent_id, + jboolean jhttp2_enabled, + jboolean jbrotli_enabled, + jboolean jdisable_cache, + jint jhttp_cache_mode, + jlong jhttp_cache_max_size, + const JavaParamRef& jexperimental_quic_connection_options, + jlong jmock_cert_verifier, + jboolean jenable_network_quality_estimator, + jboolean jbypass_public_key_pinning_for_local_trust_anchors, + jint jnetwork_thread_priority) { + std::unique_ptr url_request_context_config = + URLRequestContextConfig::CreateURLRequestContextConfig( + jquic_enabled, + ConvertNullableJavaStringToUTF8(env, jquic_default_user_agent_id), + jhttp2_enabled, jbrotli_enabled, + static_cast(jhttp_cache_mode), + jhttp_cache_max_size, jdisable_cache, + ConvertNullableJavaStringToUTF8(env, jstorage_path), + /* accept_languages */ std::string(), + ConvertNullableJavaStringToUTF8(env, juser_agent), + ConvertNullableJavaStringToUTF8( + env, jexperimental_quic_connection_options), + base::WrapUnique( + reinterpret_cast(jmock_cert_verifier)), + jenable_network_quality_estimator, + jbypass_public_key_pinning_for_local_trust_anchors, + jnetwork_thread_priority >= -20 && jnetwork_thread_priority <= 19 + ? absl::optional(jnetwork_thread_priority) + : absl::optional()); + return reinterpret_cast(url_request_context_config.release()); +} + +// Add a QUIC hint to a URLRequestContextConfig. +static void JNI_CronetUrlRequestContext_AddQuicHint( + JNIEnv* env, + jlong jurl_request_context_config, + const JavaParamRef& jhost, + jint jport, + jint jalternate_port) { + URLRequestContextConfig* config = + reinterpret_cast(jurl_request_context_config); + config->quic_hints.push_back( + std::make_unique( + base::android::ConvertJavaStringToUTF8(env, jhost), jport, + jalternate_port)); +} + +// Add a public key pin to URLRequestContextConfig. +// |jhost| is the host to apply the pin to. +// |jhashes| is an array of jbyte[32] representing SHA256 key hashes. +// |jinclude_subdomains| indicates if pin should be applied to subdomains. +// |jexpiration_time| is the time that the pin expires, in milliseconds since +// Jan. 1, 1970, midnight GMT. +static void JNI_CronetUrlRequestContext_AddPkp( + JNIEnv* env, + jlong jurl_request_context_config, + const JavaParamRef& jhost, + const JavaParamRef& jhashes, + jboolean jinclude_subdomains, + jlong jexpiration_time) { + URLRequestContextConfig* config = + reinterpret_cast(jurl_request_context_config); + std::unique_ptr pkp( + new URLRequestContextConfig::Pkp( + base::android::ConvertJavaStringToUTF8(env, jhost), + jinclude_subdomains, + base::Time::UnixEpoch() + base::Milliseconds(jexpiration_time))); + for (auto bytes_array : jhashes.ReadElements()) { + static_assert(std::is_pod::value, + "net::SHA256HashValue is not POD"); + static_assert(sizeof(net::SHA256HashValue) * CHAR_BIT == 256, + "net::SHA256HashValue contains overhead"); + if (env->GetArrayLength(bytes_array.obj()) != + sizeof(net::SHA256HashValue)) { + LOG(ERROR) << "Unable to add public key hash value."; + continue; + } + jbyte* bytes = env->GetByteArrayElements(bytes_array.obj(), nullptr); + net::HashValue hash(*reinterpret_cast(bytes)); + pkp->pin_hashes.push_back(hash); + env->ReleaseByteArrayElements(bytes_array.obj(), bytes, JNI_ABORT); + } + config->pkp_list.push_back(std::move(pkp)); +} + +// Creates RequestContextAdater if config is valid URLRequestContextConfig, +// returns 0 otherwise. +static jlong JNI_CronetUrlRequestContext_CreateRequestContextAdapter( + JNIEnv* env, + jlong jconfig) { + std::unique_ptr context_config( + reinterpret_cast(jconfig)); + + CronetURLRequestContextAdapter* context_adapter = + new CronetURLRequestContextAdapter(std::move(context_config)); + return reinterpret_cast(context_adapter); +} + +static jint JNI_CronetUrlRequestContext_SetMinLogLevel( + JNIEnv* env, + jint jlog_level) { + jint old_log_level = static_cast(logging::GetMinLogLevel()); + // MinLogLevel is global, shared by all URLRequestContexts. + logging::SetMinLogLevel(static_cast(jlog_level)); + return old_log_level; +} + +static ScopedJavaLocalRef +JNI_CronetUrlRequestContext_GetHistogramDeltas(JNIEnv* env) { + std::vector data; + if (!metrics::HistogramManager::GetInstance()->GetDeltas(&data)) + return ScopedJavaLocalRef(); + return base::android::ToJavaByteArray(env, data.data(), data.size()); +} + +} // namespace cronet diff --git a/src/components/cronet/android/cronet_url_request_context_adapter.h b/src/components/cronet/android/cronet_url_request_context_adapter.h new file mode 100644 index 0000000000..bd1c2b73de --- /dev/null +++ b/src/components/cronet/android/cronet_url_request_context_adapter.h @@ -0,0 +1,155 @@ +// Copyright 2014 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_URL_REQUEST_CONTEXT_ADAPTER_H_ +#define COMPONENTS_CRONET_ANDROID_CRONET_URL_REQUEST_CONTEXT_ADAPTER_H_ + +#include +#include + +#include + +#include "base/android/scoped_java_ref.h" +#include "base/callback.h" +#include "base/containers/queue.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/threading/thread.h" +#include "components/cronet/cronet_url_request_context.h" +#include "components/prefs/json_pref_store.h" +#include "net/nqe/effective_connection_type.h" +#include "net/nqe/effective_connection_type_observer.h" +#include "net/nqe/network_quality_estimator.h" +#include "net/nqe/network_quality_observation_source.h" +#include "net/nqe/rtt_throughput_estimates_observer.h" + +namespace net { +class NetLog; +class URLRequestContext; +} // namespace net + +namespace cronet { +class TestUtil; + +struct URLRequestContextConfig; + +// Adapter between Java CronetUrlRequestContext and CronetURLRequestContext. +class CronetURLRequestContextAdapter + : public CronetURLRequestContext::Callback { + public: + explicit CronetURLRequestContextAdapter( + std::unique_ptr context_config); + + CronetURLRequestContextAdapter(const CronetURLRequestContextAdapter&) = + delete; + CronetURLRequestContextAdapter& operator=( + const CronetURLRequestContextAdapter&) = delete; + + ~CronetURLRequestContextAdapter() override; + + // Called on init Java thread to initialize URLRequestContext. + void InitRequestContextOnInitThread( + JNIEnv* env, + const base::android::JavaParamRef& jcaller); + + // Releases all resources for the request context and deletes the object. + // Blocks until network thread is destroyed after running all pending tasks. + void Destroy(JNIEnv* env, + const base::android::JavaParamRef& jcaller); + + // Posts a task that might depend on the context being initialized + // to the network thread. + void PostTaskToNetworkThread(const base::Location& posted_from, + base::OnceClosure callback); + + bool IsOnNetworkThread() const; + + net::URLRequestContext* GetURLRequestContext(); + + // TODO(xunjieli): Keep only one version of StartNetLog(). + + // Starts NetLog logging to file. This can be called on any thread. + // Return false if |jfile_name| cannot be opened. + bool StartNetLogToFile(JNIEnv* env, + const base::android::JavaParamRef& jcaller, + const base::android::JavaParamRef& jfile_name, + jboolean jlog_all); + + // Starts NetLog logging to disk with a bounded amount of disk space. This + // can be called on any thread. + void StartNetLogToDisk(JNIEnv* env, + const base::android::JavaParamRef& jcaller, + const base::android::JavaParamRef& jdir_name, + jboolean jlog_all, + jint jmax_size); + + // Stops NetLog logging to file. This can be called on any thread. This will + // flush any remaining writes to disk. + void StopNetLog(JNIEnv* env, + const base::android::JavaParamRef& jcaller); + + // Default net::LOAD flags used to create requests. + int default_load_flags() const; + + // Called on init Java thread to initialize URLRequestContext. + void InitRequestContextOnInitThread(); + + // Configures the network quality estimator to observe requests to localhost, + // to use smaller responses when estimating throughput, and to disable the + // device offline checks when computing the effective connection type or when + // writing the prefs. This should only be used for testing. This can be + // called only after the network quality estimator has been enabled. + void ConfigureNetworkQualityEstimatorForTesting( + JNIEnv* env, + const base::android::JavaParamRef& jcaller, + jboolean use_local_host_requests, + jboolean use_smaller_responses, + jboolean disable_offline_check); + + // Request that RTT and/or throughput observations should or should not be + // provided by the network quality estimator. + void ProvideRTTObservations( + JNIEnv* env, + const base::android::JavaParamRef& jcaller, + bool should); + void ProvideThroughputObservations( + JNIEnv* env, + const base::android::JavaParamRef& jcaller, + bool should); + + CronetURLRequestContext* cronet_url_request_context() const { + return context_; + } + + // CronetURLRequestContext::Callback + void OnInitNetworkThread() override; + void OnDestroyNetworkThread() override; + void OnEffectiveConnectionTypeChanged( + net::EffectiveConnectionType effective_connection_type) override; + void OnRTTOrThroughputEstimatesComputed( + int32_t http_rtt_ms, + int32_t transport_rtt_ms, + int32_t downstream_throughput_kbps) override; + void OnRTTObservation(int32_t rtt_ms, + int32_t timestamp_ms, + net::NetworkQualityObservationSource source) override; + void OnThroughputObservation( + int32_t throughput_kbps, + int32_t timestamp_ms, + net::NetworkQualityObservationSource source) override; + void OnStopNetLogCompleted() override; + + private: + friend class TestUtil; + + // Native Cronet URL Request Context. + raw_ptr context_; + + // Java object that owns this CronetURLRequestContextAdapter. + base::android::ScopedJavaGlobalRef jcronet_url_request_context_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_CRONET_URL_REQUEST_CONTEXT_ADAPTER_H_ diff --git a/src/components/cronet/android/fake/java/org/chromium/net/test/FakeCronetController.java b/src/components/cronet/android/fake/java/org/chromium/net/test/FakeCronetController.java new file mode 100644 index 0000000000..d045ba23fd --- /dev/null +++ b/src/components/cronet/android/fake/java/org/chromium/net/test/FakeCronetController.java @@ -0,0 +1,211 @@ +// Copyright 2019 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.test; + +import android.content.Context; + +import org.chromium.net.CronetEngine; +import org.chromium.net.ExperimentalCronetEngine; +import org.chromium.net.UrlRequest; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Controller for fake Cronet implementation. Allows a test to setup responses for + * {@link UrlRequest}s. If multiple {@link ResponseMatcher}s match a specific request, the first + * {@link ResponseMatcher} added takes precedence. + */ +public final class FakeCronetController { + // List of FakeCronetEngines so that FakeCronetEngine can be accessed when created with + // the {@link FakeCronetProvider}. + private static final List sInstances = + Collections.synchronizedList(new ArrayList<>()); + + // List of ResponseMatchers to be checked for a response to a request in place of a server. + private final List mResponseMatchers = + Collections.synchronizedList(new ArrayList<>()); + + /** + * Creates a fake {@link CronetEngine.Builder} that creates {@link CronetEngine}s that return + * fake {@link UrlRequests}. Once built, the {@link CronetEngine}'s {@link UrlRequest}s will + * retrieve responses from this {@link FakeCronetController}. + * + * @param context the Android context to build the fake {@link CronetEngine} from. + * @return a fake CronetEngine.Builder that uses this {@link FakeCronetController} to manage + * responses once it is built. + */ + public CronetEngine.Builder newFakeCronetEngineBuilder(Context context) { + FakeCronetEngine.Builder builder = new FakeCronetEngine.Builder(context); + builder.setController(this); + // FakeCronetEngine.Builder is not actually a CronetEngine.Builder, so construct one with + // the child of CronetEngine.Builder: ExperimentalCronetEngine.Builder. + return new ExperimentalCronetEngine.Builder(builder); + } + + /** + * Adds a {@link UrlResponseMatcher} that will respond to the provided URL with the provided + * {@link FakeUrlResponse}. Equivalent to: + * addResponseMatcher(new UrlResponseMatcher(url, response)). + * + * @param response a {@link FakeUrlResponse} to respond with + * @param url a url for which the response should be returned + */ + public void addResponseForUrl(FakeUrlResponse response, String url) { + addResponseMatcher(new UrlResponseMatcher(url, response)); + } + + /** + * Adds a {@link ResponseMatcher} to the list of {@link ResponseMatcher}s. + * + * @param matcher the {@link ResponseMatcher} that should be matched against a request + */ + public void addResponseMatcher(ResponseMatcher matcher) { + mResponseMatchers.add(matcher); + } + + /** + * Removes a specific {@link ResponseMatcher} from the list of {@link ResponseMatcher}s. + * + * @param matcher the {@link ResponseMatcher} to remove + */ + public void removeResponseMatcher(ResponseMatcher matcher) { + mResponseMatchers.remove(matcher); + } + + /** + * Removes all {@link ResponseMatcher}s from the list of {@link ResponseMatcher}s. + */ + public void clearResponseMatchers() { + mResponseMatchers.clear(); + } + + /** + * Adds a {@link FakeUrlResponse} to the list of responses that will redirect a + * {@link UrlRequest} to the specified URL. + * + * @param redirectLocation the URL to redirect the {@link UrlRequest} to + * @param url the URL that will trigger the redirect + */ + public void addRedirectResponse(String redirectLocation, String url) { + FakeUrlResponse redirectResponse = new FakeUrlResponse.Builder() + .setHttpStatusCode(302) + .addHeader("location", redirectLocation) + .build(); + addResponseForUrl(redirectResponse, url); + } + + /** + * Adds an {@link FakeUrlResponse} that fails with the specified HTTP code for the specified + * URL. + * + * @param statusCode the code for the {@link FakeUrlResponse} + * @param url the URL that should trigger the error response when requested by a + * {@link UrlRequest} + * @throws IllegalArgumentException if the HTTP status code is not an error code (code >= 400) + */ + public void addHttpErrorResponse(int statusCode, String url) { + addResponseForUrl(getFailedResponse(statusCode), url); + } + + // TODO(kirchman): Create a function to add a response that takes a CronetException. + + /** + * Adds a successful 200 code {@link FakeUrlResponse} that will match the specified + * URL when requested by a {@link UrlRequest}. + * + * @param url the URL that triggers the successful response + * @param body the body of the response as a byte array + */ + public void addSuccessResponse(String url, byte[] body) { + addResponseForUrl(new FakeUrlResponse.Builder().setResponseBody(body).build(), url); + } + + /** + * Returns the {@link CronetEngineController} for a specified {@link CronetEngine}. This method + * should be used in conjunction with {@link FakeCronetController.getInstances}. + * + * @param engine the fake {@link CronetEngine} to get the controller for. + * @return the controller for the specified fake {@link CronetEngine}. + */ + public static FakeCronetController getControllerForFakeEngine(CronetEngine engine) { + if (engine instanceof FakeCronetEngine) { + FakeCronetEngine fakeEngine = (FakeCronetEngine) engine; + return fakeEngine.getController(); + } + throw new IllegalArgumentException("Provided CronetEngine is not a fake CronetEngine"); + } + + /** + * Returns all created fake instances of {@link CronetEngine} that have not been shut down with + * {@link CronetEngine.shutdown()} in order of creation. Can be used to retrieve a controller + * in conjunction with {@link FakeCronetController.getControllerForFakeEngine}. + * + * @return a list of all fake {@link CronetEngine}s that have been created + */ + public static List getFakeCronetEngines() { + synchronized (sInstances) { + return new ArrayList<>(sInstances); + } + } + + /** + * Removes a fake {@link CronetEngine} from the list of {@link CronetEngine} instances. + * + * @param cronetEngine the instance to remove + */ + static void removeFakeCronetEngine(CronetEngine cronetEngine) { + sInstances.remove(cronetEngine); + } + + /** + * Add a CronetEngine to the list of CronetEngines. + * + * @param engine the {@link CronetEngine} to add + */ + static void addFakeCronetEngine(FakeCronetEngine engine) { + sInstances.add(engine); + } + + /** + * Gets a response for specified request details if there is one, or a "404" failed response + * if there is no {@link ResponseMatcher} with a {@link FakeUrlResponse} for this request. + * + * @param url the URL that the {@link UrlRequest} is connecting to + * @param httpMethod the HTTP method that the {@link UrlRequest} is using to connect with + * @param headers the headers supplied by the {@link UrlRequest} + * @param body the body of the fake HTTP request + * @return a {@link FakeUrlResponse} if there is one, or a failed "404" response if none found + */ + FakeUrlResponse getResponse( + String url, String httpMethod, List> headers, byte[] body) { + synchronized (mResponseMatchers) { + for (ResponseMatcher responseMatcher : mResponseMatchers) { + FakeUrlResponse matchedResponse = + responseMatcher.getMatchingResponse(url, httpMethod, headers, body); + if (matchedResponse != null) { + return matchedResponse; + } + } + } + return getFailedResponse(404); + } + + /** + * Creates and returns a failed response with the specified HTTP status code. + * + * @param statusCode the HTTP code that the returned response will have + * @return a {@link FakeUrlResponse} with the specified code + */ + private static FakeUrlResponse getFailedResponse(int statusCode) { + if (statusCode < 400) { + throw new IllegalArgumentException( + "Expected HTTP error code (code >= 400), but was: " + statusCode); + } + return new FakeUrlResponse.Builder().setHttpStatusCode(statusCode).build(); + } +} diff --git a/src/components/cronet/android/fake/java/org/chromium/net/test/FakeCronetEngine.java b/src/components/cronet/android/fake/java/org/chromium/net/test/FakeCronetEngine.java new file mode 100644 index 0000000000..a855bb85ea --- /dev/null +++ b/src/components/cronet/android/fake/java/org/chromium/net/test/FakeCronetEngine.java @@ -0,0 +1,302 @@ +// Copyright 2019 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.test; + +import android.content.Context; + +import androidx.annotation.GuardedBy; + +import org.chromium.net.BidirectionalStream; +import org.chromium.net.CronetEngine; +import org.chromium.net.ExperimentalBidirectionalStream; +import org.chromium.net.NetworkQualityRttListener; +import org.chromium.net.NetworkQualityThroughputListener; +import org.chromium.net.RequestFinishedInfo; +import org.chromium.net.UrlRequest; +import org.chromium.net.impl.CronetEngineBase; +import org.chromium.net.impl.CronetEngineBuilderImpl; +import org.chromium.net.impl.ImplVersion; +import org.chromium.net.impl.UrlRequestBase; + +import java.io.IOException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandlerFactory; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Fake {@link CronetEngine}. This implements CronetEngine. + */ +final class FakeCronetEngine extends CronetEngineBase { + /** + * Builds a {@link FakeCronetEngine}. This implements CronetEngine.Builder. + */ + static class Builder extends CronetEngineBuilderImpl { + private FakeCronetController mController; + + /** + * Builder for {@link FakeCronetEngine}. + * + * @param context Android {@link Context}. + */ + Builder(Context context) { + super(context); + } + + @Override + public FakeCronetEngine build() { + return new FakeCronetEngine(this); + } + + void setController(FakeCronetController controller) { + mController = controller; + } + } + + private final FakeCronetController mController; + private final ExecutorService mExecutorService; + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private boolean mIsShutdown; + + @GuardedBy("mLock") + private int mActiveRequestCount; + + /** + * Creates a {@link FakeCronetEngine}. Used when {@link FakeCronetEngine} is created with the + * {@link FakeCronetEngine.Builder}. + * + * @param builder a {@link CronetEngineBuilderImpl} to build this {@link CronetEngine} + * implementation from. + */ + private FakeCronetEngine(FakeCronetEngine.Builder builder) { + if (builder.mController != null) { + mController = builder.mController; + } else { + mController = new FakeCronetController(); + } + mExecutorService = new ThreadPoolExecutor( + /* corePoolSize= */ 1, + /* maximumPoolSize= */ 5, + /* keepAliveTime= */ 50, TimeUnit.SECONDS, new LinkedBlockingQueue(), + new ThreadFactory() { + @Override + public Thread newThread(final Runnable r) { + return Executors.defaultThreadFactory().newThread(new Runnable() { + @Override + public void run() { + Thread.currentThread().setName("FakeCronetEngine"); + r.run(); + } + }); + } + }); + FakeCronetController.addFakeCronetEngine(this); + } + + /** + * Gets the controller associated with this instance that will be used for responses to + * {@link UrlRequest}s. + * + * @return the {@link FakeCronetCntroller} that controls this {@link FakeCronetEngine}. + */ + FakeCronetController getController() { + return mController; + } + + @Override + public ExperimentalBidirectionalStream.Builder newBidirectionalStreamBuilder( + String url, BidirectionalStream.Callback callback, Executor executor) { + synchronized (mLock) { + if (mIsShutdown) { + throw new IllegalStateException( + "This instance of CronetEngine has been shutdown and can no longer be " + + "used."); + } + throw new UnsupportedOperationException( + "The bidirectional stream API is not supported by the Fake implementation " + + "of CronetEngine."); + } + } + + @Override + public String getVersionString() { + return "FakeCronet/" + ImplVersion.getCronetVersionWithLastChange(); + } + + @Override + public void shutdown() { + synchronized (mLock) { + if (mActiveRequestCount != 0) { + throw new IllegalStateException("Cannot shutdown with active requests."); + } else { + mIsShutdown = true; + } + } + mExecutorService.shutdown(); + FakeCronetController.removeFakeCronetEngine(this); + } + + @Override + public void startNetLogToFile(String fileName, boolean logAll) {} + + @Override + public void startNetLogToDisk(String dirPath, boolean logAll, int maxSize) {} + + @Override + public void stopNetLog() {} + + @Override + public byte[] getGlobalMetricsDeltas() { + return new byte[0]; + } + + @Override + public int getEffectiveConnectionType() { + return EFFECTIVE_CONNECTION_TYPE_UNKNOWN; + } + + @Override + public int getHttpRttMs() { + return CONNECTION_METRIC_UNKNOWN; + } + + @Override + public int getTransportRttMs() { + return CONNECTION_METRIC_UNKNOWN; + } + + @Override + public int getDownstreamThroughputKbps() { + return CONNECTION_METRIC_UNKNOWN; + } + + @Override + public void configureNetworkQualityEstimatorForTesting(boolean useLocalHostRequests, + boolean useSmallerResponses, boolean disableOfflineCheck) {} + + @Override + public void addRttListener(NetworkQualityRttListener listener) {} + + @Override + public void removeRttListener(NetworkQualityRttListener listener) {} + + @Override + public void addThroughputListener(NetworkQualityThroughputListener listener) {} + + @Override + public void removeThroughputListener(NetworkQualityThroughputListener listener) {} + + @Override + public void addRequestFinishedListener(RequestFinishedInfo.Listener listener) {} + + @Override + public void removeRequestFinishedListener(RequestFinishedInfo.Listener listener) {} + + // TODO(crbug.com/669707) Instantiate a fake CronetHttpUrlConnection wrapping a FakeUrlRequest + // here. + @Override + public URLConnection openConnection(URL url) throws IOException { + throw new UnsupportedOperationException( + "The openConnection API is not supported by the Fake implementation of " + + "CronetEngine."); + } + + @Override + public URLConnection openConnection(URL url, Proxy proxy) throws IOException { + throw new UnsupportedOperationException( + "The openConnection API is not supported by the Fake implementation of " + + "CronetEngine."); + } + + @Override + public URLStreamHandlerFactory createURLStreamHandlerFactory() { + throw new UnsupportedOperationException( + "The URLStreamHandlerFactory API is not supported by the Fake implementation of " + + "CronetEngine."); + } + + @Override + protected UrlRequestBase createRequest(String url, UrlRequest.Callback callback, + Executor userExecutor, int priority, Collection connectionAnnotations, + boolean disableCache, boolean disableConnectionMigration, boolean allowDirectExecutor, + boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, + int trafficStatsUid, RequestFinishedInfo.Listener requestFinishedListener, + int idempotency) { + synchronized (mLock) { + if (mIsShutdown) { + throw new IllegalStateException( + "This instance of CronetEngine has been shutdown and can no longer be " + + "used."); + } + return new FakeUrlRequest(callback, userExecutor, mExecutorService, url, + allowDirectExecutor, trafficStatsTagSet, trafficStatsTag, trafficStatsUidSet, + trafficStatsUid, mController, this); + } + } + + @Override + protected ExperimentalBidirectionalStream createBidirectionalStream(String url, + BidirectionalStream.Callback callback, Executor executor, String httpMethod, + List> requestHeaders, @StreamPriority int priority, + boolean delayRequestHeadersUntilFirstFlush, Collection connectionAnnotations, + boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, + int trafficStatsUid) { + synchronized (mLock) { + if (mIsShutdown) { + throw new IllegalStateException( + "This instance of CronetEngine has been shutdown and can no longer be " + + "used."); + } + throw new UnsupportedOperationException( + "The BidirectionalStream API is not supported by the Fake implementation of " + + "CronetEngine."); + } + } + + /** + * Mark request as started to prevent shutdown when there are active + * requests, only if the engine is not shutdown. + * + * @return true if the engine is not shutdown and the request is marked as started. + */ + boolean startRequest() { + synchronized (mLock) { + if (!mIsShutdown) { + mActiveRequestCount++; + return true; + } + return false; + } + } + + /** + * Mark request as finished to allow shutdown when there are no active + * requests. + */ + void onRequestDestroyed() { + synchronized (mLock) { + // Sanity check. We should not be able to shutdown if there are still running requests. + if (mIsShutdown) { + throw new IllegalStateException( + "This instance of CronetEngine was shutdown. All requests must have been " + + "complete."); + } + mActiveRequestCount--; + } + } +} diff --git a/src/components/cronet/android/fake/java/org/chromium/net/test/FakeCronetProvider.java b/src/components/cronet/android/fake/java/org/chromium/net/test/FakeCronetProvider.java new file mode 100644 index 0000000000..9887689a6c --- /dev/null +++ b/src/components/cronet/android/fake/java/org/chromium/net/test/FakeCronetProvider.java @@ -0,0 +1,70 @@ +// Copyright 2019 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.test; + +import android.content.Context; + +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetProvider; +import org.chromium.net.ExperimentalCronetEngine; +import org.chromium.net.ICronetEngineBuilder; +import org.chromium.net.impl.ImplVersion; + +import java.util.Arrays; + +/** + * Implementation of {@link CronetProvider} that creates {@link CronetEngine.Builder} + * for building the Fake implementation of {@link CronetEngine}. + * {@hide} + */ +public class FakeCronetProvider extends CronetProvider { + /** + * String returned by {@link CronetProvider#getName} for {@link CronetProvider} + * that provides the fake Cronet implementation. + */ + public static final String PROVIDER_NAME_FAKE = "Fake-Cronet-Provider"; + + /** + * Constructs a {@link FakeCronetProvider}. + * + * @param context Android context to use + */ + public FakeCronetProvider(Context context) { + super(context); + } + + @Override + public CronetEngine.Builder createBuilder() { + ICronetEngineBuilder impl = new FakeCronetEngine.Builder(mContext); + return new ExperimentalCronetEngine.Builder(impl); + } + + @Override + public String getName() { + return PROVIDER_NAME_FAKE; + } + + @Override + public String getVersion() { + return ImplVersion.getCronetVersion(); + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] {FakeCronetProvider.class, mContext}); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof FakeCronetProvider + && this.mContext.equals(((FakeCronetProvider) other).mContext)); + } +} diff --git a/src/components/cronet/android/fake/java/org/chromium/net/test/FakeUrlRequest.java b/src/components/cronet/android/fake/java/org/chromium/net/test/FakeUrlRequest.java new file mode 100644 index 0000000000..39c58a3c97 --- /dev/null +++ b/src/components/cronet/android/fake/java/org/chromium/net/test/FakeUrlRequest.java @@ -0,0 +1,747 @@ +// Copyright 2019 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.test; + +import android.util.Log; + +import androidx.annotation.GuardedBy; +import androidx.annotation.VisibleForTesting; + +import org.chromium.net.CronetException; +import org.chromium.net.InlineExecutionProhibitedException; +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UrlResponseInfo; +import org.chromium.net.impl.CallbackExceptionImpl; +import org.chromium.net.impl.CronetExceptionImpl; +import org.chromium.net.impl.JavaUploadDataSinkBase; +import org.chromium.net.impl.JavaUrlRequestUtils; +import org.chromium.net.impl.JavaUrlRequestUtils.CheckedRunnable; +import org.chromium.net.impl.JavaUrlRequestUtils.DirectPreventingExecutor; +import org.chromium.net.impl.JavaUrlRequestUtils.State; +import org.chromium.net.impl.Preconditions; +import org.chromium.net.impl.UrlRequestBase; +import org.chromium.net.impl.UrlResponseInfoImpl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +/** + * Fake UrlRequest that retrieves responses from the associated FakeCronetController. Used for + * testing Cronet usage on Android. + */ +final class FakeUrlRequest extends UrlRequestBase { + private static final int DEFAULT_UPLOAD_BUFFER_SIZE = 8192; + // Used for logging errors. + private static final String TAG = FakeUrlRequest.class.getSimpleName(); + // Callback used to report responses to the client. + private final Callback mCallback; + // The {@link Executor} provided by the user to be used for callbacks. + private final Executor mUserExecutor; + // The {@link Executor} provided by the engine used to break up callback loops. + private final Executor mExecutor; + // The {@link FakeCronetController} that will provide responses for this request. + private final FakeCronetController mFakeCronetController; + // The fake {@link CronetEngine} that should be notified when this request starts and stops. + private final FakeCronetEngine mFakeCronetEngine; + // Source of thread safety for this class. + private final Object mLock = new Object(); + // True if direct execution is allowed for this request. + private final boolean mAllowDirectExecutor; + // The chain of URL's this request has received. + @GuardedBy("mLock") + private final List mUrlChain = new ArrayList<>(); + // The list of HTTP headers used by this request to establish a connection. + @GuardedBy("mLock") + private final ArrayList> mAllHeadersList = new ArrayList<>(); + // The current URL this request is connecting to. + @GuardedBy("mLock") + private String mCurrentUrl; + // The {@link FakeUrlResponse} for the current URL. + @GuardedBy("mLock") + private FakeUrlResponse mCurrentFakeResponse; + // The body of the request from UploadDataProvider. + @GuardedBy("mLock") + private byte[] mRequestBody; + // The {@link UploadDataProvider} to retrieve a request body from. + @GuardedBy("mLock") + private UploadDataProvider mUploadDataProvider; + // The executor to call the {@link UploadDataProvider}'s callback methods with. + @GuardedBy("mLock") + private Executor mUploadExecutor; + // The {@link UploadDataSink} for the {@link UploadDataProvider}. + @GuardedBy("mLock") + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + FakeDataSink mFakeDataSink; + // The {@link UrlResponseInfo} for the current request. + @GuardedBy("mLock") + private UrlResponseInfo mUrlResponseInfo; + // The response from the current request that needs to be sent. + @GuardedBy("mLock") + private ByteBuffer mResponse; + // The HTTP method used by this request to establish a connection. + @GuardedBy("mLock") + private String mHttpMethod; + // True after the {@link UploadDataProvider} for this request has been closed. + @GuardedBy("mLock") + private boolean mUploadProviderClosed; + + @GuardedBy("mLock") + @State + private int mState = State.NOT_STARTED; + + /** + * Holds a subset of StatusValues - {@link State#STARTED} can represent + * {@link Status#SENDING_REQUEST} or {@link Status#WAITING_FOR_RESPONSE}. While the distinction + * isn't needed to implement the logic in this class, it is needed to implement + * {@link #getStatus(StatusListener)}. + */ + @StatusValues + private volatile int mAdditionalStatusDetails = Status.INVALID; + + /** + * Used to map from HTTP status codes to the corresponding human-readable text. + */ + private static final Map HTTP_STATUS_CODE_TO_TEXT; + static { + Map httpCodeMap = new HashMap<>(); + httpCodeMap.put(100, "Continue"); + httpCodeMap.put(101, "Switching Protocols"); + httpCodeMap.put(102, "Processing"); + httpCodeMap.put(103, "Early Hints"); + httpCodeMap.put(200, "OK"); + httpCodeMap.put(201, "Created"); + httpCodeMap.put(202, "Accepted"); + httpCodeMap.put(203, "Non-Authoritative Information"); + httpCodeMap.put(204, "No Content"); + httpCodeMap.put(205, "Reset Content"); + httpCodeMap.put(206, "Partial Content"); + httpCodeMap.put(207, "Multi-Status"); + httpCodeMap.put(208, "Already Reported"); + httpCodeMap.put(226, "IM Used"); + httpCodeMap.put(300, "Multiple Choices"); + httpCodeMap.put(301, "Moved Permanently"); + httpCodeMap.put(302, "Found"); + httpCodeMap.put(303, "See Other"); + httpCodeMap.put(304, "Not Modified"); + httpCodeMap.put(305, "Use Proxy"); + httpCodeMap.put(306, "Unused"); + httpCodeMap.put(307, "Temporary Redirect"); + httpCodeMap.put(308, "Permanent Redirect"); + httpCodeMap.put(400, "Bad Request"); + httpCodeMap.put(401, "Unauthorized"); + httpCodeMap.put(402, "Payment Required"); + httpCodeMap.put(403, "Forbidden"); + httpCodeMap.put(404, "Not Found"); + httpCodeMap.put(405, "Method Not Allowed"); + httpCodeMap.put(406, "Not Acceptable"); + httpCodeMap.put(407, "Proxy Authentication Required"); + httpCodeMap.put(408, "Request Timeout"); + httpCodeMap.put(409, "Conflict"); + httpCodeMap.put(410, "Gone"); + httpCodeMap.put(411, "Length Required"); + httpCodeMap.put(412, "Precondition Failed"); + httpCodeMap.put(413, "Payload Too Large"); + httpCodeMap.put(414, "URI Too Long"); + httpCodeMap.put(415, "Unsupported Media Type"); + httpCodeMap.put(416, "Range Not Satisfiable"); + httpCodeMap.put(417, "Expectation Failed"); + httpCodeMap.put(421, "Misdirected Request"); + httpCodeMap.put(422, "Unprocessable Entity"); + httpCodeMap.put(423, "Locked"); + httpCodeMap.put(424, "Failed Dependency"); + httpCodeMap.put(425, "Too Early"); + httpCodeMap.put(426, "Upgrade Required"); + httpCodeMap.put(428, "Precondition Required"); + httpCodeMap.put(429, "Too Many Requests"); + httpCodeMap.put(431, "Request Header Fields Too Large"); + httpCodeMap.put(451, "Unavailable For Legal Reasons"); + httpCodeMap.put(500, "Internal Server Error"); + httpCodeMap.put(501, "Not Implemented"); + httpCodeMap.put(502, "Bad Gateway"); + httpCodeMap.put(503, "Service Unavailable"); + httpCodeMap.put(504, "Gateway Timeout"); + httpCodeMap.put(505, "HTTP Version Not Supported"); + httpCodeMap.put(506, "Variant Also Negotiates"); + httpCodeMap.put(507, "Insufficient Storage"); + httpCodeMap.put(508, "Loop Denied"); + httpCodeMap.put(510, "Not Extended"); + httpCodeMap.put(511, "Network Authentication Required"); + HTTP_STATUS_CODE_TO_TEXT = Collections.unmodifiableMap(httpCodeMap); + } + + FakeUrlRequest(Callback callback, Executor userExecutor, Executor executor, String url, + boolean allowDirectExecutor, boolean trafficStatsTagSet, int trafficStatsTag, + final boolean trafficStatsUidSet, final int trafficStatsUid, + FakeCronetController fakeCronetController, FakeCronetEngine fakeCronetEngine) { + if (url == null) { + throw new NullPointerException("URL is required"); + } + if (callback == null) { + throw new NullPointerException("Listener is required"); + } + if (executor == null) { + throw new NullPointerException("Executor is required"); + } + mCallback = callback; + mUserExecutor = + allowDirectExecutor ? userExecutor : new DirectPreventingExecutor(userExecutor); + mExecutor = executor; + mCurrentUrl = url; + mFakeCronetController = fakeCronetController; + mFakeCronetEngine = fakeCronetEngine; + mAllowDirectExecutor = allowDirectExecutor; + } + + @Override + public void setUploadDataProvider(UploadDataProvider uploadDataProvider, Executor executor) { + if (uploadDataProvider == null) { + throw new NullPointerException("Invalid UploadDataProvider."); + } + synchronized (mLock) { + if (!checkHasContentTypeHeader()) { + throw new IllegalArgumentException( + "Requests with upload data must have a Content-Type."); + } + checkNotStarted(); + if (mHttpMethod == null) { + mHttpMethod = "POST"; + } + mUploadExecutor = + mAllowDirectExecutor ? executor : new DirectPreventingExecutor(executor); + mUploadDataProvider = uploadDataProvider; + } + } + + @Override + public void setHttpMethod(String method) { + synchronized (mLock) { + checkNotStarted(); + if (method == null) { + throw new NullPointerException("Method is required."); + } + if ("OPTIONS".equalsIgnoreCase(method) || "GET".equalsIgnoreCase(method) + || "HEAD".equalsIgnoreCase(method) || "POST".equalsIgnoreCase(method) + || "PUT".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method) + || "TRACE".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method)) { + mHttpMethod = method; + } else { + throw new IllegalArgumentException("Invalid http method: " + method); + } + } + } + + @Override + public void addHeader(String header, String value) { + synchronized (mLock) { + checkNotStarted(); + mAllHeadersList.add(new AbstractMap.SimpleEntry(header, value)); + } + } + + /** + * Verifies that the request is not already started and throws an exception if it is. + */ + @GuardedBy("mLock") + private void checkNotStarted() { + if (mState != State.NOT_STARTED) { + throw new IllegalStateException("Request is already started. State is: " + mState); + } + } + + @Override + public void start() { + synchronized (mLock) { + if (mFakeCronetEngine.startRequest()) { + boolean transitionedState = false; + try { + transitionStates(State.NOT_STARTED, State.STARTED); + mAdditionalStatusDetails = Status.CONNECTING; + transitionedState = true; + } finally { + if (!transitionedState) { + cleanup(); + } + } + mUrlChain.add(mCurrentUrl); + if (mUploadDataProvider != null) { + mFakeDataSink = + new FakeDataSink(mUploadExecutor, mExecutor, mUploadDataProvider); + mFakeDataSink.start(/* firstTime= */ true); + } else { + fakeConnect(); + } + } else { + throw new IllegalStateException("This request's CronetEngine is already shutdown."); + } + } + } + + /** + * Fakes a request to a server by retrieving a response to this {@link UrlRequest} from the + * {@link FakeCronetController}. + */ + @GuardedBy("mLock") + private void fakeConnect() { + mAdditionalStatusDetails = Status.WAITING_FOR_RESPONSE; + mCurrentFakeResponse = mFakeCronetController.getResponse( + mCurrentUrl, mHttpMethod, mAllHeadersList, mRequestBody); + int responseCode = mCurrentFakeResponse.getHttpStatusCode(); + mUrlResponseInfo = new UrlResponseInfoImpl( + Collections.unmodifiableList(new ArrayList<>(mUrlChain)), responseCode, + getDescriptionByCode(responseCode), mCurrentFakeResponse.getAllHeadersList(), + mCurrentFakeResponse.getWasCached(), mCurrentFakeResponse.getNegotiatedProtocol(), + mCurrentFakeResponse.getProxyServer(), + mCurrentFakeResponse.getResponseBody().length); + mResponse = ByteBuffer.wrap(mCurrentFakeResponse.getResponseBody()); + // Check for a redirect. + if (responseCode >= 300 && responseCode < 400) { + processRedirectResponse(); + } else { + closeUploadDataProvider(); + final UrlResponseInfo info = mUrlResponseInfo; + transitionStates(State.STARTED, State.AWAITING_READ); + executeCheckedRunnable(new CheckedRunnable() { + @Override + public void run() throws Exception { + mCallback.onResponseStarted(FakeUrlRequest.this, info); + } + }); + } + } + + /** + * Retrieves the redirect location from the response headers and responds to the + * {@link UrlRequest.Callback#onRedirectReceived} method. Adds the redirect URL to the chain. + * + * @param url the URL that the {@link FakeUrlResponse} redirected this request to + */ + @GuardedBy("mLock") + private void processRedirectResponse() { + transitionStates(State.STARTED, State.REDIRECT_RECEIVED); + if (mUrlResponseInfo.getAllHeaders().get("location") == null) { + // Response did not have a location header, so this request must fail. + final String prevUrl = mCurrentUrl; + mUserExecutor.execute(new Runnable() { + @Override + public void run() { + tryToFailWithException(new CronetExceptionImpl( + "Request failed due to bad redirect HTTP headers", + new IllegalStateException("Response recieved from URL: " + prevUrl + + " was a redirect, but lacked a location header."))); + } + }); + return; + } + String pendingRedirectUrl = + URI.create(mCurrentUrl) + .resolve(mUrlResponseInfo.getAllHeaders().get("location").get(0)) + .toString(); + mCurrentUrl = pendingRedirectUrl; + mUrlChain.add(mCurrentUrl); + transitionStates(State.REDIRECT_RECEIVED, State.AWAITING_FOLLOW_REDIRECT); + final UrlResponseInfo info = mUrlResponseInfo; + mExecutor.execute(new Runnable() { + @Override + public void run() { + executeCheckedRunnable(new CheckedRunnable() { + @Override + public void run() throws Exception { + mCallback.onRedirectReceived(FakeUrlRequest.this, info, pendingRedirectUrl); + } + }); + } + }); + } + + @Override + public void read(ByteBuffer buffer) { + // Entering {@link #State.READING} is somewhat redundant because the entire response is + // already acquired. We should still transition so that the fake {@link UrlRequest} follows + // the same state flow as a real request. + Preconditions.checkHasRemaining(buffer); + Preconditions.checkDirect(buffer); + synchronized (mLock) { + transitionStates(State.AWAITING_READ, State.READING); + final UrlResponseInfo info = mUrlResponseInfo; + if (mResponse.hasRemaining()) { + transitionStates(State.READING, State.AWAITING_READ); + fillBufferWithResponse(buffer); + mExecutor.execute(new Runnable() { + @Override + public void run() { + executeCheckedRunnable(new CheckedRunnable() { + @Override + public void run() throws Exception { + mCallback.onReadCompleted(FakeUrlRequest.this, info, buffer); + } + }); + } + }); + } else { + if (setTerminalState(State.COMPLETE)) { + mUserExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onSucceeded(FakeUrlRequest.this, info); + } + }); + } + } + } + } + + /** + * Puts as much of the remaining response as will fit into the {@link ByteBuffer} and removes + * that part of the string from the response left to send. + * + * @param buffer the {@link ByteBuffer} to put the response into + * @return the buffer with the response that we want to send back in it + */ + @GuardedBy("mLock") + private void fillBufferWithResponse(ByteBuffer buffer) { + final int maxTransfer = Math.min(buffer.remaining(), mResponse.remaining()); + ByteBuffer temp = mResponse.duplicate(); + temp.limit(temp.position() + maxTransfer); + buffer.put(temp); + mResponse.position(mResponse.position() + maxTransfer); + } + + @Override + public void followRedirect() { + synchronized (mLock) { + transitionStates(State.AWAITING_FOLLOW_REDIRECT, State.STARTED); + if (mFakeDataSink != null) { + mFakeDataSink = new FakeDataSink(mUploadExecutor, mExecutor, mUploadDataProvider); + mFakeDataSink.start(/* firstTime= */ false); + } else { + fakeConnect(); + } + } + } + + @Override + public void cancel() { + synchronized (mLock) { + final UrlResponseInfo info = mUrlResponseInfo; + if (setTerminalState(State.CANCELLED)) { + mUserExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onCanceled(FakeUrlRequest.this, info); + } + }); + } + } + } + + @Override + public void getStatus(final StatusListener listener) { + synchronized (mLock) { + int extraStatus = mAdditionalStatusDetails; + + @StatusValues + final int status; + switch (mState) { + case State.ERROR: + case State.COMPLETE: + case State.CANCELLED: + case State.NOT_STARTED: + status = Status.INVALID; + break; + case State.STARTED: + status = extraStatus; + break; + case State.REDIRECT_RECEIVED: + case State.AWAITING_FOLLOW_REDIRECT: + case State.AWAITING_READ: + status = Status.IDLE; + break; + case State.READING: + status = Status.READING_RESPONSE; + break; + default: + throw new IllegalStateException("Switch is exhaustive: " + mState); + } + mUserExecutor.execute(new Runnable() { + @Override + public void run() { + listener.onStatus(status); + } + }); + } + } + + @Override + public boolean isDone() { + synchronized (mLock) { + return mState == State.COMPLETE || mState == State.ERROR || mState == State.CANCELLED; + } + } + + /** + * Swaps from the expected state to a new state. If the swap fails, and it's not + * due to an earlier error or cancellation, throws an exception. + */ + @GuardedBy("mLock") + private void transitionStates(@State int expected, @State int newState) { + if (mState == expected) { + mState = newState; + } else { + if (!(mState == State.CANCELLED || mState == State.ERROR)) { + throw new IllegalStateException( + "Invalid state transition - expected " + expected + " but was " + mState); + } + } + } + + /** + * Calls the callback's onFailed method if this request is not complete. Should be executed on + * the {@code mUserExecutor}, unless the error is a {@link InlineExecutionProhibitedException} + * produced by the {@code mUserExecutor}. + * + * @param e the {@link CronetException} that the request should pass to the callback. + * + */ + private void tryToFailWithException(CronetException e) { + synchronized (mLock) { + if (setTerminalState(State.ERROR)) { + mCallback.onFailed(FakeUrlRequest.this, mUrlResponseInfo, e); + } + } + } + + /** + * Execute a {@link CheckedRunnable} and call the {@link UrlRequest.Callback#onFailed} method + * if there is an exception and we can change to {@link State.ERROR}. Used to communicate with + * the {@link UrlRequest.Callback} methods using the executor provided by the constructor. This + * should be the last call in the critical section. If this is not the last call in a critical + * section, we risk modifying shared resources in a recursive call to another method + * guarded by the {@code mLock}. This is because in Java synchronized blocks are reentrant. + * + * @param checkedRunnable the runnable to execute + */ + private void executeCheckedRunnable(JavaUrlRequestUtils.CheckedRunnable checkedRunnable) { + try { + mUserExecutor.execute(new Runnable() { + @Override + public void run() { + try { + checkedRunnable.run(); + } catch (Exception e) { + tryToFailWithException(new CallbackExceptionImpl( + "Exception received from UrlRequest.Callback", e)); + } + } + }); + } catch (InlineExecutionProhibitedException e) { + // Don't try to fail using the {@code mUserExecutor} because it produced this error. + tryToFailWithException( + new CronetExceptionImpl("Exception posting task to executor", e)); + } + } + + /** + * Check the current state and if the request is started, but not complete, failed, or + * cancelled, change to the terminal state and call {@link FakeCronetEngine#onDestroyed}. This + * method ensures {@link FakeCronetEngine#onDestroyed} is only called once. + * + * @param terminalState the terminal state to set; one of {@link State.ERROR}, + * {@link State.COMPLETE}, or {@link State.CANCELLED} + * @return true if the terminal state has been set. + */ + @GuardedBy("mLock") + private boolean setTerminalState(@State int terminalState) { + switch (mState) { + case State.NOT_STARTED: + throw new IllegalStateException("Can't enter terminal state before start"); + case State.ERROR: // fallthrough + case State.COMPLETE: // fallthrough + case State.CANCELLED: + return false; // Already in a terminal state + default: { + mState = terminalState; + cleanup(); + return true; + } + } + } + + @GuardedBy("mLock") + private void cleanup() { + closeUploadDataProvider(); + mFakeCronetEngine.onRequestDestroyed(); + } + + /** + * Executed only once after the request has finished using the {@link UploadDataProvider}. + * Closes the {@link UploadDataProvider} if it exists and has not already been closed. + */ + @GuardedBy("mLock") + private void closeUploadDataProvider() { + if (mUploadDataProvider != null && !mUploadProviderClosed) { + try { + mUploadExecutor.execute(uploadErrorSetting(new CheckedRunnable() { + @Override + public void run() throws Exception { + synchronized (mLock) { + mUploadDataProvider.close(); + mUploadProviderClosed = true; + } + } + })); + } catch (RejectedExecutionException e) { + Log.e(TAG, "Exception when closing uploadDataProvider", e); + } + } + } + + /** + * Wraps a {@link CheckedRunnable} in a runnable that will attempt to fail the request if there + * is an exception. + * + * @param delegate the {@link CheckedRunnable} to try to run + * @return a {@link Runnable} that wraps the delegate runnable. + */ + private Runnable uploadErrorSetting(final CheckedRunnable delegate) { + return new Runnable() { + @Override + public void run() { + try { + delegate.run(); + } catch (Throwable t) { + enterUploadErrorState(t); + } + } + }; + } + + /** + * Fails the request with an error. Called when uploading the request body using an + * {@link UploadDataProvider} fails. + * + * @param error the error that caused this request to fail which should be returned to the + * {@link UrlRequest.Callback} + */ + private void enterUploadErrorState(final Throwable error) { + synchronized (mLock) { + mUserExecutor.execute(new Runnable() { + @Override + public void run() { + tryToFailWithException(new CronetExceptionImpl( + "Exception received from UploadDataProvider", error)); + } + }); + } + } + + /** + * Adapted from {@link JavaUrlRequest.OutputStreamDataSink}. Stores the received message in a + * {@link ByteArrayOutputStream} and transfers it to the {@code mRequestBody} when the response + * has been fully acquired. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + final class FakeDataSink extends JavaUploadDataSinkBase { + private final ByteArrayOutputStream mTotalUploadStream = new ByteArrayOutputStream(); + + FakeDataSink(final Executor userExecutor, Executor executor, UploadDataProvider provider) { + super(userExecutor, executor, provider); + } + + @Override + public Runnable getErrorSettingRunnable(JavaUrlRequestUtils.CheckedRunnable runnable) { + return new Runnable() { + @Override + public void run() { + try { + runnable.run(); + } catch (Throwable t) { + mUserExecutor.execute(new Runnable() { + @Override + public void run() { + tryToFailWithException(new CronetExceptionImpl("System error", t)); + } + }); + } + } + }; + } + + @Override + protected Runnable getUploadErrorSettingRunnable( + JavaUrlRequestUtils.CheckedRunnable runnable) { + return uploadErrorSetting(runnable); + } + + @Override + protected void processUploadError(final Throwable error) { + enterUploadErrorState(error); + } + + @Override + protected int processSuccessfulRead(ByteBuffer buffer) throws IOException { + mTotalUploadStream.write(buffer.array(), buffer.arrayOffset(), buffer.remaining()); + return buffer.remaining(); + } + + /** + * Terminates the upload stage of the request. Writes the received bytes to the byte array: + * {@code mRequestBody}. Connects to the current URL for this request. + */ + @Override + protected void finish() throws IOException { + synchronized (mLock) { + mRequestBody = mTotalUploadStream.toByteArray(); + fakeConnect(); + } + } + + @Override + protected void initializeRead() throws IOException { + // Nothing to do before every read in this implementation. + } + + @Override + protected void initializeStart(long totalBytes) { + // Nothing to do to initialize the upload in this implementation. + } + } + + /** + * Verifies that the "content-type" header is present. Must be checked before an + * {@link UploadDataProvider} is premitted to be set. + * + * @return true if the "content-type" header is present in the request headers. + */ + @GuardedBy("mLock") + private boolean checkHasContentTypeHeader() { + for (Map.Entry entry : mAllHeadersList) { + if (entry.getKey().equalsIgnoreCase("content-type")) { + return true; + } + } + return false; + } + + /** + * Gets a human readable description for a HTTP status code. + * + * @param code the code to retrieve the status for + * @return the HTTP status text as a string + */ + private static String getDescriptionByCode(Integer code) { + return HTTP_STATUS_CODE_TO_TEXT.containsKey(code) ? HTTP_STATUS_CODE_TO_TEXT.get(code) + : "Unassigned"; + } +} diff --git a/src/components/cronet/android/fake/java/org/chromium/net/test/FakeUrlResponse.java b/src/components/cronet/android/fake/java/org/chromium/net/test/FakeUrlResponse.java new file mode 100644 index 0000000000..38b078de38 --- /dev/null +++ b/src/components/cronet/android/fake/java/org/chromium/net/test/FakeUrlResponse.java @@ -0,0 +1,296 @@ +// Copyright 2019 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.test; + +import org.chromium.net.UrlResponseInfo; + +import java.io.UnsupportedEncodingException; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +// TODO(kirchman): Update this to explain inter-class usage once other classes land. +/** + * + * Fake response model for UrlRequest used by Fake Cronet. + */ +public class FakeUrlResponse { + private final int mHttpStatusCode; + // Entries to mAllHeadersList should never be mutated. + private final List> mAllHeadersList; + private final boolean mWasCached; + private final String mNegotiatedProtocol; + private final String mProxyServer; + private final byte[] mResponseBody; + + private static T getNullableOrDefault(T nullableObject, T defaultObject) { + if (nullableObject != null) { + return nullableObject; + } + return defaultObject; + } + + /** + * Constructs a {@link FakeUrlResponse} from a {@link FakeUrlResponse.Builder}. + * @param builder the {@link FakeUrlResponse.Builder} to create the response from + */ + private FakeUrlResponse(Builder builder) { + mHttpStatusCode = builder.mHttpStatusCode; + mAllHeadersList = Collections.unmodifiableList(new ArrayList<>(builder.mAllHeadersList)); + mWasCached = builder.mWasCached; + mNegotiatedProtocol = builder.mNegotiatedProtocol; + mProxyServer = builder.mProxyServer; + mResponseBody = builder.mResponseBody; + } + + /** + * Constructs a {@link FakeUrlResponse} from a {@link UrlResponseInfo}. All nullable fields in + * the {@link UrlResponseInfo} are initialized to the default value if the provided value is + * null. + * + * @param info the {@link UrlResponseInfo} used to initialize this object's fields + */ + public FakeUrlResponse(UrlResponseInfo info) { + mHttpStatusCode = info.getHttpStatusCode(); + mAllHeadersList = Collections.unmodifiableList(new ArrayList<>(info.getAllHeadersAsList())); + mWasCached = info.wasCached(); + mNegotiatedProtocol = getNullableOrDefault( + info.getNegotiatedProtocol(), Builder.DEFAULT_NEGOTIATED_PROTOCOL); + mProxyServer = getNullableOrDefault(info.getProxyServer(), Builder.DEFAULT_PROXY_SERVER); + mResponseBody = Builder.DEFAULT_RESPONSE_BODY; + } + + /** + * Builds a {@link FakeUrlResponse}. + */ + public static class Builder { + private static final int DEFAULT_HTTP_STATUS_CODE = 200; + private static final List> INTERNAL_INITIAL_HEADERS_LIST = + new ArrayList<>(); + private static final boolean DEFAULT_WAS_CACHED = false; + private static final String DEFAULT_NEGOTIATED_PROTOCOL = ""; + private static final String DEFAULT_PROXY_SERVER = ""; + private static final byte[] DEFAULT_RESPONSE_BODY = new byte[0]; + + private int mHttpStatusCode = DEFAULT_HTTP_STATUS_CODE; + // Entries to mAllHeadersList should never be mutated. + private List> mAllHeadersList = + new ArrayList<>(INTERNAL_INITIAL_HEADERS_LIST); + private boolean mWasCached = DEFAULT_WAS_CACHED; + private String mNegotiatedProtocol = DEFAULT_NEGOTIATED_PROTOCOL; + private String mProxyServer = DEFAULT_PROXY_SERVER; + private byte[] mResponseBody = DEFAULT_RESPONSE_BODY; + + /** + * Constructs a {@link FakeUrlResponse.Builder} with the default parameters. + */ + public Builder() {} + + /** + * Constructs a {@link FakeUrlResponse.Builder} from a source {@link FakeUrlResponse}. + * + * @param source a {@link FakeUrlResponse} to copy into this {@link FakeUrlResponse.Builder} + */ + private Builder(FakeUrlResponse source) { + mHttpStatusCode = source.getHttpStatusCode(); + mAllHeadersList = new ArrayList<>(source.getAllHeadersList()); + mWasCached = source.getWasCached(); + mNegotiatedProtocol = source.getNegotiatedProtocol(); + mProxyServer = source.getProxyServer(); + mResponseBody = source.getResponseBody(); + } + + /** + * Sets the HTTP status code. The default value is 200. + * + * @param httpStatusCode for {@link UrlResponseInfo.getHttpStatusCode()} + * @return the builder with the corresponding HTTP status code set + */ + public Builder setHttpStatusCode(int httpStatusCode) { + mHttpStatusCode = httpStatusCode; + return this; + } + + /** + * Adds a response header to built {@link FakeUrlResponse}s. + * + * @param name the name of the header key, for example, "location" for a redirect header + * @param value the header value + * @return the builder with the corresponding header set + */ + public Builder addHeader(String name, String value) { + mAllHeadersList.add(new AbstractMap.SimpleEntry<>(name, value)); + return this; + } + + /** + * Sets result of {@link UrlResponseInfo.wasCached()}. The default wasCached value is false. + * + * @param wasCached for {@link UrlResponseInfo.wasCached()} + * @return the builder with the corresponding wasCached field set + */ + public Builder setWasCached(boolean wasCached) { + mWasCached = wasCached; + return this; + } + + /** + * Sets result of {@link UrlResponseInfo.getNegotiatedProtocol()}. The default negotiated + * protocol is an empty string. + * + * @param negotiatedProtocol for {@link UrlResponseInfo.getNegotiatedProtocol()} + * @return the builder with the corresponding negotiatedProtocol field set + */ + public Builder setNegotiatedProtocol(String negotiatedProtocol) { + mNegotiatedProtocol = negotiatedProtocol; + return this; + } + + /** + * Sets result of {@link UrlResponseInfo.getProxyServer()}. The default proxy server is an + * empty string. + * + * @param proxyServer for {@link UrlResponseInfo.getProxyServer()} + * @return the builder with the corresponding proxyServer field set + */ + public Builder setProxyServer(String proxyServer) { + mProxyServer = proxyServer; + return this; + } + + /** + * Sets the response body for a response. The default response body is an empty byte array. + * + * @param body all the information the server returns + * @return the builder with the corresponding responseBody field set + */ + public Builder setResponseBody(byte[] body) { + mResponseBody = body; + return this; + } + + /** + * Constructs a {@link FakeUrlResponse} from this {@link FakeUrlResponse.Builder}. + * + * @return a FakeUrlResponse with all fields set according to this builder + */ + public FakeUrlResponse build() { + return new FakeUrlResponse(this); + } + } + + /** + * Returns the HTTP status code. + * + * @return the HTTP status code. + */ + int getHttpStatusCode() { + return mHttpStatusCode; + } + + /** + * Returns an unmodifiable list of the response header key and value pairs. + * + * @return an unmodifiable list of response header key and value pairs + */ + List> getAllHeadersList() { + return mAllHeadersList; + } + + /** + * Returns the wasCached value for this response. + * + * @return the wasCached value for this response + */ + boolean getWasCached() { + return mWasCached; + } + + /** + * Returns the protocol (for example 'quic/1+spdy/3') negotiated with the server. + * + * @return the protocol negotiated with the server + */ + String getNegotiatedProtocol() { + return mNegotiatedProtocol; + } + + /** + * Returns the proxy server that was used for the request. + * + * @return the proxy server that was used for the request + */ + String getProxyServer() { + return mProxyServer; + } + + /** + * Returns the body of the response as a byte array. Used for {@link UrlRequest.Callback} + * {@code read()} callback. + * + * @return the response body + */ + byte[] getResponseBody() { + return mResponseBody; + } + + /** + * Returns a mutable builder representation of this {@link FakeUrlResponse} + * + * @return a {@link FakeUrlResponse.Builder} with all fields copied from this instance. + */ + public Builder toBuilder() { + return new Builder(this); + } + + @Override + public boolean equals(Object otherObj) { + if (!(otherObj instanceof FakeUrlResponse)) { + return false; + } + FakeUrlResponse other = (FakeUrlResponse) otherObj; + return (mHttpStatusCode == other.mHttpStatusCode + && mAllHeadersList.equals(other.mAllHeadersList) && mWasCached == other.mWasCached + && mNegotiatedProtocol.equals(other.mNegotiatedProtocol) + && mProxyServer.equals(other.mProxyServer) + && Arrays.equals(mResponseBody, other.mResponseBody)); + } + + @Override + public int hashCode() { + return Objects.hash(mHttpStatusCode, mAllHeadersList, mWasCached, mNegotiatedProtocol, + mProxyServer, Arrays.hashCode(mResponseBody)); + } + + @Override + public String toString() { + StringBuilder outputString = new StringBuilder(); + outputString.append("HTTP Status Code: " + mHttpStatusCode); + outputString.append(" Headers: " + mAllHeadersList.toString()); + outputString.append(" Was Cached: " + mWasCached); + outputString.append(" Negotiated Protocol: " + mNegotiatedProtocol); + outputString.append(" Proxy Server: " + mProxyServer); + outputString.append(" Response Body "); + try { + String bodyString = new String(mResponseBody, "UTF-8"); + outputString.append("(UTF-8): " + bodyString); + } catch (UnsupportedEncodingException e) { + outputString.append("(hexadecimal): " + getHexStringFromBytes(mResponseBody)); + } + return outputString.toString(); + } + + private String getHexStringFromBytes(byte[] bytes) { + StringBuilder bytesToHexStringBuilder = new StringBuilder(); + for (byte b : mResponseBody) { + bytesToHexStringBuilder.append(String.format("%02x", b)); + } + return bytesToHexStringBuilder.toString(); + } +} diff --git a/src/components/cronet/android/fake/java/org/chromium/net/test/ResponseMatcher.java b/src/components/cronet/android/fake/java/org/chromium/net/test/ResponseMatcher.java new file mode 100644 index 0000000000..28c07022dc --- /dev/null +++ b/src/components/cronet/android/fake/java/org/chromium/net/test/ResponseMatcher.java @@ -0,0 +1,30 @@ +// Copyright 2019 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.test; + +import androidx.annotation.Nullable; + +import org.chromium.net.UrlRequest; + +import java.util.List; +import java.util.Map; + +/** + * An interface for matching {@link UrlRequest}s to {@link FakeUrlResponse}s. + */ +public interface ResponseMatcher { + /** + * Optionally gets a response based on the request parameters. + * + * @param url the URL the {@link UrlRequest} is connecting to + * @param httpMethod the HTTP method the {@link UrlRequest} is connecting with + * @param headers the {@link UrlRequest} headers + * @param body the body of the request + * @return a {@link FakeUrlResponse} if there is a matching response, or {@code null} otherwise + */ + @Nullable + FakeUrlResponse getMatchingResponse( + String url, String httpMethod, List> headers, byte[] body); +} diff --git a/src/components/cronet/android/fake/java/org/chromium/net/test/UrlResponseMatcher.java b/src/components/cronet/android/fake/java/org/chromium/net/test/UrlResponseMatcher.java new file mode 100644 index 0000000000..2bb55ab9e4 --- /dev/null +++ b/src/components/cronet/android/fake/java/org/chromium/net/test/UrlResponseMatcher.java @@ -0,0 +1,41 @@ +// Copyright 2019 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.test; + +import org.chromium.net.UrlRequest; + +import java.util.List; +import java.util.Map; + +/** + * A {@link ResponseMatcher} that matches {@link UrlRequest}s with a particular URL. + */ +public class UrlResponseMatcher implements ResponseMatcher { + private final String mUrl; + private final FakeUrlResponse mResponse; + + /** + * Constructs a {@link UrlResponseMatcher} that responds to requests for URL {@code url} with + * {@code response}. + * @param url the URL that the response should be returned for + * @param response the response to return if the URL matches the request's URL + */ + public UrlResponseMatcher(String url, FakeUrlResponse response) { + if (url == null) { + throw new NullPointerException("URL is required."); + } + if (response == null) { + throw new NullPointerException("Response is required."); + } + mUrl = url; + mResponse = response; + } + + @Override + public FakeUrlResponse getMatchingResponse( + String url, String httpMethod, List> headers, byte[] body) { + return mUrl.equals(url) ? mResponse : null; + } +} diff --git a/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeCronetControllerTest.java b/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeCronetControllerTest.java new file mode 100644 index 0000000000..f7be2c0d52 --- /dev/null +++ b/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeCronetControllerTest.java @@ -0,0 +1,255 @@ +// Copyright 2019 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.net.CronetEngine; +import org.chromium.net.impl.JavaCronetEngineBuilderImpl; + +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; + +/** + * Test functionality of {@link FakeCronetController}. + */ +@RunWith(AndroidJUnit4.class) +public class FakeCronetControllerTest { + Context mContext; + FakeCronetController mFakeCronetController; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getTargetContext(); + mFakeCronetController = new FakeCronetController(); + } + + @Test + @SmallTest + public void testGetFakeCronetEnginesStartsEmpty() { + List engines = FakeCronetController.getFakeCronetEngines(); + assertEquals(0, engines.size()); + } + + @Test + @SmallTest + public void testGetFakeCronetEnginesIncludesCreatedEngineInOrder() { + // Create an instance with the controller. + CronetEngine engine = mFakeCronetController.newFakeCronetEngineBuilder(mContext).build(); + // Create an instance with the provider. + FakeCronetProvider provider = new FakeCronetProvider(mContext); + CronetEngine providerEngine = provider.createBuilder().build(); + List engines = FakeCronetController.getFakeCronetEngines(); + + assertTrue(engines.contains(engine)); + assertTrue(engines.contains(providerEngine)); + assertEquals(engine, engines.get(0)); + assertEquals(providerEngine, engines.get(1)); + } + + @Test + @SmallTest + public void testGetControllerGetsCorrectController() { + // Create an instance with the controller. + CronetEngine engine = mFakeCronetController.newFakeCronetEngineBuilder(mContext).build(); + CronetEngine engine2 = mFakeCronetController.newFakeCronetEngineBuilder(mContext).build(); + + // Create two engines with a second controller. + FakeCronetController newController = new FakeCronetController(); + CronetEngine newControllerEngine = + newController.newFakeCronetEngineBuilder(mContext).build(); + CronetEngine newControllerEngine2 = + newController.newFakeCronetEngineBuilder(mContext).build(); + + // Create an instance with the provider. + FakeCronetProvider provider = new FakeCronetProvider(mContext); + CronetEngine providerEngine = provider.createBuilder().build(); + + assertEquals( + mFakeCronetController, FakeCronetController.getControllerForFakeEngine(engine)); + assertEquals( + mFakeCronetController, FakeCronetController.getControllerForFakeEngine(engine2)); + assertEquals(newController, + FakeCronetController.getControllerForFakeEngine(newControllerEngine)); + assertEquals(newController, + FakeCronetController.getControllerForFakeEngine(newControllerEngine2)); + + // TODO(kirchman): Test which controller the provider-created engine uses once the fake + // UrlRequest class has been implemented. + assertNotEquals(mFakeCronetController, + FakeCronetController.getControllerForFakeEngine(providerEngine)); + assertNotEquals( + newController, FakeCronetController.getControllerForFakeEngine(providerEngine)); + assertNotNull(FakeCronetController.getControllerForFakeEngine(providerEngine)); + } + + @Test + @SmallTest + public void testAddNonFakeCronetEngineNotAllowed() { + CronetEngine javaEngine = new JavaCronetEngineBuilderImpl(mContext).build(); + + try { + FakeCronetController.getControllerForFakeEngine(javaEngine); + fail("Should not be able to get a controller for a non-fake CronetEngine."); + } catch (IllegalArgumentException e) { + assertEquals("Provided CronetEngine is not a fake CronetEngine", e.getMessage()); + } + } + + @Test + @SmallTest + public void testShutdownRemovesCronetEngine() { + CronetEngine engine = mFakeCronetController.newFakeCronetEngineBuilder(mContext).build(); + CronetEngine engine2 = mFakeCronetController.newFakeCronetEngineBuilder(mContext).build(); + List engines = FakeCronetController.getFakeCronetEngines(); + assertTrue(engines.contains(engine)); + assertTrue(engines.contains(engine2)); + + engine.shutdown(); + engines = FakeCronetController.getFakeCronetEngines(); + + assertFalse(engines.contains(engine)); + assertTrue(engines.contains(engine2)); + } + + @Test + @SmallTest + public void testResponseMatchersConsultedInOrderOfAddition() { + String url = "url"; + FakeUrlResponse response = + new FakeUrlResponse.Builder().setResponseBody("body text".getBytes()).build(); + ResponseMatcher matcher = new UrlResponseMatcher(url, response); + mFakeCronetController.addResponseMatcher(matcher); + mFakeCronetController.addSuccessResponse(url, "different text".getBytes()); + + FakeUrlResponse foundResponse = + mFakeCronetController.getResponse(new String(url), null, null, null); + + assertEquals(response, foundResponse); + } + + @Test + @SmallTest + public void testRemoveResponseMatcher() { + String url = "url"; + FakeUrlResponse response = + new FakeUrlResponse.Builder().setResponseBody("body text".getBytes()).build(); + ResponseMatcher matcher = new UrlResponseMatcher(url, response); + mFakeCronetController.addResponseMatcher(matcher); + mFakeCronetController.removeResponseMatcher(matcher); + + FakeUrlResponse foundResponse = mFakeCronetController.getResponse(url, null, null, null); + + assertEquals(404, foundResponse.getHttpStatusCode()); + assertNotEquals(response, foundResponse); + } + + @Test + @SmallTest + public void testClearResponseMatchers() { + String url = "url"; + FakeUrlResponse response = + new FakeUrlResponse.Builder().setResponseBody("body text".getBytes()).build(); + ResponseMatcher matcher = new UrlResponseMatcher(url, response); + mFakeCronetController.addResponseMatcher(matcher); + mFakeCronetController.clearResponseMatchers(); + + FakeUrlResponse foundResponse = mFakeCronetController.getResponse(url, null, null, null); + + assertEquals(404, foundResponse.getHttpStatusCode()); + assertNotEquals(response, foundResponse); + } + + @Test + @SmallTest + public void testAddUrlResponseMatcher() { + String url = "url"; + FakeUrlResponse response = + new FakeUrlResponse.Builder().setResponseBody("body text".getBytes()).build(); + mFakeCronetController.addResponseForUrl(response, url); + + FakeUrlResponse foundResponse = mFakeCronetController.getResponse(url, null, null, null); + + assertEquals(foundResponse, response); + } + + @Test + @SmallTest + public void testDefaultResponseIs404() { + FakeUrlResponse foundResponse = mFakeCronetController.getResponse("url", null, null, null); + + assertEquals(404, foundResponse.getHttpStatusCode()); + } + + @Test + @SmallTest + public void testAddRedirectResponse() { + String url = "url"; + String location = "/TEST_REDIRECT_LOCATION"; + mFakeCronetController.addRedirectResponse(location, url); + + FakeUrlResponse foundResponse = mFakeCronetController.getResponse("url", null, null, null); + Map.Entry headerEntry = new AbstractMap.SimpleEntry<>("location", location); + + assertTrue(foundResponse.getAllHeadersList().contains(headerEntry)); + assertTrue(foundResponse.getHttpStatusCode() >= 300); + assertTrue(foundResponse.getHttpStatusCode() < 400); + } + + @Test + @SmallTest + public void testAddErrorResponse() { + String url = "url"; + int httpStatusCode = 400; + mFakeCronetController.addHttpErrorResponse(httpStatusCode, url); + + FakeUrlResponse foundResponse = mFakeCronetController.getResponse(url, null, null, null); + + assertEquals(foundResponse.getHttpStatusCode(), httpStatusCode); + } + + @Test + @SmallTest + public void testAddErrorResponseWithNonErrorCodeThrowsException() { + int nonErrorCode = 200; + try { + mFakeCronetController.addHttpErrorResponse(nonErrorCode, "url"); + fail("Should not be able to add an error response with a non-error code."); + } catch (IllegalArgumentException e) { + assertEquals("Expected HTTP error code (code >= 400), but was: " + nonErrorCode, + e.getMessage()); + } + } + + @Test + @SmallTest + public void testAddSuccessResponse() { + String url = "url"; + String body = "TEST_BODY"; + mFakeCronetController.addSuccessResponse(url, body.getBytes()); + + FakeUrlResponse foundResponse = mFakeCronetController.getResponse(url, null, null, null); + + assertTrue(foundResponse.getHttpStatusCode() >= 200); + assertTrue(foundResponse.getHttpStatusCode() < 300); + assertEquals(body, new String(foundResponse.getResponseBody())); + } +} diff --git a/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeCronetEngineTest.java b/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeCronetEngineTest.java new file mode 100644 index 0000000000..1b986566cb --- /dev/null +++ b/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeCronetEngineTest.java @@ -0,0 +1,291 @@ +// Copyright 2019 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.test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.net.CronetException; +import org.chromium.net.UrlRequest; +import org.chromium.net.UrlResponseInfo; +import org.chromium.net.impl.ImplVersion; + +import java.net.Proxy; +import java.nio.ByteBuffer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Test functionality of {@link FakeCronetEngine}. + */ +@RunWith(AndroidJUnit4.class) +public class FakeCronetEngineTest { + Context mContext; + FakeCronetEngine mFakeCronetEngine; + UrlRequest.Callback mCallback; + ExecutorService mExecutor; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getTargetContext(); + mFakeCronetEngine = + (FakeCronetEngine) new FakeCronetProvider(mContext).createBuilder().build(); + mCallback = new UrlRequest.Callback() { + @Override + public void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) {} + + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) {} + + @Override + public void onReadCompleted( + UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) {} + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) {} + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) {} + + @Override + public void onCanceled(UrlRequest request, UrlResponseInfo info) {} + }; + mExecutor = Executors.newSingleThreadExecutor(); + } + + @Test + @SmallTest + public void testShutdownEngineThrowsExceptionWhenApiCalled() { + mFakeCronetEngine.shutdown(); + + try { + mFakeCronetEngine.newUrlRequestBuilder("", mCallback, mExecutor).build(); + fail("newUrlRequestBuilder API not checked for shutdown engine."); + } catch (IllegalStateException e) { + assertEquals( + "This instance of CronetEngine has been shutdown and can no longer be used.", + e.getMessage()); + } + } + + @Test + @SmallTest + public void testShutdownEngineThrowsExceptionWhenBidirectionalStreamApiCalled() { + mFakeCronetEngine.shutdown(); + + try { + mFakeCronetEngine.newBidirectionalStreamBuilder("", null, null); + fail("newBidirectionalStreamBuilder API not checked for shutdown engine."); + } catch (IllegalStateException e) { + assertEquals( + "This instance of CronetEngine has been shutdown and can no longer be used.", + e.getMessage()); + } + } + + @Test + @SmallTest + public void testExceptionForNewBidirectionalStreamApi() { + try { + mFakeCronetEngine.newBidirectionalStreamBuilder("", null, null); + fail("newBidirectionalStreamBuilder API should not be available."); + } catch (UnsupportedOperationException e) { + assertEquals("The bidirectional stream API is not supported by the Fake implementation " + + "of CronetEngine.", + e.getMessage()); + } + } + + @Test + @SmallTest + public void testExceptionForOpenConnectionApi() { + try { + mFakeCronetEngine.openConnection(null); + fail("openConnection API should not be available."); + } catch (Exception e) { + assertEquals("The openConnection API is not supported by the Fake implementation of " + + "CronetEngine.", + e.getMessage()); + } + } + + @Test + @SmallTest + public void testExceptionForOpenConnectionApiWithProxy() { + try { + mFakeCronetEngine.openConnection(null, Proxy.NO_PROXY); + fail("openConnection API should not be available."); + } catch (Exception e) { + assertEquals("The openConnection API is not supported by the Fake implementation of " + + "CronetEngine.", + e.getMessage()); + } + } + + @Test + @SmallTest + public void testExceptionForCreateStreamHandlerFactoryApi() { + try { + mFakeCronetEngine.createURLStreamHandlerFactory(); + fail("createURLStreamHandlerFactory API should not be available."); + } catch (UnsupportedOperationException e) { + assertEquals( + "The URLStreamHandlerFactory API is not supported by the Fake implementation of" + + " CronetEngine.", + e.getMessage()); + } + } + + @Test + @SmallTest + public void testGetVersionString() { + assertEquals("FakeCronet/" + ImplVersion.getCronetVersionWithLastChange(), + mFakeCronetEngine.getVersionString()); + } + + @Test + @SmallTest + public void testStartNetLogToFile() { + mFakeCronetEngine.startNetLogToFile("", false); + } + + @Test + @SmallTest + public void testStartNetLogToDisk() { + mFakeCronetEngine.startNetLogToDisk("", false, 0); + } + + @Test + @SmallTest + public void testStopNetLog() { + mFakeCronetEngine.stopNetLog(); + } + + @Test + @SmallTest + public void testGetGlobalMetricsDeltas() { + assertTrue(mFakeCronetEngine.getGlobalMetricsDeltas().length == 0); + } + + @Test + @SmallTest + public void testGetEffectiveConnectionType() { + assertEquals(FakeCronetEngine.EFFECTIVE_CONNECTION_TYPE_UNKNOWN, + mFakeCronetEngine.getEffectiveConnectionType()); + } + + @Test + @SmallTest + public void testGetHttpRttMs() { + assertEquals(FakeCronetEngine.CONNECTION_METRIC_UNKNOWN, mFakeCronetEngine.getHttpRttMs()); + } + + @Test + @SmallTest + public void testGetTransportRttMs() { + assertEquals( + FakeCronetEngine.CONNECTION_METRIC_UNKNOWN, mFakeCronetEngine.getTransportRttMs()); + } + + @Test + @SmallTest + public void testGetDownstreamThroughputKbps() { + assertEquals(FakeCronetEngine.CONNECTION_METRIC_UNKNOWN, + mFakeCronetEngine.getDownstreamThroughputKbps()); + } + + @Test + @SmallTest + public void testConfigureNetworkQualityEstimatorForTesting() { + mFakeCronetEngine.configureNetworkQualityEstimatorForTesting(false, false, false); + } + + @Test + @SmallTest + public void testAddRttListener() { + mFakeCronetEngine.addRttListener(null); + } + + @Test + @SmallTest + public void testRemoveRttListener() { + mFakeCronetEngine.removeRttListener(null); + } + + @Test + @SmallTest + public void testAddThroughputListener() { + mFakeCronetEngine.addThroughputListener(null); + } + + @Test + @SmallTest + public void testRemoveThroughputListener() { + mFakeCronetEngine.removeThroughputListener(null); + } + + @Test + @SmallTest + public void testAddRequestFinishedListener() { + mFakeCronetEngine.addRequestFinishedListener(null); + } + + @Test + @SmallTest + public void testRemoveRequestFinishedListener() { + mFakeCronetEngine.removeRequestFinishedListener(null); + } + + @Test + @SmallTest + public void testShutdownBlockedWhenRequestCountNotZero() { + // Start a request and verify the engine can't be shutdown. + assertTrue(mFakeCronetEngine.startRequest()); + try { + mFakeCronetEngine.shutdown(); + fail("Shutdown not checked for active requests."); + } catch (IllegalStateException e) { + assertEquals("Cannot shutdown with active requests.", e.getMessage()); + } + + // Finish the request and verify the engine can be shutdown. + mFakeCronetEngine.onRequestDestroyed(); + mFakeCronetEngine.shutdown(); + } + + @Test + @SmallTest + public void testCantStartRequestAfterEngineShutdown() { + mFakeCronetEngine.shutdown(); + assertFalse(mFakeCronetEngine.startRequest()); + } + + @Test + @SmallTest + public void testCantDecrementOnceShutdown() { + mFakeCronetEngine.shutdown(); + + try { + mFakeCronetEngine.onRequestDestroyed(); + fail("onRequestDestroyed not checked for shutdown engine"); + } catch (IllegalStateException e) { + assertEquals("This instance of CronetEngine was shutdown. All requests must have been " + + "complete.", + e.getMessage()); + } + } +} diff --git a/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeCronetProviderTest.java b/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeCronetProviderTest.java new file mode 100644 index 0000000000..63c764a131 --- /dev/null +++ b/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeCronetProviderTest.java @@ -0,0 +1,67 @@ +// Copyright 2019 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.net.impl.ImplVersion; + +/** + * Test functionality of {@link FakeCronetProvider}. + */ +@RunWith(AndroidJUnit4.class) +public class FakeCronetProviderTest { + Context mContext; + FakeCronetProvider mProvider; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getTargetContext(); + mProvider = new FakeCronetProvider(mContext); + } + + @Test + @SmallTest + public void testGetName() { + String expectedName = "Fake-Cronet-Provider"; + assertEquals(expectedName, mProvider.getName()); + } + + @Test + @SmallTest + public void testGetVersion() { + assertEquals(ImplVersion.getCronetVersion(), mProvider.getVersion()); + } + + @Test + @SmallTest + public void testIsEnabled() { + assertTrue(mProvider.isEnabled()); + } + + @Test + @SmallTest + public void testHashCode() { + FakeCronetProvider otherProvider = new FakeCronetProvider(mContext); + assertEquals(otherProvider.hashCode(), mProvider.hashCode()); + } + + @Test + @SmallTest + public void testEquals() { + assertTrue(mProvider.equals(new FakeCronetProvider(mContext))); + } +} diff --git a/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeUrlRequestTest.java b/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeUrlRequestTest.java new file mode 100644 index 0000000000..0c6e587052 --- /dev/null +++ b/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeUrlRequestTest.java @@ -0,0 +1,1628 @@ +// Copyright 2019 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; + +import static org.chromium.net.CronetTestRule.assertContains; +import static org.chromium.net.TestUrlRequestCallback.ResponseStep.ON_CANCELED; + +import android.content.Context; +import android.os.ConditionVariable; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import org.chromium.base.test.util.DisabledTest; +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetException; +import org.chromium.net.InlineExecutionProhibitedException; +import org.chromium.net.TestUploadDataProvider; +import org.chromium.net.TestUrlRequestCallback; +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UploadDataProviders; +import org.chromium.net.UploadDataSink; +import org.chromium.net.UrlRequest; +import org.chromium.net.UrlRequest.Status; +import org.chromium.net.UrlRequest.StatusListener; +import org.chromium.net.UrlResponseInfo; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Test functionality of FakeUrlRequest. + */ +@RunWith(AndroidJUnit4.class) +public class FakeUrlRequestTest { + private CronetEngine mFakeCronetEngine; + private FakeCronetController mFakeCronetController; + + private static Context getContext() { + return InstrumentationRegistry.getTargetContext(); + } + + private static void checkStatus(FakeUrlRequest request, final int expectedStatus) { + ConditionVariable foundStatus = new ConditionVariable(); + request.getStatus(new StatusListener() { + @Override + public void onStatus(int status) { + assertEquals(expectedStatus, status); + foundStatus.open(); + } + }); + foundStatus.block(); + } + + private class EchoBodyResponseMatcher implements ResponseMatcher { + private final String mUrl; + + EchoBodyResponseMatcher(String url) { + mUrl = url; + } + + EchoBodyResponseMatcher() { + this(null); + } + + @Override + public FakeUrlResponse getMatchingResponse(String url, String httpMethod, + List> headers, byte[] body) { + if (mUrl == null || mUrl.equals(url)) { + return new FakeUrlResponse.Builder().setResponseBody(body).build(); + } + return null; + } + } + + @Before + public void setUp() { + mFakeCronetController = new FakeCronetController(); + mFakeCronetEngine = mFakeCronetController.newFakeCronetEngineBuilder(getContext()).build(); + } + + @After + public void tearDown() { + mFakeCronetEngine.shutdown(); + } + + @Test + @SmallTest + public void testDefaultResponse() { + // Setup the basic response. + String responseText = "response text"; + FakeUrlResponse response = + new FakeUrlResponse.Builder().setResponseBody(responseText.getBytes()).build(); + String url = "www.response.com"; + mFakeCronetController.addResponseForUrl(response, url); + + // Run the request. + TestUrlRequestCallback callback = Mockito.spy(new TestUrlRequestCallback()); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(url, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + // Verify correct callback methods called and correct response returned. + Mockito.verify(callback, times(1)).onResponseStarted(any(), any()); + Mockito.verify(callback, times(1)).onReadCompleted(any(), any(), any()); + Mockito.verify(callback, times(1)).onSucceeded(any(), any()); + assertEquals(callback.mResponseAsString, responseText); + } + + @Test + @SmallTest + public void testBuilderChecks() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + try { + mFakeCronetEngine.newUrlRequestBuilder(null, callback, callback.getExecutor()); + fail("URL not null-checked"); + } catch (NullPointerException e) { + assertEquals("URL is required.", e.getMessage()); + } + try { + mFakeCronetEngine.newUrlRequestBuilder("url", null, callback.getExecutor()); + fail("Callback not null-checked"); + } catch (NullPointerException e) { + assertEquals("Callback is required.", e.getMessage()); + } + try { + mFakeCronetEngine.newUrlRequestBuilder("url", callback, null); + fail("Executor not null-checked"); + } catch (NullPointerException e) { + assertEquals("Executor is required.", e.getMessage()); + } + // Verify successful creation doesn't throw. + mFakeCronetEngine.newUrlRequestBuilder("url", callback, callback.getExecutor()); + } + + @Test + @SmallTest + public void testSetHttpMethodWhenNullFails() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("url", callback, callback.getExecutor()) + .build(); + // Check exception thrown for null method. + try { + request.setHttpMethod(null); + fail("Method not null-checked"); + } catch (NullPointerException e) { + assertEquals("Method is required.", e.getMessage()); + } + } + + @Test + @SmallTest + public void testSetHttpMethodWhenInvalidFails() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("url", callback, callback.getExecutor()) + .build(); + + // Check exception thrown for invalid method. + String method = "BADMETHOD"; + try { + request.setHttpMethod(method); + fail("Method not checked for validity"); + } catch (IllegalArgumentException e) { + assertEquals("Invalid http method: " + method, e.getMessage()); + } + } + + @Test + @SmallTest + public void testSetHttpMethodSetsMethodToCorrectMethod() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("url", callback, callback.getExecutor()) + .build(); + String testMethod = "PUT"; + // Use an atomic because it is set in an inner class. We do not actually need atomic for a + // multi-threaded operation here. + AtomicBoolean foundMethod = new AtomicBoolean(); + + mFakeCronetController.addResponseMatcher(new ResponseMatcher() { + @Override + public FakeUrlResponse getMatchingResponse(String url, String httpMethod, + List> headers, byte[] body) { + assertEquals(testMethod, httpMethod); + foundMethod.set(true); + // It doesn't matter if a response is actually returned. + return null; + } + }); + + // Check no exception for correct method. + request.setHttpMethod(testMethod); + + // Run the request so that the ResponseMatcher we set is checked. + request.start(); + callback.blockForDone(); + + assertTrue(foundMethod.get()); + } + + @Test + @SmallTest + public void testAddHeader() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("TEST_URL", callback, callback.getExecutor()) + .build(); + String headerKey = "HEADERNAME"; + String headerValue = "HEADERVALUE"; + request.addHeader(headerKey, headerValue); + // Use an atomic because it is set in an inner class. We do not actually need atomic for a + // multi-threaded operation here. + AtomicBoolean foundEntry = new AtomicBoolean(); + mFakeCronetController.addResponseMatcher(new ResponseMatcher() { + @Override + public FakeUrlResponse getMatchingResponse(String url, String httpMethod, + List> headers, byte[] body) { + assertEquals(1, headers.size()); + assertEquals(headerKey, headers.get(0).getKey()); + assertEquals(headerValue, headers.get(0).getValue()); + foundEntry.set(true); + // It doesn't matter if a response is actually returned. + return null; + } + }); + // Run the request so that the ResponseMatcher we set is checked. + request.start(); + callback.blockForDone(); + + assertTrue(foundEntry.get()); + } + + @Test + @SmallTest + public void testRequestDoesNotStartWhenEngineShutDown() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("TEST_URL", callback, callback.getExecutor()) + .build(); + + mFakeCronetEngine.shutdown(); + try { + request.start(); + fail("Request should check that the CronetEngine is not shutdown before starting."); + } catch (IllegalStateException e) { + assertEquals("This request's CronetEngine is already shutdown.", e.getMessage()); + } + } + + @Test + @SmallTest + public void testRequestStopsWhenCanceled() { + TestUrlRequestCallback callback = Mockito.spy(new TestUrlRequestCallback()); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("TEST_URL", callback, callback.getExecutor()) + .build(); + callback.setAutoAdvance(false); + request.start(); + callback.waitForNextStep(); + request.cancel(); + + callback.blockForDone(); + + Mockito.verify(callback, times(1)).onCanceled(any(), any()); + Mockito.verify(callback, times(1)).onResponseStarted(any(), any()); + Mockito.verify(callback, times(0)).onReadCompleted(any(), any(), any()); + assertEquals(callback.mResponseStep, ON_CANCELED); + } + + @Test + @SmallTest + public void testRecievedByteCountInUrlResponseInfoIsEqualToResponseLength() { + // Setup the basic response. + String responseText = "response text"; + FakeUrlResponse response = + new FakeUrlResponse.Builder().setResponseBody(responseText.getBytes()).build(); + String url = "TEST_URL"; + mFakeCronetController.addResponseForUrl(response, url); + + // Run the request. + TestUrlRequestCallback callback = Mockito.spy(new TestUrlRequestCallback()); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(url, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + assertEquals(responseText.length(), callback.mResponseInfo.getReceivedByteCount()); + } + + @Test + @SmallTest + public void testRedirectResponse() { + // Setup the basic response. + String responseText = "response text"; + String redirectLocation = "/redirect_location"; + FakeUrlResponse response = new FakeUrlResponse.Builder() + .setResponseBody(responseText.getBytes()) + .addHeader("location", redirectLocation) + .setHttpStatusCode(300) + .build(); + + String url = "TEST_URL"; + mFakeCronetController.addResponseForUrl(response, url); + + String redirectText = "redirect text"; + FakeUrlResponse redirectToResponse = + new FakeUrlResponse.Builder().setResponseBody(redirectText.getBytes()).build(); + String redirectUrl = URI.create(url).resolve(redirectLocation).toString(); + + mFakeCronetController.addResponseForUrl(redirectToResponse, redirectUrl); + + // Run the request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(url, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + // Verify response from redirected URL is returned. + assertTrue(Objects.equals(callback.mResponseAsString, redirectText)); + } + + @Test + @SmallTest + public void testRedirectResponseWithNoHeaderFails() { + // Setup the basic response. + String responseText = "response text"; + FakeUrlResponse response = new FakeUrlResponse.Builder() + .setResponseBody(responseText.getBytes()) + .setHttpStatusCode(300) + .build(); + + String url = "TEST_URL"; + mFakeCronetController.addResponseForUrl(response, url); + + // Run the request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(url, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + // Verify response from redirected URL is returned. + assertEquals(TestUrlRequestCallback.ResponseStep.ON_FAILED, callback.mResponseStep); + } + + @Test + @SmallTest + public void testResponseLongerThanBuffer() { + // Build a long response string that is 3x the buffer size. + final int bufferStringLengthMultiplier = 3; + TestUrlRequestCallback callback = Mockito.spy(new TestUrlRequestCallback()); + String longResponseString = + new String(new char[callback.mReadBufferSize * bufferStringLengthMultiplier]); + + String longResponseUrl = "https://www.longResponseUrl.com"; + + FakeUrlResponse reallyLongResponse = new FakeUrlResponse.Builder() + .setResponseBody(longResponseString.getBytes()) + .build(); + mFakeCronetController.addResponseForUrl(reallyLongResponse, longResponseUrl); + + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(longResponseUrl, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + Mockito.verify(callback, times(1)).onResponseStarted(any(), any()); + Mockito.verify(callback, times(bufferStringLengthMultiplier)) + .onReadCompleted(any(), any(), any()); + Mockito.verify(callback, times(1)).onSucceeded(any(), any()); + assertTrue(Objects.equals(callback.mResponseAsString, longResponseString)); + } + + @Test + @SmallTest + public void testStatusInvalidBeforeStart() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("URL", callback, callback.getExecutor()) + .build(); + + checkStatus(request, Status.INVALID); + request.start(); + callback.blockForDone(); + } + + @Test + @SmallTest + public void testStatusIdleWhenWaitingForRead() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("URL", callback, callback.getExecutor()) + .build(); + request.start(); + checkStatus(request, Status.IDLE); + callback.setAutoAdvance(true); + callback.startNextRead(request); + callback.blockForDone(); + } + + @DisabledTest(message = "crbug.com/994722") + @Test + @SmallTest + public void testStatusIdleWhenWaitingForRedirect() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + String initialURL = "initialURL"; + String secondURL = "secondURL"; + mFakeCronetController.addRedirectResponse(secondURL, initialURL); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(initialURL, callback, callback.getExecutor()) + .build(); + + request.start(); + checkStatus(request, Status.IDLE); + callback.setAutoAdvance(true); + request.followRedirect(); + callback.blockForDone(); + } + + @Test + @SmallTest + public void testStatusInvalidWhenDone() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("URL", callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + checkStatus(request, Status.INVALID); + } + + @Test + @SmallTest + public void testIsDoneWhenComplete() { + TestUrlRequestCallback callback = Mockito.spy(new TestUrlRequestCallback()); + FakeUrlRequest request = (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("", callback, callback.getExecutor()) + .build(); + + request.start(); + callback.blockForDone(); + + Mockito.verify(callback, times(1)).onSucceeded(any(), any()); + assertTrue(request.isDone()); + } + + @Test + @SmallTest + public void testSetUploadDataProviderAfterStart() { + TestUrlRequestCallback callback = Mockito.spy(new TestUrlRequestCallback()); + FakeUrlRequest request = (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("", callback, callback.getExecutor()) + .addHeader("Content-Type", "useless/string") + .build(); + String body = "body"; + request.setUploadDataProvider( + UploadDataProviders.create(body.getBytes()), callback.getExecutor()); + request.start(); + // Must wait for the request to prevent a race in the State since it is reported in the + // error. + callback.blockForDone(); + + try { + request.setUploadDataProvider( + UploadDataProviders.create(body.getBytes()), callback.getExecutor()); + fail("UploadDataProvider cannot be changed after request has started"); + } catch (IllegalStateException e) { + assertEquals("Request is already started. State is: 7", e.getMessage()); + } + } + + @Test + @SmallTest + public void testUrlChainIsCorrectForSuccessRequest() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String testUrl = "TEST_URL"; + List expectedUrlChain = new ArrayList<>(); + expectedUrlChain.add(testUrl); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(testUrl, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + assertEquals(expectedUrlChain, callback.mResponseInfo.getUrlChain()); + } + + @Test + @SmallTest + public void testUrlChainIsCorrectForRedirectRequest() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String testUrl1 = "TEST_URL1"; + String testUrl2 = "TEST_URL2"; + mFakeCronetController.addRedirectResponse(testUrl2, testUrl1); + List expectedUrlChain = new ArrayList<>(); + expectedUrlChain.add(testUrl1); + expectedUrlChain.add(testUrl2); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(testUrl1, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + assertEquals(expectedUrlChain, callback.mResponseInfo.getUrlChain()); + } + + @Test + @SmallTest + public void testResponseCodeCorrect() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String testUrl = "TEST_URL"; + int expectedResponseCode = 208; + mFakeCronetController.addResponseForUrl( + new FakeUrlResponse.Builder().setHttpStatusCode(expectedResponseCode).build(), + testUrl); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(testUrl, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + assertEquals(expectedResponseCode, callback.mResponseInfo.getHttpStatusCode()); + } + + @Test + @SmallTest + public void testResponseTextCorrect() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String testUrl = "TEST_URL"; + int expectedResponseCode = 208; + String expectedResponseText = "Already Reported"; + mFakeCronetController.addResponseForUrl( + new FakeUrlResponse.Builder().setHttpStatusCode(expectedResponseCode).build(), + testUrl); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(testUrl, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + assertEquals(expectedResponseText, callback.mResponseInfo.getHttpStatusText()); + } + + @Test + @SmallTest + public void testResponseWasCachedCorrect() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String testUrl = "TEST_URL"; + boolean expectedWasCached = true; + mFakeCronetController.addResponseForUrl( + new FakeUrlResponse.Builder().setWasCached(expectedWasCached).build(), testUrl); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(testUrl, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + assertEquals(expectedWasCached, callback.mResponseInfo.wasCached()); + } + + @Test + @SmallTest + public void testResponseNegotiatedProtocolCorrect() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String testUrl = "TEST_URL"; + String expectedNegotiatedProtocol = "TEST_NEGOTIATED_PROTOCOL"; + mFakeCronetController.addResponseForUrl( + new FakeUrlResponse.Builder() + .setNegotiatedProtocol(expectedNegotiatedProtocol) + .build(), + testUrl); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(testUrl, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + assertEquals(expectedNegotiatedProtocol, callback.mResponseInfo.getNegotiatedProtocol()); + } + + @Test + @SmallTest + public void testResponseProxyServerCorrect() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String testUrl = "TEST_URL"; + String expectedProxyServer = "TEST_PROXY_SERVER"; + mFakeCronetController.addResponseForUrl( + new FakeUrlResponse.Builder().setProxyServer(expectedProxyServer).build(), testUrl); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(testUrl, callback, callback.getExecutor()) + .build(); + request.start(); + callback.blockForDone(); + + assertEquals(expectedProxyServer, callback.mResponseInfo.getProxyServer()); + } + + @Test + @SmallTest + public void testDirectExecutorDisabledByDefault() { + TestUrlRequestCallback callback = Mockito.spy(new TestUrlRequestCallback()); + callback.setAllowDirectExecutor(true); + Executor myExecutor = new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine.newUrlRequestBuilder("url", callback, myExecutor) + .build(); + + request.start(); + Mockito.verify(callback).onFailed(any(), any(), any()); + // Checks that the exception from {@link DirectPreventingExecutor} was successfully returned + // to the callabck in the onFailed method. + assertTrue(callback.mError.getCause() instanceof InlineExecutionProhibitedException); + } + + @Test + @SmallTest + public void testLotsOfCallsToReadDoesntOverflow() { + TestUrlRequestCallback callback = Mockito.spy(new TestUrlRequestCallback()); + // Make the buffer size small so there are lots of calls to read(). + callback.mReadBufferSize = 1; + String testUrl = "TEST_URL"; + int responseLength = 1024; + byte[] byteArray = new byte[responseLength]; + Arrays.fill(byteArray, (byte) 1); + String longResponseString = new String(byteArray); + mFakeCronetController.addResponseForUrl( + new FakeUrlResponse.Builder() + .setResponseBody(longResponseString.getBytes()) + .build(), + testUrl); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(testUrl, callback, callback.getExecutor()) + .allowDirectExecutor() + .build(); + request.start(); + callback.blockForDone(); + assertEquals(longResponseString, callback.mResponseAsString); + Mockito.verify(callback, times(responseLength)).onReadCompleted(any(), any(), any()); + } + + @Test + @SmallTest + public void testLotsOfCallsToReadDoesntOverflowWithDirectExecutor() { + TestUrlRequestCallback callback = Mockito.spy(new TestUrlRequestCallback()); + callback.setAllowDirectExecutor(true); + // Make the buffer size small so there are lots of calls to read(). + callback.mReadBufferSize = 1; + String testUrl = "TEST_URL"; + int responseLength = 1024; + byte[] byteArray = new byte[responseLength]; + Arrays.fill(byteArray, (byte) 1); + String longResponseString = new String(byteArray); + Executor myExecutor = new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + mFakeCronetController.addResponseForUrl( + new FakeUrlResponse.Builder() + .setResponseBody(longResponseString.getBytes()) + .build(), + testUrl); + FakeUrlRequest request = (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(testUrl, callback, myExecutor) + .allowDirectExecutor() + .build(); + request.start(); + callback.blockForDone(); + assertEquals(longResponseString, callback.mResponseAsString); + Mockito.verify(callback, times(responseLength)).onReadCompleted(any(), any(), any()); + } + + @Test + @SmallTest + public void testDoubleReadFails() throws Exception { + UrlRequest.Callback callback = new StubCallback(); + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder("url", callback, Executors.newSingleThreadExecutor()) + .build(); + ByteBuffer buffer = ByteBuffer.allocateDirect(32 * 1024); + request.start(); + + request.read(buffer); + try { + request.read(buffer); + fail("Double read() should be disallowed."); + } catch (IllegalStateException e) { + assertEquals("Invalid state transition - expected 4 but was 7", e.getMessage()); + } + } + + @DisabledTest(message = "crbug.com/994722") + @Test + @SmallTest + public void testReadWhileRedirectingFails() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + String url = "url"; + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(url, callback, callback.getExecutor()) + .build(); + mFakeCronetController.addRedirectResponse("location", url); + request.start(); + try { + callback.startNextRead(request); + fail("Read should be disallowed while waiting for redirect."); + } catch (IllegalStateException e) { + assertEquals("Invalid state transition - expected 4 but was 3", e.getMessage()); + } + callback.setAutoAdvance(true); + request.followRedirect(); + callback.blockForDone(); + } + + @DisabledTest(message = "crbug.com/994722") + @Test + @SmallTest + public void testShuttingDownCronetEngineWithActiveRequestFails() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + String url = "url"; + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(url, callback, callback.getExecutor()) + .build(); + + request.start(); + + try { + mFakeCronetEngine.shutdown(); + fail("Shutdown not checked for active requests."); + } catch (IllegalStateException e) { + assertEquals("Cannot shutdown with active requests.", e.getMessage()); + } + callback.setAutoAdvance(true); + callback.startNextRead(request); + callback.blockForDone(); + mFakeCronetEngine.shutdown(); + } + + @Test + @SmallTest + public void testDefaultResponseIs404() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest request = + (FakeUrlRequest) mFakeCronetEngine + .newUrlRequestBuilder(url, callback, callback.getExecutor()) + .build(); + + request.start(); + callback.blockForDone(); + + assertEquals(404, callback.mResponseInfo.getHttpStatusCode()); + } + + @Test + @SmallTest + public void testUploadSetDataProviderChecksForNullUploadDataProvider() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + try { + builder.setUploadDataProvider(null, callback.getExecutor()); + fail("Exception not thrown"); + } catch (NullPointerException e) { + assertEquals("Invalid UploadDataProvider.", e.getMessage()); + } + } + + @Test + @SmallTest + public void testUploadSetDataProviderChecksForContentTypeHeader() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + try { + builder.build().start(); + fail("Exception not thrown"); + } catch (IllegalArgumentException e) { + assertEquals("Requests with upload data must have a Content-Type.", e.getMessage()); + } + } + + @Test + @SmallTest + public void testUploadWithEmptyBody() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + + assertNotNull(callback.mResponseInfo); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("", callback.mResponseAsString); + dataProvider.assertClosed(); + } + + @Test + @SmallTest + public void testUploadSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + String body = "test"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + dataProvider.addRead(body.getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(4, dataProvider.getUploadedLength()); + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("test", callback.mResponseAsString); + } + + @Test + @SmallTest + public void testUploadSyncReadWrongState() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + String body = "test"; + callback.setAutoAdvance(false); + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + + // Add a redirect response so the request keeps the UploadDataProvider open while waiting + // to follow the redirect. + mFakeCronetController.addRedirectResponse("newUrl", url); + dataProvider.addRead(body.getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + FakeUrlRequest request = (FakeUrlRequest) builder.build(); + request.start(); + callback.waitForNextStep(); + try { + request.mFakeDataSink.onReadSucceeded(false); + fail("Cannot read before upload has started"); + } catch (IllegalStateException e) { + assertEquals("onReadSucceeded() called when not awaiting a read result; in state: 2", + e.getMessage()); + } + request.cancel(); + } + + @Test + @SmallTest + public void testUploadSyncRewindWrongState() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + String body = "test"; + callback.setAutoAdvance(false); + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + + // Add a redirect response so the request keeps the UploadDataProvider open while waiting + // to follow the redirect. + mFakeCronetController.addRedirectResponse("newUrl", url); + dataProvider.addRead(body.getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + FakeUrlRequest request = (FakeUrlRequest) builder.build(); + request.start(); + callback.waitForNextStep(); + try { + request.mFakeDataSink.onRewindSucceeded(); + fail("Cannot rewind before upload has started"); + } catch (IllegalStateException e) { + assertEquals("onRewindSucceeded() called when not awaiting a rewind; in state: 2", + e.getMessage()); + } + request.cancel(); + } + + @Test + @SmallTest + public void testUploadMultiplePiecesSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + dataProvider.addRead("Y".getBytes()); + dataProvider.addRead("et ".getBytes()); + dataProvider.addRead("another ".getBytes()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(16, dataProvider.getUploadedLength()); + assertEquals(4, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("Yet another test", callback.mResponseAsString); + } + + @Test + @SmallTest + public void testUploadMultiplePiecesAsync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.ASYNC, callback.getExecutor()); + dataProvider.addRead("Y".getBytes()); + dataProvider.addRead("et ".getBytes()); + dataProvider.addRead("another ".getBytes()); + dataProvider.addRead("test".getBytes()); + + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(16, dataProvider.getUploadedLength()); + assertEquals(4, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("Yet another test", callback.mResponseAsString); + } + + @Test + @SmallTest + public void testUploadChangesDefaultMethod() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new ResponseMatcher() { + @Override + public FakeUrlResponse getMatchingResponse(String url, String httpMethod, + List> headers, byte[] body) { + return new FakeUrlResponse.Builder().setResponseBody(httpMethod.getBytes()).build(); + } + }); + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("POST", callback.mResponseAsString); + } + + @Test + @SmallTest + public void testUploadWithSetMethod() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new ResponseMatcher() { + @Override + public FakeUrlResponse getMatchingResponse(String url, String httpMethod, + List> headers, byte[] body) { + return new FakeUrlResponse.Builder().setResponseBody(httpMethod.getBytes()).build(); + } + }); + final String method = "PUT"; + builder.setHttpMethod(method); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("PUT", callback.mResponseAsString); + } + + @Test + @SmallTest + public void testUploadRedirectSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String redirectUrl = "redirectUrl"; + String echoBodyUrl = "echobody"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + redirectUrl, callback, callback.getExecutor()); + mFakeCronetController.addRedirectResponse(echoBodyUrl, redirectUrl); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher(echoBodyUrl)); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + // 1 read call before the rewind, 1 after. + assertEquals(2, dataProvider.getNumReadCalls()); + assertEquals(1, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("test", callback.mResponseAsString); + } + + @Test + @SmallTest + public void testUploadRedirectAsync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String redirectUrl = "redirectUrl"; + String echoBodyUrl = "echobody"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + redirectUrl, callback, callback.getExecutor()); + mFakeCronetController.addRedirectResponse(echoBodyUrl, redirectUrl); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher(echoBodyUrl)); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.ASYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + // 1 read call before the rewind, 1 after. + assertEquals(2, dataProvider.getNumReadCalls()); + assertEquals(1, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("test", callback.mResponseAsString); + } + + @Test + @SmallTest + public void testUploadWithBadLength() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()) { + @Override + public long getLength() throws IOException { + return 1; + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) + throws IOException { + byteBuffer.put("12".getBytes()); + uploadDataSink.onReadSucceeded(false); + } + }; + + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Read upload data length 2 exceeds expected length 1", + callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + public void testUploadWithBadLengthBufferAligned() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()) { + @Override + public long getLength() throws IOException { + return 8191; + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) + throws IOException { + byteBuffer.put("0123456789abcdef".getBytes()); + uploadDataSink.onReadSucceeded(false); + } + }; + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Read upload data length 8192 exceeds expected length 8191", + callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + public void testUploadLengthFailSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setLengthFailure(); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(0, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Sync length failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + public void testUploadReadFailSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setReadFailure( + /* readFailIndex= */ 0, TestUploadDataProvider.FailMode.CALLBACK_SYNC); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Sync read failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + public void testUploadReadFailAsync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setReadFailure( + /* readFailIndex= */ 0, TestUploadDataProvider.FailMode.CALLBACK_ASYNC); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Async read failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + public void testUploadReadFailThrown() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setReadFailure(/* readFailIndex= */ 0, TestUploadDataProvider.FailMode.THROWN); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Thrown read failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + /** This test uses a direct executor for upload, and non direct for callbacks */ + @Test + @SmallTest + public void testDirectExecutorUploadProhibitedByDefault() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + Executor myExecutor = new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, myExecutor); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, myExecutor); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + + assertEquals(0, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Inline execution is prohibited for this request", + callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + /** This test uses a direct executor for callbacks, and non direct for upload */ + @Test + @SmallTest + public void testDirectExecutorProhibitedByDefault() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + Executor myExecutor = new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, myExecutor); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertContains("Exception posting task to executor", callback.mError.getMessage()); + assertContains("Inline execution is prohibited for this request", + callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + dataProvider.assertClosed(); + } + + @Test + @SmallTest + public void testDirectExecutorAllowed() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAllowDirectExecutor(true); + Executor myExecutor = new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + UploadDataProvider dataProvider = UploadDataProviders.create("test".getBytes()); + builder.setUploadDataProvider(dataProvider, myExecutor); + builder.addHeader("Content-Type", "useless/string"); + builder.allowDirectExecutor(); + builder.build().start(); + callback.blockForDone(); + + if (callback.mOnErrorCalled) { + throw callback.mError; + } + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("test", callback.mResponseAsString); + } + + @Test + @SmallTest + public void testUploadRewindFailSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String redirectUrl = "redirectUrl"; + String echoBodyUrl = "echobody"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + redirectUrl, callback, callback.getExecutor()); + mFakeCronetController.addRedirectResponse(echoBodyUrl, redirectUrl); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher(echoBodyUrl)); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setRewindFailure(TestUploadDataProvider.FailMode.CALLBACK_SYNC); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(1, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Sync rewind failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + public void testUploadRewindFailAsync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String redirectUrl = "redirectUrl"; + String echoBodyUrl = "echobody"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + redirectUrl, callback, callback.getExecutor()); + mFakeCronetController.addRedirectResponse(echoBodyUrl, redirectUrl); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher(echoBodyUrl)); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.ASYNC, callback.getExecutor()); + dataProvider.setRewindFailure(TestUploadDataProvider.FailMode.CALLBACK_ASYNC); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(1, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Async rewind failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + public void testUploadRewindFailThrown() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String redirectUrl = "redirectUrl"; + String echoBodyUrl = "echobody"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + redirectUrl, callback, callback.getExecutor()); + mFakeCronetController.addRedirectResponse(echoBodyUrl, redirectUrl); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher(echoBodyUrl)); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setRewindFailure(TestUploadDataProvider.FailMode.THROWN); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(1, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Thrown rewind failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + public void testUploadChunked() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test hello".getBytes()); + dataProvider.setChunked(true); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + + assertEquals(-1, dataProvider.getUploadedLength()); + + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + // 1 read call for one data chunk. + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals("test hello", callback.mResponseAsString); + } + + @Test + @SmallTest + public void testUploadChunkedLastReadZeroLengthBody() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = "url"; + FakeUrlRequest.Builder builder = + (FakeUrlRequest.Builder) mFakeCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + mFakeCronetController.addResponseMatcher(new EchoBodyResponseMatcher()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + // Add 3 reads. The last read has a 0-length body. + dataProvider.addRead("hello there".getBytes()); + dataProvider.addRead("!".getBytes()); + dataProvider.addRead("".getBytes()); + dataProvider.setChunked(true); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + + assertEquals(-1, dataProvider.getUploadedLength()); + + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + // 2 read call for the first two data chunks, and 1 for final chunk. + assertEquals(3, dataProvider.getNumReadCalls()); + assertEquals("hello there!", callback.mResponseAsString); + } + + /** + * A Cronet callback that does nothing. + */ + + private static class StubCallback extends UrlRequest.Callback { + @Override + public void onRedirectReceived(org.chromium.net.UrlRequest urlRequest, + UrlResponseInfo urlResponseInfo, String s) {} + + @Override + public void onResponseStarted( + org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) {} + + @Override + public void onReadCompleted(org.chromium.net.UrlRequest urlRequest, + UrlResponseInfo urlResponseInfo, ByteBuffer byteBuffer) {} + + @Override + public void onSucceeded( + org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) {} + + @Override + public void onFailed(org.chromium.net.UrlRequest urlRequest, + UrlResponseInfo urlResponseInfo, CronetException e) {} + } +} diff --git a/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeUrlResponseTest.java b/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeUrlResponseTest.java new file mode 100644 index 0000000000..73d62726d9 --- /dev/null +++ b/src/components/cronet/android/fake/javatests/org/chromium/net/test/FakeUrlResponseTest.java @@ -0,0 +1,219 @@ +// Copyright 2019 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.net.UrlResponseInfo; +import org.chromium.net.impl.UrlResponseInfoImpl; + +import java.io.UnsupportedEncodingException; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Test functionality of FakeUrlResponse. + */ +@RunWith(AndroidJUnit4.class) +public class FakeUrlResponseTest { + private static final int TEST_HTTP_STATUS_CODE = 201; + private static final String TEST_HEADER_NAME = "name"; + private static final String TEST_HEADER_VALUE = "value"; + private static final boolean TEST_WAS_CACHED = true; + private static final String TEST_NEGOTIATED_PROTOCOL = "test_negotiated_protocol"; + private static final String TEST_PROXY_SERVER = "test_proxy_server"; + private static final String TEST_BODY = "test_body"; + + List> mTestHeaders; + AbstractMap.SimpleEntry mTestHeaderEntry; + FakeUrlResponse mTestResponse; + + @Before + public void setUp() { + mTestHeaders = new ArrayList<>(); + mTestHeaderEntry = new AbstractMap.SimpleEntry<>(TEST_HEADER_NAME, TEST_HEADER_VALUE); + mTestHeaders.add(mTestHeaderEntry); + mTestResponse = new FakeUrlResponse.Builder() + .setHttpStatusCode(TEST_HTTP_STATUS_CODE) + .setWasCached(TEST_WAS_CACHED) + .addHeader(TEST_HEADER_NAME, TEST_HEADER_VALUE) + .setNegotiatedProtocol(TEST_NEGOTIATED_PROTOCOL) + .setProxyServer(TEST_PROXY_SERVER) + .setResponseBody(TEST_BODY.getBytes()) + .build(); + } + + @Test + @SmallTest + public void testAddHeader() { + FakeUrlResponse response = new FakeUrlResponse.Builder() + .addHeader(TEST_HEADER_NAME, TEST_HEADER_VALUE) + .build(); + + List> responseHeadersList = response.getAllHeadersList(); + + // mTestHeaderEntry is header entry of TEST_HEADER_NAME, TEST_HEADER_VALUE. + assertTrue(responseHeadersList.contains(mTestHeaderEntry)); + } + + @Test + @SmallTest + public void testEquals() { + FakeUrlResponse responseEqualToTestResponse = mTestResponse.toBuilder().build(); + FakeUrlResponse responseNotEqualToTestResponse = + mTestResponse.toBuilder().setResponseBody("Not equal".getBytes()).build(); + + assertEquals(mTestResponse, mTestResponse); + assertEquals(mTestResponse, responseEqualToTestResponse); + assertNotEquals(mTestResponse, responseNotEqualToTestResponse); + } + + @Test + @SmallTest + public void testResponseBodyIsSame() { + try { + FakeUrlResponse responseWithBodySetAsBytes = + mTestResponse.toBuilder().setResponseBody(TEST_BODY.getBytes("UTF-8")).build(); + assertTrue(Arrays.equals( + mTestResponse.getResponseBody(), responseWithBodySetAsBytes.getResponseBody())); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException( + "Exception occurred while encoding response body: " + TEST_BODY); + } + } + + @Test + @SmallTest + public void testHeadersNotShared() { + FakeUrlResponse.Builder responseBuilder = new FakeUrlResponse.Builder(); + FakeUrlResponse response = responseBuilder.build(); + FakeUrlResponse responseWithHeader = + responseBuilder.addHeader(TEST_HEADER_NAME, TEST_HEADER_VALUE).build(); + List> responseHeadersList = response.getAllHeadersList(); + List> responseHeadersListWithHeader = + responseWithHeader.getAllHeadersList(); + + assertNotEquals(responseHeadersListWithHeader, responseHeadersList); + } + + @Test + @SmallTest + public void testSettingAllHeadersCopiesHeaderList() { + String nameNotInOriginalList = "nameNotInOriginalList"; + String valueNotInOriginalList = "valueNotInOriginalList"; + AbstractMap.SimpleEntry entryNotInOriginalList = + new AbstractMap.SimpleEntry<>(nameNotInOriginalList, valueNotInOriginalList); + + FakeUrlResponse testResponseWithHeader = + mTestResponse.toBuilder() + .addHeader(nameNotInOriginalList, valueNotInOriginalList) + .build(); + + assertFalse(mTestHeaders.contains(entryNotInOriginalList)); + assertTrue(testResponseWithHeader.getAllHeadersList().contains(entryNotInOriginalList)); + } + + @Test + @SmallTest + public void testHashCodeReturnsSameIntForEqualObjects() { + FakeUrlResponse responseEqualToTest = mTestResponse.toBuilder().build(); + + assertEquals(mTestResponse.hashCode(), mTestResponse.hashCode()); + assertEquals(mTestResponse.hashCode(), responseEqualToTest.hashCode()); + // Two non-equivalent values can map to the same hashCode. + } + + @Test + @SmallTest + public void testToString() { + String expectedString = "HTTP Status Code: " + TEST_HTTP_STATUS_CODE + + " Headers: " + mTestHeaders.toString() + " Was Cached: " + TEST_WAS_CACHED + + " Negotiated Protocol: " + TEST_NEGOTIATED_PROTOCOL + + " Proxy Server: " + TEST_PROXY_SERVER + " Response Body (UTF-8): " + TEST_BODY; + String responseToString = mTestResponse.toString(); + + assertEquals(expectedString, responseToString); + } + + @Test + @SmallTest + public void testGetResponseWithUrlResponseInfo() { + UrlResponseInfo info = new UrlResponseInfoImpl(new ArrayList<>(), TEST_HTTP_STATUS_CODE, "", + mTestHeaders, TEST_WAS_CACHED, TEST_NEGOTIATED_PROTOCOL, TEST_PROXY_SERVER, 0); + FakeUrlResponse expectedResponse = new FakeUrlResponse.Builder() + .setHttpStatusCode(TEST_HTTP_STATUS_CODE) + .addHeader(TEST_HEADER_NAME, TEST_HEADER_VALUE) + .setWasCached(TEST_WAS_CACHED) + .setNegotiatedProtocol(TEST_NEGOTIATED_PROTOCOL) + .setProxyServer(TEST_PROXY_SERVER) + .build(); + + FakeUrlResponse constructedResponse = new FakeUrlResponse(info); + + assertEquals(expectedResponse, constructedResponse); + } + + @Test + @SmallTest + public void testGetResponesWithNullUrlResponseInfoGetsDefault() { + // Set params that cannot be null in UrlResponseInfo in the expected response so that the + // parameters found in the constructed response from UrlResponseInfo are the same + // as the expected. + FakeUrlResponse expectedResponse = new FakeUrlResponse.Builder() + .setHttpStatusCode(TEST_HTTP_STATUS_CODE) + .setWasCached(TEST_WAS_CACHED) + .addHeader(TEST_HEADER_NAME, TEST_HEADER_VALUE) + .build(); + // UnmodifiableList cannot be null. + UrlResponseInfo info = new UrlResponseInfoImpl(/* UrlChain */ new ArrayList<>(), + TEST_HTTP_STATUS_CODE, null, mTestHeaders, TEST_WAS_CACHED, null, null, 0); + + FakeUrlResponse constructedResponse = new FakeUrlResponse(info); + + assertEquals(expectedResponse, constructedResponse); + } + + @Test + @SmallTest + public void testInternalInitialHeadersListCantBeModified() { + FakeUrlResponse defaultResponseWithHeader = + new FakeUrlResponse.Builder() + .addHeader(TEST_HEADER_NAME, TEST_HEADER_VALUE) + .build(); + FakeUrlResponse defaultResponse = new FakeUrlResponse.Builder().build(); + + assertNotEquals( + defaultResponse.getAllHeadersList(), defaultResponseWithHeader.getAllHeadersList()); + } + + @Test + @SmallTest + public void testUrlResponseInfoHeadersMapIsCaseInsensitve() { + UrlResponseInfo info = new UrlResponseInfoImpl(new ArrayList<>(), 200, "OK", + mTestResponse.getAllHeadersList(), mTestResponse.getWasCached(), + mTestResponse.getNegotiatedProtocol(), mTestResponse.getProxyServer(), + mTestResponse.getResponseBody().length); + + Map infoMap = info.getAllHeaders(); + + assertTrue(infoMap.containsKey(TEST_HEADER_NAME.toLowerCase(Locale.ROOT))); + assertTrue(infoMap.containsKey(TEST_HEADER_NAME.toUpperCase(Locale.ROOT))); + } +} diff --git a/src/components/cronet/android/fake/javatests/org/chromium/net/test/UrlResponseMatcherTest.java b/src/components/cronet/android/fake/javatests/org/chromium/net/test/UrlResponseMatcherTest.java new file mode 100644 index 0000000000..cf52d72b2a --- /dev/null +++ b/src/components/cronet/android/fake/javatests/org/chromium/net/test/UrlResponseMatcherTest.java @@ -0,0 +1,75 @@ +// Copyright 2019 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test functionality of UrlResponseMatcher. + */ +@RunWith(AndroidJUnit4.class) +public class UrlResponseMatcherTest { + @Test + @SmallTest + public void testCheckUrlNotNull() { + try { + UrlResponseMatcher matcher = + new UrlResponseMatcher(null, new FakeUrlResponse.Builder().build()); + fail("URL not null-checked"); + } catch (NullPointerException e) { + assertEquals("URL is required.", e.getMessage()); + } + } + + @Test + @SmallTest + public void testCheckResponseNotNull() { + try { + UrlResponseMatcher matcher = new UrlResponseMatcher("url", null); + fail("Response not null-checked"); + } catch (NullPointerException e) { + assertEquals("Response is required.", e.getMessage()); + } + } + + @Test + @SmallTest + public void testGetMatchingUrlResponse() { + String url = "url"; + FakeUrlResponse response = + new FakeUrlResponse.Builder().setResponseBody("TestBody".getBytes()).build(); + ResponseMatcher matcher = new UrlResponseMatcher(url, response); + + FakeUrlResponse found = matcher.getMatchingResponse(url, null, null, null); + + assertNotNull(found); + assertEquals(found, response); + } + + @Test + @SmallTest + public void testGetResponseWithBadUrlReturnsNull() { + String url = "url"; + String urlWithoutResponse = "NO_RESPONSE"; + FakeUrlResponse response = + new FakeUrlResponse.Builder().setResponseBody("TestBody".getBytes()).build(); + ResponseMatcher matcher = new UrlResponseMatcher(url, response); + + FakeUrlResponse notFound = + matcher.getMatchingResponse(urlWithoutResponse, null, null, null); + + assertNull(notFound); + } +} diff --git a/src/components/cronet/android/io_buffer_with_byte_buffer.cc b/src/components/cronet/android/io_buffer_with_byte_buffer.cc new file mode 100644 index 0000000000..1d484ae20a --- /dev/null +++ b/src/components/cronet/android/io_buffer_with_byte_buffer.cc @@ -0,0 +1,41 @@ +// Copyright 2016 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 "components/cronet/android/io_buffer_with_byte_buffer.h" + +#include "base/check_op.h" + +namespace cronet { + +IOBufferWithByteBuffer::IOBufferWithByteBuffer( + JNIEnv* env, + const base::android::JavaParamRef& jbyte_buffer, + void* byte_buffer_data, + jint position, + jint limit) + : net::WrappedIOBuffer(static_cast(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); +} + +IOBufferWithByteBuffer::~IOBufferWithByteBuffer() {} + +ByteBufferWithIOBuffer::ByteBufferWithIOBuffer( + JNIEnv* env, + scoped_refptr io_buffer, + int io_buffer_len) + : io_buffer_(std::move(io_buffer)), io_buffer_len_(io_buffer_len) { + // An intermediate ScopedJavaLocalRef is needed here to release the local + // reference created by env->NewDirectByteBuffer(). + base::android::ScopedJavaLocalRef java_buffer( + env, env->NewDirectByteBuffer(io_buffer_->data(), io_buffer_len_)); + byte_buffer_.Reset(env, java_buffer.obj()); +} + +ByteBufferWithIOBuffer::~ByteBufferWithIOBuffer() {} + +} // namespace cronet diff --git a/src/components/cronet/android/io_buffer_with_byte_buffer.h b/src/components/cronet/android/io_buffer_with_byte_buffer.h new file mode 100644 index 0000000000..8a4f65be81 --- /dev/null +++ b/src/components/cronet/android/io_buffer_with_byte_buffer.h @@ -0,0 +1,80 @@ +// Copyright 2016 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_IO_BUFFER_WITH_BYTE_BUFFER_H_ +#define COMPONENTS_CRONET_ANDROID_IO_BUFFER_WITH_BYTE_BUFFER_H_ + +#include + +#include "base/android/scoped_java_ref.h" +#include "net/base/io_buffer.h" + +namespace cronet { + +// 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 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 base::android::JavaParamRef& jbyte_buffer, + void* byte_buffer_data, + jint position, + jint limit); + + IOBufferWithByteBuffer(const IOBufferWithByteBuffer&) = delete; + IOBufferWithByteBuffer& operator=(const IOBufferWithByteBuffer&) = delete; + + jint initial_position() const { return initial_position_; } + jint initial_limit() const { return initial_limit_; } + + const base::android::JavaRef& byte_buffer() const { + return byte_buffer_; + } + + private: + ~IOBufferWithByteBuffer() override; + + base::android::ScopedJavaGlobalRef byte_buffer_; + + const jint initial_position_; + const jint initial_limit_; +}; + +// Represents a Java direct ByteBuffer backed by a net::IOBuffer. Keeps both the +// net::IOBuffer and the Java ByteBuffer object alive until destroyed. +class ByteBufferWithIOBuffer { + public: + ByteBufferWithIOBuffer(JNIEnv* env, + scoped_refptr io_buffer, + int io_buffer_len); + + ByteBufferWithIOBuffer(const ByteBufferWithIOBuffer&) = delete; + ByteBufferWithIOBuffer& operator=(const ByteBufferWithIOBuffer&) = delete; + + ~ByteBufferWithIOBuffer(); + const net::IOBuffer* io_buffer() const { return io_buffer_.get(); } + int io_buffer_len() const { return io_buffer_len_; } + + const base::android::JavaRef& byte_buffer() const { + return byte_buffer_; + } + + private: + scoped_refptr io_buffer_; + int io_buffer_len_; + + base::android::ScopedJavaGlobalRef byte_buffer_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_IO_BUFFER_WITH_BYTE_BUFFER_H_ diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/BidirectionalStreamBuilderImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/BidirectionalStreamBuilderImpl.java new file mode 100644 index 0000000000..8497458be5 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/BidirectionalStreamBuilderImpl.java @@ -0,0 +1,156 @@ +// Copyright 2016 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.impl; + +import android.annotation.SuppressLint; + +import org.chromium.net.BidirectionalStream; +import org.chromium.net.CronetEngine; +import org.chromium.net.ExperimentalBidirectionalStream; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * Implementation of {@link ExperimentalBidirectionalStream.Builder}. + */ +public class BidirectionalStreamBuilderImpl extends ExperimentalBidirectionalStream.Builder { + // All fields are temporary storage of ExperimentalBidirectionalStream configuration to be + // copied to CronetBidirectionalStream. + + // CronetEngine to create the stream. + private final CronetEngineBase mCronetEngine; + // URL to request. + private final String mUrl; + // Callback to receive progress callbacks. + private final BidirectionalStream.Callback mCallback; + // Executor on which callbacks will be invoked. + private final Executor mExecutor; + // List of request headers, stored as header field name and value pairs. + private final ArrayList> mRequestHeaders = new ArrayList<>(); + + // HTTP method for the request. Default to POST. + private String mHttpMethod = "POST"; + // Priority of the stream. Default is medium. + @CronetEngineBase.StreamPriority + private int mPriority = STREAM_PRIORITY_MEDIUM; + + private boolean mDelayRequestHeadersUntilFirstFlush; + + // Request reporting annotations. + private Collection mRequestAnnotations; + + private boolean mTrafficStatsTagSet; + private int mTrafficStatsTag; + private boolean mTrafficStatsUidSet; + private int mTrafficStatsUid; + + /** + * Creates a builder for {@link BidirectionalStream} objects. All callbacks for + * generated {@code BidirectionalStream} objects will be invoked on + * {@code executor}. {@code executor} must not run tasks on the + * current thread, otherwise the networking operations may block and exceptions + * may be thrown at shutdown time. + * + * @param url the URL for the generated stream + * @param callback the {@link BidirectionalStream.Callback} object that gets invoked upon + * different events + * occuring + * @param executor the {@link Executor} on which {@code callback} methods will be invoked + * @param cronetEngine the {@link CronetEngine} used to create the stream + */ + BidirectionalStreamBuilderImpl(String url, BidirectionalStream.Callback callback, + Executor executor, CronetEngineBase cronetEngine) { + super(); + if (url == null) { + throw new NullPointerException("URL is required."); + } + if (callback == null) { + throw new NullPointerException("Callback is required."); + } + if (executor == null) { + throw new NullPointerException("Executor is required."); + } + if (cronetEngine == null) { + throw new NullPointerException("CronetEngine is required."); + } + mUrl = url; + mCallback = callback; + mExecutor = executor; + mCronetEngine = cronetEngine; + } + + @Override + public BidirectionalStreamBuilderImpl setHttpMethod(String method) { + if (method == null) { + throw new NullPointerException("Method is required."); + } + mHttpMethod = method; + return this; + } + + @Override + public BidirectionalStreamBuilderImpl addHeader(String header, String value) { + if (header == null) { + throw new NullPointerException("Invalid header name."); + } + if (value == null) { + throw new NullPointerException("Invalid header value."); + } + mRequestHeaders.add(new AbstractMap.SimpleImmutableEntry<>(header, value)); + return this; + } + + @Override + public BidirectionalStreamBuilderImpl setPriority( + @CronetEngineBase.StreamPriority int priority) { + mPriority = priority; + return this; + } + + @Override + public BidirectionalStreamBuilderImpl delayRequestHeadersUntilFirstFlush( + boolean delayRequestHeadersUntilFirstFlush) { + mDelayRequestHeadersUntilFirstFlush = delayRequestHeadersUntilFirstFlush; + return this; + } + + @Override + public ExperimentalBidirectionalStream.Builder addRequestAnnotation(Object annotation) { + if (annotation == null) { + throw new NullPointerException("Invalid metrics annotation."); + } + if (mRequestAnnotations == null) { + mRequestAnnotations = new ArrayList(); + } + mRequestAnnotations.add(annotation); + return this; + } + + @Override + public ExperimentalBidirectionalStream.Builder setTrafficStatsTag(int tag) { + mTrafficStatsTagSet = true; + mTrafficStatsTag = tag; + return this; + } + + @Override + public ExperimentalBidirectionalStream.Builder setTrafficStatsUid(int uid) { + mTrafficStatsUidSet = true; + mTrafficStatsUid = uid; + return this; + } + + @Override + @SuppressLint("WrongConstant") // TODO(jbudorick): Remove this after rolling to the N SDK. + public ExperimentalBidirectionalStream build() { + return mCronetEngine.createBidirectionalStream(mUrl, mCallback, mExecutor, mHttpMethod, + mRequestHeaders, mPriority, mDelayRequestHeadersUntilFirstFlush, + mRequestAnnotations, mTrafficStatsTagSet, mTrafficStatsTag, mTrafficStatsUidSet, + mTrafficStatsUid); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/BidirectionalStreamNetworkException.java b/src/components/cronet/android/java/src/org/chromium/net/impl/BidirectionalStreamNetworkException.java new file mode 100644 index 0000000000..dc3108649f --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/BidirectionalStreamNetworkException.java @@ -0,0 +1,32 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net.impl; + +import androidx.annotation.VisibleForTesting; + +import org.chromium.net.NetError; + +/** + * Used in {@link CronetBidirectionalStream}. Implements {@link NetworkExceptionImpl}. + */ +@VisibleForTesting +public class BidirectionalStreamNetworkException extends NetworkExceptionImpl { + public BidirectionalStreamNetworkException( + String message, int errorCode, int cronetInternalErrorCode) { + super(message, errorCode, cronetInternalErrorCode); + } + + @Override + public boolean immediatelyRetryable() { + switch (mCronetInternalErrorCode) { + case NetError.ERR_HTTP2_PING_FAILED: + case NetError.ERR_QUIC_HANDSHAKE_FAILED: + assert mErrorCode == ERROR_OTHER; + return true; + default: + return super.immediatelyRetryable(); + } + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/CallbackExceptionImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/CallbackExceptionImpl.java new file mode 100644 index 0000000000..94009eb57e --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/CallbackExceptionImpl.java @@ -0,0 +1,16 @@ +// Copyright 2016 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.impl; + +import org.chromium.net.CallbackException; + +/** + * An implementation of {@link CallbackException}. + */ +public class CallbackExceptionImpl extends CallbackException { + public CallbackExceptionImpl(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/CronetBidirectionalStream.java b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetBidirectionalStream.java new file mode 100644 index 0000000000..5a9889e6fc --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetBidirectionalStream.java @@ -0,0 +1,850 @@ +// 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.impl; + +import androidx.annotation.IntDef; +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.Log; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeClassQualifiedName; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.net.BidirectionalStream; +import org.chromium.net.CallbackException; +import org.chromium.net.CronetException; +import org.chromium.net.ExperimentalBidirectionalStream; +import org.chromium.net.NetworkException; +import org.chromium.net.RequestFinishedInfo; +import org.chromium.net.RequestPriority; +import org.chromium.net.UrlResponseInfo; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +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") +@VisibleForTesting +public class CronetBidirectionalStream extends ExperimentalBidirectionalStream { + /** + * 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. + */ + @IntDef({State.NOT_STARTED, State.STARTED, State.WAITING_FOR_READ, State.READING, + State.READING_DONE, State.CANCELED, State.ERROR, State.SUCCESS, State.WAITING_FOR_FLUSH, + State.WRITING, State.WRITING_DONE}) + @Retention(RetentionPolicy.SOURCE) + private @interface State { + /* Initial state, stream not started. */ + int NOT_STARTED = 0; + /* + * Stream started, request headers are being sent if mDelayRequestHeadersUntilNextFlush + * is not set to true. + */ + int STARTED = 1; + /* Waiting for {@code read()} to be called. */ + int WAITING_FOR_READ = 2; + /* Reading from the remote, {@code onReadCompleted()} callback will be called when done. */ + int READING = 3; + /* There is no more data to read and stream is half-closed by the remote side. */ + int READING_DONE = 4; + /* Stream is canceled. */ + int CANCELED = 5; + /* Error has occurred, stream is closed. */ + int ERROR = 6; + /* Reading and writing are done, and the stream is closed successfully. */ + int SUCCESS = 7; + /* Waiting for {@code CronetBidirectionalStreamJni.get().sendRequestHeaders()} or {@code + CronetBidirectionalStreamJni.get().writevData()} to be called. */ + int WAITING_FOR_FLUSH = 8; + /* Writing to the remote, {@code onWritevCompleted()} callback will be called when done. */ + int WRITING = 9; + /* There is no more data to write and stream is half-closed by the local side. */ + int WRITING_DONE = 10; + } + + private final CronetUrlRequestContext mRequestContext; + private final Executor mExecutor; + private final VersionSafeCallbacks.BidirectionalStreamCallback mCallback; + private final String mInitialUrl; + private final int mInitialPriority; + private final String mInitialMethod; + private final String mRequestHeaders[]; + private final boolean mDelayRequestHeadersUntilFirstFlush; + private final Collection mRequestAnnotations; + private final boolean mTrafficStatsTagSet; + private final int mTrafficStatsTag; + private final boolean mTrafficStatsUidSet; + private final int mTrafficStatsUid; + private CronetException mException; + + /* + * Synchronizes access to mNativeStream, mReadState and mWriteState. + */ + private final Object mNativeStreamLock = new Object(); + + @GuardedBy("mNativeStreamLock") + // Pending write data. + private LinkedList mPendingData; + + @GuardedBy("mNativeStreamLock") + // Flush data queue that should be pushed to the native stack when the previous + // CronetBidirectionalStreamJni.get().writevData completes. + private LinkedList mFlushData; + + @GuardedBy("mNativeStreamLock") + // Whether an end-of-stream flag is passed in through write(). + private boolean mEndOfStreamWritten; + + @GuardedBy("mNativeStreamLock") + // Whether request headers have been sent. + private boolean mRequestHeadersSent; + + @GuardedBy("mNativeStreamLock") + // Metrics information. Obtained when request succeeds, fails or is canceled. + private RequestFinishedInfo.Metrics mMetrics; + + /* 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 int mReadState = State.NOT_STARTED; + + /** + * Write state is tracking writing flow. + * / <--- WRITING <--- \ + * | | + * \ / + * NOT_STARTED -> STARTED --> WAITING_FOR_FLUSH -> WRITING_DONE -> SUCCESS + */ + @GuardedBy("mNativeStreamLock") + private @State int mWriteState = State.NOT_STARTED; + + // Only modified on the network thread. + private UrlResponseInfoImpl mResponseInfo; + + /* + * OnReadCompleted callback is repeatedly invoked when each read is completed, so it + * is cached as a member variable. + */ + // Only modified on the network thread. + private OnReadCompletedRunnable mOnReadCompletedTask; + + 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; + boolean maybeOnSucceeded = false; + synchronized (mNativeStreamLock) { + if (isDoneLocked()) { + return; + } + if (mEndOfStream) { + mReadState = State.READING_DONE; + maybeOnSucceeded = (mWriteState == State.WRITING_DONE); + } else { + mReadState = State.WAITING_FOR_READ; + } + } + mCallback.onReadCompleted( + CronetBidirectionalStream.this, mResponseInfo, buffer, mEndOfStream); + if (maybeOnSucceeded) { + maybeOnSucceededOnExecutor(); + } + } catch (Exception e) { + onCallbackException(e); + } + } + } + + private final class OnWriteCompletedRunnable implements Runnable { + // Buffer passed back from current invocation of onWriteCompleted. + private ByteBuffer mByteBuffer; + // End of stream flag from current call to write. + private final boolean mEndOfStream; + + OnWriteCompletedRunnable(ByteBuffer buffer, boolean endOfStream) { + mByteBuffer = buffer; + mEndOfStream = endOfStream; + } + + @Override + public void run() { + try { + // Null out mByteBuffer, to pass buffer ownership to callback or release if done. + ByteBuffer buffer = mByteBuffer; + mByteBuffer = null; + boolean maybeOnSucceeded = false; + synchronized (mNativeStreamLock) { + if (isDoneLocked()) { + return; + } + if (mEndOfStream) { + mWriteState = State.WRITING_DONE; + maybeOnSucceeded = (mReadState == State.READING_DONE); + } + } + mCallback.onWriteCompleted( + CronetBidirectionalStream.this, mResponseInfo, buffer, mEndOfStream); + if (maybeOnSucceeded) { + maybeOnSucceededOnExecutor(); + } + } catch (Exception e) { + onCallbackException(e); + } + } + } + + CronetBidirectionalStream(CronetUrlRequestContext requestContext, String url, + @CronetEngineBase.StreamPriority int priority, Callback callback, Executor executor, + String httpMethod, List> requestHeaders, + boolean delayRequestHeadersUntilNextFlush, Collection requestAnnotations, + boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, + int trafficStatsUid) { + mRequestContext = requestContext; + mInitialUrl = url; + mInitialPriority = convertStreamPriority(priority); + mCallback = new VersionSafeCallbacks.BidirectionalStreamCallback(callback); + mExecutor = executor; + mInitialMethod = httpMethod; + mRequestHeaders = stringsFromHeaderList(requestHeaders); + mDelayRequestHeadersUntilFirstFlush = delayRequestHeadersUntilNextFlush; + mPendingData = new LinkedList<>(); + mFlushData = new LinkedList<>(); + mRequestAnnotations = requestAnnotations; + mTrafficStatsTagSet = trafficStatsTagSet; + mTrafficStatsTag = trafficStatsTag; + mTrafficStatsUidSet = trafficStatsUidSet; + mTrafficStatsUid = trafficStatsUid; + } + + @Override + public void start() { + synchronized (mNativeStreamLock) { + if (mReadState != State.NOT_STARTED) { + throw new IllegalStateException("Stream is already started."); + } + try { + mNativeStream = CronetBidirectionalStreamJni.get().createBidirectionalStream( + CronetBidirectionalStream.this, + mRequestContext.getUrlRequestContextAdapter(), + !mDelayRequestHeadersUntilFirstFlush, + mRequestContext.hasRequestFinishedListener(), mTrafficStatsTagSet, + mTrafficStatsTag, mTrafficStatsUidSet, mTrafficStatsUid); + mRequestContext.onRequestStarted(); + // Non-zero startResult means an argument error. + int startResult = CronetBidirectionalStreamJni.get().start(mNativeStream, + CronetBidirectionalStream.this, 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 (!CronetBidirectionalStreamJni.get().readData(mNativeStream, + CronetBidirectionalStream.this, 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 (mEndOfStreamWritten) { + throw new IllegalArgumentException("Write after writing end of stream."); + } + if (isDoneLocked()) { + return; + } + mPendingData.add(buffer); + if (endOfStream) { + mEndOfStreamWritten = true; + } + } + } + + @Override + public void flush() { + synchronized (mNativeStreamLock) { + if (isDoneLocked() + || (mWriteState != State.WAITING_FOR_FLUSH && mWriteState != State.WRITING)) { + return; + } + if (mPendingData.isEmpty() && mFlushData.isEmpty()) { + // If there is no pending write when flush() is called, see if + // request headers need to be flushed. + if (!mRequestHeadersSent) { + mRequestHeadersSent = true; + CronetBidirectionalStreamJni.get().sendRequestHeaders( + mNativeStream, CronetBidirectionalStream.this); + if (!doesMethodAllowWriteData(mInitialMethod)) { + mWriteState = State.WRITING_DONE; + } + } + return; + } + + assert !mPendingData.isEmpty() || !mFlushData.isEmpty(); + + // Move buffers from mPendingData to the flushing queue. + if (!mPendingData.isEmpty()) { + mFlushData.addAll(mPendingData); + mPendingData.clear(); + } + + if (mWriteState == State.WRITING) { + // If there is a write already pending, wait until onWritevCompleted is + // called before pushing data to the native stack. + return; + } + sendFlushDataLocked(); + } + } + + // Helper method to send buffers in mFlushData. Caller needs to acquire + // mNativeStreamLock and make sure mWriteState is WAITING_FOR_FLUSH and + // mFlushData queue isn't empty. + @SuppressWarnings("GuardedByChecker") + private void sendFlushDataLocked() { + assert mWriteState == State.WAITING_FOR_FLUSH; + int size = mFlushData.size(); + ByteBuffer[] buffers = new ByteBuffer[size]; + int[] positions = new int[size]; + int[] limits = new int[size]; + for (int i = 0; i < size; i++) { + ByteBuffer buffer = mFlushData.poll(); + buffers[i] = buffer; + positions[i] = buffer.position(); + limits[i] = buffer.limit(); + } + assert mFlushData.isEmpty(); + assert buffers.length >= 1; + mWriteState = State.WRITING; + mRequestHeadersSent = true; + if (!CronetBidirectionalStreamJni.get().writevData(mNativeStream, + CronetBidirectionalStream.this, buffers, positions, limits, + mEndOfStreamWritten && mPendingData.isEmpty())) { + // Still waiting on flush. This is just to have consistent + // behavior with the other error cases. + mWriteState = State.WAITING_FOR_FLUSH; + throw new IllegalArgumentException("Unable to call native writev."); + } + } + + /** + * Returns a read-only copy of {@code mPendingData} for testing. + */ + @VisibleForTesting + public List getPendingDataForTesting() { + synchronized (mNativeStreamLock) { + List pendingData = new LinkedList(); + for (ByteBuffer buffer : mPendingData) { + pendingData.add(buffer.asReadOnlyBuffer()); + } + return pendingData; + } + } + + /** + * Returns a read-only copy of {@code mFlushData} for testing. + */ + @VisibleForTesting + public List getFlushDataForTesting() { + synchronized (mNativeStreamLock) { + List flushData = new LinkedList(); + for (ByteBuffer buffer : mFlushData) { + flushData.add(buffer.asReadOnlyBuffer()); + } + return flushData; + } + } + + @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; + } + + /* + * Runs an onSucceeded callback if both Read and Write sides are closed. + */ + private void maybeOnSucceededOnExecutor() { + synchronized (mNativeStreamLock) { + if (isDoneLocked()) { + return; + } + if (!(mWriteState == State.WRITING_DONE && mReadState == State.READING_DONE)) { + return; + } + mReadState = mWriteState = State.SUCCESS; + // 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); + } + } + + @SuppressWarnings("unused") + @CalledByNative + private void onStreamReady(final boolean requestHeadersSent) { + postTaskToExecutor(new Runnable() { + @Override + public void run() { + synchronized (mNativeStreamLock) { + if (isDoneLocked()) { + return; + } + mRequestHeadersSent = requestHeadersSent; + mReadState = State.WAITING_FOR_READ; + if (!doesMethodAllowWriteData(mInitialMethod) && mRequestHeadersSent) { + mWriteState = State.WRITING_DONE; + } else { + mWriteState = State.WAITING_FOR_FLUSH; + } + } + + try { + mCallback.onStreamReady(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 receivedByteCount) { + try { + mResponseInfo = prepareResponseInfoOnNetworkThread( + httpStatusCode, negotiatedProtocol, headers, receivedByteCount); + } catch (Exception e) { + failWithException(new CronetExceptionImpl("Cannot prepare ResponseInfo", null)); + return; + } + postTaskToExecutor(new Runnable() { + @Override + 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 receivedByteCount) { + mResponseInfo.setReceivedByteCount(receivedByteCount); + if (byteBuffer.position() != initialPosition || byteBuffer.limit() != initialLimit) { + failWithException( + new CronetExceptionImpl("ByteBuffer modified externally during read", null)); + return; + } + if (bytesRead < 0 || initialPosition + bytesRead > initialLimit) { + failWithException(new CronetExceptionImpl("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 onWritevCompleted(final ByteBuffer[] byteBuffers, int[] initialPositions, + int[] initialLimits, boolean endOfStream) { + assert byteBuffers.length == initialPositions.length; + assert byteBuffers.length == initialLimits.length; + synchronized (mNativeStreamLock) { + if (isDoneLocked()) return; + mWriteState = State.WAITING_FOR_FLUSH; + // Flush if there is anything in the flush queue mFlushData. + if (!mFlushData.isEmpty()) { + sendFlushDataLocked(); + } + } + for (int i = 0; i < byteBuffers.length; i++) { + ByteBuffer buffer = byteBuffers[i]; + if (buffer.position() != initialPositions[i] || buffer.limit() != initialLimits[i]) { + failWithException(new CronetExceptionImpl( + "ByteBuffer modified externally during write", null)); + return; + } + // Current implementation always writes the complete buffer. + buffer.position(buffer.limit()); + postTaskToExecutor(new OnWriteCompletedRunnable(buffer, + // Only set endOfStream flag if this buffer is the last in byteBuffers. + endOfStream && i == byteBuffers.length - 1)); + } + } + + @SuppressWarnings("unused") + @CalledByNative + private void onResponseTrailersReceived(String[] trailers) { + final UrlResponseInfo.HeaderBlock trailersBlock = + new UrlResponseInfoImpl.HeaderBlockImpl(headersListFromStrings(trailers)); + postTaskToExecutor(new Runnable() { + @Override + 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(int errorCode, int nativeError, int nativeQuicError, String errorString, + long receivedByteCount) { + if (mResponseInfo != null) { + mResponseInfo.setReceivedByteCount(receivedByteCount); + } + if (errorCode == NetworkException.ERROR_QUIC_PROTOCOL_FAILED + || errorCode == NetworkException.ERROR_NETWORK_CHANGED) { + failWithException( + new QuicExceptionImpl("Exception in BidirectionalStream: " + errorString, + errorCode, nativeError, nativeQuicError)); + } else { + failWithException(new BidirectionalStreamNetworkException( + "Exception in BidirectionalStream: " + errorString, errorCode, nativeError)); + } + } + + /** + * Called when request is canceled, no callbacks will be called afterwards. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onCanceled() { + postTaskToExecutor(new Runnable() { + @Override + public void run() { + try { + mCallback.onCanceled(CronetBidirectionalStream.this, mResponseInfo); + } catch (Exception e) { + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in onCanceled method", e); + } + } + }); + } + + /** + * Called by the native code to report metrics just before the native adapter is destroyed. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onMetricsCollected(long requestStartMs, long dnsStartMs, long dnsEndMs, + long connectStartMs, long connectEndMs, long sslStartMs, long sslEndMs, + long sendingStartMs, long sendingEndMs, long pushStartMs, long pushEndMs, + long responseStartMs, long requestEndMs, boolean socketReused, long sentByteCount, + long receivedByteCount) { + synchronized (mNativeStreamLock) { + if (mMetrics != null) { + throw new IllegalStateException("Metrics collection should only happen once."); + } + mMetrics = new CronetMetrics(requestStartMs, dnsStartMs, dnsEndMs, connectStartMs, + connectEndMs, sslStartMs, sslEndMs, sendingStartMs, sendingEndMs, pushStartMs, + pushEndMs, responseStartMs, requestEndMs, socketReused, sentByteCount, + receivedByteCount); + assert mReadState == mWriteState; + assert (mReadState == State.SUCCESS) || (mReadState == State.ERROR) + || (mReadState == State.CANCELED); + int finishedReason; + if (mReadState == State.SUCCESS) { + finishedReason = RequestFinishedInfo.SUCCEEDED; + } else if (mReadState == State.CANCELED) { + finishedReason = RequestFinishedInfo.CANCELED; + } else { + finishedReason = RequestFinishedInfo.FAILED; + } + final RequestFinishedInfo requestFinishedInfo = new RequestFinishedInfoImpl(mInitialUrl, + mRequestAnnotations, mMetrics, finishedReason, mResponseInfo, mException); + mRequestContext.reportRequestFinished(requestFinishedInfo); + } + } + + @VisibleForTesting + public void setOnDestroyedCallbackForTesting(Runnable onDestroyedCallbackForTesting) { + mOnDestroyedCallbackForTesting = onDestroyedCallbackForTesting; + } + + private static boolean doesMethodAllowWriteData(String methodName) { + return !methodName.equals("GET") && !methodName.equals("HEAD"); + } + + private static ArrayList> headersListFromStrings(String[] headers) { + ArrayList> 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> headersList) { + String headersArray[] = new String[headersList.size() * 2]; + int i = 0; + for (Map.Entry requestHeader : headersList) { + headersArray[i++] = requestHeader.getKey(); + headersArray[i++] = requestHeader.getValue(); + } + return headersArray; + } + + private static int convertStreamPriority(@CronetEngineBase.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."); + } + } + + /** + * 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 UrlResponseInfoImpl prepareResponseInfoOnNetworkThread(int httpStatusCode, + String negotiatedProtocol, String[] headers, long receivedByteCount) { + UrlResponseInfoImpl responseInfo = new UrlResponseInfoImpl(Arrays.asList(mInitialUrl), + httpStatusCode, "", headersListFromStrings(headers), false, negotiatedProtocol, + null, receivedByteCount); + return responseInfo; + } + + @GuardedBy("mNativeStreamLock") + private void destroyNativeStreamLocked(boolean sendOnCanceled) { + Log.i(CronetUrlRequestContext.LOG_TAG, "destroyNativeStreamLocked " + this.toString()); + if (mNativeStream == 0) { + return; + } + CronetBidirectionalStreamJni.get().destroy( + mNativeStream, CronetBidirectionalStream.this, sendOnCanceled); + mRequestContext.onRequestDestroyed(); + mNativeStream = 0; + if (mOnDestroyedCallbackForTesting != null) { + mOnDestroyedCallbackForTesting.run(); + } + } + + /** + * Fails the stream with an exception. Only called on the Executor. + */ + private void failWithExceptionOnExecutor(CronetException e) { + mException = e; + // Do not call into mCallback 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) { + CallbackException streamError = + new CallbackExceptionImpl("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() { + @Override + public void run() { + failWithExceptionOnExecutor(exception); + } + }); + } + + @NativeMethods + interface Natives { + // Native methods are implemented in cronet_bidirectional_stream_adapter.cc. + long createBidirectionalStream(CronetBidirectionalStream caller, + long urlRequestContextAdapter, boolean sendRequestHeadersAutomatically, + boolean enableMetricsCollection, boolean trafficStatsTagSet, int trafficStatsTag, + boolean trafficStatsUidSet, int trafficStatsUid); + + @NativeClassQualifiedName("CronetBidirectionalStreamAdapter") + int start(long nativePtr, CronetBidirectionalStream caller, String url, int priority, + String method, String[] headers, boolean endOfStream); + + @NativeClassQualifiedName("CronetBidirectionalStreamAdapter") + void sendRequestHeaders(long nativePtr, CronetBidirectionalStream caller); + + @NativeClassQualifiedName("CronetBidirectionalStreamAdapter") + boolean readData(long nativePtr, CronetBidirectionalStream caller, ByteBuffer byteBuffer, + int position, int limit); + + @NativeClassQualifiedName("CronetBidirectionalStreamAdapter") + boolean writevData(long nativePtr, CronetBidirectionalStream caller, ByteBuffer[] buffers, + int[] positions, int[] limits, boolean endOfStream); + + @NativeClassQualifiedName("CronetBidirectionalStreamAdapter") + void destroy(long nativePtr, CronetBidirectionalStream caller, boolean sendOnCanceled); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/CronetEngineBase.java b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetEngineBase.java new file mode 100644 index 0000000000..83238bd4c6 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetEngineBase.java @@ -0,0 +1,130 @@ +// Copyright 2016 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.impl; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; + +import org.chromium.net.BidirectionalStream; +import org.chromium.net.ExperimentalBidirectionalStream; +import org.chromium.net.ExperimentalCronetEngine; +import org.chromium.net.ExperimentalUrlRequest; +import org.chromium.net.RequestFinishedInfo; +import org.chromium.net.UrlRequest; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.URL; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * Base class of {@link CronetUrlRequestContext} and {@link JavaCronetEngine} that contains + * shared logic. + */ +public abstract class CronetEngineBase extends ExperimentalCronetEngine { + /** + * Creates a {@link UrlRequest} object. All callbacks will + * be called on {@code executor}'s thread. {@code executor} must not run + * tasks on the current thread to prevent blocking networking operations + * and causing exceptions during shutdown. + * + * @param url {@link URL} for the request. + * @param callback callback object that gets invoked on different events. + * @param executor {@link Executor} on which all callbacks will be invoked. + * @param priority priority of the request which should be one of the + * {@link UrlRequest.Builder#REQUEST_PRIORITY_IDLE REQUEST_PRIORITY_*} + * values. + * @param requestAnnotations Objects to pass on to + * {@link org.chromium.net.RequestFinishedInfo.Listener}. + * @param disableCache disables cache for the request. + * If context is not set up to use cache this param has no effect. + * @param disableConnectionMigration disables connection migration for this + * request if it is enabled for the session. + * @param allowDirectExecutor whether executors used by this request are permitted + * to execute submitted tasks inline. + * @param trafficStatsTagSet {@code true} if {@code trafficStatsTag} represents a TrafficStats + * tag to apply to sockets used to perform this request. + * @param trafficStatsTag TrafficStats tag to apply to sockets used to perform this request. + * @param trafficStatsUidSet {@code true} if {@code trafficStatsUid} represents a UID to + * attribute traffic used to perform this request. + * @param trafficStatsUid UID to attribute traffic used to perform this request. + * @param requestFinishedListener callback to get invoked with metrics when request is finished. + * Set to {@code null} if not used. + * @param idempotency idempotency of the request which should be one of the + * {@link ExperimentalUrlRequest.Builder#DEFAULT_IDEMPOTENCY IDEMPOTENT NOT_IDEMPOTENT} + * values. + * @return new request. + */ + protected abstract UrlRequestBase createRequest(String url, UrlRequest.Callback callback, + Executor executor, @RequestPriority int priority, Collection requestAnnotations, + boolean disableCache, boolean disableConnectionMigration, boolean allowDirectExecutor, + boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, + int trafficStatsUid, @Nullable RequestFinishedInfo.Listener requestFinishedListener, + @Idempotency int idempotency); + + /** + * Creates a {@link BidirectionalStream} object. {@code callback} methods will + * be invoked on {@code executor}. {@code executor} must not run + * tasks on the current thread to prevent blocking networking operations + * and causing exceptions during shutdown. + * + * @param url the URL for the stream + * @param callback the object whose methods get invoked upon different events + * @param executor the {@link Executor} on which all callbacks will be called + * @param httpMethod the HTTP method to use for the stream + * @param requestHeaders the list of request headers + * @param priority priority of the stream which should be one of the + * {@link BidirectionalStream.Builder#STREAM_PRIORITY_IDLE STREAM_PRIORITY_*} + * values. + * @param delayRequestHeadersUntilFirstFlush whether to delay sending request + * headers until flush() is called, and try to combine them + * with the next data frame. + * @param requestAnnotations Objects to pass on to + * {@link org.chromium.net.RequestFinishedInfo.Listener}. + * @param trafficStatsTagSet {@code true} if {@code trafficStatsTag} represents a TrafficStats + * tag to apply to sockets used to perform this request. + * @param trafficStatsTag TrafficStats tag to apply to sockets used to perform this request. + * @param trafficStatsUidSet {@code true} if {@code trafficStatsUid} represents a UID to + * attribute traffic used to perform this request. + * @param trafficStatsUid UID to attribute traffic used to perform this request. + * @return a new stream. + */ + protected abstract ExperimentalBidirectionalStream createBidirectionalStream(String url, + BidirectionalStream.Callback callback, Executor executor, String httpMethod, + List> requestHeaders, @StreamPriority int priority, + boolean delayRequestHeadersUntilFirstFlush, Collection requestAnnotations, + boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, + int trafficStatsUid); + + @Override + public ExperimentalUrlRequest.Builder newUrlRequestBuilder( + String url, UrlRequest.Callback callback, Executor executor) { + return new UrlRequestBuilderImpl(url, callback, executor, this); + } + + @IntDef({UrlRequest.Builder.REQUEST_PRIORITY_IDLE, UrlRequest.Builder.REQUEST_PRIORITY_LOWEST, + UrlRequest.Builder.REQUEST_PRIORITY_LOW, UrlRequest.Builder.REQUEST_PRIORITY_MEDIUM, + UrlRequest.Builder.REQUEST_PRIORITY_HIGHEST}) + @Retention(RetentionPolicy.SOURCE) + public @interface RequestPriority {} + + @IntDef({ + BidirectionalStream.Builder.STREAM_PRIORITY_IDLE, + BidirectionalStream.Builder.STREAM_PRIORITY_LOWEST, + BidirectionalStream.Builder.STREAM_PRIORITY_LOW, + BidirectionalStream.Builder.STREAM_PRIORITY_MEDIUM, + BidirectionalStream.Builder.STREAM_PRIORITY_HIGHEST, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface StreamPriority {} + + @IntDef({ExperimentalUrlRequest.Builder.DEFAULT_IDEMPOTENCY, + ExperimentalUrlRequest.Builder.IDEMPOTENT, + ExperimentalUrlRequest.Builder.NOT_IDEMPOTENT}) + @Retention(RetentionPolicy.SOURCE) + public @interface Idempotency {} +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/CronetEngineBuilderImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetEngineBuilderImpl.java new file mode 100644 index 0000000000..586963dfa4 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetEngineBuilderImpl.java @@ -0,0 +1,407 @@ +// Copyright 2016 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.impl; + +import static android.os.Process.THREAD_PRIORITY_LOWEST; + +import android.content.Context; +import android.util.Base64; + +import androidx.annotation.IntDef; +import androidx.annotation.VisibleForTesting; + +import org.chromium.net.CronetEngine; +import org.chromium.net.ICronetEngineBuilder; + +import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.IDN; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Implementation of {@link ICronetEngineBuilder}. + */ +public abstract class CronetEngineBuilderImpl extends ICronetEngineBuilder { + /** + * A hint that a host supports QUIC. + */ + public static class QuicHint { + // The host. + final String mHost; + // Port of the server that supports QUIC. + final int mPort; + // Alternate protocol port. + final int mAlternatePort; + + QuicHint(String host, int port, int alternatePort) { + mHost = host; + mPort = port; + mAlternatePort = alternatePort; + } + } + + /** + * A public key pin. + */ + public static class Pkp { + // Host to pin for. + final String mHost; + // Array of SHA-256 hashes of keys. + final byte[][] mHashes; + // Should pin apply to subdomains? + final boolean mIncludeSubdomains; + // When the pin expires. + final Date mExpirationDate; + + Pkp(String host, byte[][] hashes, boolean includeSubdomains, Date expirationDate) { + mHost = host; + mHashes = hashes; + mIncludeSubdomains = includeSubdomains; + mExpirationDate = expirationDate; + } + } + + private static final Pattern INVALID_PKP_HOST_NAME = Pattern.compile("^[0-9\\.]*$"); + + private static final int INVALID_THREAD_PRIORITY = THREAD_PRIORITY_LOWEST + 1; + + // Private fields are simply storage of configuration for the resulting CronetEngine. + // See setters below for verbose descriptions. + private final Context mApplicationContext; + private final List mQuicHints = new LinkedList<>(); + private final List mPkps = new LinkedList<>(); + private boolean mPublicKeyPinningBypassForLocalTrustAnchorsEnabled; + private String mUserAgent; + private String mStoragePath; + private boolean mQuicEnabled; + private boolean mHttp2Enabled; + private boolean mBrotiEnabled; + private boolean mDisableCache; + private int mHttpCacheMode; + private long mHttpCacheMaxSize; + private String mExperimentalOptions; + protected long mMockCertVerifier; + private boolean mNetworkQualityEstimatorEnabled; + private int mThreadPriority = INVALID_THREAD_PRIORITY; + + /** + * Default config enables SPDY and QUIC, disables SDCH and HTTP cache. + * @param context Android {@link Context} for engine to use. + */ + public CronetEngineBuilderImpl(Context context) { + mApplicationContext = context.getApplicationContext(); + enableQuic(true); + enableHttp2(true); + enableBrotli(false); + enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISABLED, 0); + enableNetworkQualityEstimator(false); + enablePublicKeyPinningBypassForLocalTrustAnchors(true); + } + + @Override + public String getDefaultUserAgent() { + return UserAgent.from(mApplicationContext); + } + + @Override + public CronetEngineBuilderImpl setUserAgent(String userAgent) { + mUserAgent = userAgent; + return this; + } + + String getUserAgent() { + return mUserAgent; + } + + @Override + public CronetEngineBuilderImpl setStoragePath(String value) { + if (!new File(value).isDirectory()) { + throw new IllegalArgumentException("Storage path must be set to existing directory"); + } + mStoragePath = value; + return this; + } + + String storagePath() { + return mStoragePath; + } + + @Override + public CronetEngineBuilderImpl setLibraryLoader(CronetEngine.Builder.LibraryLoader loader) { + // |CronetEngineBuilderImpl| is an abstract class that is used by concrete builder + // implementations, including the Java Cronet engine builder; therefore, the implementation + // of this method should be "no-op". Subclasses that care about the library loader + // should override this method. + return this; + } + + /** + * Default implementation of the method that returns {@code null}. + * + * @return {@code null}. + */ + VersionSafeCallbacks.LibraryLoader libraryLoader() { + return null; + } + + @Override + public CronetEngineBuilderImpl enableQuic(boolean value) { + mQuicEnabled = value; + return this; + } + + boolean quicEnabled() { + return mQuicEnabled; + } + + /** + * Constructs default QUIC User Agent Id string including application name + * and Cronet version. Returns empty string if QUIC is not enabled. + * + * @return QUIC User Agent ID string. + */ + String getDefaultQuicUserAgentId() { + return mQuicEnabled ? UserAgent.getQuicUserAgentIdFrom(mApplicationContext) : ""; + } + + @Override + public CronetEngineBuilderImpl enableHttp2(boolean value) { + mHttp2Enabled = value; + return this; + } + + boolean http2Enabled() { + return mHttp2Enabled; + } + + @Override + public CronetEngineBuilderImpl enableSdch(boolean value) { + return this; + } + + @Override + public CronetEngineBuilderImpl enableBrotli(boolean value) { + mBrotiEnabled = value; + return this; + } + + boolean brotliEnabled() { + return mBrotiEnabled; + } + + @IntDef({CronetEngine.Builder.HTTP_CACHE_DISABLED, CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, + CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, CronetEngine.Builder.HTTP_CACHE_DISK}) + @Retention(RetentionPolicy.SOURCE) + public @interface HttpCacheSetting {} + + @Override + public CronetEngineBuilderImpl enableHttpCache(@HttpCacheSetting int cacheMode, long maxSize) { + if (cacheMode == CronetEngine.Builder.HTTP_CACHE_DISK + || cacheMode == CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP) { + if (storagePath() == null) { + throw new IllegalArgumentException("Storage path must be set"); + } + } else { + if (storagePath() != null) { + throw new IllegalArgumentException("Storage path must not be set"); + } + } + mDisableCache = (cacheMode == CronetEngine.Builder.HTTP_CACHE_DISABLED + || cacheMode == CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP); + mHttpCacheMaxSize = maxSize; + + switch (cacheMode) { + case CronetEngine.Builder.HTTP_CACHE_DISABLED: + mHttpCacheMode = HttpCacheType.DISABLED; + break; + case CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP: + case CronetEngine.Builder.HTTP_CACHE_DISK: + mHttpCacheMode = HttpCacheType.DISK; + break; + case CronetEngine.Builder.HTTP_CACHE_IN_MEMORY: + mHttpCacheMode = HttpCacheType.MEMORY; + break; + default: + throw new IllegalArgumentException("Unknown cache mode"); + } + return this; + } + + boolean cacheDisabled() { + return mDisableCache; + } + + long httpCacheMaxSize() { + return mHttpCacheMaxSize; + } + + int httpCacheMode() { + return mHttpCacheMode; + } + + @Override + public CronetEngineBuilderImpl addQuicHint(String host, int port, int alternatePort) { + if (host.contains("/")) { + throw new IllegalArgumentException("Illegal QUIC Hint Host: " + host); + } + mQuicHints.add(new QuicHint(host, port, alternatePort)); + return this; + } + + List quicHints() { + return mQuicHints; + } + + @Override + public CronetEngineBuilderImpl addPublicKeyPins(String hostName, Set pinsSha256, + boolean includeSubdomains, Date expirationDate) { + if (hostName == null) { + throw new NullPointerException("The hostname cannot be null"); + } + if (pinsSha256 == null) { + throw new NullPointerException("The set of SHA256 pins cannot be null"); + } + if (expirationDate == null) { + throw new NullPointerException("The pin expiration date cannot be null"); + } + String idnHostName = validateHostNameForPinningAndConvert(hostName); + // Convert the pin to BASE64 encoding to remove duplicates. + Map hashes = new HashMap<>(); + for (byte[] pinSha256 : pinsSha256) { + if (pinSha256 == null || pinSha256.length != 32) { + throw new IllegalArgumentException("Public key pin is invalid"); + } + hashes.put(Base64.encodeToString(pinSha256, 0), pinSha256); + } + // Add new element to PKP list. + mPkps.add(new Pkp(idnHostName, hashes.values().toArray(new byte[hashes.size()][]), + includeSubdomains, expirationDate)); + return this; + } + + /** + * Returns list of public key pins. + * @return list of public key pins. + */ + List publicKeyPins() { + return mPkps; + } + + @Override + public CronetEngineBuilderImpl enablePublicKeyPinningBypassForLocalTrustAnchors(boolean value) { + mPublicKeyPinningBypassForLocalTrustAnchorsEnabled = value; + return this; + } + + boolean publicKeyPinningBypassForLocalTrustAnchorsEnabled() { + return mPublicKeyPinningBypassForLocalTrustAnchorsEnabled; + } + + /** + * Checks whether a given string represents a valid host name for PKP and converts it + * to ASCII Compatible Encoding representation according to RFC 1122, RFC 1123 and + * RFC 3490. This method is more restrictive than required by RFC 7469. Thus, a host + * that contains digits and the dot character only is considered invalid. + * + * Note: Currently Cronet doesn't have native implementation of host name validation that + * can be used. There is code that parses a provided URL but doesn't ensure its + * correctness. The implementation relies on {@code getaddrinfo} function. + * + * @param hostName host name to check and convert. + * @return true if the string is a valid host name. + * @throws IllegalArgumentException if the the given string does not represent a valid + * hostname. + */ + private static String validateHostNameForPinningAndConvert(String hostName) + throws IllegalArgumentException { + if (INVALID_PKP_HOST_NAME.matcher(hostName).matches()) { + throw new IllegalArgumentException("Hostname " + hostName + " is illegal." + + " A hostname should not consist of digits and/or dots only."); + } + // Workaround for crash, see crbug.com/634914 + if (hostName.length() > 255) { + throw new IllegalArgumentException("Hostname " + hostName + " is too long." + + " The name of the host does not comply with RFC 1122 and RFC 1123."); + } + try { + return IDN.toASCII(hostName, IDN.USE_STD3_ASCII_RULES); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException("Hostname " + hostName + " is illegal." + + " The name of the host does not comply with RFC 1122 and RFC 1123."); + } + } + + @Override + public CronetEngineBuilderImpl setExperimentalOptions(String options) { + mExperimentalOptions = options; + return this; + } + + public String experimentalOptions() { + return mExperimentalOptions; + } + + /** + * Sets a native MockCertVerifier for testing. See + * {@code MockCertVerifier.createMockCertVerifier} for a method that + * can be used to create a MockCertVerifier. + * @param mockCertVerifier pointer to native MockCertVerifier. + * @return the builder to facilitate chaining. + */ + @VisibleForTesting + public CronetEngineBuilderImpl setMockCertVerifierForTesting(long mockCertVerifier) { + mMockCertVerifier = mockCertVerifier; + return this; + } + + long mockCertVerifier() { + return mMockCertVerifier; + } + + /** + * @return true if the network quality estimator has been enabled for + * this builder. + */ + boolean networkQualityEstimatorEnabled() { + return mNetworkQualityEstimatorEnabled; + } + + @Override + public CronetEngineBuilderImpl enableNetworkQualityEstimator(boolean value) { + mNetworkQualityEstimatorEnabled = value; + return this; + } + + @Override + public CronetEngineBuilderImpl setThreadPriority(int priority) { + if (priority > THREAD_PRIORITY_LOWEST || priority < -20) { + throw new IllegalArgumentException("Thread priority invalid"); + } + mThreadPriority = priority; + return this; + } + + /** + * @return thread priority provided by user, or {@code defaultThreadPriority} if none provided. + */ + int threadPriority(int defaultThreadPriority) { + return mThreadPriority == INVALID_THREAD_PRIORITY ? defaultThreadPriority : mThreadPriority; + } + + /** + * Returns {@link Context} for builder. + * + * @return {@link Context} for builder. + */ + Context getContext() { + return mApplicationContext; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/CronetExceptionImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetExceptionImpl.java new file mode 100644 index 0000000000..de47b147aa --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetExceptionImpl.java @@ -0,0 +1,16 @@ +// Copyright 2016 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.impl; + +import org.chromium.net.CronetException; + +/** + * Implements {@link CronetException}. + */ +public class CronetExceptionImpl extends CronetException { + public CronetExceptionImpl(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/CronetLibraryLoader.java b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetLibraryLoader.java new file mode 100644 index 0000000000..f90db5f597 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetLibraryLoader.java @@ -0,0 +1,192 @@ +// 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.impl; + +import android.content.Context; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Process; + +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.ContextUtils; +import org.chromium.base.Log; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.net.NetworkChangeNotifier; + +/** + * CronetLibraryLoader loads and initializes native library on init thread. + */ +@JNINamespace("cronet") +@VisibleForTesting +public class CronetLibraryLoader { + // Synchronize initialization. + private static final Object sLoadLock = new Object(); + private static final String LIBRARY_NAME = "cronet." + ImplVersion.getCronetVersion(); + private static final String TAG = CronetLibraryLoader.class.getSimpleName(); + // Thread used for initialization work and processing callbacks for + // long-lived global singletons. This thread lives forever as things like + // the global singleton NetworkChangeNotifier live on it and are never killed. + private static final HandlerThread sInitThread = new HandlerThread("CronetInit"); + // Has library loading commenced? Setting guarded by sLoadLock. + private static volatile boolean sLibraryLoaded = IntegratedModeState.INTEGRATED_MODE_ENABLED; + // Has ensureInitThreadInitialized() completed? + private static volatile boolean sInitThreadInitDone; + // Block calling native methods until this ConditionVariable opens to indicate loadLibrary() + // is completed and native methods have been registered. + private static final ConditionVariable sWaitForLibLoad = new ConditionVariable(); + + /** + * Ensure that native library is loaded and initialized. Can be called from + * any thread, the load and initialization is performed on init thread. + */ + public static void ensureInitialized( + Context applicationContext, final CronetEngineBuilderImpl builder) { + synchronized (sLoadLock) { + if (!sInitThreadInitDone) { + if (!IntegratedModeState.INTEGRATED_MODE_ENABLED) { + // In integrated mode, application context should be initialized by the host. + ContextUtils.initApplicationContext(applicationContext); + } + if (!sInitThread.isAlive()) { + sInitThread.start(); + } + postToInitThread(new Runnable() { + @Override + public void run() { + ensureInitializedOnInitThread(); + } + }); + } + if (!sLibraryLoaded) { + if (builder.libraryLoader() != null) { + builder.libraryLoader().loadLibrary(LIBRARY_NAME); + } else { + System.loadLibrary(LIBRARY_NAME); + } + String implVersion = ImplVersion.getCronetVersion(); + if (!implVersion.equals(CronetLibraryLoaderJni.get().getCronetVersion())) { + throw new RuntimeException(String.format("Expected Cronet version number %s, " + + "actual version number %s.", + implVersion, CronetLibraryLoaderJni.get().getCronetVersion())); + } + Log.i(TAG, "Cronet version: %s, arch: %s", implVersion, + System.getProperty("os.arch")); + sLibraryLoaded = true; + sWaitForLibLoad.open(); + } + } + } + + /** + * Returns {@code true} if running on the initialization thread. + */ + private static boolean onInitThread() { + return sInitThread.getLooper() == Looper.myLooper(); + } + + /** + * Ensure that the init thread initialization has completed. Can only be called from + * the init thread. Ensures that the NetworkChangeNotifier is initialzied and the + * init thread native MessageLoop is initialized. + */ + static void ensureInitializedOnInitThread() { + assert onInitThread(); + if (sInitThreadInitDone) { + return; + } + if (IntegratedModeState.INTEGRATED_MODE_ENABLED) { + assert NetworkChangeNotifier.isInitialized(); + } else { + NetworkChangeNotifier.init(); + // Registers to always receive network notifications. Note + // that this call is fine for Cronet because Cronet + // embedders do not have API access to create network change + // observers. Existing observers in the net stack do not + // perform expensive work. + NetworkChangeNotifier.registerToReceiveNotificationsAlways(); + // Wait for loadLibrary() to complete so JNI is registered. + sWaitForLibLoad.block(); + } + assert sLibraryLoaded; + // registerToReceiveNotificationsAlways() is called before the native + // NetworkChangeNotifierAndroid is created, so as to avoid receiving + // the undesired initial network change observer notification, which + // will cause active requests to fail with ERR_NETWORK_CHANGED. + CronetLibraryLoaderJni.get().cronetInitOnInitThread(); + sInitThreadInitDone = true; + } + + /** + * Run {@code r} on the initialization thread. + */ + public static void postToInitThread(Runnable r) { + if (onInitThread()) { + r.run(); + } else { + new Handler(sInitThread.getLooper()).post(r); + } + } + + /** + * Called from native library to get default user agent constructed + * using application context. May be called on any thread. + * + * Expects that ContextUtils.initApplicationContext() was called already + * either by some testing framework or an embedder constructing a Java + * CronetEngine via CronetEngine.Builder.build(). + */ + @CalledByNative + private static String getDefaultUserAgent() { + return UserAgent.from(ContextUtils.getApplicationContext()); + } + + /** + * Called from native library to ensure that library is initialized. + * May be called on any thread, but initialization is performed on + * this.sInitThread. + * + * Expects that ContextUtils.initApplicationContext() was called already + * either by some testing framework or an embedder constructing a Java + * CronetEngine via CronetEngine.Builder.build(). + * + * TODO(mef): In the long term this should be changed to some API with + * lower overhead like CronetEngine.Builder.loadNativeCronet(). + */ + @CalledByNative + private static void ensureInitializedFromNative() { + // Called by native, so native library is already loaded. + // It is possible that loaded native library is not regular + // "libcronet.xyz.so" but test library that statically links + // native code like "libcronet_unittests.so". + synchronized (sLoadLock) { + sLibraryLoaded = true; + sWaitForLibLoad.open(); + } + + // The application context must already be initialized + // using ContextUtils.initApplicationContext(). + Context applicationContext = ContextUtils.getApplicationContext(); + assert applicationContext != null; + ensureInitialized(applicationContext, null); + } + + @CalledByNative + private static void setNetworkThreadPriorityOnNetworkThread(int priority) { + Process.setThreadPriority(priority); + } + + @NativeMethods + interface Natives { + // Native methods are implemented in cronet_library_loader.cc. + void cronetInitOnInitThread(); + + String getCronetVersion(); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/CronetMetrics.java b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetMetrics.java new file mode 100644 index 0000000000..d654495266 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetMetrics.java @@ -0,0 +1,244 @@ +// Copyright 2016 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.impl; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import org.chromium.net.RequestFinishedInfo; + +import java.util.Date; + +/** + * Implementation of {@link RequestFinishedInfo.Metrics}. + */ +@VisibleForTesting +public final class CronetMetrics extends RequestFinishedInfo.Metrics { + private final long mRequestStartMs; + private final long mDnsStartMs; + private final long mDnsEndMs; + private final long mConnectStartMs; + private final long mConnectEndMs; + private final long mSslStartMs; + private final long mSslEndMs; + private final long mSendingStartMs; + private final long mSendingEndMs; + private final long mPushStartMs; + private final long mPushEndMs; + private final long mResponseStartMs; + private final long mRequestEndMs; + private final boolean mSocketReused; + + // TODO(mgersh): Delete after the switch to the new API http://crbug.com/629194 + @Nullable + private final Long mTtfbMs; + // TODO(mgersh): Delete after the switch to the new API http://crbug.com/629194 + @Nullable + private final Long mTotalTimeMs; + @Nullable + private final Long mSentByteCount; + @Nullable + private final Long mReceivedByteCount; + + @Nullable + private static Date toDate(long timestamp) { + if (timestamp != -1) { + return new Date(timestamp); + } + return null; + } + + private static boolean checkOrder(long start, long end) { + // If end doesn't exist, start can be anything, including also not existing + // If end exists, start must also exist and be before end + return (end >= start && start != -1) || end == -1; + } + + /** + * Old-style constructor + * TODO(mgersh): Delete after the switch to the new API http://crbug.com/629194 + */ + public CronetMetrics(@Nullable Long ttfbMs, @Nullable Long totalTimeMs, + @Nullable Long sentByteCount, @Nullable Long receivedByteCount) { + mTtfbMs = ttfbMs; + mTotalTimeMs = totalTimeMs; + mSentByteCount = sentByteCount; + mReceivedByteCount = receivedByteCount; + + // Everything else is -1 (translates to null) for now + mRequestStartMs = -1; + mDnsStartMs = -1; + mDnsEndMs = -1; + mConnectStartMs = -1; + mConnectEndMs = -1; + mSslStartMs = -1; + mSslEndMs = -1; + mSendingStartMs = -1; + mSendingEndMs = -1; + mPushStartMs = -1; + mPushEndMs = -1; + mResponseStartMs = -1; + mRequestEndMs = -1; + mSocketReused = false; + } + + /** + * New-style constructor + */ + public CronetMetrics(long requestStartMs, long dnsStartMs, long dnsEndMs, long connectStartMs, + long connectEndMs, long sslStartMs, long sslEndMs, long sendingStartMs, + long sendingEndMs, long pushStartMs, long pushEndMs, long responseStartMs, + long requestEndMs, boolean socketReused, long sentByteCount, long receivedByteCount) { + // Check that no end times are before corresponding start times, + // or exist when start time doesn't. + assert checkOrder(dnsStartMs, dnsEndMs); + assert checkOrder(connectStartMs, connectEndMs); + assert checkOrder(sslStartMs, sslEndMs); + assert checkOrder(sendingStartMs, sendingEndMs); + assert checkOrder(pushStartMs, pushEndMs); + // requestEnd always exists, so just check that it's after start + assert requestEndMs >= responseStartMs; + // Spot-check some of the other orderings + assert dnsStartMs >= requestStartMs || dnsStartMs == -1; + assert sendingStartMs >= requestStartMs || sendingStartMs == -1; + assert sslStartMs >= connectStartMs || sslStartMs == -1; + assert responseStartMs >= sendingStartMs || responseStartMs == -1; + mRequestStartMs = requestStartMs; + mDnsStartMs = dnsStartMs; + mDnsEndMs = dnsEndMs; + mConnectStartMs = connectStartMs; + mConnectEndMs = connectEndMs; + mSslStartMs = sslStartMs; + mSslEndMs = sslEndMs; + mSendingStartMs = sendingStartMs; + mSendingEndMs = sendingEndMs; + mPushStartMs = pushStartMs; + mPushEndMs = pushEndMs; + mResponseStartMs = responseStartMs; + mRequestEndMs = requestEndMs; + mSocketReused = socketReused; + mSentByteCount = sentByteCount; + mReceivedByteCount = receivedByteCount; + + // TODO(mgersh): delete these after embedders stop using them http://crbug.com/629194 + if (requestStartMs != -1 && responseStartMs != -1) { + mTtfbMs = responseStartMs - requestStartMs; + } else { + mTtfbMs = null; + } + if (requestStartMs != -1 && requestEndMs != -1) { + mTotalTimeMs = requestEndMs - requestStartMs; + } else { + mTotalTimeMs = null; + } + } + + @Nullable + @Override + public Date getRequestStart() { + return toDate(mRequestStartMs); + } + + @Nullable + @Override + public Date getDnsStart() { + return toDate(mDnsStartMs); + } + + @Nullable + @Override + public Date getDnsEnd() { + return toDate(mDnsEndMs); + } + + @Nullable + @Override + public Date getConnectStart() { + return toDate(mConnectStartMs); + } + + @Nullable + @Override + public Date getConnectEnd() { + return toDate(mConnectEndMs); + } + + @Nullable + @Override + public Date getSslStart() { + return toDate(mSslStartMs); + } + + @Nullable + @Override + public Date getSslEnd() { + return toDate(mSslEndMs); + } + + @Nullable + @Override + public Date getSendingStart() { + return toDate(mSendingStartMs); + } + + @Nullable + @Override + public Date getSendingEnd() { + return toDate(mSendingEndMs); + } + + @Nullable + @Override + public Date getPushStart() { + return toDate(mPushStartMs); + } + + @Nullable + @Override + public Date getPushEnd() { + return toDate(mPushEndMs); + } + + @Nullable + @Override + public Date getResponseStart() { + return toDate(mResponseStartMs); + } + + @Nullable + @Override + public Date getRequestEnd() { + return toDate(mRequestEndMs); + } + + @Override + public boolean getSocketReused() { + return mSocketReused; + } + + @Nullable + @Override + public Long getTtfbMs() { + return mTtfbMs; + } + + @Nullable + @Override + public Long getTotalTimeMs() { + return mTotalTimeMs; + } + + @Nullable + @Override + public Long getSentByteCount() { + return mSentByteCount; + } + + @Nullable + @Override + public Long getReceivedByteCount() { + return mReceivedByteCount; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/CronetUploadDataStream.java b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetUploadDataStream.java new file mode 100644 index 0000000000..e70119e420 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetUploadDataStream.java @@ -0,0 +1,419 @@ +// 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.impl; + +import android.annotation.SuppressLint; + +import androidx.annotation.IntDef; +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.Log; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeClassQualifiedName; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UploadDataSink; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.util.concurrent.Executor; + +import javax.annotation.concurrent.GuardedBy; + +/** + * CronetUploadDataStream handles communication between an upload body + * encapsulated in the embedder's {@link UploadDataSink} and a C++ + * UploadDataStreamAdapter, which it owns. It's attached to a {@link + * CronetUrlRequest}'s during the construction of request's native C++ objects + * on the network thread, though it's created on one of the embedder's threads. + * It is called by the UploadDataStreamAdapter on the network thread, but calls + * into the UploadDataSink and the UploadDataStreamAdapter on the Executor + * passed into its constructor. + */ +@JNINamespace("cronet") +@VisibleForTesting +public final class CronetUploadDataStream extends UploadDataSink { + private static final String TAG = CronetUploadDataStream.class.getSimpleName(); + // These are never changed, once a request starts. + private final Executor mExecutor; + private final VersionSafeCallbacks.UploadDataProviderWrapper mDataProvider; + private final CronetUrlRequest mRequest; + private long mLength; + private long mRemainingLength; + private long mByteBufferLimit; + + // Reusable read task, to reduce redundant memory allocation. + private final Runnable mReadTask = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (mUploadDataStreamAdapter == 0) { + return; + } + checkState(UserCallback.NOT_IN_CALLBACK); + if (mByteBuffer == null) { + throw new IllegalStateException("Unexpected readData call. Buffer is null"); + } + mInWhichUserCallback = UserCallback.READ; + } + try { + checkCallingThread(); + assert mByteBuffer.position() == 0; + mDataProvider.read(CronetUploadDataStream.this, mByteBuffer); + } catch (Exception exception) { + onError(exception); + } + } + }; + + // ByteBuffer created in the native code and passed to + // UploadDataProvider for reading. It is only valid from the + // call to mDataProvider.read until onError or onReadSucceeded. + private ByteBuffer mByteBuffer; + + // Lock that protects all subsequent variables. The adapter has to be + // protected to ensure safe shutdown, mReading and mRewinding are protected + // to robustly detect getting read/rewind results more often than expected. + private final Object mLock = new Object(); + + // Native adapter object, owned by the CronetUploadDataStream. It's only + // deleted after the native UploadDataStream object is destroyed. All access + // to the adapter is synchronized, for safe usage and cleanup. + @GuardedBy("mLock") + private long mUploadDataStreamAdapter; + + @IntDef({UserCallback.READ, UserCallback.REWIND, UserCallback.GET_LENGTH, + UserCallback.NOT_IN_CALLBACK}) + @Retention(RetentionPolicy.SOURCE) + private @interface UserCallback { + int READ = 0; + int REWIND = 1; + int GET_LENGTH = 2; + int NOT_IN_CALLBACK = 3; + } + + @GuardedBy("mLock") + private @UserCallback int mInWhichUserCallback = UserCallback.NOT_IN_CALLBACK; + @GuardedBy("mLock") + private boolean mDestroyAdapterPostponed; + private Runnable mOnDestroyedCallbackForTesting; + + /** + * Constructs a CronetUploadDataStream. + * @param dataProvider the UploadDataProvider to read data from. + * @param executor the Executor to execute UploadDataProvider tasks. + */ + public CronetUploadDataStream( + UploadDataProvider dataProvider, Executor executor, CronetUrlRequest request) { + mExecutor = executor; + mDataProvider = new VersionSafeCallbacks.UploadDataProviderWrapper(dataProvider); + mRequest = request; + } + + /** + * Called by native code to make the UploadDataProvider read data into + * {@code byteBuffer}. + */ + @SuppressWarnings("unused") + @CalledByNative + void readData(ByteBuffer byteBuffer) { + mByteBuffer = byteBuffer; + mByteBufferLimit = byteBuffer.limit(); + postTaskToExecutor(mReadTask); + } + + // TODO(mmenke): Consider implementing a cancel method. + // currently wait for any pending read to complete. + + /** + * Called by native code to make the UploadDataProvider rewind upload data. + */ + @SuppressWarnings("unused") + @CalledByNative + void rewind() { + Runnable task = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (mUploadDataStreamAdapter == 0) { + return; + } + checkState(UserCallback.NOT_IN_CALLBACK); + mInWhichUserCallback = UserCallback.REWIND; + } + try { + checkCallingThread(); + mDataProvider.rewind(CronetUploadDataStream.this); + } catch (Exception exception) { + onError(exception); + } + } + }; + postTaskToExecutor(task); + } + + private void checkCallingThread() { + mRequest.checkCallingThread(); + } + + @GuardedBy("mLock") + private void checkState(@UserCallback int mode) { + if (mInWhichUserCallback != mode) { + throw new IllegalStateException( + "Expected " + mode + ", but was " + mInWhichUserCallback); + } + } + + /** + * Called when the native UploadDataStream is destroyed. At this point, + * the native adapter needs to be destroyed, but only after any pending + * read operation completes, as the adapter owns the read buffer. + */ + @SuppressWarnings("unused") + @CalledByNative + void onUploadDataStreamDestroyed() { + destroyAdapter(); + } + + /** + * Helper method called when an exception occurred. This method resets + * states and propagates the error to the request. + */ + private void onError(Throwable exception) { + final boolean sendClose; + synchronized (mLock) { + if (mInWhichUserCallback == UserCallback.NOT_IN_CALLBACK) { + throw new IllegalStateException( + "There is no read or rewind or length check in progress."); + } + sendClose = mInWhichUserCallback == UserCallback.GET_LENGTH; + mInWhichUserCallback = UserCallback.NOT_IN_CALLBACK; + mByteBuffer = null; + destroyAdapterIfPostponed(); + } + // Failure before length is obtained means that the request has failed before the + // adapter has been initialized. Close the UploadDataProvider. This is safe to call + // here since failure during getLength can only happen on the user's executor. + if (sendClose) { + try { + mDataProvider.close(); + } catch (Exception e) { + Log.e(TAG, "Failure closing data provider", e); + } + } + + // Just fail the request - simpler to fail directly, and + // UploadDataStream only supports failing during initialization, not + // while reading. The request is smart enough to handle the case where + // it was already canceled by the embedder. + mRequest.onUploadException(exception); + } + + @Override + @SuppressLint("DefaultLocale") + public void onReadSucceeded(boolean lastChunk) { + synchronized (mLock) { + checkState(UserCallback.READ); + if (mByteBufferLimit != mByteBuffer.limit()) { + throw new IllegalStateException("ByteBuffer limit changed"); + } + if (lastChunk && mLength >= 0) { + throw new IllegalArgumentException("Non-chunked upload can't have last chunk"); + } + int bytesRead = mByteBuffer.position(); + mRemainingLength -= bytesRead; + if (mRemainingLength < 0 && mLength >= 0) { + throw new IllegalArgumentException( + String.format("Read upload data length %d exceeds expected length %d", + mLength - mRemainingLength, mLength)); + } + mByteBuffer.position(0); + mByteBuffer = null; + mInWhichUserCallback = UserCallback.NOT_IN_CALLBACK; + + destroyAdapterIfPostponed(); + // Request may been canceled already. + if (mUploadDataStreamAdapter == 0) { + return; + } + CronetUploadDataStreamJni.get().onReadSucceeded( + mUploadDataStreamAdapter, CronetUploadDataStream.this, bytesRead, lastChunk); + } + } + + @Override + public void onReadError(Exception exception) { + synchronized (mLock) { + checkState(UserCallback.READ); + onError(exception); + } + } + + @Override + public void onRewindSucceeded() { + synchronized (mLock) { + checkState(UserCallback.REWIND); + mInWhichUserCallback = UserCallback.NOT_IN_CALLBACK; + mRemainingLength = mLength; + // Request may been canceled already. + if (mUploadDataStreamAdapter == 0) { + return; + } + CronetUploadDataStreamJni.get().onRewindSucceeded( + mUploadDataStreamAdapter, CronetUploadDataStream.this); + } + } + + @Override + public void onRewindError(Exception exception) { + synchronized (mLock) { + checkState(UserCallback.REWIND); + onError(exception); + } + } + + /** + * Posts task to application Executor. + */ + void postTaskToExecutor(Runnable task) { + try { + mExecutor.execute(task); + } catch (Throwable e) { + // Just fail the request. The request is smart enough to handle the + // case where it was already canceled by the embedder. + mRequest.onUploadException(e); + } + } + + /** + * The adapter is owned by the CronetUploadDataStream, so it can be + * destroyed safely when there is no pending read; however, destruction is + * initiated by the destruction of the native UploadDataStream. + */ + private void destroyAdapter() { + synchronized (mLock) { + if (mInWhichUserCallback == UserCallback.READ) { + // Wait for the read to complete before destroy the adapter. + mDestroyAdapterPostponed = true; + return; + } + if (mUploadDataStreamAdapter == 0) { + return; + } + CronetUploadDataStreamJni.get().destroy(mUploadDataStreamAdapter); + mUploadDataStreamAdapter = 0; + if (mOnDestroyedCallbackForTesting != null) { + mOnDestroyedCallbackForTesting.run(); + } + } + postTaskToExecutor(new Runnable() { + @Override + public void run() { + try { + checkCallingThread(); + mDataProvider.close(); + } catch (Exception e) { + Log.e(TAG, "Exception thrown when closing", e); + } + } + }); + } + + /** + * Destroys the native adapter if the destruction is postponed due to a + * pending read, which has since completed. Caller needs to be on executor + * thread. + */ + private void destroyAdapterIfPostponed() { + synchronized (mLock) { + if (mInWhichUserCallback == UserCallback.READ) { + throw new IllegalStateException( + "Method should not be called when read has not completed."); + } + if (mDestroyAdapterPostponed) { + destroyAdapter(); + } + } + } + + /** + * Initializes upload length by getting it from data provider. Submits to + * the user's executor thread to allow getLength() to block and/or report errors. + * If data provider throws an exception, then it is reported to the request. + * No native calls to urlRequest are allowed as this is done before request + * start, so native object may not exist. + */ + void initializeWithRequest() { + synchronized (mLock) { + mInWhichUserCallback = UserCallback.GET_LENGTH; + } + try { + mRequest.checkCallingThread(); + mLength = mDataProvider.getLength(); + mRemainingLength = mLength; + } catch (Throwable t) { + onError(t); + } + synchronized (mLock) { + mInWhichUserCallback = UserCallback.NOT_IN_CALLBACK; + } + } + + /** + * Creates native objects and attaches them to the underlying request + * adapter object. Always called on executor thread. + */ + void attachNativeAdapterToRequest(final long requestAdapter) { + synchronized (mLock) { + mUploadDataStreamAdapter = CronetUploadDataStreamJni.get().attachUploadDataToRequest( + CronetUploadDataStream.this, requestAdapter, mLength); + } + } + + /** + * Creates a native CronetUploadDataStreamAdapter and + * CronetUploadDataStream for testing. + * @return the address of the native CronetUploadDataStream object. + */ + @VisibleForTesting + public long createUploadDataStreamForTesting() throws IOException { + synchronized (mLock) { + mUploadDataStreamAdapter = CronetUploadDataStreamJni.get().createAdapterForTesting( + CronetUploadDataStream.this); + mLength = mDataProvider.getLength(); + mRemainingLength = mLength; + return CronetUploadDataStreamJni.get().createUploadDataStreamForTesting( + CronetUploadDataStream.this, mLength, mUploadDataStreamAdapter); + } + } + + @VisibleForTesting + void setOnDestroyedCallbackForTesting(Runnable onDestroyedCallbackForTesting) { + mOnDestroyedCallbackForTesting = onDestroyedCallbackForTesting; + } + + // Native methods are implemented in upload_data_stream_adapter.cc. + @NativeMethods + interface Natives { + long attachUploadDataToRequest( + CronetUploadDataStream caller, long urlRequestAdapter, long length); + + long createAdapterForTesting(CronetUploadDataStream caller); + long createUploadDataStreamForTesting( + CronetUploadDataStream caller, long length, long adapter); + @NativeClassQualifiedName("CronetUploadDataStreamAdapter") + void onReadSucceeded( + long nativePtr, CronetUploadDataStream caller, int bytesRead, boolean finalChunk); + + @NativeClassQualifiedName("CronetUploadDataStreamAdapter") + void onRewindSucceeded(long nativePtr, CronetUploadDataStream caller); + + @NativeClassQualifiedName("CronetUploadDataStreamAdapter") + void destroy(long nativePtr); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/CronetUrlRequest.java b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetUrlRequest.java new file mode 100644 index 0000000000..00d2815729 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetUrlRequest.java @@ -0,0 +1,878 @@ +// Copyright 2014 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.impl; + +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.Log; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNIAdditionalImport; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeClassQualifiedName; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.net.CallbackException; +import org.chromium.net.CronetException; +import org.chromium.net.Idempotency; +import org.chromium.net.InlineExecutionProhibitedException; +import org.chromium.net.NetworkException; +import org.chromium.net.RequestFinishedInfo; +import org.chromium.net.RequestPriority; +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UrlRequest; + +import java.nio.ByteBuffer; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +import javax.annotation.concurrent.GuardedBy; + +/** + * UrlRequest using Chromium HTTP stack implementation. Could be accessed from + * any thread on Executor. Cancel can be called from any thread. + * All @CallByNative methods are called on native network thread + * and post tasks with listener calls onto Executor. Upon return from listener + * callback native request adapter is called on executive thread and posts + * native tasks to native network thread. Because Cancel could be called from + * any thread it is protected by mUrlRequestAdapterLock. + */ +@JNINamespace("cronet") +// Qualifies VersionSafeCallbacks.UrlRequestStatusListener which is used in onStatus, a JNI method. +@JNIAdditionalImport(VersionSafeCallbacks.class) +@VisibleForTesting +public final class CronetUrlRequest extends UrlRequestBase { + private final boolean mAllowDirectExecutor; + + /* Native adapter object, owned by UrlRequest. */ + @GuardedBy("mUrlRequestAdapterLock") + private long mUrlRequestAdapter; + + @GuardedBy("mUrlRequestAdapterLock") + private boolean mStarted; + @GuardedBy("mUrlRequestAdapterLock") + private boolean mWaitingOnRedirect; + @GuardedBy("mUrlRequestAdapterLock") + private boolean mWaitingOnRead; + + /* + * Synchronize access to mUrlRequestAdapter, mStarted, mWaitingOnRedirect, + * and mWaitingOnRead. + */ + private final Object mUrlRequestAdapterLock = new Object(); + private final CronetUrlRequestContext mRequestContext; + private final Executor mExecutor; + + /* + * URL chain contains the URL currently being requested, and + * all URLs previously requested. New URLs are added before + * mCallback.onRedirectReceived is called. + */ + private final List mUrlChain = new ArrayList(); + + private final VersionSafeCallbacks.UrlRequestCallback mCallback; + private final String mInitialUrl; + private final int mPriority; + private final int mIdempotency; + private String mInitialMethod; + private final HeadersList mRequestHeaders = new HeadersList(); + private final Collection mRequestAnnotations; + private final boolean mDisableCache; + private final boolean mDisableConnectionMigration; + private final boolean mTrafficStatsTagSet; + private final int mTrafficStatsTag; + private final boolean mTrafficStatsUidSet; + private final int mTrafficStatsUid; + private final VersionSafeCallbacks.RequestFinishedInfoListener mRequestFinishedListener; + + private CronetUploadDataStream mUploadDataStream; + + private UrlResponseInfoImpl mResponseInfo; + + // These three should only be updated once with mUrlRequestAdapterLock held. They are read on + // UrlRequest.Callback's and RequestFinishedInfo.Listener's executors after the last update. + @RequestFinishedInfoImpl.FinishedReason + private int mFinishedReason; + private CronetException mException; + private CronetMetrics mMetrics; + + /* + * Listener callback is repeatedly invoked when each read is completed, so it + * is cached as a member variable. + */ + private OnReadCompletedRunnable mOnReadCompletedTask; + + @GuardedBy("mUrlRequestAdapterLock") + private Runnable mOnDestroyedCallbackForTesting; + + private static final class HeadersList extends ArrayList> {} + + private final class OnReadCompletedRunnable implements Runnable { + // Buffer passed back from current invocation of onReadCompleted. + ByteBuffer mByteBuffer; + + @Override + public void run() { + checkCallingThread(); + // Null out mByteBuffer, to pass buffer ownership to callback or release if done. + ByteBuffer buffer = mByteBuffer; + mByteBuffer = null; + + try { + synchronized (mUrlRequestAdapterLock) { + if (isDoneLocked()) { + return; + } + mWaitingOnRead = true; + } + mCallback.onReadCompleted(CronetUrlRequest.this, mResponseInfo, buffer); + } catch (Exception e) { + onCallbackException(e); + } + } + } + + CronetUrlRequest(CronetUrlRequestContext requestContext, String url, int priority, + UrlRequest.Callback callback, Executor executor, Collection requestAnnotations, + boolean disableCache, boolean disableConnectionMigration, boolean allowDirectExecutor, + boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, + int trafficStatsUid, RequestFinishedInfo.Listener requestFinishedListener, + int idempotency) { + if (url == null) { + throw new NullPointerException("URL is required"); + } + if (callback == null) { + throw new NullPointerException("Listener is required"); + } + if (executor == null) { + throw new NullPointerException("Executor is required"); + } + + mAllowDirectExecutor = allowDirectExecutor; + mRequestContext = requestContext; + mInitialUrl = url; + mUrlChain.add(url); + mPriority = convertRequestPriority(priority); + mCallback = new VersionSafeCallbacks.UrlRequestCallback(callback); + mExecutor = executor; + mRequestAnnotations = requestAnnotations; + mDisableCache = disableCache; + mDisableConnectionMigration = disableConnectionMigration; + mTrafficStatsTagSet = trafficStatsTagSet; + mTrafficStatsTag = trafficStatsTag; + mTrafficStatsUidSet = trafficStatsUidSet; + mTrafficStatsUid = trafficStatsUid; + mRequestFinishedListener = requestFinishedListener != null + ? new VersionSafeCallbacks.RequestFinishedInfoListener(requestFinishedListener) + : null; + mIdempotency = convertIdempotency(idempotency); + } + + @Override + public void setHttpMethod(String method) { + checkNotStarted(); + if (method == null) { + throw new NullPointerException("Method is required."); + } + mInitialMethod = method; + } + + @Override + public void addHeader(String header, String value) { + checkNotStarted(); + if (header == null) { + throw new NullPointerException("Invalid header name."); + } + if (value == null) { + throw new NullPointerException("Invalid header value."); + } + mRequestHeaders.add(new AbstractMap.SimpleImmutableEntry(header, value)); + } + + @Override + public void setUploadDataProvider(UploadDataProvider uploadDataProvider, Executor executor) { + if (uploadDataProvider == null) { + throw new NullPointerException("Invalid UploadDataProvider."); + } + if (mInitialMethod == null) { + mInitialMethod = "POST"; + } + mUploadDataStream = new CronetUploadDataStream(uploadDataProvider, executor, this); + } + + @Override + public void start() { + synchronized (mUrlRequestAdapterLock) { + checkNotStarted(); + + try { + mUrlRequestAdapter = CronetUrlRequestJni.get().createRequestAdapter( + CronetUrlRequest.this, mRequestContext.getUrlRequestContextAdapter(), + mInitialUrl, mPriority, mDisableCache, mDisableConnectionMigration, + mRequestContext.hasRequestFinishedListener() + || mRequestFinishedListener != null, + mTrafficStatsTagSet, mTrafficStatsTag, mTrafficStatsUidSet, + mTrafficStatsUid, mIdempotency); + mRequestContext.onRequestStarted(); + if (mInitialMethod != null) { + if (!CronetUrlRequestJni.get().setHttpMethod( + mUrlRequestAdapter, CronetUrlRequest.this, mInitialMethod)) { + throw new IllegalArgumentException("Invalid http method " + mInitialMethod); + } + } + + boolean hasContentType = false; + for (Map.Entry header : mRequestHeaders) { + if (header.getKey().equalsIgnoreCase("Content-Type") + && !header.getValue().isEmpty()) { + hasContentType = true; + } + if (!CronetUrlRequestJni.get().addRequestHeader(mUrlRequestAdapter, + CronetUrlRequest.this, header.getKey(), header.getValue())) { + throw new IllegalArgumentException( + "Invalid header " + header.getKey() + "=" + header.getValue()); + } + } + if (mUploadDataStream != null) { + if (!hasContentType) { + throw new IllegalArgumentException( + "Requests with upload data must have a Content-Type."); + } + mStarted = true; + mUploadDataStream.postTaskToExecutor(new Runnable() { + @Override + public void run() { + mUploadDataStream.initializeWithRequest(); + synchronized (mUrlRequestAdapterLock) { + if (isDoneLocked()) { + return; + } + mUploadDataStream.attachNativeAdapterToRequest(mUrlRequestAdapter); + startInternalLocked(); + } + } + }); + return; + } + } catch (RuntimeException e) { + // If there's an exception, cleanup and then throw the exception to the caller. + // start() is synchronized so we do not acquire mUrlRequestAdapterLock here. + destroyRequestAdapterLocked(RequestFinishedInfo.FAILED); + throw e; + } + mStarted = true; + startInternalLocked(); + } + } + + /* + * Starts fully configured request. Could execute on UploadDataProvider executor. + * Caller is expected to ensure that request isn't canceled and mUrlRequestAdapter is valid. + */ + @GuardedBy("mUrlRequestAdapterLock") + private void startInternalLocked() { + CronetUrlRequestJni.get().start(mUrlRequestAdapter, CronetUrlRequest.this); + } + + @Override + public void followRedirect() { + synchronized (mUrlRequestAdapterLock) { + if (!mWaitingOnRedirect) { + throw new IllegalStateException("No redirect to follow."); + } + mWaitingOnRedirect = false; + + if (isDoneLocked()) { + return; + } + + CronetUrlRequestJni.get().followDeferredRedirect( + mUrlRequestAdapter, CronetUrlRequest.this); + } + } + + @Override + public void read(ByteBuffer buffer) { + Preconditions.checkHasRemaining(buffer); + Preconditions.checkDirect(buffer); + synchronized (mUrlRequestAdapterLock) { + if (!mWaitingOnRead) { + throw new IllegalStateException("Unexpected read attempt."); + } + mWaitingOnRead = false; + + if (isDoneLocked()) { + return; + } + + if (!CronetUrlRequestJni.get().readData(mUrlRequestAdapter, CronetUrlRequest.this, + buffer, buffer.position(), buffer.limit())) { + // Still waiting on read. This is just to have consistent + // behavior with the other error cases. + mWaitingOnRead = true; + throw new IllegalArgumentException("Unable to call native read"); + } + } + } + + @Override + public void cancel() { + synchronized (mUrlRequestAdapterLock) { + if (isDoneLocked() || !mStarted) { + return; + } + destroyRequestAdapterLocked(RequestFinishedInfo.CANCELED); + } + } + + @Override + public boolean isDone() { + synchronized (mUrlRequestAdapterLock) { + return isDoneLocked(); + } + } + + @GuardedBy("mUrlRequestAdapterLock") + private boolean isDoneLocked() { + return mStarted && mUrlRequestAdapter == 0; + } + + @Override + public void getStatus(UrlRequest.StatusListener unsafeListener) { + final VersionSafeCallbacks.UrlRequestStatusListener listener = + new VersionSafeCallbacks.UrlRequestStatusListener(unsafeListener); + synchronized (mUrlRequestAdapterLock) { + if (mUrlRequestAdapter != 0) { + CronetUrlRequestJni.get().getStatus( + mUrlRequestAdapter, CronetUrlRequest.this, listener); + return; + } + } + Runnable task = new Runnable() { + @Override + public void run() { + listener.onStatus(UrlRequest.Status.INVALID); + } + }; + postTaskToExecutor(task); + } + + @VisibleForTesting + public void setOnDestroyedCallbackForTesting(Runnable onDestroyedCallbackForTesting) { + synchronized (mUrlRequestAdapterLock) { + mOnDestroyedCallbackForTesting = onDestroyedCallbackForTesting; + } + } + + @VisibleForTesting + public void setOnDestroyedUploadCallbackForTesting( + Runnable onDestroyedUploadCallbackForTesting) { + mUploadDataStream.setOnDestroyedCallbackForTesting(onDestroyedUploadCallbackForTesting); + } + + @VisibleForTesting + public long getUrlRequestAdapterForTesting() { + synchronized (mUrlRequestAdapterLock) { + return mUrlRequestAdapter; + } + } + + /** + * Posts task to application Executor. Used for Listener 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 we fail the request. This exception could + // be permanent (executor shutdown), transient (AbortPolicy, or CallerRunsPolicy with + // direct execution not permitted), or caused by the runnables we submit if + // mUserExecutor is a direct executor and direct execution is not permitted. In the + // latter two cases, there is at least have a chance to inform the embedder of the + // request's failure, since failWithException does not enforce that onFailed() is not + // executed inline. + failWithException( + new CronetExceptionImpl("Exception posting task to executor", failException)); + } + } + + private static int convertRequestPriority(int priority) { + switch (priority) { + case Builder.REQUEST_PRIORITY_IDLE: + return RequestPriority.IDLE; + case Builder.REQUEST_PRIORITY_LOWEST: + return RequestPriority.LOWEST; + case Builder.REQUEST_PRIORITY_LOW: + return RequestPriority.LOW; + case Builder.REQUEST_PRIORITY_MEDIUM: + return RequestPriority.MEDIUM; + case Builder.REQUEST_PRIORITY_HIGHEST: + return RequestPriority.HIGHEST; + default: + return RequestPriority.MEDIUM; + } + } + + private static int convertIdempotency(int idempotency) { + switch (idempotency) { + case Builder.DEFAULT_IDEMPOTENCY: + return Idempotency.DEFAULT_IDEMPOTENCY; + case Builder.IDEMPOTENT: + return Idempotency.IDEMPOTENT; + case Builder.NOT_IDEMPOTENT: + return Idempotency.NOT_IDEMPOTENT; + default: + return Idempotency.DEFAULT_IDEMPOTENCY; + } + } + + private UrlResponseInfoImpl prepareResponseInfoOnNetworkThread(int httpStatusCode, + String httpStatusText, String[] headers, boolean wasCached, String negotiatedProtocol, + String proxyServer, long receivedByteCount) { + HeadersList headersList = new HeadersList(); + for (int i = 0; i < headers.length; i += 2) { + headersList.add(new AbstractMap.SimpleImmutableEntry( + headers[i], headers[i + 1])); + } + return new UrlResponseInfoImpl(new ArrayList(mUrlChain), httpStatusCode, + httpStatusText, headersList, wasCached, negotiatedProtocol, proxyServer, + receivedByteCount); + } + + private void checkNotStarted() { + synchronized (mUrlRequestAdapterLock) { + if (mStarted || isDoneLocked()) { + throw new IllegalStateException("Request is already started."); + } + } + } + + /** + * Helper method to set final status of CronetUrlRequest and clean up the + * native request adapter. + */ + @GuardedBy("mUrlRequestAdapterLock") + private void destroyRequestAdapterLocked( + @RequestFinishedInfoImpl.FinishedReason int finishedReason) { + assert mException == null || finishedReason == RequestFinishedInfo.FAILED; + mFinishedReason = finishedReason; + if (mUrlRequestAdapter == 0) { + return; + } + mRequestContext.onRequestDestroyed(); + // Posts a task to destroy the native adapter. + CronetUrlRequestJni.get().destroy(mUrlRequestAdapter, CronetUrlRequest.this, + finishedReason == RequestFinishedInfo.CANCELED); + mUrlRequestAdapter = 0; + } + + /** + * If callback method throws an exception, request gets canceled + * and exception is reported via onFailed listener callback. + * Only called on the Executor. + */ + private void onCallbackException(Exception e) { + CallbackException requestError = + new CallbackExceptionImpl("Exception received from UrlRequest.Callback", e); + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in CalledByNative method", e); + failWithException(requestError); + } + + /** + * Called when UploadDataProvider encounters an error. + */ + void onUploadException(Throwable e) { + CallbackException uploadError = + new CallbackExceptionImpl("Exception received from UploadDataProvider", e); + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in upload method", e); + failWithException(uploadError); + } + + /** + * Fails the request with an exception on any thread. + */ + private void failWithException(final CronetException exception) { + synchronized (mUrlRequestAdapterLock) { + if (isDoneLocked()) { + return; + } + assert mException == null; + mException = exception; + destroyRequestAdapterLocked(RequestFinishedInfo.FAILED); + } + // onFailed will be invoked from onNativeAdapterDestroyed() to ensure metrics collection. + } + + //////////////////////////////////////////////// + // Private methods called by the native code. + // Always called on network thread. + //////////////////////////////////////////////// + + /** + * Called before following redirects. The redirect will only be followed if + * {@link #followRedirect()} is called. If the redirect response has a body, it will be ignored. + * This will only be called between start and onResponseStarted. + * + * @param newLocation Location where request is redirected. + * @param httpStatusCode from redirect response + * @param receivedByteCount count of bytes received for redirect response + * @param headers an array of response headers with keys at the even indices + * followed by the corresponding values at the odd indices. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onRedirectReceived(final String newLocation, int httpStatusCode, + String httpStatusText, String[] headers, boolean wasCached, String negotiatedProtocol, + String proxyServer, long receivedByteCount) { + final UrlResponseInfoImpl responseInfo = + prepareResponseInfoOnNetworkThread(httpStatusCode, httpStatusText, headers, + wasCached, negotiatedProtocol, proxyServer, receivedByteCount); + + // Have to do this after creating responseInfo. + mUrlChain.add(newLocation); + + Runnable task = new Runnable() { + @Override + public void run() { + checkCallingThread(); + synchronized (mUrlRequestAdapterLock) { + if (isDoneLocked()) { + return; + } + mWaitingOnRedirect = true; + } + + try { + mCallback.onRedirectReceived(CronetUrlRequest.this, responseInfo, newLocation); + } catch (Exception e) { + onCallbackException(e); + } + } + }; + postTaskToExecutor(task); + } + + /** + * Called when the final set of headers, after all redirects, + * is received. Can only be called once for each request. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onResponseStarted(int httpStatusCode, String httpStatusText, String[] headers, + boolean wasCached, String negotiatedProtocol, String proxyServer, + long receivedByteCount) { + mResponseInfo = prepareResponseInfoOnNetworkThread(httpStatusCode, httpStatusText, headers, + wasCached, negotiatedProtocol, proxyServer, receivedByteCount); + Runnable task = new Runnable() { + @Override + public void run() { + checkCallingThread(); + synchronized (mUrlRequestAdapterLock) { + if (isDoneLocked()) { + return; + } + mWaitingOnRead = true; + } + + try { + mCallback.onResponseStarted(CronetUrlRequest.this, mResponseInfo); + } catch (Exception e) { + onCallbackException(e); + } + } + }; + postTaskToExecutor(task); + } + + /** + * Called whenever data is received. The ByteBuffer remains + * valid only until listener callback. Or if the callback + * pauses the request, it remains valid until the request is resumed. + * Cancelling the request also invalidates the buffer. + * + * @param byteBuffer ByteBuffer containing received data, starting at + * initialPosition. Guaranteed to have at least one read byte. Its + * limit has not yet been updated to reflect the bytes read. + * @param bytesRead Number of bytes read. + * @param initialPosition Original position of byteBuffer when passed to + * read(). Used as a minimal check that the buffer hasn't been + * modified while reading from the network. + * @param initialLimit Original limit of byteBuffer when passed to + * read(). Used as a minimal check that the buffer hasn't been + * modified while reading from the network. + * @param receivedByteCount number of bytes received. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onReadCompleted(final ByteBuffer byteBuffer, int bytesRead, int initialPosition, + int initialLimit, long receivedByteCount) { + mResponseInfo.setReceivedByteCount(receivedByteCount); + if (byteBuffer.position() != initialPosition || byteBuffer.limit() != initialLimit) { + failWithException( + new CronetExceptionImpl("ByteBuffer modified externally during read", null)); + return; + } + if (mOnReadCompletedTask == null) { + mOnReadCompletedTask = new OnReadCompletedRunnable(); + } + byteBuffer.position(initialPosition + bytesRead); + mOnReadCompletedTask.mByteBuffer = byteBuffer; + postTaskToExecutor(mOnReadCompletedTask); + } + + /** + * Called when request is completed successfully, no callbacks will be + * called afterwards. + * + * @param receivedByteCount number of bytes received. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onSucceeded(long receivedByteCount) { + mResponseInfo.setReceivedByteCount(receivedByteCount); + Runnable task = new Runnable() { + @Override + public void run() { + synchronized (mUrlRequestAdapterLock) { + if (isDoneLocked()) { + return; + } + // Destroy adapter first, so request context could be shut + // down from the listener. + destroyRequestAdapterLocked(RequestFinishedInfo.SUCCEEDED); + } + try { + mCallback.onSucceeded(CronetUrlRequest.this, mResponseInfo); + maybeReportMetrics(); + } catch (Exception e) { + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in onSucceeded method", e); + } + } + }; + postTaskToExecutor(task); + } + + /** + * Called when error has occurred, no callbacks will be called afterwards. + * + * @param errorCode Error code represented by {@code UrlRequestError} that should be mapped + * to one of {@link NetworkException#ERROR_HOSTNAME_NOT_RESOLVED + * NetworkException.ERROR_*}. + * @param nativeError native net error code. + * @param errorString textual representation of the error code. + * @param receivedByteCount number of bytes received. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onError(int errorCode, int nativeError, int nativeQuicError, String errorString, + long receivedByteCount) { + if (mResponseInfo != null) { + mResponseInfo.setReceivedByteCount(receivedByteCount); + } + if (errorCode == NetworkException.ERROR_QUIC_PROTOCOL_FAILED + || errorCode == NetworkException.ERROR_NETWORK_CHANGED) { + failWithException(new QuicExceptionImpl("Exception in CronetUrlRequest: " + errorString, + errorCode, nativeError, nativeQuicError)); + } else { + int javaError = mapUrlRequestErrorToApiErrorCode(errorCode); + failWithException(new NetworkExceptionImpl( + "Exception in CronetUrlRequest: " + errorString, javaError, nativeError)); + } + } + + /** + * Called when request is canceled, no callbacks will be called afterwards. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onCanceled() { + Runnable task = new Runnable() { + @Override + public void run() { + try { + mCallback.onCanceled(CronetUrlRequest.this, mResponseInfo); + maybeReportMetrics(); + } catch (Exception e) { + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in onCanceled method", e); + } + } + }; + postTaskToExecutor(task); + } + + /** + * Called by the native code when request status is fetched from the + * native stack. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onStatus( + final VersionSafeCallbacks.UrlRequestStatusListener listener, final int loadState) { + Runnable task = new Runnable() { + @Override + public void run() { + listener.onStatus(convertLoadState(loadState)); + } + }; + postTaskToExecutor(task); + } + + /** + * Called by the native code on the network thread to report metrics. Happens before + * onSucceeded, onError and onCanceled. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onMetricsCollected(long requestStartMs, long dnsStartMs, long dnsEndMs, + long connectStartMs, long connectEndMs, long sslStartMs, long sslEndMs, + long sendingStartMs, long sendingEndMs, long pushStartMs, long pushEndMs, + long responseStartMs, long requestEndMs, boolean socketReused, long sentByteCount, + long receivedByteCount) { + synchronized (mUrlRequestAdapterLock) { + if (mMetrics != null) { + throw new IllegalStateException("Metrics collection should only happen once."); + } + mMetrics = new CronetMetrics(requestStartMs, dnsStartMs, dnsEndMs, connectStartMs, + connectEndMs, sslStartMs, sslEndMs, sendingStartMs, sendingEndMs, pushStartMs, + pushEndMs, responseStartMs, requestEndMs, socketReused, sentByteCount, + receivedByteCount); + } + // Metrics are reported to RequestFinishedListener when the final UrlRequest.Callback has + // been invoked. + } + + /** + * Called when the native adapter is destroyed. + */ + @SuppressWarnings("unused") + @CalledByNative + private void onNativeAdapterDestroyed() { + synchronized (mUrlRequestAdapterLock) { + if (mOnDestroyedCallbackForTesting != null) { + mOnDestroyedCallbackForTesting.run(); + } + // mException is set when an error is encountered (in native code via onError or in + // Java code). If mException is not null, notify the mCallback and report metrics. + if (mException == null) { + return; + } + } + Runnable task = new Runnable() { + @Override + public void run() { + try { + mCallback.onFailed(CronetUrlRequest.this, mResponseInfo, mException); + maybeReportMetrics(); + } catch (Exception e) { + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in onFailed method", e); + } + } + }; + try { + mExecutor.execute(task); + } catch (RejectedExecutionException e) { + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception posting task to executor", e); + } + } + + /** Enforces prohibition of direct execution. */ + void checkCallingThread() { + if (!mAllowDirectExecutor && mRequestContext.isNetworkThread(Thread.currentThread())) { + throw new InlineExecutionProhibitedException(); + } + } + + private int mapUrlRequestErrorToApiErrorCode(int errorCode) { + switch (errorCode) { + case UrlRequestError.HOSTNAME_NOT_RESOLVED: + return NetworkException.ERROR_HOSTNAME_NOT_RESOLVED; + case UrlRequestError.INTERNET_DISCONNECTED: + return NetworkException.ERROR_INTERNET_DISCONNECTED; + case UrlRequestError.NETWORK_CHANGED: + return NetworkException.ERROR_NETWORK_CHANGED; + case UrlRequestError.TIMED_OUT: + return NetworkException.ERROR_TIMED_OUT; + case UrlRequestError.CONNECTION_CLOSED: + return NetworkException.ERROR_CONNECTION_CLOSED; + case UrlRequestError.CONNECTION_TIMED_OUT: + return NetworkException.ERROR_CONNECTION_TIMED_OUT; + case UrlRequestError.CONNECTION_REFUSED: + return NetworkException.ERROR_CONNECTION_REFUSED; + case UrlRequestError.CONNECTION_RESET: + return NetworkException.ERROR_CONNECTION_RESET; + case UrlRequestError.ADDRESS_UNREACHABLE: + return NetworkException.ERROR_ADDRESS_UNREACHABLE; + case UrlRequestError.QUIC_PROTOCOL_FAILED: + return NetworkException.ERROR_QUIC_PROTOCOL_FAILED; + case UrlRequestError.OTHER: + return NetworkException.ERROR_OTHER; + default: + Log.e(CronetUrlRequestContext.LOG_TAG, "Unknown error code: " + errorCode); + return errorCode; + } + } + + // Maybe report metrics. This method should only be called on Callback's executor thread and + // after Callback's onSucceeded, onFailed and onCanceled. + private void maybeReportMetrics() { + if (mMetrics != null) { + final RequestFinishedInfo requestInfo = new RequestFinishedInfoImpl(mInitialUrl, + mRequestAnnotations, mMetrics, mFinishedReason, mResponseInfo, mException); + mRequestContext.reportRequestFinished(requestInfo); + if (mRequestFinishedListener != null) { + try { + mRequestFinishedListener.getExecutor().execute(new Runnable() { + @Override + public void run() { + mRequestFinishedListener.onRequestFinished(requestInfo); + } + }); + } catch (RejectedExecutionException failException) { + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception posting task to executor", + failException); + } + } + } + } + + // Native methods are implemented in cronet_url_request_adapter.cc. + @NativeMethods + interface Natives { + long createRequestAdapter(CronetUrlRequest caller, long urlRequestContextAdapter, + String url, int priority, boolean disableCache, boolean disableConnectionMigration, + boolean enableMetrics, boolean trafficStatsTagSet, int trafficStatsTag, + boolean trafficStatsUidSet, int trafficStatsUid, int idempotency); + + @NativeClassQualifiedName("CronetURLRequestAdapter") + boolean setHttpMethod(long nativePtr, CronetUrlRequest caller, String method); + + @NativeClassQualifiedName("CronetURLRequestAdapter") + boolean addRequestHeader( + long nativePtr, CronetUrlRequest caller, String name, String value); + + @NativeClassQualifiedName("CronetURLRequestAdapter") + void start(long nativePtr, CronetUrlRequest caller); + + @NativeClassQualifiedName("CronetURLRequestAdapter") + void followDeferredRedirect(long nativePtr, CronetUrlRequest caller); + + @NativeClassQualifiedName("CronetURLRequestAdapter") + boolean readData(long nativePtr, CronetUrlRequest caller, ByteBuffer byteBuffer, + int position, int capacity); + + @NativeClassQualifiedName("CronetURLRequestAdapter") + void destroy(long nativePtr, CronetUrlRequest caller, boolean sendOnCanceled); + + @NativeClassQualifiedName("CronetURLRequestAdapter") + void getStatus(long nativePtr, CronetUrlRequest caller, + VersionSafeCallbacks.UrlRequestStatusListener listener); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/CronetUrlRequestContext.java b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetUrlRequestContext.java new file mode 100644 index 0000000000..7a9da43851 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/CronetUrlRequestContext.java @@ -0,0 +1,759 @@ +// Copyright 2014 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.impl; + +import android.os.ConditionVariable; +import android.os.Process; + +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.Log; +import org.chromium.base.ObserverList; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeClassQualifiedName; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.base.annotations.UsedByReflection; +import org.chromium.net.BidirectionalStream; +import org.chromium.net.EffectiveConnectionType; +import org.chromium.net.ExperimentalBidirectionalStream; +import org.chromium.net.NetworkQualityRttListener; +import org.chromium.net.NetworkQualityThroughputListener; +import org.chromium.net.RequestFinishedInfo; +import org.chromium.net.RttThroughputValues; +import org.chromium.net.UrlRequest; +import org.chromium.net.urlconnection.CronetHttpURLConnection; +import org.chromium.net.urlconnection.CronetURLStreamHandlerFactory; + +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandlerFactory; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.concurrent.GuardedBy; + +/** + * CronetEngine using Chromium HTTP stack implementation. + */ +@JNINamespace("cronet") +@UsedByReflection("CronetEngine.java") +@VisibleForTesting +public class CronetUrlRequestContext extends CronetEngineBase { + private static final int LOG_NONE = 3; // LOG(FATAL), no VLOG. + private static final int LOG_DEBUG = -1; // LOG(FATAL...INFO), VLOG(1) + private static final int LOG_VERBOSE = -2; // LOG(FATAL...INFO), VLOG(2) + static final String LOG_TAG = CronetUrlRequestContext.class.getSimpleName(); + + /** + * Synchronize access to mUrlRequestContextAdapter and shutdown routine. + */ + private final Object mLock = new Object(); + private final ConditionVariable mInitCompleted = new ConditionVariable(false); + private final AtomicInteger mActiveRequestCount = new AtomicInteger(0); + + @GuardedBy("mLock") + private long mUrlRequestContextAdapter; + /** + * This field is accessed without synchronization, but only for the purposes of reference + * equality comparison with other threads. If such a comparison is performed on the network + * thread, then there is a happens-before edge between the write of this field and the + * subsequent read; if it's performed on another thread, then observing a value of null won't + * change the result of the comparison. + */ + private Thread mNetworkThread; + + private final boolean mNetworkQualityEstimatorEnabled; + + /** + * Locks operations on network quality listeners, because listener + * addition and removal may occur on a different thread from notification. + */ + private final Object mNetworkQualityLock = new Object(); + + /** + * Locks operations on the list of RequestFinishedInfo.Listeners, because operations can happen + * on any thread. This should be used for fine-grained locking only. In particular, don't call + * any UrlRequest methods that acquire mUrlRequestAdapterLock while holding this lock. + */ + private final Object mFinishedListenerLock = new Object(); + + /** + * Current effective connection type as computed by the network quality + * estimator. + */ + @GuardedBy("mNetworkQualityLock") + private int mEffectiveConnectionType = EffectiveConnectionType.TYPE_UNKNOWN; + + /** + * Current estimate of the HTTP RTT (in milliseconds) computed by the + * network quality estimator. + */ + @GuardedBy("mNetworkQualityLock") + private int mHttpRttMs = RttThroughputValues.INVALID_RTT_THROUGHPUT; + + /** + * Current estimate of the transport RTT (in milliseconds) computed by the + * network quality estimator. + */ + @GuardedBy("mNetworkQualityLock") + private int mTransportRttMs = RttThroughputValues.INVALID_RTT_THROUGHPUT; + + /** + * Current estimate of the downstream throughput (in kilobits per second) + * computed by the network quality estimator. + */ + @GuardedBy("mNetworkQualityLock") + private int mDownstreamThroughputKbps = RttThroughputValues.INVALID_RTT_THROUGHPUT; + + @GuardedBy("mNetworkQualityLock") + private final ObserverList + mRttListenerList = + new ObserverList(); + + @GuardedBy("mNetworkQualityLock") + private final ObserverList + mThroughputListenerList = + new ObserverList(); + + @GuardedBy("mFinishedListenerLock") + private final Map mFinishedListenerMap = + new HashMap(); + + private final ConditionVariable mStopNetLogCompleted = new ConditionVariable(); + + /** Set of storage paths currently in use. */ + @GuardedBy("sInUseStoragePaths") + private static final HashSet sInUseStoragePaths = new HashSet(); + + /** Storage path used by this context. */ + private final String mInUseStoragePath; + + /** + * True if a NetLog observer is active. + */ + @GuardedBy("mLock") + private boolean mIsLogging; + + /** + * True if NetLog is being shutdown. + */ + @GuardedBy("mLock") + private boolean mIsStoppingNetLog; + + @UsedByReflection("CronetEngine.java") + public CronetUrlRequestContext(final CronetEngineBuilderImpl builder) { + mRttListenerList.disableThreadAsserts(); + mThroughputListenerList.disableThreadAsserts(); + mNetworkQualityEstimatorEnabled = builder.networkQualityEstimatorEnabled(); + CronetLibraryLoader.ensureInitialized(builder.getContext(), builder); + if (!IntegratedModeState.INTEGRATED_MODE_ENABLED) { + CronetUrlRequestContextJni.get().setMinLogLevel(getLoggingLevel()); + } + if (builder.httpCacheMode() == HttpCacheType.DISK) { + mInUseStoragePath = builder.storagePath(); + synchronized (sInUseStoragePaths) { + if (!sInUseStoragePaths.add(mInUseStoragePath)) { + throw new IllegalStateException("Disk cache storage path already in use"); + } + } + } else { + mInUseStoragePath = null; + } + synchronized (mLock) { + mUrlRequestContextAdapter = + CronetUrlRequestContextJni.get().createRequestContextAdapter( + createNativeUrlRequestContextConfig(builder)); + if (mUrlRequestContextAdapter == 0) { + throw new NullPointerException("Context Adapter creation failed."); + } + } + + // Init native Chromium URLRequestContext on init thread. + CronetLibraryLoader.postToInitThread(new Runnable() { + @Override + public void run() { + CronetLibraryLoader.ensureInitializedOnInitThread(); + synchronized (mLock) { + // mUrlRequestContextAdapter is guaranteed to exist until + // initialization on init and network threads completes and + // initNetworkThread is called back on network thread. + CronetUrlRequestContextJni.get().initRequestContextOnInitThread( + mUrlRequestContextAdapter, CronetUrlRequestContext.this); + } + } + }); + } + + @VisibleForTesting + public static long createNativeUrlRequestContextConfig(CronetEngineBuilderImpl builder) { + final long urlRequestContextConfig = + CronetUrlRequestContextJni.get().createRequestContextConfig(builder.getUserAgent(), + builder.storagePath(), builder.quicEnabled(), + builder.getDefaultQuicUserAgentId(), builder.http2Enabled(), + builder.brotliEnabled(), builder.cacheDisabled(), builder.httpCacheMode(), + builder.httpCacheMaxSize(), builder.experimentalOptions(), + builder.mockCertVerifier(), builder.networkQualityEstimatorEnabled(), + builder.publicKeyPinningBypassForLocalTrustAnchorsEnabled(), + builder.threadPriority(Process.THREAD_PRIORITY_BACKGROUND)); + if (urlRequestContextConfig == 0) { + throw new IllegalArgumentException("Experimental options parsing failed."); + } + for (CronetEngineBuilderImpl.QuicHint quicHint : builder.quicHints()) { + CronetUrlRequestContextJni.get().addQuicHint(urlRequestContextConfig, quicHint.mHost, + quicHint.mPort, quicHint.mAlternatePort); + } + for (CronetEngineBuilderImpl.Pkp pkp : builder.publicKeyPins()) { + CronetUrlRequestContextJni.get().addPkp(urlRequestContextConfig, pkp.mHost, pkp.mHashes, + pkp.mIncludeSubdomains, pkp.mExpirationDate.getTime()); + } + return urlRequestContextConfig; + } + + @Override + public ExperimentalBidirectionalStream.Builder newBidirectionalStreamBuilder( + String url, BidirectionalStream.Callback callback, Executor executor) { + return new BidirectionalStreamBuilderImpl(url, callback, executor, this); + } + + @Override + public UrlRequestBase createRequest(String url, UrlRequest.Callback callback, Executor executor, + int priority, Collection requestAnnotations, boolean disableCache, + boolean disableConnectionMigration, boolean allowDirectExecutor, + boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, + int trafficStatsUid, RequestFinishedInfo.Listener requestFinishedListener, + int idempotency) { + synchronized (mLock) { + checkHaveAdapter(); + return new CronetUrlRequest(this, url, priority, callback, executor, requestAnnotations, + disableCache, disableConnectionMigration, allowDirectExecutor, + trafficStatsTagSet, trafficStatsTag, trafficStatsUidSet, trafficStatsUid, + requestFinishedListener, idempotency); + } + } + + @Override + protected ExperimentalBidirectionalStream createBidirectionalStream(String url, + BidirectionalStream.Callback callback, Executor executor, String httpMethod, + List> requestHeaders, @StreamPriority int priority, + boolean delayRequestHeadersUntilFirstFlush, Collection requestAnnotations, + boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, + int trafficStatsUid) { + synchronized (mLock) { + checkHaveAdapter(); + return new CronetBidirectionalStream(this, url, priority, callback, executor, + httpMethod, requestHeaders, delayRequestHeadersUntilFirstFlush, + requestAnnotations, trafficStatsTagSet, trafficStatsTag, trafficStatsUidSet, + trafficStatsUid); + } + } + + @Override + public String getVersionString() { + return "Cronet/" + ImplVersion.getCronetVersionWithLastChange(); + } + + @Override + public void shutdown() { + if (mInUseStoragePath != null) { + synchronized (sInUseStoragePaths) { + sInUseStoragePaths.remove(mInUseStoragePath); + } + } + synchronized (mLock) { + checkHaveAdapter(); + if (mActiveRequestCount.get() != 0) { + throw new IllegalStateException("Cannot shutdown with active requests."); + } + // Destroying adapter stops the network thread, so it cannot be + // called on network thread. + if (Thread.currentThread() == mNetworkThread) { + throw new IllegalThreadStateException("Cannot shutdown from network thread."); + } + } + // Wait for init to complete on init and network thread (without lock, + // so other thread could access it). + mInitCompleted.block(); + + // If not logging, this is a no-op. + stopNetLog(); + + synchronized (mLock) { + // It is possible that adapter is already destroyed on another thread. + if (!haveRequestContextAdapter()) { + return; + } + CronetUrlRequestContextJni.get().destroy( + mUrlRequestContextAdapter, CronetUrlRequestContext.this); + mUrlRequestContextAdapter = 0; + } + } + + @Override + public void startNetLogToFile(String fileName, boolean logAll) { + synchronized (mLock) { + checkHaveAdapter(); + if (mIsLogging) { + return; + } + if (!CronetUrlRequestContextJni.get().startNetLogToFile(mUrlRequestContextAdapter, + CronetUrlRequestContext.this, fileName, logAll)) { + throw new RuntimeException("Unable to start NetLog"); + } + mIsLogging = true; + } + } + + @Override + public void startNetLogToDisk(String dirPath, boolean logAll, int maxSize) { + synchronized (mLock) { + checkHaveAdapter(); + if (mIsLogging) { + return; + } + CronetUrlRequestContextJni.get().startNetLogToDisk(mUrlRequestContextAdapter, + CronetUrlRequestContext.this, dirPath, logAll, maxSize); + mIsLogging = true; + } + } + + @Override + public void stopNetLog() { + synchronized (mLock) { + checkHaveAdapter(); + if (!mIsLogging || mIsStoppingNetLog) { + return; + } + CronetUrlRequestContextJni.get().stopNetLog( + mUrlRequestContextAdapter, CronetUrlRequestContext.this); + mIsStoppingNetLog = true; + } + mStopNetLogCompleted.block(); + mStopNetLogCompleted.close(); + synchronized (mLock) { + mIsStoppingNetLog = false; + mIsLogging = false; + } + } + + @CalledByNative + public void stopNetLogCompleted() { + mStopNetLogCompleted.open(); + } + + // This method is intentionally non-static to ensure Cronet native library + // is loaded by class constructor. + @Override + public byte[] getGlobalMetricsDeltas() { + return CronetUrlRequestContextJni.get().getHistogramDeltas(); + } + + @Override + public int getEffectiveConnectionType() { + if (!mNetworkQualityEstimatorEnabled) { + throw new IllegalStateException("Network quality estimator must be enabled"); + } + synchronized (mNetworkQualityLock) { + return convertConnectionTypeToApiValue(mEffectiveConnectionType); + } + } + + @Override + public int getHttpRttMs() { + if (!mNetworkQualityEstimatorEnabled) { + throw new IllegalStateException("Network quality estimator must be enabled"); + } + synchronized (mNetworkQualityLock) { + return mHttpRttMs != RttThroughputValues.INVALID_RTT_THROUGHPUT + ? mHttpRttMs + : CONNECTION_METRIC_UNKNOWN; + } + } + + @Override + public int getTransportRttMs() { + if (!mNetworkQualityEstimatorEnabled) { + throw new IllegalStateException("Network quality estimator must be enabled"); + } + synchronized (mNetworkQualityLock) { + return mTransportRttMs != RttThroughputValues.INVALID_RTT_THROUGHPUT + ? mTransportRttMs + : CONNECTION_METRIC_UNKNOWN; + } + } + + @Override + public int getDownstreamThroughputKbps() { + if (!mNetworkQualityEstimatorEnabled) { + throw new IllegalStateException("Network quality estimator must be enabled"); + } + synchronized (mNetworkQualityLock) { + return mDownstreamThroughputKbps != RttThroughputValues.INVALID_RTT_THROUGHPUT + ? mDownstreamThroughputKbps + : CONNECTION_METRIC_UNKNOWN; + } + } + + @VisibleForTesting + @Override + public void configureNetworkQualityEstimatorForTesting(boolean useLocalHostRequests, + boolean useSmallerResponses, boolean disableOfflineCheck) { + if (!mNetworkQualityEstimatorEnabled) { + throw new IllegalStateException("Network quality estimator must be enabled"); + } + synchronized (mLock) { + checkHaveAdapter(); + CronetUrlRequestContextJni.get().configureNetworkQualityEstimatorForTesting( + mUrlRequestContextAdapter, CronetUrlRequestContext.this, useLocalHostRequests, + useSmallerResponses, disableOfflineCheck); + } + } + + @Override + public void addRttListener(NetworkQualityRttListener listener) { + if (!mNetworkQualityEstimatorEnabled) { + throw new IllegalStateException("Network quality estimator must be enabled"); + } + synchronized (mNetworkQualityLock) { + if (mRttListenerList.isEmpty()) { + synchronized (mLock) { + checkHaveAdapter(); + CronetUrlRequestContextJni.get().provideRTTObservations( + mUrlRequestContextAdapter, CronetUrlRequestContext.this, true); + } + } + mRttListenerList.addObserver( + new VersionSafeCallbacks.NetworkQualityRttListenerWrapper(listener)); + } + } + + @Override + public void removeRttListener(NetworkQualityRttListener listener) { + if (!mNetworkQualityEstimatorEnabled) { + throw new IllegalStateException("Network quality estimator must be enabled"); + } + synchronized (mNetworkQualityLock) { + if (mRttListenerList.removeObserver( + new VersionSafeCallbacks.NetworkQualityRttListenerWrapper(listener))) { + if (mRttListenerList.isEmpty()) { + synchronized (mLock) { + checkHaveAdapter(); + CronetUrlRequestContextJni.get().provideRTTObservations( + mUrlRequestContextAdapter, CronetUrlRequestContext.this, false); + } + } + } + } + } + + @Override + public void addThroughputListener(NetworkQualityThroughputListener listener) { + if (!mNetworkQualityEstimatorEnabled) { + throw new IllegalStateException("Network quality estimator must be enabled"); + } + synchronized (mNetworkQualityLock) { + if (mThroughputListenerList.isEmpty()) { + synchronized (mLock) { + checkHaveAdapter(); + CronetUrlRequestContextJni.get().provideThroughputObservations( + mUrlRequestContextAdapter, CronetUrlRequestContext.this, true); + } + } + mThroughputListenerList.addObserver( + new VersionSafeCallbacks.NetworkQualityThroughputListenerWrapper(listener)); + } + } + + @Override + public void removeThroughputListener(NetworkQualityThroughputListener listener) { + if (!mNetworkQualityEstimatorEnabled) { + throw new IllegalStateException("Network quality estimator must be enabled"); + } + synchronized (mNetworkQualityLock) { + if (mThroughputListenerList.removeObserver( + new VersionSafeCallbacks.NetworkQualityThroughputListenerWrapper( + listener))) { + if (mThroughputListenerList.isEmpty()) { + synchronized (mLock) { + checkHaveAdapter(); + CronetUrlRequestContextJni.get().provideThroughputObservations( + mUrlRequestContextAdapter, CronetUrlRequestContext.this, false); + } + } + } + } + } + + @Override + public void addRequestFinishedListener(RequestFinishedInfo.Listener listener) { + synchronized (mFinishedListenerLock) { + mFinishedListenerMap.put( + listener, new VersionSafeCallbacks.RequestFinishedInfoListener(listener)); + } + } + + @Override + public void removeRequestFinishedListener(RequestFinishedInfo.Listener listener) { + synchronized (mFinishedListenerLock) { + mFinishedListenerMap.remove(listener); + } + } + + boolean hasRequestFinishedListener() { + synchronized (mFinishedListenerLock) { + return !mFinishedListenerMap.isEmpty(); + } + } + + @Override + public URLConnection openConnection(URL url) { + return openConnection(url, Proxy.NO_PROXY); + } + + @Override + public URLConnection openConnection(URL url, Proxy proxy) { + if (proxy.type() != Proxy.Type.DIRECT) { + throw new UnsupportedOperationException(); + } + String protocol = url.getProtocol(); + if ("http".equals(protocol) || "https".equals(protocol)) { + return new CronetHttpURLConnection(url, this); + } + throw new UnsupportedOperationException("Unexpected protocol:" + protocol); + } + + @Override + public URLStreamHandlerFactory createURLStreamHandlerFactory() { + return new CronetURLStreamHandlerFactory(this); + } + + /** + * Mark request as started to prevent shutdown when there are active + * requests. + */ + void onRequestStarted() { + mActiveRequestCount.incrementAndGet(); + } + + /** + * Mark request as finished to allow shutdown when there are no active + * requests. + */ + void onRequestDestroyed() { + mActiveRequestCount.decrementAndGet(); + } + + @VisibleForTesting + public long getUrlRequestContextAdapter() { + synchronized (mLock) { + checkHaveAdapter(); + return mUrlRequestContextAdapter; + } + } + + @GuardedBy("mLock") + private void checkHaveAdapter() throws IllegalStateException { + if (!haveRequestContextAdapter()) { + throw new IllegalStateException("Engine is shut down."); + } + } + + @GuardedBy("mLock") + private boolean haveRequestContextAdapter() { + return mUrlRequestContextAdapter != 0; + } + + /** + * @return loggingLevel see {@link #LOG_NONE}, {@link #LOG_DEBUG} and + * {@link #LOG_VERBOSE}. + */ + private int getLoggingLevel() { + int loggingLevel; + if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + loggingLevel = LOG_VERBOSE; + } else if (Log.isLoggable(LOG_TAG, Log.DEBUG)) { + loggingLevel = LOG_DEBUG; + } else { + loggingLevel = LOG_NONE; + } + return loggingLevel; + } + + private static int convertConnectionTypeToApiValue(@EffectiveConnectionType int type) { + switch (type) { + case EffectiveConnectionType.TYPE_OFFLINE: + return EFFECTIVE_CONNECTION_TYPE_OFFLINE; + case EffectiveConnectionType.TYPE_SLOW_2G: + return EFFECTIVE_CONNECTION_TYPE_SLOW_2G; + case EffectiveConnectionType.TYPE_2G: + return EFFECTIVE_CONNECTION_TYPE_2G; + case EffectiveConnectionType.TYPE_3G: + return EFFECTIVE_CONNECTION_TYPE_3G; + case EffectiveConnectionType.TYPE_4G: + return EFFECTIVE_CONNECTION_TYPE_4G; + case EffectiveConnectionType.TYPE_UNKNOWN: + return EFFECTIVE_CONNECTION_TYPE_UNKNOWN; + default: + throw new RuntimeException( + "Internal Error: Illegal EffectiveConnectionType value " + type); + } + } + + @SuppressWarnings("unused") + @CalledByNative + private void initNetworkThread() { + mNetworkThread = Thread.currentThread(); + mInitCompleted.open(); + if (!IntegratedModeState.INTEGRATED_MODE_ENABLED) { + // In integrated mode, network thread is shared from the host. + // Cronet shouldn't change the property of the thread. + Thread.currentThread().setName("ChromiumNet"); + } + } + + @SuppressWarnings("unused") + @CalledByNative + private void onEffectiveConnectionTypeChanged(int effectiveConnectionType) { + synchronized (mNetworkQualityLock) { + // Convert the enum returned by the network quality estimator to an enum of type + // EffectiveConnectionType. + mEffectiveConnectionType = effectiveConnectionType; + } + } + + @SuppressWarnings("unused") + @CalledByNative + private void onRTTOrThroughputEstimatesComputed( + final int httpRttMs, final int transportRttMs, final int downstreamThroughputKbps) { + synchronized (mNetworkQualityLock) { + mHttpRttMs = httpRttMs; + mTransportRttMs = transportRttMs; + mDownstreamThroughputKbps = downstreamThroughputKbps; + } + } + + @SuppressWarnings("unused") + @CalledByNative + private void onRttObservation(final int rttMs, final long whenMs, final int source) { + synchronized (mNetworkQualityLock) { + for (final VersionSafeCallbacks.NetworkQualityRttListenerWrapper listener : + mRttListenerList) { + Runnable task = new Runnable() { + @Override + public void run() { + listener.onRttObservation(rttMs, whenMs, source); + } + }; + postObservationTaskToExecutor(listener.getExecutor(), task); + } + } + } + + @SuppressWarnings("unused") + @CalledByNative + private void onThroughputObservation( + final int throughputKbps, final long whenMs, final int source) { + synchronized (mNetworkQualityLock) { + for (final VersionSafeCallbacks.NetworkQualityThroughputListenerWrapper listener : + mThroughputListenerList) { + Runnable task = new Runnable() { + @Override + public void run() { + listener.onThroughputObservation(throughputKbps, whenMs, source); + } + }; + postObservationTaskToExecutor(listener.getExecutor(), task); + } + } + } + + void reportRequestFinished(final RequestFinishedInfo requestInfo) { + ArrayList currentListeners; + synchronized (mFinishedListenerLock) { + if (mFinishedListenerMap.isEmpty()) return; + currentListeners = new ArrayList( + mFinishedListenerMap.values()); + } + for (final VersionSafeCallbacks.RequestFinishedInfoListener listener : currentListeners) { + Runnable task = new Runnable() { + @Override + public void run() { + listener.onRequestFinished(requestInfo); + } + }; + postObservationTaskToExecutor(listener.getExecutor(), task); + } + } + + private static void postObservationTaskToExecutor(Executor executor, Runnable task) { + try { + executor.execute(task); + } catch (RejectedExecutionException failException) { + Log.e(CronetUrlRequestContext.LOG_TAG, "Exception posting task to executor", + failException); + } + } + + public boolean isNetworkThread(Thread thread) { + return thread == mNetworkThread; + } + + // Native methods are implemented in cronet_url_request_context_adapter.cc. + @NativeMethods + interface Natives { + long createRequestContextConfig(String userAgent, String storagePath, boolean quicEnabled, + String quicUserAgentId, boolean http2Enabled, boolean brotliEnabled, + boolean disableCache, int httpCacheMode, long httpCacheMaxSize, + String experimentalOptions, long mockCertVerifier, + boolean enableNetworkQualityEstimator, + boolean bypassPublicKeyPinningForLocalTrustAnchors, int networkThreadPriority); + + void addQuicHint(long urlRequestContextConfig, String host, int port, int alternatePort); + void addPkp(long urlRequestContextConfig, String host, byte[][] hashes, + boolean includeSubdomains, long expirationTime); + long createRequestContextAdapter(long urlRequestContextConfig); + int setMinLogLevel(int loggingLevel); + byte[] getHistogramDeltas(); + @NativeClassQualifiedName("CronetURLRequestContextAdapter") + void destroy(long nativePtr, CronetUrlRequestContext caller); + + @NativeClassQualifiedName("CronetURLRequestContextAdapter") + boolean startNetLogToFile( + long nativePtr, CronetUrlRequestContext caller, String fileName, boolean logAll); + + @NativeClassQualifiedName("CronetURLRequestContextAdapter") + void startNetLogToDisk(long nativePtr, CronetUrlRequestContext caller, String dirPath, + boolean logAll, int maxSize); + + @NativeClassQualifiedName("CronetURLRequestContextAdapter") + void stopNetLog(long nativePtr, CronetUrlRequestContext caller); + + @NativeClassQualifiedName("CronetURLRequestContextAdapter") + void initRequestContextOnInitThread(long nativePtr, CronetUrlRequestContext caller); + + @NativeClassQualifiedName("CronetURLRequestContextAdapter") + void configureNetworkQualityEstimatorForTesting(long nativePtr, + CronetUrlRequestContext caller, boolean useLocalHostRequests, + boolean useSmallerResponses, boolean disableOfflineCheck); + + @NativeClassQualifiedName("CronetURLRequestContextAdapter") + void provideRTTObservations(long nativePtr, CronetUrlRequestContext caller, boolean should); + + @NativeClassQualifiedName("CronetURLRequestContextAdapter") + void provideThroughputObservations( + long nativePtr, CronetUrlRequestContext caller, boolean should); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/ImplVersion.template b/src/components/cronet/android/java/src/org/chromium/net/impl/ImplVersion.template new file mode 100644 index 0000000000..cd7f249db4 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/ImplVersion.template @@ -0,0 +1,33 @@ +// Copyright 2014 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.impl; + +// Version based on chrome/VERSION. +public class ImplVersion { + private static final String CRONET_VERSION = "@MAJOR@.@MINOR@.@BUILD@.@PATCH@"; + private static final int API_LEVEL = @API_LEVEL@; + private static final String LAST_CHANGE = "@LASTCHANGE@"; + + /** + * Private constructor. All members of this class should be static. + */ + private ImplVersion() {} + + public static String getCronetVersionWithLastChange() { + return CRONET_VERSION + "@" + LAST_CHANGE.substring(0, 8); + } + + public static int getApiLevel() { + return API_LEVEL; + } + + public static String getCronetVersion() { + return CRONET_VERSION; + } + + public static String getLastChange() { + return LAST_CHANGE; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/InputStreamChannel.java b/src/components/cronet/android/java/src/org/chromium/net/impl/InputStreamChannel.java new file mode 100644 index 0000000000..d0e34ee05c --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/InputStreamChannel.java @@ -0,0 +1,77 @@ +// 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.impl; + +import androidx.annotation.NonNull; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Adapts an {@link InputStream} into a {@link ReadableByteChannel}, exactly like + * {@link java.nio.channels.Channels#newChannel(InputStream)} does, but more efficiently, since it + * does not allocate a temporary buffer if it doesn't have to, and it freely takes advantage of + * {@link FileInputStream}'s trivial conversion to {@link java.nio.channels.FileChannel}. + */ +final class InputStreamChannel implements ReadableByteChannel { + private static final int MAX_TMP_BUFFER_SIZE = 16384; + private static final int MIN_TMP_BUFFER_SIZE = 4096; + private final InputStream mInputStream; + private final AtomicBoolean mIsOpen = new AtomicBoolean(true); + + private InputStreamChannel(@NonNull InputStream inputStream) { + mInputStream = inputStream; + } + + static ReadableByteChannel wrap(@NonNull InputStream inputStream) { + if (inputStream instanceof FileInputStream) { + return ((FileInputStream) inputStream).getChannel(); + } + return new InputStreamChannel(inputStream); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + final int read; + if (dst.hasArray()) { + read = mInputStream.read( + dst.array(), dst.arrayOffset() + dst.position(), dst.remaining()); + if (read > 0) { + dst.position(dst.position() + read); + } + } else { + // Since we're allocating a buffer for every read, we want to choose a good size - on + // Android, the only case where a ByteBuffer won't have a backing byte[] is if it was + // created wrapping a void * in native code, or if it represents a memory-mapped file. + // Especially in the latter case, we want to avoid allocating a buffer that could be + // very large. + final int possibleToRead = Math.min( + Math.max(mInputStream.available(), MIN_TMP_BUFFER_SIZE), dst.remaining()); + final int reasonableToRead = Math.min(MAX_TMP_BUFFER_SIZE, possibleToRead); + byte[] tmpBuf = new byte[reasonableToRead]; + read = mInputStream.read(tmpBuf); + if (read > 0) { + dst.put(tmpBuf, 0, read); + } + } + return read; + } + + @Override + public boolean isOpen() { + return mIsOpen.get(); + } + + @Override + public void close() throws IOException { + if (mIsOpen.compareAndSet(true, false)) { + mInputStream.close(); + } + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/IntegratedModeState.template b/src/components/cronet/android/java/src/org/chromium/net/impl/IntegratedModeState.template new file mode 100644 index 0000000000..28fb6c3560 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/IntegratedModeState.template @@ -0,0 +1,21 @@ +// Copyright 2018 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.impl; + +/** + * This template file provides the flags of Cronet integrated mode for Java side. + */ +public class IntegratedModeState { + + // A boolean flag indicating whether integrated mode is enabled. In integrated mode, CronetEngine + // would use the shared network task runner by other Chromium-based clients like webview, Chrome + // Android, Cronet without self-initialization. + public static final boolean INTEGRATED_MODE_ENABLED = +#if defined(INTEGRATED_MODE) + true; +#else + false; +#endif +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/JavaCronetEngine.java b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaCronetEngine.java new file mode 100644 index 0000000000..b5e1c49d20 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaCronetEngine.java @@ -0,0 +1,186 @@ +// 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.impl; + +import static android.os.Process.THREAD_PRIORITY_BACKGROUND; +import static android.os.Process.THREAD_PRIORITY_MORE_FAVORABLE; + +import org.chromium.net.BidirectionalStream; +import org.chromium.net.ExperimentalBidirectionalStream; +import org.chromium.net.NetworkQualityRttListener; +import org.chromium.net.NetworkQualityThroughputListener; +import org.chromium.net.RequestFinishedInfo; +import org.chromium.net.UrlRequest; + +import java.io.IOException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +/** + * {@link java.net.HttpURLConnection} backed CronetEngine. + * + *

Does not support netlogs, transferred data measurement, bidistream, cache, or priority. + */ +public final class JavaCronetEngine extends CronetEngineBase { + private final String mUserAgent; + private final ExecutorService mExecutorService; + + public JavaCronetEngine(CronetEngineBuilderImpl builder) { + // On android, all background threads (and all threads that are part + // of background processes) are put in a cgroup that is allowed to + // consume up to 5% of CPU - these worker threads spend the vast + // majority of their time waiting on I/O, so making them contend with + // background applications for a slice of CPU doesn't make much sense. + // We want to hurry up and get idle. + final int threadPriority = + builder.threadPriority(THREAD_PRIORITY_BACKGROUND + THREAD_PRIORITY_MORE_FAVORABLE); + this.mUserAgent = builder.getUserAgent(); + this.mExecutorService = new ThreadPoolExecutor(10, 20, 50, TimeUnit.SECONDS, + new LinkedBlockingQueue(), new ThreadFactory() { + @Override + public Thread newThread(final Runnable r) { + return Executors.defaultThreadFactory().newThread(new Runnable() { + @Override + public void run() { + Thread.currentThread().setName("JavaCronetEngine"); + android.os.Process.setThreadPriority(threadPriority); + r.run(); + } + }); + } + }); + } + + @Override + public UrlRequestBase createRequest(String url, UrlRequest.Callback callback, Executor executor, + int priority, Collection connectionAnnotations, boolean disableCache, + boolean disableConnectionMigration, boolean allowDirectExecutor, + boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, + int trafficStatsUid, RequestFinishedInfo.Listener requestFinishedListener, + int idempotency) { + return new JavaUrlRequest(callback, mExecutorService, executor, url, mUserAgent, + allowDirectExecutor, trafficStatsTagSet, trafficStatsTag, trafficStatsUidSet, + trafficStatsUid); + } + + @Override + protected ExperimentalBidirectionalStream createBidirectionalStream(String url, + BidirectionalStream.Callback callback, Executor executor, String httpMethod, + List> requestHeaders, @StreamPriority int priority, + boolean delayRequestHeadersUntilFirstFlush, Collection connectionAnnotations, + boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, + int trafficStatsUid) { + throw new UnsupportedOperationException( + "Can't create a bidi stream - httpurlconnection doesn't have those APIs"); + } + + @Override + public ExperimentalBidirectionalStream.Builder newBidirectionalStreamBuilder( + String url, BidirectionalStream.Callback callback, Executor executor) { + throw new UnsupportedOperationException( + "The bidirectional stream API is not supported by the Java implementation " + + "of Cronet Engine"); + } + + @Override + public String getVersionString() { + return "CronetHttpURLConnection/" + ImplVersion.getCronetVersionWithLastChange(); + } + + @Override + public void shutdown() { + mExecutorService.shutdown(); + } + + @Override + public void startNetLogToFile(String fileName, boolean logAll) {} + + @Override + public void startNetLogToDisk(String dirPath, boolean logAll, int maxSize) {} + + @Override + public void stopNetLog() {} + + @Override + public byte[] getGlobalMetricsDeltas() { + return new byte[0]; + } + + @Override + public int getEffectiveConnectionType() { + return EFFECTIVE_CONNECTION_TYPE_UNKNOWN; + } + + @Override + public int getHttpRttMs() { + return CONNECTION_METRIC_UNKNOWN; + } + + @Override + public int getTransportRttMs() { + return CONNECTION_METRIC_UNKNOWN; + } + + @Override + public int getDownstreamThroughputKbps() { + return CONNECTION_METRIC_UNKNOWN; + } + + @Override + public void configureNetworkQualityEstimatorForTesting(boolean useLocalHostRequests, + boolean useSmallerResponses, boolean disableOfflineCheck) {} + + @Override + public void addRttListener(NetworkQualityRttListener listener) {} + + @Override + public void removeRttListener(NetworkQualityRttListener listener) {} + + @Override + public void addThroughputListener(NetworkQualityThroughputListener listener) {} + + @Override + public void removeThroughputListener(NetworkQualityThroughputListener listener) {} + + @Override + public void addRequestFinishedListener(RequestFinishedInfo.Listener listener) {} + + @Override + public void removeRequestFinishedListener(RequestFinishedInfo.Listener listener) {} + + @Override + public URLConnection openConnection(URL url) throws IOException { + return url.openConnection(); + } + + @Override + public URLConnection openConnection(URL url, Proxy proxy) throws IOException { + return url.openConnection(proxy); + } + + @Override + public URLStreamHandlerFactory createURLStreamHandlerFactory() { + // Returning null causes this factory to pass though, which ends up using the platform's + // implementation. + return new URLStreamHandlerFactory() { + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + return null; + } + }; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/JavaCronetEngineBuilderImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaCronetEngineBuilderImpl.java new file mode 100644 index 0000000000..e1ee10dfec --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaCronetEngineBuilderImpl.java @@ -0,0 +1,32 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net.impl; + +import android.content.Context; + +import org.chromium.net.ExperimentalCronetEngine; +import org.chromium.net.ICronetEngineBuilder; + +/** + * Implementation of {@link ICronetEngineBuilder} that builds Java-based Cronet engine. + */ +public class JavaCronetEngineBuilderImpl extends CronetEngineBuilderImpl { + /** + * Builder for Platform Cronet Engine. + * + * @param context Android {@link Context} for engine to use. + */ + public JavaCronetEngineBuilderImpl(Context context) { + super(context); + } + + @Override + public ExperimentalCronetEngine build() { + if (getUserAgent() == null) { + setUserAgent(getDefaultUserAgent()); + } + return new JavaCronetEngine(this); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/JavaCronetProvider.java b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaCronetProvider.java new file mode 100644 index 0000000000..15d960d800 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaCronetProvider.java @@ -0,0 +1,62 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net.impl; + +import android.content.Context; + +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetProvider; +import org.chromium.net.ExperimentalCronetEngine; +import org.chromium.net.ICronetEngineBuilder; + +import java.util.Arrays; + +/** + * Implementation of {@link CronetProvider} that creates {@link CronetEngine.Builder} + * for building the Java-based implementation of {@link CronetEngine}. + */ +public class JavaCronetProvider extends CronetProvider { + /** + * Constructor. + * + * @param context Android context to use. + */ + public JavaCronetProvider(Context context) { + super(context); + } + + @Override + public CronetEngine.Builder createBuilder() { + ICronetEngineBuilder impl = new JavaCronetEngineBuilderImpl(mContext); + return new ExperimentalCronetEngine.Builder(impl); + } + + @Override + public String getName() { + return CronetProvider.PROVIDER_NAME_FALLBACK; + } + + @Override + public String getVersion() { + return ImplVersion.getCronetVersion(); + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] {JavaCronetProvider.class, mContext}); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof JavaCronetProvider + && this.mContext.equals(((JavaCronetProvider) other).mContext)); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/JavaUploadDataSinkBase.java b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaUploadDataSinkBase.java new file mode 100644 index 0000000000..3acceb02a0 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaUploadDataSinkBase.java @@ -0,0 +1,255 @@ +// Copyright 2019 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.impl; + +import androidx.annotation.IntDef; + +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UploadDataSink; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.util.Locale; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Base class for Java UrlRequest implementations of UploadDataSink. Handles asynchronicity and + * manages the executors for this upload. + */ +public abstract class JavaUploadDataSinkBase extends UploadDataSink { + @IntDef({SinkState.AWAITING_READ_RESULT, SinkState.AWAITING_REWIND_RESULT, SinkState.UPLOADING, + SinkState.NOT_STARTED}) + @Retention(RetentionPolicy.SOURCE) + @interface SinkState { + int AWAITING_READ_RESULT = 0; + int AWAITING_REWIND_RESULT = 1; + int UPLOADING = 2; + int NOT_STARTED = 3; + } + + public static final int DEFAULT_UPLOAD_BUFFER_SIZE = 8192; + + private final AtomicInteger /*SinkState*/ mSinkState = new AtomicInteger(SinkState.NOT_STARTED); + private final Executor mUserUploadExecutor; + private final Executor mExecutor; + private final UploadDataProvider mUploadProvider; + private ByteBuffer mBuffer; + /** This holds the total bytes to send (the content-length). -1 if unknown. */ + private long mTotalBytes; + /** This holds the bytes written so far */ + private long mWrittenBytes; + + public JavaUploadDataSinkBase( + final Executor userExecutor, Executor executor, UploadDataProvider provider) { + mUserUploadExecutor = new Executor() { + @Override + public void execute(Runnable runnable) { + try { + userExecutor.execute(runnable); + } catch (RejectedExecutionException e) { + processUploadError(e); + } + } + }; + mExecutor = executor; + mUploadProvider = provider; + } + + @Override + public void onReadSucceeded(final boolean finalChunk) { + if (!mSinkState.compareAndSet(/* expected= */ SinkState.AWAITING_READ_RESULT, + /* updated= */ SinkState.UPLOADING)) { + throw new IllegalStateException( + "onReadSucceeded() called when not awaiting a read result; in state: " + + mSinkState.get()); + } + mExecutor.execute(getErrorSettingRunnable(new JavaUrlRequestUtils.CheckedRunnable() { + @Override + public void run() throws Exception { + mBuffer.flip(); + if (mTotalBytes != -1 && mTotalBytes - mWrittenBytes < mBuffer.remaining()) { + processUploadError( + new IllegalArgumentException(String.format(Locale.getDefault(), + "Read upload data length %d exceeds expected length %d", + mWrittenBytes + mBuffer.remaining(), mTotalBytes))); + return; + } + + mWrittenBytes += processSuccessfulRead(mBuffer); + + if (mWrittenBytes < mTotalBytes || (mTotalBytes == -1 && !finalChunk)) { + mBuffer.clear(); + mSinkState.set(SinkState.AWAITING_READ_RESULT); + executeOnUploadExecutor(new JavaUrlRequestUtils.CheckedRunnable() { + @Override + public void run() throws Exception { + mUploadProvider.read(JavaUploadDataSinkBase.this, mBuffer); + } + }); + } else if (mTotalBytes == -1) { + finish(); + } else if (mTotalBytes == mWrittenBytes) { + finish(); + } else { + processUploadError( + new IllegalArgumentException(String.format(Locale.getDefault(), + "Read upload data length %d exceeds expected length %d", + mWrittenBytes, mTotalBytes))); + } + } + })); + } + + @Override + public void onRewindSucceeded() { + if (!mSinkState.compareAndSet(/* expected= */ SinkState.AWAITING_REWIND_RESULT, + /* updated= */ SinkState.UPLOADING)) { + throw new IllegalStateException( + "onRewindSucceeded() called when not awaiting a rewind; in state: " + + mSinkState.get()); + } + startRead(); + } + + @Override + public void onReadError(Exception exception) { + processUploadError(exception); + } + + @Override + public void onRewindError(Exception exception) { + processUploadError(exception); + } + + private void startRead() { + mExecutor.execute(getErrorSettingRunnable(new JavaUrlRequestUtils.CheckedRunnable() { + @Override + public void run() throws Exception { + initializeRead(); + mSinkState.set(SinkState.AWAITING_READ_RESULT); + executeOnUploadExecutor(new JavaUrlRequestUtils.CheckedRunnable() { + @Override + public void run() throws Exception { + mUploadProvider.read(JavaUploadDataSinkBase.this, mBuffer); + } + }); + } + })); + } + + /** + * Helper method to execute a checked runnable on the upload executor and process any errors + * that occur as upload errors. + * + * @param runnable the runnable to attempt to run and check for errors + */ + private void executeOnUploadExecutor(JavaUrlRequestUtils.CheckedRunnable runnable) { + try { + mUserUploadExecutor.execute(getUploadErrorSettingRunnable(runnable)); + } catch (RejectedExecutionException e) { + processUploadError(e); + } + } + + /** + * Starts the upload. This method can be called multiple times. If it is not the first time it + * is called the {@link UploadDataProvider} must rewind. + * + * @param firstTime true if this is the first time this {@link UploadDataSink} has started an + * upload + */ + public void start(final boolean firstTime) { + executeOnUploadExecutor(new JavaUrlRequestUtils.CheckedRunnable() { + @Override + public void run() throws Exception { + mTotalBytes = mUploadProvider.getLength(); + if (mTotalBytes == 0) { + finish(); + } else { + // If we know how much data we have to upload, and it's small, we can save + // memory by allocating a reasonably sized buffer to read into. + if (mTotalBytes > 0 && mTotalBytes < DEFAULT_UPLOAD_BUFFER_SIZE) { + // Allocate one byte more than necessary, to detect callers uploading + // more bytes than they specified in length. + mBuffer = ByteBuffer.allocateDirect((int) mTotalBytes + 1); + } else { + mBuffer = ByteBuffer.allocateDirect(DEFAULT_UPLOAD_BUFFER_SIZE); + } + + initializeStart(mTotalBytes); + + if (firstTime) { + startRead(); + } else { + mSinkState.set(SinkState.AWAITING_REWIND_RESULT); + mUploadProvider.rewind(JavaUploadDataSinkBase.this); + } + } + } + }); + } + + /** + * Gets a runnable that checks for errors and processes them by setting an error state when + * executing a {@link CheckedRunnable}. + * + * @param runnable The runnable to run. + * @return a runnable that checks for errors + */ + protected abstract Runnable getErrorSettingRunnable( + JavaUrlRequestUtils.CheckedRunnable runnable); + + /** + * Gets a runnable that checks for errors and processes them by setting an upload error state + * when executing a {@link CheckedRunnable}. + * + * @param runnable The runnable to run. + * @return a runnable that checks for errors + */ + protected abstract Runnable getUploadErrorSettingRunnable( + JavaUrlRequestUtils.CheckedRunnable runnable); + + /** + * Processes an error encountered while uploading data. + * + * @param error the {@link Throwable} to process + */ + protected abstract void processUploadError(final Throwable error); + + /** + * Called when a successful read has occurred and there is new data in the {@code mBuffer} to + * process. + * + * @return the number of bytes processed in this read + * @throws IOException + */ + protected abstract int processSuccessfulRead(ByteBuffer buffer) throws IOException; + + /** + * Finishes this upload. Called when the upload is complete. + * + * @throws IOException + */ + protected abstract void finish() throws IOException; + + /** + * Initializes the {@link UploadDataSink} before each call to {@code read} in the + * {@link UploadDataProvider}. + * + * @throws IOException + */ + protected abstract void initializeRead() throws IOException; + + /** + * Initializes the {@link UploadDataSink} at the start of the upload. + * + * @param totalBytes the total number of bytes to be retrieved in this upload + */ + protected abstract void initializeStart(long totalBytes); +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/JavaUrlRequest.java b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaUrlRequest.java new file mode 100644 index 0000000000..34f5462c08 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaUrlRequest.java @@ -0,0 +1,901 @@ +// 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.impl; + +import android.annotation.TargetApi; +import android.net.TrafficStats; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.Nullable; + +import org.chromium.net.CronetException; +import org.chromium.net.InlineExecutionProhibitedException; +import org.chromium.net.ThreadStatsUid; +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UrlResponseInfo; +import org.chromium.net.impl.JavaUrlRequestUtils.CheckedRunnable; +import org.chromium.net.impl.JavaUrlRequestUtils.DirectPreventingExecutor; +import org.chromium.net.impl.JavaUrlRequestUtils.State; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.concurrent.GuardedBy; + +/** + * Pure java UrlRequest, backed by {@link HttpURLConnection}. + */ +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) // TrafficStats only available on ICS +final class JavaUrlRequest extends UrlRequestBase { + private static final String X_ANDROID = "X-Android"; + private static final String X_ANDROID_SELECTED_TRANSPORT = "X-Android-Selected-Transport"; + private static final String TAG = JavaUrlRequest.class.getSimpleName(); + private static final int DEFAULT_CHUNK_LENGTH = + JavaUploadDataSinkBase.DEFAULT_UPLOAD_BUFFER_SIZE; + private static final String USER_AGENT = "User-Agent"; + private final AsyncUrlRequestCallback mCallbackAsync; + private final Executor mExecutor; + private final String mUserAgent; + private final Map mRequestHeaders = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + private final List mUrlChain = new ArrayList<>(); + /** + * This is the source of thread safety in this class - no other synchronization is performed. + * By compare-and-swapping from one state to another, we guarantee that operations aren't + * running concurrently. Only the winner of a CAS proceeds. + * + *

A caller can lose a CAS for three reasons - user error (two calls to read() without + * waiting for the read to succeed), runtime error (network code or user code throws an + * exception), or cancellation. + */ + private final AtomicInteger /* State */ mState = new AtomicInteger(State.NOT_STARTED); + private final AtomicBoolean mUploadProviderClosed = new AtomicBoolean(false); + + private final boolean mAllowDirectExecutor; + + /* These don't change with redirects */ + private String mInitialMethod; + private VersionSafeCallbacks.UploadDataProviderWrapper mUploadDataProvider; + private Executor mUploadExecutor; + + /** + * Holds a subset of StatusValues - {@link State#STARTED} can represent + * {@link Status#SENDING_REQUEST} or {@link Status#WAITING_FOR_RESPONSE}. While the distinction + * isn't needed to implement the logic in this class, it is needed to implement + * {@link #getStatus(StatusListener)}. + * + *

Concurrency notes - this value is not atomically updated with mState, so there is some + * risk that we'd get an inconsistent snapshot of both - however, it also happens that this + * value is only used with the STARTED state, so it's inconsequential. + */ + @StatusValues + private volatile int mAdditionalStatusDetails = Status.INVALID; + + /* These change with redirects. */ + private String mCurrentUrl; + @Nullable + private ReadableByteChannel mResponseChannel; // Only accessed on mExecutor. + private UrlResponseInfoImpl mUrlResponseInfo; + private String mPendingRedirectUrl; + private HttpURLConnection mCurrentUrlConnection; // Only accessed on mExecutor. + private OutputStreamDataSink mOutputStreamDataSink; // Only accessed on mExecutor. + + // Executor that runs one task at a time on an underlying Executor. + // NOTE: Do not use to wrap user supplied Executor as lock is held while underlying execute() + // is called. + private static final class SerializingExecutor implements Executor { + private final Executor mUnderlyingExecutor; + private final Runnable mRunTasks = new Runnable() { + @Override + public void run() { + Runnable task; + synchronized (mTaskQueue) { + if (mRunning) { + return; + } + task = mTaskQueue.pollFirst(); + mRunning = task != null; + } + while (task != null) { + boolean threw = true; + try { + task.run(); + threw = false; + } finally { + synchronized (mTaskQueue) { + if (threw) { + // If task.run() threw, this method will abort without looping + // again, so repost to keep running tasks. + mRunning = false; + try { + mUnderlyingExecutor.execute(mRunTasks); + } catch (RejectedExecutionException e) { + // Give up if a task run at shutdown throws. + } + } else { + task = mTaskQueue.pollFirst(); + mRunning = task != null; + } + } + } + } + } + }; + // Queue of tasks to run. Tasks are added to the end and taken from the front. + // Synchronized on itself. + @GuardedBy("mTaskQueue") + private final ArrayDeque mTaskQueue = new ArrayDeque<>(); + // Indicates if mRunTasks is actively running tasks. Synchronized on mTaskQueue. + @GuardedBy("mTaskQueue") + private boolean mRunning; + + SerializingExecutor(Executor underlyingExecutor) { + mUnderlyingExecutor = underlyingExecutor; + } + + @Override + public void execute(Runnable command) { + synchronized (mTaskQueue) { + mTaskQueue.addLast(command); + try { + mUnderlyingExecutor.execute(mRunTasks); + } catch (RejectedExecutionException e) { + // If shutting down, do not add new tasks to the queue. + mTaskQueue.removeLast(); + } + } +}; + } + + /** + * @param executor The executor used for reading and writing from sockets + * @param userExecutor The executor used to dispatch to {@code callback} + */ + JavaUrlRequest(Callback callback, final Executor executor, Executor userExecutor, String url, + String userAgent, boolean allowDirectExecutor, boolean trafficStatsTagSet, + int trafficStatsTag, final boolean trafficStatsUidSet, final int trafficStatsUid) { + if (url == null) { + throw new NullPointerException("URL is required"); + } + if (callback == null) { + throw new NullPointerException("Listener is required"); + } + if (executor == null) { + throw new NullPointerException("Executor is required"); + } + if (userExecutor == null) { + throw new NullPointerException("userExecutor is required"); + } + + this.mAllowDirectExecutor = allowDirectExecutor; + this.mCallbackAsync = new AsyncUrlRequestCallback(callback, userExecutor); + final int trafficStatsTagToUse = + trafficStatsTagSet ? trafficStatsTag : TrafficStats.getThreadStatsTag(); + this.mExecutor = new SerializingExecutor(new Executor() { + @Override + public void execute(final Runnable command) { + executor.execute(new Runnable() { + @Override + public void run() { + int oldTag = TrafficStats.getThreadStatsTag(); + TrafficStats.setThreadStatsTag(trafficStatsTagToUse); + if (trafficStatsUidSet) { + ThreadStatsUid.set(trafficStatsUid); + } + try { + command.run(); + } finally { + if (trafficStatsUidSet) { + ThreadStatsUid.clear(); + } + TrafficStats.setThreadStatsTag(oldTag); + } + } + }); + } + }); + this.mCurrentUrl = url; + this.mUserAgent = userAgent; + } + + @Override + public void setHttpMethod(String method) { + checkNotStarted(); + if (method == null) { + throw new NullPointerException("Method is required."); + } + if ("OPTIONS".equalsIgnoreCase(method) || "GET".equalsIgnoreCase(method) + || "HEAD".equalsIgnoreCase(method) || "POST".equalsIgnoreCase(method) + || "PUT".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method) + || "TRACE".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method)) { + mInitialMethod = method; + } else { + throw new IllegalArgumentException("Invalid http method " + method); + } + } + + private void checkNotStarted() { + @State + int state = mState.get(); + if (state != State.NOT_STARTED) { + throw new IllegalStateException("Request is already started. State is: " + state); + } + } + + @Override + public void addHeader(String header, String value) { + checkNotStarted(); + if (!isValidHeaderName(header) || value.contains("\r\n")) { + throw new IllegalArgumentException("Invalid header " + header + "=" + value); + } + if (mRequestHeaders.containsKey(header)) { + mRequestHeaders.remove(header); + } + mRequestHeaders.put(header, value); + } + + private boolean isValidHeaderName(String header) { + for (int i = 0; i < header.length(); i++) { + char c = header.charAt(i); + switch (c) { + case '(': + case ')': + case '<': + case '>': + case '@': + case ',': + case ';': + case ':': + case '\\': + case '\'': + case '/': + case '[': + case ']': + case '?': + case '=': + case '{': + case '}': + return false; + default: { + if (Character.isISOControl(c) || Character.isWhitespace(c)) { + return false; + } + } + } + } + return true; + } + + @Override + public void setUploadDataProvider(UploadDataProvider uploadDataProvider, Executor executor) { + if (uploadDataProvider == null) { + throw new NullPointerException("Invalid UploadDataProvider."); + } + if (!mRequestHeaders.containsKey("Content-Type")) { + throw new IllegalArgumentException( + "Requests with upload data must have a Content-Type."); + } + checkNotStarted(); + if (mInitialMethod == null) { + mInitialMethod = "POST"; + } + this.mUploadDataProvider = + new VersionSafeCallbacks.UploadDataProviderWrapper(uploadDataProvider); + if (mAllowDirectExecutor) { + this.mUploadExecutor = executor; + } else { + this.mUploadExecutor = new DirectPreventingExecutor(executor); + } + } + + private final class OutputStreamDataSink extends JavaUploadDataSinkBase { + private final HttpURLConnection mUrlConnection; + private final AtomicBoolean mOutputChannelClosed = new AtomicBoolean(false); + private WritableByteChannel mOutputChannel; + private OutputStream mUrlConnectionOutputStream; + + OutputStreamDataSink(final Executor userExecutor, Executor executor, + HttpURLConnection urlConnection, + VersionSafeCallbacks.UploadDataProviderWrapper provider) { + super(userExecutor, executor, provider); + mUrlConnection = urlConnection; + } + + @Override + protected void initializeRead() throws IOException { + if (mOutputChannel == null) { + mAdditionalStatusDetails = Status.CONNECTING; + mUrlConnection.setDoOutput(true); + mUrlConnection.connect(); + mAdditionalStatusDetails = Status.SENDING_REQUEST; + mUrlConnectionOutputStream = mUrlConnection.getOutputStream(); + mOutputChannel = Channels.newChannel(mUrlConnectionOutputStream); + } + } + + void closeOutputChannel() throws IOException { + if (mOutputChannel != null + && mOutputChannelClosed.compareAndSet( + /* expected= */ false, /* updated= */ true)) { + mOutputChannel.close(); + } + } + + @Override + protected void finish() throws IOException { + closeOutputChannel(); + fireGetHeaders(); + } + + @Override + protected void initializeStart(long totalBytes) { + if (totalBytes > 0) { + mUrlConnection.setFixedLengthStreamingMode(totalBytes); + } else { + mUrlConnection.setChunkedStreamingMode(DEFAULT_CHUNK_LENGTH); + } + } + + @Override + protected int processSuccessfulRead(ByteBuffer buffer) throws IOException { + int totalBytesProcessed = 0; + while (buffer.hasRemaining()) { + totalBytesProcessed += mOutputChannel.write(buffer); + } + // Forces a chunk to be sent, rather than buffering to the DEFAULT_CHUNK_LENGTH. + // This allows clients to trickle-upload bytes as they become available without + // introducing latency due to buffering. + mUrlConnectionOutputStream.flush(); + return totalBytesProcessed; + } + + @Override + protected Runnable getErrorSettingRunnable(CheckedRunnable runnable) { + return errorSetting(runnable); + } + + @Override + protected Runnable getUploadErrorSettingRunnable(CheckedRunnable runnable) { + return uploadErrorSetting(runnable); + } + + @Override + protected void processUploadError(Throwable exception) { + enterUploadErrorState(exception); + } + } + + @Override + public void start() { + mAdditionalStatusDetails = Status.CONNECTING; + transitionStates(State.NOT_STARTED, State.STARTED, new Runnable() { + @Override + public void run() { + mUrlChain.add(mCurrentUrl); + fireOpenConnection(); + } + }); + } + + private void enterErrorState(final CronetException error) { + if (setTerminalState(State.ERROR)) { + fireDisconnect(); + fireCloseUploadDataProvider(); + mCallbackAsync.onFailed(mUrlResponseInfo, error); + } + } + + private boolean setTerminalState(@State int error) { + while (true) { + @State + int oldState = mState.get(); + switch (oldState) { + case State.NOT_STARTED: + throw new IllegalStateException("Can't enter error state before start"); + case State.ERROR: // fallthrough + case State.COMPLETE: // fallthrough + case State.CANCELLED: + return false; // Already in a terminal state + default: { + if (mState.compareAndSet(/* expected= */ oldState, /* updated= */ error)) { + return true; + } + } + } + } + } + + /** Ends the request with an error, caused by an exception thrown from user code. */ + private void enterUserErrorState(final Throwable error) { + enterErrorState( + new CallbackExceptionImpl("Exception received from UrlRequest.Callback", error)); + } + + /** Ends the request with an error, caused by an exception thrown from user code. */ + private void enterUploadErrorState(final Throwable error) { + enterErrorState( + new CallbackExceptionImpl("Exception received from UploadDataProvider", error)); + } + + private void enterCronetErrorState(final Throwable error) { + // TODO(clm) mapping from Java exception (UnknownHostException, for example) to net error + // code goes here. + enterErrorState(new CronetExceptionImpl("System error", error)); + } + + /** + * Atomically swaps from the expected state to a new state. If the swap fails, and it's not + * due to an earlier error or cancellation, throws an exception. + * + * @param afterTransition Callback to run after transition completes successfully. + */ + private void transitionStates( + @State int expected, @State int newState, Runnable afterTransition) { + if (!mState.compareAndSet(expected, newState)) { + @State + int state = mState.get(); + if (!(state == State.CANCELLED || state == State.ERROR)) { + throw new IllegalStateException( + "Invalid state transition - expected " + expected + " but was " + state); + } + } else { + afterTransition.run(); + } + } + + @Override + public void followRedirect() { + transitionStates(State.AWAITING_FOLLOW_REDIRECT, State.STARTED, new Runnable() { + @Override + public void run() { + mCurrentUrl = mPendingRedirectUrl; + mPendingRedirectUrl = null; + fireOpenConnection(); + } + }); + } + + private void fireGetHeaders() { + mAdditionalStatusDetails = Status.WAITING_FOR_RESPONSE; + mExecutor.execute(errorSetting(new CheckedRunnable() { + @Override + public void run() throws Exception { + if (mCurrentUrlConnection == null) { + return; // We've been cancelled + } + final List> headerList = new ArrayList<>(); + String selectedTransport = "http/1.1"; + String headerKey; + for (int i = 0; (headerKey = mCurrentUrlConnection.getHeaderFieldKey(i)) != null; + i++) { + if (X_ANDROID_SELECTED_TRANSPORT.equalsIgnoreCase(headerKey)) { + selectedTransport = mCurrentUrlConnection.getHeaderField(i); + } + if (!headerKey.startsWith(X_ANDROID)) { + headerList.add(new SimpleEntry<>( + headerKey, mCurrentUrlConnection.getHeaderField(i))); + } + } + + int responseCode = mCurrentUrlConnection.getResponseCode(); + // Important to copy the list here, because although we never concurrently modify + // the list ourselves, user code might iterate over it while we're redirecting, and + // that would throw ConcurrentModificationException. + mUrlResponseInfo = new UrlResponseInfoImpl(new ArrayList<>(mUrlChain), responseCode, + mCurrentUrlConnection.getResponseMessage(), + Collections.unmodifiableList(headerList), false, selectedTransport, "", 0); + // TODO(clm) actual redirect handling? post -> get and whatnot? + if (responseCode >= 300 && responseCode < 400) { + List locationFields = mUrlResponseInfo.getAllHeaders().get("location"); + if (locationFields != null) { + fireRedirectReceived(locationFields.get(0)); + return; + } + } + fireCloseUploadDataProvider(); + if (responseCode >= 400) { + InputStream inputStream = mCurrentUrlConnection.getErrorStream(); + mResponseChannel = + inputStream == null ? null : InputStreamChannel.wrap(inputStream); + mCallbackAsync.onResponseStarted(mUrlResponseInfo); + } else { + mResponseChannel = + InputStreamChannel.wrap(mCurrentUrlConnection.getInputStream()); + mCallbackAsync.onResponseStarted(mUrlResponseInfo); + } + } + })); + } + + private void fireCloseUploadDataProvider() { + if (mUploadDataProvider != null + && mUploadProviderClosed.compareAndSet( + /* expected= */ false, /* updated= */ true)) { + try { + mUploadExecutor.execute(uploadErrorSetting(new CheckedRunnable() { + @Override + public void run() throws Exception { + mUploadDataProvider.close(); + } + })); + } catch (RejectedExecutionException e) { + Log.e(TAG, "Exception when closing uploadDataProvider", e); + } + } + } + + private void fireRedirectReceived(final String locationField) { + transitionStates(State.STARTED, State.REDIRECT_RECEIVED, new Runnable() { + @Override + public void run() { + mPendingRedirectUrl = URI.create(mCurrentUrl).resolve(locationField).toString(); + mUrlChain.add(mPendingRedirectUrl); + transitionStates( + State.REDIRECT_RECEIVED, State.AWAITING_FOLLOW_REDIRECT, new Runnable() { + @Override + public void run() { + mCallbackAsync.onRedirectReceived( + mUrlResponseInfo, mPendingRedirectUrl); + } + }); + } + }); + } + + private void fireOpenConnection() { + mExecutor.execute(errorSetting(new CheckedRunnable() { + @Override + public void run() throws Exception { + // If we're cancelled, then our old connection will be disconnected for us and + // we shouldn't open a new one. + if (mState.get() == State.CANCELLED) { + return; + } + + final URL url = new URL(mCurrentUrl); + if (mCurrentUrlConnection != null) { + mCurrentUrlConnection.disconnect(); + mCurrentUrlConnection = null; + } + mCurrentUrlConnection = (HttpURLConnection) url.openConnection(); + mCurrentUrlConnection.setInstanceFollowRedirects(false); + if (!mRequestHeaders.containsKey(USER_AGENT)) { + mRequestHeaders.put(USER_AGENT, mUserAgent); + } + for (Map.Entry entry : mRequestHeaders.entrySet()) { + mCurrentUrlConnection.setRequestProperty(entry.getKey(), entry.getValue()); + } + if (mInitialMethod == null) { + mInitialMethod = "GET"; + } + mCurrentUrlConnection.setRequestMethod(mInitialMethod); + if (mUploadDataProvider != null) { + mOutputStreamDataSink = new OutputStreamDataSink( + mUploadExecutor, mExecutor, mCurrentUrlConnection, mUploadDataProvider); + mOutputStreamDataSink.start(mUrlChain.size() == 1); + } else { + mAdditionalStatusDetails = Status.CONNECTING; + mCurrentUrlConnection.connect(); + fireGetHeaders(); + } + } + })); + } + + private Runnable errorSetting(final CheckedRunnable delegate) { + return new Runnable() { + @Override + public void run() { + try { + delegate.run(); + } catch (Throwable t) { + enterCronetErrorState(t); + } + } + }; + } + + private Runnable userErrorSetting(final CheckedRunnable delegate) { + return new Runnable() { + @Override + public void run() { + try { + delegate.run(); + } catch (Throwable t) { + enterUserErrorState(t); + } + } + }; + } + + private Runnable uploadErrorSetting(final CheckedRunnable delegate) { + return new Runnable() { + @Override + public void run() { + try { + delegate.run(); + } catch (Throwable t) { + enterUploadErrorState(t); + } + } + }; + } + + @Override + public void read(final ByteBuffer buffer) { + Preconditions.checkDirect(buffer); + Preconditions.checkHasRemaining(buffer); + transitionStates(State.AWAITING_READ, State.READING, new Runnable() { + @Override + public void run() { + mExecutor.execute(errorSetting(new CheckedRunnable() { + @Override + public void run() throws Exception { + int read = mResponseChannel == null ? -1 : mResponseChannel.read(buffer); + processReadResult(read, buffer); + } + })); + } + }); + } + + private void processReadResult(int read, final ByteBuffer buffer) throws IOException { + if (read != -1) { + mCallbackAsync.onReadCompleted(mUrlResponseInfo, buffer); + } else { + if (mResponseChannel != null) { + mResponseChannel.close(); + } + if (mState.compareAndSet( + /* expected= */ State.READING, /* updated= */ State.COMPLETE)) { + fireDisconnect(); + mCallbackAsync.onSucceeded(mUrlResponseInfo); + } + } + } + + private void fireDisconnect() { + mExecutor.execute(new Runnable() { + @Override + public void run() { + if (mOutputStreamDataSink != null) { + try { + mOutputStreamDataSink.closeOutputChannel(); + } catch (IOException e) { + Log.e(TAG, "Exception when closing OutputChannel", e); + } + } + if (mCurrentUrlConnection != null) { + mCurrentUrlConnection.disconnect(); + mCurrentUrlConnection = null; + } + } + }); + } + + @Override + public void cancel() { + @State + int oldState = mState.getAndSet(State.CANCELLED); + switch (oldState) { + // We've just scheduled some user code to run. When they perform their next operation, + // they'll observe it and fail. However, if user code is cancelling in response to one + // of these callbacks, we'll never actually cancel! + // TODO(clm) figure out if it's possible to avoid concurrency in user callbacks. + case State.REDIRECT_RECEIVED: + case State.AWAITING_FOLLOW_REDIRECT: + case State.AWAITING_READ: + + // User code is waiting on us - cancel away! + case State.STARTED: + case State.READING: + fireDisconnect(); + fireCloseUploadDataProvider(); + mCallbackAsync.onCanceled(mUrlResponseInfo); + break; + // The rest are all termination cases - we're too late to cancel. + case State.ERROR: + case State.COMPLETE: + case State.CANCELLED: + break; + default: + break; + } + } + + @Override + public boolean isDone() { + @State + int state = mState.get(); + return state == State.COMPLETE || state == State.ERROR || state == State.CANCELLED; + } + + @Override + public void getStatus(StatusListener listener) { + @State + int state = mState.get(); + int extraStatus = this.mAdditionalStatusDetails; + + @StatusValues + final int status; + switch (state) { + case State.ERROR: + case State.COMPLETE: + case State.CANCELLED: + case State.NOT_STARTED: + status = Status.INVALID; + break; + case State.STARTED: + status = extraStatus; + break; + case State.REDIRECT_RECEIVED: + case State.AWAITING_FOLLOW_REDIRECT: + case State.AWAITING_READ: + status = Status.IDLE; + break; + case State.READING: + status = Status.READING_RESPONSE; + break; + default: + throw new IllegalStateException("Switch is exhaustive: " + state); + } + + mCallbackAsync.sendStatus( + new VersionSafeCallbacks.UrlRequestStatusListener(listener), status); + } + + /** This wrapper ensures that callbacks are always called on the correct executor */ + private final class AsyncUrlRequestCallback { + final VersionSafeCallbacks.UrlRequestCallback mCallback; + final Executor mUserExecutor; + final Executor mFallbackExecutor; + + AsyncUrlRequestCallback(Callback callback, final Executor userExecutor) { + this.mCallback = new VersionSafeCallbacks.UrlRequestCallback(callback); + if (mAllowDirectExecutor) { + this.mUserExecutor = userExecutor; + this.mFallbackExecutor = null; + } else { + mUserExecutor = new DirectPreventingExecutor(userExecutor); + mFallbackExecutor = userExecutor; + } + } + + void sendStatus( + final VersionSafeCallbacks.UrlRequestStatusListener listener, final int status) { + mUserExecutor.execute(new Runnable() { + @Override + public void run() { + listener.onStatus(status); + } + }); + } + + void execute(CheckedRunnable runnable) { + try { + mUserExecutor.execute(userErrorSetting(runnable)); + } catch (RejectedExecutionException e) { + enterErrorState(new CronetExceptionImpl("Exception posting task to executor", e)); + } + } + + void onRedirectReceived(final UrlResponseInfo info, final String newLocationUrl) { + execute(new CheckedRunnable() { + @Override + public void run() throws Exception { + mCallback.onRedirectReceived(JavaUrlRequest.this, info, newLocationUrl); + } + }); + } + + void onResponseStarted(UrlResponseInfo info) { + execute(new CheckedRunnable() { + @Override + public void run() throws Exception { + if (mState.compareAndSet(/* expected= */ State.STARTED, + /* updated= */ State.AWAITING_READ)) { + mCallback.onResponseStarted(JavaUrlRequest.this, mUrlResponseInfo); + } + } + }); + } + + void onReadCompleted(final UrlResponseInfo info, final ByteBuffer byteBuffer) { + execute(new CheckedRunnable() { + @Override + public void run() throws Exception { + if (mState.compareAndSet(/* expected= */ State.READING, + /* updated= */ State.AWAITING_READ)) { + mCallback.onReadCompleted(JavaUrlRequest.this, info, byteBuffer); + } + } + }); + } + + void onCanceled(final UrlResponseInfo info) { + closeResponseChannel(); + mUserExecutor.execute(new Runnable() { + @Override + public void run() { + try { + mCallback.onCanceled(JavaUrlRequest.this, info); + } catch (Exception exception) { + Log.e(TAG, "Exception in onCanceled method", exception); + } + } + }); + } + + void onSucceeded(final UrlResponseInfo info) { + mUserExecutor.execute(new Runnable() { + @Override + public void run() { + try { + mCallback.onSucceeded(JavaUrlRequest.this, info); + } catch (Exception exception) { + Log.e(TAG, "Exception in onSucceeded method", exception); + } + } + }); + } + + void onFailed(final UrlResponseInfo urlResponseInfo, final CronetException e) { + closeResponseChannel(); + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + mCallback.onFailed(JavaUrlRequest.this, urlResponseInfo, e); + } catch (Exception exception) { + Log.e(TAG, "Exception in onFailed method", exception); + } + } + }; + try { + mUserExecutor.execute(runnable); + } catch (InlineExecutionProhibitedException wasDirect) { + if (mFallbackExecutor != null) { + mFallbackExecutor.execute(runnable); + } + } + } + } + + private void closeResponseChannel() { + mExecutor.execute(new Runnable() { + @Override + public void run() { + if (mResponseChannel != null) { + try { + mResponseChannel.close(); + } catch (IOException e) { + e.printStackTrace(); + } + mResponseChannel = null; + } + } + }); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/JavaUrlRequestUtils.java b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaUrlRequestUtils.java new file mode 100644 index 0000000000..92a4a7e011 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/JavaUrlRequestUtils.java @@ -0,0 +1,121 @@ +// Copyright 2019 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.impl; + +import androidx.annotation.IntDef; + +import org.chromium.net.InlineExecutionProhibitedException; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; + +/** + * Utilities for Java-based UrlRequest implementations. + * {@hide} + */ +public final class JavaUrlRequestUtils { + /** + * State interface for keeping track of the internal state of a {@link UrlRequest}. + * + * /- AWAITING_FOLLOW_REDIRECT <- REDIRECT_RECEIVED <-\ /- READING <--\ + * | | | | + * V / V / + * NOT_STARTED -> STARTED -----------------------------------------------> AWAITING_READ ------- + * --> COMPLETE + * + * + */ + @IntDef({State.NOT_STARTED, State.STARTED, State.REDIRECT_RECEIVED, + State.AWAITING_FOLLOW_REDIRECT, State.AWAITING_READ, State.READING, State.ERROR, + State.COMPLETE, State.CANCELLED}) + @Retention(RetentionPolicy.SOURCE) + public @interface State { + int NOT_STARTED = 0; + int STARTED = 1; + int REDIRECT_RECEIVED = 2; + int AWAITING_FOLLOW_REDIRECT = 3; + int AWAITING_READ = 4; + int READING = 5; + int ERROR = 6; + int COMPLETE = 7; + int CANCELLED = 8; + } + + /** + * Interface used to run commands that could throw an exception. Specifically useful for + * calling {@link UrlRequest.Callback}s on a user-supplied executor. + */ + public interface CheckedRunnable { void run() throws Exception; } + + /** + * Executor that detects and throws if its mDelegate runs a submitted runnable inline. + */ + public static final class DirectPreventingExecutor implements Executor { + private final Executor mDelegate; + + /** + * Constructs an {@link DirectPreventingExecutor} that executes {@link runnable}s on the + * provided {@link Executor}. + * + * @param delegate the {@link Executor} used to run {@link Runnable}s + */ + public DirectPreventingExecutor(Executor delegate) { + this.mDelegate = delegate; + } + + /** + * Executes a {@link Runnable} on this {@link Executor} and throws an exception if it is + * being run on the same thread as the calling thread. + * + * @param command the {@link Runnable} to attempt to run + */ + @Override + public void execute(Runnable command) { + Thread currentThread = Thread.currentThread(); + InlineCheckingRunnable runnable = new InlineCheckingRunnable(command, currentThread); + mDelegate.execute(runnable); + // This next read doesn't require synchronization; only the current thread could have + // written to runnable.mExecutedInline. + if (runnable.mExecutedInline != null) { + throw runnable.mExecutedInline; + } else { + // It's possible that this method is being called on an executor, and the runnable + // that was just queued will run on this thread after the current runnable returns. + // By nulling out the mCallingThread field, the InlineCheckingRunnable's current + // thread comparison will not fire. + // + // Java reference assignment is always atomic (no tearing, even on 64-bit VMs, see + // JLS 17.7), but other threads aren't guaranteed to ever see updates without + // something like locking, volatile, or AtomicReferences. We're ok in + // this instance, since this write only needs to be seen in the case that + // InlineCheckingRunnable.run() runs on the same thread as this execute() method. + runnable.mCallingThread = null; + } + } + + private static final class InlineCheckingRunnable implements Runnable { + private final Runnable mCommand; + private Thread mCallingThread; + private InlineExecutionProhibitedException mExecutedInline; + + private InlineCheckingRunnable(Runnable command, Thread callingThread) { + this.mCommand = command; + this.mCallingThread = callingThread; + } + + @Override + public void run() { + if (Thread.currentThread() == mCallingThread) { + // Can't throw directly from here, since the delegate executor could catch this + // exception. + mExecutedInline = new InlineExecutionProhibitedException(); + return; + } + mCommand.run(); + } + } + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/LoadState.template b/src/components/cronet/android/java/src/org/chromium/net/impl/LoadState.template new file mode 100644 index 0000000000..0224d35755 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/LoadState.template @@ -0,0 +1,13 @@ +// 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.impl; + +// A simple auto-generated interface used to list load states as used by +// org.chromium.net.RequestStatus. +public interface LoadState { +#define LOAD_STATE(x, y) public static final int x = y; +#include "net/base/load_states_list.h" +#undef LOAD_STATE +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java new file mode 100644 index 0000000000..696d7c8455 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java @@ -0,0 +1,40 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net.impl; + +import android.content.Context; + +import org.chromium.net.ExperimentalCronetEngine; +import org.chromium.net.ICronetEngineBuilder; + +/** + * Implementation of {@link ICronetEngineBuilder} that builds native Cronet engine. + */ +public class NativeCronetEngineBuilderImpl extends CronetEngineBuilderImpl { + /** + * Builder for Native Cronet Engine. + * Default config enables SPDY, disables QUIC and HTTP cache. + * + * @param context Android {@link Context} for engine to use. + */ + public NativeCronetEngineBuilderImpl(Context context) { + super(context); + } + + @Override + public ExperimentalCronetEngine build() { + if (getUserAgent() == null) { + setUserAgent(getDefaultUserAgent()); + } + + ExperimentalCronetEngine builder = new CronetUrlRequestContext(this); + + // Clear MOCK_CERT_VERIFIER reference if there is any, since + // the ownership has been transferred to the engine. + mMockCertVerifier = 0; + + return builder; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/NativeCronetEngineBuilderWithLibraryLoaderImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/NativeCronetEngineBuilderWithLibraryLoaderImpl.java new file mode 100644 index 0000000000..6cedfe7028 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/NativeCronetEngineBuilderWithLibraryLoaderImpl.java @@ -0,0 +1,39 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net.impl; + +import android.content.Context; + +import org.chromium.net.CronetEngine.Builder.LibraryLoader; +import org.chromium.net.ICronetEngineBuilder; + +/** + * An extension of {@link NativeCronetEngineBuilderImpl} that implements + * {@link ICronetEngineBuilder#setLibraryLoader}. + */ +public class NativeCronetEngineBuilderWithLibraryLoaderImpl extends NativeCronetEngineBuilderImpl { + private VersionSafeCallbacks.LibraryLoader mLibraryLoader; + + /** + * Constructs a builder for Native Cronet Engine. + * Default config enables SPDY, disables QUIC and HTTP cache. + * + * @param context Android {@link Context} for engine to use. + */ + public NativeCronetEngineBuilderWithLibraryLoaderImpl(Context context) { + super(context); + } + + @Override + public CronetEngineBuilderImpl setLibraryLoader(LibraryLoader loader) { + mLibraryLoader = new VersionSafeCallbacks.LibraryLoader(loader); + return this; + } + + @Override + VersionSafeCallbacks.LibraryLoader libraryLoader() { + return mLibraryLoader; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/NativeCronetProvider.java b/src/components/cronet/android/java/src/org/chromium/net/impl/NativeCronetProvider.java new file mode 100644 index 0000000000..7e858bb94c --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/NativeCronetProvider.java @@ -0,0 +1,64 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net.impl; + +import android.content.Context; + +import org.chromium.base.annotations.UsedByReflection; +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetProvider; +import org.chromium.net.ExperimentalCronetEngine; +import org.chromium.net.ICronetEngineBuilder; + +import java.util.Arrays; + +/** + * Implementation of {@link CronetProvider} that creates {@link CronetEngine.Builder} + * for building the native implementation of {@link CronetEngine}. + */ +public class NativeCronetProvider extends CronetProvider { + /** + * Constructor. + * + * @param context Android context to use. + */ + @UsedByReflection("CronetProvider.java") + public NativeCronetProvider(Context context) { + super(context); + } + + @Override + public CronetEngine.Builder createBuilder() { + ICronetEngineBuilder impl = new NativeCronetEngineBuilderWithLibraryLoaderImpl(mContext); + return new ExperimentalCronetEngine.Builder(impl); + } + + @Override + public String getName() { + return CronetProvider.PROVIDER_NAME_APP_PACKAGED; + } + + @Override + public String getVersion() { + return ImplVersion.getCronetVersion(); + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] {NativeCronetProvider.class, mContext}); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof NativeCronetProvider + && this.mContext.equals(((NativeCronetProvider) other).mContext)); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/NetworkExceptionImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/NetworkExceptionImpl.java new file mode 100644 index 0000000000..f32b59d5a5 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/NetworkExceptionImpl.java @@ -0,0 +1,74 @@ +// Copyright 2016 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.impl; + +import org.chromium.net.NetworkException; + +/** + * Implements {@link NetworkException}. + */ +public class NetworkExceptionImpl extends NetworkException { + // Error code, one of ERROR_* + protected final int mErrorCode; + // Cronet internal error code. + protected final int mCronetInternalErrorCode; + + /** + * Constructs an exception with a specific error. + * + * @param message explanation of failure. + * @param errorCode error code, one of {@link #ERROR_HOSTNAME_NOT_RESOLVED ERROR_*}. + * @param cronetInternalErrorCode Cronet internal error code, one of + * + * these. + */ + public NetworkExceptionImpl(String message, int errorCode, int cronetInternalErrorCode) { + super(message, null); + assert errorCode > 0 && errorCode < 12; + assert cronetInternalErrorCode < 0; + mErrorCode = errorCode; + mCronetInternalErrorCode = cronetInternalErrorCode; + } + + @Override + public int getErrorCode() { + return mErrorCode; + } + + @Override + public int getCronetInternalErrorCode() { + return mCronetInternalErrorCode; + } + + @Override + public boolean immediatelyRetryable() { + switch (mErrorCode) { + case ERROR_HOSTNAME_NOT_RESOLVED: + case ERROR_INTERNET_DISCONNECTED: + case ERROR_CONNECTION_REFUSED: + case ERROR_ADDRESS_UNREACHABLE: + case ERROR_OTHER: + default: + return false; + case ERROR_NETWORK_CHANGED: + case ERROR_TIMED_OUT: + case ERROR_CONNECTION_CLOSED: + case ERROR_CONNECTION_TIMED_OUT: + case ERROR_CONNECTION_RESET: + return true; + } + } + + @Override + public String getMessage() { + StringBuilder b = new StringBuilder(super.getMessage()); + b.append(", ErrorCode=").append(mErrorCode); + if (mCronetInternalErrorCode != 0) { + b.append(", InternalErrorCode=").append(mCronetInternalErrorCode); + } + b.append(", Retryable=").append(immediatelyRetryable()); + return b.toString(); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/Preconditions.java b/src/components/cronet/android/java/src/org/chromium/net/impl/Preconditions.java new file mode 100644 index 0000000000..4910f22f5a --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/Preconditions.java @@ -0,0 +1,26 @@ +// 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.impl; + +import java.nio.ByteBuffer; + +/** + * Utility class to check preconditions. + */ +public final class Preconditions { + private Preconditions() {} + + public static void checkDirect(ByteBuffer buffer) { + if (!buffer.isDirect()) { + throw new IllegalArgumentException("byteBuffer must be a direct ByteBuffer."); + } + } + + public static void checkHasRemaining(ByteBuffer buffer) { + if (!buffer.hasRemaining()) { + throw new IllegalArgumentException("ByteBuffer is already full."); + } + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/QuicExceptionImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/QuicExceptionImpl.java new file mode 100644 index 0000000000..98a0798524 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/QuicExceptionImpl.java @@ -0,0 +1,61 @@ +// Copyright 2016 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.impl; + +import org.chromium.net.QuicException; + +/** + * Implements {@link QuicException}. + */ +public class QuicExceptionImpl extends QuicException { + private final int mQuicDetailedErrorCode; + private final NetworkExceptionImpl mNetworkException; + + /** + * Constructs an exception with a specific error. + * + * @param message explanation of failure. + * @param netErrorCode Error code from + * + * this list. + * @param quicDetailedErrorCode Detailed QUIC error + * code from + * QuicErrorCode. + */ + public QuicExceptionImpl( + String message, int errorCode, int netErrorCode, int quicDetailedErrorCode) { + super(message, null); + mNetworkException = new NetworkExceptionImpl(message, errorCode, netErrorCode); + mQuicDetailedErrorCode = quicDetailedErrorCode; + } + + @Override + public String getMessage() { + StringBuilder b = new StringBuilder(mNetworkException.getMessage()); + b.append(", QuicDetailedErrorCode=").append(mQuicDetailedErrorCode); + return b.toString(); + } + + @Override + public int getErrorCode() { + return mNetworkException.getErrorCode(); + } + + @Override + public int getCronetInternalErrorCode() { + return mNetworkException.getCronetInternalErrorCode(); + } + + @Override + public boolean immediatelyRetryable() { + return mNetworkException.immediatelyRetryable(); + } + + @Override + public int getQuicDetailedErrorCode() { + return mQuicDetailedErrorCode; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/RequestFinishedInfoImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/RequestFinishedInfoImpl.java new file mode 100644 index 0000000000..051279d220 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/RequestFinishedInfoImpl.java @@ -0,0 +1,85 @@ +// Copyright 2016 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.impl; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; + +import org.chromium.net.CronetException; +import org.chromium.net.RequestFinishedInfo; +import org.chromium.net.UrlResponseInfo; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collection; +import java.util.Collections; + +/** + * Implements information about a finished request. Passed to {@link RequestFinishedInfo.Listener}. + */ +public class RequestFinishedInfoImpl extends RequestFinishedInfo { + private final String mUrl; + private final Collection mAnnotations; + private final RequestFinishedInfo.Metrics mMetrics; + + @FinishedReason + private final int mFinishedReason; + + @Nullable + private final UrlResponseInfo mResponseInfo; + @Nullable + private final CronetException mException; + + @IntDef({SUCCEEDED, FAILED, CANCELED}) + @Retention(RetentionPolicy.SOURCE) + public @interface FinishedReason {} + + public RequestFinishedInfoImpl(String url, Collection annotations, + RequestFinishedInfo.Metrics metrics, @FinishedReason int finishedReason, + @Nullable UrlResponseInfo responseInfo, @Nullable CronetException exception) { + mUrl = url; + mAnnotations = annotations; + mMetrics = metrics; + mFinishedReason = finishedReason; + mResponseInfo = responseInfo; + mException = exception; + } + + @Override + public String getUrl() { + return mUrl; + } + + @Override + public Collection getAnnotations() { + if (mAnnotations == null) { + return Collections.emptyList(); + } + return mAnnotations; + } + + @Override + public Metrics getMetrics() { + return mMetrics; + } + + @Override + @FinishedReason + public int getFinishedReason() { + return mFinishedReason; + } + + @Override + @Nullable + public UrlResponseInfo getResponseInfo() { + return mResponseInfo; + } + + @Override + @Nullable + public CronetException getException() { + return mException; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/UrlRequestBase.java b/src/components/cronet/android/java/src/org/chromium/net/impl/UrlRequestBase.java new file mode 100644 index 0000000000..120f5b3253 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/UrlRequestBase.java @@ -0,0 +1,128 @@ +// Copyright 2016 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.impl; + +import androidx.annotation.IntDef; + +import org.chromium.net.ExperimentalUrlRequest; +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UrlRequest; +import org.chromium.net.UrlRequest.Status; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; + +/** + * Base class for classes that implement {@link UrlRequest} including experimental + * features. {@link CronetUrlRequest} and {@link JavaUrlRequest} extends this class. + */ +public abstract class UrlRequestBase extends ExperimentalUrlRequest { + /** + * Sets the HTTP method verb to use for this request. Must be done before + * request has started. + * + *

The default when this method is not called is "GET" if the request has + * no body or "POST" if it does. + * + * @param method "GET", "HEAD", "DELETE", "POST" or "PUT". + */ + protected abstract void setHttpMethod(String method); + + /** + * Adds a request header. Must be done before request has started. + * + * @param header header name. + * @param value header value. + */ + protected abstract void addHeader(String header, String value); + + /** + * Sets upload data provider. Must be done before request has started. May only be + * invoked once per request. Switches method to "POST" if not explicitly + * set. Starting the request will throw an exception if a Content-Type + * header is not set. + * + * @param uploadDataProvider responsible for providing the upload data. + * @param executor All {@code uploadDataProvider} methods will be invoked + * using this {@code Executor}. May optionally be the same + * {@code Executor} the request itself is using. + */ + protected abstract void setUploadDataProvider( + UploadDataProvider uploadDataProvider, Executor executor); + + /** + * Possible URL Request statuses. + */ + @IntDef({Status.INVALID, Status.IDLE, Status.WAITING_FOR_STALLED_SOCKET_POOL, + Status.WAITING_FOR_AVAILABLE_SOCKET, Status.WAITING_FOR_DELEGATE, + Status.WAITING_FOR_CACHE, Status.DOWNLOADING_PAC_FILE, Status.RESOLVING_PROXY_FOR_URL, + Status.RESOLVING_HOST_IN_PAC_FILE, Status.ESTABLISHING_PROXY_TUNNEL, + Status.RESOLVING_HOST, Status.CONNECTING, Status.SSL_HANDSHAKE, Status.SENDING_REQUEST, + Status.WAITING_FOR_RESPONSE, Status.READING_RESPONSE}) + @Retention(RetentionPolicy.SOURCE) + public @interface StatusValues {} + + /** + * Convert a LoadState int to one of values listed above. + * @param loadState a LoadState to convert. + * @return static int Status. + */ + @StatusValues + public static int convertLoadState(int loadState) { + assert loadState >= LoadState.IDLE && loadState <= LoadState.READING_RESPONSE; + switch (loadState) { + case (LoadState.IDLE): + return Status.IDLE; + + case (LoadState.WAITING_FOR_STALLED_SOCKET_POOL): + return Status.WAITING_FOR_STALLED_SOCKET_POOL; + + case (LoadState.WAITING_FOR_AVAILABLE_SOCKET): + return Status.WAITING_FOR_AVAILABLE_SOCKET; + + case (LoadState.WAITING_FOR_DELEGATE): + return Status.WAITING_FOR_DELEGATE; + + case (LoadState.WAITING_FOR_CACHE): + return Status.WAITING_FOR_CACHE; + + case (LoadState.DOWNLOADING_PAC_FILE): + return Status.DOWNLOADING_PAC_FILE; + + case (LoadState.RESOLVING_PROXY_FOR_URL): + return Status.RESOLVING_PROXY_FOR_URL; + + case (LoadState.RESOLVING_HOST_IN_PAC_FILE): + return Status.RESOLVING_HOST_IN_PAC_FILE; + + case (LoadState.ESTABLISHING_PROXY_TUNNEL): + return Status.ESTABLISHING_PROXY_TUNNEL; + + case (LoadState.RESOLVING_HOST): + return Status.RESOLVING_HOST; + + case (LoadState.CONNECTING): + return Status.CONNECTING; + + case (LoadState.SSL_HANDSHAKE): + return Status.SSL_HANDSHAKE; + + case (LoadState.SENDING_REQUEST): + return Status.SENDING_REQUEST; + + case (LoadState.WAITING_FOR_RESPONSE): + return Status.WAITING_FOR_RESPONSE; + + case (LoadState.READING_RESPONSE): + return Status.READING_RESPONSE; + + default: + // A load state is retrieved but there is no corresponding + // request status. This most likely means that the mapping is + // incorrect. + throw new IllegalArgumentException("No request status found."); + } + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/UrlRequestBuilderImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/UrlRequestBuilderImpl.java new file mode 100644 index 0000000000..16b5f1063d --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/UrlRequestBuilderImpl.java @@ -0,0 +1,224 @@ +// Copyright 2016 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.impl; + +import android.annotation.SuppressLint; +import android.util.Log; +import android.util.Pair; + +import org.chromium.net.CronetEngine; +import org.chromium.net.ExperimentalUrlRequest; +import org.chromium.net.RequestFinishedInfo; +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UrlRequest; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.Executor; + +/** + * Implements {@link org.chromium.net.ExperimentalUrlRequest.Builder}. + */ +public class UrlRequestBuilderImpl extends ExperimentalUrlRequest.Builder { + private static final String ACCEPT_ENCODING = "Accept-Encoding"; + private static final String TAG = UrlRequestBuilderImpl.class.getSimpleName(); + + // All fields are temporary storage of ExperimentalUrlRequest configuration to be + // copied to built ExperimentalUrlRequest. + + // CronetEngineBase to execute request. + private final CronetEngineBase mCronetEngine; + // URL to request. + private final String mUrl; + // Callback to receive progress callbacks. + private final UrlRequest.Callback mCallback; + // Executor to invoke callback on. + private final Executor mExecutor; + // HTTP method (e.g. GET, POST etc). + private String mMethod; + + // List of request headers, stored as header field name and value pairs. + private final ArrayList> mRequestHeaders = new ArrayList<>(); + // Disable the cache for just this request. + private boolean mDisableCache; + // Disable connection migration for just this request. + private boolean mDisableConnectionMigration; + // Priority of request. Default is medium. + @CronetEngineBase.RequestPriority + private int mPriority = REQUEST_PRIORITY_MEDIUM; + // Request reporting annotations. Avoid extra object creation if no annotations added. + private Collection mRequestAnnotations; + // If request is an upload, this provides the request body data. + private UploadDataProvider mUploadDataProvider; + // Executor to call upload data provider back on. + private Executor mUploadDataProviderExecutor; + private boolean mAllowDirectExecutor; + private boolean mTrafficStatsTagSet; + private int mTrafficStatsTag; + private boolean mTrafficStatsUidSet; + private int mTrafficStatsUid; + private RequestFinishedInfo.Listener mRequestFinishedListener; + // Idempotency of the request. + @CronetEngineBase.Idempotency + private int mIdempotency = DEFAULT_IDEMPOTENCY; + + /** + * Creates a builder for {@link UrlRequest} objects. All callbacks for + * generated {@link UrlRequest} objects will be invoked on + * {@code executor}'s thread. {@code executor} must not run tasks on the + * current thread to prevent blocking networking operations and causing + * exceptions during shutdown. + * + * @param url URL for the generated requests. + * @param callback callback object that gets invoked on different events. + * @param executor {@link Executor} on which all callbacks will be invoked. + * @param cronetEngine {@link CronetEngine} used to execute this request. + */ + UrlRequestBuilderImpl(String url, UrlRequest.Callback callback, Executor executor, + CronetEngineBase cronetEngine) { + super(); + if (url == null) { + throw new NullPointerException("URL is required."); + } + if (callback == null) { + throw new NullPointerException("Callback is required."); + } + if (executor == null) { + throw new NullPointerException("Executor is required."); + } + if (cronetEngine == null) { + throw new NullPointerException("CronetEngine is required."); + } + mUrl = url; + mCallback = callback; + mExecutor = executor; + mCronetEngine = cronetEngine; + } + + @Override + public ExperimentalUrlRequest.Builder setHttpMethod(String method) { + if (method == null) { + throw new NullPointerException("Method is required."); + } + mMethod = method; + return this; + } + + @Override + public UrlRequestBuilderImpl addHeader(String header, String value) { + if (header == null) { + throw new NullPointerException("Invalid header name."); + } + if (value == null) { + throw new NullPointerException("Invalid header value."); + } + if (ACCEPT_ENCODING.equalsIgnoreCase(header)) { + Log.w(TAG, "It's not necessary to set Accept-Encoding on requests - cronet will do" + + " this automatically for you, and setting it yourself has no " + + "effect. See https://crbug.com/581399 for details.", + new Exception()); + return this; + } + mRequestHeaders.add(Pair.create(header, value)); + return this; + } + + @Override + public UrlRequestBuilderImpl disableCache() { + mDisableCache = true; + return this; + } + + @Override + public UrlRequestBuilderImpl disableConnectionMigration() { + mDisableConnectionMigration = true; + return this; + } + + @Override + public UrlRequestBuilderImpl setPriority(@CronetEngineBase.RequestPriority int priority) { + mPriority = priority; + return this; + } + + @Override + public UrlRequestBuilderImpl setIdempotency(@CronetEngineBase.Idempotency int idempotency) { + mIdempotency = idempotency; + return this; + } + + @Override + public UrlRequestBuilderImpl setUploadDataProvider( + UploadDataProvider uploadDataProvider, Executor executor) { + if (uploadDataProvider == null) { + throw new NullPointerException("Invalid UploadDataProvider."); + } + if (executor == null) { + throw new NullPointerException("Invalid UploadDataProvider Executor."); + } + if (mMethod == null) { + mMethod = "POST"; + } + mUploadDataProvider = uploadDataProvider; + mUploadDataProviderExecutor = executor; + return this; + } + + @Override + public UrlRequestBuilderImpl allowDirectExecutor() { + mAllowDirectExecutor = true; + return this; + } + + @Override + public UrlRequestBuilderImpl addRequestAnnotation(Object annotation) { + if (annotation == null) { + throw new NullPointerException("Invalid metrics annotation."); + } + if (mRequestAnnotations == null) { + mRequestAnnotations = new ArrayList<>(); + } + mRequestAnnotations.add(annotation); + return this; + } + + @Override + public UrlRequestBuilderImpl setTrafficStatsTag(int tag) { + mTrafficStatsTagSet = true; + mTrafficStatsTag = tag; + return this; + } + + @Override + public UrlRequestBuilderImpl setTrafficStatsUid(int uid) { + mTrafficStatsUidSet = true; + mTrafficStatsUid = uid; + return this; + } + + @Override + public UrlRequestBuilderImpl setRequestFinishedListener(RequestFinishedInfo.Listener listener) { + mRequestFinishedListener = listener; + return this; + } + + @Override + public UrlRequestBase build() { + @SuppressLint("WrongConstant") // TODO(jbudorick): Remove this after rolling to the N SDK. + final UrlRequestBase request = mCronetEngine.createRequest(mUrl, mCallback, mExecutor, + mPriority, mRequestAnnotations, mDisableCache, mDisableConnectionMigration, + mAllowDirectExecutor, mTrafficStatsTagSet, mTrafficStatsTag, mTrafficStatsUidSet, + mTrafficStatsUid, mRequestFinishedListener, mIdempotency); + if (mMethod != null) { + request.setHttpMethod(mMethod); + } + for (Pair header : mRequestHeaders) { + request.addHeader(header.first, header.second); + } + if (mUploadDataProvider != null) { + request.setUploadDataProvider(mUploadDataProvider, mUploadDataProviderExecutor); + } + return request; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/UrlResponseInfoImpl.java b/src/components/cronet/android/java/src/org/chromium/net/impl/UrlResponseInfoImpl.java new file mode 100644 index 0000000000..ab0006204d --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/UrlResponseInfoImpl.java @@ -0,0 +1,177 @@ +// Copyright 2016 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.impl; + +import org.chromium.net.UrlResponseInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Implements the container for basic information about a response. Included in + * {@link org.chromium.net.UrlRequest.Callback} callbacks. Each + * {@link org.chromium.net.UrlRequest.Callback#onRedirectReceived onRedirectReceived()} + * callback gets a different copy of {@code UrlResponseInfo} describing a particular + * redirect response. + */ +public final class UrlResponseInfoImpl extends UrlResponseInfo { + private final List mResponseInfoUrlChain; + private final int mHttpStatusCode; + private final String mHttpStatusText; + private final boolean mWasCached; + private final String mNegotiatedProtocol; + private final String mProxyServer; + private final AtomicLong mReceivedByteCount; + private final HeaderBlockImpl mHeaders; + + /** + * Unmodifiable container of response headers or trailers. + */ + public static final class HeaderBlockImpl extends HeaderBlock { + private final List> mAllHeadersList; + private Map> mHeadersMap; + + HeaderBlockImpl(List> allHeadersList) { + mAllHeadersList = allHeadersList; + } + + @Override + public List> getAsList() { + return mAllHeadersList; + } + + @Override + public Map> getAsMap() { + // This is potentially racy...but races will only result in wasted resource. + if (mHeadersMap != null) { + return mHeadersMap; + } + Map> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (Map.Entry entry : mAllHeadersList) { + List values = new ArrayList(); + if (map.containsKey(entry.getKey())) { + values.addAll(map.get(entry.getKey())); + } + values.add(entry.getValue()); + map.put(entry.getKey(), Collections.unmodifiableList(values)); + } + mHeadersMap = Collections.unmodifiableMap(map); + return mHeadersMap; + } + } + + /** + * Creates an implementation of {@link UrlResponseInfo}. + * + * @param urlChain the URL chain. The first entry is the originally requested URL; + * the following entries are redirects followed. + * @param httpStatusCode the HTTP status code. + * @param httpStatusText the HTTP status text of the status line. + * @param allHeadersList list of response header field and value pairs. + * @param wasCached {@code true} if the response came from the cache, {@code false} + * otherwise. + * @param negotiatedProtocol the protocol negotiated with the server. + * @param proxyServer the proxy server that was used for the request. + * @param receivedByteCount minimum count of bytes received from the network to process this + * request. + */ + public UrlResponseInfoImpl(List urlChain, int httpStatusCode, String httpStatusText, + List> allHeadersList, boolean wasCached, + String negotiatedProtocol, String proxyServer, long receivedByteCount) { + mResponseInfoUrlChain = Collections.unmodifiableList(urlChain); + mHttpStatusCode = httpStatusCode; + mHttpStatusText = httpStatusText; + mHeaders = new HeaderBlockImpl(Collections.unmodifiableList(allHeadersList)); + mWasCached = wasCached; + mNegotiatedProtocol = negotiatedProtocol; + mProxyServer = proxyServer; + mReceivedByteCount = new AtomicLong(receivedByteCount); + } + + /** + * Constructor for backwards compatibility. See main constructor above for more info. + */ + @Deprecated + public UrlResponseInfoImpl(List urlChain, int httpStatusCode, String httpStatusText, + List> allHeadersList, boolean wasCached, + String negotiatedProtocol, String proxyServer) { + this(urlChain, httpStatusCode, httpStatusText, allHeadersList, wasCached, + negotiatedProtocol, proxyServer, 0); + } + + @Override + public String getUrl() { + return mResponseInfoUrlChain.get(mResponseInfoUrlChain.size() - 1); + } + + @Override + public List getUrlChain() { + return mResponseInfoUrlChain; + } + + @Override + public int getHttpStatusCode() { + return mHttpStatusCode; + } + + @Override + public String getHttpStatusText() { + return mHttpStatusText; + } + + @Override + public List> getAllHeadersAsList() { + return mHeaders.getAsList(); + } + + @Override + public Map> getAllHeaders() { + return mHeaders.getAsMap(); + } + + @Override + public boolean wasCached() { + return mWasCached; + } + + @Override + public String getNegotiatedProtocol() { + return mNegotiatedProtocol; + } + + @Override + public String getProxyServer() { + return mProxyServer; + } + + @Override + public long getReceivedByteCount() { + return mReceivedByteCount.get(); + } + + @Override + public String toString() { + return String.format(Locale.ROOT, "UrlResponseInfo@[%s][%s]: urlChain = %s, " + + "httpStatus = %d %s, headers = %s, wasCached = %b, " + + "negotiatedProtocol = %s, proxyServer= %s, receivedByteCount = %d", + // Prevent asserting on the contents of this string + Integer.toHexString(System.identityHashCode(this)), getUrl(), + getUrlChain().toString(), getHttpStatusCode(), getHttpStatusText(), + getAllHeadersAsList().toString(), wasCached(), getNegotiatedProtocol(), + getProxyServer(), getReceivedByteCount()); + } + + /** + * Sets mReceivedByteCount. Must not be called after request completion or cancellation. + */ + public void setReceivedByteCount(long currentReceivedByteCount) { + mReceivedByteCount.set(currentReceivedByteCount); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/UserAgent.java b/src/components/cronet/android/java/src/org/chromium/net/impl/UserAgent.java new file mode 100644 index 0000000000..46f857d496 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/UserAgent.java @@ -0,0 +1,103 @@ +// Copyright 2014 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.impl; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; + +import java.util.Locale; + +/** + * Constructs a User-Agent string. + */ +public final class UserAgent { + private static final Object sLock = new Object(); + + private static final int VERSION_CODE_UNINITIALIZED = 0; + private static int sVersionCode = VERSION_CODE_UNINITIALIZED; + + private UserAgent() {} + + /** + * Constructs a User-Agent string including application name and version, + * system build version, model and Id, and Cronet version. + * @param context the context to fetch the application name and version + * from. + * @return User-Agent string. + */ + public static String from(Context context) { + StringBuilder builder = new StringBuilder(); + + // Our package name and version. + builder.append(context.getPackageName()); + builder.append('/'); + builder.append(versionFromContext(context)); + + // The platform version. + builder.append(" (Linux; U; Android "); + builder.append(Build.VERSION.RELEASE); + builder.append("; "); + builder.append(Locale.getDefault().toString()); + + String model = Build.MODEL; + if (model.length() > 0) { + builder.append("; "); + builder.append(model); + } + + String id = Build.ID; + if (id.length() > 0) { + builder.append("; Build/"); + builder.append(id); + } + + builder.append(";"); + appendCronetVersion(builder); + + builder.append(')'); + + return builder.toString(); + } + + /** + * Constructs default QUIC User Agent Id string including application name + * and Cronet version. + * @param context the context to fetch the application name from. + * @return User-Agent string. + */ + static String getQuicUserAgentIdFrom(Context context) { + StringBuilder builder = new StringBuilder(); + + // Application name and cronet version. + builder.append(context.getPackageName()); + appendCronetVersion(builder); + + return builder.toString(); + } + + private static int versionFromContext(Context context) { + synchronized (sLock) { + if (sVersionCode == VERSION_CODE_UNINITIALIZED) { + PackageManager packageManager = context.getPackageManager(); + String packageName = context.getPackageName(); + try { + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); + sVersionCode = packageInfo.versionCode; + } catch (NameNotFoundException e) { + throw new IllegalStateException("Cannot determine package version"); + } + } + return sVersionCode; + } + } + + private static void appendCronetVersion(StringBuilder builder) { + builder.append(" Cronet/"); + builder.append(ImplVersion.getCronetVersion()); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/impl/VersionSafeCallbacks.java b/src/components/cronet/android/java/src/org/chromium/net/impl/VersionSafeCallbacks.java new file mode 100644 index 0000000000..52854ecdef --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/impl/VersionSafeCallbacks.java @@ -0,0 +1,291 @@ +// Copyright 2016 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.impl; + +import org.chromium.net.BidirectionalStream; +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetException; +import org.chromium.net.NetworkQualityRttListener; +import org.chromium.net.NetworkQualityThroughputListener; +import org.chromium.net.RequestFinishedInfo; +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UploadDataSink; +import org.chromium.net.UrlRequest; +import org.chromium.net.UrlResponseInfo; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.Executor; + +/** + * This class contains wrapper classes for all Cronet API callback/listener classes. These classes + * only permit callbacks that the version of the client API is known to support. For example, if + * version 2 of the API adds a callback onFoo() but the client API this class is implementing is + * version 1, these wrapper classes should not call {@code mWrappedCallback.onFoo()} and should + * instead silently drop the callback. + * + * When adding any callback wrapping here, be sure you add the proper version check. Only callbacks + * supported in all versions of the API should forgo a version check. + */ +public class VersionSafeCallbacks { + /** + * Wrap a {@link UrlRequest.Callback} in a version safe manner. + */ + public static final class UrlRequestCallback extends UrlRequest.Callback { + private final UrlRequest.Callback mWrappedCallback; + + public UrlRequestCallback(UrlRequest.Callback callback) { + mWrappedCallback = callback; + } + + @Override + public void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) throws Exception { + mWrappedCallback.onRedirectReceived(request, info, newLocationUrl); + } + + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) throws Exception { + mWrappedCallback.onResponseStarted(request, info); + } + + @Override + public void onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) + throws Exception { + mWrappedCallback.onReadCompleted(request, info, byteBuffer); + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + mWrappedCallback.onSucceeded(request, info); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) { + mWrappedCallback.onFailed(request, info, error); + } + + @Override + public void onCanceled(UrlRequest request, UrlResponseInfo info) { + mWrappedCallback.onCanceled(request, info); + } + } + + /** + * Wrap a {@link UrlRequest.StatusListener} in a version safe manner. + */ + public static final class UrlRequestStatusListener extends UrlRequest.StatusListener { + private final UrlRequest.StatusListener mWrappedListener; + + public UrlRequestStatusListener(UrlRequest.StatusListener listener) { + mWrappedListener = listener; + } + + @Override + public void onStatus(int status) { + mWrappedListener.onStatus(status); + } + } + + /** + * Wrap a {@link BidirectionalStream.Callback} in a version safe manner. + */ + public static final class BidirectionalStreamCallback extends BidirectionalStream.Callback { + private final BidirectionalStream.Callback mWrappedCallback; + + public BidirectionalStreamCallback(BidirectionalStream.Callback callback) { + mWrappedCallback = callback; + } + + @Override + public void onStreamReady(BidirectionalStream stream) { + mWrappedCallback.onStreamReady(stream); + } + + @Override + public void onResponseHeadersReceived(BidirectionalStream stream, UrlResponseInfo info) { + mWrappedCallback.onResponseHeadersReceived(stream, info); + } + + @Override + public void onReadCompleted(BidirectionalStream stream, UrlResponseInfo info, + ByteBuffer buffer, boolean endOfStream) { + mWrappedCallback.onReadCompleted(stream, info, buffer, endOfStream); + } + + @Override + public void onWriteCompleted(BidirectionalStream stream, UrlResponseInfo info, + ByteBuffer buffer, boolean endOfStream) { + mWrappedCallback.onWriteCompleted(stream, info, buffer, endOfStream); + } + + @Override + public void onResponseTrailersReceived(BidirectionalStream stream, UrlResponseInfo info, + UrlResponseInfo.HeaderBlock trailers) { + mWrappedCallback.onResponseTrailersReceived(stream, info, trailers); + } + + @Override + public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info) { + mWrappedCallback.onSucceeded(stream, info); + } + + @Override + public void onFailed( + BidirectionalStream stream, UrlResponseInfo info, CronetException error) { + mWrappedCallback.onFailed(stream, info, error); + } + + @Override + public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) { + mWrappedCallback.onCanceled(stream, info); + } + } + + /** + * Wrap a {@link UploadDataProvider} in a version safe manner. + */ + public static final class UploadDataProviderWrapper extends UploadDataProvider { + private final UploadDataProvider mWrappedProvider; + + public UploadDataProviderWrapper(UploadDataProvider provider) { + mWrappedProvider = provider; + } + + @Override + public long getLength() throws IOException { + return mWrappedProvider.getLength(); + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) throws IOException { + mWrappedProvider.read(uploadDataSink, byteBuffer); + } + + @Override + public void rewind(UploadDataSink uploadDataSink) throws IOException { + mWrappedProvider.rewind(uploadDataSink); + } + + @Override + public void close() throws IOException { + mWrappedProvider.close(); + } + } + + /** + * Wrap a {@link RequestFinishedInfo.Listener} in a version safe manner. + */ + public static final class RequestFinishedInfoListener extends RequestFinishedInfo.Listener { + private final RequestFinishedInfo.Listener mWrappedListener; + + public RequestFinishedInfoListener(RequestFinishedInfo.Listener listener) { + super(listener.getExecutor()); + mWrappedListener = listener; + } + + @Override + public void onRequestFinished(RequestFinishedInfo requestInfo) { + mWrappedListener.onRequestFinished(requestInfo); + } + + @Override + public Executor getExecutor() { + return mWrappedListener.getExecutor(); + } + } + + /** + * Wrap a {@link NetworkQualityRttListener} in a version safe manner. + * NOTE(pauljensen): Delegates equals() and hashCode() to wrapped listener to + * facilitate looking up by wrapped listener in an ArrayList.indexOf(). + */ + public static final class NetworkQualityRttListenerWrapper extends NetworkQualityRttListener { + private final NetworkQualityRttListener mWrappedListener; + + public NetworkQualityRttListenerWrapper(NetworkQualityRttListener listener) { + super(listener.getExecutor()); + mWrappedListener = listener; + } + + @Override + public void onRttObservation(int rttMs, long whenMs, int source) { + mWrappedListener.onRttObservation(rttMs, whenMs, source); + } + + @Override + public Executor getExecutor() { + return mWrappedListener.getExecutor(); + } + + @Override + public int hashCode() { + return mWrappedListener.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof NetworkQualityRttListenerWrapper)) { + return false; + } + return mWrappedListener.equals(((NetworkQualityRttListenerWrapper) o).mWrappedListener); + } + } + + /** + * Wrap a {@link NetworkQualityThroughputListener} in a version safe manner. + * NOTE(pauljensen): Delegates equals() and hashCode() to wrapped listener to + * facilitate looking up by wrapped listener in an ArrayList.indexOf(). + */ + public static final class NetworkQualityThroughputListenerWrapper + extends NetworkQualityThroughputListener { + private final NetworkQualityThroughputListener mWrappedListener; + + public NetworkQualityThroughputListenerWrapper(NetworkQualityThroughputListener listener) { + super(listener.getExecutor()); + mWrappedListener = listener; + } + + @Override + public void onThroughputObservation(int throughputKbps, long whenMs, int source) { + mWrappedListener.onThroughputObservation(throughputKbps, whenMs, source); + } + + @Override + public Executor getExecutor() { + return mWrappedListener.getExecutor(); + } + + @Override + public int hashCode() { + return mWrappedListener.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof NetworkQualityThroughputListenerWrapper)) { + return false; + } + return mWrappedListener.equals( + ((NetworkQualityThroughputListenerWrapper) o).mWrappedListener); + } + } + + /** + * Wrap a {@link CronetEngine.Builder.LibraryLoader} in a version safe manner. + */ + public static final class LibraryLoader extends CronetEngine.Builder.LibraryLoader { + private final CronetEngine.Builder.LibraryLoader mWrappedLoader; + + public LibraryLoader(CronetEngine.Builder.LibraryLoader libraryLoader) { + mWrappedLoader = libraryLoader; + } + + @Override + public void loadLibrary(String libName) { + mWrappedLoader.loadLibrary(libName); + } + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetBufferedOutputStream.java b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetBufferedOutputStream.java new file mode 100644 index 0000000000..4489d0b333 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetBufferedOutputStream.java @@ -0,0 +1,174 @@ +// 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.urlconnection; + +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UploadDataSink; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; + +/** + * An implementation of {@link java.io.OutputStream} that buffers entire request + * body in memory. This is used when neither + * {@link CronetHttpURLConnection#setFixedLengthStreamingMode} + * nor {@link CronetHttpURLConnection#setChunkedStreamingMode} is set. + */ +final class CronetBufferedOutputStream extends CronetOutputStream { + // QUIC uses a read buffer of 14520 bytes, SPDY uses 2852 bytes, and normal + // stream uses 16384 bytes. Therefore, use 16384 for now to avoid growing + // the buffer too many times. + private static final int INITIAL_BUFFER_SIZE = 16384; + // If content length is not passed in the constructor, this is -1. + private final int mInitialContentLength; + private final CronetHttpURLConnection mConnection; + private final UploadDataProvider mUploadDataProvider = new UploadDataProviderImpl(); + // Internal buffer that is used to buffer the request body. + private ByteBuffer mBuffer; + private boolean mConnected; + + /** + * Package protected constructor. + * @param connection The CronetHttpURLConnection object. + * @param contentLength The content length of the request body. It must not + * be smaller than 0 or bigger than {@link Integer.MAX_VALUE}. + */ + CronetBufferedOutputStream(final CronetHttpURLConnection connection, + final long contentLength) { + if (connection == null) { + throw new NullPointerException("Argument connection cannot be null."); + } + + if (contentLength > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Use setFixedLengthStreamingMode()" + + " or setChunkedStreamingMode() for requests larger than 2GB."); + } + if (contentLength < 0) { + throw new IllegalArgumentException("Content length < 0."); + } + mConnection = connection; + mInitialContentLength = (int) contentLength; + mBuffer = ByteBuffer.allocate(mInitialContentLength); + } + + /** + * Package protected constructor used when content length is not known. + * @param connection The CronetHttpURLConnection object. + */ + CronetBufferedOutputStream(final CronetHttpURLConnection connection) { + if (connection == null) { + throw new NullPointerException(); + } + + mConnection = connection; + mInitialContentLength = -1; + // Buffering without knowing content-length. + mBuffer = ByteBuffer.allocate(INITIAL_BUFFER_SIZE); + } + + @Override + public void write(int oneByte) throws IOException { + checkNotClosed(); + ensureCanWrite(1); + mBuffer.put((byte) oneByte); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + checkNotClosed(); + ensureCanWrite(count); + mBuffer.put(buffer, offset, count); + } + + /** + * Ensures that {@code count} bytes can be written to the internal buffer. + */ + private void ensureCanWrite(int count) throws IOException { + if (mInitialContentLength != -1 + && mBuffer.position() + count > mInitialContentLength) { + // Error message is to match that of the default implementation. + throw new ProtocolException("exceeded content-length limit of " + + mInitialContentLength + " bytes"); + } + if (mConnected) { + throw new IllegalStateException("Use setFixedLengthStreamingMode() or " + + "setChunkedStreamingMode() for writing after connect"); + } + if (mInitialContentLength != -1) { + // If mInitialContentLength is known, the buffer should not grow. + return; + } + if (mBuffer.limit() - mBuffer.position() > count) { + // If there is enough capacity, the buffer should not grow. + return; + } + int afterSize = Math.max(mBuffer.capacity() * 2, mBuffer.capacity() + count); + ByteBuffer newByteBuffer = ByteBuffer.allocate(afterSize); + mBuffer.flip(); + newByteBuffer.put(mBuffer); + mBuffer = newByteBuffer; + } + + // Below are CronetOutputStream implementations: + + /** + * Sets {@link #mConnected} to {@code true}. + */ + @Override + void setConnected() throws IOException { + mConnected = true; + if (mBuffer.position() < mInitialContentLength) { + throw new ProtocolException("Content received is less than Content-Length"); + } + // Flip the buffer to prepare it for UploadDataProvider read calls. + mBuffer.flip(); + } + + @Override + void checkReceivedEnoughContent() throws IOException { + // Already checked in setConnected. Skip the check here, since mBuffer + // might be flipped. + } + + @Override + UploadDataProvider getUploadDataProvider() { + return mUploadDataProvider; + } + + private class UploadDataProviderImpl extends UploadDataProvider { + @Override + public long getLength() { + // This method is supposed to be called just before starting the request. + // If content length is not initially passed in, the number of bytes + // written will be used as the content length. + // TODO(xunjieli): Think of a less fragile way, since getLength() can be + // potentially called in other places in the future. + if (mInitialContentLength == -1) { + // Account for the fact that setConnected() flip()s mBuffer. + return mConnected ? mBuffer.limit() : mBuffer.position(); + } + return mInitialContentLength; + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) { + final int availableSpace = byteBuffer.remaining(); + if (availableSpace < mBuffer.remaining()) { + byteBuffer.put(mBuffer.array(), mBuffer.position(), availableSpace); + mBuffer.position(mBuffer.position() + availableSpace); + } else { + byteBuffer.put(mBuffer); + } + uploadDataSink.onReadSucceeded(false); + } + + @Override + public void rewind(UploadDataSink uploadDataSink) { + mBuffer.position(0); + uploadDataSink.onRewindSucceeded(); + } + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetChunkedOutputStream.java b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetChunkedOutputStream.java new file mode 100644 index 0000000000..329f1ba3e0 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetChunkedOutputStream.java @@ -0,0 +1,150 @@ +// 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.urlconnection; + +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UploadDataSink; + +import java.io.IOException; +import java.net.HttpRetryException; +import java.nio.ByteBuffer; + +/** + * An implementation of {@link java.io.OutputStream} to send data to a server, + * when {@link CronetHttpURLConnection#setChunkedStreamingMode} is used. + * This implementation does not buffer the entire request body in memory. + * It does not support rewind. Note that {@link #write} should only be called + * from the thread on which the {@link #mConnection} is created. + */ +final class CronetChunkedOutputStream extends CronetOutputStream { + private final CronetHttpURLConnection mConnection; + private final MessageLoop mMessageLoop; + private final ByteBuffer mBuffer; + private final UploadDataProvider mUploadDataProvider = new UploadDataProviderImpl(); + private boolean mLastChunk; + + /** + * Package protected constructor. + * @param connection The CronetHttpURLConnection object. + * @param chunkLength The chunk length of the request body in bytes. It must + * be a positive number. + */ + CronetChunkedOutputStream( + CronetHttpURLConnection connection, int chunkLength, MessageLoop messageLoop) { + if (connection == null) { + throw new NullPointerException(); + } + if (chunkLength <= 0) { + throw new IllegalArgumentException("chunkLength should be greater than 0"); + } + mBuffer = ByteBuffer.allocate(chunkLength); + mConnection = connection; + mMessageLoop = messageLoop; + } + + @Override + public void write(int oneByte) throws IOException { + ensureBufferHasRemaining(); + mBuffer.put((byte) oneByte); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + checkNotClosed(); + if (buffer.length - offset < count || offset < 0 || count < 0) { + throw new IndexOutOfBoundsException(); + } + int toSend = count; + while (toSend > 0) { + int sent = Math.min(toSend, mBuffer.remaining()); + mBuffer.put(buffer, offset + count - toSend, sent); + toSend -= sent; + // Upload mBuffer now if an entire chunk is written. + ensureBufferHasRemaining(); + } + } + + @Override + public void close() throws IOException { + super.close(); + if (!mLastChunk) { + // Consumer can only call close() when message loop is not running. + // Set mLastChunk to be true and flip mBuffer to upload its contents. + mLastChunk = true; + mBuffer.flip(); + } + } + + // Below are CronetOutputStream implementations: + + @Override + void setConnected() throws IOException { + // Do nothing. + } + + @Override + void checkReceivedEnoughContent() throws IOException { + // Do nothing. + } + + @Override + UploadDataProvider getUploadDataProvider() { + return mUploadDataProvider; + } + + private class UploadDataProviderImpl extends UploadDataProvider { + @Override + public long getLength() { + return -1; + } + + @Override + public void read(final UploadDataSink uploadDataSink, final ByteBuffer byteBuffer) { + if (byteBuffer.remaining() >= mBuffer.remaining()) { + byteBuffer.put(mBuffer); + mBuffer.clear(); + uploadDataSink.onReadSucceeded(mLastChunk); + if (!mLastChunk) { + // Quit message loop so embedder can write more data. + mMessageLoop.quit(); + } + } else { + int oldLimit = mBuffer.limit(); + mBuffer.limit(mBuffer.position() + byteBuffer.remaining()); + byteBuffer.put(mBuffer); + mBuffer.limit(oldLimit); + uploadDataSink.onReadSucceeded(false); + } + } + + @Override + public void rewind(UploadDataSink uploadDataSink) { + uploadDataSink.onRewindError( + new HttpRetryException("Cannot retry streamed Http body", -1)); + } + } + + /** + * If {@code mBuffer} is full, wait until it is consumed and there is + * space to write more data to it. + */ + private void ensureBufferHasRemaining() throws IOException { + if (!mBuffer.hasRemaining()) { + uploadBufferInternal(); + } + } + + /** + * Helper function to upload {@code mBuffer} to the native stack. This + * function blocks until {@code mBuffer} is consumed and there is space to + * write more data. + */ + private void uploadBufferInternal() throws IOException { + checkNotClosed(); + mBuffer.flip(); + mMessageLoop.loop(); + checkNoException(); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetFixedModeOutputStream.java b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetFixedModeOutputStream.java new file mode 100644 index 0000000000..4a5af54f8f --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetFixedModeOutputStream.java @@ -0,0 +1,207 @@ +// 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.urlconnection; + +import androidx.annotation.VisibleForTesting; + +import org.chromium.net.UploadDataProvider; +import org.chromium.net.UploadDataSink; + +import java.io.IOException; +import java.net.HttpRetryException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; + +/** + * An implementation of {@link java.io.OutputStream} to send data to a server, + * when {@link CronetHttpURLConnection#setFixedLengthStreamingMode} is used. + * This implementation does not buffer the entire request body in memory. + * It does not support rewind. Note that {@link #write} should only be called + * from the thread on which the {@link #mConnection} is created. + */ +final class CronetFixedModeOutputStream extends CronetOutputStream { + // CronetFixedModeOutputStream buffers up to this value and wait for UploadDataStream + // to consume the data. This field is non-final, so it can be changed for tests. + // Using 16384 bytes is because the internal read buffer is 14520 for QUIC, + // 16384 for SPDY, and 16384 for normal HTTP/1.1 stream. + @VisibleForTesting + private static int sDefaultBufferLength = 16384; + private final CronetHttpURLConnection mConnection; + private final MessageLoop mMessageLoop; + private final long mContentLength; + // Internal buffer for holding bytes from the client until the bytes are + // copied to the UploadDataSink in UploadDataProvider.read(). + // CronetFixedModeOutputStream allows client to provide up to + // sDefaultBufferLength bytes, and wait for UploadDataProvider.read() to be + // called after which point mBuffer is cleared so client can fill in again. + // While the client is filling the buffer (via {@code write()}), the buffer's + // position points to the next byte to be provided by the client, and limit + // points to the end of the buffer. The buffer is flipped before it is + // passed to the UploadDataProvider for consuming. Once it is flipped, + // buffer position points to the next byte to be copied to the + // UploadDataSink, and limit points to the end of data available to be + // copied to UploadDataSink. When the UploadDataProvider has provided all + // remaining bytes from the buffer to UploadDataSink, it clears the buffer + // so client can fill it again. + private final ByteBuffer mBuffer; + private final UploadDataProvider mUploadDataProvider = new UploadDataProviderImpl(); + private long mBytesWritten; + + /** + * Package protected constructor. + * @param connection The CronetHttpURLConnection object. + * @param contentLength The content length of the request body. Non-zero for + * non-chunked upload. + */ + CronetFixedModeOutputStream(CronetHttpURLConnection connection, + long contentLength, MessageLoop messageLoop) { + if (connection == null) { + throw new NullPointerException(); + } + if (contentLength < 0) { + throw new IllegalArgumentException( + "Content length must be larger than 0 for non-chunked upload."); + } + mContentLength = contentLength; + int bufferSize = (int) Math.min(mContentLength, sDefaultBufferLength); + mBuffer = ByteBuffer.allocate(bufferSize); + mConnection = connection; + mMessageLoop = messageLoop; + mBytesWritten = 0; + } + + @Override + public void write(int oneByte) throws IOException { + checkNotClosed(); + checkNotExceedContentLength(1); + ensureBufferHasRemaining(); + mBuffer.put((byte) oneByte); + mBytesWritten++; + uploadIfComplete(); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + checkNotClosed(); + if (buffer.length - offset < count || offset < 0 || count < 0) { + throw new IndexOutOfBoundsException(); + } + checkNotExceedContentLength(count); + int toSend = count; + while (toSend > 0) { + ensureBufferHasRemaining(); + int sent = Math.min(toSend, mBuffer.remaining()); + mBuffer.put(buffer, offset + count - toSend, sent); + toSend -= sent; + } + mBytesWritten += count; + uploadIfComplete(); + } + + /** + * If {@code mBuffer} is full, wait until it is consumed and there is + * space to write more data to it. + */ + private void ensureBufferHasRemaining() throws IOException { + if (!mBuffer.hasRemaining()) { + uploadBufferInternal(); + } + } + + /** + * Waits for the native stack to upload {@code mBuffer}'s contents because + * the client has provided all bytes to be uploaded and there is no need to + * wait for or expect the client to provide more bytes. + */ + private void uploadIfComplete() throws IOException { + if (mBytesWritten == mContentLength) { + // Entire post data has been received. Now wait for network stack to + // read it. + uploadBufferInternal(); + } + } + + /** + * Helper function to upload {@code mBuffer} to the native stack. This + * function blocks until {@code mBuffer} is consumed and there is space to + * write more data. + */ + private void uploadBufferInternal() throws IOException { + checkNotClosed(); + mBuffer.flip(); + mMessageLoop.loop(); + checkNoException(); + } + + /** + * Throws {@link java.net.ProtocolException} if adding {@code numBytes} will + * exceed content length. + */ + private void checkNotExceedContentLength(int numBytes) throws ProtocolException { + if (mBytesWritten + numBytes > mContentLength) { + throw new ProtocolException("expected " + + (mContentLength - mBytesWritten) + " bytes but received " + + numBytes); + } + } + + // Below are CronetOutputStream implementations: + + @Override + void setConnected() throws IOException { + // Do nothing. + } + + @Override + void checkReceivedEnoughContent() throws IOException { + if (mBytesWritten < mContentLength) { + throw new ProtocolException("Content received is less than Content-Length."); + } + } + + @Override + UploadDataProvider getUploadDataProvider() { + return mUploadDataProvider; + } + + private class UploadDataProviderImpl extends UploadDataProvider { + @Override + public long getLength() { + return mContentLength; + } + + @Override + public void read(final UploadDataSink uploadDataSink, final ByteBuffer byteBuffer) { + if (byteBuffer.remaining() >= mBuffer.remaining()) { + byteBuffer.put(mBuffer); + // Reuse this buffer. + mBuffer.clear(); + uploadDataSink.onReadSucceeded(false); + // Quit message loop so embedder can write more data. + mMessageLoop.quit(); + } else { + int oldLimit = mBuffer.limit(); + mBuffer.limit(mBuffer.position() + byteBuffer.remaining()); + byteBuffer.put(mBuffer); + mBuffer.limit(oldLimit); + uploadDataSink.onReadSucceeded(false); + } + } + + @Override + public void rewind(UploadDataSink uploadDataSink) { + uploadDataSink.onRewindError( + new HttpRetryException("Cannot retry streamed Http body", -1)); + } + } + + /** + * Sets the default buffer length for use in tests. + */ + @VisibleForTesting + static void setDefaultBufferLengthForTesting(int length) { + sDefaultBufferLength = length; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetHttpURLConnection.java b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetHttpURLConnection.java new file mode 100644 index 0000000000..d42cefa3a8 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetHttpURLConnection.java @@ -0,0 +1,701 @@ +// Copyright 2014 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.urlconnection; + +import android.net.TrafficStats; +import android.os.Build; +import android.util.Log; +import android.util.Pair; + +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetException; +import org.chromium.net.ExperimentalUrlRequest; +import org.chromium.net.UrlRequest; +import org.chromium.net.UrlResponseInfo; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.ByteBuffer; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * An implementation of {@link HttpURLConnection} that uses Cronet to send + * requests and receive responses. + * {@hide} + */ +public class CronetHttpURLConnection extends HttpURLConnection { + private static final String TAG = CronetHttpURLConnection.class.getSimpleName(); + private static final String CONTENT_LENGTH = "Content-Length"; + private final CronetEngine mCronetEngine; + private final MessageLoop mMessageLoop; + private UrlRequest mRequest; + private final List> mRequestHeaders; + private boolean mTrafficStatsTagSet; + private int mTrafficStatsTag; + private boolean mTrafficStatsUidSet; + private int mTrafficStatsUid; + + private CronetInputStream mInputStream; + private CronetOutputStream mOutputStream; + private UrlResponseInfo mResponseInfo; + private IOException mException; + private boolean mOnRedirectCalled; + // Whether response headers are received, the request is failed, or the request is canceled. + private boolean mHasResponseHeadersOrCompleted; + private List> mResponseHeadersList; + private Map> mResponseHeadersMap; + + public CronetHttpURLConnection(URL url, CronetEngine cronetEngine) { + super(url); + mCronetEngine = cronetEngine; + mMessageLoop = new MessageLoop(); + mInputStream = new CronetInputStream(this); + mRequestHeaders = new ArrayList>(); + } + + /** + * Opens a connection to the resource. If the connect method is called when + * the connection has already been opened (indicated by the connected field + * having the value {@code true}), the call is ignored. + */ + @Override + public void connect() throws IOException { + getOutputStream(); + // If request is started in getOutputStream, calling startRequest() + // again has no effect. + startRequest(); + } + + /** + * Releases this connection so that its resources may be either reused or + * closed. + */ + @Override + public void disconnect() { + // Disconnect before connection is made should have no effect. + if (connected) { + mRequest.cancel(); + } + } + + /** + * Returns the response message returned by the remote HTTP server. + */ + @Override + public String getResponseMessage() throws IOException { + getResponse(); + return mResponseInfo.getHttpStatusText(); + } + + /** + * Returns the response code returned by the remote HTTP server. + */ + @Override + public int getResponseCode() throws IOException { + getResponse(); + return mResponseInfo.getHttpStatusCode(); + } + + /** + * Returns an unmodifiable map of the response-header fields and values. + */ + @Override + public Map> getHeaderFields() { + try { + getResponse(); + } catch (IOException e) { + return Collections.emptyMap(); + } + return getAllHeaders(); + } + + /** + * Returns the value of the named header field. If called on a connection + * that sets the same header multiple times with possibly different values, + * only the last value is returned. + */ + @Override + public final String getHeaderField(String fieldName) { + try { + getResponse(); + } catch (IOException e) { + return null; + } + Map> map = getAllHeaders(); + if (!map.containsKey(fieldName)) { + return null; + } + List values = map.get(fieldName); + return values.get(values.size() - 1); + } + + /** + * Returns the name of the header field at the given position {@code pos}, or {@code null} + * if there are fewer than {@code pos} fields. + */ + @Override + public final String getHeaderFieldKey(int pos) { + Map.Entry header = getHeaderFieldEntry(pos); + if (header == null) { + return null; + } + return header.getKey(); + } + + /** + * Returns the header value at the field position {@code pos} or {@code null} if the header + * has fewer than {@code pos} fields. + */ + @Override + public final String getHeaderField(int pos) { + Map.Entry header = getHeaderFieldEntry(pos); + if (header == null) { + return null; + } + return header.getValue(); + } + + /** + * Returns an InputStream for reading data from the resource pointed by this + * {@link java.net.URLConnection}. + * @throws FileNotFoundException if http response code is equal or greater + * than {@link HTTP_BAD_REQUEST}. + * @throws IOException If the request gets a network error or HTTP error + * status code, or if the caller tried to read the response body + * of a redirect when redirects are disabled. + */ + @Override + public InputStream getInputStream() throws IOException { + getResponse(); + if (!instanceFollowRedirects && mOnRedirectCalled) { + throw new IOException("Cannot read response body of a redirect."); + } + // Emulate default implementation's behavior to throw + // FileNotFoundException when we get a 400 and above. + if (mResponseInfo.getHttpStatusCode() >= HTTP_BAD_REQUEST) { + throw new FileNotFoundException(url.toString()); + } + return mInputStream; + } + + /** + * Returns an {@link OutputStream} for writing data to this {@link URLConnection}. + * @throws IOException if no {@code OutputStream} could be created. + */ + @Override + public OutputStream getOutputStream() throws IOException { + if (mOutputStream == null && doOutput) { + if (connected) { + throw new ProtocolException( + "Cannot write to OutputStream after receiving response."); + } + if (isChunkedUpload()) { + mOutputStream = new CronetChunkedOutputStream(this, chunkLength, mMessageLoop); + // Start the request now since all headers can be sent. + startRequest(); + } else { + long fixedStreamingModeContentLength = getStreamingModeContentLength(); + if (fixedStreamingModeContentLength != -1) { + mOutputStream = new CronetFixedModeOutputStream( + this, fixedStreamingModeContentLength, mMessageLoop); + // Start the request now since all headers can be sent. + startRequest(); + } else { + // For the buffered case, start the request only when + // content-length bytes are received, or when a + // connect action is initiated by the consumer. + Log.d(TAG, "Outputstream is being buffered in memory."); + String length = getRequestProperty(CONTENT_LENGTH); + if (length == null) { + mOutputStream = new CronetBufferedOutputStream(this); + } else { + long lengthParsed = Long.parseLong(length); + mOutputStream = new CronetBufferedOutputStream(this, lengthParsed); + } + } + } + } + return mOutputStream; + } + + /** + * Helper method to get content length passed in by + * {@link #setFixedLengthStreamingMode} + */ + private long getStreamingModeContentLength() { + // Calling setFixedLengthStreamingMode(long) does not seem to set fixedContentLength (same + // for setFixedLengthStreamingMode(int) and fixedContentLengthLong). Check both to get this + // right. + long contentLength = fixedContentLength; + if (fixedContentLengthLong != -1) { + contentLength = fixedContentLengthLong; + } + + return contentLength; + } + + /** + * Starts the request if {@code connected} is false. + */ + private void startRequest() throws IOException { + if (connected) { + return; + } + final ExperimentalUrlRequest.Builder requestBuilder = + (ExperimentalUrlRequest.Builder) mCronetEngine.newUrlRequestBuilder( + getURL().toString(), new CronetUrlRequestCallback(), mMessageLoop); + if (doOutput) { + if (method.equals("GET")) { + method = "POST"; + } + if (mOutputStream != null) { + requestBuilder.setUploadDataProvider( + mOutputStream.getUploadDataProvider(), mMessageLoop); + if (getRequestProperty(CONTENT_LENGTH) == null && !isChunkedUpload()) { + addRequestProperty(CONTENT_LENGTH, + Long.toString(mOutputStream.getUploadDataProvider().getLength())); + } + // Tells mOutputStream that startRequest() has been called, so + // the underlying implementation can prepare for reading if needed. + mOutputStream.setConnected(); + } else { + if (getRequestProperty(CONTENT_LENGTH) == null) { + addRequestProperty(CONTENT_LENGTH, "0"); + } + } + // Default Content-Type to application/x-www-form-urlencoded + if (getRequestProperty("Content-Type") == null) { + addRequestProperty("Content-Type", + "application/x-www-form-urlencoded"); + } + } + for (Pair requestHeader : mRequestHeaders) { + requestBuilder.addHeader(requestHeader.first, requestHeader.second); + } + if (!getUseCaches()) { + requestBuilder.disableCache(); + } + // Set HTTP method. + requestBuilder.setHttpMethod(method); + if (checkTrafficStatsTag()) { + requestBuilder.setTrafficStatsTag(mTrafficStatsTag); + } + if (checkTrafficStatsUid()) { + requestBuilder.setTrafficStatsUid(mTrafficStatsUid); + } + + mRequest = requestBuilder.build(); + // Start the request. + mRequest.start(); + connected = true; + } + + private boolean checkTrafficStatsTag() { + if (mTrafficStatsTagSet) { + return true; + } + + int tag = TrafficStats.getThreadStatsTag(); + if (tag != -1) { + mTrafficStatsTag = tag; + mTrafficStatsTagSet = true; + } + + return mTrafficStatsTagSet; + } + + private boolean checkTrafficStatsUid() { + if (mTrafficStatsUidSet) { + return true; + } + + // TrafficStats#getThreadStatsUid() is available on API level 28+. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return false; + } + + int uid = TrafficStats.getThreadStatsUid(); + if (uid != -1) { + mTrafficStatsUid = uid; + mTrafficStatsUidSet = true; + } + + return mTrafficStatsUidSet; + } + + /** + * Returns an input stream from the server in the case of an error such as + * the requested file has not been found on the remote server. + */ + @Override + public InputStream getErrorStream() { + try { + getResponse(); + } catch (IOException e) { + return null; + } + if (mResponseInfo.getHttpStatusCode() >= HTTP_BAD_REQUEST) { + return mInputStream; + } + return null; + } + + /** + * Adds the given property to the request header. + */ + @Override + public final void addRequestProperty(String key, String value) { + setRequestPropertyInternal(key, value, false); + } + + /** + * Sets the value of the specified request header field. + */ + @Override + public final void setRequestProperty(String key, String value) { + setRequestPropertyInternal(key, value, true); + } + + private final void setRequestPropertyInternal(String key, String value, + boolean overwrite) { + if (connected) { + throw new IllegalStateException( + "Cannot modify request property after connection is made."); + } + int index = findRequestProperty(key); + if (index >= 0) { + if (overwrite) { + mRequestHeaders.remove(index); + } else { + // Cronet does not support adding multiple headers + // of the same key, see crbug.com/432719 for more details. + throw new UnsupportedOperationException( + "Cannot add multiple headers of the same key, " + key + + ". crbug.com/432719."); + } + } + // Adds the new header at the end of mRequestHeaders. + mRequestHeaders.add(Pair.create(key, value)); + } + + /** + * Returns an unmodifiable map of general request properties used by this + * connection. + */ + @Override + public Map> getRequestProperties() { + if (connected) { + throw new IllegalStateException( + "Cannot access request headers after connection is set."); + } + Map> map = new TreeMap>( + String.CASE_INSENSITIVE_ORDER); + for (Pair entry : mRequestHeaders) { + if (map.containsKey(entry.first)) { + // This should not happen due to setRequestPropertyInternal. + throw new IllegalStateException( + "Should not have multiple values."); + } else { + List values = new ArrayList(); + values.add(entry.second); + map.put(entry.first, Collections.unmodifiableList(values)); + } + } + return Collections.unmodifiableMap(map); + } + + /** + * Returns the value of the request header property specified by {@code + * key} or {@code null} if there is no key with this name. + */ + @Override + public String getRequestProperty(String key) { + int index = findRequestProperty(key); + if (index >= 0) { + return mRequestHeaders.get(index).second; + } + return null; + } + + /** + * Returns whether this connection uses a proxy server. + */ + @Override + public boolean usingProxy() { + // TODO(xunjieli): implement this. + return false; + } + + @Override + public void setConnectTimeout(int timeout) { + // Per-request connect timeout is not supported because of late binding. + // Sockets are assigned to requests according to request priorities + // when sockets are connected. This requires requests with the same host, + // domain and port to have same timeout. + Log.d(TAG, "setConnectTimeout is not supported by CronetHttpURLConnection"); + } + + /** + * Used by {@link CronetInputStream} to get more data from the network + * stack. This should only be called after the request has started. Note + * that this call might block if there isn't any more data to be read. + * Since byteBuffer is passed to the UrlRequest, it must be a direct + * ByteBuffer. + */ + void getMoreData(ByteBuffer byteBuffer) throws IOException { + mRequest.read(byteBuffer); + mMessageLoop.loop(getReadTimeout()); + } + + /** + * Sets {@link android.net.TrafficStats} tag to use when accounting socket traffic caused by + * this request. See {@link android.net.TrafficStats} for more information. If no tag is + * set (e.g. this method isn't called), then Android accounts for the socket traffic caused + * by this request as if the tag value were set to 0. + *

+ * NOTE:Setting a tag disallows sharing of sockets with requests + * with other tags, which may adversely effect performance by prohibiting + * connection sharing. In other words use of multiplexed sockets (e.g. HTTP/2 + * and QUIC) will only be allowed if all requests have the same socket tag. + * + * @param tag the tag value used to when accounting for socket traffic caused by this + * request. Tags between 0xFFFFFF00 and 0xFFFFFFFF are reserved and used + * internally by system services like {@link android.app.DownloadManager} when + * performing traffic on behalf of an application. + */ + public void setTrafficStatsTag(int tag) { + if (connected) { + throw new IllegalStateException( + "Cannot modify traffic stats tag after connection is made."); + } + mTrafficStatsTagSet = true; + mTrafficStatsTag = tag; + } + + /** + * Sets specific UID to use when accounting socket traffic caused by this request. See + * {@link android.net.TrafficStats} for more information. Designed for use when performing + * an operation on behalf of another application. Caller must hold + * {@link android.Manifest.permission#MODIFY_NETWORK_ACCOUNTING} permission. By default + * traffic is attributed to UID of caller. + *

+ * NOTE:Setting a UID disallows sharing of sockets with requests + * with other UIDs, which may adversely effect performance by prohibiting + * connection sharing. In other words use of multiplexed sockets (e.g. HTTP/2 + * and QUIC) will only be allowed if all requests have the same UID set. + * + * @param uid the UID to attribute socket traffic caused by this request. + */ + public void setTrafficStatsUid(int uid) { + if (connected) { + throw new IllegalStateException( + "Cannot modify traffic stats UID after connection is made."); + } + mTrafficStatsUidSet = true; + mTrafficStatsUid = uid; + } + + /** + * Returns the index of request header in {@link #mRequestHeaders} or + * -1 if not found. + */ + private int findRequestProperty(String key) { + for (int i = 0; i < mRequestHeaders.size(); i++) { + Pair entry = mRequestHeaders.get(i); + if (entry.first.equalsIgnoreCase(key)) { + return i; + } + } + return -1; + } + + private class CronetUrlRequestCallback extends UrlRequest.Callback { + public CronetUrlRequestCallback() {} + + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + mResponseInfo = info; + mHasResponseHeadersOrCompleted = true; + // Quits the message loop since we have the headers now. + mMessageLoop.quit(); + } + + @Override + public void onReadCompleted( + UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) { + mResponseInfo = info; + mMessageLoop.quit(); + } + + @Override + public void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) { + mOnRedirectCalled = true; + try { + URL newUrl = new URL(newLocationUrl); + boolean sameProtocol = newUrl.getProtocol().equals(url.getProtocol()); + if (instanceFollowRedirects) { + // Update the url variable even if the redirect will not be + // followed due to different protocols. + url = newUrl; + } + if (instanceFollowRedirects && sameProtocol) { + mRequest.followRedirect(); + return; + } + } catch (MalformedURLException e) { + // Ignored. Just cancel the request and not follow the redirect. + } + mResponseInfo = info; + mRequest.cancel(); + setResponseDataCompleted(null); + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + mResponseInfo = info; + setResponseDataCompleted(null); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException exception) { + if (exception == null) { + throw new IllegalStateException( + "Exception cannot be null in onFailed."); + } + mResponseInfo = info; + setResponseDataCompleted(exception); + } + + @Override + public void onCanceled(UrlRequest request, UrlResponseInfo info) { + mResponseInfo = info; + setResponseDataCompleted(new IOException("disconnect() called")); + } + + /** + * Notifies {@link #mInputStream} that transferring of response data has + * completed. + * @param exception if not {@code null}, it is the exception to report when + * caller tries to read more data. + */ + private void setResponseDataCompleted(IOException exception) { + mException = exception; + if (mInputStream != null) { + mInputStream.setResponseDataCompleted(exception); + } + if (mOutputStream != null) { + mOutputStream.setRequestCompleted(exception); + } + mHasResponseHeadersOrCompleted = true; + mMessageLoop.quit(); + } + } + + /** + * Blocks until the respone headers are received. + */ + private void getResponse() throws IOException { + // Check to see if enough data has been received. + if (mOutputStream != null) { + mOutputStream.checkReceivedEnoughContent(); + if (isChunkedUpload()) { + // Write last chunk. + mOutputStream.close(); + } + } + if (!mHasResponseHeadersOrCompleted) { + startRequest(); + // Blocks until onResponseStarted or onFailed is called. + mMessageLoop.loop(); + } + checkHasResponseHeaders(); + } + + /** + * Checks whether response headers are received, and throws an exception if + * an exception occurred before headers received. This method should only be + * called after onResponseStarted or onFailed. + */ + private void checkHasResponseHeaders() throws IOException { + if (!mHasResponseHeadersOrCompleted) throw new IllegalStateException("No response."); + if (mException != null) { + throw mException; + } else if (mResponseInfo == null) { + throw new NullPointerException( + "Response info is null when there is no exception."); + } + } + + /** + * Helper method to return the response header field at position pos. + */ + private Map.Entry getHeaderFieldEntry(int pos) { + try { + getResponse(); + } catch (IOException e) { + return null; + } + List> headers = getAllHeadersAsList(); + if (pos >= headers.size()) { + return null; + } + return headers.get(pos); + } + + /** + * Returns whether the client has used {@link #setChunkedStreamingMode} to + * set chunked encoding for upload. + */ + private boolean isChunkedUpload() { + return chunkLength > 0; + } + + // TODO(xunjieli): Refactor to reuse code in UrlResponseInfo. + private Map> getAllHeaders() { + if (mResponseHeadersMap != null) { + return mResponseHeadersMap; + } + Map> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (Map.Entry entry : getAllHeadersAsList()) { + List values = new ArrayList(); + if (map.containsKey(entry.getKey())) { + values.addAll(map.get(entry.getKey())); + } + values.add(entry.getValue()); + map.put(entry.getKey(), Collections.unmodifiableList(values)); + } + mResponseHeadersMap = Collections.unmodifiableMap(map); + return mResponseHeadersMap; + } + + private List> getAllHeadersAsList() { + if (mResponseHeadersList != null) { + return mResponseHeadersList; + } + mResponseHeadersList = new ArrayList>(); + for (Map.Entry entry : mResponseInfo.getAllHeadersAsList()) { + // Strips Content-Encoding response header. See crbug.com/592700. + if (!entry.getKey().equalsIgnoreCase("Content-Encoding")) { + mResponseHeadersList.add( + new AbstractMap.SimpleImmutableEntry(entry)); + } + } + mResponseHeadersList = Collections.unmodifiableList(mResponseHeadersList); + return mResponseHeadersList; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetHttpURLStreamHandler.java b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetHttpURLStreamHandler.java new file mode 100644 index 0000000000..8cd0eade65 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetHttpURLStreamHandler.java @@ -0,0 +1,50 @@ +// Copyright 2014 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.urlconnection; + +import org.chromium.net.ExperimentalCronetEngine; + +import java.io.IOException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +/** + * A {@link URLStreamHandler} that handles HTTP and HTTPS connections. One can use this class to + * create {@link java.net.HttpURLConnection} instances implemented by Cronet; for example:

+ *
+ * CronetHttpURLStreamHandler streamHandler = new CronetHttpURLStreamHandler(myContext);
+ * HttpURLConnection connection = (HttpURLConnection)streamHandler.openConnection(
+ *         new URL("http://chromium.org"));
+ * Note: Cronet's {@code HttpURLConnection} implementation is subject to some limitations + * listed {@link CronetURLStreamHandlerFactory here}. + */ +class CronetHttpURLStreamHandler extends URLStreamHandler { + private final ExperimentalCronetEngine mCronetEngine; + + public CronetHttpURLStreamHandler(ExperimentalCronetEngine cronetEngine) { + mCronetEngine = cronetEngine; + } + + /** + * Establishes a new connection to the resource specified by the {@link URL} {@code url}. + * @return an {@link java.net.HttpURLConnection} instance implemented by Cronet. + */ + @Override + public URLConnection openConnection(URL url) throws IOException { + return mCronetEngine.openConnection(url); + } + + /** + * Establishes a new connection to the resource specified by the {@link URL} {@code url} + * using the given proxy. + * @return an {@link java.net.HttpURLConnection} instance implemented by Cronet. + */ + @Override + public URLConnection openConnection(URL url, Proxy proxy) throws IOException { + return mCronetEngine.openConnection(url, proxy); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetInputStream.java b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetInputStream.java new file mode 100644 index 0000000000..cf6069c7a2 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetInputStream.java @@ -0,0 +1,118 @@ +// Copyright 2014 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.urlconnection; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * An InputStream that is used by {@link CronetHttpURLConnection} to request + * data from the network stack as needed. + */ +class CronetInputStream extends InputStream { + private final CronetHttpURLConnection mHttpURLConnection; + // Indicates whether listener's onSucceeded or onFailed callback is invoked. + private boolean mResponseDataCompleted; + private ByteBuffer mBuffer; + private IOException mException; + + private static final int READ_BUFFER_SIZE = 32 * 1024; + + /** + * Constructs a CronetInputStream. + * @param httpURLConnection the CronetHttpURLConnection that is associated + * with this InputStream. + */ + public CronetInputStream(CronetHttpURLConnection httpURLConnection) { + mHttpURLConnection = httpURLConnection; + } + + @Override + public int read() throws IOException { + getMoreDataIfNeeded(); + if (hasUnreadData()) { + return mBuffer.get() & 0xFF; + } + return -1; + } + + @Override + public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + if (byteOffset < 0 || byteCount < 0 || byteOffset + byteCount > buffer.length) { + throw new IndexOutOfBoundsException(); + } + if (byteCount == 0) { + return 0; + } + getMoreDataIfNeeded(); + if (hasUnreadData()) { + int bytesRead = Math.min(mBuffer.limit() - mBuffer.position(), byteCount); + mBuffer.get(buffer, byteOffset, bytesRead); + return bytesRead; + } + return -1; + } + + @Override + public int available() throws IOException { + if (mResponseDataCompleted) { + if (mException != null) { + throw mException; + } + return 0; + } + if (hasUnreadData()) { + return mBuffer.remaining(); + } else { + return 0; + } + } + + /** + * Called by {@link CronetHttpURLConnection} to notify that the entire + * response body has been read. + * @param exception if not {@code null}, it is the exception to throw when caller + * tries to read more data. + */ + void setResponseDataCompleted(IOException exception) { + mException = exception; + mResponseDataCompleted = true; + // Nothing else to read, so can free the buffer. + mBuffer = null; + } + + private void getMoreDataIfNeeded() throws IOException { + if (mResponseDataCompleted) { + if (mException != null) { + throw mException; + } + return; + } + if (!hasUnreadData()) { + // Allocate read buffer if needed. + if (mBuffer == null) { + mBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE); + } + mBuffer.clear(); + + // Requests more data from CronetHttpURLConnection. + mHttpURLConnection.getMoreData(mBuffer); + if (mException != null) { + throw mException; + } + if (mBuffer != null) { + mBuffer.flip(); + } + } + } + + /** + * Returns whether {@link #mBuffer} has unread data. + */ + private boolean hasUnreadData() { + return mBuffer != null && mBuffer.hasRemaining(); + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetOutputStream.java b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetOutputStream.java new file mode 100644 index 0000000000..c73426a6a9 --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetOutputStream.java @@ -0,0 +1,74 @@ +// 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.urlconnection; + +import org.chromium.net.UploadDataProvider; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * An abstract class of {@link OutputStream} that concrete implementations must + * extend in order to be used in {@link CronetHttpURLConnection}. + */ +abstract class CronetOutputStream extends OutputStream { + private IOException mException; + private boolean mClosed; + private boolean mRequestCompleted; + + @Override + public void close() throws IOException { + mClosed = true; + } + + /** + * Tells the underlying implementation that connection has been established. + * Used in {@link CronetHttpURLConnection}. + */ + abstract void setConnected() throws IOException; + + /** + * Checks whether content received is less than Content-Length. + * Used in {@link CronetHttpURLConnection}. + */ + abstract void checkReceivedEnoughContent() throws IOException; + + /** + * Returns {@link UploadDataProvider} implementation. + */ + abstract UploadDataProvider getUploadDataProvider(); + + /** + * Signals that the request is done. If there is no error, + * {@code exception} is null. Used by {@link CronetHttpURLConnection}. + */ + void setRequestCompleted(IOException exception) { + mException = exception; + mRequestCompleted = true; + } + + /** + * Throws an IOException if the stream is closed or the request is done. + */ + protected void checkNotClosed() throws IOException { + if (mRequestCompleted) { + checkNoException(); + throw new IOException("Writing after request completed."); + } + if (mClosed) { + throw new IOException("Stream has been closed."); + } + } + + /** + * Throws the same IOException that the request is failed with. If there + * is no exception reported, this method is no-op. + */ + protected void checkNoException() throws IOException { + if (mException != null) { + throw mException; + } + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetURLStreamHandlerFactory.java b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetURLStreamHandlerFactory.java new file mode 100644 index 0000000000..be10858d0d --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/CronetURLStreamHandlerFactory.java @@ -0,0 +1,69 @@ +// Copyright 2014 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.urlconnection; + +import org.chromium.net.ExperimentalCronetEngine; + +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; + +/** + * An implementation of {@link URLStreamHandlerFactory} to handle HTTP and HTTPS + * traffic. An instance of this class can be installed via + * {@link java.net.URL#setURLStreamHandlerFactory} thus using Cronet by default for all requests + * created via {@link java.net.URL#openConnection}. + *

+ * Cronet does not use certain HTTP features provided via the system: + *

    + *
  • the HTTP cache installed via + * {@link android.net.http.HttpResponseCache#install}
  • + *
  • the HTTP authentication method installed via + * {@link java.net.Authenticator#setDefault}
  • + *
  • the HTTP cookie storage installed via {@link java.net.CookieHandler#setDefault}
  • + *
+ *

+ * While Cronet supports and encourages requests using the HTTPS protocol, + * Cronet does not provide support for the + * {@link javax.net.ssl.HttpsURLConnection} API. This lack of support also + * includes not using certain HTTPS features provided via the system: + *

    + *
  • the HTTPS hostname verifier installed via {@link + * javax.net.ssl.HttpsURLConnection#setDefaultHostnameVerifier(javax.net.ssl.HostnameVerifier) + * HttpsURLConnection.setDefaultHostnameVerifier(javax.net.ssl.HostnameVerifier)}
  • + *
  • the HTTPS socket factory installed via {@link + * javax.net.ssl.HttpsURLConnection#setDefaultSSLSocketFactory(javax.net.ssl.SSLSocketFactory) + * HttpsURLConnection.setDefaultSSLSocketFactory(javax.net.ssl.SSLSocketFactory)}
  • + *
+ * + * {@hide} + */ +public class CronetURLStreamHandlerFactory implements URLStreamHandlerFactory { + private final ExperimentalCronetEngine mCronetEngine; + + /** + * Creates a {@link CronetURLStreamHandlerFactory} to handle HTTP and HTTPS + * traffic. + * @param cronetEngine the {@link CronetEngine} to be used. + * @throws NullPointerException if config is null. + */ + public CronetURLStreamHandlerFactory(ExperimentalCronetEngine cronetEngine) { + if (cronetEngine == null) { + throw new NullPointerException("CronetEngine is null."); + } + mCronetEngine = cronetEngine; + } + + /** + * Returns a {@link CronetHttpURLStreamHandler} for HTTP and HTTPS, and + * {@code null} for other protocols. + */ + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + if ("http".equals(protocol) || "https".equals(protocol)) { + return new CronetHttpURLStreamHandler(mCronetEngine); + } + return null; + } +} diff --git a/src/components/cronet/android/java/src/org/chromium/net/urlconnection/MessageLoop.java b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/MessageLoop.java new file mode 100644 index 0000000000..231c3e2c2e --- /dev/null +++ b/src/components/cronet/android/java/src/org/chromium/net/urlconnection/MessageLoop.java @@ -0,0 +1,177 @@ +// Copyright 2014 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.urlconnection; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.SocketTimeoutException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * A MessageLoop class for use in {@link CronetHttpURLConnection}. + */ +class MessageLoop implements Executor { + private final BlockingQueue mQueue; + + // Indicates whether this message loop is currently running. + private boolean mLoopRunning; + + // Indicates whether an InterruptedException or a RuntimeException has + // occurred in loop(). If true, the loop cannot be safely started because + // this might cause the loop to terminate immediately if there is a quit + // task enqueued. + private boolean mLoopFailed; + // The exception that caused mLoopFailed to be set to true. Will be + // rethrown if loop() is called again. If mLoopFailed is set then + // exactly one of mPriorInterruptedIOException and mPriorRuntimeException + // will be set. + private InterruptedIOException mPriorInterruptedIOException; + private RuntimeException mPriorRuntimeException; + + // Used when assertions are enabled to enforce single-threaded use. + private static final long INVALID_THREAD_ID = -1; + private long mThreadId = INVALID_THREAD_ID; + + MessageLoop() { + mQueue = new LinkedBlockingQueue(); + } + + private boolean calledOnValidThread() { + if (mThreadId == INVALID_THREAD_ID) { + mThreadId = Thread.currentThread().getId(); + return true; + } + return mThreadId == Thread.currentThread().getId(); + } + + /** + * Retrieves a task from the queue with the given timeout. + * + * @param useTimeout whether to use a timeout. + * @param timeoutNano Time to wait, in nanoseconds. + * @return A non-{@code null} Runnable from the queue. + * @throws InterruptedIOException + */ + private Runnable take(boolean useTimeout, long timeoutNano) throws InterruptedIOException { + Runnable task = null; + try { + if (!useTimeout) { + task = mQueue.take(); // Blocks if the queue is empty. + } else { + // poll returns null upon timeout. + task = mQueue.poll(timeoutNano, TimeUnit.NANOSECONDS); + } + } catch (InterruptedException e) { + InterruptedIOException exception = new InterruptedIOException(); + exception.initCause(e); + throw exception; + } + if (task == null) { + // This will terminate the loop. + throw new SocketTimeoutException(); + } + return task; + } + + /** + * Runs the message loop. Be sure to call {@link MessageLoop#quit()} + * to end the loop. If an interruptedException occurs, the loop cannot be + * started again (see {@link #mLoopFailed}). + * @throws IOException + */ + public void loop() throws IOException { + loop(0); + } + + /** + * Runs the message loop. Be sure to call {@link MessageLoop#quit()} + * to end the loop. If an interruptedException occurs, the loop cannot be + * started again (see {@link #mLoopFailed}). + * @param timeoutMilli Timeout, in milliseconds, or 0 for no timeout. + * @throws IOException + */ + public void loop(int timeoutMilli) throws IOException { + assert calledOnValidThread(); + // Use System.nanoTime() which is monotonically increasing. + long startNano = System.nanoTime(); + long timeoutNano = TimeUnit.NANOSECONDS.convert(timeoutMilli, TimeUnit.MILLISECONDS); + if (mLoopFailed) { + if (mPriorInterruptedIOException != null) { + throw mPriorInterruptedIOException; + } else { + throw mPriorRuntimeException; + } + } + if (mLoopRunning) { + throw new IllegalStateException( + "Cannot run loop when it is already running."); + } + mLoopRunning = true; + while (mLoopRunning) { + try { + if (timeoutMilli == 0) { + take(false, 0).run(); + } else { + take(true, timeoutNano - System.nanoTime() + startNano).run(); + } + } catch (InterruptedIOException e) { + mLoopRunning = false; + mLoopFailed = true; + mPriorInterruptedIOException = e; + throw e; + } catch (RuntimeException e) { + mLoopRunning = false; + mLoopFailed = true; + mPriorRuntimeException = e; + throw e; + } + } + } + + /** + * This causes {@link #loop()} to stop executing messages after the current + * message being executed. Should only be called from the currently + * executing message. + */ + public void quit() { + assert calledOnValidThread(); + mLoopRunning = false; + } + + /** + * Posts a task to the message loop. + */ + @Override + public void execute(Runnable task) throws RejectedExecutionException { + if (task == null) { + throw new IllegalArgumentException(); + } + try { + mQueue.put(task); + } catch (InterruptedException e) { + // In theory this exception won't happen, since we have an blocking + // queue with Integer.MAX_Value capacity, put() call will not block. + throw new RejectedExecutionException(e); + } + } + + /** + * Returns whether the loop is currently running. Used in testing. + */ + public boolean isRunning() { + return mLoopRunning; + } + + /** + * Returns whether an exception occurred in {#loop()}. Used in testing. + */ + public boolean hasLoopFailed() { + return mLoopFailed; + } +} diff --git a/src/components/cronet/android/lint-baseline.xml b/src/components/cronet/android/lint-baseline.xml new file mode 100644 index 0000000000..c72cc3e495 --- /dev/null +++ b/src/components/cronet/android/lint-baseline.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/src/components/cronet/android/lint-suppressions.xml b/src/components/cronet/android/lint-suppressions.xml new file mode 100644 index 0000000000..60179a6294 --- /dev/null +++ b/src/components/cronet/android/lint-suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/cronet/android/sample/AndroidManifest.xml b/src/components/cronet/android/sample/AndroidManifest.xml new file mode 100644 index 0000000000..1a722e5f64 --- /dev/null +++ b/src/components/cronet/android/sample/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/cronet/android/sample/README b/src/components/cronet/android/sample/README new file mode 100644 index 0000000000..ea26f5cc00 --- /dev/null +++ b/src/components/cronet/android/sample/README @@ -0,0 +1,174 @@ + +How to set up and run the sample app as an Android Studio project. + +Linux (Android Studio version 0.8.11 beta) +===== +(1) Launch Android Studio. + +(2) Choose "Import project". + - Navigate to the location of the sample soure code. + You should be looking at a directory named "sample" + containing a file named "AndroidManifest.xml". + - Pick a new destination for it. + +(3) Copy in the '.jar' files. + (a) Directly under the "app" directory of your project, + create a "libs" directory. Use a shell command if you like, + or use "File|New|Directory" from the menu. But note that + you only get "Directory" as an option if you are in + "Project" view, not "Android" view. "Project" models + the local machine's filesystem, but Android is a virtual + layout of files corresponding to the deployed hierarchy. + That is to say, do step (b) before step (a) if you're inclined. + (b) Toggle the view from "Android" to "Project" + in the selection list above the file hierarchy. + Otherwise you won't see "libs". + (c) Copy 'cronet.jar' and 'cronet_stub.jar' to "libs". + + [Also note that it is possible to leave the '.jar' files + in their original locations, though this seems to be + somewhat discouraged by convention] + +(4) Inform the IDE about the '.jar' files. + (a) Select both files at once. + (b) Bring up the context menu and choose "Add as Library". + (d) Confirm "OK" at the "Add to module" dialog. + [Note: the keyboard shortcut and/or main menu selection + for these steps seems to be missing from Android Studio. + If you prefer, the advice under problem #2 + in "Troubleshooting" below will perform the same thing + without reliance on menu selections] + +(5) Copy in the '.so' file. + (a) Under "app/src/main" create a directory named "jniLibs" + (b) Copy armeabi and ameabi-v7a into jniLibs, which should + contain only subdirectories, not directly a '.so' file + (c) The IDE will automatically know about these. + +(6) Click "Run". + +Troubleshooting: + +If you have vast swaths of red text (errors) in the edit window +for CronetSampleActivity, you should confirm that the requisite +jar files are present in 'build.gradle'. There are at least 2 +files which are named 'build.gradle'. Check them both. +You should observe the following lines [see footnote 1] + +dependencies { + compile file('libs/cronet.jar') + compile file('libs/cronet_stub.jar') +} + +If absent, the lines may be added by hand to the gradle file +which corresponds to the module named "app", and not the project +s a whole. You might have to press a "Sync" button in the IDE +which tells it to re-scan the 'build.gradle' files. + +(II) If the project builds but doesn't run, verify that the '.so' files +are present in your Android package (which is just a jar file in disguise): +% jar tf build/outputs/apk/app-debug.apk +AndroidManifest.xml +res/layout/cronet_sample_activity.xml +resource.arsc +classes.dex +lib/armeabi/libcronet.so +lib/armeabi-v7/libcronet.so +META-INF +etc + +If the '.so' files are not present, it is likely that Android Studio +misinterpreted the containing folder as "ordinary" source code, +which, due to lack of any special directive pertaining to it, failed +to be copied to the apk. One thing to check for is the spelling +of "jniLibs" - it must literally be that, with a capital "L" or +it won't work. This is a bit of magic that allows users without +the NDK (Native Development Kit) to deploy '.so' files. +[With the NDK, things are different because in that case you have to +produce the '.so' files, and create build rules to do so, +so the setup is naturally more flexible to begin with.] +As a visual cue that the folder has been recognized as special, +its icon should match that of the "res" (resources) folder +which resembles a tabbed manila folder with some extra cross-hatches +on the front, and not the icon of the "java" folder. +The marking on the icon signifies that its contents land on the +the target device. But to keep things interesting, the icon is +distinct visually only in "Android" view, not "Project" view. + +MacOS (Android Studio version 1.0.1) +===== +(0) You might or might not have to set a magic environment + variable as follows [see footnote 3]: + export STUDIO_JDK=/Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk + +(1) Launch Android Studio, which can be achieved from the command + line with + % open '/Applications/Android Studio.app' + +(2) Choose "Import Non-Android Studio Project" + (a) Navigate to the path containing "sample" + (b) Pick a place to put it, and choose "Finish" + +(3) If you are comfortable using shell commands to create directories, + you may proceed to step (3) for Linux above. + Otherwise, if you prefer to create directories from the UI, + proceed with the following steps. + +(4) Choose "File -> New -> Folder -> Assets Folder". + Check "Change Folder Location" and delete the entire + pathname that was there. Change it to 'libs' + which is conventional for pre-built jar files. + +(5) Copy and paste the two pre-built '.jar' files into 'assets'. + When you do this, it will say that the destination is + "app/libs". This is right. If you prefer to see + the file hierarchy as it really exists, you can change + the dropdown above the tree view from "android view" + to "project view". Or just keep in mind that assets = libs + at this level of the hierarchy. + +(6) Select both jar files that you added (Shift+click). + and pull up the menu for them (Ctrl+click). + Select "Add as library" + +(7) Choose "File -> New -> Folder -> JNI Folder". + Choose "Change Folder Location" + and change the name to "src/main/jniLibs" [see footnote 2] + +---- + +Footnotes: +[1] "compile file" as used in a dependency line means to package the + named jars into the apk, and not to make those files. + The opposite of this is "provided file" which assumes that the + jars exist on the device already (in whatever the standard + location is for systemwide jar files), and not that a file + has been externally provided to Android Studio. + +[2] The menu option to add JNI files assumes that you have the + NDK (Native Development Kit) installed and want to produce + files into the named directory. This is triggered by an + automatic rule that tries to look for C++ source code + and the NDK based on the existence of "src/main/jni". + Changing this directory to "src/main/jniLibs" is magical + in a different way: it informs Android Studio that you will + place precompiled binaries into that directory. + +[3] This has to do with differences between the JDK that the studio + runs in as distinct from the JDK that the studio understands + to be present on the target machine. + There is discussion of the issue in + https://code.google.com/p/android/issues/detail?id=82378 + +Additional notes: + +Ideally the two .jar files and one .so file could be delivered as one .aar +(Android Archive) file, but Android Studio will try to pull aar files from +a Maven repository without some workarounds that are about as much trouble +as adding in three separate files. +See https://code.google.com/p/android/issues/detail?id=55863 + +Additionally, it is unclear how to automate the creation of a '.aar' file +outside of Android Studio and Gradle. If the entire workflow were controlled +by Gradle, it would be one thing; but presently the cronet jars are +produced as artifacts of the Chromium build which uses Ninja. diff --git a/src/components/cronet/android/sample/javatests/AndroidManifest.xml b/src/components/cronet/android/sample/javatests/AndroidManifest.xml new file mode 100644 index 0000000000..2fd4b4b91c --- /dev/null +++ b/src/components/cronet/android/sample/javatests/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/src/components/cronet/android/sample/javatests/proguard.cfg b/src/components/cronet/android/sample/javatests/proguard.cfg new file mode 100644 index 0000000000..efce0ae10a --- /dev/null +++ b/src/components/cronet/android/sample/javatests/proguard.cfg @@ -0,0 +1,4 @@ +# Proguard settings for CronetSampleTestApk. +-keep class org.chromium.cronet_sample_apk.CronetSampleActivity { + *; +} diff --git a/src/components/cronet/android/sample/javatests/src/org/chromium/cronet_sample_apk/CronetSampleTest.java b/src/components/cronet/android/sample/javatests/src/org/chromium/cronet_sample_apk/CronetSampleTest.java new file mode 100644 index 0000000000..1f88e7800b --- /dev/null +++ b/src/components/cronet/android/sample/javatests/src/org/chromium/cronet_sample_apk/CronetSampleTest.java @@ -0,0 +1,130 @@ +// Copyright 2014 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.cronet_sample_apk; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.pressBack; +import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; + +import android.content.ComponentName; +import android.content.Intent; +import android.net.Uri; +import android.os.ConditionVariable; +import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.text.Editable; +import android.text.TextWatcher; +import android.widget.TextView; + +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.filters.SmallTest; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Base test class for all CronetSample based tests. + */ +@RunWith(AndroidJUnit4.class) +public class CronetSampleTest { + private final String mUrl = "https://localhost"; + + @Rule + public ActivityTestRule mActivityTestRule = + new ActivityTestRule<>(CronetSampleActivity.class, false, false); + + @Test + @SmallTest + public void testLoadUrl() throws Exception { + CronetSampleActivity activity = launchCronetSampleWithUrl(mUrl); + + // Make sure the activity was created as expected. + Assert.assertNotNull(activity); + + // Verify successful fetch. + final TextView textView = (TextView) activity.findViewById(R.id.resultView); + final ConditionVariable done = new ConditionVariable(); + final TextWatcher textWatcher = new TextWatcher() { + @Override + public void afterTextChanged(Editable s) {} + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (s.toString().startsWith("Failed " + mUrl + + " (Exception in CronetUrlRequest: net::ERR_CONNECTION_REFUSED")) { + done.open(); + } + } + }; + textView.addTextChangedListener(textWatcher); + // Check current text in case it changed before |textWatcher| was added. + textWatcher.onTextChanged(textView.getText(), 0, 0, 0); + done.block(); + } + + @Test + @SmallTest + public void testOpenUrlPromptWhenDataViewClicked() { + CronetSampleActivity activity = launchCronetSampleWithUrl(mUrl); + // Make sure the activity was created as expected. + Assert.assertNotNull(activity); + + // Blindly interacting with the view is racy as it is generated by a runnable spawned by the + // onFailed or onSucceeded methods (which are also asynchronously called after returning + // from launchCronetSampleWithUrl()). Unfortunately, there doesn't seem to be a simple way + // to synchronize with a runnable on the UI thread (?), so all we can do is spin. + boolean isWaitingForView = true; + while (isWaitingForView) { + isWaitingForView = false; + try { + onView(withId(R.id.urlView)).perform(pressBack()); + onView(withId(R.id.urlView)).check(doesNotExist()); + } catch (NoMatchingViewException e) { + isWaitingForView = true; + try { + final long sleepDurationMs = 200; + Thread.sleep(sleepDurationMs); + } catch (Exception e2) { + } + } + } + + // The activity begins with the url prompt showing. + // Press back on the url prompt to clear it from the screen. + final TextView dataView = (TextView) activity.findViewById(R.id.dataView); + final ConditionVariable done = new ConditionVariable(); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + dataView.performClick(); + done.open(); + } + }); + done.block(); + onView(withId(R.id.urlView)).check(matches(isDisplayed())); + } + + /** + * Starts the CronetSample activity and loads the given URL. + */ + protected CronetSampleActivity launchCronetSampleWithUrl(String url) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData(Uri.parse(url)); + intent.setComponent(new ComponentName( + InstrumentationRegistry.getTargetContext(), CronetSampleActivity.class)); + return mActivityTestRule.launchActivity(intent); + } +} diff --git a/src/components/cronet/android/sample/res/layout/activity_main.xml b/src/components/cronet/android/sample/res/layout/activity_main.xml new file mode 100644 index 0000000000..7fb02578f4 --- /dev/null +++ b/src/components/cronet/android/sample/res/layout/activity_main.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/src/components/cronet/android/sample/res/layout/dialog_url.xml b/src/components/cronet/android/sample/res/layout/dialog_url.xml new file mode 100644 index 0000000000..dec548d92f --- /dev/null +++ b/src/components/cronet/android/sample/res/layout/dialog_url.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + diff --git a/src/components/cronet/android/sample/res/values/dimens.xml b/src/components/cronet/android/sample/res/values/dimens.xml new file mode 100644 index 0000000000..47c8224673 --- /dev/null +++ b/src/components/cronet/android/sample/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/src/components/cronet/android/sample/res/values/strings.xml b/src/components/cronet/android/sample/res/values/strings.xml new file mode 100644 index 0000000000..6b9589d540 --- /dev/null +++ b/src/components/cronet/android/sample/res/values/strings.xml @@ -0,0 +1,12 @@ + + + + + + Enter a URL + Enter post data (leave it blank for GET request) + diff --git a/src/components/cronet/android/sample/src/org/chromium/cronet_sample_apk/CronetSampleActivity.java b/src/components/cronet/android/sample/src/org/chromium/cronet_sample_apk/CronetSampleActivity.java new file mode 100644 index 0000000000..0d72426a56 --- /dev/null +++ b/src/components/cronet/android/sample/src/org/chromium/cronet_sample_apk/CronetSampleActivity.java @@ -0,0 +1,200 @@ +// Copyright 2014 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.cronet_sample_apk; + +import android.app.Activity; +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; + +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetException; +import org.chromium.net.UploadDataProviders; +import org.chromium.net.UrlRequest; +import org.chromium.net.UrlResponseInfo; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * Activity for managing the Cronet Sample. + */ +public class CronetSampleActivity extends Activity { + private static final String TAG = CronetSampleActivity.class.getSimpleName(); + + private CronetEngine mCronetEngine; + + private String mUrl; + private TextView mResultText; + private TextView mReceiveDataText; + + class SimpleUrlRequestCallback extends UrlRequest.Callback { + private ByteArrayOutputStream mBytesReceived = new ByteArrayOutputStream(); + private WritableByteChannel mReceiveChannel = Channels.newChannel(mBytesReceived); + + @Override + public void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) { + Log.i(TAG, "****** onRedirectReceived ******"); + request.followRedirect(); + } + + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + Log.i(TAG, "****** Response Started ******"); + Log.i(TAG, "*** Headers Are *** " + info.getAllHeaders()); + + request.read(ByteBuffer.allocateDirect(32 * 1024)); + } + + @Override + public void onReadCompleted( + UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) { + byteBuffer.flip(); + Log.i(TAG, "****** onReadCompleted ******" + byteBuffer); + + try { + mReceiveChannel.write(byteBuffer); + } catch (IOException e) { + Log.i(TAG, "IOException during ByteBuffer read. Details: ", e); + } + byteBuffer.clear(); + request.read(byteBuffer); + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + Log.i(TAG, "****** Request Completed, status code is " + info.getHttpStatusCode() + + ", total received bytes is " + info.getReceivedByteCount()); + + final String receivedData = mBytesReceived.toString(); + final String url = info.getUrl(); + final String text = "Completed " + url + " (" + info.getHttpStatusCode() + ")"; + CronetSampleActivity.this.runOnUiThread(new Runnable() { + @Override + public void run() { + mResultText.setText(text); + mReceiveDataText.setText(receivedData); + promptForURL(url); + } + }); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) { + Log.i(TAG, "****** onFailed, error is: " + error.getMessage()); + + final String url = mUrl; + final String text = "Failed " + mUrl + " (" + error.getMessage() + ")"; + CronetSampleActivity.this.runOnUiThread(new Runnable() { + @Override + public void run() { + mResultText.setText(text); + promptForURL(url); + } + }); + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + mResultText = (TextView) findViewById(R.id.resultView); + mReceiveDataText = (TextView) findViewById(R.id.dataView); + mReceiveDataText.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + promptForURL(mUrl); + } + }); + + CronetEngine.Builder myBuilder = new CronetEngine.Builder(this); + myBuilder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 100 * 1024) + .enableHttp2(true) + .enableQuic(true); + + mCronetEngine = myBuilder.build(); + + String appUrl = (getIntent() != null ? getIntent().getDataString() : null); + if (appUrl == null) { + promptForURL("https://"); + } else { + startWithURL(appUrl); + } + } + + private void promptForURL(String url) { + Log.i(TAG, "No URL provided via intent, prompting user..."); + AlertDialog.Builder alert = new AlertDialog.Builder(this); + alert.setTitle("Enter a URL"); + LayoutInflater inflater = getLayoutInflater(); + View alertView = inflater.inflate(R.layout.dialog_url, null); + final EditText urlInput = (EditText) alertView.findViewById(R.id.urlText); + urlInput.setText(url); + final EditText postInput = (EditText) alertView.findViewById(R.id.postText); + alert.setView(alertView); + + alert.setPositiveButton("Load", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int button) { + String url = urlInput.getText().toString(); + String postData = postInput.getText().toString(); + startWithURL(url, postData); + } + }); + alert.show(); + } + + private void applyPostDataToUrlRequestBuilder( + UrlRequest.Builder builder, Executor executor, String postData) { + if (postData != null && postData.length() > 0) { + builder.setHttpMethod("POST"); + builder.addHeader("Content-Type", "application/x-www-form-urlencoded"); + builder.setUploadDataProvider( + UploadDataProviders.create(postData.getBytes()), executor); + } + } + + private void startWithURL(String url) { + startWithURL(url, null); + } + + private void startWithURL(String url, String postData) { + Log.i(TAG, "Cronet started: " + url); + mUrl = url; + + Executor executor = Executors.newSingleThreadExecutor(); + UrlRequest.Callback callback = new SimpleUrlRequestCallback(); + UrlRequest.Builder builder = mCronetEngine.newUrlRequestBuilder(url, callback, executor); + applyPostDataToUrlRequestBuilder(builder, executor, postData); + builder.build().start(); + } + + // Starts writing NetLog to disk. startNetLog() should be called afterwards. + private void startNetLog() { + mCronetEngine.startNetLogToFile(getCacheDir().getPath() + "/netlog.json", false); + } + + // Stops writing NetLog to disk. Should be called after calling startNetLog(). + // NetLog can be downloaded afterwards via: + // adb root + // adb pull /data/data/org.chromium.cronet_sample_apk/cache/netlog.json + // netlog.json can then be viewed in a Chrome tab navigated to chrome://net-internals/#import + private void stopNetLog() { + mCronetEngine.stopNetLog(); + } +} diff --git a/src/components/cronet/android/sample/src/org/chromium/cronet_sample_apk/CronetSampleApplication.java b/src/components/cronet/android/sample/src/org/chromium/cronet_sample_apk/CronetSampleApplication.java new file mode 100644 index 0000000000..dc19cffbc9 --- /dev/null +++ b/src/components/cronet/android/sample/src/org/chromium/cronet_sample_apk/CronetSampleApplication.java @@ -0,0 +1,13 @@ +// Copyright 2014 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.cronet_sample_apk; + +import android.app.Application; + +/** + * Application for managing the Cronet Sample. + */ +public class CronetSampleApplication extends Application { +} diff --git a/src/components/cronet/android/test/cronet_test_jni.cc b/src/components/cronet/android/test/cronet_test_jni.cc new file mode 100644 index 0000000000..ec1d70e704 --- /dev/null +++ b/src/components/cronet/android/test/cronet_test_jni.cc @@ -0,0 +1,24 @@ +// Copyright 2014 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 + +#include "base/android/base_jni_onload.h" +#include "base/android/jni_android.h" +#include "base/android/jni_registrar.h" +#include "base/android/library_loader/library_loader_hooks.h" + +// This is called by the VM when the shared library is first loaded. +// Checks the available version of JNI. Also, caches Java reflection artifacts. +extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) { + base::android::InitVM(vm); + if (!base::android::OnJNIOnLoadInit()) { + return -1; + } + return JNI_VERSION_1_6; +} + +extern "C" void JNI_OnUnLoad(JavaVM* vm, void* reserved) { + base::android::LibraryLoaderExitHook(); +} diff --git a/src/components/cronet/android/test/cronet_test_util.cc b/src/components/cronet/android/test/cronet_test_util.cc new file mode 100644 index 0000000000..86de253690 --- /dev/null +++ b/src/components/cronet/android/test/cronet_test_util.cc @@ -0,0 +1,133 @@ +// Copyright 2016 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 "components/cronet/android/test/cronet_test_util.h" + +#include "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/bind.h" +#include "base/message_loop/message_pump.h" +#include "base/message_loop/message_pump_type.h" +#include "base/task/sequence_manager/sequence_manager.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/cronet/android/cronet_tests_jni_headers/CronetTestUtil_jni.h" +#include "components/cronet/android/cronet_url_request_adapter.h" +#include "components/cronet/android/cronet_url_request_context_adapter.h" +#include "components/cronet/cronet_url_request.h" +#include "components/cronet/cronet_url_request_context.h" +#include "net/socket/socket_test_util.h" +#include "net/url_request/url_request.h" + +namespace cronet { +namespace { + +using ::base::MessagePump; +using ::base::MessagePumpType; +using ::base::android::JavaParamRef; +using ::base::sequence_manager::SequenceManager; + +SequenceManager* g_sequence_manager = nullptr; + +} // namespace + +jint JNI_CronetTestUtil_GetLoadFlags(JNIEnv* env, + const jlong jurl_request_adapter) { + return TestUtil::GetURLRequest(jurl_request_adapter)->load_flags(); +} + +// static +scoped_refptr TestUtil::GetTaskRunner( + jlong jcontext_adapter) { + CronetURLRequestContextAdapter* context_adapter = + reinterpret_cast(jcontext_adapter); + return context_adapter->context_->network_task_runner_; +} + +// static +net::URLRequestContext* TestUtil::GetURLRequestContext(jlong jcontext_adapter) { + CronetURLRequestContextAdapter* context_adapter = + reinterpret_cast(jcontext_adapter); + return context_adapter->context_->network_tasks_->context_.get(); +} + +// static +void TestUtil::RunAfterContextInitOnNetworkThread(jlong jcontext_adapter, + base::OnceClosure task) { + CronetURLRequestContextAdapter* context_adapter = + reinterpret_cast(jcontext_adapter); + if (context_adapter->context_->network_tasks_->is_context_initialized_) { + std::move(task).Run(); + } else { + context_adapter->context_->network_tasks_->tasks_waiting_for_context_.push( + std::move(task)); + } +} + +// static +void TestUtil::RunAfterContextInit(jlong jcontext_adapter, + base::OnceClosure task) { + GetTaskRunner(jcontext_adapter) + ->PostTask(FROM_HERE, + base::BindOnce(&TestUtil::RunAfterContextInitOnNetworkThread, + jcontext_adapter, std::move(task))); +} + +// static +net::URLRequest* TestUtil::GetURLRequest(jlong jrequest_adapter) { + CronetURLRequestAdapter* request_adapter = + reinterpret_cast(jrequest_adapter); + return request_adapter->request_->network_tasks_.url_request_.get(); +} + +static void PrepareNetworkThreadOnNetworkThread(jlong jcontext_adapter) { + g_sequence_manager = + base::sequence_manager::CreateSequenceManagerOnCurrentThreadWithPump( + MessagePump::Create(MessagePumpType::IO), + SequenceManager::Settings::Builder() + .SetMessagePumpType(MessagePumpType::IO) + .Build()) + .release(); + g_sequence_manager->SetDefaultTaskRunner( + TestUtil::GetTaskRunner(jcontext_adapter)); +} + +// Tests need to call into libcronet.so code on libcronet.so threads. +// libcronet.so's threads are registered with static tables for MessageLoops +// and SingleThreadTaskRunners in libcronet.so, so libcronet_test.so +// functions that try and access these tables will find missing entries in +// the corresponding static tables in libcronet_test.so. Fix this by +// initializing a MessageLoop and SingleThreadTaskRunner in libcronet_test.so +// for these threads. Called from Java CronetTestUtil class. +void JNI_CronetTestUtil_PrepareNetworkThread( + JNIEnv* env, + jlong jcontext_adapter) { + TestUtil::GetTaskRunner(jcontext_adapter) + ->PostTask(FROM_HERE, base::BindOnce(&PrepareNetworkThreadOnNetworkThread, + jcontext_adapter)); +} + +static void CleanupNetworkThreadOnNetworkThread() { + DCHECK(g_sequence_manager); + delete g_sequence_manager; + g_sequence_manager = nullptr; +} + +// Called from Java CronetTestUtil class. +void JNI_CronetTestUtil_CleanupNetworkThread( + JNIEnv* env, + jlong jcontext_adapter) { + TestUtil::RunAfterContextInit( + jcontext_adapter, base::BindOnce(&CleanupNetworkThreadOnNetworkThread)); +} + +jboolean JNI_CronetTestUtil_CanGetTaggedBytes(JNIEnv* env) { + return net::CanGetTaggedBytes(); +} + +jlong JNI_CronetTestUtil_GetTaggedBytes(JNIEnv* env, + jint jexpected_tag) { + return net::GetTaggedBytes(jexpected_tag); +} + +} // namespace cronet diff --git a/src/components/cronet/android/test/cronet_test_util.h b/src/components/cronet/android/test/cronet_test_util.h new file mode 100644 index 0000000000..b6f49ff599 --- /dev/null +++ b/src/components/cronet/android/test/cronet_test_util.h @@ -0,0 +1,53 @@ +// Copyright 2016 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_TEST_CRONET_TEST_UTIL_H_ +#define COMPONENTS_CRONET_ANDROID_TEST_CRONET_TEST_UTIL_H_ + +#include +#include "base/android/jni_android.h" +#include "base/memory/ref_counted.h" +#include "base/task/single_thread_task_runner.h" + +namespace net { +class URLRequest; +class URLRequestContext; +} // namespace net + +namespace cronet { + +// Various test utility functions for testing Cronet. +// NOTE(pauljensen): This class is friended by Cronet internal implementation +// classes to provide access to internals. +class TestUtil { + public: + TestUtil() = delete; + TestUtil(const TestUtil&) = delete; + TestUtil& operator=(const TestUtil&) = delete; + + // CronetURLRequestContextAdapter manipulation: + + // Returns SingleThreadTaskRunner for the network thread of the context + // adapter. + static scoped_refptr GetTaskRunner( + jlong jcontext_adapter); + // Returns underlying URLRequestContext. + static net::URLRequestContext* GetURLRequestContext(jlong jcontext_adapter); + // Run |task| after URLRequestContext is initialized. + static void RunAfterContextInit(jlong jcontext_adapter, + base::OnceClosure task); + + // CronetURLRequestAdapter manipulation: + + // Returns underlying URLRequest. + static net::URLRequest* GetURLRequest(jlong jrequest_adapter); + + private: + static void RunAfterContextInitOnNetworkThread(jlong jcontext_adapter, + base::OnceClosure task); +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_TEST_CRONET_TEST_UTIL_H_ diff --git a/src/components/cronet/android/test/cronet_url_request_context_config_test.cc b/src/components/cronet/android/test/cronet_url_request_context_config_test.cc new file mode 100644 index 0000000000..b751f849fa --- /dev/null +++ b/src/components/cronet/android/test/cronet_url_request_context_config_test.cc @@ -0,0 +1,70 @@ +// 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_url_request_context_config_test.h" + +#include + +#include "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/android/scoped_java_ref.h" +#include "base/check_op.h" +#include "components/cronet/android/cronet_tests_jni_headers/CronetUrlRequestContextTest_jni.h" +#include "components/cronet/url_request_context_config.h" +#include "components/cronet/version.h" + +using base::android::JavaParamRef; + +namespace cronet { + +// Verifies that all the configuration options set by +// CronetUrlRequestContextTest.testCronetEngineBuilderConfig +// made it from the CronetEngine.Builder to the URLRequestContextConfig. +static void JNI_CronetUrlRequestContextTest_VerifyUrlRequestContextConfig( + JNIEnv* env, + jlong jurl_request_context_config, + const JavaParamRef& jstorage_path) { + URLRequestContextConfig* config = + reinterpret_cast(jurl_request_context_config); + CHECK_EQ(config->enable_spdy, false); + CHECK_EQ(config->enable_quic, true); + CHECK_EQ(config->bypass_public_key_pinning_for_local_trust_anchors, false); + CHECK_EQ(config->quic_hints.size(), 1u); + CHECK_EQ((*config->quic_hints.begin())->host, "example.com"); + CHECK_EQ((*config->quic_hints.begin())->port, 12); + CHECK_EQ((*config->quic_hints.begin())->alternate_port, 34); + CHECK_NE(config->quic_user_agent_id.find("Cronet/" CRONET_VERSION), + std::string::npos); + CHECK_EQ(config->load_disable_cache, false); + CHECK_EQ(config->http_cache, URLRequestContextConfig::HttpCacheType::MEMORY); + CHECK_EQ(config->http_cache_max_size, 54321); + CHECK_EQ(config->user_agent, "efgh"); + CHECK(config->effective_experimental_options.empty()); + CHECK_EQ(config->storage_path, + base::android::ConvertJavaStringToUTF8(env, jstorage_path)); +} + +// Verify that QUIC can be turned off in CronetEngine.Builder. +// Note that the config expectation is hard coded here because it's very hard to +// create an expected config in the JAVA package. +static void +JNI_CronetUrlRequestContextTest_VerifyUrlRequestContextQuicOffConfig( + JNIEnv* env, + jlong jurl_request_context_config, + const JavaParamRef& jstorage_path) { + URLRequestContextConfig* config = + reinterpret_cast(jurl_request_context_config); + CHECK_EQ(config->enable_spdy, false); + CHECK_EQ(config->enable_quic, false); + CHECK_EQ(config->bypass_public_key_pinning_for_local_trust_anchors, false); + CHECK_EQ(config->load_disable_cache, false); + CHECK_EQ(config->http_cache, URLRequestContextConfig::HttpCacheType::MEMORY); + CHECK_EQ(config->http_cache_max_size, 54321); + CHECK_EQ(config->user_agent, "efgh"); + CHECK(config->effective_experimental_options.empty()); + CHECK_EQ(config->storage_path, + base::android::ConvertJavaStringToUTF8(env, jstorage_path)); +} + +} // namespace cronet diff --git a/src/components/cronet/android/test/cronet_url_request_context_config_test.h b/src/components/cronet/android/test/cronet_url_request_context_config_test.h new file mode 100644 index 0000000000..ff6f3aca48 --- /dev/null +++ b/src/components/cronet/android/test/cronet_url_request_context_config_test.h @@ -0,0 +1,16 @@ +// 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_TEST_CRONET_URL_REQUEST_CONTEXT_CONFIG_TEST_H_ +#define COMPONENTS_CRONET_ANDROID_TEST_CRONET_URL_REQUEST_CONTEXT_CONFIG_TEST_H_ + +#include + +namespace cronet { + +bool RegisterCronetUrlRequestContextConfigTest(JNIEnv* env); + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_TEST_CRONET_URL_REQUEST_CONTEXT_CONFIG_TEST_H_ diff --git a/src/components/cronet/android/test/cronet_url_request_test.cc b/src/components/cronet/android/test/cronet_url_request_test.cc new file mode 100644 index 0000000000..b4aa1b3ff6 --- /dev/null +++ b/src/components/cronet/android/test/cronet_url_request_test.cc @@ -0,0 +1,13 @@ +// Copyright 2018 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 "components/cronet/android/cronet_tests_jni_headers/CronetUrlRequestTest_jni.h" +#include "net/base/load_flags.h" + +using base::android::JavaParamRef; + +static jint JNI_CronetUrlRequestTest_GetConnectionMigrationDisableLoadFlag( + JNIEnv* env) { + return net::LOAD_DISABLE_CONNECTION_MIGRATION_TO_CELLULAR; +} diff --git a/src/components/cronet/android/test/experimental_options_test.cc b/src/components/cronet/android/test/experimental_options_test.cc new file mode 100644 index 0000000000..a4fa3268ab --- /dev/null +++ b/src/components/cronet/android/test/experimental_options_test.cc @@ -0,0 +1,73 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/android/scoped_java_ref.h" +#include "base/bind.h" +#include "base/time/time.h" +#include "components/cronet/android/cronet_tests_jni_headers/ExperimentalOptionsTest_jni.h" +#include "components/cronet/android/test/cronet_test_util.h" +#include "components/cronet/url_request_context_config.h" +#include "net/base/address_family.h" +#include "net/base/net_errors.h" +#include "net/base/network_isolation_key.h" +#include "net/dns/host_cache.h" +#include "net/dns/host_resolver.h" +#include "net/dns/public/dns_query_type.h" +#include "net/dns/public/host_resolver_source.h" +#include "net/url_request/url_request_context.h" + +using base::android::JavaParamRef; + +namespace cronet { + +namespace { +void WriteToHostCacheOnNetworkThread(jlong jcontext_adapter, + const std::string& address_string) { + net::URLRequestContext* context = + TestUtil::GetURLRequestContext(jcontext_adapter); + net::HostCache* cache = context->host_resolver()->GetHostCache(); + const std::string hostname = "host-cache-test-host"; + + // Create multiple keys to ensure the test works in a variety of network + // conditions. + net::HostCache::Key key1(hostname, net::DnsQueryType::UNSPECIFIED, 0, + net::HostResolverSource::ANY, + net::NetworkIsolationKey()); + net::HostCache::Key key2(hostname, net::DnsQueryType::A, + net::HOST_RESOLVER_DEFAULT_FAMILY_SET_DUE_TO_NO_IPV6, + net::HostResolverSource::ANY, + net::NetworkIsolationKey()); + + net::IPAddress address; + CHECK(address.AssignFromIPLiteral(address_string)); + net::AddressList address_list = + net::AddressList::CreateFromIPAddress(address, 0); + net::HostCache::Entry entry(net::OK, address_list, + net::HostCache::Entry::SOURCE_UNKNOWN); + cache->Set(key1, entry, base::TimeTicks::Now(), base::Seconds(1)); + cache->Set(key2, entry, base::TimeTicks::Now(), base::Seconds(1)); +} +} // namespace + +static void JNI_ExperimentalOptionsTest_WriteToHostCache( + JNIEnv* env, + jlong jcontext_adapter, + const JavaParamRef& jaddress) { + TestUtil::RunAfterContextInit( + jcontext_adapter, + base::BindOnce(&WriteToHostCacheOnNetworkThread, jcontext_adapter, + base::android::ConvertJavaStringToUTF8(env, jaddress))); +} + +static jboolean +JNI_ExperimentalOptionsTest_ExperimentalOptionsParsingIsAllowedToFail( + JNIEnv* env) { + return URLRequestContextConfig::ExperimentalOptionsParsingIsAllowedToFail(); +} + +} // namespace cronet diff --git a/src/components/cronet/android/test/javaperftests/AndroidManifest.xml b/src/components/cronet/android/test/javaperftests/AndroidManifest.xml new file mode 100644 index 0000000000..35548fcb7e --- /dev/null +++ b/src/components/cronet/android/test/javaperftests/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/src/components/cronet/android/test/javaperftests/expectations.config b/src/components/cronet/android/test/javaperftests/expectations.config new file mode 100644 index 0000000000..077755a06c --- /dev/null +++ b/src/components/cronet/android/test/javaperftests/expectations.config @@ -0,0 +1,8 @@ +# Test Expectation file for javaperftests. + +# tags: All Android_Svelte Android_Webview Android_but_not_webview Mac Win Linux +# tags: ChromeOS Android Desktop Mobile Nexus_5 Nexus_5X Nexus_6 Nexus_6P +# tags: Nexus_7 Cherry_Mobile_Android_One Mac_10.11 Mac_10.12 Nexus6_Webview +# tags: Nexus5X_Webview + +# Benchmark: run.CronetPerfTestBenchmark diff --git a/src/components/cronet/android/test/javaperftests/run.py b/src/components/cronet/android/test/javaperftests/run.py new file mode 100755 index 0000000000..bbbace433a --- /dev/null +++ b/src/components/cronet/android/test/javaperftests/run.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python +# 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. + +"""This script runs an automated Cronet performance benchmark. + +This script: +1. Sets up "USB reverse tethering" which allow network traffic to flow from + an Android device connected to the host machine via a USB cable. +2. Starts HTTP and QUIC servers on the host machine. +3. Installs an Android app on the attached Android device and runs it. +4. Collects the results from the app. + +Prerequisites: +1. A rooted (i.e. "adb root" succeeds) Android device connected via a USB cable + to the host machine (i.e. the computer running this script). +2. quic_server has been built for the host machine, e.g. via: + gn gen out/Release --args="is_debug=false" + ninja -C out/Release quic_server +3. cronet_perf_test_apk has been built for the Android device, e.g. via: + ./components/cronet/tools/cr_cronet.py gn -r + ninja -C out/Release cronet_perf_test_apk +4. If "sudo ufw status" doesn't say "Status: inactive", run "sudo ufw disable". +5. sudo apt-get install lighttpd +6. If the usb0 interface on the host keeps losing it's IPv4 address + (WaitFor(HasHostAddress) will keep failing), NetworkManager may need to be + told to leave usb0 alone with these commands: + sudo bash -c "printf \"\\n[keyfile]\ + \\nunmanaged-devices=interface-name:usb0\\n\" \ + >> /etc/NetworkManager/NetworkManager.conf" + sudo service network-manager restart + +Invocation: +./run.py + +Output: +Benchmark timings are output by telemetry to stdout and written to +./results.html + +""" + +import json +import optparse +import os +import shutil +import sys +import tempfile +import time +import six.moves.urllib_parse # pylint: disable=import-error + +REPOSITORY_ROOT = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', '..', '..', '..')) + +sys.path.append(os.path.join(REPOSITORY_ROOT, 'tools', 'perf')) +sys.path.append(os.path.join(REPOSITORY_ROOT, 'build', 'android')) +sys.path.append(os.path.join(REPOSITORY_ROOT, 'components')) + +# pylint: disable=wrong-import-position +from chrome_telemetry_build import chromium_config +from devil.android import device_utils +from devil.android.sdk import intent +from core import benchmark_runner +from cronet.tools import android_rndis_forwarder +from cronet.tools import perf_test_utils +import lighttpd_server +from pylib import constants +from telemetry import android +from telemetry import benchmark +from telemetry import story as story_module +from telemetry.web_perf import timeline_based_measurement +# pylint: enable=wrong-import-position + + +def GetDevice(): + devices = device_utils.DeviceUtils.HealthyDevices() + assert len(devices) == 1 + return devices[0] + + +class CronetPerfTestAndroidStory(android.AndroidStory): + # Android AppStory implementation wrapping CronetPerfTest app. + # Launches Cronet perf test app and waits for execution to complete + # by waiting for presence of DONE_FILE. + + def __init__(self, device): + self._device = device + config = perf_test_utils.GetConfig(device) + device.RemovePath(config['DONE_FILE'], force=True) + self.url ='http://dummy/?' + six.moves.urllib_parse.urlencode(config) + start_intent = intent.Intent( + package=perf_test_utils.APP_PACKAGE, + activity=perf_test_utils.APP_ACTIVITY, + action=perf_test_utils.APP_ACTION, + # |config| maps from configuration value names to the configured values. + # |config| is encoded as URL parameter names and values and passed to + # the Cronet perf test app via the Intent data field. + data=self.url, + extras=None, + category=None) + super(CronetPerfTestAndroidStory, self).__init__( + start_intent, name='CronetPerfTest', + # No reason to wait for app; Run() will wait for results. By default + # StartActivity will timeout waiting for CronetPerfTest, so override + # |is_app_ready_predicate| to not wait. + is_app_ready_predicate=lambda app: True) + + def Run(self, shared_user_story_state): + while not self._device.FileExists( + perf_test_utils.GetConfig(self._device)['DONE_FILE']): + time.sleep(1.0) + + +class CronetPerfTestStorySet(story_module.StorySet): + + def __init__(self, device): + super(CronetPerfTestStorySet, self).__init__() + # Create and add Cronet perf test AndroidStory. + self.AddStory(CronetPerfTestAndroidStory(device)) + + +class CronetPerfTestMeasurement( + timeline_based_measurement.TimelineBasedMeasurement): + # For now AndroidStory's SharedAppState works only with + # TimelineBasedMeasurements, so implement one that just forwards results from + # Cronet perf test app. + + def __init__(self, device, options): + super(CronetPerfTestMeasurement, self).__init__(options) + self._device = device + + def WillRunStory(self, platform, story=None): + # Skip parent implementation which doesn't apply to Cronet perf test app as + # it is not a browser with a timeline interface. + pass + + def Measure(self, platform, results): + # Reads results from |RESULTS_FILE| on target and adds to |results|. + jsonResults = json.loads(self._device.ReadFile( + perf_test_utils.GetConfig(self._device)['RESULTS_FILE'])) + for test in jsonResults: + results.AddMeasurement(test, 'ms', jsonResults[test]) + + def DidRunStory(self, platform, results): + # Skip parent implementation which calls into tracing_controller which this + # doesn't have. + pass + + +class CronetPerfTestBenchmark(benchmark.Benchmark): + # Benchmark implementation spawning off Cronet perf test measurement and + # StorySet. + SUPPORTED_PLATFORMS = [story_module.expectations.ALL_ANDROID] + + def __init__(self, max_failures=None): + super(CronetPerfTestBenchmark, self).__init__(max_failures) + self._device = GetDevice() + + def CreatePageTest(self, options): + return CronetPerfTestMeasurement(self._device, options) + + def CreateStorySet(self, options): + return CronetPerfTestStorySet(self._device) + + +def main(): + parser = optparse.OptionParser() + parser.add_option('--output-format', default='html', + help='The output format of the results file.') + parser.add_option('--output-dir', default=None, + help='The directory for the output file. Default value is ' + 'the base directory of this script.') + options, _ = parser.parse_args() + constants.SetBuildType(perf_test_utils.BUILD_TYPE) + # Install APK + device = GetDevice() + device.EnableRoot() + device.Install(perf_test_utils.APP_APK) + # Start USB reverse tethering. + android_rndis_forwarder.AndroidRndisForwarder(device, + perf_test_utils.GetAndroidRndisConfig(device)) + # Start HTTP server. + http_server_doc_root = perf_test_utils.GenerateHttpTestResources() + config_file = tempfile.NamedTemporaryFile() + http_server = lighttpd_server.LighttpdServer(http_server_doc_root, + port=perf_test_utils.HTTP_PORT, + base_config_path=config_file.name) + perf_test_utils.GenerateLighttpdConfig(config_file, http_server_doc_root, + http_server) + assert http_server.StartupHttpServer() + config_file.close() + # Start QUIC server. + quic_server_doc_root = perf_test_utils.GenerateQuicTestResources(device) + quic_server = perf_test_utils.QuicServer(quic_server_doc_root) + quic_server.StartupQuicServer(device) + # Launch Telemetry's benchmark_runner on CronetPerfTestBenchmark. + # By specifying this file's directory as the benchmark directory, it will + # allow benchmark_runner to in turn open this file up and find the + # CronetPerfTestBenchmark class to run the benchmark. + top_level_dir = os.path.dirname(os.path.realpath(__file__)) + expectations_files = [os.path.join(top_level_dir, 'expectations.config')] + runner_config = chromium_config.ChromiumConfig( + top_level_dir=top_level_dir, + benchmark_dirs=[top_level_dir], + expectations_files=expectations_files) + sys.argv.insert(1, 'run') + sys.argv.insert(2, 'run.CronetPerfTestBenchmark') + sys.argv.insert(3, '--browser=android-system-chrome') + sys.argv.insert(4, '--output-format=' + options.output_format) + if options.output_dir: + sys.argv.insert(5, '--output-dir=' + options.output_dir) + benchmark_runner.main(runner_config) + # Shutdown. + quic_server.ShutdownQuicServer() + shutil.rmtree(quic_server_doc_root) + http_server.ShutdownHttpServer() + shutil.rmtree(http_server_doc_root) + + +if __name__ == '__main__': + main() diff --git a/src/components/cronet/android/test/javaperftests/src/org/chromium/net/CronetPerfTestActivity.java b/src/components/cronet/android/test/javaperftests/src/org/chromium/net/CronetPerfTestActivity.java new file mode 100644 index 0000000000..352a1ea98b --- /dev/null +++ b/src/components/cronet/android/test/javaperftests/src/org/chromium/net/CronetPerfTestActivity.java @@ -0,0 +1,622 @@ +// 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.app.Activity; +import android.net.Uri; +import android.os.Bundle; +import android.os.Debug; + +import org.json.JSONException; +import org.json.JSONObject; + +import org.chromium.base.ContextUtils; +import org.chromium.base.PathUtils; +import org.chromium.base.task.PostTask; +import org.chromium.base.task.TaskTraits; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Runs networking benchmarks and saves results to a file. + */ +public class CronetPerfTestActivity extends Activity { + private static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "cronet_perf_test"; + // Benchmark configuration passed down from host via Intent data. + // Call getConfig*(key) to extract individual configuration values. + private Uri mConfig; + + // Functions that retrieve individual benchmark configuration values. + private String getConfigString(String key) { + return mConfig.getQueryParameter(key); + } + private int getConfigInt(String key) { + return Integer.parseInt(mConfig.getQueryParameter(key)); + } + private boolean getConfigBoolean(String key) { + return Boolean.parseBoolean(mConfig.getQueryParameter(key)); + } + + private enum Mode { + SYSTEM_HUC, // Benchmark system HttpURLConnection + CRONET_HUC, // Benchmark Cronet's HttpURLConnection + CRONET_ASYNC, // Benchmark Cronet's asynchronous API + } + private enum Direction { + UP, // Benchmark upload (i.e. POST) + DOWN, // Benchmark download (i.e. GET) + } + private enum Size { + LARGE, // Large benchmark + SMALL, // Small benchmark + } + private enum Protocol { + HTTP, + QUIC, + } + + // Put together a benchmark configuration into a benchmark name. + // Make it fixed length for more readable tables. + // Benchmark names are written to the JSON output file and slurped up by Telemetry on the host. + private static String buildBenchmarkName( + Mode mode, Direction direction, Protocol protocol, int concurrency, int iterations) { + String name = direction == Direction.UP ? "Up___" : "Down_"; + switch (protocol) { + case HTTP: + name += "H_"; + break; + case QUIC: + name += "Q_"; + break; + default: + throw new IllegalArgumentException("Unknown protocol: " + protocol); + } + name += iterations + "_" + concurrency + "_"; + switch (mode) { + case SYSTEM_HUC: + name += "SystemHUC__"; + break; + case CRONET_HUC: + name += "CronetHUC__"; + break; + case CRONET_ASYNC: + name += "CronetAsync"; + break; + default: + throw new IllegalArgumentException("Unknown mode: " + mode); + } + return name; + } + + // Responsible for running one particular benchmark and timing it. + private class Benchmark { + private final Mode mMode; + private final Direction mDirection; + private final Protocol mProtocol; + private final URL mUrl; + private final String mName; + private final CronetEngine mCronetEngine; + // Size in bytes of content being uploaded or downloaded. + private final int mLength; + // How many requests to execute. + private final int mIterations; + // How many requests to execute in parallel at any one time. + private final int mConcurrency; + // Dictionary of benchmark names mapped to times to complete the benchmarks. + private final JSONObject mResults; + // How large a buffer to use for passing content, in bytes. + private final int mBufferSize; + // Cached copy of getConfigBoolean("CRONET_ASYNC_USE_NETWORK_THREAD") for faster access. + private final boolean mUseNetworkThread; + + private long mStartTimeMs = -1; + private long mStopTimeMs = -1; + + /** + * Create a new benchmark to run. Sets up various configuration settings. + * @param mode The API to benchmark. + * @param direction The transfer direction to benchmark (i.e. upload or download). + * @param size The size of the transfers to benchmark (i.e. large or small). + * @param protocol The transfer protocol to benchmark (i.e. HTTP or QUIC). + * @param concurrency The number of transfers to perform concurrently. + * @param results Mapping of benchmark names to time required to run the benchmark in ms. + * When the benchmark completes this is updated with the result. + */ + public Benchmark(Mode mode, Direction direction, Size size, Protocol protocol, + int concurrency, JSONObject results) { + mMode = mode; + mDirection = direction; + mProtocol = protocol; + final String resource; + switch (size) { + case SMALL: + resource = getConfigString("SMALL_RESOURCE"); + mIterations = getConfigInt("SMALL_ITERATIONS"); + mLength = getConfigInt("SMALL_RESOURCE_SIZE"); + break; + case LARGE: + // When measuring a large upload, only download a small amount so download time + // isn't significant. + resource = getConfigString( + direction == Direction.UP ? "SMALL_RESOURCE" : "LARGE_RESOURCE"); + mIterations = getConfigInt("LARGE_ITERATIONS"); + mLength = getConfigInt("LARGE_RESOURCE_SIZE"); + break; + default: + throw new IllegalArgumentException("Unknown size: " + size); + } + final String scheme; + final String host; + final int port; + switch (protocol) { + case HTTP: + scheme = "http"; + host = getConfigString("HOST_IP"); + port = getConfigInt("HTTP_PORT"); + break; + case QUIC: + scheme = "https"; + host = getConfigString("HOST"); + port = getConfigInt("QUIC_PORT"); + break; + default: + throw new IllegalArgumentException("Unknown protocol: " + protocol); + } + try { + mUrl = new URL(scheme, host, port, resource); + } catch (MalformedURLException e) { + throw new IllegalArgumentException( + "Bad URL: " + host + ":" + port + "/" + resource); + } + final ExperimentalCronetEngine.Builder cronetEngineBuilder = + new ExperimentalCronetEngine.Builder(CronetPerfTestActivity.this); + System.loadLibrary("cronet_tests"); + if (mProtocol == Protocol.QUIC) { + cronetEngineBuilder.enableQuic(true); + cronetEngineBuilder.addQuicHint(host, port, port); + CronetTestUtil.setMockCertVerifierForTesting(cronetEngineBuilder, + MockCertVerifier.createMockCertVerifier( + new String[] {getConfigString("QUIC_CERT_FILE")}, true)); + } + + try { + JSONObject hostResolverParams = + CronetTestUtil.generateHostResolverRules(getConfigString("HOST_IP")); + JSONObject experimentalOptions = + new JSONObject() + .put("HostResolverRules", hostResolverParams); + cronetEngineBuilder.setExperimentalOptions(experimentalOptions.toString()); + } catch (JSONException e) { + throw new IllegalStateException("JSON failed: " + e); + } + mCronetEngine = cronetEngineBuilder.build(); + mName = buildBenchmarkName(mode, direction, protocol, concurrency, mIterations); + mConcurrency = concurrency; + mResults = results; + mBufferSize = mLength > getConfigInt("MAX_BUFFER_SIZE") + ? getConfigInt("MAX_BUFFER_SIZE") + : mLength; + mUseNetworkThread = getConfigBoolean("CRONET_ASYNC_USE_NETWORK_THREAD"); + } + + private void startTimer() { + mStartTimeMs = System.currentTimeMillis(); + } + + private void stopTimer() { + mStopTimeMs = System.currentTimeMillis(); + } + + private void reportResult() { + if (mStartTimeMs == -1 || mStopTimeMs == -1) { + throw new IllegalStateException("startTimer() or stopTimer() not called"); + } + try { + mResults.put(mName, mStopTimeMs - mStartTimeMs); + } catch (JSONException e) { + System.out.println("Failed to write JSON result for " + mName); + } + } + + // NOTE(pauljensen): Sampling profiling won't work on KitKat and earlier devices. + private void startLogging() { + if (getConfigBoolean("CAPTURE_NETLOG")) { + mCronetEngine.startNetLogToFile( + getFilesDir().getPath() + "/" + mName + ".json", false); + } + if (getConfigBoolean("CAPTURE_TRACE")) { + Debug.startMethodTracing(getFilesDir().getPath() + "/" + mName + ".trace"); + } else if (getConfigBoolean("CAPTURE_SAMPLED_TRACE")) { + Debug.startMethodTracingSampling( + getFilesDir().getPath() + "/" + mName + ".trace", 8000000, 10); + } + } + + private void stopLogging() { + if (getConfigBoolean("CAPTURE_NETLOG")) { + mCronetEngine.stopNetLog(); + } + if (getConfigBoolean("CAPTURE_TRACE") || getConfigBoolean("CAPTURE_SAMPLED_TRACE")) { + Debug.stopMethodTracing(); + } + } + + /** + * Transfer {@code mLength} bytes through HttpURLConnection in {@code mDirection} direction. + * @param urlConnection The HttpURLConnection to use for transfer. + * @param buffer A buffer of length |mBufferSize| to use for transfer. + * @return {@code true} if transfer completed successfully. + */ + private boolean exerciseHttpURLConnection(URLConnection urlConnection, byte[] buffer) + throws IOException { + final HttpURLConnection connection = (HttpURLConnection) urlConnection; + try { + int bytesTransfered = 0; + if (mDirection == Direction.DOWN) { + final InputStream inputStream = connection.getInputStream(); + while (true) { + final int bytesRead = inputStream.read(buffer, 0, mBufferSize); + if (bytesRead == -1) { + break; + } else { + bytesTransfered += bytesRead; + } + } + } else { + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Length", Integer.toString(mLength)); + final OutputStream outputStream = connection.getOutputStream(); + for (int remaining = mLength; remaining > 0; remaining -= mBufferSize) { + outputStream.write(buffer, 0, Math.min(remaining, mBufferSize)); + } + bytesTransfered = mLength; + } + return connection.getResponseCode() == 200 && bytesTransfered == mLength; + } finally { + connection.disconnect(); + } + } + + // GET or POST to one particular URL using URL.openConnection() + private class SystemHttpURLConnectionFetchTask implements Callable { + private final byte[] mBuffer = new byte[mBufferSize]; + + @Override + public Boolean call() { + try { + return exerciseHttpURLConnection(mUrl.openConnection(), mBuffer); + } catch (IOException e) { + System.out.println("System HttpURLConnection failed with " + e); + return false; + } + } + } + + // GET or POST to one particular URL using Cronet HttpURLConnection API + private class CronetHttpURLConnectionFetchTask implements Callable { + private final byte[] mBuffer = new byte[mBufferSize]; + + @Override + public Boolean call() { + try { + return exerciseHttpURLConnection(mCronetEngine.openConnection(mUrl), mBuffer); + } catch (IOException e) { + System.out.println("Cronet HttpURLConnection failed with " + e); + return false; + } + } + } + + // GET or POST to one particular URL using Cronet's asynchronous API + private class CronetAsyncFetchTask implements Callable { + // A message-queue for asynchronous tasks to post back to. + private final LinkedBlockingQueue mWorkQueue = new LinkedBlockingQueue<>(); + private final WorkQueueExecutor mWorkQueueExecutor = new WorkQueueExecutor(); + + private int mRemainingRequests; + private int mConcurrentFetchersDone; + private boolean mFailed; + + CronetAsyncFetchTask() { + mRemainingRequests = mIterations; + mConcurrentFetchersDone = 0; + mFailed = false; + } + + private void initiateRequest(final ByteBuffer buffer) { + if (mRemainingRequests == 0) { + mConcurrentFetchersDone++; + if (mUseNetworkThread) { + // Post empty task so message loop exit condition is retested. + postToWorkQueue(new Runnable() { + @Override + public void run() {} + }); + } + return; + } + mRemainingRequests--; + final Runnable completionCallback = new Runnable() { + @Override + public void run() { + initiateRequest(buffer); + } + }; + final UrlRequest.Builder builder = + mCronetEngine.newUrlRequestBuilder(mUrl.toString(), + new Callback(buffer, completionCallback), mWorkQueueExecutor); + if (mDirection == Direction.UP) { + builder.setUploadDataProvider(new Uploader(buffer), mWorkQueueExecutor); + builder.addHeader("Content-Type", "application/octet-stream"); + } + builder.build().start(); + } + + private class Uploader extends UploadDataProvider { + private final ByteBuffer mBuffer; + private int mRemainingBytes; + + Uploader(ByteBuffer buffer) { + mBuffer = buffer; + mRemainingBytes = mLength; + } + + @Override + public long getLength() { + return mLength; + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) { + mBuffer.clear(); + // Don't post more than |mLength|. + if (mRemainingBytes < mBuffer.limit()) { + mBuffer.limit(mRemainingBytes); + } + // Don't overflow |byteBuffer|. + if (byteBuffer.remaining() < mBuffer.limit()) { + mBuffer.limit(byteBuffer.remaining()); + } + byteBuffer.put(mBuffer); + mRemainingBytes -= mBuffer.position(); + uploadDataSink.onReadSucceeded(false); + } + + @Override + public void rewind(UploadDataSink uploadDataSink) { + uploadDataSink.onRewindError(new Exception("no rewinding")); + } + } + + private class Callback extends UrlRequest.Callback { + private final ByteBuffer mBuffer; + private final Runnable mCompletionCallback; + private int mBytesReceived; + + Callback(ByteBuffer buffer, Runnable completionCallback) { + mBuffer = buffer; + mCompletionCallback = completionCallback; + } + + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + mBuffer.clear(); + request.read(mBuffer); + } + + @Override + public void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) { + request.followRedirect(); + } + + @Override + public void onReadCompleted( + UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) { + mBytesReceived += byteBuffer.position(); + mBuffer.clear(); + request.read(mBuffer); + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + if (info.getHttpStatusCode() != 200 || mBytesReceived != mLength) { + System.out.println("Failed: response code: " + info.getHttpStatusCode() + + " bytes: " + mBytesReceived); + mFailed = true; + } + mCompletionCallback.run(); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException e) { + System.out.println("Async request failed with " + e); + mFailed = true; + } + } + + private void postToWorkQueue(Runnable task) { + try { + mWorkQueue.put(task); + } catch (InterruptedException e) { + mFailed = true; + } + } + + private class WorkQueueExecutor implements Executor { + @Override + public void execute(Runnable task) { + if (mUseNetworkThread) { + task.run(); + } else { + postToWorkQueue(task); + } + } + } + + @Override + public Boolean call() { + // Initiate concurrent requests. + for (int i = 0; i < mConcurrency; i++) { + initiateRequest(ByteBuffer.allocateDirect(mBufferSize)); + } + // Wait for all jobs to finish. + try { + while (mConcurrentFetchersDone != mConcurrency && !mFailed) { + mWorkQueue.take().run(); + } + } catch (InterruptedException e) { + System.out.println("Async tasks failed with " + e); + mFailed = true; + } + return !mFailed; + } + } + + /** + * Executes the benchmark, times how long it takes, and records time in |mResults|. + */ + public void run() { + final ExecutorService executor = Executors.newFixedThreadPool(mConcurrency); + final List> tasks = new ArrayList<>(mIterations); + startLogging(); + // Prepare list of tasks to run. + switch (mMode) { + case SYSTEM_HUC: + for (int i = 0; i < mIterations; i++) { + tasks.add(new SystemHttpURLConnectionFetchTask()); + } + break; + case CRONET_HUC: { + for (int i = 0; i < mIterations; i++) { + tasks.add(new CronetHttpURLConnectionFetchTask()); + } + break; + } + case CRONET_ASYNC: + tasks.add(new CronetAsyncFetchTask()); + break; + default: + throw new IllegalArgumentException("Unknown mode: " + mMode); + } + // Execute tasks. + boolean success = true; + List> futures = new ArrayList<>(); + try { + startTimer(); + // If possible execute directly to lessen impact of thread-pool overhead. + if (tasks.size() == 1 || mConcurrency == 1) { + for (int i = 0; i < tasks.size(); i++) { + if (!tasks.get(i).call()) { + success = false; + } + } + } else { + futures = executor.invokeAll(tasks); + executor.shutdown(); + executor.awaitTermination(240, TimeUnit.SECONDS); + } + stopTimer(); + for (Future future : futures) { + if (!future.isDone() || !future.get()) { + success = false; + break; + } + } + } catch (Exception e) { + System.out.println("Batch execution failed with " + e); + success = false; + } + stopLogging(); + if (success) { + reportResult(); + } + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Initializing application context here due to lack of custom CronetPerfTestApplication. + ContextUtils.initApplicationContext(getApplicationContext()); + PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX); + mConfig = getIntent().getData(); + // Execute benchmarks on another thread to avoid networking on main thread. + + PostTask.postTask(TaskTraits.USER_BLOCKING, () -> { + JSONObject results = new JSONObject(); + for (Mode mode : Mode.values()) { + for (Direction direction : Direction.values()) { + for (Protocol protocol : Protocol.values()) { + if (protocol == Protocol.QUIC && mode == Mode.SYSTEM_HUC) { + // Unsupported; skip. + continue; + } + // Run large and small benchmarks one at a time to test single-threaded use. + // Also run them four at a time to see how they benefit from concurrency. + // The value four was chosen as many devices are now quad-core. + new Benchmark(mode, direction, Size.LARGE, protocol, 1, results).run(); + new Benchmark(mode, direction, Size.LARGE, protocol, 4, results).run(); + new Benchmark(mode, direction, Size.SMALL, protocol, 1, results).run(); + new Benchmark(mode, direction, Size.SMALL, protocol, 4, results).run(); + // Large benchmarks are generally bandwidth bound and unaffected by + // per-request overhead. Small benchmarks are not, so test at + // further increased concurrency to see if further benefit is possible. + new Benchmark(mode, direction, Size.SMALL, protocol, 8, results).run(); + } + } + } + final File outputFile = new File(getConfigString("RESULTS_FILE")); + final File doneFile = new File(getConfigString("DONE_FILE")); + // If DONE_FILE exists, something is horribly wrong, produce no results to convey this. + if (doneFile.exists()) { + results = new JSONObject(); + } + // Write out results to RESULTS_FILE, then create DONE_FILE. + FileOutputStream outputFileStream = null; + FileOutputStream doneFileStream = null; + try { + outputFileStream = new FileOutputStream(outputFile); + outputFileStream.write(results.toString().getBytes()); + outputFileStream.close(); + doneFileStream = new FileOutputStream(doneFile); + doneFileStream.close(); + } catch (Exception e) { + System.out.println("Failed write results file: " + e); + } finally { + try { + if (outputFileStream != null) { + outputFileStream.close(); + } + if (doneFileStream != null) { + doneFileStream.close(); + } + } catch (IOException e) { + System.out.println("Failed to close output file: " + e); + } + } + finish(); + }); + } +} diff --git a/src/components/cronet/android/test/javatests/AndroidManifest.xml b/src/components/cronet/android/test/javatests/AndroidManifest.xml new file mode 100644 index 0000000000..edf981e529 --- /dev/null +++ b/src/components/cronet/android/test/javatests/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamQuicTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamQuicTest.java new file mode 100644 index 0000000000..4d41c31972 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamQuicTest.java @@ -0,0 +1,399 @@ +// Copyright 2016 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import static org.chromium.base.CollectionUtil.newHashSet; +import static org.chromium.net.CronetTestRule.getContext; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.MetricsTestUtil.TestRequestFinishedListener; + +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.HashSet; + +/** + * Tests functionality of BidirectionalStream's QUIC implementation. + */ +@RunWith(AndroidJUnit4.class) +public class BidirectionalStreamQuicTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private ExperimentalCronetEngine mCronetEngine; + private enum QuicBidirectionalStreams { + ENABLED, + DISABLED, + } + + private void setUp(QuicBidirectionalStreams enabled) throws Exception { + // Load library first to create MockCertVerifier. + System.loadLibrary("cronet_tests"); + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + + QuicTestServer.startQuicTestServer(getContext()); + + builder.enableQuic(true); + JSONObject quicParams = new JSONObject(); + if (enabled == QuicBidirectionalStreams.DISABLED) { + quicParams.put("quic_disable_bidirectional_streams", true); + } + JSONObject hostResolverParams = CronetTestUtil.generateHostResolverRules(); + JSONObject experimentalOptions = new JSONObject() + .put("QUIC", quicParams) + .put("HostResolverRules", hostResolverParams); + builder.setExperimentalOptions(experimentalOptions.toString()); + + builder.addQuicHint(QuicTestServer.getServerHost(), QuicTestServer.getServerPort(), + QuicTestServer.getServerPort()); + + CronetTestUtil.setMockCertVerifierForTesting( + builder, QuicTestServer.createMockCertVerifier()); + + mCronetEngine = builder.build(); + } + + @After + public void tearDown() throws Exception { + QuicTestServer.shutdownQuicTestServer(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Test that QUIC is negotiated. + public void testSimpleGet() throws Exception { + setUp(QuicBidirectionalStreams.ENABLED); + String path = "/simple.txt"; + String quicURL = QuicTestServer.getServerURL() + path; + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + BidirectionalStream stream = + mCronetEngine + .newBidirectionalStreamBuilder(quicURL, callback, callback.getExecutor()) + .setHttpMethod("GET") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("This is a simple text file served by QUIC.\n", callback.mResponseAsString); + assertEquals("quic/1+spdy/3", callback.mResponseInfo.getNegotiatedProtocol()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimplePost() throws Exception { + setUp(QuicBidirectionalStreams.ENABLED); + String path = "/simple.txt"; + String quicURL = QuicTestServer.getServerURL() + path; + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + // Although we have no way to verify data sent at this point, this test + // can make sure that onWriteCompleted is invoked appropriately. + callback.addWriteData("Test String".getBytes()); + callback.addWriteData("1234567890".getBytes()); + callback.addWriteData("woot!".getBytes()); + TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); + mCronetEngine.addRequestFinishedListener(requestFinishedListener); + BidirectionalStream stream = + mCronetEngine + .newBidirectionalStreamBuilder(quicURL, callback, callback.getExecutor()) + .addHeader("foo", "bar") + .addHeader("empty", "") + .addHeader("Content-Type", "zebra") + .addRequestAnnotation("request annotation") + .addRequestAnnotation(this) + .build(); + Date startTime = new Date(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + requestFinishedListener.blockUntilDone(); + Date endTime = new Date(); + RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo(); + MetricsTestUtil.checkRequestFinishedInfo(finishedInfo, quicURL, startTime, endTime); + assertEquals(RequestFinishedInfo.SUCCEEDED, finishedInfo.getFinishedReason()); + MetricsTestUtil.checkHasConnectTiming(finishedInfo.getMetrics(), startTime, endTime, true); + assertEquals(newHashSet("request annotation", this), + new HashSet(finishedInfo.getAnnotations())); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("This is a simple text file served by QUIC.\n", callback.mResponseAsString); + assertEquals("quic/1+spdy/3", callback.mResponseInfo.getNegotiatedProtocol()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimplePostWithFlush() throws Exception { + setUp(QuicBidirectionalStreams.ENABLED); + // TODO(xunjieli): Use ParameterizedTest instead of the loop. + for (int i = 0; i < 2; i++) { + String path = "/simple.txt"; + String quicURL = QuicTestServer.getServerURL() + path; + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + // Although we have no way to verify data sent at this point, this test + // can make sure that onWriteCompleted is invoked appropriately. + callback.addWriteData("Test String".getBytes(), false); + callback.addWriteData("1234567890".getBytes(), false); + callback.addWriteData("woot!".getBytes(), true); + BidirectionalStream stream = mCronetEngine + .newBidirectionalStreamBuilder( + quicURL, callback, callback.getExecutor()) + .delayRequestHeadersUntilFirstFlush(i == 0) + .addHeader("foo", "bar") + .addHeader("empty", "") + .addHeader("Content-Type", "zebra") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals( + "This is a simple text file served by QUIC.\n", callback.mResponseAsString); + assertEquals("quic/1+spdy/3", callback.mResponseInfo.getNegotiatedProtocol()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimplePostWithFlushTwice() throws Exception { + setUp(QuicBidirectionalStreams.ENABLED); + // TODO(xunjieli): Use ParameterizedTest instead of the loop. + for (int i = 0; i < 2; i++) { + String path = "/simple.txt"; + String quicURL = QuicTestServer.getServerURL() + path; + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + // Although we have no way to verify data sent at this point, this test + // can make sure that onWriteCompleted is invoked appropriately. + callback.addWriteData("Test String".getBytes(), false); + callback.addWriteData("1234567890".getBytes(), false); + callback.addWriteData("woot!".getBytes(), true); + callback.addWriteData("Test String".getBytes(), false); + callback.addWriteData("1234567890".getBytes(), false); + callback.addWriteData("woot!".getBytes(), true); + BidirectionalStream stream = mCronetEngine + .newBidirectionalStreamBuilder( + quicURL, callback, callback.getExecutor()) + .delayRequestHeadersUntilFirstFlush(i == 0) + .addHeader("foo", "bar") + .addHeader("empty", "") + .addHeader("Content-Type", "zebra") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals( + "This is a simple text file served by QUIC.\n", callback.mResponseAsString); + assertEquals("quic/1+spdy/3", callback.mResponseInfo.getNegotiatedProtocol()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimpleGetWithFlush() throws Exception { + setUp(QuicBidirectionalStreams.ENABLED); + // TODO(xunjieli): Use ParameterizedTest instead of the loop. + for (int i = 0; i < 2; i++) { + String path = "/simple.txt"; + String url = QuicTestServer.getServerURL() + path; + + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() { + @Override + public void onStreamReady(BidirectionalStream stream) { + // This flush should send the delayed headers. + stream.flush(); + super.onStreamReady(stream); + } + }; + BidirectionalStream stream = + mCronetEngine + .newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .setHttpMethod("GET") + .delayRequestHeadersUntilFirstFlush(i == 0) + .addHeader("foo", "bar") + .addHeader("empty", "") + .build(); + // Flush before stream is started should not crash. + stream.flush(); + + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + + // Flush after stream is completed is no-op. It shouldn't call into the destroyed + // adapter. + stream.flush(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals( + "This is a simple text file served by QUIC.\n", callback.mResponseAsString); + assertEquals("quic/1+spdy/3", callback.mResponseInfo.getNegotiatedProtocol()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimplePostWithFlushAfterOneWrite() throws Exception { + setUp(QuicBidirectionalStreams.ENABLED); + // TODO(xunjieli): Use ParameterizedTest instead of the loop. + for (int i = 0; i < 2; i++) { + String path = "/simple.txt"; + String url = QuicTestServer.getServerURL() + path; + + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.addWriteData("Test String".getBytes(), true); + BidirectionalStream stream = + mCronetEngine + .newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .delayRequestHeadersUntilFirstFlush(i == 0) + .addHeader("foo", "bar") + .addHeader("empty", "") + .addHeader("Content-Type", "zebra") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals( + "This is a simple text file served by QUIC.\n", callback.mResponseAsString); + assertEquals("quic/1+spdy/3", callback.mResponseInfo.getNegotiatedProtocol()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testQuicBidirectionalStreamDisabled() throws Exception { + setUp(QuicBidirectionalStreams.DISABLED); + String path = "/simple.txt"; + String quicURL = QuicTestServer.getServerURL() + path; + + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + BidirectionalStream stream = + mCronetEngine + .newBidirectionalStreamBuilder(quicURL, callback, callback.getExecutor()) + .setHttpMethod("GET") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertTrue(callback.mOnErrorCalled); + assertNull(callback.mResponseInfo); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that if the stream failed between the time when we issue a Write() + // and when the Write() is executed in the native stack, there is no crash. + // This test is racy, but it should catch a crash (if there is any) most of + // the time. + public void testStreamFailBeforeWriteIsExecutedOnNetworkThread() throws Exception { + setUp(QuicBidirectionalStreams.ENABLED); + String path = "/simple.txt"; + String quicURL = QuicTestServer.getServerURL() + path; + + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() { + @Override + public void onWriteCompleted(BidirectionalStream stream, UrlResponseInfo info, + ByteBuffer buffer, boolean endOfStream) { + // Super class will write the next piece of data. + super.onWriteCompleted(stream, info, buffer, endOfStream); + // Shut down the server, and the stream should error out. + // The second call to shutdownQuicTestServer is no-op. + QuicTestServer.shutdownQuicTestServer(); + } + }; + + callback.addWriteData("Test String".getBytes()); + callback.addWriteData("1234567890".getBytes()); + callback.addWriteData("woot!".getBytes()); + + BidirectionalStream stream = + mCronetEngine + .newBidirectionalStreamBuilder(quicURL, callback, callback.getExecutor()) + .addHeader("foo", "bar") + .addHeader("empty", "") + .addHeader("Content-Type", "zebra") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + // Server terminated on us, so the stream must fail. + // QUIC reports this as ERR_QUIC_PROTOCOL_ERROR. Sometimes we get ERR_CONNECTION_REFUSED. + assertNotNull(callback.mError); + assertTrue(callback.mError instanceof NetworkException); + NetworkException networkError = (NetworkException) callback.mError; + assertTrue(NetError.ERR_QUIC_PROTOCOL_ERROR == networkError.getCronetInternalErrorCode() + || NetError.ERR_CONNECTION_REFUSED == networkError.getCronetInternalErrorCode()); + if (NetError.ERR_CONNECTION_REFUSED == networkError.getCronetInternalErrorCode()) return; + assertTrue(callback.mError instanceof QuicException); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testStreamFailWithQuicDetailedErrorCode() throws Exception { + setUp(QuicBidirectionalStreams.ENABLED); + String path = "/simple.txt"; + String quicURL = QuicTestServer.getServerURL() + path; + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() { + @Override + public void onStreamReady(BidirectionalStream stream) { + // Shut down the server, and the stream should error out. + // The second call to shutdownQuicTestServer is no-op. + QuicTestServer.shutdownQuicTestServer(); + } + }; + BidirectionalStream stream = + mCronetEngine + .newBidirectionalStreamBuilder(quicURL, callback, callback.getExecutor()) + .setHttpMethod("GET") + .delayRequestHeadersUntilFirstFlush(true) + .addHeader("Content-Type", "zebra") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertNotNull(callback.mError); + if (callback.mError instanceof QuicException) { + QuicException quicException = (QuicException) callback.mError; + // Checks that detailed quic error code is not QUIC_NO_ERROR == 0. + assertTrue("actual error " + quicException.getQuicDetailedErrorCode(), + 0 < quicException.getQuicDetailedErrorCode()); + } + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java new file mode 100644 index 0000000000..eb5505c136 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/BidirectionalStreamTest.java @@ -0,0 +1,1673 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.base.CollectionUtil.newHashSet; +import static org.chromium.net.CronetTestRule.SERVER_CERT_PEM; +import static org.chromium.net.CronetTestRule.SERVER_KEY_PKCS8_PEM; +import static org.chromium.net.CronetTestRule.assertContains; +import static org.chromium.net.CronetTestRule.getContext; + +import android.os.ConditionVariable; +import android.os.Process; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.Log; +import org.chromium.base.test.util.DisabledTest; +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.CronetTestRule.RequiresMinApi; +import org.chromium.net.MetricsTestUtil.TestRequestFinishedListener; +import org.chromium.net.TestBidirectionalStreamCallback.FailureType; +import org.chromium.net.TestBidirectionalStreamCallback.ResponseStep; +import org.chromium.net.impl.BidirectionalStreamNetworkException; +import org.chromium.net.impl.CronetBidirectionalStream; +import org.chromium.net.impl.UrlResponseInfoImpl; + +import java.nio.ByteBuffer; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Test functionality of BidirectionalStream interface. + */ +@RunWith(AndroidJUnit4.class) +public class BidirectionalStreamTest { + private static final String TAG = BidirectionalStreamTest.class.getSimpleName(); + + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private ExperimentalCronetEngine mCronetEngine; + + @Before + public void setUp() throws Exception { + // Load library first to create MockCertVerifier. + System.loadLibrary("cronet_tests"); + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + CronetTestUtil.setMockCertVerifierForTesting( + builder, QuicTestServer.createMockCertVerifier()); + + mCronetEngine = builder.build(); + assertTrue(Http2TestServer.startHttp2TestServer( + getContext(), SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM)); + } + + @After + public void tearDown() throws Exception { + assertTrue(Http2TestServer.shutdownHttp2TestServer()); + if (mCronetEngine != null) { + mCronetEngine.shutdown(); + } + } + + 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 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(); + } + + private static UrlResponseInfo createUrlResponseInfo( + String[] urls, String message, int statusCode, int receivedBytes, String... headers) { + ArrayList> headersList = new ArrayList<>(); + for (int i = 0; i < headers.length; i += 2) { + headersList.add(new AbstractMap.SimpleImmutableEntry( + headers[i], headers[i + 1])); + } + UrlResponseInfoImpl urlResponseInfo = new UrlResponseInfoImpl(Arrays.asList(urls), + statusCode, message, headersList, false, "h2", null, receivedBytes); + return urlResponseInfo; + } + + private void runSimpleGetWithExpectedReceivedByteCount(int expectedReceivedBytes) + throws Exception { + String url = Http2TestServer.getEchoMethodUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); + mCronetEngine.addRequestFinishedListener(requestFinishedListener); + // Create stream. + BidirectionalStream stream = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .setHttpMethod("GET") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + requestFinishedListener.blockUntilDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + // Default method is 'GET'. + assertEquals("GET", callback.mResponseAsString); + UrlResponseInfo urlResponseInfo = createUrlResponseInfo( + new String[] {url}, "", 200, expectedReceivedBytes, ":status", "200"); + mTestRule.assertResponseEquals(urlResponseInfo, callback.mResponseInfo); + checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodUrl(), 200, ""); + RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo(); + assertTrue(finishedInfo.getAnnotations().isEmpty()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testBuilderCheck() throws Exception { + if (mTestRule.testingJavaImpl()) { + runBuilderCheckJavaImpl(); + } else { + runBuilderCheckNativeImpl(); + } + } + + private void runBuilderCheckNativeImpl() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + try { + mCronetEngine.newBidirectionalStreamBuilder(null, callback, callback.getExecutor()); + fail("URL not null-checked"); + } catch (NullPointerException e) { + assertEquals("URL is required.", e.getMessage()); + } + try { + mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getServerUrl(), null, callback.getExecutor()); + fail("Callback not null-checked"); + } catch (NullPointerException e) { + assertEquals("Callback is required.", e.getMessage()); + } + try { + mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getServerUrl(), callback, null); + fail("Executor not null-checked"); + } catch (NullPointerException e) { + assertEquals("Executor is required.", e.getMessage()); + } + // Verify successful creation doesn't throw. + BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getServerUrl(), callback, callback.getExecutor()); + 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()); + } + } + + private void runBuilderCheckJavaImpl() { + try { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + mTestRule.createJavaEngineBuilder().build().newBidirectionalStreamBuilder( + Http2TestServer.getServerUrl(), callback, callback.getExecutor()); + fail("JavaCronetEngine doesn't support BidirectionalStream." + + " Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // Expected. + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testFailPlainHttp() throws Exception { + String url = "http://example.com"; + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + // Create stream. + BidirectionalStream stream = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertContains("Exception in BidirectionalStream: net::ERR_DISALLOWED_URL_SCHEME", + callback.mError.getMessage()); + assertEquals(-301, ((NetworkException) callback.mError).getCronetInternalErrorCode()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimpleGet() throws Exception { + // Since this is the first request on the connection, the expected received bytes count + // must account for an HPACK dynamic table size update. + runSimpleGetWithExpectedReceivedByteCount(31); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimpleHead() throws Exception { + String url = Http2TestServer.getEchoMethodUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + // Create stream. + BidirectionalStream stream = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .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, 32, ":status", "200"); + mTestRule.assertResponseEquals(urlResponseInfo, callback.mResponseInfo); + checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodUrl(), 200, ""); + } + + @Test + @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()); + TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); + mCronetEngine.addRequestFinishedListener(requestFinishedListener); + // Create stream. + BidirectionalStream stream = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .addHeader("foo", "bar") + .addHeader("empty", "") + .addHeader("Content-Type", "zebra") + .addRequestAnnotation(this) + .addRequestAnnotation("request annotation") + .build(); + Date startTime = new Date(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + requestFinishedListener.blockUntilDone(); + Date endTime = new Date(); + RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo(); + MetricsTestUtil.checkRequestFinishedInfo(finishedInfo, url, startTime, endTime); + assertEquals(RequestFinishedInfo.SUCCEEDED, finishedInfo.getFinishedReason()); + MetricsTestUtil.checkHasConnectTiming(finishedInfo.getMetrics(), startTime, endTime, true); + assertEquals(newHashSet("request annotation", this), + new HashSet(finishedInfo.getAnnotations())); + 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)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimpleGetWithCombinedHeader() throws Exception { + String url = Http2TestServer.getCombinedHeadersUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); + mCronetEngine.addRequestFinishedListener(requestFinishedListener); + // Create stream. + BidirectionalStream stream = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .setHttpMethod("GET") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + requestFinishedListener.blockUntilDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + // Default method is 'GET'. + assertEquals("GET", callback.mResponseAsString); + assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("foo").get(0)); + assertEquals("bar2", callback.mResponseInfo.getAllHeaders().get("foo").get(1)); + RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo(); + assertTrue(finishedInfo.getAnnotations().isEmpty()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimplePostWithFlush() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.addWriteData("Test String".getBytes(), false); + callback.addWriteData("1234567890".getBytes(), false); + callback.addWriteData("woot!".getBytes(), true); + BidirectionalStream stream = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .addHeader("foo", "bar") + .addHeader("empty", "") + .addHeader("Content-Type", "zebra") + .build(); + // Flush before stream is started should not crash. + stream.flush(); + + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + + // Flush after stream is completed is no-op. It shouldn't call into the destroyed adapter. + stream.flush(); + + 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)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that a delayed flush() only sends buffers that have been written + // before it is called, and it doesn't flush buffers in mPendingQueue. + public void testFlushData() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + final ConditionVariable waitOnStreamReady = new ConditionVariable(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() { + // Number of onWriteCompleted callbacks that have been invoked. + private int mNumWriteCompleted; + + @Override + public void onStreamReady(BidirectionalStream stream) { + mResponseStep = ResponseStep.ON_STREAM_READY; + waitOnStreamReady.open(); + } + + @Override + public void onWriteCompleted(BidirectionalStream stream, UrlResponseInfo info, + ByteBuffer buffer, boolean endOfStream) { + super.onWriteCompleted(stream, info, buffer, endOfStream); + mNumWriteCompleted++; + if (mNumWriteCompleted <= 3) { + // "6" is in pending queue. + List pendingData = + ((CronetBidirectionalStream) stream).getPendingDataForTesting(); + assertEquals(1, pendingData.size()); + ByteBuffer pendingBuffer = pendingData.get(0); + byte[] content = new byte[pendingBuffer.remaining()]; + pendingBuffer.get(content); + assertTrue(Arrays.equals("6".getBytes(), content)); + + // "4" and "5" have been flushed. + assertEquals(0, + ((CronetBidirectionalStream) stream).getFlushDataForTesting().size()); + } else if (mNumWriteCompleted == 5) { + // Now flush "6", which is still in pending queue. + List pendingData = + ((CronetBidirectionalStream) stream).getPendingDataForTesting(); + assertEquals(1, pendingData.size()); + ByteBuffer pendingBuffer = pendingData.get(0); + byte[] content = new byte[pendingBuffer.remaining()]; + pendingBuffer.get(content); + assertTrue(Arrays.equals("6".getBytes(), content)); + + stream.flush(); + + assertEquals(0, + ((CronetBidirectionalStream) stream).getPendingDataForTesting().size()); + assertEquals(0, + ((CronetBidirectionalStream) stream).getFlushDataForTesting().size()); + } + } + }; + callback.addWriteData("1".getBytes(), false); + callback.addWriteData("2".getBytes(), false); + callback.addWriteData("3".getBytes(), true); + callback.addWriteData("4".getBytes(), false); + callback.addWriteData("5".getBytes(), true); + callback.addWriteData("6".getBytes(), false); + CronetBidirectionalStream stream = + (CronetBidirectionalStream) mCronetEngine + .newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .addHeader("foo", "bar") + .addHeader("empty", "") + .addHeader("Content-Type", "zebra") + .build(); + stream.start(); + waitOnStreamReady.block(); + + assertEquals(0, stream.getPendingDataForTesting().size()); + assertEquals(0, stream.getFlushDataForTesting().size()); + + // Write 1, 2, 3 and flush(). + callback.startNextWrite(stream); + // Write 4, 5 and flush(). 4, 5 will be in flush queue. + callback.startNextWrite(stream); + // Write 6, but do not flush. 6 will be in pending queue. + callback.startNextWrite(stream); + + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("123456", 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)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Regression test for crbug.com/692168. + public void testCancelWhileWriteDataPending() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + // Use a direct executor to avoid race. + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback( + /*useDirectExecutor*/ true) { + @Override + public void onStreamReady(BidirectionalStream stream) { + // Start the first write. + stream.write(getDummyData(), false); + stream.flush(); + } + @Override + public void onReadCompleted(BidirectionalStream stream, UrlResponseInfo info, + ByteBuffer byteBuffer, boolean endOfStream) { + super.onReadCompleted(stream, info, byteBuffer, endOfStream); + // Cancel now when the write side is busy. + stream.cancel(); + } + @Override + public void onWriteCompleted(BidirectionalStream stream, UrlResponseInfo info, + ByteBuffer buffer, boolean endOfStream) { + // Flush twice to keep the flush queue non-empty. + stream.write(getDummyData(), false); + stream.flush(); + stream.write(getDummyData(), false); + stream.flush(); + } + // Returns a piece of dummy data to send to the server. + private ByteBuffer getDummyData() { + byte[] data = new byte[100]; + for (int i = 0; i < data.length; i++) { + data[i] = 'x'; + } + ByteBuffer dummyData = ByteBuffer.allocateDirect(data.length); + dummyData.put(data); + dummyData.flip(); + return dummyData; + } + }; + CronetBidirectionalStream stream = + (CronetBidirectionalStream) mCronetEngine + .newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(callback.mOnCanceledCalled); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimpleGetWithFlush() throws Exception { + // TODO(xunjieli): Use ParameterizedTest instead of the loop. + for (int i = 0; i < 2; i++) { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() { + @Override + public void onStreamReady(BidirectionalStream stream) { + try { + // Attempt to write data for GET request. + stream.write(ByteBuffer.wrap("dummy".getBytes()), true); + } catch (IllegalArgumentException e) { + // Expected. + } + // If there are delayed headers, this flush should try to send them. + // If nothing to flush, it should not crash. + stream.flush(); + super.onStreamReady(stream); + try { + // Attempt to write data for GET request. + stream.write(ByteBuffer.wrap("dummy".getBytes()), true); + } catch (IllegalArgumentException e) { + // Expected. + } + } + }; + BidirectionalStream stream = + mCronetEngine + .newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .setHttpMethod("GET") + .delayRequestHeadersUntilFirstFlush(i == 0) + .addHeader("foo", "bar") + .addHeader("empty", "") + .build(); + // Flush before stream is started should not crash. + stream.flush(); + + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + + // Flush after stream is completed is no-op. It shouldn't call into the destroyed + // adapter. + stream.flush(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("", callback.mResponseAsString); + assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0)); + assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty").get(0)); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimplePostWithFlushAfterOneWrite() throws Exception { + // TODO(xunjieli): Use ParameterizedTest instead of the loop. + for (int i = 0; i < 2; i++) { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.addWriteData("Test String".getBytes(), true); + BidirectionalStream stream = + mCronetEngine + .newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .delayRequestHeadersUntilFirstFlush(i == 0) + .addHeader("foo", "bar") + .addHeader("empty", "") + .addHeader("Content-Type", "zebra") + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("Test String", 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)); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSimplePostWithFlushTwice() throws Exception { + // TODO(xunjieli): Use ParameterizedTest instead of the loop. + for (int i = 0; i < 2; i++) { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.addWriteData("Test String".getBytes(), false); + callback.addWriteData("1234567890".getBytes(), false); + callback.addWriteData("woot!".getBytes(), true); + callback.addWriteData("Test String".getBytes(), false); + callback.addWriteData("1234567890".getBytes(), false); + callback.addWriteData("woot!".getBytes(), true); + BidirectionalStream stream = + mCronetEngine + .newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .delayRequestHeadersUntilFirstFlush(i == 0) + .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!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)); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that it is legal to call read() in onStreamReady(). + public void testReadDuringOnStreamReady() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() { + @Override + public void onStreamReady(BidirectionalStream stream) { + super.onStreamReady(stream); + startNextRead(stream); + } + @Override + public void onResponseHeadersReceived( + BidirectionalStream stream, UrlResponseInfo info) { + // Do nothing. Skip readng. + } + }; + callback.addWriteData("Test String".getBytes()); + callback.addWriteData("1234567890".getBytes()); + callback.addWriteData("woot!".getBytes()); + BidirectionalStream stream = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .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)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that it is legal to call flush() when previous nativeWritevData has + // yet to complete. + public void testSimplePostWithFlushBeforePreviousWriteCompleted() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() { + @Override + public void onStreamReady(BidirectionalStream stream) { + super.onStreamReady(stream); + // Write a second time before the previous nativeWritevData has completed. + startNextWrite(stream); + assertEquals(0, numPendingWrites()); + } + }; + callback.addWriteData("Test String".getBytes(), false); + callback.addWriteData("1234567890".getBytes(), false); + callback.addWriteData("woot!".getBytes(), true); + callback.addWriteData("Test String".getBytes(), false); + callback.addWriteData("1234567890".getBytes(), false); + callback.addWriteData("woot!".getBytes(), true); + BidirectionalStream stream = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .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!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)); + } + + @Test + @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 = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getServerUrl(), callback, callback.getExecutor()); + 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)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBadMethod() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getServerUrl(), callback, callback.getExecutor()); + try { + builder.setHttpMethod("bad:method!"); + builder.build().start(); + fail("IllegalArgumentException not thrown."); + } catch (IllegalArgumentException e) { + assertEquals("Invalid http method bad:method!", e.getMessage()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBadHeaderName() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getServerUrl(), callback, callback.getExecutor()); + 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()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBadHeaderValue() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getServerUrl(), callback, callback.getExecutor()); + 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()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testAddHeader() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + String headerName = "header-name"; + String headerValue = "header-value"; + BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoHeaderUrl(headerName), callback, callback.getExecutor()); + builder.addHeader(headerName, headerValue); + builder.setHttpMethod("GET"); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(headerValue, callback.mResponseAsString); + } + + @Test + @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 = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoAllHeadersUrl(), callback, callback.getExecutor()); + 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 actualValues = new ArrayList(); + while (matcher.find()) { + actualValues.add(matcher.group(1)); + } + assertEquals(1, actualValues.size()); + assertEquals("header-value2", actualValues.get(0)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testEchoTrailers() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + String headerName = "header-name"; + String headerValue = "header-value"; + BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoTrailersUrl(), callback, callback.getExecutor()); + 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)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testCustomUserAgent() throws Exception { + String userAgentName = "User-Agent"; + String userAgentValue = "User-Agent-Value"; + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoHeaderUrl(userAgentName), callback, callback.getExecutor()); + builder.setHttpMethod("GET"); + builder.addHeader(userAgentName, userAgentValue); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(userAgentValue, callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testCustomCronetEngineUserAgent() throws Exception { + String userAgentName = "User-Agent"; + String userAgentValue = "User-Agent-Value"; + ExperimentalCronetEngine.Builder engineBuilder = + new ExperimentalCronetEngine.Builder(getContext()); + engineBuilder.setUserAgent(userAgentValue); + CronetTestUtil.setMockCertVerifierForTesting( + engineBuilder, QuicTestServer.createMockCertVerifier()); + ExperimentalCronetEngine engine = engineBuilder.build(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + BidirectionalStream.Builder builder = engine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoHeaderUrl(userAgentName), callback, callback.getExecutor()); + builder.setHttpMethod("GET"); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(userAgentValue, callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testDefaultUserAgent() throws Exception { + String userAgentName = "User-Agent"; + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoHeaderUrl(userAgentName), callback, callback.getExecutor()); + builder.setHttpMethod("GET"); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(new CronetEngine.Builder(getContext()).getDefaultUserAgent(), + callback.mResponseAsString); + } + + @Test + @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 = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .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)); + } + + @Test + @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 = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testDoubleWrite() throws Exception { + String url = Http2TestServer.getEchoStreamUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() { + @Override + public void onStreamReady(BidirectionalStream stream) { + // super class will call Write() once. + super.onStreamReady(stream); + // Call Write() again. + startNextWrite(stream); + // Make sure there is no pending write. + assertEquals(0, numPendingWrites()); + } + }; + callback.addWriteData("1".getBytes()); + callback.addWriteData("2".getBytes()); + // Create stream. + BidirectionalStream stream = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("12", callback.mResponseAsString); + } + + @Test + @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 = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("12", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + @DisabledTest(message = "Disabled due to timeout. See crbug.com/591112") + 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 = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .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); + } + + @Test + @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 = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .build(); + stream.start(); + // Write first. + callback.waitForNextWriteStep(); // onStreamReady + for (String expected : testData) { + // Write next chunk of test data. + callback.startNextWrite(stream); + callback.waitForNextWriteStep(); // onWriteCompleted + } + + // Wait for read step, but don't read yet. + callback.waitForNextReadStep(); // onResponseHeadersReceived + assertEquals("", callback.mResponseAsString); + // Read back. + callback.startNextRead(stream); + callback.waitForNextReadStep(); // onReadCompleted + // 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); + } + + @Test + @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 = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .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. + */ + @Test + @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 = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor()); + 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.setAutoAdvance(true); + callback.startNextRead(stream, readBuffer); + callback.blockForDone(); + + 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. + // The expected received bytes count is lower than it would be for the first request on the + // connection, because the server includes an HPACK dynamic table size update only in the + // first response HEADERS frame. + runSimpleGetWithExpectedReceivedByteCount(27); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBadBuffers() throws Exception { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.setAutoAdvance(false); + BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor()); + 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 expectError) { + // Use a fresh CronetEngine each time so Http2 session is not reused. + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + CronetTestUtil.setMockCertVerifierForTesting( + builder, QuicTestServer.createMockCertVerifier()); + mCronetEngine = builder.build(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.setFailure(failureType, failureStep); + TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); + mCronetEngine.addRequestFinishedListener(requestFinishedListener); + BidirectionalStream.Builder streamBuilder = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor()); + BidirectionalStream stream = streamBuilder.setHttpMethod("GET").build(); + Date startTime = new Date(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + requestFinishedListener.blockUntilDone(); + Date endTime = new Date(); + RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo(); + RequestFinishedInfo.Metrics metrics = finishedInfo.getMetrics(); + assertNotNull(metrics); + // Cancellation when stream is ready does not guarantee that + // mResponseInfo is null because there might be a + // onResponseHeadersReceived already queued in the executor. + // See crbug.com/594432. + if (failureStep != ResponseStep.ON_STREAM_READY) { + assertNotNull(callback.mResponseInfo); + } + // Check metrics information. + if (failureStep == ResponseStep.ON_RESPONSE_STARTED + || failureStep == ResponseStep.ON_READ_COMPLETED + || failureStep == ResponseStep.ON_TRAILERS) { + // For steps after response headers are received, there will be + // connect timing metrics. + MetricsTestUtil.checkTimingMetrics(metrics, startTime, endTime); + MetricsTestUtil.checkHasConnectTiming(metrics, startTime, endTime, true); + assertTrue(metrics.getSentByteCount() > 0); + assertTrue(metrics.getReceivedByteCount() > 0); + } else if (failureStep == ResponseStep.ON_STREAM_READY) { + assertNotNull(metrics.getRequestStart()); + MetricsTestUtil.assertAfter(metrics.getRequestStart(), startTime); + assertNotNull(metrics.getRequestEnd()); + MetricsTestUtil.assertAfter(endTime, metrics.getRequestEnd()); + MetricsTestUtil.assertAfter(metrics.getRequestEnd(), metrics.getRequestStart()); + } + assertEquals(expectError, callback.mError != null); + assertEquals(expectError, callback.mOnErrorCalled); + if (expectError) { + assertNotNull(finishedInfo.getException()); + assertEquals(RequestFinishedInfo.FAILED, finishedInfo.getFinishedReason()); + } else { + assertNull(finishedInfo.getException()); + assertEquals(RequestFinishedInfo.CANCELED, finishedInfo.getFinishedReason()); + } + assertEquals(failureType == FailureType.CANCEL_SYNC + || failureType == FailureType.CANCEL_ASYNC + || failureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, + callback.mOnCanceledCalled); + mCronetEngine.removeRequestFinishedListener(requestFinishedListener); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testFailures() throws Exception { + throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_STREAM_READY, false); + throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_STREAM_READY, false); + throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_STREAM_READY, false); + throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_STREAM_READY, true); + + throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_RESPONSE_STARTED, false); + throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_RESPONSE_STARTED, false); + throwOrCancel( + FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_RESPONSE_STARTED, false); + throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_RESPONSE_STARTED, true); + + throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_READ_COMPLETED, false); + throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_READ_COMPLETED, false); + throwOrCancel( + FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_READ_COMPLETED, false); + throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED, true); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testThrowOnSucceeded() { + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_SUCCEEDED); + BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor()); + 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); + } + + @Test + @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 = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor()); + 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) { + mCronetEngine.shutdown(); + // Clear mCronetEngine so it doesn't get shut down second time in tearDown(). + mCronetEngine = null; + super.onSucceeded(stream, info); + } + + @Override + public void onFailed( + BidirectionalStream stream, UrlResponseInfo info, CronetException error) { + mCronetEngine.shutdown(); + // Clear mCronetEngine so it doesn't get shut down second time in tearDown(). + mCronetEngine = null; + super.onFailed(stream, info, error); + } + + @Override + public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) { + mCronetEngine.shutdown(); + // Clear mCronetEngine so it doesn't get shut down second time in tearDown(). + mCronetEngine = null; + super.onCanceled(stream, info); + } + } + + @Test + @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 = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor()); + CronetBidirectionalStream stream = + (CronetBidirectionalStream) builder.setHttpMethod("GET").build(); + stream.start(); + try { + 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 { + 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 { + 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(); + } + + @Test + @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 = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor()); + CronetBidirectionalStream stream = + (CronetBidirectionalStream) builder.setHttpMethod("GET").build(); + stream.start(); + callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED); + callback.blockForDone(); + assertTrue(callback.mOnErrorCalled); + assertNull(mCronetEngine); + } + + @Test + @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 = mCronetEngine.newBidirectionalStreamBuilder( + Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor()); + 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 { + 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(mCronetEngine); + } + + /* + * Verifies NetworkException constructed from specific error codes are retryable. + */ + @SmallTest + @Feature({"Cronet"}) + @Test + @OnlyRunNativeCronet + public void testErrorCodes() throws Exception { + // Non-BidirectionalStream specific error codes. + checkSpecificErrorCode(NetError.ERR_NAME_NOT_RESOLVED, + NetworkException.ERROR_HOSTNAME_NOT_RESOLVED, false); + checkSpecificErrorCode(NetError.ERR_INTERNET_DISCONNECTED, + NetworkException.ERROR_INTERNET_DISCONNECTED, false); + checkSpecificErrorCode( + NetError.ERR_NETWORK_CHANGED, NetworkException.ERROR_NETWORK_CHANGED, true); + checkSpecificErrorCode( + NetError.ERR_CONNECTION_CLOSED, NetworkException.ERROR_CONNECTION_CLOSED, true); + checkSpecificErrorCode( + NetError.ERR_CONNECTION_REFUSED, NetworkException.ERROR_CONNECTION_REFUSED, false); + checkSpecificErrorCode( + NetError.ERR_CONNECTION_RESET, NetworkException.ERROR_CONNECTION_RESET, true); + checkSpecificErrorCode(NetError.ERR_CONNECTION_TIMED_OUT, + NetworkException.ERROR_CONNECTION_TIMED_OUT, true); + checkSpecificErrorCode(NetError.ERR_TIMED_OUT, NetworkException.ERROR_TIMED_OUT, true); + checkSpecificErrorCode(NetError.ERR_ADDRESS_UNREACHABLE, + NetworkException.ERROR_ADDRESS_UNREACHABLE, false); + // BidirectionalStream specific retryable error codes. + checkSpecificErrorCode(NetError.ERR_HTTP2_PING_FAILED, NetworkException.ERROR_OTHER, true); + checkSpecificErrorCode( + NetError.ERR_QUIC_HANDSHAKE_FAILED, NetworkException.ERROR_OTHER, true); + } + + // 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); + } + + private static void checkSpecificErrorCode( + int netError, int errorCode, boolean immediatelyRetryable) throws Exception { + NetworkException exception = + new BidirectionalStreamNetworkException("", errorCode, netError); + assertEquals(immediatelyRetryable, exception.immediatelyRetryable()); + assertEquals(netError, exception.getCronetInternalErrorCode()); + assertEquals(errorCode, exception.getErrorCode()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + @RequiresMinApi(10) // Tagging support added in API level 10: crrev.com/c/chromium/src/+/937583 + public void testTagging() throws Exception { + if (!CronetTestUtil.nativeCanGetTaggedBytes()) { + Log.i(TAG, "Skipping test - GetTaggedBytes unsupported."); + return; + } + String url = Http2TestServer.getEchoStreamUrl(); + + // Test untagged requests are given tag 0. + int tag = 0; + long priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + callback.addWriteData(new byte[] {0}); + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .build() + .start(); + callback.blockForDone(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + + // Test explicit tagging. + tag = 0x12345678; + priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + callback = new TestBidirectionalStreamCallback(); + callback.addWriteData(new byte[] {0}); + ExperimentalBidirectionalStream.Builder builder = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()); + assertEquals(builder.setTrafficStatsTag(tag), builder); + builder.build().start(); + callback.blockForDone(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + + // Test a different tag value to make sure reused connections are retagged. + tag = 0x87654321; + priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + callback = new TestBidirectionalStreamCallback(); + callback.addWriteData(new byte[] {0}); + builder = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()); + assertEquals(builder.setTrafficStatsTag(tag), builder); + builder.build().start(); + callback.blockForDone(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + + // Test tagging with our UID. + tag = 0; + priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + callback = new TestBidirectionalStreamCallback(); + callback.addWriteData(new byte[] {0}); + builder = + mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()); + assertEquals(builder.setTrafficStatsUid(Process.myUid()), builder); + builder.build().start(); + callback.blockForDone(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/BrotliTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/BrotliTest.java new file mode 100644 index 0000000000..6f142594f3 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/BrotliTest.java @@ -0,0 +1,115 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import static org.chromium.net.CronetTestRule.SERVER_CERT_PEM; +import static org.chromium.net.CronetTestRule.SERVER_KEY_PKCS8_PEM; +import static org.chromium.net.CronetTestRule.getContext; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.CronetTestRule.RequiresMinApi; + +/** + * Simple test for Brotli support. + */ +@RunWith(AndroidJUnit4.class) +@RequiresMinApi(5) // Brotli support added in API version 5: crrev.com/465216 +public class BrotliTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private CronetEngine mCronetEngine; + + @Before + public void setUp() throws Exception { + TestFilesInstaller.installIfNeeded(getContext()); + assertTrue(Http2TestServer.startHttp2TestServer( + getContext(), SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM)); + } + + @After + public void tearDown() throws Exception { + assertTrue(Http2TestServer.shutdownHttp2TestServer()); + if (mCronetEngine != null) { + mCronetEngine.shutdown(); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBrotliAdvertised() throws Exception { + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + builder.enableBrotli(true); + CronetTestUtil.setMockCertVerifierForTesting( + builder, QuicTestServer.createMockCertVerifier()); + mCronetEngine = builder.build(); + String url = Http2TestServer.getEchoAllHeadersUrl(); + TestUrlRequestCallback callback = startAndWaitForComplete(url); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertTrue(callback.mResponseAsString.contains("accept-encoding: gzip, deflate, br")); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBrotliNotAdvertised() throws Exception { + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + CronetTestUtil.setMockCertVerifierForTesting( + builder, QuicTestServer.createMockCertVerifier()); + mCronetEngine = builder.build(); + String url = Http2TestServer.getEchoAllHeadersUrl(); + TestUrlRequestCallback callback = startAndWaitForComplete(url); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertFalse(callback.mResponseAsString.contains("br")); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBrotliDecoded() throws Exception { + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + builder.enableBrotli(true); + CronetTestUtil.setMockCertVerifierForTesting( + builder, QuicTestServer.createMockCertVerifier()); + mCronetEngine = builder.build(); + String url = Http2TestServer.getServeSimpleBrotliResponse(); + TestUrlRequestCallback callback = startAndWaitForComplete(url); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + String expectedResponse = "The quick brown fox jumps over the lazy dog"; + assertEquals(expectedResponse, callback.mResponseAsString); + assertEquals(callback.mResponseInfo.getAllHeaders().get("content-encoding").get(0), "br"); + } + + private TestUrlRequestCallback startAndWaitForComplete(String url) { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = + mCronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + builder.build().start(); + callback.blockForDone(); + return callback; + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/Criteria.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/Criteria.java new file mode 100644 index 0000000000..8c503aaeb0 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/Criteria.java @@ -0,0 +1,17 @@ +// Copyright 2014 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; + +/** + * Provides a means for validating whether some condition/criteria has been met. + */ +public interface Criteria { + + /** + * @return Whether the criteria this is testing has been satisfied. + */ + public boolean isSatisfied(); + +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetEngineBuilderTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetEngineBuilderTest.java new file mode 100644 index 0000000000..9edf180d38 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetEngineBuilderTest.java @@ -0,0 +1,163 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import static org.chromium.net.CronetProvider.PROVIDER_NAME_APP_PACKAGED; +import static org.chromium.net.CronetProvider.PROVIDER_NAME_FALLBACK; +import static org.chromium.net.CronetTestRule.getContext; + +import android.content.Context; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Tests {@link CronetEngine.Builder}. + */ +@RunWith(AndroidJUnit4.class) +public class CronetEngineBuilderTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + /** + * Tests the comparison of two strings that contain versions. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @CronetTestRule.OnlyRunNativeCronet + public void testVersionComparison() { + assertVersionIsHigher("22.44", "22.43.12"); + assertVersionIsLower("22.43.12", "022.124"); + assertVersionIsLower("22.99", "22.100"); + assertVersionIsHigher("22.100", "22.99"); + assertVersionIsEqual("11.2.33", "11.2.33"); + assertIllegalArgumentException(null, "1.2.3"); + assertIllegalArgumentException("1.2.3", null); + assertIllegalArgumentException("1.2.3", "1.2.3x"); + } + + /** + * Tests the correct ordering of the providers. The platform provider should be + * the last in the list. Other providers should be ordered by placing providers + * with the higher version first. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testProviderOrdering() { + final CronetProvider[] availableProviders = new CronetProvider[] { + new FakeProvider(getContext(), PROVIDER_NAME_APP_PACKAGED, "99.77", true), + new FakeProvider(getContext(), PROVIDER_NAME_FALLBACK, "99.99", true), + new FakeProvider(getContext(), "Some other provider", "99.88", true), + }; + + ArrayList providers = new ArrayList<>(Arrays.asList(availableProviders)); + List orderedProviders = + CronetEngine.Builder.getEnabledCronetProviders(getContext(), providers); + + // Check the result + assertEquals(availableProviders[2], orderedProviders.get(0)); + assertEquals(availableProviders[0], orderedProviders.get(1)); + assertEquals(availableProviders[1], orderedProviders.get(2)); + } + + /** + * Tests that the providers that are disabled are not included in the list of available + * providers when the provider is selected by the default selection logic. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testThatDisabledProvidersAreExcluded() { + final CronetProvider[] availableProviders = new CronetProvider[] { + new FakeProvider(getContext(), PROVIDER_NAME_FALLBACK, "99.99", true), + new FakeProvider(getContext(), PROVIDER_NAME_APP_PACKAGED, "99.77", true), + new FakeProvider(getContext(), "Some other provider", "99.88", false), + }; + + ArrayList providers = new ArrayList<>(Arrays.asList(availableProviders)); + List orderedProviders = + CronetEngine.Builder.getEnabledCronetProviders(getContext(), providers); + + assertEquals("Unexpected number of providers in the list", 2, orderedProviders.size()); + assertEquals(PROVIDER_NAME_APP_PACKAGED, orderedProviders.get(0).getName()); + assertEquals(PROVIDER_NAME_FALLBACK, orderedProviders.get(1).getName()); + } + + private void assertVersionIsHigher(String s1, String s2) { + assertEquals(1, CronetEngine.Builder.compareVersions(s1, s2)); + } + + private void assertVersionIsLower(String s1, String s2) { + assertEquals(-1, CronetEngine.Builder.compareVersions(s1, s2)); + } + + private void assertVersionIsEqual(String s1, String s2) { + assertEquals(0, CronetEngine.Builder.compareVersions(s1, s2)); + } + + private void assertIllegalArgumentException(String s1, String s2) { + try { + CronetEngine.Builder.compareVersions(s1, s2); + } catch (IllegalArgumentException e) { + // Do nothing. It is expected. + return; + } + fail("Expected IllegalArgumentException"); + } + + // TODO(kapishnikov): Replace with a mock when mockito is supported. + private static class FakeProvider extends CronetProvider { + private final String mName; + private final String mVersion; + private final boolean mEnabled; + + protected FakeProvider(Context context, String name, String version, boolean enabled) { + super(context); + mName = name; + mVersion = version; + mEnabled = enabled; + } + + @Override + public CronetEngine.Builder createBuilder() { + return new CronetEngine.Builder((ICronetEngineBuilder) null); + } + + @Override + public String getName() { + return mName; + } + + @Override + public String getVersion() { + return mVersion; + } + + @Override + public boolean isEnabled() { + return mEnabled; + } + + @Override + public String toString() { + return mName; + } + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetStressTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetStressTest.java new file mode 100644 index 0000000000..5f29dbfed7 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetStressTest.java @@ -0,0 +1,73 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import static org.chromium.net.CronetTestRule.getContext; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.LargeTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.CronetTestFramework; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; + +/** + * Tests that making a large number of requests do not lead to crashes. + */ +@RunWith(AndroidJUnit4.class) +public class CronetStressTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + private CronetTestFramework mTestFramework; + + @Before + public void setUp() throws Exception { + mTestFramework = mTestRule.startCronetTestFramework(); + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @After + public void tearDown() throws Exception { + NativeTestServer.shutdownNativeTestServer(); + mTestFramework.mCronetEngine.shutdown(); + } + + @Test + @LargeTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + public void testLargeNumberOfUploads() throws Exception { + final int kNumRequest = 1000; + final int kNumRequestHeaders = 100; + final int kNumUploadBytes = 1000; + final byte[] b = new byte[kNumUploadBytes]; + for (int i = 0; i < kNumRequest; i++) { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoAllHeadersURL(), callback, callback.getExecutor()); + for (int j = 0; j < kNumRequestHeaders; j++) { + builder.addHeader("header" + j, Integer.toString(j)); + } + builder.addHeader("content-type", "useless/string"); + builder.setUploadDataProvider( + UploadDataProviders.create(b, 0, kNumUploadBytes), callback.getExecutor()); + UrlRequest request = builder.build(); + request.start(); + callback.blockForDone(); + callback.shutdownExecutor(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + } + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestRule.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestRule.java new file mode 100644 index 0000000000..7121d78d96 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestRule.java @@ -0,0 +1,359 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net; + +import android.content.Context; +import android.os.StrictMode; +import android.support.test.InstrumentationRegistry; + +import org.junit.Assert; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import org.chromium.base.ContextUtils; +import org.chromium.base.Log; +import org.chromium.base.PathUtils; +import org.chromium.net.impl.JavaCronetEngine; +import org.chromium.net.impl.JavaCronetProvider; +import org.chromium.net.impl.UserAgent; + +import java.io.File; +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.net.URL; +import java.net.URLStreamHandlerFactory; + +/** + * Custom TestRule for Cronet instrumentation tests. + * + * TODO(yolandyan): refactor this to three TestRules, one for setUp and tearDown, + * one for tests under org.chromium.net.urlconnection, one for test under + * org.chromium.net + */ +public class CronetTestRule implements TestRule { + private static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "cronet_test"; + + private CronetTestFramework mCronetTestFramework; + + // {@code true} when test is being run against system HttpURLConnection implementation. + private boolean mTestingSystemHttpURLConnection; + private boolean mTestingJavaImpl; + private StrictMode.VmPolicy mOldVmPolicy; + + /** + * Name of the file that contains the test server certificate in PEM format. + */ + public static final String SERVER_CERT_PEM = "quic-chain.pem"; + + /** + * Name of the file that contains the test server private key in PKCS8 PEM format. + */ + public static final String SERVER_KEY_PKCS8_PEM = "quic-leaf-cert.key.pkcs8.pem"; + + private static final String TAG = CronetTestRule.class.getSimpleName(); + + /** + * Creates and holds pointer to CronetEngine. + */ + public static class CronetTestFramework { + public ExperimentalCronetEngine mCronetEngine; + + public CronetTestFramework(Context context) { + mCronetEngine = new ExperimentalCronetEngine.Builder(context).enableQuic(true).build(); + // Start collecting metrics. + mCronetEngine.getGlobalMetricsDeltas(); + } + } + + int getMaximumAvailableApiLevel() { + // Prior to M59 the ApiVersion.getMaximumAvailableApiLevel API didn't exist + if (ApiVersion.getCronetVersion().compareTo("59") < 0) { + return 3; + } + return ApiVersion.getMaximumAvailableApiLevel(); + } + + public static Context getContext() { + return InstrumentationRegistry.getTargetContext(); + } + + @Override + public Statement apply(final Statement base, final Description desc) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + setUp(); + runBase(base, desc); + tearDown(); + } + }; + } + + /** + * Returns {@code true} when test is being run against system HttpURLConnection implementation. + */ + public boolean testingSystemHttpURLConnection() { + return mTestingSystemHttpURLConnection; + } + + /** + * Returns {@code true} when test is being run against the java implementation of CronetEngine. + */ + public boolean testingJavaImpl() { + return mTestingJavaImpl; + } + + // TODO(yolandyan): refactor this using parameterize framework + private void runBase(Statement base, Description desc) throws Throwable { + setTestingSystemHttpURLConnection(false); + setTestingJavaImpl(false); + String packageName = desc.getTestClass().getPackage().getName(); + + // Find the API version required by the test. + int requiredApiVersion = getMaximumAvailableApiLevel(); + for (Annotation a : desc.getTestClass().getAnnotations()) { + if (a instanceof RequiresMinApi) { + requiredApiVersion = ((RequiresMinApi) a).value(); + } + } + for (Annotation a : desc.getAnnotations()) { + if (a instanceof RequiresMinApi) { + // Method scoped requirements take precedence over class scoped + // requirements. + requiredApiVersion = ((RequiresMinApi) a).value(); + } + } + + if (requiredApiVersion > getMaximumAvailableApiLevel()) { + Log.i(TAG, + desc.getMethodName() + " skipped because it requires API " + requiredApiVersion + + " but only API " + getMaximumAvailableApiLevel() + " is present."); + } else if (packageName.equals("org.chromium.net.urlconnection")) { + try { + if (desc.getAnnotation(CompareDefaultWithCronet.class) != null) { + // Run with the default HttpURLConnection implementation first. + setTestingSystemHttpURLConnection(true); + base.evaluate(); + // Use Cronet's implementation, and run the same test. + setTestingSystemHttpURLConnection(false); + base.evaluate(); + } else { + // For all other tests. + base.evaluate(); + } + } catch (Throwable e) { + throw new Throwable("Cronet Test failed.", e); + } + } else if (packageName.equals("org.chromium.net")) { + try { + base.evaluate(); + if (desc.getAnnotation(OnlyRunNativeCronet.class) == null) { + setTestingJavaImpl(true); + base.evaluate(); + } + } catch (Throwable e) { + throw new Throwable("CronetTestBase#runTest failed.", e); + } + } else { + base.evaluate(); + } + } + + void setUp() throws Exception { + System.loadLibrary("cronet_tests"); + ContextUtils.initApplicationContext(getContext().getApplicationContext()); + PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX); + prepareTestStorage(getContext()); + mOldVmPolicy = StrictMode.getVmPolicy(); + // Only enable StrictMode testing after leaks were fixed in crrev.com/475945 + if (getMaximumAvailableApiLevel() >= 7) { + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectLeakedClosableObjects() + .penaltyLog() + .penaltyDeath() + .build()); + } + } + + void tearDown() throws Exception { + try { + // Run GC and finalizers a few times to pick up leaked closeables + for (int i = 0; i < 10; i++) { + System.gc(); + System.runFinalization(); + } + System.gc(); + System.runFinalization(); + } finally { + StrictMode.setVmPolicy(mOldVmPolicy); + } + } + + /** + * Starts the CronetTest framework. + */ + public CronetTestFramework startCronetTestFramework() { + mCronetTestFramework = new CronetTestFramework(getContext()); + if (testingJavaImpl()) { + ExperimentalCronetEngine.Builder builder = createJavaEngineBuilder(); + builder.setUserAgent(UserAgent.from(getContext())); + mCronetTestFramework.mCronetEngine = builder.build(); + // Make sure that the instantiated engine is JavaCronetEngine. + assert mCronetTestFramework.mCronetEngine.getClass() == JavaCronetEngine.class; + } + return mCronetTestFramework; + } + + /** + * Creates and returns {@link ExperimentalCronetEngine.Builder} that creates + * Java (platform) based {@link CronetEngine.Builder}. + * + * @return the {@code CronetEngine.Builder} that builds Java-based {@code Cronet engine}. + */ + public ExperimentalCronetEngine.Builder createJavaEngineBuilder() { + return (ExperimentalCronetEngine.Builder) new JavaCronetProvider(getContext()) + .createBuilder(); + } + + public void assertResponseEquals(UrlResponseInfo expected, UrlResponseInfo actual) { + Assert.assertEquals(expected.getAllHeaders(), actual.getAllHeaders()); + Assert.assertEquals(expected.getAllHeadersAsList(), actual.getAllHeadersAsList()); + Assert.assertEquals(expected.getHttpStatusCode(), actual.getHttpStatusCode()); + Assert.assertEquals(expected.getHttpStatusText(), actual.getHttpStatusText()); + Assert.assertEquals(expected.getUrlChain(), actual.getUrlChain()); + Assert.assertEquals(expected.getUrl(), actual.getUrl()); + // Transferred bytes and proxy server are not supported in pure java + if (!testingJavaImpl()) { + Assert.assertEquals(expected.getReceivedByteCount(), actual.getReceivedByteCount()); + Assert.assertEquals(expected.getProxyServer(), actual.getProxyServer()); + // This is a place where behavior intentionally differs between native and java + Assert.assertEquals(expected.getNegotiatedProtocol(), actual.getNegotiatedProtocol()); + } + } + + public static void assertContains(String expectedSubstring, String actualString) { + Assert.assertNotNull(actualString); + if (!actualString.contains(expectedSubstring)) { + Assert.fail("String [" + actualString + "] doesn't contain substring [" + + expectedSubstring + "]"); + } + } + + public CronetEngine.Builder enableDiskCache(CronetEngine.Builder cronetEngineBuilder) { + cronetEngineBuilder.setStoragePath(getTestStorage(getContext())); + cronetEngineBuilder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, 1000 * 1024); + return cronetEngineBuilder; + } + + /** + * Sets the {@link URLStreamHandlerFactory} from {@code cronetEngine}. This should be called + * during setUp() and is installed by {@link runTest()} as the default when Cronet is tested. + */ + public void setStreamHandlerFactory(CronetEngine cronetEngine) { + if (!testingSystemHttpURLConnection()) { + URL.setURLStreamHandlerFactory(cronetEngine.createURLStreamHandlerFactory()); + } + } + + /** + * Annotation for test methods in org.chromium.net.urlconnection pacakage that runs them + * against both Cronet's HttpURLConnection implementation, and against the system's + * HttpURLConnection implementation. + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface CompareDefaultWithCronet {} + + /** + * Annotation for test methods in org.chromium.net.urlconnection pacakage that runs them + * only against Cronet's HttpURLConnection implementation, and not against the system's + * HttpURLConnection implementation. + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface OnlyRunCronetHttpURLConnection {} + + /** + * Annotation for test methods in org.chromium.net package that disables rerunning the test + * against the Java-only implementation. When this annotation is present the test is only run + * against the native implementation. + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface OnlyRunNativeCronet {} + + /** + * Annotation allowing classes or individual tests to be skipped based on the version of the + * Cronet API present. Takes the minimum API version upon which the test should be run. + * For example if a test should only be run with API version 2 or greater: + * @RequiresMinApi(2) + * public void testFoo() {} + */ + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface RequiresMinApi { + int value(); + } + /** + * Prepares the path for the test storage (http cache, QUIC server info). + */ + public static void prepareTestStorage(Context context) { + File storage = new File(getTestStorageDirectory()); + if (storage.exists()) { + Assert.assertTrue(recursiveDelete(storage)); + } + ensureTestStorageExists(); + } + + /** + * Returns the path for the test storage (http cache, QUIC server info). + * Also ensures it exists. + */ + public static String getTestStorage(Context context) { + ensureTestStorageExists(); + return getTestStorageDirectory(); + } + + /** + * Returns the path for the test storage (http cache, QUIC server info). + * NOTE: Does not ensure it exists; tests should use {@link #getTestStorage}. + */ + private static String getTestStorageDirectory() { + return PathUtils.getDataDirectory() + "/test_storage"; + } + + /** + * Ensures test storage directory exists, i.e. creates one if it does not exist. + */ + private static void ensureTestStorageExists() { + File storage = new File(getTestStorageDirectory()); + if (!storage.exists()) { + Assert.assertTrue(storage.mkdir()); + } + } + + private static boolean recursiveDelete(File path) { + if (path.isDirectory()) { + for (File c : path.listFiles()) { + if (!recursiveDelete(c)) { + return false; + } + } + } + return path.delete(); + } + + private void setTestingSystemHttpURLConnection(boolean value) { + mTestingSystemHttpURLConnection = value; + } + + private void setTestingJavaImpl(boolean value) { + mTestingJavaImpl = value; + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestRuleTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestRuleTest.java new file mode 100644 index 0000000000..05ac2dfd94 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestRuleTest.java @@ -0,0 +1,100 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.CronetTestFramework; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.CronetTestRule.RequiresMinApi; +import org.chromium.net.impl.CronetUrlRequestContext; +import org.chromium.net.impl.JavaCronetEngine; + +/** + * Tests features of CronetTestRule. + */ +@RunWith(AndroidJUnit4.class) +public class CronetTestRuleTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + @Rule + public final TestName mTestName = new TestName(); + + private CronetTestFramework mTestFramework; + /** + * For any test whose name contains "MustRun", it's enforced that the test must run and set + * {@code mTestWasRun} to {@code true}. + */ + private boolean mTestWasRun; + + @Before + public void setUp() throws Exception { + mTestWasRun = false; + mTestFramework = mTestRule.startCronetTestFramework(); + } + + @After + public void tearDown() throws Exception { + if (mTestName.getMethodName().contains("MustRun") && !mTestWasRun) { + fail(mTestName.getMethodName() + " should have run but didn't."); + } + } + + @Test + @SmallTest + @RequiresMinApi(999999999) + @Feature({"Cronet"}) + public void testRequiresMinApiDisable() { + fail("RequiresMinApi failed to disable."); + } + + @Test + @SmallTest + @RequiresMinApi(-999999999) + @Feature({"Cronet"}) + public void testRequiresMinApiMustRun() { + mTestWasRun = true; + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testRunBothImplsMustRun() { + if (mTestRule.testingJavaImpl()) { + assertFalse(mTestWasRun); + mTestWasRun = true; + assertEquals(mTestFramework.mCronetEngine.getClass(), JavaCronetEngine.class); + } else { + assertFalse(mTestWasRun); + mTestWasRun = true; + assertEquals(mTestFramework.mCronetEngine.getClass(), CronetUrlRequestContext.class); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testRunOnlyNativeMustRun() { + assertFalse(mTestRule.testingJavaImpl()); + assertFalse(mTestWasRun); + mTestWasRun = true; + assertEquals(mTestFramework.mCronetEngine.getClass(), CronetUrlRequestContext.class); + } +} \ No newline at end of file diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetUploadTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetUploadTest.java new file mode 100644 index 0000000000..06032c69f4 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetUploadTest.java @@ -0,0 +1,338 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import static org.chromium.net.CronetTestRule.getContext; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.CronetTestFramework; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.impl.CronetUploadDataStream; +import org.chromium.net.impl.CronetUrlRequest; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Tests that directly drive {@code CronetUploadDataStream} and + * {@code UploadDataProvider} to simulate different ordering of reset, init, + * read, and rewind calls. + */ +@RunWith(AndroidJUnit4.class) +public class CronetUploadTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private TestDrivenDataProvider mDataProvider; + private CronetUploadDataStream mUploadDataStream; + private TestUploadDataStreamHandler mHandler; + private CronetTestFramework mTestFramework; + + @Before + @SuppressWarnings({"PrimitiveArrayPassedToVarargsMethod", "ArraysAsListPrimitiveArray"}) + public void setUp() throws Exception { + mTestFramework = mTestRule.startCronetTestFramework(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + List reads = Arrays.asList("hello".getBytes()); + mDataProvider = new TestDrivenDataProvider(executor, reads); + + // Creates a dummy CronetUrlRequest, which is not used to drive CronetUploadDataStream. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + "https://dummy.url", callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + + mUploadDataStream = + new CronetUploadDataStream(mDataProvider, executor, (CronetUrlRequest) urlRequest); + mHandler = new TestUploadDataStreamHandler( + getContext(), mUploadDataStream.createUploadDataStreamForTesting()); + } + + @After + public void tearDown() throws Exception { + // Destroy handler's native objects. + mHandler.destroyNativeObjects(); + } + + /** + * Tests that after some data is read, init triggers a rewind, and that + * before the rewind completes, init blocks. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testInitTriggersRewindAndInitBeforeRewindCompletes() throws Exception { + // Init completes synchronously and read succeeds. + assertTrue(mHandler.init()); + mHandler.read(); + mDataProvider.waitForReadRequest(); + mHandler.checkReadCallbackNotInvoked(); + mDataProvider.onReadSucceeded(mUploadDataStream); + mHandler.waitForReadComplete(); + mDataProvider.assertReadNotPending(); + assertEquals(0, mDataProvider.getNumRewindCalls()); + assertEquals(1, mDataProvider.getNumReadCalls()); + assertEquals("hello", mHandler.getData()); + + // Reset and then init, which should trigger a rewind. + mHandler.reset(); + assertEquals("", mHandler.getData()); + assertFalse(mHandler.init()); + mDataProvider.waitForRewindRequest(); + mHandler.checkInitCallbackNotInvoked(); + + // Before rewind completes, reset and init should block. + mHandler.reset(); + assertFalse(mHandler.init()); + + // Signal rewind completes, and wait for init to complete. + mHandler.checkInitCallbackNotInvoked(); + mDataProvider.onRewindSucceeded(mUploadDataStream); + mHandler.waitForInitComplete(); + mDataProvider.assertRewindNotPending(); + + // Read should complete successfully since init has completed. + mHandler.read(); + mDataProvider.waitForReadRequest(); + mHandler.checkReadCallbackNotInvoked(); + mDataProvider.onReadSucceeded(mUploadDataStream); + mHandler.waitForReadComplete(); + mDataProvider.assertReadNotPending(); + assertEquals(1, mDataProvider.getNumRewindCalls()); + assertEquals(2, mDataProvider.getNumReadCalls()); + assertEquals("hello", mHandler.getData()); + } + + /** + * Tests that after some data is read, init triggers a rewind, and that + * after the rewind completes, init does not block. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testInitTriggersRewindAndInitAfterRewindCompletes() throws Exception { + // Init completes synchronously and read succeeds. + assertTrue(mHandler.init()); + mHandler.read(); + mDataProvider.waitForReadRequest(); + mHandler.checkReadCallbackNotInvoked(); + mDataProvider.onReadSucceeded(mUploadDataStream); + mHandler.waitForReadComplete(); + mDataProvider.assertReadNotPending(); + assertEquals(0, mDataProvider.getNumRewindCalls()); + assertEquals(1, mDataProvider.getNumReadCalls()); + assertEquals("hello", mHandler.getData()); + + // Reset and then init, which should trigger a rewind. + mHandler.reset(); + assertEquals("", mHandler.getData()); + assertFalse(mHandler.init()); + mDataProvider.waitForRewindRequest(); + mHandler.checkInitCallbackNotInvoked(); + + // Signal rewind completes, and wait for init to complete. + mDataProvider.onRewindSucceeded(mUploadDataStream); + mHandler.waitForInitComplete(); + mDataProvider.assertRewindNotPending(); + + // Reset and init should not block, since rewind has completed. + mHandler.reset(); + assertTrue(mHandler.init()); + + // Read should complete successfully since init has completed. + mHandler.read(); + mDataProvider.waitForReadRequest(); + mHandler.checkReadCallbackNotInvoked(); + mDataProvider.onReadSucceeded(mUploadDataStream); + mHandler.waitForReadComplete(); + mDataProvider.assertReadNotPending(); + assertEquals(1, mDataProvider.getNumRewindCalls()); + assertEquals(2, mDataProvider.getNumReadCalls()); + assertEquals("hello", mHandler.getData()); + } + + /** + * Tests that if init before read completes, a rewind is triggered when + * read completes. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testReadCompleteTriggerRewind() throws Exception { + // Reset and init before read completes. + assertTrue(mHandler.init()); + mHandler.read(); + mDataProvider.waitForReadRequest(); + mHandler.checkReadCallbackNotInvoked(); + mHandler.reset(); + // Init should return asynchronously, since there is a pending read. + assertFalse(mHandler.init()); + mDataProvider.assertRewindNotPending(); + mHandler.checkInitCallbackNotInvoked(); + assertEquals(0, mDataProvider.getNumRewindCalls()); + assertEquals(1, mDataProvider.getNumReadCalls()); + assertEquals("", mHandler.getData()); + + // Read completes should trigger a rewind. + mDataProvider.onReadSucceeded(mUploadDataStream); + mDataProvider.waitForRewindRequest(); + mHandler.checkInitCallbackNotInvoked(); + mDataProvider.onRewindSucceeded(mUploadDataStream); + mHandler.waitForInitComplete(); + mDataProvider.assertRewindNotPending(); + assertEquals(1, mDataProvider.getNumRewindCalls()); + assertEquals(1, mDataProvider.getNumReadCalls()); + assertEquals("", mHandler.getData()); + } + + /** + * Tests that when init again after rewind completes, no additional rewind + * is triggered. This test is the same as testReadCompleteTriggerRewind + * except that this test invokes reset and init again in the end. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testReadCompleteTriggerRewindOnlyOneRewind() throws Exception { + testReadCompleteTriggerRewind(); + // Reset and Init again, no rewind should happen. + mHandler.reset(); + assertTrue(mHandler.init()); + mDataProvider.assertRewindNotPending(); + assertEquals(1, mDataProvider.getNumRewindCalls()); + assertEquals(1, mDataProvider.getNumReadCalls()); + assertEquals("", mHandler.getData()); + } + + /** + * Tests that if reset before read completes, no rewind is triggered, and + * that a following init triggers rewind. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testResetBeforeReadCompleteAndInitTriggerRewind() throws Exception { + // Reset before read completes. Rewind is not triggered. + assertTrue(mHandler.init()); + mHandler.read(); + mDataProvider.waitForReadRequest(); + mHandler.checkReadCallbackNotInvoked(); + mHandler.reset(); + mDataProvider.onReadSucceeded(mUploadDataStream); + mDataProvider.assertRewindNotPending(); + assertEquals(0, mDataProvider.getNumRewindCalls()); + assertEquals(1, mDataProvider.getNumReadCalls()); + assertEquals("", mHandler.getData()); + + // Init should trigger a rewind. + assertFalse(mHandler.init()); + mDataProvider.waitForRewindRequest(); + mHandler.checkInitCallbackNotInvoked(); + mDataProvider.onRewindSucceeded(mUploadDataStream); + mHandler.waitForInitComplete(); + mDataProvider.assertRewindNotPending(); + assertEquals(1, mDataProvider.getNumRewindCalls()); + assertEquals(1, mDataProvider.getNumReadCalls()); + assertEquals("", mHandler.getData()); + } + + /** + * Tests that there is no crash when native CronetUploadDataStream is + * destroyed while read is pending. The test is racy since the read could + * complete either before or after the Java CronetUploadDataStream's + * onDestroyUploadDataStream() method is invoked. However, the test should + * pass either way, though we are interested in the latter case. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testDestroyNativeStreamBeforeReadComplete() throws Exception { + // Start a read and wait for it to be pending. + assertTrue(mHandler.init()); + mHandler.read(); + mDataProvider.waitForReadRequest(); + mHandler.checkReadCallbackNotInvoked(); + + // Destroy the C++ TestUploadDataStreamHandler. The handler will then + // destroy the C++ CronetUploadDataStream it owns on the network thread. + // That will result in calling the Java CronetUploadDataSteam's + // onUploadDataStreamDestroyed() method on its executor thread, which + // will then destroy the CronetUploadDataStreamAdapter. + mHandler.destroyNativeObjects(); + + // Make the read complete should not encounter a crash. + mDataProvider.onReadSucceeded(mUploadDataStream); + + assertEquals(0, mDataProvider.getNumRewindCalls()); + assertEquals(1, mDataProvider.getNumReadCalls()); + } + + /** + * Tests that there is no crash when native CronetUploadDataStream is + * destroyed while rewind is pending. The test is racy since rewind could + * complete either before or after the Java CronetUploadDataStream's + * onDestroyUploadDataStream() method is invoked. However, the test should + * pass either way, though we are interested in the latter case. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testDestroyNativeStreamBeforeRewindComplete() throws Exception { + // Start a read and wait for it to complete. + assertTrue(mHandler.init()); + mHandler.read(); + mDataProvider.waitForReadRequest(); + mHandler.checkReadCallbackNotInvoked(); + mDataProvider.onReadSucceeded(mUploadDataStream); + mHandler.waitForReadComplete(); + mDataProvider.assertReadNotPending(); + assertEquals(0, mDataProvider.getNumRewindCalls()); + assertEquals(1, mDataProvider.getNumReadCalls()); + assertEquals("hello", mHandler.getData()); + + // Reset and then init, which should trigger a rewind. + mHandler.reset(); + assertEquals("", mHandler.getData()); + assertFalse(mHandler.init()); + mDataProvider.waitForRewindRequest(); + mHandler.checkInitCallbackNotInvoked(); + + // Destroy the C++ TestUploadDataStreamHandler. The handler will then + // destroy the C++ CronetUploadDataStream it owns on the network thread. + // That will result in calling the Java CronetUploadDataSteam's + // onUploadDataStreamDestroyed() method on its executor thread, which + // will then destroy the CronetUploadDataStreamAdapter. + mHandler.destroyNativeObjects(); + + // Signal rewind completes, and wait for init to complete. + mDataProvider.onRewindSucceeded(mUploadDataStream); + + assertEquals(1, mDataProvider.getNumRewindCalls()); + assertEquals(1, mDataProvider.getNumReadCalls()); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestContextTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestContextTest.java new file mode 100644 index 0000000000..ed0df41278 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestContextTest.java @@ -0,0 +1,1458 @@ +// Copyright 2014 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.net.CronetEngine.Builder.HTTP_CACHE_IN_MEMORY; +import static org.chromium.net.CronetTestRule.assertContains; +import static org.chromium.net.CronetTestRule.getContext; +import static org.chromium.net.CronetTestRule.getTestStorage; + +import android.content.Context; +import android.content.ContextWrapper; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.FileUtils; +import org.chromium.base.PathUtils; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.CronetTestFramework; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.CronetTestRule.RequiresMinApi; +import org.chromium.net.TestUrlRequestCallback.ResponseStep; +import org.chromium.net.impl.CronetEngineBuilderImpl; +import org.chromium.net.impl.CronetUrlRequestContext; +import org.chromium.net.impl.NativeCronetEngineBuilderImpl; +import org.chromium.net.test.EmbeddedTestServer; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.FutureTask; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Test CronetEngine. + */ +@RunWith(AndroidJUnit4.class) +@JNINamespace("cronet") +public class CronetUrlRequestContextTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private static final String TAG = CronetUrlRequestContextTest.class.getSimpleName(); + + // URLs used for tests. + private static final String MOCK_CRONET_TEST_FAILED_URL = + "http://mock.failed.request/-2"; + private static final String MOCK_CRONET_TEST_SUCCESS_URL = + "http://mock.http/success.txt"; + private static final int MAX_FILE_SIZE = 1000000000; + private static final int NUM_EVENT_FILES = 10; + + private EmbeddedTestServer mTestServer; + private String mUrl; + private String mUrl404; + private String mUrl500; + + @Before + public void setUp() throws Exception { + mTestServer = EmbeddedTestServer.createAndStartServer(getContext()); + mUrl = mTestServer.getURL("/echo?status=200"); + mUrl404 = mTestServer.getURL("/echo?status=404"); + mUrl500 = mTestServer.getURL("/echo?status=500"); + } + + @After + public void tearDown() throws Exception { + mTestServer.stopAndDestroyServer(); + } + + static class RequestThread extends Thread { + public TestUrlRequestCallback mCallback; + + final String mUrl; + final ConditionVariable mRunBlocker; + + public RequestThread(String url, ConditionVariable runBlocker) { + mUrl = url; + mRunBlocker = runBlocker; + } + + @Override + public void run() { + mRunBlocker.block(); + CronetEngine cronetEngine = new CronetEngine.Builder(getContext()).build(); + mCallback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(mUrl, mCallback, mCallback.getExecutor()); + urlRequestBuilder.build().start(); + mCallback.blockForDone(); + } + } + + /** + * Callback that shutdowns the request context when request has succeeded + * or failed. + */ + static class ShutdownTestUrlRequestCallback extends TestUrlRequestCallback { + private final CronetEngine mCronetEngine; + private final ConditionVariable mCallbackCompletionBlock = new ConditionVariable(); + + ShutdownTestUrlRequestCallback(CronetEngine cronetEngine) { + mCronetEngine = cronetEngine; + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + super.onSucceeded(request, info); + mCronetEngine.shutdown(); + mCallbackCompletionBlock.open(); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) { + super.onFailed(request, info, error); + mCronetEngine.shutdown(); + mCallbackCompletionBlock.open(); + } + + // Wait for request completion callback. + void blockForCallbackToComplete() { + mCallbackCompletionBlock.block(); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @SuppressWarnings("deprecation") + public void testConfigUserAgent() throws Exception { + String userAgentName = "User-Agent"; + String userAgentValue = "User-Agent-Value"; + ExperimentalCronetEngine.Builder cronetEngineBuilder = + new ExperimentalCronetEngine.Builder(getContext()); + if (mTestRule.testingJavaImpl()) { + cronetEngineBuilder = mTestRule.createJavaEngineBuilder(); + } + cronetEngineBuilder.setUserAgent(userAgentValue); + final CronetEngine cronetEngine = cronetEngineBuilder.build(); + NativeTestServer.shutdownNativeTestServer(); // startNativeTestServer returns false if it's + // already running + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = cronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoHeaderURL(userAgentName), callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + assertEquals(userAgentValue, callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + // TODO: Remove the annotation after fixing http://crbug.com/637979 & http://crbug.com/637972 + @OnlyRunNativeCronet + public void testShutdown() throws Exception { + final CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + ShutdownTestUrlRequestCallback callback = + new ShutdownTestUrlRequestCallback(testFramework.mCronetEngine); + // Block callback when response starts to verify that shutdown fails + // if there are active requests. + callback.setAutoAdvance(false); + UrlRequest.Builder urlRequestBuilder = testFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + UrlRequest urlRequest = urlRequestBuilder.build(); + urlRequest.start(); + try { + testFramework.mCronetEngine.shutdown(); + fail("Should throw an exception"); + } catch (Exception e) { + assertEquals("Cannot shutdown with active requests.", e.getMessage()); + } + + callback.waitForNextStep(); + assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep); + try { + testFramework.mCronetEngine.shutdown(); + fail("Should throw an exception"); + } catch (Exception e) { + assertEquals("Cannot shutdown with active requests.", e.getMessage()); + } + callback.startNextRead(urlRequest); + + callback.waitForNextStep(); + assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep); + try { + testFramework.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(urlRequest); + callback.blockForDone(); + callback.blockForCallbackToComplete(); + callback.shutdownExecutor(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testShutdownDuringInit() throws Exception { + final ConditionVariable block = new ConditionVariable(false); + + // Post a task to main thread to block until shutdown is called to test + // scenario when shutdown is called right after construction before + // context is fully initialized on the main thread. + Runnable blockingTask = new Runnable() { + @Override + public void run() { + try { + block.block(); + } catch (Exception e) { + fail("Caught " + e.getMessage()); + } + } + }; + // Ensure that test is not running on the main thread. + assertTrue(Looper.getMainLooper() != Looper.myLooper()); + new Handler(Looper.getMainLooper()).post(blockingTask); + + // Create new request context, but its initialization on the main thread + // will be stuck behind blockingTask. + final CronetUrlRequestContext cronetEngine = + (CronetUrlRequestContext) new CronetEngine.Builder(getContext()).build(); + // Unblock the main thread, so context gets initialized and shutdown on + // it. + block.open(); + // Shutdown will wait for init to complete on main thread. + cronetEngine.shutdown(); + // Verify that context is shutdown. + try { + cronetEngine.getUrlRequestContextAdapter(); + fail("Should throw an exception."); + } catch (Exception e) { + assertEquals("Engine is shut down.", e.getMessage()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testInitAndShutdownOnMainThread() throws Exception { + final ConditionVariable block = new ConditionVariable(false); + + // Post a task to main thread to init and shutdown on the main thread. + Runnable blockingTask = new Runnable() { + @Override + public void run() { + // Create new request context, loading the library. + final CronetUrlRequestContext cronetEngine = + (CronetUrlRequestContext) new CronetEngine.Builder(getContext()).build(); + // Shutdown right after init. + cronetEngine.shutdown(); + // Verify that context is shutdown. + try { + cronetEngine.getUrlRequestContextAdapter(); + fail("Should throw an exception."); + } catch (Exception e) { + assertEquals("Engine is shut down.", e.getMessage()); + } + block.open(); + } + }; + new Handler(Looper.getMainLooper()).post(blockingTask); + // Wait for shutdown to complete on main thread. + block.block(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // JavaCronetEngine doesn't support throwing on repeat shutdown() + public void testMultipleShutdown() throws Exception { + final CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + try { + testFramework.mCronetEngine.shutdown(); + testFramework.mCronetEngine.shutdown(); + fail("Should throw an exception"); + } catch (Exception e) { + assertEquals("Engine is shut down.", e.getMessage()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + // TODO: Remove the annotation after fixing http://crbug.com/637972 + @OnlyRunNativeCronet + public void testShutdownAfterError() throws Exception { + final CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + ShutdownTestUrlRequestCallback callback = + new ShutdownTestUrlRequestCallback(testFramework.mCronetEngine); + UrlRequest.Builder urlRequestBuilder = testFramework.mCronetEngine.newUrlRequestBuilder( + MOCK_CRONET_TEST_FAILED_URL, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + assertTrue(callback.mOnErrorCalled); + callback.blockForCallbackToComplete(); + callback.shutdownExecutor(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // JavaCronetEngine doesn't support throwing on shutdown() + public void testShutdownAfterCancel() throws Exception { + final CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + // Block callback when response starts to verify that shutdown fails + // if there are active requests. + callback.setAutoAdvance(false); + UrlRequest.Builder urlRequestBuilder = testFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + UrlRequest urlRequest = urlRequestBuilder.build(); + urlRequest.start(); + try { + testFramework.mCronetEngine.shutdown(); + fail("Should throw an exception"); + } catch (Exception e) { + assertEquals("Cannot shutdown with active requests.", e.getMessage()); + } + callback.waitForNextStep(); + assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep); + urlRequest.cancel(); + testFramework.mCronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // No netlogs for pure java impl + public void testNetLog() throws Exception { + Context context = getContext(); + File directory = new File(PathUtils.getDataDirectory()); + File file = File.createTempFile("cronet", "json", directory); + CronetEngine cronetEngine = new CronetEngine.Builder(context).build(); + // Start NetLog immediately after the request context is created to make + // sure that the call won't crash the app even when the native request + // context is not fully initialized. See crbug.com/470196. + cronetEngine.startNetLogToFile(file.getPath(), false); + + // Start a request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + cronetEngine.stopNetLog(); + assertTrue(file.exists()); + assertTrue(file.length() != 0); + assertFalse(hasBytesInNetLog(file)); + assertTrue(file.delete()); + assertTrue(!file.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // No netlogs for pure java impl + public void testBoundedFileNetLog() throws Exception { + Context context = getContext(); + File directory = new File(PathUtils.getDataDirectory()); + File netLogDir = new File(directory, "NetLog"); + assertFalse(netLogDir.exists()); + assertTrue(netLogDir.mkdir()); + File logFile = new File(netLogDir, "netlog.json"); + ExperimentalCronetEngine cronetEngine = + new ExperimentalCronetEngine.Builder(context).build(); + // Start NetLog immediately after the request context is created to make + // sure that the call won't crash the app even when the native request + // context is not fully initialized. See crbug.com/470196. + cronetEngine.startNetLogToDisk(netLogDir.getPath(), false, MAX_FILE_SIZE); + + // Start a request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + cronetEngine.stopNetLog(); + assertTrue(logFile.exists()); + assertTrue(logFile.length() != 0); + assertFalse(hasBytesInNetLog(logFile)); + FileUtils.recursivelyDeleteFile(netLogDir, FileUtils.DELETE_ALL); + assertFalse(netLogDir.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // No netlogs for pure java impl + // Tests that if stopNetLog is not explicity called, CronetEngine.shutdown() + // will take care of it. crbug.com/623701. + public void testNoStopNetLog() throws Exception { + Context context = getContext(); + File directory = new File(PathUtils.getDataDirectory()); + File file = File.createTempFile("cronet", "json", directory); + CronetEngine cronetEngine = new CronetEngine.Builder(context).build(); + cronetEngine.startNetLogToFile(file.getPath(), false); + + // Start a request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + // Shut down the engine without calling stopNetLog. + cronetEngine.shutdown(); + assertTrue(file.exists()); + assertTrue(file.length() != 0); + assertFalse(hasBytesInNetLog(file)); + assertTrue(file.delete()); + assertTrue(!file.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // No netlogs for pure java impl + // Tests that if stopNetLog is not explicity called, CronetEngine.shutdown() + // will take care of it. crbug.com/623701. + public void testNoStopBoundedFileNetLog() throws Exception { + Context context = getContext(); + File directory = new File(PathUtils.getDataDirectory()); + File netLogDir = new File(directory, "NetLog"); + assertFalse(netLogDir.exists()); + assertTrue(netLogDir.mkdir()); + File logFile = new File(netLogDir, "netlog.json"); + ExperimentalCronetEngine cronetEngine = + new ExperimentalCronetEngine.Builder(context).build(); + cronetEngine.startNetLogToDisk(netLogDir.getPath(), false, MAX_FILE_SIZE); + + // Start a request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + // Shut down the engine without calling stopNetLog. + cronetEngine.shutdown(); + assertTrue(logFile.exists()); + assertTrue(logFile.length() != 0); + + FileUtils.recursivelyDeleteFile(netLogDir, FileUtils.DELETE_ALL); + assertFalse(netLogDir.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that NetLog contains events emitted by all live CronetEngines. + public void testNetLogContainEventsFromAllLiveEngines() throws Exception { + Context context = getContext(); + File directory = new File(PathUtils.getDataDirectory()); + File file1 = File.createTempFile("cronet1", "json", directory); + File file2 = File.createTempFile("cronet2", "json", directory); + CronetEngine cronetEngine1 = new CronetEngine.Builder(context).build(); + CronetEngine cronetEngine2 = new CronetEngine.Builder(context).build(); + + cronetEngine1.startNetLogToFile(file1.getPath(), false); + cronetEngine2.startNetLogToFile(file2.getPath(), false); + + // Warm CronetEngine and make sure both CronetUrlRequestContexts are + // initialized before testing the logs. + makeRequestAndCheckStatus(cronetEngine1, mUrl, 200); + makeRequestAndCheckStatus(cronetEngine2, mUrl, 200); + + // Use cronetEngine1 to make a request to mUrl404. + makeRequestAndCheckStatus(cronetEngine1, mUrl404, 404); + + // Use cronetEngine2 to make a request to mUrl500. + makeRequestAndCheckStatus(cronetEngine2, mUrl500, 500); + + cronetEngine1.stopNetLog(); + cronetEngine2.stopNetLog(); + assertTrue(file1.exists()); + assertTrue(file2.exists()); + // Make sure both files contain the two requests made separately using + // different engines. + assertTrue(containsStringInNetLog(file1, mUrl404)); + assertTrue(containsStringInNetLog(file1, mUrl500)); + assertTrue(containsStringInNetLog(file2, mUrl404)); + assertTrue(containsStringInNetLog(file2, mUrl500)); + assertTrue(file1.delete()); + assertTrue(file2.delete()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that NetLog contains events emitted by all live CronetEngines. + public void testBoundedFileNetLogContainEventsFromAllLiveEngines() throws Exception { + Context context = getContext(); + File directory = new File(PathUtils.getDataDirectory()); + File netLogDir1 = new File(directory, "NetLog1"); + assertFalse(netLogDir1.exists()); + assertTrue(netLogDir1.mkdir()); + File netLogDir2 = new File(directory, "NetLog2"); + assertFalse(netLogDir2.exists()); + assertTrue(netLogDir2.mkdir()); + File logFile1 = new File(netLogDir1, "netlog.json"); + File logFile2 = new File(netLogDir2, "netlog.json"); + + ExperimentalCronetEngine cronetEngine1 = + new ExperimentalCronetEngine.Builder(context).build(); + ExperimentalCronetEngine cronetEngine2 = + new ExperimentalCronetEngine.Builder(context).build(); + + cronetEngine1.startNetLogToDisk(netLogDir1.getPath(), false, MAX_FILE_SIZE); + cronetEngine2.startNetLogToDisk(netLogDir2.getPath(), false, MAX_FILE_SIZE); + + // Warm CronetEngine and make sure both CronetUrlRequestContexts are + // initialized before testing the logs. + makeRequestAndCheckStatus(cronetEngine1, mUrl, 200); + makeRequestAndCheckStatus(cronetEngine2, mUrl, 200); + + // Use cronetEngine1 to make a request to mUrl404. + makeRequestAndCheckStatus(cronetEngine1, mUrl404, 404); + + // Use cronetEngine2 to make a request to mUrl500. + makeRequestAndCheckStatus(cronetEngine2, mUrl500, 500); + + cronetEngine1.stopNetLog(); + cronetEngine2.stopNetLog(); + + assertTrue(logFile1.exists()); + assertTrue(logFile2.exists()); + assertTrue(logFile1.length() != 0); + assertTrue(logFile2.length() != 0); + + // Make sure both files contain the two requests made separately using + // different engines. + assertTrue(containsStringInNetLog(logFile1, mUrl404)); + assertTrue(containsStringInNetLog(logFile1, mUrl500)); + assertTrue(containsStringInNetLog(logFile2, mUrl404)); + assertTrue(containsStringInNetLog(logFile2, mUrl500)); + + FileUtils.recursivelyDeleteFile(netLogDir1, FileUtils.DELETE_ALL); + assertFalse(netLogDir1.exists()); + FileUtils.recursivelyDeleteFile(netLogDir2, FileUtils.DELETE_ALL); + assertFalse(netLogDir2.exists()); + } + + private CronetEngine createCronetEngineWithCache(int cacheType) { + CronetEngine.Builder builder = new CronetEngine.Builder(getContext()); + if (cacheType == CronetEngine.Builder.HTTP_CACHE_DISK + || cacheType == CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP) { + builder.setStoragePath(getTestStorage(getContext())); + } + builder.enableHttpCache(cacheType, 100 * 1024); + // Don't check the return value here, because startNativeTestServer() returns false when the + // NativeTestServer is already running and this method needs to be called twice without + // shutting down the NativeTestServer in between. + NativeTestServer.startNativeTestServer(getContext()); + return builder.build(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that if CronetEngine is shut down on the network thread, an appropriate exception + // is thrown. + public void testShutDownEngineOnNetworkThread() throws Exception { + final CronetEngine cronetEngine = + createCronetEngineWithCache(CronetEngine.Builder.HTTP_CACHE_DISK); + String url = NativeTestServer.getFileURL("/cacheable.txt"); + // Make a request to a cacheable resource. + checkRequestCaching(cronetEngine, url, false); + + final AtomicReference thrown = new AtomicReference<>(); + // Shut down the server. + NativeTestServer.shutdownNativeTestServer(); + class CancelUrlRequestCallback extends TestUrlRequestCallback { + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + super.onResponseStarted(request, info); + request.cancel(); + // Shut down CronetEngine immediately after request is destroyed. + try { + cronetEngine.shutdown(); + } catch (Exception e) { + thrown.set(e); + } + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + // onSucceeded will not happen, because the request is canceled + // after sending first read and the executor is single threaded. + throw new RuntimeException("Unexpected"); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) { + throw new RuntimeException("Unexpected"); + } + } + Executor directExecutor = new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + CancelUrlRequestCallback callback = new CancelUrlRequestCallback(); + callback.setAllowDirectExecutor(true); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(url, callback, directExecutor); + urlRequestBuilder.allowDirectExecutor(); + urlRequestBuilder.build().start(); + callback.blockForDone(); + assertTrue(thrown.get() instanceof RuntimeException); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that if CronetEngine is shut down when reading from disk cache, + // there isn't a crash. See crbug.com/486120. + public void testShutDownEngineWhenReadingFromDiskCache() throws Exception { + final CronetEngine cronetEngine = + createCronetEngineWithCache(CronetEngine.Builder.HTTP_CACHE_DISK); + String url = NativeTestServer.getFileURL("/cacheable.txt"); + // Make a request to a cacheable resource. + checkRequestCaching(cronetEngine, url, false); + + // Shut down the server. + NativeTestServer.shutdownNativeTestServer(); + class CancelUrlRequestCallback extends TestUrlRequestCallback { + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + super.onResponseStarted(request, info); + request.cancel(); + // Shut down CronetEngine immediately after request is destroyed. + cronetEngine.shutdown(); + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + // onSucceeded will not happen, because the request is canceled + // after sending first read and the executor is single threaded. + throw new RuntimeException("Unexpected"); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) { + throw new RuntimeException("Unexpected"); + } + } + CancelUrlRequestCallback callback = new CancelUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertTrue(callback.mResponseInfo.wasCached()); + assertTrue(callback.mOnCanceledCalled); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testNetLogAfterShutdown() throws Exception { + final CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = testFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + testFramework.mCronetEngine.shutdown(); + + File directory = new File(PathUtils.getDataDirectory()); + File file = File.createTempFile("cronet", "json", directory); + try { + testFramework.mCronetEngine.startNetLogToFile(file.getPath(), false); + fail("Should throw an exception."); + } catch (Exception e) { + assertEquals("Engine is shut down.", e.getMessage()); + } + assertFalse(hasBytesInNetLog(file)); + assertTrue(file.delete()); + assertTrue(!file.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBoundedFileNetLogAfterShutdown() throws Exception { + final CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = testFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + testFramework.mCronetEngine.shutdown(); + + File directory = new File(PathUtils.getDataDirectory()); + File netLogDir = new File(directory, "NetLog"); + assertFalse(netLogDir.exists()); + assertTrue(netLogDir.mkdir()); + File logFile = new File(netLogDir, "netlog.json"); + try { + testFramework.mCronetEngine.startNetLogToDisk( + netLogDir.getPath(), false, MAX_FILE_SIZE); + fail("Should throw an exception."); + } catch (Exception e) { + assertEquals("Engine is shut down.", e.getMessage()); + } + assertFalse(logFile.exists()); + FileUtils.recursivelyDeleteFile(netLogDir, FileUtils.DELETE_ALL); + assertFalse(netLogDir.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testNetLogStartMultipleTimes() throws Exception { + final CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + File directory = new File(PathUtils.getDataDirectory()); + File file = File.createTempFile("cronet", "json", directory); + // Start NetLog multiple times. + testFramework.mCronetEngine.startNetLogToFile(file.getPath(), false); + testFramework.mCronetEngine.startNetLogToFile(file.getPath(), false); + testFramework.mCronetEngine.startNetLogToFile(file.getPath(), false); + testFramework.mCronetEngine.startNetLogToFile(file.getPath(), false); + // Start a request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = testFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + testFramework.mCronetEngine.stopNetLog(); + assertTrue(file.exists()); + assertTrue(file.length() != 0); + assertFalse(hasBytesInNetLog(file)); + assertTrue(file.delete()); + assertTrue(!file.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBoundedFileNetLogStartMultipleTimes() throws Exception { + final CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + File directory = new File(PathUtils.getDataDirectory()); + File netLogDir = new File(directory, "NetLog"); + assertFalse(netLogDir.exists()); + assertTrue(netLogDir.mkdir()); + File logFile = new File(netLogDir, "netlog.json"); + // Start NetLog multiple times. This should be equivalent to starting NetLog + // once. Each subsequent start (without calling stopNetLog) should be a no-op. + testFramework.mCronetEngine.startNetLogToDisk(netLogDir.getPath(), false, MAX_FILE_SIZE); + testFramework.mCronetEngine.startNetLogToDisk(netLogDir.getPath(), false, MAX_FILE_SIZE); + testFramework.mCronetEngine.startNetLogToDisk(netLogDir.getPath(), false, MAX_FILE_SIZE); + testFramework.mCronetEngine.startNetLogToDisk(netLogDir.getPath(), false, MAX_FILE_SIZE); + // Start a request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = testFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + testFramework.mCronetEngine.stopNetLog(); + assertTrue(logFile.exists()); + assertTrue(logFile.length() != 0); + assertFalse(hasBytesInNetLog(logFile)); + FileUtils.recursivelyDeleteFile(netLogDir, FileUtils.DELETE_ALL); + assertFalse(netLogDir.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testNetLogStopMultipleTimes() throws Exception { + final CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + File directory = new File(PathUtils.getDataDirectory()); + File file = File.createTempFile("cronet", "json", directory); + testFramework.mCronetEngine.startNetLogToFile(file.getPath(), false); + // Start a request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = testFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + // Stop NetLog multiple times. + testFramework.mCronetEngine.stopNetLog(); + testFramework.mCronetEngine.stopNetLog(); + testFramework.mCronetEngine.stopNetLog(); + testFramework.mCronetEngine.stopNetLog(); + testFramework.mCronetEngine.stopNetLog(); + assertTrue(file.exists()); + assertTrue(file.length() != 0); + assertFalse(hasBytesInNetLog(file)); + assertTrue(file.delete()); + assertTrue(!file.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBoundedFileNetLogStopMultipleTimes() throws Exception { + final CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + File directory = new File(PathUtils.getDataDirectory()); + File netLogDir = new File(directory, "NetLog"); + assertFalse(netLogDir.exists()); + assertTrue(netLogDir.mkdir()); + File logFile = new File(netLogDir, "netlog.json"); + testFramework.mCronetEngine.startNetLogToDisk(netLogDir.getPath(), false, MAX_FILE_SIZE); + // Start a request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = testFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + // Stop NetLog multiple times. This should be equivalent to stopping NetLog once. + // Each subsequent stop (without calling startNetLogToDisk first) should be a no-op. + testFramework.mCronetEngine.stopNetLog(); + testFramework.mCronetEngine.stopNetLog(); + testFramework.mCronetEngine.stopNetLog(); + testFramework.mCronetEngine.stopNetLog(); + testFramework.mCronetEngine.stopNetLog(); + assertTrue(logFile.exists()); + assertTrue(logFile.length() != 0); + assertFalse(hasBytesInNetLog(logFile)); + FileUtils.recursivelyDeleteFile(netLogDir, FileUtils.DELETE_ALL); + assertFalse(netLogDir.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testNetLogWithBytes() throws Exception { + Context context = getContext(); + File directory = new File(PathUtils.getDataDirectory()); + File file = File.createTempFile("cronet", "json", directory); + CronetEngine cronetEngine = new CronetEngine.Builder(context).build(); + // Start NetLog with logAll as true. + cronetEngine.startNetLogToFile(file.getPath(), true); + // Start a request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + cronetEngine.stopNetLog(); + assertTrue(file.exists()); + assertTrue(file.length() != 0); + assertTrue(hasBytesInNetLog(file)); + assertTrue(file.delete()); + assertTrue(!file.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testBoundedFileNetLogWithBytes() throws Exception { + Context context = getContext(); + File directory = new File(PathUtils.getDataDirectory()); + File netLogDir = new File(directory, "NetLog"); + assertFalse(netLogDir.exists()); + assertTrue(netLogDir.mkdir()); + File logFile = new File(netLogDir, "netlog.json"); + ExperimentalCronetEngine cronetEngine = + new ExperimentalCronetEngine.Builder(context).build(); + // Start NetLog with logAll as true. + cronetEngine.startNetLogToDisk(netLogDir.getPath(), true, MAX_FILE_SIZE); + // Start a request. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + cronetEngine.stopNetLog(); + + assertTrue(logFile.exists()); + assertTrue(logFile.length() != 0); + assertTrue(hasBytesInNetLog(logFile)); + FileUtils.recursivelyDeleteFile(netLogDir, FileUtils.DELETE_ALL); + assertFalse(netLogDir.exists()); + } + + private boolean hasBytesInNetLog(File logFile) throws Exception { + return containsStringInNetLog(logFile, "\"bytes\""); + } + + private boolean containsStringInNetLog(File logFile, String content) throws Exception { + BufferedReader logReader = new BufferedReader(new FileReader(logFile)); + try { + String logLine; + while ((logLine = logReader.readLine()) != null) { + if (logLine.contains(content)) { + return true; + } + } + return false; + } finally { + logReader.close(); + } + } + + /** + * Helper method to make a request to {@code url}, wait for it to + * complete, and check that the status code is the same as {@code expectedStatusCode}. + */ + private void makeRequestAndCheckStatus( + CronetEngine engine, String url, int expectedStatusCode) { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest request = + engine.newUrlRequestBuilder(url, callback, callback.getExecutor()).build(); + request.start(); + callback.blockForDone(); + assertEquals(expectedStatusCode, callback.mResponseInfo.getHttpStatusCode()); + } + + private void checkRequestCaching(CronetEngine engine, String url, boolean expectCached) { + checkRequestCaching(engine, url, expectCached, false); + } + + private void checkRequestCaching( + CronetEngine engine, String url, boolean expectCached, boolean disableCache) { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + engine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + if (disableCache) { + urlRequestBuilder.disableCache(); + } + urlRequestBuilder.build().start(); + callback.blockForDone(); + assertEquals(expectCached, callback.mResponseInfo.wasCached()); + assertEquals("this is a cacheable file\n", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testEnableHttpCacheDisabled() throws Exception { + CronetEngine cronetEngine = + createCronetEngineWithCache(CronetEngine.Builder.HTTP_CACHE_DISABLED); + String url = NativeTestServer.getFileURL("/cacheable.txt"); + checkRequestCaching(cronetEngine, url, false); + checkRequestCaching(cronetEngine, url, false); + checkRequestCaching(cronetEngine, url, false); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testEnableHttpCacheInMemory() throws Exception { + CronetEngine cronetEngine = + createCronetEngineWithCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY); + String url = NativeTestServer.getFileURL("/cacheable.txt"); + checkRequestCaching(cronetEngine, url, false); + checkRequestCaching(cronetEngine, url, true); + NativeTestServer.shutdownNativeTestServer(); + checkRequestCaching(cronetEngine, url, true); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testEnableHttpCacheDisk() throws Exception { + CronetEngine cronetEngine = + createCronetEngineWithCache(CronetEngine.Builder.HTTP_CACHE_DISK); + String url = NativeTestServer.getFileURL("/cacheable.txt"); + checkRequestCaching(cronetEngine, url, false); + checkRequestCaching(cronetEngine, url, true); + NativeTestServer.shutdownNativeTestServer(); + checkRequestCaching(cronetEngine, url, true); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testNoConcurrentDiskUsage() throws Exception { + CronetEngine cronetEngine = + createCronetEngineWithCache(CronetEngine.Builder.HTTP_CACHE_DISK); + try { + createCronetEngineWithCache(CronetEngine.Builder.HTTP_CACHE_DISK); + fail(); + } catch (IllegalStateException e) { + assertEquals("Disk cache storage path already in use", e.getMessage()); + } + String url = NativeTestServer.getFileURL("/cacheable.txt"); + checkRequestCaching(cronetEngine, url, false); + checkRequestCaching(cronetEngine, url, true); + NativeTestServer.shutdownNativeTestServer(); + checkRequestCaching(cronetEngine, url, true); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testEnableHttpCacheDiskNoHttp() throws Exception { + CronetEngine cronetEngine = + createCronetEngineWithCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP); + String url = NativeTestServer.getFileURL("/cacheable.txt"); + checkRequestCaching(cronetEngine, url, false); + checkRequestCaching(cronetEngine, url, false); + checkRequestCaching(cronetEngine, url, false); + + // Make a new CronetEngine and try again to make sure the response didn't get cached on the + // first request. See https://crbug.com/743232. + cronetEngine.shutdown(); + cronetEngine = createCronetEngineWithCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP); + checkRequestCaching(cronetEngine, url, false); + checkRequestCaching(cronetEngine, url, false); + checkRequestCaching(cronetEngine, url, false); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testDisableCache() throws Exception { + CronetEngine cronetEngine = + createCronetEngineWithCache(CronetEngine.Builder.HTTP_CACHE_DISK); + String url = NativeTestServer.getFileURL("/cacheable.txt"); + + // When cache is disabled, making a request does not write to the cache. + checkRequestCaching(cronetEngine, url, false, true /** disable cache */); + checkRequestCaching(cronetEngine, url, false); + + // When cache is enabled, the second request is cached. + checkRequestCaching(cronetEngine, url, false, true /** disable cache */); + checkRequestCaching(cronetEngine, url, true); + + // Shut down the server, next request should have a cached response. + NativeTestServer.shutdownNativeTestServer(); + checkRequestCaching(cronetEngine, url, true); + + // Cache is disabled after server is shut down, request should fail. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + urlRequestBuilder.disableCache(); + urlRequestBuilder.build().start(); + callback.blockForDone(); + assertNotNull(callback.mError); + assertContains("Exception in CronetUrlRequest: net::ERR_CONNECTION_REFUSED", + callback.mError.getMessage()); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testEnableHttpCacheDiskNewEngine() throws Exception { + CronetEngine cronetEngine = + createCronetEngineWithCache(CronetEngine.Builder.HTTP_CACHE_DISK); + String url = NativeTestServer.getFileURL("/cacheable.txt"); + checkRequestCaching(cronetEngine, url, false); + checkRequestCaching(cronetEngine, url, true); + NativeTestServer.shutdownNativeTestServer(); + checkRequestCaching(cronetEngine, url, true); + + // Shutdown original context and create another that uses the same cache. + cronetEngine.shutdown(); + cronetEngine = + mTestRule.enableDiskCache(new CronetEngine.Builder(getContext())).build(); + checkRequestCaching(cronetEngine, url, true); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testInitEngineAndStartRequest() { + // Immediately make a request after initializing the engine. + CronetEngine cronetEngine = new CronetEngine.Builder(getContext()).build(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testInitEngineStartTwoRequests() throws Exception { + // Make two requests after initializing the context. + CronetEngine cronetEngine = new CronetEngine.Builder(getContext()).build(); + int[] statusCodes = {0, 0}; + String[] urls = {mUrl, mUrl404}; + for (int i = 0; i < 2; i++) { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = + cronetEngine.newUrlRequestBuilder(urls[i], callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + statusCodes[i] = callback.mResponseInfo.getHttpStatusCode(); + } + assertEquals(200, statusCodes[0]); + assertEquals(404, statusCodes[1]); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testInitTwoEnginesSimultaneously() throws Exception { + // Threads will block on runBlocker to ensure simultaneous execution. + ConditionVariable runBlocker = new ConditionVariable(false); + RequestThread thread1 = new RequestThread(mUrl, runBlocker); + RequestThread thread2 = new RequestThread(mUrl404, runBlocker); + + thread1.start(); + thread2.start(); + runBlocker.open(); + thread1.join(); + thread2.join(); + assertEquals(200, thread1.mCallback.mResponseInfo.getHttpStatusCode()); + assertEquals(404, thread2.mCallback.mResponseInfo.getHttpStatusCode()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testInitTwoEnginesInSequence() throws Exception { + ConditionVariable runBlocker = new ConditionVariable(true); + RequestThread thread1 = new RequestThread(mUrl, runBlocker); + RequestThread thread2 = new RequestThread(mUrl404, runBlocker); + + thread1.start(); + thread1.join(); + thread2.start(); + thread2.join(); + assertEquals(200, thread1.mCallback.mResponseInfo.getHttpStatusCode()); + assertEquals(404, thread2.mCallback.mResponseInfo.getHttpStatusCode()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testInitDifferentEngines() throws Exception { + // Test that concurrently instantiating Cronet context's upon various + // different versions of the same Android Context does not cause crashes + // like crbug.com/453845 + CronetEngine firstEngine = new CronetEngine.Builder(getContext()).build(); + CronetEngine secondEngine = + new CronetEngine.Builder(getContext().getApplicationContext()).build(); + CronetEngine thirdEngine = + new CronetEngine.Builder(new ContextWrapper(getContext())).build(); + firstEngine.shutdown(); + secondEngine.shutdown(); + thirdEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // Java engine doesn't produce metrics + public void testGetGlobalMetricsDeltas() throws Exception { + final CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + + byte delta1[] = testFramework.mCronetEngine.getGlobalMetricsDeltas(); + + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = testFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + builder.build().start(); + callback.blockForDone(); + // Fetch deltas on a different thread the second time to make sure this is permitted. + // See crbug.com/719448 + FutureTask task = new FutureTask(new Callable() { + @Override + public byte[] call() { + return testFramework.mCronetEngine.getGlobalMetricsDeltas(); + } + }); + new Thread(task).start(); + byte delta2[] = task.get(); + assertTrue(delta2.length != 0); + assertFalse(Arrays.equals(delta1, delta2)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testCronetEngineBuilderConfig() throws Exception { + // This is to prompt load of native library. + mTestRule.startCronetTestFramework(); + // Verify CronetEngine.Builder config is passed down accurately to native code. + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + builder.enableHttp2(false); + builder.enableQuic(true); + builder.addQuicHint("example.com", 12, 34); + builder.enableHttpCache(HTTP_CACHE_IN_MEMORY, 54321); + builder.setUserAgent("efgh"); + builder.setExperimentalOptions(""); + builder.setStoragePath(getTestStorage(getContext())); + builder.enablePublicKeyPinningBypassForLocalTrustAnchors(false); + nativeVerifyUrlRequestContextConfig( + CronetUrlRequestContext.createNativeUrlRequestContextConfig( + (CronetEngineBuilderImpl) builder.mBuilderDelegate), + getTestStorage(getContext())); + } + + // Verifies that CronetEngine.Builder config from testCronetEngineBuilderConfig() is properly + // translated to a native UrlRequestContextConfig. + private static native void nativeVerifyUrlRequestContextConfig(long config, String storagePath); + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testCronetEngineQuicOffConfig() throws Exception { + // This is to prompt load of native library. + mTestRule.startCronetTestFramework(); + // Verify CronetEngine.Builder config is passed down accurately to native code. + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + builder.enableHttp2(false); + // QUIC is on by default. Disabling it here to make sure the built config can correctly + // reflect the change. + builder.enableQuic(false); + builder.enableHttpCache(HTTP_CACHE_IN_MEMORY, 54321); + builder.setExperimentalOptions(""); + builder.setUserAgent("efgh"); + builder.setStoragePath(getTestStorage(getContext())); + builder.enablePublicKeyPinningBypassForLocalTrustAnchors(false); + nativeVerifyUrlRequestContextQuicOffConfig( + CronetUrlRequestContext.createNativeUrlRequestContextConfig( + (CronetEngineBuilderImpl) builder.mBuilderDelegate), + getTestStorage(getContext())); + } + + // Verifies that CronetEngine.Builder config from testCronetEngineQuicOffConfig() is properly + // translated to a native UrlRequestContextConfig and QUIC is turned off. + private static native void nativeVerifyUrlRequestContextQuicOffConfig( + long config, String storagePath); + + private static class TestBadLibraryLoader extends CronetEngine.Builder.LibraryLoader { + private boolean mWasCalled; + + @Override + public void loadLibrary(String libName) { + // Report that this method was called, but don't load the library + mWasCalled = true; + } + + boolean wasCalled() { + return mWasCalled; + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider() throws Exception { + CronetEngine.Builder builder = new CronetEngine.Builder(getContext()); + TestBadLibraryLoader loader = new TestBadLibraryLoader(); + builder.setLibraryLoader(loader); + try { + builder.build(); + fail("Native library should not be loaded"); + } catch (UnsatisfiedLinkError e) { + assertTrue(loader.wasCalled()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl() throws Exception { + CronetEngine.Builder builder = + new CronetEngine.Builder(new NativeCronetEngineBuilderImpl(getContext())); + TestBadLibraryLoader loader = new TestBadLibraryLoader(); + builder.setLibraryLoader(loader); + CronetEngine engine = builder.build(); + assertNotNull(engine); + assertFalse(loader.wasCalled()); + } + + // Creates a CronetEngine on another thread and then one on the main thread. This shouldn't + // crash. + @Test + @SmallTest + @Feature({"Cronet"}) + public void testThreadedStartup() throws Exception { + final ConditionVariable otherThreadDone = new ConditionVariable(); + final ConditionVariable uiThreadDone = new ConditionVariable(); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + final ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + new Thread() { + @Override + public void run() { + CronetEngine cronetEngine = builder.build(); + otherThreadDone.open(); + cronetEngine.shutdown(); + } + } + .start(); + otherThreadDone.block(); + builder.build().shutdown(); + uiThreadDone.open(); + } + }); + assertTrue(uiThreadDone.block(1000)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testHostResolverRules() throws Exception { + String resolverTestHostname = "some-weird-hostname"; + URL testUrl = new URL(mUrl); + ExperimentalCronetEngine.Builder cronetEngineBuilder = + new ExperimentalCronetEngine.Builder(getContext()); + JSONObject hostResolverRules = new JSONObject().put( + "host_resolver_rules", "MAP " + resolverTestHostname + " " + testUrl.getHost()); + JSONObject experimentalOptions = + new JSONObject().put("HostResolverRules", hostResolverRules); + cronetEngineBuilder.setExperimentalOptions(experimentalOptions.toString()); + + final CronetEngine cronetEngine = cronetEngineBuilder.build(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + URL requestUrl = + new URL("http", resolverTestHostname, testUrl.getPort(), testUrl.getFile()); + UrlRequest.Builder urlRequestBuilder = cronetEngine.newUrlRequestBuilder( + requestUrl.toString(), callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + } + + /** + * Runs {@code r} on {@code engine}'s network thread. + */ + private static void postToNetworkThread(final CronetEngine engine, final Runnable r) { + // Works by requesting an invalid URL which results in onFailed() being called, which is + // done through a direct executor which causes onFailed to be run on the network thread. + Executor directExecutor = new Executor() { + @Override + public void execute(Runnable runable) { + runable.run(); + } + }; + UrlRequest.Callback callback = new UrlRequest.Callback() { + @Override + public void onRedirectReceived( + UrlRequest request, UrlResponseInfo responseInfo, String newLocationUrl) {} + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo responseInfo) {} + @Override + public void onReadCompleted( + UrlRequest request, UrlResponseInfo responseInfo, ByteBuffer byteBuffer) {} + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo responseInfo) {} + + @Override + public void onFailed( + UrlRequest request, UrlResponseInfo responseInfo, CronetException error) { + r.run(); + } + }; + engine.newUrlRequestBuilder("", callback, directExecutor).build().start(); + } + + /** + * @returns the thread priority of {@code engine}'s network thread. + */ + private int getThreadPriority(CronetEngine engine) throws Exception { + FutureTask task = new FutureTask(new Callable() { + @Override + public Integer call() { + return Process.getThreadPriority(Process.myTid()); + } + }); + postToNetworkThread(engine, task); + return task.get(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @RequiresMinApi(6) // setThreadPriority added in API 6: crrev.com/472449 + public void testCronetEngineThreadPriority() throws Exception { + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + // Try out of bounds thread priorities. + try { + builder.setThreadPriority(-21); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Thread priority invalid", e.getMessage()); + } + try { + builder.setThreadPriority(20); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Thread priority invalid", e.getMessage()); + } + // Test that valid thread priority range (-20..19) is working. + for (int threadPriority = -20; threadPriority < 20; threadPriority++) { + builder.setThreadPriority(threadPriority); + CronetEngine engine = builder.build(); + assertEquals(threadPriority, getThreadPriority(engine)); + engine.shutdown(); + } + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java new file mode 100644 index 0000000000..3ecf65c5c8 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/CronetUrlRequestTest.java @@ -0,0 +1,2515 @@ +// Copyright 2014 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.net.CronetTestRule.assertContains; +import static org.chromium.net.CronetTestRule.getContext; + +import android.os.Build; +import android.os.ConditionVariable; +import android.os.Process; +import android.os.StrictMode; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.Log; +import org.chromium.base.test.util.DisabledTest; +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.CronetTestFramework; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.CronetTestRule.RequiresMinApi; +import org.chromium.net.TestUrlRequestCallback.FailureType; +import org.chromium.net.TestUrlRequestCallback.ResponseStep; +import org.chromium.net.impl.CronetUrlRequest; +import org.chromium.net.impl.UrlResponseInfoImpl; +import org.chromium.net.test.EmbeddedTestServer; +import org.chromium.net.test.FailurePhase; +import org.chromium.net.test.ServerCertificate; + +import java.io.IOException; +import java.net.ConnectException; +import java.nio.ByteBuffer; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Test functionality of CronetUrlRequest. + */ +@RunWith(AndroidJUnit4.class) +public class CronetUrlRequestTest { + private static final String TAG = CronetUrlRequestTest.class.getSimpleName(); + + // URL used for base tests. + private static final String TEST_URL = "http://127.0.0.1:8000"; + + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private CronetTestFramework mTestFramework; + private MockUrlRequestJobFactory mMockUrlRequestJobFactory; + + @Before + public void setUp() throws Exception { + mTestFramework = mTestRule.startCronetTestFramework(); + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + // Add url interceptors after native application context is initialized. + if (!mTestRule.testingJavaImpl()) { + mMockUrlRequestJobFactory = new MockUrlRequestJobFactory(mTestFramework.mCronetEngine); + } + } + + @After + public void tearDown() throws Exception { + if (!mTestRule.testingJavaImpl()) { + mMockUrlRequestJobFactory.shutdown(); + } + NativeTestServer.shutdownNativeTestServer(); + mTestFramework.mCronetEngine.shutdown(); + } + + private TestUrlRequestCallback startAndWaitForComplete(String url) throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + // Create request. + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + // Wait for all posted tasks to be executed to ensure there is no unhandled exception. + callback.shutdownExecutorAndWait(); + assertTrue(urlRequest.isDone()); + return callback; + } + + private 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 void checkResponseInfoHeader( + UrlResponseInfo responseInfo, String headerName, String headerValue) { + Map> responseHeaders = + responseInfo.getAllHeaders(); + List header = responseHeaders.get(headerName); + assertNotNull(header); + assertTrue(header.contains(headerValue)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testBuilderChecks() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + try { + mTestFramework.mCronetEngine.newUrlRequestBuilder( + null, callback, callback.getExecutor()); + fail("URL not null-checked"); + } catch (NullPointerException e) { + assertEquals("URL is required.", e.getMessage()); + } + try { + mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectURL(), null, callback.getExecutor()); + fail("Callback not null-checked"); + } catch (NullPointerException e) { + assertEquals("Callback is required.", e.getMessage()); + } + try { + mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectURL(), callback, null); + fail("Executor not null-checked"); + } catch (NullPointerException e) { + assertEquals("Executor is required.", e.getMessage()); + } + // Verify successful creation doesn't throw. + mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectURL(), callback, callback.getExecutor()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testSimpleGet() throws Exception { + String url = NativeTestServer.getEchoMethodURL(); + TestUrlRequestCallback callback = startAndWaitForComplete(url); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + // Default method is 'GET'. + assertEquals("GET", callback.mResponseAsString); + assertEquals(0, callback.mRedirectCount); + assertEquals(callback.mResponseStep, ResponseStep.ON_SUCCEEDED); + UrlResponseInfo urlResponseInfo = createUrlResponseInfo(new String[] {url}, "OK", 200, 86, + "Connection", "close", "Content-Length", "3", "Content-Type", "text/plain"); + mTestRule.assertResponseEquals(urlResponseInfo, callback.mResponseInfo); + checkResponseInfo(callback.mResponseInfo, NativeTestServer.getEchoMethodURL(), 200, "OK"); + } + + UrlResponseInfo createUrlResponseInfo( + String[] urls, String message, int statusCode, int receivedBytes, String... headers) { + ArrayList> headersList = new ArrayList<>(); + for (int i = 0; i < headers.length; i += 2) { + headersList.add(new AbstractMap.SimpleImmutableEntry( + headers[i], headers[i + 1])); + } + UrlResponseInfoImpl unknown = new UrlResponseInfoImpl(Arrays.asList(urls), statusCode, + message, headersList, false, "unknown", ":0", receivedBytes); + return unknown; + } + + void runConnectionMigrationTest(boolean disableConnectionMigration) { + // URLRequest load flags at net/base/load_flags_list.h. + int connectionMigrationLoadFlag = nativeGetConnectionMigrationDisableLoadFlag(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + // Create builder, start a request, and check if default load_flags are set correctly. + ExperimentalUrlRequest.Builder builder = + (ExperimentalUrlRequest.Builder) mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getFileURL("/success.txt"), callback, + callback.getExecutor()); + // Disable connection migration. + if (disableConnectionMigration) builder.disableConnectionMigration(); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.waitForNextStep(); + int loadFlags = CronetTestUtil.getLoadFlags(urlRequest); + if (disableConnectionMigration) { + assertEquals(connectionMigrationLoadFlag, loadFlags & connectionMigrationLoadFlag); + } else { + assertEquals(0, loadFlags & connectionMigrationLoadFlag); + } + callback.setAutoAdvance(true); + callback.startNextRead(urlRequest); + callback.blockForDone(); + } + + /** + * Tests that disabling connection migration sets the URLRequest load flag correctly. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testLoadFlagsWithConnectionMigration() throws Exception { + runConnectionMigrationTest(/*disableConnectionMigration=*/false); + runConnectionMigrationTest(/*disableConnectionMigration=*/true); + } + + /** + * 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 + * messages, does a GET between UrlRequest.Callback callbacks. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testRedirectAsync() throws Exception { + // Start the request and wait to see the redirect. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectURL(), callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.waitForNextStep(); + + // Check the redirect. + assertEquals(ResponseStep.ON_RECEIVED_REDIRECT, callback.mResponseStep); + assertEquals(1, callback.mRedirectResponseInfoList.size()); + checkResponseInfo(callback.mRedirectResponseInfoList.get(0), + NativeTestServer.getRedirectURL(), 302, "Found"); + assertEquals(1, callback.mRedirectResponseInfoList.get(0).getUrlChain().size()); + assertEquals(NativeTestServer.getSuccessURL(), callback.mRedirectUrlList.get(0)); + checkResponseInfoHeader( + callback.mRedirectResponseInfoList.get(0), "redirect-header", "header-value"); + + UrlResponseInfo expected = + createUrlResponseInfo(new String[] {NativeTestServer.getRedirectURL()}, "Found", + 302, 73, "Location", "/success.txt", "redirect-header", "header-value"); + mTestRule.assertResponseEquals(expected, callback.mRedirectResponseInfoList.get(0)); + + // Wait for an unrelated request to finish. The request should not + // advance until followRedirect is invoked. + testSimpleGet(); + assertEquals(ResponseStep.ON_RECEIVED_REDIRECT, callback.mResponseStep); + assertEquals(1, callback.mRedirectResponseInfoList.size()); + + // Follow the redirect and wait for the next set of headers. + urlRequest.followRedirect(); + callback.waitForNextStep(); + + assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep); + assertEquals(1, callback.mRedirectResponseInfoList.size()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + checkResponseInfo(callback.mResponseInfo, NativeTestServer.getSuccessURL(), 200, "OK"); + assertEquals(2, callback.mResponseInfo.getUrlChain().size()); + assertEquals( + NativeTestServer.getRedirectURL(), callback.mResponseInfo.getUrlChain().get(0)); + assertEquals(NativeTestServer.getSuccessURL(), callback.mResponseInfo.getUrlChain().get(1)); + + // Wait for an unrelated request to finish. The request should not + // advance until read is invoked. + testSimpleGet(); + assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep); + + // One read should get all the characters, but best not to depend on + // how much is actually read from the socket at once. + while (!callback.isDone()) { + callback.startNextRead(urlRequest); + callback.waitForNextStep(); + String response = callback.mResponseAsString; + ResponseStep step = callback.mResponseStep; + if (!callback.isDone()) { + assertEquals(ResponseStep.ON_READ_COMPLETED, step); + } + // Should not receive any messages while waiting for another get, + // as the next read has not been started. + testSimpleGet(); + assertEquals(response, callback.mResponseAsString); + assertEquals(step, callback.mResponseStep); + } + assertEquals(ResponseStep.ON_SUCCEEDED, callback.mResponseStep); + assertEquals(NativeTestServer.SUCCESS_BODY, callback.mResponseAsString); + + UrlResponseInfo urlResponseInfo = createUrlResponseInfo( + new String[] {NativeTestServer.getRedirectURL(), NativeTestServer.getSuccessURL()}, + "OK", 200, 258, "Content-Type", "text/plain", "Access-Control-Allow-Origin", "*", + "header-name", "header-value", "multi-header-name", "header-value1", + "multi-header-name", "header-value2"); + + mTestRule.assertResponseEquals(urlResponseInfo, callback.mResponseInfo); + // Make sure there are no other pending messages, which would trigger + // asserts in TestUrlRequestCallback. + testSimpleGet(); + } + + /** + * Tests redirect without location header doesn't cause a crash. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testRedirectWithNullLocationHeader() throws Exception { + String url = NativeTestServer.getFileURL("/redirect_broken_header.html"); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + final UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + assertEquals("\n\n\nRedirect\n" + + "

Redirecting...

\n\n\n", + callback.mResponseAsString); + assertEquals(ResponseStep.ON_SUCCEEDED, callback.mResponseStep); + assertEquals(302, callback.mResponseInfo.getHttpStatusCode()); + assertNull(callback.mError); + assertFalse(callback.mOnErrorCalled); + } + + /** + * Tests onRedirectReceived after cancel doesn't cause a crash. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testOnRedirectReceivedAfterCancel() throws Exception { + final AtomicBoolean failedExpectation = new AtomicBoolean(); + TestUrlRequestCallback callback = new TestUrlRequestCallback() { + @Override + public void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) { + assertEquals(0, mRedirectCount); + failedExpectation.compareAndSet(false, 0 != mRedirectCount); + super.onRedirectReceived(request, info, newLocationUrl); + // Cancel the request, so the second redirect will not be received. + request.cancel(); + } + + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + failedExpectation.set(true); + fail(); + } + + @Override + public void onReadCompleted( + UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) { + failedExpectation.set(true); + fail(); + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + failedExpectation.set(true); + fail(); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) { + failedExpectation.set(true); + fail(); + } + + @Override + public void onCanceled(UrlRequest request, UrlResponseInfo info) { + assertEquals(1, mRedirectCount); + failedExpectation.compareAndSet(false, 1 != mRedirectCount); + super.onCanceled(request, info); + } + }; + + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getMultiRedirectURL(), callback, callback.getExecutor()); + + final UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + assertFalse(failedExpectation.get()); + // Check that only one redirect is received. + assertEquals(1, callback.mRedirectCount); + // Check that onCanceled is called. + assertTrue(callback.mOnCanceledCalled); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testNotFound() throws Exception { + String url = NativeTestServer.getFileURL("/notfound.html"); + TestUrlRequestCallback callback = startAndWaitForComplete(url); + checkResponseInfo(callback.mResponseInfo, url, 404, "Not Found"); + assertEquals("\n\n\nNot found\n" + + "

Test page loaded.

\n\n\n", + callback.mResponseAsString); + assertEquals(0, callback.mRedirectCount); + assertEquals(callback.mResponseStep, ResponseStep.ON_SUCCEEDED); + } + + // Checks that UrlRequest.Callback.onFailed is only called once in the case + // of ERR_CONTENT_LENGTH_MISMATCH, which has an unusual failure path. + // See http://crbug.com/468803. + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // No canonical exception to assert on + public void testContentLengthMismatchFailsOnce() throws Exception { + String url = NativeTestServer.getFileURL( + "/content_length_mismatch.html"); + TestUrlRequestCallback callback = startAndWaitForComplete(url); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + // The entire response body will be read before the error is returned. + // This is because the network stack returns data as it's read from the + // socket, and the socket close message which triggers the error will + // only be passed along after all data has been read. + assertEquals("Response that lies about content length.", callback.mResponseAsString); + assertNotNull(callback.mError); + assertContains("Exception in CronetUrlRequest: net::ERR_CONTENT_LENGTH_MISMATCH", + callback.mError.getMessage()); + // Wait for a couple round trips to make sure there are no pending + // onFailed messages. This test relies on checks in + // TestUrlRequestCallback catching a second onFailed call. + testSimpleGet(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testSetHttpMethod() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String methodName = "HEAD"; + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoMethodURL(), callback, callback.getExecutor()); + // Try to set 'null' method. + try { + builder.setHttpMethod(null); + fail("Exception not thrown"); + } catch (NullPointerException e) { + assertEquals("Method is required.", e.getMessage()); + } + + builder.setHttpMethod(methodName); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(0, callback.mHttpResponseDataLength); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testBadMethod() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + TEST_URL, callback, callback.getExecutor()); + try { + builder.setHttpMethod("bad:method!"); + builder.build().start(); + fail("IllegalArgumentException not thrown."); + } catch (IllegalArgumentException e) { + assertEquals("Invalid http method bad:method!", + e.getMessage()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testBadHeaderName() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + TEST_URL, callback, callback.getExecutor()); + try { + builder.addHeader("header:name", "headervalue"); + builder.build().start(); + fail("IllegalArgumentException not thrown."); + } catch (IllegalArgumentException e) { + assertEquals("Invalid header header:name=headervalue", + e.getMessage()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testAcceptEncodingIgnored() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoAllHeadersURL(), callback, callback.getExecutor()); + // This line should eventually throw an exception, once callers have migrated + builder.addHeader("accept-encoding", "foozip"); + builder.build().start(); + callback.blockForDone(); + assertFalse(callback.mResponseAsString.contains("foozip")); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testBadHeaderValue() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + TEST_URL, callback, callback.getExecutor()); + 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()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testAddHeader() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String headerName = "header-name"; + String headerValue = "header-value"; + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoHeaderURL(headerName), callback, callback.getExecutor()); + + builder.addHeader(headerName, headerValue); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(headerValue, callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testMultiRequestHeaders() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String headerName = "header-name"; + String headerValue1 = "header-value1"; + String headerValue2 = "header-value2"; + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoAllHeadersURL(), callback, callback.getExecutor()); + builder.addHeader(headerName, headerValue1); + builder.addHeader(headerName, headerValue2); + 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 actualValues = new ArrayList(); + while (matcher.find()) { + actualValues.add(matcher.group(1)); + } + assertEquals(1, actualValues.size()); + assertEquals("header-value2", actualValues.get(0)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testCustomReferer_verbatim() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String refererName = "Referer"; + String refererValue = "http://example.com/"; + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoHeaderURL(refererName), callback, callback.getExecutor()); + builder.addHeader(refererName, refererValue); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(refererValue, callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testCustomReferer_changeToCanonical() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String refererName = "Referer"; + String refererValueNoTrailingSlash = "http://example.com"; + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoHeaderURL(refererName), callback, callback.getExecutor()); + builder.addHeader(refererName, refererValueNoTrailingSlash); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(refererValueNoTrailingSlash + "/", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testCustomReferer_discardInvalid() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String refererName = "Referer"; + String invalidRefererValue = "foobar"; + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoHeaderURL(refererName), callback, callback.getExecutor()); + builder.addHeader(refererName, invalidRefererValue); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("Header not found. :(", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testCustomUserAgent() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String userAgentName = "User-Agent"; + String userAgentValue = "User-Agent-Value"; + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoHeaderURL(userAgentName), callback, callback.getExecutor()); + builder.addHeader(userAgentName, userAgentValue); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(userAgentValue, callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testDefaultUserAgent() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String headerName = "User-Agent"; + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoHeaderURL(headerName), callback, callback.getExecutor()); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertTrue("Default User-Agent should contain Cronet/n.n.n.n but is " + + callback.mResponseAsString, + Pattern.matches( + ".+Cronet/\\d+\\.\\d+\\.\\d+\\.\\d+.+", callback.mResponseAsString)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testMockSuccess() throws Exception { + TestUrlRequestCallback callback = startAndWaitForComplete(NativeTestServer.getSuccessURL()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(0, callback.mRedirectResponseInfoList.size()); + assertTrue(callback.mHttpResponseDataLength != 0); + assertEquals(callback.mResponseStep, ResponseStep.ON_SUCCEEDED); + Map> responseHeaders = callback.mResponseInfo.getAllHeaders(); + assertEquals("header-value", responseHeaders.get("header-name").get(0)); + List multiHeader = responseHeaders.get("multi-header-name"); + assertEquals(2, multiHeader.size()); + assertEquals("header-value1", multiHeader.get(0)); + assertEquals("header-value2", multiHeader.get(1)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testResponseHeadersList() throws Exception { + TestUrlRequestCallback callback = startAndWaitForComplete(NativeTestServer.getSuccessURL()); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + List> responseHeaders = + callback.mResponseInfo.getAllHeadersAsList(); + + assertEquals(responseHeaders.get(0), + new AbstractMap.SimpleEntry<>("Content-Type", "text/plain")); + assertEquals(responseHeaders.get(1), + new AbstractMap.SimpleEntry<>("Access-Control-Allow-Origin", "*")); + assertEquals(responseHeaders.get(2), + new AbstractMap.SimpleEntry<>("header-name", "header-value")); + assertEquals(responseHeaders.get(3), + new AbstractMap.SimpleEntry<>("multi-header-name", "header-value1")); + assertEquals(responseHeaders.get(4), + new AbstractMap.SimpleEntry<>("multi-header-name", "header-value2")); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testMockMultiRedirect() throws Exception { + TestUrlRequestCallback callback = + startAndWaitForComplete(NativeTestServer.getMultiRedirectURL()); + UrlResponseInfo mResponseInfo = callback.mResponseInfo; + assertEquals(2, callback.mRedirectCount); + assertEquals(200, mResponseInfo.getHttpStatusCode()); + assertEquals(2, callback.mRedirectResponseInfoList.size()); + + // Check first redirect (multiredirect.html -> redirect.html) + UrlResponseInfo firstExpectedResponseInfo = createUrlResponseInfo( + new String[] {NativeTestServer.getMultiRedirectURL()}, "Found", 302, 76, "Location", + "/redirect.html", "redirect-header0", "header-value"); + UrlResponseInfo firstRedirectResponseInfo = callback.mRedirectResponseInfoList.get(0); + mTestRule.assertResponseEquals(firstExpectedResponseInfo, firstRedirectResponseInfo); + + // Check second redirect (redirect.html -> success.txt) + UrlResponseInfo secondExpectedResponseInfo = createUrlResponseInfo( + new String[] {NativeTestServer.getMultiRedirectURL(), + NativeTestServer.getRedirectURL(), NativeTestServer.getSuccessURL()}, + "OK", 200, 334, "Content-Type", "text/plain", "Access-Control-Allow-Origin", "*", + "header-name", "header-value", "multi-header-name", "header-value1", + "multi-header-name", "header-value2"); + + mTestRule.assertResponseEquals(secondExpectedResponseInfo, mResponseInfo); + assertTrue(callback.mHttpResponseDataLength != 0); + assertEquals(2, callback.mRedirectCount); + assertEquals(callback.mResponseStep, ResponseStep.ON_SUCCEEDED); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testMockNotFound() throws Exception { + TestUrlRequestCallback callback = + startAndWaitForComplete(NativeTestServer.getNotFoundURL()); + UrlResponseInfo expected = createUrlResponseInfo( + new String[] {NativeTestServer.getNotFoundURL()}, "Not Found", 404, 120); + mTestRule.assertResponseEquals(expected, callback.mResponseInfo); + assertTrue(callback.mHttpResponseDataLength != 0); + assertEquals(0, callback.mRedirectCount); + assertFalse(callback.mOnErrorCalled); + assertEquals(callback.mResponseStep, ResponseStep.ON_SUCCEEDED); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // Java impl doesn't support MockUrlRequestJobFactory + public void testMockStartAsyncError() throws Exception { + final int arbitraryNetError = -3; + TestUrlRequestCallback callback = + startAndWaitForComplete(MockUrlRequestJobFactory.getMockUrlWithFailure( + FailurePhase.START, arbitraryNetError)); + assertNull(callback.mResponseInfo); + assertNotNull(callback.mError); + assertEquals(arbitraryNetError, + ((NetworkException) callback.mError).getCronetInternalErrorCode()); + assertEquals(0, callback.mRedirectCount); + assertTrue(callback.mOnErrorCalled); + assertEquals(ResponseStep.ON_FAILED, callback.mResponseStep); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // Java impl doesn't support MockUrlRequestJobFactory + public void testMockReadDataSyncError() throws Exception { + final int arbitraryNetError = -4; + TestUrlRequestCallback callback = + startAndWaitForComplete(MockUrlRequestJobFactory.getMockUrlWithFailure( + FailurePhase.READ_SYNC, arbitraryNetError)); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(15, callback.mResponseInfo.getReceivedByteCount()); + assertNotNull(callback.mError); + assertEquals(arbitraryNetError, + ((NetworkException) callback.mError).getCronetInternalErrorCode()); + assertEquals(0, callback.mRedirectCount); + assertTrue(callback.mOnErrorCalled); + assertEquals(ResponseStep.ON_FAILED, callback.mResponseStep); + } + + @DisabledTest(message = "crbug.com/738183") + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // Java impl doesn't support MockUrlRequestJobFactory + public void testMockReadDataAsyncError() throws Exception { + final int arbitraryNetError = -5; + TestUrlRequestCallback callback = + startAndWaitForComplete(MockUrlRequestJobFactory.getMockUrlWithFailure( + FailurePhase.READ_ASYNC, arbitraryNetError)); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(15, callback.mResponseInfo.getReceivedByteCount()); + assertNotNull(callback.mError); + assertEquals(arbitraryNetError, + ((NetworkException) callback.mError).getCronetInternalErrorCode()); + assertEquals(0, callback.mRedirectCount); + assertTrue(callback.mOnErrorCalled); + assertEquals(ResponseStep.ON_FAILED, callback.mResponseStep); + } + + /** + * Tests that request continues when client certificate is requested. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testMockClientCertificateRequested() throws Exception { + TestUrlRequestCallback callback = startAndWaitForComplete( + MockUrlRequestJobFactory.getMockUrlForClientCertificateRequest()); + assertNotNull(callback.mResponseInfo); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("data", callback.mResponseAsString); + assertEquals(0, callback.mRedirectCount); + assertNull(callback.mError); + assertFalse(callback.mOnErrorCalled); + } + + /** + * Tests that an SSL cert error will be reported via {@link UrlRequest.Callback#onFailed}. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // Java impl doesn't support MockUrlRequestJobFactory + public void testMockSSLCertificateError() throws Exception { + TestUrlRequestCallback callback = startAndWaitForComplete( + MockUrlRequestJobFactory.getMockUrlForSSLCertificateError()); + assertNull(callback.mResponseInfo); + assertNotNull(callback.mError); + assertTrue(callback.mOnErrorCalled); + assertEquals(-201, ((NetworkException) callback.mError).getCronetInternalErrorCode()); + assertContains("Exception in CronetUrlRequest: net::ERR_CERT_DATE_INVALID", + callback.mError.getMessage()); + assertEquals(ResponseStep.ON_FAILED, callback.mResponseStep); + } + + /** + * Tests that an SSL cert error with upload will be reported via {@link + * UrlRequest.Callback#onFailed}. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // Java impl doesn't support MockUrlRequestJobFactory + public void testSSLCertificateError() throws Exception { + EmbeddedTestServer sslServer = EmbeddedTestServer.createAndStartHTTPSServer( + getContext(), ServerCertificate.CERT_EXPIRED); + + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + sslServer.getURL("/"), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertNull(callback.mResponseInfo); + assertNotNull(callback.mError); + assertTrue(callback.mOnErrorCalled); + assertEquals(-201, ((NetworkException) callback.mError).getCronetInternalErrorCode()); + assertContains("Exception in CronetUrlRequest: net::ERR_CERT_DATE_INVALID", + callback.mError.getMessage()); + assertEquals(ResponseStep.ON_FAILED, callback.mResponseStep); + + sslServer.stopAndDestroyServer(); + } + + /** + * Checks that the buffer is updated correctly, when starting at an offset. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testSimpleGetBufferUpdates() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + // Since the default method is "GET", the expected response body is also + // "GET". + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoMethodURL(), callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.waitForNextStep(); + + 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(urlRequest, readBuffer); + callback.waitForNextStep(); + } + + // 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(urlRequest, readBuffer); + callback.waitForNextStep(); + + // 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(urlRequest, readBuffer); + callback.waitForNextStep(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("GET", callback.mResponseAsString); + checkResponseInfo(callback.mResponseInfo, NativeTestServer.getEchoMethodURL(), 200, "OK"); + + // 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 TestUrlRequestCallback. + testSimpleGet(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testBadBuffers() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoMethodURL(), callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.waitForNextStep(); + + // Try to read using a full buffer. + try { + ByteBuffer readBuffer = ByteBuffer.allocateDirect(4); + readBuffer.put("full".getBytes()); + urlRequest.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); + urlRequest.read(readBuffer); + fail("Exception not thrown"); + } catch (IllegalArgumentException e) { + assertEquals("byteBuffer must be a direct ByteBuffer.", + e.getMessage()); + } + + // Finish the request with a direct ByteBuffer. + callback.setAutoAdvance(true); + ByteBuffer readBuffer = ByteBuffer.allocateDirect(5); + urlRequest.read(readBuffer); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("GET", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testNoIoInCancel() throws Exception { + final TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + final UrlRequest urlRequest = + mTestFramework.mCronetEngine + .newUrlRequestBuilder(NativeTestServer.getEchoHeaderURL("blah-header"), + callback, callback.getExecutor()) + .addHeader("blah-header", "blahblahblah") + .build(); + urlRequest.start(); + callback.waitForNextStep(); + callback.startNextRead(urlRequest, ByteBuffer.allocateDirect(4)); + callback.waitForNextStep(); + StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyDeath() + .penaltyLog() + .build()); + try { + urlRequest.cancel(); + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + callback.blockForDone(); + assertEquals(true, callback.mOnCanceledCalled); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUnexpectedReads() throws Exception { + final TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + final UrlRequest urlRequest = + mTestFramework.mCronetEngine + .newUrlRequestBuilder( + NativeTestServer.getRedirectURL(), callback, callback.getExecutor()) + .build(); + + // Try to read before starting request. + try { + callback.startNextRead(urlRequest); + fail("Exception not thrown"); + } catch (IllegalStateException e) { + } + + // Verify reading right after start throws an assertion. Both must be + // invoked on the Executor thread, to prevent receiving data until after + // startNextRead has been invoked. + Runnable startAndRead = new Runnable() { + @Override + public void run() { + urlRequest.start(); + try { + callback.startNextRead(urlRequest); + fail("Exception not thrown"); + } catch (IllegalStateException e) { + } + } + }; + callback.getExecutor().submit(startAndRead).get(); + callback.waitForNextStep(); + + assertEquals(callback.mResponseStep, ResponseStep.ON_RECEIVED_REDIRECT); + // Try to read after the redirect. + try { + callback.startNextRead(urlRequest); + fail("Exception not thrown"); + } catch (IllegalStateException e) { + } + urlRequest.followRedirect(); + callback.waitForNextStep(); + + assertEquals(callback.mResponseStep, ResponseStep.ON_RESPONSE_STARTED); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + + while (!callback.isDone()) { + Runnable readTwice = new Runnable() { + @Override + public void run() { + callback.startNextRead(urlRequest); + // Try to read again before the last read completes. + try { + callback.startNextRead(urlRequest); + fail("Exception not thrown"); + } catch (IllegalStateException e) { + } + } + }; + callback.getExecutor().submit(readTwice).get(); + callback.waitForNextStep(); + } + + assertEquals(callback.mResponseStep, ResponseStep.ON_SUCCEEDED); + assertEquals(NativeTestServer.SUCCESS_BODY, callback.mResponseAsString); + + // Try to read after request is complete. + try { + callback.startNextRead(urlRequest); + fail("Exception not thrown"); + } catch (IllegalStateException e) { + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUnexpectedFollowRedirects() throws Exception { + final TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + final UrlRequest urlRequest = + mTestFramework.mCronetEngine + .newUrlRequestBuilder( + NativeTestServer.getRedirectURL(), callback, callback.getExecutor()) + .build(); + + // Try to follow a redirect before starting the request. + try { + urlRequest.followRedirect(); + fail("Exception not thrown"); + } catch (IllegalStateException e) { + } + + // Try to follow a redirect just after starting the request. Has to be + // done on the executor thread to avoid a race. + Runnable startAndRead = new Runnable() { + @Override + public void run() { + urlRequest.start(); + try { + urlRequest.followRedirect(); + fail("Exception not thrown"); + } catch (IllegalStateException e) { + } + } + }; + callback.getExecutor().execute(startAndRead); + callback.waitForNextStep(); + + assertEquals(callback.mResponseStep, ResponseStep.ON_RECEIVED_REDIRECT); + // Try to follow the redirect twice. Second attempt should fail. + Runnable followRedirectTwice = new Runnable() { + @Override + public void run() { + urlRequest.followRedirect(); + try { + urlRequest.followRedirect(); + fail("Exception not thrown"); + } catch (IllegalStateException e) { + } + } + }; + callback.getExecutor().execute(followRedirectTwice); + callback.waitForNextStep(); + + assertEquals(callback.mResponseStep, ResponseStep.ON_RESPONSE_STARTED); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + + while (!callback.isDone()) { + try { + urlRequest.followRedirect(); + fail("Exception not thrown"); + } catch (IllegalStateException e) { + } + callback.startNextRead(urlRequest); + callback.waitForNextStep(); + } + + assertEquals(callback.mResponseStep, ResponseStep.ON_SUCCEEDED); + assertEquals(NativeTestServer.SUCCESS_BODY, callback.mResponseAsString); + + // Try to follow redirect after request is complete. + try { + urlRequest.followRedirect(); + fail("Exception not thrown"); + } catch (IllegalStateException e) { + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadSetDataProvider() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + try { + builder.setUploadDataProvider(null, callback.getExecutor()); + fail("Exception not thrown"); + } catch (NullPointerException e) { + assertEquals("Invalid UploadDataProvider.", e.getMessage()); + } + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + try { + builder.build().start(); + fail("Exception not thrown"); + } catch (IllegalArgumentException e) { + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadEmptyBodySync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + + assertEquals(0, dataProvider.getUploadedLength()); + assertEquals(0, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("", callback.mResponseAsString); + dataProvider.assertClosed(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(4, dataProvider.getUploadedLength()); + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("test", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadMultiplePiecesSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("Y".getBytes()); + dataProvider.addRead("et ".getBytes()); + dataProvider.addRead("another ".getBytes()); + dataProvider.addRead("test".getBytes()); + + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(16, dataProvider.getUploadedLength()); + assertEquals(4, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("Yet another test", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadMultiplePiecesAsync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.ASYNC, callback.getExecutor()); + dataProvider.addRead("Y".getBytes()); + dataProvider.addRead("et ".getBytes()); + dataProvider.addRead("another ".getBytes()); + dataProvider.addRead("test".getBytes()); + + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(16, dataProvider.getUploadedLength()); + assertEquals(4, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("Yet another test", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadChangesDefaultMethod() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoMethodURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("POST", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadWithSetMethod() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoMethodURL(), callback, callback.getExecutor()); + + final String method = "PUT"; + builder.setHttpMethod(method); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("PUT", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadRedirectSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectToEchoBody(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + // 1 read call before the rewind, 1 after. + assertEquals(2, dataProvider.getNumReadCalls()); + assertEquals(1, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("test", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadRedirectAsync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectToEchoBody(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.ASYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + dataProvider.assertClosed(); + callback.blockForDone(); + + // 1 read call before the rewind, 1 after. + assertEquals(2, dataProvider.getNumReadCalls()); + assertEquals(1, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("test", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadWithBadLength() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()) { + @Override + public long getLength() throws IOException { + return 1; + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) + throws IOException { + byteBuffer.put("12".getBytes()); + uploadDataSink.onReadSucceeded(false); + } + }; + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Read upload data length 2 exceeds expected length 1", + callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadWithBadLengthBufferAligned() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()) { + @Override + public long getLength() throws IOException { + return 8191; + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) + throws IOException { + byteBuffer.put("0123456789abcdef".getBytes()); + uploadDataSink.onReadSucceeded(false); + } + }; + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Read upload data length 8192 exceeds expected length 8191", + callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadReadFailSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setReadFailure(0, TestUploadDataProvider.FailMode.CALLBACK_SYNC); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Sync read failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadLengthFailSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setLengthFailure(); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(0, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Sync length failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadReadFailAsync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setReadFailure(0, TestUploadDataProvider.FailMode.CALLBACK_ASYNC); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Async read failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + /** This test uses a direct executor for upload, and non direct for callbacks */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testDirectExecutorUploadProhibitedByDefault() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + Executor myExecutor = new Executor() { + + @Override + public void execute(Runnable command) { + command.run(); + } + }; + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, myExecutor); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, myExecutor); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + + assertEquals(0, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Inline execution is prohibited for this request", + callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + /** This test uses a direct executor for callbacks, and non direct for upload */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testDirectExecutorProhibitedByDefault() throws Exception { + System.out.println("testing with " + mTestFramework.mCronetEngine); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + Executor myExecutor = new Executor() { + + @Override + public void execute(Runnable command) { + command.run(); + } + }; + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, myExecutor); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + callback.mError.printStackTrace(); + assertContains("Exception posting task to executor", callback.mError.getMessage()); + assertContains("Inline execution is prohibited for this request", + callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + dataProvider.assertClosed(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testDirectExecutorAllowed() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAllowDirectExecutor(true); + Executor myExecutor = new Executor() { + + @Override + public void execute(Runnable command) { + command.run(); + } + }; + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, myExecutor); + UploadDataProvider dataProvider = UploadDataProviders.create("test".getBytes()); + builder.setUploadDataProvider(dataProvider, myExecutor); + builder.addHeader("Content-Type", "useless/string"); + builder.allowDirectExecutor(); + builder.build().start(); + callback.blockForDone(); + + if (callback.mOnErrorCalled) { + throw callback.mError; + } + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("test", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadReadFailThrown() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setReadFailure(0, TestUploadDataProvider.FailMode.THROWN); + // This will never be read, but if the length is 0, read may never be + // called. + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Thrown read failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadRewindFailSync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectToEchoBody(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setRewindFailure(TestUploadDataProvider.FailMode.CALLBACK_SYNC); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(1, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Sync rewind failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadRewindFailAsync() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectToEchoBody(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.ASYNC, callback.getExecutor()); + dataProvider.setRewindFailure(TestUploadDataProvider.FailMode.CALLBACK_ASYNC); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(1, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Async rewind failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadRewindFailThrown() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectToEchoBody(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.setRewindFailure(TestUploadDataProvider.FailMode.THROWN); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(1, dataProvider.getNumRewindCalls()); + + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("Thrown rewind failure", callback.mError.getCause().getMessage()); + assertEquals(null, callback.mResponseInfo); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadChunked() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test hello".getBytes()); + dataProvider.setChunked(true); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + + assertEquals(-1, dataProvider.getUploadedLength()); + + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + // 1 read call for one data chunk. + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals("test hello", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadChunkedLastReadZeroLengthBody() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + // Add 3 reads. The last read has a 0-length body. + dataProvider.addRead("hello there".getBytes()); + dataProvider.addRead("!".getBytes()); + dataProvider.addRead("".getBytes()); + dataProvider.setChunked(true); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + + assertEquals(-1, dataProvider.getUploadedLength()); + + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + // 2 read call for the first two data chunks, and 1 for final chunk. + assertEquals(3, dataProvider.getNumReadCalls()); + assertEquals("hello there!", callback.mResponseAsString); + } + + // Test where an upload fails without ever initializing the + // UploadDataStream, because it can't connect to the server. + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadFailsWithoutInitializingStream() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + // The port for PTP will always refuse a TCP connection + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + "http://127.0.0.1:319", callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertNull(callback.mResponseInfo); + if (mTestRule.testingJavaImpl()) { + Throwable cause = callback.mError.getCause(); + assertTrue("Exception was: " + cause, cause instanceof ConnectException); + } else { + assertContains("Exception in CronetUrlRequest: net::ERR_CONNECTION_REFUSED", + callback.mError.getMessage()); + } + } + + private void throwOrCancel(FailureType failureType, ResponseStep failureStep, + boolean expectResponseInfo, boolean expectError) { + if (Log.isLoggable("TESTING", Log.VERBOSE)) { + Log.v("TESTING", "Testing " + failureType + " during " + failureStep); + } + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setFailure(failureType, failureStep); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectURL(), callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + // Wait for all posted tasks to be executed to ensure there is no unhandled exception. + callback.shutdownExecutorAndWait(); + assertEquals(1, callback.mRedirectCount); + if (failureType == FailureType.CANCEL_SYNC || failureType == FailureType.CANCEL_ASYNC) { + assertResponseStepCanceled(callback); + } else if (failureType == FailureType.THROW_SYNC) { + assertEquals(ResponseStep.ON_FAILED, callback.mResponseStep); + } + assertTrue(urlRequest.isDone()); + assertEquals(expectResponseInfo, callback.mResponseInfo != null); + assertEquals(expectError, callback.mError != null); + assertEquals(expectError, callback.mOnErrorCalled); + // When failureType is FailureType.CANCEL_ASYNC_WITHOUT_PAUSE and failureStep is + // ResponseStep.ON_READ_COMPLETED, there might be an onSucceeded() task already posted. If + // that's the case, onCanceled() will not be invoked. See crbug.com/657415. + if (!(failureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE + && failureStep == ResponseStep.ON_READ_COMPLETED)) { + assertEquals(failureType == FailureType.CANCEL_SYNC + || failureType == FailureType.CANCEL_ASYNC + || failureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, + callback.mOnCanceledCalled); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testFailures() throws Exception { + throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_RECEIVED_REDIRECT, + false, false); + throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_RECEIVED_REDIRECT, + false, false); + throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_RECEIVED_REDIRECT, + false, false); + throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_RECEIVED_REDIRECT, + 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); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testThrowOrCancelInOnSucceeded() { + FailureType[] testTypes = new FailureType[] { + FailureType.THROW_SYNC, FailureType.CANCEL_SYNC, FailureType.CANCEL_ASYNC}; + for (FailureType type : testTypes) { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setFailure(type, ResponseStep.ON_SUCCEEDED); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoMethodURL(), callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + // Wait for all posted tasks to be executed to ensure there is no unhandled exception. + callback.shutdownExecutorAndWait(); + assertNull(callback.mError); + assertEquals(ResponseStep.ON_SUCCEEDED, callback.mResponseStep); + assertTrue(urlRequest.isDone()); + assertNotNull(callback.mResponseInfo); + assertFalse(callback.mOnErrorCalled); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("GET", callback.mResponseAsString); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testThrowOrCancelInOnFailed() { + FailureType[] testTypes = new FailureType[] { + FailureType.THROW_SYNC, FailureType.CANCEL_SYNC, FailureType.CANCEL_ASYNC}; + for (FailureType type : testTypes) { + String url = NativeTestServer.getEchoBodyURL(); + // Shut down NativeTestServer so request will fail. + NativeTestServer.shutdownNativeTestServer(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setFailure(type, ResponseStep.ON_FAILED); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + // Wait for all posted tasks to be executed to ensure there is no unhandled exception. + callback.shutdownExecutorAndWait(); + assertEquals(ResponseStep.ON_FAILED, callback.mResponseStep); + assertTrue(callback.mOnErrorCalled); + assertNotNull(callback.mError); + assertTrue(urlRequest.isDone()); + // Start NativeTestServer again to run the test for a second time. + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testThrowOrCancelInOnCanceled() { + FailureType[] testTypes = new FailureType[] { + FailureType.THROW_SYNC, FailureType.CANCEL_SYNC, FailureType.CANCEL_ASYNC}; + for (FailureType type : testTypes) { + TestUrlRequestCallback callback = new TestUrlRequestCallback() { + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + super.onResponseStarted(request, info); + request.cancel(); + } + }; + callback.setFailure(type, ResponseStep.ON_CANCELED); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + // Wait for all posted tasks to be executed to ensure there is no unhandled exception. + callback.shutdownExecutorAndWait(); + assertResponseStepCanceled(callback); + assertTrue(urlRequest.isDone()); + assertNotNull(callback.mResponseInfo); + assertNull(callback.mError); + assertTrue(callback.mOnCanceledCalled); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // No destroyed callback for tests + public void testExecutorShutdown() { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + + callback.setAutoAdvance(false); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + CronetUrlRequest urlRequest = (CronetUrlRequest) builder.build(); + urlRequest.start(); + callback.waitForNextStep(); + assertFalse(callback.isDone()); + assertFalse(urlRequest.isDone()); + + final ConditionVariable requestDestroyed = new ConditionVariable(false); + urlRequest.setOnDestroyedCallbackForTesting(new Runnable() { + @Override + public void run() { + requestDestroyed.open(); + } + }); + + // Shutdown the executor, so posting the task will throw an exception. + callback.shutdownExecutor(); + ByteBuffer readBuffer = ByteBuffer.allocateDirect(5); + urlRequest.read(readBuffer); + // Callback will never be called again because executor is shutdown, + // but request will be destroyed from network thread. + requestDestroyed.block(); + + assertFalse(callback.isDone()); + assertTrue(urlRequest.isDone()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testUploadExecutorShutdown() throws Exception { + class HangingUploadDataProvider extends UploadDataProvider { + UploadDataSink mUploadDataSink; + ByteBuffer mByteBuffer; + ConditionVariable mReadCalled = new ConditionVariable(false); + + @Override + public long getLength() { + return 69; + } + + @Override + public void read(final UploadDataSink uploadDataSink, + final ByteBuffer byteBuffer) { + mUploadDataSink = uploadDataSink; + mByteBuffer = byteBuffer; + mReadCalled.open(); + } + + @Override + public void rewind(final UploadDataSink uploadDataSink) { + } + } + + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + ExecutorService uploadExecutor = Executors.newSingleThreadExecutor(); + HangingUploadDataProvider dataProvider = new HangingUploadDataProvider(); + builder.setUploadDataProvider(dataProvider, uploadExecutor); + builder.addHeader("Content-Type", "useless/string"); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + // Wait for read to be called on executor. + dataProvider.mReadCalled.block(); + // Shutdown the executor, so posting next task will throw an exception. + uploadExecutor.shutdown(); + // Continue the upload. + dataProvider.mByteBuffer.putInt(42); + dataProvider.mUploadDataSink.onReadSucceeded(false); + // Callback.onFailed will be called on request executor even though upload + // executor is shutdown. + callback.blockForDone(); + assertTrue(callback.isDone()); + assertTrue(callback.mOnErrorCalled); + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertTrue(urlRequest.isDone()); + } + + /** + * A TestUrlRequestCallback that shuts down executor upon receiving onSucceeded callback. + */ + private static class QuitOnSuccessCallback extends TestUrlRequestCallback { + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + // Stop accepting new tasks. + shutdownExecutor(); + super.onSucceeded(request, info); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // No adapter to destroy in pure java + // Regression test for crbug.com/564946. + public void testDestroyUploadDataStreamAdapterOnSucceededCallback() throws Exception { + TestUrlRequestCallback callback = new QuitOnSuccessCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + CronetUrlRequest request = (CronetUrlRequest) builder.build(); + final ConditionVariable uploadDataStreamAdapterDestroyed = new ConditionVariable(); + request.setOnDestroyedUploadCallbackForTesting(new Runnable() { + @Override + public void run() { + uploadDataStreamAdapterDestroyed.open(); + } + }); + + request.start(); + uploadDataStreamAdapterDestroyed.block(); + callback.blockForDone(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("", callback.mResponseAsString); + } + + /* + * Verifies error codes are passed through correctly. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet // Java impl doesn't support MockUrlRequestJobFactory + public void testErrorCodes() throws Exception { + checkSpecificErrorCode( + -105, NetworkException.ERROR_HOSTNAME_NOT_RESOLVED, "NAME_NOT_RESOLVED", false); + checkSpecificErrorCode( + -106, NetworkException.ERROR_INTERNET_DISCONNECTED, "INTERNET_DISCONNECTED", false); + checkSpecificErrorCode( + -21, NetworkException.ERROR_NETWORK_CHANGED, "NETWORK_CHANGED", true); + checkSpecificErrorCode( + -100, NetworkException.ERROR_CONNECTION_CLOSED, "CONNECTION_CLOSED", true); + checkSpecificErrorCode( + -102, NetworkException.ERROR_CONNECTION_REFUSED, "CONNECTION_REFUSED", false); + checkSpecificErrorCode( + -101, NetworkException.ERROR_CONNECTION_RESET, "CONNECTION_RESET", true); + checkSpecificErrorCode( + -118, NetworkException.ERROR_CONNECTION_TIMED_OUT, "CONNECTION_TIMED_OUT", true); + checkSpecificErrorCode(-7, NetworkException.ERROR_TIMED_OUT, "TIMED_OUT", true); + checkSpecificErrorCode( + -109, NetworkException.ERROR_ADDRESS_UNREACHABLE, "ADDRESS_UNREACHABLE", false); + checkSpecificErrorCode(-2, NetworkException.ERROR_OTHER, "FAILED", false); + } + + /* + * Verifies no cookies are saved or sent by default. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testCookiesArentSavedOrSent() throws Exception { + // Make a request to a url that sets the cookie + String url = NativeTestServer.getFileURL("/set_cookie.html"); + TestUrlRequestCallback callback = startAndWaitForComplete(url); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("A=B", callback.mResponseInfo.getAllHeaders().get("Set-Cookie").get(0)); + + // Make a request that check that cookie header isn't sent. + String headerName = "Cookie"; + String url2 = NativeTestServer.getEchoHeaderURL(headerName); + TestUrlRequestCallback callback2 = startAndWaitForComplete(url2); + assertEquals(200, callback2.mResponseInfo.getHttpStatusCode()); + assertEquals("Header not found. :(", callback2.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testQuicErrorCode() throws Exception { + TestUrlRequestCallback callback = + startAndWaitForComplete(MockUrlRequestJobFactory.getMockUrlWithFailure( + FailurePhase.START, NetError.ERR_QUIC_PROTOCOL_ERROR)); + assertNull(callback.mResponseInfo); + assertNotNull(callback.mError); + assertEquals(NetworkException.ERROR_QUIC_PROTOCOL_FAILED, + ((NetworkException) callback.mError).getErrorCode()); + assertTrue(callback.mError instanceof QuicException); + QuicException quicException = (QuicException) callback.mError; + // 1 is QUIC_INTERNAL_ERROR + assertEquals(1, quicException.getQuicDetailedErrorCode()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testQuicErrorCodeForNetworkChanged() throws Exception { + TestUrlRequestCallback callback = + startAndWaitForComplete(MockUrlRequestJobFactory.getMockUrlWithFailure( + FailurePhase.START, NetError.ERR_NETWORK_CHANGED)); + assertNull(callback.mResponseInfo); + assertNotNull(callback.mError); + assertEquals(NetworkException.ERROR_NETWORK_CHANGED, + ((NetworkException) callback.mError).getErrorCode()); + assertTrue(callback.mError instanceof QuicException); + QuicException quicException = (QuicException) callback.mError; + // QUIC_CONNECTION_MIGRATION_NO_NEW_NETWORK(83) is set in + // URLRequestFailedJob::PopulateNetErrorDetails for this test. + final int quicErrorCode = 83; + assertEquals(quicErrorCode, quicException.getQuicDetailedErrorCode()); + } + + /** + * Tests that legacy onFailed callback is invoked with UrlRequestException if there + * is no onFailed callback implementation that takes CronetException. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testLegacyOnFailedCallback() throws Exception { + final int netError = -123; + final AtomicBoolean failedExpectation = new AtomicBoolean(); + final ConditionVariable done = new ConditionVariable(); + UrlRequest.Callback callback = new UrlRequest.Callback() { + @Override + public void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) { + failedExpectation.set(true); + fail(); + } + + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + failedExpectation.set(true); + fail(); + } + + @Override + public void onReadCompleted( + UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) { + failedExpectation.set(true); + fail(); + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + failedExpectation.set(true); + fail(); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) { + assertTrue(error instanceof NetworkException); + assertEquals(netError, ((NetworkException) error).getCronetInternalErrorCode()); + failedExpectation.set( + ((NetworkException) error).getCronetInternalErrorCode() != netError); + done.open(); + } + + @Override + public void onCanceled(UrlRequest request, UrlResponseInfo info) { + failedExpectation.set(true); + fail(); + } + }; + + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + MockUrlRequestJobFactory.getMockUrlWithFailure(FailurePhase.START, netError), + callback, Executors.newSingleThreadExecutor()); + final UrlRequest urlRequest = builder.build(); + urlRequest.start(); + done.block(); + // Check that onFailed is called. + assertFalse(failedExpectation.get()); + } + + private void checkSpecificErrorCode(int netError, int errorCode, String name, + boolean immediatelyRetryable) throws Exception { + TestUrlRequestCallback callback = startAndWaitForComplete( + MockUrlRequestJobFactory.getMockUrlWithFailure(FailurePhase.START, netError)); + assertNull(callback.mResponseInfo); + assertNotNull(callback.mError); + assertEquals(netError, ((NetworkException) callback.mError).getCronetInternalErrorCode()); + assertEquals(errorCode, ((NetworkException) callback.mError).getErrorCode()); + assertEquals( + immediatelyRetryable, ((NetworkException) callback.mError).immediatelyRetryable()); + assertContains( + "Exception in CronetUrlRequest: net::ERR_" + name, callback.mError.getMessage()); + assertEquals(0, callback.mRedirectCount); + assertTrue(callback.mOnErrorCalled); + assertEquals(ResponseStep.ON_FAILED, callback.mResponseStep); + } + + // Returns the contents of byteBuffer, from its position() to its limit(), + // as a String. Does not modify byteBuffer's position(). + private 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); + } + + private void assertResponseStepCanceled(TestUrlRequestCallback callback) { + if (callback.mResponseStep == ResponseStep.ON_FAILED && callback.mError != null) { + throw new Error( + "Unexpected response state: " + ResponseStep.ON_FAILED, callback.mError); + } + assertEquals(ResponseStep.ON_CANCELED, callback.mResponseStep); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testCleartextTrafficBlocked() throws Exception { + // This feature only works starting from N. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + final int cleartextNotPermitted = -29; + // This hostname needs to match the one in network_security_config.xml and the one used + // by QuicTestServer. + // https requests to it are tested in QuicTest, so this checks that we're only blocking + // cleartext. + final String url = "http://example.com/simple.txt"; + TestUrlRequestCallback callback = startAndWaitForComplete(url); + assertNull(callback.mResponseInfo); + assertNotNull(callback.mError); + assertEquals(cleartextNotPermitted, + ((NetworkException) callback.mError).getCronetInternalErrorCode()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + /** + * Open many connections and cancel them right away. This test verifies all internal + * sockets and other Closeables are properly closed. See crbug.com/726193. + */ + public void testGzipCancel() throws Exception { + String url = NativeTestServer.getFileURL("/gzipped.html"); + for (int i = 0; i < 100; i++) { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + UrlRequest urlRequest = + mTestFramework.mCronetEngine + .newUrlRequestBuilder(url, callback, callback.getExecutor()) + .build(); + urlRequest.start(); + urlRequest.cancel(); + // If the test blocks until each UrlRequest finishes before starting the next UrlRequest + // then it never catches the leak. If it starts all UrlRequests and then blocks until + // all UrlRequests finish, it only catches the leak ~10% of the time. In its current + // form it appears to catch the leak ~70% of the time. + // Catching the leak may require a lot of busy threads so that the cancel() happens + // before the UrlRequest has made much progress (and set mCurrentUrlConnection and + // mResponseChannel). This may be why blocking until each UrlRequest finishes doesn't + // catch the leak. + // The other quirk of this is that from teardown(), JavaCronetEngine.shutdown() is + // called which calls ExecutorService.shutdown() which doesn't wait for the thread to + // finish running tasks, and then teardown() calls GC looking for leaks. One possible + // modification would be to expose the ExecutorService and then have tests call + // awaitTermination() but this would complicate things, and adding a 1s sleep() to + // allow the ExecutorService to terminate did not increase the chances of catching the + // leak. + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @RequiresMinApi(8) // JavaUrlRequest fixed in API level 8: crrev.com/499303 + /** Do a HEAD request and get back a 404. */ + public void test404Head() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getFileURL("/notfound.html"), callback, callback.getExecutor()); + builder.setHttpMethod("HEAD").build().start(); + callback.blockForDone(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @RequiresMinApi(9) // Tagging support added in API level 9: crrev.com/c/chromium/src/+/930086 + public void testTagging() throws Exception { + if (!CronetTestUtil.nativeCanGetTaggedBytes()) { + Log.i(TAG, "Skipping test - GetTaggedBytes unsupported."); + return; + } + String url = NativeTestServer.getEchoMethodURL(); + + // Test untagged requests are given tag 0. + int tag = 0; + long priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + ExperimentalUrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + builder.build().start(); + callback.blockForDone(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + + // Test explicit tagging. + tag = 0x12345678; + priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + callback = new TestUrlRequestCallback(); + builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + assertEquals(builder.setTrafficStatsTag(tag), builder); + builder.build().start(); + callback.blockForDone(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + + // Test a different tag value to make sure reused connections are retagged. + tag = 0x87654321; + priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + callback = new TestUrlRequestCallback(); + builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + assertEquals(builder.setTrafficStatsTag(tag), builder); + builder.build().start(); + callback.blockForDone(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + + // Test tagging with our UID. + tag = 0; + priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + callback = new TestUrlRequestCallback(); + builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + assertEquals(builder.setTrafficStatsUid(Process.myUid()), builder); + builder.build().start(); + callback.blockForDone(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + /** + * Initiate many requests concurrently to make sure neither Cronet implementation crashes. + * Regression test for https://crbug.com/844031. + */ + public void testManyRequests() throws Exception { + String url = NativeTestServer.getMultiRedirectURL(); + final int numRequests = 2000; + TestUrlRequestCallback callbacks[] = new TestUrlRequestCallback[numRequests]; + UrlRequest requests[] = new UrlRequest[numRequests]; + for (int i = 0; i < numRequests; i++) { + // Share the first callback's executor to avoid creating too many single-threaded + // executors and hence too many threads. + if (i == 0) { + callbacks[i] = new TestUrlRequestCallback(); + } else { + callbacks[i] = new TestUrlRequestCallback(callbacks[0].getExecutor()); + } + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + url, callbacks[i], callbacks[i].getExecutor()); + requests[i] = builder.build(); + } + for (UrlRequest request : requests) { + request.start(); + } + for (UrlRequest request : requests) { + request.cancel(); + } + for (TestUrlRequestCallback callback : callbacks) { + callback.blockForDone(); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testSetIdempotency() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + ExperimentalUrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoMethodURL(), callback, callback.getExecutor()); + assertEquals(builder.setIdempotency(ExperimentalUrlRequest.Builder.IDEMPOTENT), builder); + + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("POST", callback.mResponseAsString); + } + + // Return connection migration disable load flag value. + private static native int nativeGetConnectionMigrationDisableLoadFlag(); +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/DiskStorageTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/DiskStorageTest.java new file mode 100644 index 0000000000..430cbb7f76 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/DiskStorageTest.java @@ -0,0 +1,231 @@ +// Copyright 2016 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import static org.chromium.net.CronetTestRule.getContext; +import static org.chromium.net.CronetTestRule.getTestStorage; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.FileUtils; +import org.chromium.base.PathUtils; +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.util.Arrays; + +/** + * Test CronetEngine disk storage. + */ +@RunWith(AndroidJUnit4.class) +public class DiskStorageTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private String mReadOnlyStoragePath; + + @Before + public void setUp() throws Exception { + System.loadLibrary("cronet_tests"); + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @After + public void tearDown() throws Exception { + if (mReadOnlyStoragePath != null) { + FileUtils.recursivelyDeleteFile(new File(mReadOnlyStoragePath), FileUtils.DELETE_ALL); + } + NativeTestServer.shutdownNativeTestServer(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Crashing on Android Cronet Builder, see crbug.com/601409. + public void testReadOnlyStorageDirectory() throws Exception { + mReadOnlyStoragePath = PathUtils.getDataDirectory() + "/read_only"; + File readOnlyStorage = new File(mReadOnlyStoragePath); + assertTrue(readOnlyStorage.mkdir()); + // Setting the storage directory as readonly has no effect. + assertTrue(readOnlyStorage.setReadOnly()); + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + builder.setStoragePath(mReadOnlyStoragePath); + builder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, 1024 * 1024); + + CronetEngine cronetEngine = builder.build(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = NativeTestServer.getFileURL("/cacheable.txt"); + UrlRequest.Builder requestBuilder = + cronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + UrlRequest urlRequest = requestBuilder.build(); + urlRequest.start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + cronetEngine.shutdown(); + FileInputStream newVersionFile = null; + // Make sure that version file is in readOnlyStoragePath. + File versionFile = new File(mReadOnlyStoragePath + "/version"); + try { + newVersionFile = new FileInputStream(versionFile); + byte[] buffer = new byte[] {0, 0, 0, 0}; + int bytesRead = newVersionFile.read(buffer, 0, 4); + assertEquals(4, bytesRead); + assertTrue(Arrays.equals(new byte[] {1, 0, 0, 0}, buffer)); + } finally { + if (newVersionFile != null) { + newVersionFile.close(); + } + } + File diskCacheDir = new File(mReadOnlyStoragePath + "/disk_cache"); + assertTrue(diskCacheDir.exists()); + File prefsDir = new File(mReadOnlyStoragePath + "/prefs"); + assertTrue(prefsDir.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Crashing on Android Cronet Builder, see crbug.com/601409. + public void testPurgeOldVersion() throws Exception { + String testStorage = getTestStorage(getContext()); + File versionFile = new File(testStorage + "/version"); + FileOutputStream versionOut = null; + try { + versionOut = new FileOutputStream(versionFile); + versionOut.write(new byte[] {0, 0, 0, 0}, 0, 4); + } finally { + if (versionOut != null) { + versionOut.close(); + } + } + File oldPrefsFile = new File(testStorage + "/local_prefs.json"); + FileOutputStream oldPrefsOut = null; + try { + oldPrefsOut = new FileOutputStream(oldPrefsFile); + String dummy = "dummy content"; + oldPrefsOut.write(dummy.getBytes(), 0, dummy.length()); + } finally { + if (oldPrefsOut != null) { + oldPrefsOut.close(); + } + } + + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + builder.setStoragePath(getTestStorage(getContext())); + builder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, 1024 * 1024); + + CronetEngine cronetEngine = builder.build(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = NativeTestServer.getFileURL("/cacheable.txt"); + UrlRequest.Builder requestBuilder = + cronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + UrlRequest urlRequest = requestBuilder.build(); + urlRequest.start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + cronetEngine.shutdown(); + FileInputStream newVersionFile = null; + try { + newVersionFile = new FileInputStream(versionFile); + byte[] buffer = new byte[] {0, 0, 0, 0}; + int bytesRead = newVersionFile.read(buffer, 0, 4); + assertEquals(4, bytesRead); + assertTrue(Arrays.equals(new byte[] {1, 0, 0, 0}, buffer)); + } finally { + if (newVersionFile != null) { + newVersionFile.close(); + } + } + oldPrefsFile = new File(testStorage + "/local_prefs.json"); + assertTrue(!oldPrefsFile.exists()); + File diskCacheDir = new File(testStorage + "/disk_cache"); + assertTrue(diskCacheDir.exists()); + File prefsDir = new File(testStorage + "/prefs"); + assertTrue(prefsDir.exists()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that if cache version is current, Cronet does not purge the directory. + public void testCacheVersionCurrent() throws Exception { + // Initialize a CronetEngine and shut it down. + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + builder.setStoragePath(getTestStorage(getContext())); + builder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, 1024 * 1024); + + CronetEngine cronetEngine = builder.build(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + String url = NativeTestServer.getFileURL("/cacheable.txt"); + UrlRequest.Builder requestBuilder = + cronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + UrlRequest urlRequest = requestBuilder.build(); + urlRequest.start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + cronetEngine.shutdown(); + + // Create a dummy file in storage directory. + String testStorage = getTestStorage(getContext()); + File dummyFile = new File(testStorage + "/dummy.json"); + FileOutputStream dummyFileOut = null; + String dummyContent = "dummy content"; + try { + dummyFileOut = new FileOutputStream(dummyFile); + dummyFileOut.write(dummyContent.getBytes(), 0, dummyContent.length()); + } finally { + if (dummyFileOut != null) { + dummyFileOut.close(); + } + } + + // Creates a new CronetEngine and make a request. + CronetEngine engine = builder.build(); + TestUrlRequestCallback callback2 = new TestUrlRequestCallback(); + String url2 = NativeTestServer.getFileURL("/cacheable.txt"); + UrlRequest.Builder requestBuilder2 = + engine.newUrlRequestBuilder(url2, callback2, callback2.getExecutor()); + UrlRequest urlRequest2 = requestBuilder2.build(); + urlRequest2.start(); + callback2.blockForDone(); + assertEquals(200, callback2.mResponseInfo.getHttpStatusCode()); + engine.shutdown(); + // Dummy file still exists. + BufferedReader reader = new BufferedReader(new FileReader(dummyFile)); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + reader.close(); + assertEquals(dummyContent, stringBuilder.toString()); + File diskCacheDir = new File(testStorage + "/disk_cache"); + assertTrue(diskCacheDir.exists()); + File prefsDir = new File(testStorage + "/prefs"); + assertTrue(prefsDir.exists()); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/ExperimentalOptionsTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/ExperimentalOptionsTest.java new file mode 100644 index 0000000000..8f15ff12fe --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/ExperimentalOptionsTest.java @@ -0,0 +1,313 @@ +// Copyright 2016 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import static org.chromium.net.CronetTestRule.SERVER_CERT_PEM; +import static org.chromium.net.CronetTestRule.SERVER_KEY_PKCS8_PEM; +import static org.chromium.net.CronetTestRule.assertContains; +import static org.chromium.net.CronetTestRule.getContext; +import static org.chromium.net.CronetTestRule.getTestStorage; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.LargeTest; +import androidx.test.filters.MediumTest; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; + +import org.chromium.base.Log; +import org.chromium.base.PathUtils; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.test.util.DisabledTest; +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.MetricsTestUtil.TestRequestFinishedListener; +import org.chromium.net.impl.CronetUrlRequestContext; +import org.chromium.net.test.EmbeddedTestServer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.CountDownLatch; + +/** + * Tests for experimental options. + */ +@RunWith(AndroidJUnit4.class) +@JNINamespace("cronet") +public class ExperimentalOptionsTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private static final String TAG = ExperimentalOptionsTest.class.getSimpleName(); + private ExperimentalCronetEngine.Builder mBuilder; + private CountDownLatch mHangingUrlLatch; + + @Before + public void setUp() throws Exception { + mBuilder = new ExperimentalCronetEngine.Builder(getContext()); + mHangingUrlLatch = new CountDownLatch(1); + CronetTestUtil.setMockCertVerifierForTesting( + mBuilder, QuicTestServer.createMockCertVerifier()); + assertTrue(Http2TestServer.startHttp2TestServer( + getContext(), SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM, mHangingUrlLatch)); + } + + @After + public void tearDown() throws Exception { + mHangingUrlLatch.countDown(); + assertTrue(Http2TestServer.shutdownHttp2TestServer()); + } + + @Test + @MediumTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that NetLog writes effective experimental options to NetLog. + public void testNetLog() throws Exception { + File directory = new File(PathUtils.getDataDirectory()); + File logfile = File.createTempFile("cronet", "json", directory); + JSONObject hostResolverParams = CronetTestUtil.generateHostResolverRules(); + JSONObject experimentalOptions = + new JSONObject().put("HostResolverRules", hostResolverParams); + mBuilder.setExperimentalOptions(experimentalOptions.toString()); + + CronetEngine cronetEngine = mBuilder.build(); + cronetEngine.startNetLogToFile(logfile.getPath(), false); + String url = Http2TestServer.getEchoMethodUrl(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = + cronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("GET", callback.mResponseAsString); + cronetEngine.stopNetLog(); + assertFileContainsString(logfile, "HostResolverRules"); + assertTrue(logfile.delete()); + assertFalse(logfile.exists()); + cronetEngine.shutdown(); + } + + @DisabledTest(message = "crbug.com/1021941") + @Test + @MediumTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSetSSLKeyLogFile() throws Exception { + String url = Http2TestServer.getEchoMethodUrl(); + File dir = new File(PathUtils.getDataDirectory()); + File file = File.createTempFile("ssl_key_log_file", "", dir); + + JSONObject experimentalOptions = new JSONObject().put("ssl_key_log_file", file.getPath()); + mBuilder.setExperimentalOptions(experimentalOptions.toString()); + CronetEngine cronetEngine = mBuilder.build(); + + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = + cronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("GET", callback.mResponseAsString); + + assertFileContainsString(file, "CLIENT_RANDOM"); + assertTrue(file.delete()); + assertFalse(file.exists()); + cronetEngine.shutdown(); + } + + // Helper method to assert that file contains content. It retries 5 times + // with a 100ms interval. + private void assertFileContainsString(File file, String content) throws Exception { + boolean contains = false; + for (int i = 0; i < 5; i++) { + contains = fileContainsString(file, content); + if (contains) break; + Log.i(TAG, "Retrying..."); + Thread.sleep(100); + } + assertTrue("file content doesn't match", contains); + } + + // Returns whether a file contains a particular string. + private boolean fileContainsString(File file, String content) throws IOException { + FileInputStream fileInputStream = null; + Log.i(TAG, "looking for [%s] in %s", content, file.getName()); + try { + fileInputStream = new FileInputStream(file); + byte[] data = new byte[(int) file.length()]; + fileInputStream.read(data); + String actual = new String(data, "UTF-8"); + boolean contains = actual.contains(content); + if (!contains) { + Log.i(TAG, "file content [%s]", actual); + } + return contains; + } catch (FileNotFoundException e) { + // Ignored this exception since the file will only be created when updates are + // flushed to the disk. + Log.i(TAG, "file not found"); + } finally { + if (fileInputStream != null) { + fileInputStream.close(); + } + } + return false; + } + + @Test + @MediumTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that basic Cronet functionality works when host cache persistence is enabled, and that + // persistence works. + public void testHostCachePersistence() throws Exception { + EmbeddedTestServer testServer = EmbeddedTestServer.createAndStartServer(getContext()); + + String realUrl = testServer.getURL("/echo?status=200"); + URL javaUrl = new URL(realUrl); + String realHost = javaUrl.getHost(); + int realPort = javaUrl.getPort(); + String testHost = "host-cache-test-host"; + String testUrl = new URL("http", testHost, realPort, javaUrl.getPath()).toString(); + + mBuilder.setStoragePath(getTestStorage(getContext())) + .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, 0); + + // Set a short delay so the pref gets written quickly. + JSONObject staleDns = new JSONObject() + .put("enable", true) + .put("delay_ms", 0) + .put("allow_other_network", true) + .put("persist_to_disk", true) + .put("persist_delay_ms", 0); + JSONObject experimentalOptions = new JSONObject().put("StaleDNS", staleDns); + mBuilder.setExperimentalOptions(experimentalOptions.toString()); + CronetUrlRequestContext context = (CronetUrlRequestContext) mBuilder.build(); + + // Create a HostCache entry for "host-cache-test-host". + nativeWriteToHostCache(context.getUrlRequestContextAdapter(), realHost); + + // Do a request for the test URL to make sure it's cached. + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = + context.newUrlRequestBuilder(testUrl, callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + assertNull(callback.mError); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + + // Shut down the context, persisting contents to disk, and build a new one. + context.shutdown(); + context = (CronetUrlRequestContext) mBuilder.build(); + + // Use the test URL without creating a new cache entry first. It should use the persisted + // one. + callback = new TestUrlRequestCallback(); + builder = context.newUrlRequestBuilder(testUrl, callback, callback.getExecutor()); + urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + assertNull(callback.mError); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + context.shutdown(); + } + + @Test + @MediumTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Experimental options should be specified through a JSON compliant string. When that is not + // the case building a Cronet engine should fail when it is allowed to do so. + public void testWrongJsonExperimentalOptions() throws Exception { + if (nativeExperimentalOptionsParsingIsAllowedToFail()) { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Experimental options parsing failed"); + } + mBuilder.setExperimentalOptions("Not a serialized JSON object"); + CronetEngine cronetEngine = mBuilder.build(); + } + + @Test + @MediumTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testDetectBrokenConnection() throws Exception { + String url = Http2TestServer.getEchoMethodUrl(); + int heartbeatIntervalSecs = 1; + JSONObject experimentalOptions = + new JSONObject().put("bidi_stream_detect_broken_connection", heartbeatIntervalSecs); + mBuilder.setExperimentalOptions(experimentalOptions.toString()); + ExperimentalCronetEngine cronetEngine = (ExperimentalCronetEngine) mBuilder.build(); + + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + ExperimentalBidirectionalStream.Builder builder = + cronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor()) + .setHttpMethod("GET"); + BidirectionalStream stream = builder.build(); + stream.start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("GET", callback.mResponseAsString); + cronetEngine.shutdown(); + } + + @Test + @LargeTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testDetectBrokenConnectionOnNetworkFailure() throws Exception { + // HangingRequestUrl stops the server from replying until mHangingUrlLatch is opened, + // simulating a network failure between client and server. + String hangingUrl = Http2TestServer.getHangingRequestUrl(); + TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback(); + TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); + int heartbeatIntervalSecs = 1; + JSONObject experimentalOptions = + new JSONObject().put("bidi_stream_detect_broken_connection", heartbeatIntervalSecs); + mBuilder.setExperimentalOptions(experimentalOptions.toString()); + ExperimentalCronetEngine cronetEngine = (ExperimentalCronetEngine) mBuilder.build(); + cronetEngine.addRequestFinishedListener(requestFinishedListener); + ExperimentalBidirectionalStream.Builder builder = + cronetEngine + .newBidirectionalStreamBuilder(hangingUrl, callback, callback.getExecutor()) + .setHttpMethod("GET"); + BidirectionalStream stream = builder.build(); + stream.start(); + callback.blockForDone(); + assertTrue(stream.isDone()); + assertTrue(callback.mOnErrorCalled); + assertContains("Exception in BidirectionalStream: net::ERR_HTTP2_PING_FAILED", + callback.mError.getMessage()); + assertEquals(NetError.ERR_HTTP2_PING_FAILED, + ((NetworkException) callback.mError).getCronetInternalErrorCode()); + cronetEngine.shutdown(); + } + + // Sets a host cache entry with hostname "host-cache-test-host" and an AddressList containing + // the provided address. + private static native void nativeWriteToHostCache(long adapter, String address); + // Whether Cronet engine creation can fail due to failure during experimental options parsing. + private static native boolean nativeExperimentalOptionsParsingIsAllowedToFail(); +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/GetStatusTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/GetStatusTest.java new file mode 100644 index 0000000000..fb742679ed --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/GetStatusTest.java @@ -0,0 +1,209 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.net.CronetTestRule.getContext; + +import android.os.ConditionVariable; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.CronetTestFramework; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.TestUrlRequestCallback.ResponseStep; +import org.chromium.net.UrlRequest.Status; +import org.chromium.net.UrlRequest.StatusListener; +import org.chromium.net.impl.LoadState; +import org.chromium.net.impl.UrlRequestBase; + +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * Tests that {@link org.chromium.net.impl.CronetUrlRequest#getStatus(StatusListener)} works as + * expected. + */ +@RunWith(AndroidJUnit4.class) +public class GetStatusTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private CronetTestFramework mTestFramework; + + private static class TestStatusListener extends StatusListener { + boolean mOnStatusCalled; + int mStatus = Integer.MAX_VALUE; + private final ConditionVariable mBlock = new ConditionVariable(); + + @Override + public void onStatus(int status) { + mOnStatusCalled = true; + mStatus = status; + mBlock.open(); + } + + public void waitUntilOnStatusCalled() { + mBlock.block(); + mBlock.close(); + } + } + @Before + public void setUp() throws Exception { + mTestFramework = mTestRule.startCronetTestFramework(); + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @After + public void tearDown() throws Exception { + NativeTestServer.shutdownNativeTestServer(); + mTestFramework.mCronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testSimpleGet() throws Exception { + String url = NativeTestServer.getEchoMethodURL(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAutoAdvance(false); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + url, callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + // Calling before request is started should give Status.INVALID, + // since the native adapter is not created. + TestStatusListener statusListener0 = new TestStatusListener(); + urlRequest.getStatus(statusListener0); + statusListener0.waitUntilOnStatusCalled(); + assertTrue(statusListener0.mOnStatusCalled); + assertEquals(Status.INVALID, statusListener0.mStatus); + + urlRequest.start(); + + // Should receive a valid status. + TestStatusListener statusListener1 = new TestStatusListener(); + urlRequest.getStatus(statusListener1); + statusListener1.waitUntilOnStatusCalled(); + assertTrue(statusListener1.mOnStatusCalled); + assertTrue("Status is :" + statusListener1.mStatus, statusListener1.mStatus >= Status.IDLE); + assertTrue("Status is :" + statusListener1.mStatus, + statusListener1.mStatus <= Status.READING_RESPONSE); + + callback.waitForNextStep(); + assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep); + callback.startNextRead(urlRequest); + + // Should receive a valid status. + TestStatusListener statusListener2 = new TestStatusListener(); + urlRequest.getStatus(statusListener2); + statusListener2.waitUntilOnStatusCalled(); + assertTrue(statusListener2.mOnStatusCalled); + assertTrue(statusListener1.mStatus >= Status.IDLE); + assertTrue(statusListener1.mStatus <= Status.READING_RESPONSE); + + callback.waitForNextStep(); + assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep); + + callback.startNextRead(urlRequest); + callback.blockForDone(); + + // Calling after request done should give Status.INVALID, since + // the native adapter is destroyed. + TestStatusListener statusListener3 = new TestStatusListener(); + urlRequest.getStatus(statusListener3); + statusListener3.waitUntilOnStatusCalled(); + assertTrue(statusListener3.mOnStatusCalled); + assertEquals(Status.INVALID, statusListener3.mStatus); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("GET", callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testInvalidLoadState() throws Exception { + try { + UrlRequestBase.convertLoadState(LoadState.OBSOLETE_WAITING_FOR_APPCACHE); + fail(); + } catch (IllegalArgumentException e) { + // Expected because LoadState.WAITING_FOR_APPCACHE is not mapped. + } + + thrown.expect(Throwable.class); + UrlRequestBase.convertLoadState(-1); + UrlRequestBase.convertLoadState(16); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + // Regression test for crbug.com/606872. + @OnlyRunNativeCronet + public void testGetStatusForUpload() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + + final ConditionVariable block = new ConditionVariable(); + // Use a separate executor for UploadDataProvider so the upload can be + // stalled while getStatus gets processed. + Executor uploadProviderExecutor = Executors.newSingleThreadExecutor(); + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, uploadProviderExecutor) { + @Override + public long getLength() throws IOException { + // Pause the data provider. + block.block(); + block.close(); + return super.getLength(); + } + }; + dataProvider.addRead("test".getBytes()); + builder.setUploadDataProvider(dataProvider, uploadProviderExecutor); + builder.addHeader("Content-Type", "useless/string"); + UrlRequest urlRequest = builder.build(); + TestStatusListener statusListener = new TestStatusListener(); + urlRequest.start(); + // Call getStatus() immediately after start(), which will post + // startInternal() to the upload provider's executor because there is an + // upload. When CronetUrlRequestAdapter::GetStatusOnNetworkThread is + // executed, the |url_request_| is null. + urlRequest.getStatus(statusListener); + statusListener.waitUntilOnStatusCalled(); + assertTrue(statusListener.mOnStatusCalled); + // The request should be in IDLE state because GetStatusOnNetworkThread + // is called before |url_request_| is initialized and started. + assertEquals(Status.IDLE, statusListener.mStatus); + // Resume the UploadDataProvider. + block.open(); + + // Make sure the request is successful and there is no crash. + callback.blockForDone(); + dataProvider.assertClosed(); + + assertEquals(4, dataProvider.getUploadedLength()); + assertEquals(1, dataProvider.getNumReadCalls()); + assertEquals(0, dataProvider.getNumRewindCalls()); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("test", callback.mResponseAsString); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/MetricsTestUtil.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/MetricsTestUtil.java new file mode 100644 index 0000000000..0492390603 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/MetricsTestUtil.java @@ -0,0 +1,177 @@ +// Copyright 2016 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.os.ConditionVariable; + +import java.util.Date; +import java.util.LinkedList; +import java.util.NoSuchElementException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * Classes which are useful for testing Cronet's metrics implementation and are needed in more than + * one test file. + */ +public class MetricsTestUtil { + /** + * Executor which runs tasks only when told to with runAllTasks(). + */ + public static class TestExecutor implements Executor { + private final LinkedList mTaskQueue = new LinkedList(); + + @Override + public void execute(Runnable task) { + mTaskQueue.add(task); + } + + public void runAllTasks() { + try { + while (mTaskQueue.size() > 0) { + mTaskQueue.remove().run(); + } + } catch (NoSuchElementException e) { + throw new RuntimeException("Task was removed during iteration", e); + } + } + } + + /** + * RequestFinishedInfo.Listener for testing, which saves the RequestFinishedInfo + */ + public static class TestRequestFinishedListener extends RequestFinishedInfo.Listener { + private final ConditionVariable mBlock; + private RequestFinishedInfo mRequestInfo; + + // TODO(mgersh): it's weird that you can use either this constructor or blockUntilDone() but + // not both. Either clean it up or document why it has to work this way. + public TestRequestFinishedListener(Executor executor) { + super(executor); + mBlock = new ConditionVariable(); + } + + public TestRequestFinishedListener() { + super(Executors.newSingleThreadExecutor()); + mBlock = new ConditionVariable(); + } + + public RequestFinishedInfo getRequestInfo() { + return mRequestInfo; + } + + @Override + public void onRequestFinished(RequestFinishedInfo requestInfo) { + assertNull("onRequestFinished called repeatedly", mRequestInfo); + assertNotNull(requestInfo); + mRequestInfo = requestInfo; + mBlock.open(); + } + + public void blockUntilDone() { + mBlock.block(); + } + + public void reset() { + mBlock.close(); + mRequestInfo = null; + } + } + + // Helper method to assert date1 is equals to or after date2. + // Some implementation of java.util.Date broke the symmetric property, so + // check both directions. + public static void assertAfter(Date date1, Date date2) { + assertTrue("date1: " + date1.getTime() + ", date2: " + date2.getTime(), + date1.after(date2) || date1.equals(date2) || date2.equals(date1)); + } + + /** + * Check existence of all the timing metrics that apply to most test requests, + * except those that come from net::LoadTimingInfo::ConnectTiming. + * Also check some timing differences, focusing on things we can't check with asserts in the + * CronetMetrics constructor. + * Don't check push times here. + */ + public static void checkTimingMetrics( + RequestFinishedInfo.Metrics metrics, Date startTime, Date endTime) { + assertNotNull(metrics.getRequestStart()); + assertAfter(metrics.getRequestStart(), startTime); + assertNotNull(metrics.getSendingStart()); + assertAfter(metrics.getSendingStart(), startTime); + assertNotNull(metrics.getSendingEnd()); + assertAfter(endTime, metrics.getSendingEnd()); + assertNotNull(metrics.getResponseStart()); + assertAfter(metrics.getResponseStart(), startTime); + assertNotNull(metrics.getRequestEnd()); + assertAfter(endTime, metrics.getRequestEnd()); + assertAfter(metrics.getRequestEnd(), metrics.getRequestStart()); + } + + /** + * Check that the timing metrics which come from net::LoadTimingInfo::ConnectTiming exist, + * except SSL times in the case of non-https requests. + */ + public static void checkHasConnectTiming( + RequestFinishedInfo.Metrics metrics, Date startTime, Date endTime, boolean isSsl) { + assertNotNull(metrics.getDnsStart()); + assertAfter(metrics.getDnsStart(), startTime); + assertNotNull(metrics.getDnsEnd()); + assertAfter(endTime, metrics.getDnsEnd()); + assertNotNull(metrics.getConnectStart()); + assertAfter(metrics.getConnectStart(), startTime); + assertNotNull(metrics.getConnectEnd()); + assertAfter(endTime, metrics.getConnectEnd()); + if (isSsl) { + assertNotNull(metrics.getSslStart()); + assertAfter(metrics.getSslStart(), startTime); + assertNotNull(metrics.getSslEnd()); + assertAfter(endTime, metrics.getSslEnd()); + } else { + assertNull(metrics.getSslStart()); + assertNull(metrics.getSslEnd()); + } + } + + /** + * Check that the timing metrics from net::LoadTimingInfo::ConnectTiming don't exist. + */ + public static void checkNoConnectTiming(RequestFinishedInfo.Metrics metrics) { + assertNull(metrics.getDnsStart()); + assertNull(metrics.getDnsEnd()); + assertNull(metrics.getSslStart()); + assertNull(metrics.getSslEnd()); + assertNull(metrics.getConnectStart()); + assertNull(metrics.getConnectEnd()); + } + + /** + * Check that RequestFinishedInfo looks the way it should look for a normal successful request. + */ + public static void checkRequestFinishedInfo( + RequestFinishedInfo info, String url, Date startTime, Date endTime) { + assertNotNull("RequestFinishedInfo.Listener must be called", info); + assertEquals(url, info.getUrl()); + assertNotNull(info.getResponseInfo()); + assertNull(info.getException()); + RequestFinishedInfo.Metrics metrics = info.getMetrics(); + assertNotNull("RequestFinishedInfo.getMetrics() must not be null", metrics); + // Check old (deprecated) timing metrics + assertTrue(metrics.getTotalTimeMs() >= 0); + assertTrue(metrics.getTotalTimeMs() >= metrics.getTtfbMs()); + // Check new timing metrics + checkTimingMetrics(metrics, startTime, endTime); + assertNull(metrics.getPushStart()); + assertNull(metrics.getPushEnd()); + // Check data use metrics + assertTrue(metrics.getSentByteCount() > 0); + assertTrue(metrics.getReceivedByteCount() > 0); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/NQETest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/NQETest.java new file mode 100644 index 0000000000..6637b106fd --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/NQETest.java @@ -0,0 +1,428 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.net.CronetTestRule.getContext; +import static org.chromium.net.CronetTestRule.getTestStorage; + +import android.os.StrictMode; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.Log; +import org.chromium.base.test.util.DisabledTest; +import org.chromium.base.test.util.Feature; +import org.chromium.base.test.util.MetricsUtils.HistogramDelta; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.MetricsTestUtil.TestExecutor; +import org.chromium.net.test.EmbeddedTestServer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * Test Network Quality Estimator. + */ +@RunWith(AndroidJUnit4.class) +public class NQETest { + private static final String TAG = NQETest.class.getSimpleName(); + + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private EmbeddedTestServer mTestServer; + private String mUrl; + + // Thread on which network quality listeners should be notified. + private Thread mNetworkQualityThread; + + @Before + public void setUp() throws Exception { + mTestServer = EmbeddedTestServer.createAndStartServer(getContext()); + mUrl = mTestServer.getURL("/echo?status=200"); + } + + @After + public void tearDown() throws Exception { + mTestServer.stopAndDestroyServer(); + } + + private class ExecutorThreadFactory implements ThreadFactory { + @Override + public Thread newThread(final Runnable r) { + mNetworkQualityThread = new Thread(new Runnable() { + @Override + public void run() { + StrictMode.ThreadPolicy threadPolicy = StrictMode.getThreadPolicy(); + try { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectNetwork() + .penaltyLog() + .penaltyDeath() + .build()); + r.run(); + } finally { + StrictMode.setThreadPolicy(threadPolicy); + } + } + }); + return mNetworkQualityThread; + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testNotEnabled() throws Exception { + ExperimentalCronetEngine.Builder cronetEngineBuilder = + new ExperimentalCronetEngine.Builder(getContext()); + final ExperimentalCronetEngine cronetEngine = cronetEngineBuilder.build(); + Executor networkQualityExecutor = Executors.newSingleThreadExecutor(); + TestNetworkQualityRttListener rttListener = + new TestNetworkQualityRttListener(networkQualityExecutor); + TestNetworkQualityThroughputListener throughputListener = + new TestNetworkQualityThroughputListener(networkQualityExecutor); + try { + cronetEngine.addRttListener(rttListener); + fail("Should throw an exception."); + } catch (IllegalStateException e) { + } + try { + cronetEngine.addThroughputListener(throughputListener); + fail("Should throw an exception."); + } catch (IllegalStateException e) { + } + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + + urlRequest.start(); + callback.blockForDone(); + assertEquals(0, rttListener.rttObservationCount()); + assertEquals(0, throughputListener.throughputObservationCount()); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testListenerRemoved() throws Exception { + ExperimentalCronetEngine.Builder cronetEngineBuilder = + new ExperimentalCronetEngine.Builder(getContext()); + TestExecutor networkQualityExecutor = new TestExecutor(); + TestNetworkQualityRttListener rttListener = + new TestNetworkQualityRttListener(networkQualityExecutor); + cronetEngineBuilder.enableNetworkQualityEstimator(true); + final ExperimentalCronetEngine cronetEngine = cronetEngineBuilder.build(); + cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, false); + + cronetEngine.addRttListener(rttListener); + cronetEngine.removeRttListener(rttListener); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + networkQualityExecutor.runAllTasks(); + assertEquals(0, rttListener.rttObservationCount()); + cronetEngine.shutdown(); + } + + // Returns whether a file contains a particular string. + private boolean prefsFileContainsString(String content) throws IOException { + File file = new File(getTestStorage(getContext()) + "/prefs/local_prefs.json"); + FileInputStream fileInputStream = new FileInputStream(file); + byte[] data = new byte[(int) file.length()]; + fileInputStream.read(data); + fileInputStream.close(); + return new String(data, "UTF-8").contains(content); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + @DisabledTest(message = "crbug.com/796260") + public void testQuicDisabled() throws Exception { + ExperimentalCronetEngine.Builder cronetEngineBuilder = + new ExperimentalCronetEngine.Builder(getContext()); + assertTrue(RttThroughputValues.INVALID_RTT_THROUGHPUT < 0); + Executor listenersExecutor = Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); + TestNetworkQualityRttListener rttListener = + new TestNetworkQualityRttListener(listenersExecutor); + TestNetworkQualityThroughputListener throughputListener = + new TestNetworkQualityThroughputListener(listenersExecutor); + cronetEngineBuilder.enableNetworkQualityEstimator(true).enableHttp2(true).enableQuic(false); + + // The pref may not be written if the computed Effective Connection Type (ECT) matches the + // default ECT for the current connection type. Force the ECT to "Slow-2G". Since "Slow-2G" + // is not the default ECT for any connection type, this ensures that the pref is written to. + JSONObject nqeOptions = new JSONObject().put("force_effective_connection_type", "Slow-2G"); + JSONObject experimentalOptions = + new JSONObject().put("NetworkQualityEstimator", nqeOptions); + + cronetEngineBuilder.setExperimentalOptions(experimentalOptions.toString()); + + cronetEngineBuilder.setStoragePath(getTestStorage(getContext())); + final ExperimentalCronetEngine cronetEngine = cronetEngineBuilder.build(); + cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, true); + + cronetEngine.addRttListener(rttListener); + cronetEngine.addThroughputListener(throughputListener); + + HistogramDelta writeCountHistogram = new HistogramDelta("NQE.Prefs.WriteCount", 1); + assertEquals(0, writeCountHistogram.getDelta()); // Sanity check. + + HistogramDelta readCountHistogram = new HistogramDelta("NQE.Prefs.ReadCount", 1); + assertEquals(0, readCountHistogram.getDelta()); // Sanity check. + + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + + // Throughput observation is posted to the network quality estimator on the network thread + // after the UrlRequest is completed. The observations are then eventually posted to + // throughput listeners on the executor provided to network quality. + throughputListener.waitUntilFirstThroughputObservationReceived(); + + // Wait for RTT observation (at the URL request layer) to be posted. + rttListener.waitUntilFirstUrlRequestRTTReceived(); + + assertTrue(throughputListener.throughputObservationCount() > 0); + + // Prefs must be read at startup. + assertTrue(readCountHistogram.getDelta() > 0); + + // Check RTT observation count after throughput observation has been received. This ensures + // that executor has finished posting the RTT observation to the RTT listeners. + assertTrue(rttListener.rttObservationCount() > 0); + + // NETWORK_QUALITY_OBSERVATION_SOURCE_URL_REQUEST + assertTrue(rttListener.rttObservationCount(0) > 0); + + // NETWORK_QUALITY_OBSERVATION_SOURCE_TCP + assertTrue(rttListener.rttObservationCount(1) > 0); + + // NETWORK_QUALITY_OBSERVATION_SOURCE_QUIC + assertEquals(0, rttListener.rttObservationCount(2)); + + // Verify that the listeners were notified on the expected thread. + assertEquals(mNetworkQualityThread, rttListener.getThread()); + assertEquals(mNetworkQualityThread, throughputListener.getThread()); + + // Verify that effective connection type callback is received and + // effective connection type is correctly set. + assertTrue( + cronetEngine.getEffectiveConnectionType() != EffectiveConnectionType.TYPE_UNKNOWN); + + // Verify that the HTTP RTT, transport RTT and downstream throughput + // estimates are available. + assertTrue(cronetEngine.getHttpRttMs() >= 0); + assertTrue(cronetEngine.getTransportRttMs() >= 0); + assertTrue(cronetEngine.getDownstreamThroughputKbps() >= 0); + + // Verify that the cached estimates were written to the prefs. + while (true) { + Log.i(TAG, "Still waiting for pref file update....."); + Thread.sleep(12000); + try { + if (prefsFileContainsString("network_qualities")) { + break; + } + } catch (FileNotFoundException e) { + // Ignored this exception since the file will only be created when updates are + // flushed to the disk. + } + } + assertTrue(prefsFileContainsString("network_qualities")); + + cronetEngine.shutdown(); + assertTrue(writeCountHistogram.getDelta() > 0); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + public void testPrefsWriteRead() throws Exception { + // When the loop is run for the first time, network quality is written to the disk. The + // test verifies that in the next loop, the network quality is read back. + for (int i = 0; i <= 1; ++i) { + ExperimentalCronetEngine.Builder cronetEngineBuilder = + new ExperimentalCronetEngine.Builder(getContext()); + assertTrue(RttThroughputValues.INVALID_RTT_THROUGHPUT < 0); + Executor listenersExecutor = + Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); + TestNetworkQualityRttListener rttListener = + new TestNetworkQualityRttListener(listenersExecutor); + cronetEngineBuilder.enableNetworkQualityEstimator(true).enableHttp2(true).enableQuic( + false); + + // The pref may not be written if the computed Effective Connection Type (ECT) matches + // the default ECT for the current connection type. Force the ECT to "Slow-2G". Since + // "Slow-2G" is not the default ECT for any connection type, this ensures that the pref + // is written to. + JSONObject nqeOptions = + new JSONObject().put("force_effective_connection_type", "Slow-2G"); + JSONObject experimentalOptions = + new JSONObject().put("NetworkQualityEstimator", nqeOptions); + + cronetEngineBuilder.setExperimentalOptions(experimentalOptions.toString()); + + cronetEngineBuilder.setStoragePath(getTestStorage(getContext())); + + final ExperimentalCronetEngine cronetEngine = cronetEngineBuilder.build(); + cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, true); + cronetEngine.addRttListener(rttListener); + + HistogramDelta writeCountHistogram = new HistogramDelta("NQE.Prefs.WriteCount", 1); + assertEquals(0, writeCountHistogram.getDelta()); // Sanity check. + + HistogramDelta readCountHistogram = new HistogramDelta("NQE.Prefs.ReadCount", 1); + assertEquals(0, readCountHistogram.getDelta()); // Sanity check. + + HistogramDelta readPrefsSizeHistogram = new HistogramDelta("NQE.Prefs.ReadSize", 1); + assertEquals(0, readPrefsSizeHistogram.getDelta()); // Sanity check. + + // NETWORK_QUALITY_OBSERVATION_SOURCE_HTTP_CACHED_ESTIMATE: 3 + HistogramDelta cachedRttHistogram = new HistogramDelta("NQE.RTT.ObservationSource", 3); + assertEquals(0, cachedRttHistogram.getDelta()); // Sanity check. + + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + + // Wait for RTT observation (at the URL request layer) to be posted. + rttListener.waitUntilFirstUrlRequestRTTReceived(); + + // Prefs must be read at startup. + assertTrue(readCountHistogram.getDelta() > 0); + + // Check RTT observation count after throughput observation has been received. This + // ensures + // that executor has finished posting the RTT observation to the RTT listeners. + assertTrue(rttListener.rttObservationCount() > 0); + + // Verify that effective connection type callback is received and + // effective connection type is correctly set. + assertTrue(cronetEngine.getEffectiveConnectionType() + != EffectiveConnectionType.TYPE_UNKNOWN); + + cronetEngine.shutdown(); + + if (i == 0) { + // Verify that the cached estimates were written to the prefs. + assertTrue(prefsFileContainsString("network_qualities")); + } + + // Stored network quality in the pref should be read in the second iteration. + assertEquals(readPrefsSizeHistogram.getDelta() > 0, i > 0); + if (i > 0) { + assertTrue(cachedRttHistogram.getDelta() > 0); + } + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + @DisabledTest(message = "crbug.com/796260") + public void testQuicDisabledWithParams() throws Exception { + ExperimentalCronetEngine.Builder cronetEngineBuilder = + new ExperimentalCronetEngine.Builder(getContext()); + Executor listenersExecutor = Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); + TestNetworkQualityRttListener rttListener = + new TestNetworkQualityRttListener(listenersExecutor); + TestNetworkQualityThroughputListener throughputListener = + new TestNetworkQualityThroughputListener(listenersExecutor); + + // Force the effective connection type to "2G". + JSONObject nqeOptions = new JSONObject().put("force_effective_connection_type", "Slow-2G"); + // Add one more extra param two times to ensure robustness. + nqeOptions.put("some_other_param_1", "value1"); + nqeOptions.put("some_other_param_2", "value2"); + JSONObject experimentalOptions = + new JSONObject().put("NetworkQualityEstimator", nqeOptions); + experimentalOptions.put("SomeOtherFieldTrialName", new JSONObject()); + + cronetEngineBuilder.enableNetworkQualityEstimator(true).enableHttp2(true).enableQuic(false); + cronetEngineBuilder.setExperimentalOptions(experimentalOptions.toString()); + final ExperimentalCronetEngine cronetEngine = cronetEngineBuilder.build(); + cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, false); + + cronetEngine.addRttListener(rttListener); + cronetEngine.addThroughputListener(throughputListener); + + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = + cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + + // Throughput observation is posted to the network quality estimator on the network thread + // after the UrlRequest is completed. The observations are then eventually posted to + // throughput listeners on the executor provided to network quality. + throughputListener.waitUntilFirstThroughputObservationReceived(); + + // Wait for RTT observation (at the URL request layer) to be posted. + rttListener.waitUntilFirstUrlRequestRTTReceived(); + + assertTrue(throughputListener.throughputObservationCount() > 0); + + // Check RTT observation count after throughput observation has been received. This ensures + // that executor has finished posting the RTT observation to the RTT listeners. + assertTrue(rttListener.rttObservationCount() > 0); + + // NETWORK_QUALITY_OBSERVATION_SOURCE_URL_REQUEST + assertTrue(rttListener.rttObservationCount(0) > 0); + + // NETWORK_QUALITY_OBSERVATION_SOURCE_TCP + assertTrue(rttListener.rttObservationCount(1) > 0); + + // NETWORK_QUALITY_OBSERVATION_SOURCE_QUIC + assertEquals(0, rttListener.rttObservationCount(2)); + + // Verify that the listeners were notified on the expected thread. + assertEquals(mNetworkQualityThread, rttListener.getThread()); + assertEquals(mNetworkQualityThread, throughputListener.getThread()); + + // Verify that effective connection type callback is received and effective connection type + // is correctly set to the forced value. This also verifies that the configuration params + // from Cronet embedders were correctly read by NetworkQualityEstimator. + assertEquals( + EffectiveConnectionType.TYPE_SLOW_2G, cronetEngine.getEffectiveConnectionType()); + + cronetEngine.shutdown(); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/NetworkChangeNotifierTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/NetworkChangeNotifierTest.java new file mode 100644 index 0000000000..ea6fabfad3 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/NetworkChangeNotifierTest.java @@ -0,0 +1,114 @@ +// Copyright 2014 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 static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.SOCK_STREAM; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import static org.chromium.net.CronetTestRule.assertContains; + +import android.os.Build; +import android.support.test.runner.AndroidJUnit4; +import android.system.Os; + +import androidx.test.filters.SmallTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.CronetTestFramework; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.impl.CronetLibraryLoader; + +import java.io.FileDescriptor; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Test NetworkChangeNotifier. + */ +@RunWith(AndroidJUnit4.class) +public class NetworkChangeNotifierTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + /** + * Verify NetworkChangeNotifier signals trigger appropriate action, like + * aborting pending connect() jobs. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testNetworkChangeNotifier() throws Exception { + CronetTestFramework testFramework = mTestRule.startCronetTestFramework(); + assertNotNull(testFramework); + + // Os and OsConstants aren't exposed until Lollipop so we cannot run this test. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + + // Bind a listening socket to a local port. The socket won't be used to accept any + // connections, but rather to get connection stuck waiting to connect. + FileDescriptor s = Os.socket(AF_INET6, SOCK_STREAM, 0); + // Bind to 127.0.0.1 and a random port (indicated by special 0 value). + Os.bind(s, InetAddress.getByAddress(null, new byte[] {127, 0, 0, 1}), 0); + // Set backlog to 0 so connections end up stuck waiting to connect(). + Os.listen(s, 0); + + // Make URL pointing at this local port, where requests will get stuck connecting. + String url = "https://127.0.0.1:" + ((InetSocketAddress) Os.getsockname(s)).getPort(); + + // Launch a few requests at this local port. Four seems to be the magic number where + // that request and any further request get stuck connecting. + TestUrlRequestCallback callback = null; + UrlRequest request = null; + for (int i = 0; i < 4; i++) { + callback = new TestUrlRequestCallback(); + request = testFramework.mCronetEngine + .newUrlRequestBuilder(url, callback, callback.getExecutor()) + .build(); + request.start(); + } + + // Wait for request to get to connecting stage + final AtomicBoolean requestConnecting = new AtomicBoolean(); + while (!requestConnecting.get()) { + request.getStatus(new UrlRequest.StatusListener() { + @Override + public void onStatus(int status) { + requestConnecting.set(status == UrlRequest.Status.CONNECTING); + } + }); + Thread.sleep(100); + } + + // Simulate network change which should abort connect jobs + CronetLibraryLoader.postToInitThread(new Runnable() { + @Override + public void run() { + NetworkChangeNotifier.getInstance().notifyObserversOfConnectionTypeChange( + ConnectionType.CONNECTION_4G); + } + }); + + // Wait for ERR_NETWORK_CHANGED + callback.blockForDone(); + assertNotNull(callback.mError); + assertTrue(callback.mOnErrorCalled); + assertEquals(NetError.ERR_NETWORK_CHANGED, + ((NetworkException) callback.mError).getCronetInternalErrorCode()); + assertContains("Exception in CronetUrlRequest: net::ERR_NETWORK_CHANGED", + callback.mError.getMessage()); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/NetworkErrorLoggingTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/NetworkErrorLoggingTest.java new file mode 100644 index 0000000000..771d012224 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/NetworkErrorLoggingTest.java @@ -0,0 +1,180 @@ +// Copyright 2018 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import static org.chromium.net.CronetTestRule.SERVER_CERT_PEM; +import static org.chromium.net.CronetTestRule.SERVER_KEY_PKCS8_PEM; +import static org.chromium.net.CronetTestRule.getContext; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; + +/** + * Tests requests that generate Network Error Logging reports. + */ +@RunWith(AndroidJUnit4.class) +public class NetworkErrorLoggingTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private CronetEngine mCronetEngine; + + @Before + public void setUp() throws Exception { + TestFilesInstaller.installIfNeeded(getContext()); + assertTrue(Http2TestServer.startHttp2TestServer( + getContext(), SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM)); + } + + @After + public void tearDown() throws Exception { + assertTrue(Http2TestServer.shutdownHttp2TestServer()); + if (mCronetEngine != null) { + mCronetEngine.shutdown(); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testManualReportUpload() throws Exception { + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + CronetTestUtil.setMockCertVerifierForTesting( + builder, QuicTestServer.createMockCertVerifier()); + mCronetEngine = builder.build(); + String url = Http2TestServer.getReportingCollectorUrl(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder requestBuilder = + mCronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + TestUploadDataProvider dataProvider = new TestUploadDataProvider( + TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor()); + dataProvider.addRead("[{\"type\": \"test_report\"}]".getBytes()); + requestBuilder.setUploadDataProvider(dataProvider, callback.getExecutor()); + requestBuilder.addHeader("Content-Type", "application/reports+json"); + requestBuilder.build().start(); + callback.blockForDone(); + dataProvider.assertClosed(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertTrue(Http2TestServer.getReportingCollector().containsReport( + "{\"type\": \"test_report\"}")); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testUploadNELReportsFromHeaders() throws Exception { + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + builder.setExperimentalOptions("{\"NetworkErrorLogging\": {\"enable\": true}}"); + CronetTestUtil.setMockCertVerifierForTesting( + builder, QuicTestServer.createMockCertVerifier()); + mCronetEngine = builder.build(); + String url = Http2TestServer.getSuccessWithNELHeadersUrl(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder requestBuilder = + mCronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + requestBuilder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + Http2TestServer.getReportingCollector().waitForReports(1); + assertTrue(Http2TestServer.getReportingCollector().containsReport("" + + "{" + + " \"type\": \"network-error\"," + + " \"url\": \"" + url + "\"," + + " \"body\": {" + + " \"method\": \"GET\"," + + " \"phase\": \"application\"," + + " \"protocol\": \"h2\"," + + " \"referrer\": \"\"," + + " \"sampling_fraction\": 1.0," + + " \"server_ip\": \"127.0.0.1\"," + + " \"status_code\": 200," + + " \"type\": \"ok\"" + + " }" + + "}")); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testUploadNELReportsFromPreloadedPolicy() throws Exception { + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + String serverOrigin = Http2TestServer.getServerUrl(); + String collectorUrl = Http2TestServer.getReportingCollectorUrl(); + builder.setExperimentalOptions("" + + "{\"NetworkErrorLogging\": {" + + " \"enable\": true," + + " \"preloaded_report_to_headers\": [" + + " {" + + " \"origin\": \"" + serverOrigin + "\"," + + " \"value\": {" + + " \"group\": \"nel\"," + + " \"max_age\": 86400," + + " \"endpoints\": [" + + " {\"url\": \"" + collectorUrl + "\"}" + + " ]" + + " }" + + " }" + + " ]," + + " \"preloaded_nel_headers\": [" + + " {" + + " \"origin\": \"" + serverOrigin + "\"," + + " \"value\": {" + + " \"report_to\": \"nel\"," + + " \"max_age\": 86400," + + " \"success_fraction\": 1.0" + + " }" + + " }" + + " ]" + + "}}"); + CronetTestUtil.setMockCertVerifierForTesting( + builder, QuicTestServer.createMockCertVerifier()); + mCronetEngine = builder.build(); + String url = Http2TestServer.getEchoMethodUrl(); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder requestBuilder = + mCronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor()); + requestBuilder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + Http2TestServer.getReportingCollector().waitForReports(1); + // Note that because we don't know in advance what the server IP address is for preloaded + // origins, we'll always get a "downgraded" dns.address_changed NEL report if we don't + // receive a replacement NEL policy with the request. + assertTrue(Http2TestServer.getReportingCollector().containsReport("" + + "{" + + " \"type\": \"network-error\"," + + " \"url\": \"" + url + "\"," + + " \"body\": {" + + " \"method\": \"GET\"," + + " \"phase\": \"dns\"," + + " \"protocol\": \"h2\"," + + " \"referrer\": \"\"," + + " \"sampling_fraction\": 1.0," + + " \"server_ip\": \"127.0.0.1\"," + + " \"status_code\": 0," + + " \"type\": \"dns.address_changed\"" + + " }" + + "}")); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java new file mode 100644 index 0000000000..230059b47d --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java @@ -0,0 +1,523 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.net.CronetTestRule.SERVER_CERT_PEM; +import static org.chromium.net.CronetTestRule.SERVER_KEY_PKCS8_PEM; +import static org.chromium.net.CronetTestRule.getContext; +import static org.chromium.net.CronetTestRule.getTestStorage; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.test.util.CertTestUtil; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +/** + * Public-Key-Pinning tests of Cronet Java API. + */ +@RunWith(AndroidJUnit4.class) +public class PkpTest { + private static final int DISTANT_FUTURE = Integer.MAX_VALUE; + private static final boolean INCLUDE_SUBDOMAINS = true; + private static final boolean EXCLUDE_SUBDOMAINS = false; + private static final boolean KNOWN_ROOT = true; + private static final boolean UNKNOWN_ROOT = false; + private static final boolean ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS = true; + private static final boolean DISABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS = false; + + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private CronetEngine mCronetEngine; + private ExperimentalCronetEngine.Builder mBuilder; + private TestUrlRequestCallback mListener; + private String mServerUrl; // https://test.example.com:8443 + private String mServerHost; // test.example.com + private String mDomain; // example.com + + @Before + public void setUp() throws Exception { + if (mTestRule.testingJavaImpl()) { + return; + } + // Start HTTP2 Test Server + System.loadLibrary("cronet_tests"); + assertTrue(Http2TestServer.startHttp2TestServer( + getContext(), SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM)); + mServerHost = "test.example.com"; + mServerUrl = "https://" + mServerHost + ":" + Http2TestServer.getServerPort(); + mDomain = mServerHost.substring(mServerHost.indexOf('.') + 1, mServerHost.length()); + } + + @After + public void tearDown() throws Exception { + Http2TestServer.shutdownHttp2TestServer(); + shutdownCronetEngine(); + } + + /** + * Tests the case when the pin hash does not match. The client is expected to + * receive the error response. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testErrorCodeIfPinDoesNotMatch() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + byte[] nonMatchingHash = generateSomeSha256(); + addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, DISTANT_FUTURE); + startCronetEngine(); + sendRequestAndWaitForResult(); + + assertErrorResponse(); + } + + /** + * Tests the case when the pin hash matches. The client is expected to + * receive the successful response with the response code 200. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSuccessIfPinMatches() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + // Get PKP hash of the real certificate + X509Certificate cert = readCertFromFileInPemFormat(SERVER_CERT_PEM); + byte[] matchingHash = CertTestUtil.getPublicKeySha256(cert); + + addPkpSha256(mServerHost, matchingHash, EXCLUDE_SUBDOMAINS, DISTANT_FUTURE); + startCronetEngine(); + sendRequestAndWaitForResult(); + + assertSuccessfulResponse(); + } + + /** + * Tests the case when the pin hash does not match and the client accesses the subdomain of + * the configured PKP host with includeSubdomains flag set to true. The client is + * expected to receive the error response. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testIncludeSubdomainsFlagEqualTrue() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + byte[] nonMatchingHash = generateSomeSha256(); + addPkpSha256(mDomain, nonMatchingHash, INCLUDE_SUBDOMAINS, DISTANT_FUTURE); + startCronetEngine(); + sendRequestAndWaitForResult(); + + assertErrorResponse(); + } + + /** + * Tests the case when the pin hash does not match and the client accesses the subdomain of + * the configured PKP host with includeSubdomains flag set to false. The client is expected to + * receive the successful response with the response code 200. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testIncludeSubdomainsFlagEqualFalse() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + byte[] nonMatchingHash = generateSomeSha256(); + addPkpSha256(mDomain, nonMatchingHash, EXCLUDE_SUBDOMAINS, DISTANT_FUTURE); + startCronetEngine(); + sendRequestAndWaitForResult(); + + assertSuccessfulResponse(); + } + + /** + * Tests the case when the mismatching pin is set for some host that is different from the one + * the client wants to access. In that case the other host pinning policy should not be applied + * and the client is expected to receive the successful response with the response code 200. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSuccessIfNoPinSpecified() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + byte[] nonMatchingHash = generateSomeSha256(); + addPkpSha256("otherhost.com", nonMatchingHash, INCLUDE_SUBDOMAINS, DISTANT_FUTURE); + startCronetEngine(); + sendRequestAndWaitForResult(); + + assertSuccessfulResponse(); + } + + /** + * Tests mismatching pins that will expire in 10 seconds. The pins should be still valid and + * enforced during the request; thus returning PIN mismatch error. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testSoonExpiringPin() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + final int tenSecondsAhead = 10; + byte[] nonMatchingHash = generateSomeSha256(); + addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, tenSecondsAhead); + startCronetEngine(); + sendRequestAndWaitForResult(); + + assertErrorResponse(); + } + + /** + * Tests mismatching pins that expired 1 second ago. Since the pins have expired, they + * should not be enforced during the request; thus a successful response is expected. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testRecentlyExpiredPin() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + final int oneSecondAgo = -1; + byte[] nonMatchingHash = generateSomeSha256(); + addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, oneSecondAgo); + startCronetEngine(); + sendRequestAndWaitForResult(); + + assertSuccessfulResponse(); + } + + /** + * Tests that the pinning of local trust anchors is enforced when pinning bypass for local + * trust anchors is disabled. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testLocalTrustAnchorPinningEnforced() throws Exception { + createCronetEngineBuilder(DISABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, UNKNOWN_ROOT); + byte[] nonMatchingHash = generateSomeSha256(); + addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, DISTANT_FUTURE); + startCronetEngine(); + sendRequestAndWaitForResult(); + + assertErrorResponse(); + shutdownCronetEngine(); + } + + /** + * Tests that the pinning of local trust anchors is not enforced when pinning bypass for local + * trust anchors is enabled. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testLocalTrustAnchorPinningNotEnforced() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, UNKNOWN_ROOT); + byte[] nonMatchingHash = generateSomeSha256(); + addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, DISTANT_FUTURE); + startCronetEngine(); + sendRequestAndWaitForResult(); + + assertSuccessfulResponse(); + shutdownCronetEngine(); + } + + /** + * Tests that host pinning is not persisted between multiple CronetEngine instances. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testPinsAreNotPersisted() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + byte[] nonMatchingHash = generateSomeSha256(); + addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, DISTANT_FUTURE); + startCronetEngine(); + sendRequestAndWaitForResult(); + assertErrorResponse(); + shutdownCronetEngine(); + + // Restart Cronet engine and try the same request again. Since the pins are not persisted, + // a successful response is expected. + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + startCronetEngine(); + sendRequestAndWaitForResult(); + assertSuccessfulResponse(); + } + + /** + * Tests that the client receives {@code InvalidArgumentException} when the pinned host name + * is invalid. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testHostNameArgumentValidation() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + final String label63 = "123456789-123456789-123456789-123456789-123456789-123456789-123"; + final String host255 = label63 + "." + label63 + "." + label63 + "." + label63; + // Valid host names. + assertNoExceptionWhenHostNameIsValid("domain.com"); + assertNoExceptionWhenHostNameIsValid("my-domain.com"); + assertNoExceptionWhenHostNameIsValid("section4.domain.info"); + assertNoExceptionWhenHostNameIsValid("44.domain44.info"); + assertNoExceptionWhenHostNameIsValid("very.long.long.long.long.long.long.long.domain.com"); + assertNoExceptionWhenHostNameIsValid("host"); + assertNoExceptionWhenHostNameIsValid("новости.ру"); + assertNoExceptionWhenHostNameIsValid("самые-последние.новости.рус"); + assertNoExceptionWhenHostNameIsValid("最新消息.中国"); + // Checks max size of the host label (63 characters) + assertNoExceptionWhenHostNameIsValid(label63 + ".com"); + // Checks max size of the host name (255 characters) + assertNoExceptionWhenHostNameIsValid(host255); + assertNoExceptionWhenHostNameIsValid("127.0.0.z"); + + // Invalid host names. + assertExceptionWhenHostNameIsInvalid("domain.com:300"); + assertExceptionWhenHostNameIsInvalid("-domain.com"); + assertExceptionWhenHostNameIsInvalid("domain-.com"); + assertExceptionWhenHostNameIsInvalid("http://domain.com"); + assertExceptionWhenHostNameIsInvalid("domain.com:"); + assertExceptionWhenHostNameIsInvalid("domain.com/"); + assertExceptionWhenHostNameIsInvalid("новости.ру:"); + assertExceptionWhenHostNameIsInvalid("новости.ру/"); + assertExceptionWhenHostNameIsInvalid("_http.sctp.www.example.com"); + assertExceptionWhenHostNameIsInvalid("http.sctp._www.example.com"); + // Checks a host that exceeds max allowed length of the host label (63 characters) + assertExceptionWhenHostNameIsInvalid(label63 + "4.com"); + // Checks a host that exceeds max allowed length of hostname (255 characters) + assertExceptionWhenHostNameIsInvalid(host255.substring(3) + ".com"); + assertExceptionWhenHostNameIsInvalid("FE80:0000:0000:0000:0202:B3FF:FE1E:8329"); + assertExceptionWhenHostNameIsInvalid("[2001:db8:0:1]:80"); + + // Invalid host names for PKP that contain IPv4 addresses + // or names with digits and dots only. + assertExceptionWhenHostNameIsInvalid("127.0.0.1"); + assertExceptionWhenHostNameIsInvalid("68.44.222.12"); + assertExceptionWhenHostNameIsInvalid("256.0.0.1"); + assertExceptionWhenHostNameIsInvalid("127.0.0.1.1"); + assertExceptionWhenHostNameIsInvalid("127.0.0"); + assertExceptionWhenHostNameIsInvalid("127.0.0."); + assertExceptionWhenHostNameIsInvalid("127.0.0.299"); + } + + /** + * Tests that NullPointerException is thrown if the host name or the collection of pins or + * the expiration date is null. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testNullArguments() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + verifyExceptionWhenAddPkpArgumentIsNull(true, false, false); + verifyExceptionWhenAddPkpArgumentIsNull(false, true, false); + verifyExceptionWhenAddPkpArgumentIsNull(false, false, true); + verifyExceptionWhenAddPkpArgumentIsNull(false, false, false); + } + + /** + * Tests that IllegalArgumentException is thrown if SHA1 is passed as the value of a pin. + * + * @throws Exception + */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testIllegalArgumentExceptionWhenPinValueIsSHA1() throws Exception { + createCronetEngineBuilder(ENABLE_PINNING_BYPASS_FOR_LOCAL_ANCHORS, KNOWN_ROOT); + byte[] sha1 = new byte[20]; + try { + addPkpSha256(mServerHost, sha1, EXCLUDE_SUBDOMAINS, DISTANT_FUTURE); + } catch (IllegalArgumentException ex) { + // Expected exception + return; + } + fail("Expected IllegalArgumentException with pin value: " + Arrays.toString(sha1)); + } + + /** + * Asserts that the response from the server contains an PKP error. + */ + private void assertErrorResponse() { + assertNotNull("Expected an error", mListener.mError); + int errorCode = ((NetworkException) mListener.mError).getCronetInternalErrorCode(); + Set expectedErrors = new HashSet<>(); + expectedErrors.add(NetError.ERR_CONNECTION_REFUSED); + expectedErrors.add(NetError.ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN); + assertTrue(String.format("Incorrect error code. Expected one of %s but received %s", + expectedErrors, errorCode), + expectedErrors.contains(errorCode)); + } + + /** + * Asserts a successful response with response code 200. + */ + private void assertSuccessfulResponse() { + if (mListener.mError != null) { + fail("Did not expect an error but got error code " + + ((NetworkException) mListener.mError).getCronetInternalErrorCode()); + } + assertNotNull("Expected non-null response from the server", mListener.mResponseInfo); + assertEquals(200, mListener.mResponseInfo.getHttpStatusCode()); + } + + private void createCronetEngineBuilder(boolean bypassPinningForLocalAnchors, boolean knownRoot) + throws Exception { + // Set common CronetEngine parameters + mBuilder = new ExperimentalCronetEngine.Builder(getContext()); + mBuilder.enablePublicKeyPinningBypassForLocalTrustAnchors(bypassPinningForLocalAnchors); + JSONObject hostResolverParams = CronetTestUtil.generateHostResolverRules(); + JSONObject experimentalOptions = new JSONObject() + .put("HostResolverRules", hostResolverParams); + mBuilder.setExperimentalOptions(experimentalOptions.toString()); + mBuilder.setStoragePath(getTestStorage(getContext())); + mBuilder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 1000 * 1024); + final String[] server_certs = {SERVER_CERT_PEM}; + CronetTestUtil.setMockCertVerifierForTesting( + mBuilder, MockCertVerifier.createMockCertVerifier(server_certs, knownRoot)); + } + + private void startCronetEngine() { + mCronetEngine = mBuilder.build(); + } + + private void shutdownCronetEngine() { + if (mCronetEngine != null) { + mCronetEngine.shutdown(); + mCronetEngine = null; + } + } + + private byte[] generateSomeSha256() { + byte[] sha256 = new byte[32]; + Arrays.fill(sha256, (byte) 58); + return sha256; + } + + @SuppressWarnings("ArrayAsKeyOfSetOrMap") + private void addPkpSha256( + String host, byte[] pinHashValue, boolean includeSubdomain, int maxAgeInSec) { + Set hashes = new HashSet<>(); + hashes.add(pinHashValue); + mBuilder.addPublicKeyPins(host, hashes, includeSubdomain, dateInFuture(maxAgeInSec)); + } + + private void sendRequestAndWaitForResult() { + mListener = new TestUrlRequestCallback(); + + String httpURL = mServerUrl + "/simple.txt"; + UrlRequest.Builder requestBuilder = + mCronetEngine.newUrlRequestBuilder(httpURL, mListener, mListener.getExecutor()); + requestBuilder.build().start(); + mListener.blockForDone(); + } + + private X509Certificate readCertFromFileInPemFormat(String certFileName) throws Exception { + byte[] certDer = CertTestUtil.pemToDer(CertTestUtil.CERTS_DIRECTORY + certFileName); + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certDer)); + } + + private Date dateInFuture(int secondsIntoFuture) { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.SECOND, secondsIntoFuture); + return cal.getTime(); + } + + private void assertNoExceptionWhenHostNameIsValid(String hostName) { + try { + addPkpSha256(hostName, generateSomeSha256(), INCLUDE_SUBDOMAINS, DISTANT_FUTURE); + } catch (IllegalArgumentException ex) { + fail("Host name " + hostName + " should be valid but the exception was thrown: " + + ex.toString()); + } + } + + private void assertExceptionWhenHostNameIsInvalid(String hostName) { + try { + addPkpSha256(hostName, generateSomeSha256(), INCLUDE_SUBDOMAINS, DISTANT_FUTURE); + } catch (IllegalArgumentException ex) { + // Expected exception. + return; + } + fail("Expected IllegalArgumentException when passing " + hostName + " host name"); + } + + @SuppressWarnings("ArrayAsKeyOfSetOrMap") + private void verifyExceptionWhenAddPkpArgumentIsNull( + boolean hostNameIsNull, boolean pinsAreNull, boolean expirationDataIsNull) { + String hostName = hostNameIsNull ? null : "some-host.com"; + Set pins = pinsAreNull ? null : new HashSet(); + Date expirationDate = expirationDataIsNull ? null : new Date(); + + boolean shouldThrowNpe = hostNameIsNull || pinsAreNull || expirationDataIsNull; + try { + mBuilder.addPublicKeyPins(hostName, pins, INCLUDE_SUBDOMAINS, expirationDate); + } catch (NullPointerException ex) { + if (!shouldThrowNpe) { + fail("Null pointer exception was not expected: " + ex.toString()); + } + return; + } + if (shouldThrowNpe) { + fail("NullPointerException was expected"); + } + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java new file mode 100644 index 0000000000..39d3dddbd3 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/QuicTest.java @@ -0,0 +1,306 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import static org.chromium.net.CronetTestRule.getContext; +import static org.chromium.net.CronetTestRule.getTestStorage; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.LargeTest; +import androidx.test.filters.SmallTest; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.Log; +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.MetricsTestUtil.TestRequestFinishedListener; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Date; +import java.util.concurrent.Executors; + +/** + * Tests making requests using QUIC. + */ +@RunWith(AndroidJUnit4.class) +public class QuicTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private static final String TAG = QuicTest.class.getSimpleName(); + private ExperimentalCronetEngine.Builder mBuilder; + + @Before + public void setUp() throws Exception { + // Load library first, since we need the Quic test server's URL. + System.loadLibrary("cronet_tests"); + QuicTestServer.startQuicTestServer(getContext()); + + mBuilder = new ExperimentalCronetEngine.Builder(getContext()); + mBuilder.enableNetworkQualityEstimator(true).enableQuic(true); + mBuilder.addQuicHint(QuicTestServer.getServerHost(), QuicTestServer.getServerPort(), + QuicTestServer.getServerPort()); + + // The pref may not be written if the computed Effective Connection Type (ECT) matches the + // default ECT for the current connection type. Force the ECT to "Slow-2G". Since "Slow-2G" + // is not the default ECT for any connection type, this ensures that the pref is written to. + JSONObject nqeParams = new JSONObject().put("force_effective_connection_type", "Slow-2G"); + + // TODO(mgersh): Enable connection migration once it works, see http://crbug.com/634910 + JSONObject quicParams = new JSONObject() + .put("connection_options", "PACE,IW10,FOO,DEADBEEF") + .put("max_server_configs_stored_in_properties", 2) + .put("idle_connection_timeout_seconds", 300) + .put("migrate_sessions_on_network_change_v2", false) + .put("migrate_sessions_early_v2", false) + .put("race_cert_verification", true); + JSONObject hostResolverParams = CronetTestUtil.generateHostResolverRules(); + JSONObject experimentalOptions = new JSONObject() + .put("QUIC", quicParams) + .put("HostResolverRules", hostResolverParams) + .put("NetworkQualityEstimator", nqeParams); + mBuilder.setExperimentalOptions(experimentalOptions.toString()); + mBuilder.setStoragePath(getTestStorage(getContext())); + mBuilder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 1000 * 1024); + CronetTestUtil.setMockCertVerifierForTesting( + mBuilder, QuicTestServer.createMockCertVerifier()); + } + + @After + public void tearDown() throws Exception { + QuicTestServer.shutdownQuicTestServer(); + } + + @Test + @LargeTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + public void testQuicLoadUrl() throws Exception { + ExperimentalCronetEngine cronetEngine = mBuilder.build(); + String quicURL = QuicTestServer.getServerURL() + "/simple.txt"; + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + + // Although the native stack races QUIC and SPDY for the first request, + // since there is no http server running on the corresponding TCP port, + // QUIC will always succeed with a 200 (see + // net::HttpStreamFactoryImpl::Request::OnStreamFailed). + UrlRequest.Builder requestBuilder = + cronetEngine.newUrlRequestBuilder(quicURL, callback, callback.getExecutor()); + requestBuilder.build().start(); + callback.blockForDone(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + String expectedContent = "This is a simple text file served by QUIC.\n"; + assertEquals(expectedContent, callback.mResponseAsString); + assertIsQuic(callback.mResponseInfo); + // The total received bytes should be larger than the content length, to account for + // headers. + assertTrue(callback.mResponseInfo.getReceivedByteCount() > expectedContent.length()); + // This test takes a long time, since the update will only be scheduled + // after kUpdatePrefsDelayMs in http_server_properties_manager.cc. + while (true) { + Log.i(TAG, "Still waiting for pref file update....."); + Thread.sleep(10000); + boolean contains = false; + try { + if (fileContainsString("local_prefs.json", "quic")) break; + } catch (FileNotFoundException e) { + // Ignored this exception since the file will only be created when updates are + // flushed to the disk. + } + } + assertTrue(fileContainsString("local_prefs.json", + QuicTestServer.getServerHost() + ":" + QuicTestServer.getServerPort())); + cronetEngine.shutdown(); + + // Make another request using a new context but with no QUIC hints. + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + builder.setStoragePath(getTestStorage(getContext())); + builder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, 1000 * 1024); + builder.enableQuic(true); + JSONObject hostResolverParams = CronetTestUtil.generateHostResolverRules(); + JSONObject experimentalOptions = new JSONObject() + .put("HostResolverRules", hostResolverParams); + builder.setExperimentalOptions(experimentalOptions.toString()); + CronetTestUtil.setMockCertVerifierForTesting( + builder, QuicTestServer.createMockCertVerifier()); + cronetEngine = builder.build(); + TestUrlRequestCallback callback2 = new TestUrlRequestCallback(); + requestBuilder = + cronetEngine.newUrlRequestBuilder(quicURL, callback2, callback2.getExecutor()); + requestBuilder.build().start(); + callback2.blockForDone(); + assertEquals(200, callback2.mResponseInfo.getHttpStatusCode()); + assertEquals(expectedContent, callback2.mResponseAsString); + assertIsQuic(callback.mResponseInfo); + // The total received bytes should be larger than the content length, to account for + // headers. + assertTrue(callback2.mResponseInfo.getReceivedByteCount() > expectedContent.length()); + cronetEngine.shutdown(); + } + + // Returns whether a file contains a particular string. + private boolean fileContainsString(String filename, String content) throws IOException { + File file = new File(getTestStorage(getContext()) + "/prefs/" + filename); + FileInputStream fileInputStream = new FileInputStream(file); + byte[] data = new byte[(int) file.length()]; + fileInputStream.read(data); + fileInputStream.close(); + return new String(data, "UTF-8").contains(content); + } + + /** + * Tests that the network quality listeners are propoerly notified when QUIC is enabled. + */ + @Test + @LargeTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + @SuppressWarnings("deprecation") + public void testNQEWithQuic() throws Exception { + ExperimentalCronetEngine cronetEngine = mBuilder.build(); + String quicURL = QuicTestServer.getServerURL() + "/simple.txt"; + + TestNetworkQualityRttListener rttListener = + new TestNetworkQualityRttListener(Executors.newSingleThreadExecutor()); + TestNetworkQualityThroughputListener throughputListener = + new TestNetworkQualityThroughputListener(Executors.newSingleThreadExecutor()); + + cronetEngine.addRttListener(rttListener); + cronetEngine.addThroughputListener(throughputListener); + + cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, true); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + + // Although the native stack races QUIC and SPDY for the first request, + // since there is no http server running on the corresponding TCP port, + // QUIC will always succeed with a 200 (see + // net::HttpStreamFactoryImpl::Request::OnStreamFailed). + UrlRequest.Builder requestBuilder = + cronetEngine.newUrlRequestBuilder(quicURL, callback, callback.getExecutor()); + requestBuilder.build().start(); + callback.blockForDone(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + String expectedContent = "This is a simple text file served by QUIC.\n"; + assertEquals(expectedContent, callback.mResponseAsString); + assertIsQuic(callback.mResponseInfo); + + // Throughput observation is posted to the network quality estimator on the network thread + // after the UrlRequest is completed. The observations are then eventually posted to + // throughput listeners on the executor provided to network quality. + throughputListener.waitUntilFirstThroughputObservationReceived(); + + // Wait for RTT observation (at the URL request layer) to be posted. + rttListener.waitUntilFirstUrlRequestRTTReceived(); + + assertTrue(throughputListener.throughputObservationCount() > 0); + + // Check RTT observation count after throughput observation has been received. This ensures + // that executor has finished posting the RTT observation to the RTT listeners. + // NETWORK_QUALITY_OBSERVATION_SOURCE_URL_REQUEST + assertTrue(rttListener.rttObservationCount(0) > 0); + + // NETWORK_QUALITY_OBSERVATION_SOURCE_QUIC + assertTrue(rttListener.rttObservationCount(2) > 0); + + // Verify that effective connection type callback is received and + // effective connection type is correctly set. + assertTrue( + cronetEngine.getEffectiveConnectionType() != EffectiveConnectionType.TYPE_UNKNOWN); + + // Verify that the HTTP RTT, transport RTT and downstream throughput + // estimates are available. + assertTrue(cronetEngine.getHttpRttMs() >= 0); + assertTrue(cronetEngine.getTransportRttMs() >= 0); + assertTrue(cronetEngine.getDownstreamThroughputKbps() >= 0); + + // Verify that the cached estimates were written to the prefs. + while (true) { + Log.i(TAG, "Still waiting for pref file update....."); + Thread.sleep(10000); + try { + if (fileContainsString("local_prefs.json", "network_qualities")) { + break; + } + } catch (FileNotFoundException e) { + // Ignored this exception since the file will only be created when updates are + // flushed to the disk. + } + } + assertTrue(fileContainsString("local_prefs.json", "network_qualities")); + cronetEngine.shutdown(); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + public void testMetricsWithQuic() throws Exception { + ExperimentalCronetEngine cronetEngine = mBuilder.build(); + TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); + cronetEngine.addRequestFinishedListener(requestFinishedListener); + + String quicURL = QuicTestServer.getServerURL() + "/simple.txt"; + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + + UrlRequest.Builder requestBuilder = + cronetEngine.newUrlRequestBuilder(quicURL, callback, callback.getExecutor()); + Date startTime = new Date(); + requestBuilder.build().start(); + callback.blockForDone(); + requestFinishedListener.blockUntilDone(); + Date endTime = new Date(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertIsQuic(callback.mResponseInfo); + + RequestFinishedInfo requestInfo = requestFinishedListener.getRequestInfo(); + MetricsTestUtil.checkRequestFinishedInfo(requestInfo, quicURL, startTime, endTime); + assertEquals(RequestFinishedInfo.SUCCEEDED, requestInfo.getFinishedReason()); + MetricsTestUtil.checkHasConnectTiming(requestInfo.getMetrics(), startTime, endTime, true); + + // Second request should use the same connection and not have ConnectTiming numbers + callback = new TestUrlRequestCallback(); + requestFinishedListener.reset(); + requestBuilder = + cronetEngine.newUrlRequestBuilder(quicURL, callback, callback.getExecutor()); + startTime = new Date(); + requestBuilder.build().start(); + callback.blockForDone(); + requestFinishedListener.blockUntilDone(); + endTime = new Date(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertIsQuic(callback.mResponseInfo); + + requestInfo = requestFinishedListener.getRequestInfo(); + MetricsTestUtil.checkRequestFinishedInfo(requestInfo, quicURL, startTime, endTime); + assertEquals(RequestFinishedInfo.SUCCEEDED, requestInfo.getFinishedReason()); + MetricsTestUtil.checkNoConnectTiming(requestInfo.getMetrics()); + + cronetEngine.shutdown(); + } + + // Helper method to assert that the request is negotiated over QUIC. + private void assertIsQuic(UrlResponseInfo responseInfo) { + assertTrue(responseInfo.getNegotiatedProtocol().startsWith("http/2+quic") + || responseInfo.getNegotiatedProtocol().startsWith("h3")); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/RequestFinishedInfoTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/RequestFinishedInfoTest.java new file mode 100644 index 0000000000..1c6003675a --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/RequestFinishedInfoTest.java @@ -0,0 +1,569 @@ +// Copyright 2016 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.base.CollectionUtil.newHashSet; +import static org.chromium.net.CronetTestRule.getContext; + +import android.os.ConditionVariable; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.CronetTestFramework; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.CronetTestRule.RequiresMinApi; +import org.chromium.net.MetricsTestUtil.TestExecutor; +import org.chromium.net.MetricsTestUtil.TestRequestFinishedListener; +import org.chromium.net.impl.CronetMetrics; +import org.chromium.net.test.EmbeddedTestServer; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Test RequestFinishedInfo.Listener and the metrics information it provides. + */ +@RunWith(AndroidJUnit4.class) +public class RequestFinishedInfoTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + CronetTestFramework mTestFramework; + private EmbeddedTestServer mTestServer; + private String mUrl; + + // A subclass of TestRequestFinishedListener to additionally assert that UrlRequest.Callback's + // terminal callbacks have been invoked at the time of onRequestFinished(). + // See crbug.com/710877. + private static class AssertCallbackDoneRequestFinishedListener + extends TestRequestFinishedListener { + private final TestUrlRequestCallback mCallback; + public AssertCallbackDoneRequestFinishedListener(TestUrlRequestCallback callback) { + // Use same executor as request callback to verify stable call order. + super(callback.getExecutor()); + mCallback = callback; + } + + @Override + public void onRequestFinished(RequestFinishedInfo requestInfo) { + assertTrue(mCallback.isDone()); + super.onRequestFinished(requestInfo); + } + }; + + @Before + public void setUp() throws Exception { + mTestServer = EmbeddedTestServer.createAndStartServer(getContext()); + mUrl = mTestServer.getURL("/echo?status=200"); + mTestFramework = mTestRule.startCronetTestFramework(); + } + + @After + public void tearDown() throws Exception { + mTestFramework.mCronetEngine.shutdown(); + mTestServer.stopAndDestroyServer(); + } + + static class DirectExecutor implements Executor { + private ConditionVariable mBlock = new ConditionVariable(); + + @Override + public void execute(Runnable task) { + task.run(); + mBlock.open(); + } + + public void blockUntilDone() { + mBlock.block(); + } + } + + static class ThreadExecutor implements Executor { + private List mThreads = new ArrayList(); + + @Override + public void execute(Runnable task) { + Thread newThread = new Thread(task); + mThreads.add(newThread); + newThread.start(); + } + + public void joinAll() throws InterruptedException { + for (Thread thread : mThreads) { + thread.join(); + } + } + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + @SuppressWarnings("deprecation") + public void testRequestFinishedListener() throws Exception { + TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); + mTestFramework.mCronetEngine.addRequestFinishedListener(requestFinishedListener); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + ExperimentalUrlRequest.Builder urlRequestBuilder = + (ExperimentalUrlRequest.Builder) mTestFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + Date startTime = new Date(); + urlRequestBuilder.addRequestAnnotation("request annotation") + .addRequestAnnotation(this) + .build() + .start(); + callback.blockForDone(); + requestFinishedListener.blockUntilDone(); + Date endTime = new Date(); + + RequestFinishedInfo requestInfo = requestFinishedListener.getRequestInfo(); + MetricsTestUtil.checkRequestFinishedInfo(requestInfo, mUrl, startTime, endTime); + assertEquals(RequestFinishedInfo.SUCCEEDED, requestInfo.getFinishedReason()); + MetricsTestUtil.checkHasConnectTiming(requestInfo.getMetrics(), startTime, endTime, false); + assertEquals(newHashSet("request annotation", this), // Use sets for unordered comparison. + new HashSet(requestInfo.getAnnotations())); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + @SuppressWarnings("deprecation") + public void testRequestFinishedListenerDirectExecutor() throws Exception { + DirectExecutor testExecutor = new DirectExecutor(); + TestRequestFinishedListener requestFinishedListener = + new TestRequestFinishedListener(testExecutor); + mTestFramework.mCronetEngine.addRequestFinishedListener(requestFinishedListener); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + ExperimentalUrlRequest.Builder urlRequestBuilder = + (ExperimentalUrlRequest.Builder) mTestFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + Date startTime = new Date(); + urlRequestBuilder.addRequestAnnotation("request annotation") + .addRequestAnnotation(this) + .build() + .start(); + callback.blockForDone(); + // Block on the executor, not the listener, since blocking on the listener doesn't work when + // it's created with a non-default executor. + testExecutor.blockUntilDone(); + Date endTime = new Date(); + + RequestFinishedInfo requestInfo = requestFinishedListener.getRequestInfo(); + MetricsTestUtil.checkRequestFinishedInfo(requestInfo, mUrl, startTime, endTime); + assertEquals(RequestFinishedInfo.SUCCEEDED, requestInfo.getFinishedReason()); + MetricsTestUtil.checkHasConnectTiming(requestInfo.getMetrics(), startTime, endTime, false); + assertEquals(newHashSet("request annotation", this), // Use sets for unordered comparison. + new HashSet(requestInfo.getAnnotations())); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + @SuppressWarnings("deprecation") + public void testRequestFinishedListenerDifferentThreads() throws Exception { + TestRequestFinishedListener firstListener = new TestRequestFinishedListener(); + TestRequestFinishedListener secondListener = new TestRequestFinishedListener(); + mTestFramework.mCronetEngine.addRequestFinishedListener(firstListener); + mTestFramework.mCronetEngine.addRequestFinishedListener(secondListener); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + ExperimentalUrlRequest.Builder urlRequestBuilder = + (ExperimentalUrlRequest.Builder) mTestFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + Date startTime = new Date(); + urlRequestBuilder.addRequestAnnotation("request annotation") + .addRequestAnnotation(this) + .build() + .start(); + callback.blockForDone(); + firstListener.blockUntilDone(); + secondListener.blockUntilDone(); + Date endTime = new Date(); + + RequestFinishedInfo firstRequestInfo = firstListener.getRequestInfo(); + RequestFinishedInfo secondRequestInfo = secondListener.getRequestInfo(); + + MetricsTestUtil.checkRequestFinishedInfo(firstRequestInfo, mUrl, startTime, endTime); + assertEquals(RequestFinishedInfo.SUCCEEDED, firstRequestInfo.getFinishedReason()); + MetricsTestUtil.checkHasConnectTiming( + firstRequestInfo.getMetrics(), startTime, endTime, false); + + MetricsTestUtil.checkRequestFinishedInfo(secondRequestInfo, mUrl, startTime, endTime); + assertEquals(RequestFinishedInfo.SUCCEEDED, secondRequestInfo.getFinishedReason()); + MetricsTestUtil.checkHasConnectTiming( + secondRequestInfo.getMetrics(), startTime, endTime, false); + + assertEquals(newHashSet("request annotation", this), // Use sets for unordered comparison. + new HashSet(firstRequestInfo.getAnnotations())); + assertEquals(newHashSet("request annotation", this), + new HashSet(secondRequestInfo.getAnnotations())); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + @SuppressWarnings("deprecation") + public void testRequestFinishedListenerFailedRequest() throws Exception { + String connectionRefusedUrl = "http://127.0.0.1:3"; + TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); + mTestFramework.mCronetEngine.addRequestFinishedListener(requestFinishedListener); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + connectionRefusedUrl, callback, callback.getExecutor()); + Date startTime = new Date(); + urlRequestBuilder.build().start(); + callback.blockForDone(); + assertTrue(callback.mOnErrorCalled); + requestFinishedListener.blockUntilDone(); + Date endTime = new Date(); + + RequestFinishedInfo requestInfo = requestFinishedListener.getRequestInfo(); + assertNotNull("RequestFinishedInfo.Listener must be called", requestInfo); + assertEquals(connectionRefusedUrl, requestInfo.getUrl()); + assertTrue(requestInfo.getAnnotations().isEmpty()); + assertEquals(RequestFinishedInfo.FAILED, requestInfo.getFinishedReason()); + assertNotNull(requestInfo.getException()); + assertEquals(NetworkException.ERROR_CONNECTION_REFUSED, + ((NetworkException) requestInfo.getException()).getErrorCode()); + RequestFinishedInfo.Metrics metrics = requestInfo.getMetrics(); + assertNotNull("RequestFinishedInfo.getMetrics() must not be null", metrics); + // The failure is occasionally fast enough that time reported is 0, so just check for null + assertNotNull(metrics.getTotalTimeMs()); + assertNull(metrics.getTtfbMs()); + + // Check the timing metrics + assertNotNull(metrics.getRequestStart()); + MetricsTestUtil.assertAfter(metrics.getRequestStart(), startTime); + MetricsTestUtil.checkNoConnectTiming(metrics); + assertNull(metrics.getSendingStart()); + assertNull(metrics.getSendingEnd()); + assertNull(metrics.getResponseStart()); + assertNotNull(metrics.getRequestEnd()); + MetricsTestUtil.assertAfter(endTime, metrics.getRequestEnd()); + MetricsTestUtil.assertAfter(metrics.getRequestEnd(), metrics.getRequestStart()); + assertTrue(metrics.getSentByteCount() == 0); + assertTrue(metrics.getReceivedByteCount() == 0); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + @SuppressWarnings("deprecation") + public void testRequestFinishedListenerRemoved() throws Exception { + TestExecutor testExecutor = new TestExecutor(); + TestRequestFinishedListener requestFinishedListener = + new TestRequestFinishedListener(testExecutor); + mTestFramework.mCronetEngine.addRequestFinishedListener(requestFinishedListener); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder urlRequestBuilder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + UrlRequest request = urlRequestBuilder.build(); + mTestFramework.mCronetEngine.removeRequestFinishedListener(requestFinishedListener); + request.start(); + callback.blockForDone(); + testExecutor.runAllTasks(); + + assertNull("RequestFinishedInfo.Listener must not be called", + requestFinishedListener.getRequestInfo()); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + public void testRequestFinishedListenerCanceledRequest() throws Exception { + TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); + mTestFramework.mCronetEngine.addRequestFinishedListener(requestFinishedListener); + TestUrlRequestCallback callback = new TestUrlRequestCallback() { + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + super.onResponseStarted(request, info); + request.cancel(); + } + }; + ExperimentalUrlRequest.Builder urlRequestBuilder = + mTestFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + Date startTime = new Date(); + urlRequestBuilder.addRequestAnnotation("request annotation") + .addRequestAnnotation(this) + .build() + .start(); + callback.blockForDone(); + requestFinishedListener.blockUntilDone(); + Date endTime = new Date(); + + RequestFinishedInfo requestInfo = requestFinishedListener.getRequestInfo(); + MetricsTestUtil.checkRequestFinishedInfo(requestInfo, mUrl, startTime, endTime); + assertEquals(RequestFinishedInfo.CANCELED, requestInfo.getFinishedReason()); + MetricsTestUtil.checkHasConnectTiming(requestInfo.getMetrics(), startTime, endTime, false); + + assertEquals(newHashSet("request annotation", this), // Use sets for unordered comparison. + new HashSet(requestInfo.getAnnotations())); + } + + private static class RejectAllTasksExecutor implements Executor { + @Override + public void execute(Runnable task) { + throw new RejectedExecutionException(); + } + } + + // Checks that CronetURLRequestAdapter::DestroyOnNetworkThread() doesn't crash when metrics + // collection is enabled and the URLRequest hasn't been created. See http://crbug.com/675629. + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + public void testExceptionInRequestStart() throws Exception { + // The listener in this test shouldn't get any tasks. + Executor executor = new RejectAllTasksExecutor(); + TestRequestFinishedListener requestFinishedListener = + new TestRequestFinishedListener(executor); + mTestFramework.mCronetEngine.addRequestFinishedListener(requestFinishedListener); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + ExperimentalUrlRequest.Builder urlRequestBuilder = + mTestFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + // Empty headers are invalid and will cause start() to throw an exception. + UrlRequest request = urlRequestBuilder.addHeader("", "").build(); + try { + request.start(); + fail("UrlRequest.start() should throw IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("Invalid header =", e.getMessage()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testMetricsGetters() throws Exception { + long requestStart = 1; + long dnsStart = 2; + long dnsEnd = -1; + long connectStart = 4; + long connectEnd = 5; + long sslStart = 6; + long sslEnd = 7; + long sendingStart = 8; + long sendingEnd = 9; + long pushStart = 10; + long pushEnd = 11; + long responseStart = 12; + long requestEnd = 13; + boolean socketReused = true; + long sentByteCount = 14; + long receivedByteCount = 15; + // Make sure nothing gets reordered inside the Metrics class + RequestFinishedInfo.Metrics metrics = new CronetMetrics(requestStart, dnsStart, dnsEnd, + connectStart, connectEnd, sslStart, sslEnd, sendingStart, sendingEnd, pushStart, + pushEnd, responseStart, requestEnd, socketReused, sentByteCount, receivedByteCount); + assertEquals(new Date(requestStart), metrics.getRequestStart()); + // -1 timestamp should translate to null + assertNull(metrics.getDnsEnd()); + assertEquals(new Date(dnsStart), metrics.getDnsStart()); + assertEquals(new Date(connectStart), metrics.getConnectStart()); + assertEquals(new Date(connectEnd), metrics.getConnectEnd()); + assertEquals(new Date(sslStart), metrics.getSslStart()); + assertEquals(new Date(sslEnd), metrics.getSslEnd()); + assertEquals(new Date(pushStart), metrics.getPushStart()); + assertEquals(new Date(pushEnd), metrics.getPushEnd()); + assertEquals(new Date(responseStart), metrics.getResponseStart()); + assertEquals(new Date(requestEnd), metrics.getRequestEnd()); + assertEquals(socketReused, metrics.getSocketReused()); + assertEquals(sentByteCount, (long) metrics.getSentByteCount()); + assertEquals(receivedByteCount, (long) metrics.getReceivedByteCount()); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + @SuppressWarnings("deprecation") + public void testOrderSuccessfulRequest() throws Exception { + final TestUrlRequestCallback callback = new TestUrlRequestCallback(); + TestRequestFinishedListener requestFinishedListener = + new AssertCallbackDoneRequestFinishedListener(callback); + mTestFramework.mCronetEngine.addRequestFinishedListener(requestFinishedListener); + ExperimentalUrlRequest.Builder urlRequestBuilder = + (ExperimentalUrlRequest.Builder) mTestFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + Date startTime = new Date(); + urlRequestBuilder.addRequestAnnotation("request annotation") + .addRequestAnnotation(this) + .build() + .start(); + callback.blockForDone(); + requestFinishedListener.blockUntilDone(); + Date endTime = new Date(); + + RequestFinishedInfo requestInfo = requestFinishedListener.getRequestInfo(); + MetricsTestUtil.checkRequestFinishedInfo(requestInfo, mUrl, startTime, endTime); + assertEquals(RequestFinishedInfo.SUCCEEDED, requestInfo.getFinishedReason()); + MetricsTestUtil.checkHasConnectTiming(requestInfo.getMetrics(), startTime, endTime, false); + assertEquals(newHashSet("request annotation", this), // Use sets for unordered comparison. + new HashSet(requestInfo.getAnnotations())); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + @RequiresMinApi(11) + public void testUpdateAnnotationOnSucceeded() throws Exception { + // The annotation that is updated in onSucceeded() callback. + AtomicBoolean requestAnnotation = new AtomicBoolean(false); + final TestUrlRequestCallback callback = new TestUrlRequestCallback() { + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + // Add processing information to request annotation. + requestAnnotation.set(true); + super.onSucceeded(request, info); + } + }; + TestRequestFinishedListener requestFinishedListener = + new AssertCallbackDoneRequestFinishedListener(callback); + ExperimentalUrlRequest.Builder urlRequestBuilder = + (ExperimentalUrlRequest.Builder) mTestFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + Date startTime = new Date(); + urlRequestBuilder.addRequestAnnotation(requestAnnotation) + .setRequestFinishedListener(requestFinishedListener) + .build() + .start(); + callback.blockForDone(); + requestFinishedListener.blockUntilDone(); + Date endTime = new Date(); + RequestFinishedInfo requestInfo = requestFinishedListener.getRequestInfo(); + MetricsTestUtil.checkRequestFinishedInfo(requestInfo, mUrl, startTime, endTime); + assertEquals(RequestFinishedInfo.SUCCEEDED, requestInfo.getFinishedReason()); + MetricsTestUtil.checkHasConnectTiming(requestInfo.getMetrics(), startTime, endTime, false); + // Check that annotation got updated in onSucceeded() callback. + assertEquals(requestAnnotation, requestInfo.getAnnotations().iterator().next()); + assertTrue(requestAnnotation.get()); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + // Tests a failed request where the error originates from Java. + public void testOrderFailedRequestJava() throws Exception { + final TestUrlRequestCallback callback = new TestUrlRequestCallback() { + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + throw new RuntimeException("make this request fail"); + } + }; + TestRequestFinishedListener requestFinishedListener = + new AssertCallbackDoneRequestFinishedListener(callback); + mTestFramework.mCronetEngine.addRequestFinishedListener(requestFinishedListener); + UrlRequest.Builder urlRequestBuilder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + assertTrue(callback.mOnErrorCalled); + requestFinishedListener.blockUntilDone(); + RequestFinishedInfo requestInfo = requestFinishedListener.getRequestInfo(); + assertNotNull("RequestFinishedInfo.Listener must be called", requestInfo); + assertEquals(mUrl, requestInfo.getUrl()); + assertTrue(requestInfo.getAnnotations().isEmpty()); + assertEquals(RequestFinishedInfo.FAILED, requestInfo.getFinishedReason()); + assertNotNull(requestInfo.getException()); + assertEquals("Exception received from UrlRequest.Callback", + requestInfo.getException().getMessage()); + RequestFinishedInfo.Metrics metrics = requestInfo.getMetrics(); + assertNotNull("RequestFinishedInfo.getMetrics() must not be null", metrics); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + // Tests a failed request where the error originates from native code. + public void testOrderFailedRequestNative() throws Exception { + String connectionRefusedUrl = "http://127.0.0.1:3"; + final TestUrlRequestCallback callback = new TestUrlRequestCallback(); + TestRequestFinishedListener requestFinishedListener = + new AssertCallbackDoneRequestFinishedListener(callback); + mTestFramework.mCronetEngine.addRequestFinishedListener(requestFinishedListener); + UrlRequest.Builder urlRequestBuilder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + connectionRefusedUrl, callback, callback.getExecutor()); + urlRequestBuilder.build().start(); + callback.blockForDone(); + assertTrue(callback.mOnErrorCalled); + requestFinishedListener.blockUntilDone(); + RequestFinishedInfo requestInfo = requestFinishedListener.getRequestInfo(); + assertNotNull("RequestFinishedInfo.Listener must be called", requestInfo); + assertEquals(connectionRefusedUrl, requestInfo.getUrl()); + assertTrue(requestInfo.getAnnotations().isEmpty()); + assertEquals(RequestFinishedInfo.FAILED, requestInfo.getFinishedReason()); + assertNotNull(requestInfo.getException()); + assertEquals(NetworkException.ERROR_CONNECTION_REFUSED, + ((NetworkException) requestInfo.getException()).getErrorCode()); + RequestFinishedInfo.Metrics metrics = requestInfo.getMetrics(); + assertNotNull("RequestFinishedInfo.getMetrics() must not be null", metrics); + } + + @Test + @SmallTest + @OnlyRunNativeCronet + @Feature({"Cronet"}) + public void testOrderCanceledRequest() throws Exception { + final TestUrlRequestCallback callback = new TestUrlRequestCallback() { + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + super.onResponseStarted(request, info); + request.cancel(); + } + }; + + TestRequestFinishedListener requestFinishedListener = + new AssertCallbackDoneRequestFinishedListener(callback); + mTestFramework.mCronetEngine.addRequestFinishedListener(requestFinishedListener); + ExperimentalUrlRequest.Builder urlRequestBuilder = + mTestFramework.mCronetEngine.newUrlRequestBuilder( + mUrl, callback, callback.getExecutor()); + Date startTime = new Date(); + urlRequestBuilder.addRequestAnnotation("request annotation") + .addRequestAnnotation(this) + .build() + .start(); + callback.blockForDone(); + requestFinishedListener.blockUntilDone(); + Date endTime = new Date(); + + RequestFinishedInfo requestInfo = requestFinishedListener.getRequestInfo(); + MetricsTestUtil.checkRequestFinishedInfo(requestInfo, mUrl, startTime, endTime); + assertEquals(RequestFinishedInfo.CANCELED, requestInfo.getFinishedReason()); + MetricsTestUtil.checkHasConnectTiming(requestInfo.getMetrics(), startTime, endTime, false); + + assertEquals(newHashSet("request annotation", this), // Use sets for unordered comparison. + new HashSet(requestInfo.getAnnotations())); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java new file mode 100644 index 0000000000..51e45b4cf1 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestBidirectionalStreamCallback.java @@ -0,0 +1,414 @@ +// 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.Iterator; +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; + public boolean mOnCanceledCalled; + + public int mHttpResponseDataLength; + 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 final ArrayList mWriteBuffers = new ArrayList(); + + // Buffers that we yet to receive the corresponding onWriteCompleted callback. + private final ArrayList mWriteBuffersToBeAcked = new ArrayList(); + + // Whether to use a direct executor. + private final boolean mUseDirectExecutor; + private final DirectExecutor mDirectExecutor; + + private class ExecutorThreadFactory implements ThreadFactory { + @Override + public Thread newThread(Runnable r) { + mExecutorThread = new Thread(r); + return mExecutorThread; + } + } + + private static class WriteBuffer { + final ByteBuffer mBuffer; + final boolean mFlush; + public WriteBuffer(ByteBuffer buffer, boolean flush) { + mBuffer = buffer; + mFlush = flush; + } + } + + private static class DirectExecutor implements Executor { + @Override + public void execute(Runnable task) { + task.run(); + } + } + + public enum ResponseStep { + NOTHING, + ON_STREAM_READY, + 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 TestBidirectionalStreamCallback() { + mUseDirectExecutor = false; + mDirectExecutor = null; + } + + public TestBidirectionalStreamCallback(boolean useDirectExecutor) { + mUseDirectExecutor = useDirectExecutor; + mDirectExecutor = new DirectExecutor(); + } + + 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() { + if (mUseDirectExecutor) { + return mDirectExecutor; + } + return mExecutorService; + } + + public void shutdownExecutor() { + if (mUseDirectExecutor) { + throw new UnsupportedOperationException("DirectExecutor doesn't support shutdown"); + } + mExecutorService.shutdown(); + } + + public void addWriteData(byte[] data) { + addWriteData(data, true); + } + + public void addWriteData(byte[] data, boolean flush) { + ByteBuffer writeBuffer = ByteBuffer.allocateDirect(data.length); + writeBuffer.put(data); + writeBuffer.flip(); + mWriteBuffers.add(new WriteBuffer(writeBuffer, flush)); + mWriteBuffersToBeAcked.add(new WriteBuffer(writeBuffer, flush)); + } + + @Override + public void onStreamReady(BidirectionalStream stream) { + checkOnValidThread(); + assertFalse(stream.isDone()); + assertEquals(ResponseStep.NOTHING, mResponseStep); + assertNull(mError); + mResponseStep = ResponseStep.ON_STREAM_READY; + if (maybeThrowCancelOrPause(stream, mWriteStepBlock)) { + return; + } + startNextWrite(stream); + } + + @Override + public void onResponseHeadersReceived(BidirectionalStream stream, UrlResponseInfo info) { + checkOnValidThread(); + assertFalse(stream.isDone()); + assertTrue(mResponseStep == ResponseStep.NOTHING + || mResponseStep == ResponseStep.ON_STREAM_READY + || 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, boolean endOfStream) { + checkOnValidThread(); + 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; + mResponseInfo = info; + + 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; + } + // Do not read if EOF has been reached. + if (!endOfStream) { + startNextRead(stream); + } + } + + @Override + public void onWriteCompleted(BidirectionalStream stream, UrlResponseInfo info, + ByteBuffer buffer, boolean endOfStream) { + checkOnValidThread(); + assertFalse(stream.isDone()); + assertNull(mError); + mResponseStep = ResponseStep.ON_WRITE_COMPLETED; + mResponseInfo = info; + if (!mWriteBuffersToBeAcked.isEmpty()) { + assertEquals(buffer, mWriteBuffersToBeAcked.get(0).mBuffer); + mWriteBuffersToBeAcked.remove(0); + } + if (maybeThrowCancelOrPause(stream, mWriteStepBlock)) { + return; + } + startNextWrite(stream); + } + + @Override + public void onResponseTrailersReceived(BidirectionalStream stream, UrlResponseInfo info, + UrlResponseInfo.HeaderBlock trailers) { + checkOnValidThread(); + assertFalse(stream.isDone()); + assertNull(mError); + mResponseStep = ResponseStep.ON_TRAILERS; + mResponseInfo = info; + mTrailers = trailers; + if (maybeThrowCancelOrPause(stream, mReadStepBlock)) { + return; + } + } + + @Override + public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info) { + checkOnValidThread(); + 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); + assertEquals(0, mWriteBuffers.size()); + assertEquals(0, mWriteBuffersToBeAcked.size()); + + mResponseStep = ResponseStep.ON_SUCCEEDED; + mResponseInfo = info; + openDone(); + maybeThrowCancelOrPause(stream, mReadStepBlock); + } + + @Override + public void onFailed(BidirectionalStream stream, UrlResponseInfo info, CronetException error) { + checkOnValidThread(); + 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; + mResponseInfo = info; + + mOnErrorCalled = true; + mError = error; + openDone(); + maybeThrowCancelOrPause(stream, mReadStepBlock); + } + + @Override + public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) { + checkOnValidThread(); + assertTrue(stream.isDone()); + // Should happen at most once for a single stream. + assertFalse(mOnCanceledCalled); + assertFalse(mOnErrorCalled); + assertNull(mError); + mResponseStep = ResponseStep.ON_CANCELED; + mResponseInfo = info; + + 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()) { + Iterator iterator = mWriteBuffers.iterator(); + while (iterator.hasNext()) { + WriteBuffer b = iterator.next(); + stream.write(b.mBuffer, !iterator.hasNext()); + iterator.remove(); + if (b.mFlush) { + stream.flush(); + break; + } + } + } + } + + 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); + } + + /** + * Returns the number of pending Writes. + */ + public int numPendingWrites() { + return mWriteBuffers.size(); + } + + 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() { + @Override + 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; + } + + /** + * Checks whether callback methods are invoked on the correct thread. + */ + private void checkOnValidThread() { + if (!mUseDirectExecutor) { + assertEquals(mExecutorThread, Thread.currentThread()); + } + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/TestDrivenDataProvider.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestDrivenDataProvider.java new file mode 100644 index 0000000000..5893513f0d --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestDrivenDataProvider.java @@ -0,0 +1,192 @@ +// 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 java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * An UploadDataProvider that allows tests to invoke {@code onReadSucceeded} + * and {@code onRewindSucceeded} on the UploadDataSink directly. + * Chunked mode is not supported here, since the main interest is to test + * different order of init/read/rewind calls. + */ +class TestDrivenDataProvider extends UploadDataProvider { + private final Executor mExecutor; + private final List mReads; + private final ConditionVariable mWaitForReadRequest = + new ConditionVariable(); + private final ConditionVariable mWaitForRewindRequest = + new ConditionVariable(); + // Lock used to synchronize access to mReadPending and mRewindPending. + private final Object mLock = new Object(); + + private int mNextRead; + + // Only accessible when holding mLock. + + private boolean mReadPending; + private boolean mRewindPending; + private int mNumRewindCalls; + private int mNumReadCalls; + + /** + * Constructor. + * @param Executor executor. Executor to run callbacks of UploadDataSink. + * @param List reads. Results to be returned by successful read + * requests. Returned bytes must all fit within the read buffer + * provided by Cronet. After a rewind, if there is one, all reads + * will be repeated. + */ + TestDrivenDataProvider(Executor executor, List reads) { + mExecutor = executor; + mReads = reads; + } + + // Called by UploadDataSink on the main thread. + @Override + public long getLength() { + long length = 0; + for (byte[] read : mReads) { + length += read.length; + } + return length; + } + + // Called by UploadDataSink on the executor thread. + @Override + public void read(final UploadDataSink uploadDataSink, + final ByteBuffer byteBuffer) throws IOException { + synchronized (mLock) { + ++mNumReadCalls; + assertIdle(); + + mReadPending = true; + if (mNextRead != mReads.size()) { + if ((byteBuffer.limit() - byteBuffer.position()) + < mReads.get(mNextRead).length) { + throw new IllegalStateException("Read buffer smaller than expected."); + } + byteBuffer.put(mReads.get(mNextRead)); + ++mNextRead; + } else { + throw new IllegalStateException("Too many reads: " + mNextRead); + } + mWaitForReadRequest.open(); + } + } + + // Called by UploadDataSink on the executor thread. + @Override + public void rewind(final UploadDataSink uploadDataSink) throws IOException { + synchronized (mLock) { + ++mNumRewindCalls; + assertIdle(); + + if (mNextRead == 0) { + // Should never try and rewind when rewinding does nothing. + throw new IllegalStateException( + "Unexpected rewind when already at beginning"); + } + mRewindPending = true; + mNextRead = 0; + mWaitForRewindRequest.open(); + } + } + + // Called by test fixture on the main thread. + public void onReadSucceeded(final UploadDataSink uploadDataSink) { + Runnable completeRunnable = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (!mReadPending) { + throw new IllegalStateException("No read pending."); + } + mReadPending = false; + uploadDataSink.onReadSucceeded(false); + } + } + }; + mExecutor.execute(completeRunnable); + } + + + // Called by test fixture on the main thread. + public void onRewindSucceeded(final UploadDataSink uploadDataSink) { + Runnable completeRunnable = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (!mRewindPending) { + throw new IllegalStateException("No rewind pending."); + } + mRewindPending = false; + uploadDataSink.onRewindSucceeded(); + } + } + }; + mExecutor.execute(completeRunnable); + } + + // Called by test fixture on the main thread. + public int getNumReadCalls() { + synchronized (mLock) { + return mNumReadCalls; + } + } + + // Called by test fixture on the main thread. + public int getNumRewindCalls() { + synchronized (mLock) { + return mNumRewindCalls; + } + } + + // Called by test fixture on the main thread. + public void waitForReadRequest() { + mWaitForReadRequest.block(); + } + + // Called by test fixture on the main thread. + public void resetWaitForReadRequest() { + mWaitForReadRequest.close(); + } + + // Called by test fixture on the main thread. + public void waitForRewindRequest() { + mWaitForRewindRequest.block(); + } + + // Called by test fixture on the main thread. + public void assertReadNotPending() { + synchronized (mLock) { + if (mReadPending) { + throw new IllegalStateException("Read is pending."); + } + } + } + + // Called by test fixture on the main thread. + public void assertRewindNotPending() { + synchronized (mLock) { + if (mRewindPending) { + throw new IllegalStateException("Rewind is pending."); + } + } + } + + /** + * Helper method to ensure no read or rewind is in progress. + */ + private void assertIdle() { + assertReadNotPending(); + assertRewindNotPending(); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/TestNetworkQualityRttListener.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestNetworkQualityRttListener.java new file mode 100644 index 0000000000..c2a937ea25 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestNetworkQualityRttListener.java @@ -0,0 +1,80 @@ +// Copyright 2016 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 static junit.framework.Assert.assertEquals; + +import android.os.ConditionVariable; +import android.util.SparseIntArray; + +import java.util.concurrent.Executor; + +class TestNetworkQualityRttListener extends NetworkQualityRttListener { + // Lock to ensure that observation counts can be updated and read by different threads. + private final Object mLock = new Object(); + + // Signals when the first RTT observation at the URL request layer is received. + private final ConditionVariable mWaitForUrlRequestRtt = new ConditionVariable(); + + private int mRttObservationCount; + + // Holds the RTT observations counts indexed by source. + private SparseIntArray mRttObservationCountBySource = new SparseIntArray(); + + private Thread mExecutorThread; + + /* + * Constructs a NetworkQualityRttListener that can listen to the RTT observations at various + * layers of the network stack. + * @param executor The executor on which the observations are reported. + */ + TestNetworkQualityRttListener(Executor executor) { + super(executor); + } + + @Override + public void onRttObservation(int rttMs, long when, int source) { + synchronized (mLock) { + if (source == 0) { + // Source 0 indicates that the RTT was observed at the URL request layer. + mWaitForUrlRequestRtt.open(); + } + + mRttObservationCount++; + mRttObservationCountBySource.put(source, mRttObservationCountBySource.get(source) + 1); + + if (mExecutorThread == null) { + mExecutorThread = Thread.currentThread(); + } + // Verify that the listener is always notified on the same thread. + assertEquals(mExecutorThread, Thread.currentThread()); + } + } + + /* + * Blocks until the first RTT observation at the URL request layer is received. + */ + public void waitUntilFirstUrlRequestRTTReceived() { + mWaitForUrlRequestRtt.block(); + } + + public int rttObservationCount() { + synchronized (mLock) { + return mRttObservationCount; + } + } + + public int rttObservationCount(int source) { + synchronized (mLock) { + return mRttObservationCountBySource.get(source); + } + } + + public Thread getThread() { + synchronized (mLock) { + return mExecutorThread; + } + } +} \ No newline at end of file diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/TestNetworkQualityThroughputListener.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestNetworkQualityThroughputListener.java new file mode 100644 index 0000000000..5f64ac6235 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestNetworkQualityThroughputListener.java @@ -0,0 +1,62 @@ +// Copyright 2016 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 static junit.framework.Assert.assertEquals; + +import android.os.ConditionVariable; + +import java.util.concurrent.Executor; + +class TestNetworkQualityThroughputListener extends NetworkQualityThroughputListener { + // Lock to ensure that observation counts can be updated and read by different threads. + private final Object mLock = new Object(); + + // Signals when the first throughput observation is received. + private final ConditionVariable mWaitForThroughput = new ConditionVariable(); + + private int mThroughputObservationCount; + private Thread mExecutorThread; + + /* + * Constructs a NetworkQualityThroughputListener that can listen to the throughput observations. + * @param executor The executor on which the observations are reported. + */ + TestNetworkQualityThroughputListener(Executor executor) { + super(executor); + } + + @Override + public void onThroughputObservation(int throughputKbps, long when, int source) { + synchronized (mLock) { + mWaitForThroughput.open(); + mThroughputObservationCount++; + if (mExecutorThread == null) { + mExecutorThread = Thread.currentThread(); + } + // Verify that the listener is always notified on the same thread. + assertEquals(mExecutorThread, Thread.currentThread()); + } + } + + /* + * Blocks until the first throughput observation is received. + */ + public void waitUntilFirstThroughputObservationReceived() { + mWaitForThroughput.block(); + } + + public int throughputObservationCount() { + synchronized (mLock) { + return mThroughputObservationCount; + } + } + + public Thread getThread() { + synchronized (mLock) { + return mExecutorThread; + } + } +} \ No newline at end of file diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/TestUploadDataProvider.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestUploadDataProvider.java new file mode 100644 index 0000000000..d492bdec6d --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestUploadDataProvider.java @@ -0,0 +1,281 @@ +// 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 java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * An UploadDataProvider implementation used in tests. + */ +public class TestUploadDataProvider extends UploadDataProvider { + // Indicates whether all success callbacks are synchronous or asynchronous. + // Doesn't apply to errors. + public enum SuccessCallbackMode { SYNC, ASYNC } + + // Indicates whether failures should throw exceptions, invoke callbacks + // synchronously, or invoke callback asynchronously. + public enum FailMode { NONE, THROWN, CALLBACK_SYNC, CALLBACK_ASYNC } + + private ArrayList mReads = new ArrayList(); + private final SuccessCallbackMode mSuccessCallbackMode; + private final Executor mExecutor; + + private boolean mChunked; + + // Index of read to fail on. + private int mReadFailIndex = -1; + // Indicates how to fail on a read. + private FailMode mReadFailMode = FailMode.NONE; + + private FailMode mRewindFailMode = FailMode.NONE; + + private FailMode mLengthFailMode = FailMode.NONE; + + private int mNumReadCalls; + private int mNumRewindCalls; + + private int mNextRead; + private boolean mStarted; + private boolean mReadPending; + private boolean mRewindPending; + // Used to ensure there are no read/rewind requests after a failure. + private boolean mFailed; + + private AtomicBoolean mClosed = new AtomicBoolean(false); + private ConditionVariable mAwaitingClose = new ConditionVariable(false); + + public TestUploadDataProvider( + SuccessCallbackMode successCallbackMode, final Executor executor) { + mSuccessCallbackMode = successCallbackMode; + mExecutor = executor; + } + + // Adds the result to be returned by a successful read request. The + // returned bytes must all fit within the read buffer provided by Cronet. + // After a rewind, if there is one, all reads will be repeated. + public void addRead(byte[] read) { + if (mStarted) { + throw new IllegalStateException("Adding bytes after read"); + } + mReads.add(read); + } + + public void setReadFailure(int readFailIndex, FailMode readFailMode) { + mReadFailIndex = readFailIndex; + mReadFailMode = readFailMode; + } + + public void setLengthFailure() { + mLengthFailMode = FailMode.THROWN; + } + + public void setRewindFailure(FailMode rewindFailMode) { + mRewindFailMode = rewindFailMode; + } + + public void setChunked(boolean chunked) { + mChunked = chunked; + } + + public int getNumReadCalls() { + return mNumReadCalls; + } + + public int getNumRewindCalls() { + return mNumRewindCalls; + } + + /** + * Returns the cumulative length of all data added by calls to addRead. + */ + @Override + public long getLength() throws IOException { + if (mClosed.get()) { + throw new ClosedChannelException(); + } + if (mLengthFailMode == FailMode.THROWN) { + throw new IllegalStateException("Sync length failure"); + } + return getUploadedLength(); + } + + public long getUploadedLength() { + if (mChunked) { + return -1; + } + long length = 0; + for (byte[] read : mReads) { + length += read.length; + } + return length; + } + + @Override + public void read(final UploadDataSink uploadDataSink, + final ByteBuffer byteBuffer) throws IOException { + int currentReadCall = mNumReadCalls; + ++mNumReadCalls; + if (mClosed.get()) { + throw new ClosedChannelException(); + } + assertIdle(); + + if (maybeFailRead(currentReadCall, uploadDataSink)) { + mFailed = true; + return; + } + + mReadPending = true; + mStarted = true; + + final boolean finalChunk = (mChunked && mNextRead == mReads.size() - 1); + if (mNextRead < mReads.size()) { + if ((byteBuffer.limit() - byteBuffer.position()) + < mReads.get(mNextRead).length) { + throw new IllegalStateException( + "Read buffer smaller than expected."); + } + byteBuffer.put(mReads.get(mNextRead)); + ++mNextRead; + } else { + throw new IllegalStateException( + "Too many reads: " + mNextRead); + } + + Runnable completeRunnable = new Runnable() { + @Override + public void run() { + mReadPending = false; + uploadDataSink.onReadSucceeded(finalChunk); + } + }; + if (mSuccessCallbackMode == SuccessCallbackMode.SYNC) { + completeRunnable.run(); + } else { + mExecutor.execute(completeRunnable); + } + } + + @Override + public void rewind(final UploadDataSink uploadDataSink) throws IOException { + ++mNumRewindCalls; + if (mClosed.get()) { + throw new ClosedChannelException(); + } + assertIdle(); + + if (maybeFailRewind(uploadDataSink)) { + mFailed = true; + return; + } + + if (mNextRead == 0) { + // Should never try and rewind when rewinding does nothing. + throw new IllegalStateException( + "Unexpected rewind when already at beginning"); + } + + mRewindPending = true; + mNextRead = 0; + + Runnable completeRunnable = new Runnable() { + @Override + public void run() { + mRewindPending = false; + uploadDataSink.onRewindSucceeded(); + } + }; + if (mSuccessCallbackMode == SuccessCallbackMode.SYNC) { + completeRunnable.run(); + } else { + mExecutor.execute(completeRunnable); + } + } + + private void assertIdle() { + if (mReadPending) { + throw new IllegalStateException("Unexpected operation during read"); + } + if (mRewindPending) { + throw new IllegalStateException( + "Unexpected operation during rewind"); + } + if (mFailed) { + throw new IllegalStateException( + "Unexpected operation after failure"); + } + } + + private boolean maybeFailRead(int readIndex, + final UploadDataSink uploadDataSink) { + if (readIndex != mReadFailIndex) return false; + + switch (mReadFailMode) { + case THROWN: + throw new IllegalStateException("Thrown read failure"); + case CALLBACK_SYNC: + uploadDataSink.onReadError( + new IllegalStateException("Sync read failure")); + return true; + case CALLBACK_ASYNC: + Runnable errorRunnable = new Runnable() { + @Override + public void run() { + uploadDataSink.onReadError( + new IllegalStateException("Async read failure")); + } + }; + mExecutor.execute(errorRunnable); + return true; + default: + return false; + } + } + + private boolean maybeFailRewind(final UploadDataSink uploadDataSink) { + switch (mRewindFailMode) { + case THROWN: + throw new IllegalStateException("Thrown rewind failure"); + case CALLBACK_SYNC: + uploadDataSink.onRewindError( + new IllegalStateException("Sync rewind failure")); + return true; + case CALLBACK_ASYNC: + Runnable errorRunnable = new Runnable() { + @Override + public void run() { + uploadDataSink.onRewindError(new IllegalStateException( + "Async rewind failure")); + } + }; + mExecutor.execute(errorRunnable); + return true; + default: + return false; + } + } + + @Override + public void close() throws IOException { + if (!mClosed.compareAndSet(false, true)) { + throw new AssertionError("Closed twice"); + } + mAwaitingClose.open(); + } + + public void assertClosed() { + mAwaitingClose.block(5000); + if (!mClosed.get()) { + throw new AssertionError("Was not closed"); + } + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/TestUrlRequestCallback.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestUrlRequestCallback.java new file mode 100644 index 0000000000..86442202d3 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/TestUrlRequestCallback.java @@ -0,0 +1,370 @@ +// Copyright 2014 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 static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import static org.chromium.net.CronetTestRule.assertContains; + +import android.os.ConditionVariable; +import android.os.StrictMode; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * Callback that tracks information from different callbacks and and has a + * method to block thread until the request completes on another thread. + * Allows to cancel, block request or throw an exception from an arbitrary step. + */ +public class TestUrlRequestCallback extends UrlRequest.Callback { + public ArrayList mRedirectResponseInfoList = new ArrayList(); + public ArrayList mRedirectUrlList = new ArrayList(); + public UrlResponseInfo mResponseInfo; + public CronetException mError; + + public ResponseStep mResponseStep = ResponseStep.NOTHING; + + public int mRedirectCount; + public boolean mOnErrorCalled; + public boolean mOnCanceledCalled; + + public int mHttpResponseDataLength; + public String mResponseAsString = ""; + + public int mReadBufferSize = 32 * 1024; + + // When false, the consumer is responsible for all calls into the request + // that advance it. + private boolean mAutoAdvance = true; + // Whether an exception is thrown by maybeThrowCancelOrPause(). + private boolean mCallbackExceptionThrown; + + // Whether to permit calls on the network thread. + private boolean mAllowDirectExecutor; + + // Conditionally fail on certain steps. + private FailureType mFailureType = FailureType.NONE; + private ResponseStep mFailureStep = ResponseStep.NOTHING; + + // Signals when request is done either successfully or not. + private final ConditionVariable mDone = new ConditionVariable(); + + // Signaled on each step when mAutoAdvance is false. + private final ConditionVariable mStepBlock = new ConditionVariable(); + + // Executor Service for Cronet callbacks. + private final ExecutorService mExecutorService; + private Thread mExecutorThread; + + // position() of ByteBuffer prior to read() call. + private int mBufferPositionBeforeRead; + + private static class ExecutorThreadFactory implements ThreadFactory { + @Override + public Thread newThread(final Runnable r) { + return new Thread(new Runnable() { + @Override + public void run() { + StrictMode.ThreadPolicy threadPolicy = StrictMode.getThreadPolicy(); + try { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectNetwork() + .penaltyLog() + .penaltyDeath() + .build()); + r.run(); + } finally { + StrictMode.setThreadPolicy(threadPolicy); + } + } + }); + } + } + + public enum ResponseStep { + NOTHING, + ON_RECEIVED_REDIRECT, + ON_RESPONSE_STARTED, + ON_READ_COMPLETED, + ON_SUCCEEDED, + ON_FAILED, + ON_CANCELED, + } + + public enum FailureType { + NONE, + CANCEL_SYNC, + CANCEL_ASYNC, + // Same as above, but continues to advance the request after posting + // the cancellation task. + CANCEL_ASYNC_WITHOUT_PAUSE, + THROW_SYNC + } + + /** + * Set {@code mExecutorThread}. + */ + private void fillInExecutorThread() { + mExecutorService.execute(new Runnable() { + @Override + public void run() { + mExecutorThread = Thread.currentThread(); + } + }); + } + + /** + * Create a {@link TestUrlRequestCallback} with a new single-threaded executor. + */ + public TestUrlRequestCallback() { + this(Executors.newSingleThreadExecutor(new ExecutorThreadFactory())); + } + + /** + * Create a {@link TestUrlRequestCallback} using a custom single-threaded executor. + * NOTE(pauljensen): {@code executorService} should be a new single-threaded executor. + */ + public TestUrlRequestCallback(ExecutorService executorService) { + mExecutorService = executorService; + fillInExecutorThread(); + } + + public void setAutoAdvance(boolean autoAdvance) { + mAutoAdvance = autoAdvance; + } + + public void setAllowDirectExecutor(boolean allowed) { + mAllowDirectExecutor = allowed; + } + + public void setFailure(FailureType failureType, ResponseStep failureStep) { + mFailureStep = failureStep; + mFailureType = failureType; + } + + public void blockForDone() { + mDone.block(); + } + + public void waitForNextStep() { + mStepBlock.block(); + mStepBlock.close(); + } + + public ExecutorService getExecutor() { + return mExecutorService; + } + + public void shutdownExecutor() { + mExecutorService.shutdown(); + } + + /** + * Shuts down the ExecutorService and waits until it executes all posted + * tasks. + */ + public void shutdownExecutorAndWait() { + mExecutorService.shutdown(); + try { + // Termination shouldn't take long. Use 1 min which should be more than enough. + mExecutorService.awaitTermination(1, TimeUnit.MINUTES); + } catch (InterruptedException e) { + assertTrue("ExecutorService is interrupted while waiting for termination", false); + } + assertTrue(mExecutorService.isTerminated()); + } + + @Override + public void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) { + checkExecutorThread(); + assertFalse(request.isDone()); + assertTrue(mResponseStep == ResponseStep.NOTHING + || mResponseStep == ResponseStep.ON_RECEIVED_REDIRECT); + assertNull(mError); + + mResponseStep = ResponseStep.ON_RECEIVED_REDIRECT; + mRedirectUrlList.add(newLocationUrl); + mRedirectResponseInfoList.add(info); + ++mRedirectCount; + if (maybeThrowCancelOrPause(request)) { + return; + } + request.followRedirect(); + } + + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + checkExecutorThread(); + assertFalse(request.isDone()); + assertTrue(mResponseStep == ResponseStep.NOTHING + || mResponseStep == ResponseStep.ON_RECEIVED_REDIRECT); + assertNull(mError); + + mResponseStep = ResponseStep.ON_RESPONSE_STARTED; + mResponseInfo = info; + if (maybeThrowCancelOrPause(request)) { + return; + } + startNextRead(request); + } + + @Override + public void onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) { + checkExecutorThread(); + assertFalse(request.isDone()); + assertTrue(mResponseStep == ResponseStep.ON_RESPONSE_STARTED + || mResponseStep == ResponseStep.ON_READ_COMPLETED); + assertNull(mError); + + mResponseStep = ResponseStep.ON_READ_COMPLETED; + + final byte[] lastDataReceivedAsBytes; + final int bytesRead = byteBuffer.position() - mBufferPositionBeforeRead; + mHttpResponseDataLength += bytesRead; + 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(request)) { + return; + } + startNextRead(request); + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + checkExecutorThread(); + assertTrue(request.isDone()); + assertTrue(mResponseStep == ResponseStep.ON_RESPONSE_STARTED + || mResponseStep == ResponseStep.ON_READ_COMPLETED); + assertFalse(mOnErrorCalled); + assertFalse(mOnCanceledCalled); + assertNull(mError); + + mResponseStep = ResponseStep.ON_SUCCEEDED; + mResponseInfo = info; + openDone(); + maybeThrowCancelOrPause(request); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) { + // If the failure is because of prohibited direct execution, the test shouldn't fail + // since the request already did. + if (error.getCause() instanceof InlineExecutionProhibitedException) { + mAllowDirectExecutor = true; + } + checkExecutorThread(); + assertTrue(request.isDone()); + // Shouldn't happen after success. + assertTrue(mResponseStep != ResponseStep.ON_SUCCEEDED); + // Should happen at most once for a single request. + assertFalse(mOnErrorCalled); + assertFalse(mOnCanceledCalled); + assertNull(mError); + if (mCallbackExceptionThrown) { + assertTrue(error instanceof CallbackException); + assertContains("Exception received from UrlRequest.Callback", error.getMessage()); + assertNotNull(error.getCause()); + assertTrue(error.getCause() instanceof IllegalStateException); + assertContains("Listener Exception.", error.getCause().getMessage()); + } + + mResponseStep = ResponseStep.ON_FAILED; + mOnErrorCalled = true; + mError = error; + openDone(); + maybeThrowCancelOrPause(request); + } + + @Override + public void onCanceled(UrlRequest request, UrlResponseInfo info) { + checkExecutorThread(); + assertTrue(request.isDone()); + // Should happen at most once for a single request. + assertFalse(mOnCanceledCalled); + assertFalse(mOnErrorCalled); + assertNull(mError); + + mResponseStep = ResponseStep.ON_CANCELED; + mOnCanceledCalled = true; + openDone(); + maybeThrowCancelOrPause(request); + } + + public void startNextRead(UrlRequest request) { + startNextRead(request, ByteBuffer.allocateDirect(mReadBufferSize)); + } + + public void startNextRead(UrlRequest request, ByteBuffer buffer) { + mBufferPositionBeforeRead = buffer.position(); + request.read(buffer); + } + + 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(); + } + + private void checkExecutorThread() { + if (!mAllowDirectExecutor) { + assertEquals(mExecutorThread, Thread.currentThread()); + } + } + + /** + * Returns {@code false} if the listener should continue to advance the + * request. + */ + private boolean maybeThrowCancelOrPause(final UrlRequest request) { + checkExecutorThread(); + if (mResponseStep != mFailureStep || mFailureType == FailureType.NONE) { + if (!mAutoAdvance) { + mStepBlock.open(); + return true; + } + return false; + } + + if (mFailureType == FailureType.THROW_SYNC) { + assertFalse(mCallbackExceptionThrown); + mCallbackExceptionThrown = true; + throw new IllegalStateException("Listener Exception."); + } + Runnable task = new Runnable() { + @Override + public void run() { + request.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/src/components/cronet/android/test/javatests/src/org/chromium/net/UploadDataProvidersTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/UploadDataProvidersTest.java new file mode 100644 index 0000000000..f1eef4483e --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/UploadDataProvidersTest.java @@ -0,0 +1,274 @@ +// Copyright 2016 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import static org.chromium.net.CronetTestRule.assertContains; +import static org.chromium.net.CronetTestRule.getContext; + +import android.os.ConditionVariable; +import android.os.ParcelFileDescriptor; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule.CronetTestFramework; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** Test the default provided implementations of {@link UploadDataProvider} */ +@RunWith(AndroidJUnit4.class) +public class UploadDataProvidersTest { + private static final String LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Proin elementum, libero laoreet fringilla faucibus, metus tortor vehicula ante, " + + "lacinia lorem eros vel sapien."; + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + private CronetTestFramework mTestFramework; + private File mFile; + + @Before + public void setUp() throws Exception { + mTestFramework = mTestRule.startCronetTestFramework(); + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + // Add url interceptors after native application context is initialized. + mFile = new File(getContext().getCacheDir().getPath() + "/tmpfile"); + FileOutputStream fileOutputStream = new FileOutputStream(mFile); + try { + fileOutputStream.write(LOREM.getBytes("UTF-8")); + } finally { + fileOutputStream.close(); + } + } + + @After + public void tearDown() throws Exception { + NativeTestServer.shutdownNativeTestServer(); + mTestFramework.mCronetEngine.shutdown(); + assertTrue(mFile.delete()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testFileProvider() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectToEchoBody(), callback, callback.getExecutor()); + UploadDataProvider dataProvider = UploadDataProviders.create(mFile); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(LOREM, callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testFileDescriptorProvider() throws Exception { + ParcelFileDescriptor descriptor = + ParcelFileDescriptor.open(mFile, ParcelFileDescriptor.MODE_READ_ONLY); + assertTrue(descriptor.getFileDescriptor().valid()); + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectToEchoBody(), callback, callback.getExecutor()); + UploadDataProvider dataProvider = UploadDataProviders.create(descriptor); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(LOREM, callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testBadFileDescriptorProvider() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectToEchoBody(), callback, callback.getExecutor()); + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + try { + UploadDataProvider dataProvider = UploadDataProviders.create(pipe[0]); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + + assertTrue(callback.mError.getCause() instanceof IllegalArgumentException); + } finally { + pipe[1].close(); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testBufferProvider() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectToEchoBody(), callback, callback.getExecutor()); + UploadDataProvider dataProvider = UploadDataProviders.create(LOREM.getBytes("UTF-8")); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.build().start(); + callback.blockForDone(); + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(LOREM, callback.mResponseAsString); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Tests that ByteBuffer's limit cannot be changed by the caller. + public void testUploadChangeBufferLimit() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + builder.setUploadDataProvider(new UploadDataProvider() { + private static final String CONTENT = "hello"; + @Override + public long getLength() throws IOException { + return CONTENT.length(); + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) + throws IOException { + int oldPos = byteBuffer.position(); + int oldLimit = byteBuffer.limit(); + byteBuffer.put(CONTENT.getBytes()); + assertEquals(oldPos + CONTENT.length(), byteBuffer.position()); + assertEquals(oldLimit, byteBuffer.limit()); + // Now change the limit to something else. This should give an error. + byteBuffer.limit(oldLimit - 1); + uploadDataSink.onReadSucceeded(false); + } + + @Override + public void rewind(UploadDataSink uploadDataSink) throws IOException {} + }, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + assertTrue(callback.mOnErrorCalled); + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains("ByteBuffer limit changed", callback.mError.getCause().getMessage()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testNoErrorWhenCanceledDuringStart() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + final ConditionVariable first = new ConditionVariable(); + final ConditionVariable second = new ConditionVariable(); + builder.addHeader("Content-Type", "useless/string"); + builder.setUploadDataProvider(new UploadDataProvider() { + @Override + public long getLength() throws IOException { + first.open(); + second.block(); + return 0; + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) + throws IOException {} + + @Override + public void rewind(UploadDataSink uploadDataSink) throws IOException {} + }, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + first.block(); + urlRequest.cancel(); + second.open(); + callback.blockForDone(); + assertTrue(callback.mOnCanceledCalled); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testNoErrorWhenExceptionDuringStart() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getEchoBodyURL(), callback, callback.getExecutor()); + final ConditionVariable first = new ConditionVariable(); + final String exceptionMessage = "Bad Length"; + builder.addHeader("Content-Type", "useless/string"); + builder.setUploadDataProvider(new UploadDataProvider() { + @Override + public long getLength() throws IOException { + first.open(); + throw new IOException(exceptionMessage); + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) + throws IOException {} + + @Override + public void rewind(UploadDataSink uploadDataSink) throws IOException {} + }, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + first.block(); + callback.blockForDone(); + assertFalse(callback.mOnCanceledCalled); + assertTrue(callback.mError instanceof CallbackException); + assertContains("Exception received from UploadDataProvider", callback.mError.getMessage()); + assertContains(exceptionMessage, callback.mError.getCause().getMessage()); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + // Tests that creating a ByteBufferUploadProvider using a byte array with an + // offset gives a ByteBuffer with position 0. crbug.com/603124. + public void testCreateByteBufferUploadWithArrayOffset() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + // This URL will trigger a rewind(). + UrlRequest.Builder builder = mTestFramework.mCronetEngine.newUrlRequestBuilder( + NativeTestServer.getRedirectToEchoBody(), callback, callback.getExecutor()); + builder.addHeader("Content-Type", "useless/string"); + byte[] uploadData = LOREM.getBytes("UTF-8"); + int offset = 5; + byte[] uploadDataWithPadding = new byte[uploadData.length + offset]; + System.arraycopy(uploadData, 0, uploadDataWithPadding, offset, uploadData.length); + UploadDataProvider dataProvider = + UploadDataProviders.create(uploadDataWithPadding, offset, uploadData.length); + assertEquals(uploadData.length, dataProvider.getLength()); + builder.setUploadDataProvider(dataProvider, callback.getExecutor()); + UrlRequest urlRequest = builder.build(); + urlRequest.start(); + callback.blockForDone(); + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals(LOREM, callback.mResponseAsString); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/UrlResponseInfoTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/UrlResponseInfoTest.java new file mode 100644 index 0000000000..5035d32613 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/UrlResponseInfoTest.java @@ -0,0 +1,77 @@ +// 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.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.impl.UrlResponseInfoImpl; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Tests for {@link UrlResponseInfo}. + */ +@RunWith(AndroidJUnit4.class) +public class UrlResponseInfoTest { + /** + * Test for public API of {@link UrlResponseInfo}. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + public void testPublicAPI() throws Exception { + final List urlChain = new ArrayList(); + urlChain.add("chromium.org"); + final int httpStatusCode = 200; + final String httpStatusText = "OK"; + final List> allHeadersList = + new ArrayList>(); + allHeadersList.add(new AbstractMap.SimpleImmutableEntry( + "Date", "Fri, 30 Oct 2015 14:26:41 GMT")); + final boolean wasCached = true; + final String negotiatedProtocol = "quic/1+spdy/3"; + final String proxyServer = "example.com"; + final long receivedByteCount = 42; + + final UrlResponseInfo info = + new UrlResponseInfoImpl(urlChain, httpStatusCode, httpStatusText, allHeadersList, + wasCached, negotiatedProtocol, proxyServer, receivedByteCount); + Assert.assertEquals(info.getUrlChain(), urlChain); + try { + info.getUrlChain().add("example.com"); + Assert.fail("getUrlChain() returned modifyable list."); + } catch (UnsupportedOperationException e) { + // Expected. + } + Assert.assertEquals(info.getHttpStatusCode(), httpStatusCode); + Assert.assertEquals(info.getHttpStatusText(), httpStatusText); + Assert.assertEquals(info.getAllHeadersAsList(), allHeadersList); + try { + info.getAllHeadersAsList().add( + new AbstractMap.SimpleImmutableEntry("X", "Y")); + Assert.fail("getAllHeadersAsList() returned modifyable list."); + } catch (UnsupportedOperationException e) { + // Expected. + } + Assert.assertEquals(info.getAllHeaders().size(), allHeadersList.size()); + Assert.assertEquals(info.getAllHeaders().get(allHeadersList.get(0).getKey()).size(), 1); + Assert.assertEquals(info.getAllHeaders().get(allHeadersList.get(0).getKey()).get(0), + allHeadersList.get(0).getValue()); + Assert.assertEquals(info.wasCached(), wasCached); + Assert.assertEquals(info.getNegotiatedProtocol(), negotiatedProtocol); + Assert.assertEquals(info.getProxyServer(), proxyServer); + Assert.assertEquals(info.getReceivedByteCount(), receivedByteCount); + } +} \ No newline at end of file diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetBufferedOutputStreamTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetBufferedOutputStreamTest.java new file mode 100644 index 0000000000..cb2b8a416b --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetBufferedOutputStreamTest.java @@ -0,0 +1,468 @@ +// 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.urlconnection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.net.CronetTestRule.getContext; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetTestRule; +import org.chromium.net.CronetTestRule.CompareDefaultWithCronet; +import org.chromium.net.CronetTestRule.OnlyRunCronetHttpURLConnection; +import org.chromium.net.NativeTestServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * Tests the CronetBufferedOutputStream implementation. + */ +@RunWith(AndroidJUnit4.class) +public class CronetBufferedOutputStreamTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + @Before + public void setUp() throws Exception { + mTestRule.setStreamHandlerFactory(new CronetEngine.Builder(getContext()).build()); + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @After + public void tearDown() throws Exception { + NativeTestServer.shutdownNativeTestServer(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testGetOutputStreamAfterConnectionMade() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + assertEquals(200, connection.getResponseCode()); + try { + connection.getOutputStream(); + fail(); + } catch (java.net.ProtocolException e) { + // Expected. + } + } + + /** + * Tests write after connect. Strangely, the default implementation allows + * writing after being connected, so this test only runs against Cronet's + * implementation. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testWriteAfterConnect() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + OutputStream out = connection.getOutputStream(); + out.write(TestUtil.UPLOAD_DATA); + connection.connect(); + try { + // Attemp to write some more. + out.write(TestUtil.UPLOAD_DATA); + fail(); + } catch (IllegalStateException e) { + assertEquals("Use setFixedLengthStreamingMode() or " + + "setChunkedStreamingMode() for writing after connect", + e.getMessage()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testWriteAfterReadingResponse() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + OutputStream out = connection.getOutputStream(); + assertEquals(200, connection.getResponseCode()); + try { + out.write(TestUtil.UPLOAD_DATA); + fail(); + } catch (Exception e) { + // Default implementation gives an IOException and says that the + // stream is closed. Cronet gives an IllegalStateException and + // complains about write after connected. + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostWithContentLength() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + byte[] largeData = TestUtil.getLargeData(); + connection.setRequestProperty("Content-Length", + Integer.toString(largeData.length)); + OutputStream out = connection.getOutputStream(); + int totalBytesWritten = 0; + // Number of bytes to write each time. It is doubled each time + // to make sure that the buffer grows. + int bytesToWrite = 683; + while (totalBytesWritten < largeData.length) { + if (bytesToWrite > largeData.length - totalBytesWritten) { + // Do not write out of bound. + bytesToWrite = largeData.length - totalBytesWritten; + } + out.write(largeData, totalBytesWritten, bytesToWrite); + totalBytesWritten += bytesToWrite; + bytesToWrite *= 2; + } + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostWithContentLengthOneMassiveWrite() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + byte[] largeData = TestUtil.getLargeData(); + connection.setRequestProperty("Content-Length", + Integer.toString(largeData.length)); + OutputStream out = connection.getOutputStream(); + out.write(largeData); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostWithContentLengthWriteOneByte() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + byte[] largeData = TestUtil.getLargeData(); + connection.setRequestProperty("Content-Length", + Integer.toString(largeData.length)); + OutputStream out = connection.getOutputStream(); + for (int i = 0; i < largeData.length; i++) { + out.write(largeData[i]); + } + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostWithZeroContentLength() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Length", "0"); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals("", TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostZeroByteWithoutContentLength() throws Exception { + // Make sure both implementation sets the Content-Length header to 0. + URL url = new URL(NativeTestServer.getEchoHeaderURL("Content-Length")); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals("0", TestUtil.getResponseAsString(connection)); + connection.disconnect(); + + // Make sure the server echoes back empty body for both implementation. + URL echoBody = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection2 = + (HttpURLConnection) echoBody.openConnection(); + connection2.setDoOutput(true); + connection2.setRequestMethod("POST"); + assertEquals(200, connection2.getResponseCode()); + assertEquals("OK", connection2.getResponseMessage()); + assertEquals("", TestUtil.getResponseAsString(connection2)); + connection2.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostWithoutContentLengthSmall() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + OutputStream out = connection.getOutputStream(); + out.write(TestUtil.UPLOAD_DATA); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(TestUtil.UPLOAD_DATA_STRING, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostWithoutContentLength() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + byte[] largeData = TestUtil.getLargeData(); + OutputStream out = connection.getOutputStream(); + int totalBytesWritten = 0; + // Number of bytes to write each time. It is doubled each time + // to make sure that the buffer grows. + int bytesToWrite = 683; + while (totalBytesWritten < largeData.length) { + if (bytesToWrite > largeData.length - totalBytesWritten) { + // Do not write out of bound. + bytesToWrite = largeData.length - totalBytesWritten; + } + out.write(largeData, totalBytesWritten, bytesToWrite); + totalBytesWritten += bytesToWrite; + bytesToWrite *= 2; + } + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostWithoutContentLengthOneMassiveWrite() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + OutputStream out = connection.getOutputStream(); + byte[] largeData = TestUtil.getLargeData(); + out.write(largeData); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostWithoutContentLengthWriteOneByte() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + OutputStream out = connection.getOutputStream(); + byte[] largeData = TestUtil.getLargeData(); + for (int i = 0; i < largeData.length; i++) { + out.write(largeData[i]); + } + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testWriteLessThanContentLength() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + // Set a content length that's 1 byte more. + connection.setRequestProperty( + "Content-Length", Integer.toString(TestUtil.UPLOAD_DATA.length + 1)); + OutputStream out = connection.getOutputStream(); + out.write(TestUtil.UPLOAD_DATA); + try { + connection.getResponseCode(); + fail(); + } catch (IOException e) { + // Expected. + } + connection.disconnect(); + } + + /** + * Tests that if caller writes more than the content length provided, + * an exception should occur. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testWriteMoreThanContentLength() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + // Use a content length that is 1 byte shorter than actual data. + connection.setRequestProperty( + "Content-Length", Integer.toString(TestUtil.UPLOAD_DATA.length - 1)); + OutputStream out = connection.getOutputStream(); + // Write a few bytes first. + out.write(TestUtil.UPLOAD_DATA, 0, 3); + try { + // Write remaining bytes. + out.write(TestUtil.UPLOAD_DATA, 3, TestUtil.UPLOAD_DATA.length - 3); + // On Lollipop, default implementation only triggers the error when reading response. + connection.getInputStream(); + fail(); + } catch (IOException e) { + assertEquals("exceeded content-length limit of " + (TestUtil.UPLOAD_DATA.length - 1) + + " bytes", + e.getMessage()); + } + connection.disconnect(); + } + + /** + * Same as {@code testWriteMoreThanContentLength()}, but it only writes one byte + * at a time. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testWriteMoreThanContentLengthWriteOneByte() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + // Use a content length that is 1 byte shorter than actual data. + connection.setRequestProperty( + "Content-Length", Integer.toString(TestUtil.UPLOAD_DATA.length - 1)); + OutputStream out = connection.getOutputStream(); + try { + for (int i = 0; i < TestUtil.UPLOAD_DATA.length; i++) { + out.write(TestUtil.UPLOAD_DATA[i]); + } + // On Lollipop, default implementation only triggers the error when reading response. + connection.getInputStream(); + fail(); + } catch (IOException e) { + assertEquals("exceeded content-length limit of " + (TestUtil.UPLOAD_DATA.length - 1) + + " bytes", + e.getMessage()); + } + connection.disconnect(); + } + + /** + * Tests that {@link CronetBufferedOutputStream} supports rewind in a + * POST preserving redirect. + * Use {@code OnlyRunCronetHttpURLConnection} as the default implementation + * does not pass this test. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testRewind() throws Exception { + URL url = new URL(NativeTestServer.getRedirectToEchoBody()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty( + "Content-Length", Integer.toString(TestUtil.UPLOAD_DATA.length)); + OutputStream out = connection.getOutputStream(); + out.write(TestUtil.UPLOAD_DATA); + assertEquals(TestUtil.UPLOAD_DATA_STRING, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + /** + * Like {@link #testRewind} but does not set Content-Length header. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testRewindWithoutContentLength() throws Exception { + URL url = new URL(NativeTestServer.getRedirectToEchoBody()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + OutputStream out = connection.getOutputStream(); + out.write(TestUtil.UPLOAD_DATA); + assertEquals(TestUtil.UPLOAD_DATA_STRING, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetChunkedOutputStreamTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetChunkedOutputStreamTest.java new file mode 100644 index 0000000000..e1a2480a47 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetChunkedOutputStreamTest.java @@ -0,0 +1,320 @@ +// 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.urlconnection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.net.CronetTestRule.getContext; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetTestRule; +import org.chromium.net.CronetTestRule.CompareDefaultWithCronet; +import org.chromium.net.CronetTestRule.OnlyRunCronetHttpURLConnection; +import org.chromium.net.NativeTestServer; +import org.chromium.net.NetworkException; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.URL; + +/** + * Tests {@code getOutputStream} when {@code setChunkedStreamingMode} is enabled. + * Tests annotated with {@code CompareDefaultWithCronet} will run once with the + * default HttpURLConnection implementation and then with Cronet's + * HttpURLConnection implementation. Tests annotated with + * {@code OnlyRunCronetHttpURLConnection} only run Cronet's implementation. + * See {@link CronetTestBase#runTest()} for details. + */ +@RunWith(AndroidJUnit4.class) +public class CronetChunkedOutputStreamTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private static final String UPLOAD_DATA_STRING = "Nifty upload data!"; + private static final byte[] UPLOAD_DATA = UPLOAD_DATA_STRING.getBytes(); + private static final int REPEAT_COUNT = 100000; + + @Before + public void setUp() throws Exception { + mTestRule.setStreamHandlerFactory(new CronetEngine.Builder(getContext()).build()); + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @After + public void tearDown() throws Exception { + NativeTestServer.shutdownNativeTestServer(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testGetOutputStreamAfterConnectionMade() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setChunkedStreamingMode(0); + assertEquals(200, connection.getResponseCode()); + try { + connection.getOutputStream(); + fail(); + } catch (ProtocolException e) { + // Expected. + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testWriteAfterReadingResponse() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setChunkedStreamingMode(0); + OutputStream out = connection.getOutputStream(); + assertEquals(200, connection.getResponseCode()); + try { + out.write(UPLOAD_DATA); + fail(); + } catch (IOException e) { + // Expected. + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testWriteAfterRequestFailed() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setChunkedStreamingMode(0); + OutputStream out = connection.getOutputStream(); + out.write(UPLOAD_DATA); + NativeTestServer.shutdownNativeTestServer(); + try { + out.write(TestUtil.getLargeData()); + connection.getResponseCode(); + fail(); + } catch (IOException e) { + if (!mTestRule.testingSystemHttpURLConnection()) { + NetworkException requestException = (NetworkException) e; + assertEquals( + NetworkException.ERROR_CONNECTION_REFUSED, requestException.getErrorCode()); + } + } + connection.disconnect(); + // Restarting server to run the test for a second time. + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testGetResponseAfterWriteFailed() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + NativeTestServer.shutdownNativeTestServer(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + // Set 1 byte as chunk size so internally Cronet will try upload when + // 1 byte is filled. + connection.setChunkedStreamingMode(1); + try { + OutputStream out = connection.getOutputStream(); + out.write(1); + out.write(1); + // Forces OutputStream implementation to flush. crbug.com/653072 + out.flush(); + // System's implementation is flaky see crbug.com/653072. + if (!mTestRule.testingSystemHttpURLConnection()) { + fail(); + } + } catch (IOException e) { + if (!mTestRule.testingSystemHttpURLConnection()) { + NetworkException requestException = (NetworkException) e; + assertEquals( + NetworkException.ERROR_CONNECTION_REFUSED, requestException.getErrorCode()); + } + } + // Make sure IOException is reported again when trying to read response + // from the connection. + try { + connection.getResponseCode(); + fail(); + } catch (IOException e) { + // Expected. + if (!mTestRule.testingSystemHttpURLConnection()) { + NetworkException requestException = (NetworkException) e; + assertEquals( + NetworkException.ERROR_CONNECTION_REFUSED, requestException.getErrorCode()); + } + } + // Restarting server to run the test for a second time. + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPost() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setChunkedStreamingMode(0); + OutputStream out = connection.getOutputStream(); + out.write(UPLOAD_DATA); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(UPLOAD_DATA_STRING, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testTransferEncodingHeaderSet() throws Exception { + URL url = new URL(NativeTestServer.getEchoHeaderURL("Transfer-Encoding")); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setChunkedStreamingMode(0); + OutputStream out = connection.getOutputStream(); + out.write(UPLOAD_DATA); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals("chunked", TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostOneMassiveWrite() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setChunkedStreamingMode(0); + OutputStream out = connection.getOutputStream(); + byte[] largeData = TestUtil.getLargeData(); + out.write(largeData); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostWriteOneByte() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setChunkedStreamingMode(0); + OutputStream out = connection.getOutputStream(); + for (int i = 0; i < UPLOAD_DATA.length; i++) { + out.write(UPLOAD_DATA[i]); + } + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(UPLOAD_DATA_STRING, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostOneMassiveWriteWriteOneByte() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setChunkedStreamingMode(0); + OutputStream out = connection.getOutputStream(); + byte[] largeData = TestUtil.getLargeData(); + for (int i = 0; i < largeData.length; i++) { + out.write(largeData[i]); + } + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testPostWholeNumberOfChunks() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + int totalSize = UPLOAD_DATA.length * REPEAT_COUNT; + int chunkSize = 18000; + // Ensure total data size is a multiple of chunk size, so no partial + // chunks will be used. + assertEquals(0, totalSize % chunkSize); + connection.setChunkedStreamingMode(chunkSize); + OutputStream out = connection.getOutputStream(); + byte[] largeData = TestUtil.getLargeData(); + out.write(largeData); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + // Regression testing for crbug.com/618872. + public void testOneMassiveWriteLargerThanInternalBuffer() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + // Use a super big chunk size so that it exceeds the UploadDataProvider + // read buffer size. + byte[] largeData = TestUtil.getLargeData(); + connection.setChunkedStreamingMode(largeData.length); + OutputStream out = connection.getOutputStream(); + out.write(largeData); + assertEquals(200, connection.getResponseCode()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetFixedModeOutputStreamTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetFixedModeOutputStreamTest.java new file mode 100644 index 0000000000..96a2f05c19 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetFixedModeOutputStreamTest.java @@ -0,0 +1,489 @@ +// 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.urlconnection; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.net.CronetTestRule.getContext; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetTestRule; +import org.chromium.net.CronetTestRule.CompareDefaultWithCronet; +import org.chromium.net.CronetTestRule.OnlyRunCronetHttpURLConnection; +import org.chromium.net.NativeTestServer; +import org.chromium.net.NetworkException; +import org.chromium.net.impl.CallbackExceptionImpl; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpRetryException; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * Tests {@code getOutputStream} when {@code setFixedLengthStreamingMode} is + * enabled. + * Tests annotated with {@code CompareDefaultWithCronet} will run once with the + * default HttpURLConnection implementation and then with Cronet's + * HttpURLConnection implementation. Tests annotated with + * {@code OnlyRunCronetHttpURLConnection} only run Cronet's implementation. + * See {@link CronetTestBase#runTest()} for details. + */ +@RunWith(AndroidJUnit4.class) +public class CronetFixedModeOutputStreamTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() throws Exception { + mTestRule.setStreamHandlerFactory(new CronetEngine.Builder(getContext()).build()); + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @After + public void tearDown() throws Exception { + NativeTestServer.shutdownNativeTestServer(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testConnectBeforeWrite() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length); + OutputStream out = connection.getOutputStream(); + connection.connect(); + out.write(TestUtil.UPLOAD_DATA); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(TestUtil.UPLOAD_DATA_STRING, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + // Regression test for crbug.com/687600. + public void testZeroLengthWriteWithNoResponseBody() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setFixedLengthStreamingMode(0); + OutputStream out = connection.getOutputStream(); + out.write(new byte[] {}); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testWriteAfterRequestFailed() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + byte[] largeData = TestUtil.getLargeData(); + connection.setFixedLengthStreamingMode(largeData.length); + OutputStream out = connection.getOutputStream(); + out.write(largeData, 0, 10); + NativeTestServer.shutdownNativeTestServer(); + try { + out.write(largeData, 10, largeData.length - 10); + connection.getResponseCode(); + fail(); + } catch (IOException e) { + // Expected. + if (!mTestRule.testingSystemHttpURLConnection()) { + NetworkException requestException = (NetworkException) e; + assertEquals( + NetworkException.ERROR_CONNECTION_REFUSED, requestException.getErrorCode()); + } + } + connection.disconnect(); + // Restarting server to run the test for a second time. + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testGetResponseAfterWriteFailed() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + NativeTestServer.shutdownNativeTestServer(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + // Set content-length as 1 byte, so Cronet will upload once that 1 byte + // is passed to it. + connection.setFixedLengthStreamingMode(1); + try { + OutputStream out = connection.getOutputStream(); + out.write(1); + // Forces OutputStream implementation to flush. crbug.com/653072 + out.flush(); + // System's implementation is flaky see crbug.com/653072. + if (!mTestRule.testingSystemHttpURLConnection()) { + fail(); + } + } catch (IOException e) { + if (!mTestRule.testingSystemHttpURLConnection()) { + NetworkException requestException = (NetworkException) e; + assertEquals( + NetworkException.ERROR_CONNECTION_REFUSED, requestException.getErrorCode()); + } + } + // Make sure IOException is reported again when trying to read response + // from the connection. + try { + connection.getResponseCode(); + fail(); + } catch (IOException e) { + // Expected. + if (!mTestRule.testingSystemHttpURLConnection()) { + NetworkException requestException = (NetworkException) e; + assertEquals( + NetworkException.ERROR_CONNECTION_REFUSED, requestException.getErrorCode()); + } + } + // Restarting server to run the test for a second time. + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testFixedLengthStreamingModeZeroContentLength() throws Exception { + // Check content length is set. + URL echoLength = new URL(NativeTestServer.getEchoHeaderURL("Content-Length")); + HttpURLConnection connection1 = + (HttpURLConnection) echoLength.openConnection(); + connection1.setDoOutput(true); + connection1.setRequestMethod("POST"); + connection1.setFixedLengthStreamingMode(0); + assertEquals(200, connection1.getResponseCode()); + assertEquals("OK", connection1.getResponseMessage()); + assertEquals("0", TestUtil.getResponseAsString(connection1)); + connection1.disconnect(); + + // Check body is empty. + URL echoBody = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection2 = + (HttpURLConnection) echoBody.openConnection(); + connection2.setDoOutput(true); + connection2.setRequestMethod("POST"); + connection2.setFixedLengthStreamingMode(0); + assertEquals(200, connection2.getResponseCode()); + assertEquals("OK", connection2.getResponseMessage()); + assertEquals("", TestUtil.getResponseAsString(connection2)); + connection2.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testWriteLessThanContentLength() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + // Set a content length that's 1 byte more. + connection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length + 1); + OutputStream out = connection.getOutputStream(); + out.write(TestUtil.UPLOAD_DATA); + try { + connection.getResponseCode(); + fail(); + } catch (IOException e) { + // Expected. + } + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testWriteMoreThanContentLength() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + // Set a content length that's 1 byte short. + connection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length - 1); + OutputStream out = connection.getOutputStream(); + try { + out.write(TestUtil.UPLOAD_DATA); + // On Lollipop, default implementation only triggers the error when reading response. + connection.getInputStream(); + fail(); + } catch (IOException e) { + // Expected. + assertEquals("expected " + (TestUtil.UPLOAD_DATA.length - 1) + " bytes but received " + + TestUtil.UPLOAD_DATA.length, + e.getMessage()); + } + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testWriteMoreThanContentLengthWriteOneByte() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + // Set a content length that's 1 byte short. + connection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length - 1); + OutputStream out = connection.getOutputStream(); + for (int i = 0; i < TestUtil.UPLOAD_DATA.length - 1; i++) { + out.write(TestUtil.UPLOAD_DATA[i]); + } + try { + // Try upload an extra byte. + out.write(TestUtil.UPLOAD_DATA[TestUtil.UPLOAD_DATA.length - 1]); + // On Lollipop, default implementation only triggers the error when reading response. + connection.getInputStream(); + fail(); + } catch (IOException e) { + // Expected. + String expectedVariant = "expected 0 bytes but received 1"; + String expectedVariantOnLollipop = "expected " + (TestUtil.UPLOAD_DATA.length - 1) + + " bytes but received " + TestUtil.UPLOAD_DATA.length; + assertTrue(expectedVariant.equals(e.getMessage()) + || expectedVariantOnLollipop.equals(e.getMessage())); + } + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testFixedLengthStreamingMode() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length); + OutputStream out = connection.getOutputStream(); + out.write(TestUtil.UPLOAD_DATA); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(TestUtil.UPLOAD_DATA_STRING, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testFixedLengthStreamingModeWriteOneByte() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length); + OutputStream out = connection.getOutputStream(); + for (int i = 0; i < TestUtil.UPLOAD_DATA.length; i++) { + // Write one byte at a time. + out.write(TestUtil.UPLOAD_DATA[i]); + } + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(TestUtil.UPLOAD_DATA_STRING, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testFixedLengthStreamingModeLargeData() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + // largeData is 1.8 MB. + byte[] largeData = TestUtil.getLargeData(); + connection.setFixedLengthStreamingMode(largeData.length); + OutputStream out = connection.getOutputStream(); + int totalBytesWritten = 0; + // Number of bytes to write each time. It is doubled each time + // to make sure that the implementation can handle large writes. + int bytesToWrite = 683; + while (totalBytesWritten < largeData.length) { + if (bytesToWrite > largeData.length - totalBytesWritten) { + // Do not write out of bound. + bytesToWrite = largeData.length - totalBytesWritten; + } + out.write(largeData, totalBytesWritten, bytesToWrite); + totalBytesWritten += bytesToWrite; + // About 5th iteration of this loop, bytesToWrite will be bigger than 16384. + bytesToWrite *= 2; + } + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testFixedLengthStreamingModeLargeDataWriteOneByte() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + byte[] largeData = TestUtil.getLargeData(); + connection.setFixedLengthStreamingMode(largeData.length); + OutputStream out = connection.getOutputStream(); + for (int i = 0; i < largeData.length; i++) { + // Write one byte at a time. + out.write(largeData[i]); + } + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testJavaBufferSizeLargerThanNativeBufferSize() throws Exception { + // Set an internal buffer of size larger than the buffer size used + // in network stack internally. + // Normal stream uses 16384, QUIC uses 14520, and SPDY uses 16384. + // Try two different buffer lengths. 17384 will make the last write + // smaller than the native buffer length; 18384 will make the last write + // bigger than the native buffer length + // (largeData.length % 17384 = 9448, largeData.length % 18384 = 16752). + int[] bufferLengths = new int[] {17384, 18384}; + for (int length : bufferLengths) { + CronetFixedModeOutputStream.setDefaultBufferLengthForTesting(length); + // Run the following three tests with this custom buffer size. + testFixedLengthStreamingModeLargeDataWriteOneByte(); + testFixedLengthStreamingModeLargeData(); + testOneMassiveWrite(); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testOneMassiveWrite() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + byte[] largeData = TestUtil.getLargeData(); + connection.setFixedLengthStreamingMode(largeData.length); + OutputStream out = connection.getOutputStream(); + // Write everything at one go, so the data is larger than the buffer + // used in CronetFixedModeOutputStream. + out.write(largeData); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + TestUtil.checkLargeData(TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + private static class CauseMatcher extends TypeSafeMatcher { + private final Class mType; + private final String mExpectedMessage; + private final Class mInnerCauseType; + private final String mInnerCauseExpectedMessage; + + public CauseMatcher(Class type, String expectedMessage, + Class innerCauseType, String innerCauseExpectedMessage) { + this.mType = type; + this.mExpectedMessage = expectedMessage; + this.mInnerCauseType = innerCauseType; + this.mInnerCauseExpectedMessage = innerCauseExpectedMessage; + } + + @Override + protected boolean matchesSafely(Throwable item) { + return item.getClass().isAssignableFrom(mType) + && item.getMessage().equals(mExpectedMessage) + && item.getCause().getClass().isAssignableFrom(mInnerCauseType) + && item.getCause().getMessage().equals(mInnerCauseExpectedMessage); + } + @Override + public void describeTo(Description description) {} + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testRewindWithCronet() throws Exception { + assertFalse(mTestRule.testingSystemHttpURLConnection()); + // Post preserving redirect should fail. + URL url = new URL(NativeTestServer.getRedirectToEchoBody()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setFixedLengthStreamingMode(TestUtil.UPLOAD_DATA.length); + thrown.expectMessage("Cronet Test failed."); + thrown.expectCause(instanceOf(CallbackExceptionImpl.class)); + thrown.expectCause(new CauseMatcher(CallbackExceptionImpl.class, + "Exception received from UploadDataProvider", HttpRetryException.class, + "Cannot retry streamed Http body")); + OutputStream out = connection.getOutputStream(); + out.write(TestUtil.UPLOAD_DATA); + connection.getResponseCode(); + connection.disconnect(); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetHttpURLConnectionTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetHttpURLConnectionTest.java new file mode 100644 index 0000000000..7ca7a6f881 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetHttpURLConnectionTest.java @@ -0,0 +1,1541 @@ +// Copyright 2014 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.urlconnection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.net.CronetTestRule.getContext; + +import android.net.TrafficStats; +import android.os.Build; +import android.os.Process; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.Log; +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetException; +import org.chromium.net.CronetTestRule; +import org.chromium.net.CronetTestRule.CompareDefaultWithCronet; +import org.chromium.net.CronetTestRule.OnlyRunCronetHttpURLConnection; +import org.chromium.net.CronetTestRule.RequiresMinApi; +import org.chromium.net.CronetTestUtil; +import org.chromium.net.MockUrlRequestJobFactory; +import org.chromium.net.NativeTestServer; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Basic tests of Cronet's HttpURLConnection implementation. + * Tests annotated with {@code CompareDefaultWithCronet} will run once with the + * default HttpURLConnection implementation and then with Cronet's + * HttpURLConnection implementation. Tests annotated with + * {@code OnlyRunCronetHttpURLConnection} only run Cronet's implementation. + * See {@link CronetTestBase#runTest()} for details. + */ +@RunWith(AndroidJUnit4.class) +public class CronetHttpURLConnectionTest { + private static final String TAG = CronetHttpURLConnectionTest.class.getSimpleName(); + + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private CronetEngine mCronetEngine; + + @Before + public void setUp() throws Exception { + mCronetEngine = mTestRule.enableDiskCache(new CronetEngine.Builder(getContext())).build(); + mTestRule.setStreamHandlerFactory(mCronetEngine); + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @After + public void tearDown() throws Exception { + NativeTestServer.shutdownNativeTestServer(); + mCronetEngine.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testBasicGet() throws Exception { + URL url = new URL(NativeTestServer.getEchoMethodURL()); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + assertEquals(200, urlConnection.getResponseCode()); + assertEquals("OK", urlConnection.getResponseMessage()); + assertEquals("GET", TestUtil.getResponseAsString(urlConnection)); + urlConnection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + // Regression test for crbug.com/561678. + public void testSetRequestMethod() throws Exception { + URL url = new URL(NativeTestServer.getEchoMethodURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("PUT"); + OutputStream out = connection.getOutputStream(); + out.write("dummy data".getBytes()); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals("PUT", TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testConnectTimeout() throws Exception { + URL url = new URL(NativeTestServer.getEchoMethodURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + // This should not throw an exception. + connection.setConnectTimeout(1000); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals("GET", TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testReadTimeout() throws Exception { + // Add url interceptors. + MockUrlRequestJobFactory mockUrlRequestJobFactory = + new MockUrlRequestJobFactory(mCronetEngine); + URL url = new URL(MockUrlRequestJobFactory.getMockUrlForHangingRead()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setReadTimeout(1000); + assertEquals(200, connection.getResponseCode()); + InputStream in = connection.getInputStream(); + try { + in.read(); + fail(); + } catch (SocketTimeoutException e) { + // Expected + } + connection.disconnect(); + mockUrlRequestJobFactory.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + // Regression test for crbug.com/571436. + public void testDefaultToPostWhenDoOutput() throws Exception { + URL url = new URL(NativeTestServer.getEchoMethodURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + OutputStream out = connection.getOutputStream(); + out.write("dummy data".getBytes()); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals("POST", TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + /** + * Tests that calling {@link HttpURLConnection#connect} will also initialize + * {@code OutputStream} if necessary in the case where + * {@code setFixedLengthStreamingMode} is called. + * Regression test for crbug.com/582975. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testInitOutputStreamInConnect() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + String dataString = "some very important data"; + byte[] data = dataString.getBytes(); + connection.setFixedLengthStreamingMode(data.length); + connection.connect(); + OutputStream out = connection.getOutputStream(); + out.write(data); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(dataString, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + /** + * Tests that calling {@link HttpURLConnection#connect} will also initialize + * {@code OutputStream} if necessary in the case where + * {@code setChunkedStreamingMode} is called. + * Regression test for crbug.com/582975. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testInitChunkedOutputStreamInConnect() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + String dataString = "some very important chunked data"; + byte[] data = dataString.getBytes(); + connection.setChunkedStreamingMode(0); + connection.connect(); + OutputStream out = connection.getOutputStream(); + out.write(data); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(dataString, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testSetFixedLengthStreamingModeLong() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + String dataString = "some very important data"; + byte[] data = dataString.getBytes(); + connection.setFixedLengthStreamingMode((long) data.length); + OutputStream out = connection.getOutputStream(); + out.write(data); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(dataString, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testSetFixedLengthStreamingModeInt() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + String dataString = "some very important data"; + byte[] data = dataString.getBytes(); + connection.setFixedLengthStreamingMode((int) data.length); + OutputStream out = connection.getOutputStream(); + out.write(data); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(dataString, TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testNotFoundURLRequest() throws Exception { + URL url = new URL(NativeTestServer.getFileURL("/notfound.html")); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + assertEquals(404, urlConnection.getResponseCode()); + assertEquals("Not Found", urlConnection.getResponseMessage()); + try { + urlConnection.getInputStream(); + fail(); + } catch (FileNotFoundException e) { + // Expected. + } + InputStream errorStream = urlConnection.getErrorStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int byteRead; + while ((byteRead = errorStream.read()) != -1) { + out.write(byteRead); + } + assertEquals("\n\n\n" + + "Not found\n

Test page loaded.

\n" + + "\n\n", + out.toString()); + urlConnection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testServerNotAvailable() throws Exception { + URL url = new URL(NativeTestServer.getFileURL("/success.txt")); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + assertEquals("this is a text file\n", TestUtil.getResponseAsString(urlConnection)); + // After shutting down the server, the server should not be handling + // new requests. + NativeTestServer.shutdownNativeTestServer(); + HttpURLConnection secondConnection = + (HttpURLConnection) url.openConnection(); + // Default implementation reports this type of error in connect(). + // However, since Cronet's wrapper only receives the error in its listener + // callback when message loop is running, Cronet's wrapper only knows + // about the error when it starts to read response. + try { + secondConnection.getResponseCode(); + fail(); + } catch (IOException e) { + assertTrue(e instanceof java.net.ConnectException || e instanceof CronetException); + assertTrue(e.getMessage().contains("ECONNREFUSED") + || e.getMessage().contains("Connection refused") + || e.getMessage().contains("net::ERR_CONNECTION_REFUSED") + || e.getMessage().contains("Failed to connect")); + } + checkExceptionsAreThrown(secondConnection); + urlConnection.disconnect(); + // Starts the server to avoid crashing on shutdown in tearDown(). + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testBadIP() throws Exception { + URL url = new URL("http://0.0.0.0/"); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + // Default implementation reports this type of error in connect(). + // However, since Cronet's wrapper only receives the error in its listener + // callback when message loop is running, Cronet's wrapper only knows + // about the error when it starts to read response. + try { + urlConnection.getResponseCode(); + fail(); + } catch (IOException e) { + assertTrue(e instanceof java.net.ConnectException || e instanceof CronetException); + assertTrue(e.getMessage().contains("ECONNREFUSED") + || e.getMessage().contains("Connection refused") + || e.getMessage().contains("net::ERR_CONNECTION_REFUSED") + || e.getMessage().contains("Failed to connect")); + } + checkExceptionsAreThrown(urlConnection); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testBadHostname() throws Exception { + URL url = new URL("http://this-weird-host-name-does-not-exist/"); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + // Default implementation reports this type of error in connect(). + // However, since Cronet's wrapper only receives the error in its listener + // callback when message loop is running, Cronet's wrapper only knows + // about the error when it starts to read response. + try { + urlConnection.getResponseCode(); + fail(); + } catch (java.net.UnknownHostException e) { + // Expected. + } catch (CronetException e) { + // Expected. + } + checkExceptionsAreThrown(urlConnection); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testBadScheme() throws Exception { + try { + new URL("flying://goat"); + fail(); + } catch (MalformedURLException e) { + // Expected. + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testDisconnectBeforeConnectionIsMade() throws Exception { + URL url = new URL(NativeTestServer.getEchoMethodURL()); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + // Close connection before connection is made has no effect. + // Default implementation passes this test. + urlConnection.disconnect(); + assertEquals(200, urlConnection.getResponseCode()); + assertEquals("OK", urlConnection.getResponseMessage()); + assertEquals("GET", TestUtil.getResponseAsString(urlConnection)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + // TODO(xunjieli): Currently the wrapper does not throw an exception. + // Need to change the behavior. + // @CompareDefaultWithCronet + public void testDisconnectAfterConnectionIsMade() throws Exception { + URL url = new URL(NativeTestServer.getEchoMethodURL()); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + // Close connection before connection is made has no effect. + urlConnection.connect(); + urlConnection.disconnect(); + try { + urlConnection.getResponseCode(); + fail(); + } catch (Exception e) { + // Ignored. + } + try { + InputStream in = urlConnection.getInputStream(); + fail(); + } catch (Exception e) { + // Ignored. + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testMultipleDisconnect() throws Exception { + URL url = new URL(NativeTestServer.getEchoMethodURL()); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + assertEquals(200, urlConnection.getResponseCode()); + assertEquals("OK", urlConnection.getResponseMessage()); + assertEquals("GET", TestUtil.getResponseAsString(urlConnection)); + // Disconnect multiple times should be fine. + for (int i = 0; i < 10; i++) { + urlConnection.disconnect(); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testAddRequestProperty() throws Exception { + URL url = new URL(NativeTestServer.getEchoAllHeadersURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.addRequestProperty("foo-header", "foo"); + connection.addRequestProperty("bar-header", "bar"); + + // Before connection is made, check request headers are set. + Map> requestHeadersMap = + connection.getRequestProperties(); + List fooValues = requestHeadersMap.get("foo-header"); + assertEquals(1, fooValues.size()); + assertEquals("foo", fooValues.get(0)); + assertEquals("foo", connection.getRequestProperty("foo-header")); + List barValues = requestHeadersMap.get("bar-header"); + assertEquals(1, barValues.size()); + assertEquals("bar", barValues.get(0)); + assertEquals("bar", connection.getRequestProperty("bar-header")); + + // Check the request headers echoed back by the server. + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + String headers = TestUtil.getResponseAsString(connection); + List fooHeaderValues = + getRequestHeaderValues(headers, "foo-header"); + List barHeaderValues = + getRequestHeaderValues(headers, "bar-header"); + assertEquals(1, fooHeaderValues.size()); + assertEquals("foo", fooHeaderValues.get(0)); + assertEquals(1, fooHeaderValues.size()); + assertEquals("bar", barHeaderValues.get(0)); + + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testAddRequestPropertyWithSameKey() throws Exception { + URL url = new URL(NativeTestServer.getEchoAllHeadersURL()); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + urlConnection.addRequestProperty("header-name", "value1"); + try { + urlConnection.addRequestProperty("header-Name", "value2"); + fail(); + } catch (UnsupportedOperationException e) { + assertEquals(e.getMessage(), + "Cannot add multiple headers of the same key, header-Name. " + + "crbug.com/432719."); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testSetRequestPropertyWithSameKey() throws Exception { + URL url = new URL(NativeTestServer.getEchoAllHeadersURL()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + // The test always sets and retrieves one header with the same + // capitalization, and the other header with slightly different + // capitalization. + conn.setRequestProperty("same-capitalization", "yo"); + conn.setRequestProperty("diFFerent-cApitalization", "foo"); + Map> headersMap = conn.getRequestProperties(); + List values1 = headersMap.get("same-capitalization"); + assertEquals(1, values1.size()); + assertEquals("yo", values1.get(0)); + assertEquals("yo", conn.getRequestProperty("same-capitalization")); + + List values2 = headersMap.get("different-capitalization"); + assertEquals(1, values2.size()); + assertEquals("foo", values2.get(0)); + assertEquals("foo", + conn.getRequestProperty("Different-capitalization")); + + // Check request header is updated. + conn.setRequestProperty("same-capitalization", "hi"); + conn.setRequestProperty("different-Capitalization", "bar"); + Map> newHeadersMap = conn.getRequestProperties(); + List newValues1 = newHeadersMap.get("same-capitalization"); + assertEquals(1, newValues1.size()); + assertEquals("hi", newValues1.get(0)); + assertEquals("hi", conn.getRequestProperty("same-capitalization")); + + List newValues2 = newHeadersMap.get("differENT-capitalization"); + assertEquals(1, newValues2.size()); + assertEquals("bar", newValues2.get(0)); + assertEquals("bar", + conn.getRequestProperty("different-capitalization")); + + // Check the request headers echoed back by the server. + assertEquals(200, conn.getResponseCode()); + assertEquals("OK", conn.getResponseMessage()); + String headers = TestUtil.getResponseAsString(conn); + List actualValues1 = + getRequestHeaderValues(headers, "same-capitalization"); + assertEquals(1, actualValues1.size()); + assertEquals("hi", actualValues1.get(0)); + List actualValues2 = + getRequestHeaderValues(headers, "different-Capitalization"); + assertEquals(1, actualValues2.size()); + assertEquals("bar", actualValues2.get(0)); + conn.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testAddAndSetRequestPropertyWithSameKey() throws Exception { + URL url = new URL(NativeTestServer.getEchoAllHeadersURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.addRequestProperty("header-name", "value1"); + connection.setRequestProperty("Header-nAme", "value2"); + + // Before connection is made, check request headers are set. + assertEquals("value2", connection.getRequestProperty("header-namE")); + Map> requestHeadersMap = + connection.getRequestProperties(); + assertEquals(1, requestHeadersMap.get("HeAder-name").size()); + assertEquals("value2", requestHeadersMap.get("HeAder-name").get(0)); + + // Check the request headers echoed back by the server. + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + String headers = TestUtil.getResponseAsString(connection); + List actualValues = + getRequestHeaderValues(headers, "Header-nAme"); + assertEquals(1, actualValues.size()); + assertEquals("value2", actualValues.get(0)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testAddSetRequestPropertyAfterConnected() throws Exception { + URL url = new URL(NativeTestServer.getEchoAllHeadersURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.addRequestProperty("header-name", "value"); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + try { + connection.setRequestProperty("foo", "bar"); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + try { + connection.addRequestProperty("foo", "bar"); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testGetRequestPropertyAfterConnected() throws Exception { + URL url = new URL(NativeTestServer.getEchoAllHeadersURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.addRequestProperty("header-name", "value"); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + + try { + connection.getRequestProperties(); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + + // Default implementation allows querying a particular request property. + try { + assertEquals("value", connection.getRequestProperty("header-name")); + } catch (IllegalStateException e) { + fail(); + } + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testGetRequestPropertiesUnmodifiable() throws Exception { + URL url = new URL(NativeTestServer.getEchoAllHeadersURL()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.addRequestProperty("header-name", "value"); + Map> headers = connection.getRequestProperties(); + try { + headers.put("foo", Arrays.asList("v1", "v2")); + fail(); + } catch (UnsupportedOperationException e) { + // Expected. + } + + List values = headers.get("header-name"); + try { + values.add("v3"); + fail(); + } catch (UnsupportedOperationException e) { + // Expected. + } + + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testInputStreamBatchReadBoundaryConditions() throws Exception { + String testInputString = "this is a very important header"; + URL url = new URL(NativeTestServer.getEchoHeaderURL("foo")); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + urlConnection.addRequestProperty("foo", testInputString); + assertEquals(200, urlConnection.getResponseCode()); + assertEquals("OK", urlConnection.getResponseMessage()); + InputStream in = urlConnection.getInputStream(); + try { + // Negative byteOffset. + int r = in.read(new byte[10], -1, 1); + fail(); + } catch (IndexOutOfBoundsException e) { + // Expected. + } + try { + // Negative byteCount. + int r = in.read(new byte[10], 1, -1); + fail(); + } catch (IndexOutOfBoundsException e) { + // Expected. + } + try { + // Read more than what buffer can hold. + int r = in.read(new byte[10], 0, 11); + fail(); + } catch (IndexOutOfBoundsException e) { + // Expected. + } + urlConnection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testInputStreamReadOneByte() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + // Make the server echo a large request body, so it exceeds the internal + // read buffer. + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + byte[] largeData = TestUtil.getLargeData(); + connection.setFixedLengthStreamingMode(largeData.length); + connection.getOutputStream().write(largeData); + InputStream in = connection.getInputStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int b; + while ((b = in.read()) != -1) { + out.write(b); + } + + // All data has been read. Try reading beyond what is available should give -1. + assertEquals(-1, in.read()); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + String responseData = new String(out.toByteArray()); + TestUtil.checkLargeData(responseData); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testInputStreamReadMoreBytesThanAvailable() throws Exception { + String testInputString = "this is a really long header"; + byte[] testInputBytes = testInputString.getBytes(); + URL url = new URL(NativeTestServer.getEchoHeaderURL("foo")); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + urlConnection.addRequestProperty("foo", testInputString); + assertEquals(200, urlConnection.getResponseCode()); + assertEquals("OK", urlConnection.getResponseMessage()); + InputStream in = urlConnection.getInputStream(); + byte[] actualOutput = new byte[testInputBytes.length + 256]; + int bytesRead = in.read(actualOutput, 0, actualOutput.length); + assertEquals(testInputBytes.length, bytesRead); + byte[] readSomeMore = new byte[10]; + int bytesReadBeyondAvailable = in.read(readSomeMore, 0, 10); + assertEquals(-1, bytesReadBeyondAvailable); + for (int i = 0; i < bytesRead; i++) { + assertEquals(testInputBytes[i], actualOutput[i]); + } + urlConnection.disconnect(); + } + + /** + * Tests batch reading on CronetInputStream when + * {@link CronetHttpURLConnection#getMoreData} is called multiple times. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testBigDataRead() throws Exception { + String data = "MyBigFunkyData"; + int dataLength = data.length(); + int repeatCount = 100000; + MockUrlRequestJobFactory mockUrlRequestJobFactory = + new MockUrlRequestJobFactory(mCronetEngine); + URL url = new URL(MockUrlRequestJobFactory.getMockUrlForData(data, repeatCount)); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + InputStream in = connection.getInputStream(); + byte[] actualOutput = new byte[dataLength * repeatCount]; + int totalBytesRead = 0; + // Number of bytes to read each time. It is incremented by one from 0. + int numBytesToRead = 0; + while (totalBytesRead < actualOutput.length) { + if (actualOutput.length - totalBytesRead < numBytesToRead) { + // Do not read out of bound. + numBytesToRead = actualOutput.length - totalBytesRead; + } + int bytesRead = in.read(actualOutput, totalBytesRead, numBytesToRead); + assertTrue(bytesRead <= numBytesToRead); + totalBytesRead += bytesRead; + numBytesToRead++; + } + + // All data has been read. Try reading beyond what is available should give -1. + assertEquals(0, in.read(actualOutput, 0, 0)); + assertEquals(-1, in.read(actualOutput, 0, 1)); + + String responseData = new String(actualOutput); + for (int i = 0; i < repeatCount; ++i) { + assertEquals(data, responseData.substring(dataLength * i, + dataLength * (i + 1))); + } + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + mockUrlRequestJobFactory.shutdown(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testInputStreamReadExactBytesAvailable() throws Exception { + String testInputString = "this is a really long header"; + byte[] testInputBytes = testInputString.getBytes(); + URL url = new URL(NativeTestServer.getEchoHeaderURL("foo")); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + urlConnection.addRequestProperty("foo", testInputString); + assertEquals(200, urlConnection.getResponseCode()); + assertEquals("OK", urlConnection.getResponseMessage()); + InputStream in = urlConnection.getInputStream(); + byte[] actualOutput = new byte[testInputBytes.length]; + int bytesRead = in.read(actualOutput, 0, actualOutput.length); + urlConnection.disconnect(); + assertEquals(testInputBytes.length, bytesRead); + assertTrue(Arrays.equals(testInputBytes, actualOutput)); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testInputStreamReadLessBytesThanAvailable() throws Exception { + String testInputString = "this is a really long header"; + byte[] testInputBytes = testInputString.getBytes(); + URL url = new URL(NativeTestServer.getEchoHeaderURL("foo")); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + urlConnection.addRequestProperty("foo", testInputString); + assertEquals(200, urlConnection.getResponseCode()); + assertEquals("OK", urlConnection.getResponseMessage()); + InputStream in = urlConnection.getInputStream(); + byte[] firstPart = new byte[testInputBytes.length - 10]; + int firstBytesRead = in.read(firstPart, 0, testInputBytes.length - 10); + byte[] secondPart = new byte[10]; + int secondBytesRead = in.read(secondPart, 0, 10); + assertEquals(testInputBytes.length - 10, firstBytesRead); + assertEquals(10, secondBytesRead); + for (int i = 0; i < firstPart.length; i++) { + assertEquals(testInputBytes[i], firstPart[i]); + } + for (int i = 0; i < secondPart.length; i++) { + assertEquals(testInputBytes[firstPart.length + i], secondPart[i]); + } + urlConnection.disconnect(); + } + + /** + * Makes sure that disconnect while reading from InputStream, the message + * loop does not block. Regression test for crbug.com/550605. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testDisconnectWhileReadingDoesnotBlock() throws Exception { + URL url = new URL(NativeTestServer.getEchoBodyURL()); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + // Make the server echo a large request body, so it exceeds the internal + // read buffer. + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + byte[] largeData = TestUtil.getLargeData(); + connection.setFixedLengthStreamingMode(largeData.length); + OutputStream out = connection.getOutputStream(); + out.write(largeData); + + InputStream in = connection.getInputStream(); + // Read one byte and disconnect. + assertTrue(in.read() != 1); + connection.disconnect(); + // Continue reading, and make sure the message loop will not block. + try { + int b = 0; + while (b != -1) { + b = in.read(); + } + // The response body is big, the connection should be disconnected + // before EOF can be received. + fail(); + } catch (IOException e) { + // Expected. + if (!mTestRule.testingSystemHttpURLConnection()) { + assertEquals("disconnect() called", e.getMessage()); + } + } + // Read once more, and make sure exception is thrown. + try { + in.read(); + fail(); + } catch (IOException e) { + // Expected. + if (!mTestRule.testingSystemHttpURLConnection()) { + assertEquals("disconnect() called", e.getMessage()); + } + } + } + + /** + * Makes sure that {@link UrlRequest.Callback#onFailed} exception is + * propagated when calling read on the input stream. + */ + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testServerHangsUp() throws Exception { + URL url = new URL(NativeTestServer.getExabyteResponseURL()); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + InputStream in = connection.getInputStream(); + // Read one byte and shut down the server. + assertTrue(in.read() != -1); + NativeTestServer.shutdownNativeTestServer(); + // Continue reading, and make sure the message loop will not block. + try { + int b = 0; + while (b != -1) { + b = in.read(); + } + // On KitKat, the default implementation doesn't throw an error. + if (!mTestRule.testingSystemHttpURLConnection()) { + // Server closes the connection before EOF can be received. + fail(); + } + } catch (IOException e) { + // Expected. + // Cronet gives a net::ERR_CONTENT_LENGTH_MISMATCH while the + // default implementation sometimes gives a + // java.net.ProtocolException with "unexpected end of stream" + // message. + } + + // Read once more, and make sure exception is thrown. + try { + in.read(); + // On KitKat, the default implementation doesn't throw an error. + if (!mTestRule.testingSystemHttpURLConnection()) { + fail(); + } + } catch (IOException e) { + // Expected. + // Cronet gives a net::ERR_CONTENT_LENGTH_MISMATCH while the + // default implementation sometimes gives a + // java.net.ProtocolException with "unexpected end of stream" + // message. + } + // Spins up server to avoid crash when shutting it down in tearDown(). + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testFollowRedirects() throws Exception { + URL url = new URL(NativeTestServer.getFileURL("/redirect.html")); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(true); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(NativeTestServer.getFileURL("/success.txt"), + connection.getURL().toString()); + assertEquals("this is a text file\n", TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testDisableRedirects() throws Exception { + URL url = new URL(NativeTestServer.getFileURL("/redirect.html")); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); + // Redirect following control broken in Android Marshmallow: + // https://code.google.com/p/android/issues/detail?id=194495 + if (!mTestRule.testingSystemHttpURLConnection() + || Build.VERSION.SDK_INT != Build.VERSION_CODES.M) { + assertEquals(302, connection.getResponseCode()); + assertEquals("Found", connection.getResponseMessage()); + assertEquals("/success.txt", connection.getHeaderField("Location")); + assertEquals( + NativeTestServer.getFileURL("/redirect.html"), connection.getURL().toString()); + } + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testDisableRedirectsGlobal() throws Exception { + HttpURLConnection.setFollowRedirects(false); + URL url = new URL(NativeTestServer.getFileURL("/redirect.html")); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + // Redirect following control broken in Android Marshmallow: + // https://code.google.com/p/android/issues/detail?id=194495 + if (!mTestRule.testingSystemHttpURLConnection() + || Build.VERSION.SDK_INT != Build.VERSION_CODES.M) { + assertEquals(302, connection.getResponseCode()); + assertEquals("Found", connection.getResponseMessage()); + assertEquals("/success.txt", connection.getHeaderField("Location")); + assertEquals( + NativeTestServer.getFileURL("/redirect.html"), connection.getURL().toString()); + } + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testDisableRedirectsGlobalAfterConnectionIsCreated() throws Exception { + HttpURLConnection.setFollowRedirects(true); + URL url = new URL(NativeTestServer.getFileURL("/redirect.html")); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + // Disabling redirects globally after creating the HttpURLConnection + // object should have no effect on the request. + HttpURLConnection.setFollowRedirects(false); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals(NativeTestServer.getFileURL("/success.txt"), + connection.getURL().toString()); + assertEquals("this is a text file\n", TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + // Cronet does not support reading response body of a 302 response. + public void testDisableRedirectsTryReadBody() throws Exception { + URL url = new URL(NativeTestServer.getFileURL("/redirect.html")); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); + try { + connection.getInputStream(); + fail(); + } catch (IOException e) { + // Expected. + } + assertNull(connection.getErrorStream()); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + // Tests that redirects across the HTTP and HTTPS boundary are not followed. + public void testDoNotFollowRedirectsIfSchemesDontMatch() throws Exception { + URL url = new URL(NativeTestServer.getFileURL("/redirect_invalid_scheme.html")); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(true); + assertEquals(302, connection.getResponseCode()); + assertEquals("Found", connection.getResponseMessage()); + // Behavior changed in Android Marshmallow to not update the URL. + if (mTestRule.testingSystemHttpURLConnection() + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Redirected port is randomized, verify everything but port. + assertEquals(url.getProtocol(), connection.getURL().getProtocol()); + assertEquals(url.getHost(), connection.getURL().getHost()); + assertEquals(url.getFile(), connection.getURL().getFile()); + } else { + // Redirect is not followed, but the url is updated to the Location header. + assertEquals("https://127.0.0.1:8000/success.txt", connection.getURL().toString()); + } + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testGetResponseHeadersAsMap() throws Exception { + URL url = new URL(NativeTestServer.getFileURL("/success.txt")); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + Map> responseHeaders = + connection.getHeaderFields(); + // Make sure response header map is not modifiable. + try { + responseHeaders.put("foo", Arrays.asList("v1", "v2")); + fail(); + } catch (UnsupportedOperationException e) { + // Expected. + } + List contentType = responseHeaders.get("Content-type"); + // Make sure map value is not modifiable as well. + try { + contentType.add("v3"); + fail(); + } catch (UnsupportedOperationException e) { + // Expected. + } + // Make sure map look up is key insensitive. + List contentTypeWithOddCase = + responseHeaders.get("ContENt-tYpe"); + assertEquals(contentType, contentTypeWithOddCase); + + assertEquals(1, contentType.size()); + assertEquals("text/plain", contentType.get(0)); + List accessControl = + responseHeaders.get("Access-Control-Allow-Origin"); + assertEquals(1, accessControl.size()); + assertEquals("*", accessControl.get(0)); + List singleHeader = responseHeaders.get("header-name"); + assertEquals(1, singleHeader.size()); + assertEquals("header-value", singleHeader.get(0)); + List multiHeader = responseHeaders.get("multi-header-name"); + assertEquals(2, multiHeader.size()); + assertEquals("header-value1", multiHeader.get(0)); + assertEquals("header-value2", multiHeader.get(1)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testGetResponseHeaderField() throws Exception { + URL url = new URL(NativeTestServer.getFileURL("/success.txt")); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + assertEquals("text/plain", connection.getHeaderField("Content-Type")); + assertEquals("*", + connection.getHeaderField("Access-Control-Allow-Origin")); + assertEquals("header-value", connection.getHeaderField("header-name")); + // If there are multiple headers with the same name, the last should be + // returned. + assertEquals("header-value2", + connection.getHeaderField("multi-header-name")); + // Lastly, make sure lookup is case-insensitive. + assertEquals("header-value2", + connection.getHeaderField("MUlTi-heAder-name")); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testGetResponseHeaderFieldWithPos() throws Exception { + URL url = new URL(NativeTestServer.getFileURL("/success.txt")); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + assertEquals("Content-Type", connection.getHeaderFieldKey(0)); + assertEquals("text/plain", connection.getHeaderField(0)); + assertEquals("Access-Control-Allow-Origin", + connection.getHeaderFieldKey(1)); + assertEquals("*", connection.getHeaderField(1)); + assertEquals("header-name", connection.getHeaderFieldKey(2)); + assertEquals("header-value", connection.getHeaderField(2)); + assertEquals("multi-header-name", connection.getHeaderFieldKey(3)); + assertEquals("header-value1", connection.getHeaderField(3)); + assertEquals("multi-header-name", connection.getHeaderFieldKey(4)); + assertEquals("header-value2", connection.getHeaderField(4)); + // Note that the default implementation also adds additional response + // headers, which are not tested here. + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + // The default implementation adds additional response headers, so this test + // only tests Cronet's implementation. + public void testGetResponseHeaderFieldWithPosExceed() throws Exception { + URL url = new URL(NativeTestServer.getFileURL("/success.txt")); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + // Expect null if we exceed the number of header entries. + assertEquals(null, connection.getHeaderFieldKey(5)); + assertEquals(null, connection.getHeaderField(5)); + assertEquals(null, connection.getHeaderFieldKey(6)); + assertEquals(null, connection.getHeaderField(6)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + // Test that Cronet strips content-encoding header. + public void testStripContentEncoding() throws Exception { + URL url = new URL(NativeTestServer.getFileURL("/gzipped.html")); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + assertEquals("foo", connection.getHeaderFieldKey(0)); + assertEquals("bar", connection.getHeaderField(0)); + assertEquals(null, connection.getHeaderField("content-encoding")); + Map> responseHeaders = connection.getHeaderFields(); + assertEquals(1, responseHeaders.size()); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + // Make sure Cronet decodes the gzipped content. + assertEquals("Hello, World!", TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + private static enum CacheSetting { USE_CACHE, DONT_USE_CACHE }; + + private static enum ExpectedOutcome { SUCCESS, FAILURE }; + + /** + * Helper method to make a request with cache enabled or disabled, and check + * whether the request is successful. + * @param requestUrl request url. + * @param cacheSetting indicates cache should be used. + * @param outcome indicates request is expected to be successful. + */ + private void checkRequestCaching(String requestUrl, + CacheSetting cacheSetting, + ExpectedOutcome outcome) throws Exception { + URL url = new URL(requestUrl); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setUseCaches(cacheSetting == CacheSetting.USE_CACHE); + if (outcome == ExpectedOutcome.SUCCESS) { + assertEquals(200, connection.getResponseCode()); + assertEquals("this is a cacheable file\n", TestUtil.getResponseAsString(connection)); + } else { + try { + connection.getResponseCode(); + fail(); + } catch (IOException e) { + // Expected. + } + } + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + // Strangely, the default implementation fails to return a cached response. + // If the server is shut down, the request just fails with a connection + // refused error. Therefore, this test and the next only runs Cronet. + public void testSetUseCaches() throws Exception { + String url = NativeTestServer.getFileURL("/cacheable.txt"); + checkRequestCaching(url, + CacheSetting.USE_CACHE, ExpectedOutcome.SUCCESS); + // Shut down the server, we should be able to receive a cached response. + NativeTestServer.shutdownNativeTestServer(); + checkRequestCaching(url, + CacheSetting.USE_CACHE, ExpectedOutcome.SUCCESS); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + public void testSetUseCachesFalse() throws Exception { + String url = NativeTestServer.getFileURL("/cacheable.txt"); + checkRequestCaching(url, + CacheSetting.USE_CACHE, ExpectedOutcome.SUCCESS); + NativeTestServer.shutdownNativeTestServer(); + // Disables caching. No cached response is received. + checkRequestCaching(url, + CacheSetting.DONT_USE_CACHE, ExpectedOutcome.FAILURE); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection + // Tests that if disconnect() is called on a different thread when + // getResponseCode() is still waiting for response, there is no + // NPE but only IOException. + // Regression test for crbug.com/751786 + public void testDisconnectWhenGetResponseCodeIsWaiting() throws Exception { + ServerSocket hangingServer = new ServerSocket(0); + URL url = new URL("http://localhost:" + hangingServer.getLocalPort()); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + // connect() is non-blocking. This is to make sure disconnect() triggers cancellation. + connection.connect(); + FutureTask task = new FutureTask(new Callable() { + @Override + public IOException call() { + try { + connection.getResponseCode(); + } catch (IOException e) { + return e; + } + return null; + } + }); + new Thread(task).start(); + Socket s = hangingServer.accept(); + connection.disconnect(); + IOException e = task.get(); + assertEquals("disconnect() called", e.getMessage()); + s.close(); + hangingServer.close(); + } + + private void checkExceptionsAreThrown(HttpURLConnection connection) + throws Exception { + try { + connection.getInputStream(); + fail(); + } catch (IOException e) { + // Expected. + } + + try { + connection.getResponseCode(); + fail(); + } catch (IOException e) { + // Expected. + } + + try { + connection.getResponseMessage(); + fail(); + } catch (IOException e) { + // Expected. + } + + // Default implementation of getHeaderFields() returns null on K, but + // returns an empty map on L. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Map> headers = connection.getHeaderFields(); + assertNotNull(headers); + assertTrue(headers.isEmpty()); + } + // Skip getHeaderFields(), since it can return null or an empty map. + assertNull(connection.getHeaderField("foo")); + assertNull(connection.getHeaderFieldKey(0)); + assertNull(connection.getHeaderField(0)); + + // getErrorStream() does not have a throw clause, it returns null if + // there's an exception. + InputStream errorStream = connection.getErrorStream(); + assertNull(errorStream); + } + + /** + * Helper method to extract a list of header values with the give header + * name. + */ + private List getRequestHeaderValues(String allHeaders, + String headerName) { + Pattern pattern = Pattern.compile(headerName + ":\\s(.*)\\r\\n"); + Matcher matcher = pattern.matcher(allHeaders); + List headerValues = new ArrayList(); + while (matcher.find()) { + headerValues.add(matcher.group(1)); + } + return headerValues; + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @RequiresMinApi(9) // Tagging support added in API level 9: crrev.com/c/chromium/src/+/930086 + public void testTagging() throws Exception { + if (!CronetTestUtil.nativeCanGetTaggedBytes()) { + Log.i(TAG, "Skipping test - GetTaggedBytes unsupported."); + return; + } + URL url = new URL(NativeTestServer.getEchoMethodURL()); + + // Test untagged requests are given tag 0. + int tag = 0; + long priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + CronetHttpURLConnection urlConnection = (CronetHttpURLConnection) url.openConnection(); + assertEquals(200, urlConnection.getResponseCode()); + urlConnection.disconnect(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + + // Test explicit tagging. + tag = 0x12345678; + priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + urlConnection = (CronetHttpURLConnection) url.openConnection(); + urlConnection.setTrafficStatsTag(tag); + assertEquals(200, urlConnection.getResponseCode()); + urlConnection.disconnect(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + + // Test a different tag value. + tag = 0x87654321; + priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + urlConnection = (CronetHttpURLConnection) url.openConnection(); + urlConnection.setTrafficStatsTag(tag); + assertEquals(200, urlConnection.getResponseCode()); + urlConnection.disconnect(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + + // Test tagging with TrafficStats. + tag = 0x12348765; + priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + urlConnection = (CronetHttpURLConnection) url.openConnection(); + TrafficStats.setThreadStatsTag(tag); + assertEquals(200, urlConnection.getResponseCode()); + urlConnection.disconnect(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + + // Test tagging with our UID. + // NOTE(pauljensen): Explicitly setting the UID to the current UID isn't a particularly + // thorough test of this API but at least provides coverage of the underlying code, and + // verifies that traffic is still properly attributed. + // The code path for UID is parallel to that for the tag, which we do have more thorough + // testing for. More thorough testing of setting the UID would require running tests with + // a rare permission which isn't realistic for most apps. Apps are allowed to set the UID + // to their own UID as per this logic in the tagging kernel module: + // https://android.googlesource.com/kernel/common/+/21dd5d7/net/netfilter/xt_qtaguid.c#154 + tag = 0; + priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + urlConnection = (CronetHttpURLConnection) url.openConnection(); + urlConnection.setTrafficStatsUid(Process.myUid()); + assertEquals(200, urlConnection.getResponseCode()); + urlConnection.disconnect(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + + // TrafficStats.getThreadStatsUid() which is required for this feature is added in API level + // 28. + // Note, currently this part won't run as CronetTestUtil.nativeCanGetTaggedBytes() will + // return false on P+. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + tag = 0; + priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag); + urlConnection = (CronetHttpURLConnection) url.openConnection(); + TrafficStats.setThreadStatsUid(Process.myUid()); + assertEquals(200, urlConnection.getResponseCode()); + urlConnection.disconnect(); + assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @CompareDefaultWithCronet + public void testIOExceptionErrorRethrown() throws Exception { + // URL that should fail to connect. + URL url = new URL("http://localhost"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + // This should not throw, even though internally it may encounter an exception. + connection.getHeaderField("blah"); + try { + // This should throw an IOException. + connection.getResponseCode(); + fail(); + } catch (IOException e) { + // Expected + } + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection // System impl flakily responds to interrupt. + public void testIOExceptionInterruptRethrown() throws Exception { + ServerSocket hangingServer = new ServerSocket(0); + URL url = new URL("http://localhost:" + hangingServer.getLocalPort()); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + // connect() is non-blocking. + connection.connect(); + FutureTask task = new FutureTask(new Callable() { + @Override + public IOException call() { + // This should not throw, even though internally it may encounter an exception. + connection.getHeaderField("blah"); + try { + // This should throw an InterruptedIOException. + connection.getResponseCode(); + } catch (InterruptedIOException e) { + // Expected + return e; + } catch (IOException e) { + return null; + } + return null; + } + }); + Thread t = new Thread(task); + t.start(); + Socket s = hangingServer.accept(); + hangingServer.close(); + Thread.sleep(100); // Give time for thread to get blocked, so interrupt is noticed. + // This will trigger an InterruptException in getHeaderField() and getResponseCode(). + // getHeaderField() should not re-throw it. getResponseCode() should re-throw it as an + // InterruptedIOException. + t.interrupt(); + // Make sure an IOException is thrown. + assertNotNull(task.get()); + s.close(); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunCronetHttpURLConnection // Not interested in system crashes. + // Regression test for crashes in disconnect() impl. + public void testCancelRace() throws Exception { + URL url = new URL(NativeTestServer.getEchoMethodURL()); + final HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + final AtomicBoolean connected = new AtomicBoolean(); + // Start request on another thread. + new Thread(new Runnable() { + @Override + public void run() { + try { + assertEquals(200, urlConnection.getResponseCode()); + } catch (IOException e) { + } + connected.set(true); + } + }) + .start(); + // Repeatedly call disconnect(). This used to crash. + do { + urlConnection.disconnect(); + } while (!connected.get()); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetHttpURLStreamHandlerTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetHttpURLStreamHandlerTest.java new file mode 100644 index 0000000000..df721f977b --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetHttpURLStreamHandlerTest.java @@ -0,0 +1,113 @@ +// 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.urlconnection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static org.chromium.net.CronetTestRule.getContext; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule; +import org.chromium.net.CronetTestRule.CronetTestFramework; +import org.chromium.net.NativeTestServer; + +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URL; + +/** + * Tests for CronetHttpURLStreamHandler class. + */ +@RunWith(AndroidJUnit4.class) +public class CronetHttpURLStreamHandlerTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private CronetTestFramework mTestFramework; + + @Before + public void setUp() throws Exception { + mTestFramework = mTestRule.startCronetTestFramework(); + assertTrue(NativeTestServer.startNativeTestServer(getContext())); + } + + @After + public void tearDown() throws Exception { + NativeTestServer.shutdownNativeTestServer(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testOpenConnectionHttp() throws Exception { + URL url = new URL(NativeTestServer.getEchoMethodURL()); + CronetHttpURLStreamHandler streamHandler = + new CronetHttpURLStreamHandler(mTestFramework.mCronetEngine); + HttpURLConnection connection = + (HttpURLConnection) streamHandler.openConnection(url); + assertEquals(200, connection.getResponseCode()); + assertEquals("OK", connection.getResponseMessage()); + assertEquals("GET", TestUtil.getResponseAsString(connection)); + connection.disconnect(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testOpenConnectionHttps() throws Exception { + URL url = new URL("https://example.com"); + CronetHttpURLStreamHandler streamHandler = + new CronetHttpURLStreamHandler(mTestFramework.mCronetEngine); + HttpURLConnection connection = + (HttpURLConnection) streamHandler.openConnection(url); + assertNotNull(connection); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testOpenConnectionProtocolNotSupported() throws Exception { + URL url = new URL("ftp://example.com"); + CronetHttpURLStreamHandler streamHandler = + new CronetHttpURLStreamHandler(mTestFramework.mCronetEngine); + try { + streamHandler.openConnection(url); + fail(); + } catch (UnsupportedOperationException e) { + assertEquals("Unexpected protocol:ftp", e.getMessage()); + } + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testOpenConnectionWithProxy() throws Exception { + URL url = new URL(NativeTestServer.getEchoMethodURL()); + CronetHttpURLStreamHandler streamHandler = + new CronetHttpURLStreamHandler(mTestFramework.mCronetEngine); + Proxy proxy = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress("127.0.0.1", 8080)); + try { + streamHandler.openConnection(url, proxy); + fail(); + } catch (UnsupportedOperationException e) { + // Expected. + } + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetInputStreamTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetInputStreamTest.java new file mode 100644 index 0000000000..bc558c6ff6 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetInputStreamTest.java @@ -0,0 +1,156 @@ +// Copyright 2021 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.urlconnection; + +import static com.google.common.truth.Truth.assertThat; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.Callable; + +/** Test for {@link CronetInputStream}. */ +@RunWith(AndroidJUnit4.class) +public class CronetInputStreamTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + // public to squelch lint warning about naming + public CronetInputStream underTest; + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testAvailable_closed_withoutException() throws Exception { + underTest = new CronetInputStream(new MockHttpURLConnection()); + + underTest.setResponseDataCompleted(null); + + assertThat(underTest.available()).isEqualTo(0); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testAvailable_closed_withException() throws Exception { + underTest = new CronetInputStream(new MockHttpURLConnection()); + IOException expected = new IOException(); + underTest.setResponseDataCompleted(expected); + + IOException actual = assertThrowsIoException(() -> underTest.available()); + + assertThat(actual).isSameInstanceAs(expected); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testAvailable_noReads() throws Exception { + underTest = new CronetInputStream(new MockHttpURLConnection()); + + assertThat(underTest.available()).isEqualTo(0); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testAvailable_everythingRead() throws Exception { + int bytesInBuffer = 10; + + underTest = new CronetInputStream(new MockHttpURLConnection(bytesInBuffer)); + + for (int i = 0; i < bytesInBuffer; i++) { + underTest.read(); + } + + assertThat(underTest.available()).isEqualTo(0); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testAvailable_partiallyRead() throws Exception { + int bytesInBuffer = 10; + int consumed = 3; + + underTest = new CronetInputStream(new MockHttpURLConnection(bytesInBuffer)); + + for (int i = 0; i < consumed; i++) { + underTest.read(); + } + + assertThat(underTest.available()).isEqualTo(bytesInBuffer - consumed); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testRead_afterDataCompleted() throws Exception { + int bytesInBuffer = 10; + int consumed = 3; + + underTest = new CronetInputStream(new MockHttpURLConnection(bytesInBuffer)); + + for (int i = 0; i < consumed; i++) { + underTest.read(); + } + + IOException expected = new IOException(); + underTest.setResponseDataCompleted(expected); + + IOException actual = assertThrowsIoException(() -> underTest.read()); + + assertThat(actual).isSameInstanceAs(expected); + } + + private static IOException assertThrowsIoException(Callable callable) throws Exception { + try { + callable.call(); + } catch (IOException e) { + return e; + } catch (Exception e) { + throw e; + } + throw new AssertionError("No exception was thrown!"); + } + + private static class MockHttpURLConnection extends CronetHttpURLConnection { + private final int mBytesToFill; + private boolean mGetMoreDataExpected; + + MockHttpURLConnection() { + super(null, null); + this.mBytesToFill = 0; + mGetMoreDataExpected = false; + } + + MockHttpURLConnection(int bytesToFill) { + super(null, null); + this.mBytesToFill = bytesToFill; + mGetMoreDataExpected = true; + } + + @Override + public void getMoreData(ByteBuffer buffer) { + if (!mGetMoreDataExpected) { + throw new IllegalStateException("getMoreData call not expected!"); + } + mGetMoreDataExpected = false; + for (int i = 0; i < mBytesToFill; i++) { + buffer.put((byte) 0); + } + } + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetURLStreamHandlerFactoryTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetURLStreamHandlerFactoryTest.java new file mode 100644 index 0000000000..5063c2c668 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/CronetURLStreamHandlerFactoryTest.java @@ -0,0 +1,42 @@ +// 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.urlconnection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule; + +/** + * Test for CronetURLStreamHandlerFactory. + */ +@RunWith(AndroidJUnit4.class) +@SuppressWarnings("deprecation") +public class CronetURLStreamHandlerFactoryTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testRequireConfig() throws Exception { + mTestRule.startCronetTestFramework(); + try { + new CronetURLStreamHandlerFactory(null); + fail(); + } catch (NullPointerException e) { + assertEquals("CronetEngine is null.", e.getMessage()); + } + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/MessageLoopTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/MessageLoopTest.java new file mode 100644 index 0000000000..118c758f75 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/MessageLoopTest.java @@ -0,0 +1,156 @@ +// Copyright 2014 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.urlconnection; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetTestRule; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.SocketTimeoutException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; + +/** + * Tests the MessageLoop implementation. + */ +@RunWith(AndroidJUnit4.class) +public class MessageLoopTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private Thread mTestThread; + private final ExecutorService mExecutorService = + Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); + private class ExecutorThreadFactory implements ThreadFactory { + @Override + public Thread newThread(Runnable r) { + mTestThread = new Thread(r); + return mTestThread; + } + } + private boolean mFailed; + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testInterrupt() throws Exception { + final MessageLoop loop = new MessageLoop(); + assertFalse(loop.isRunning()); + Future future = mExecutorService.submit(new Runnable() { + @Override + public void run() { + try { + loop.loop(); + mFailed = true; + } catch (IOException e) { + // Expected interrupt. + } + } + }); + Thread.sleep(1000); + assertTrue(loop.isRunning()); + assertFalse(loop.hasLoopFailed()); + mTestThread.interrupt(); + future.get(); + assertFalse(loop.isRunning()); + assertTrue(loop.hasLoopFailed()); + assertFalse(mFailed); + // Re-spinning the message loop is not allowed after interrupt. + mExecutorService.submit(new Runnable() { + @Override + public void run() { + try { + loop.loop(); + fail(); + } catch (Exception e) { + if (!(e instanceof InterruptedIOException)) { + fail(); + } + } + } + }).get(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testTaskFailed() throws Exception { + final MessageLoop loop = new MessageLoop(); + assertFalse(loop.isRunning()); + Future future = mExecutorService.submit(new Runnable() { + @Override + public void run() { + try { + loop.loop(); + mFailed = true; + } catch (Exception e) { + if (!(e instanceof NullPointerException)) { + mFailed = true; + } + } + } + }); + Runnable failedTask = new Runnable() { + @Override + public void run() { + throw new NullPointerException(); + } + }; + Thread.sleep(1000); + assertTrue(loop.isRunning()); + assertFalse(loop.hasLoopFailed()); + loop.execute(failedTask); + future.get(); + assertFalse(loop.isRunning()); + assertTrue(loop.hasLoopFailed()); + assertFalse(mFailed); + // Re-spinning the message loop is not allowed after exception. + mExecutorService.submit(new Runnable() { + @Override + public void run() { + try { + loop.loop(); + fail(); + } catch (Exception e) { + if (!(e instanceof NullPointerException)) { + fail(); + } + } + } + }).get(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + public void testLoopWithTimeout() throws Exception { + final MessageLoop loop = new MessageLoop(); + assertFalse(loop.isRunning()); + // The MessageLoop queue is empty. Use a timeout of 100ms to check that + // it doesn't block forever. + try { + loop.loop(100); + fail(); + } catch (SocketTimeoutException e) { + // Expected. + } + assertFalse(loop.isRunning()); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/QuicUploadTest.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/QuicUploadTest.java new file mode 100644 index 0000000000..74e56cee07 --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/QuicUploadTest.java @@ -0,0 +1,91 @@ +// Copyright 2016 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.urlconnection; + +import static org.junit.Assert.assertEquals; + +import static org.chromium.net.CronetTestRule.getContext; + +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.base.test.util.Feature; +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetTestRule; +import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; +import org.chromium.net.CronetTestUtil; +import org.chromium.net.ExperimentalCronetEngine; +import org.chromium.net.QuicTestServer; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; + +/** + * Tests HttpURLConnection upload using QUIC. + */ +@RunWith(AndroidJUnit4.class) +public class QuicUploadTest { + @Rule + public final CronetTestRule mTestRule = new CronetTestRule(); + + private CronetEngine mCronetEngine; + + @Before + public void setUp() throws Exception { + // Load library first to create MockCertVerifier. + System.loadLibrary("cronet_tests"); + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(getContext()); + + QuicTestServer.startQuicTestServer(getContext()); + + builder.enableQuic(true); + JSONObject hostResolverParams = CronetTestUtil.generateHostResolverRules(); + JSONObject experimentalOptions = new JSONObject() + .put("HostResolverRules", hostResolverParams); + builder.setExperimentalOptions(experimentalOptions.toString()); + + builder.addQuicHint(QuicTestServer.getServerHost(), QuicTestServer.getServerPort(), + QuicTestServer.getServerPort()); + + CronetTestUtil.setMockCertVerifierForTesting( + builder, QuicTestServer.createMockCertVerifier()); + + mCronetEngine = builder.build(); + } + + @Test + @SmallTest + @Feature({"Cronet"}) + @OnlyRunNativeCronet + // Regression testing for crbug.com/618872. + public void testOneMassiveWrite() throws Exception { + String path = "/simple.txt"; + URL url = new URL(QuicTestServer.getServerURL() + path); + HttpURLConnection connection = (HttpURLConnection) mCronetEngine.openConnection(url); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + // Size is chosen so the last time mBuffer will be written 14831 bytes, + // which is larger than the internal QUIC read buffer size of 14520. + byte[] largeData = new byte[195055]; + Arrays.fill(largeData, "a".getBytes("UTF-8")[0]); + connection.setFixedLengthStreamingMode(largeData.length); + OutputStream out = connection.getOutputStream(); + // Write everything at one go, so the data is larger than the buffer + // used in CronetFixedModeOutputStream. + out.write(largeData); + assertEquals(200, connection.getResponseCode()); + connection.disconnect(); + } +} diff --git a/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/TestUtil.java b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/TestUtil.java new file mode 100644 index 0000000000..e1957901fa --- /dev/null +++ b/src/components/cronet/android/test/javatests/src/org/chromium/net/urlconnection/TestUtil.java @@ -0,0 +1,56 @@ +// 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.urlconnection; + +import org.junit.Assert; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.HttpURLConnection; + +/** + * Helper functions and fields used in Cronet's HttpURLConnection tests. + */ +public class TestUtil { + static final String UPLOAD_DATA_STRING = "Nifty upload data!"; + static final byte[] UPLOAD_DATA = UPLOAD_DATA_STRING.getBytes(); + static final int REPEAT_COUNT = 100000; + + /** + * Helper method to extract response body as a string for testing. + */ + static String getResponseAsString(HttpURLConnection connection) throws Exception { + InputStream in = connection.getInputStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int b; + while ((b = in.read()) != -1) { + out.write(b); + } + return out.toString(); + } + + /** + * Produces a byte array that contains {@code REPEAT_COUNT} of + * {@code UPLOAD_DATA_STRING}. + */ + static byte[] getLargeData() { + byte[] largeData = new byte[REPEAT_COUNT * UPLOAD_DATA.length]; + for (int i = 0; i < REPEAT_COUNT; i++) { + System.arraycopy(UPLOAD_DATA, 0, largeData, i * UPLOAD_DATA.length, UPLOAD_DATA.length); + } + return largeData; + } + + /** + * Helper function to check whether {@code data} is a concatenation of + * {@code REPEAT_COUNT} {@code UPLOAD_DATA_STRING} strings. + */ + static void checkLargeData(String data) { + for (int i = 0; i < REPEAT_COUNT; i++) { + Assert.assertEquals(UPLOAD_DATA_STRING, data.substring(UPLOAD_DATA_STRING.length() * i, + UPLOAD_DATA_STRING.length() * (i + 1))); + } + } +} diff --git a/src/components/cronet/android/test/mock_cert_verifier.cc b/src/components/cronet/android/test/mock_cert_verifier.cc new file mode 100644 index 0000000000..e66c531783 --- /dev/null +++ b/src/components/cronet/android/test/mock_cert_verifier.cc @@ -0,0 +1,83 @@ +// 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 +#include + +#include "base/android/jni_android.h" +#include "base/android/jni_array.h" +#include "base/android/jni_string.h" +#include "base/test/test_support_android.h" +#include "components/cronet/android/cronet_tests_jni_headers/MockCertVerifier_jni.h" +#include "crypto/sha2.h" +#include "net/base/net_errors.h" +#include "net/cert/asn1_util.h" +#include "net/cert/cert_verifier.h" +#include "net/cert/cert_verify_result.h" +#include "net/cert/mock_cert_verifier.h" +#include "net/cert/x509_util.h" +#include "net/test/cert_test_util.h" +#include "net/test/test_data_directory.h" + +using base::android::JavaParamRef; + +namespace cronet { + +namespace { + +// Populates |out_hash_value| with the SHA256 hash of the |cert| public key. +// Returns true on success. +static bool CalculatePublicKeySha256(const net::X509Certificate& cert, + net::HashValue* out_hash_value) { + // Extract the public key from the cert. + base::StringPiece spki_bytes; + if (!net::asn1::ExtractSPKIFromDERCert( + net::x509_util::CryptoBufferAsStringPiece(cert.cert_buffer()), + &spki_bytes)) { + LOG(INFO) << "Unable to retrieve the public key from the DER cert"; + return false; + } + // Calculate SHA256 hash of public key bytes. + *out_hash_value = net::HashValue(net::HASH_VALUE_SHA256); + crypto::SHA256HashString(spki_bytes, out_hash_value->data(), + crypto::kSHA256Length); + return true; +} + +} // namespace + +static jlong JNI_MockCertVerifier_CreateMockCertVerifier( + JNIEnv* env, + const JavaParamRef& jcerts, + const jboolean jknown_root, + const JavaParamRef& jtest_data_dir) { + base::FilePath test_data_dir( + base::android::ConvertJavaStringToUTF8(env, jtest_data_dir)); + base::InitAndroidTestPaths(test_data_dir); + + std::vector certs; + base::android::AppendJavaStringArrayToStringVector(env, jcerts, &certs); + net::MockCertVerifier* mock_cert_verifier = new net::MockCertVerifier(); + for (const auto& cert : certs) { + net::CertVerifyResult verify_result; + verify_result.verified_cert = + net::ImportCertFromFile(net::GetTestCertsDirectory(), cert); + + // By default, HPKP verification is enabled for known trust roots only. + verify_result.is_issued_by_known_root = jknown_root; + + // Calculate the public key hash and add it to the verify_result. + net::HashValue hashValue; + CHECK(CalculatePublicKeySha256(*verify_result.verified_cert.get(), + &hashValue)); + verify_result.public_key_hashes.push_back(hashValue); + + mock_cert_verifier->AddResultForCert(verify_result.verified_cert.get(), + verify_result, net::OK); + } + + return reinterpret_cast(mock_cert_verifier); +} + +} // namespace cronet diff --git a/src/components/cronet/android/test/mock_url_request_job_factory.cc b/src/components/cronet/android/test/mock_url_request_job_factory.cc new file mode 100644 index 0000000000..7eedfff1b8 --- /dev/null +++ b/src/components/cronet/android/test/mock_url_request_job_factory.cc @@ -0,0 +1,138 @@ +// Copyright 2014 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 "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/bind.h" +#include "base/memory/raw_ptr.h" +#include "components/cronet/android/cronet_tests_jni_headers/MockUrlRequestJobFactory_jni.h" +#include "components/cronet/android/test/cronet_test_util.h" +#include "components/cronet/android/test/url_request_intercepting_job_factory.h" +#include "net/test/url_request/ssl_certificate_error_job.h" +#include "net/test/url_request/url_request_failed_job.h" +#include "net/test/url_request/url_request_hanging_read_job.h" +#include "net/test/url_request/url_request_mock_data_job.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_filter.h" +#include "url/gurl.h" + +using base::android::JavaParamRef; +using base::android::ScopedJavaLocalRef; + +namespace cronet { + +// Intercept URLRequestJob creation using URLRequestFilter from +// libcronet_tests.so +class UrlInterceptorJobFactoryHandle { + public: + // |jcontext_adapter| points to a URLRequestContextAdapater. + UrlInterceptorJobFactoryHandle(jlong jcontext_adapter) + : jcontext_adapter_(jcontext_adapter) { + TestUtil::RunAfterContextInit( + jcontext_adapter, + base::BindOnce(&UrlInterceptorJobFactoryHandle::InitOnNetworkThread, + base::Unretained(this))); + } + // Should only be called on network thread; other threads should use + // ShutDown(). + ~UrlInterceptorJobFactoryHandle() { + DCHECK( + TestUtil::GetTaskRunner(jcontext_adapter_)->BelongsToCurrentThread()); + TestUtil::GetURLRequestContext(jcontext_adapter_) + ->set_job_factory(old_job_factory_); + } + + void ShutDown() { + TestUtil::RunAfterContextInit( + jcontext_adapter_, + base::BindOnce(&UrlInterceptorJobFactoryHandle::ShutdownOnNetworkThread, + base::Unretained(this))); + } + + private: + void InitOnNetworkThread() { + net::URLRequestContext* request_context = + TestUtil::GetURLRequestContext(jcontext_adapter_); + old_job_factory_ = request_context->job_factory(); + new_job_factory_.reset(new URLRequestInterceptingJobFactory( + const_cast(old_job_factory_.get()), + net::URLRequestFilter::GetInstance())); + request_context->set_job_factory(new_job_factory_.get()); + } + + void ShutdownOnNetworkThread() { delete this; } + + // The URLRequestContextAdapater this object intercepts from. + const jlong jcontext_adapter_; + // URLRequestJobFactory previously used in URLRequestContext. + raw_ptr old_job_factory_; + // URLRequestJobFactory inserted during tests to intercept URLRequests with + // libcronet's URLRequestFilter. + std::unique_ptr new_job_factory_; +}; + +// URL interceptors are registered with the URLRequestFilter in +// libcronet_tests.so. However tests are run on libcronet.so. Use the +// URLRequestFilter in libcronet_tests.so with the URLRequestContext in +// libcronet.so by installing a URLRequestInterceptingJobFactory +// that calls into libcronet_tests.so's URLRequestFilter. +jlong JNI_MockUrlRequestJobFactory_AddUrlInterceptors( + JNIEnv* env, + jlong jcontext_adapter) { + net::URLRequestMockDataJob::AddUrlHandler(); + net::URLRequestFailedJob::AddUrlHandler(); + net::URLRequestHangingReadJob::AddUrlHandler(); + net::SSLCertificateErrorJob::AddUrlHandler(); + return reinterpret_cast( + new UrlInterceptorJobFactoryHandle(jcontext_adapter)); +} + +// Put back the old URLRequestJobFactory into the URLRequestContext. +void JNI_MockUrlRequestJobFactory_RemoveUrlInterceptorJobFactory( + JNIEnv* env, + jlong jinterceptor_handle) { + reinterpret_cast(jinterceptor_handle) + ->ShutDown(); +} + +ScopedJavaLocalRef JNI_MockUrlRequestJobFactory_GetMockUrlWithFailure( + JNIEnv* jenv, + jint jphase, + jint jnet_error) { + GURL url(net::URLRequestFailedJob::GetMockHttpUrlWithFailurePhase( + static_cast(jphase), + static_cast(jnet_error))); + return base::android::ConvertUTF8ToJavaString(jenv, url.spec()); +} + +ScopedJavaLocalRef JNI_MockUrlRequestJobFactory_GetMockUrlForData( + JNIEnv* jenv, + const JavaParamRef& jdata, + jint jdata_repeat_count) { + std::string data(base::android::ConvertJavaStringToUTF8(jenv, jdata)); + GURL url(net::URLRequestMockDataJob::GetMockHttpUrl(data, + jdata_repeat_count)); + return base::android::ConvertUTF8ToJavaString(jenv, url.spec()); +} + +ScopedJavaLocalRef +JNI_MockUrlRequestJobFactory_GetMockUrlForSSLCertificateError(JNIEnv* jenv) { + GURL url(net::SSLCertificateErrorJob::GetMockUrl()); + return base::android::ConvertUTF8ToJavaString(jenv, url.spec()); +} + +ScopedJavaLocalRef +JNI_MockUrlRequestJobFactory_GetMockUrlForClientCertificateRequest( + JNIEnv* jenv) { + GURL url(net::URLRequestMockDataJob::GetMockUrlForClientCertificateRequest()); + return base::android::ConvertUTF8ToJavaString(jenv, url.spec()); +} + +ScopedJavaLocalRef +JNI_MockUrlRequestJobFactory_GetMockUrlForHangingRead(JNIEnv* jenv) { + GURL url(net::URLRequestHangingReadJob::GetMockHttpUrl()); + return base::android::ConvertUTF8ToJavaString(jenv, url.spec()); +} + +} // namespace cronet diff --git a/src/components/cronet/android/test/native_test_server.cc b/src/components/cronet/android/test/native_test_server.cc new file mode 100644 index 0000000000..7fbfb8427c --- /dev/null +++ b/src/components/cronet/android/test/native_test_server.cc @@ -0,0 +1,91 @@ +// Copyright 2014 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 +#include +#include + +#include "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/files/file_path.h" +#include "base/test/test_support_android.h" +#include "components/cronet/android/cronet_tests_jni_headers/NativeTestServer_jni.h" +#include "components/cronet/testing/test_server/test_server.h" + +using base::android::JavaParamRef; +using base::android::ScopedJavaLocalRef; + +namespace cronet { + +jboolean JNI_NativeTestServer_StartNativeTestServer( + JNIEnv* env, + const JavaParamRef& jtest_files_root, + const JavaParamRef& jtest_data_dir) { + base::FilePath test_data_dir( + base::android::ConvertJavaStringToUTF8(env, jtest_data_dir)); + base::InitAndroidTestPaths(test_data_dir); + + base::FilePath test_files_root( + base::android::ConvertJavaStringToUTF8(env, jtest_files_root)); + return cronet::TestServer::StartServeFilesFromDirectory(test_files_root); +} + +void JNI_NativeTestServer_ShutdownNativeTestServer(JNIEnv* env) { + cronet::TestServer::Shutdown(); +} + +ScopedJavaLocalRef JNI_NativeTestServer_GetEchoBodyURL(JNIEnv* env) { + return base::android::ConvertUTF8ToJavaString( + env, cronet::TestServer::GetEchoRequestBodyURL()); +} + +ScopedJavaLocalRef JNI_NativeTestServer_GetEchoHeaderURL( + JNIEnv* env, + const JavaParamRef& jheader) { + return base::android::ConvertUTF8ToJavaString( + env, cronet::TestServer::GetEchoHeaderURL( + base::android::ConvertJavaStringToUTF8(env, jheader))); +} + +ScopedJavaLocalRef JNI_NativeTestServer_GetEchoAllHeadersURL( + JNIEnv* env) { + return base::android::ConvertUTF8ToJavaString( + env, cronet::TestServer::GetEchoAllHeadersURL()); +} + +ScopedJavaLocalRef JNI_NativeTestServer_GetEchoMethodURL(JNIEnv* env) { + return base::android::ConvertUTF8ToJavaString( + env, cronet::TestServer::GetEchoMethodURL()); +} + +ScopedJavaLocalRef JNI_NativeTestServer_GetRedirectToEchoBody( + JNIEnv* env) { + return base::android::ConvertUTF8ToJavaString( + env, cronet::TestServer::GetRedirectToEchoBodyURL()); +} + +ScopedJavaLocalRef JNI_NativeTestServer_GetFileURL( + JNIEnv* env, + const JavaParamRef& jfile_path) { + return base::android::ConvertUTF8ToJavaString( + env, cronet::TestServer::GetFileURL( + base::android::ConvertJavaStringToUTF8(env, jfile_path))); +} + +jint JNI_NativeTestServer_GetPort(JNIEnv* env) { + return cronet::TestServer::GetPort(); +} + +ScopedJavaLocalRef JNI_NativeTestServer_GetExabyteResponseURL( + JNIEnv* env) { + return base::android::ConvertUTF8ToJavaString( + env, cronet::TestServer::GetExabyteResponseURL()); +} + +ScopedJavaLocalRef JNI_NativeTestServer_GetHostPort(JNIEnv* env) { + return base::android::ConvertUTF8ToJavaString( + env, cronet::TestServer::GetHostPort()); +} + +} // namespace cronet diff --git a/src/components/cronet/android/test/proguard.cfg b/src/components/cronet/android/test/proguard.cfg new file mode 100644 index 0000000000..797504ab4b --- /dev/null +++ b/src/components/cronet/android/test/proguard.cfg @@ -0,0 +1,29 @@ +# Proguard configuration that is common for all type of tests. + +-keepattributes Signature,InnerClasses,SourceFile,LineNumberTable,EnclosingMethod +-dontwarn io.netty.** +-keep class io.netty.** { *; } +# Keep ChromiumNativeTestSupport & ChromiumPlatformOnlyTestSupport since they are +# instantiated through Reflection by the smoke tests. +-keep class org.chromium.net.smoke.ChromiumNativeTestSupport +-keep class org.chromium.net.smoke.ChromiumPlatformOnlyTestSupport + +# https://android.googlesource.com/platform/sdk/+/marshmallow-mr1-release/files/proguard-android.txt#54 +-dontwarn android.support.** + +# Do not obfuscate this class for testing since some of the tests check the class +# name in order to check that an instantiated engine is the Java one. +-keepnames class org.chromium.net.impl.JavaCronetEngine + +# These classes should be explicitly kept to avoid failure if +# class/merging/horizontal proguard optimization is enabled. +# NOTE: make sure that only test classes are added to this list. +-keep class org.chromium.base.test.** { + *; +} + +-keep class org.chromium.net.TestFilesInstaller +-keep class org.chromium.net.MetricsTestUtil + +# Generated for chrome apk and not included into cronet. +-dontwarn org.chromium.build.NativeLibraries diff --git a/src/components/cronet/android/test/quic_test_server.cc b/src/components/cronet/android/test/quic_test_server.cc new file mode 100644 index 0000000000..6f82b95a6d --- /dev/null +++ b/src/components/cronet/android/test/quic_test_server.cc @@ -0,0 +1,113 @@ +// 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 "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/android/path_utils.h" +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/message_loop/message_pump_type.h" +#include "base/test/test_support_android.h" +#include "base/threading/thread.h" +#include "components/cronet/android/cronet_tests_jni_headers/QuicTestServer_jni.h" +#include "components/cronet/android/test/cronet_test_util.h" +#include "net/base/ip_address.h" +#include "net/base/ip_endpoint.h" +#include "net/quic/crypto/proof_source_chromium.h" +#include "net/test/test_data_directory.h" +#include "net/third_party/quiche/src/quic/tools/quic_memory_cache_backend.h" +#include "net/tools/quic/quic_simple_server.h" + +using base::android::JavaParamRef; +using base::android::ScopedJavaLocalRef; + +namespace cronet { + +namespace { + +static const int kServerPort = 6121; + +base::Thread* g_quic_server_thread = nullptr; +quic::QuicMemoryCacheBackend* g_quic_memory_cache_backend = nullptr; +net::QuicSimpleServer* g_quic_server = nullptr; + +void StartOnServerThread(const base::FilePath& test_files_root, + const base::FilePath& test_data_dir) { + DCHECK(g_quic_server_thread->task_runner()->BelongsToCurrentThread()); + DCHECK(!g_quic_server); + + // Set up in-memory cache. + base::FilePath file_dir = test_files_root.Append("quic_data"); + CHECK(base::PathExists(file_dir)) << "Quic data does not exist"; + g_quic_memory_cache_backend = new quic::QuicMemoryCacheBackend(); + g_quic_memory_cache_backend->InitializeBackend(file_dir.value()); + quic::QuicConfig config; + + // Set up server certs. + base::FilePath directory = test_data_dir.Append("net/data/ssl/certificates"); + std::unique_ptr proof_source( + new net::ProofSourceChromium()); + CHECK(proof_source->Initialize( + directory.Append("quic-chain.pem"), + directory.Append("quic-leaf-cert.key"), + base::FilePath())); + g_quic_server = new net::QuicSimpleServer( + std::move(proof_source), config, + quic::QuicCryptoServerConfig::ConfigOptions(), + quic::AllSupportedVersions(), g_quic_memory_cache_backend); + + // Start listening. + int rv = g_quic_server->Listen( + net::IPEndPoint(net::IPAddress::IPv4AllZeros(), kServerPort)); + CHECK_GE(rv, 0) << "Quic server fails to start"; + JNIEnv* env = base::android::AttachCurrentThread(); + Java_QuicTestServer_onServerStarted(env); +} + +void ShutdownOnServerThread() { + DCHECK(g_quic_server_thread->task_runner()->BelongsToCurrentThread()); + g_quic_server->Shutdown(); + delete g_quic_server; + delete g_quic_memory_cache_backend; +} + +} // namespace + +// Quic server is currently hardcoded to run on port 6121 of the localhost on +// the device. +void JNI_QuicTestServer_StartQuicTestServer( + JNIEnv* env, + const JavaParamRef& jtest_files_root, + const JavaParamRef& jtest_data_dir) { + DCHECK(!g_quic_server_thread); + base::FilePath test_data_dir( + base::android::ConvertJavaStringToUTF8(env, jtest_data_dir)); + base::InitAndroidTestPaths(test_data_dir); + + g_quic_server_thread = new base::Thread("quic server thread"); + base::Thread::Options thread_options; + thread_options.message_pump_type = base::MessagePumpType::IO; + bool started = + g_quic_server_thread->StartWithOptions(std::move(thread_options)); + DCHECK(started); + base::FilePath test_files_root( + base::android::ConvertJavaStringToUTF8(env, jtest_files_root)); + g_quic_server_thread->task_runner()->PostTask( + FROM_HERE, + base::BindOnce(&StartOnServerThread, test_files_root, test_data_dir)); +} + +void JNI_QuicTestServer_ShutdownQuicTestServer(JNIEnv* env) { + DCHECK(!g_quic_server_thread->task_runner()->BelongsToCurrentThread()); + g_quic_server_thread->task_runner()->PostTask( + FROM_HERE, base::BindOnce(&ShutdownOnServerThread)); + delete g_quic_server_thread; +} + +int JNI_QuicTestServer_GetServerPort(JNIEnv* env) { + return kServerPort; +} + +} // namespace cronet diff --git a/src/components/cronet/android/test/res/values/strings.xml b/src/components/cronet/android/test/res/values/strings.xml new file mode 100644 index 0000000000..bbb7900c10 --- /dev/null +++ b/src/components/cronet/android/test/res/values/strings.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/components/cronet/android/test/res/xml/network_security_config.xml b/src/components/cronet/android/test/res/xml/network_security_config.xml new file mode 100644 index 0000000000..f1149a7c49 --- /dev/null +++ b/src/components/cronet/android/test/res/xml/network_security_config.xml @@ -0,0 +1,40 @@ + + + + + + + + + example.com + + + + + + 127.0.0.1 + + localhost + + 0.0.0.0 + + host-cache-test-host + + this-weird-host-name-does-not-exist + + some-weird-hostname + + diff --git a/src/components/cronet/android/test/smoketests/res/native/values/strings.xml b/src/components/cronet/android/test/smoketests/res/native/values/strings.xml new file mode 100644 index 0000000000..8538e13fe1 --- /dev/null +++ b/src/components/cronet/android/test/smoketests/res/native/values/strings.xml @@ -0,0 +1,11 @@ + + + + + + org.chromium.net.smoke.ChromiumNativeTestSupport + diff --git a/src/components/cronet/android/test/smoketests/res/platform_only/values/strings.xml b/src/components/cronet/android/test/smoketests/res/platform_only/values/strings.xml new file mode 100644 index 0000000000..bb27feafa6 --- /dev/null +++ b/src/components/cronet/android/test/smoketests/res/platform_only/values/strings.xml @@ -0,0 +1,11 @@ + + + + + + org.chromium.net.smoke.ChromiumPlatformOnlyTestSupport + diff --git a/src/components/cronet/android/test/smoketests/res/platform_only/xml/network_security_config.xml b/src/components/cronet/android/test/smoketests/res/platform_only/xml/network_security_config.xml new file mode 100644 index 0000000000..fea9020ccd --- /dev/null +++ b/src/components/cronet/android/test/smoketests/res/platform_only/xml/network_security_config.xml @@ -0,0 +1,17 @@ + + + + + + + + + 127.0.0.1 + + + diff --git a/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumNativeTestSupport.java b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumNativeTestSupport.java new file mode 100644 index 0000000000..27d44be73e --- /dev/null +++ b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumNativeTestSupport.java @@ -0,0 +1,122 @@ +// Copyright 2016 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.smoke; + +import android.content.Context; + +import org.json.JSONObject; + +import org.chromium.base.Log; +import org.chromium.net.CronetTestUtil; +import org.chromium.net.ExperimentalCronetEngine; + +/** + * Provides support for tests that depend on QUIC and HTTP2 servers. + */ +class ChromiumNativeTestSupport extends ChromiumPlatformOnlyTestSupport { + private static final String TAG = ChromiumNativeTestSupport.class.getSimpleName(); + + /** + * Name of the file that contains the test server certificate in PEM format. + */ + private static final String SERVER_CERT_PEM = "quic-chain.pem"; + + /** + * Name of the file that contains the test server private key in PKCS8 PEM format. + */ + private static final String SERVER_KEY_PKCS8_PEM = "quic-leaf-cert.key.pkcs8.pem"; + + @Override + public TestServer createTestServer(Context context, Protocol protocol) { + switch (protocol) { + case QUIC: + return new QuicTestServer(context); + case HTTP2: + return new Http2TestServer(context); + case HTTP1: + return super.createTestServer(context, protocol); + default: + throw new RuntimeException("Unknown server protocol: " + protocol); + } + } + + @Override + public void addHostResolverRules(JSONObject experimentalOptionsJson) { + try { + JSONObject hostResolverParams = CronetTestUtil.generateHostResolverRules(); + experimentalOptionsJson.put("HostResolverRules", hostResolverParams); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void installMockCertVerifierForTesting(ExperimentalCronetEngine.Builder builder) { + CronetTestUtil.setMockCertVerifierForTesting( + builder, org.chromium.net.QuicTestServer.createMockCertVerifier()); + } + + @Override + public void loadTestNativeLibrary() { + System.loadLibrary("cronet_tests"); + } + + private static class QuicTestServer implements TestServer { + private final Context mContext; + + QuicTestServer(Context context) { + mContext = context; + } + + @Override + public boolean start() { + org.chromium.net.QuicTestServer.startQuicTestServer(mContext); + return true; + } + + @Override + public void shutdown() { + org.chromium.net.QuicTestServer.shutdownQuicTestServer(); + } + + @Override + public String getSuccessURL() { + return org.chromium.net.QuicTestServer.getServerURL() + "/simple.txt"; + } + } + + private static class Http2TestServer implements TestServer { + private final Context mContext; + + Http2TestServer(Context context) { + mContext = context; + } + + @Override + public boolean start() { + try { + return org.chromium.net.Http2TestServer.startHttp2TestServer( + mContext, SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM); + } catch (Exception e) { + Log.e(TAG, "Exception during Http2TestServer start", e); + return false; + } + } + + @Override + public void shutdown() { + try { + org.chromium.net.Http2TestServer.shutdownHttp2TestServer(); + } catch (Exception e) { + Log.e(TAG, "Exception during Http2TestServer shutdown", e); + } + } + + @Override + public String getSuccessURL() { + return org.chromium.net.Http2TestServer.getEchoMethodUrl(); + } + } +} diff --git a/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumPlatformOnlyTestSupport.java b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumPlatformOnlyTestSupport.java new file mode 100644 index 0000000000..0545f04482 --- /dev/null +++ b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumPlatformOnlyTestSupport.java @@ -0,0 +1,53 @@ +// Copyright 2016 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.smoke; + +import android.content.Context; + +import org.json.JSONObject; + +import org.chromium.net.ExperimentalCronetEngine; + +import java.io.File; + +/** + * Tests support for Java only Cronet engine tests. This class should not depend on + * Chromium 'base' or 'net'. + */ +public class ChromiumPlatformOnlyTestSupport implements TestSupport { + @Override + public TestServer createTestServer(Context context, Protocol protocol) { + switch (protocol) { + case QUIC: + throw new IllegalArgumentException("QUIC is not supported"); + case HTTP2: + throw new IllegalArgumentException("HTTP2 is not supported"); + case HTTP1: + return new HttpTestServer(); + default: + throw new IllegalArgumentException("Unknown server protocol: " + protocol); + } + } + + @Override + public void processNetLog(Context context, File file) { + // Do nothing + } + + @Override + public void addHostResolverRules(JSONObject experimentalOptionsJson) { + throw new UnsupportedOperationException("Unsupported by ChromiumPlatformOnlyTestSupport"); + } + + @Override + public void installMockCertVerifierForTesting(ExperimentalCronetEngine.Builder builder) { + throw new UnsupportedOperationException("Unsupported by ChromiumPlatformOnlyTestSupport"); + } + + @Override + public void loadTestNativeLibrary() { + throw new UnsupportedOperationException("Unsupported by ChromiumPlatformOnlyTestSupport"); + } +} diff --git a/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/CronetSmokeTestRule.java b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/CronetSmokeTestRule.java new file mode 100644 index 0000000000..a00ada0dc9 --- /dev/null +++ b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/CronetSmokeTestRule.java @@ -0,0 +1,118 @@ +// Copyright 2016 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.smoke; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; + +import org.junit.Assert; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import org.chromium.net.CronetEngine; +import org.chromium.net.ExperimentalCronetEngine; +import org.chromium.net.UrlResponseInfo; + +/** + * Base test class. This class should not import any classes from the org.chromium.base package. + */ +public class CronetSmokeTestRule implements TestRule { + /** + * The key in the string resource file that specifies {@link TestSupport} that should + * be instantiated. + */ + private static final String SUPPORT_IMPL_RES_KEY = "TestSupportImplClass"; + + public ExperimentalCronetEngine.Builder mCronetEngineBuilder; + public CronetEngine mCronetEngine; + public TestSupport mTestSupport; + + @Override + public Statement apply(final Statement base, Description desc) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + ruleSetUp(); + base.evaluate(); + ruleTearDown(); + } + }; + } + + public TestSupport getTestSupport() { + return mTestSupport; + } + + public CronetEngine getCronetEngine() { + return mCronetEngine; + } + + public ExperimentalCronetEngine.Builder getCronetEngineBuilder() { + return mCronetEngineBuilder; + } + + private void ruleSetUp() throws Exception { + mCronetEngineBuilder = + new ExperimentalCronetEngine.Builder(InstrumentationRegistry.getTargetContext()); + initTestSupport(); + } + + private void ruleTearDown() throws Exception { + if (mCronetEngine != null) { + mCronetEngine.shutdown(); + } + } + + public void initCronetEngine() { + mCronetEngine = mCronetEngineBuilder.build(); + } + + static void assertSuccessfulNonEmptyResponse(SmokeTestRequestCallback callback, String url) { + // Check the request state + if (callback.getFinalState() == SmokeTestRequestCallback.State.Failed) { + throw new RuntimeException( + "The request failed with an error", callback.getFailureError()); + } + Assert.assertEquals(SmokeTestRequestCallback.State.Succeeded, callback.getFinalState()); + + // Check the response info + UrlResponseInfo responseInfo = callback.getResponseInfo(); + Assert.assertNotNull(responseInfo); + Assert.assertFalse(responseInfo.wasCached()); + Assert.assertEquals(url, responseInfo.getUrl()); + Assert.assertEquals( + url, responseInfo.getUrlChain().get(responseInfo.getUrlChain().size() - 1)); + Assert.assertEquals(200, responseInfo.getHttpStatusCode()); + Assert.assertTrue(responseInfo.toString().length() > 0); + } + + static void assertJavaEngine(CronetEngine engine) { + Assert.assertNotNull(engine); + Assert.assertEquals("org.chromium.net.impl.JavaCronetEngine", engine.getClass().getName()); + } + + static void assertNativeEngine(CronetEngine engine) { + Assert.assertNotNull(engine); + Assert.assertEquals( + "org.chromium.net.impl.CronetUrlRequestContext", engine.getClass().getName()); + } + + /** + * Instantiates a concrete implementation of {@link TestSupport} interface. + * The name of the implementation class is determined dynamically by reading + * the value of |TestSupportImplClass| from the Android string resource file. + * + * @throws Exception if the class cannot be instantiated. + */ + private void initTestSupport() throws Exception { + Context ctx = InstrumentationRegistry.getTargetContext(); + String packageName = ctx.getPackageName(); + int resId = ctx.getResources().getIdentifier(SUPPORT_IMPL_RES_KEY, "string", packageName); + String className = ctx.getResources().getString(resId); + Class cl = Class.forName(className).asSubclass(TestSupport.class); + mTestSupport = cl.newInstance(); + } +} diff --git a/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/Http2Test.java b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/Http2Test.java new file mode 100644 index 0000000000..17abeb0b55 --- /dev/null +++ b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/Http2Test.java @@ -0,0 +1,58 @@ +// Copyright 2016 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.smoke; + +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.net.UrlRequest; + +/** + * HTTP2 Tests. + */ +@RunWith(AndroidJUnit4.class) +public class Http2Test { + private TestSupport.TestServer mServer; + + @Rule + public NativeCronetTestRule mRule = new NativeCronetTestRule(); + + @Before + public void setUp() throws Exception { + mServer = mRule.getTestSupport().createTestServer( + InstrumentationRegistry.getTargetContext(), TestSupport.Protocol.HTTP2); + } + + @After + public void tearDown() throws Exception { + mServer.shutdown(); + } + + // Test that HTTP/2 is enabled by default but QUIC is not. + @Test + @SmallTest + public void testHttp2() throws Exception { + mRule.getTestSupport().installMockCertVerifierForTesting(mRule.getCronetEngineBuilder()); + mRule.initCronetEngine(); + Assert.assertTrue(mServer.start()); + SmokeTestRequestCallback callback = new SmokeTestRequestCallback(); + UrlRequest.Builder requestBuilder = mRule.getCronetEngine().newUrlRequestBuilder( + mServer.getSuccessURL(), callback, callback.getExecutor()); + requestBuilder.build().start(); + callback.blockForDone(); + + CronetSmokeTestRule.assertSuccessfulNonEmptyResponse(callback, mServer.getSuccessURL()); + Assert.assertEquals("h2", callback.getResponseInfo().getNegotiatedProtocol()); + } +} diff --git a/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/HttpTestServer.java b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/HttpTestServer.java new file mode 100644 index 0000000000..8e57b03606 --- /dev/null +++ b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/HttpTestServer.java @@ -0,0 +1,121 @@ +// Copyright 2016 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.smoke; + +import android.os.ConditionVariable; +import android.util.Log; + +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.CharsetUtil; + +/** + * A simple HTTP server for testing. + */ +public class HttpTestServer implements TestSupport.TestServer { + private static final String TAG = HttpTestServer.class.getSimpleName(); + private static final String HOST = "127.0.0.1"; + private static final int PORT = 8080; + + private Channel mServerChannel; + private ConditionVariable mStartBlock = new ConditionVariable(); + private ConditionVariable mShutdownBlock = new ConditionVariable(); + + @Override + public boolean start() { + new Thread(new Runnable() { + @Override + public void run() { + try { + HttpTestServer.this.run(); + } catch (Exception e) { + Log.e(TAG, "Unable to start HttpTestServer", e); + } + } + }).start(); + // Return false if the server cannot start within 5 seconds. + return mStartBlock.block(5000); + } + + @Override + public void shutdown() { + if (mServerChannel != null) { + mServerChannel.close(); + boolean success = mShutdownBlock.block(10000); + if (!success) { + Log.e(TAG, "Unable to shutdown the server. Is it already dead?"); + } + mServerChannel = null; + } + } + + @Override + public String getSuccessURL() { + return getServerUrl() + "/success"; + } + + private String getServerUrl() { + return "http://" + HOST + ":" + PORT; + } + + private void run() throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(4); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(new HttpRequestDecoder()); + p.addLast(new HttpResponseEncoder()); + p.addLast(new TestServerHandler()); + } + }); + + // Start listening fo incoming connections. + mServerChannel = b.bind(PORT).sync().channel(); + mStartBlock.open(); + // Block until the channel is closed. + mServerChannel.closeFuture().sync(); + mShutdownBlock.open(); + Log.i(TAG, "HttpServer stopped"); + } finally { + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } + + private static class TestServerHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + FullHttpResponse response = new DefaultFullHttpResponse( + HTTP_1_1, OK, Unpooled.copiedBuffer("Hello!", CharsetUtil.UTF_8)); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + } +} diff --git a/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/MissingNativeLibraryTest.java b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/MissingNativeLibraryTest.java new file mode 100644 index 0000000000..9954b6ff48 --- /dev/null +++ b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/MissingNativeLibraryTest.java @@ -0,0 +1,84 @@ +// Copyright 2016 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.smoke; + +import static org.chromium.net.smoke.CronetSmokeTestRule.assertJavaEngine; + +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; + +import org.chromium.net.CronetEngine; +import org.chromium.net.CronetProvider; +import org.chromium.net.ExperimentalCronetEngine; + +import java.util.List; + +/** + * Tests scenarios when the native shared library file is missing in the APK or was built for a + * wrong architecture. + */ +@RunWith(AndroidJUnit4.class) +public class MissingNativeLibraryTest { + @Rule + public CronetSmokeTestRule mRule = new CronetSmokeTestRule(); + @Rule + public ExpectedException thrown = ExpectedException.none(); + + /** + * If the ".so" file is missing, instantiating the Cronet engine should throw an exception. + */ + @Test + @SmallTest + public void testExceptionWhenSoFileIsAbsent() throws Exception { + ExperimentalCronetEngine.Builder builder = + new ExperimentalCronetEngine.Builder(InstrumentationRegistry.getTargetContext()); + thrown.expect(UnsatisfiedLinkError.class); + builder.build(); + } + + /** + * Tests the embedder ability to select Java (platform) based implementation when + * the native library is missing or doesn't load for some reason, + */ + @Test + @SmallTest + public void testForceChoiceOfJavaEngine() throws Exception { + List availableProviders = + CronetProvider.getAllProviders(InstrumentationRegistry.getTargetContext()); + boolean foundNativeProvider = false; + CronetProvider platformProvider = null; + for (CronetProvider provider : availableProviders) { + Assert.assertTrue(provider.isEnabled()); + if (provider.getName().equals(CronetProvider.PROVIDER_NAME_APP_PACKAGED)) { + foundNativeProvider = true; + } else if (provider.getName().equals(CronetProvider.PROVIDER_NAME_FALLBACK)) { + platformProvider = provider; + } + } + + Assert.assertTrue("Unable to find the native cronet provider", foundNativeProvider); + Assert.assertNotNull("Unable to find the platform cronet provider", platformProvider); + + CronetEngine.Builder builder = platformProvider.createBuilder(); + CronetEngine engine = builder.build(); + assertJavaEngine(engine); + + Assert.assertTrue("It should be always possible to cast the created builder to" + + " ExperimentalCronetEngine.Builder", + builder instanceof ExperimentalCronetEngine.Builder); + + Assert.assertTrue("It should be always possible to cast the created engine to" + + " ExperimentalCronetEngine.Builder", + engine instanceof ExperimentalCronetEngine); + } +} diff --git a/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/NativeCronetTestRule.java b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/NativeCronetTestRule.java new file mode 100644 index 0000000000..0e11674058 --- /dev/null +++ b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/NativeCronetTestRule.java @@ -0,0 +1,69 @@ +// Copyright 2016 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.smoke; + +import android.support.test.InstrumentationRegistry; + +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import org.chromium.base.ContextUtils; +import org.chromium.base.PathUtils; + +import java.io.File; + +/** + * Test base class for testing native Engine implementation. This class can import classes from the + * org.chromium.base package. + */ +public class NativeCronetTestRule extends CronetSmokeTestRule { + private static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "cronet_test"; + private static final String LOGFILE_NAME = "cronet-netlog.json"; + + @Override + public Statement apply(final Statement base, Description desc) { + return super.apply(new Statement() { + @Override + public void evaluate() throws Throwable { + ruleSetUp(); + base.evaluate(); + ruleTearDown(); + } + }, desc); + } + + @Override + public void initCronetEngine() { + super.initCronetEngine(); + assertNativeEngine(mCronetEngine); + startNetLog(); + } + + private void ruleSetUp() throws Exception { + ContextUtils.initApplicationContext( + InstrumentationRegistry.getTargetContext().getApplicationContext()); + PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX); + mTestSupport.loadTestNativeLibrary(); + } + + private void ruleTearDown() throws Exception { + stopAndSaveNetLog(); + } + + private void startNetLog() { + if (mCronetEngine != null) { + mCronetEngine.startNetLogToFile( + PathUtils.getDataDirectory() + "/" + LOGFILE_NAME, false); + } + } + + private void stopAndSaveNetLog() { + if (mCronetEngine == null) return; + mCronetEngine.stopNetLog(); + File netLogFile = new File(PathUtils.getDataDirectory(), LOGFILE_NAME); + if (!netLogFile.exists()) return; + mTestSupport.processNetLog(InstrumentationRegistry.getTargetContext(), netLogFile); + } +} diff --git a/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/PlatformOnlyEngineTest.java b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/PlatformOnlyEngineTest.java new file mode 100644 index 0000000000..bd31d3bac2 --- /dev/null +++ b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/PlatformOnlyEngineTest.java @@ -0,0 +1,64 @@ +// Copyright 2016 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.smoke; + +import static org.chromium.net.smoke.CronetSmokeTestRule.assertJavaEngine; +import static org.chromium.net.smoke.CronetSmokeTestRule.assertSuccessfulNonEmptyResponse; + +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.net.UrlRequest; + +/** + * Tests scenario when an app doesn't contain the native Cronet implementation. + */ +@RunWith(AndroidJUnit4.class) +public class PlatformOnlyEngineTest { + private String mURL; + private TestSupport.TestServer mServer; + + @Rule + public CronetSmokeTestRule mRule = new CronetSmokeTestRule(); + + @Before + public void setUp() throws Exception { + // Java-only implementation of the Cronet engine only supports Http/1 protocol. + mServer = mRule.getTestSupport().createTestServer( + InstrumentationRegistry.getTargetContext(), TestSupport.Protocol.HTTP1); + Assert.assertTrue(mServer.start()); + mURL = mServer.getSuccessURL(); + } + + @After + public void tearDown() throws Exception { + mServer.shutdown(); + } + + /** + * Test a successful response when a request is sent by the Java Cronet Engine. + */ + @Test + @SmallTest + public void testSuccessfulResponse() { + mRule.initCronetEngine(); + assertJavaEngine(mRule.getCronetEngine()); + SmokeTestRequestCallback callback = new SmokeTestRequestCallback(); + UrlRequest.Builder requestBuilder = mRule.getCronetEngine().newUrlRequestBuilder( + mURL, callback, callback.getExecutor()); + requestBuilder.build().start(); + callback.blockForDone(); + assertSuccessfulNonEmptyResponse(callback, mURL); + } +} diff --git a/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/QuicTest.java b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/QuicTest.java new file mode 100644 index 0000000000..8231f789ee --- /dev/null +++ b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/QuicTest.java @@ -0,0 +1,84 @@ +// Copyright 2016 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.smoke; + +import static org.chromium.net.smoke.TestSupport.Protocol.QUIC; + +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.chromium.net.UrlRequest; + +import java.net.URL; + +/** + * QUIC Tests. + */ +@RunWith(AndroidJUnit4.class) +public class QuicTest { + private TestSupport.TestServer mServer; + + @Rule + public NativeCronetTestRule mRule = new NativeCronetTestRule(); + + @Before + public void setUp() throws Exception { + mServer = mRule.getTestSupport().createTestServer( + InstrumentationRegistry.getTargetContext(), QUIC); + } + + @After + public void tearDown() throws Exception { + mServer.shutdown(); + } + + @Test + @SmallTest + public void testQuic() throws Exception { + Assert.assertTrue(mServer.start()); + final String urlString = mServer.getSuccessURL(); + final URL url = new URL(urlString); + + mRule.getCronetEngineBuilder().enableQuic(true); + mRule.getCronetEngineBuilder().addQuicHint(url.getHost(), url.getPort(), url.getPort()); + mRule.getTestSupport().installMockCertVerifierForTesting(mRule.getCronetEngineBuilder()); + + JSONObject quicParams = new JSONObject(); + JSONObject experimentalOptions = new JSONObject().put("QUIC", quicParams); + mRule.getTestSupport().addHostResolverRules(experimentalOptions); + mRule.getCronetEngineBuilder().setExperimentalOptions(experimentalOptions.toString()); + + mRule.initCronetEngine(); + + // QUIC is not guaranteed to win the race, so try multiple times. + boolean quicNegotiated = false; + + for (int i = 0; i < 5; i++) { + SmokeTestRequestCallback callback = new SmokeTestRequestCallback(); + UrlRequest.Builder requestBuilder = + mRule.getCronetEngine().newUrlRequestBuilder( + urlString, callback, callback.getExecutor()); + requestBuilder.build().start(); + callback.blockForDone(); + NativeCronetTestRule.assertSuccessfulNonEmptyResponse(callback, urlString); + if (callback.getResponseInfo().getNegotiatedProtocol().startsWith("http/2+quic") + || callback.getResponseInfo().getNegotiatedProtocol().startsWith("h3")) { + quicNegotiated = true; + break; + } + } + Assert.assertTrue(quicNegotiated); + } +} diff --git a/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/SmokeTestRequestCallback.java b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/SmokeTestRequestCallback.java new file mode 100644 index 0000000000..fa07bfec34 --- /dev/null +++ b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/SmokeTestRequestCallback.java @@ -0,0 +1,126 @@ +// Copyright 2016 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.smoke; + +import static junit.framework.Assert.assertTrue; + +import android.os.ConditionVariable; + +import org.chromium.net.CronetException; +import org.chromium.net.UrlRequest; +import org.chromium.net.UrlResponseInfo; + +import java.nio.ByteBuffer; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * A simple boilerplate implementation of {@link UrlRequest.Callback} that is used by smoke tests. + */ +class SmokeTestRequestCallback extends UrlRequest.Callback { + private static final int READ_BUFFER_SIZE = 10000; + + // An executor that is used to execute {@link UrlRequest.Callback UrlRequest callbacks}. + private ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + + // Signals when the request is done either successfully or not. + private final ConditionVariable mDone = new ConditionVariable(); + + // The state of the request. + public enum State { NotSet, Succeeded, Failed, Canceled } + + // The current state of the request. + private State mState = State.NotSet; + + // Response info of the finished request. + private UrlResponseInfo mResponseInfo; + + // Holds an error if the request failed. + private CronetException mError; + + @Override + public void onRedirectReceived(UrlRequest request, UrlResponseInfo info, String newLocationUrl) + throws Exception { + request.followRedirect(); + } + + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) throws Exception { + request.read(ByteBuffer.allocateDirect(READ_BUFFER_SIZE)); + } + + @Override + public void onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) + throws Exception { + request.read(ByteBuffer.allocateDirect(READ_BUFFER_SIZE)); + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + done(State.Succeeded, info); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) { + mError = error; + done(State.Failed, info); + } + + @Override + public void onCanceled(UrlRequest request, UrlResponseInfo info) { + done(State.Canceled, info); + } + + /** + * Returns the request executor. + * + * @return the executor. + */ + public Executor getExecutor() { + return mExecutor; + } + + /** + * Blocks until the request is either succeeded, failed or canceled. + */ + public void blockForDone() { + mDone.block(); + } + + /** + * Returns the final state of the request. + * + * @return the state. + */ + public State getFinalState() { + return mState; + } + + /** + * Returns an error that was passed to {@link #onFailed} when the request failed. + * + * @return the error if the request failed; {@code null} otherwise. + */ + public CronetException getFailureError() { + return mError; + } + + /** + * Returns {@link UrlResponseInfo} of the finished response. + * + * @return the response info. {@code null} if the request hasn't completed yet. + */ + public UrlResponseInfo getResponseInfo() { + return mResponseInfo; + } + + private void done(State finalState, UrlResponseInfo responseInfo) { + assertTrue(mState == State.NotSet); + mResponseInfo = responseInfo; + mState = finalState; + mDone.open(); + } +} diff --git a/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/TestSupport.java b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/TestSupport.java new file mode 100644 index 0000000000..f2051065c6 --- /dev/null +++ b/src/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/TestSupport.java @@ -0,0 +1,93 @@ +// Copyright 2016 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.smoke; + +import android.content.Context; + +import org.json.JSONObject; + +import org.chromium.net.ExperimentalCronetEngine; + +import java.io.File; + +/** + * Provides support for tests, so they can be run in different environments against different + * servers. It contains methods, which behavior can be different in different testing environments. + * The concrete implementation of this interface is determined dynamically at runtime by reading + * the value of |TestSupportImplClass| from the Android string resource file. + */ +public interface TestSupport { + enum Protocol { + HTTP1, + HTTP2, + QUIC, + } + + /** + * Creates a new test server that supports a given {@code protocol}. + * + * @param context context. + * @param protocol protocol that should be supported by the server. + * @return an instance of the server. + * + * @throws UnsupportedOperationException if the implementation of this interface + * does not support a given {@code protocol}. + */ + TestServer createTestServer(Context context, Protocol protocol); + + /** + * This method is called at the end of a test run if the netlog is available. An implementer + * of {@link TestSupport} can use it to process the result netlog; e.g., to copy the netlog + * to a directory where all test logs are collected. This method is optional and can be no-op. + * + * @param file the netlog file. + */ + void processNetLog(Context context, File file); + + /** + * Adds host resolver rules to a given experimental option JSON file. + * This method is optional. + * + * @param experimentalOptionsJson experimental options. + */ + void addHostResolverRules(JSONObject experimentalOptionsJson); + + /** + * Installs mock certificate verifier for a given {@code builder}. + * This method is optional. + * + * @param builder that should have the verifier installed. + */ + void installMockCertVerifierForTesting(ExperimentalCronetEngine.Builder builder); + + /** + * Loads a native library that is required for testing if any required. + */ + void loadTestNativeLibrary(); + + /** + * A test server. + */ + interface TestServer { + /** + * Starts the server. + * + * @return true if the server started successfully. + */ + boolean start(); + + /** + * Shuts down the server. + */ + void shutdown(); + + /** + * Return a URL that can be used by the test code to receive a successful response. + * + * @return the URL as a string. + */ + String getSuccessURL(); + } +} diff --git a/src/components/cronet/android/test/src/org/chromium/net/CronetTestApplication.java b/src/components/cronet/android/test/src/org/chromium/net/CronetTestApplication.java new file mode 100644 index 0000000000..1185ecd6d8 --- /dev/null +++ b/src/components/cronet/android/test/src/org/chromium/net/CronetTestApplication.java @@ -0,0 +1,13 @@ +// Copyright 2014 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.app.Application; + +/** + * Application for managing the Cronet Test. + */ +public class CronetTestApplication extends Application { +} diff --git a/src/components/cronet/android/test/src/org/chromium/net/CronetTestUtil.java b/src/components/cronet/android/test/src/org/chromium/net/CronetTestUtil.java new file mode 100644 index 0000000000..5bb884b50f --- /dev/null +++ b/src/components/cronet/android/test/src/org/chromium/net/CronetTestUtil.java @@ -0,0 +1,106 @@ +// Copyright 2014 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.json.JSONException; +import org.json.JSONObject; + +import org.chromium.base.annotations.JNINamespace; +import org.chromium.net.impl.CronetEngineBuilderImpl; +import org.chromium.net.impl.CronetUrlRequest; +import org.chromium.net.impl.CronetUrlRequestContext; + +/** + * Utilities for Cronet testing + */ +@JNINamespace("cronet") +public class CronetTestUtil { + // QUIC test domain must match the certificate used + // (quic-chain.pem and quic-leaf-cert.key), and the file served ( + // components/cronet/android/test/assets/test/quic_data/simple.txt). + static final String QUIC_FAKE_HOST = "test.example.com"; + private static final String[] TEST_DOMAINS = {QUIC_FAKE_HOST}; + private static final String LOOPBACK_ADDRESS = "127.0.0.1"; + + /** + * Generates rules for customized DNS mapping for testing hostnames used by test servers, + * namely: + *
    + *
  • {@link QuicTestServer#getServerHost}
  • + *
+ * Maps the test hostnames to 127.0.0.1. + */ + public static JSONObject generateHostResolverRules() throws JSONException { + return generateHostResolverRules(LOOPBACK_ADDRESS); + } + + /** + * Generates rules for customized DNS mapping for testing hostnames used by test servers, + * namely: + *
    + *
  • {@link QuicTestServer#getServerHost}
  • + *
+ * @param destination host to map to + */ + public static JSONObject generateHostResolverRules(String destination) throws JSONException { + StringBuilder rules = new StringBuilder(); + for (String domain : TEST_DOMAINS) { + rules.append("MAP " + domain + " " + destination + ","); + } + return new JSONObject().put("host_resolver_rules", rules); + } + + /** + * Prepare {@code cronetEngine}'s network thread so libcronet_test code can run on it. + */ + public static class NetworkThreadTestConnector { + private final CronetUrlRequestContext mRequestContext; + + public NetworkThreadTestConnector(CronetEngine cronetEngine) { + mRequestContext = (CronetUrlRequestContext) cronetEngine; + nativePrepareNetworkThread(mRequestContext.getUrlRequestContextAdapter()); + } + + public void shutdown() { + nativeCleanupNetworkThread(mRequestContext.getUrlRequestContextAdapter()); + } + } + + /** + * Returns the value of load flags in |urlRequest|. + * @param urlRequest is the UrlRequest object of interest. + */ + public static int getLoadFlags(UrlRequest urlRequest) { + return nativeGetLoadFlags(((CronetUrlRequest) urlRequest).getUrlRequestAdapterForTesting()); + } + + public static void setMockCertVerifierForTesting( + ExperimentalCronetEngine.Builder builder, long mockCertVerifier) { + getCronetEngineBuilderImpl(builder).setMockCertVerifierForTesting(mockCertVerifier); + } + + public static CronetEngineBuilderImpl getCronetEngineBuilderImpl( + ExperimentalCronetEngine.Builder builder) { + return (CronetEngineBuilderImpl) builder.getBuilderDelegate(); + } + + /** + * Returns whether the device supports calling nativeGetTaggedBytes(). + */ + public static native boolean nativeCanGetTaggedBytes(); + + /** + * Query the system to find out how many bytes were received with tag + * {@code expectedTag} for our UID. + * @param expectedTag the tag to query for. + * @return the count of recieved bytes. + */ + public static native long nativeGetTaggedBytes(int expectedTag); + + private static native int nativeGetLoadFlags(long urlRequestAdapter); + + private static native void nativePrepareNetworkThread(long contextAdapter); + private static native void nativeCleanupNetworkThread(long contextAdapter); +} diff --git a/src/components/cronet/android/test/src/org/chromium/net/Http2TestHandler.java b/src/components/cronet/android/test/src/org/chromium/net/Http2TestHandler.java new file mode 100644 index 0000000000..8396bb17ba --- /dev/null +++ b/src/components/cronet/android/test/src/org/chromium/net/Http2TestHandler.java @@ -0,0 +1,450 @@ +// 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 static io.netty.buffer.Unpooled.copiedBuffer; +import static io.netty.buffer.Unpooled.unreleasableBuffer; +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.logging.LogLevel.INFO; + +import org.chromium.base.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +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"; + public static final String SERVE_SIMPLE_BROTLI_RESPONSE = "/simplebrotli"; + public static final String REPORTING_COLLECTOR_PATH = "/reporting-collector"; + public static final String SUCCESS_WITH_NEL_HEADERS_PATH = "/success-with-nel"; + public static final String COMBINED_HEADERS_PATH = "/combinedheaders"; + public static final String HANGING_REQUEST_PATH = "/hanging-request"; + + private static final String TAG = Http2TestHandler.class.getSimpleName(); + 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 mResponderMap = new HashMap<>(); + + private ReportingCollector mReportingCollector; + private String mServerUrl; + private CountDownLatch mHangingUrlLatch; + + /** + * Builder for HTTP/2 test handler. + */ + public static final class Builder + extends AbstractHttp2ConnectionHandlerBuilder { + public Builder() { + frameLogger(sLogger); + } + + public Builder setReportingCollector(ReportingCollector reportingCollector) { + mReportingCollector = reportingCollector; + return this; + } + + public Builder setServerUrl(String serverUrl) { + mServerUrl = serverUrl; + return this; + } + + public Builder setHangingUrlLatch(CountDownLatch hangingUrlLatch) { + mHangingUrlLatch = hangingUrlLatch; + return this; + } + + @Override + public Http2TestHandler build() { + return super.build(); + } + + @Override + protected Http2TestHandler build(Http2ConnectionDecoder decoder, + Http2ConnectionEncoder encoder, Http2Settings initialSettings) { + Http2TestHandler handler = new Http2TestHandler(decoder, encoder, initialSettings, + mReportingCollector, mServerUrl, mHangingUrlLatch); + frameListener(handler); + return handler; + } + + private ReportingCollector mReportingCollector; + private String mServerUrl; + private CountDownLatch mHangingUrlLatch; + } + + private class RequestResponder { + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) { + encoder().writeHeaders(ctx, streamId, createResponseHeadersFromRequestHeaders(headers), + 0, endOfStream, 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 CombinedHeadersResponder extends RequestResponder { + @Override + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) { + ByteBuf content = ctx.alloc().buffer(); + ByteBufUtil.writeAscii(content, "GET"); + Http2Headers responseHeaders = new DefaultHttp2Headers().status(OK.codeAsText()); + // Upon receiving, the following two headers will be jointed by '\0'. + responseHeaders.add("foo", "bar"); + responseHeaders.add("foo", "bar2"); + encoder().writeHeaders(ctx, streamId, responseHeaders, 0, false, ctx.newPromise()); + encoder().writeData(ctx, streamId, content, 0, true, ctx.newPromise()); + ctx.flush(); + } + } + + private class HangingRequestResponder extends RequestResponder { + @Override + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) { + try { + mHangingUrlLatch.await(); + } catch (InterruptedException e) { + } + } + } + + 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 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(); + } + } + + // A RequestResponder that serves a simple Brotli-encoded response. + private class ServeSimpleBrotliResponder extends RequestResponder { + @Override + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) { + Http2Headers responseHeaders = new DefaultHttp2Headers().status(OK.codeAsText()); + byte[] quickfoxCompressed = {0x0b, 0x15, -0x80, 0x54, 0x68, 0x65, 0x20, 0x71, 0x75, + 0x69, 0x63, 0x6b, 0x20, 0x62, 0x72, 0x6f, 0x77, 0x6e, 0x20, 0x66, 0x6f, 0x78, + 0x20, 0x6a, 0x75, 0x6d, 0x70, 0x73, 0x20, 0x6f, 0x76, 0x65, 0x72, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x6c, 0x61, 0x7a, 0x79, 0x20, 0x64, 0x6f, 0x67, 0x03}; + ByteBuf content = copiedBuffer(quickfoxCompressed); + responseHeaders.add("content-encoding", "br"); + encoder().writeHeaders(ctx, streamId, responseHeaders, 0, false, ctx.newPromise()); + encoder().writeData(ctx, streamId, content, 0, true, ctx.newPromise()); + ctx.flush(); + } + } + + // A RequestResponder that implements a Reporting collector. + private class ReportingCollectorResponder extends RequestResponder { + private ByteArrayOutputStream mPartialPayload = new ByteArrayOutputStream(); + + @Override + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) {} + + @Override + int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, + boolean endOfStream) { + int processed = data.readableBytes() + padding; + try { + data.readBytes(mPartialPayload, data.readableBytes()); + } catch (IOException e) { + } + if (endOfStream) { + processPayload(ctx, streamId); + } + return processed; + } + + private void processPayload(ChannelHandlerContext ctx, int streamId) { + boolean succeeded = false; + try { + String payload = mPartialPayload.toString(CharsetUtil.UTF_8.name()); + succeeded = mReportingCollector.addReports(payload); + } catch (UnsupportedEncodingException e) { + } + Http2Headers responseHeaders; + if (succeeded) { + responseHeaders = new DefaultHttp2Headers().status(OK.codeAsText()); + } else { + responseHeaders = new DefaultHttp2Headers().status(BAD_REQUEST.codeAsText()); + } + encoder().writeHeaders(ctx, streamId, responseHeaders, 0, true, ctx.newPromise()); + ctx.flush(); + } + } + + // A RequestResponder that serves a successful response with Reporting and NEL headers + private class SuccessWithNELHeadersResponder extends RequestResponder { + @Override + void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, + Http2Headers headers) { + Http2Headers responseHeaders = new DefaultHttp2Headers().status(OK.codeAsText()); + responseHeaders.add("report-to", getReportToHeader()); + responseHeaders.add("nel", getNELHeader()); + encoder().writeHeaders(ctx, streamId, responseHeaders, 0, true, ctx.newPromise()); + ctx.flush(); + } + + @Override + int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, + boolean endOfStream) { + int processed = data.readableBytes() + padding; + return processed; + } + + private String getReportToHeader() { + return String.format("{\"group\": \"nel\", \"max_age\": 86400, " + + "\"endpoints\": [{\"url\": \"%s%s\"}]}", + mServerUrl, REPORTING_COLLECTOR_PATH); + } + + private String getNELHeader() { + return "{\"report_to\": \"nel\", \"max_age\": 86400, \"success_fraction\": 1.0}"; + } + } + + 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 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, ReportingCollector reportingCollector, String serverUrl, + CountDownLatch hangingUrlLatch) { + super(decoder, encoder, initialSettings); + mReportingCollector = reportingCollector; + mServerUrl = serverUrl; + mHangingUrlLatch = hangingUrlLatch; + } + + @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 if (path.startsWith(SERVE_SIMPLE_BROTLI_RESPONSE)) { + responder = new ServeSimpleBrotliResponder(); + } else if (path.startsWith(REPORTING_COLLECTOR_PATH)) { + responder = new ReportingCollectorResponder(); + } else if (path.startsWith(SUCCESS_WITH_NEL_HEADERS_PATH)) { + responder = new SuccessWithNELHeadersResponder(); + } else if (path.startsWith(COMBINED_HEADERS_PATH)) { + responder = new CombinedHeadersResponder(); + } else if (path.startsWith(HANGING_REQUEST_PATH)) { + responder = new HangingRequestResponder(); + } 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/src/components/cronet/android/test/src/org/chromium/net/Http2TestServer.java b/src/components/cronet/android/test/src/org/chromium/net/Http2TestServer.java new file mode 100644 index 0000000000..d9cb9eb501 --- /dev/null +++ b/src/components/cronet/android/test/src/org/chromium/net/Http2TestServer.java @@ -0,0 +1,263 @@ +// 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 java.util.concurrent.CountDownLatch; + +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 Channel sServerChannel; + private static final String TAG = Http2TestServer.class.getSimpleName(); + + private static final String HOST = "127.0.0.1"; + // Server port. + private static final int PORT = 8443; + + private static ReportingCollector sReportingCollector; + + public static boolean shutdownHttp2TestServer() throws Exception { + if (sServerChannel != null) { + sServerChannel.close().sync(); + sServerChannel = null; + sReportingCollector = 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 ReportingCollector getReportingCollector() { + return sReportingCollector; + } + + 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; + } + + /** + * When using this you must provide a CountDownLatch in the call to startHttp2TestServer. + * The request handler will continue to hang until the provided CountDownLatch reaches 0. + * + * @return url of the server resource which will hang indefinitely. + */ + public static String getHangingRequestUrl() { + return getServerUrl() + Http2TestHandler.HANGING_REQUEST_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; + } + + /** + * @return url of a brotli-encoded server resource. + */ + public static String getServeSimpleBrotliResponse() { + return getServerUrl() + Http2TestHandler.SERVE_SIMPLE_BROTLI_RESPONSE; + } + + /** + * @return url of the reporting collector + */ + public static String getReportingCollectorUrl() { + return getServerUrl() + Http2TestHandler.REPORTING_COLLECTOR_PATH; + } + + /** + * @return url of a resource that includes Reporting and NEL policy headers in its response + */ + public static String getSuccessWithNELHeadersUrl() { + return getServerUrl() + Http2TestHandler.SUCCESS_WITH_NEL_HEADERS_PATH; + } + + /** + * @return url of a resource that sends response headers with the same key + */ + public static String getCombinedHeadersUrl() { + return getServerUrl() + Http2TestHandler.COMBINED_HEADERS_PATH; + } + + public static boolean startHttp2TestServer( + Context context, String certFileName, String keyFileName) throws Exception { + return startHttp2TestServer(context, certFileName, keyFileName, null); + } + + public static boolean startHttp2TestServer(Context context, String certFileName, + String keyFileName, CountDownLatch hangingUrlLatch) throws Exception { + sReportingCollector = new ReportingCollector(); + Http2TestServerRunnable http2TestServerRunnable = + new Http2TestServerRunnable(new File(CertTestUtil.CERTS_DIRECTORY + certFileName), + new File(CertTestUtil.CERTS_DIRECTORY + keyFileName), hangingUrlLatch); + new Thread(http2TestServerRunnable).start(); + http2TestServerRunnable.blockUntilStarted(); + return true; + } + + private Http2TestServer() {} + + private static class Http2TestServerRunnable implements Runnable { + private final ConditionVariable mBlock = new ConditionVariable(); + private final SslContext mSslCtx; + private final CountDownLatch mHangingUrlLatch; + + Http2TestServerRunnable(File certFile, File keyFile, CountDownLatch hangingUrlLatch) + throws Exception { + ApplicationProtocolConfig applicationProtocolConfig = new ApplicationProtocolConfig( + Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE, + SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2); + + // Don't make netty use java.security.KeyStore.getInstance("JKS") as it doesn't + // exist. Just avoid a KeyManagerFactory as it's unnecessary for our testing. + System.setProperty("io.netty.handler.ssl.openssl.useKeyManagerFactory", "false"); + + mSslCtx = new OpenSslServerContext(certFile, keyFile, null, null, + Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE, + applicationProtocolConfig, 0, 0); + + mHangingUrlLatch = hangingUrlLatch; + } + + public void blockUntilStarted() { + mBlock.block(); + } + + @Override + public void run() { + boolean retry = false; + do { + 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, mHangingUrlLatch)); + + sServerChannel = b.bind(PORT).sync().channel(); + Log.i(TAG, "Netty HTTP/2 server started on " + getServerUrl()); + mBlock.open(); + sServerChannel.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + Log.i(TAG, "Stopped Http2TestServerRunnable!"); + retry = false; + } catch (Exception e) { + Log.e(TAG, "Netty server failed to start", e); + // Retry once if we hit https://github.com/netty/netty/issues/2616 before the + // server starts. + retry = !retry && sServerChannel == null + && e.toString().contains("java.nio.channels.ClosedChannelException"); + } + } while (retry); + } + } + + /** + * Sets up the Netty pipeline for the test server. + */ + private static class Http2ServerInitializer extends ChannelInitializer { + private final SslContext mSslCtx; + private final CountDownLatch mHangingUrlLatch; + + public Http2ServerInitializer(SslContext sslCtx, CountDownLatch hangingUrlLatch) { + mSslCtx = sslCtx; + mHangingUrlLatch = hangingUrlLatch; + } + + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast( + mSslCtx.newHandler(ch.alloc()), new Http2NegotiationHandler(mHangingUrlLatch)); + } + } + + private static class Http2NegotiationHandler extends ApplicationProtocolNegotiationHandler { + private final CountDownLatch mHangingUrlLatch; + + protected Http2NegotiationHandler(CountDownLatch hangingUrlLatch) { + super(ApplicationProtocolNames.HTTP_1_1); + mHangingUrlLatch = hangingUrlLatch; + } + + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) + throws Exception { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ctx.pipeline().addLast(new Http2TestHandler.Builder() + .setReportingCollector(sReportingCollector) + .setServerUrl(getServerUrl()) + .setHangingUrlLatch(mHangingUrlLatch) + .build()); + return; + } + + throw new IllegalStateException("unknown protocol: " + protocol); + } + } +} diff --git a/src/components/cronet/android/test/src/org/chromium/net/MockCertVerifier.java b/src/components/cronet/android/test/src/org/chromium/net/MockCertVerifier.java new file mode 100644 index 0000000000..9754a761b9 --- /dev/null +++ b/src/components/cronet/android/test/src/org/chromium/net/MockCertVerifier.java @@ -0,0 +1,31 @@ +// 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.annotations.JNINamespace; +import org.chromium.base.test.util.UrlUtils; + +/** + * A Java wrapper to supply a net::MockCertVerifier which can be then passed + * into {@link CronetEngine.Builder#setMockCertVerifierForTesting}. + * The native pointer will be freed when the CronetEngine is torn down. + */ +@JNINamespace("cronet") +public class MockCertVerifier { + private MockCertVerifier() {} + + /** + * Creates a new net::MockCertVerifier, and returns a pointer to it. + * @param certs a String array of certificate filenames in + * net::GetTestCertsDirectory() to accept in testing. + * @return a pointer to the newly created net::MockCertVerifier. + */ + public static long createMockCertVerifier(String[] certs, boolean knownRoot) { + return nativeCreateMockCertVerifier(certs, knownRoot, UrlUtils.getIsolatedTestRoot()); + } + + private static native long nativeCreateMockCertVerifier( + String[] certs, boolean knownRoot, String testDataDir); +} diff --git a/src/components/cronet/android/test/src/org/chromium/net/MockUrlRequestJobFactory.java b/src/components/cronet/android/test/src/org/chromium/net/MockUrlRequestJobFactory.java new file mode 100644 index 0000000000..43823554ed --- /dev/null +++ b/src/components/cronet/android/test/src/org/chromium/net/MockUrlRequestJobFactory.java @@ -0,0 +1,107 @@ +// Copyright 2014 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 static junit.framework.Assert.assertTrue; + +import org.chromium.base.annotations.JNINamespace; +import org.chromium.net.impl.CronetUrlRequestContext; +import org.chromium.net.test.FailurePhase; + +/** + * Helper class to set up url interceptors for testing purposes. + */ +@JNINamespace("cronet") +public final class MockUrlRequestJobFactory { + private final long mInterceptorHandle; + private final CronetTestUtil.NetworkThreadTestConnector mNetworkThreadTestConnector; + + /** + * Sets up URL interceptors. + */ + public MockUrlRequestJobFactory(CronetEngine cronetEngine) { + mNetworkThreadTestConnector = new CronetTestUtil.NetworkThreadTestConnector(cronetEngine); + + mInterceptorHandle = nativeAddUrlInterceptors( + ((CronetUrlRequestContext) cronetEngine).getUrlRequestContextAdapter()); + } + + /** + * Remove URL Interceptors. + */ + public void shutdown() { + nativeRemoveUrlInterceptorJobFactory(mInterceptorHandle); + mNetworkThreadTestConnector.shutdown(); + } + + /** + * Constructs a mock URL that hangs or fails at certain phase. + * + * @param phase at which request fails. It should be a value in + * org.chromium.net.test.FailurePhase. + * @param netError reported by UrlRequestJob. Passing -1, results in hang. + */ + public static String getMockUrlWithFailure(int phase, int netError) { + assertTrue(netError < 0); + switch (phase) { + case FailurePhase.START: + case FailurePhase.READ_SYNC: + case FailurePhase.READ_ASYNC: + break; + default: + throw new IllegalArgumentException( + "phase not in org.chromium.net.test.FailurePhase"); + } + return nativeGetMockUrlWithFailure(phase, netError); + } + + /** + * Constructs a mock URL that synchronously responds with data repeated many + * times. + * + * @param data to return in response. + * @param dataRepeatCount number of times to repeat the data. + */ + public static String getMockUrlForData(String data, int dataRepeatCount) { + return nativeGetMockUrlForData(data, dataRepeatCount); + } + + /** + * Constructs a mock URL that will request client certificate and return + * the string "data" as the response. + */ + public static String getMockUrlForClientCertificateRequest() { + return nativeGetMockUrlForClientCertificateRequest(); + } + + /** + * Constructs a mock URL that will fail with an SSL certificate error. + */ + public static String getMockUrlForSSLCertificateError() { + return nativeGetMockUrlForSSLCertificateError(); + } + + /** + * Constructs a mock URL that will hang when try to read response body from the remote. + */ + public static String getMockUrlForHangingRead() { + return nativeGetMockUrlForHangingRead(); + } + + private static native long nativeAddUrlInterceptors(long requestContextAdapter); + + private static native void nativeRemoveUrlInterceptorJobFactory(long interceptorHandle); + + private static native String nativeGetMockUrlWithFailure(int phase, int netError); + + private static native String nativeGetMockUrlForData(String data, + int dataRepeatCount); + + private static native String nativeGetMockUrlForClientCertificateRequest(); + + private static native String nativeGetMockUrlForSSLCertificateError(); + + private static native String nativeGetMockUrlForHangingRead(); +} diff --git a/src/components/cronet/android/test/src/org/chromium/net/NativeTestServer.java b/src/components/cronet/android/test/src/org/chromium/net/NativeTestServer.java new file mode 100644 index 0000000000..6f703abe9f --- /dev/null +++ b/src/components/cronet/android/test/src/org/chromium/net/NativeTestServer.java @@ -0,0 +1,98 @@ +// Copyright 2014 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 org.chromium.base.annotations.JNINamespace; +import org.chromium.base.test.util.UrlUtils; + +/** + * Wrapper class to start an in-process native test server, and get URLs + * needed to talk to it. + */ +@JNINamespace("cronet") +public final class NativeTestServer { + // This variable contains the response body of a request to getSuccessURL(). + public static final String SUCCESS_BODY = "this is a text file\n"; + + public static boolean startNativeTestServer(Context context) { + TestFilesInstaller.installIfNeeded(context); + return nativeStartNativeTestServer( + TestFilesInstaller.getInstalledPath(context), UrlUtils.getIsolatedTestRoot()); + } + + public static void shutdownNativeTestServer() { + nativeShutdownNativeTestServer(); + } + + public static String getEchoBodyURL() { + return nativeGetEchoBodyURL(); + } + + public static String getEchoHeaderURL(String header) { + return nativeGetEchoHeaderURL(header); + } + + public static String getEchoAllHeadersURL() { + return nativeGetEchoAllHeadersURL(); + } + + public static String getEchoMethodURL() { + return nativeGetEchoMethodURL(); + } + + public static String getRedirectToEchoBody() { + return nativeGetRedirectToEchoBody(); + } + + public static String getFileURL(String filePath) { + return nativeGetFileURL(filePath); + } + + // Returns a URL that the server will return an Exabyte of data + public static String getExabyteResponseURL() { + return nativeGetExabyteResponseURL(); + } + + // The following URLs will make NativeTestServer serve a response based on + // the contents of the corresponding file and its mock-http-headers file. + + public static String getSuccessURL() { + return nativeGetFileURL("/success.txt"); + } + + public static String getRedirectURL() { + return nativeGetFileURL("/redirect.html"); + } + + public static String getMultiRedirectURL() { + return nativeGetFileURL("/multiredirect.html"); + } + + public static String getNotFoundURL() { + return nativeGetFileURL("/notfound.html"); + } + + public static int getPort() { + return nativeGetPort(); + } + + public static String getHostPort() { + return nativeGetHostPort(); + } + + private static native boolean nativeStartNativeTestServer(String filePath, String testDataDir); + private static native void nativeShutdownNativeTestServer(); + private static native String nativeGetEchoBodyURL(); + private static native String nativeGetEchoHeaderURL(String header); + private static native String nativeGetEchoAllHeadersURL(); + private static native String nativeGetEchoMethodURL(); + private static native String nativeGetRedirectToEchoBody(); + private static native String nativeGetFileURL(String filePath); + private static native String nativeGetExabyteResponseURL(); + private static native String nativeGetHostPort(); + private static native int nativeGetPort(); +} diff --git a/src/components/cronet/android/test/src/org/chromium/net/QuicTestServer.java b/src/components/cronet/android/test/src/org/chromium/net/QuicTestServer.java new file mode 100644 index 0000000000..7537c21ad3 --- /dev/null +++ b/src/components/cronet/android/test/src/org/chromium/net/QuicTestServer.java @@ -0,0 +1,90 @@ +// 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.ContextUtils; +import org.chromium.base.Log; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.test.util.UrlUtils; + +/** + * Wrapper class to start a Quic test server. + */ +@JNINamespace("cronet") +public final class QuicTestServer { + private static final ConditionVariable sBlock = new ConditionVariable(); + private static final String TAG = QuicTestServer.class.getSimpleName(); + + private static final String CERT_USED = "quic-chain.pem"; + private static final String KEY_USED = "quic-leaf-cert.key"; + private static final String[] CERTS_USED = {CERT_USED}; + + private static boolean sServerRunning; + + /* + * Starts the server. + */ + public static void startQuicTestServer(Context context) { + if (sServerRunning) { + throw new IllegalStateException("Quic server is already running"); + } + TestFilesInstaller.installIfNeeded(context); + nativeStartQuicTestServer( + TestFilesInstaller.getInstalledPath(context), UrlUtils.getIsolatedTestRoot()); + sBlock.block(); + sBlock.close(); + sServerRunning = true; + } + + /** + * Shuts down the server. No-op if the server is already shut down. + */ + public static void shutdownQuicTestServer() { + if (!sServerRunning) { + return; + } + nativeShutdownQuicTestServer(); + sServerRunning = false; + } + + public static String getServerURL() { + return "https://" + getServerHost() + ":" + getServerPort(); + } + + public static String getServerHost() { + return CronetTestUtil.QUIC_FAKE_HOST; + } + + public static int getServerPort() { + return nativeGetServerPort(); + } + + public static final String getServerCert() { + return CERT_USED; + } + + public static final String getServerCertKey() { + return KEY_USED; + } + + public static long createMockCertVerifier() { + TestFilesInstaller.installIfNeeded(ContextUtils.getApplicationContext()); + return MockCertVerifier.createMockCertVerifier(CERTS_USED, true); + } + + @CalledByNative + private static void onServerStarted() { + Log.i(TAG, "Quic server started."); + sBlock.open(); + } + + private static native void nativeStartQuicTestServer(String filePath, String testDataDir); + private static native void nativeShutdownQuicTestServer(); + private static native int nativeGetServerPort(); +} diff --git a/src/components/cronet/android/test/src/org/chromium/net/ReportingCollector.java b/src/components/cronet/android/test/src/org/chromium/net/ReportingCollector.java new file mode 100644 index 0000000000..a120bda293 --- /dev/null +++ b/src/components/cronet/android/test/src/org/chromium/net/ReportingCollector.java @@ -0,0 +1,109 @@ +// Copyright 2018 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.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +/** + * Stores Reporting API reports received by a test collector, providing helper methods for checking + * whether expected reports were actually received. + */ +class ReportingCollector { + private ArrayList mReceivedReports = new ArrayList(); + private Semaphore mReceivedReportsSemaphore = new Semaphore(0); + + /** + * Stores a batch of uploaded reports. + * @param payload the POST payload from the upload + * @return whether the payload was parsed successfully + */ + public boolean addReports(String payload) { + try { + JSONArray reports = new JSONArray(payload); + int elementCount = 0; + synchronized (mReceivedReports) { + for (int i = 0; i < reports.length(); i++) { + JSONObject element = reports.optJSONObject(i); + if (element != null) { + mReceivedReports.add(element); + elementCount++; + } + } + } + mReceivedReportsSemaphore.release(elementCount); + return true; + } catch (JSONException e) { + return false; + } + } + + /** + * Checks whether a report with the given payload exists or not. + */ + public boolean containsReport(String expected) { + try { + JSONObject expectedReport = new JSONObject(expected); + synchronized (mReceivedReports) { + for (JSONObject received : mReceivedReports) { + if (isJSONObjectSubset(expectedReport, received)) { + return true; + } + } + } + return false; + } catch (JSONException e) { + return false; + } + } + + /** + * Waits until the requested number of reports have been received, with a 5-second timeout. + */ + public void waitForReports(int reportCount) { + final int timeoutSeconds = 5; + try { + mReceivedReportsSemaphore.tryAcquire(reportCount, timeoutSeconds, TimeUnit.SECONDS); + } catch (InterruptedException e) { + } + } + + /** + * Checks whether one {@link JSONObject} is a subset of another. Any fields that appear in + * {@code lhs} must also appear in {@code rhs}, with the same value. There can be extra fields + * in {@code rhs}; if so, they are ignored. + */ + private boolean isJSONObjectSubset(JSONObject lhs, JSONObject rhs) { + Iterator keys = lhs.keys(); + while (keys.hasNext()) { + String key = keys.next(); + Object lhsElement = lhs.opt(key); + Object rhsElement = rhs.opt(key); + + if (rhsElement == null) { + // lhs has an element that doesn't appear in rhs + return false; + } + + if (lhsElement instanceof JSONObject) { + if (!(rhsElement instanceof JSONObject)) { + return false; + } + return isJSONObjectSubset((JSONObject) lhsElement, (JSONObject) rhsElement); + } + + if (!lhsElement.equals(rhsElement)) { + return false; + } + } + return true; + } +}; diff --git a/src/components/cronet/android/test/src/org/chromium/net/TestFilesInstaller.java b/src/components/cronet/android/test/src/org/chromium/net/TestFilesInstaller.java new file mode 100644 index 0000000000..5ca1056237 --- /dev/null +++ b/src/components/cronet/android/test/src/org/chromium/net/TestFilesInstaller.java @@ -0,0 +1,33 @@ +// Copyright 2014 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 org.chromium.base.test.util.UrlUtils; + +/** + * Helper class to install test files. + */ +public final class TestFilesInstaller { + // Name of the asset directory in which test files are stored. + private static final String TEST_FILE_ASSET_PATH = "components/cronet/testing/test_server/data"; + + /** + * Installs test files if files have not been installed. + */ + public static void installIfNeeded(Context context) { + // Do nothing. + // NOTE(pauljensen): This hook is used (overridden) when tests are run in other + // configurations, so it should not be removed. + } + + /** + * Returns the installed path of the test files. + */ + public static String getInstalledPath(Context context) { + return UrlUtils.getIsolatedTestRoot() + "/" + TEST_FILE_ASSET_PATH; + } +} diff --git a/src/components/cronet/android/test/src/org/chromium/net/TestUploadDataStreamHandler.java b/src/components/cronet/android/test/src/org/chromium/net/TestUploadDataStreamHandler.java new file mode 100644 index 0000000000..45705ee666 --- /dev/null +++ b/src/components/cronet/android/test/src/org/chromium/net/TestUploadDataStreamHandler.java @@ -0,0 +1,181 @@ +// 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.junit.Assert; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeClassQualifiedName; +import org.chromium.net.impl.CronetUrlRequestContext; + +/** + * A wrapper class on top of the native net::UploadDataStream. This class is + * used in tests to drive the native UploadDataStream directly. + */ +@JNINamespace("cronet") +public final class TestUploadDataStreamHandler { + private final CronetTestUtil.NetworkThreadTestConnector mNetworkThreadTestConnector; + private final CronetEngine mCronetEngine; + private long mTestUploadDataStreamHandler; + private ConditionVariable mWaitInitCalled = new ConditionVariable(); + private ConditionVariable mWaitInitComplete = new ConditionVariable(); + private ConditionVariable mWaitReadComplete = new ConditionVariable(); + private ConditionVariable mWaitResetComplete = new ConditionVariable(); + // Waits for checkIfInitCallbackInvoked() returns result asynchronously. + private ConditionVariable mWaitCheckInit = new ConditionVariable(); + // Waits for checkIfReadCallbackInvoked() returns result asynchronously. + private ConditionVariable mWaitCheckRead = new ConditionVariable(); + // If true, init completes synchronously. + private boolean mInitCompletedSynchronously; + private String mData = ""; + + public TestUploadDataStreamHandler(Context context, final long uploadDataStream) { + mCronetEngine = new CronetEngine.Builder(context).build(); + mNetworkThreadTestConnector = new CronetTestUtil.NetworkThreadTestConnector(mCronetEngine); + CronetUrlRequestContext requestContext = (CronetUrlRequestContext) mCronetEngine; + mTestUploadDataStreamHandler = nativeCreateTestUploadDataStreamHandler( + uploadDataStream, requestContext.getUrlRequestContextAdapter()); + } + + public void destroyNativeObjects() { + if (mTestUploadDataStreamHandler != 0) { + nativeDestroy(mTestUploadDataStreamHandler); + mTestUploadDataStreamHandler = 0; + mNetworkThreadTestConnector.shutdown(); + mCronetEngine.shutdown(); + } + } + + /** + * Init and returns whether init completes synchronously. + */ + public boolean init() { + mData = ""; + nativeInit(mTestUploadDataStreamHandler); + mWaitInitCalled.block(); + mWaitInitCalled.close(); + return mInitCompletedSynchronously; + } + + public void read() { + nativeRead(mTestUploadDataStreamHandler); + } + + public void reset() { + mData = ""; + nativeReset(mTestUploadDataStreamHandler); + mWaitResetComplete.block(); + mWaitResetComplete.close(); + } + + /** + * Checks that {@link #onInitCompleted} has not invoked asynchronously + * by the native UploadDataStream. + */ + public void checkInitCallbackNotInvoked() { + nativeCheckInitCallbackNotInvoked(mTestUploadDataStreamHandler); + mWaitCheckInit.block(); + mWaitCheckInit.close(); + } + + /** + * Checks that {@link #onReadCompleted} has not been invoked asynchronously + * by the native UploadDataStream. + */ + public void checkReadCallbackNotInvoked() { + nativeCheckReadCallbackNotInvoked(mTestUploadDataStreamHandler); + mWaitCheckRead.block(); + mWaitCheckRead.close(); + } + + public String getData() { + return mData; + } + + public void waitForReadComplete() { + mWaitReadComplete.block(); + mWaitReadComplete.close(); + } + + public void waitForInitComplete() { + mWaitInitComplete.block(); + mWaitInitComplete.close(); + } + + // Called on network thread. + @CalledByNative + private void onInitCalled(int res) { + if (res == 0) { + mInitCompletedSynchronously = true; + } else { + mInitCompletedSynchronously = false; + } + mWaitInitCalled.open(); + } + + // Called on network thread. + @CalledByNative + private void onReadCompleted(int bytesRead, String data) { + mData = data; + mWaitReadComplete.open(); + } + + // Called on network thread. + @CalledByNative + private void onInitCompleted(int res) { + // If init() completed synchronously, waitForInitComplete() will + // not be invoked in the test, so skip mWaitInitComplete.open(). + if (!mInitCompletedSynchronously) { + mWaitInitComplete.open(); + } + } + + // Called on network thread. + @CalledByNative + private void onResetCompleted() { + mWaitResetComplete.open(); + } + + // Called on network thread. + @CalledByNative + private void onCheckInitCallbackNotInvoked(boolean initCallbackNotInvoked) { + Assert.assertTrue(initCallbackNotInvoked); + mWaitCheckInit.open(); + } + + // Called on network thread. + @CalledByNative + private void onCheckReadCallbackNotInvoked(boolean readCallbackNotInvoked) { + Assert.assertTrue(readCallbackNotInvoked); + mWaitCheckRead.open(); + } + + @NativeClassQualifiedName("TestUploadDataStreamHandler") + private native void nativeInit(long nativePtr); + + @NativeClassQualifiedName("TestUploadDataStreamHandler") + private native void nativeRead(long nativePtr); + + @NativeClassQualifiedName("TestUploadDataStreamHandler") + private native void nativeReset(long nativePtr); + + @NativeClassQualifiedName("TestUploadDataStreamHandler") + private native void nativeCheckInitCallbackNotInvoked( + long nativePtr); + + @NativeClassQualifiedName("TestUploadDataStreamHandler") + private native void nativeCheckReadCallbackNotInvoked( + long nativePtr); + + @NativeClassQualifiedName("TestUploadDataStreamHandler") + private native void nativeDestroy(long nativePtr); + + private native long nativeCreateTestUploadDataStreamHandler( + long uploadDataStream, long contextAdapter); +} diff --git a/src/components/cronet/android/test/test_upload_data_stream_handler.cc b/src/components/cronet/android/test/test_upload_data_stream_handler.cc new file mode 100644 index 0000000000..fe1df4c53d --- /dev/null +++ b/src/components/cronet/android/test/test_upload_data_stream_handler.cc @@ -0,0 +1,194 @@ +// 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 "components/cronet/android/test/test_upload_data_stream_handler.h" + +#include +#include +#include + +#include "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/bind.h" +#include "components/cronet/android/cronet_tests_jni_headers/TestUploadDataStreamHandler_jni.h" +#include "components/cronet/android/test/cronet_test_util.h" +#include "net/base/net_errors.h" +#include "net/log/net_log_with_source.h" + +using base::android::JavaParamRef; + +namespace cronet { + +static const size_t kReadBufferSize = 32768; + +TestUploadDataStreamHandler::TestUploadDataStreamHandler( + std::unique_ptr upload_data_stream, + JNIEnv* env, + jobject jtest_upload_data_stream_handler, + jlong jcontext_adapter) + : init_callback_invoked_(false), + read_callback_invoked_(false), + bytes_read_(0), + network_thread_(TestUtil::GetTaskRunner(jcontext_adapter)) { + upload_data_stream_ = std::move(upload_data_stream); + jtest_upload_data_stream_handler_.Reset(env, + jtest_upload_data_stream_handler); +} + +TestUploadDataStreamHandler::~TestUploadDataStreamHandler() { +} + +void TestUploadDataStreamHandler::Destroy( + JNIEnv* env, + const JavaParamRef& jcaller) { + DCHECK(!network_thread_->BelongsToCurrentThread()); + network_thread_->DeleteSoon(FROM_HERE, this); +} + +void TestUploadDataStreamHandler::OnInitCompleted(int res) { + DCHECK(network_thread_->BelongsToCurrentThread()); + init_callback_invoked_ = true; + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_TestUploadDataStreamHandler_onInitCompleted( + env, jtest_upload_data_stream_handler_, res); +} + +void TestUploadDataStreamHandler::OnReadCompleted(int res) { + DCHECK(network_thread_->BelongsToCurrentThread()); + read_callback_invoked_ = true; + bytes_read_ = res; + NotifyJavaReadCompleted(); +} + +void TestUploadDataStreamHandler::Init(JNIEnv* env, + const JavaParamRef& jcaller) { + DCHECK(!network_thread_->BelongsToCurrentThread()); + network_thread_->PostTask( + FROM_HERE, + base::BindOnce(&TestUploadDataStreamHandler::InitOnNetworkThread, + base::Unretained(this))); +} + +void TestUploadDataStreamHandler::Read(JNIEnv* env, + const JavaParamRef& jcaller) { + DCHECK(!network_thread_->BelongsToCurrentThread()); + network_thread_->PostTask( + FROM_HERE, + base::BindOnce(&TestUploadDataStreamHandler::ReadOnNetworkThread, + base::Unretained(this))); +} + +void TestUploadDataStreamHandler::Reset(JNIEnv* env, + const JavaParamRef& jcaller) { + DCHECK(!network_thread_->BelongsToCurrentThread()); + network_thread_->PostTask( + FROM_HERE, + base::BindOnce(&TestUploadDataStreamHandler::ResetOnNetworkThread, + base::Unretained(this))); +} + +void TestUploadDataStreamHandler::CheckInitCallbackNotInvoked( + JNIEnv* env, + const JavaParamRef& jcaller) { + DCHECK(!network_thread_->BelongsToCurrentThread()); + network_thread_->PostTask( + FROM_HERE, base::BindOnce(&TestUploadDataStreamHandler:: + CheckInitCallbackNotInvokedOnNetworkThread, + base::Unretained(this))); +} + +void TestUploadDataStreamHandler::CheckReadCallbackNotInvoked( + JNIEnv* env, + const JavaParamRef& jcaller) { + DCHECK(!network_thread_->BelongsToCurrentThread()); + network_thread_->PostTask( + FROM_HERE, base::BindOnce(&TestUploadDataStreamHandler:: + CheckReadCallbackNotInvokedOnNetworkThread, + base::Unretained(this))); +} + +void TestUploadDataStreamHandler::InitOnNetworkThread() { + DCHECK(network_thread_->BelongsToCurrentThread()); + init_callback_invoked_ = false; + read_buffer_ = nullptr; + bytes_read_ = 0; + int res = upload_data_stream_->Init( + base::BindOnce(&TestUploadDataStreamHandler::OnInitCompleted, + base::Unretained(this)), + net::NetLogWithSource()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_TestUploadDataStreamHandler_onInitCalled( + env, jtest_upload_data_stream_handler_, res); + + if (res == net::OK) { + cronet::Java_TestUploadDataStreamHandler_onInitCompleted( + env, jtest_upload_data_stream_handler_, res); + } +} + +void TestUploadDataStreamHandler::ReadOnNetworkThread() { + DCHECK(network_thread_->BelongsToCurrentThread()); + read_callback_invoked_ = false; + if (!read_buffer_.get()) + read_buffer_ = base::MakeRefCounted(kReadBufferSize); + + int bytes_read = upload_data_stream_->Read( + read_buffer_.get(), kReadBufferSize, + base::BindOnce(&TestUploadDataStreamHandler::OnReadCompleted, + base::Unretained(this))); + if (bytes_read == net::OK) { + bytes_read_ = bytes_read; + NotifyJavaReadCompleted(); + } +} + +void TestUploadDataStreamHandler::ResetOnNetworkThread() { + DCHECK(network_thread_->BelongsToCurrentThread()); + read_buffer_ = nullptr; + bytes_read_ = 0; + upload_data_stream_->Reset(); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_TestUploadDataStreamHandler_onResetCompleted( + env, jtest_upload_data_stream_handler_); +} + +void TestUploadDataStreamHandler::CheckInitCallbackNotInvokedOnNetworkThread() { + DCHECK(network_thread_->BelongsToCurrentThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_TestUploadDataStreamHandler_onCheckInitCallbackNotInvoked( + env, jtest_upload_data_stream_handler_, !init_callback_invoked_); +} + +void TestUploadDataStreamHandler::CheckReadCallbackNotInvokedOnNetworkThread() { + DCHECK(network_thread_->BelongsToCurrentThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + cronet::Java_TestUploadDataStreamHandler_onCheckReadCallbackNotInvoked( + env, jtest_upload_data_stream_handler_, !read_callback_invoked_); +} + +void TestUploadDataStreamHandler::NotifyJavaReadCompleted() { + DCHECK(network_thread_->BelongsToCurrentThread()); + JNIEnv* env = base::android::AttachCurrentThread(); + std::string data_read = ""; + if (read_buffer_.get() && bytes_read_ > 0) + data_read = std::string(read_buffer_->data(), bytes_read_); + cronet::Java_TestUploadDataStreamHandler_onReadCompleted( + env, jtest_upload_data_stream_handler_, bytes_read_, + base::android::ConvertUTF8ToJavaString(env, data_read)); +} + +static jlong JNI_TestUploadDataStreamHandler_CreateTestUploadDataStreamHandler( + JNIEnv* env, + const JavaParamRef& jtest_upload_data_stream_handler, + jlong jupload_data_stream, + jlong jcontext_adapter) { + std::unique_ptr upload_data_stream( + reinterpret_cast(jupload_data_stream)); + TestUploadDataStreamHandler* handler = new TestUploadDataStreamHandler( + std::move(upload_data_stream), env, jtest_upload_data_stream_handler, + jcontext_adapter); + return reinterpret_cast(handler); +} + +} // namespace cronet diff --git a/src/components/cronet/android/test/test_upload_data_stream_handler.h b/src/components/cronet/android/test/test_upload_data_stream_handler.h new file mode 100644 index 0000000000..863b7c4092 --- /dev/null +++ b/src/components/cronet/android/test/test_upload_data_stream_handler.h @@ -0,0 +1,105 @@ +// 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_TEST_TEST_UPLOAD_DATA_STREAM_HANDLER_H_ +#define COMPONENTS_CRONET_ANDROID_TEST_TEST_UPLOAD_DATA_STREAM_HANDLER_H_ + +#include + +#include + +#include "base/android/scoped_java_ref.h" +#include "base/memory/ref_counted.h" +#include "base/task/single_thread_task_runner.h" +#include "net/base/io_buffer.h" +#include "net/base/upload_data_stream.h" + +namespace cronet { + +/** + * This class allows a net::UploadDataStream to be driven directly from + * Java, for use in tests. + */ +class TestUploadDataStreamHandler { + public: + TestUploadDataStreamHandler( + std::unique_ptr upload_data_stream, + JNIEnv* env, + jobject jtest_upload_data_stream_handler, + jlong jcontext_adapter); + + TestUploadDataStreamHandler(const TestUploadDataStreamHandler&) = delete; + TestUploadDataStreamHandler& operator=(const TestUploadDataStreamHandler&) = + delete; + + ~TestUploadDataStreamHandler(); + + // Destroys |network_thread_| created by this class. + void Destroy(JNIEnv* env, + const base::android::JavaParamRef& jcaller); + + // Posts a task to |network_thread_| to call the corresponding method of + // net::UploadDataStream on |upload_data_stream_|. + + void Init(JNIEnv* env, const base::android::JavaParamRef& jcaller); + void Read(JNIEnv* env, const base::android::JavaParamRef& jcaller); + void Reset(JNIEnv* env, const base::android::JavaParamRef& jcaller); + + // Posts a task to |network_thread_| to check whether init complete callback + // has been invoked by net::UploadDataStream asynchronously, and notifies the + // Java side of the result. + void CheckInitCallbackNotInvoked( + JNIEnv* env, + const base::android::JavaParamRef& jcaller); + // Posts a task to |network_thread_| to check whether read complete callback + // has been invoked by net::UploadDataStream asynchronously, and notifies the + // Java side of the result. + void CheckReadCallbackNotInvoked( + JNIEnv* env, + const base::android::JavaParamRef& jcaller); + + private: + // Complete callbacks that are passed to the |upload_data_stream_|. + void OnInitCompleted(int res); + void OnReadCompleted(int res); + + // Helper methods that run corresponding task on |network_thread_|. + + void InitOnNetworkThread(); + void ReadOnNetworkThread(); + void ResetOnNetworkThread(); + void CheckInitCallbackNotInvokedOnNetworkThread(); + void CheckReadCallbackNotInvokedOnNetworkThread(); + + // Notify the Java TestUploadDataStreamHandler that read has completed. + void NotifyJavaReadCompleted(); + + // True if |OnInitCompleted| callback has been invoked. It is set to false + // when init or reset is called again. Created on a Java thread, but is only + // accessed from |network_thread_|. + bool init_callback_invoked_; + // True if |OnReadCompleted| callback has been invoked. It is set to false + // when init or reset is called again. Created on a Java thread, but is only + // accessed from |network_thread_|. + bool read_callback_invoked_; + // Indicates the number of bytes read. It is reset to 0 when init, reset, or + // read is called again. Created on a Java thread, but is only accessed from + // |network_thread_|. + int bytes_read_; + + // Created and destroyed on the same Java thread. This is where methods of + // net::UploadDataStream run on. + scoped_refptr network_thread_; + // Created on a Java thread. Accessed only on |network_thread_|. + std::unique_ptr upload_data_stream_; + // Created and accessed only on |network_thread_|. + scoped_refptr read_buffer_; + // A Java reference pointer for calling methods on the Java + // TestUploadDataStreamHandler object. Initialized during construction. + base::android::ScopedJavaGlobalRef jtest_upload_data_stream_handler_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_TEST_TEST_UPLOAD_DATA_STREAM_HANDLER_H_ diff --git a/src/components/cronet/android/test/url_request_intercepting_job_factory.cc b/src/components/cronet/android/test/url_request_intercepting_job_factory.cc new file mode 100644 index 0000000000..9bb40a22a1 --- /dev/null +++ b/src/components/cronet/android/test/url_request_intercepting_job_factory.cc @@ -0,0 +1,38 @@ +// Copyright 2014 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 "components/cronet/android/test/url_request_intercepting_job_factory.h" + +#include + +#include "base/check_op.h" +#include "base/memory/ptr_util.h" +#include "net/url_request/url_request_interceptor.h" +#include "net/url_request/url_request_job.h" + +namespace cronet { + +URLRequestInterceptingJobFactory::URLRequestInterceptingJobFactory( + net::URLRequestJobFactory* job_factory, + net::URLRequestInterceptor* interceptor) + : job_factory_(job_factory), interceptor_(interceptor) {} + +URLRequestInterceptingJobFactory::~URLRequestInterceptingJobFactory() = default; + +std::unique_ptr URLRequestInterceptingJobFactory::CreateJob( + net::URLRequest* request) const { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + std::unique_ptr job = + interceptor_->MaybeInterceptRequest(request); + if (job) + return job; + return job_factory_->CreateJob(request); +} + +bool URLRequestInterceptingJobFactory::IsSafeRedirectTarget( + const GURL& location) const { + return job_factory_->IsSafeRedirectTarget(location); +} + +} // namespace cronet diff --git a/src/components/cronet/android/test/url_request_intercepting_job_factory.h b/src/components/cronet/android/test/url_request_intercepting_job_factory.h new file mode 100644 index 0000000000..f4303c040e --- /dev/null +++ b/src/components/cronet/android/test/url_request_intercepting_job_factory.h @@ -0,0 +1,57 @@ +// Copyright 2014 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_TEST_URL_REQUEST_INTERCEPTING_JOB_FACTORY_H_ +#define COMPONENTS_CRONET_ANDROID_TEST_URL_REQUEST_INTERCEPTING_JOB_FACTORY_H_ + +#include + +#include "base/compiler_specific.h" +#include "base/memory/raw_ptr.h" +#include "net/url_request/url_request_job_factory.h" + +class GURL; + +namespace net { +class URLRequest; +class URLRequestJob; +class URLRequestInterceptor; +} // namespace net + +namespace cronet { + +// This class acts as a wrapper for URLRequestJobFactory. The +// URLRequestInteceptor is given the option of creating a URLRequestJob for each +// URLRequest. If the interceptor does not create a job, the URLRequest is +// forwarded to the wrapped URLRequestJobFactory instead. +// +// This class is only intended for use in intercepting requests before they +// are passed on to their default ProtocolHandler. Each supported scheme should +// have its own ProtocolHandler. +class URLRequestInterceptingJobFactory : public net::URLRequestJobFactory { + public: + // Does not take ownership of |job_factory| and |interceptor|. + URLRequestInterceptingJobFactory(net::URLRequestJobFactory* job_factory, + net::URLRequestInterceptor* interceptor); + + URLRequestInterceptingJobFactory(const URLRequestInterceptingJobFactory&) = + delete; + URLRequestInterceptingJobFactory& operator=( + const URLRequestInterceptingJobFactory&) = delete; + + ~URLRequestInterceptingJobFactory() override; + + // URLRequestJobFactory implementation + std::unique_ptr CreateJob( + net::URLRequest* request) const override; + bool IsSafeRedirectTarget(const GURL& location) const override; + + private: + const raw_ptr job_factory_; + const raw_ptr interceptor_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_TEST_URL_REQUEST_INTERCEPTING_JOB_FACTORY_H_ diff --git a/src/components/cronet/android/test_instructions.md b/src/components/cronet/android/test_instructions.md new file mode 100644 index 0000000000..99f070b2e3 --- /dev/null +++ b/src/components/cronet/android/test_instructions.md @@ -0,0 +1,128 @@ +# Testing and debugging Cronet for Android + +[TOC] + +## Checkout and build + +See instructions in the [common checkout and +build](/components/cronet/build_instructions.md). + +## Running tests locally + +First, connect an Android device by following the [Plug in your Android +device](/docs/android_build_instructions.md#Plug-in-your-Android-device) +steps. Prefer using a device running a userdebug build. + +Alternatively, you can pass the --x86 flag to `gn` to test on a local emulator +-- make sure you substitute `out/Debug` for `out/Debug-x86` in the instructions +below. + +### Running Cronet Java unit tests + +To run Java unit tests that actuate the Cronet API: + +```shell +$ ./components/cronet/tools/cr_cronet.py gn +$ ./components/cronet/tools/cr_cronet.py build-test +``` + +To run particular tests specify the test class and method name to the build-test +command. For example: + +```shell +$ ./components/cronet/tools/cr_cronet.py build-test -f QuicTest#testQuicLoadUrl +``` + +### Running net_unittests and cronet_unittests_android + +To run C++ and Java unit tests of net/ functionality: + +```shell +$ ./components/cronet/tools/cr_cronet.py gn +$ ninja -C out/Debug net_unittests +$ ./out/Debug/bin/run_net_unittests --fast-local-dev +``` + +For more information about running net_unittests, read +[Android Test Instructions](/docs/testing/android_test_instructions.md). + +There are a small number of C++ Cronet unit tests, called +cronet_unittests_android, that can be run by following the above instructions +and substituting cronet_unittests_android for net_unittests. + +### Running Cronet performance tests + +To run Cronet's perf tests, follow the instructions in +[components/cronet/android/test/javaperftests/run.py](test/javaperftests/run.py) + +## Running tests remotely + +Once you've uploaded a Chromium change list using `git cl upload`, you can +launch a bot to build and test your change list: + +```shell +$ git cl try -b android-cronet-arm-dbg +``` + +This will run both the Cronet Java unit tests and net_unittests. + +## Debugging + +### Debug Log + +Messages from native (C++) code appear in the Android system log accessible with +`adb logcat`. By default you will see only messages designated as FATAL. To +enable more verbosity: + +#### See VLOG(1) and VLOG(2) logging: + +```shell +$ adb shell setprop log.tag.CronetUrlRequestContext VERBOSE +``` + +#### See VLOG(1) logging: + +```shell +$ adb shell setprop log.tag.CronetUrlRequestContext DEBUG +``` + +#### See NO (only FATAL) logging: + +```shell +$ adb shell setprop log.tag.CronetUrlRequestContext NONE +``` + +### Network Log + +NetLog is Chromium's network logging system. To create a NetLog dump, you can +use the following pair of methods: + +``` +CronetEngine.startNetLogToFile() +CronetEngine.stopNetLog() +``` + +Unlike the Android system log which is line-based, the Chromium log is formatted +in JSON. As such, it will probably not be well-formed until you have called the +`stopNetLog()` method, as filesystem buffers will not have been flushed. + +Retrieve the file from your device's file system, and import it to chrome +browser at chrome://net-internals/#import, or +http://catapult-project.github.io/catapult/netlog_viewer which helps to +visualize the data. + +### Symbolicating crash stacks + +If an app or test using Cronet crashes it can be useful to know the functions +and line numbers involved in the stack trace. This can be done using the +Android system log: + +```shell +$ ./components/cronet/tools/cr_cronet.py stack +``` + +Or using tombstones left behind after crashes: + +```shell +$ CHROMIUM_OUTPUT_DIR=out/Debug ./build/android/tombstones.py +``` diff --git a/src/components/cronet/android/url_request_error.cc b/src/components/cronet/android/url_request_error.cc new file mode 100644 index 0000000000..921078e4d6 --- /dev/null +++ b/src/components/cronet/android/url_request_error.cc @@ -0,0 +1,38 @@ +// Copyright 2016 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 "components/cronet/android/url_request_error.h" + +#include "net/base/net_errors.h" + +namespace cronet { + +UrlRequestError NetErrorToUrlRequestError(int net_error) { + switch (net_error) { + case net::ERR_NAME_NOT_RESOLVED: + return HOSTNAME_NOT_RESOLVED; + case net::ERR_INTERNET_DISCONNECTED: + return INTERNET_DISCONNECTED; + case net::ERR_NETWORK_CHANGED: + return NETWORK_CHANGED; + case net::ERR_TIMED_OUT: + return TIMED_OUT; + case net::ERR_CONNECTION_CLOSED: + return CONNECTION_CLOSED; + case net::ERR_CONNECTION_TIMED_OUT: + return CONNECTION_TIMED_OUT; + case net::ERR_CONNECTION_REFUSED: + return CONNECTION_REFUSED; + case net::ERR_CONNECTION_RESET: + return CONNECTION_RESET; + case net::ERR_ADDRESS_UNREACHABLE: + return ADDRESS_UNREACHABLE; + case net::ERR_QUIC_PROTOCOL_ERROR: + return QUIC_PROTOCOL_FAILED; + default: + return OTHER; + } +} + +} // namespace cronet diff --git a/src/components/cronet/android/url_request_error.h b/src/components/cronet/android/url_request_error.h new file mode 100644 index 0000000000..3cdc787269 --- /dev/null +++ b/src/components/cronet/android/url_request_error.h @@ -0,0 +1,34 @@ +// Copyright 2016 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_URL_REQUEST_ERROR_H_ +#define COMPONENTS_CRONET_ANDROID_URL_REQUEST_ERROR_H_ + +namespace cronet { + +// Error codes for the most popular network stack error codes. +// For descriptions see corresponding constants in UrlRequestException.java. +// A Java counterpart will be generated for this enum. +// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.net.impl +enum UrlRequestError { + LISTENER_EXCEPTION_THROWN, + HOSTNAME_NOT_RESOLVED, + INTERNET_DISCONNECTED, + NETWORK_CHANGED, + TIMED_OUT, + CONNECTION_CLOSED, + CONNECTION_TIMED_OUT, + CONNECTION_REFUSED, + CONNECTION_RESET, + ADDRESS_UNREACHABLE, + QUIC_PROTOCOL_FAILED, + OTHER, +}; + +// Converts most popular net::ERR_* values to counterparts accessible in Java. +UrlRequestError NetErrorToUrlRequestError(int net_error); + +} // namespace cronet + +#endif // COMPONENTS_CRONET_ANDROID_URL_REQUEST_ERROR_H_ diff --git a/src/components/cronet/build_instructions.md b/src/components/cronet/build_instructions.md new file mode 100644 index 0000000000..cd1e5aa525 --- /dev/null +++ b/src/components/cronet/build_instructions.md @@ -0,0 +1,75 @@ +# Cronet build instructions + +[TOC] + +## Checking out the code + +Follow all the +[Get the Code](https://www.chromium.org/developers/how-tos/get-the-code) +instructions for your target platform up to and including running hooks. + +## Building Cronet for development and debugging + +To build Cronet for development and debugging purposes: + +First, `gn` is used to create ninja files targeting the intended platform, then +`ninja` executes the ninja files to run the build. + +### Android / iOS builds + +```shell +$ ./components/cronet/tools/cr_cronet.py gn --out_dir=out/Cronet +``` + +If the build host is Linux, Android binaries will be built. If the build host is +macOS, iOS binaries will be built. + +Note: these commands clobber output of previously executed gn commands in +`out/Cronet`. If `--out_dir` is left out, the output directory defaults to +`out/Debug` for debug builds and `out/Release` for release builds (see below). + +If `--x86` option is specified, then a native library is built for Intel x86 +architecture, and the output directory defaults to `out/Debug-x86` if +unspecified. This can be useful for running on mobile emulators. + +### Desktop builds (targets the current OS) + +TODO(caraitto): Specify how to target Chrome OS and Fuchsia. + +```shell +gn gen out/Cronet +``` + +### Running the ninja files + +Now, use the generated ninja files to execute the build against the +`cronet_package` build target: + +```shell +$ ninja -C out/Cronet cronet_package +``` + +## Building Cronet mobile for releases + +To build Cronet with optimizations and with debug information stripped out: + +```shell +$ ./components/cronet/tools/cr_cronet.py gn --release +$ ninja -C out/Release cronet_package +``` + +Note: these commands clobber output of previously executed gn commands in +`out/Release`. + +## Building for other architectures + +By default ARMv7 32-bit executables are generated. To generate executables +targeting other architectures modify [cr_cronet.py](tools/cr_cronet.py)'s +`gn_args` variable to include: + +* For ARMv8 64-bit: `target_cpu="arm64"` +* For x86 32-bit: `target_cpu="x86"` +* For x86 64-bit: `target_cpu="x64"` + +Alternatively you can run `gn args {out_dir}` and modify arguments in the editor +that comes up. This has advantage of not changing `cr_cronet.py`. diff --git a/src/components/cronet/cronet_global_state.h b/src/components/cronet/cronet_global_state.h new file mode 100644 index 0000000000..ab4efe0a04 --- /dev/null +++ b/src/components/cronet/cronet_global_state.h @@ -0,0 +1,62 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_CRONET_CRONET_GLOBAL_STATE_H_ +#define COMPONENTS_CRONET_CRONET_GLOBAL_STATE_H_ + +#include +#include +#include "base/memory/scoped_refptr.h" +#include "base/task/sequenced_task_runner.h" + +namespace net { +class NetLog; +class ProxyConfigService; +class ProxyResolutionService; +} // namespace net + +namespace cronet { + +// Returns true when called on the initialization thread. +// May only be called after EnsureInitialized() has returned. +bool OnInitThread(); + +// Posts a task to run on initialization thread. Blocks until initialization +// thread is started. +void PostTaskToInitThread(const base::Location& posted_from, + base::OnceClosure task); + +// Performs one-off initialization of Cronet global state, including creating, +// or binding to an existing thread, to run initialization and process +// network notifications on. The implementation must be thread-safe and +// idempotent, and must complete initialization before returning. +void EnsureInitialized(); + +// Creates a proxy config service appropriate for this platform that fetches the +// system proxy settings. Cronet will call this API only after a prior call +// to EnsureInitialized() has returned. +std::unique_ptr CreateProxyConfigService( + const scoped_refptr& io_task_runner); + +// Creates a proxy resolution service appropriate for this platform that fetches +// the system proxy settings. Cronet will call this API only after a prior call +// to EnsureInitialized() has returned. +std::unique_ptr CreateProxyResolutionService( + std::unique_ptr proxy_config_service, + net::NetLog* net_log); + +// Creates default User-Agent request value, combining optional +// |partial_user_agent| with system-dependent values. This API may be invoked +// before EnsureInitialized(), in which case it may trigger initialization +// itself, if necessary. +std::string CreateDefaultUserAgent(const std::string& partial_user_agent); + +// Set network thread priority to |priority|. Must be called on the network +// thread. On Android, corresponds to android.os.Process.setThreadPriority() +// values. On iOS, corresponds to NSThread::setThreadPriority values. +void SetNetworkThreadPriorityOnNetworkThread(double priority); + +} // namespace cronet + +#endif // COMPONENTS_CRONET_CRONET_GLOBAL_STATE_H_ diff --git a/src/components/cronet/cronet_global_state_stubs.cc b/src/components/cronet/cronet_global_state_stubs.cc new file mode 100644 index 0000000000..9ff771b7b3 --- /dev/null +++ b/src/components/cronet/cronet_global_state_stubs.cc @@ -0,0 +1,85 @@ +// Copyright 2018 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 "components/cronet/cronet_global_state.h" + +#include + +#include "base/at_exit.h" +#include "base/feature_list.h" +#include "base/task/post_task.h" +#include "base/task/thread_pool.h" +#include "base/task/thread_pool/thread_pool_instance.h" +#include "net/proxy_resolution/configured_proxy_resolution_service.h" +#include "net/proxy_resolution/proxy_config_service.h" + +// This file provides minimal "stub" implementations of the Cronet global-state +// functions for the native library build, sufficient to have cronet_tests and +// cronet_unittests build. + +namespace cronet { + +namespace { + +scoped_refptr InitializeAndCreateTaskRunner() { +// Cronet tests sets AtExitManager as part of TestSuite, so statically linked +// library is not allowed to set its own. +#if !defined(CRONET_TESTS_IMPLEMENTATION) + std::ignore = new base::AtExitManager; +#endif + + base::FeatureList::InitializeInstance(std::string(), std::string()); + + // Note that in component builds this ThreadPoolInstance will be shared with + // the calling process, if it also depends on //base. In particular this means + // that the Cronet test binaries must avoid initializing or shutting-down the + // ThreadPoolInstance themselves. + base::ThreadPoolInstance::CreateAndStartWithDefaultParams("cronet"); + + return base::ThreadPool::CreateSingleThreadTaskRunner({}); +} + +base::SingleThreadTaskRunner* InitTaskRunner() { + static scoped_refptr init_task_runner = + InitializeAndCreateTaskRunner(); + return init_task_runner.get(); +} + +} // namespace + +void EnsureInitialized() { + std::ignore = InitTaskRunner(); +} + +bool OnInitThread() { + return InitTaskRunner()->BelongsToCurrentThread(); +} + +void PostTaskToInitThread(const base::Location& posted_from, + base::OnceClosure task) { + InitTaskRunner()->PostTask(posted_from, std::move(task)); +} + +std::unique_ptr CreateProxyConfigService( + const scoped_refptr& io_task_runner) { + return net::ConfiguredProxyResolutionService::CreateSystemProxyConfigService( + io_task_runner); +} + +std::unique_ptr CreateProxyResolutionService( + std::unique_ptr proxy_config_service, + net::NetLog* net_log) { + return net::ConfiguredProxyResolutionService::CreateUsingSystemProxyResolver( + std::move(proxy_config_service), net_log, /*quick_check_enabled=*/true); +} + +std::string CreateDefaultUserAgent(const std::string& partial_user_agent) { + return partial_user_agent; +} + +void SetNetworkThreadPriorityOnNetworkThread(double priority) { + NOTIMPLEMENTED(); +} + +} // namespace cronet diff --git a/src/components/cronet/cronet_prefs_manager.cc b/src/components/cronet/cronet_prefs_manager.cc new file mode 100644 index 0000000000..fe6a591491 --- /dev/null +++ b/src/components/cronet/cronet_prefs_manager.cc @@ -0,0 +1,307 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/cronet/cronet_prefs_manager.h" + +#include + +#include "base/bind.h" +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/location.h" +#include "base/memory/raw_ptr.h" +#include "base/metrics/histogram_macros.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/threading/thread_restrictions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "components/cronet/host_cache_persistence_manager.h" +#include "components/prefs/json_pref_store.h" +#include "components/prefs/pref_change_registrar.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/pref_service_factory.h" +#include "net/http/http_server_properties.h" +#include "net/nqe/network_qualities_prefs_manager.h" +#include "net/url_request/url_request_context_builder.h" + +namespace cronet { +namespace { + +// Name of the pref used for HTTP server properties persistence. +const char kHttpServerPropertiesPref[] = "net.http_server_properties"; +// Name of preference directory. +const base::FilePath::CharType kPrefsDirectoryName[] = + FILE_PATH_LITERAL("prefs"); +// Name of preference file. +const base::FilePath::CharType kPrefsFileName[] = + FILE_PATH_LITERAL("local_prefs.json"); +// Current version of disk storage. +const int32_t kStorageVersion = 1; +// Version number used when the version of disk storage is unknown. +const uint32_t kStorageVersionUnknown = 0; +// Name of the pref used for host cache persistence. +const char kHostCachePref[] = "net.host_cache"; +// Name of the pref used for NQE persistence. +const char kNetworkQualitiesPref[] = "net.network_qualities"; + +bool IsCurrentVersion(const base::FilePath& version_filepath) { + if (!base::PathExists(version_filepath)) + return false; + base::File version_file(version_filepath, + base::File::FLAG_OPEN | base::File::FLAG_READ); + uint32_t version = kStorageVersionUnknown; + int bytes_read = + version_file.Read(0, reinterpret_cast(&version), sizeof(version)); + if (bytes_read != sizeof(version)) { + DLOG(WARNING) << "Cannot read from version file."; + return false; + } + return version == kStorageVersion; +} + +// TODO(xunjieli): Handle failures. +void InitializeStorageDirectory(const base::FilePath& dir) { + // Checks version file and clear old storage. + base::FilePath version_filepath(dir.AppendASCII("version")); + if (IsCurrentVersion(version_filepath)) { + // The version is up to date, so there is nothing to do. + return; + } + // Delete old directory recursively and create a new directory. + // base::DeletePathRecursively() returns true if the directory does not exist, + // so it is fine if there is nothing on disk. + if (!(base::DeletePathRecursively(dir) && base::CreateDirectory(dir))) { + DLOG(WARNING) << "Cannot purge directory."; + return; + } + base::File new_version_file(version_filepath, base::File::FLAG_CREATE_ALWAYS | + base::File::FLAG_WRITE); + + if (!new_version_file.IsValid()) { + DLOG(WARNING) << "Cannot create a version file."; + return; + } + + DCHECK(new_version_file.created()); + uint32_t new_version = kStorageVersion; + int bytes_written = new_version_file.Write( + 0, reinterpret_cast(&new_version), sizeof(new_version)); + if (bytes_written != sizeof(new_version)) { + DLOG(WARNING) << "Cannot write to version file."; + return; + } + base::FilePath prefs_dir = dir.Append(kPrefsDirectoryName); + if (!base::CreateDirectory(prefs_dir)) { + DLOG(WARNING) << "Cannot create prefs directory"; + return; + } +} + +// Connects the HttpServerProperties's storage to the prefs. +class PrefServiceAdapter : public net::HttpServerProperties::PrefDelegate { + public: + explicit PrefServiceAdapter(PrefService* pref_service) + : pref_service_(pref_service), path_(kHttpServerPropertiesPref) { + pref_change_registrar_.Init(pref_service_); + } + + PrefServiceAdapter(const PrefServiceAdapter&) = delete; + PrefServiceAdapter& operator=(const PrefServiceAdapter&) = delete; + + ~PrefServiceAdapter() override {} + + // PrefDelegate implementation. + const base::Value* GetServerProperties() const override { + return pref_service_->Get(path_); + } + + void SetServerProperties(const base::Value& value, + base::OnceClosure callback) override { + pref_service_->Set(path_, value); + if (callback) + pref_service_->CommitPendingWrite(std::move(callback)); + } + + void WaitForPrefLoad(base::OnceClosure callback) override { + // Notify the pref manager that settings are already loaded, as a result + // of initializing the pref store synchronously. + base::SequencedTaskRunnerHandle::Get()->PostTask(FROM_HERE, + std::move(callback)); + } + + private: + raw_ptr pref_service_; + const std::string path_; + PrefChangeRegistrar pref_change_registrar_; +}; // class PrefServiceAdapter + +class NetworkQualitiesPrefDelegateImpl + : public net::NetworkQualitiesPrefsManager::PrefDelegate { + public: + // Caller must guarantee that |pref_service| outlives |this|. + explicit NetworkQualitiesPrefDelegateImpl(PrefService* pref_service) + : pref_service_(pref_service), lossy_prefs_writing_task_posted_(false) { + DCHECK(pref_service_); + } + + NetworkQualitiesPrefDelegateImpl(const NetworkQualitiesPrefDelegateImpl&) = + delete; + NetworkQualitiesPrefDelegateImpl& operator=( + const NetworkQualitiesPrefDelegateImpl&) = delete; + + ~NetworkQualitiesPrefDelegateImpl() override {} + + // net::NetworkQualitiesPrefsManager::PrefDelegate implementation. + void SetDictionaryValue(const base::DictionaryValue& value) override { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + + pref_service_->Set(kNetworkQualitiesPref, value); + if (lossy_prefs_writing_task_posted_) + return; + + // Post the task that schedules the writing of the lossy prefs. + lossy_prefs_writing_task_posted_ = true; + + // Delay after which the task that schedules the writing of the lossy prefs. + // This is needed in case the writing of the lossy prefs is not scheduled + // automatically. The delay was chosen so that it is large enough that it + // does not affect the startup performance. + static const int32_t kUpdatePrefsDelaySeconds = 10; + + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce( + &NetworkQualitiesPrefDelegateImpl::SchedulePendingLossyWrites, + weak_ptr_factory_.GetWeakPtr()), + base::Seconds(kUpdatePrefsDelaySeconds)); + } + // TODO(crbug.com/1187061): Refactor this to remove DictionaryValue. + std::unique_ptr GetDictionaryValue() override { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + UMA_HISTOGRAM_EXACT_LINEAR("NQE.Prefs.ReadCount", 1, 2); + return base::DictionaryValue::From(base::Value::ToUniquePtrValue( + pref_service_->GetDictionary(kNetworkQualitiesPref)->Clone())); + } + + private: + // Schedules the writing of the lossy prefs. + void SchedulePendingLossyWrites() { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + UMA_HISTOGRAM_EXACT_LINEAR("NQE.Prefs.WriteCount", 1, 2); + pref_service_->SchedulePendingLossyWrites(); + lossy_prefs_writing_task_posted_ = false; + } + + raw_ptr pref_service_; + + // True if the task that schedules the writing of the lossy prefs has been + // posted. + bool lossy_prefs_writing_task_posted_; + + THREAD_CHECKER(thread_checker_); + + base::WeakPtrFactory weak_ptr_factory_{ + this}; +}; + +} // namespace + +CronetPrefsManager::CronetPrefsManager( + const std::string& storage_path, + scoped_refptr network_task_runner, + scoped_refptr file_task_runner, + bool enable_network_quality_estimator, + bool enable_host_cache_persistence, + net::NetLog* net_log, + net::URLRequestContextBuilder* context_builder) { + DCHECK(network_task_runner->BelongsToCurrentThread()); + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + +#if BUILDFLAG(IS_WIN) + base::FilePath storage_file_path( + base::FilePath::FromUTF8Unsafe(storage_path)); +#else + base::FilePath storage_file_path(storage_path); +#endif + + // Make sure storage directory has correct version. + { + base::ScopedAllowBlocking allow_blocking; + InitializeStorageDirectory(storage_file_path); + } + + base::FilePath filepath = + storage_file_path.Append(kPrefsDirectoryName).Append(kPrefsFileName); + + json_pref_store_ = new JsonPrefStore(filepath, std::unique_ptr(), + file_task_runner); + + // Register prefs and set up the PrefService. + PrefServiceFactory factory; + factory.set_user_prefs(json_pref_store_); + scoped_refptr registry(new PrefRegistrySimple()); + registry->RegisterDictionaryPref(kHttpServerPropertiesPref); + + if (enable_network_quality_estimator) { + // Use lossy prefs to limit the overhead of reading/writing the prefs. + registry->RegisterDictionaryPref(kNetworkQualitiesPref, + PrefRegistry::LOSSY_PREF); + } + + if (enable_host_cache_persistence) { + registry->RegisterListPref(kHostCachePref); + } + + { + base::ScopedAllowBlocking allow_blocking; + pref_service_ = factory.Create(registry.get()); + } + + context_builder->SetHttpServerProperties( + std::make_unique( + std::make_unique(pref_service_.get()), net_log)); +} + +CronetPrefsManager::~CronetPrefsManager() { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); +} + +void CronetPrefsManager::SetupNqePersistence( + net::NetworkQualityEstimator* nqe) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + network_qualities_prefs_manager_ = + std::make_unique( + std::make_unique( + pref_service_.get())); + + network_qualities_prefs_manager_->InitializeOnNetworkThread(nqe); +} + +void CronetPrefsManager::SetupHostCachePersistence( + net::HostCache* host_cache, + int host_cache_persistence_delay_ms, + net::NetLog* net_log) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + host_cache_persistence_manager_ = + std::make_unique( + host_cache, pref_service_.get(), kHostCachePref, + base::Milliseconds(host_cache_persistence_delay_ms), net_log); +} + +void CronetPrefsManager::PrepareForShutdown() { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + if (pref_service_) + pref_service_->CommitPendingWrite(); + + // Shutdown managers on the Pref sequence. + if (network_qualities_prefs_manager_) + network_qualities_prefs_manager_->ShutdownOnPrefSequence(); + + host_cache_persistence_manager_.reset(); +} + +} // namespace cronet diff --git a/src/components/cronet/cronet_prefs_manager.h b/src/components/cronet/cronet_prefs_manager.h new file mode 100644 index 0000000000..974a4861f6 --- /dev/null +++ b/src/components/cronet/cronet_prefs_manager.h @@ -0,0 +1,85 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_CRONET_CRONET_PREFS_MANAGER_H_ +#define COMPONENTS_CRONET_CRONET_PREFS_MANAGER_H_ + +#include + +#include "base/memory/ref_counted.h" +#include "base/threading/thread_checker.h" + +class JsonPrefStore; +class NetLog; +class PrefService; + +namespace base { +class SingleThreadTaskRunner; +class SequencedTaskRunner; +} // namespace base + +namespace net { +class HostCache; +class NetLog; +class NetworkQualitiesPrefsManager; +class NetworkQualityEstimator; +class URLRequestContextBuilder; +} // namespace net + +namespace cronet { +class HostCachePersistenceManager; + +// Manages the PrefService, JsonPrefStore and all associated persistence +// managers used by Cronet such as NetworkQualityPrefsManager, +// HostCachePersistenceManager, etc. The constructor, destructor and all +// other methods of this class should be called on the network thread. +class CronetPrefsManager { + public: + CronetPrefsManager( + const std::string& storage_path, + scoped_refptr network_task_runner, + scoped_refptr file_task_runner, + bool enable_network_quality_estimator, + bool enable_host_cache_persistence, + net::NetLog* net_log, + net::URLRequestContextBuilder* context_builder); + + CronetPrefsManager(const CronetPrefsManager&) = delete; + CronetPrefsManager& operator=(const CronetPrefsManager&) = delete; + + virtual ~CronetPrefsManager(); + + void SetupNqePersistence(net::NetworkQualityEstimator* nqe); + + void SetupHostCachePersistence(net::HostCache* host_cache, + int host_cache_persistence_delay_ms, + net::NetLog* net_log); + + // Prepares |this| for shutdown. + void PrepareForShutdown(); + + private: + // |pref_service_| should outlive the HttpServerPropertiesManager owned by + // |host_cache_persistence_manager_|. + std::unique_ptr pref_service_; + scoped_refptr json_pref_store_; + + // Manages the writing and reading of the network quality prefs. + std::unique_ptr + network_qualities_prefs_manager_; + + // Manages reading and writing the HostCache pref when persistence is enabled. + // Must be destroyed before |context_| owned by CronetUrlContextAdapter + // (because it owns the HostResolverImpl, + // which owns the HostCache) and |pref_service_|. + std::unique_ptr host_cache_persistence_manager_; + + // Checks that all methods are called on the network thread. + THREAD_CHECKER(thread_checker_); + +}; // class CronetPrefsManager + +} // namespace cronet + +#endif // COMPONENTS_CRONET_CRONET_PREFS_MANAGER_H_ diff --git a/src/components/cronet/cronet_upload_data_stream.cc b/src/components/cronet/cronet_upload_data_stream.cc new file mode 100644 index 0000000000..23f2b3a315 --- /dev/null +++ b/src/components/cronet/cronet_upload_data_stream.cc @@ -0,0 +1,137 @@ +// 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 "components/cronet/cronet_upload_data_stream.h" + +#include "net/base/io_buffer.h" +#include "net/base/net_errors.h" + +namespace cronet { + +CronetUploadDataStream::CronetUploadDataStream(Delegate* delegate, int64_t size) + : UploadDataStream(size < 0, 0), + size_(size), + waiting_on_read_(false), + read_in_progress_(false), + waiting_on_rewind_(false), + rewind_in_progress_(false), + at_front_of_stream_(true), + delegate_(delegate) {} + +CronetUploadDataStream::~CronetUploadDataStream() { + delegate_->OnUploadDataStreamDestroyed(); +} + +int CronetUploadDataStream::InitInternal(const net::NetLogWithSource& net_log) { + // ResetInternal should have been called before init, if the stream was in + // use. + DCHECK(!waiting_on_read_); + DCHECK(!waiting_on_rewind_); + + if (!weak_factory_.HasWeakPtrs()) + delegate_->InitializeOnNetworkThread(weak_factory_.GetWeakPtr()); + + // Set size of non-chunked uploads. + if (size_ >= 0) + SetSize(static_cast(size_)); + + // If already at the front of the stream, nothing to do. + if (at_front_of_stream_) { + // Being at the front of the stream implies there's no read or rewind in + // progress. + DCHECK(!read_in_progress_); + DCHECK(!rewind_in_progress_); + return net::OK; + } + + // Otherwise, the request is now waiting for the stream to be rewound. + waiting_on_rewind_ = true; + + // Start rewinding the stream if no operation is in progress. + if (!read_in_progress_ && !rewind_in_progress_) + StartRewind(); + return net::ERR_IO_PENDING; +} + +int CronetUploadDataStream::ReadInternal(net::IOBuffer* buf, int buf_len) { + // All pending operations should have completed before a read can start. + DCHECK(!waiting_on_read_); + DCHECK(!read_in_progress_); + DCHECK(!waiting_on_rewind_); + DCHECK(!rewind_in_progress_); + + DCHECK(buf); + DCHECK_GT(buf_len, 0); + + read_in_progress_ = true; + waiting_on_read_ = true; + at_front_of_stream_ = false; + scoped_refptr buffer(base::WrapRefCounted(buf)); + delegate_->Read(std::move(buffer), buf_len); + return net::ERR_IO_PENDING; +} + +void CronetUploadDataStream::ResetInternal() { + // Consumer is not waiting on any operation. Note that the active operation, + // if any, will continue. + waiting_on_read_ = false; + waiting_on_rewind_ = false; +} + +void CronetUploadDataStream::OnReadSuccess(int bytes_read, bool final_chunk) { + DCHECK(read_in_progress_); + DCHECK(!rewind_in_progress_); + DCHECK(bytes_read > 0 || (final_chunk && bytes_read == 0)); + if (!is_chunked()) { + DCHECK(!final_chunk); + } + + read_in_progress_ = false; + + if (waiting_on_rewind_) { + DCHECK(!waiting_on_read_); + // Since a read just completed, can't be at the front of the stream. + StartRewind(); + return; + } + // ResetInternal has been called, but still waiting on InitInternal. + if (!waiting_on_read_) + return; + + waiting_on_read_ = false; + if (final_chunk) + SetIsFinalChunk(); + OnReadCompleted(bytes_read); +} + +void CronetUploadDataStream::OnRewindSuccess() { + DCHECK(!waiting_on_read_); + DCHECK(!read_in_progress_); + DCHECK(rewind_in_progress_); + DCHECK(!at_front_of_stream_); + + rewind_in_progress_ = false; + at_front_of_stream_ = true; + + // Possible that ResetInternal was called since the rewind was started, but + // InitInternal has not been. + if (!waiting_on_rewind_) + return; + + waiting_on_rewind_ = false; + OnInitCompleted(net::OK); +} + +void CronetUploadDataStream::StartRewind() { + DCHECK(!waiting_on_read_); + DCHECK(!read_in_progress_); + DCHECK(waiting_on_rewind_); + DCHECK(!rewind_in_progress_); + DCHECK(!at_front_of_stream_); + + rewind_in_progress_ = true; + delegate_->Rewind(); +} + +} // namespace cronet diff --git a/src/components/cronet/cronet_upload_data_stream.h b/src/components/cronet/cronet_upload_data_stream.h new file mode 100644 index 0000000000..e3a4763a77 --- /dev/null +++ b/src/components/cronet/cronet_upload_data_stream.h @@ -0,0 +1,114 @@ +// 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_CRONET_UPLOAD_DATA_STREAM_H_ +#define COMPONENTS_CRONET_CRONET_UPLOAD_DATA_STREAM_H_ + +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.h" +#include "net/base/upload_data_stream.h" + +namespace net { +class IOBuffer; +} // namespace net + +namespace cronet { + +// The CronetUploadDataStream is created on a client thread, but afterwards, +// lives and is deleted on the network thread. It's responsible for ensuring +// only one read/rewind request sent to client is outstanding at a time. +// The main complexity is around Reset/Initialize calls while there's a pending +// read or rewind. +class CronetUploadDataStream : public net::UploadDataStream { + public: + class Delegate { + public: + Delegate(const Delegate&) = delete; + Delegate& operator=(const Delegate&) = delete; + + // Called once during initial setup on the network thread, called before + // all other methods. + virtual void InitializeOnNetworkThread( + base::WeakPtr upload_data_stream) = 0; + + // Called for each read request. Delegate must respond by calling + // OnReadSuccess on the network thread asynchronous, or failing the request. + // Only called when there's no other pending read or rewind operation. + virtual void Read(scoped_refptr buffer, int buf_len) = 0; + + // Called to rewind the stream. Not called when already at the start of the + // stream. The delegate must respond by calling OnRewindSuccess + // asynchronously on the network thread, or failing the request. Only called + // when there's no other pending read or rewind operation. + virtual void Rewind() = 0; + + // Called when the CronetUploadDataStream is destroyed. The Delegate is then + // responsible for destroying itself. May be called when there's a pending + // read or rewind operation. + virtual void OnUploadDataStreamDestroyed() = 0; + + protected: + Delegate() {} + virtual ~Delegate() {} + }; + + CronetUploadDataStream(Delegate* delegate, int64_t size); + + CronetUploadDataStream(const CronetUploadDataStream&) = delete; + CronetUploadDataStream& operator=(const CronetUploadDataStream&) = delete; + + ~CronetUploadDataStream() override; + + // Failure is handled at the Java layer. These two success callbacks are + // invoked by client UploadDataSink upon completion of the operation. + void OnReadSuccess(int bytes_read, bool final_chunk); + void OnRewindSuccess(); + + private: + // net::UploadDataStream implementation: + int InitInternal(const net::NetLogWithSource& net_log) override; + int ReadInternal(net::IOBuffer* buf, int buf_len) override; + void ResetInternal() override; + + // Starts rewinding the stream. Only called when not already at the front of + // the stream, and no operation is pending. Completes asynchronously. + void StartRewind(); + + // Size of the upload. -1 if chunked. + const int64_t size_; + + // True if ReadInternal has been called, the read hasn't completed, and there + // hasn't been a ResetInternal call yet. + bool waiting_on_read_; + // True if there's a read operation in progress. This will always be true + // when |waiting_on_read_| is true. This will only be set to false once it + // completes, even though ResetInternal may have been called since the read + // started. + bool read_in_progress_; + + // True if InitInternal has been called, the rewind hasn't completed, and + // there hasn't been a ResetInternal call yet. Note that this may be true + // even when the rewind hasn't yet started, if there's a read in progress. + bool waiting_on_rewind_; + // True if there's a rewind operation in progress. Rewinding will only start + // when |waiting_on_rewind_| is true, and |read_in_progress_| is false. This + // will only be set to false once it completes, even though ResetInternal may + // have been called since the rewind started. + bool rewind_in_progress_; + + // Set to false when a read starts, true when a rewind completes. + bool at_front_of_stream_; + + const raw_ptr delegate_; + + // Vends pointers on the network thread, though created on a client thread. + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_CRONET_UPLOAD_DATA_STREAM_H_ diff --git a/src/components/cronet/cronet_url_request.cc b/src/components/cronet/cronet_url_request.cc new file mode 100644 index 0000000000..5eca14e58b --- /dev/null +++ b/src/components/cronet/cronet_url_request.cc @@ -0,0 +1,413 @@ +// Copyright 2014 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 "components/cronet/cronet_url_request.h" + +#include +#include + +#include "base/bind.h" +#include "base/location.h" +#include "base/logging.h" +#include "build/build_config.h" +#include "components/cronet/cronet_url_request_context.h" +#include "net/base/idempotency.h" +#include "net/base/load_flags.h" +#include "net/base/load_states.h" +#include "net/base/net_errors.h" +#include "net/base/proxy_server.h" +#include "net/base/request_priority.h" +#include "net/base/upload_data_stream.h" +#include "net/cert/cert_status_flags.h" +#include "net/cert/x509_certificate.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_status_code.h" +#include "net/http/http_util.h" +#include "net/ssl/ssl_info.h" +#include "net/ssl/ssl_private_key.h" +#include "net/third_party/quiche/src/quic/core/quic_packets.h" +#include "net/traffic_annotation/network_traffic_annotation.h" +#include "net/url_request/redirect_info.h" +#include "net/url_request/url_request_context.h" + +namespace cronet { + +namespace { + +// Returns the string representation of the HostPortPair of the proxy server +// that was used to fetch the response. +std::string GetProxy(const net::HttpResponseInfo& info) { + if (!info.proxy_server.is_valid() || info.proxy_server.is_direct()) + return net::HostPortPair().ToString(); + return info.proxy_server.host_port_pair().ToString(); +} + +int CalculateLoadFlags(int load_flags, + bool disable_cache, + bool disable_connection_migration) { + if (disable_cache) + load_flags |= net::LOAD_DISABLE_CACHE; + if (disable_connection_migration) + load_flags |= net::LOAD_DISABLE_CONNECTION_MIGRATION_TO_CELLULAR; + return load_flags; +} + +} // namespace + +CronetURLRequest::CronetURLRequest(CronetURLRequestContext* context, + std::unique_ptr callback, + const GURL& url, + net::RequestPriority priority, + bool disable_cache, + bool disable_connection_migration, + bool enable_metrics, + bool traffic_stats_tag_set, + int32_t traffic_stats_tag, + bool traffic_stats_uid_set, + int32_t traffic_stats_uid, + net::Idempotency idempotency) + : context_(context), + network_tasks_(std::move(callback), + url, + priority, + CalculateLoadFlags(context->default_load_flags(), + disable_cache, + disable_connection_migration), + enable_metrics, + traffic_stats_tag_set, + traffic_stats_tag, + traffic_stats_uid_set, + traffic_stats_uid, + idempotency), + initial_method_("GET"), + initial_request_headers_(std::make_unique()) { + DCHECK(!context_->IsOnNetworkThread()); +} + +CronetURLRequest::~CronetURLRequest() { + DCHECK(context_->IsOnNetworkThread()); +} + +bool CronetURLRequest::SetHttpMethod(const std::string& method) { + DCHECK(!context_->IsOnNetworkThread()); + // Http method is a token, just as header name. + if (!net::HttpUtil::IsValidHeaderName(method)) + return false; + initial_method_ = method; + return true; +} + +bool CronetURLRequest::AddRequestHeader(const std::string& name, + const std::string& value) { + DCHECK(!context_->IsOnNetworkThread()); + DCHECK(initial_request_headers_); + if (!net::HttpUtil::IsValidHeaderName(name) || + !net::HttpUtil::IsValidHeaderValue(value)) { + return false; + } + initial_request_headers_->SetHeader(name, value); + return true; +} + +void CronetURLRequest::SetUpload( + std::unique_ptr upload) { + DCHECK(!context_->IsOnNetworkThread()); + DCHECK(!upload_); + upload_ = std::move(upload); +} + +void CronetURLRequest::Start() { + DCHECK(!context_->IsOnNetworkThread()); + context_->PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce(&CronetURLRequest::NetworkTasks::Start, + base::Unretained(&network_tasks_), + base::Unretained(context_), initial_method_, + std::move(initial_request_headers_), std::move(upload_))); +} + +void CronetURLRequest::GetStatus(OnStatusCallback callback) const { + context_->PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce(&CronetURLRequest::NetworkTasks::GetStatus, + base::Unretained(&network_tasks_), std::move(callback))); +} + +void CronetURLRequest::FollowDeferredRedirect() { + context_->PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce(&CronetURLRequest::NetworkTasks::FollowDeferredRedirect, + base::Unretained(&network_tasks_))); +} + +bool CronetURLRequest::ReadData(net::IOBuffer* raw_read_buffer, int max_size) { + scoped_refptr read_buffer(raw_read_buffer); + context_->PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce(&CronetURLRequest::NetworkTasks::ReadData, + base::Unretained(&network_tasks_), read_buffer, max_size)); + return true; +} + +void CronetURLRequest::Destroy(bool send_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() must be called from + // within a synchronized block that guarantees no future posts to the + // network thread with the request pointer. + context_->PostTaskToNetworkThread( + FROM_HERE, base::BindOnce(&CronetURLRequest::NetworkTasks::Destroy, + base::Unretained(&network_tasks_), + base::Unretained(this), send_on_canceled)); +} + +void CronetURLRequest::MaybeReportMetricsAndRunCallback( + base::OnceClosure callback) { + context_->PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce( + &CronetURLRequest::NetworkTasks::MaybeReportMetricsAndRunCallback, + base::Unretained(&network_tasks_), std::move(callback))); +} + +CronetURLRequest::NetworkTasks::NetworkTasks(std::unique_ptr callback, + const GURL& url, + net::RequestPriority priority, + int load_flags, + bool enable_metrics, + bool traffic_stats_tag_set, + int32_t traffic_stats_tag, + bool traffic_stats_uid_set, + int32_t traffic_stats_uid, + net::Idempotency idempotency) + : callback_(std::move(callback)), + initial_url_(url), + initial_priority_(priority), + initial_load_flags_(load_flags), + received_byte_count_from_redirects_(0l), + error_reported_(false), + enable_metrics_(enable_metrics), + metrics_reported_(false), + traffic_stats_tag_set_(traffic_stats_tag_set), + traffic_stats_tag_(traffic_stats_tag), + traffic_stats_uid_set_(traffic_stats_uid_set), + traffic_stats_uid_(traffic_stats_uid), + idempotency_(idempotency) { + DETACH_FROM_THREAD(network_thread_checker_); +} + +CronetURLRequest::NetworkTasks::~NetworkTasks() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); +} + +void CronetURLRequest::NetworkTasks::OnReceivedRedirect( + net::URLRequest* request, + const net::RedirectInfo& redirect_info, + bool* defer_redirect) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + received_byte_count_from_redirects_ += request->GetTotalReceivedBytes(); + callback_->OnReceivedRedirect( + redirect_info.new_url.spec(), redirect_info.status_code, + request->response_headers()->GetStatusText(), request->response_headers(), + request->response_info().was_cached, + request->response_info().alpn_negotiated_protocol, + GetProxy(request->response_info()), received_byte_count_from_redirects_); + *defer_redirect = true; +} + +void CronetURLRequest::NetworkTasks::OnCertificateRequested( + net::URLRequest* request, + net::SSLCertRequestInfo* cert_request_info) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + // Cronet does not support client certificates. + request->ContinueWithCertificate(nullptr, nullptr); +} + +void CronetURLRequest::NetworkTasks::OnSSLCertificateError( + net::URLRequest* request, + int net_error, + const net::SSLInfo& ssl_info, + bool fatal) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + ReportError(request, net_error); + request->Cancel(); +} + +void CronetURLRequest::NetworkTasks::OnResponseStarted(net::URLRequest* request, + int net_error) { + DCHECK_NE(net::ERR_IO_PENDING, net_error); + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + + if (net_error != net::OK) { + ReportError(request, net_error); + return; + } + callback_->OnResponseStarted( + request->GetResponseCode(), request->response_headers()->GetStatusText(), + request->response_headers(), request->response_info().was_cached, + request->response_info().alpn_negotiated_protocol, + GetProxy(request->response_info()), + received_byte_count_from_redirects_ + request->GetTotalReceivedBytes()); +} + +void CronetURLRequest::NetworkTasks::OnReadCompleted(net::URLRequest* request, + int bytes_read) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + + if (bytes_read < 0) { + ReportError(request, bytes_read); + return; + } + + if (bytes_read == 0) { + DCHECK(!error_reported_); + MaybeReportMetrics(); + callback_->OnSucceeded(received_byte_count_from_redirects_ + + request->GetTotalReceivedBytes()); + } else { + callback_->OnReadCompleted( + read_buffer_, bytes_read, + received_byte_count_from_redirects_ + request->GetTotalReceivedBytes()); + } + // Free the read buffer. + read_buffer_ = nullptr; +} + +void CronetURLRequest::NetworkTasks::Start( + CronetURLRequestContext* context, + const std::string& method, + std::unique_ptr request_headers, + std::unique_ptr upload) { + DCHECK(context->IsOnNetworkThread()); + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + VLOG(1) << "Starting chromium request: " + << initial_url_.possibly_invalid_spec().c_str() + << " priority: " << RequestPriorityToString(initial_priority_); + url_request_ = context->GetURLRequestContext()->CreateRequest( + initial_url_, net::DEFAULT_PRIORITY, this, MISSING_TRAFFIC_ANNOTATION); + url_request_->SetLoadFlags(initial_load_flags_); + url_request_->set_method(method); + url_request_->SetExtraRequestHeaders(*request_headers); + url_request_->SetPriority(initial_priority_); + url_request_->SetIdempotency(idempotency_); + std::string referer; + if (request_headers->GetHeader(net::HttpRequestHeaders::kReferer, &referer)) { + url_request_->SetReferrer(referer); + } + if (upload) + url_request_->set_upload(std::move(upload)); + if (traffic_stats_tag_set_ || traffic_stats_uid_set_) { +#if BUILDFLAG(IS_ANDROID) + url_request_->set_socket_tag(net::SocketTag( + traffic_stats_uid_set_ ? traffic_stats_uid_ : net::SocketTag::UNSET_UID, + traffic_stats_tag_set_ ? traffic_stats_tag_ + : net::SocketTag::UNSET_TAG)); +#else + CHECK(false); +#endif + } + url_request_->Start(); +} + +void CronetURLRequest::NetworkTasks::GetStatus( + OnStatusCallback callback) const { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + net::LoadState status = net::LOAD_STATE_IDLE; + // |url_request_| is initialized in StartOnNetworkThread, and it is + // never nulled. If it is null, it must be that StartOnNetworkThread + // has not been called, pretend that we are in LOAD_STATE_IDLE. + // See https://crbug.com/606872. + if (url_request_) + status = url_request_->GetLoadState().state; + std::move(callback).Run(status); +} + +void CronetURLRequest::NetworkTasks::FollowDeferredRedirect() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + url_request_->FollowDeferredRedirect( + absl::nullopt /* removed_request_headers */, + absl::nullopt /* modified_request_headers */); +} + +void CronetURLRequest::NetworkTasks::ReadData( + scoped_refptr read_buffer, + int buffer_size) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + DCHECK(read_buffer); + DCHECK(!read_buffer_); + + read_buffer_ = read_buffer; + + int result = url_request_->Read(read_buffer_.get(), buffer_size); + // If IO is pending, wait for the URLRequest to call OnReadCompleted. + if (result == net::ERR_IO_PENDING) + return; + + OnReadCompleted(url_request_.get(), result); +} + +void CronetURLRequest::NetworkTasks::Destroy(CronetURLRequest* request, + bool send_on_canceled) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + MaybeReportMetrics(); + if (send_on_canceled) + callback_->OnCanceled(); + callback_->OnDestroyed(); + // Deleting owner request also deletes |this|. + delete request; +} + +void CronetURLRequest::NetworkTasks::ReportError(net::URLRequest* request, + int net_error) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + DCHECK_NE(net::ERR_IO_PENDING, net_error); + DCHECK_LT(net_error, 0); + DCHECK_EQ(request, url_request_.get()); + // Error may have already been reported. + if (error_reported_) + return; + error_reported_ = true; + net::NetErrorDetails net_error_details; + url_request_->PopulateNetErrorDetails(&net_error_details); + VLOG(1) << "Error " << net::ErrorToString(net_error) + << " on chromium request: " << initial_url_.possibly_invalid_spec(); + MaybeReportMetrics(); + callback_->OnError( + net_error, net_error_details.quic_connection_error, + net::ErrorToString(net_error), + received_byte_count_from_redirects_ + request->GetTotalReceivedBytes()); +} + +void CronetURLRequest::NetworkTasks::MaybeReportMetrics() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + // If there was an exception while starting the CronetUrlRequest, there won't + // be a native URLRequest. In this case, the caller gets the exception + // immediately, and the onFailed callback isn't called, so don't report + // metrics either. + if (!enable_metrics_ || metrics_reported_ || !url_request_) { + return; + } + metrics_reported_ = true; + net::LoadTimingInfo metrics; + url_request_->GetLoadTimingInfo(&metrics); + callback_->OnMetricsCollected( + metrics.request_start_time, metrics.request_start, + metrics.connect_timing.dns_start, metrics.connect_timing.dns_end, + metrics.connect_timing.connect_start, metrics.connect_timing.connect_end, + metrics.connect_timing.ssl_start, metrics.connect_timing.ssl_end, + metrics.send_start, metrics.send_end, metrics.push_start, + metrics.push_end, metrics.receive_headers_end, base::TimeTicks::Now(), + metrics.socket_reused, url_request_->GetTotalSentBytes(), + received_byte_count_from_redirects_ + + url_request_->GetTotalReceivedBytes()); +} + +void CronetURLRequest::NetworkTasks::MaybeReportMetricsAndRunCallback( + base::OnceClosure callback) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + MaybeReportMetrics(); + std::move(callback).Run(); +} + +} // namespace cronet diff --git a/src/components/cronet/cronet_url_request.h b/src/components/cronet/cronet_url_request.h new file mode 100644 index 0000000000..a96c790d5a --- /dev/null +++ b/src/components/cronet/cronet_url_request.h @@ -0,0 +1,317 @@ +// Copyright 2014 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_CRONET_URL_REQUEST_H_ +#define COMPONENTS_CRONET_CRONET_URL_REQUEST_H_ + +#include +#include + +#include "base/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/time/time.h" +#include "net/base/idempotency.h" +#include "net/base/request_priority.h" +#include "net/url_request/url_request.h" +#include "url/gurl.h" + +namespace net { +class HttpRequestHeaders; +enum LoadState; +class SSLCertRequestInfo; +class SSLInfo; +class UploadDataStream; +} // namespace net + +namespace cronet { + +class CronetURLRequestContext; +class TestUtil; + +// Wrapper around net::URLRequestContext. +// Created and configured from client thread. Start, ReadData, and Destroy are +// posted to network thread and all callbacks into the Callback() are +// done on the network thread. CronetUrlRequest client is expected to initiate +// the next step like FollowDeferredRedirect, ReadData or Destroy. Public +// methods can be called on any thread. +class CronetURLRequest { + public: + // Callback implemented by CronetURLRequest() caller and owned by + // CronetURLRequest::NetworkTasks. All callback methods are invoked on network + // thread. + class Callback { + public: + virtual ~Callback() = default; + + // Invoked whenever a redirect is encountered. This will only be invoked + // between the call to CronetURLRequest::Start() and + // Callback::OnResponseStarted(). The body of the redirect response, if + // it has one, will be ignored. + // + // The redirect will not be followed until + // CronetURLRequest::FollowDeferredRedirect() method is called, either + // synchronously or asynchronously. + virtual void OnReceivedRedirect(const std::string& new_location, + int http_status_code, + const std::string& http_status_text, + const net::HttpResponseHeaders* headers, + bool was_cached, + const std::string& negotiated_protocol, + const std::string& proxy_server, + int64_t received_byte_count) = 0; + + // Invoked when the final set of headers, after all redirects, is received. + // Will only be invoked once for each request. + // + // With the exception of Callback::OnCanceled(), + // no other Callback method will be invoked for the request, + // including Callback::OnSucceeded() and Callback::OnFailed(), until + // CronetUrlRequest::Read() is called to attempt to start reading the + // response body. + virtual void OnResponseStarted(int http_status_code, + const std::string& http_status_text, + const net::HttpResponseHeaders* headers, + bool was_cached, + const std::string& negotiated_protocol, + const std::string& proxy_server, + int64_t received_byte_count) = 0; + + // Invoked whenever part of the response body has been read. Only part of + // the buffer may be populated, even if the entire response body has not yet + // been consumed. + // + // With the exception of Callback::OnCanceled(), + // no other Callback method will be invoked for the request, + // including Callback::OnSucceeded() and Callback::OnFailed(), until + // CronetUrlRequest::Read() is called to attempt to continue reading the + // response body. + virtual void OnReadCompleted(scoped_refptr buffer, + int bytes_read, + int64_t received_byte_count) = 0; + + // Invoked when request is completed successfully. + virtual void OnSucceeded(int64_t received_byte_count) = 0; + + // Invoked if request failed for any reason after CronetURLRequest::Start(). + // |net_error| provides information about the failure. |quic_error| is only + // valid if |net_error| is net::QUIC_PROTOCOL_ERROR. + virtual void OnError(int net_error, + int quic_error, + const std::string& error_string, + int64_t received_byte_count) = 0; + + // Invoked if request was canceled via CronetURLRequest::Destroy(). + virtual void OnCanceled() = 0; + + // Invoked when request is destroyed. Once invoked, no other Callback + // methods will be invoked. + virtual void OnDestroyed() = 0; + + // Invoked right before request is destroyed to report collected metrics if + // |enable_metrics| is true in CronetURLRequest::CronetURLRequest(). + virtual void OnMetricsCollected(const base::Time& request_start_time, + const base::TimeTicks& request_start, + const base::TimeTicks& dns_start, + const base::TimeTicks& dns_end, + const base::TimeTicks& connect_start, + const base::TimeTicks& connect_end, + const base::TimeTicks& ssl_start, + const base::TimeTicks& ssl_end, + const base::TimeTicks& send_start, + const base::TimeTicks& send_end, + const base::TimeTicks& push_start, + const base::TimeTicks& push_end, + const base::TimeTicks& receive_headers_end, + const base::TimeTicks& request_end, + bool socket_reused, + int64_t sent_bytes_count, + int64_t received_bytes_count) = 0; + }; + // Invoked in response to CronetURLRequest::GetStatus() to allow multiple + // overlapping calls. The load states correspond to the lengthy periods of + // time that a request load may be blocked and unable to make progress. + using OnStatusCallback = base::OnceCallback; + + // Bypasses cache if |disable_cache| is true. If context is not set up to + // use cache, |disable_cache| has no effect. |disable_connection_migration| + // causes connection migration to be disabled for this request if true. If + // global connection migration flag is not enabled, + // |disable_connection_migration| has no effect. + CronetURLRequest(CronetURLRequestContext* context, + std::unique_ptr callback, + const GURL& url, + net::RequestPriority priority, + bool disable_cache, + bool disable_connection_migration, + bool enable_metrics, + bool traffic_stats_tag_set, + int32_t traffic_stats_tag, + bool traffic_stats_uid_set, + int32_t traffic_stats_uid, + net::Idempotency idempotency); + + CronetURLRequest(const CronetURLRequest&) = delete; + CronetURLRequest& operator=(const CronetURLRequest&) = delete; + + // Methods called prior to Start are never called on network thread. + + // Sets the request method GET, POST etc. + bool SetHttpMethod(const std::string& method); + + // Adds a header to the request before it starts. + bool AddRequestHeader(const std::string& name, const std::string& value); + + // Adds a request body to the request before it starts. + void SetUpload(std::unique_ptr upload); + + // Starts the request. + void Start(); + + // GetStatus invokes |on_status_callback| on network thread to allow multiple + // overlapping calls. + void GetStatus(OnStatusCallback on_status_callback) const; + + // Follows redirect. + void FollowDeferredRedirect(); + + // Reads more data. + bool ReadData(net::IOBuffer* buffer, int max_bytes); + + // Releases all resources for the request and deletes the object itself. + // |send_on_canceled| indicates whether OnCanceled callback should be + // issued to indicate when no more callbacks will be issued. + void Destroy(bool send_on_canceled); + + // On the network thread, reports metrics to the registered + // CronetURLRequest::Callback, and then runs |callback| on the network thread. + // + // Since metrics are only reported once, this can be used to ensure metrics + // are reported to the registered CronetURLRequest::Callback before resources + // used by the callback are deleted. + void MaybeReportMetricsAndRunCallback(base::OnceClosure callback); + + private: + friend class TestUtil; + + // Private destructor invoked fron NetworkTasks::Destroy() on network thread. + ~CronetURLRequest(); + + // NetworkTasks performs tasks on the network thread and owns objects that + // live on the network thread. + class NetworkTasks : public net::URLRequest::Delegate { + public: + // Invoked off the network thread. + NetworkTasks(std::unique_ptr callback, + const GURL& url, + net::RequestPriority priority, + int load_flags, + bool enable_metrics, + bool traffic_stats_tag_set, + int32_t traffic_stats_tag, + bool traffic_stats_uid_set, + int32_t traffic_stats_uid, + net::Idempotency idempotency); + + NetworkTasks(const NetworkTasks&) = delete; + NetworkTasks& operator=(const NetworkTasks&) = delete; + + // Invoked on the network thread. + ~NetworkTasks() override; + + // Starts the request. + void Start(CronetURLRequestContext* context, + const std::string& method, + std::unique_ptr request_headers, + std::unique_ptr upload); + + // Gets status of the requrest and invokes |on_status_callback| to allow + // multiple overlapping calls. + void GetStatus(OnStatusCallback on_status_callback) const; + + // Follows redirect. + void FollowDeferredRedirect(); + + // Reads more data. + void ReadData(scoped_refptr read_buffer, int buffer_size); + + // Releases all resources for the request and deletes the |request|, which + // owns |this|, so |this| is also deleted. + // |send_on_canceled| indicates whether OnCanceled callback should be + // issued to indicate when no more callbacks will be issued. + void Destroy(CronetURLRequest* request, bool send_on_canceled); + + // Runs MaybeReportMetrics(), then runs |callback|. + void MaybeReportMetricsAndRunCallback(base::OnceClosure callback); + + private: + friend class TestUtil; + + // net::URLRequest::Delegate implementations: + void OnReceivedRedirect(net::URLRequest* request, + const net::RedirectInfo& redirect_info, + bool* defer_redirect) override; + void OnCertificateRequested( + net::URLRequest* request, + net::SSLCertRequestInfo* cert_request_info) override; + void OnSSLCertificateError(net::URLRequest* request, + int net_error, + const net::SSLInfo& ssl_info, + bool fatal) override; + void OnResponseStarted(net::URLRequest* request, int net_error) override; + void OnReadCompleted(net::URLRequest* request, int bytes_read) override; + + // Report error and cancel request_adapter. + void ReportError(net::URLRequest* request, int net_error); + // Reports metrics collected. + void MaybeReportMetrics(); + + // Callback implemented by the client. + std::unique_ptr callback_; + + const GURL initial_url_; + const net::RequestPriority initial_priority_; + const int initial_load_flags_; + // Count of bytes received during redirect is added to received byte count. + int64_t received_byte_count_from_redirects_; + + // Whether error has been already reported, for example from + // OnSSLCertificateError(). + bool error_reported_; + + // Whether detailed metrics should be collected and reported. + const bool enable_metrics_; + // Whether metrics have been reported. + bool metrics_reported_; + + // Whether |traffic_stats_tag_| should be applied. + const bool traffic_stats_tag_set_; + // TrafficStats tag to apply to URLRequest. + const int32_t traffic_stats_tag_; + // Whether |traffic_stats_uid_| should be applied. + const bool traffic_stats_uid_set_; + // UID to be applied to URLRequest. + const int32_t traffic_stats_uid_; + // Idempotency of the request. + const net::Idempotency idempotency_; + + scoped_refptr read_buffer_; + std::unique_ptr url_request_; + + THREAD_CHECKER(network_thread_checker_); + }; + + raw_ptr context_; + // |network_tasks_| is invoked on network thread. + NetworkTasks network_tasks_; + + // Request parameters set off network thread before Start(). + std::string initial_method_; + std::unique_ptr initial_request_headers_; + std::unique_ptr upload_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_CRONET_URL_REQUEST_H_ diff --git a/src/components/cronet/cronet_url_request_context.cc b/src/components/cronet/cronet_url_request_context.cc new file mode 100644 index 0000000000..5a0e573d9b --- /dev/null +++ b/src/components/cronet/cronet_url_request_context.cc @@ -0,0 +1,717 @@ +// Copyright 2014 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 "components/cronet/cronet_url_request_context.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "base/base64.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_file.h" +#include "base/lazy_instance.h" +#include "base/logging.h" +#include "base/memory/raw_ptr.h" +#include "base/message_loop/message_pump_type.h" +#include "base/metrics/histogram_macros.h" +#include "base/metrics/statistics_recorder.h" +#include "base/task/single_thread_task_runner.h" +#include "base/threading/thread_restrictions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "base/values.h" +#include "build/build_config.h" +#include "components/cronet/cronet_global_state.h" +#include "components/cronet/cronet_prefs_manager.h" +#include "components/cronet/host_cache_persistence_manager.h" +#include "components/cronet/url_request_context_config.h" +#include "net/base/ip_address.h" +#include "net/base/load_flags.h" +#include "net/base/logging_network_change_observer.h" +#include "net/base/net_errors.h" +#include "net/base/network_delegate_impl.h" +#include "net/base/network_isolation_key.h" +#include "net/base/url_util.h" +#include "net/cert/caching_cert_verifier.h" +#include "net/cert/cert_verifier.h" +#include "net/cert/x509_certificate.h" +#include "net/cookies/cookie_monster.h" +#include "net/http/http_auth_handler_factory.h" +#include "net/http/transport_security_state.h" +#include "net/log/file_net_log_observer.h" +#include "net/log/net_log_util.h" +#include "net/net_buildflags.h" +#include "net/nqe/network_quality_estimator_params.h" +#include "net/proxy_resolution/proxy_resolution_service.h" +#include "net/third_party/quiche/src/quic/core/quic_versions.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_context_builder.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_interceptor.h" + +#if BUILDFLAG(ENABLE_REPORTING) +#include "net/network_error_logging/network_error_logging_service.h" +#include "net/reporting/reporting_service.h" +#endif // BUILDFLAG(ENABLE_REPORTING) + +namespace { + +// This class wraps a NetLog that also contains network change events. +class NetLogWithNetworkChangeEvents { + public: + NetLogWithNetworkChangeEvents() : net_log_(net::NetLog::Get()) {} + + NetLogWithNetworkChangeEvents(const NetLogWithNetworkChangeEvents&) = delete; + NetLogWithNetworkChangeEvents& operator=( + const NetLogWithNetworkChangeEvents&) = delete; + + net::NetLog* net_log() { return net_log_; } + // This function registers with the NetworkChangeNotifier and so must be + // called *after* the NetworkChangeNotifier is created. Should only be + // called on the init thread as it is not thread-safe and the init thread is + // the thread the NetworkChangeNotifier is created on. This function is + // not thread-safe because accesses to |net_change_logger_| are not atomic. + // There might be multiple CronetEngines each with a network thread so + // so the init thread is used. |g_net_log_| also outlives the network threads + // so it would be unsafe to receive callbacks on the network threads without + // a complicated thread-safe reference-counting system to control callback + // registration. + void EnsureInitializedOnInitThread() { + DCHECK(cronet::OnInitThread()); + if (net_change_logger_) + return; + net_change_logger_ = + std::make_unique(net_log_); + } + + private: + raw_ptr net_log_; + // LoggingNetworkChangeObserver logs network change events to a NetLog. + // This class bundles one LoggingNetworkChangeObserver with one NetLog, + // so network change event are logged just once in the NetLog. + std::unique_ptr net_change_logger_; +}; + +// Use a global NetLog instance. See crbug.com/486120. +static base::LazyInstance::Leaky g_net_log = + LAZY_INSTANCE_INITIALIZER; + +class BasicNetworkDelegate : public net::NetworkDelegateImpl { + public: + BasicNetworkDelegate() {} + + BasicNetworkDelegate(const BasicNetworkDelegate&) = delete; + BasicNetworkDelegate& operator=(const BasicNetworkDelegate&) = delete; + + ~BasicNetworkDelegate() override {} + + private: + // net::NetworkDelegate implementation. + bool OnAnnotateAndMoveUserBlockedCookies( + const net::URLRequest& request, + net::CookieAccessResultList& maybe_included_cookies, + net::CookieAccessResultList& excluded_cookies, + bool allowed_from_caller) override { + // Disallow sending cookies by default. + ExcludeAllCookies(net::CookieInclusionStatus::EXCLUDE_USER_PREFERENCES, + maybe_included_cookies, excluded_cookies); + return false; + } + + bool OnCanSetCookie(const net::URLRequest& request, + const net::CanonicalCookie& cookie, + net::CookieOptions* options, + bool allowed_from_caller) override { + // Disallow saving cookies by default. + return false; + } +}; + +} // namespace + +namespace cronet { + +CronetURLRequestContext::CronetURLRequestContext( + std::unique_ptr context_config, + std::unique_ptr callback, + scoped_refptr network_task_runner) + : bidi_stream_detect_broken_connection_( + context_config->bidi_stream_detect_broken_connection), + heartbeat_interval_(context_config->heartbeat_interval), + default_load_flags_( + net::LOAD_NORMAL | + (context_config->load_disable_cache ? net::LOAD_DISABLE_CACHE : 0)), + network_tasks_( + new NetworkTasks(std::move(context_config), std::move(callback))), + network_task_runner_(network_task_runner) { + if (!network_task_runner_) { + network_thread_ = std::make_unique("network"); + base::Thread::Options options; + options.message_pump_type = base::MessagePumpType::IO; + network_thread_->StartWithOptions(std::move(options)); + network_task_runner_ = network_thread_->task_runner(); + } +} + +CronetURLRequestContext::~CronetURLRequestContext() { + DCHECK(!GetNetworkTaskRunner()->BelongsToCurrentThread()); + GetNetworkTaskRunner()->DeleteSoon(FROM_HERE, network_tasks_.get()); +} + +CronetURLRequestContext::NetworkTasks::NetworkTasks( + std::unique_ptr context_config, + std::unique_ptr callback) + : is_context_initialized_(false), + context_config_(std::move(context_config)), + callback_(std::move(callback)) { + DETACH_FROM_THREAD(network_thread_checker_); +} + +CronetURLRequestContext::NetworkTasks::~NetworkTasks() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + callback_->OnDestroyNetworkThread(); + + if (cronet_prefs_manager_) + cronet_prefs_manager_->PrepareForShutdown(); + + if (network_quality_estimator_) { + network_quality_estimator_->RemoveRTTObserver(this); + network_quality_estimator_->RemoveThroughputObserver(this); + network_quality_estimator_->RemoveEffectiveConnectionTypeObserver(this); + network_quality_estimator_->RemoveRTTAndThroughputEstimatesObserver(this); + } +} + +void CronetURLRequestContext::InitRequestContextOnInitThread() { + DCHECK(OnInitThread()); + auto proxy_config_service = + cronet::CreateProxyConfigService(GetNetworkTaskRunner()); + g_net_log.Get().EnsureInitializedOnInitThread(); + GetNetworkTaskRunner()->PostTask( + FROM_HERE, + base::BindOnce(&CronetURLRequestContext::NetworkTasks::Initialize, + base::Unretained(network_tasks_), GetNetworkTaskRunner(), + GetFileThread()->task_runner(), + std::move(proxy_config_service))); +} + +void CronetURLRequestContext::NetworkTasks:: + ConfigureNetworkQualityEstimatorForTesting(bool use_local_host_requests, + bool use_smaller_responses, + bool disable_offline_check) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + network_quality_estimator_->SetUseLocalHostRequestsForTesting( + use_local_host_requests); + network_quality_estimator_->SetUseSmallResponsesForTesting( + use_smaller_responses); + network_quality_estimator_->DisableOfflineCheckForTesting( + disable_offline_check); +} + +void CronetURLRequestContext::ConfigureNetworkQualityEstimatorForTesting( + bool use_local_host_requests, + bool use_smaller_responses, + bool disable_offline_check) { + PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce(&CronetURLRequestContext::NetworkTasks:: + ConfigureNetworkQualityEstimatorForTesting, + base::Unretained(network_tasks_), use_local_host_requests, + use_smaller_responses, disable_offline_check)); +} + +void CronetURLRequestContext::NetworkTasks::ProvideRTTObservations( + bool should) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + if (!network_quality_estimator_) + return; + if (should) { + network_quality_estimator_->AddRTTObserver(this); + } else { + network_quality_estimator_->RemoveRTTObserver(this); + } +} + +void CronetURLRequestContext::ProvideRTTObservations(bool should) { + PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce( + &CronetURLRequestContext::NetworkTasks::ProvideRTTObservations, + base::Unretained(network_tasks_), should)); +} + +void CronetURLRequestContext::NetworkTasks::ProvideThroughputObservations( + bool should) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + if (!network_quality_estimator_) + return; + if (should) { + network_quality_estimator_->AddThroughputObserver(this); + } else { + network_quality_estimator_->RemoveThroughputObserver(this); + } +} + +void CronetURLRequestContext::ProvideThroughputObservations(bool should) { + PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce( + &CronetURLRequestContext::NetworkTasks::ProvideThroughputObservations, + base::Unretained(network_tasks_), should)); +} + +void CronetURLRequestContext::NetworkTasks::InitializeNQEPrefs() const { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + // Initializing |network_qualities_prefs_manager_| may post a callback to + // |this|. So, |network_qualities_prefs_manager_| should be initialized after + // |callback_| has been initialized. + DCHECK(is_context_initialized_); + cronet_prefs_manager_->SetupNqePersistence(network_quality_estimator_.get()); +} + +void CronetURLRequestContext::NetworkTasks::Initialize( + scoped_refptr network_task_runner, + scoped_refptr file_task_runner, + std::unique_ptr proxy_config_service) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + DCHECK(!is_context_initialized_); + + std::unique_ptr config(std::move(context_config_)); + network_task_runner_ = network_task_runner; + if (config->network_thread_priority) + SetNetworkThreadPriorityOnNetworkThread( + config->network_thread_priority.value()); + base::DisallowBlocking(); + net::URLRequestContextBuilder context_builder; + context_builder.set_network_delegate( + std::make_unique()); + context_builder.set_net_log(g_net_log.Get().net_log()); + + context_builder.set_proxy_resolution_service( + cronet::CreateProxyResolutionService(std::move(proxy_config_service), + g_net_log.Get().net_log())); + + config->ConfigureURLRequestContextBuilder(&context_builder); + effective_experimental_options_ = + base::Value(config->effective_experimental_options); + + if (config->enable_network_quality_estimator) { + DCHECK(!network_quality_estimator_); + std::unique_ptr nqe_params = + std::make_unique( + std::map()); + if (config->nqe_forced_effective_connection_type) { + nqe_params->SetForcedEffectiveConnectionType( + config->nqe_forced_effective_connection_type.value()); + } + + network_quality_estimator_ = std::make_unique( + std::move(nqe_params), g_net_log.Get().net_log()); + network_quality_estimator_->AddEffectiveConnectionTypeObserver(this); + network_quality_estimator_->AddRTTAndThroughputEstimatesObserver(this); + + context_builder.set_network_quality_estimator( + network_quality_estimator_.get()); + } + + DCHECK(!cronet_prefs_manager_); + + // Set up pref file if storage path is specified. + if (!config->storage_path.empty()) { +#if BUILDFLAG(IS_WIN) + base::FilePath storage_path( + base::FilePath::FromUTF8Unsafe(config->storage_path)); +#else + base::FilePath storage_path(config->storage_path); +#endif + // Set up the HttpServerPropertiesManager. + cronet_prefs_manager_ = std::make_unique( + config->storage_path, network_task_runner_, file_task_runner, + config->enable_network_quality_estimator, + config->enable_host_cache_persistence, g_net_log.Get().net_log(), + &context_builder); + } + + // Explicitly disable the persister for Cronet to avoid persistence of dynamic + // HPKP. This is a safety measure ensuring that nobody enables the persistence + // of HPKP by specifying transport_security_persister_file_path in the future. + context_builder.set_transport_security_persister_file_path(base::FilePath()); + + // Disable net::CookieStore. + context_builder.SetCookieStore(nullptr); + + context_ = context_builder.Build(); + + // Set up host cache persistence if it's enabled. Happens after building the + // URLRequestContext to get access to the HostCache. + if (config->enable_host_cache_persistence && cronet_prefs_manager_) { + net::HostCache* host_cache = context_->host_resolver()->GetHostCache(); + cronet_prefs_manager_->SetupHostCachePersistence( + host_cache, config->host_cache_persistence_delay_ms, + g_net_log.Get().net_log()); + } + + context_->set_check_cleartext_permitted(true); + context_->set_enable_brotli(config->enable_brotli); + + if (config->enable_quic) { + for (const auto& quic_hint : config->quic_hints) { + if (quic_hint->host.empty()) { + LOG(ERROR) << "Empty QUIC hint host: " << quic_hint->host; + continue; + } + + url::CanonHostInfo host_info; + std::string canon_host( + net::CanonicalizeHost(quic_hint->host, &host_info)); + if (!host_info.IsIPAddress() && + !net::IsCanonicalizedHostCompliant(canon_host)) { + LOG(ERROR) << "Invalid QUIC hint host: " << quic_hint->host; + continue; + } + + if (quic_hint->port <= std::numeric_limits::min() || + quic_hint->port > std::numeric_limits::max()) { + LOG(ERROR) << "Invalid QUIC hint port: " << quic_hint->port; + continue; + } + + if (quic_hint->alternate_port <= std::numeric_limits::min() || + quic_hint->alternate_port > std::numeric_limits::max()) { + LOG(ERROR) << "Invalid QUIC hint alternate port: " + << quic_hint->alternate_port; + continue; + } + + url::SchemeHostPort quic_server("https", canon_host, quic_hint->port); + net::AlternativeService alternative_service( + net::kProtoQUIC, "", + static_cast(quic_hint->alternate_port)); + context_->http_server_properties()->SetQuicAlternativeService( + quic_server, net::NetworkIsolationKey(), alternative_service, + base::Time::Max(), quic::ParsedQuicVersionVector()); + } + } + + // Iterate through PKP configuration for every host. + for (const auto& pkp : config->pkp_list) { + // Add the host pinning. + context_->transport_security_state()->AddHPKP( + pkp->host, pkp->expiration_date, pkp->include_subdomains, + pkp->pin_hashes, GURL::EmptyGURL()); + } + + context_->transport_security_state() + ->SetEnablePublicKeyPinningBypassForLocalTrustAnchors( + config->bypass_public_key_pinning_for_local_trust_anchors); + + callback_->OnInitNetworkThread(); + is_context_initialized_ = true; + + // Set up network quality prefs. + if (config->enable_network_quality_estimator && cronet_prefs_manager_) { + // TODO(crbug.com/758401): execute the content of + // InitializeNQEPrefsOnNetworkThread method directly (i.e. without posting) + // after the bug has been fixed. + network_task_runner_->PostTask( + FROM_HERE, + base::BindOnce( + &CronetURLRequestContext::NetworkTasks::InitializeNQEPrefs, + base::Unretained(this))); + } + +#if BUILDFLAG(ENABLE_REPORTING) + if (context_->reporting_service()) { + for (const auto& preloaded_header : config->preloaded_report_to_headers) { + context_->reporting_service()->ProcessReportToHeader( + preloaded_header.origin, net::NetworkIsolationKey(), + preloaded_header.value); + } + } + + if (context_->network_error_logging_service()) { + for (const auto& preloaded_header : config->preloaded_nel_headers) { + context_->network_error_logging_service()->OnHeader( + net::NetworkIsolationKey(), preloaded_header.origin, net::IPAddress(), + preloaded_header.value); + } + } +#endif // BUILDFLAG(ENABLE_REPORTING) + + while (!tasks_waiting_for_context_.empty()) { + std::move(tasks_waiting_for_context_.front()).Run(); + tasks_waiting_for_context_.pop(); + } +} + +net::URLRequestContext* +CronetURLRequestContext::NetworkTasks::GetURLRequestContext() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + if (!context_) { + LOG(ERROR) << "URLRequestContext is not set up"; + } + return context_.get(); +} + +// Request context getter for CronetURLRequestContext. +class CronetURLRequestContext::ContextGetter + : public net::URLRequestContextGetter { + public: + explicit ContextGetter(CronetURLRequestContext* cronet_context) + : cronet_context_(cronet_context) { + DCHECK(cronet_context_); + } + + ContextGetter(const ContextGetter&) = delete; + ContextGetter& operator=(const ContextGetter&) = delete; + + net::URLRequestContext* GetURLRequestContext() override { + return cronet_context_->GetURLRequestContext(); + } + + scoped_refptr GetNetworkTaskRunner() + const override { + return cronet_context_->GetNetworkTaskRunner(); + } + + private: + // Must be called on the network thread. + ~ContextGetter() override { DCHECK(cronet_context_->IsOnNetworkThread()); } + + // CronetURLRequestContext associated with this ContextGetter. + const raw_ptr cronet_context_; +}; + +net::URLRequestContextGetter* +CronetURLRequestContext::CreateURLRequestContextGetter() { + DCHECK(IsOnNetworkThread()); + return new ContextGetter(this); +} + +net::URLRequestContext* CronetURLRequestContext::GetURLRequestContext() { + DCHECK(IsOnNetworkThread()); + return network_tasks_->GetURLRequestContext(); +} + +void CronetURLRequestContext::PostTaskToNetworkThread( + const base::Location& posted_from, + base::OnceClosure callback) { + GetNetworkTaskRunner()->PostTask( + posted_from, + base::BindOnce( + &CronetURLRequestContext::NetworkTasks::RunTaskAfterContextInit, + base::Unretained(network_tasks_), std::move(callback))); +} + +void CronetURLRequestContext::NetworkTasks::RunTaskAfterContextInit( + base::OnceClosure task_to_run_after_context_init) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + if (is_context_initialized_) { + DCHECK(tasks_waiting_for_context_.empty()); + std::move(task_to_run_after_context_init).Run(); + return; + } + tasks_waiting_for_context_.push(std::move(task_to_run_after_context_init)); +} + +bool CronetURLRequestContext::IsOnNetworkThread() const { + return GetNetworkTaskRunner()->BelongsToCurrentThread(); +} + +scoped_refptr +CronetURLRequestContext::GetNetworkTaskRunner() const { + return network_task_runner_; +} + +bool CronetURLRequestContext::StartNetLogToFile(const std::string& file_name, + bool log_all) { +#if BUILDFLAG(IS_WIN) + base::FilePath file_path(base::FilePath::FromUTF8Unsafe(file_name)); +#else + base::FilePath file_path(file_name); +#endif + base::ScopedFILE file(base::OpenFile(file_path, "w")); + if (!file) { + LOG(ERROR) << "Failed to open NetLog file for writing."; + return false; + } + PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce(&CronetURLRequestContext::NetworkTasks::StartNetLog, + base::Unretained(network_tasks_), file_path, log_all)); + return true; +} + +void CronetURLRequestContext::StartNetLogToDisk(const std::string& dir_name, + bool log_all, + int max_size) { + PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce( + &CronetURLRequestContext::NetworkTasks::StartNetLogToBoundedFile, + base::Unretained(network_tasks_), dir_name, log_all, max_size)); +} + +void CronetURLRequestContext::StopNetLog() { + DCHECK(!GetNetworkTaskRunner()->BelongsToCurrentThread()); + PostTaskToNetworkThread( + FROM_HERE, + base::BindOnce(&CronetURLRequestContext::NetworkTasks::StopNetLog, + base::Unretained(network_tasks_))); +} + +int CronetURLRequestContext::default_load_flags() const { + return default_load_flags_; +} + +base::Thread* CronetURLRequestContext::GetFileThread() { + DCHECK(OnInitThread()); + if (!file_thread_) { + file_thread_ = std::make_unique("Network File Thread"); + file_thread_->Start(); + } + return file_thread_.get(); +} + +void CronetURLRequestContext::NetworkTasks::OnEffectiveConnectionTypeChanged( + net::EffectiveConnectionType effective_connection_type) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + callback_->OnEffectiveConnectionTypeChanged(effective_connection_type); +} + +void CronetURLRequestContext::NetworkTasks::OnRTTOrThroughputEstimatesComputed( + base::TimeDelta http_rtt, + base::TimeDelta transport_rtt, + int32_t downstream_throughput_kbps) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + + int32_t http_rtt_ms = http_rtt.InMilliseconds() <= INT32_MAX + ? static_cast(http_rtt.InMilliseconds()) + : INT32_MAX; + int32_t transport_rtt_ms = + transport_rtt.InMilliseconds() <= INT32_MAX + ? static_cast(transport_rtt.InMilliseconds()) + : INT32_MAX; + + callback_->OnRTTOrThroughputEstimatesComputed(http_rtt_ms, transport_rtt_ms, + downstream_throughput_kbps); +} + +void CronetURLRequestContext::NetworkTasks::OnRTTObservation( + int32_t rtt_ms, + const base::TimeTicks& timestamp, + net::NetworkQualityObservationSource source) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + + callback_->OnRTTObservation( + rtt_ms, (timestamp - base::TimeTicks::UnixEpoch()).InMilliseconds(), + source); +} + +void CronetURLRequestContext::NetworkTasks::OnThroughputObservation( + int32_t throughput_kbps, + const base::TimeTicks& timestamp, + net::NetworkQualityObservationSource source) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + + callback_->OnThroughputObservation( + throughput_kbps, + (timestamp - base::TimeTicks::UnixEpoch()).InMilliseconds(), source); +} + +void CronetURLRequestContext::NetworkTasks::StartNetLog( + const base::FilePath& file_path, + bool include_socket_bytes) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + + // Do nothing if already logging to a file. + if (net_log_file_observer_) + return; + + net::NetLogCaptureMode capture_mode = + include_socket_bytes ? net::NetLogCaptureMode::kEverything + : net::NetLogCaptureMode::kDefault; + net_log_file_observer_ = net::FileNetLogObserver::CreateUnbounded( + file_path, capture_mode, /*constants=*/nullptr); + CreateNetLogEntriesForActiveObjects({context_.get()}, + net_log_file_observer_.get()); + net_log_file_observer_->StartObserving(g_net_log.Get().net_log()); +} + +void CronetURLRequestContext::NetworkTasks::StartNetLogToBoundedFile( + const std::string& dir_path, + bool include_socket_bytes, + int size) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + + // Do nothing if already logging to a directory. + if (net_log_file_observer_) + return; + + // TODO(eroman): The cronet API passes a directory here. But it should now + // just pass a file path. +#if BUILDFLAG(IS_WIN) + base::FilePath file_path(base::FilePath::FromUTF8Unsafe(dir_path)); +#else + base::FilePath file_path(dir_path); +#endif + file_path = file_path.AppendASCII("netlog.json"); + + { + base::ScopedAllowBlocking allow_blocking; + if (!base::PathIsWritable(file_path)) { + LOG(ERROR) << "Path is not writable: " << file_path.value(); + } + } + + net::NetLogCaptureMode capture_mode = + include_socket_bytes ? net::NetLogCaptureMode::kEverything + : net::NetLogCaptureMode::kDefault; + net_log_file_observer_ = net::FileNetLogObserver::CreateBounded( + file_path, size, capture_mode, /*constants=*/nullptr); + + CreateNetLogEntriesForActiveObjects({context_.get()}, + net_log_file_observer_.get()); + + net_log_file_observer_->StartObserving(g_net_log.Get().net_log()); +} + +void CronetURLRequestContext::NetworkTasks::StopNetLog() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + + if (!net_log_file_observer_) + return; + net_log_file_observer_->StopObserving( + base::Value::ToUniquePtrValue(GetNetLogInfo()), + base::BindOnce( + &CronetURLRequestContext::NetworkTasks::StopNetLogCompleted, + base::Unretained(this))); + net_log_file_observer_.reset(); +} + +void CronetURLRequestContext::NetworkTasks::StopNetLogCompleted() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + callback_->OnStopNetLogCompleted(); +} + +base::Value CronetURLRequestContext::NetworkTasks::GetNetLogInfo() const { + base::Value net_info = net::GetNetInfo(context_.get()); + if (!effective_experimental_options_.DictEmpty()) { + net_info.SetKey("cronetExperimentalParams", + effective_experimental_options_.Clone()); + } + return net_info; +} + +} // namespace cronet diff --git a/src/components/cronet/cronet_url_request_context.h b/src/components/cronet/cronet_url_request_context.h new file mode 100644 index 0000000000..e91ede64fe --- /dev/null +++ b/src/components/cronet/cronet_url_request_context.h @@ -0,0 +1,324 @@ +// Copyright 2014 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_CRONET_URL_REQUEST_CONTEXT_H_ +#define COMPONENTS_CRONET_CRONET_URL_REQUEST_CONTEXT_H_ + +#include + +#include +#include + +#include "base/callback.h" +#include "base/containers/queue.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/threading/thread.h" +#include "base/threading/thread_checker.h" +#include "components/prefs/json_pref_store.h" +#include "net/nqe/effective_connection_type.h" +#include "net/nqe/effective_connection_type_observer.h" +#include "net/nqe/network_quality_estimator.h" +#include "net/nqe/network_quality_observation_source.h" +#include "net/nqe/rtt_throughput_estimates_observer.h" + +class PrefService; + +namespace base { +class SingleThreadTaskRunner; +class TimeTicks; +} // namespace base + +namespace net { +enum EffectiveConnectionType; +class NetLog; +class ProxyConfigService; +class URLRequestContext; +class URLRequestContextGetter; +class FileNetLogObserver; +} // namespace net + +namespace cronet { +class CronetPrefsManager; +class TestUtil; + +struct URLRequestContextConfig; + +// Wrapper around net::URLRequestContext. +class CronetURLRequestContext { + public: + // Callback implemented by CronetURLRequestContext() caller and owned by + // CronetURLRequestContext::NetworkTasks. + class Callback { + public: + virtual ~Callback() = default; + + // Invoked on network thread when initialized. + virtual void OnInitNetworkThread() = 0; + + // Invoked on network thread immediately prior to destruction. + virtual void OnDestroyNetworkThread() = 0; + + // net::NetworkQualityEstimator::EffectiveConnectionTypeObserver forwarder. + virtual void OnEffectiveConnectionTypeChanged( + net::EffectiveConnectionType effective_connection_type) = 0; + + // net::NetworkQualityEstimator::RTTAndThroughputEstimatesObserver + // forwarder. + virtual void OnRTTOrThroughputEstimatesComputed( + int32_t http_rtt_ms, + int32_t transport_rtt_ms, + int32_t downstream_throughput_kbps) = 0; + + // net::NetworkQualityEstimator::RTTObserver forwarder. + virtual void OnRTTObservation( + int32_t rtt_ms, + int32_t timestamp_ms, + net::NetworkQualityObservationSource source) = 0; + + // net::NetworkQualityEstimator::RTTObserver forwarder. + virtual void OnThroughputObservation( + int32_t throughput_kbps, + int32_t timestamp_ms, + net::NetworkQualityObservationSource source) = 0; + + // Callback for StopNetLog() that signals that it is safe to access + // the NetLog files. + virtual void OnStopNetLogCompleted() = 0; + }; + + // Constructs CronetURLRequestContext using |context_config|. The |callback| + // is owned by |this| and is deleted on network thread. + // All |callback| methods are invoked on network thread. + // If the network_task_runner is not assigned, a network thread would be + // created for network tasks. Otherwise the tasks would be running on the + // assigned task runner. + CronetURLRequestContext( + std::unique_ptr context_config, + std::unique_ptr callback, + scoped_refptr network_task_runner = + nullptr); + + CronetURLRequestContext(const CronetURLRequestContext&) = delete; + CronetURLRequestContext& operator=(const CronetURLRequestContext&) = delete; + + // Releases all resources for the request context and deletes the object. + // Blocks until network thread is destroyed after running all pending tasks. + virtual ~CronetURLRequestContext(); + + // Called on init thread to initialize URLRequestContext. + void InitRequestContextOnInitThread(); + + // Posts a task that might depend on the context being initialized + // to the network thread. + void PostTaskToNetworkThread(const base::Location& posted_from, + base::OnceClosure callback); + + // Returns true if running on network thread. + bool IsOnNetworkThread() const; + + // Returns net::URLRequestContext owned by |this|. + net::URLRequestContext* GetURLRequestContext(); + + // Returns a new instance of net::URLRequestContextGetter. + // The net::URLRequestContext and base::SingleThreadTaskRunner that + // net::URLRequestContextGetter returns are owned by |this|. + net::URLRequestContextGetter* CreateURLRequestContextGetter(); + + // TODO(xunjieli): Keep only one version of StartNetLog(). + + // Starts NetLog logging to file. This can be called on any thread. + // Return false if |file_name| cannot be opened. + bool StartNetLogToFile(const std::string& file_name, bool log_all); + + // Starts NetLog logging to disk with a bounded amount of disk space. This + // can be called on any thread. + void StartNetLogToDisk(const std::string& dir_name, + bool log_all, + int max_size); + + // Stops NetLog logging to file. This can be called on any thread. This will + // flush any remaining writes to disk. + void StopNetLog(); + + // Default net::LOAD flags used to create requests. + int default_load_flags() const; + + // Configures the network quality estimator to observe requests to localhost, + // to use smaller responses when estimating throughput, and to disable the + // device offline checks when computing the effective connection type or when + // writing the prefs. This should only be used for testing. This can be + // called only after the network quality estimator has been enabled. + void ConfigureNetworkQualityEstimatorForTesting(bool use_local_host_requests, + bool use_smaller_responses, + bool disable_offline_check); + + // Request that RTT and/or throughput observations should or should not be + // provided by the network quality estimator. + void ProvideRTTObservations(bool should); + void ProvideThroughputObservations(bool should); + + bool bidi_stream_detect_broken_connection() const { + return bidi_stream_detect_broken_connection_; + } + base::TimeDelta heartbeat_interval() const { return heartbeat_interval_; } + + private: + friend class TestUtil; + class ContextGetter; + + // NetworkTasks performs tasks on the network thread and owns objects that + // live on the network thread. + class NetworkTasks : public net::EffectiveConnectionTypeObserver, + public net::RTTAndThroughputEstimatesObserver, + public net::NetworkQualityEstimator::RTTObserver, + public net::NetworkQualityEstimator::ThroughputObserver { + public: + // Invoked off the network thread. + NetworkTasks(std::unique_ptr config, + std::unique_ptr callback); + + NetworkTasks(const NetworkTasks&) = delete; + NetworkTasks& operator=(const NetworkTasks&) = delete; + + // Invoked on the network thread. + ~NetworkTasks() override; + + // Initializes |context_| on the network thread. + void Initialize( + scoped_refptr network_task_runner, + scoped_refptr file_task_runner, + std::unique_ptr proxy_config_service); + + // Runs a task that might depend on the context being initialized. + void RunTaskAfterContextInit( + base::OnceClosure task_to_run_after_context_init); + + // Configures the network quality estimator to observe requests to + // localhost, to use smaller responses when estimating throughput, and to + // disable the device offline checks when computing the effective connection + // type or when writing the prefs. This should only be used for testing. + void ConfigureNetworkQualityEstimatorForTesting( + bool use_local_host_requests, + bool use_smaller_responses, + bool disable_offline_check); + + void ProvideRTTObservations(bool should); + void ProvideThroughputObservations(bool should); + + // net::NetworkQualityEstimator::EffectiveConnectionTypeObserver + // implementation. + void OnEffectiveConnectionTypeChanged( + net::EffectiveConnectionType effective_connection_type) override; + + // net::NetworkQualityEstimator::RTTAndThroughputEstimatesObserver + // implementation. + void OnRTTOrThroughputEstimatesComputed( + base::TimeDelta http_rtt, + base::TimeDelta transport_rtt, + int32_t downstream_throughput_kbps) override; + + // net::NetworkQualityEstimator::RTTObserver implementation. + void OnRTTObservation(int32_t rtt_ms, + const base::TimeTicks& timestamp, + net::NetworkQualityObservationSource source) override; + + // net::NetworkQualityEstimator::ThroughputObserver implementation. + void OnThroughputObservation( + int32_t throughput_kbps, + const base::TimeTicks& timestamp, + net::NetworkQualityObservationSource source) override; + + net::URLRequestContext* GetURLRequestContext(); + + // Same as StartNetLogToDisk. + void StartNetLogToBoundedFile(const std::string& dir_path, + bool include_socket_bytes, + int size); + + // Same as StartNetLogToFile, but called only on the network thread. + void StartNetLog(const base::FilePath& file_path, + bool include_socket_bytes); + + // Stops NetLog logging. + void StopNetLog(); + + // Callback for StopObserving() that unblocks the client thread and + // signals that it is safe to access the NetLog files. + void StopNetLogCompleted(); + + // Initializes Network Quality Estimator (NQE) prefs manager on network + // thread. + void InitializeNQEPrefs() const; + + private: + friend class TestUtil; + base::Value GetNetLogInfo() const; + + std::unique_ptr net_log_file_observer_; + + // A network quality estimator. This member variable has to be destroyed + // after destroying |cronet_prefs_manager_|, which owns + // NetworkQualityPrefsManager that weakly references + // |network_quality_estimator_|. + std::unique_ptr network_quality_estimator_; + + // Manages the PrefService and all associated persistence managers + // such as NetworkQualityPrefsManager, HostCachePersistenceManager, etc. + // It should be destroyed before |network_quality_estimator_| and + // after |context_|. + std::unique_ptr cronet_prefs_manager_; + + std::unique_ptr context_; + bool is_context_initialized_; + + // Context config is only valid until context is initialized. + std::unique_ptr context_config_; + + // Effective experimental options. Kept for NetLog. + base::Value effective_experimental_options_; + + // A queue of tasks that need to be run after context has been initialized. + base::queue tasks_waiting_for_context_; + + // Task runner that runs network tasks. + scoped_refptr network_task_runner_; + + // Callback implemented by the client. + std::unique_ptr callback_; + + THREAD_CHECKER(network_thread_checker_); + }; + + scoped_refptr GetNetworkTaskRunner() const; + + // Gets the file thread. Create one if there is none. + base::Thread* GetFileThread(); + + // Whether the connection status of active bidirectional streams should be + // monitored. + bool bidi_stream_detect_broken_connection_; + // If |bidi_stream_detect_broken_connection_| is true, this suggests the + // period of the heartbeat signal. + base::TimeDelta heartbeat_interval_; + + const int default_load_flags_; + + // File thread should be destroyed last. + std::unique_ptr file_thread_; + + // |network_tasks_| is owned by |this|. It is created off the network thread, + // but invoked and destroyed on network thread. + raw_ptr network_tasks_; + + // Network thread is destroyed from client thread. + std::unique_ptr network_thread_; + + // Task runner that runs network tasks. + scoped_refptr network_task_runner_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_CRONET_URL_REQUEST_CONTEXT_H_ diff --git a/src/components/cronet/host_cache_persistence_manager.cc b/src/components/cronet/host_cache_persistence_manager.cc new file mode 100644 index 0000000000..7c1777af6a --- /dev/null +++ b/src/components/cronet/host_cache_persistence_manager.cc @@ -0,0 +1,90 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/cronet/host_cache_persistence_manager.h" + +#include + +#include "base/bind.h" +#include "base/values.h" +#include "components/prefs/pref_service.h" +#include "net/log/net_log.h" + +namespace cronet { + +HostCachePersistenceManager::HostCachePersistenceManager( + net::HostCache* cache, + PrefService* pref_service, + std::string pref_name, + base::TimeDelta delay, + net::NetLog* net_log) + : cache_(cache), + pref_service_(pref_service), + pref_name_(pref_name), + writing_pref_(false), + delay_(delay), + net_log_(net::NetLogWithSource::Make( + net_log, + net::NetLogSourceType::HOST_CACHE_PERSISTENCE_MANAGER)) { + DCHECK(cache_); + DCHECK(pref_service_); + + // Get the initial value of the pref if it's already initialized. + if (pref_service_->HasPrefPath(pref_name_)) + ReadFromDisk(); + + registrar_.Init(pref_service_); + registrar_.Add(pref_name_, + base::BindRepeating(&HostCachePersistenceManager::ReadFromDisk, + weak_factory_.GetWeakPtr())); + cache_->set_persistence_delegate(this); +} + +HostCachePersistenceManager::~HostCachePersistenceManager() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + timer_.Stop(); + registrar_.RemoveAll(); + cache_->set_persistence_delegate(nullptr); +} + +void HostCachePersistenceManager::ReadFromDisk() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (writing_pref_) + return; + + net_log_.BeginEvent(net::NetLogEventType::HOST_CACHE_PREF_READ); + const base::Value* pref_value = pref_service_->GetList(pref_name_); + bool success = cache_->RestoreFromListValue(*pref_value); + net_log_.AddEntryWithBoolParams(net::NetLogEventType::HOST_CACHE_PREF_READ, + net::NetLogEventPhase::END, "success", + success); +} + +void HostCachePersistenceManager::ScheduleWrite() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (timer_.IsRunning()) + return; + + net_log_.AddEvent(net::NetLogEventType::HOST_CACHE_PERSISTENCE_START_TIMER); + timer_.Start(FROM_HERE, delay_, + base::BindOnce(&HostCachePersistenceManager::WriteToDisk, + weak_factory_.GetWeakPtr())); +} + +void HostCachePersistenceManager::WriteToDisk() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + net_log_.AddEvent(net::NetLogEventType::HOST_CACHE_PREF_WRITE); + base::Value value(base::Value::Type::LIST); + cache_->GetList(&value, false, + net::HostCache::SerializationType::kRestorable); + writing_pref_ = true; + pref_service_->Set(pref_name_, value); + writing_pref_ = false; +} + +} // namespace cronet diff --git a/src/components/cronet/host_cache_persistence_manager.h b/src/components/cronet/host_cache_persistence_manager.h new file mode 100644 index 0000000000..daac0bd881 --- /dev/null +++ b/src/components/cronet/host_cache_persistence_manager.h @@ -0,0 +1,84 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_CRONET_HOST_CACHE_PERSISTENCE_MANAGER_H_ +#define COMPONENTS_CRONET_HOST_CACHE_PERSISTENCE_MANAGER_H_ + +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/sequence_checker.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "components/prefs/pref_change_registrar.h" +#include "net/dns/host_cache.h" +#include "net/log/net_log_with_source.h" + +class PrefService; + +namespace net { +class NetLog; +} + +namespace cronet { +// Handles the interaction between HostCache and prefs for persistence. +// When notified of a change in the HostCache, starts a timer, or ignores if the +// timer is already running. When that timer expires, writes the current state +// of the HostCache to prefs. +// +// Can be used with synchronous or asynchronous prefs loading. Not appropriate +// for use outside of Cronet because its network and prefs operations run on +// the same sequence. Must be created after and destroyed before the HostCache +// and PrefService. +class HostCachePersistenceManager : public net::HostCache::PersistenceDelegate { + public: + // |cache| is the HostCache whose contents will be persisted. It must be + // non-null and must outlive the HostCachePersistenceManager. + // |pref_service| is the PrefService that will be used to persist the cache + // contents. It must outlive the HostCachePersistenceManager. + // |pref_name| is the name of the pref to read and write. + // |delay| is the maximum time between a change in the cache and writing that + // change to prefs. + HostCachePersistenceManager(net::HostCache* cache, + PrefService* pref_service, + std::string pref_name, + base::TimeDelta delay, + net::NetLog* net_log); + + HostCachePersistenceManager(const HostCachePersistenceManager&) = delete; + HostCachePersistenceManager& operator=(const HostCachePersistenceManager&) = + delete; + + virtual ~HostCachePersistenceManager(); + + // net::HostCache::PersistenceDelegate implementation + void ScheduleWrite() override; + + private: + // Gets the serialized HostCache and writes it to prefs. + void WriteToDisk(); + // On initial prefs read, passes the serialized entries to the HostCache. + void ReadFromDisk(); + + const raw_ptr cache_; + + PrefChangeRegistrar registrar_; + const raw_ptr pref_service_; + const std::string pref_name_; + bool writing_pref_; + + const base::TimeDelta delay_; + base::OneShotTimer timer_; + + const net::NetLogWithSource net_log_; + + SEQUENCE_CHECKER(sequence_checker_); + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_HOST_CACHE_PERSISTENCE_MANAGER_H_ diff --git a/src/components/cronet/host_cache_persistence_manager_unittest.cc b/src/components/cronet/host_cache_persistence_manager_unittest.cc new file mode 100644 index 0000000000..ad929ffa21 --- /dev/null +++ b/src/components/cronet/host_cache_persistence_manager_unittest.cc @@ -0,0 +1,192 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/cronet/host_cache_persistence_manager.h" + +#include "base/test/scoped_mock_time_message_loop_task_runner.h" +#include "base/test/task_environment.h" +#include "base/values.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/testing_pref_service.h" +#include "net/base/net_errors.h" +#include "net/base/network_isolation_key.h" +#include "net/dns/host_cache.h" +#include "net/dns/public/dns_query_type.h" +#include "net/dns/public/host_resolver_source.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace cronet { + +class HostCachePersistenceManagerTest : public testing::Test { + protected: + void SetUp() override { + cache_ = net::HostCache::CreateDefaultCache(); + pref_service_ = std::make_unique(); + pref_service_->registry()->RegisterListPref(kPrefName); + } + + void MakePersistenceManager(base::TimeDelta delay) { + persistence_manager_ = std::make_unique( + cache_.get(), pref_service_.get(), kPrefName, delay, nullptr); + } + + // Sets an entry in the HostCache in order to trigger a pref write. The + // caller is responsible for making sure this is a change that will trigger + // a write, and the HostCache's interaction with its PersistenceDelegate is + // assumed to work (it's tested in net/dns/host_cache_unittest.cc). + void WriteToCache(const std::string& host) { + net::HostCache::Key key(host, net::DnsQueryType::UNSPECIFIED, 0, + net::HostResolverSource::ANY, + net::NetworkIsolationKey()); + net::HostCache::Entry entry(net::OK, net::AddressList(), + net::HostCache::Entry::SOURCE_UNKNOWN); + cache_->Set(key, entry, base::TimeTicks::Now(), base::Seconds(1)); + } + + // Reads the current value of the pref from the TestingPrefServiceSimple + // and deserializes it into a temporary new HostCache. Only checks the size, + // not the full contents, since the tests in this file are only intended + // to test that writes happen when they're supposed to, not serialization + // correctness. + void CheckPref(size_t expected_size) { + const base::Value* value = pref_service_->GetUserPref(kPrefName); + base::Value list(base::Value::Type::LIST); + if (value) + list = base::Value(value->GetListDeprecated()); + net::HostCache temp_cache(10); + temp_cache.RestoreFromListValue(base::Value::AsListValue(list)); + ASSERT_EQ(expected_size, temp_cache.size()); + } + + // Generates a temporary HostCache with a few entries and uses it to + // initialize the value in prefs. + void InitializePref() { + net::HostCache temp_cache(10); + + net::HostCache::Key key1("1.test", net::DnsQueryType::UNSPECIFIED, 0, + net::HostResolverSource::ANY, + net::NetworkIsolationKey()); + net::HostCache::Key key2("2.test", net::DnsQueryType::UNSPECIFIED, 0, + net::HostResolverSource::ANY, + net::NetworkIsolationKey()); + net::HostCache::Key key3("3.test", net::DnsQueryType::UNSPECIFIED, 0, + net::HostResolverSource::ANY, + net::NetworkIsolationKey()); + net::HostCache::Entry entry(net::OK, net::AddressList(), + net::HostCache::Entry::SOURCE_UNKNOWN); + + temp_cache.Set(key1, entry, base::TimeTicks::Now(), base::Seconds(1)); + temp_cache.Set(key2, entry, base::TimeTicks::Now(), base::Seconds(1)); + temp_cache.Set(key3, entry, base::TimeTicks::Now(), base::Seconds(1)); + + base::Value value(base::Value::Type::LIST); + temp_cache.GetList(&value, false /* include_stale */, + net::HostCache::SerializationType::kRestorable); + pref_service_->Set(kPrefName, value); + } + + static const char kPrefName[]; + + base::test::TaskEnvironment task_environment_; + base::ScopedMockTimeMessageLoopTaskRunner task_runner_; + + // The HostCache and PrefService have to outlive the + // HostCachePersistenceManager. + std::unique_ptr cache_; + std::unique_ptr pref_service_; + std::unique_ptr persistence_manager_; +}; + +const char HostCachePersistenceManagerTest::kPrefName[] = "net.test"; + +// Make a single change to the HostCache and make sure that it's written +// when the timer expires. Then repeat. +TEST_F(HostCachePersistenceManagerTest, SeparateWrites) { + MakePersistenceManager(base::Seconds(60)); + + WriteToCache("1.test"); + task_runner_->FastForwardBy(base::Seconds(59)); + CheckPref(0); + task_runner_->FastForwardBy(base::Seconds(1)); + CheckPref(1); + + WriteToCache("2.test"); + task_runner_->FastForwardBy(base::Seconds(59)); + CheckPref(1); + task_runner_->FastForwardBy(base::Seconds(1)); + CheckPref(2); +} + +// Write to the HostCache multiple times and make sure that all changes +// are written to prefs at the appropriate times. +TEST_F(HostCachePersistenceManagerTest, MultipleWrites) { + MakePersistenceManager(base::Seconds(300)); + + WriteToCache("1.test"); + WriteToCache("2.test"); + task_runner_->FastForwardBy(base::Seconds(299)); + CheckPref(0); + task_runner_->FastForwardBy(base::Seconds(1)); + CheckPref(2); + + WriteToCache("3.test"); + WriteToCache("4.test"); + task_runner_->FastForwardBy(base::Seconds(299)); + CheckPref(2); + task_runner_->FastForwardBy(base::Seconds(1)); + CheckPref(4); +} + +// Make changes to the HostCache at different times and ensure that the writes +// to prefs are batched as expected. +TEST_F(HostCachePersistenceManagerTest, BatchedWrites) { + MakePersistenceManager(base::Milliseconds(100)); + + WriteToCache("1.test"); + task_runner_->FastForwardBy(base::Milliseconds(30)); + WriteToCache("2.test"); + task_runner_->FastForwardBy(base::Milliseconds(30)); + WriteToCache("3.test"); + CheckPref(0); + task_runner_->FastForwardBy(base::Milliseconds(40)); + CheckPref(3); + + // Add a delay in between batches. + task_runner_->FastForwardBy(base::Milliseconds(50)); + + WriteToCache("4.test"); + task_runner_->FastForwardBy(base::Milliseconds(30)); + WriteToCache("5.test"); + task_runner_->FastForwardBy(base::Milliseconds(30)); + WriteToCache("6.test"); + CheckPref(3); + task_runner_->FastForwardBy(base::Milliseconds(40)); + CheckPref(6); +} + +// Set the pref before the HostCachePersistenceManager is created, and make +// sure it gets picked up by the HostCache. +TEST_F(HostCachePersistenceManagerTest, InitAfterPrefs) { + CheckPref(0); + InitializePref(); + CheckPref(3); + + MakePersistenceManager(base::Seconds(1)); + task_runner_->RunUntilIdle(); + ASSERT_EQ(3u, cache_->size()); +} + +// Set the pref after the HostCachePersistenceManager is created, and make +// sure it gets picked up by the HostCache. +TEST_F(HostCachePersistenceManagerTest, InitBeforePrefs) { + MakePersistenceManager(base::Seconds(1)); + ASSERT_EQ(0u, cache_->size()); + + CheckPref(0); + InitializePref(); + CheckPref(3); + ASSERT_EQ(3u, cache_->size()); +} + +} // namespace cronet diff --git a/src/components/cronet/ios/BUILD.gn b/src/components/cronet/ios/BUILD.gn new file mode 100644 index 0000000000..4c0d32f475 --- /dev/null +++ b/src/components/cronet/ios/BUILD.gn @@ -0,0 +1,363 @@ +# 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. + +import("//build/apple/tweak_info_plist.gni") +import("//build/config/apple/symbols.gni") +import("//build/config/c++/c++.gni") +import("//build/config/ios/ios_sdk.gni") +import("//build/config/ios/rules.gni") +import("//build/util/lastchange.gni") +import("//components/cronet/native/include/headers.gni") +import("//components/grpc_support/include/headers.gni") +import("//testing/test.gni") +import("//url/features.gni") + +assert(!is_component_build, "Cronet requires static library build.") + +group("cronet_consumer_group") { + deps = [ "//components/cronet/ios/cronet_consumer" ] +} + +# TODO(crbug.com/1238839): Remove this once deprecated APIs are unused. +config("disable_deprecated_errors") { + cflags = [ + "-Wno-deprecated", + "-Wno-deprecated-declarations", + ] +} + +config("cronet_include_config") { + include_dirs = [ "//components/grpc_support/include" ] +} + +config("cronet_static_config") { + frameworks = [ + "Cronet.framework", + "CoreTelephony.framework", + "UIKit.framework", + "CFNetwork.framework", + "MobileCoreServices.framework", + "Security.framework", + "SystemConfiguration.framework", + ] + libs = [ "resolv" ] + configs = [ ":cronet_include_config" ] +} + +_cronet_deps = [ + ":generate_accept_languages", + "//base:base", + "//components/cronet:cronet_buildflags", + "//components/cronet:cronet_common", + "//components/cronet:cronet_version_header", + "//components/cronet/native:cronet_native_impl", + "//components/grpc_support", + "//components/prefs:prefs", + "//ios/net", + "//ios/net:network_protocol", + "//ios/web/common:user_agent", + "//ios/web/init:global_state", + "//ios/web/public/init:global_state", + "//net", + "//url", +] + +_cronet_sources = [ + "Cronet.h", + "Cronet.mm", + "cronet_environment.h", + "cronet_environment.mm", + "cronet_global_state_ios.mm", + "cronet_metrics.h", + "cronet_metrics.mm", +] + +_cronet_public_headers = [ "Cronet.h" ] +_cronet_public_headers += grpc_public_headers +_cronet_public_headers += cronet_native_public_headers + +source_set("cronet_sources") { + deps = _cronet_deps + + sources = _cronet_sources + + include_dirs = [ "//components/grpc_support/include" ] + + if (!use_platform_icu_alternatives) { + deps += [ "//base:i18n" ] + } + + configs += [ + ":disable_deprecated_errors", + "//build/config/compiler:enable_arc", + ] +} + +source_set("cronet_sources_with_global_state") { + deps = [ + "//base", + "//ios/web/init:global_state", + "//ios/web/public/init:global_state", + ] + + public_deps = [ ":cronet_sources" ] + + sources = [ "ios_global_state_configuration.cc" ] +} + +# Tweak |info_plist| with current version and revision. +tweak_info_plist("tweak_cronet_plist") { + info_plist = "Info.plist" +} + +ios_framework_bundle("cronet_framework") { + output_name = "Cronet" + info_plist_target = ":tweak_cronet_plist" + + deps = [ + ":cronet_sources_with_global_state", + "//base", + "//net:net", + ] + + frameworks = [ "UIKit.framework" ] + + public_deps = [ "//components/grpc_support:headers" ] + + public_headers = _cronet_public_headers + + sources = [ "Cronet.h" ] + + configs -= [ "//build/config/compiler:default_symbols" ] + configs += [ "//build/config/compiler:symbols" ] + + public_configs = [ ":cronet_include_config" ] +} + +test("cronet_unittests_ios") { + testonly = true + + sources = [ "../run_all_unittests.cc" ] + + deps = [ + ":cronet_sources_with_global_state", + "//base", + "//base/test:test_support", + "//components/cronet:cronet_common_unittests", + "//components/cronet/native:cronet_native_unittests", + "//net", + "//testing/gtest", + ] + + bundle_deps = [ "//components/cronet/ios/test:cronet_test" ] +} + +action("generate_accept_languages") { + script = "//components/cronet/tools/generate_accept_languages.py" + args = [ + rebase_path("$target_gen_dir"), + rebase_path("//"), + ] + outputs = [ "$target_gen_dir/accept_languages_table.h" ] +} + +# A static library which contains just _cronet_sources. +static_library("cronet_static") { + visibility = [ ":*" ] + deps = _cronet_deps + sources = _cronet_sources + [ "ios_global_state_configuration.cc" ] + public_configs = [ ":cronet_include_config" ] + public_deps = [ "//components/grpc_support" ] + + configs += [ "//build/config/compiler:enable_arc" ] +} + +# A static library which contains all dependencies of :cronet_static. +static_library("cronet_deps_complete") { + visibility = [ ":*" ] + complete_static_lib = true + configs -= [ "//build/config/compiler:thin_archive" ] + deps = [ ":cronet_static" ] + + if (use_custom_libcxx) { + deps += [ + # Add shared_library_deps to include custom libc++ into dependencies. + # They are by default only added to executable(), loadable_module(), and + # shared_library() targets, but cronet_static_complete library needs it as well to + # avoid linking with different versions of libc++. + "//build/config:shared_library_deps", + ] + } +} + +# A static library which contains cronet and all dependendencies hidden inside. +action("cronet_static_complete") { + visibility = [ ":*" ] + script = "//components/cronet/tools/hide_symbols.py" + deps = [ + ":cronet_deps_complete", + ":cronet_static", + ] + outputs = [ "$target_out_dir/$current_cpu/cronet_static_complete.a" ] + args = [ + "--input_libs", + rebase_path("$target_out_dir/libcronet_static.a", root_build_dir), + "--deps_lib", + rebase_path("$target_out_dir/libcronet_deps_complete.a", root_build_dir), + "--output_obj", + rebase_path("$target_out_dir/$current_cpu/cronet_static_complete.o", + root_build_dir), + "--output_lib", + rebase_path("$target_out_dir/$current_cpu/cronet_static_complete.a", + root_build_dir), + "--current_cpu", + current_cpu, + ] + if (use_custom_libcxx) { + args += [ "--use_custom_libcxx" ] + } + + public_configs = [ ":cronet_static_config" ] +} + +# A fat static library which exports cronet public symbols and hides all +# dependendencies. +if (!is_fat_secondary_toolchain) { + lipo_binary("libcronet") { + arch_binary_target = ":cronet_static_complete" + arch_binary_output = "cronet_static_complete.a" + output_name = "libcronet.a" + enable_stripping = false + enable_dsyms = false + } + + template("ios_static_framework") { + _target_name = target_name + _output_name = target_name + if (defined(invoker.output_name)) { + _output_name = invoker.output_name + } + _framework_name = target_name + if (defined(invoker.framework_name)) { + _framework_name = invoker.framework_name + } + + _framework_headers_target = _target_name + "_framework_headers" + bundle_data(_framework_headers_target) { + visibility = [ ":$_target_name" ] + sources = invoker.public_headers + outputs = [ "{{bundle_contents_dir}}/Headers/{{source_file_part}}" ] + } + + _framework_binary_target = _target_name + "_framework_binary" + _static_library_target = invoker.static_library_target + + bundle_data(_framework_binary_target) { + visibility = [ ":$_target_name" ] + sources = get_target_outputs(_static_library_target) + outputs = [ "{{bundle_executable_dir}}/$_framework_name" ] + public_deps = [ _static_library_target ] + } + + create_bundle(_target_name) { + product_type = "com.apple.product-type.framework" + bundle_root_dir = "$root_out_dir/Static/${_output_name}" + bundle_contents_dir = bundle_root_dir + bundle_executable_dir = bundle_contents_dir + bundle_resources_dir = bundle_contents_dir + deps = [ + ":$_framework_binary_target", + ":$_framework_headers_target", + ] + public_configs = invoker.public_configs + } + } + + ios_static_framework("cronet_static_framework") { + output_name = "Cronet.framework" + framework_name = "Cronet" + public_headers = _cronet_public_headers + static_library_target = ":libcronet" + public_configs = [ ":cronet_static_config" ] + } + + _package_dir = "$root_out_dir/cronet" + + action("generate_license") { + _license_path = "$_package_dir/LICENSE" + + script = "//tools/licenses.py" + inputs = [ lastchange_file ] + outputs = [ _license_path ] + args = [ + "license_file", + rebase_path(_license_path, root_build_dir), + "--gn-target", + "//components/cronet/ios:cronet_framework", + "--gn-out-dir", + ".", + "--target-os", + "ios", + ] + } + + copy("cronet_static_copy") { + sources = [ "$root_out_dir/Static/Cronet.framework" ] + outputs = [ "$_package_dir/Static/Cronet.framework" ] + + deps = [ ":cronet_static_framework" ] + } + + copy("cronet_package_copy") { + sources = [ + "$root_out_dir/Cronet.framework", + "//AUTHORS", + "//chrome/VERSION", + ] + outputs = [ "$_package_dir/{{source_file_part}}" ] + + deps = [ + ":cronet_framework", + ":cronet_static_copy", + ] + } + + if (enable_dsyms) { + action("cronet_dsym_archive") { + script = "//chrome/tools/build/mac/archive_symbols.py" + + # These are the dSYMs that will be archived. The sources list must be + # the target outputs that correspond to the dSYMs (since a dSYM is a + # directory it cannot be listed as a source file). The targets that + # generate both the dSYM and binary image are listed in deps. + _dsyms = [ "$root_out_dir/Cronet.dSYM" ] + + sources = [ "$root_out_dir/Cronet.framework" ] + + _output = "$_package_dir/Cronet.dSYM.tar.bz2" + + outputs = [ _output ] + + args = [ rebase_path(_output, root_out_dir) ] + + rebase_path(_dsyms, root_out_dir) + + deps = [ ":cronet_framework" ] + } + } else { + group("cronet_dsym_archive") { + } + } + + group("cronet_package_ios") { + deps = [ + ":cronet_dsym_archive", + ":cronet_package_copy", + ":generate_license", + ] + } +} else { + group("cronet_package_ios") { + public_deps = [ ":cronet_package_ios($primary_fat_toolchain_name)" ] + } +} diff --git a/src/components/cronet/ios/Cronet.h b/src/components/cronet/ios/Cronet.h new file mode 100644 index 0000000000..13bf535213 --- /dev/null +++ b/src/components/cronet/ios/Cronet.h @@ -0,0 +1,210 @@ +// Copyright 2016 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. + +#import + +#include "bidirectional_stream_c.h" +#include "cronet.idl_c.h" +#include "cronet_c.h" +#include "cronet_export.h" + +// Type of HTTP cache; public interface to private implementation defined in +// URLRequestContextConfig class. +typedef NS_ENUM(NSInteger, CRNHttpCacheType) { + // Disabled HTTP cache. Some data may still be temporarily stored in memory. + CRNHttpCacheTypeDisabled, + // Enable on-disk HTTP cache, including HTTP data. + CRNHttpCacheTypeDisk, + // Enable in-memory cache, including HTTP data. + CRNHttpCacheTypeMemory, +}; + +/// Cronet error domain name. +FOUNDATION_EXPORT GRPC_SUPPORT_EXPORT NSString* const CRNCronetErrorDomain; + +/// Enum of Cronet NSError codes. +NS_ENUM(NSInteger){ + CRNErrorInvalidArgument = 1001, CRNErrorUnsupportedConfig = 1002, +}; + +/// The corresponding value is a String object that contains the name of +/// an invalid argument inside the NSError userInfo dictionary. +FOUNDATION_EXPORT GRPC_SUPPORT_EXPORT NSString* const CRNInvalidArgumentKey; + +// A block, that takes a request, and returns YES if the request should +// be handled. +typedef BOOL (^RequestFilterBlock)(NSURLRequest* request); + +// Interface for installing Cronet. +// TODO(gcasto): Should this macro be separate from the one defined in +// bidirectional_stream_c.h? +GRPC_SUPPORT_EXPORT +@interface Cronet : NSObject + +// Sets the HTTP Accept-Language header. This method only has any effect before +// |start| is called. ++ (void)setAcceptLanguages:(NSString*)acceptLanguages; + +// Sets whether HTTP/2 should be supported by CronetEngine. This method only has +// any effect before |start| is called. ++ (void)setHttp2Enabled:(BOOL)http2Enabled; + +// Sets whether QUIC should be supported by CronetEngine. This method only has +// any effect before |start| is called. ++ (void)setQuicEnabled:(BOOL)quicEnabled; + +// Sets whether Brotli should be supported by CronetEngine. This method only has +// any effect before |start| is called. ++ (void)setBrotliEnabled:(BOOL)brotliEnabled; + +// Sets whether Metrics should be collected by CronetEngine. This method only +// has any effect before |start| is called. ++ (void)setMetricsEnabled:(BOOL)metricsEnabled; + +// Set HTTP Cache type to be used by CronetEngine. This method only has any +// effect before |start| is called. See HttpCacheType enum for available +// options. ++ (void)setHttpCacheType:(CRNHttpCacheType)httpCacheType; + +// Adds hint that host supports QUIC on altPort. This method only has any effect +// before |start| is called. Returns NO if it fails to add hint (because the +// host is invalid). ++ (BOOL)addQuicHint:(NSString*)host port:(int)port altPort:(int)altPort; + +// Set experimental Cronet options. Argument is a JSON string; see +// |URLRequestContextConfig| for more details. This method only has +// any effect before |start| is called. ++ (void)setExperimentalOptions:(NSString*)experimentalOptions; + +// Sets the User-Agent request header string to be sent with all requests. +// If |partial| is set to YES, then actual user agent value is based on device +// model, OS version, and |userAgent| argument. For example "Foo/3.0.0.0" is +// sent as "Mozilla/5.0 (iPhone; CPU iPhone OS 9_3 like Mac OS X) +// AppleWebKit/601.1 (KHTML, like Gecko) Foo/3.0.0.0 Mobile/15G31 +// Safari/601.1.46". +// If |partial| is set to NO, then |userAgent| value is complete value sent to +// the remote. For Example: "Foo/3.0.0.0" is sent as "Foo/3.0.0.0". +// +// This method only has any effect before |start| is called. ++ (void)setUserAgent:(NSString*)userAgent partial:(BOOL)partial; + +// Sets SSLKEYLogFileName to export SSL key for Wireshark decryption of packet +// captures. This method only has any effect before |start| is called. ++ (void)setSslKeyLogFileName:(NSString*)sslKeyLogFileName; + +/// Pins a set of public keys for a given host. This method only has any effect +/// before |start| is called. By pinning a set of public keys, |pinHashes|, +/// communication with |host| is required to authenticate with a certificate +/// with a public key from the set of pinned ones. +/// An app can pin the public key of the root certificate, any of the +/// intermediate certificates or the end-entry certificate. Authentication will +/// fail and secure communication will not be established if none of the public +/// keys is present in the host's certificate chain, even if the host attempts +/// to authenticate with a certificate allowed by the device's trusted store of +/// certificates. +/// +/// Calling this method multiple times with the same host name overrides the +/// previously set pins for the host. +/// +/// More information about the public key pinning can be found in +/// [RFC 7469](https://tools.ietf.org/html/rfc7469). +/// +/// @param host name of the host to which the public keys should be pinned. +/// A host that consists only of digits and the dot character +/// is treated as invalid. +/// @param pinHashes a set of pins. Each pin is the SHA-256 cryptographic +/// hash of the DER-encoded ASN.1 representation of the +/// Subject Public Key Info (SPKI) of the host's X.509 +/// certificate. Although, the method does not mandate the +/// presence of the backup pin that can be used if the control +/// of the primary private key has been lost, it is highly +/// recommended to supply one. +/// @param includeSubdomains indicates whether the pinning policy should be +/// applied to subdomains of |host|. +/// @param expirationDate specifies the expiration date for the pins. +/// @param outError on return, if the pin cannot be added, a pointer to an +/// error object that encapsulates the reason for the error. +/// @return returns |YES| if the pins were added successfully; |NO|, otherwise. ++ (BOOL)addPublicKeyPinsForHost:(NSString*)host + pinHashes:(NSSet*)pinHashes + includeSubdomains:(BOOL)includeSubdomains + expirationDate:(NSDate*)expirationDate + error:(NSError**)outError; + +// Sets the block used to determine whether or not Cronet should handle the +// request. If the block is not set, Cronet will handle all requests. Cronet +// retains strong reference to the block, which can be released by calling this +// method with nil block. ++ (void)setRequestFilterBlock:(RequestFilterBlock)block; + +// Starts CronetEngine. It is recommended to call this method on the application +// main thread. If the method is called on any thread other than the main one, +// the method will internally try to execute synchronously using the main GCD +// queue. Please make sure that the main thread is not blocked by a job +// that calls this method; otherwise, a deadlock can occur. ++ (void)start; + +// Registers Cronet as HttpProtocol Handler. Once registered, Cronet intercepts +// and handles all requests made through NSURLConnection and shared +// NSURLSession. +// This method must be called after |start|. ++ (void)registerHttpProtocolHandler; + +// Unregister Cronet as HttpProtocol Handler. This means that Cronet will stop +// intercepting requests, however, it won't tear down the Cronet environment. +// This method must be called after |start|. ++ (void)unregisterHttpProtocolHandler; + +// Installs Cronet into NSURLSessionConfiguration so that all +// NSURLSessions created with this configuration will use the Cronet stack. +// Note that all Cronet settings are global and are shared between +// all NSURLSessions & NSURLConnections that use the Cronet stack. +// This method must be called after |start|. ++ (void)installIntoSessionConfiguration:(NSURLSessionConfiguration*)config; + +// Returns the absolute path that startNetLogToFile:fileName will actually +// write to. ++ (NSString*)getNetLogPathForFile:(NSString*)fileName; + +// Starts net-internals logging to a file named |fileName|. Where fileName is +// relative to the application documents directory. |fileName| must not be +// empty. Log level is determined by |logBytes| - if YES then LOG_ALL otherwise +// LOG_ALL_BUT_BYTES. If the file exists it is truncated before starting. If +// actively logging the call is ignored. ++ (BOOL)startNetLogToFile:(NSString*)fileName logBytes:(BOOL)logBytes; + +// Stop net-internals logging and flush file to disk. If a logging session is +// not in progress this call is ignored. ++ (void)stopNetLog; + +// Returns the full user-agent that will be used unless it is overridden on the +// NSURLRequest used. ++ (NSString*)getUserAgent; + +// Sets priority of the network thread. The |priority| should be a +// floating point number between 0.0 to 1.0, where 1.0 is highest priority. +// This method can be called multiple times before or after |start| method. ++ (void)setNetworkThreadPriority:(double)priority; + +// Get a pointer to global instance of cronet_engine for GRPC C API. ++ (stream_engine*)getGlobalEngine; + +// Returns differences in metrics collected by Cronet since the last call to +// getGlobalMetricsDeltas, serialized as a [protobuf] +// (https://developers.google.com/protocol-buffers). +// +// Cronet starts collecting these metrics after the first call to +// getGlobalMetricsDeltras, so the first call returns no +// useful data as no metrics have yet been collected. ++ (NSData*)getGlobalMetricsDeltas; + +// Sets Host Resolver Rules for testing. +// This method must be called after |start| has been called. ++ (void)setHostResolverRulesForTesting:(NSString*)hostResolverRulesForTesting; + +// Enables TestCertVerifier which accepts all certificates for testing. +// This method only has any effect before |start| is called. ++ (void)enableTestCertVerifierForTesting; + +@end diff --git a/src/components/cronet/ios/Cronet.mm b/src/components/cronet/ios/Cronet.mm new file mode 100644 index 0000000000..5708902f6e --- /dev/null +++ b/src/components/cronet/ios/Cronet.mm @@ -0,0 +1,571 @@ +// Copyright 2016 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. + +#import "components/cronet/ios/Cronet.h" + +#include +#include + +#include "base/lazy_instance.h" +#include "base/logging.h" +#include "base/mac/bundle_locations.h" +#include "base/strings/sys_string_conversions.h" +#include "base/synchronization/lock.h" +#include "components/cronet/cronet_global_state.h" +#include "components/cronet/ios/accept_languages_table.h" +#include "components/cronet/ios/cronet_environment.h" +#include "components/cronet/ios/cronet_metrics.h" +#include "components/cronet/native/url_request.h" +#include "components/cronet/url_request_context_config.h" +#include "ios/net/crn_http_protocol_handler.h" +#include "ios/net/empty_nsurlcache.h" +#include "net/base/url_util.h" +#include "net/cert/cert_verifier.h" +#include "net/url_request/url_request_context_getter.h" + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +// Cronet NSError constants. +NSString* const CRNCronetErrorDomain = @"CRNCronetErrorDomain"; +NSString* const CRNInvalidArgumentKey = @"CRNInvalidArgumentKey"; + +namespace { + +class CronetHttpProtocolHandlerDelegate; + +using QuicHintVector = + std::vector>; +// Currently there is one and only one instance of CronetEnvironment, +// which is leaked at the shutdown. We should consider allowing multiple +// instances if that makes sense in the future. +base::LazyInstance>::Leaky + gChromeNet = LAZY_INSTANCE_INITIALIZER; + +base::LazyInstance>::Leaky + gHttpProtocolHandlerDelegate = LAZY_INSTANCE_INITIALIZER; + +base::LazyInstance>::Leaky + gMetricsDelegate = LAZY_INSTANCE_INITIALIZER; + +// See [Cronet initialize] method to set the default values of the global +// variables. +BOOL gHttp2Enabled; +BOOL gQuicEnabled; +BOOL gBrotliEnabled; +BOOL gMetricsEnabled; +cronet::URLRequestContextConfig::HttpCacheType gHttpCache; +QuicHintVector gQuicHints; +NSString* gExperimentalOptions; +NSString* gUserAgent; +BOOL gUserAgentPartial; +double gNetworkThreadPriority; +NSString* gSslKeyLogFileName; +std::vector> gPkpList; +RequestFilterBlock gRequestFilterBlock; +NSURLCache* gPreservedSharedURLCache; +BOOL gEnableTestCertVerifierForTesting; +std::unique_ptr gMockCertVerifier; +NSString* gAcceptLanguages; +BOOL gEnablePKPBypassForLocalTrustAnchors; +dispatch_once_t gSwizzleOnceToken; + +// CertVerifier, which allows any certificates for testing. +class TestCertVerifier : public net::CertVerifier { + int Verify(const RequestParams& params, + net::CertVerifyResult* verify_result, + net::CompletionOnceCallback callback, + std::unique_ptr* out_req, + const net::NetLogWithSource& net_log) override { + verify_result->Reset(); + verify_result->verified_cert = params.certificate(); + verify_result->is_issued_by_known_root = true; + return net::OK; + } + void SetConfig(const Config& config) override {} +}; + +// net::HTTPProtocolHandlerDelegate for Cronet. +class CronetHttpProtocolHandlerDelegate + : public net::HTTPProtocolHandlerDelegate { + public: + CronetHttpProtocolHandlerDelegate(net::URLRequestContextGetter* getter, + RequestFilterBlock filter) + : getter_(getter), filter_(filter) {} + + void SetRequestFilterBlock(RequestFilterBlock filter) { + base::AutoLock auto_lock(lock_); + filter_ = filter; + } + + private: + // net::HTTPProtocolHandlerDelegate implementation: + bool CanHandleRequest(NSURLRequest* request) override { + base::AutoLock auto_lock(lock_); + if (!IsRequestSupported(request)) + return false; + if (filter_) + return filter_(request); + return true; + } + + bool IsRequestSupported(NSURLRequest* request) override { + NSString* scheme = [[request URL] scheme]; + if (!scheme) + return false; + return [scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || + [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame; + } + + net::URLRequestContextGetter* GetDefaultURLRequestContext() override { + return getter_.get(); + } + + scoped_refptr getter_; + __strong RequestFilterBlock filter_; + base::Lock lock_; +}; + +} // namespace + +@implementation Cronet + ++ (void)configureCronetEnvironmentForTesting: + (cronet::CronetEnvironment*)cronetEnvironment { + if (gEnableTestCertVerifierForTesting) { + std::unique_ptr test_cert_verifier = + std::make_unique(); + cronetEnvironment->set_mock_cert_verifier(std::move(test_cert_verifier)); + } + if (gMockCertVerifier) { + gChromeNet.Get()->set_mock_cert_verifier(std::move(gMockCertVerifier)); + } +} + ++ (NSString*)getAcceptLanguagesFromPreferredLanguages: + (NSArray*)languages { + NSMutableArray* acceptLanguages = [NSMutableArray new]; + for (NSString* lang_region in languages) { + NSString* lang = [lang_region componentsSeparatedByString:@"-"][0]; + NSString* localeAcceptLangs = acceptLangs[lang_region] ?: acceptLangs[lang]; + if (localeAcceptLangs) + [acceptLanguages + addObjectsFromArray:[localeAcceptLangs + componentsSeparatedByString:@","]]; + } + + NSString* acceptLanguageString = + [[[NSOrderedSet orderedSetWithArray:acceptLanguages] array] + componentsJoinedByString:@","]; + + return [acceptLanguageString length] != 0 ? acceptLanguageString + : @"en-US,en"; +} + ++ (NSString*)getAcceptLanguages { + return [self + getAcceptLanguagesFromPreferredLanguages:[NSLocale preferredLanguages]]; +} + ++ (void)setAcceptLanguages:(NSString*)acceptLanguages { + [self checkNotStarted]; + gAcceptLanguages = acceptLanguages; +} + +// TODO(lilyhoughton) this should either be removed, or made more sophisticated ++ (void)checkNotStarted { + CHECK(!gChromeNet.Get()) << "Cronet is already started."; +} + ++ (void)setHttp2Enabled:(BOOL)http2Enabled { + [self checkNotStarted]; + gHttp2Enabled = http2Enabled; +} + ++ (void)setQuicEnabled:(BOOL)quicEnabled { + [self checkNotStarted]; + gQuicEnabled = quicEnabled; +} + ++ (void)setBrotliEnabled:(BOOL)brotliEnabled { + [self checkNotStarted]; + gBrotliEnabled = brotliEnabled; +} + ++ (void)setMetricsEnabled:(BOOL)metricsEnabled { + // https://crbug.com/878589 + // Don't collect NSURLSessionTaskMetrics until iOS 10.2 to avoid crash in iOS. + if (@available(iOS 10.2, *)) { + [self checkNotStarted]; + gMetricsEnabled = metricsEnabled; + } +} + ++ (BOOL)addQuicHint:(NSString*)host port:(int)port altPort:(int)altPort { + [self checkNotStarted]; + + std::string quic_host = base::SysNSStringToUTF8(host); + + url::CanonHostInfo host_info; + std::string canon_host(net::CanonicalizeHost(quic_host, &host_info)); + if (!host_info.IsIPAddress() && + !net::IsCanonicalizedHostCompliant(canon_host)) { + LOG(ERROR) << "Invalid QUIC hint host: " << quic_host; + return NO; + } + + gQuicHints.push_back( + std::make_unique( + quic_host, port, altPort)); + + return YES; +} + ++ (void)setExperimentalOptions:(NSString*)experimentalOptions { + [self checkNotStarted]; + gExperimentalOptions = experimentalOptions; +} + ++ (void)setUserAgent:(NSString*)userAgent partial:(BOOL)partial { + [self checkNotStarted]; + gUserAgent = userAgent; + gUserAgentPartial = partial; +} + ++ (void)setSslKeyLogFileName:(NSString*)sslKeyLogFileName { + [self checkNotStarted]; + gSslKeyLogFileName = [self getNetLogPathForFile:sslKeyLogFileName]; +} + ++ (void)setHttpCacheType:(CRNHttpCacheType)httpCacheType { + [self checkNotStarted]; + switch (httpCacheType) { + case CRNHttpCacheTypeDisabled: + gHttpCache = cronet::URLRequestContextConfig::HttpCacheType::DISABLED; + break; + case CRNHttpCacheTypeDisk: + gHttpCache = cronet::URLRequestContextConfig::HttpCacheType::DISK; + break; + case CRNHttpCacheTypeMemory: + gHttpCache = cronet::URLRequestContextConfig::HttpCacheType::MEMORY; + break; + default: + DCHECK(NO) << "Invalid HTTP cache type: " << httpCacheType; + } +} + ++ (void)setRequestFilterBlock:(RequestFilterBlock)block { + if (gHttpProtocolHandlerDelegate.Get().get()) + gHttpProtocolHandlerDelegate.Get().get()->SetRequestFilterBlock(block); + else + gRequestFilterBlock = block; +} + ++ (BOOL)addPublicKeyPinsForHost:(NSString*)host + pinHashes:(NSSet*)pinHashes + includeSubdomains:(BOOL)includeSubdomains + expirationDate:(NSDate*)expirationDate + error:(NSError**)outError { + [self checkNotStarted]; + + // Pinning a key only makes sense if pin bypassing has been disabled + if (gEnablePKPBypassForLocalTrustAnchors) { + if (outError != nil) { + *outError = + [self createUnsupportedConfigurationError: + @"Cannot pin keys while public key pinning is bypassed"]; + } + return NO; + } + + auto pkp = std::make_unique( + base::SysNSStringToUTF8(host), includeSubdomains, + base::Time::FromCFAbsoluteTime( + [expirationDate timeIntervalSinceReferenceDate])); + + for (NSData* hash in pinHashes) { + net::SHA256HashValue hashValue = net::SHA256HashValue(); + if (sizeof(hashValue.data) != hash.length) { + *outError = + [self createIllegalArgumentErrorWithArgument:@"pinHashes" + reason: + @"The length of PKP SHA256 " + @"hash should be 256 bits"]; + return NO; + } + memcpy((void*)(hashValue.data), [hash bytes], sizeof(hashValue.data)); + pkp->pin_hashes.push_back(net::HashValue(hashValue)); + } + gPkpList.push_back(std::move(pkp)); + if (outError) { + *outError = nil; + } + return YES; +} + ++ (void)setEnablePublicKeyPinningBypassForLocalTrustAnchors:(BOOL)enable { + gEnablePKPBypassForLocalTrustAnchors = enable; +} + ++ (base::SingleThreadTaskRunner*)getFileThreadRunnerForTesting { + return gChromeNet.Get()->GetFileThreadRunnerForTesting(); +} + ++ (base::SingleThreadTaskRunner*)getNetworkThreadRunnerForTesting { + return gChromeNet.Get()->GetNetworkThreadRunnerForTesting(); +} + ++ (void)startInternal { + std::string user_agent = base::SysNSStringToUTF8(gUserAgent); + + gChromeNet.Get().reset( + new cronet::CronetEnvironment(user_agent, gUserAgentPartial)); + + gChromeNet.Get()->set_accept_language( + base::SysNSStringToUTF8(gAcceptLanguages ?: [self getAcceptLanguages])); + + gChromeNet.Get()->set_http2_enabled(gHttp2Enabled); + gChromeNet.Get()->set_quic_enabled(gQuicEnabled); + gChromeNet.Get()->set_brotli_enabled(gBrotliEnabled); + gChromeNet.Get()->set_experimental_options( + base::SysNSStringToUTF8(gExperimentalOptions)); + gChromeNet.Get()->set_http_cache(gHttpCache); + gChromeNet.Get()->set_ssl_key_log_file_name( + base::SysNSStringToUTF8(gSslKeyLogFileName)); + gChromeNet.Get()->set_pkp_list(std::move(gPkpList)); + gChromeNet.Get() + ->set_enable_public_key_pinning_bypass_for_local_trust_anchors( + gEnablePKPBypassForLocalTrustAnchors); + if (gNetworkThreadPriority != + cronet::CronetEnvironment::kKeepDefaultThreadPriority) { + gChromeNet.Get()->SetNetworkThreadPriority(gNetworkThreadPriority); + } + for (const auto& quicHint : gQuicHints) { + gChromeNet.Get()->AddQuicHint(quicHint->host, quicHint->port, + quicHint->alternate_port); + } + + [self configureCronetEnvironmentForTesting:gChromeNet.Get().get()]; + gChromeNet.Get()->Start(); + gHttpProtocolHandlerDelegate.Get().reset( + new CronetHttpProtocolHandlerDelegate( + gChromeNet.Get()->GetURLRequestContextGetter(), gRequestFilterBlock)); + net::HTTPProtocolHandlerDelegate::SetInstance( + gHttpProtocolHandlerDelegate.Get().get()); + + if (gMetricsEnabled) { + gMetricsDelegate.Get().reset(new cronet::CronetMetricsDelegate()); + net::MetricsDelegate::SetInstance(gMetricsDelegate.Get().get()); + + dispatch_once(&gSwizzleOnceToken, ^{ + cronet::SwizzleSessionWithConfiguration(); + }); + } else { + net::MetricsDelegate::SetInstance(nullptr); + } + + gRequestFilterBlock = nil; +} + ++ (void)start { + cronet::EnsureInitialized(); + [self startInternal]; +} + ++ (void)unswizzleForTesting { + if (gSwizzleOnceToken) + cronet::SwizzleSessionWithConfiguration(); + gSwizzleOnceToken = 0; +} + ++ (void)shutdownForTesting { + [Cronet unswizzleForTesting]; + [Cronet initialize]; +} + ++ (void)registerHttpProtocolHandler { + if (gPreservedSharedURLCache == nil) { + gPreservedSharedURLCache = [NSURLCache sharedURLCache]; + } + // Disable the default cache. + [NSURLCache setSharedURLCache:[EmptyNSURLCache emptyNSURLCache]]; + // Register the chrome http protocol handler to replace the default one. + BOOL success = + [NSURLProtocol registerClass:[CRNHTTPProtocolHandler class]]; + DCHECK(success); +} + ++ (void)unregisterHttpProtocolHandler { + // Set up SharedURLCache preserved in registerHttpProtocolHandler. + if (gPreservedSharedURLCache != nil) { + [NSURLCache setSharedURLCache:gPreservedSharedURLCache]; + gPreservedSharedURLCache = nil; + } + [NSURLProtocol unregisterClass:[CRNHTTPProtocolHandler class]]; +} + ++ (void)installIntoSessionConfiguration:(NSURLSessionConfiguration*)config { + config.protocolClasses = @[ [CRNHTTPProtocolHandler class] ]; +} + ++ (NSString*)getNetLogPathForFile:(NSString*)fileName { + return [[[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory + inDomains:NSUserDomainMask] + lastObject] URLByAppendingPathComponent:fileName] path]; +} + ++ (BOOL)startNetLogToFile:(NSString*)fileName logBytes:(BOOL)logBytes { + if (gChromeNet.Get().get() && [fileName length] && + ![fileName isAbsolutePath]) { + return gChromeNet.Get()->StartNetLog( + base::SysNSStringToUTF8([self getNetLogPathForFile:fileName]), + logBytes); + } + + return NO; +} + ++ (void)stopNetLog { + if (gChromeNet.Get().get()) { + gChromeNet.Get()->StopNetLog(); + } +} + ++ (NSString*)getUserAgent { + if (!gChromeNet.Get().get()) { + return nil; + } + + return [NSString stringWithCString:gChromeNet.Get()->user_agent().c_str() + encoding:[NSString defaultCStringEncoding]]; +} + ++ (void)setNetworkThreadPriority:(double)priority { + gNetworkThreadPriority = priority; + if (gChromeNet.Get()) { + gChromeNet.Get()->SetNetworkThreadPriority(priority); + }; +} + ++ (stream_engine*)getGlobalEngine { + DCHECK(gChromeNet.Get().get()); + if (gChromeNet.Get().get()) { + static stream_engine engine; + engine.obj = gChromeNet.Get()->GetURLRequestContextGetter(); + return &engine; + } + return nil; +} + ++ (NSData*)getGlobalMetricsDeltas { + if (!gChromeNet.Get().get()) { + return nil; + } + std::vector deltas(gChromeNet.Get()->GetHistogramDeltas()); + return [NSData dataWithBytes:deltas.data() length:deltas.size()]; +} + ++ (void)enableTestCertVerifierForTesting { + gEnableTestCertVerifierForTesting = YES; +} + ++ (void)setMockCertVerifierForTesting: + (std::unique_ptr)certVerifier { + gMockCertVerifier = std::move(certVerifier); +} + ++ (void)setHostResolverRulesForTesting:(NSString*)hostResolverRulesForTesting { + DCHECK(gChromeNet.Get().get()); + gChromeNet.Get()->SetHostResolverRules( + base::SysNSStringToUTF8(hostResolverRulesForTesting)); +} + +// This is a private dummy method that prevents the linker from stripping out +// the otherwise unreferenced methods from 'bidirectional_stream.cc'. ++ (void)preventStrippingCronetBidirectionalStream { + bidirectional_stream_create(NULL, 0, 0); +} + +// This is a private dummy method that prevents the linker from stripping out +// the otherwise unreferenced modules from 'native'. ++ (void)preventStrippingNativeCronetModules { + Cronet_Buffer_Create(); + Cronet_Engine_Create(); + Cronet_UrlRequest_Create(); +} + ++ (NSError*)createIllegalArgumentErrorWithArgument:(NSString*)argumentName + reason:(NSString*)reason { + NSMutableDictionary* errorDictionary = + [[NSMutableDictionary alloc] initWithDictionary:@{ + NSLocalizedDescriptionKey : + [NSString stringWithFormat:@"Invalid argument: %@", argumentName], + CRNInvalidArgumentKey : argumentName + }]; + if (reason) { + errorDictionary[NSLocalizedFailureReasonErrorKey] = reason; + } + return [self createCronetErrorWithCode:CRNErrorInvalidArgument + userInfo:errorDictionary]; +} + ++ (NSError*)createUnsupportedConfigurationError:(NSString*)contradiction { + NSMutableDictionary* errorDictionary = + [[NSMutableDictionary alloc] initWithDictionary:@{ + NSLocalizedDescriptionKey : @"Unsupported configuration", + NSLocalizedRecoverySuggestionErrorKey : + @"Try disabling Public Key Pinning Bypass before pinning keys.", + NSLocalizedFailureReasonErrorKey : @"Pinning public keys while local " + @"anchor bypass is enabled is " + @"currently not supported.", + }]; + if (contradiction) { + errorDictionary[NSLocalizedFailureReasonErrorKey] = contradiction; + } + + return [self createCronetErrorWithCode:CRNErrorUnsupportedConfig + userInfo:errorDictionary]; +} + ++ (NSError*)createCronetErrorWithCode:(int)errorCode + userInfo:(NSDictionary*)userInfo { + return [NSError errorWithDomain:CRNCronetErrorDomain + code:errorCode + userInfo:userInfo]; +} + +// Used by tests to query the size of the map that contains metrics for +// individual NSURLSession tasks. ++ (size_t)getMetricsMapSize { + return cronet::CronetMetricsDelegate::GetMetricsMapSize(); +} + +// Static class initializer. ++ (void)initialize { + gChromeNet.Get().reset(); + gHttp2Enabled = YES; + gQuicEnabled = NO; + gBrotliEnabled = NO; + gMetricsEnabled = NO; + gHttpCache = cronet::URLRequestContextConfig::HttpCacheType::DISK; + gQuicHints.clear(); + gExperimentalOptions = @"{}"; + gUserAgent = nil; + gUserAgentPartial = NO; + gNetworkThreadPriority = + cronet::CronetEnvironment::kKeepDefaultThreadPriority; + gSslKeyLogFileName = nil; + gPkpList.clear(); + gRequestFilterBlock = nil; + gHttpProtocolHandlerDelegate.Get().reset(nullptr); + gMetricsDelegate.Get().reset(nullptr); + gPreservedSharedURLCache = nil; + gEnableTestCertVerifierForTesting = NO; + gMockCertVerifier.reset(nullptr); + gAcceptLanguages = nil; + gEnablePKPBypassForLocalTrustAnchors = YES; +} + +@end diff --git a/src/components/cronet/ios/DEPS b/src/components/cronet/ios/DEPS new file mode 100644 index 0000000000..07c67f3f15 --- /dev/null +++ b/src/components/cronet/ios/DEPS @@ -0,0 +1,5 @@ +include_rules = [ + "+ios/net", + "+ios/web/common", + "+ios/web/public", +] diff --git a/src/components/cronet/ios/Info.plist b/src/components/cronet/ios/Info.plist new file mode 100644 index 0000000000..3b48c36b96 --- /dev/null +++ b/src/components/cronet/ios/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + Cronet + CFBundleIdentifier + org.chromium.net.Cronet + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Cronet + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + NSHumanReadableCopyright + Copyright 2016 The Chromium Authors. All rights reserved. + NSPrincipalClass + + + \ No newline at end of file diff --git a/src/components/cronet/ios/cronet_consumer/BUILD.gn b/src/components/cronet/ios/cronet_consumer/BUILD.gn new file mode 100644 index 0000000000..9ecafda30c --- /dev/null +++ b/src/components/cronet/ios/cronet_consumer/BUILD.gn @@ -0,0 +1,49 @@ +# Copyright 2016 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. + +import("//build/config/ios/rules.gni") +import("//ios/features.gni") + +template("cronet_consumer_template") { + _target_name = target_name + + ios_app_bundle(_target_name) { + info_plist = "cronet-consumer-Info.plist" + + deps = [ "//base:base" ] + + deps += invoker.deps + + sources = [ + "cronet_consumer_app_delegate.h", + "cronet_consumer_app_delegate.mm", + "cronet_consumer_view_controller.h", + "cronet_consumer_view_controller.m", + "main.mm", + ] + + forward_variables_from(invoker, + [ + "bundle_deps", + "framework_dirs", + ]) + + configs += [ "//build/config/compiler:enable_arc" ] + } +} + +cronet_consumer_template("cronet_consumer") { + deps = [ "//components/cronet/ios:cronet_framework+link" ] + bundle_deps = [ "//components/cronet/ios:cronet_framework+bundle" ] +} + +# TODO(mef): Building "cronet_consumer_static" app with additional_target_cpus +# causes "cronet_static_framework" to build lipo_binary("libcronet") for +# duplicate architecture (e.g. arm64+arm64) and breaks the build. +if (!defined(additional_target_cpus) || additional_target_cpus == []) { + cronet_consumer_template("cronet_consumer_static") { + deps = [ "//components/cronet/ios:cronet_static_framework" ] + framework_dirs = [ "$root_out_dir/Static" ] + } +} diff --git a/src/components/cronet/ios/cronet_consumer/Default.png b/src/components/cronet/ios/cronet_consumer/Default.png new file mode 100644 index 0000000000..4c8ca6f693 Binary files /dev/null and b/src/components/cronet/ios/cronet_consumer/Default.png differ diff --git a/src/components/cronet/ios/cronet_consumer/cronet-consumer-Info.plist b/src/components/cronet/ios/cronet_consumer/cronet-consumer-Info.plist new file mode 100644 index 0000000000..57ee6ae8a4 --- /dev/null +++ b/src/components/cronet/ios/cronet_consumer/cronet-consumer-Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + chromium.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/src/components/cronet/ios/cronet_consumer/cronet_consumer_app_delegate.h b/src/components/cronet/ios/cronet_consumer/cronet_consumer_app_delegate.h new file mode 100644 index 0000000000..08e1cea237 --- /dev/null +++ b/src/components/cronet/ios/cronet_consumer/cronet_consumer_app_delegate.h @@ -0,0 +1,20 @@ +// Copyright 2014 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_IOS_CRONET_CONSUMER_CRONET_CONSUMER_APP_DELEGATE_H_ +#define COMPONENTS_CRONET_IOS_CRONET_CONSUMER_CRONET_CONSUMER_APP_DELEGATE_H_ + +#import + +@class CronetConsumerViewController; + +// The main app controller and UIApplicationDelegate. +@interface CronetConsumerAppDelegate : UIResponder + +@property(strong, nonatomic) UIWindow* window; +@property(strong, nonatomic) CronetConsumerViewController* viewController; + +@end + +#endif // COMPONENTS_CRONET_IOS_CRONET_CONSUMER_CRONET_CONSUMER_APP_DELEGATE_H_ diff --git a/src/components/cronet/ios/cronet_consumer/cronet_consumer_app_delegate.mm b/src/components/cronet/ios/cronet_consumer/cronet_consumer_app_delegate.mm new file mode 100644 index 0000000000..94002fce24 --- /dev/null +++ b/src/components/cronet/ios/cronet_consumer/cronet_consumer_app_delegate.mm @@ -0,0 +1,53 @@ +// Copyright 2014 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. + +#import "cronet_consumer_app_delegate.h" + +#import + +#include "base/format_macros.h" +#import "cronet_consumer_view_controller.h" + +@implementation CronetConsumerAppDelegate { + NSUInteger _counter; +} + +@synthesize window; +@synthesize viewController; + +// Returns a file name to save net internals logging. This method suffixes +// the ivar |_counter| to the file name so a new name can be obtained by +// modifying that. +- (NSString*)currentNetLogFileName { + return [NSString + stringWithFormat:@"cronet-consumer-net-log%" PRIuNS ".json", _counter]; +} + +- (BOOL)application:(UIApplication*)application + didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { + [Cronet setUserAgent:@"Dummy/1.0" partial:YES]; + [Cronet setQuicEnabled:YES]; + [Cronet start]; + [Cronet startNetLogToFile:[self currentNetLogFileName] logBytes:NO]; + + [Cronet registerHttpProtocolHandler]; + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.viewController = + [[CronetConsumerViewController alloc] initWithNibName:nil bundle:nil]; + self.window.rootViewController = self.viewController; + [self.window makeKeyAndVisible]; + + return YES; +} + +- (void)applicationDidEnterBackground:(UIApplication*)application { + [Cronet stopNetLog]; +} + +- (void)applicationWillEnterForeground:(UIApplication*)application { + _counter++; + [Cronet startNetLogToFile:[self currentNetLogFileName] logBytes:NO]; +} + +@end diff --git a/src/components/cronet/ios/cronet_consumer/cronet_consumer_view_controller.h b/src/components/cronet/ios/cronet_consumer/cronet_consumer_view_controller.h new file mode 100644 index 0000000000..7d7f1f3e26 --- /dev/null +++ b/src/components/cronet/ios/cronet_consumer/cronet_consumer_view_controller.h @@ -0,0 +1,14 @@ +// Copyright 2014 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_IOS_CRONET_CONSUMER_CRONET_CONSUMER_VIEW_CONTROLLER_H_ +#define COMPONENTS_CRONET_IOS_CRONET_CONSUMER_CRONET_CONSUMER_VIEW_CONTROLLER_H_ + +#import +#import + +@interface CronetConsumerViewController : UIViewController +@end + +#endif // COMPONENTS_CRONET_IOS_CRONET_CONSUMER_CRONET_CONSUMER_VIEW_CONTROLLER_H_ diff --git a/src/components/cronet/ios/cronet_consumer/cronet_consumer_view_controller.m b/src/components/cronet/ios/cronet_consumer/cronet_consumer_view_controller.m new file mode 100644 index 0000000000..d8a971007b --- /dev/null +++ b/src/components/cronet/ios/cronet_consumer/cronet_consumer_view_controller.m @@ -0,0 +1,48 @@ +// Copyright 2014 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. + +#import "cronet_consumer_view_controller.h" + +#import + +@implementation CronetConsumerViewController +#if !defined(__IPHONE_12_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_12_0 +{ + UIWebView* _webView; +} + +- (void)viewDidLoad { + self.view.backgroundColor = [UIColor whiteColor]; + + UIButton* button = [UIButton buttonWithType:UIButtonTypeSystem]; + [button setTitle:@"chromium.org" forState:UIControlStateNormal]; + [button setFrame:CGRectMake(5, 0, 95, 50)]; + [button addTarget:self + action:@selector(loadChromium) + forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:button]; + + _webView = [[UIWebView alloc] + initWithFrame:CGRectMake(0, 52, self.view.bounds.size.width, + self.view.bounds.size.height - 52)]; + [self.view addSubview:_webView]; + _webView.autoresizingMask = + UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + [self loadChromium]; +} + +// Disable the status bar to sidestep all the iOS7 status bar issues. +- (BOOL)prefersStatusBarHidden { + return YES; +} + +- (void)loadChromium { + [_webView + loadRequest:[NSURLRequest + requestWithURL: + [NSURL URLWithString:@"https://www.chromium.org"]]]; +} +#endif +@end diff --git a/src/components/cronet/ios/cronet_consumer/main.mm b/src/components/cronet/ios/cronet_consumer/main.mm new file mode 100644 index 0000000000..fa38b78c62 --- /dev/null +++ b/src/components/cronet/ios/cronet_consumer/main.mm @@ -0,0 +1,15 @@ +// Copyright 2014 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. + +#import +#import + +#import "cronet_consumer_app_delegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain( + argc, argv, nil, NSStringFromClass([CronetConsumerAppDelegate class])); + } +} diff --git a/src/components/cronet/ios/cronet_environment.h b/src/components/cronet/ios/cronet_environment.h new file mode 100644 index 0000000000..193dabd7a1 --- /dev/null +++ b/src/components/cronet/ios/cronet_environment.h @@ -0,0 +1,217 @@ +// Copyright 2016 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_IOS_CRONET_ENVIRONMENT_H_ +#define COMPONENTS_CRONET_IOS_CRONET_ENVIRONMENT_H_ + +#include +#include +#include +#include +#include + +#include "base/files/file_path.h" +#include "base/files/scoped_file.h" +#include "base/strings/sys_string_conversions.h" +#include "base/synchronization/waitable_event.h" +#include "base/threading/thread.h" +#include "components/cronet/url_request_context_config.h" +#include "components/cronet/version.h" +#include "net/cert/cert_verifier.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_context_getter.h" + +namespace base { +class WaitableEvent; +} // namespace base + +namespace net { +class CookieStore; +class HttpNetworkSession; +class NetLog; +class FileNetLogObserver; +} // namespace net + +namespace cronet { +class CronetPrefsManager; + +// CronetEnvironment contains all the network stack configuration +// and initialization. +class CronetEnvironment { + public: + using PkpVector = std::vector>; + + // A special thread priority value that indicates that the thread priority + // should not be altered when a thread is created. + static const double kKeepDefaultThreadPriority; + + // |user_agent| will be used to generate the user-agent if + // |user_agent_partial| is true, or will be used as the complete user-agent + // otherwise. + CronetEnvironment(const std::string& user_agent, bool user_agent_partial); + + CronetEnvironment(const CronetEnvironment&) = delete; + CronetEnvironment& operator=(const CronetEnvironment&) = delete; + + ~CronetEnvironment(); + + // Starts this instance of Cronet environment. + void Start(); + + // The full user-agent. + std::string user_agent(); + + // Get global UMA histogram deltas. + std::vector GetHistogramDeltas(); + + // Creates a new net log (overwrites existing file with this name). If + // actively logging, this call is ignored. + bool StartNetLog(base::FilePath::StringType file_name, bool log_bytes); + // Stops logging and flushes file. If not currently logging this call is + // ignored. + void StopNetLog(); + + void AddQuicHint(const std::string& host, int port, int alternate_port); + + // Setters and getters for |http2_enabled_|, |quic_enabled_|, and + // |brotli_enabled| These only have any effect + // before Start() is called. + void set_http2_enabled(bool enabled) { http2_enabled_ = enabled; } + void set_quic_enabled(bool enabled) { quic_enabled_ = enabled; } + void set_brotli_enabled(bool enabled) { brotli_enabled_ = enabled; } + + bool http2_enabled() const { return http2_enabled_; } + bool quic_enabled() const { return quic_enabled_; } + bool brotli_enabled() const { return brotli_enabled_; } + + void set_accept_language(const std::string& accept_language) { + accept_language_ = accept_language; + } + + void set_mock_cert_verifier( + std::unique_ptr mock_cert_verifier) { + mock_cert_verifier_ = std::move(mock_cert_verifier); + } + + void set_http_cache(URLRequestContextConfig::HttpCacheType http_cache) { + http_cache_ = http_cache; + } + + void set_experimental_options(const std::string& experimental_options) { + experimental_options_ = experimental_options; + } + + void SetHostResolverRules(const std::string& host_resolver_rules); + + void set_ssl_key_log_file_name(const std::string& ssl_key_log_file_name) { + ssl_key_log_file_name_ = ssl_key_log_file_name; + } + + void set_pkp_list(PkpVector pkp_list) { pkp_list_ = std::move(pkp_list); } + + void set_enable_public_key_pinning_bypass_for_local_trust_anchors( + bool enable) { + enable_pkp_bypass_for_local_trust_anchors_ = enable; + } + + // Sets priority of the network thread. The |priority| should be a + // floating point number between 0.0 to 1.0, where 1.0 is highest priority. + void SetNetworkThreadPriority(double priority); + + // Returns the URLRequestContext associated with this object. + net::URLRequestContext* GetURLRequestContext() const; + + // Return the URLRequestContextGetter associated with this object. + net::URLRequestContextGetter* GetURLRequestContextGetter() const; + + // The methods below are used for testing. + base::SingleThreadTaskRunner* GetFileThreadRunnerForTesting() const; + base::SingleThreadTaskRunner* GetNetworkThreadRunnerForTesting() const; + + private: + // Extends the base thread class to add the Cronet specific cleanup logic. + class CronetNetworkThread : public base::Thread { + public: + CronetNetworkThread(const std::string& name, + cronet::CronetEnvironment* cronet_environment); + + CronetNetworkThread(const CronetNetworkThread&) = delete; + CronetNetworkThread& operator=(const CronetNetworkThread&) = delete; + + protected: + ~CronetNetworkThread() override; + void CleanUp() override; + + private: + cronet::CronetEnvironment* const cronet_environment_; + }; + + // Performs initialization tasks that must happen on the network thread. + void InitializeOnNetworkThread(); + + // Returns the task runner for the network thread. + base::SingleThreadTaskRunner* GetNetworkThreadTaskRunner() const; + + // Runs a closure on the network thread. + void PostToNetworkThread(const base::Location& from_here, + base::OnceClosure task); + + // Helper methods that start/stop net logging on the network thread. + void StartNetLogOnNetworkThread(const base::FilePath&, bool log_bytes); + void StopNetLogOnNetworkThread(base::WaitableEvent* log_stopped_event); + + base::Value GetNetLogInfo() const; + + // Returns the HttpNetworkSession object from the passed in + // URLRequestContext or NULL if none exists. + net::HttpNetworkSession* GetHttpNetworkSession( + net::URLRequestContext* context); + + // Sets host resolver rules on the network_io_thread_. + void SetHostResolverRulesOnNetworkThread(const std::string& rules, + base::WaitableEvent* event); + + // Sets priority of the network thread. This method should only be called + // on the network thread. + void SetNetworkThreadPriorityOnNetworkThread(double priority); + + std::string getDefaultQuicUserAgentId() const; + + // Prepares the Cronet environment to be destroyed. The method must be + // executed on the network thread. No other tasks should be posted to the + // network thread after calling this method. + void CleanUpOnNetworkThread(); + + bool http2_enabled_; + bool quic_enabled_; + bool brotli_enabled_; + std::string accept_language_; + std::string experimental_options_; + // Effective experimental options. Kept for NetLog. + base::Value effective_experimental_options_; + std::string ssl_key_log_file_name_; + URLRequestContextConfig::HttpCacheType http_cache_; + PkpVector pkp_list_; + + std::list quic_hints_; + + std::unique_ptr network_io_thread_; + std::unique_ptr file_thread_; + scoped_refptr pref_store_worker_pool_; + std::unique_ptr mock_cert_verifier_; + std::unique_ptr cookie_store_; + std::unique_ptr main_context_; + scoped_refptr main_context_getter_; + std::string user_agent_; + bool user_agent_partial_; + net::NetLog* net_log_; + std::unique_ptr file_net_log_observer_; + bool enable_pkp_bypass_for_local_trust_anchors_; + double network_thread_priority_; + std::unique_ptr cronet_prefs_manager_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_IOS_CRONET_ENVIRONMENT_H_ diff --git a/src/components/cronet/ios/cronet_environment.mm b/src/components/cronet/ios/cronet_environment.mm new file mode 100644 index 0000000000..e1d61a69ad --- /dev/null +++ b/src/components/cronet/ios/cronet_environment.mm @@ -0,0 +1,511 @@ +// Copyright 2016 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 "components/cronet/ios/cronet_environment.h" + +#include +#include + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/feature_list.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_file.h" +#include "base/mac/foundation_util.h" +#include "base/message_loop/message_pump_type.h" +#include "base/path_service.h" +#include "base/synchronization/waitable_event.h" +#include "base/task/single_thread_task_runner.h" +#include "base/threading/thread_restrictions.h" +#include "components/cronet/cronet_buildflags.h" +#include "components/cronet/cronet_global_state.h" +#include "components/cronet/cronet_prefs_manager.h" +#include "components/metrics/library_support/histogram_manager.h" +#include "components/prefs/pref_filter.h" +#include "ios/net/cookies/cookie_store_ios.h" +#include "ios/net/cookies/cookie_store_ios_client.h" +#include "ios/web/common/user_agent.h" +#include "ios/web/public/init/ios_global_state.h" +#include "ios/web/public/init/ios_global_state_configuration.h" +#include "net/base/http_user_agent_settings.h" +#include "net/base/network_change_notifier.h" +#include "net/base/network_isolation_key.h" +#include "net/base/url_util.h" +#include "net/cert/cert_verifier.h" +#include "net/dns/host_resolver.h" +#include "net/dns/mapped_host_resolver.h" +#include "net/http/http_network_session.h" +#include "net/http/http_server_properties.h" +#include "net/http/http_transaction_factory.h" +#include "net/http/http_util.h" +#include "net/http/transport_security_state.h" +#include "net/log/file_net_log_observer.h" +#include "net/log/net_log.h" +#include "net/log/net_log_capture_mode.h" +#include "net/log/net_log_util.h" +#include "net/proxy_resolution/proxy_resolution_service.h" +#include "net/socket/ssl_client_socket.h" +#include "net/ssl/ssl_key_logger_impl.h" +#include "net/third_party/quiche/src/quic/core/quic_versions.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_context_builder.h" +#include "net/url_request/url_request_context_storage.h" +#include "url/scheme_host_port.h" +#include "url/url_util.h" + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +namespace { + +// Request context getter for Cronet. +class CronetURLRequestContextGetter : public net::URLRequestContextGetter { + public: + CronetURLRequestContextGetter( + cronet::CronetEnvironment* environment, + const scoped_refptr& task_runner) + : environment_(environment), task_runner_(task_runner) {} + + CronetURLRequestContextGetter(const CronetURLRequestContextGetter&) = delete; + CronetURLRequestContextGetter& operator=( + const CronetURLRequestContextGetter&) = delete; + + net::URLRequestContext* GetURLRequestContext() override { + DCHECK(environment_); + return environment_->GetURLRequestContext(); + } + + scoped_refptr GetNetworkTaskRunner() + const override { + return task_runner_; + } + + private: + // Must be called on the IO thread. + ~CronetURLRequestContextGetter() override {} + + cronet::CronetEnvironment* environment_; + scoped_refptr task_runner_; +}; + +// Cronet implementation of net::CookieStoreIOSClient. +// Used to provide Cronet Network IO TaskRunner. +class CronetCookieStoreIOSClient : public net::CookieStoreIOSClient { + public: + CronetCookieStoreIOSClient( + const scoped_refptr& task_runner) + : task_runner_(task_runner) {} + + CronetCookieStoreIOSClient(const CronetCookieStoreIOSClient&) = delete; + CronetCookieStoreIOSClient& operator=(const CronetCookieStoreIOSClient&) = + delete; + + scoped_refptr GetTaskRunner() const override { + return task_runner_; + } + + private: + ~CronetCookieStoreIOSClient() override {} + + scoped_refptr task_runner_; +}; + +void SignalEvent(base::WaitableEvent* event) { + event->Signal(); +} + +// TODO(eroman): Creating the file(s) for a netlog is an internal detail for +// FileNetLogObsever. This code assumes that the unbounded format is being used, +// which writes a single file at |path| (creating or overwriting it). +bool IsNetLogPathValid(const base::FilePath& path) { + base::ScopedFILE file(base::OpenFile(path, "w")); + return !!file; +} + +} // namespace + +namespace cronet { + +const double CronetEnvironment::kKeepDefaultThreadPriority = -1; + +base::SingleThreadTaskRunner* CronetEnvironment::GetNetworkThreadTaskRunner() + const { + if (network_io_thread_) { + return network_io_thread_->task_runner().get(); + } + return ios_global_state::GetSharedNetworkIOThreadTaskRunner().get(); +} + +void CronetEnvironment::PostToNetworkThread(const base::Location& from_here, + base::OnceClosure task) { + GetNetworkThreadTaskRunner()->PostTask(from_here, std::move(task)); +} + +net::URLRequestContext* CronetEnvironment::GetURLRequestContext() const { + return main_context_.get(); +} + +net::URLRequestContextGetter* CronetEnvironment::GetURLRequestContextGetter() + const { + return main_context_getter_.get(); +} + +bool CronetEnvironment::StartNetLog(base::FilePath::StringType file_name, + bool log_bytes) { + if (file_name.empty()) + return false; + + base::FilePath path(file_name); + if (!IsNetLogPathValid(path)) { + LOG(ERROR) << "Can not start NetLog to " << path.value() << ": " + << strerror(errno); + return false; + } + + LOG(WARNING) << "Starting NetLog to " << path.value(); + PostToNetworkThread( + FROM_HERE, base::BindOnce(&CronetEnvironment::StartNetLogOnNetworkThread, + base::Unretained(this), path, log_bytes)); + + return true; +} + +void CronetEnvironment::StartNetLogOnNetworkThread(const base::FilePath& path, + bool log_bytes) { + DCHECK(net_log_); + + if (file_net_log_observer_) + return; + + net::NetLogCaptureMode capture_mode = + log_bytes ? net::NetLogCaptureMode::kEverything + : net::NetLogCaptureMode::kDefault; + + file_net_log_observer_ = + net::FileNetLogObserver::CreateUnbounded(path, capture_mode, nullptr); + file_net_log_observer_->StartObserving(main_context_->net_log()); + LOG(WARNING) << "Started NetLog"; +} + +void CronetEnvironment::StopNetLog() { + base::WaitableEvent log_stopped_event( + base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED); + PostToNetworkThread( + FROM_HERE, base::BindOnce(&CronetEnvironment::StopNetLogOnNetworkThread, + base::Unretained(this), &log_stopped_event)); + log_stopped_event.Wait(); +} + +void CronetEnvironment::StopNetLogOnNetworkThread( + base::WaitableEvent* log_stopped_event) { + if (file_net_log_observer_) { + DLOG(WARNING) << "Stopped NetLog."; + file_net_log_observer_->StopObserving( + base::Value::ToUniquePtrValue(GetNetLogInfo()), + base::BindOnce(&SignalEvent, log_stopped_event)); + file_net_log_observer_.reset(); + } else { + log_stopped_event->Signal(); + } +} + +base::Value CronetEnvironment::GetNetLogInfo() const { + base::Value net_info = net::GetNetInfo(main_context_.get()); + if (!effective_experimental_options_.DictEmpty()) { + net_info.SetKey("cronetExperimentalParams", + effective_experimental_options_.Clone()); + } + return net_info; +} + +net::HttpNetworkSession* CronetEnvironment::GetHttpNetworkSession( + net::URLRequestContext* context) { + DCHECK(context); + if (!context->http_transaction_factory()) + return nullptr; + + return context->http_transaction_factory()->GetSession(); +} + +void CronetEnvironment::AddQuicHint(const std::string& host, + int port, + int alternate_port) { + DCHECK(port == alternate_port); + quic_hints_.push_back(net::HostPortPair(host, port)); +} + +CronetEnvironment::CronetEnvironment(const std::string& user_agent, + bool user_agent_partial) + : http2_enabled_(false), + quic_enabled_(true), + brotli_enabled_(false), + http_cache_(URLRequestContextConfig::HttpCacheType::DISK), + user_agent_(user_agent), + user_agent_partial_(user_agent_partial), + net_log_(net::NetLog::Get()), + enable_pkp_bypass_for_local_trust_anchors_(true), + network_thread_priority_(kKeepDefaultThreadPriority) {} + +void CronetEnvironment::Start() { + // Threads setup. + file_thread_.reset(new base::Thread("Chrome File Thread")); + file_thread_->StartWithOptions( + base::Thread::Options(base::MessagePumpType::IO, 0)); + // Fetching the task_runner will create the shared thread if necessary. + scoped_refptr task_runner = + ios_global_state::GetSharedNetworkIOThreadTaskRunner(); + if (!task_runner) { + network_io_thread_.reset( + new CronetNetworkThread("Chrome Network IO Thread", this)); + network_io_thread_->StartWithOptions( + base::Thread::Options(base::MessagePumpType::IO, 0)); + } + + net::SetCookieStoreIOSClient(new CronetCookieStoreIOSClient( + CronetEnvironment::GetNetworkThreadTaskRunner())); + + main_context_getter_ = new CronetURLRequestContextGetter( + this, CronetEnvironment::GetNetworkThreadTaskRunner()); + std::atomic_thread_fence(std::memory_order_seq_cst); + PostToNetworkThread( + FROM_HERE, base::BindOnce(&CronetEnvironment::InitializeOnNetworkThread, + base::Unretained(this))); +} + +void CronetEnvironment::CleanUpOnNetworkThread() { + // TODO(lilyhoughton) make unregistering of this work. + // net::HTTPProtocolHandlerDelegate::SetInstance(nullptr); + + // TODO(lilyhoughton) this can only be run once, so right now leaking it. + // Should be be called when the _last_ CronetEnvironment is destroyed. + // base::ThreadPoolInstance* ts = base::ThreadPoolInstance::Get(); + // if (ts) + // ts->Shutdown(); + + if (cronet_prefs_manager_) { + cronet_prefs_manager_->PrepareForShutdown(); + } + + // TODO(lilyhoughton) this should be smarter about making sure there are no + // pending requests, etc. + main_context_.reset(); + + // cronet_prefs_manager_ should be deleted on the network thread. + cronet_prefs_manager_.reset(); +} + +CronetEnvironment::~CronetEnvironment() { + // Deleting a thread blocks the current thread and waits until all pending + // tasks are completed. + network_io_thread_.reset(); + file_thread_.reset(); +} + +void CronetEnvironment::InitializeOnNetworkThread() { + DCHECK(GetNetworkThreadTaskRunner()->BelongsToCurrentThread()); + base::DisallowBlocking(); + + static bool ssl_key_log_file_set = false; + if (!ssl_key_log_file_set && !ssl_key_log_file_name_.empty()) { + ssl_key_log_file_set = true; + base::FilePath ssl_key_log_file(ssl_key_log_file_name_); + net::SSLClientSocket::SetSSLKeyLogger( + std::make_unique(ssl_key_log_file)); + } + + if (user_agent_partial_) + user_agent_ = web::BuildMobileUserAgent(user_agent_); + + // Cache + base::FilePath storage_path; + if (!base::PathService::Get(base::DIR_CACHE, &storage_path)) + return; + storage_path = storage_path.Append(FILE_PATH_LITERAL("cronet")); + + URLRequestContextConfigBuilder context_config_builder; + context_config_builder.enable_quic = quic_enabled_; // Enable QUIC. + context_config_builder.quic_user_agent_id = + getDefaultQuicUserAgentId(); // QUIC User Agent ID. + context_config_builder.enable_spdy = http2_enabled_; // Enable HTTP/2. + context_config_builder.http_cache = http_cache_; // Set HTTP cache. + context_config_builder.storage_path = + storage_path.value(); // Storage path for http cache and prefs storage. + context_config_builder.accept_language = + accept_language_; // Accept-Language request header field. + context_config_builder.user_agent = + user_agent_; // User-Agent request header field. + context_config_builder.experimental_options = + experimental_options_; // Set experimental Cronet options. + context_config_builder.mock_cert_verifier = std::move( + mock_cert_verifier_); // MockCertVerifier to use for testing purposes. + if (network_thread_priority_ != kKeepDefaultThreadPriority) + context_config_builder.network_thread_priority = network_thread_priority_; + std::unique_ptr config = + context_config_builder.Build(); + + config->pkp_list = std::move(pkp_list_); + + net::URLRequestContextBuilder context_builder; + + // Explicitly disable the persister for Cronet to avoid persistence of dynamic + // HPKP. This is a safety measure ensuring that nobody enables the + // persistence of HPKP by specifying transport_security_persister_file_path in + // the future. + context_builder.set_transport_security_persister_file_path(base::FilePath()); + + config->ConfigureURLRequestContextBuilder(&context_builder); + + effective_experimental_options_ = + base::Value(config->effective_experimental_options); + + // TODO(crbug.com/934402): Use a shared HostResolverManager instead of a + // global HostResolver. + std::unique_ptr mapped_host_resolver( + new net::MappedHostResolver( + net::HostResolver::CreateStandaloneResolver(nullptr))); + + if (!config->storage_path.empty()) { + cronet_prefs_manager_ = std::make_unique( + config->storage_path, GetNetworkThreadTaskRunner(), + file_thread_->task_runner(), false /* nqe */, false /* host_cache */, + net_log_, &context_builder); + } + + context_builder.set_host_resolver(std::move(mapped_host_resolver)); + + // TODO(690969): This behavior matches previous behavior of CookieStoreIOS in + // CrNet, but should change to adhere to App's Cookie Accept Policy instead + // of changing it. + [[NSHTTPCookieStorage sharedHTTPCookieStorage] + setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways]; + auto cookie_store = std::make_unique( + [NSHTTPCookieStorage sharedHTTPCookieStorage], nullptr /* net_log */); + context_builder.SetCookieStore(std::move(cookie_store)); + + context_builder.set_enable_brotli(brotli_enabled_); + main_context_ = context_builder.Build(); + + for (const auto& quic_hint : quic_hints_) { + url::CanonHostInfo host_info; + std::string canon_host(net::CanonicalizeHost(quic_hint.host(), &host_info)); + if (!host_info.IsIPAddress() && + !net::IsCanonicalizedHostCompliant(canon_host)) { + LOG(ERROR) << "Invalid QUIC hint host: " << quic_hint.host(); + continue; + } + + net::AlternativeService alternative_service(net::kProtoQUIC, "", + quic_hint.port()); + + url::SchemeHostPort quic_hint_server("https", quic_hint.host(), + quic_hint.port()); + main_context_->http_server_properties()->SetQuicAlternativeService( + quic_hint_server, net::NetworkIsolationKey(), alternative_service, + base::Time::Max(), quic::ParsedQuicVersionVector()); + } + + main_context_->transport_security_state() + ->SetEnablePublicKeyPinningBypassForLocalTrustAnchors( + enable_pkp_bypass_for_local_trust_anchors_); + + // Iterate trhough PKP configuration for every host. + for (const auto& pkp : config->pkp_list) { + // Add the host pinning. + main_context_->transport_security_state()->AddHPKP( + pkp->host, pkp->expiration_date, pkp->include_subdomains, + pkp->pin_hashes, GURL::EmptyGURL()); + } +} + +void CronetEnvironment::SetNetworkThreadPriority(double priority) { + DCHECK_LE(priority, 1.0); + DCHECK_GE(priority, 0.0); + network_thread_priority_ = priority; + if (network_io_thread_) { + PostToNetworkThread( + FROM_HERE, + base::BindRepeating( + &CronetEnvironment::SetNetworkThreadPriorityOnNetworkThread, + base::Unretained(this), priority)); + } +} + +std::string CronetEnvironment::user_agent() { + const net::HttpUserAgentSettings* user_agent_settings = + main_context_->http_user_agent_settings(); + if (!user_agent_settings) { + return nullptr; + } + + return user_agent_settings->GetUserAgent(); +} + +std::vector CronetEnvironment::GetHistogramDeltas() { + std::vector data; +#if BUILDFLAG(DISABLE_HISTOGRAM_SUPPORT) + NOTREACHED() << "Histogram support is disabled"; +#else // BUILDFLAG(DISABLE_HISTOGRAM_SUPPORT) + if (!metrics::HistogramManager::GetInstance()->GetDeltas(&data)) + return std::vector(); +#endif // BUILDFLAG(DISABLE_HISTOGRAM_SUPPORT) + return data; +} + +void CronetEnvironment::SetHostResolverRules(const std::string& rules) { + base::WaitableEvent event(base::WaitableEvent::ResetPolicy::AUTOMATIC, + base::WaitableEvent::InitialState::NOT_SIGNALED); + PostToNetworkThread( + FROM_HERE, + base::BindOnce(&CronetEnvironment::SetHostResolverRulesOnNetworkThread, + base::Unretained(this), rules, &event)); + event.Wait(); +} + +void CronetEnvironment::SetHostResolverRulesOnNetworkThread( + const std::string& rules, + base::WaitableEvent* event) { + static_cast(main_context_->host_resolver()) + ->SetRulesFromString(rules); + event->Signal(); +} + +void CronetEnvironment::SetNetworkThreadPriorityOnNetworkThread( + double priority) { + DCHECK(GetNetworkThreadTaskRunner()->BelongsToCurrentThread()); + cronet::SetNetworkThreadPriorityOnNetworkThread(priority); +} + +std::string CronetEnvironment::getDefaultQuicUserAgentId() const { + return base::SysNSStringToUTF8([[NSBundle mainBundle] + objectForInfoDictionaryKey:@"CFBundleDisplayName"]) + + " Cronet/" + CRONET_VERSION; +} + +base::SingleThreadTaskRunner* CronetEnvironment::GetFileThreadRunnerForTesting() + const { + return file_thread_->task_runner().get(); +} + +base::SingleThreadTaskRunner* +CronetEnvironment::GetNetworkThreadRunnerForTesting() const { + return GetNetworkThreadTaskRunner(); +} + +CronetEnvironment::CronetNetworkThread::CronetNetworkThread( + const std::string& name, + cronet::CronetEnvironment* cronet_environment) + : base::Thread(name), cronet_environment_(cronet_environment) {} + +CronetEnvironment::CronetNetworkThread::~CronetNetworkThread() { + Stop(); +} + +void CronetEnvironment::CronetNetworkThread::CleanUp() { + cronet_environment_->CleanUpOnNetworkThread(); +} + +} // namespace cronet diff --git a/src/components/cronet/ios/cronet_global_state_ios.mm b/src/components/cronet/ios/cronet_global_state_ios.mm new file mode 100644 index 0000000000..acd8aa50a0 --- /dev/null +++ b/src/components/cronet/ios/cronet_global_state_ios.mm @@ -0,0 +1,94 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/cronet/cronet_global_state.h" + +#import + +#include + +#include "base/callback.h" +#include "ios/web/common/user_agent.h" +#include "ios/web/public/init/ios_global_state.h" +#include "ios/web/public/init/ios_global_state_configuration.h" +#include "net/proxy_resolution/proxy_config_service.h" +#include "net/proxy_resolution/proxy_resolution_service.h" + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +namespace { + +void InitializeOnMainThread() { + // This method must be called once from the main thread. + DCHECK_EQ([NSThread currentThread], [NSThread mainThread]); + + ios_global_state::CreateParams create_params; + create_params.install_at_exit_manager = true; + ios_global_state::Create(create_params); + ios_global_state::StartThreadPool(); + + ios_global_state::BuildSingleThreadTaskExecutor(); + ios_global_state::CreateNetworkChangeNotifier(); +} + +} // namespace + +namespace cronet { + +bool OnInitThread() { + return [NSThread isMainThread] == YES; +} + +void PostTaskToInitThread(const base::Location& posted_from, + base::OnceClosure task) { + __block base::OnceClosure block_task(std::move(task)); + if (!OnInitThread()) { + dispatch_async(dispatch_get_main_queue(), ^(void) { + std::move(block_task).Run(); + }); + } else { + std::move(block_task).Run(); + } +} + +void EnsureInitialized() { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (!OnInitThread()) { + dispatch_sync(dispatch_get_main_queue(), ^(void) { + InitializeOnMainThread(); + }); + } else { + InitializeOnMainThread(); + } + }); +} + +std::unique_ptr CreateProxyConfigService( + const scoped_refptr& io_task_runner) { + return nullptr; +} + +std::unique_ptr CreateProxyResolutionService( + std::unique_ptr proxy_config_service, + net::NetLog* net_log) { + return nullptr; +} + +// Creates default User-Agent request value, combining optional +// |partial_user_agent| with system-dependent values. +std::string CreateDefaultUserAgent(const std::string& partial_user_agent) { + return web::BuildMobileUserAgent(partial_user_agent); +} + +void SetNetworkThreadPriorityOnNetworkThread(double priority) { + DCHECK_LE(priority, 1.0); + DCHECK_GE(priority, 0.0); + if (priority >= 0.0 && priority <= 1.0) + [NSThread setThreadPriority:priority]; +} + +} // namespace cronet diff --git a/src/components/cronet/ios/cronet_metrics.h b/src/components/cronet/ios/cronet_metrics.h new file mode 100644 index 0000000000..f8a25dd390 --- /dev/null +++ b/src/components/cronet/ios/cronet_metrics.h @@ -0,0 +1,112 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_CRONET_IOS_CRONET_METRICS_H_ +#define COMPONENTS_CRONET_IOS_CRONET_METRICS_H_ + +#import + +#include "components/grpc_support/include/bidirectional_stream_c.h" +#import "ios/net/crn_http_protocol_handler.h" +#include "net/http/http_network_session.h" + +// These are internal versions of NSURLSessionTaskTransactionMetrics and +// NSURLSessionTaskMetrics, defined primarily so that Cronet can +// initialize them and set their properties (the iOS classes are readonly). + +// The correspondences are +// CronetTransactionMetrics -> NSURLSessionTaskTransactionMetrics +// CronetMetrics -> NSURLSessionTaskMetrics + +FOUNDATION_EXPORT GRPC_SUPPORT_EXPORT NS_AVAILABLE_IOS(10.0) +@interface CronetTransactionMetrics : NSURLSessionTaskTransactionMetrics + +// All of the below redefined as readwrite. + +// This is set to [task currentRequest]. +@property(copy, readwrite) NSURLRequest* request; +// This is set to [task response]. +@property(copy, readwrite) NSURLResponse* response; + +// This is set to net::LoadTimingInfo::request_start_time. +@property(copy, readwrite) NSDate* fetchStartDate; +// This is set to net::LoadTimingInfo::ConnectTiming::dns_start. +@property(copy, readwrite) NSDate* domainLookupStartDate; +// This is set to net::LoadTimingInfo::ConnectTiming::dns_end. +@property(copy, readwrite) NSDate* domainLookupEndDate; +// This is set to net::LoadTimingInfo::ConnectTiming::connect_start. +@property(copy, readwrite) NSDate* connectStartDate; +// This is set to net::LoadTimingInfo::ConnectTiming::ssl_start. +@property(copy, readwrite) NSDate* secureConnectionStartDate; +// This is set to net::LoadTimingInfo::ConnectTiming::ssl_end. +@property(copy, readwrite) NSDate* secureConnectionEndDate; +// This is set to net::LoadTimingInfo::ConnectTiming::connect_end. +@property(copy, readwrite) NSDate* connectEndDate; +// This is set to net::LoadTimingInfo::sent_start. +@property(copy, readwrite) NSDate* requestStartDate; +// This is set to net::LoadTimingInfo::send_end. +@property(copy, readwrite) NSDate* requestEndDate; +// This is set to net::LoadTimingInfo::receive_headers_end. +@property(copy, readwrite) NSDate* responseStartDate; +// This is set to net::MetricsDelegate::Metrics::response_end_time. +@property(copy, readwrite) NSDate* responseEndDate; + +// This is set to net::HttpResponseInfo::connection_info. +@property(copy, readwrite) NSString* networkProtocolName; +// This is set to YES if net::HttpResponseInfo::proxy_server.is_direct() +// returns false. +@property(assign, readwrite, getter=isProxyConnection) BOOL proxyConnection; +// This is set to YES if net::LoadTimingInfo::ConnectTiming::conect_start is +// null. +@property(assign, readwrite, getter=isReusedConnection) BOOL reusedConnection; +// This is set to LocalCache if net::HttpResponseInfo::was_cached is true, set +// to ServerPush if net::LoadTimingInfo::push_start is non-null, and set to +// NetworkLoad otherwise. +@property(assign, readwrite) + NSURLSessionTaskMetricsResourceFetchType resourceFetchType; + +- (NSString*)description; + +@end + +// This is an internal version of NSURLSessionTaskMetrics - see comment above +// CronetTransactionMetrics. +NS_AVAILABLE_IOS(10.0) @interface CronetMetrics : NSURLSessionTaskMetrics +// Redefined as readwrite. +@property(copy, readwrite) + NSArray* transactionMetrics; +@end + +namespace cronet { + +// net::MetricsDelegate for Cronet. +class CronetMetricsDelegate : public net::MetricsDelegate { + public: + using Metrics = net::MetricsDelegate::Metrics; + + CronetMetricsDelegate() {} + void OnStartNetRequest(NSURLSessionTask* task) override; + void OnStopNetRequest(std::unique_ptr metrics) override; + + // Returns the metrics collected for a specific task (removing that task's + // entry from the map in the process). + // It is called exactly once by the swizzled delegate proxy (see below), + // uses it to retrieve metrics data collected by net/ and pass them on to + // the client. If there is no metrics data for the passed task, this returns + // nullptr. + static std::unique_ptr MetricsForTask(NSURLSessionTask* task); + + // Used by tests to query the size of the |gTaskMetricsMap| map. + static size_t GetMetricsMapSize(); +}; + +// This is the swizzling function that Cronet (in its startInternal +// method) calls to inject the proxy delegate into iOS networking API and +// intercept didFinishCollectingMetrics to replace the (empty) iOS metrics data +// with metrics data from net. +void SwizzleSessionWithConfiguration(); + +} // namespace cronet + +#endif // COMPONENTS_CRONET_IOS_CRONET_METRICS_H_ diff --git a/src/components/cronet/ios/cronet_metrics.mm b/src/components/cronet/ios/cronet_metrics.mm new file mode 100644 index 0000000000..14021979c0 --- /dev/null +++ b/src/components/cronet/ios/cronet_metrics.mm @@ -0,0 +1,368 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "components/cronet/ios/cronet_metrics.h" + +#include + +#include "base/lazy_instance.h" +#include "base/strings/sys_string_conversions.h" + +@implementation CronetTransactionMetrics + +@synthesize request = _request; +@synthesize response = _response; + +@synthesize fetchStartDate = _fetchStartDate; +@synthesize domainLookupStartDate = _domainLookupStartDate; +@synthesize domainLookupEndDate = _domainLookupEndDate; +@synthesize connectStartDate = _connectStartDate; +@synthesize secureConnectionStartDate = _secureConnectionStartDate; +@synthesize secureConnectionEndDate = _secureConnectionEndDate; +@synthesize connectEndDate = _connectEndDate; +@synthesize requestStartDate = _requestStartDate; +@synthesize requestEndDate = _requestEndDate; +@synthesize responseStartDate = _responseStartDate; +@synthesize responseEndDate = _responseEndDate; + +@synthesize networkProtocolName = _networkProtocolName; +@synthesize proxyConnection = _proxyConnection; +@synthesize reusedConnection = _reusedConnection; +@synthesize resourceFetchType = _resourceFetchType; + +// The NSURLSessionTaskTransactionMetrics and NSURLSessionTaskMetrics classes +// are not supposed to be extended. Its default init method initialized an +// internal class, and therefore needs to be overridden to explicitly +// initialize (and return) an instance of this class. +// The |self = old_self| swap is necessary because [super init] must be +// assigned to self (or returned immediately), but in this case is returning +// a value of the wrong type. + +- (instancetype)init { + id old_self = self; + self = [super init]; + self = old_self; + return old_self; +} + +- (NSString*)description { + return [NSString + stringWithFormat: + @"" + "fetchStartDate: %@\n" + "domainLookupStartDate: %@\n" + "domainLookupEndDate: %@\n" + "connectStartDate: %@\n" + "secureConnectionStartDate: %@\n" + "secureConnectionEndDate: %@\n" + "connectEndDate: %@\n" + "requestStartDate: %@\n" + "requestEndDate: %@\n" + "responseStartDate: %@\n" + "responseEndDate: %@\n" + "networkProtocolName: %@\n" + "proxyConnection: %i\n" + "reusedConnection: %i\n" + "resourceFetchType: %lu\n", + [self fetchStartDate], [self domainLookupStartDate], + [self domainLookupEndDate], [self connectStartDate], + [self secureConnectionStartDate], [self secureConnectionEndDate], + [self connectEndDate], [self requestStartDate], [self requestEndDate], + [self responseStartDate], [self responseEndDate], + [self networkProtocolName], [self isProxyConnection], + [self isReusedConnection], (long)[self resourceFetchType]]; +} + +@end + +@implementation CronetMetrics + +@synthesize transactionMetrics = _transactionMetrics; + +- (instancetype)init { + id old_self = self; + self = [super init]; + self = old_self; + return old_self; +} + +@end + +namespace { + +using Metrics = net::MetricsDelegate::Metrics; + +// Synchronizes access to |gTaskMetricsMap|. +base::LazyInstance::Leaky gTaskMetricsMapLock = + LAZY_INSTANCE_INITIALIZER; + +// A global map that contains metrics information for pending URLSessionTasks. +// The map has to be "leaky"; otherwise, it will be destroyed on the main thread +// when the client app terminates. When the client app terminates, the network +// thread may still be finishing some work that requires access to the map. +base::LazyInstance>>::Leaky + gTaskMetricsMap = LAZY_INSTANCE_INITIALIZER; + +// Helper method that converts the ticks data found in LoadTimingInfo to an +// NSDate value to be used in client-side data. +NSDate* TicksToDate(const net::LoadTimingInfo& reference, + const base::TimeTicks& ticks) { + if (ticks.is_null()) + return nil; + base::Time ticks_since_1970 = + (reference.request_start_time + (ticks - reference.request_start)); + return [NSDate dateWithTimeIntervalSince1970:ticks_since_1970.ToDoubleT()]; +} + +// Converts Metrics metrics data into CronetTransactionMetrics (which +// importantly implements the NSURLSessionTaskTransactionMetrics API) +CronetTransactionMetrics* NativeToIOSMetrics(Metrics& metrics) + NS_AVAILABLE_IOS(10.0) { + NSURLSessionTask* task = metrics.task; + const net::LoadTimingInfo& load_timing_info = metrics.load_timing_info; + const net::HttpResponseInfo& response_info = metrics.response_info; + + CronetTransactionMetrics* transaction_metrics = + [[CronetTransactionMetrics alloc] init]; + + [transaction_metrics setRequest:[task currentRequest]]; + [transaction_metrics setResponse:[task response]]; + + transaction_metrics.fetchStartDate = + [NSDate dateWithTimeIntervalSince1970:load_timing_info.request_start_time + .ToDoubleT()]; + + transaction_metrics.domainLookupStartDate = + TicksToDate(load_timing_info, load_timing_info.connect_timing.dns_start); + transaction_metrics.domainLookupEndDate = + TicksToDate(load_timing_info, load_timing_info.connect_timing.dns_end); + + transaction_metrics.connectStartDate = TicksToDate( + load_timing_info, load_timing_info.connect_timing.connect_start); + transaction_metrics.secureConnectionStartDate = + TicksToDate(load_timing_info, load_timing_info.connect_timing.ssl_start); + transaction_metrics.secureConnectionEndDate = + TicksToDate(load_timing_info, load_timing_info.connect_timing.ssl_end); + transaction_metrics.connectEndDate = TicksToDate( + load_timing_info, load_timing_info.connect_timing.connect_end); + + transaction_metrics.requestStartDate = + TicksToDate(load_timing_info, load_timing_info.send_start); + transaction_metrics.requestEndDate = + TicksToDate(load_timing_info, load_timing_info.send_end); + transaction_metrics.responseStartDate = + TicksToDate(load_timing_info, load_timing_info.receive_headers_end); + transaction_metrics.responseEndDate = [NSDate + dateWithTimeIntervalSince1970:metrics.response_end_time.ToDoubleT()]; + + transaction_metrics.networkProtocolName = + base::SysUTF8ToNSString(net::HttpResponseInfo::ConnectionInfoToString( + response_info.connection_info)); + transaction_metrics.proxyConnection = !response_info.proxy_server.is_direct(); + + // If the connect timing information is null, then there was no connection + // establish - i.e., one was reused. + // The corrolary to this is that, if reusedConnection is YES, then + // domainLookupStartDate, domainLookupEndDate, connectStartDate, + // connectEndDate, secureConnectionStartDate, and secureConnectionEndDate are + // all meaningless. + transaction_metrics.reusedConnection = + load_timing_info.connect_timing.connect_start.is_null(); + + // Guess the resource fetch type based on some heuristics about what data is + // present. + if (response_info.was_cached) { + transaction_metrics.resourceFetchType = + NSURLSessionTaskMetricsResourceFetchTypeLocalCache; + } else if (!load_timing_info.push_start.is_null()) { + transaction_metrics.resourceFetchType = + NSURLSessionTaskMetricsResourceFetchTypeServerPush; + } else { + transaction_metrics.resourceFetchType = + NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad; + } + + return transaction_metrics; +} + +} // namespace + +// A blank implementation of NSURLSessionDelegate that contains no methods. +// It is used as a substitution for a session delegate when the client +// either creates a session without a delegate or passes 'nil' as its value. +@interface BlankNSURLSessionDelegate : NSObject +@end + +@implementation BlankNSURLSessionDelegate : NSObject +@end + +// In order for Cronet to use the iOS metrics collection API, it needs to +// replace the normal NSURLSession mechanism for calling into the delegate +// (so it can provide metrics from net/, instead of the empty metrics that iOS +// would provide otherwise. +// To this end, Cronet's startInternal method replaces the NSURLSession's +// sessionWithConfiguration method to inject a delegateProxy in between the +// client delegate and iOS code. +// This class represrents that delegateProxy. The important function is the +// didFinishCollectingMetrics callback, which when a request is being handled +// by Cronet, replaces the metrics collected by iOS with those connected by +// Cronet. +@interface URLSessionTaskDelegateProxy : NSProxy +- (instancetype)initWithDelegate:(id)delegate; +@end + +@implementation URLSessionTaskDelegateProxy { + id _delegate; + BOOL _respondsToDidFinishCollectingMetrics; +} + +// As this is a proxy delegate, it needs to be initialized with a real client +// delegate, to whom all of the method invocations will eventually get passed. +- (instancetype)initWithDelegate:(id)delegate { + // If the client passed a real delegate, use it. Otherwise, create a blank + // delegate that will handle method invocations that are forwarded by this + // proxy implementation. It is incorrect to forward calls to a 'nil' object. + if (delegate) { + _delegate = delegate; + } else { + _delegate = [[BlankNSURLSessionDelegate alloc] init]; + } + + _respondsToDidFinishCollectingMetrics = + [_delegate respondsToSelector:@selector + (URLSession:task:didFinishCollectingMetrics:)]; + return self; +} + +// Any methods other than didFinishCollectingMetrics should be forwarded +// directly to the client delegate. +- (void)forwardInvocation:(NSInvocation*)invocation { + [invocation setTarget:_delegate]; + [invocation invoke]; +} + +// And for that reason, URLSessionTaskDelegateProxy should act like it responds +// to any of the selectors that the client delegate does. +- (nullable NSMethodSignature*)methodSignatureForSelector:(SEL)sel { + return [(id)_delegate methodSignatureForSelector:sel]; +} + +// didFinishCollectionMetrics ultimately calls into the corresponding method on +// the client delegate (if it exists), but first replaces the iOS-supplied +// metrics with metrics collected by Cronet (if they exist). +- (void)URLSession:(NSURLSession*)session + task:(NSURLSessionTask*)task + didFinishCollectingMetrics:(NSURLSessionTaskMetrics*)metrics + NS_AVAILABLE_IOS(10.0) { + std::unique_ptr netMetrics = + cronet::CronetMetricsDelegate::MetricsForTask(task); + + if (_respondsToDidFinishCollectingMetrics) { + if (netMetrics) { + CronetTransactionMetrics* cronetTransactionMetrics = + NativeToIOSMetrics(*netMetrics); + + CronetMetrics* cronetMetrics = [[CronetMetrics alloc] init]; + [cronetMetrics setTransactionMetrics:@[ cronetTransactionMetrics ]]; + + [(id)_delegate URLSession:session + task:task + didFinishCollectingMetrics:cronetMetrics]; + } else { + // If there are no metrics is Cronet's task->metrics map, then Cronet is + // not handling this request, so just transparently pass iOS's collected + // metrics. + [(id)_delegate URLSession:session + task:task + didFinishCollectingMetrics:metrics]; + } + } +} + +- (BOOL)respondsToSelector:(SEL)aSelector { + // Regardless whether the underlying session delegate handles + // URLSession:task:didFinishCollectingMetrics: or not, always + // return 'YES' for that selector. Otherwise, the method may + // not be called, causing unbounded growth of |gTaskMetricsMap|. + if (aSelector == @selector(URLSession:task:didFinishCollectingMetrics:)) { + return YES; + } + return [_delegate respondsToSelector:aSelector]; +} + +@end + +@implementation NSURLSession (Cronet) + ++ (NSURLSession*) +hookSessionWithConfiguration:(NSURLSessionConfiguration*)configuration + delegate:(nullable id)delegate + delegateQueue:(nullable NSOperationQueue*)queue { + URLSessionTaskDelegateProxy* delegate_proxy = + [[URLSessionTaskDelegateProxy alloc] initWithDelegate:delegate]; + // Because the the method implementations are swapped, this is not a + // recursive call, and instead just forwards the call to the original + // sessionWithConfiguration method. + return [self hookSessionWithConfiguration:configuration + delegate:delegate_proxy + delegateQueue:queue]; +} + +@end + +namespace cronet { + +std::unique_ptr CronetMetricsDelegate::MetricsForTask( + NSURLSessionTask* task) { + base::AutoLock auto_lock(gTaskMetricsMapLock.Get()); + auto metrics_search = gTaskMetricsMap.Get().find(task); + if (metrics_search == gTaskMetricsMap.Get().end()) { + return nullptr; + } + + std::unique_ptr metrics = std::move(metrics_search->second); + // Remove the entry to free memory. + gTaskMetricsMap.Get().erase(metrics_search); + + return metrics; +} + +void CronetMetricsDelegate::OnStartNetRequest(NSURLSessionTask* task) { + base::AutoLock auto_lock(gTaskMetricsMapLock.Get()); + if ([task state] == NSURLSessionTaskStateRunning) { + gTaskMetricsMap.Get()[task] = nullptr; + } +} + +void CronetMetricsDelegate::OnStopNetRequest(std::unique_ptr metrics) { + base::AutoLock auto_lock(gTaskMetricsMapLock.Get()); + auto metrics_search = gTaskMetricsMap.Get().find(metrics->task); + if (metrics_search != gTaskMetricsMap.Get().end()) + metrics_search->second = std::move(metrics); +} + +size_t CronetMetricsDelegate::GetMetricsMapSize() { + base::AutoLock auto_lock(gTaskMetricsMapLock.Get()); + return gTaskMetricsMap.Get().size(); +} + +#pragma mark - Swizzle + +void SwizzleSessionWithConfiguration() { + Class nsurlsession_class = object_getClass([NSURLSession class]); + + SEL original_selector = + @selector(sessionWithConfiguration:delegate:delegateQueue:); + SEL swizzled_selector = + @selector(hookSessionWithConfiguration:delegate:delegateQueue:); + + Method original_method = + class_getInstanceMethod(nsurlsession_class, original_selector); + Method swizzled_method = + class_getInstanceMethod(nsurlsession_class, swizzled_selector); + + method_exchangeImplementations(original_method, swizzled_method); +} + +} // namespace cronet diff --git a/src/components/cronet/ios/docs/BUILD.md b/src/components/cronet/ios/docs/BUILD.md new file mode 100644 index 0000000000..90f95a0f59 --- /dev/null +++ b/src/components/cronet/ios/docs/BUILD.md @@ -0,0 +1,73 @@ +# Building Cronet (on iOS) + +## Get source and dependencies +### source +- Install depot_tools per https://chromium.googlesource.com/chromium/src/+/main/docs/ios/build_instructions.md +- Make directory for the chromium source, and then fetch: +``` + ~ $ mkdir chromium && cd chromium + ~/chromium $ fetch --nohooks ios +``` + +- Enter the ./src directory: +``` + ~/chromium $ cd src +``` +### deps +- Download the depenedencies +``` + ~/chromium/src $ gclient sync +``` + +## Build it! + +- We'll be using it a bunch, so you may want to put cr_cronet.py in your path. Of course, you can just use its full name every time if you want... +``` + ~/chromium/src $ ln -s /path/to/components/cronet/tools/cr_cronet.py /somewhere/in/your/path +``` + + or however else you want to do this + +This sets up the build directory... +``` + ~/chromium/src $ cr_cronet.py gn +``` +...and this builds it! +``` + ~/chromium/src $ cr_cronet.py build -d out/Debug-iphonesimulator +``` + +- You can also use build-test to run tests on the simulator +``` + ~/chromium/src $ cr_cronet.py build-test -d out/Debug-iphonesimulator +``` + +- If you want to deploy to hardware, you will have to set up XCode for deploying to hardware, and then use cr_cronet.py gn with the -i flag (for iphoneos build), and cr_cronet.py build with either the -i flag, or using the out/Debug-iphoneos directory. +``` + ~/chromium/src $ cr_cronet.py gn -i +``` +and then +``` + ~/chromium/src $ cr_cronet.py build -i +``` +or +``` + ~/chromium/src $ cr_cronet.py build -d out/Debug-iphoneos +``` + +## Updating + +- Acquire the most recent version of the source with: +``` + ~/chromium/src $ cr_cronet.py sync +``` +and then rebuild: +``` + ~/chromium/src $ cr_cronet.py build -d out/Debug-iphoneos + ~/chromium/src $ cr_cronet.py build -d out/Debug-iphonesimulator +``` + +For more information, you can run +``` + ~ $ cr_cronet.py -h +``` diff --git a/src/components/cronet/ios/empty.cc b/src/components/cronet/ios/empty.cc new file mode 100644 index 0000000000..f59ef9b668 --- /dev/null +++ b/src/components/cronet/ios/empty.cc @@ -0,0 +1,6 @@ +// Copyright 2016 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. + +// An empty C++ file that is needed to trigger the usage of clang++ instead of +// clang. diff --git a/src/components/cronet/ios/ios_global_state_configuration.cc b/src/components/cronet/ios/ios_global_state_configuration.cc new file mode 100644 index 0000000000..244f30620b --- /dev/null +++ b/src/components/cronet/ios/ios_global_state_configuration.cc @@ -0,0 +1,14 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ios/web/public/init/ios_global_state_configuration.h" + +namespace ios_global_state { + +scoped_refptr +GetSharedNetworkIOThreadTaskRunner() { + return nullptr; +} + +} // namespace ios_global_state diff --git a/src/components/cronet/ios/test/BUILD.gn b/src/components/cronet/ios/test/BUILD.gn new file mode 100644 index 0000000000..ea564f394c --- /dev/null +++ b/src/components/cronet/ios/test/BUILD.gn @@ -0,0 +1,47 @@ +# Copyright 2016 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. + +import("//build/config/ios/rules.gni") +import("//testing/test.gni") + +test("cronet_test") { + testonly = true + sources = [ + "../../run_all_unittests.cc", + "cronet_acceptlang_test.mm", + "cronet_http_test.mm", + "cronet_metrics_test.mm", + "cronet_netlog_test.mm", + "cronet_performance_test.mm", + "cronet_pkp_test.mm", + "cronet_prefs_test.mm", + "cronet_quic_test.mm", + "cronet_test_base.h", + "cronet_test_base.mm", + + # Use native stream engine instead (https://crbug.com/874542) + # "get_stream_engine.mm", + "start_cronet.h", + "start_cronet.mm", + ] + + deps = [ + "//base", + "//base:i18n", + "//components/cronet:cronet_buildflags", + "//components/cronet/ios:cronet_framework+link", + "//components/cronet/native/test:cronet_native_tests", + "//components/cronet/testing:test_support", + "//components/grpc_support:bidirectional_stream_test", + "//net", + "//net:simple_quic_tools", + "//net:test_support", + "//third_party/icu", + ] + + defines = [ "CRONET_TESTS_IMPLEMENTATION" ] + + bundle_deps = [ "//components/cronet/ios:cronet_framework+bundle" ] + configs += [ "//build/config/compiler:enable_arc" ] +} diff --git a/src/components/cronet/ios/test/DEPS b/src/components/cronet/ios/test/DEPS new file mode 100644 index 0000000000..a740940b4d --- /dev/null +++ b/src/components/cronet/ios/test/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+crypto", +] \ No newline at end of file diff --git a/src/components/cronet/ios/test/cronet_acceptlang_test.mm b/src/components/cronet/ios/test/cronet_acceptlang_test.mm new file mode 100644 index 0000000000..d19ace698c --- /dev/null +++ b/src/components/cronet/ios/test/cronet_acceptlang_test.mm @@ -0,0 +1,57 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// These tests are somewhat dependent on the exact contents of the +// accept languages table generated at build-time. + +#import +#import + +#include "testing/gtest/include/gtest/gtest.h" + +@interface Cronet (ExposedForTesting) ++ (NSString*)getAcceptLanguagesFromPreferredLanguages: + (NSArray*)languages; +@end + +namespace cronet { + +#define EXPECT_NSEQ(a, b) EXPECT_TRUE([(a) isEqual:(b)]) + +TEST(AcceptLangTest, Region) { + NSString* acceptLangs = + [Cronet getAcceptLanguagesFromPreferredLanguages:@[ @"en-GB" ]]; + + EXPECT_NSEQ(acceptLangs, @"en-GB,en-US,en"); +} + +TEST(AcceptLangTest, Lang) { + NSString* acceptLangs = + [Cronet getAcceptLanguagesFromPreferredLanguages:@[ @"ja-JP" ]]; + + EXPECT_NSEQ(acceptLangs, @"ja,en-US,en"); +} + +TEST(AcceptLangTest, Default) { + NSString* acceptLangs = + [Cronet getAcceptLanguagesFromPreferredLanguages:@[ @"lol-LOL" ]]; + + EXPECT_NSEQ(acceptLangs, @"en-US,en"); +} + +TEST(AcceptLangTest, Append) { + NSString* acceptLangs = + [Cronet getAcceptLanguagesFromPreferredLanguages:@[ @"ja-JP", @"en-GB" ]]; + + EXPECT_NSEQ(acceptLangs, @"ja,en-US,en,en-GB"); +} + +TEST(AcceptLangTest, NoDefaultAppend) { + NSString* acceptLangs = [Cronet + getAcceptLanguagesFromPreferredLanguages:@[ @"en-GB", @"lol-LOL" ]]; + + NSLog(@"%@", acceptLangs); + EXPECT_NSEQ(acceptLangs, @"en-GB,en-US,en"); +} +} diff --git a/src/components/cronet/ios/test/cronet_http_test.mm b/src/components/cronet/ios/test/cronet_http_test.mm new file mode 100644 index 0000000000..760f966b95 --- /dev/null +++ b/src/components/cronet/ios/test/cronet_http_test.mm @@ -0,0 +1,768 @@ +// 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. + +#import +#import + +#include + +#include "TargetConditionals.h" + +#include "base/location.h" +#include "base/logging.h" +#include "base/strings/sys_string_conversions.h" +#include "components/cronet/cronet_buildflags.h" +#include "components/cronet/ios/test/cronet_test_base.h" +#include "components/cronet/ios/test/start_cronet.h" +#include "components/cronet/testing/test_server/test_server.h" +#include "net/base/mac/url_conversions.h" +#include "net/base/net_errors.h" +#include "net/cert/mock_cert_verifier.h" +#include "net/test/quic_simple_test_server.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gtest_mac.h" + +#include "url/gurl.h" + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +namespace { + +// The buffer size of the stream for HTTPBodyStream post test. +const NSUInteger kRequestBodyBufferLength = 1024; + +// The buffer size of the stream for HTTPBodyStream post test when +// testing the stream buffered data size larger than the net stack internal +// buffer size. +const NSUInteger kLargeRequestBodyBufferLength = 100 * kRequestBodyBufferLength; + +// The body data write times for HTTPBodyStream post test. +const NSInteger kRequestBodyWriteTimes = 16; +} + +@interface StreamBodyRequestDelegate : NSObject +- (void)setOutputStream:(NSOutputStream*)outputStream; +- (NSMutableString*)requestBody; +@end +@implementation StreamBodyRequestDelegate { + NSOutputStream* _stream; + NSInteger _count; + + NSMutableString* _requestBody; +} + +- (instancetype)init { + _requestBody = [NSMutableString string]; + return self; +} + +- (void)setOutputStream:(NSOutputStream*)outputStream { + _stream = outputStream; +} + +- (NSMutableString*)requestBody { + return _requestBody; +} + +- (void)stream:(NSStream*)stream handleEvent:(NSStreamEvent)event { + ASSERT_EQ(stream, _stream); + switch (event) { + case NSStreamEventHasSpaceAvailable: { + if (_count < kRequestBodyWriteTimes) { + uint8_t buffer[kRequestBodyBufferLength]; + memset(buffer, 'a' + _count, kRequestBodyBufferLength); + NSUInteger bytes_write = + [_stream write:buffer maxLength:kRequestBodyBufferLength]; + ASSERT_EQ(kRequestBodyBufferLength, bytes_write); + [_requestBody appendString:[[NSString alloc] + initWithBytes:buffer + length:kRequestBodyBufferLength + encoding:NSUTF8StringEncoding]]; + ++_count; + } else { + [_stream close]; + } + break; + } + case NSStreamEventErrorOccurred: + case NSStreamEventEndEncountered: { + [_stream close]; + [_stream setDelegate:nil]; + [_stream removeFromRunLoop:[NSRunLoop currentRunLoop] + forMode:NSDefaultRunLoopMode]; + break; + } + default: + break; + } +} +@end + +namespace cronet { +const char kUserAgent[] = "CronetTest/1.0.0.0"; + +class HttpTest : public CronetTestBase { + protected: + HttpTest() {} + ~HttpTest() override {} + + void SetUp() override { + CronetTestBase::SetUp(); + TestServer::Start(); + + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + return YES; + }]; + StartCronet(net::QuicSimpleTestServer::GetPort()); + [Cronet registerHttpProtocolHandler]; + NSURLSessionConfiguration* config = + [NSURLSessionConfiguration ephemeralSessionConfiguration]; + config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + [Cronet installIntoSessionConfiguration:config]; + session_ = [NSURLSession sessionWithConfiguration:config + delegate:delegate_ + delegateQueue:nil]; + } + + void TearDown() override { + TestServer::Shutdown(); + + [Cronet stopNetLog]; + [Cronet shutdownForTesting]; + CronetTestBase::TearDown(); + } + + NSURLSession* session_; +}; + +TEST_F(HttpTest, CreateSslKeyLogFile) { + // Shutdown Cronet so that it can be restarted with specific configuration + // (SSL key log file specified in experimental options) for this one test. + // This is necessary because SslKeyLogFile can only be set once, before any + // SSL Client Sockets are created. + + [Cronet shutdownForTesting]; + + NSString* ssl_key_log_file = [Cronet getNetLogPathForFile:@"SSLKEYLOGFILE"]; + + // Ensure that the keylog file doesn't exist. + [[NSFileManager defaultManager] removeItemAtPath:ssl_key_log_file error:nil]; + + [Cronet setExperimentalOptions: + [NSString stringWithFormat:@"{\"ssl_key_log_file\":\"%@\"}", + ssl_key_log_file]]; + + StartCronet(net::QuicSimpleTestServer::GetPort()); + + bool ssl_file_created = + [[NSFileManager defaultManager] fileExistsAtPath:ssl_key_log_file]; + + [[NSFileManager defaultManager] removeItemAtPath:ssl_key_log_file error:nil]; + + [Cronet shutdownForTesting]; + [Cronet setExperimentalOptions:@""]; + + EXPECT_TRUE(ssl_file_created); +} + +TEST_F(HttpTest, NSURLSessionReceivesData) { + NSURL* url = net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + __block BOOL block_used = NO; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ([request URL], url); + return YES; + }]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_TRUE(block_used); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_EQ(net::QuicSimpleTestServer::GetSimpleBodyValue(), + base::SysNSStringToUTF8([delegate_ responseBody])); +} + +// https://crbug.com/830005 Disable histogram support to reduce binary size. +#if BUILDFLAG(DISABLE_HISTOGRAM_SUPPORT) +#define MAYBE_GetGlobalMetricsDeltas DISABLED_GetGlobalMetricsDeltas +#else // BUILDFLAG(DISABLE_HISTOGRAM_SUPPORT) +#define MAYBE_GetGlobalMetricsDeltas GetGlobalMetricsDeltas +#endif // BUILDFLAG(DISABLE_HISTOGRAM_SUPPORT) +TEST_F(HttpTest, MAYBE_GetGlobalMetricsDeltas) { + NSData* delta1 = [Cronet getGlobalMetricsDeltas]; + NSURL* url = net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_EQ(net::QuicSimpleTestServer::GetSimpleBodyValue(), + base::SysNSStringToUTF8([delegate_ responseBody])); + + NSData* delta2 = [Cronet getGlobalMetricsDeltas]; + EXPECT_FALSE([delta2 isEqualToData:delta1]); +} + +TEST_F(HttpTest, SdchDisabledByDefault) { + NSURL* url = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("Accept-Encoding"))); + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_FALSE([[delegate_ responseBody] containsString:@"sdch"]); +} + +// Verify that explictly setting Accept-Encoding request header to 'gzip,sdch" +// is passed to the server and does not trigger any failures. This behavior may +// In the future Cronet may not allow caller to set Accept-Encoding header and +// could limit it to set of internally suported and enabled encodings, matching +// behavior of Cronet on Android. +TEST_F(HttpTest, AcceptEncodingSdchIsAllowed) { + NSURL* url = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("Accept-Encoding"))); + NSMutableURLRequest* mutableRequest = + [[NSURLRequest requestWithURL:url] mutableCopy]; + [mutableRequest addValue:@"gzip,sdch" forHTTPHeaderField:@"Accept-Encoding"]; + NSURLSessionDataTask* task = [session_ dataTaskWithRequest:mutableRequest]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_TRUE([[delegate_ responseBody] containsString:@"gzip,sdch"]); +} + +// Verify that explictly setting Accept-Encoding request header to 'foo,bar" +// is passed to the server and does not trigger any failures. This behavior may +// In the future Cronet may not allow caller to set Accept-Encoding header and +// could limit it to set of internally suported and enabled encodings, matching +// behavior of Cronet on Android. +TEST_F(HttpTest, AcceptEncodingFooBarIsAllowed) { + NSURL* url = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("Accept-Encoding"))); + NSMutableURLRequest* mutableRequest = + [[NSURLRequest requestWithURL:url] mutableCopy]; + [mutableRequest addValue:@"foo,bar" forHTTPHeaderField:@"Accept-Encoding"]; + NSURLSessionDataTask* task = [session_ dataTaskWithRequest:mutableRequest]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_TRUE([[delegate_ responseBody] containsString:@"foo,bar"]); +} + +TEST_F(HttpTest, NSURLSessionAcceptLanguage) { + NSURL* url = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("Accept-Language"))); + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + ASSERT_STREQ("en-US,en", + base::SysNSStringToUTF8([delegate_ responseBody]).c_str()); +} + +TEST_F(HttpTest, SetUserAgentIsExact) { + NSURL* url = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("User-Agent"))); + [Cronet setRequestFilterBlock:nil]; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_STREQ(kUserAgent, + base::SysNSStringToUTF8([delegate_ responseBody]).c_str()); +} + +TEST_F(HttpTest, SetUserAgentIsAllowed) { + NSURL* url = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("User-Agent"))); + NSMutableURLRequest* mutableRequest = + [[NSURLRequest requestWithURL:url] mutableCopy]; + [mutableRequest addValue:@"foo,bar" forHTTPHeaderField:@"User-Agent"]; + NSURLSessionDataTask* task = [session_ dataTaskWithRequest:mutableRequest]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_TRUE([[delegate_ responseBody] containsString:@"foo,bar"]); + + // Now check to see if the User-Agent string is restored to default when + // creating a new task from a request. + mutableRequest = [[NSURLRequest requestWithURL:url] mutableCopy]; + task = [session_ dataTaskWithRequest:mutableRequest]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_STREQ(kUserAgent, + base::SysNSStringToUTF8([delegate_ responseBody]).c_str()); +} + +TEST_F(HttpTest, SetCookie) { + const char kCookieHeader[] = "Cookie"; + NSString* cookieName = + [NSString stringWithFormat:@"SetCookie-%@", [[NSUUID UUID] UUIDString]]; + NSString* cookieValue = [[NSUUID UUID] UUIDString]; + NSString* cookieLine = + [NSString stringWithFormat:@"%@=%@", cookieName, cookieValue]; + NSHTTPCookieStorage* systemCookieStorage = + [NSHTTPCookieStorage sharedHTTPCookieStorage]; + NSURL* cookieUrl = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL(kCookieHeader))); + // Verify that cookie is not set in system storage. + for (NSHTTPCookie* cookie in [systemCookieStorage cookiesForURL:cookieUrl]) { + EXPECT_FALSE([[cookie name] isEqualToString:cookieName]); + } + + StartDataTaskAndWaitForCompletion([session_ dataTaskWithURL:cookieUrl]); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_STREQ("Header not found. :(", + base::SysNSStringToUTF8([delegate_ responseBody]).c_str()); + + NSURL* setCookieUrl = net::NSURLWithGURL( + GURL(TestServer::GetSetCookieURL(base::SysNSStringToUTF8(cookieLine)))); + StartDataTaskAndWaitForCompletion([session_ dataTaskWithURL:setCookieUrl]); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_TRUE([[delegate_ responseBody] containsString:cookieLine]); + + StartDataTaskAndWaitForCompletion([session_ dataTaskWithURL:cookieUrl]); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_TRUE([[delegate_ responseBody] containsString:cookieLine]); + + // Verify that cookie is set in system storage. + NSHTTPCookie* systemCookie = nil; + for (NSHTTPCookie* cookie in [systemCookieStorage cookiesForURL:cookieUrl]) { + if ([cookie.name isEqualToString:cookieName]) { + systemCookie = cookie; + break; + } + } + EXPECT_TRUE([[systemCookie value] isEqualToString:cookieValue]); + [systemCookieStorage deleteCookie:systemCookie]; +} + +TEST_F(HttpTest, SetSystemCookie) { + const char kCookieHeader[] = "Cookie"; + NSString* cookieName = [NSString + stringWithFormat:@"SetSystemCookie-%@", [[NSUUID UUID] UUIDString]]; + NSString* cookieValue = [[NSUUID UUID] UUIDString]; + NSHTTPCookieStorage* systemCookieStorage = + [NSHTTPCookieStorage sharedHTTPCookieStorage]; + NSURL* echoCookieUrl = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL(kCookieHeader))); + NSHTTPCookie* systemCookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookiePath : [echoCookieUrl path], + NSHTTPCookieName : cookieName, + NSHTTPCookieValue : cookieValue, + NSHTTPCookieDomain : [echoCookieUrl host], + }]; + [systemCookieStorage setCookie:systemCookie]; + + StartDataTaskAndWaitForCompletion([session_ dataTaskWithURL:echoCookieUrl]); + [systemCookieStorage deleteCookie:systemCookie]; + EXPECT_EQ(nil, [delegate_ error]); + // Verify that cookie set in system store was sent to the serever. + EXPECT_TRUE([[delegate_ responseBody] containsString:cookieName]); + EXPECT_TRUE([[delegate_ responseBody] containsString:cookieValue]); +} + +TEST_F(HttpTest, SystemCookieWithNullCreationTime) { + const char kCookieHeader[] = "Cookie"; + NSString* cookieName = [NSString + stringWithFormat:@"SetSystemCookie-%@", [[NSUUID UUID] UUIDString]]; + NSString* cookieValue = [[NSUUID UUID] UUIDString]; + NSHTTPCookieStorage* systemCookieStorage = + [NSHTTPCookieStorage sharedHTTPCookieStorage]; + NSURL* echoCookieUrl = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL(kCookieHeader))); + NSHTTPCookie* nullCreationTimeCookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookiePath : [echoCookieUrl path], + NSHTTPCookieName : cookieName, + NSHTTPCookieValue : cookieValue, + NSHTTPCookieDomain : [echoCookieUrl host], + @"Created" : [NSNumber numberWithDouble:0.0], + }]; + [systemCookieStorage setCookie:nullCreationTimeCookie]; + NSHTTPCookie* normalCookie = [NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookiePath : [echoCookieUrl path], + NSHTTPCookieName : [cookieName stringByAppendingString:@"-normal"], + NSHTTPCookieValue : cookieValue, + NSHTTPCookieDomain : [echoCookieUrl host], + }]; + [systemCookieStorage setCookie:normalCookie]; + StartDataTaskAndWaitForCompletion([session_ dataTaskWithURL:echoCookieUrl]); + [systemCookieStorage deleteCookie:nullCreationTimeCookie]; + [systemCookieStorage deleteCookie:normalCookie]; + EXPECT_EQ(nil, [delegate_ error]); + // Verify that cookie set in system store was sent to the serever. + EXPECT_TRUE([[delegate_ responseBody] containsString:cookieName]); + EXPECT_TRUE([[delegate_ responseBody] containsString:cookieValue]); +} + +TEST_F(HttpTest, FilterOutRequest) { + NSURL* url = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("User-Agent"))); + __block BOOL block_used = NO; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ([request URL], url); + return NO; + }]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_TRUE(block_used); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_FALSE([[delegate_ responseBody] + containsString:base::SysUTF8ToNSString(kUserAgent)]); + EXPECT_TRUE([[delegate_ responseBody] containsString:@"CFNetwork"]); +} + +TEST_F(HttpTest, FileSchemeNotSupported) { + NSString* fileData = @"Hello, World!"; + NSString* documentsDirectory = [NSSearchPathForDirectoriesInDomains( + NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + NSString* filePath = [documentsDirectory + stringByAppendingPathComponent:[[NSProcessInfo processInfo] + globallyUniqueString]]; + [fileData writeToFile:filePath + atomically:YES + encoding:NSUTF8StringEncoding + error:nil]; + + NSURL* url = [NSURL fileURLWithPath:filePath]; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + EXPECT_TRUE(false) << "Block should not be called for unsupported requests"; + return YES; + }]; + StartDataTaskAndWaitForCompletion(task); + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_TRUE([[delegate_ responseBody] containsString:fileData]); +} + +TEST_F(HttpTest, DataSchemeNotSupported) { + NSString* testString = @"Hello, World!"; + NSData* testData = [testString dataUsingEncoding:NSUTF8StringEncoding]; + NSString* dataString = + [NSString stringWithFormat:@"data:text/plain;base64,%@", + [testData base64EncodedStringWithOptions:0]]; + NSURL* url = [NSURL URLWithString:dataString]; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + EXPECT_TRUE(false) << "Block should not be called for unsupported requests"; + return YES; + }]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_TRUE([[delegate_ responseBody] containsString:testString]); +} + +TEST_F(HttpTest, BrotliAdvertisedTest) { + [Cronet shutdownForTesting]; + + [Cronet setBrotliEnabled:YES]; + + StartCronet(net::QuicSimpleTestServer::GetPort()); + + NSURL* url = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("Accept-Encoding"))); + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_TRUE([[delegate_ responseBody] containsString:@"br"]); +} + +TEST_F(HttpTest, BrotliNotAdvertisedTest) { + [Cronet shutdownForTesting]; + + [Cronet setBrotliEnabled:NO]; + + StartCronet(net::QuicSimpleTestServer::GetPort()); + + NSURL* url = + net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("Accept-Encoding"))); + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_FALSE([[delegate_ responseBody] containsString:@"br"]); +} + +TEST_F(HttpTest, BrotliHandleDecoding) { + [Cronet shutdownForTesting]; + + [Cronet setBrotliEnabled:YES]; + + StartCronet(net::QuicSimpleTestServer::GetPort()); + + NSURL* url = + net::NSURLWithGURL(GURL(TestServer::GetUseEncodingURL("brotli"))); + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_STREQ(base::SysNSStringToUTF8([delegate_ responseBody]).c_str(), + "The quick brown fox jumps over the lazy dog"); +} + +TEST_F(HttpTest, PostRequest) { + // Create request body. + NSString* request_body = [NSString stringWithFormat:@"Post Data %i", rand()]; + NSData* post_data = [request_body dataUsingEncoding:NSUTF8StringEncoding]; + + // Prepare the request. + NSURL* url = net::NSURLWithGURL(GURL(TestServer::GetEchoRequestBodyURL())); + NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url]; + request.HTTPMethod = @"POST"; + request.HTTPBody = post_data; + + // Set the request filter to check that the request was handled by the Cronet + // stack. + __block BOOL block_used = NO; + [Cronet setRequestFilterBlock:^(NSURLRequest* req) { + block_used = YES; + EXPECT_EQ([req URL], url); + return YES; + }]; + + // Send the request and wait for the response. + NSURLSessionDataTask* data_task = [session_ dataTaskWithRequest:request]; + StartDataTaskAndWaitForCompletion(data_task); + + // Verify that the response from the server matches the request body. + NSString* response_body = [delegate_ responseBody]; + ASSERT_EQ(nil, [delegate_ error]); + ASSERT_STREQ(base::SysNSStringToUTF8(request_body).c_str(), + base::SysNSStringToUTF8(response_body).c_str()); + ASSERT_TRUE(block_used); +} + +TEST_F(HttpTest, PostRequestWithLargeBody) { + // Create request body. + std::string request_body(kLargeRequestBodyBufferLength, 'z'); + NSData* post_data = [NSData dataWithBytes:request_body.c_str() + length:request_body.length()]; + + // Prepare the request. + NSURL* url = net::NSURLWithGURL(GURL(TestServer::GetEchoRequestBodyURL())); + NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url]; + request.HTTPMethod = @"POST"; + request.HTTPBody = post_data; + + // Set the request filter to check that the request was handled by the Cronet + // stack. + __block BOOL block_used = NO; + [Cronet setRequestFilterBlock:^(NSURLRequest* req) { + block_used = YES; + EXPECT_EQ([req URL], url); + return YES; + }]; + + // Send the request and wait for the response. + NSURLSessionDataTask* data_task = [session_ dataTaskWithRequest:request]; + StartDataTaskAndWaitForCompletion(data_task); + + // Verify that the response from the server matches the request body. + NSString* response_body = [delegate_ responseBody]; + ASSERT_EQ(nil, [delegate_ error]); + ASSERT_STREQ(request_body.c_str(), + base::SysNSStringToUTF8(response_body).c_str()); + ASSERT_TRUE(block_used); +} + +// Verify the chunked request body upload function. +TEST_F(HttpTest, PostRequestWithBodyStream) { + // Create request body stream. + CFReadStreamRef read_stream = NULL; + CFWriteStreamRef write_stream = NULL; + CFStreamCreateBoundPair(NULL, &read_stream, &write_stream, + kRequestBodyBufferLength); + + NSInputStream* input_stream = CFBridgingRelease(read_stream); + NSOutputStream* output_stream = CFBridgingRelease(write_stream); + + StreamBodyRequestDelegate* stream_delegate = + [[StreamBodyRequestDelegate alloc] init]; + output_stream.delegate = stream_delegate; + [stream_delegate setOutputStream:output_stream]; + + dispatch_queue_t queue = + dispatch_queue_create("data upload queue", DISPATCH_QUEUE_SERIAL); + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_async(queue, ^{ + [output_stream scheduleInRunLoop:[NSRunLoop currentRunLoop] + forMode:NSDefaultRunLoopMode]; + [output_stream open]; + + [[NSRunLoop currentRunLoop] + runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10.0]]; + + dispatch_semaphore_signal(semaphore); + }); + + // Prepare the request. + NSURL* url = net::NSURLWithGURL(GURL(TestServer::GetEchoRequestBodyURL())); + NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url]; + request.HTTPMethod = @"POST"; + request.HTTPBodyStream = input_stream; + + // Set the request filter to check that the request was handled by the Cronet + // stack. + __block BOOL block_used = NO; + [Cronet setRequestFilterBlock:^(NSURLRequest* req) { + block_used = YES; + EXPECT_EQ([req URL], url); + return YES; + }]; + + // Send the request and wait for the response. + NSURLSessionDataTask* data_task = [session_ dataTaskWithRequest:request]; + StartDataTaskAndWaitForCompletion(data_task); + + // Verify that the response from the server matches the request body. + ASSERT_EQ(nil, [delegate_ error]); + NSString* response_body = [delegate_ responseBody]; + NSMutableString* request_body = [stream_delegate requestBody]; + ASSERT_STREQ(base::SysNSStringToUTF8(request_body).c_str(), + base::SysNSStringToUTF8(response_body).c_str()); + ASSERT_TRUE(block_used); + + // Wait for the run loop of the child thread exits. Timeout is 5 seconds. + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC); + ASSERT_EQ(0, dispatch_semaphore_wait(semaphore, timeout)); +} + +// Verify that the chunked data uploader can correctly handle the request body +// if the stream contains data length exceed the internal upload buffer. +TEST_F(HttpTest, PostRequestWithLargeBodyStream) { + // Create request body stream. + CFReadStreamRef read_stream = NULL; + CFWriteStreamRef write_stream = NULL; + // 100KB data is written in one time. + CFStreamCreateBoundPair(NULL, &read_stream, &write_stream, + kLargeRequestBodyBufferLength); + + NSInputStream* input_stream = CFBridgingRelease(read_stream); + NSOutputStream* output_stream = CFBridgingRelease(write_stream); + [output_stream open]; + + uint8_t buffer[kLargeRequestBodyBufferLength]; + memset(buffer, 'a', kLargeRequestBodyBufferLength); + NSUInteger bytes_write = + [output_stream write:buffer maxLength:kLargeRequestBodyBufferLength]; + ASSERT_EQ(kLargeRequestBodyBufferLength, bytes_write); + [output_stream close]; + + // Prepare the request. + NSURL* url = net::NSURLWithGURL(GURL(TestServer::GetEchoRequestBodyURL())); + NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:url]; + request.HTTPMethod = @"POST"; + request.HTTPBodyStream = input_stream; + + // Set the request filter to check that the request was handled by the Cronet + // stack. + __block BOOL block_used = NO; + [Cronet setRequestFilterBlock:^(NSURLRequest* req) { + block_used = YES; + EXPECT_EQ([req URL], url); + return YES; + }]; + + // Send the request and wait for the response. + NSURLSessionDataTask* data_task = [session_ dataTaskWithRequest:request]; + StartDataTaskAndWaitForCompletion(data_task); + + // Verify that the response from the server matches the request body. + ASSERT_EQ(nil, [delegate_ error]); + NSString* response_body = [delegate_ responseBody]; + ASSERT_EQ(kLargeRequestBodyBufferLength, [response_body length]); + ASSERT_TRUE(block_used); +} + +// iOS Simulator doesn't support changing thread priorities. +// Therefore, run these tests only on a physical device. +#if TARGET_OS_SIMULATOR +#define MAYBE_ChangeThreadPriorityAfterStart \ + DISABLED_ChangeThreadPriorityAfterStart +#define MAYBE_ChangeThreadPriorityBeforeStart \ + DISABLED_ChangeThreadPriorityBeforeStart +#else +#define MAYBE_ChangeThreadPriorityAfterStart ChangeThreadPriorityAfterStart +#define MAYBE_ChangeThreadPriorityBeforeStart ChangeThreadPriorityBeforeStart +#endif // TARGET_OS_SIMULATOR + +// Tests that the network thread priority can be changed after +// Cronet has been started. +TEST_F(HttpTest, MAYBE_ChangeThreadPriorityAfterStart) { + // Get current (default) priority of the network thread. + __block double default_priority; + PostBlockToNetworkThread(FROM_HERE, ^{ + default_priority = NSThread.threadPriority; + }); + + // Modify the network thread priority. + const double new_priority = 1.0; + [Cronet setNetworkThreadPriority:new_priority]; + + // Get modified priority of the network thread. + dispatch_semaphore_t lock = dispatch_semaphore_create(0); + __block double actual_priority; + PostBlockToNetworkThread(FROM_HERE, ^{ + actual_priority = NSThread.threadPriority; + dispatch_semaphore_signal(lock); + }); + + // Wait until the posted tasks are completed. + dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); + + EXPECT_EQ(0.5, default_priority); + + // Check that the priority was modified and is close to the set priority. + EXPECT_TRUE(abs(actual_priority - new_priority) < 0.01) + << "Unexpected thread priority. Expected " << new_priority << " but got " + << actual_priority; +} + +// Tests that the network thread priority can be changed before +// Cronet has been started. +TEST_F(HttpTest, MAYBE_ChangeThreadPriorityBeforeStart) { + // Start a new Cronet engine modifying the network thread priority before the + // start. + [Cronet shutdownForTesting]; + const double new_priority = 0.8; + [Cronet setNetworkThreadPriority:new_priority]; + [Cronet start]; + + // Get modified priority of the network thread. + dispatch_semaphore_t lock = dispatch_semaphore_create(0); + __block double actual_priority; + PostBlockToNetworkThread(FROM_HERE, ^{ + actual_priority = NSThread.threadPriority; + dispatch_semaphore_signal(lock); + }); + + // Wait until the posted task is completed. + dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); + + // Check that the priority was modified and is close to the set priority. + EXPECT_TRUE(abs(actual_priority - new_priority) < 0.01) + << "Unexpected thread priority. Expected " << new_priority << " but got " + << actual_priority; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +TEST_F(HttpTest, LegacyApi) { + NSURL* url = net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + + __block BOOL block_used = NO; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return YES; + }]; + + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + NSError* err; + NSHTTPURLResponse* response; + [NSURLConnection sendSynchronousRequest:request + returningResponse:&response + error:&err]; + + EXPECT_EQ(200, [response statusCode]); + EXPECT_TRUE(block_used); + EXPECT_FALSE(err); +} +#pragma clang diagnostic pop + +} // namespace cronet diff --git a/src/components/cronet/ios/test/cronet_metrics_test.mm b/src/components/cronet/ios/test/cronet_metrics_test.mm new file mode 100644 index 0000000000..dac5ca5996 --- /dev/null +++ b/src/components/cronet/ios/test/cronet_metrics_test.mm @@ -0,0 +1,433 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#include "base/strings/sys_string_conversions.h" +#include "components/cronet/ios/test/cronet_test_base.h" +#include "components/cronet/ios/test/start_cronet.h" +#include "components/cronet/testing/test_server/test_server.h" +#import "net/base/mac/url_conversions.h" +#include "net/test/quic_simple_test_server.h" +#include "testing/gtest_mac.h" +#include "url/gurl.h" + +// Forward declaration of class in cronet_metrics.h for testing. +NS_AVAILABLE_IOS(10.0) +@interface CronetTransactionMetrics : NSURLSessionTaskTransactionMetrics +@end + +namespace cronet { + +class CronetMetricsTest : public CronetTestBase { + protected: + void SetUpWithMetrics(BOOL metrics_enabled) { + TestServer::Start(); + + [Cronet setMetricsEnabled:metrics_enabled]; + StartCronet(net::QuicSimpleTestServer::GetPort()); + + [Cronet registerHttpProtocolHandler]; + NSURLSessionConfiguration* config = + [NSURLSessionConfiguration ephemeralSessionConfiguration]; + config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + [Cronet installIntoSessionConfiguration:config]; + session_ = [NSURLSession sessionWithConfiguration:config + delegate:delegate_ + delegateQueue:nil]; + } + + void TearDown() override { + [Cronet shutdownForTesting]; + + TestServer::Shutdown(); + CronetTestBase::TearDown(); + } + + NSURLSession* session_; +}; + +class CronetEnabledMetricsTest : public CronetMetricsTest { + protected: + void SetUp() override { + CronetMetricsTest::SetUp(); + SetUpWithMetrics(YES); + } +}; + +class CronetDisabledMetricsTest : public CronetMetricsTest { + protected: + void SetUp() override { + CronetMetricsTest::SetUp(); + SetUpWithMetrics(NO); + } +}; + +// Tests that metrics data is sane for a QUIC request. +TEST_F(CronetEnabledMetricsTest, ProtocolIsQuic) { + if (@available(iOS 10.2, *)) { + NSURL* url = net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + + __block BOOL block_used = NO; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return YES; + }]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_TRUE(block_used); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_EQ(net::QuicSimpleTestServer::GetSimpleBodyValue(), + base::SysNSStringToUTF8([delegate_ responseBody])); + + NSURLSessionTaskMetrics* task_metrics = delegate_.taskMetrics; + ASSERT_TRUE(task_metrics); + ASSERT_EQ(1lU, task_metrics.transactionMetrics.count); + NSURLSessionTaskTransactionMetrics* metrics = + task_metrics.transactionMetrics.firstObject; + EXPECT_TRUE([metrics isMemberOfClass:[CronetTransactionMetrics class]]); + + // Confirm that metrics data is the correct type. + EXPECT_TRUE([metrics.fetchStartDate isKindOfClass:[NSDate class]]); + EXPECT_TRUE([metrics.domainLookupStartDate isKindOfClass:[NSDate class]]); + EXPECT_TRUE([metrics.domainLookupEndDate isKindOfClass:[NSDate class]]); + EXPECT_TRUE([metrics.connectStartDate isKindOfClass:[NSDate class]]); + EXPECT_TRUE( + [metrics.secureConnectionStartDate isKindOfClass:[NSDate class]]); + EXPECT_TRUE([metrics.secureConnectionEndDate isKindOfClass:[NSDate class]]); + EXPECT_TRUE([metrics.connectEndDate isKindOfClass:[NSDate class]]); + EXPECT_TRUE([metrics.requestStartDate isKindOfClass:[NSDate class]]); + EXPECT_TRUE([metrics.requestEndDate isKindOfClass:[NSDate class]]); + EXPECT_TRUE([metrics.responseStartDate isKindOfClass:[NSDate class]]); + EXPECT_TRUE([metrics.responseEndDate isKindOfClass:[NSDate class]]); + EXPECT_TRUE([metrics.networkProtocolName isKindOfClass:[NSString class]]); + + // Confirm that the metrics values are sane. + EXPECT_NE(NSOrderedDescending, [metrics.domainLookupStartDate + compare:metrics.domainLookupEndDate]); + EXPECT_NE(NSOrderedDescending, + [metrics.connectStartDate compare:metrics.connectEndDate]); + EXPECT_NE(NSOrderedDescending, + [metrics.secureConnectionStartDate + compare:metrics.secureConnectionEndDate]); + EXPECT_NE(NSOrderedDescending, + [metrics.requestStartDate compare:metrics.requestEndDate]); + EXPECT_NE(NSOrderedDescending, + [metrics.responseStartDate compare:metrics.responseEndDate]); + + EXPECT_FALSE(metrics.proxyConnection); + + EXPECT_TRUE([metrics.networkProtocolName containsString:@"quic"] || + [metrics.networkProtocolName containsString:@"h3"]) + << base::SysNSStringToUTF8(metrics.networkProtocolName); + } +} + +// Tests that metrics data is sane for an HTTP/1.1 request. +TEST_F(CronetEnabledMetricsTest, ProtocolIsNotQuic) { + if (@available(iOS 10.2, *)) { + NSURL* url = net::NSURLWithGURL(GURL(TestServer::GetSimpleURL())); + + __block BOOL block_used = NO; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return YES; + }]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_TRUE(block_used); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_STREQ("The quick brown fox jumps over the lazy dog.", + base::SysNSStringToUTF8([delegate_ responseBody]).c_str()); + + NSURLSessionTaskMetrics* task_metrics = delegate_.taskMetrics; + ASSERT_TRUE(task_metrics); + ASSERT_EQ(1lU, task_metrics.transactionMetrics.count); + NSURLSessionTaskTransactionMetrics* metrics = + task_metrics.transactionMetrics.firstObject; + EXPECT_TRUE([metrics isMemberOfClass:[CronetTransactionMetrics class]]); + + EXPECT_NSEQ(metrics.networkProtocolName, @"http/1.1"); + } +} + +// Tests that Cronet provides similar metrics data to iOS. +TEST_F(CronetEnabledMetricsTest, PlatformComparison) { + if (@available(iOS 10.2, *)) { + NSURL* url = net::NSURLWithGURL(GURL(TestServer::GetSimpleURL())); + + // Perform a connection using Cronet. + + __block BOOL block_used = NO; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return YES; + }]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_TRUE(block_used); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_STREQ("The quick brown fox jumps over the lazy dog.", + base::SysNSStringToUTF8([delegate_ responseBody]).c_str()); + + NSURLSessionTaskMetrics* cronet_task_metrics = delegate_.taskMetrics; + ASSERT_TRUE(cronet_task_metrics); + ASSERT_EQ(1lU, cronet_task_metrics.transactionMetrics.count); + NSURLSessionTaskTransactionMetrics* cronet_metrics = + cronet_task_metrics.transactionMetrics.firstObject; + + // Perform a connection using the platform stack. + + block_used = NO; + task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return NO; + }]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_TRUE(block_used); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_STREQ("The quick brown fox jumps over the lazy dog.", + base::SysNSStringToUTF8([delegate_ responseBody]).c_str()); + + NSURLSessionTaskMetrics* platform_task_metrics = delegate_.taskMetrics; + ASSERT_TRUE(platform_task_metrics); + ASSERT_EQ(1lU, platform_task_metrics.transactionMetrics.count); + NSURLSessionTaskTransactionMetrics* platform_metrics = + platform_task_metrics.transactionMetrics.firstObject; + + // Compare platform and Cronet metrics data. + + EXPECT_NSEQ(cronet_metrics.networkProtocolName, + platform_metrics.networkProtocolName); + } +} + +// Tests that the metrics API behaves sanely when making a request to an +// invalid URL. +TEST_F(CronetEnabledMetricsTest, InvalidURL) { + if (@available(iOS 10.2, *)) { + NSURL* url = net::NSURLWithGURL(GURL("http://notfound.example.com")); + + __block BOOL block_used = NO; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return YES; + }]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_TRUE(block_used); + EXPECT_TRUE([delegate_ error]); + + NSURLSessionTaskMetrics* task_metrics = delegate_.taskMetrics; + ASSERT_TRUE(task_metrics); + ASSERT_EQ(1lU, task_metrics.transactionMetrics.count); + NSURLSessionTaskTransactionMetrics* metrics = + task_metrics.transactionMetrics.firstObject; + EXPECT_TRUE([metrics isMemberOfClass:[CronetTransactionMetrics class]]); + + EXPECT_TRUE(metrics.fetchStartDate); + EXPECT_FALSE(metrics.domainLookupStartDate); + EXPECT_FALSE(metrics.domainLookupEndDate); + EXPECT_FALSE(metrics.connectStartDate); + EXPECT_FALSE(metrics.secureConnectionStartDate); + EXPECT_FALSE(metrics.secureConnectionEndDate); + EXPECT_FALSE(metrics.connectEndDate); + EXPECT_FALSE(metrics.requestStartDate); + EXPECT_FALSE(metrics.requestEndDate); + EXPECT_FALSE(metrics.responseStartDate); + } +} + +// Tests that the metrics API behaves sanely when the request is canceled. +TEST_F(CronetEnabledMetricsTest, CanceledRequest) { + if (@available(iOS 10.2, *)) { + NSURL* url = net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + + __block BOOL block_used = NO; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return YES; + }]; + + StartDataTaskAndWaitForCompletion(task, 1); + [task cancel]; + + EXPECT_TRUE(block_used); + EXPECT_NE(nil, [delegate_ error]); + } +} + +// Tests the metrics data for a reused connection is correct. +TEST_F(CronetEnabledMetricsTest, ReusedConnection) { + if (@available(iOS 10.2, *)) { + NSURL* url = net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + + __block BOOL block_used = NO; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return YES; + }]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_TRUE(block_used); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_EQ(net::QuicSimpleTestServer::GetSimpleBodyValue(), + base::SysNSStringToUTF8([delegate_ responseBody])); + + NSURLSessionTaskMetrics* task_metrics = [delegate_ taskMetrics]; + ASSERT_TRUE(task_metrics); + ASSERT_EQ(1lU, task_metrics.transactionMetrics.count); + NSURLSessionTaskTransactionMetrics* metrics = + task_metrics.transactionMetrics.firstObject; + EXPECT_TRUE([metrics isMemberOfClass:[CronetTransactionMetrics class]]); + + // Second connection + + block_used = NO; + task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return YES; + }]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_TRUE(block_used); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_EQ(net::QuicSimpleTestServer::GetSimpleBodyValue(), + base::SysNSStringToUTF8([delegate_ responseBody])); + + task_metrics = delegate_.taskMetrics; + ASSERT_TRUE(task_metrics); + ASSERT_EQ(1lU, task_metrics.transactionMetrics.count); + metrics = task_metrics.transactionMetrics.firstObject; + + EXPECT_TRUE(metrics.isReusedConnection); + EXPECT_FALSE(metrics.domainLookupStartDate); + EXPECT_FALSE(metrics.domainLookupEndDate); + EXPECT_FALSE(metrics.connectStartDate); + EXPECT_FALSE(metrics.secureConnectionStartDate); + EXPECT_FALSE(metrics.secureConnectionEndDate); + EXPECT_FALSE(metrics.connectEndDate); + } +} + +// Checks that there is no crash if the session delegate is not set when a +// NSURLSession is created. Also checks that the internal metrics map is cleaned +// and contains 0 records at the end of the request. This is a regression test +// for http://crbug/834401. +TEST_F(CronetEnabledMetricsTest, SessionWithoutDelegate) { + if (@available(iOS 10.2, *)) { + NSURLSessionConfiguration* default_config = + [NSURLSessionConfiguration defaultSessionConfiguration]; + [Cronet installIntoSessionConfiguration:default_config]; + NSURLSession* default_session = + [NSURLSession sessionWithConfiguration:default_config]; + NSURL* url = net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + + __block BOOL no_error = NO; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + NSURLSessionDataTask* task = [default_session + dataTaskWithRequest:request + completionHandler:^(NSData* data, NSURLResponse* response, + NSError* error) { + EXPECT_TRUE(error == nil) + << base::SysNSStringToUTF8([error description]); + no_error = YES; + dispatch_semaphore_signal(semaphore); + }]; + __block BOOL block_used = NO; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return YES; + }]; + + [task resume]; + long wait_result = dispatch_semaphore_wait( + semaphore, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); + + // Check results + EXPECT_EQ(0, wait_result); + EXPECT_TRUE(block_used); + EXPECT_TRUE(no_error); + EXPECT_EQ(0UL, [Cronet getMetricsMapSize]); + } +} + +// Tests that the metrics disable switch works. +TEST_F(CronetDisabledMetricsTest, MetricsDisabled) { + if (@available(iOS 10.2, *)) { + NSURL* url = net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + + __block BOOL block_used = NO; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return YES; + }]; + StartDataTaskAndWaitForCompletion(task); + EXPECT_TRUE(block_used); + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_EQ(net::QuicSimpleTestServer::GetSimpleBodyValue(), + base::SysNSStringToUTF8([delegate_ responseBody])); + + NSURLSessionTaskMetrics* task_metrics = [delegate_ taskMetrics]; + ASSERT_TRUE(task_metrics); + ASSERT_EQ(1lU, task_metrics.transactionMetrics.count); + NSURLSessionTaskTransactionMetrics* metrics = + task_metrics.transactionMetrics.firstObject; + EXPECT_FALSE([metrics isMemberOfClass:[CronetTransactionMetrics class]]); + + EXPECT_TRUE(metrics.fetchStartDate); + EXPECT_FALSE(metrics.domainLookupStartDate); + EXPECT_FALSE(metrics.domainLookupEndDate); + EXPECT_FALSE(metrics.connectStartDate); + EXPECT_FALSE(metrics.secureConnectionStartDate); + EXPECT_FALSE(metrics.secureConnectionEndDate); + EXPECT_FALSE(metrics.connectEndDate); + EXPECT_FALSE(metrics.requestStartDate); + EXPECT_FALSE(metrics.requestEndDate); + EXPECT_FALSE(metrics.responseStartDate); + EXPECT_FALSE(metrics.responseEndDate); + EXPECT_FALSE(metrics.networkProtocolName); + } +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +TEST_F(CronetEnabledMetricsTest, LegacyApi) { + NSURL* url = net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + + __block BOOL block_used = NO; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ(request.URL, url); + return YES; + }]; + + NSURLRequest* request = [NSURLRequest requestWithURL:url]; + NSError* err; + NSHTTPURLResponse* response; + [NSURLConnection sendSynchronousRequest:request + returningResponse:&response + error:&err]; + + EXPECT_EQ(200, [response statusCode]); + EXPECT_TRUE(block_used); + EXPECT_FALSE(err); +} +#pragma clang diagnostic pop + +} // namespace cronet diff --git a/src/components/cronet/ios/test/cronet_netlog_test.mm b/src/components/cronet/ios/test/cronet_netlog_test.mm new file mode 100644 index 0000000000..3eabe0eeeb --- /dev/null +++ b/src/components/cronet/ios/test/cronet_netlog_test.mm @@ -0,0 +1,135 @@ +// Copyright 2016 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. + +#import +#import + +#include "components/cronet/ios/test/cronet_test_base.h" +#include "components/cronet/ios/test/start_cronet.h" +#include "net/test/quic_simple_test_server.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace cronet { + +class NetLogTest : public ::testing::Test { + protected: + NetLogTest() {} + ~NetLogTest() override {} + + void SetUp() override { StartCronet(net::QuicSimpleTestServer::GetPort()); } + + void TearDown() override { + [Cronet stopNetLog]; + [Cronet shutdownForTesting]; + } +}; + +TEST_F(NetLogTest, OpenFile) { + bool netlog_started = + [Cronet startNetLogToFile:@"cronet_netlog.json" logBytes:YES]; + + EXPECT_TRUE(netlog_started); +} + +TEST_F(NetLogTest, CreateFile) { + NSString* filename = [[[NSProcessInfo processInfo] globallyUniqueString] + stringByAppendingString:@"_netlog.json"]; + bool netlog_started = [Cronet startNetLogToFile:filename logBytes:YES]; + [Cronet stopNetLog]; + + bool file_created = [[NSFileManager defaultManager] + fileExistsAtPath:[Cronet getNetLogPathForFile:filename]]; + + [[NSFileManager defaultManager] + removeItemAtPath:[Cronet getNetLogPathForFile:filename] + error:nil]; + + EXPECT_TRUE(netlog_started); + EXPECT_TRUE(file_created); +} + +TEST_F(NetLogTest, NonExistantDir) { + NSString* notdir = [[[NSProcessInfo processInfo] globallyUniqueString] + stringByAppendingString:@"/netlog.json"]; + bool netlog_started = [Cronet startNetLogToFile:notdir logBytes:NO]; + + EXPECT_FALSE(netlog_started); +} + +TEST_F(NetLogTest, ExistantDir) { + NSString* dir = [[NSProcessInfo processInfo] globallyUniqueString]; + + bool dir_created = [[NSFileManager defaultManager] + createDirectoryAtPath:[Cronet getNetLogPathForFile:dir] + withIntermediateDirectories:NO + attributes:nil + error:nil]; + + bool netlog_started = + [Cronet startNetLogToFile:[dir stringByAppendingString:@"/netlog.json"] + logBytes:NO]; + + [Cronet stopNetLog]; + + [[NSFileManager defaultManager] + removeItemAtPath:[Cronet + getNetLogPathForFile: + [dir stringByAppendingString:@"/netlog.json"]] + error:nil]; + + [[NSFileManager defaultManager] + removeItemAtPath:[Cronet getNetLogPathForFile:dir] + error:nil]; + + EXPECT_TRUE(dir_created); + EXPECT_TRUE(netlog_started); +} + +TEST_F(NetLogTest, EmptyFilename) { + bool netlog_started = [Cronet startNetLogToFile:@"" logBytes:NO]; + + EXPECT_FALSE(netlog_started); +} + +TEST_F(NetLogTest, AbsoluteFilename) { + bool netlog_started = + [Cronet startNetLogToFile:@"/home/netlog.json" logBytes:NO]; + + EXPECT_FALSE(netlog_started); +} + +TEST_F(NetLogTest, ExperimentalOptions) { + [Cronet shutdownForTesting]; + NSString* netlog_file = @"cronet_netlog.json"; + NSString* netlog_path = [Cronet getNetLogPathForFile:netlog_file]; + + // Remove old netlog if any. + [[NSFileManager defaultManager] removeItemAtPath:netlog_path error:nil]; + + // Set experimental options and start the netlog. + [Cronet + setExperimentalOptions: + @"{ \"QUIC\" : {\"max_server_configs_stored_in_properties\" : 8} }"]; + + StartCronet(net::QuicSimpleTestServer::GetPort()); + bool netlog_started = + [Cronet startNetLogToFile:@"cronet_netlog.json" logBytes:NO]; + ASSERT_TRUE(netlog_started); + + // Stop the netlog and check that it contains the experimental options. + [Cronet stopNetLog]; + + NSError* error = nil; + NSString* netlog_content = + [NSString stringWithContentsOfFile:netlog_path + encoding:NSASCIIStringEncoding + error:&error]; + ASSERT_FALSE(error) << error.localizedDescription.UTF8String; + ASSERT_TRUE(netlog_content); + ASSERT_TRUE([netlog_content + containsString:@"\"cronetExperimentalParams\":{\"QUIC\":{\"max_server_" + @"configs_stored_in_properties\":8}}"]) + << "Netlog doesn't contain 'cronetExperimentalParams'."; +} +} diff --git a/src/components/cronet/ios/test/cronet_performance_test.mm b/src/components/cronet/ios/test/cronet_performance_test.mm new file mode 100644 index 0000000000..24a24b21b5 --- /dev/null +++ b/src/components/cronet/ios/test/cronet_performance_test.mm @@ -0,0 +1,275 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#include + +#include "base/logging.h" +#include "base/strings/stringprintf.h" +#include "base/strings/sys_string_conversions.h" +#include "components/cronet/ios/test/cronet_test_base.h" +#include "components/cronet/testing/test_server/test_server.h" +#include "net/base/mac/url_conversions.h" +#include "net/base/net_errors.h" +#include "net/cert/mock_cert_verifier.h" +#include "net/test/quic_simple_test_server.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gtest_mac.h" +#include "url/gurl.h" + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +namespace { + +const int kTestIterations = 10; +const BOOL kUseExternalUrl = NO; +const int kDownloadSize = 19307439; // used for internal server only +const char* kExternalUrl = "https://www.gstatic.com/chat/hangouts/bg/davec.jpg"; + +struct PerfResult { + NSTimeInterval total; + NSTimeInterval mean; + NSTimeInterval max; + int64_t total_bytes_downloaded; + int failed_requests; + int total_requests; +}; + +struct TestConfig { + BOOL quic; + BOOL http2; + BOOL akd4; + BOOL cronet; +}; + +bool operator<(TestConfig a, TestConfig b) { + return std::tie(a.quic, a.http2, a.akd4, a.cronet) < + std::tie(b.quic, b.http2, b.akd4, b.cronet); +} + +const TestConfig test_combinations[] = { + // QUIC HTTP2 AKD4 Cronet + { false, false, false, false, }, + { false, false, false, true, }, + { false, true, false, true, }, + { true, false, false, true, }, + { true, false, true, true, }, +}; + +} // namespace + +namespace cronet { + +class PerfTest : public CronetTestBase, + public ::testing::WithParamInterface { + public: + static void TearDownTestCase() { + NSMutableString* perf_data_acc = [NSMutableString stringWithCapacity:0]; + + LOG(INFO) << "Performance Data:"; + for (auto const& entry : perf_test_results) { + NSString* formatted_entry = [NSString + stringWithFormat: + @"Quic %i\tHttp2 %i\tAKD4 %i\tCronet %i: Mean: %fs " + @"(%fmbps)\tMax: " + @"%fs with %i fails out of %i total requests.", + entry.first.quic, entry.first.http2, entry.first.akd4, + entry.first.cronet, entry.second.mean, + entry.second.total ? 8 * entry.second.total_bytes_downloaded / + entry.second.total / 1e6 + : 0, + entry.second.max, entry.second.failed_requests, + entry.second.total_requests]; + + [perf_data_acc appendFormat:@"%@\n", formatted_entry]; + + LOG(INFO) << base::SysNSStringToUTF8(formatted_entry); + } + + NSString* filename = [NSString + stringWithFormat:@"performance_metrics-%@.txt", + [[NSDate date] + descriptionWithLocale:[NSLocale currentLocale]]]; + NSString* path = + [[[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory + inDomains:NSUserDomainMask] + lastObject] URLByAppendingPathComponent:filename] path]; + + NSData* filedata = [perf_data_acc dataUsingEncoding:NSUTF8StringEncoding]; + [[NSFileManager defaultManager] createFileAtPath:path + contents:filedata + attributes:nil]; + } + + protected: + static std::map perf_test_results; + + PerfTest() {} + ~PerfTest() override {} + + void SetUp() override { + CronetTestBase::SetUp(); + TestServer::Start(); + + // These are normally called by StartCronet(), but because of the test + // parameterization we need to call them inline, and not use StartCronet() + [Cronet setUserAgent:@"CronetTest/1.0.0.0" partial:NO]; + [Cronet setQuicEnabled:GetParam().quic]; + [Cronet setHttp2Enabled:GetParam().http2]; + [Cronet setAcceptLanguages:@"en-US,en"]; + if (kUseExternalUrl) { + NSString* external_host = [[NSURL + URLWithString:[NSString stringWithUTF8String:kExternalUrl]] host]; + [Cronet addQuicHint:external_host port:443 altPort:443]; + } else { + [Cronet addQuicHint:@"test.example.com" port:443 altPort:443]; + } + [Cronet enableTestCertVerifierForTesting]; + [Cronet setHttpCacheType:CRNHttpCacheTypeDisabled]; + if (GetParam().akd4) { + [Cronet setExperimentalOptions: + @"{\"QUIC\":{\"connection_options\":\"AKD4\"}}"]; + } + + [Cronet start]; + + NSString* rules = base::SysUTF8ToNSString( + base::StringPrintf("MAP test.example.com 127.0.0.1:%d," + "MAP notfound.example.com ~NOTFOUND", + net::QuicSimpleTestServer::GetPort())); + [Cronet setHostResolverRulesForTesting:rules]; + // This is the end of the behavior normally performed by StartCronet() + + NSURLSessionConfiguration* config = + [NSURLSessionConfiguration ephemeralSessionConfiguration]; + config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + if (GetParam().cronet) { + [Cronet registerHttpProtocolHandler]; + [Cronet installIntoSessionConfiguration:config]; + } else { + [Cronet unregisterHttpProtocolHandler]; + } + session_ = [NSURLSession sessionWithConfiguration:config + delegate:delegate_ + delegateQueue:nil]; + } + + void TearDown() override { + TestServer::Shutdown(); + + [Cronet shutdownForTesting]; + CronetTestBase::TearDown(); + } + + NSURLSession* session_; +}; + +// static +std::map PerfTest::perf_test_results; + +TEST_P(PerfTest, NSURLSessionReceivesImageLoop) { + int iterations = kTestIterations; + int failed_iterations = 0; + int64_t total_bytes_received = 0; + NSTimeInterval elapsed_total = 0; + NSTimeInterval elapsed_max = 0; + + int first_log = false; + + LOG(INFO) << "Running with parameters: " + << "QUIC: " << GetParam().quic << "\t" + << "HTTP2: " << GetParam().http2 << "\t" + << "AKD4: " << GetParam().akd4 << "\t" + << "Cronet: " << GetParam().cronet << "\t"; + + NSURL* url; + if (kUseExternalUrl) { + url = net::NSURLWithGURL(GURL(kExternalUrl)); + } else { + LOG(INFO) << "Downloading " << kDownloadSize << " bytes per iteration"; + url = + net::NSURLWithGURL(GURL(TestServer::PrepareBigDataURL(kDownloadSize))); + } + + for (int i = 0; i < iterations; ++i) { + __block BOOL block_used = NO; + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + block_used = YES; + EXPECT_EQ([request URL], url); + return YES; + }]; + + NSDate* start = [NSDate date]; + BOOL success = StartDataTaskAndWaitForCompletion(task); + + if (!success) { + [task cancel]; + } + + success = success && IsResponseSuccessful(task); + + NSTimeInterval elapsed = -[start timeIntervalSinceNow]; + + // Do not tolerate failures on internal server. + if (!kUseExternalUrl) { + CHECK(success); + } + + if (kUseExternalUrl && success && !first_log) { + LOG(INFO) << "Downloaded " + << [[delegate_ totalBytesReceivedPerTask][task] intValue] + << " bytes on first iteration."; + first_log = true; + } + + if (!success) { + if ([delegate_ errorPerTask][task]) { + LOG(WARNING) << "Request failed during performance testing: " + << base::SysNSStringToUTF8([[delegate_ errorPerTask][task] + localizedDescription]); + } else { + LOG(WARNING) << "Request timed out during performance testing."; + } + ++failed_iterations; + } else { + // Checking that the correct amount of data was downloaded only makes + // sense if the request succeeded. + EXPECT_EQ([[delegate_ expectedContentLengthPerTask][task] intValue], + [[delegate_ totalBytesReceivedPerTask][task] intValue]); + + elapsed_total += elapsed; + elapsed_max = MAX(elapsed, elapsed_max); + + total_bytes_received += + [[delegate_ totalBytesReceivedPerTask][task] intValue]; + } + + EXPECT_EQ(block_used, GetParam().cronet); + } + + LOG(INFO) << "Elapsed Total:" << elapsed_total * 1000 << "ms"; + + // Reject performance data from too many failures. + if (kUseExternalUrl) { + CHECK_LE(failed_iterations, iterations / 2); + } + + perf_test_results[GetParam()] = { + elapsed_total, elapsed_total / iterations, elapsed_max, + total_bytes_received, failed_iterations, iterations}; + + if (!kUseExternalUrl) { + TestServer::ReleaseBigDataURL(); + } +} + +INSTANTIATE_TEST_SUITE_P(Loops, + PerfTest, + ::testing::ValuesIn(test_combinations)); +} // namespace cronet diff --git a/src/components/cronet/ios/test/cronet_pkp_test.mm b/src/components/cronet/ios/test/cronet_pkp_test.mm new file mode 100644 index 0000000000..e9defa60e1 --- /dev/null +++ b/src/components/cronet/ios/test/cronet_pkp_test.mm @@ -0,0 +1,272 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#include "base/strings/sys_string_conversions.h" +#include "components/cronet/ios/test/cronet_test_base.h" +#include "components/cronet/ios/test/start_cronet.h" +#include "net/base/mac/url_conversions.h" +#include "net/cert/mock_cert_verifier.h" +#include "net/test/cert_test_util.h" +#include "net/test/quic_simple_test_server.h" +#include "net/test/test_data_directory.h" +#include "testing/gtest_mac.h" +#include "url/gurl.h" + +namespace { +const bool kIncludeSubdomains = true; +const bool kExcludeSubdomains = false; +const bool kSuccess = true; +const bool kError = false; +const std::string kServerCert = "quic-chain.pem"; +NSDate* const kDistantFuture = [NSDate distantFuture]; +} // namespace + +namespace cronet { +// Tests public-key-pinning functionality. +class PkpTest : public CronetTestBase { + protected: + void SetUp() override { + CronetTestBase::SetUp(); + + server_host_ = + base::SysUTF8ToNSString(net::QuicSimpleTestServer::GetHost()); + server_domain_ = + base::SysUTF8ToNSString(net::QuicSimpleTestServer::GetDomain()); + request_url_ = + net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + + // Create a Cronet enabled NSURLSession. + NSURLSessionConfiguration* sessionConfig = + [NSURLSessionConfiguration defaultSessionConfiguration]; + [Cronet installIntoSessionConfiguration:sessionConfig]; + url_session_ = [NSURLSession sessionWithConfiguration:sessionConfig + delegate:delegate_ + delegateQueue:nil]; + + // Set mock cert verifier. + [Cronet setMockCertVerifierForTesting:CreateMockCertVerifier({kServerCert}, + YES)]; + } + + void TearDown() override { + // It is safe to call the shutdownForTesting method even if a test + // didn't call StartCronet(). + [Cronet shutdownForTesting]; + CronetTestBase::TearDown(); + } + + // Sends a request to a given URL, waits for the response and asserts that + // the response is either successful or containing an error depending on + // the value of the passed |expected_success| parameter. + void sendRequestAndAssertResult(NSURL* url, bool expected_success) { + NSURLSessionDataTask* dataTask = + [url_session_ dataTaskWithURL:request_url_]; + StartDataTaskAndWaitForCompletion(dataTask); + if (expected_success) { + ASSERT_TRUE(IsResponseSuccessful(dataTask)); + } else { + ASSERT_FALSE(IsResponseSuccessful(dataTask)); + ASSERT_FALSE(IsResponseCanceled(dataTask)); + } + } + + // Adds a given public-key-pin and starts a Cronet engine for testing. + void AddPkpAndStartCronet(NSString* host, + NSData* hash, + BOOL include_subdomains, + NSDate* expiration_date) { + [Cronet setEnablePublicKeyPinningBypassForLocalTrustAnchors:NO]; + NSSet* hashes = [NSSet setWithObject:hash]; + NSError* error; + BOOL success = [Cronet addPublicKeyPinsForHost:host + pinHashes:hashes + includeSubdomains:include_subdomains + expirationDate:(NSDate*)expiration_date + error:&error]; + CHECK(success); + CHECK(!error); + StartCronet(net::QuicSimpleTestServer::GetPort()); + } + + // Returns an arbitrary public key hash that doesn't match with any test + // certificate. + static NSData* NonMatchingHash() { + const int length = 32; + std::string hash(length, '\077'); + return [NSData dataWithBytes:hash.c_str() length:length]; + } + + // Returns hash value that matches the hash of the public key certificate used + // for testing. + static NSData* MatchingHash() { + scoped_refptr cert = + net::ImportCertFromFile(net::GetTestCertsDirectory(), kServerCert); + net::HashValue hash_value; + CalculatePublicKeySha256(*cert, &hash_value); + CHECK_EQ(32ul, hash_value.size()); + return [NSData dataWithBytes:hash_value.data() length:hash_value.size()]; + } + + NSURLSession* url_session_; + NSURL* request_url_; // "https://test.example.com/simple.txt" + NSString* server_host_; // test.example.com + NSString* server_domain_; // example.com +}; // class PkpTest + +// Tests the case when a mismatching pin is set for some host that is +// different from the one the client wants to access. In that case the other +// host pinning policy should not be applied and the client is expected to +// receive the successful response with the response code 200. +TEST_F(PkpTest, TestSuccessIfPinSetForDifferentHost) { + AddPkpAndStartCronet(@"some-other-host.com", NonMatchingHash(), + kExcludeSubdomains, kDistantFuture); + ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); +} + +// Tests the case when the pin hash does not match. The client is expected to +// receive the error response. +TEST_F(PkpTest, TestErrorIfPinDoesNotMatch) { + AddPkpAndStartCronet(server_host_, NonMatchingHash(), kExcludeSubdomains, + kDistantFuture); + ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kError)); +} + +// Tests the case when the pin hash matches. The client is expected to +// receive the successful response with the response code 200. +TEST_F(PkpTest, TestSuccessIfPinMatches) { + AddPkpAndStartCronet(server_host_, MatchingHash(), kExcludeSubdomains, + kDistantFuture); + ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); +} + +TEST_F(PkpTest, TestBypass) { + [Cronet setEnablePublicKeyPinningBypassForLocalTrustAnchors:YES]; + + NSSet* hashes = [NSSet setWithObject:NonMatchingHash()]; + NSError* error; + BOOL success = [Cronet addPublicKeyPinsForHost:server_host_ + pinHashes:hashes + includeSubdomains:kExcludeSubdomains + expirationDate:(NSDate*)kDistantFuture + error:&error]; + + EXPECT_FALSE(success); + EXPECT_EQ([error code], CRNErrorUnsupportedConfig); +} + +// Tests the case when the pin hash does not match and the client accesses the +// subdomain of the configured PKP host with includeSubdomains flag set to true. +// The client is expected to receive the error response. +TEST_F(PkpTest, TestIncludeSubdomainsFlagEqualTrue) { + AddPkpAndStartCronet(server_domain_, NonMatchingHash(), kIncludeSubdomains, + kDistantFuture); + ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kError)); +} + +// Tests the case when the pin hash does not match and the client accesses the +// subdomain of the configured PKP host with includeSubdomains flag set to +// false. The client is expected to receive the successful response with the +// response code 200. +TEST_F(PkpTest, TestIncludeSubdomainsFlagEqualFalse) { + AddPkpAndStartCronet(server_domain_, NonMatchingHash(), kExcludeSubdomains, + kDistantFuture); + ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); +} + +// Tests a mismatching pin that will expire in 10 seconds. The pins should be +// still valid and enforced during the request; thus returning the pin match +// error. +TEST_F(PkpTest, TestSoonExpiringPin) { + AddPkpAndStartCronet(server_host_, NonMatchingHash(), kExcludeSubdomains, + [NSDate dateWithTimeIntervalSinceNow:10]); + ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kError)); +} + +// Tests mismatching pin that expired 1 second ago. Since the pin has +// expired, it should not be enforced during the request; thus a successful +// response is expected. +TEST_F(PkpTest, TestRecentlyExpiredPin) { + AddPkpAndStartCronet(server_host_, NonMatchingHash(), kExcludeSubdomains, + [NSDate dateWithTimeIntervalSinceNow:-1]); + ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); +} + +// Tests that host pinning is not persisted between multiple CronetEngine +// instances. +TEST_F(PkpTest, TestPinsAreNotPersisted) { + AddPkpAndStartCronet(server_host_, NonMatchingHash(), kExcludeSubdomains, + kDistantFuture); + ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kError)); + [Cronet shutdownForTesting]; + + // Restart Cronet engine and try the same request again. Since the pins are + // not persisted, a successful response is expected. + StartCronet(net::QuicSimpleTestServer::GetPort()); + ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); +} + +// Tests that an error is returned when PKP hash size is not equal to 256 bits. +TEST_F(PkpTest, TestHashLengthError) { + [Cronet setEnablePublicKeyPinningBypassForLocalTrustAnchors:NO]; + char hash[31]; + NSData* shortHash = [NSData dataWithBytes:hash length:sizeof(hash)]; + NSSet* hashes = [NSSet setWithObject:shortHash]; + NSError* error; + BOOL success = [Cronet addPublicKeyPinsForHost:server_host_ + pinHashes:hashes + includeSubdomains:kExcludeSubdomains + expirationDate:kDistantFuture + error:&error]; + EXPECT_FALSE(success); + ASSERT_TRUE(error != nil); + EXPECT_STREQ([CRNCronetErrorDomain cStringUsingEncoding:NSUTF8StringEncoding], + [error.domain cStringUsingEncoding:NSUTF8StringEncoding]); + EXPECT_EQ(CRNErrorInvalidArgument, error.code); + EXPECT_TRUE([error.description rangeOfString:@"Invalid argument"].location != + NSNotFound); + EXPECT_TRUE([error.description rangeOfString:@"pinHashes"].location != + NSNotFound); + EXPECT_STREQ("pinHashes", [error.userInfo[CRNInvalidArgumentKey] + cStringUsingEncoding:NSUTF8StringEncoding]); +} + +// Tests that setting pins for the same host second time overrides the previous +// pins. +TEST_F(PkpTest, TestPkpOverrideNonMatchingToMatching) { + [Cronet setEnablePublicKeyPinningBypassForLocalTrustAnchors:NO]; + // Add non-matching pin. + BOOL success = + [Cronet addPublicKeyPinsForHost:server_host_ + pinHashes:[NSSet setWithObject:NonMatchingHash()] + includeSubdomains:kExcludeSubdomains + expirationDate:kDistantFuture + error:nil]; + ASSERT_TRUE(success); + // Add matching pin. + AddPkpAndStartCronet(server_host_, MatchingHash(), kExcludeSubdomains, + kDistantFuture); + ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kSuccess)); +} + +// Tests that setting pins for the same host second time overrides the previous +// pins. +TEST_F(PkpTest, TestPkpOverrideMatchingToNonMatching) { + [Cronet setEnablePublicKeyPinningBypassForLocalTrustAnchors:NO]; + // Add matching pin. + BOOL success = + [Cronet addPublicKeyPinsForHost:server_host_ + pinHashes:[NSSet setWithObject:MatchingHash()] + includeSubdomains:kExcludeSubdomains + expirationDate:kDistantFuture + error:nil]; + ASSERT_TRUE(success); + // Add non-matching pin. + AddPkpAndStartCronet(server_host_, NonMatchingHash(), kExcludeSubdomains, + kDistantFuture); + ASSERT_NO_FATAL_FAILURE(sendRequestAndAssertResult(request_url_, kError)); +} + +} // namespace cronet diff --git a/src/components/cronet/ios/test/cronet_prefs_test.mm b/src/components/cronet/ios/test/cronet_prefs_test.mm new file mode 100644 index 0000000000..107b83cc3a --- /dev/null +++ b/src/components/cronet/ios/test/cronet_prefs_test.mm @@ -0,0 +1,129 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/location.h" +#include "base/path_service.h" +#include "components/cronet/ios/test/cronet_test_base.h" +#include "components/cronet/ios/test/start_cronet.h" +#include "components/cronet/testing/test_server/test_server.h" +#include "net/base/mac/url_conversions.h" +#include "net/test/quic_simple_test_server.h" +#include "testing/gtest_mac.h" +#include "url/gurl.h" + +namespace cronet { + +class PrefsTest : public CronetTestBase { + protected: + void SetUp() override { + CronetTestBase::SetUp(); + TestServer::Start(); + + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + return YES; + }]; + NSURLSessionConfiguration* config = + [NSURLSessionConfiguration ephemeralSessionConfiguration]; + [Cronet installIntoSessionConfiguration:config]; + session_ = [NSURLSession sessionWithConfiguration:config + delegate:delegate_ + delegateQueue:nil]; + } + + void TearDown() override { + TestServer::Shutdown(); + [Cronet stopNetLog]; + [Cronet shutdownForTesting]; + CronetTestBase::TearDown(); + } + + NSString* GetFileContentWaitUntilCreated(NSString* file, + NSTimeInterval timeout, + NSError** error) { + // Wait until the file appears on disk. + NSFileManager* file_manager = [NSFileManager defaultManager]; + NSLog(@"Waiting for file %@.", file); + while (timeout > 0) { + if ([file_manager fileExistsAtPath:file]) { + NSLog(@"File %@ exists.", file); + break; + } + NSLog(@"Time left: %i seconds", (int)timeout); + NSTimeInterval sleep_interval = fmin(5.0, timeout); + [NSThread sleepForTimeInterval:sleep_interval]; + timeout -= sleep_interval; + } + + // Read the file on the file thread to avoid reading the changing file. + dispatch_semaphore_t lock = dispatch_semaphore_create(0); + __block NSString* file_content = nil; + __block NSError* block_error = nil; + PostBlockToFileThread(FROM_HERE, ^{ + file_content = [NSString stringWithContentsOfFile:file + encoding:NSUTF8StringEncoding + error:&block_error]; + dispatch_semaphore_signal(lock); + }); + + // Wait for the file thread to finish reading the file content. + dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); + if (block_error) { + *error = block_error; + } + return file_content; + } + + NSURLSession* session_; +}; + +TEST_F(PrefsTest, HttpServerProperties) { + base::FilePath storage_path; + bool result = base::PathService::Get(base::DIR_CACHE, &storage_path); + ASSERT_TRUE(result); + storage_path = + storage_path.Append(FILE_PATH_LITERAL("cronet/prefs/local_prefs.json")); + NSString* prefs_file_name = + [NSString stringWithCString:storage_path.AsUTF8Unsafe().c_str() + encoding:NSUTF8StringEncoding]; + + // Delete the prefs file if it exists. + [[NSFileManager defaultManager] removeItemAtPath:prefs_file_name error:nil]; + + // Add "max_server_configs_stored_in_properties" experimental option. + NSString* options = + @"{ \"QUIC\" : {\"max_server_configs_stored_in_properties\" : 5} }"; + [Cronet setExperimentalOptions:options]; + + // Start Cronet Engine + StartCronet(net::QuicSimpleTestServer::GetPort()); + + // Start the request + NSURL* url = net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + NSURLSessionDataTask* task = [session_ dataTaskWithURL:url]; + StartDataTaskAndWaitForCompletion(task); + + // Wait 80 seconds for the prefs file to appear on the disk. + NSError* error = nil; + NSString* prefs_file_content = + GetFileContentWaitUntilCreated(prefs_file_name, 80, &error); + ASSERT_FALSE(error) << "Unable to read " << storage_path << " file. Error: " + << error.localizedDescription.UTF8String; + + // Check the file content + ASSERT_TRUE(prefs_file_content); + ASSERT_TRUE( + [prefs_file_content containsString:@"{\"http_server_properties\":"]) + << "Unable to find 'http_server_properties' in the JSON prefs: " + << prefs_file_content.UTF8String; + ASSERT_TRUE([prefs_file_content containsString:@"\"supports_quic\":"]) + << "Unable to find 'supports_quic' in the JSON prefs: " + << prefs_file_content.UTF8String; + + // Delete the prefs file to avoid side effects with other tests. + [[NSFileManager defaultManager] removeItemAtPath:prefs_file_name error:nil]; +} + +} // namespace cronet diff --git a/src/components/cronet/ios/test/cronet_quic_test.mm b/src/components/cronet/ios/test/cronet_quic_test.mm new file mode 100644 index 0000000000..9820a6cb20 --- /dev/null +++ b/src/components/cronet/ios/test/cronet_quic_test.mm @@ -0,0 +1,111 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#include "base/strings/stringprintf.h" +#include "base/strings/sys_string_conversions.h" +#include "components/cronet/ios/test/cronet_test_base.h" +#include "net/base/mac/url_conversions.h" +#include "net/cert/mock_cert_verifier.h" +#include "net/test/quic_simple_test_server.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gtest_mac.h" +#include "url/gurl.h" + +namespace cronet { + +class QuicTest : public CronetTestBase { + protected: + QuicTest() {} + ~QuicTest() override {} + + void SetUp() override { + CronetTestBase::SetUp(); + + // Prepare Cronet + [Cronet setUserAgent:@"CronetTest/1.0.0.0" partial:NO]; + [Cronet setHttp2Enabled:false]; + [Cronet setQuicEnabled:true]; + [Cronet setAcceptLanguages:@"en-US,en"]; + [Cronet addQuicHint:@"test.example.com" port:443 altPort:443]; + [Cronet enableTestCertVerifierForTesting]; + [Cronet setHttpCacheType:CRNHttpCacheTypeDisabled]; + [Cronet setMetricsEnabled:YES]; + [Cronet setRequestFilterBlock:^(NSURLRequest* request) { + return YES; + }]; + + // QUIC Server simple URL. + simple_url_ = net::NSURLWithGURL(net::QuicSimpleTestServer::GetSimpleURL()); + } + + void TearDown() override { + [Cronet stopNetLog]; + [Cronet shutdownForTesting]; + CronetTestBase::TearDown(); + } + + void StartCronet() { + [Cronet start]; + + // Add URL mapping to test server. + NSString* rules = base::SysUTF8ToNSString( + base::StringPrintf("MAP test.example.com 127.0.0.1:%d," + "MAP notfound.example.com ~NOTFOUND", + net::QuicSimpleTestServer::GetPort())); + [Cronet setHostResolverRulesForTesting:rules]; + + // Prepare a session. + NSURLSessionConfiguration* config = + [NSURLSessionConfiguration ephemeralSessionConfiguration]; + config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + [Cronet installIntoSessionConfiguration:config]; + session_ = [NSURLSession sessionWithConfiguration:config + delegate:delegate_ + delegateQueue:nil]; + } + + NSURLSession* session_; + NSURL* simple_url_; +}; + +TEST_F(QuicTest, InvalidQuicHost) { + BOOL success = + [Cronet addQuicHint:@"https://test.example.com/" port:443 altPort:443]; + + EXPECT_FALSE(success); +} + +TEST_F(QuicTest, ValidQuicHost) { + BOOL success = [Cronet addQuicHint:@"test.example.com" port:443 altPort:443]; + + EXPECT_TRUE(success); +} + +// Tests a request with enabled "enable_socket_recv_optimization" QUIC +// experimental option. +TEST_F(QuicTest, RequestWithSocketOptimizationEnabled) { + // Apply test specific Cronet configuration and start it. + [Cronet setExperimentalOptions: + @"{\"QUIC\" : {\"enable_socket_recv_optimization\" : true} }"]; + StartCronet(); + + // Make request and wait for the response. + NSURLSessionDataTask* task = [session_ dataTaskWithURL:simple_url_]; + StartDataTaskAndWaitForCompletion(task); + + // Check that a successful response was received using QUIC. + EXPECT_EQ(nil, [delegate_ error]); + EXPECT_EQ(net::QuicSimpleTestServer::GetSimpleBodyValue(), + base::SysNSStringToUTF8(delegate_.responseBody)); + if (@available(iOS 10.2, *)) { + NSURLSessionTaskTransactionMetrics* metrics = + delegate_.taskMetrics.transactionMetrics[0]; + EXPECT_TRUE([metrics.networkProtocolName containsString:@"quic"] || + [metrics.networkProtocolName containsString:@"h3"]) + << base::SysNSStringToUTF8(metrics.networkProtocolName); + } +} +} diff --git a/src/components/cronet/ios/test/cronet_test_base.h b/src/components/cronet/ios/test/cronet_test_base.h new file mode 100644 index 0000000000..fff603f608 --- /dev/null +++ b/src/components/cronet/ios/test/cronet_test_base.h @@ -0,0 +1,121 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_CRONET_IOS_TEST_CRONET_TEST_BASE_H_ +#define COMPONENTS_CRONET_IOS_TEST_CRONET_TEST_BASE_H_ + +#include + +#include "base/bind.h" +#include "net/cert/cert_verifier.h" +#include "net/cert/x509_certificate.h" +#include "testing/gtest/include/gtest/gtest.h" + +#pragma mark + +namespace base { +class Location; +class SingleThreadTaskRunner; +class Thread; +} + +namespace { +typedef void (^BlockType)(void); +} // namespace + +// Exposes private test-only methods of the Cronet class. +@interface Cronet (ExposedForTesting) ++ (void)shutdownForTesting; ++ (void)setMockCertVerifierForTesting: + (std::unique_ptr)certVerifier; ++ (void)setEnablePublicKeyPinningBypassForLocalTrustAnchors:(BOOL)enable; ++ (base::SingleThreadTaskRunner*)getFileThreadRunnerForTesting; ++ (base::SingleThreadTaskRunner*)getNetworkThreadRunnerForTesting; ++ (size_t)getMetricsMapSize; +@end + +// NSURLSessionDataDelegate delegate implementation used by the tests to +// wait for a response and check its status. +@interface TestDelegate : NSObject + +// Error the request this delegate is attached to failed with, if any. +@property(retain, atomic) + NSMutableDictionary* errorPerTask; + +// Contains total amount of received data. +@property(readonly) NSMutableDictionary* + totalBytesReceivedPerTask; + +// Contains the expected amount of received data. +@property(readonly) NSMutableDictionary* + expectedContentLengthPerTask; + +// Contains metrics data. +@property(readonly) NSURLSessionTaskMetrics* taskMetrics NS_AVAILABLE_IOS(10.0); + +// Contains NSHTTPURLResponses for the tasks. +@property(readonly) + NSMutableDictionary* + responsePerTask; + +// Resets the delegate, so it can be used again for another request. +- (void)reset; + +// Contains the response body. +- (NSString*)responseBody:(NSURLSessionDataTask*)task; + +/// Waits for a single request to complete. + +/// @return |NO| if the request didn't complete and the method timed-out. +- (BOOL)waitForDone:(NSURLSessionDataTask*)task + withTimeout:(int64_t)deadline_ns; + +// Convenience functions for single-task delegates +- (NSError*)error; +- (long)totalBytesReceived; +- (long)expectedContentLength; +- (NSString*)responseBody; + +@end + +// Forward declaration. +namespace net { +class MockCertVerifier; +} + +namespace cronet { + +// A base class that should be extended by all other Cronet tests. +// The class automatically starts and stops the test QUIC server. +class CronetTestBase : public ::testing::Test { + protected: + static bool CalculatePublicKeySha256(const net::X509Certificate& cert, + net::HashValue* out_hash_value); + + void SetUp() override; + void TearDown() override; + bool StartDataTaskAndWaitForCompletion(NSURLSessionDataTask* task, + int64_t deadline_ns = 15 * + NSEC_PER_SEC); + std::unique_ptr CreateMockCertVerifier( + const std::vector& certs, + bool known_root); + + void PostBlockToFileThread(const base::Location& from_here, BlockType block); + void PostBlockToNetworkThread(const base::Location& from_here, + BlockType block); + + ::testing::AssertionResult IsResponseSuccessful(NSURLSessionDataTask* task); + ::testing::AssertionResult IsResponseCanceled(NSURLSessionDataTask* task); + + TestDelegate* delegate_; + + private: + void ExecuteBlock(BlockType block); + +}; // class CronetTestBase + +} // namespace cronet + +#endif // COMPONENTS_CRONET_IOS_TEST_CRONET_TEST_BASE_H_ diff --git a/src/components/cronet/ios/test/cronet_test_base.mm b/src/components/cronet/ios/test/cronet_test_base.mm new file mode 100644 index 0000000000..79455ed091 --- /dev/null +++ b/src/components/cronet/ios/test/cronet_test_base.mm @@ -0,0 +1,327 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cronet_test_base.h" + +#include "base/location.h" +#include "base/threading/thread.h" +#include "crypto/sha2.h" +#include "net/base/net_errors.h" +#include "net/cert/asn1_util.h" +#include "net/cert/mock_cert_verifier.h" +#include "net/cert/x509_util.h" +#include "net/test/cert_test_util.h" +#include "net/test/quic_simple_test_server.h" +#include "net/test/test_data_directory.h" + +#pragma mark + +@implementation TestDelegate { + // Dictionary which maps tasks to completion semaphores for this TestDelegate. + // When a request this delegate is attached to finishes (either successfully + // or with an error), this delegate signals that task's semaphore. + NSMutableDictionary* _semaphores; + + NSMutableDictionary*>* + _responseDataPerTask; +} + +@synthesize errorPerTask = _errorPerTask; +@synthesize totalBytesReceivedPerTask = _totalBytesReceivedPerTask; +@synthesize expectedContentLengthPerTask = _expectedContentLengthPerTask; +@synthesize taskMetrics = _taskMetrics; +@synthesize responsePerTask = _responsePerTask; + +- (id)init { + if (self = [super init]) { + _semaphores = [NSMutableDictionary dictionaryWithCapacity:0]; + } + return self; +} + +- (void)reset { + _semaphores = [NSMutableDictionary dictionaryWithCapacity:0]; + _responseDataPerTask = [NSMutableDictionary dictionaryWithCapacity:0]; + _errorPerTask = [NSMutableDictionary dictionaryWithCapacity:0]; + _totalBytesReceivedPerTask = [NSMutableDictionary dictionaryWithCapacity:0]; + _expectedContentLengthPerTask = + [NSMutableDictionary dictionaryWithCapacity:0]; + _responsePerTask = [NSMutableDictionary dictionaryWithCapacity:0]; + _taskMetrics = nil; +} + +- (NSError*)error { + if ([_errorPerTask count] == 0) + return nil; + + DCHECK([_errorPerTask count] == 1); + return [[_errorPerTask objectEnumerator] nextObject]; +} + +- (long)totalBytesReceived { + DCHECK([_totalBytesReceivedPerTask count] == 1); + return [[[_totalBytesReceivedPerTask objectEnumerator] nextObject] intValue]; +} + +- (long)expectedContentLength { + DCHECK([_expectedContentLengthPerTask count] == 1); + return + [[[_expectedContentLengthPerTask objectEnumerator] nextObject] intValue]; +} + +- (NSString*)responseBody { + if ([_responseDataPerTask count] == 0) + return nil; + + DCHECK([_responseDataPerTask count] == 1); + NSURLSessionDataTask* task = + [[_responseDataPerTask keyEnumerator] nextObject]; + + return [self responseBody:task]; +} + +- (NSString*)responseBody:(NSURLSessionDataTask*)task { + if (_responseDataPerTask[task] == nil) { + return nil; + } + NSMutableString* body = [NSMutableString string]; + for (NSData* data in _responseDataPerTask[task]) { + [body appendString:[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]]; + } + VLOG(3) << "responseBody size:" << [body length] + << " chunks:" << [_responseDataPerTask[task] count]; + return body; +} + +- (dispatch_semaphore_t)getSemaphoreForTask:(NSURLSessionTask*)task { + @synchronized(_semaphores) { + if (!_semaphores[task]) { + _semaphores[task] = dispatch_semaphore_create(0); + } + return _semaphores[task]; + } +} + +// |timeout_ns|, if positive, specifies how long to wait before timing out in +// nanoseconds, a value of 0 or less means do not ever time out. +- (BOOL)waitForDone:(NSURLSessionDataTask*)task + withTimeout:(int64_t)timeout_ns { + BOOL request_completed = NO; + dispatch_semaphore_t semaphore = [self getSemaphoreForTask:task]; + if (timeout_ns > 0) { + request_completed = + dispatch_semaphore_wait( + semaphore, dispatch_time(DISPATCH_TIME_NOW, timeout_ns)) == 0; + if (!request_completed) { + // Cancel the pending request; otherwise, the request is still active and + // may invoke the delegate methods later. + [task cancel]; + LOG(WARNING) << "The request was canceled due to timeout."; + // Give the canceled request some time to execute didCompleteWithError + // method with NSURLErrorCancelled error code. + dispatch_semaphore_wait( + semaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)); + } + } else { + request_completed = + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) == 0; + } + @synchronized(_semaphores) { + if (request_completed) { + [_semaphores removeObjectForKey:task]; + } + } + return request_completed; +} + +- (void)URLSession:(NSURLSession*)session + didBecomeInvalidWithError:(NSError*)error { +} + +- (void)URLSession:(NSURLSession*)session + task:(NSURLSessionTask*)task + didCompleteWithError:(NSError*)error { + if (error) + _errorPerTask[task] = error; + + dispatch_semaphore_t _semaphore = [self getSemaphoreForTask:task]; + dispatch_semaphore_signal(_semaphore); +} + +- (void)URLSession:(NSURLSession*)session + task:(NSURLSessionTask*)task + didReceiveChallenge:(NSURLAuthenticationChallenge*)challenge + completionHandler: + (void (^)(NSURLSessionAuthChallengeDisposition disp, + NSURLCredential* credential))completionHandler { + completionHandler(NSURLSessionAuthChallengeUseCredential, nil); +} + +- (void)URLSession:(NSURLSession*)session + task:(NSURLSessionTask*)task + didFinishCollectingMetrics:(NSURLSessionTaskMetrics*)metrics + NS_AVAILABLE_IOS(10.0) { + _taskMetrics = metrics; +} + +- (void)URLSession:(NSURLSession*)session + dataTask:(NSURLSessionDataTask*)dataTask + didReceiveResponse:(NSURLResponse*)response + completionHandler:(void (^)(NSURLSessionResponseDisposition disposition)) + completionHandler { + _expectedContentLengthPerTask[dataTask] = + [NSNumber numberWithInt:[response expectedContentLength]]; + _responsePerTask[dataTask] = static_cast(response); + completionHandler(NSURLSessionResponseAllow); +} + +- (void)URLSession:(NSURLSession*)session + dataTask:(NSURLSessionDataTask*)dataTask + didReceiveData:(NSData*)data { + if (_totalBytesReceivedPerTask[dataTask]) { + _totalBytesReceivedPerTask[dataTask] = [NSNumber + numberWithInt:[_totalBytesReceivedPerTask[dataTask] intValue] + + [data length]]; + } else { + _totalBytesReceivedPerTask[dataTask] = + [NSNumber numberWithInt:[data length]]; + } + + if (_responseDataPerTask[dataTask] == nil) { + _responseDataPerTask[dataTask] = [[NSMutableArray alloc] init]; + } + [_responseDataPerTask[dataTask] addObject:data]; +} + +- (void)URLSession:(NSURLSession*)session + dataTask:(NSURLSessionDataTask*)dataTask + willCacheResponse:(NSCachedURLResponse*)proposedResponse + completionHandler: + (void (^)(NSCachedURLResponse* cachedResponse))completionHandler { + completionHandler(proposedResponse); +} + +@end + +namespace cronet { + +void CronetTestBase::SetUp() { + ::testing::Test::SetUp(); + net::QuicSimpleTestServer::Start(); + delegate_ = [[TestDelegate alloc] init]; +} + +void CronetTestBase::TearDown() { + net::QuicSimpleTestServer::Shutdown(); + ::testing::Test::TearDown(); +} + +// Launches the supplied |task| and blocks until it completes, with a default +// timeout of 20 seconds. |deadline_ns|, if specified, is in nanoseconds. +// If |deadline_ns| is 0 or negative, the request will not time out. +bool CronetTestBase::StartDataTaskAndWaitForCompletion( + NSURLSessionDataTask* task, + int64_t deadline_ns) { + [delegate_ reset]; + [task resume]; + return [delegate_ waitForDone:task withTimeout:deadline_ns]; +} + +::testing::AssertionResult CronetTestBase::IsResponseSuccessful( + NSURLSessionDataTask* task) { + if ([delegate_ errorPerTask][task]) { + return ::testing::AssertionFailure() << "error in response: " << + [[[delegate_ error] description] + cStringUsingEncoding:NSUTF8StringEncoding]; + } + + if (![delegate_ responsePerTask][task]) { + return ::testing::AssertionFailure() << " no response has been received"; + } + + NSInteger statusCode = [delegate_ responsePerTask][task].statusCode; + if (statusCode < 200 || statusCode > 299) { + return ::testing::AssertionFailure() + << " the response code was " << statusCode; + } + + return ::testing::AssertionSuccess() << "no errors in response"; +} + +::testing::AssertionResult CronetTestBase::IsResponseCanceled( + NSURLSessionDataTask* task) { + NSError* error = [delegate_ errorPerTask][task]; + if (error && [error code] == NSURLErrorCancelled) + return ::testing::AssertionSuccess() << "the response is canceled"; + return ::testing::AssertionFailure() << "the response is not canceled." + << " The response error is " << + [[error description] cStringUsingEncoding:NSUTF8StringEncoding]; +} + +std::unique_ptr CronetTestBase::CreateMockCertVerifier( + const std::vector& certs, + bool known_root) { + std::unique_ptr mock_cert_verifier( + new net::MockCertVerifier()); + for (const auto& cert : certs) { + net::CertVerifyResult verify_result; + verify_result.verified_cert = + net::ImportCertFromFile(net::GetTestCertsDirectory(), cert); + + // By default, HPKP verification is enabled for known trust roots only. + verify_result.is_issued_by_known_root = known_root; + + // Calculate the public key hash and add it to the verify_result. + net::HashValue hashValue; + CHECK(CalculatePublicKeySha256(*verify_result.verified_cert.get(), + &hashValue)); + verify_result.public_key_hashes.push_back(hashValue); + + mock_cert_verifier->AddResultForCert(verify_result.verified_cert.get(), + verify_result, net::OK); + } + return mock_cert_verifier; +} + +void CronetTestBase::PostBlockToFileThread(const base::Location& from_here, + BlockType block) { + base::SingleThreadTaskRunner* file_runner = + [Cronet getFileThreadRunnerForTesting]; + file_runner->PostTask(from_here, + base::BindOnce(&CronetTestBase::ExecuteBlock, + base::Unretained(this), block)); +} + +void CronetTestBase::PostBlockToNetworkThread(const base::Location& from_here, + BlockType block) { + base::SingleThreadTaskRunner* network_runner = + [Cronet getNetworkThreadRunnerForTesting]; + network_runner->PostTask(from_here, + base::BindOnce(&CronetTestBase::ExecuteBlock, + base::Unretained(this), block)); +} + +bool CronetTestBase::CalculatePublicKeySha256(const net::X509Certificate& cert, + net::HashValue* out_hash_value) { + // Extract the public key from the cert. + base::StringPiece spki_bytes; + if (!net::asn1::ExtractSPKIFromDERCert( + net::x509_util::CryptoBufferAsStringPiece(cert.cert_buffer()), + &spki_bytes)) { + LOG(INFO) << "Unable to retrieve the public key from the DER cert"; + return false; + } + // Calculate SHA256 hash of public key bytes. + *out_hash_value = net::HashValue(net::HASH_VALUE_SHA256); + crypto::SHA256HashString(spki_bytes, out_hash_value->data(), + crypto::kSHA256Length); + return true; +} + +void CronetTestBase::ExecuteBlock(BlockType block) { + block(); +} + +} // namespace cronet diff --git a/src/components/cronet/ios/test/get_stream_engine.mm b/src/components/cronet/ios/test/get_stream_engine.mm new file mode 100644 index 0000000000..6e20db7078 --- /dev/null +++ b/src/components/cronet/ios/test/get_stream_engine.mm @@ -0,0 +1,29 @@ +// Copyright 2016 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. + +#import +#import + +#include "components/cronet/ios/test/start_cronet.h" +#include "components/grpc_support/test/get_stream_engine.h" + +@interface Cronet (ExposedForTesting) ++ (void)shutdownForTesting; +@end + +namespace grpc_support { + +stream_engine* GetTestStreamEngine(int port) { + return [Cronet getGlobalEngine]; +} + +void StartTestStreamEngine(int port) { + cronet::StartCronet(port); +} + +void ShutdownTestStreamEngine() { + [Cronet shutdownForTesting]; +} + +} // namespace grpc_support diff --git a/src/components/cronet/ios/test/start_cronet.h b/src/components/cronet/ios/test/start_cronet.h new file mode 100644 index 0000000000..47962b1370 --- /dev/null +++ b/src/components/cronet/ios/test/start_cronet.h @@ -0,0 +1,11 @@ +// Copyright 2016 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. + +namespace cronet { + +// Starts Cronet, or restarts if Cronet is already running. Will have Cronet +// point test.example.com" to "localhost:|port|". +void StartCronet(int port); + +} // namespace cronet diff --git a/src/components/cronet/ios/test/start_cronet.mm b/src/components/cronet/ios/test/start_cronet.mm new file mode 100644 index 0000000000..bfccfd79d2 --- /dev/null +++ b/src/components/cronet/ios/test/start_cronet.mm @@ -0,0 +1,32 @@ +// Copyright 2016 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. + +#import + +#include "components/cronet/ios/test/start_cronet.h" + +#include "base/strings/stringprintf.h" +#include "base/strings/sys_string_conversions.h" + +namespace cronet { + +void StartCronet(int port) { + [Cronet setUserAgent:@"CronetTest/1.0.0.0" partial:NO]; + [Cronet setHttp2Enabled:true]; + [Cronet setQuicEnabled:true]; + [Cronet setAcceptLanguages:@"en-US,en"]; + [Cronet addQuicHint:@"test.example.com" port:443 altPort:443]; + [Cronet enableTestCertVerifierForTesting]; + [Cronet setHttpCacheType:CRNHttpCacheTypeDisabled]; + + [Cronet start]; + + NSString* rules = base::SysUTF8ToNSString( + base::StringPrintf("MAP test.example.com 127.0.0.1:%d," + "MAP notfound.example.com ~NOTFOUND", + port)); + [Cronet setHostResolverRulesForTesting:rules]; +} + +} // namespace cronet diff --git a/src/components/cronet/metrics_util.cc b/src/components/cronet/metrics_util.cc new file mode 100644 index 0000000000..34829735b2 --- /dev/null +++ b/src/components/cronet/metrics_util.cc @@ -0,0 +1,25 @@ +// Copyright 2016 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 "components/cronet/metrics_util.h" + +#include "base/check.h" + +namespace cronet { + +namespace metrics_util { + +int64_t ConvertTime(const base::TimeTicks& ticks, + const base::TimeTicks& start_ticks, + const base::Time& start_time) { + if (ticks.is_null() || start_ticks.is_null()) { + return kNullTime; + } + DCHECK(!start_time.is_null()); + return (start_time + (ticks - start_ticks)).ToJavaTime(); +} + +} // namespace metrics_util + +} // namespace cronet diff --git a/src/components/cronet/metrics_util.h b/src/components/cronet/metrics_util.h new file mode 100644 index 0000000000..f381c48e5d --- /dev/null +++ b/src/components/cronet/metrics_util.h @@ -0,0 +1,46 @@ +// Copyright 2016 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_METRICS_UTIL_H_ +#define COMPONENTS_CRONET_METRICS_UTIL_H_ + +#include + +#include "base/time/time.h" + +namespace cronet { + +namespace metrics_util { + +constexpr int64_t kNullTime = -1; + +// Converts timing metrics stored as TimeTicks into the format expected by the +// API layer: ms since Unix epoch, or kNullTime for null (if either |ticks| or +// |start_ticks| is null). +// +// By calculating time values using a base (|start_ticks|, |start_time|) pair, +// time values are normalized. This allows time deltas between pairs of events +// to be accurately computed, even if the system clock changed between those +// events, as long as times for both events were calculated using the same +// (|start_ticks|, |start_time|) pair. +// +// Args: +// +// ticks: The ticks value corresponding to the time of the event -- the returned +// time corresponds to this event. +// +// start_ticks: The ticks measurement at some base time -- the ticks equivalent +// of start_time. Should be smaller than ticks. +// +// start_time: Time measurement at some base time -- the time equivalent of +// start_ticks. Must not be null. +int64_t ConvertTime(const base::TimeTicks& ticks, + const base::TimeTicks& start_ticks, + const base::Time& start_time); + +} // namespace metrics_util + +} // namespace cronet + +#endif // COMPONENTS_CRONET_METRICS_UTIL_H_ diff --git a/src/components/cronet/native/BUILD.gn b/src/components/cronet/native/BUILD.gn new file mode 100644 index 0000000000..6ee1271cf3 --- /dev/null +++ b/src/components/cronet/native/BUILD.gn @@ -0,0 +1,98 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//components/cronet/native/include/headers.gni") +import("//components/grpc_support/include/headers.gni") +import("//testing/test.gni") + +config("cronet_native_include_config") { + include_dirs = [ + "//components/cronet/native/generated", + "//components/cronet/native/include", + "//components/grpc_support/include", + ] +} + +source_set("cronet_native_headers") { + deps = [ + "//base", + "//components/grpc_support:headers", + ] + + configs += [ ":cronet_native_include_config" ] + public_configs = [ ":cronet_native_include_config" ] + + public = [ + "include/cronet_c.h", + "include/cronet_export.h", + + # Generated from cronet.idl. + "generated/cronet.idl_c.h", + "generated/cronet.idl_impl_interface.h", + "generated/cronet.idl_impl_struct.h", + ] +} + +# Cross-platform portion of Cronet native API implementation. +source_set("cronet_native_impl") { + deps = [ + ":cronet_native_headers", + "//base", + "//components/cronet:cronet_common", + "//components/cronet:cronet_version_header", + "//components/cronet:metrics_util", + "//components/grpc_support:grpc_support", + "//net", + ] + + configs += [ ":cronet_native_include_config" ] + public_configs = [ ":cronet_native_include_config" ] + public_deps = [ ":cronet_native_headers" ] + + sources = [ + "buffer.cc", + "engine.cc", + "engine.h", + "io_buffer_with_cronet_buffer.cc", + "io_buffer_with_cronet_buffer.h", + "native_metrics_util.cc", + "native_metrics_util.h", + "runnables.cc", + "runnables.h", + "upload_data_sink.cc", + "upload_data_sink.h", + "url_request.cc", + "url_request.h", + + # Generated from cronet.idl. + "generated/cronet.idl_impl_interface.cc", + "generated/cronet.idl_impl_struct.cc", + ] +} + +# Unit tests for Cronet native API. Depends on cronet_native_impl to test +# implementation details. +source_set("cronet_native_unittests") { + testonly = true + + deps = [ + ":cronet_native_impl", + "//base/test:test_support", + "//components/cronet/native/test:cronet_native_testutil", + "//net:test_support", + "//testing/gtest", + ] + + configs += [ ":cronet_native_include_config" ] + + sources = [ + "engine_unittest.cc", + "native_metrics_util_test.cc", + "runnables_unittest.cc", + + # Generated from cronet.idl. + "generated/cronet.idl_impl_interface_unittest.cc", + "generated/cronet.idl_impl_struct_unittest.cc", + ] +} diff --git a/src/components/cronet/native/buffer.cc b/src/components/cronet/native/buffer.cc new file mode 100644 index 0000000000..c06d6967d1 --- /dev/null +++ b/src/components/cronet/native/buffer.cc @@ -0,0 +1,87 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/cronet/native/generated/cronet.idl_impl_interface.h" + +#include "base/no_destructor.h" +#include "base/numerics/safe_conversions.h" + +namespace { + +// Implementation of Cronet_BufferCallback that calls free() to malloc() buffer. +class Cronet_BufferCallbackFree : public Cronet_BufferCallback { + public: + Cronet_BufferCallbackFree() = default; + + Cronet_BufferCallbackFree(const Cronet_BufferCallbackFree&) = delete; + Cronet_BufferCallbackFree& operator=(const Cronet_BufferCallbackFree&) = + delete; + + ~Cronet_BufferCallbackFree() override = default; + + void OnDestroy(Cronet_BufferPtr buffer) override { free(buffer->GetData()); } +}; + +// Concrete implementation of abstract Cronet_Buffer interface. +class Cronet_BufferImpl : public Cronet_Buffer { + public: + Cronet_BufferImpl() = default; + + Cronet_BufferImpl(const Cronet_BufferImpl&) = delete; + Cronet_BufferImpl& operator=(const Cronet_BufferImpl&) = delete; + + ~Cronet_BufferImpl() override; + + // Cronet_Buffer implementation + void InitWithDataAndCallback(Cronet_RawDataPtr data, + uint64_t size, + Cronet_BufferCallbackPtr callback) override; + void InitWithAlloc(uint64_t size) override; + uint64_t GetSize() override; + Cronet_RawDataPtr GetData() override; + + private: + Cronet_RawDataPtr data_ = nullptr; + uint64_t size_ = 0; + Cronet_BufferCallbackPtr callback_ = nullptr; +}; + +Cronet_BufferImpl::~Cronet_BufferImpl() { + if (callback_) + callback_->OnDestroy(this); +} + +void Cronet_BufferImpl::InitWithDataAndCallback( + Cronet_RawDataPtr data, + uint64_t size, + Cronet_BufferCallbackPtr callback) { + data_ = data; + size_ = size; + callback_ = callback; +} + +void Cronet_BufferImpl::InitWithAlloc(uint64_t size) { + if (!base::IsValueInRangeForNumericType(size)) + return; + data_ = malloc(size); + if (!data_) + return; + size_ = size; + static base::NoDestructor static_callback; + callback_ = static_callback.get(); +} + +uint64_t Cronet_BufferImpl::GetSize() { + return size_; +} + +Cronet_RawDataPtr Cronet_BufferImpl::GetData() { + return data_; +} + +} // namespace + +CRONET_EXPORT Cronet_BufferPtr Cronet_Buffer_Create() { + return new Cronet_BufferImpl(); +} diff --git a/src/components/cronet/native/cronet.idl b/src/components/cronet/native/cronet.idl new file mode 100644 index 0000000000..f0be146362 --- /dev/null +++ b/src/components/cronet/native/cronet.idl @@ -0,0 +1,1501 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// The IDL in this file is based on Mojo IDL, but is not currently targeted +// for direct use in Mojo projects. + +module Cronet; + +// General system support interfaces. + +/** + * Data buffer provided by the application to read and write data. + */ +interface Buffer { + /** + * Initialize Buffer with raw buffer |data| of |size| allocated by the app. + * The |callback| is invoked when buffer is destroyed. + */ + InitWithDataAndCallback(handle data, uint64 size, BufferCallback callback); + + /** + * Initialize Buffer by allocating buffer of |size|. The content of allocated + * data is not initialized. + */ + InitWithAlloc(uint64 size); + + /** + * Return size of data owned by this buffer. + */ + [Sync] + GetSize() => (uint64 size); + + /** + * Return raw pointer to |data| owned by this buffer. + */ + [Sync] + GetData() => (handle data); +}; + +/** + * App-provided callback passed to Buffer::InitWithDataAndCallback that gets invoked + * when Buffer is destroyed. + */ +[Abstract] +interface BufferCallback { + /** + * Method invoked when |buffer| is destroyed so its app-allocated |data| can + * be freed. If a UrlRequest has ownership of a Buffer and the UrlRequest is destroyed + * (e.g. Cronet_UrlRequest_Destroy() is called), then Cronet will call OnDestroy(). + */ + OnDestroy(Buffer buffer); +}; + +// Base error passed to UrlRequestCallback.onFailed(). +struct Error { + enum ERROR_CODE { + /** + * Error code indicating the error returned by app callback. + */ + ERROR_CALLBACK = 0, + + /** + * Error code indicating the host being sent the request could not be resolved to an IP address. + */ + ERROR_HOSTNAME_NOT_RESOLVED = 1, + + /** + * Error code indicating the device was not connected to any network. + */ + ERROR_INTERNET_DISCONNECTED = 2, + + /** + * Error code indicating that as the request was processed the network configuration changed. + */ + ERROR_NETWORK_CHANGED = 3, + + /** + * Error code indicating a timeout expired. Timeouts expiring while attempting to connect will + * be reported as the more specific {@link #ERROR_CONNECTION_TIMED_OUT}. + */ + ERROR_TIMED_OUT = 4, + + /** + * Error code indicating the connection was closed unexpectedly. + */ + ERROR_CONNECTION_CLOSED = 5, + + /** + * Error code indicating the connection attempt timed out. + */ + ERROR_CONNECTION_TIMED_OUT = 6, + + /** + * Error code indicating the connection attempt was refused. + */ + ERROR_CONNECTION_REFUSED = 7, + + /** + * Error code indicating the connection was unexpectedly reset. + */ + ERROR_CONNECTION_RESET = 8, + + /** + * Error code indicating the IP address being contacted is unreachable, meaning there is no + * route to the specified host or network. + */ + ERROR_ADDRESS_UNREACHABLE = 9, + + /** + * Error code indicating an error related to the + * QUIC protocol. When {@link #error_code} is this code, see + * {@link quic_detailed_error_code} for more information. + */ + ERROR_QUIC_PROTOCOL_FAILED = 10, + + /** + * Error code indicating another type of error was encountered. + * |internal_error_code| can be consulted to get a more specific cause. + */ + ERROR_OTHER = 11 + }; + + /** + * Error code, one of ERROR_* values. + */ + ERROR_CODE error_code = ERROR_CALLBACK; + + /** + * Message, explaining the error. + */ + string message; + + /** + * Cronet internal error code. This may provide more specific error + * diagnosis than |error_code|, but the constant values may change over time. + * See + * + * here for the lastest list of values. + */ + int32 internal_error_code = 0; + + /** + * |true| if retrying this request right away might succeed, |false| + * otherwise. For example, is |true| when |error_code| is |ERROR_NETWORK_CHANGED| + * because trying the request might succeed using the new + * network configuration, but |false| when |error_code| is + * ERROR_INTERNET_DISCONNECTED because retrying the request right away will + * encounter the same failure (instead retrying should be delayed until device regains + * network connectivity). + */ + bool immediately_retryable = false; + + /** + * Contains detailed QUIC error code from + * + * QuicErrorCode when the |error_code| code is |ERROR_QUIC_PROTOCOL_FAILED|. + */ + int32 quic_detailed_error_code = 0; +}; + +/** + * An interface to run commands on the |Executor|. + * + * Note: In general creating Runnables should only be done by Cronet. Runnables + * created by the app don't have the ability to perform operations when the + * Runnable is being destroyed (i.e. by Cronet_Runnable_Destroy) so resource + * leaks are possible if the Runnable is posted to an Executor that is being + * shutdown with unexecuted Runnables. In controlled testing environments + * deallocation of associated resources can be performed in Run() if the + * runnable can be assumed to always be executed. + */ +[Abstract] +interface Runnable { + Run(); +}; + +/** + * An interface provided by the app to run |command| asynchronously. + */ +[Abstract] +interface Executor { + /** + * Takes ownership of |command| and runs it synchronously or asynchronously. + * Destroys the |command| after execution, or if executor is shutting down. + */ + Execute(Runnable command); +}; + +/** + * Runtime result code returned by Engine and UrlRequest. Equivalent to + * runtime exceptions in Android Java API. All results except SUCCESS trigger + * native crash (via SIGABRT triggered by CHECK failure) unless + * EngineParams.enableCheckResult is set to false. + */ +enum RESULT { + /** + * Operation completed successfully + */ + SUCCESS = 0, + + /** + * Illegal argument + */ + ILLEGAL_ARGUMENT = -100, + /** + * Storage path must be set to existing directory + */ + ILLEGAL_ARGUMENT_STORAGE_PATH_MUST_EXIST = -101, + /** + * Public key pin is invalid + */ + ILLEGAL_ARGUMENT_INVALID_PIN = -102, + /** + * Host name is invalid + */ + ILLEGAL_ARGUMENT_INVALID_HOSTNAME = -103, + /** + * Invalid http method + */ + ILLEGAL_ARGUMENT_INVALID_HTTP_METHOD = -104, + /** + * Invalid http header + */ + ILLEGAL_ARGUMENT_INVALID_HTTP_HEADER = -105, + + /** + * Illegal state + */ + ILLEGAL_STATE = -200, + /** + * Storage path is used by another engine + */ + ILLEGAL_STATE_STORAGE_PATH_IN_USE = -201, + /** + * Cannot shutdown engine from network thread + */ + ILLEGAL_STATE_CANNOT_SHUTDOWN_ENGINE_FROM_NETWORK_THREAD = -202, + /** + * The engine has already started + */ + ILLEGAL_STATE_ENGINE_ALREADY_STARTED = -203, + /** + * The request has already started + */ + ILLEGAL_STATE_REQUEST_ALREADY_STARTED = -204, + /** + * The request is not initialized + */ + ILLEGAL_STATE_REQUEST_NOT_INITIALIZED = -205, + /** + * The request is already initialized + */ + ILLEGAL_STATE_REQUEST_ALREADY_INITIALIZED = -206, + /** + * The request is not started + */ + ILLEGAL_STATE_REQUEST_NOT_STARTED = -207, + /** + * No redirect to follow + */ + ILLEGAL_STATE_UNEXPECTED_REDIRECT = -208, + /** + * Unexpected read attempt + */ + ILLEGAL_STATE_UNEXPECTED_READ = -209, + /** + * Unexpected read failure + */ + ILLEGAL_STATE_READ_FAILED = -210, + + /** + * Null pointer or empty data + */ + NULL_POINTER = -300, + /** + * The hostname cannot be null + */ + NULL_POINTER_HOSTNAME = -301, + /** + * The set of SHA256 pins cannot be null + */ + NULL_POINTER_SHA256_PINS = -302, + /** + * The pin expiration date cannot be null + */ + NULL_POINTER_EXPIRATION_DATE = -303, + /** + * Engine is required + */ + NULL_POINTER_ENGINE = -304, + /** + * URL is required + */ + NULL_POINTER_URL = -305, + /** + * Callback is required + */ + NULL_POINTER_CALLBACK = -306, + /** + * Executor is required + */ + NULL_POINTER_EXECUTOR = -307, + /** + * Method is required + */ + NULL_POINTER_METHOD = -308, + /** + * Invalid header name + */ + NULL_POINTER_HEADER_NAME = -309, + /** + * Invalid header value + */ + NULL_POINTER_HEADER_VALUE = -310, + /** + * Params is required + */ + NULL_POINTER_PARAMS = -311, + /** + * Executor for RequestFinishedInfoListener is required + */ + NULL_POINTER_REQUEST_FINISHED_INFO_LISTENER_EXECUTOR = -312, +}; + +/** + * An engine to process UrlRequests, which uses the best HTTP stack + * available on the current platform. An instance of this class can be started + * using StartWithParams. + */ +interface Engine { + /** + * Starts Engine using given |params|. The engine must be started once + * and only once before other methods can be used. + */ + [Sync] + StartWithParams(EngineParams params) => (RESULT result); + + /** + * Starts NetLog logging to a file. The NetLog will contain events emitted + * by all live Engines. The NetLog is useful for debugging. + * The file can be viewed using a Chrome browser navigated to + * chrome://net-internals/#import + * Returns |true| if netlog has started successfully, |false| otherwise. + * @param file_name the complete file path. It must not be empty. If the file + * exists, it is truncated before starting. If actively logging, + * this method is ignored. + * @param log_all {@code true} to include basic events, user cookies, + * credentials and all transferred bytes in the log. This option presents + * a privacy risk, since it exposes the user's credentials, and should + * only be used with the user's consent and in situations where the log + * won't be public. {@code false} to just include basic events. + */ + [Sync] + StartNetLogToFile(string file_name, bool log_all) => (bool started); + + /** + * Stops NetLog logging and flushes file to disk. If a logging session is + * not in progress, this call is ignored. This method blocks until the log is + * closed to ensure that log file is complete and available. + */ + [Sync] + StopNetLog() => (); + + /** + * Shuts down the Engine if there are no active requests, + * otherwise returns a failure RESULT. + * + * Cannot be called on network thread - the thread Cronet calls into + * Executor on (which is different from the thread the Executor invokes + * callbacks on). This method blocks until all the Engine's resources have + * been cleaned up. + */ + [Sync] + Shutdown() => (RESULT result); + + /** + * Returns a human-readable version string of the engine. + */ + [Sync] + GetVersionString() => (string version_string); + + /** + * Returns default human-readable version string of the engine. Can be used + * before StartWithParams() is called. + */ + [Sync] + GetDefaultUserAgent() => (string default_user_agent); + + /** + * Registers a listener that gets called at the end of each request. + * + * The listener is called on {@code executor}. + * + * The listener is called before {@link UrlRequestCallback.OnCanceled()}, + * {@link UrlRequestCallback.OnFailed()} or {@link + * UrlRequestCallback.OnSucceeded()} is called -- note that if {@code + * executor} runs the listener asyncronously, the actual call to the listener + * may happen after a {@code UrlRequestCallback} method is called. + * + * Listeners are only guaranteed to be called for requests that are started + * after the listener is added. + * + * Ownership is **not** taken for {@code listener} or {@code executor}. + * + * Assuming the listener won't run again (there are no pending requests with + * the listener attached, either via {@code Engine} or {@code UrlRequest}), + * the app may destroy it once its {@code OnRequestFinished()} has started, + * even inside that method. + * + * Similarly, the app may destroy {@code executor} in or after {@code + * OnRequestFinished()}. + * + * It's also OK to destroy {@code executor} in or after one of {@link + * UrlRequestCallback.OnCanceled()}, {@link UrlRequestCallback.OnFailed()} or + * {@link UrlRequestCallback.OnSucceeded()}. + * + * Of course, both of these are only true if {@code listener} won't run again + * and {@code executor} isn't being used for anything else that might start + * running in the future. + * + * @param listener the listener for finished requests. + * @param executor the executor upon which to run {@code listener}. + */ + AddRequestFinishedListener(RequestFinishedInfoListener listener, + Executor executor); + + /** + * Unregisters a RequestFinishedInfoListener, including its association with + * its registered Executor. + */ + RemoveRequestFinishedListener(RequestFinishedInfoListener listener); +}; + +/** + * Hint that |host| supports QUIC. + */ +struct QuicHint { + /** + * Name of the host that supports QUIC. + */ + string host; + + /** + * Port of the server that supports QUIC. + */ + int32 port = 0; + + /** + * Alternate port to use for QUIC. + */ + int32 alternate_port = 0; +}; + +/** + * Pins a set of public keys for a given |host|. By pinning a set of public keys, + * |pinsSha256|, communication with |host| is required to + * authenticate with a certificate with a public key from the set of pinned ones. + * An app can pin the public key of the root certificate, any of the intermediate + * certificates or the end-entry certificate. Authentication will fail and secure + * communication will not be established if none of the public keys is present in the + * host's certificate chain, even if the host attempts to authenticate with a + * certificate allowed by the device's trusted store of certificates. + * + * More information about the public key pinning can be found in + * RFC 7469. + */ +struct PublicKeyPins { + /** + * Name of the host to which the public keys should be pinned. A host that + * consists only of digits and the dot character is treated as invalid. + */ + string host; + + /** + * An array of pins. Each pin is the SHA-256 cryptographic + * hash (in the form of "sha256/") of the DER-encoded ASN.1 + * representation of the Subject Public Key Info (SPKI) of the host's X.509 certificate. + * Although, the method does not mandate the presence of the backup pin + * that can be used if the control of the primary private key has been + * lost, it is highly recommended to supply one. + */ + array pins_sha256; + + /** + * Indicates whether the pinning policy should be applied to subdomains of |host|. + */ + bool include_subdomains = false; + + /** + * The expiration date for the pins in milliseconds since epoch (as in java.util.Date). + */ + int64 expiration_date = 0; +}; + +/** + * Parameters for Engine, which allows its configuration before start. + * Configuration options are set on the EngineParms and + * then Engine.StartWithParams is called to start the Engine. + */ +struct EngineParams { + /** + * Override strict result checking for all operations that return RESULT. + * If set to true, then failed result will cause native crash via SIGABORT. + */ + bool enable_check_result = true; + + /** + * Override of the User-Agent header for all requests. An explicitly + * set User-Agent header will override a value set using this param. + */ + string user_agent; + + /** + * Sets a default value for the Accept-Language header value for UrlRequests + * created by this engine. Explicitly setting the Accept-Language header + * value for individual UrlRequests will override this value. + */ + string accept_language; + + /** + * Directory for HTTP Cache and Prefs Storage. The directory must exist. + */ + string storage_path; + + /** + * Whether QUIC protocol + * is enabled. If QUIC is enabled, then QUIC User Agent Id + * containing application name and Cronet version is sent to the server. + */ + bool enable_quic = true; + + /** + * Whether HTTP/2 + * protocol is enabled. + */ + bool enable_http2 = true; + + /** + * Whether Brotli compression is + * enabled. If enabled, Brotli will be advertised in Accept-Encoding request headers. + */ + bool enable_brotli = true; + + /** + * Enables or disables caching of HTTP data and other information like QUIC + * server information. + */ + enum HTTP_CACHE_MODE { + /** + * Disable HTTP cache. Some data may still be temporarily stored in memory. + */ + DISABLED = 0, + + /** + * Enable in-memory HTTP cache, including HTTP data. + */ + IN_MEMORY = 1, + + /** + * Enable on-disk cache, excluding HTTP data. + * |storagePath| must be set to existing directory. + */ + DISK_NO_HTTP = 2, + + /** + * Enable on-disk cache, including HTTP data. + * |storagePath| must be set to existing directory. + */ + DISK = 3 + }; + HTTP_CACHE_MODE http_cache_mode = DISABLED; + + /** + * Maximum size in bytes used to cache data (advisory and maybe exceeded at + * times). + */ + int64 http_cache_max_size = 0; + + /** + * Hints that hosts support QUIC. + */ + array quic_hints; + + /** + * Pins a set of public keys for given hosts. See |PublicKeyPins| for explanation. + */ + array public_key_pins; + + /** + * Enables or disables public key pinning bypass for local trust anchors. Disabling the + * bypass for local trust anchors is highly discouraged since it may prohibit the app + * from communicating with the pinned hosts. E.g., a user may want to send all traffic + * through an SSL enabled proxy by changing the device proxy settings and adding the + * proxy certificate to the list of local trust anchor. Disabling the bypass will most + * likly prevent the app from sending any traffic to the pinned hosts. For more + * information see 'How does key pinning interact with local proxies and filters?' at + * https://www.chromium.org/Home/chromium-security/security-faq + */ + bool enable_public_key_pinning_bypass_for_local_trust_anchors = true; + + /** + * Optional network thread priority. NAN indicates unset, use default. + * On Android, corresponds to android.os.Process.setThreadPriority() values. + * On iOS, corresponds to NSThread::setThreadPriority values. + * Do not specify for other platforms. + */ + double network_thread_priority = double.NAN; + + /** + * JSON formatted experimental options to be used in Cronet Engine. + */ + string experimental_options; +}; + +/** + * Single HTTP request or response header. + */ +struct HttpHeader { + /** + * Header name. + */ + string name; + + /** + * Header value. + */ + string value; +}; + +struct UrlResponseInfo { + /** + * The URL the response is for. This is the URL after following + * redirects, so it may not be the originally requested URL. + */ + string url; + + /** + * The URL chain. The first entry is the originally requested URL; + * the following entries are redirects followed. + */ + array url_chain; + + /** + * The HTTP status code. When a resource is retrieved from the cache, + * whether it was revalidated or not, the original status code is returned. + */ + int32 http_status_code = 0; + + /** + * The HTTP status text of the status line. For example, if the + * request received a "HTTP/1.1 200 OK" response, this method returns "OK". + */ + string http_status_text; + + /** + * Returns an unmodifiable list of response header field and value pairs. + * The headers are in the same order they are received over the wire. + */ + array all_headers_list; + + /** + * True if the response came from the cache, including + * requests that were revalidated over the network before being retrieved + * from the cache, failed otherwise. + */ + bool was_cached = false; + + /** + * The protocol (for example 'quic/1+spdy/3') negotiated with the server. + * An empty string if no protocol was negotiated, the protocol is + * not known, or when using plain HTTP or HTTPS. + */ + string negotiated_protocol; + + /** + * The proxy server that was used for the request. + */ + string proxy_server; + + /** + * A minimum count of bytes received from the network to process this + * request. This count may ignore certain overheads (for example IP and + * TCP/UDP framing, SSL handshake and framing, proxy handling). This count is + * taken prior to decompression (for example GZIP and Brotli) and includes + * headers and data from all redirects. + */ + int64 received_byte_count = 0; +}; + +/** + * Listener class used with UrlRequest.GetStatus() to receive the status of a + * UrlRequest. + */ +[Abstract] +interface UrlRequestStatusListener { + enum Status { + /** + * This state indicates that the request is completed, canceled, or is not + * started. + */ + INVALID = -1, + + /** + * This state corresponds to a resource load that has either not yet begun + * or is idle waiting for the consumer to do something to move things along + * (e.g. when the consumer of a UrlRequest has not called + * UrlRequest.read() yet). + */ + IDLE = 0, + + /** + * When a socket pool group is below the maximum number of sockets allowed + * per group, but a new socket cannot be created due to the per-pool socket + * limit, this state is returned by all requests for the group waiting on an + * idle connection, except those that may be serviced by a pending new + * connection. + */ + WAITING_FOR_STALLED_SOCKET_POOL = 1, + + /** + * When a socket pool group has reached the maximum number of sockets + * allowed per group, this state is returned for all requests that don't + * have a socket, except those that correspond to a pending new connection. + */ + WAITING_FOR_AVAILABLE_SOCKET = 2, + + /** + * This state indicates that the URLRequest delegate has chosen to block + * this request before it was sent over the network. + */ + WAITING_FOR_DELEGATE = 3, + + /** + * This state corresponds to a resource load that is blocked waiting for + * access to a resource in the cache. If multiple requests are made for the + * same resource, the first request will be responsible for writing (or + * updating) the cache entry and the second request will be deferred until + * the first completes. This may be done to optimize for cache reuse. + */ + WAITING_FOR_CACHE = 4, + + /** + * This state corresponds to a resource being blocked waiting for the + * PAC script to be downloaded. + */ + DOWNLOADING_PAC_FILE = 5, + + /** + * This state corresponds to a resource load that is blocked waiting for a + * proxy autoconfig script to return a proxy server to use. + */ + RESOLVING_PROXY_FOR_URL = 6, + + /** + * This state corresponds to a resource load that is blocked waiting for a + * proxy autoconfig script to return a proxy server to use, but that proxy + * script is busy resolving the IP address of a host. + */ + RESOLVING_HOST_IN_PAC_FILE = 7, + + /** + * This state indicates that we're in the process of establishing a tunnel + * through the proxy server. + */ + ESTABLISHING_PROXY_TUNNEL = 8, + + /** + * This state corresponds to a resource load that is blocked waiting for a + * host name to be resolved. This could either indicate resolution of the + * origin server corresponding to the resource or to the host name of a + * proxy server used to fetch the resource. + */ + RESOLVING_HOST = 9, + + /** + * This state corresponds to a resource load that is blocked waiting for a + * TCP connection (or other network connection) to be established. HTTP + * requests that reuse a keep-alive connection skip this state. + */ + CONNECTING = 10, + + /** + * This state corresponds to a resource load that is blocked waiting for the + * SSL handshake to complete. + */ + SSL_HANDSHAKE = 11, + + /** + * This state corresponds to a resource load that is blocked waiting to + * completely upload a request to a server. In the case of a HTTP POST + * request, this state includes the period of time during which the message + * body is being uploaded. + */ + SENDING_REQUEST = 12, + + /** + * This state corresponds to a resource load that is blocked waiting for the + * response to a network request. In the case of a HTTP transaction, this + * corresponds to the period after the request is sent and before all of the + * response headers have been received. + */ + WAITING_FOR_RESPONSE = 13, + + /** + * This state corresponds to a resource load that is blocked waiting for a + * read to complete. In the case of a HTTP transaction, this corresponds to + * the period after the response headers have been received and before all + * of the response body has been downloaded. (NOTE: This state only applies + * for an {@link UrlRequest} while there is an outstanding + * {@link UrlRequest#read read()} operation.) + */ + READING_RESPONSE = 14 + }; + + /** + * Invoked on UrlRequest's Executor when request status is obtained. + * |status| is representing the status of the request. + */ + OnStatus(Status status); +}; + +/** + * Users of Cronet implement this interface to receive callbacks indicating the + * progress of a UrlRequest being processed. An instance of this interface + * is passed in to UrlRequest's method InitWithParams(). + *

+ * Note: All methods will be invoked on the Executor passed to UrlRequest.InitWithParams(); + */ +[Abstract] +interface UrlRequestCallback { + /** + * Invoked whenever a redirect is encountered. This will only be invoked + * between the call to UrlRequest.start() and + * UrlRequestCallback.onResponseStarted(). + * The body of the redirect response, if it has one, will be ignored. + * + * The redirect will not be followed until the URLRequest.followRedirect() + * ethod is called, either synchronously or asynchronously. + * + * @param request Request being redirected. + * @param info Response information. + * @param new_location_url Location where request is redirected. + */ + OnRedirectReceived(UrlRequest request, UrlResponseInfo info, string new_location_url); + + /** + * Invoked when the final set of headers, after all redirects, is received. + * Will only be invoked once for each request. + * + * With the exception of UrlRequestCallback.onCanceled(), + * no other UrlRequestCallback method will be invoked for the request, + * including UrlRequestCallback.onSucceeded() and + * UrlRequestCallback.onFailed(), until UrlRequest.read()} is called to attempt + * to start reading the response body. + * + * @param request Request that started to get response. + * @param info Response information. + */ + OnResponseStarted(UrlRequest request, UrlResponseInfo info); + + /** + * Invoked whenever part of the response body has been read. Only part of + * the buffer may be populated, even if the entire response body has not yet + * been consumed. This callback transfers ownership of |buffer| back to the app, + * and Cronet guarantees not to access it. + * + * With the exception of UrlRequestCallback.onCanceled(), + * no other UrlRequestCallback method will be invoked for the request, + * including UrlRequestCallback.onSucceeded() and + * UrlRequestCallback.onFailed(), until UrlRequest.read()} is called to attempt + * to continue reading the response body. + * + * @param request Request that received data. + * @param info Response information. + * @param buffer The buffer that was passed in to UrlRequest.read(), now + * containing the received data. + * @param bytes_read The number of bytes read into buffer. + */ + OnReadCompleted(UrlRequest request, UrlResponseInfo info, Buffer buffer, uint64 bytes_read); + + /** + * Invoked when request is completed successfully. Once invoked, no other + * UrlRequestCallback methods will be invoked. + * + * Implementations of {@link #OnSucceeded} are allowed to call {@code + * Cronet_UrlRequest_Destroy(request)}, but note that destroying {@code + * request} destroys {@code info}. + * + * @param request Request that succeeded. + * @param info Response information. NOTE: this is owned by {@code request}. + */ + OnSucceeded(UrlRequest request, UrlResponseInfo info); + + /** + * Invoked if request failed for any reason after UrlRequest.start(). + * Once invoked, no other UrlRequestCallback methods will be invoked. + * |error| provides information about the failure. + * + * Implementations of {@link #OnFailed} are allowed to call {@code + * Cronet_UrlRequest_Destroy(request)}, but note that destroying {@code + * request} destroys {@code info} and {@code error}. + * + * @param request Request that failed. + * @param info Response information. May be null if no response was + * received. NOTE: this is owned by {@code request}. + * @param error information about error. NOTE: this is owned by {@code + * request}. + */ + OnFailed(UrlRequest request, UrlResponseInfo info, Error error); + + /** + * Invoked if request was canceled via UrlRequest.cancel(). Once + * invoked, no other UrlRequestCallback methods will be invoked. + * + * Implementations of {@link #OnCanceled} are allowed to call {@code + * Cronet_UrlRequest_Destroy(request)}, but note that destroying {@code + * request} destroys {@code info}. + * + * @param request Request that was canceled. + * @param info Response information. May be null if no response was + * received. NOTE: this is owned by {@code request}. + */ + OnCanceled(UrlRequest request, UrlResponseInfo info); +}; + +/** + * Defines callbacks methods for UploadDataProvider. All methods + * may be called synchronously or asynchronously, on any thread. + */ +interface UploadDataSink { + /** + * Called by UploadDataProvider when a read succeeds. + * + * @param bytes_read number of bytes read into buffer passed to read(). + * @param final_chunk For chunked uploads, |true| if this is the final + * read. It must be |false| for non-chunked uploads. + */ + OnReadSucceeded(uint64 bytes_read, bool final_chunk); + + /** + * Called by UploadDataProvider when a read fails. + * @param error_message to pass on to UrlRequestCallback.onFailed(). + */ + OnReadError(string error_message); + + /** + * Called by UploadDataProvider when a rewind succeeds. + */ + OnRewindSucceeded(); + + /** + * Called by UploadDataProvider when a rewind fails, or if rewinding + * uploads is not supported. + * @param error_message to pass on to UrlRequestCallback.onFailed(). + */ + OnRewindError(string error_message); +}; + +/** + * The interface allowing the embedder to provide an upload body to + * UrlRequest. It supports both non-chunked (size known in advanced) and + * chunked (size not known in advance) uploads. Be aware that not all servers + * support chunked uploads. + * + * An upload is either always chunked, across multiple uploads if the data + * ends up being sent more than once, or never chunked. + */ +[Abstract] +interface UploadDataProvider { + /** + * If this is a non-chunked upload, returns the length of the upload. Must + * always return -1 if this is a chunked upload. + */ + [Sync] + GetLength() => (int64 length); + + /** + * Reads upload data into |buffer|. Each call of this method must be followed be a + * single call, either synchronous or asynchronous, to + * UploadDataSink.onReadSucceeded() on success + * or UploadDataSink.onReadError() on failure. Neither read nor rewind + * will be called until one of those methods or the other is called. Even if + * the associated UrlRequest is canceled, one or the other must + * still be called before resources can be safely freed. + * + * @param upload_data_sink The object to notify when the read has completed, + * successfully or otherwise. + * @param buffer The buffer to copy the read bytes into. + */ + Read(UploadDataSink upload_data_sink, Buffer buffer); + + /** + * Rewinds upload data. Each call must be followed be a single + * call, either synchronous or asynchronous, to + * UploadDataSink.onRewindSucceeded() on success or + * UploadDataSink.onRewindError() on failure. Neither read nor rewind + * will be called until one of those methods or the other is called. + * Even if the associated UrlRequest is canceled, one or the other + * must still be called before resources can be safely freed. + * + * If rewinding is not supported, this should call + * UploadDataSink.onRewindError(). Note that rewinding is required to + * follow redirects that preserve the upload body, and for retrying when the + * server times out stale sockets. + * + * @param upload_data_sink The object to notify when the rewind operation has + * completed, successfully or otherwise. + */ + Rewind(UploadDataSink upload_data_sink); + + /** + * Called when this UploadDataProvider is no longer needed by a request, so that resources + * (like a file) can be explicitly released. + */ + Close(); +}; + +/** + * Controls an HTTP request (GET, PUT, POST etc). + * Initialized by InitWithParams(). + * Note: All methods must be called on the Executor passed to InitWithParams(). + */ +interface UrlRequest { + /** + * Initialized UrlRequest to |url| with |params|. All methods of |callback| for + * request will be invoked on |executor|. The |executor| must not run tasks on + * the thread calling Executor.execute() to prevent blocking networking + * operations and causing failure RESULTs during shutdown. + * + * @param engine Engine to process the request. + * @param url URL for the request. + * @param params additional parameters for the request, like headers and priority. + * @param callback Callback that gets invoked on different events. + * @param executor Executor on which all callbacks will be invoked. + */ + [Sync] + InitWithParams(Engine engine, + string url, + UrlRequestParams params, + UrlRequestCallback callback, + Executor executor) => (RESULT result); + + /** + * Starts the request, all callbacks go to UrlRequestCallback. May only be called + * once. May not be called if Cancel() has been called. + */ + [Sync] + Start() => (RESULT result); + + /** + * Follows a pending redirect. Must only be called at most once for each + * invocation of UrlRequestCallback.OnRedirectReceived(). + */ + [Sync] + FollowRedirect() => (RESULT result); + + /** + * Attempts to read part of the response body into the provided buffer. + * Must only be called at most once in response to each invocation of the + * UrlRequestCallback.OnResponseStarted() and + * UrlRequestCallback.OnReadCompleted()} methods of the UrlRequestCallback. + * Each call will result in an asynchronous call to + * either the UrlRequestCallback.OnReadCompleted() method if data + * is read, its UrlRequestCallback.OnSucceeded() method if + * there's no more data to read, or its UrlRequestCallback.OnFailed() + * method if there's an error. + * This method transfers ownership of |buffer| to Cronet, and app should + * not access it until one of these callbacks is invoked. + * + * @param buffer to write response body to. The app must not read or + * modify buffer's position, limit, or data between its position and + * limit until the request calls back into the UrlRequestCallback. + */ + [Sync] + Read(Buffer buffer) => (RESULT result); + + /** + * Cancels the request. Can be called at any time. + * UrlRequestCallback.OnCanceled() will be invoked when cancellation + * is complete and no further callback methods will be invoked. If the + * request has completed or has not started, calling Cancel() has no + * effect and OnCanceled() will not be invoked. If the + * Executor passed in to UrlRequest.InitWithParams() runs + * tasks on a single thread, and Cancel() is called on that thread, + * no callback methods (besides OnCanceled() will be invoked after + * Cancel() is called. Otherwise, at most one callback method may be + * invoked after Cancel() has completed. + */ + Cancel(); + + /** + * Returns true if the request was successfully started and is now + * finished (completed, canceled, or failed). + */ + [Sync] + IsDone() => (bool done); + + /** + * Queries the status of the request. + * @param listener a UrlRequestStatusListener that will be invoked with + * the request's current status. Listener will be invoked + * back on the Executor passed in when the request was + * created. + */ + GetStatus(UrlRequestStatusListener listener); +}; + +/** + * Parameters for UrlRequest. Allows configuring requests before initializing them + * with UrlRequest.InitWithParams(). + */ +struct UrlRequestParams { + enum REQUEST_PRIORITY { + /** + * Lowest request priority. + */ + REQUEST_PRIORITY_IDLE = 0, + /** + * Very low request priority. + */ + REQUEST_PRIORITY_LOWEST = 1, + /** + * Low request priority. + */ + REQUEST_PRIORITY_LOW = 2, + /** + * Medium request priority. This is the default priority given to the request. + */ + REQUEST_PRIORITY_MEDIUM = 3, + /** + * Highest request priority. + */ + REQUEST_PRIORITY_HIGHEST = 4, + }; + + /** + * The HTTP method verb to use for this request. + * + * The default when this value is not set is "GET" if the request has + * no body or "POST" if it does. + * + * Allowed methods are "GET", "HEAD", "DELETE", "POST" or "PUT". + */ + string http_method; + + /** + * Array of HTTP headers for this request.. + */ + array request_headers; + + /** + * Disables cache for the request. If context is not set up to use cache, + * this call has no effect. + */ + bool disable_cache = false; + + /** + * Priority of the request which should be one of the REQUEST_PRIORITY values. + */ + REQUEST_PRIORITY priority = REQUEST_PRIORITY_MEDIUM; + + /** + * Upload data provider. Setting this value switches method to "POST" if not + * explicitly set. Starting the request will fail if a Content-Type header is not set. + */ + UploadDataProvider? upload_data_provider; + + /** + * Upload data provider executor that will be used to invoke uploadDataProvider. + */ + Executor? upload_data_provider_executor; + + /** + * Marks that the executors this request will use to notify callbacks (for + * UploadDataProvider and UrlRequestCallback) is intentionally performing + * inline execution without switching to another thread. + * + *

Warning: This option makes it easy to accidentally block the network thread. + * It should not be used if your callbacks perform disk I/O, acquire locks, or call into + * other code you don't carefully control and audit. + */ + bool allow_direct_executor = false; + + /** + * Associates the annotation object with this request. May add more than one. + * Passed through to a RequestFinishedInfoListener. + */ + array annotations; + + /** + * A listener that gets invoked at the end of each request. + * + * The listener is invoked with the request finished info on {@code + * request_finished_executor}, which must be set. + * + * The listener is called before {@link UrlRequestCallback.OnCanceled()}, + * {@link UrlRequestCallback.OnFailed()} or {@link + * UrlRequestCallback.OnSucceeded()} is called -- note that if {@code + * request_finished_executor} runs the listener asyncronously, the actual + * call to the listener may happen after a {@code UrlRequestCallback} method + * is called. + + * Ownership is **not** taken. + * + * Assuming the listener won't run again (there are no pending requests with + * the listener attached, either via {@code Engine} or {@code UrlRequest}), + * the app may destroy it once its {@code OnRequestFinished()} has started, + * even inside that method. + */ + RequestFinishedInfoListener? request_finished_listener; + + /** + * The Executor used to run the {@code request_finished_listener}. + * + * Ownership is **not** taken. + * + * Similar to {@code request_finished_listener}, the app may destroy {@code + * request_finished_executor} in or after {@code OnRequestFinished()}. + * + * It's also OK to destroy {@code request_finished_executor} in or after one + * of {@link UrlRequestCallback.OnCanceled()}, {@link + * UrlRequestCallback.OnFailed()} or {@link + * UrlRequestCallback.OnSucceeded()}. + * + * Of course, both of these are only true if {@code + * request_finished_executor} isn't being used for anything else that might + * start running in the future. + */ + Executor? request_finished_executor; + + enum IDEMPOTENCY { + DEFAULT_IDEMPOTENCY = 0, + IDEMPOTENT = 1, + NOT_IDEMPOTENT = 2, + }; + + /** + * Idempotency of the request, which determines that if it is safe to enable + * 0-RTT for the Cronet request. By default, 0-RTT is only enabled for safe + * HTTP methods, i.e., GET, HEAD, OPTIONS, and TRACE. For other methods, + * enabling 0-RTT may cause security issues since a network observer can + * replay the request. If the request has any side effects, those effects can + * happen multiple times. It is only safe to enable the 0-RTT if it is known + * that the request is idempotent. + */ + IDEMPOTENCY idempotency = DEFAULT_IDEMPOTENCY; +}; + +/** + * Represents a date and time expressed as the number of milliseconds since the + * UNIX epoch. + */ +struct DateTime { + /** + * Number of milliseconds since the UNIX epoch. + */ + int64 value = 0; +}; + +/* + * Represents metrics collected for a single request. Most of these metrics are + * timestamps for events during the lifetime of the request, which can be used + * to build a detailed timeline for investigating performance. + * + * Events happen in this order: + *

    + *
  1. {@link #request_start request start}
  2. + *
  3. {@link #dns_start DNS start}
  4. + *
  5. {@link #dns_end DNS end}
  6. + *
  7. {@link #connect_start connect start}
  8. + *
  9. {@link #ssl_start SSL start}
  10. + *
  11. {@link #ssl_end SSL end}
  12. + *
  13. {@link #connect_end connect end}
  14. + *
  15. {@link #sending_start sending start}
  16. + *
  17. {@link #sending_end sending end}
  18. + *
  19. {@link #response_start response start}
  20. + *
  21. {@link #request_end request end}
  22. + *
+ * + * Start times are reported as the time when a request started blocking on the + * event, not when the event actually occurred, with the exception of push + * start and end. If a metric is not meaningful or not available, including + * cases when a request finished before reaching that stage, start and end + * times will be null. If no time was spent blocking on an event, start and end + * will be the same time. + * + * Timestamps are recorded using a clock that is guaranteed not to run + * backwards. All timestamps are correct relative to the system clock at the + * time of request start, and taking the difference between two timestamps will + * give the correct difference between the events. In order to preserve this + * property, timestamps for events other than request start are not guaranteed + * to match the system clock at the times they represent. + * + * Most timing metrics are taken from + * LoadTimingInfo, + * which holds the information for and + * . + */ +struct Metrics { + /** + * Time when the request started, which corresponds to calling + * Cronet_UrlRequest_Start(). This timestamp will match the system clock at + * the time it represents. + */ + DateTime request_start; + + /** + * Time when DNS lookup started. This and {@link #dns_end} will be set to + * non-null regardless of whether the result came from a DNS server or the + * local cache. Will equal null if the socket was reused (see {@link + * #socket_reused}). + */ + DateTime? dns_start; + + /** + * Time when DNS lookup finished. This and {@link dns_start} will return + * non-null regardless of whether the result came from a DNS server or the + * local cache. Will equal null if the socket was reused (see {@link + * #socket_reused}). + */ + DateTime? dns_end; + + /** + * Time when connection establishment started, typically when DNS resolution + * finishes. Will equal null if the socket was reused (see {@link + * #socket_reused}). + */ + DateTime? connect_start; + + /** + * Time when connection establishment finished, after TCP connection is + * established and, if using HTTPS, SSL handshake is completed. For QUIC + * 0-RTT, this represents the time of handshake confirmation and might happen + * later than {@link #sending_start}. Will equal null if the socket was + * reused (see {@link #socket_reused}). + */ + DateTime? connect_end; + + /** + * Time when SSL handshake started. For QUIC, this will be the same time as + * {@link #connect_start}. Will equal null if SSL is not used or if the + * socket was reused (see {@link #socket_reused}). + */ + DateTime? ssl_start; + + /** + * Time when SSL handshake finished. For QUIC, this will be the same time as + * {@link #connect_end}. Will equal null if SSL is not used or if the socket + * was reused (see {@link #socket_reused}). + */ + DateTime? ssl_end; + + /** + * Time when sending HTTP request headers started. + * + * Will equal null if the request failed or was canceled before sending + * started. + */ + DateTime? sending_start; + + /** + * Time when sending HTTP request body finished. (Sending request body + * happens after sending request headers.) + * + * Will equal null if the request failed or was canceled before sending + * ended. + */ + DateTime? sending_end; + + /** + * Time when first byte of HTTP/2 server push was received. Will equal + * null if server push is not used. + */ + DateTime? push_start; + + /** + * Time when last byte of HTTP/2 server push was received. Will equal + * null if server push is not used. + */ + DateTime? push_end; + + /** + * Time when the end of the response headers was received. + * + * Will equal null if the request failed or was canceled before the response + * started. + */ + DateTime? response_start; + + /** + * Time when the request finished. + */ + DateTime request_end; + + /** + * True if the socket was reused from a previous request, false otherwise. + * In HTTP/2 or QUIC, if streams are multiplexed in a single connection, this + * will be {@code true} for all streams after the first. When {@code true}, + * DNS, connection, and SSL times will be null. + */ + bool socket_reused = false; + + /** + * Returns total bytes sent over the network transport layer, or -1 if not + * collected. + */ + int64 sent_byte_count = -1; + + /** + * Total bytes received over the network transport layer, or -1 if not + * collected. Number of bytes does not include any previous redirects. + */ + int64 received_byte_count = -1; +}; + +/** + * Information about a finished request. + */ +struct RequestFinishedInfo { + /** + * The reason why the request finished. + */ + enum FINISHED_REASON { + /** + * The request succeeded. + */ + SUCCEEDED = 0, + /** + * The request failed or returned an error. + */ + FAILED = 1, + /** + * The request was canceled. + */ + CANCELED = 2, + }; + + /** + * Metrics collected for this request. + */ + Metrics metrics; + + /** + * The objects that the caller has supplied when initiating the request, + * using {@link UrlRequestParams.annotations}. + * + * Annotations can be used to associate a {@link RequestFinishedInfo} with + * the original request or type of request. + */ + array annotations; + + /** + * Returns the reason why the request finished. + */ + FINISHED_REASON finished_reason = SUCCEEDED; +}; + +/** + * Listens for finished requests for the purpose of collecting metrics. + */ +[Abstract] +interface RequestFinishedInfoListener { + /** + * Will be called in a task submitted to the {@code Executor} passed with + * this {@code RequestFinishedInfoListener}. + * + * The listener is called before {@link UrlRequestCallback.OnCanceled()}, + * {@link UrlRequestCallback.OnFailed()} or {@link + * UrlRequestCallback.OnSucceeded()} is called -- note that if the executor + * runs the listener asyncronously, the actual call to the listener may + * happen after a {@code UrlRequestCallback} method is called. + * + * @param request_info {@link RequestFinishedInfo} for finished request. + * Ownership is *not* transferred by this call, do not destroy + * {@code request_info}. + * + * {@code request_info} will be valid as long as the {@code UrlRequest} + * that created it hasn't been destroyed -- **additionally**, it will + * also always be valid for the duration of {@code OnRequestFinished()}, + * even if the {@code UrlRequest} has been destroyed. + * + * This is accomplished by ownership being shared between the {@code + * UrlRequest} and the code that calls this listener. + * + * @param response_info A pointer to the same UrlResponseInfo passed to + * {@link UrlRequestCallback.OnCanceled()}, {@link + * UrlRequestCallback.OnFailed()} or {@link + * UrlRequestCallback.OnSucceeded()}. The lifetime and ownership of + * {@code response_info} works the same as for {@code request_info}. + * + * @param error A pointer to the same Error passed to + * {@code UrlRequestCallback.OnFailed()}, or null if there was no error. + * The lifetime and ownership of {@code error} works the same as for + * {@code request_info}. + */ + OnRequestFinished(RequestFinishedInfo request_info, + UrlResponseInfo response_info, + Error error); +}; diff --git a/src/components/cronet/native/engine.cc b/src/components/cronet/native/engine.cc new file mode 100644 index 0000000000..dae346479f --- /dev/null +++ b/src/components/cronet/native/engine.cc @@ -0,0 +1,499 @@ +// Copyright 2018 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 "components/cronet/native/engine.h" + +#include +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/memory/raw_ptr.h" +#include "base/no_destructor.h" +#include "build/build_config.h" +#include "components/cronet/cronet_global_state.h" +#include "components/cronet/cronet_url_request_context.h" +#include "components/cronet/native/generated/cronet.idl_impl_struct.h" +#include "components/cronet/native/include/cronet_c.h" +#include "components/cronet/native/runnables.h" +#include "components/cronet/url_request_context_config.h" +#include "components/cronet/version.h" +#include "components/grpc_support/include/bidirectional_stream_c.h" +#include "net/base/hash_value.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_context_builder.h" +#include "net/url_request/url_request_context_getter.h" + +namespace { + +class SharedEngineState { + public: + SharedEngineState() + : default_user_agent_(cronet::CreateDefaultUserAgent(CRONET_VERSION)) {} + + SharedEngineState(const SharedEngineState&) = delete; + SharedEngineState& operator=(const SharedEngineState&) = delete; + + // Marks |storage_path| in use, so multiple engines would not use it at the + // same time. Returns |true| if marked successfully, |false| if it is in use + // by another engine. + bool MarkStoragePathInUse(const std::string& storage_path) + LOCKS_EXCLUDED(lock_) { + base::AutoLock lock(lock_); + return in_use_storage_paths_.emplace(storage_path).second; + } + + // Unmarks |storage_path| in use, so another engine could use it. + void UnmarkStoragePathInUse(const std::string& storage_path) + LOCKS_EXCLUDED(lock_) { + base::AutoLock lock(lock_); + in_use_storage_paths_.erase(storage_path); + } + + // Returns default user agent, based on Cronet version, application info and + // platform-specific additional information. + Cronet_String GetDefaultUserAgent() const { + return default_user_agent_.c_str(); + } + + static SharedEngineState* GetInstance(); + + private: + const std::string default_user_agent_; + // Protecting shared state. + base::Lock lock_; + std::unordered_set in_use_storage_paths_ GUARDED_BY(lock_); +}; + +SharedEngineState* SharedEngineState::GetInstance() { + static base::NoDestructor instance; + return instance.get(); +} + +// Does basic validation of host name for PKP and returns |true| if +// host is valid. +bool IsValidHostnameForPkp(const std::string& host) { + if (host.empty()) + return false; + if (host.size() > 255) + return false; + if (host.find_first_of(":\\/=\'\",") != host.npos) + return false; + return true; +} + +} // namespace + +namespace cronet { + +Cronet_EngineImpl::Cronet_EngineImpl() + : init_completed_(base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED), + stop_netlog_completed_(base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED) {} + +Cronet_EngineImpl::~Cronet_EngineImpl() { + Shutdown(); +} + +Cronet_RESULT Cronet_EngineImpl::StartWithParams( + Cronet_EngineParamsPtr params) { + cronet::EnsureInitialized(); + base::AutoLock lock(lock_); + + enable_check_result_ = params->enable_check_result; + if (context_) { + return CheckResult(Cronet_RESULT_ILLEGAL_STATE_ENGINE_ALREADY_STARTED); + } + + URLRequestContextConfigBuilder context_config_builder; + context_config_builder.enable_quic = params->enable_quic; + context_config_builder.enable_spdy = params->enable_http2; + context_config_builder.enable_brotli = params->enable_brotli; + switch (params->http_cache_mode) { + case Cronet_EngineParams_HTTP_CACHE_MODE_DISABLED: + context_config_builder.http_cache = URLRequestContextConfig::DISABLED; + break; + case Cronet_EngineParams_HTTP_CACHE_MODE_IN_MEMORY: + context_config_builder.http_cache = URLRequestContextConfig::MEMORY; + break; + case Cronet_EngineParams_HTTP_CACHE_MODE_DISK: { + context_config_builder.http_cache = URLRequestContextConfig::DISK; +#if BUILDFLAG(IS_WIN) + const base::FilePath storage_path( + base::FilePath::FromUTF8Unsafe(params->storage_path)); +#else + const base::FilePath storage_path(params->storage_path); +#endif + if (!base::DirectoryExists(storage_path)) { + return CheckResult( + Cronet_RESULT_ILLEGAL_ARGUMENT_STORAGE_PATH_MUST_EXIST); + } + if (!SharedEngineState::GetInstance()->MarkStoragePathInUse( + params->storage_path)) { + LOG(ERROR) << "Disk cache path " << params->storage_path + << " is already used, cache disabled."; + return CheckResult(Cronet_RESULT_ILLEGAL_STATE_STORAGE_PATH_IN_USE); + } + in_use_storage_path_ = params->storage_path; + break; + } + default: + context_config_builder.http_cache = URLRequestContextConfig::DISABLED; + } + context_config_builder.http_cache_max_size = params->http_cache_max_size; + context_config_builder.storage_path = params->storage_path; + context_config_builder.accept_language = params->accept_language; + context_config_builder.user_agent = params->user_agent; + context_config_builder.experimental_options = params->experimental_options; + context_config_builder.bypass_public_key_pinning_for_local_trust_anchors = + params->enable_public_key_pinning_bypass_for_local_trust_anchors; + if (!isnan(params->network_thread_priority)) { + context_config_builder.network_thread_priority = + params->network_thread_priority; + } + + // MockCertVerifier to use for testing purposes. + context_config_builder.mock_cert_verifier = std::move(mock_cert_verifier_); + std::unique_ptr config = + context_config_builder.Build(); + + for (const auto& public_key_pins : params->public_key_pins) { + auto pkp = std::make_unique( + public_key_pins.host, public_key_pins.include_subdomains, + base::Time::FromJavaTime(public_key_pins.expiration_date)); + if (pkp->host.empty()) + return CheckResult(Cronet_RESULT_NULL_POINTER_HOSTNAME); + if (!IsValidHostnameForPkp(pkp->host)) + return CheckResult(Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_HOSTNAME); + if (pkp->expiration_date.is_null()) + return CheckResult(Cronet_RESULT_NULL_POINTER_EXPIRATION_DATE); + if (public_key_pins.pins_sha256.empty()) + return CheckResult(Cronet_RESULT_NULL_POINTER_SHA256_PINS); + for (const auto& pin_sha256 : public_key_pins.pins_sha256) { + net::HashValue pin_hash; + if (!pin_hash.FromString(pin_sha256)) + return CheckResult(Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_PIN); + pkp->pin_hashes.push_back(pin_hash); + } + config->pkp_list.push_back(std::move(pkp)); + } + + for (const auto& quic_hint : params->quic_hints) { + config->quic_hints.push_back( + std::make_unique( + quic_hint.host, quic_hint.port, quic_hint.alternate_port)); + } + + context_ = std::make_unique( + std::move(config), std::make_unique(this)); + + // TODO(mef): It'd be nice to remove the java code and this code, and get + // rid of CronetURLRequestContextAdapter::InitRequestContextOnInitThread. + // Could also make CronetURLRequestContext::InitRequestContextOnInitThread() + // private and mark CronetLibraryLoader.postToInitThread() as + // @VisibleForTesting (as the only external use will be in a test). + + // Initialize context on the init thread. + cronet::PostTaskToInitThread( + FROM_HERE, + base::BindOnce(&CronetURLRequestContext::InitRequestContextOnInitThread, + base::Unretained(context_.get()))); + return CheckResult(Cronet_RESULT_SUCCESS); +} + +bool Cronet_EngineImpl::StartNetLogToFile(Cronet_String file_name, + bool log_all) { + base::AutoLock lock(lock_); + if (is_logging_ || !context_) + return false; + is_logging_ = context_->StartNetLogToFile(file_name, log_all); + return is_logging_; +} + +void Cronet_EngineImpl::StopNetLog() { + { + base::AutoLock lock(lock_); + if (!is_logging_ || !context_) + return; + context_->StopNetLog(); + // Release |lock| so it could be acquired in OnStopNetLog. + } + stop_netlog_completed_.Wait(); + stop_netlog_completed_.Reset(); +} + +Cronet_String Cronet_EngineImpl::GetVersionString() { + return CRONET_VERSION; +} + +Cronet_String Cronet_EngineImpl::GetDefaultUserAgent() { + return SharedEngineState::GetInstance()->GetDefaultUserAgent(); +} + +Cronet_RESULT Cronet_EngineImpl::Shutdown() { + { // Check whether engine is running. + base::AutoLock lock(lock_); + if (!context_) + return CheckResult(Cronet_RESULT_SUCCESS); + } + // Wait for init to complete on init and network thread (without lock, so + // other thread could access it). + init_completed_.Wait(); + // If not logging, this is a no-op. + StopNetLog(); + // Stop the engine. + base::AutoLock lock(lock_); + if (context_->IsOnNetworkThread()) { + return CheckResult( + Cronet_RESULT_ILLEGAL_STATE_CANNOT_SHUTDOWN_ENGINE_FROM_NETWORK_THREAD); + } + + if (!in_use_storage_path_.empty()) { + SharedEngineState::GetInstance()->UnmarkStoragePathInUse( + in_use_storage_path_); + } + + stream_engine_.reset(); + context_.reset(); + return CheckResult(Cronet_RESULT_SUCCESS); +} + +void Cronet_EngineImpl::AddRequestFinishedListener( + Cronet_RequestFinishedInfoListenerPtr listener, + Cronet_ExecutorPtr executor) { + if (listener == nullptr || executor == nullptr) { + LOG(DFATAL) << "Both listener and executor must be non-null. listener: " + << listener << " executor: " << executor << "."; + return; + } + base::AutoLock lock(lock_); + if (request_finished_registrations_.count(listener) > 0) { + LOG(DFATAL) << "Listener " << listener + << " already registered with executor " + << request_finished_registrations_[listener] + << ", *NOT* changing to new executor " << executor << "."; + return; + } + request_finished_registrations_.insert({listener, executor}); +} + +void Cronet_EngineImpl::RemoveRequestFinishedListener( + Cronet_RequestFinishedInfoListenerPtr listener) { + base::AutoLock lock(lock_); + if (request_finished_registrations_.erase(listener) != 1) { + LOG(DFATAL) << "Asked to erase non-existent RequestFinishedInfoListener " + << listener << "."; + } +} + +namespace { + +using RequestFinishedInfo = base::RefCountedData; +using UrlResponseInfo = base::RefCountedData; +using CronetError = base::RefCountedData; + +template +T* GetData(scoped_refptr> ptr) { + return ptr == nullptr ? nullptr : &ptr->data; +} + +} // namespace + +void Cronet_EngineImpl::ReportRequestFinished( + scoped_refptr request_info, + scoped_refptr url_response_info, + scoped_refptr error) { + base::flat_map + registrations; + { + base::AutoLock lock(lock_); + // We copy under to avoid calling callbacks (which may run on direct + // executors and call Engine methods) with the lock held. + // + // The map holds only pointers and shouldn't be very large. + registrations = request_finished_registrations_; + } + for (auto& pair : registrations) { + auto* request_finished_listener = pair.first; + auto* request_finished_executor = pair.second; + + request_finished_executor->Execute( + new cronet::OnceClosureRunnable(base::BindOnce( + [](Cronet_RequestFinishedInfoListenerPtr request_finished_listener, + scoped_refptr request_info, + scoped_refptr url_response_info, + scoped_refptr error) { + request_finished_listener->OnRequestFinished( + GetData(request_info), GetData(url_response_info), + GetData(error)); + }, + request_finished_listener, request_info, url_response_info, + error))); + } +} + +Cronet_RESULT Cronet_EngineImpl::CheckResult(Cronet_RESULT result) { + if (enable_check_result_) + CHECK_EQ(Cronet_RESULT_SUCCESS, result); + return result; +} + +bool Cronet_EngineImpl::HasRequestFinishedListener() { + base::AutoLock lock(lock_); + return request_finished_registrations_.size() > 0; +} + +// The struct stream_engine for grpc support. +// Holds net::URLRequestContextGetter and app-specific annotation. +class Cronet_EngineImpl::StreamEngineImpl : public stream_engine { + public: + explicit StreamEngineImpl(net::URLRequestContextGetter* context_getter) { + context_getter_ = context_getter; + obj = context_getter_.get(); + annotation = nullptr; + } + + ~StreamEngineImpl() { + obj = nullptr; + annotation = nullptr; + } + + private: + scoped_refptr context_getter_; +}; + +// Callback is owned by CronetURLRequestContext. It is invoked and deleted +// on the network thread. +class Cronet_EngineImpl::Callback : public CronetURLRequestContext::Callback { + public: + explicit Callback(Cronet_EngineImpl* engine); + + Callback(const Callback&) = delete; + Callback& operator=(const Callback&) = delete; + + ~Callback() override; + + // CronetURLRequestContext::Callback implementation: + void OnInitNetworkThread() override LOCKS_EXCLUDED(engine_->lock_); + void OnDestroyNetworkThread() override; + void OnEffectiveConnectionTypeChanged( + net::EffectiveConnectionType effective_connection_type) override; + void OnRTTOrThroughputEstimatesComputed( + int32_t http_rtt_ms, + int32_t transport_rtt_ms, + int32_t downstream_throughput_kbps) override; + void OnRTTObservation(int32_t rtt_ms, + int32_t timestamp_ms, + net::NetworkQualityObservationSource source) override; + void OnThroughputObservation( + int32_t throughput_kbps, + int32_t timestamp_ms, + net::NetworkQualityObservationSource source) override; + void OnStopNetLogCompleted() override LOCKS_EXCLUDED(engine_->lock_); + + private: + // The engine which owns context that owns |this| callback. + const raw_ptr engine_; + + // All methods are invoked on the network thread. + THREAD_CHECKER(network_thread_checker_); +}; + +Cronet_EngineImpl::Callback::Callback(Cronet_EngineImpl* engine) + : engine_(engine) { + DETACH_FROM_THREAD(network_thread_checker_); +} + +Cronet_EngineImpl::Callback::~Callback() = default; + +void Cronet_EngineImpl::Callback::OnInitNetworkThread() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + // It is possible that engine_->context_ is reset from main thread while + // being intialized on network thread. + base::AutoLock lock(engine_->lock_); + if (engine_->context_) { + // Initialize bidirectional stream engine for grpc. + engine_->stream_engine_ = std::make_unique( + engine_->context_->CreateURLRequestContextGetter()); + engine_->init_completed_.Signal(); + } +} + +void Cronet_EngineImpl::Callback::OnDestroyNetworkThread() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + DCHECK(!engine_->stream_engine_); +} + +void Cronet_EngineImpl::Callback::OnEffectiveConnectionTypeChanged( + net::EffectiveConnectionType effective_connection_type) { + NOTIMPLEMENTED(); +} + +void Cronet_EngineImpl::Callback::OnRTTOrThroughputEstimatesComputed( + int32_t http_rtt_ms, + int32_t transport_rtt_ms, + int32_t downstream_throughput_kbps) { + NOTIMPLEMENTED(); +} + +void Cronet_EngineImpl::Callback::OnRTTObservation( + int32_t rtt_ms, + int32_t timestamp_ms, + net::NetworkQualityObservationSource source) { + NOTIMPLEMENTED(); +} + +void Cronet_EngineImpl::Callback::OnThroughputObservation( + int32_t throughput_kbps, + int32_t timestamp_ms, + net::NetworkQualityObservationSource source) { + NOTIMPLEMENTED(); +} + +void Cronet_EngineImpl::Callback::OnStopNetLogCompleted() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + CHECK(engine_); + base::AutoLock lock(engine_->lock_); + DCHECK(engine_->is_logging_); + engine_->is_logging_ = false; + engine_->stop_netlog_completed_.Signal(); +} + +void Cronet_EngineImpl::SetMockCertVerifierForTesting( + std::unique_ptr mock_cert_verifier) { + CHECK(!context_); + mock_cert_verifier_ = std::move(mock_cert_verifier); +} + +stream_engine* Cronet_EngineImpl::GetBidirectionalStreamEngine() { + init_completed_.Wait(); + return stream_engine_.get(); +} + +} // namespace cronet + +CRONET_EXPORT Cronet_EnginePtr Cronet_Engine_Create() { + return new cronet::Cronet_EngineImpl(); +} + +CRONET_EXPORT void Cronet_Engine_SetMockCertVerifierForTesting( + Cronet_EnginePtr engine, + void* raw_mock_cert_verifier) { + cronet::Cronet_EngineImpl* engine_impl = + static_cast(engine); + std::unique_ptr cert_verifier; + cert_verifier.reset(static_cast(raw_mock_cert_verifier)); + engine_impl->SetMockCertVerifierForTesting(std::move(cert_verifier)); +} + +CRONET_EXPORT stream_engine* Cronet_Engine_GetStreamEngine( + Cronet_EnginePtr engine) { + cronet::Cronet_EngineImpl* engine_impl = + static_cast(engine); + return engine_impl->GetBidirectionalStreamEngine(); +} diff --git a/src/components/cronet/native/engine.h b/src/components/cronet/native/engine.h new file mode 100644 index 0000000000..395a02e9d8 --- /dev/null +++ b/src/components/cronet/native/engine.h @@ -0,0 +1,117 @@ +// Copyright 2018 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_NATIVE_ENGINE_H_ +#define COMPONENTS_CRONET_NATIVE_ENGINE_H_ + +#include +#include + +#include "base/containers/flat_map.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_refptr.h" +#include "base/synchronization/lock.h" +#include "base/synchronization/waitable_event.h" +#include "base/thread_annotations.h" +#include "components/cronet/native/generated/cronet.idl_impl_interface.h" + +extern "C" typedef struct stream_engine stream_engine; + +namespace net { +class CertVerifier; +} + +namespace cronet { +class CronetURLRequestContext; + +// Implementation of Cronet_Engine that uses CronetURLRequestContext. +class Cronet_EngineImpl : public Cronet_Engine { + public: + Cronet_EngineImpl(); + + Cronet_EngineImpl(const Cronet_EngineImpl&) = delete; + Cronet_EngineImpl& operator=(const Cronet_EngineImpl&) = delete; + + ~Cronet_EngineImpl() override; + + // Cronet_Engine implementation: + Cronet_RESULT StartWithParams(Cronet_EngineParamsPtr params) override + LOCKS_EXCLUDED(lock_); + bool StartNetLogToFile(Cronet_String file_name, bool log_all) override + LOCKS_EXCLUDED(lock_); + void StopNetLog() override LOCKS_EXCLUDED(lock_); + Cronet_String GetVersionString() override; + Cronet_String GetDefaultUserAgent() override; + Cronet_RESULT Shutdown() override LOCKS_EXCLUDED(lock_); + void AddRequestFinishedListener( + Cronet_RequestFinishedInfoListenerPtr listener, + Cronet_ExecutorPtr executor) override; + void RemoveRequestFinishedListener( + Cronet_RequestFinishedInfoListenerPtr listener) override; + + // Check |result| and aborts if result is not SUCCESS and enableCheckResult + // is true. + Cronet_RESULT CheckResult(Cronet_RESULT result); + + // Set Mock CertVerifier for testing. Must be called before StartWithParams. + void SetMockCertVerifierForTesting( + std::unique_ptr mock_cert_verifier); + + // Get stream engine for GRPC Bidirectional Stream support. The returned + // stream_engine is owned by |this| and is only valid until |this| shutdown. + stream_engine* GetBidirectionalStreamEngine(); + + CronetURLRequestContext* cronet_url_request_context() const { + return context_.get(); + } + + // Returns true if there is a listener currently registered (using + // AddRequestFinishedListener()), and false otherwise. + bool HasRequestFinishedListener(); + + // Provide |request_info| to all registered RequestFinishedListeners. + void ReportRequestFinished( + scoped_refptr> + request_info, + scoped_refptr> + url_response_info, + scoped_refptr> error); + + private: + class StreamEngineImpl; + class Callback; + + // Enable runtime CHECK of the result. + bool enable_check_result_ = true; + + // Synchronize access to member variables from different threads. + base::Lock lock_; + // Cronet URLRequest context used for all network operations. + std::unique_ptr context_; + // Signaled when |context_| initialization is done. + base::WaitableEvent init_completed_; + + // Flag that indicates whether logging is in progress. + bool is_logging_ GUARDED_BY(lock_) = false; + // Signaled when |StopNetLog| is done. + base::WaitableEvent stop_netlog_completed_; + + // Storage path used by this engine. + std::string in_use_storage_path_ GUARDED_BY(lock_); + + // Stream engine for GRPC Bidirectional Stream support. + std::unique_ptr stream_engine_; + + // Mock CertVerifier for testing. Only valid until StartWithParams. + std::unique_ptr mock_cert_verifier_; + + // Stores registered RequestFinishedInfoListeners with their associated + // Executors. + base::flat_map + request_finished_registrations_ GUARDED_BY(lock_); +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_NATIVE_ENGINE_H_ diff --git a/src/components/cronet/native/engine_unittest.cc b/src/components/cronet/native/engine_unittest.cc new file mode 100644 index 0000000000..4e7c9777e7 --- /dev/null +++ b/src/components/cronet/native/engine_unittest.cc @@ -0,0 +1,253 @@ +// Copyright 2019 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_c.h" + +#include "base/test/gtest_util.h" +#include "build/build_config.h" +#include "components/cronet/native/engine.h" +#include "components/cronet/native/generated/cronet.idl_impl_struct.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace cronet { + +namespace { + +// Fake sent byte count for metrics testing. +constexpr int64_t kSentByteCount = 12345; + +// App implementation of Cronet_Executor methods. +void TestExecutor_Execute(Cronet_ExecutorPtr self, Cronet_RunnablePtr command) { + CHECK(self); + Cronet_Runnable_Run(command); + Cronet_Runnable_Destroy(command); +} + +// Context for TestRequestInfoListener_OnRequestFinished(). +using TestOnRequestFinishedClientContext = int; + +// App implementation of Cronet_RequestFinishedInfoListener methods. +// +// Expects a client context of type TestOnRequestFinishedClientContext -- will +// increment this value. +void TestRequestInfoListener_OnRequestFinished( + Cronet_RequestFinishedInfoListenerPtr self, + Cronet_RequestFinishedInfoPtr request_info, + Cronet_UrlResponseInfoPtr url_response_info, + Cronet_ErrorPtr error) { + CHECK(self); + Cronet_ClientContext context = + Cronet_RequestFinishedInfoListener_GetClientContext(self); + auto* listener_run_count = + static_cast(context); + ++(*listener_run_count); + auto* metrics = Cronet_RequestFinishedInfo_metrics_get(request_info); + EXPECT_EQ(kSentByteCount, Cronet_Metrics_sent_byte_count_get(metrics)); + EXPECT_NE(nullptr, url_response_info); + EXPECT_NE(nullptr, error); +} + +TEST(EngineUnitTest, HasNoRequestFinishedInfoListener) { + Cronet_EnginePtr engine = Cronet_Engine_Create(); + + auto* engine_impl = static_cast(engine); + EXPECT_FALSE(engine_impl->HasRequestFinishedListener()); + + Cronet_Engine_Destroy(engine); +} + +TEST(EngineUnitTest, HasRequestFinishedInfoListener) { + Cronet_EnginePtr engine = Cronet_Engine_Create(); + + Cronet_RequestFinishedInfoListenerPtr listener = + Cronet_RequestFinishedInfoListener_CreateWith( + TestRequestInfoListener_OnRequestFinished); + Cronet_ExecutorPtr executor = + Cronet_Executor_CreateWith(TestExecutor_Execute); + Cronet_Engine_AddRequestFinishedListener(engine, listener, executor); + + auto* engine_impl = static_cast(engine); + EXPECT_TRUE(engine_impl->HasRequestFinishedListener()); + + Cronet_Engine_RemoveRequestFinishedListener(engine, listener); + EXPECT_FALSE(engine_impl->HasRequestFinishedListener()); + Cronet_Executor_Destroy(executor); + Cronet_RequestFinishedInfoListener_Destroy(listener); + Cronet_Engine_Destroy(engine); +} + +TEST(EngineUnitTest, RequestFinishedInfoListeners) { + using RequestInfo = base::RefCountedData; + using UrlResponseInfo = base::RefCountedData; + using CronetError = base::RefCountedData; + constexpr int kNumListeners = 5; + TestOnRequestFinishedClientContext listener_run_count = 0; + + Cronet_EnginePtr engine = Cronet_Engine_Create(); + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + + Cronet_RequestFinishedInfoListenerPtr listeners[kNumListeners]; + Cronet_ExecutorPtr executor = + Cronet_Executor_CreateWith(TestExecutor_Execute); + for (int i = 0; i < kNumListeners; ++i) { + listeners[i] = Cronet_RequestFinishedInfoListener_CreateWith( + TestRequestInfoListener_OnRequestFinished); + Cronet_RequestFinishedInfoListener_SetClientContext(listeners[i], + &listener_run_count); + Cronet_Engine_AddRequestFinishedListener(engine, listeners[i], executor); + } + + // Simulate the UrlRequest reporting metrics to the engine. + auto* engine_impl = static_cast(engine); + auto request_info = base::MakeRefCounted(); + auto url_response_info = base::MakeRefCounted(); + auto error = base::MakeRefCounted(); + auto metrics = std::make_unique(); + metrics->sent_byte_count = kSentByteCount; + request_info->data.metrics.emplace(*metrics); + engine_impl->ReportRequestFinished(request_info, url_response_info, error); + EXPECT_EQ(kNumListeners, listener_run_count); + + for (auto* listener : listeners) { + Cronet_RequestFinishedInfoListener_Destroy(listener); + Cronet_Engine_RemoveRequestFinishedListener(engine, listener); + } + Cronet_Executor_Destroy(executor); + Cronet_Engine_Destroy(engine); + Cronet_EngineParams_Destroy(engine_params); +} + +TEST(EngineUnitTest, AddNullRequestFinishedInfoListener) { + Cronet_EnginePtr engine = Cronet_Engine_Create(); + + Cronet_ExecutorPtr executor = + Cronet_Executor_CreateWith(TestExecutor_Execute); + EXPECT_DCHECK_DEATH_WITH( + Cronet_Engine_AddRequestFinishedListener(engine, nullptr, executor), + "Both listener and executor must be non-null. listener: .* executor: " + ".*\\."); + + auto* engine_impl = static_cast(engine); + EXPECT_FALSE(engine_impl->HasRequestFinishedListener()); + + Cronet_Executor_Destroy(executor); + Cronet_Engine_Destroy(engine); +} + +TEST(EngineUnitTest, AddNullRequestFinishedInfoExecutor) { + Cronet_EnginePtr engine = Cronet_Engine_Create(); + + Cronet_RequestFinishedInfoListenerPtr listener = + Cronet_RequestFinishedInfoListener_CreateWith( + TestRequestInfoListener_OnRequestFinished); + EXPECT_DCHECK_DEATH_WITH( + Cronet_Engine_AddRequestFinishedListener(engine, listener, nullptr), + "Both listener and executor must be non-null. listener: .* executor: " + ".*\\."); + + auto* engine_impl = static_cast(engine); + EXPECT_FALSE(engine_impl->HasRequestFinishedListener()); + + Cronet_RequestFinishedInfoListener_Destroy(listener); + Cronet_Engine_Destroy(engine); +} + +TEST(EngineUnitTest, AddNullRequestFinishedInfoListenerAndExecutor) { + Cronet_EnginePtr engine = Cronet_Engine_Create(); + + EXPECT_DCHECK_DEATH_WITH( + Cronet_Engine_AddRequestFinishedListener(engine, nullptr, nullptr), + "Both listener and executor must be non-null. listener: .* executor: " + ".*\\."); + + auto* engine_impl = static_cast(engine); + EXPECT_FALSE(engine_impl->HasRequestFinishedListener()); + + Cronet_Engine_Destroy(engine); +} + +TEST(EngineUnitTest, AddListenerTwice) { + Cronet_EnginePtr engine = Cronet_Engine_Create(); + + Cronet_RequestFinishedInfoListenerPtr listener = + Cronet_RequestFinishedInfoListener_CreateWith( + TestRequestInfoListener_OnRequestFinished); + Cronet_ExecutorPtr executor = + Cronet_Executor_CreateWith(TestExecutor_Execute); + Cronet_Engine_AddRequestFinishedListener(engine, listener, executor); + EXPECT_DCHECK_DEATH_WITH( + Cronet_Engine_AddRequestFinishedListener(engine, listener, executor), + "Listener .* already registered with executor .*, \\*NOT\\* changing to " + "new executor .*\\."); + + auto* engine_impl = static_cast(engine); + EXPECT_TRUE(engine_impl->HasRequestFinishedListener()); + + Cronet_Engine_RemoveRequestFinishedListener(engine, listener); + EXPECT_FALSE(engine_impl->HasRequestFinishedListener()); + Cronet_Executor_Destroy(executor); + Cronet_RequestFinishedInfoListener_Destroy(listener); + Cronet_Engine_Destroy(engine); +} + +TEST(EngineUnitTest, RemoveNonexistentListener) { + Cronet_EnginePtr engine = Cronet_Engine_Create(); + + Cronet_RequestFinishedInfoListenerPtr listener = + Cronet_RequestFinishedInfoListener_CreateWith( + TestRequestInfoListener_OnRequestFinished); + EXPECT_DCHECK_DEATH_WITH( + Cronet_Engine_RemoveRequestFinishedListener(engine, listener), + "Asked to erase non-existent RequestFinishedInfoListener .*\\."); + + auto* engine_impl = static_cast(engine); + EXPECT_FALSE(engine_impl->HasRequestFinishedListener()); + + Cronet_RequestFinishedInfoListener_Destroy(listener); + Cronet_Engine_Destroy(engine); +} + +TEST(EngineUnitTest, RemoveNonexistentListenerWithAddedListener) { + Cronet_EnginePtr engine = Cronet_Engine_Create(); + + Cronet_RequestFinishedInfoListenerPtr listener = + Cronet_RequestFinishedInfoListener_CreateWith( + TestRequestInfoListener_OnRequestFinished); + Cronet_RequestFinishedInfoListenerPtr listener2 = + Cronet_RequestFinishedInfoListener_CreateWith( + TestRequestInfoListener_OnRequestFinished); + Cronet_ExecutorPtr executor = + Cronet_Executor_CreateWith(TestExecutor_Execute); + Cronet_Engine_AddRequestFinishedListener(engine, listener, executor); + + EXPECT_DCHECK_DEATH_WITH( + Cronet_Engine_RemoveRequestFinishedListener(engine, listener2), + "Asked to erase non-existent RequestFinishedInfoListener .*\\."); + + auto* engine_impl = static_cast(engine); + EXPECT_TRUE(engine_impl->HasRequestFinishedListener()); + + Cronet_Engine_RemoveRequestFinishedListener(engine, listener); + EXPECT_FALSE(engine_impl->HasRequestFinishedListener()); + Cronet_RequestFinishedInfoListener_Destroy(listener); + Cronet_RequestFinishedInfoListener_Destroy(listener2); + Cronet_Executor_Destroy(executor); + Cronet_Engine_Destroy(engine); +} + +TEST(EngineUnitTest, RemoveNullListener) { + Cronet_EnginePtr engine = Cronet_Engine_Create(); + + EXPECT_DCHECK_DEATH_WITH( + Cronet_Engine_RemoveRequestFinishedListener(engine, nullptr), + "Asked to erase non-existent RequestFinishedInfoListener .*\\."); + + auto* engine_impl = static_cast(engine); + EXPECT_FALSE(engine_impl->HasRequestFinishedListener()); + + Cronet_Engine_Destroy(engine); +} + +} // namespace +} // namespace cronet diff --git a/src/components/cronet/native/generated/cronet.idl_c.h b/src/components/cronet/native/generated/cronet.idl_c.h new file mode 100644 index 0000000000..041fd8f588 --- /dev/null +++ b/src/components/cronet/native/generated/cronet.idl_c.h @@ -0,0 +1,1284 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from components/cronet/native/generated/cronet.idl */ + +#ifndef COMPONENTS_CRONET_NATIVE_GENERATED_CRONET_IDL_C_H_ +#define COMPONENTS_CRONET_NATIVE_GENERATED_CRONET_IDL_C_H_ +#include "cronet_export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef const char* Cronet_String; +typedef void* Cronet_RawDataPtr; +typedef void* Cronet_ClientContext; + +// Forward declare interfaces. +typedef struct Cronet_Buffer Cronet_Buffer; +typedef struct Cronet_Buffer* Cronet_BufferPtr; +typedef struct Cronet_BufferCallback Cronet_BufferCallback; +typedef struct Cronet_BufferCallback* Cronet_BufferCallbackPtr; +typedef struct Cronet_Runnable Cronet_Runnable; +typedef struct Cronet_Runnable* Cronet_RunnablePtr; +typedef struct Cronet_Executor Cronet_Executor; +typedef struct Cronet_Executor* Cronet_ExecutorPtr; +typedef struct Cronet_Engine Cronet_Engine; +typedef struct Cronet_Engine* Cronet_EnginePtr; +typedef struct Cronet_UrlRequestStatusListener Cronet_UrlRequestStatusListener; +typedef struct Cronet_UrlRequestStatusListener* + Cronet_UrlRequestStatusListenerPtr; +typedef struct Cronet_UrlRequestCallback Cronet_UrlRequestCallback; +typedef struct Cronet_UrlRequestCallback* Cronet_UrlRequestCallbackPtr; +typedef struct Cronet_UploadDataSink Cronet_UploadDataSink; +typedef struct Cronet_UploadDataSink* Cronet_UploadDataSinkPtr; +typedef struct Cronet_UploadDataProvider Cronet_UploadDataProvider; +typedef struct Cronet_UploadDataProvider* Cronet_UploadDataProviderPtr; +typedef struct Cronet_UrlRequest Cronet_UrlRequest; +typedef struct Cronet_UrlRequest* Cronet_UrlRequestPtr; +typedef struct Cronet_RequestFinishedInfoListener + Cronet_RequestFinishedInfoListener; +typedef struct Cronet_RequestFinishedInfoListener* + Cronet_RequestFinishedInfoListenerPtr; + +// Forward declare structs. +typedef struct Cronet_Error Cronet_Error; +typedef struct Cronet_Error* Cronet_ErrorPtr; +typedef struct Cronet_QuicHint Cronet_QuicHint; +typedef struct Cronet_QuicHint* Cronet_QuicHintPtr; +typedef struct Cronet_PublicKeyPins Cronet_PublicKeyPins; +typedef struct Cronet_PublicKeyPins* Cronet_PublicKeyPinsPtr; +typedef struct Cronet_EngineParams Cronet_EngineParams; +typedef struct Cronet_EngineParams* Cronet_EngineParamsPtr; +typedef struct Cronet_HttpHeader Cronet_HttpHeader; +typedef struct Cronet_HttpHeader* Cronet_HttpHeaderPtr; +typedef struct Cronet_UrlResponseInfo Cronet_UrlResponseInfo; +typedef struct Cronet_UrlResponseInfo* Cronet_UrlResponseInfoPtr; +typedef struct Cronet_UrlRequestParams Cronet_UrlRequestParams; +typedef struct Cronet_UrlRequestParams* Cronet_UrlRequestParamsPtr; +typedef struct Cronet_DateTime Cronet_DateTime; +typedef struct Cronet_DateTime* Cronet_DateTimePtr; +typedef struct Cronet_Metrics Cronet_Metrics; +typedef struct Cronet_Metrics* Cronet_MetricsPtr; +typedef struct Cronet_RequestFinishedInfo Cronet_RequestFinishedInfo; +typedef struct Cronet_RequestFinishedInfo* Cronet_RequestFinishedInfoPtr; + +// Declare enums +typedef enum Cronet_RESULT { + Cronet_RESULT_SUCCESS = 0, + Cronet_RESULT_ILLEGAL_ARGUMENT = -100, + Cronet_RESULT_ILLEGAL_ARGUMENT_STORAGE_PATH_MUST_EXIST = -101, + Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_PIN = -102, + Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_HOSTNAME = -103, + Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_HTTP_METHOD = -104, + Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_HTTP_HEADER = -105, + Cronet_RESULT_ILLEGAL_STATE = -200, + Cronet_RESULT_ILLEGAL_STATE_STORAGE_PATH_IN_USE = -201, + Cronet_RESULT_ILLEGAL_STATE_CANNOT_SHUTDOWN_ENGINE_FROM_NETWORK_THREAD = -202, + Cronet_RESULT_ILLEGAL_STATE_ENGINE_ALREADY_STARTED = -203, + Cronet_RESULT_ILLEGAL_STATE_REQUEST_ALREADY_STARTED = -204, + Cronet_RESULT_ILLEGAL_STATE_REQUEST_NOT_INITIALIZED = -205, + Cronet_RESULT_ILLEGAL_STATE_REQUEST_ALREADY_INITIALIZED = -206, + Cronet_RESULT_ILLEGAL_STATE_REQUEST_NOT_STARTED = -207, + Cronet_RESULT_ILLEGAL_STATE_UNEXPECTED_REDIRECT = -208, + Cronet_RESULT_ILLEGAL_STATE_UNEXPECTED_READ = -209, + Cronet_RESULT_ILLEGAL_STATE_READ_FAILED = -210, + Cronet_RESULT_NULL_POINTER = -300, + Cronet_RESULT_NULL_POINTER_HOSTNAME = -301, + Cronet_RESULT_NULL_POINTER_SHA256_PINS = -302, + Cronet_RESULT_NULL_POINTER_EXPIRATION_DATE = -303, + Cronet_RESULT_NULL_POINTER_ENGINE = -304, + Cronet_RESULT_NULL_POINTER_URL = -305, + Cronet_RESULT_NULL_POINTER_CALLBACK = -306, + Cronet_RESULT_NULL_POINTER_EXECUTOR = -307, + Cronet_RESULT_NULL_POINTER_METHOD = -308, + Cronet_RESULT_NULL_POINTER_HEADER_NAME = -309, + Cronet_RESULT_NULL_POINTER_HEADER_VALUE = -310, + Cronet_RESULT_NULL_POINTER_PARAMS = -311, + Cronet_RESULT_NULL_POINTER_REQUEST_FINISHED_INFO_LISTENER_EXECUTOR = -312, +} Cronet_RESULT; + +typedef enum Cronet_Error_ERROR_CODE { + Cronet_Error_ERROR_CODE_ERROR_CALLBACK = 0, + Cronet_Error_ERROR_CODE_ERROR_HOSTNAME_NOT_RESOLVED = 1, + Cronet_Error_ERROR_CODE_ERROR_INTERNET_DISCONNECTED = 2, + Cronet_Error_ERROR_CODE_ERROR_NETWORK_CHANGED = 3, + Cronet_Error_ERROR_CODE_ERROR_TIMED_OUT = 4, + Cronet_Error_ERROR_CODE_ERROR_CONNECTION_CLOSED = 5, + Cronet_Error_ERROR_CODE_ERROR_CONNECTION_TIMED_OUT = 6, + Cronet_Error_ERROR_CODE_ERROR_CONNECTION_REFUSED = 7, + Cronet_Error_ERROR_CODE_ERROR_CONNECTION_RESET = 8, + Cronet_Error_ERROR_CODE_ERROR_ADDRESS_UNREACHABLE = 9, + Cronet_Error_ERROR_CODE_ERROR_QUIC_PROTOCOL_FAILED = 10, + Cronet_Error_ERROR_CODE_ERROR_OTHER = 11, +} Cronet_Error_ERROR_CODE; + +typedef enum Cronet_EngineParams_HTTP_CACHE_MODE { + Cronet_EngineParams_HTTP_CACHE_MODE_DISABLED = 0, + Cronet_EngineParams_HTTP_CACHE_MODE_IN_MEMORY = 1, + Cronet_EngineParams_HTTP_CACHE_MODE_DISK_NO_HTTP = 2, + Cronet_EngineParams_HTTP_CACHE_MODE_DISK = 3, +} Cronet_EngineParams_HTTP_CACHE_MODE; + +typedef enum Cronet_UrlRequestParams_REQUEST_PRIORITY { + Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_IDLE = 0, + Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_LOWEST = 1, + Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_LOW = 2, + Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_MEDIUM = 3, + Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_HIGHEST = 4, +} Cronet_UrlRequestParams_REQUEST_PRIORITY; + +typedef enum Cronet_UrlRequestParams_IDEMPOTENCY { + Cronet_UrlRequestParams_IDEMPOTENCY_DEFAULT_IDEMPOTENCY = 0, + Cronet_UrlRequestParams_IDEMPOTENCY_IDEMPOTENT = 1, + Cronet_UrlRequestParams_IDEMPOTENCY_NOT_IDEMPOTENT = 2, +} Cronet_UrlRequestParams_IDEMPOTENCY; + +typedef enum Cronet_RequestFinishedInfo_FINISHED_REASON { + Cronet_RequestFinishedInfo_FINISHED_REASON_SUCCEEDED = 0, + Cronet_RequestFinishedInfo_FINISHED_REASON_FAILED = 1, + Cronet_RequestFinishedInfo_FINISHED_REASON_CANCELED = 2, +} Cronet_RequestFinishedInfo_FINISHED_REASON; + +typedef enum Cronet_UrlRequestStatusListener_Status { + Cronet_UrlRequestStatusListener_Status_INVALID = -1, + Cronet_UrlRequestStatusListener_Status_IDLE = 0, + Cronet_UrlRequestStatusListener_Status_WAITING_FOR_STALLED_SOCKET_POOL = 1, + Cronet_UrlRequestStatusListener_Status_WAITING_FOR_AVAILABLE_SOCKET = 2, + Cronet_UrlRequestStatusListener_Status_WAITING_FOR_DELEGATE = 3, + Cronet_UrlRequestStatusListener_Status_WAITING_FOR_CACHE = 4, + Cronet_UrlRequestStatusListener_Status_DOWNLOADING_PAC_FILE = 5, + Cronet_UrlRequestStatusListener_Status_RESOLVING_PROXY_FOR_URL = 6, + Cronet_UrlRequestStatusListener_Status_RESOLVING_HOST_IN_PAC_FILE = 7, + Cronet_UrlRequestStatusListener_Status_ESTABLISHING_PROXY_TUNNEL = 8, + Cronet_UrlRequestStatusListener_Status_RESOLVING_HOST = 9, + Cronet_UrlRequestStatusListener_Status_CONNECTING = 10, + Cronet_UrlRequestStatusListener_Status_SSL_HANDSHAKE = 11, + Cronet_UrlRequestStatusListener_Status_SENDING_REQUEST = 12, + Cronet_UrlRequestStatusListener_Status_WAITING_FOR_RESPONSE = 13, + Cronet_UrlRequestStatusListener_Status_READING_RESPONSE = 14, +} Cronet_UrlRequestStatusListener_Status; + +// Declare constants + +/////////////////////// +// Concrete interface Cronet_Buffer. + +// Create an instance of Cronet_Buffer. +CRONET_EXPORT Cronet_BufferPtr Cronet_Buffer_Create(void); +// Destroy an instance of Cronet_Buffer. +CRONET_EXPORT void Cronet_Buffer_Destroy(Cronet_BufferPtr self); +// Set and get app-specific Cronet_ClientContext. +CRONET_EXPORT void Cronet_Buffer_SetClientContext( + Cronet_BufferPtr self, + Cronet_ClientContext client_context); +CRONET_EXPORT Cronet_ClientContext +Cronet_Buffer_GetClientContext(Cronet_BufferPtr self); +// Concrete methods of Cronet_Buffer implemented by Cronet. +// The app calls them to manipulate Cronet_Buffer. +CRONET_EXPORT +void Cronet_Buffer_InitWithDataAndCallback(Cronet_BufferPtr self, + Cronet_RawDataPtr data, + uint64_t size, + Cronet_BufferCallbackPtr callback); +CRONET_EXPORT +void Cronet_Buffer_InitWithAlloc(Cronet_BufferPtr self, uint64_t size); +CRONET_EXPORT +uint64_t Cronet_Buffer_GetSize(Cronet_BufferPtr self); +CRONET_EXPORT +Cronet_RawDataPtr Cronet_Buffer_GetData(Cronet_BufferPtr self); +// Concrete interface Cronet_Buffer is implemented by Cronet. +// The app can implement these for testing / mocking. +typedef void (*Cronet_Buffer_InitWithDataAndCallbackFunc)( + Cronet_BufferPtr self, + Cronet_RawDataPtr data, + uint64_t size, + Cronet_BufferCallbackPtr callback); +typedef void (*Cronet_Buffer_InitWithAllocFunc)(Cronet_BufferPtr self, + uint64_t size); +typedef uint64_t (*Cronet_Buffer_GetSizeFunc)(Cronet_BufferPtr self); +typedef Cronet_RawDataPtr (*Cronet_Buffer_GetDataFunc)(Cronet_BufferPtr self); +// Concrete interface Cronet_Buffer is implemented by Cronet. +// The app can use this for testing / mocking. +CRONET_EXPORT Cronet_BufferPtr Cronet_Buffer_CreateWith( + Cronet_Buffer_InitWithDataAndCallbackFunc InitWithDataAndCallbackFunc, + Cronet_Buffer_InitWithAllocFunc InitWithAllocFunc, + Cronet_Buffer_GetSizeFunc GetSizeFunc, + Cronet_Buffer_GetDataFunc GetDataFunc); + +/////////////////////// +// Abstract interface Cronet_BufferCallback is implemented by the app. + +// There is no method to create a concrete implementation. + +// Destroy an instance of Cronet_BufferCallback. +CRONET_EXPORT void Cronet_BufferCallback_Destroy(Cronet_BufferCallbackPtr self); +// Set and get app-specific Cronet_ClientContext. +CRONET_EXPORT void Cronet_BufferCallback_SetClientContext( + Cronet_BufferCallbackPtr self, + Cronet_ClientContext client_context); +CRONET_EXPORT Cronet_ClientContext +Cronet_BufferCallback_GetClientContext(Cronet_BufferCallbackPtr self); +// Abstract interface Cronet_BufferCallback is implemented by the app. +// The following concrete methods forward call to app implementation. +// The app doesn't normally call them. +CRONET_EXPORT +void Cronet_BufferCallback_OnDestroy(Cronet_BufferCallbackPtr self, + Cronet_BufferPtr buffer); +// The app implements abstract interface Cronet_BufferCallback by defining +// custom functions for each method. +typedef void (*Cronet_BufferCallback_OnDestroyFunc)( + Cronet_BufferCallbackPtr self, + Cronet_BufferPtr buffer); +// The app creates an instance of Cronet_BufferCallback by providing custom +// functions for each method. +CRONET_EXPORT Cronet_BufferCallbackPtr Cronet_BufferCallback_CreateWith( + Cronet_BufferCallback_OnDestroyFunc OnDestroyFunc); + +/////////////////////// +// Abstract interface Cronet_Runnable is implemented by the app. + +// There is no method to create a concrete implementation. + +// Destroy an instance of Cronet_Runnable. +CRONET_EXPORT void Cronet_Runnable_Destroy(Cronet_RunnablePtr self); +// Set and get app-specific Cronet_ClientContext. +CRONET_EXPORT void Cronet_Runnable_SetClientContext( + Cronet_RunnablePtr self, + Cronet_ClientContext client_context); +CRONET_EXPORT Cronet_ClientContext +Cronet_Runnable_GetClientContext(Cronet_RunnablePtr self); +// Abstract interface Cronet_Runnable is implemented by the app. +// The following concrete methods forward call to app implementation. +// The app doesn't normally call them. +CRONET_EXPORT +void Cronet_Runnable_Run(Cronet_RunnablePtr self); +// The app implements abstract interface Cronet_Runnable by defining custom +// functions for each method. +typedef void (*Cronet_Runnable_RunFunc)(Cronet_RunnablePtr self); +// The app creates an instance of Cronet_Runnable by providing custom functions +// for each method. +CRONET_EXPORT Cronet_RunnablePtr +Cronet_Runnable_CreateWith(Cronet_Runnable_RunFunc RunFunc); + +/////////////////////// +// Abstract interface Cronet_Executor is implemented by the app. + +// There is no method to create a concrete implementation. + +// Destroy an instance of Cronet_Executor. +CRONET_EXPORT void Cronet_Executor_Destroy(Cronet_ExecutorPtr self); +// Set and get app-specific Cronet_ClientContext. +CRONET_EXPORT void Cronet_Executor_SetClientContext( + Cronet_ExecutorPtr self, + Cronet_ClientContext client_context); +CRONET_EXPORT Cronet_ClientContext +Cronet_Executor_GetClientContext(Cronet_ExecutorPtr self); +// Abstract interface Cronet_Executor is implemented by the app. +// The following concrete methods forward call to app implementation. +// The app doesn't normally call them. +CRONET_EXPORT +void Cronet_Executor_Execute(Cronet_ExecutorPtr self, + Cronet_RunnablePtr command); +// The app implements abstract interface Cronet_Executor by defining custom +// functions for each method. +typedef void (*Cronet_Executor_ExecuteFunc)(Cronet_ExecutorPtr self, + Cronet_RunnablePtr command); +// The app creates an instance of Cronet_Executor by providing custom functions +// for each method. +CRONET_EXPORT Cronet_ExecutorPtr +Cronet_Executor_CreateWith(Cronet_Executor_ExecuteFunc ExecuteFunc); + +/////////////////////// +// Concrete interface Cronet_Engine. + +// Create an instance of Cronet_Engine. +CRONET_EXPORT Cronet_EnginePtr Cronet_Engine_Create(void); +// Destroy an instance of Cronet_Engine. +CRONET_EXPORT void Cronet_Engine_Destroy(Cronet_EnginePtr self); +// Set and get app-specific Cronet_ClientContext. +CRONET_EXPORT void Cronet_Engine_SetClientContext( + Cronet_EnginePtr self, + Cronet_ClientContext client_context); +CRONET_EXPORT Cronet_ClientContext +Cronet_Engine_GetClientContext(Cronet_EnginePtr self); +// Concrete methods of Cronet_Engine implemented by Cronet. +// The app calls them to manipulate Cronet_Engine. +CRONET_EXPORT +Cronet_RESULT Cronet_Engine_StartWithParams(Cronet_EnginePtr self, + Cronet_EngineParamsPtr params); +CRONET_EXPORT +bool Cronet_Engine_StartNetLogToFile(Cronet_EnginePtr self, + Cronet_String file_name, + bool log_all); +CRONET_EXPORT +void Cronet_Engine_StopNetLog(Cronet_EnginePtr self); +CRONET_EXPORT +Cronet_RESULT Cronet_Engine_Shutdown(Cronet_EnginePtr self); +CRONET_EXPORT +Cronet_String Cronet_Engine_GetVersionString(Cronet_EnginePtr self); +CRONET_EXPORT +Cronet_String Cronet_Engine_GetDefaultUserAgent(Cronet_EnginePtr self); +CRONET_EXPORT +void Cronet_Engine_AddRequestFinishedListener( + Cronet_EnginePtr self, + Cronet_RequestFinishedInfoListenerPtr listener, + Cronet_ExecutorPtr executor); +CRONET_EXPORT +void Cronet_Engine_RemoveRequestFinishedListener( + Cronet_EnginePtr self, + Cronet_RequestFinishedInfoListenerPtr listener); +// Concrete interface Cronet_Engine is implemented by Cronet. +// The app can implement these for testing / mocking. +typedef Cronet_RESULT (*Cronet_Engine_StartWithParamsFunc)( + Cronet_EnginePtr self, + Cronet_EngineParamsPtr params); +typedef bool (*Cronet_Engine_StartNetLogToFileFunc)(Cronet_EnginePtr self, + Cronet_String file_name, + bool log_all); +typedef void (*Cronet_Engine_StopNetLogFunc)(Cronet_EnginePtr self); +typedef Cronet_RESULT (*Cronet_Engine_ShutdownFunc)(Cronet_EnginePtr self); +typedef Cronet_String (*Cronet_Engine_GetVersionStringFunc)( + Cronet_EnginePtr self); +typedef Cronet_String (*Cronet_Engine_GetDefaultUserAgentFunc)( + Cronet_EnginePtr self); +typedef void (*Cronet_Engine_AddRequestFinishedListenerFunc)( + Cronet_EnginePtr self, + Cronet_RequestFinishedInfoListenerPtr listener, + Cronet_ExecutorPtr executor); +typedef void (*Cronet_Engine_RemoveRequestFinishedListenerFunc)( + Cronet_EnginePtr self, + Cronet_RequestFinishedInfoListenerPtr listener); +// Concrete interface Cronet_Engine is implemented by Cronet. +// The app can use this for testing / mocking. +CRONET_EXPORT Cronet_EnginePtr Cronet_Engine_CreateWith( + Cronet_Engine_StartWithParamsFunc StartWithParamsFunc, + Cronet_Engine_StartNetLogToFileFunc StartNetLogToFileFunc, + Cronet_Engine_StopNetLogFunc StopNetLogFunc, + Cronet_Engine_ShutdownFunc ShutdownFunc, + Cronet_Engine_GetVersionStringFunc GetVersionStringFunc, + Cronet_Engine_GetDefaultUserAgentFunc GetDefaultUserAgentFunc, + Cronet_Engine_AddRequestFinishedListenerFunc AddRequestFinishedListenerFunc, + Cronet_Engine_RemoveRequestFinishedListenerFunc + RemoveRequestFinishedListenerFunc); + +/////////////////////// +// Abstract interface Cronet_UrlRequestStatusListener is implemented by the app. + +// There is no method to create a concrete implementation. + +// Destroy an instance of Cronet_UrlRequestStatusListener. +CRONET_EXPORT void Cronet_UrlRequestStatusListener_Destroy( + Cronet_UrlRequestStatusListenerPtr self); +// Set and get app-specific Cronet_ClientContext. +CRONET_EXPORT void Cronet_UrlRequestStatusListener_SetClientContext( + Cronet_UrlRequestStatusListenerPtr self, + Cronet_ClientContext client_context); +CRONET_EXPORT Cronet_ClientContext +Cronet_UrlRequestStatusListener_GetClientContext( + Cronet_UrlRequestStatusListenerPtr self); +// Abstract interface Cronet_UrlRequestStatusListener is implemented by the app. +// The following concrete methods forward call to app implementation. +// The app doesn't normally call them. +CRONET_EXPORT +void Cronet_UrlRequestStatusListener_OnStatus( + Cronet_UrlRequestStatusListenerPtr self, + Cronet_UrlRequestStatusListener_Status status); +// The app implements abstract interface Cronet_UrlRequestStatusListener by +// defining custom functions for each method. +typedef void (*Cronet_UrlRequestStatusListener_OnStatusFunc)( + Cronet_UrlRequestStatusListenerPtr self, + Cronet_UrlRequestStatusListener_Status status); +// The app creates an instance of Cronet_UrlRequestStatusListener by providing +// custom functions for each method. +CRONET_EXPORT Cronet_UrlRequestStatusListenerPtr +Cronet_UrlRequestStatusListener_CreateWith( + Cronet_UrlRequestStatusListener_OnStatusFunc OnStatusFunc); + +/////////////////////// +// Abstract interface Cronet_UrlRequestCallback is implemented by the app. + +// There is no method to create a concrete implementation. + +// Destroy an instance of Cronet_UrlRequestCallback. +CRONET_EXPORT void Cronet_UrlRequestCallback_Destroy( + Cronet_UrlRequestCallbackPtr self); +// Set and get app-specific Cronet_ClientContext. +CRONET_EXPORT void Cronet_UrlRequestCallback_SetClientContext( + Cronet_UrlRequestCallbackPtr self, + Cronet_ClientContext client_context); +CRONET_EXPORT Cronet_ClientContext +Cronet_UrlRequestCallback_GetClientContext(Cronet_UrlRequestCallbackPtr self); +// Abstract interface Cronet_UrlRequestCallback is implemented by the app. +// The following concrete methods forward call to app implementation. +// The app doesn't normally call them. +CRONET_EXPORT +void Cronet_UrlRequestCallback_OnRedirectReceived( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String new_location_url); +CRONET_EXPORT +void Cronet_UrlRequestCallback_OnResponseStarted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); +CRONET_EXPORT +void Cronet_UrlRequestCallback_OnReadCompleted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytes_read); +CRONET_EXPORT +void Cronet_UrlRequestCallback_OnSucceeded(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); +CRONET_EXPORT +void Cronet_UrlRequestCallback_OnFailed(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error); +CRONET_EXPORT +void Cronet_UrlRequestCallback_OnCanceled(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); +// The app implements abstract interface Cronet_UrlRequestCallback by defining +// custom functions for each method. +typedef void (*Cronet_UrlRequestCallback_OnRedirectReceivedFunc)( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String new_location_url); +typedef void (*Cronet_UrlRequestCallback_OnResponseStartedFunc)( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); +typedef void (*Cronet_UrlRequestCallback_OnReadCompletedFunc)( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytes_read); +typedef void (*Cronet_UrlRequestCallback_OnSucceededFunc)( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); +typedef void (*Cronet_UrlRequestCallback_OnFailedFunc)( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error); +typedef void (*Cronet_UrlRequestCallback_OnCanceledFunc)( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); +// The app creates an instance of Cronet_UrlRequestCallback by providing custom +// functions for each method. +CRONET_EXPORT Cronet_UrlRequestCallbackPtr Cronet_UrlRequestCallback_CreateWith( + Cronet_UrlRequestCallback_OnRedirectReceivedFunc OnRedirectReceivedFunc, + Cronet_UrlRequestCallback_OnResponseStartedFunc OnResponseStartedFunc, + Cronet_UrlRequestCallback_OnReadCompletedFunc OnReadCompletedFunc, + Cronet_UrlRequestCallback_OnSucceededFunc OnSucceededFunc, + Cronet_UrlRequestCallback_OnFailedFunc OnFailedFunc, + Cronet_UrlRequestCallback_OnCanceledFunc OnCanceledFunc); + +/////////////////////// +// Concrete interface Cronet_UploadDataSink. + +// Create an instance of Cronet_UploadDataSink. +CRONET_EXPORT Cronet_UploadDataSinkPtr Cronet_UploadDataSink_Create(void); +// Destroy an instance of Cronet_UploadDataSink. +CRONET_EXPORT void Cronet_UploadDataSink_Destroy(Cronet_UploadDataSinkPtr self); +// Set and get app-specific Cronet_ClientContext. +CRONET_EXPORT void Cronet_UploadDataSink_SetClientContext( + Cronet_UploadDataSinkPtr self, + Cronet_ClientContext client_context); +CRONET_EXPORT Cronet_ClientContext +Cronet_UploadDataSink_GetClientContext(Cronet_UploadDataSinkPtr self); +// Concrete methods of Cronet_UploadDataSink implemented by Cronet. +// The app calls them to manipulate Cronet_UploadDataSink. +CRONET_EXPORT +void Cronet_UploadDataSink_OnReadSucceeded(Cronet_UploadDataSinkPtr self, + uint64_t bytes_read, + bool final_chunk); +CRONET_EXPORT +void Cronet_UploadDataSink_OnReadError(Cronet_UploadDataSinkPtr self, + Cronet_String error_message); +CRONET_EXPORT +void Cronet_UploadDataSink_OnRewindSucceeded(Cronet_UploadDataSinkPtr self); +CRONET_EXPORT +void Cronet_UploadDataSink_OnRewindError(Cronet_UploadDataSinkPtr self, + Cronet_String error_message); +// Concrete interface Cronet_UploadDataSink is implemented by Cronet. +// The app can implement these for testing / mocking. +typedef void (*Cronet_UploadDataSink_OnReadSucceededFunc)( + Cronet_UploadDataSinkPtr self, + uint64_t bytes_read, + bool final_chunk); +typedef void (*Cronet_UploadDataSink_OnReadErrorFunc)( + Cronet_UploadDataSinkPtr self, + Cronet_String error_message); +typedef void (*Cronet_UploadDataSink_OnRewindSucceededFunc)( + Cronet_UploadDataSinkPtr self); +typedef void (*Cronet_UploadDataSink_OnRewindErrorFunc)( + Cronet_UploadDataSinkPtr self, + Cronet_String error_message); +// Concrete interface Cronet_UploadDataSink is implemented by Cronet. +// The app can use this for testing / mocking. +CRONET_EXPORT Cronet_UploadDataSinkPtr Cronet_UploadDataSink_CreateWith( + Cronet_UploadDataSink_OnReadSucceededFunc OnReadSucceededFunc, + Cronet_UploadDataSink_OnReadErrorFunc OnReadErrorFunc, + Cronet_UploadDataSink_OnRewindSucceededFunc OnRewindSucceededFunc, + Cronet_UploadDataSink_OnRewindErrorFunc OnRewindErrorFunc); + +/////////////////////// +// Abstract interface Cronet_UploadDataProvider is implemented by the app. + +// There is no method to create a concrete implementation. + +// Destroy an instance of Cronet_UploadDataProvider. +CRONET_EXPORT void Cronet_UploadDataProvider_Destroy( + Cronet_UploadDataProviderPtr self); +// Set and get app-specific Cronet_ClientContext. +CRONET_EXPORT void Cronet_UploadDataProvider_SetClientContext( + Cronet_UploadDataProviderPtr self, + Cronet_ClientContext client_context); +CRONET_EXPORT Cronet_ClientContext +Cronet_UploadDataProvider_GetClientContext(Cronet_UploadDataProviderPtr self); +// Abstract interface Cronet_UploadDataProvider is implemented by the app. +// The following concrete methods forward call to app implementation. +// The app doesn't normally call them. +CRONET_EXPORT +int64_t Cronet_UploadDataProvider_GetLength(Cronet_UploadDataProviderPtr self); +CRONET_EXPORT +void Cronet_UploadDataProvider_Read(Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink, + Cronet_BufferPtr buffer); +CRONET_EXPORT +void Cronet_UploadDataProvider_Rewind( + Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink); +CRONET_EXPORT +void Cronet_UploadDataProvider_Close(Cronet_UploadDataProviderPtr self); +// The app implements abstract interface Cronet_UploadDataProvider by defining +// custom functions for each method. +typedef int64_t (*Cronet_UploadDataProvider_GetLengthFunc)( + Cronet_UploadDataProviderPtr self); +typedef void (*Cronet_UploadDataProvider_ReadFunc)( + Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink, + Cronet_BufferPtr buffer); +typedef void (*Cronet_UploadDataProvider_RewindFunc)( + Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink); +typedef void (*Cronet_UploadDataProvider_CloseFunc)( + Cronet_UploadDataProviderPtr self); +// The app creates an instance of Cronet_UploadDataProvider by providing custom +// functions for each method. +CRONET_EXPORT Cronet_UploadDataProviderPtr Cronet_UploadDataProvider_CreateWith( + Cronet_UploadDataProvider_GetLengthFunc GetLengthFunc, + Cronet_UploadDataProvider_ReadFunc ReadFunc, + Cronet_UploadDataProvider_RewindFunc RewindFunc, + Cronet_UploadDataProvider_CloseFunc CloseFunc); + +/////////////////////// +// Concrete interface Cronet_UrlRequest. + +// Create an instance of Cronet_UrlRequest. +CRONET_EXPORT Cronet_UrlRequestPtr Cronet_UrlRequest_Create(void); +// Destroy an instance of Cronet_UrlRequest. +CRONET_EXPORT void Cronet_UrlRequest_Destroy(Cronet_UrlRequestPtr self); +// Set and get app-specific Cronet_ClientContext. +CRONET_EXPORT void Cronet_UrlRequest_SetClientContext( + Cronet_UrlRequestPtr self, + Cronet_ClientContext client_context); +CRONET_EXPORT Cronet_ClientContext +Cronet_UrlRequest_GetClientContext(Cronet_UrlRequestPtr self); +// Concrete methods of Cronet_UrlRequest implemented by Cronet. +// The app calls them to manipulate Cronet_UrlRequest. +CRONET_EXPORT +Cronet_RESULT Cronet_UrlRequest_InitWithParams( + Cronet_UrlRequestPtr self, + Cronet_EnginePtr engine, + Cronet_String url, + Cronet_UrlRequestParamsPtr params, + Cronet_UrlRequestCallbackPtr callback, + Cronet_ExecutorPtr executor); +CRONET_EXPORT +Cronet_RESULT Cronet_UrlRequest_Start(Cronet_UrlRequestPtr self); +CRONET_EXPORT +Cronet_RESULT Cronet_UrlRequest_FollowRedirect(Cronet_UrlRequestPtr self); +CRONET_EXPORT +Cronet_RESULT Cronet_UrlRequest_Read(Cronet_UrlRequestPtr self, + Cronet_BufferPtr buffer); +CRONET_EXPORT +void Cronet_UrlRequest_Cancel(Cronet_UrlRequestPtr self); +CRONET_EXPORT +bool Cronet_UrlRequest_IsDone(Cronet_UrlRequestPtr self); +CRONET_EXPORT +void Cronet_UrlRequest_GetStatus(Cronet_UrlRequestPtr self, + Cronet_UrlRequestStatusListenerPtr listener); +// Concrete interface Cronet_UrlRequest is implemented by Cronet. +// The app can implement these for testing / mocking. +typedef Cronet_RESULT (*Cronet_UrlRequest_InitWithParamsFunc)( + Cronet_UrlRequestPtr self, + Cronet_EnginePtr engine, + Cronet_String url, + Cronet_UrlRequestParamsPtr params, + Cronet_UrlRequestCallbackPtr callback, + Cronet_ExecutorPtr executor); +typedef Cronet_RESULT (*Cronet_UrlRequest_StartFunc)(Cronet_UrlRequestPtr self); +typedef Cronet_RESULT (*Cronet_UrlRequest_FollowRedirectFunc)( + Cronet_UrlRequestPtr self); +typedef Cronet_RESULT (*Cronet_UrlRequest_ReadFunc)(Cronet_UrlRequestPtr self, + Cronet_BufferPtr buffer); +typedef void (*Cronet_UrlRequest_CancelFunc)(Cronet_UrlRequestPtr self); +typedef bool (*Cronet_UrlRequest_IsDoneFunc)(Cronet_UrlRequestPtr self); +typedef void (*Cronet_UrlRequest_GetStatusFunc)( + Cronet_UrlRequestPtr self, + Cronet_UrlRequestStatusListenerPtr listener); +// Concrete interface Cronet_UrlRequest is implemented by Cronet. +// The app can use this for testing / mocking. +CRONET_EXPORT Cronet_UrlRequestPtr Cronet_UrlRequest_CreateWith( + Cronet_UrlRequest_InitWithParamsFunc InitWithParamsFunc, + Cronet_UrlRequest_StartFunc StartFunc, + Cronet_UrlRequest_FollowRedirectFunc FollowRedirectFunc, + Cronet_UrlRequest_ReadFunc ReadFunc, + Cronet_UrlRequest_CancelFunc CancelFunc, + Cronet_UrlRequest_IsDoneFunc IsDoneFunc, + Cronet_UrlRequest_GetStatusFunc GetStatusFunc); + +/////////////////////// +// Abstract interface Cronet_RequestFinishedInfoListener is implemented by the +// app. + +// There is no method to create a concrete implementation. + +// Destroy an instance of Cronet_RequestFinishedInfoListener. +CRONET_EXPORT void Cronet_RequestFinishedInfoListener_Destroy( + Cronet_RequestFinishedInfoListenerPtr self); +// Set and get app-specific Cronet_ClientContext. +CRONET_EXPORT void Cronet_RequestFinishedInfoListener_SetClientContext( + Cronet_RequestFinishedInfoListenerPtr self, + Cronet_ClientContext client_context); +CRONET_EXPORT Cronet_ClientContext +Cronet_RequestFinishedInfoListener_GetClientContext( + Cronet_RequestFinishedInfoListenerPtr self); +// Abstract interface Cronet_RequestFinishedInfoListener is implemented by the +// app. The following concrete methods forward call to app implementation. The +// app doesn't normally call them. +CRONET_EXPORT +void Cronet_RequestFinishedInfoListener_OnRequestFinished( + Cronet_RequestFinishedInfoListenerPtr self, + Cronet_RequestFinishedInfoPtr request_info, + Cronet_UrlResponseInfoPtr response_info, + Cronet_ErrorPtr error); +// The app implements abstract interface Cronet_RequestFinishedInfoListener by +// defining custom functions for each method. +typedef void (*Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc)( + Cronet_RequestFinishedInfoListenerPtr self, + Cronet_RequestFinishedInfoPtr request_info, + Cronet_UrlResponseInfoPtr response_info, + Cronet_ErrorPtr error); +// The app creates an instance of Cronet_RequestFinishedInfoListener by +// providing custom functions for each method. +CRONET_EXPORT Cronet_RequestFinishedInfoListenerPtr +Cronet_RequestFinishedInfoListener_CreateWith( + Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc + OnRequestFinishedFunc); + +/////////////////////// +// Struct Cronet_Error. +CRONET_EXPORT Cronet_ErrorPtr Cronet_Error_Create(void); +CRONET_EXPORT void Cronet_Error_Destroy(Cronet_ErrorPtr self); +// Cronet_Error setters. +CRONET_EXPORT +void Cronet_Error_error_code_set(Cronet_ErrorPtr self, + const Cronet_Error_ERROR_CODE error_code); +CRONET_EXPORT +void Cronet_Error_message_set(Cronet_ErrorPtr self, + const Cronet_String message); +CRONET_EXPORT +void Cronet_Error_internal_error_code_set(Cronet_ErrorPtr self, + const int32_t internal_error_code); +CRONET_EXPORT +void Cronet_Error_immediately_retryable_set(Cronet_ErrorPtr self, + const bool immediately_retryable); +CRONET_EXPORT +void Cronet_Error_quic_detailed_error_code_set( + Cronet_ErrorPtr self, + const int32_t quic_detailed_error_code); +// Cronet_Error getters. +CRONET_EXPORT +Cronet_Error_ERROR_CODE Cronet_Error_error_code_get(const Cronet_ErrorPtr self); +CRONET_EXPORT +Cronet_String Cronet_Error_message_get(const Cronet_ErrorPtr self); +CRONET_EXPORT +int32_t Cronet_Error_internal_error_code_get(const Cronet_ErrorPtr self); +CRONET_EXPORT +bool Cronet_Error_immediately_retryable_get(const Cronet_ErrorPtr self); +CRONET_EXPORT +int32_t Cronet_Error_quic_detailed_error_code_get(const Cronet_ErrorPtr self); + +/////////////////////// +// Struct Cronet_QuicHint. +CRONET_EXPORT Cronet_QuicHintPtr Cronet_QuicHint_Create(void); +CRONET_EXPORT void Cronet_QuicHint_Destroy(Cronet_QuicHintPtr self); +// Cronet_QuicHint setters. +CRONET_EXPORT +void Cronet_QuicHint_host_set(Cronet_QuicHintPtr self, + const Cronet_String host); +CRONET_EXPORT +void Cronet_QuicHint_port_set(Cronet_QuicHintPtr self, const int32_t port); +CRONET_EXPORT +void Cronet_QuicHint_alternate_port_set(Cronet_QuicHintPtr self, + const int32_t alternate_port); +// Cronet_QuicHint getters. +CRONET_EXPORT +Cronet_String Cronet_QuicHint_host_get(const Cronet_QuicHintPtr self); +CRONET_EXPORT +int32_t Cronet_QuicHint_port_get(const Cronet_QuicHintPtr self); +CRONET_EXPORT +int32_t Cronet_QuicHint_alternate_port_get(const Cronet_QuicHintPtr self); + +/////////////////////// +// Struct Cronet_PublicKeyPins. +CRONET_EXPORT Cronet_PublicKeyPinsPtr Cronet_PublicKeyPins_Create(void); +CRONET_EXPORT void Cronet_PublicKeyPins_Destroy(Cronet_PublicKeyPinsPtr self); +// Cronet_PublicKeyPins setters. +CRONET_EXPORT +void Cronet_PublicKeyPins_host_set(Cronet_PublicKeyPinsPtr self, + const Cronet_String host); +CRONET_EXPORT +void Cronet_PublicKeyPins_pins_sha256_add(Cronet_PublicKeyPinsPtr self, + const Cronet_String element); +CRONET_EXPORT +void Cronet_PublicKeyPins_include_subdomains_set(Cronet_PublicKeyPinsPtr self, + const bool include_subdomains); +CRONET_EXPORT +void Cronet_PublicKeyPins_expiration_date_set(Cronet_PublicKeyPinsPtr self, + const int64_t expiration_date); +// Cronet_PublicKeyPins getters. +CRONET_EXPORT +Cronet_String Cronet_PublicKeyPins_host_get(const Cronet_PublicKeyPinsPtr self); +CRONET_EXPORT +uint32_t Cronet_PublicKeyPins_pins_sha256_size( + const Cronet_PublicKeyPinsPtr self); +CRONET_EXPORT +Cronet_String Cronet_PublicKeyPins_pins_sha256_at( + const Cronet_PublicKeyPinsPtr self, + uint32_t index); +CRONET_EXPORT +void Cronet_PublicKeyPins_pins_sha256_clear(Cronet_PublicKeyPinsPtr self); +CRONET_EXPORT +bool Cronet_PublicKeyPins_include_subdomains_get( + const Cronet_PublicKeyPinsPtr self); +CRONET_EXPORT +int64_t Cronet_PublicKeyPins_expiration_date_get( + const Cronet_PublicKeyPinsPtr self); + +/////////////////////// +// Struct Cronet_EngineParams. +CRONET_EXPORT Cronet_EngineParamsPtr Cronet_EngineParams_Create(void); +CRONET_EXPORT void Cronet_EngineParams_Destroy(Cronet_EngineParamsPtr self); +// Cronet_EngineParams setters. +CRONET_EXPORT +void Cronet_EngineParams_enable_check_result_set( + Cronet_EngineParamsPtr self, + const bool enable_check_result); +CRONET_EXPORT +void Cronet_EngineParams_user_agent_set(Cronet_EngineParamsPtr self, + const Cronet_String user_agent); +CRONET_EXPORT +void Cronet_EngineParams_accept_language_set( + Cronet_EngineParamsPtr self, + const Cronet_String accept_language); +CRONET_EXPORT +void Cronet_EngineParams_storage_path_set(Cronet_EngineParamsPtr self, + const Cronet_String storage_path); +CRONET_EXPORT +void Cronet_EngineParams_enable_quic_set(Cronet_EngineParamsPtr self, + const bool enable_quic); +CRONET_EXPORT +void Cronet_EngineParams_enable_http2_set(Cronet_EngineParamsPtr self, + const bool enable_http2); +CRONET_EXPORT +void Cronet_EngineParams_enable_brotli_set(Cronet_EngineParamsPtr self, + const bool enable_brotli); +CRONET_EXPORT +void Cronet_EngineParams_http_cache_mode_set( + Cronet_EngineParamsPtr self, + const Cronet_EngineParams_HTTP_CACHE_MODE http_cache_mode); +CRONET_EXPORT +void Cronet_EngineParams_http_cache_max_size_set( + Cronet_EngineParamsPtr self, + const int64_t http_cache_max_size); +CRONET_EXPORT +void Cronet_EngineParams_quic_hints_add(Cronet_EngineParamsPtr self, + const Cronet_QuicHintPtr element); +CRONET_EXPORT +void Cronet_EngineParams_public_key_pins_add( + Cronet_EngineParamsPtr self, + const Cronet_PublicKeyPinsPtr element); +CRONET_EXPORT +void Cronet_EngineParams_enable_public_key_pinning_bypass_for_local_trust_anchors_set( + Cronet_EngineParamsPtr self, + const bool enable_public_key_pinning_bypass_for_local_trust_anchors); +CRONET_EXPORT +void Cronet_EngineParams_network_thread_priority_set( + Cronet_EngineParamsPtr self, + const double network_thread_priority); +CRONET_EXPORT +void Cronet_EngineParams_experimental_options_set( + Cronet_EngineParamsPtr self, + const Cronet_String experimental_options); +// Cronet_EngineParams getters. +CRONET_EXPORT +bool Cronet_EngineParams_enable_check_result_get( + const Cronet_EngineParamsPtr self); +CRONET_EXPORT +Cronet_String Cronet_EngineParams_user_agent_get( + const Cronet_EngineParamsPtr self); +CRONET_EXPORT +Cronet_String Cronet_EngineParams_accept_language_get( + const Cronet_EngineParamsPtr self); +CRONET_EXPORT +Cronet_String Cronet_EngineParams_storage_path_get( + const Cronet_EngineParamsPtr self); +CRONET_EXPORT +bool Cronet_EngineParams_enable_quic_get(const Cronet_EngineParamsPtr self); +CRONET_EXPORT +bool Cronet_EngineParams_enable_http2_get(const Cronet_EngineParamsPtr self); +CRONET_EXPORT +bool Cronet_EngineParams_enable_brotli_get(const Cronet_EngineParamsPtr self); +CRONET_EXPORT +Cronet_EngineParams_HTTP_CACHE_MODE Cronet_EngineParams_http_cache_mode_get( + const Cronet_EngineParamsPtr self); +CRONET_EXPORT +int64_t Cronet_EngineParams_http_cache_max_size_get( + const Cronet_EngineParamsPtr self); +CRONET_EXPORT +uint32_t Cronet_EngineParams_quic_hints_size(const Cronet_EngineParamsPtr self); +CRONET_EXPORT +Cronet_QuicHintPtr Cronet_EngineParams_quic_hints_at( + const Cronet_EngineParamsPtr self, + uint32_t index); +CRONET_EXPORT +void Cronet_EngineParams_quic_hints_clear(Cronet_EngineParamsPtr self); +CRONET_EXPORT +uint32_t Cronet_EngineParams_public_key_pins_size( + const Cronet_EngineParamsPtr self); +CRONET_EXPORT +Cronet_PublicKeyPinsPtr Cronet_EngineParams_public_key_pins_at( + const Cronet_EngineParamsPtr self, + uint32_t index); +CRONET_EXPORT +void Cronet_EngineParams_public_key_pins_clear(Cronet_EngineParamsPtr self); +CRONET_EXPORT +bool Cronet_EngineParams_enable_public_key_pinning_bypass_for_local_trust_anchors_get( + const Cronet_EngineParamsPtr self); +CRONET_EXPORT +double Cronet_EngineParams_network_thread_priority_get( + const Cronet_EngineParamsPtr self); +CRONET_EXPORT +Cronet_String Cronet_EngineParams_experimental_options_get( + const Cronet_EngineParamsPtr self); + +/////////////////////// +// Struct Cronet_HttpHeader. +CRONET_EXPORT Cronet_HttpHeaderPtr Cronet_HttpHeader_Create(void); +CRONET_EXPORT void Cronet_HttpHeader_Destroy(Cronet_HttpHeaderPtr self); +// Cronet_HttpHeader setters. +CRONET_EXPORT +void Cronet_HttpHeader_name_set(Cronet_HttpHeaderPtr self, + const Cronet_String name); +CRONET_EXPORT +void Cronet_HttpHeader_value_set(Cronet_HttpHeaderPtr self, + const Cronet_String value); +// Cronet_HttpHeader getters. +CRONET_EXPORT +Cronet_String Cronet_HttpHeader_name_get(const Cronet_HttpHeaderPtr self); +CRONET_EXPORT +Cronet_String Cronet_HttpHeader_value_get(const Cronet_HttpHeaderPtr self); + +/////////////////////// +// Struct Cronet_UrlResponseInfo. +CRONET_EXPORT Cronet_UrlResponseInfoPtr Cronet_UrlResponseInfo_Create(void); +CRONET_EXPORT void Cronet_UrlResponseInfo_Destroy( + Cronet_UrlResponseInfoPtr self); +// Cronet_UrlResponseInfo setters. +CRONET_EXPORT +void Cronet_UrlResponseInfo_url_set(Cronet_UrlResponseInfoPtr self, + const Cronet_String url); +CRONET_EXPORT +void Cronet_UrlResponseInfo_url_chain_add(Cronet_UrlResponseInfoPtr self, + const Cronet_String element); +CRONET_EXPORT +void Cronet_UrlResponseInfo_http_status_code_set( + Cronet_UrlResponseInfoPtr self, + const int32_t http_status_code); +CRONET_EXPORT +void Cronet_UrlResponseInfo_http_status_text_set( + Cronet_UrlResponseInfoPtr self, + const Cronet_String http_status_text); +CRONET_EXPORT +void Cronet_UrlResponseInfo_all_headers_list_add( + Cronet_UrlResponseInfoPtr self, + const Cronet_HttpHeaderPtr element); +CRONET_EXPORT +void Cronet_UrlResponseInfo_was_cached_set(Cronet_UrlResponseInfoPtr self, + const bool was_cached); +CRONET_EXPORT +void Cronet_UrlResponseInfo_negotiated_protocol_set( + Cronet_UrlResponseInfoPtr self, + const Cronet_String negotiated_protocol); +CRONET_EXPORT +void Cronet_UrlResponseInfo_proxy_server_set(Cronet_UrlResponseInfoPtr self, + const Cronet_String proxy_server); +CRONET_EXPORT +void Cronet_UrlResponseInfo_received_byte_count_set( + Cronet_UrlResponseInfoPtr self, + const int64_t received_byte_count); +// Cronet_UrlResponseInfo getters. +CRONET_EXPORT +Cronet_String Cronet_UrlResponseInfo_url_get( + const Cronet_UrlResponseInfoPtr self); +CRONET_EXPORT +uint32_t Cronet_UrlResponseInfo_url_chain_size( + const Cronet_UrlResponseInfoPtr self); +CRONET_EXPORT +Cronet_String Cronet_UrlResponseInfo_url_chain_at( + const Cronet_UrlResponseInfoPtr self, + uint32_t index); +CRONET_EXPORT +void Cronet_UrlResponseInfo_url_chain_clear(Cronet_UrlResponseInfoPtr self); +CRONET_EXPORT +int32_t Cronet_UrlResponseInfo_http_status_code_get( + const Cronet_UrlResponseInfoPtr self); +CRONET_EXPORT +Cronet_String Cronet_UrlResponseInfo_http_status_text_get( + const Cronet_UrlResponseInfoPtr self); +CRONET_EXPORT +uint32_t Cronet_UrlResponseInfo_all_headers_list_size( + const Cronet_UrlResponseInfoPtr self); +CRONET_EXPORT +Cronet_HttpHeaderPtr Cronet_UrlResponseInfo_all_headers_list_at( + const Cronet_UrlResponseInfoPtr self, + uint32_t index); +CRONET_EXPORT +void Cronet_UrlResponseInfo_all_headers_list_clear( + Cronet_UrlResponseInfoPtr self); +CRONET_EXPORT +bool Cronet_UrlResponseInfo_was_cached_get( + const Cronet_UrlResponseInfoPtr self); +CRONET_EXPORT +Cronet_String Cronet_UrlResponseInfo_negotiated_protocol_get( + const Cronet_UrlResponseInfoPtr self); +CRONET_EXPORT +Cronet_String Cronet_UrlResponseInfo_proxy_server_get( + const Cronet_UrlResponseInfoPtr self); +CRONET_EXPORT +int64_t Cronet_UrlResponseInfo_received_byte_count_get( + const Cronet_UrlResponseInfoPtr self); + +/////////////////////// +// Struct Cronet_UrlRequestParams. +CRONET_EXPORT Cronet_UrlRequestParamsPtr Cronet_UrlRequestParams_Create(void); +CRONET_EXPORT void Cronet_UrlRequestParams_Destroy( + Cronet_UrlRequestParamsPtr self); +// Cronet_UrlRequestParams setters. +CRONET_EXPORT +void Cronet_UrlRequestParams_http_method_set(Cronet_UrlRequestParamsPtr self, + const Cronet_String http_method); +CRONET_EXPORT +void Cronet_UrlRequestParams_request_headers_add( + Cronet_UrlRequestParamsPtr self, + const Cronet_HttpHeaderPtr element); +CRONET_EXPORT +void Cronet_UrlRequestParams_disable_cache_set(Cronet_UrlRequestParamsPtr self, + const bool disable_cache); +CRONET_EXPORT +void Cronet_UrlRequestParams_priority_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_UrlRequestParams_REQUEST_PRIORITY priority); +CRONET_EXPORT +void Cronet_UrlRequestParams_upload_data_provider_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_UploadDataProviderPtr upload_data_provider); +CRONET_EXPORT +void Cronet_UrlRequestParams_upload_data_provider_executor_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_ExecutorPtr upload_data_provider_executor); +CRONET_EXPORT +void Cronet_UrlRequestParams_allow_direct_executor_set( + Cronet_UrlRequestParamsPtr self, + const bool allow_direct_executor); +CRONET_EXPORT +void Cronet_UrlRequestParams_annotations_add(Cronet_UrlRequestParamsPtr self, + const Cronet_RawDataPtr element); +CRONET_EXPORT +void Cronet_UrlRequestParams_request_finished_listener_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_RequestFinishedInfoListenerPtr request_finished_listener); +CRONET_EXPORT +void Cronet_UrlRequestParams_request_finished_executor_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_ExecutorPtr request_finished_executor); +CRONET_EXPORT +void Cronet_UrlRequestParams_idempotency_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_UrlRequestParams_IDEMPOTENCY idempotency); +// Cronet_UrlRequestParams getters. +CRONET_EXPORT +Cronet_String Cronet_UrlRequestParams_http_method_get( + const Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +uint32_t Cronet_UrlRequestParams_request_headers_size( + const Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +Cronet_HttpHeaderPtr Cronet_UrlRequestParams_request_headers_at( + const Cronet_UrlRequestParamsPtr self, + uint32_t index); +CRONET_EXPORT +void Cronet_UrlRequestParams_request_headers_clear( + Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +bool Cronet_UrlRequestParams_disable_cache_get( + const Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +Cronet_UrlRequestParams_REQUEST_PRIORITY Cronet_UrlRequestParams_priority_get( + const Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +Cronet_UploadDataProviderPtr Cronet_UrlRequestParams_upload_data_provider_get( + const Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +Cronet_ExecutorPtr Cronet_UrlRequestParams_upload_data_provider_executor_get( + const Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +bool Cronet_UrlRequestParams_allow_direct_executor_get( + const Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +uint32_t Cronet_UrlRequestParams_annotations_size( + const Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +Cronet_RawDataPtr Cronet_UrlRequestParams_annotations_at( + const Cronet_UrlRequestParamsPtr self, + uint32_t index); +CRONET_EXPORT +void Cronet_UrlRequestParams_annotations_clear(Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +Cronet_RequestFinishedInfoListenerPtr +Cronet_UrlRequestParams_request_finished_listener_get( + const Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +Cronet_ExecutorPtr Cronet_UrlRequestParams_request_finished_executor_get( + const Cronet_UrlRequestParamsPtr self); +CRONET_EXPORT +Cronet_UrlRequestParams_IDEMPOTENCY Cronet_UrlRequestParams_idempotency_get( + const Cronet_UrlRequestParamsPtr self); + +/////////////////////// +// Struct Cronet_DateTime. +CRONET_EXPORT Cronet_DateTimePtr Cronet_DateTime_Create(void); +CRONET_EXPORT void Cronet_DateTime_Destroy(Cronet_DateTimePtr self); +// Cronet_DateTime setters. +CRONET_EXPORT +void Cronet_DateTime_value_set(Cronet_DateTimePtr self, const int64_t value); +// Cronet_DateTime getters. +CRONET_EXPORT +int64_t Cronet_DateTime_value_get(const Cronet_DateTimePtr self); + +/////////////////////// +// Struct Cronet_Metrics. +CRONET_EXPORT Cronet_MetricsPtr Cronet_Metrics_Create(void); +CRONET_EXPORT void Cronet_Metrics_Destroy(Cronet_MetricsPtr self); +// Cronet_Metrics setters. +CRONET_EXPORT +void Cronet_Metrics_request_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr request_start); +// Move data from |request_start|. The caller retains ownership of +// |request_start| and must destroy it. +void Cronet_Metrics_request_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr request_start); +CRONET_EXPORT +void Cronet_Metrics_dns_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr dns_start); +// Move data from |dns_start|. The caller retains ownership of |dns_start| and +// must destroy it. +void Cronet_Metrics_dns_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr dns_start); +CRONET_EXPORT +void Cronet_Metrics_dns_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr dns_end); +// Move data from |dns_end|. The caller retains ownership of |dns_end| and must +// destroy it. +void Cronet_Metrics_dns_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr dns_end); +CRONET_EXPORT +void Cronet_Metrics_connect_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr connect_start); +// Move data from |connect_start|. The caller retains ownership of +// |connect_start| and must destroy it. +void Cronet_Metrics_connect_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr connect_start); +CRONET_EXPORT +void Cronet_Metrics_connect_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr connect_end); +// Move data from |connect_end|. The caller retains ownership of |connect_end| +// and must destroy it. +void Cronet_Metrics_connect_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr connect_end); +CRONET_EXPORT +void Cronet_Metrics_ssl_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr ssl_start); +// Move data from |ssl_start|. The caller retains ownership of |ssl_start| and +// must destroy it. +void Cronet_Metrics_ssl_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr ssl_start); +CRONET_EXPORT +void Cronet_Metrics_ssl_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr ssl_end); +// Move data from |ssl_end|. The caller retains ownership of |ssl_end| and must +// destroy it. +void Cronet_Metrics_ssl_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr ssl_end); +CRONET_EXPORT +void Cronet_Metrics_sending_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr sending_start); +// Move data from |sending_start|. The caller retains ownership of +// |sending_start| and must destroy it. +void Cronet_Metrics_sending_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr sending_start); +CRONET_EXPORT +void Cronet_Metrics_sending_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr sending_end); +// Move data from |sending_end|. The caller retains ownership of |sending_end| +// and must destroy it. +void Cronet_Metrics_sending_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr sending_end); +CRONET_EXPORT +void Cronet_Metrics_push_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr push_start); +// Move data from |push_start|. The caller retains ownership of |push_start| and +// must destroy it. +void Cronet_Metrics_push_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr push_start); +CRONET_EXPORT +void Cronet_Metrics_push_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr push_end); +// Move data from |push_end|. The caller retains ownership of |push_end| and +// must destroy it. +void Cronet_Metrics_push_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr push_end); +CRONET_EXPORT +void Cronet_Metrics_response_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr response_start); +// Move data from |response_start|. The caller retains ownership of +// |response_start| and must destroy it. +void Cronet_Metrics_response_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr response_start); +CRONET_EXPORT +void Cronet_Metrics_request_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr request_end); +// Move data from |request_end|. The caller retains ownership of |request_end| +// and must destroy it. +void Cronet_Metrics_request_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr request_end); +CRONET_EXPORT +void Cronet_Metrics_socket_reused_set(Cronet_MetricsPtr self, + const bool socket_reused); +CRONET_EXPORT +void Cronet_Metrics_sent_byte_count_set(Cronet_MetricsPtr self, + const int64_t sent_byte_count); +CRONET_EXPORT +void Cronet_Metrics_received_byte_count_set(Cronet_MetricsPtr self, + const int64_t received_byte_count); +// Cronet_Metrics getters. +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_request_start_get( + const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_dns_start_get(const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_dns_end_get(const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_connect_start_get( + const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_connect_end_get(const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_ssl_start_get(const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_ssl_end_get(const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_sending_start_get( + const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_sending_end_get(const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_push_start_get(const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_push_end_get(const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_response_start_get( + const Cronet_MetricsPtr self); +CRONET_EXPORT +Cronet_DateTimePtr Cronet_Metrics_request_end_get(const Cronet_MetricsPtr self); +CRONET_EXPORT +bool Cronet_Metrics_socket_reused_get(const Cronet_MetricsPtr self); +CRONET_EXPORT +int64_t Cronet_Metrics_sent_byte_count_get(const Cronet_MetricsPtr self); +CRONET_EXPORT +int64_t Cronet_Metrics_received_byte_count_get(const Cronet_MetricsPtr self); + +/////////////////////// +// Struct Cronet_RequestFinishedInfo. +CRONET_EXPORT Cronet_RequestFinishedInfoPtr +Cronet_RequestFinishedInfo_Create(void); +CRONET_EXPORT void Cronet_RequestFinishedInfo_Destroy( + Cronet_RequestFinishedInfoPtr self); +// Cronet_RequestFinishedInfo setters. +CRONET_EXPORT +void Cronet_RequestFinishedInfo_metrics_set(Cronet_RequestFinishedInfoPtr self, + const Cronet_MetricsPtr metrics); +// Move data from |metrics|. The caller retains ownership of |metrics| and must +// destroy it. +void Cronet_RequestFinishedInfo_metrics_move(Cronet_RequestFinishedInfoPtr self, + Cronet_MetricsPtr metrics); +CRONET_EXPORT +void Cronet_RequestFinishedInfo_annotations_add( + Cronet_RequestFinishedInfoPtr self, + const Cronet_RawDataPtr element); +CRONET_EXPORT +void Cronet_RequestFinishedInfo_finished_reason_set( + Cronet_RequestFinishedInfoPtr self, + const Cronet_RequestFinishedInfo_FINISHED_REASON finished_reason); +// Cronet_RequestFinishedInfo getters. +CRONET_EXPORT +Cronet_MetricsPtr Cronet_RequestFinishedInfo_metrics_get( + const Cronet_RequestFinishedInfoPtr self); +CRONET_EXPORT +uint32_t Cronet_RequestFinishedInfo_annotations_size( + const Cronet_RequestFinishedInfoPtr self); +CRONET_EXPORT +Cronet_RawDataPtr Cronet_RequestFinishedInfo_annotations_at( + const Cronet_RequestFinishedInfoPtr self, + uint32_t index); +CRONET_EXPORT +void Cronet_RequestFinishedInfo_annotations_clear( + Cronet_RequestFinishedInfoPtr self); +CRONET_EXPORT +Cronet_RequestFinishedInfo_FINISHED_REASON +Cronet_RequestFinishedInfo_finished_reason_get( + const Cronet_RequestFinishedInfoPtr self); + +#ifdef __cplusplus +} +#endif + +#endif // COMPONENTS_CRONET_NATIVE_GENERATED_CRONET_IDL_C_H_ diff --git a/src/components/cronet/native/generated/cronet.idl_impl_interface.cc b/src/components/cronet/native/generated/cronet.idl_impl_interface.cc new file mode 100644 index 0000000000..86f9324807 --- /dev/null +++ b/src/components/cronet/native/generated/cronet.idl_impl_interface.cc @@ -0,0 +1,997 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from components/cronet/native/generated/cronet.idl */ + +#include "components/cronet/native/generated/cronet.idl_impl_interface.h" + +#include "base/check.h" + +// C functions of Cronet_Buffer that forward calls to C++ implementation. +void Cronet_Buffer_Destroy(Cronet_BufferPtr self) { + DCHECK(self); + return delete self; +} + +void Cronet_Buffer_SetClientContext(Cronet_BufferPtr self, + Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext Cronet_Buffer_GetClientContext(Cronet_BufferPtr self) { + DCHECK(self); + return self->client_context(); +} + +void Cronet_Buffer_InitWithDataAndCallback(Cronet_BufferPtr self, + Cronet_RawDataPtr data, + uint64_t size, + Cronet_BufferCallbackPtr callback) { + DCHECK(self); + self->InitWithDataAndCallback(data, size, callback); +} + +void Cronet_Buffer_InitWithAlloc(Cronet_BufferPtr self, uint64_t size) { + DCHECK(self); + self->InitWithAlloc(size); +} + +uint64_t Cronet_Buffer_GetSize(Cronet_BufferPtr self) { + DCHECK(self); + return self->GetSize(); +} + +Cronet_RawDataPtr Cronet_Buffer_GetData(Cronet_BufferPtr self) { + DCHECK(self); + return self->GetData(); +} + +// Implementation of Cronet_Buffer that forwards calls to C functions +// implemented by the app. +class Cronet_BufferStub : public Cronet_Buffer { + public: + Cronet_BufferStub( + Cronet_Buffer_InitWithDataAndCallbackFunc InitWithDataAndCallbackFunc, + Cronet_Buffer_InitWithAllocFunc InitWithAllocFunc, + Cronet_Buffer_GetSizeFunc GetSizeFunc, + Cronet_Buffer_GetDataFunc GetDataFunc) + : InitWithDataAndCallbackFunc_(InitWithDataAndCallbackFunc), + InitWithAllocFunc_(InitWithAllocFunc), + GetSizeFunc_(GetSizeFunc), + GetDataFunc_(GetDataFunc) {} + + Cronet_BufferStub(const Cronet_BufferStub&) = delete; + Cronet_BufferStub& operator=(const Cronet_BufferStub&) = delete; + + ~Cronet_BufferStub() override {} + + protected: + void InitWithDataAndCallback(Cronet_RawDataPtr data, + uint64_t size, + Cronet_BufferCallbackPtr callback) override { + InitWithDataAndCallbackFunc_(this, data, size, callback); + } + + void InitWithAlloc(uint64_t size) override { InitWithAllocFunc_(this, size); } + + uint64_t GetSize() override { return GetSizeFunc_(this); } + + Cronet_RawDataPtr GetData() override { return GetDataFunc_(this); } + + private: + const Cronet_Buffer_InitWithDataAndCallbackFunc InitWithDataAndCallbackFunc_; + const Cronet_Buffer_InitWithAllocFunc InitWithAllocFunc_; + const Cronet_Buffer_GetSizeFunc GetSizeFunc_; + const Cronet_Buffer_GetDataFunc GetDataFunc_; +}; + +Cronet_BufferPtr Cronet_Buffer_CreateWith( + Cronet_Buffer_InitWithDataAndCallbackFunc InitWithDataAndCallbackFunc, + Cronet_Buffer_InitWithAllocFunc InitWithAllocFunc, + Cronet_Buffer_GetSizeFunc GetSizeFunc, + Cronet_Buffer_GetDataFunc GetDataFunc) { + return new Cronet_BufferStub(InitWithDataAndCallbackFunc, InitWithAllocFunc, + GetSizeFunc, GetDataFunc); +} + +// C functions of Cronet_BufferCallback that forward calls to C++ +// implementation. +void Cronet_BufferCallback_Destroy(Cronet_BufferCallbackPtr self) { + DCHECK(self); + return delete self; +} + +void Cronet_BufferCallback_SetClientContext( + Cronet_BufferCallbackPtr self, + Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext Cronet_BufferCallback_GetClientContext( + Cronet_BufferCallbackPtr self) { + DCHECK(self); + return self->client_context(); +} + +void Cronet_BufferCallback_OnDestroy(Cronet_BufferCallbackPtr self, + Cronet_BufferPtr buffer) { + DCHECK(self); + self->OnDestroy(buffer); +} + +// Implementation of Cronet_BufferCallback that forwards calls to C functions +// implemented by the app. +class Cronet_BufferCallbackStub : public Cronet_BufferCallback { + public: + explicit Cronet_BufferCallbackStub( + Cronet_BufferCallback_OnDestroyFunc OnDestroyFunc) + : OnDestroyFunc_(OnDestroyFunc) {} + + Cronet_BufferCallbackStub(const Cronet_BufferCallbackStub&) = delete; + Cronet_BufferCallbackStub& operator=(const Cronet_BufferCallbackStub&) = + delete; + + ~Cronet_BufferCallbackStub() override {} + + protected: + void OnDestroy(Cronet_BufferPtr buffer) override { + OnDestroyFunc_(this, buffer); + } + + private: + const Cronet_BufferCallback_OnDestroyFunc OnDestroyFunc_; +}; + +Cronet_BufferCallbackPtr Cronet_BufferCallback_CreateWith( + Cronet_BufferCallback_OnDestroyFunc OnDestroyFunc) { + return new Cronet_BufferCallbackStub(OnDestroyFunc); +} + +// C functions of Cronet_Runnable that forward calls to C++ implementation. +void Cronet_Runnable_Destroy(Cronet_RunnablePtr self) { + DCHECK(self); + return delete self; +} + +void Cronet_Runnable_SetClientContext(Cronet_RunnablePtr self, + Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext Cronet_Runnable_GetClientContext(Cronet_RunnablePtr self) { + DCHECK(self); + return self->client_context(); +} + +void Cronet_Runnable_Run(Cronet_RunnablePtr self) { + DCHECK(self); + self->Run(); +} + +// Implementation of Cronet_Runnable that forwards calls to C functions +// implemented by the app. +class Cronet_RunnableStub : public Cronet_Runnable { + public: + explicit Cronet_RunnableStub(Cronet_Runnable_RunFunc RunFunc) + : RunFunc_(RunFunc) {} + + Cronet_RunnableStub(const Cronet_RunnableStub&) = delete; + Cronet_RunnableStub& operator=(const Cronet_RunnableStub&) = delete; + + ~Cronet_RunnableStub() override {} + + protected: + void Run() override { RunFunc_(this); } + + private: + const Cronet_Runnable_RunFunc RunFunc_; +}; + +Cronet_RunnablePtr Cronet_Runnable_CreateWith(Cronet_Runnable_RunFunc RunFunc) { + return new Cronet_RunnableStub(RunFunc); +} + +// C functions of Cronet_Executor that forward calls to C++ implementation. +void Cronet_Executor_Destroy(Cronet_ExecutorPtr self) { + DCHECK(self); + return delete self; +} + +void Cronet_Executor_SetClientContext(Cronet_ExecutorPtr self, + Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext Cronet_Executor_GetClientContext(Cronet_ExecutorPtr self) { + DCHECK(self); + return self->client_context(); +} + +void Cronet_Executor_Execute(Cronet_ExecutorPtr self, + Cronet_RunnablePtr command) { + DCHECK(self); + self->Execute(command); +} + +// Implementation of Cronet_Executor that forwards calls to C functions +// implemented by the app. +class Cronet_ExecutorStub : public Cronet_Executor { + public: + explicit Cronet_ExecutorStub(Cronet_Executor_ExecuteFunc ExecuteFunc) + : ExecuteFunc_(ExecuteFunc) {} + + Cronet_ExecutorStub(const Cronet_ExecutorStub&) = delete; + Cronet_ExecutorStub& operator=(const Cronet_ExecutorStub&) = delete; + + ~Cronet_ExecutorStub() override {} + + protected: + void Execute(Cronet_RunnablePtr command) override { + ExecuteFunc_(this, command); + } + + private: + const Cronet_Executor_ExecuteFunc ExecuteFunc_; +}; + +Cronet_ExecutorPtr Cronet_Executor_CreateWith( + Cronet_Executor_ExecuteFunc ExecuteFunc) { + return new Cronet_ExecutorStub(ExecuteFunc); +} + +// C functions of Cronet_Engine that forward calls to C++ implementation. +void Cronet_Engine_Destroy(Cronet_EnginePtr self) { + DCHECK(self); + return delete self; +} + +void Cronet_Engine_SetClientContext(Cronet_EnginePtr self, + Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext Cronet_Engine_GetClientContext(Cronet_EnginePtr self) { + DCHECK(self); + return self->client_context(); +} + +Cronet_RESULT Cronet_Engine_StartWithParams(Cronet_EnginePtr self, + Cronet_EngineParamsPtr params) { + DCHECK(self); + return self->StartWithParams(params); +} + +bool Cronet_Engine_StartNetLogToFile(Cronet_EnginePtr self, + Cronet_String file_name, + bool log_all) { + DCHECK(self); + return self->StartNetLogToFile(file_name, log_all); +} + +void Cronet_Engine_StopNetLog(Cronet_EnginePtr self) { + DCHECK(self); + self->StopNetLog(); +} + +Cronet_RESULT Cronet_Engine_Shutdown(Cronet_EnginePtr self) { + DCHECK(self); + return self->Shutdown(); +} + +Cronet_String Cronet_Engine_GetVersionString(Cronet_EnginePtr self) { + DCHECK(self); + return self->GetVersionString(); +} + +Cronet_String Cronet_Engine_GetDefaultUserAgent(Cronet_EnginePtr self) { + DCHECK(self); + return self->GetDefaultUserAgent(); +} + +void Cronet_Engine_AddRequestFinishedListener( + Cronet_EnginePtr self, + Cronet_RequestFinishedInfoListenerPtr listener, + Cronet_ExecutorPtr executor) { + DCHECK(self); + self->AddRequestFinishedListener(listener, executor); +} + +void Cronet_Engine_RemoveRequestFinishedListener( + Cronet_EnginePtr self, + Cronet_RequestFinishedInfoListenerPtr listener) { + DCHECK(self); + self->RemoveRequestFinishedListener(listener); +} + +// Implementation of Cronet_Engine that forwards calls to C functions +// implemented by the app. +class Cronet_EngineStub : public Cronet_Engine { + public: + Cronet_EngineStub( + Cronet_Engine_StartWithParamsFunc StartWithParamsFunc, + Cronet_Engine_StartNetLogToFileFunc StartNetLogToFileFunc, + Cronet_Engine_StopNetLogFunc StopNetLogFunc, + Cronet_Engine_ShutdownFunc ShutdownFunc, + Cronet_Engine_GetVersionStringFunc GetVersionStringFunc, + Cronet_Engine_GetDefaultUserAgentFunc GetDefaultUserAgentFunc, + Cronet_Engine_AddRequestFinishedListenerFunc + AddRequestFinishedListenerFunc, + Cronet_Engine_RemoveRequestFinishedListenerFunc + RemoveRequestFinishedListenerFunc) + : StartWithParamsFunc_(StartWithParamsFunc), + StartNetLogToFileFunc_(StartNetLogToFileFunc), + StopNetLogFunc_(StopNetLogFunc), + ShutdownFunc_(ShutdownFunc), + GetVersionStringFunc_(GetVersionStringFunc), + GetDefaultUserAgentFunc_(GetDefaultUserAgentFunc), + AddRequestFinishedListenerFunc_(AddRequestFinishedListenerFunc), + RemoveRequestFinishedListenerFunc_(RemoveRequestFinishedListenerFunc) {} + + Cronet_EngineStub(const Cronet_EngineStub&) = delete; + Cronet_EngineStub& operator=(const Cronet_EngineStub&) = delete; + + ~Cronet_EngineStub() override {} + + protected: + Cronet_RESULT StartWithParams(Cronet_EngineParamsPtr params) override { + return StartWithParamsFunc_(this, params); + } + + bool StartNetLogToFile(Cronet_String file_name, bool log_all) override { + return StartNetLogToFileFunc_(this, file_name, log_all); + } + + void StopNetLog() override { StopNetLogFunc_(this); } + + Cronet_RESULT Shutdown() override { return ShutdownFunc_(this); } + + Cronet_String GetVersionString() override { + return GetVersionStringFunc_(this); + } + + Cronet_String GetDefaultUserAgent() override { + return GetDefaultUserAgentFunc_(this); + } + + void AddRequestFinishedListener( + Cronet_RequestFinishedInfoListenerPtr listener, + Cronet_ExecutorPtr executor) override { + AddRequestFinishedListenerFunc_(this, listener, executor); + } + + void RemoveRequestFinishedListener( + Cronet_RequestFinishedInfoListenerPtr listener) override { + RemoveRequestFinishedListenerFunc_(this, listener); + } + + private: + const Cronet_Engine_StartWithParamsFunc StartWithParamsFunc_; + const Cronet_Engine_StartNetLogToFileFunc StartNetLogToFileFunc_; + const Cronet_Engine_StopNetLogFunc StopNetLogFunc_; + const Cronet_Engine_ShutdownFunc ShutdownFunc_; + const Cronet_Engine_GetVersionStringFunc GetVersionStringFunc_; + const Cronet_Engine_GetDefaultUserAgentFunc GetDefaultUserAgentFunc_; + const Cronet_Engine_AddRequestFinishedListenerFunc + AddRequestFinishedListenerFunc_; + const Cronet_Engine_RemoveRequestFinishedListenerFunc + RemoveRequestFinishedListenerFunc_; +}; + +Cronet_EnginePtr Cronet_Engine_CreateWith( + Cronet_Engine_StartWithParamsFunc StartWithParamsFunc, + Cronet_Engine_StartNetLogToFileFunc StartNetLogToFileFunc, + Cronet_Engine_StopNetLogFunc StopNetLogFunc, + Cronet_Engine_ShutdownFunc ShutdownFunc, + Cronet_Engine_GetVersionStringFunc GetVersionStringFunc, + Cronet_Engine_GetDefaultUserAgentFunc GetDefaultUserAgentFunc, + Cronet_Engine_AddRequestFinishedListenerFunc AddRequestFinishedListenerFunc, + Cronet_Engine_RemoveRequestFinishedListenerFunc + RemoveRequestFinishedListenerFunc) { + return new Cronet_EngineStub( + StartWithParamsFunc, StartNetLogToFileFunc, StopNetLogFunc, ShutdownFunc, + GetVersionStringFunc, GetDefaultUserAgentFunc, + AddRequestFinishedListenerFunc, RemoveRequestFinishedListenerFunc); +} + +// C functions of Cronet_UrlRequestStatusListener that forward calls to C++ +// implementation. +void Cronet_UrlRequestStatusListener_Destroy( + Cronet_UrlRequestStatusListenerPtr self) { + DCHECK(self); + return delete self; +} + +void Cronet_UrlRequestStatusListener_SetClientContext( + Cronet_UrlRequestStatusListenerPtr self, + Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext Cronet_UrlRequestStatusListener_GetClientContext( + Cronet_UrlRequestStatusListenerPtr self) { + DCHECK(self); + return self->client_context(); +} + +void Cronet_UrlRequestStatusListener_OnStatus( + Cronet_UrlRequestStatusListenerPtr self, + Cronet_UrlRequestStatusListener_Status status) { + DCHECK(self); + self->OnStatus(status); +} + +// Implementation of Cronet_UrlRequestStatusListener that forwards calls to C +// functions implemented by the app. +class Cronet_UrlRequestStatusListenerStub + : public Cronet_UrlRequestStatusListener { + public: + explicit Cronet_UrlRequestStatusListenerStub( + Cronet_UrlRequestStatusListener_OnStatusFunc OnStatusFunc) + : OnStatusFunc_(OnStatusFunc) {} + + Cronet_UrlRequestStatusListenerStub( + const Cronet_UrlRequestStatusListenerStub&) = delete; + Cronet_UrlRequestStatusListenerStub& operator=( + const Cronet_UrlRequestStatusListenerStub&) = delete; + + ~Cronet_UrlRequestStatusListenerStub() override {} + + protected: + void OnStatus(Cronet_UrlRequestStatusListener_Status status) override { + OnStatusFunc_(this, status); + } + + private: + const Cronet_UrlRequestStatusListener_OnStatusFunc OnStatusFunc_; +}; + +Cronet_UrlRequestStatusListenerPtr Cronet_UrlRequestStatusListener_CreateWith( + Cronet_UrlRequestStatusListener_OnStatusFunc OnStatusFunc) { + return new Cronet_UrlRequestStatusListenerStub(OnStatusFunc); +} + +// C functions of Cronet_UrlRequestCallback that forward calls to C++ +// implementation. +void Cronet_UrlRequestCallback_Destroy(Cronet_UrlRequestCallbackPtr self) { + DCHECK(self); + return delete self; +} + +void Cronet_UrlRequestCallback_SetClientContext( + Cronet_UrlRequestCallbackPtr self, + Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext Cronet_UrlRequestCallback_GetClientContext( + Cronet_UrlRequestCallbackPtr self) { + DCHECK(self); + return self->client_context(); +} + +void Cronet_UrlRequestCallback_OnRedirectReceived( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String new_location_url) { + DCHECK(self); + self->OnRedirectReceived(request, info, new_location_url); +} + +void Cronet_UrlRequestCallback_OnResponseStarted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + DCHECK(self); + self->OnResponseStarted(request, info); +} + +void Cronet_UrlRequestCallback_OnReadCompleted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytes_read) { + DCHECK(self); + self->OnReadCompleted(request, info, buffer, bytes_read); +} + +void Cronet_UrlRequestCallback_OnSucceeded(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + DCHECK(self); + self->OnSucceeded(request, info); +} + +void Cronet_UrlRequestCallback_OnFailed(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error) { + DCHECK(self); + self->OnFailed(request, info, error); +} + +void Cronet_UrlRequestCallback_OnCanceled(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + DCHECK(self); + self->OnCanceled(request, info); +} + +// Implementation of Cronet_UrlRequestCallback that forwards calls to C +// functions implemented by the app. +class Cronet_UrlRequestCallbackStub : public Cronet_UrlRequestCallback { + public: + Cronet_UrlRequestCallbackStub( + Cronet_UrlRequestCallback_OnRedirectReceivedFunc OnRedirectReceivedFunc, + Cronet_UrlRequestCallback_OnResponseStartedFunc OnResponseStartedFunc, + Cronet_UrlRequestCallback_OnReadCompletedFunc OnReadCompletedFunc, + Cronet_UrlRequestCallback_OnSucceededFunc OnSucceededFunc, + Cronet_UrlRequestCallback_OnFailedFunc OnFailedFunc, + Cronet_UrlRequestCallback_OnCanceledFunc OnCanceledFunc) + : OnRedirectReceivedFunc_(OnRedirectReceivedFunc), + OnResponseStartedFunc_(OnResponseStartedFunc), + OnReadCompletedFunc_(OnReadCompletedFunc), + OnSucceededFunc_(OnSucceededFunc), + OnFailedFunc_(OnFailedFunc), + OnCanceledFunc_(OnCanceledFunc) {} + + Cronet_UrlRequestCallbackStub(const Cronet_UrlRequestCallbackStub&) = delete; + Cronet_UrlRequestCallbackStub& operator=( + const Cronet_UrlRequestCallbackStub&) = delete; + + ~Cronet_UrlRequestCallbackStub() override {} + + protected: + void OnRedirectReceived(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String new_location_url) override { + OnRedirectReceivedFunc_(this, request, info, new_location_url); + } + + void OnResponseStarted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) override { + OnResponseStartedFunc_(this, request, info); + } + + void OnReadCompleted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytes_read) override { + OnReadCompletedFunc_(this, request, info, buffer, bytes_read); + } + + void OnSucceeded(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) override { + OnSucceededFunc_(this, request, info); + } + + void OnFailed(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error) override { + OnFailedFunc_(this, request, info, error); + } + + void OnCanceled(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) override { + OnCanceledFunc_(this, request, info); + } + + private: + const Cronet_UrlRequestCallback_OnRedirectReceivedFunc + OnRedirectReceivedFunc_; + const Cronet_UrlRequestCallback_OnResponseStartedFunc OnResponseStartedFunc_; + const Cronet_UrlRequestCallback_OnReadCompletedFunc OnReadCompletedFunc_; + const Cronet_UrlRequestCallback_OnSucceededFunc OnSucceededFunc_; + const Cronet_UrlRequestCallback_OnFailedFunc OnFailedFunc_; + const Cronet_UrlRequestCallback_OnCanceledFunc OnCanceledFunc_; +}; + +Cronet_UrlRequestCallbackPtr Cronet_UrlRequestCallback_CreateWith( + Cronet_UrlRequestCallback_OnRedirectReceivedFunc OnRedirectReceivedFunc, + Cronet_UrlRequestCallback_OnResponseStartedFunc OnResponseStartedFunc, + Cronet_UrlRequestCallback_OnReadCompletedFunc OnReadCompletedFunc, + Cronet_UrlRequestCallback_OnSucceededFunc OnSucceededFunc, + Cronet_UrlRequestCallback_OnFailedFunc OnFailedFunc, + Cronet_UrlRequestCallback_OnCanceledFunc OnCanceledFunc) { + return new Cronet_UrlRequestCallbackStub( + OnRedirectReceivedFunc, OnResponseStartedFunc, OnReadCompletedFunc, + OnSucceededFunc, OnFailedFunc, OnCanceledFunc); +} + +// C functions of Cronet_UploadDataSink that forward calls to C++ +// implementation. +void Cronet_UploadDataSink_Destroy(Cronet_UploadDataSinkPtr self) { + DCHECK(self); + return delete self; +} + +void Cronet_UploadDataSink_SetClientContext( + Cronet_UploadDataSinkPtr self, + Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext Cronet_UploadDataSink_GetClientContext( + Cronet_UploadDataSinkPtr self) { + DCHECK(self); + return self->client_context(); +} + +void Cronet_UploadDataSink_OnReadSucceeded(Cronet_UploadDataSinkPtr self, + uint64_t bytes_read, + bool final_chunk) { + DCHECK(self); + self->OnReadSucceeded(bytes_read, final_chunk); +} + +void Cronet_UploadDataSink_OnReadError(Cronet_UploadDataSinkPtr self, + Cronet_String error_message) { + DCHECK(self); + self->OnReadError(error_message); +} + +void Cronet_UploadDataSink_OnRewindSucceeded(Cronet_UploadDataSinkPtr self) { + DCHECK(self); + self->OnRewindSucceeded(); +} + +void Cronet_UploadDataSink_OnRewindError(Cronet_UploadDataSinkPtr self, + Cronet_String error_message) { + DCHECK(self); + self->OnRewindError(error_message); +} + +// Implementation of Cronet_UploadDataSink that forwards calls to C functions +// implemented by the app. +class Cronet_UploadDataSinkStub : public Cronet_UploadDataSink { + public: + Cronet_UploadDataSinkStub( + Cronet_UploadDataSink_OnReadSucceededFunc OnReadSucceededFunc, + Cronet_UploadDataSink_OnReadErrorFunc OnReadErrorFunc, + Cronet_UploadDataSink_OnRewindSucceededFunc OnRewindSucceededFunc, + Cronet_UploadDataSink_OnRewindErrorFunc OnRewindErrorFunc) + : OnReadSucceededFunc_(OnReadSucceededFunc), + OnReadErrorFunc_(OnReadErrorFunc), + OnRewindSucceededFunc_(OnRewindSucceededFunc), + OnRewindErrorFunc_(OnRewindErrorFunc) {} + + Cronet_UploadDataSinkStub(const Cronet_UploadDataSinkStub&) = delete; + Cronet_UploadDataSinkStub& operator=(const Cronet_UploadDataSinkStub&) = + delete; + + ~Cronet_UploadDataSinkStub() override {} + + protected: + void OnReadSucceeded(uint64_t bytes_read, bool final_chunk) override { + OnReadSucceededFunc_(this, bytes_read, final_chunk); + } + + void OnReadError(Cronet_String error_message) override { + OnReadErrorFunc_(this, error_message); + } + + void OnRewindSucceeded() override { OnRewindSucceededFunc_(this); } + + void OnRewindError(Cronet_String error_message) override { + OnRewindErrorFunc_(this, error_message); + } + + private: + const Cronet_UploadDataSink_OnReadSucceededFunc OnReadSucceededFunc_; + const Cronet_UploadDataSink_OnReadErrorFunc OnReadErrorFunc_; + const Cronet_UploadDataSink_OnRewindSucceededFunc OnRewindSucceededFunc_; + const Cronet_UploadDataSink_OnRewindErrorFunc OnRewindErrorFunc_; +}; + +Cronet_UploadDataSinkPtr Cronet_UploadDataSink_CreateWith( + Cronet_UploadDataSink_OnReadSucceededFunc OnReadSucceededFunc, + Cronet_UploadDataSink_OnReadErrorFunc OnReadErrorFunc, + Cronet_UploadDataSink_OnRewindSucceededFunc OnRewindSucceededFunc, + Cronet_UploadDataSink_OnRewindErrorFunc OnRewindErrorFunc) { + return new Cronet_UploadDataSinkStub(OnReadSucceededFunc, OnReadErrorFunc, + OnRewindSucceededFunc, + OnRewindErrorFunc); +} + +// C functions of Cronet_UploadDataProvider that forward calls to C++ +// implementation. +void Cronet_UploadDataProvider_Destroy(Cronet_UploadDataProviderPtr self) { + DCHECK(self); + return delete self; +} + +void Cronet_UploadDataProvider_SetClientContext( + Cronet_UploadDataProviderPtr self, + Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext Cronet_UploadDataProvider_GetClientContext( + Cronet_UploadDataProviderPtr self) { + DCHECK(self); + return self->client_context(); +} + +int64_t Cronet_UploadDataProvider_GetLength(Cronet_UploadDataProviderPtr self) { + DCHECK(self); + return self->GetLength(); +} + +void Cronet_UploadDataProvider_Read(Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink, + Cronet_BufferPtr buffer) { + DCHECK(self); + self->Read(upload_data_sink, buffer); +} + +void Cronet_UploadDataProvider_Rewind( + Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink) { + DCHECK(self); + self->Rewind(upload_data_sink); +} + +void Cronet_UploadDataProvider_Close(Cronet_UploadDataProviderPtr self) { + DCHECK(self); + self->Close(); +} + +// Implementation of Cronet_UploadDataProvider that forwards calls to C +// functions implemented by the app. +class Cronet_UploadDataProviderStub : public Cronet_UploadDataProvider { + public: + Cronet_UploadDataProviderStub( + Cronet_UploadDataProvider_GetLengthFunc GetLengthFunc, + Cronet_UploadDataProvider_ReadFunc ReadFunc, + Cronet_UploadDataProvider_RewindFunc RewindFunc, + Cronet_UploadDataProvider_CloseFunc CloseFunc) + : GetLengthFunc_(GetLengthFunc), + ReadFunc_(ReadFunc), + RewindFunc_(RewindFunc), + CloseFunc_(CloseFunc) {} + + Cronet_UploadDataProviderStub(const Cronet_UploadDataProviderStub&) = delete; + Cronet_UploadDataProviderStub& operator=( + const Cronet_UploadDataProviderStub&) = delete; + + ~Cronet_UploadDataProviderStub() override {} + + protected: + int64_t GetLength() override { return GetLengthFunc_(this); } + + void Read(Cronet_UploadDataSinkPtr upload_data_sink, + Cronet_BufferPtr buffer) override { + ReadFunc_(this, upload_data_sink, buffer); + } + + void Rewind(Cronet_UploadDataSinkPtr upload_data_sink) override { + RewindFunc_(this, upload_data_sink); + } + + void Close() override { CloseFunc_(this); } + + private: + const Cronet_UploadDataProvider_GetLengthFunc GetLengthFunc_; + const Cronet_UploadDataProvider_ReadFunc ReadFunc_; + const Cronet_UploadDataProvider_RewindFunc RewindFunc_; + const Cronet_UploadDataProvider_CloseFunc CloseFunc_; +}; + +Cronet_UploadDataProviderPtr Cronet_UploadDataProvider_CreateWith( + Cronet_UploadDataProvider_GetLengthFunc GetLengthFunc, + Cronet_UploadDataProvider_ReadFunc ReadFunc, + Cronet_UploadDataProvider_RewindFunc RewindFunc, + Cronet_UploadDataProvider_CloseFunc CloseFunc) { + return new Cronet_UploadDataProviderStub(GetLengthFunc, ReadFunc, RewindFunc, + CloseFunc); +} + +// C functions of Cronet_UrlRequest that forward calls to C++ implementation. +void Cronet_UrlRequest_Destroy(Cronet_UrlRequestPtr self) { + DCHECK(self); + return delete self; +} + +void Cronet_UrlRequest_SetClientContext(Cronet_UrlRequestPtr self, + Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext Cronet_UrlRequest_GetClientContext( + Cronet_UrlRequestPtr self) { + DCHECK(self); + return self->client_context(); +} + +Cronet_RESULT Cronet_UrlRequest_InitWithParams( + Cronet_UrlRequestPtr self, + Cronet_EnginePtr engine, + Cronet_String url, + Cronet_UrlRequestParamsPtr params, + Cronet_UrlRequestCallbackPtr callback, + Cronet_ExecutorPtr executor) { + DCHECK(self); + return self->InitWithParams(engine, url, params, callback, executor); +} + +Cronet_RESULT Cronet_UrlRequest_Start(Cronet_UrlRequestPtr self) { + DCHECK(self); + return self->Start(); +} + +Cronet_RESULT Cronet_UrlRequest_FollowRedirect(Cronet_UrlRequestPtr self) { + DCHECK(self); + return self->FollowRedirect(); +} + +Cronet_RESULT Cronet_UrlRequest_Read(Cronet_UrlRequestPtr self, + Cronet_BufferPtr buffer) { + DCHECK(self); + return self->Read(buffer); +} + +void Cronet_UrlRequest_Cancel(Cronet_UrlRequestPtr self) { + DCHECK(self); + self->Cancel(); +} + +bool Cronet_UrlRequest_IsDone(Cronet_UrlRequestPtr self) { + DCHECK(self); + return self->IsDone(); +} + +void Cronet_UrlRequest_GetStatus(Cronet_UrlRequestPtr self, + Cronet_UrlRequestStatusListenerPtr listener) { + DCHECK(self); + self->GetStatus(listener); +} + +// Implementation of Cronet_UrlRequest that forwards calls to C functions +// implemented by the app. +class Cronet_UrlRequestStub : public Cronet_UrlRequest { + public: + Cronet_UrlRequestStub(Cronet_UrlRequest_InitWithParamsFunc InitWithParamsFunc, + Cronet_UrlRequest_StartFunc StartFunc, + Cronet_UrlRequest_FollowRedirectFunc FollowRedirectFunc, + Cronet_UrlRequest_ReadFunc ReadFunc, + Cronet_UrlRequest_CancelFunc CancelFunc, + Cronet_UrlRequest_IsDoneFunc IsDoneFunc, + Cronet_UrlRequest_GetStatusFunc GetStatusFunc) + : InitWithParamsFunc_(InitWithParamsFunc), + StartFunc_(StartFunc), + FollowRedirectFunc_(FollowRedirectFunc), + ReadFunc_(ReadFunc), + CancelFunc_(CancelFunc), + IsDoneFunc_(IsDoneFunc), + GetStatusFunc_(GetStatusFunc) {} + + Cronet_UrlRequestStub(const Cronet_UrlRequestStub&) = delete; + Cronet_UrlRequestStub& operator=(const Cronet_UrlRequestStub&) = delete; + + ~Cronet_UrlRequestStub() override {} + + protected: + Cronet_RESULT InitWithParams(Cronet_EnginePtr engine, + Cronet_String url, + Cronet_UrlRequestParamsPtr params, + Cronet_UrlRequestCallbackPtr callback, + Cronet_ExecutorPtr executor) override { + return InitWithParamsFunc_(this, engine, url, params, callback, executor); + } + + Cronet_RESULT Start() override { return StartFunc_(this); } + + Cronet_RESULT FollowRedirect() override { return FollowRedirectFunc_(this); } + + Cronet_RESULT Read(Cronet_BufferPtr buffer) override { + return ReadFunc_(this, buffer); + } + + void Cancel() override { CancelFunc_(this); } + + bool IsDone() override { return IsDoneFunc_(this); } + + void GetStatus(Cronet_UrlRequestStatusListenerPtr listener) override { + GetStatusFunc_(this, listener); + } + + private: + const Cronet_UrlRequest_InitWithParamsFunc InitWithParamsFunc_; + const Cronet_UrlRequest_StartFunc StartFunc_; + const Cronet_UrlRequest_FollowRedirectFunc FollowRedirectFunc_; + const Cronet_UrlRequest_ReadFunc ReadFunc_; + const Cronet_UrlRequest_CancelFunc CancelFunc_; + const Cronet_UrlRequest_IsDoneFunc IsDoneFunc_; + const Cronet_UrlRequest_GetStatusFunc GetStatusFunc_; +}; + +Cronet_UrlRequestPtr Cronet_UrlRequest_CreateWith( + Cronet_UrlRequest_InitWithParamsFunc InitWithParamsFunc, + Cronet_UrlRequest_StartFunc StartFunc, + Cronet_UrlRequest_FollowRedirectFunc FollowRedirectFunc, + Cronet_UrlRequest_ReadFunc ReadFunc, + Cronet_UrlRequest_CancelFunc CancelFunc, + Cronet_UrlRequest_IsDoneFunc IsDoneFunc, + Cronet_UrlRequest_GetStatusFunc GetStatusFunc) { + return new Cronet_UrlRequestStub(InitWithParamsFunc, StartFunc, + FollowRedirectFunc, ReadFunc, CancelFunc, + IsDoneFunc, GetStatusFunc); +} + +// C functions of Cronet_RequestFinishedInfoListener that forward calls to C++ +// implementation. +void Cronet_RequestFinishedInfoListener_Destroy( + Cronet_RequestFinishedInfoListenerPtr self) { + DCHECK(self); + return delete self; +} + +void Cronet_RequestFinishedInfoListener_SetClientContext( + Cronet_RequestFinishedInfoListenerPtr self, + Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext Cronet_RequestFinishedInfoListener_GetClientContext( + Cronet_RequestFinishedInfoListenerPtr self) { + DCHECK(self); + return self->client_context(); +} + +void Cronet_RequestFinishedInfoListener_OnRequestFinished( + Cronet_RequestFinishedInfoListenerPtr self, + Cronet_RequestFinishedInfoPtr request_info, + Cronet_UrlResponseInfoPtr response_info, + Cronet_ErrorPtr error) { + DCHECK(self); + self->OnRequestFinished(request_info, response_info, error); +} + +// Implementation of Cronet_RequestFinishedInfoListener that forwards calls to C +// functions implemented by the app. +class Cronet_RequestFinishedInfoListenerStub + : public Cronet_RequestFinishedInfoListener { + public: + explicit Cronet_RequestFinishedInfoListenerStub( + Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc + OnRequestFinishedFunc) + : OnRequestFinishedFunc_(OnRequestFinishedFunc) {} + + Cronet_RequestFinishedInfoListenerStub( + const Cronet_RequestFinishedInfoListenerStub&) = delete; + Cronet_RequestFinishedInfoListenerStub& operator=( + const Cronet_RequestFinishedInfoListenerStub&) = delete; + + ~Cronet_RequestFinishedInfoListenerStub() override {} + + protected: + void OnRequestFinished(Cronet_RequestFinishedInfoPtr request_info, + Cronet_UrlResponseInfoPtr response_info, + Cronet_ErrorPtr error) override { + OnRequestFinishedFunc_(this, request_info, response_info, error); + } + + private: + const Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc + OnRequestFinishedFunc_; +}; + +Cronet_RequestFinishedInfoListenerPtr +Cronet_RequestFinishedInfoListener_CreateWith( + Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc + OnRequestFinishedFunc) { + return new Cronet_RequestFinishedInfoListenerStub(OnRequestFinishedFunc); +} diff --git a/src/components/cronet/native/generated/cronet.idl_impl_interface.h b/src/components/cronet/native/generated/cronet.idl_impl_interface.h new file mode 100644 index 0000000000..c32ef70803 --- /dev/null +++ b/src/components/cronet/native/generated/cronet.idl_impl_interface.h @@ -0,0 +1,276 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from components/cronet/native/generated/cronet.idl */ + +#ifndef COMPONENTS_CRONET_NATIVE_GENERATED_CRONET_IDL_IMPL_INTERFACE_H_ +#define COMPONENTS_CRONET_NATIVE_GENERATED_CRONET_IDL_IMPL_INTERFACE_H_ + +#include "components/cronet/native/generated/cronet.idl_c.h" + +struct Cronet_Buffer { + Cronet_Buffer() = default; + + Cronet_Buffer(const Cronet_Buffer&) = delete; + Cronet_Buffer& operator=(const Cronet_Buffer&) = delete; + + virtual ~Cronet_Buffer() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + + virtual void InitWithDataAndCallback(Cronet_RawDataPtr data, + uint64_t size, + Cronet_BufferCallbackPtr callback) = 0; + virtual void InitWithAlloc(uint64_t size) = 0; + virtual uint64_t GetSize() = 0; + virtual Cronet_RawDataPtr GetData() = 0; + + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +struct Cronet_BufferCallback { + Cronet_BufferCallback() = default; + + Cronet_BufferCallback(const Cronet_BufferCallback&) = delete; + Cronet_BufferCallback& operator=(const Cronet_BufferCallback&) = delete; + + virtual ~Cronet_BufferCallback() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + + virtual void OnDestroy(Cronet_BufferPtr buffer) = 0; + + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +struct Cronet_Runnable { + Cronet_Runnable() = default; + + Cronet_Runnable(const Cronet_Runnable&) = delete; + Cronet_Runnable& operator=(const Cronet_Runnable&) = delete; + + virtual ~Cronet_Runnable() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + + virtual void Run() = 0; + + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +struct Cronet_Executor { + Cronet_Executor() = default; + + Cronet_Executor(const Cronet_Executor&) = delete; + Cronet_Executor& operator=(const Cronet_Executor&) = delete; + + virtual ~Cronet_Executor() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + + virtual void Execute(Cronet_RunnablePtr command) = 0; + + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +struct Cronet_Engine { + Cronet_Engine() = default; + + Cronet_Engine(const Cronet_Engine&) = delete; + Cronet_Engine& operator=(const Cronet_Engine&) = delete; + + virtual ~Cronet_Engine() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + + virtual Cronet_RESULT StartWithParams(Cronet_EngineParamsPtr params) = 0; + virtual bool StartNetLogToFile(Cronet_String file_name, bool log_all) = 0; + virtual void StopNetLog() = 0; + virtual Cronet_RESULT Shutdown() = 0; + virtual Cronet_String GetVersionString() = 0; + virtual Cronet_String GetDefaultUserAgent() = 0; + virtual void AddRequestFinishedListener( + Cronet_RequestFinishedInfoListenerPtr listener, + Cronet_ExecutorPtr executor) = 0; + virtual void RemoveRequestFinishedListener( + Cronet_RequestFinishedInfoListenerPtr listener) = 0; + + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +struct Cronet_UrlRequestStatusListener { + Cronet_UrlRequestStatusListener() = default; + + Cronet_UrlRequestStatusListener(const Cronet_UrlRequestStatusListener&) = + delete; + Cronet_UrlRequestStatusListener& operator=( + const Cronet_UrlRequestStatusListener&) = delete; + + virtual ~Cronet_UrlRequestStatusListener() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + + virtual void OnStatus(Cronet_UrlRequestStatusListener_Status status) = 0; + + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +struct Cronet_UrlRequestCallback { + Cronet_UrlRequestCallback() = default; + + Cronet_UrlRequestCallback(const Cronet_UrlRequestCallback&) = delete; + Cronet_UrlRequestCallback& operator=(const Cronet_UrlRequestCallback&) = + delete; + + virtual ~Cronet_UrlRequestCallback() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + + virtual void OnRedirectReceived(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String new_location_url) = 0; + virtual void OnResponseStarted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) = 0; + virtual void OnReadCompleted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytes_read) = 0; + virtual void OnSucceeded(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) = 0; + virtual void OnFailed(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error) = 0; + virtual void OnCanceled(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) = 0; + + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +struct Cronet_UploadDataSink { + Cronet_UploadDataSink() = default; + + Cronet_UploadDataSink(const Cronet_UploadDataSink&) = delete; + Cronet_UploadDataSink& operator=(const Cronet_UploadDataSink&) = delete; + + virtual ~Cronet_UploadDataSink() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + + virtual void OnReadSucceeded(uint64_t bytes_read, bool final_chunk) = 0; + virtual void OnReadError(Cronet_String error_message) = 0; + virtual void OnRewindSucceeded() = 0; + virtual void OnRewindError(Cronet_String error_message) = 0; + + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +struct Cronet_UploadDataProvider { + Cronet_UploadDataProvider() = default; + + Cronet_UploadDataProvider(const Cronet_UploadDataProvider&) = delete; + Cronet_UploadDataProvider& operator=(const Cronet_UploadDataProvider&) = + delete; + + virtual ~Cronet_UploadDataProvider() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + + virtual int64_t GetLength() = 0; + virtual void Read(Cronet_UploadDataSinkPtr upload_data_sink, + Cronet_BufferPtr buffer) = 0; + virtual void Rewind(Cronet_UploadDataSinkPtr upload_data_sink) = 0; + virtual void Close() = 0; + + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +struct Cronet_UrlRequest { + Cronet_UrlRequest() = default; + + Cronet_UrlRequest(const Cronet_UrlRequest&) = delete; + Cronet_UrlRequest& operator=(const Cronet_UrlRequest&) = delete; + + virtual ~Cronet_UrlRequest() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + + virtual Cronet_RESULT InitWithParams(Cronet_EnginePtr engine, + Cronet_String url, + Cronet_UrlRequestParamsPtr params, + Cronet_UrlRequestCallbackPtr callback, + Cronet_ExecutorPtr executor) = 0; + virtual Cronet_RESULT Start() = 0; + virtual Cronet_RESULT FollowRedirect() = 0; + virtual Cronet_RESULT Read(Cronet_BufferPtr buffer) = 0; + virtual void Cancel() = 0; + virtual bool IsDone() = 0; + virtual void GetStatus(Cronet_UrlRequestStatusListenerPtr listener) = 0; + + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +struct Cronet_RequestFinishedInfoListener { + Cronet_RequestFinishedInfoListener() = default; + + Cronet_RequestFinishedInfoListener( + const Cronet_RequestFinishedInfoListener&) = delete; + Cronet_RequestFinishedInfoListener& operator=( + const Cronet_RequestFinishedInfoListener&) = delete; + + virtual ~Cronet_RequestFinishedInfoListener() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + + virtual void OnRequestFinished(Cronet_RequestFinishedInfoPtr request_info, + Cronet_UrlResponseInfoPtr response_info, + Cronet_ErrorPtr error) = 0; + + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +#endif // COMPONENTS_CRONET_NATIVE_GENERATED_CRONET_IDL_IMPL_INTERFACE_H_ diff --git a/src/components/cronet/native/generated/cronet.idl_impl_interface_unittest.cc b/src/components/cronet/native/generated/cronet.idl_impl_interface_unittest.cc new file mode 100644 index 0000000000..f99c0eb335 --- /dev/null +++ b/src/components/cronet/native/generated/cronet.idl_impl_interface_unittest.cc @@ -0,0 +1,849 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from components/cronet/native/generated/cronet.idl */ + +#include "components/cronet/native/generated/cronet.idl_c.h" + +#include "base/check.h" +#include "testing/gtest/include/gtest/gtest.h" + +// Test of Cronet_Buffer interface. +class Cronet_BufferTest : public ::testing::Test { + public: + Cronet_BufferTest(const Cronet_BufferTest&) = delete; + Cronet_BufferTest& operator=(const Cronet_BufferTest&) = delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + Cronet_BufferTest() = default; + ~Cronet_BufferTest() override = default; + + public: + bool InitWithDataAndCallback_called_ = false; + bool InitWithAlloc_called_ = false; + bool GetSize_called_ = false; + bool GetData_called_ = false; +}; + +namespace { +// Implementation of Cronet_Buffer methods for testing. +void TestCronet_Buffer_InitWithDataAndCallback( + Cronet_BufferPtr self, + Cronet_RawDataPtr data, + uint64_t size, + Cronet_BufferCallbackPtr callback) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Buffer_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->InitWithDataAndCallback_called_ = true; +} +void TestCronet_Buffer_InitWithAlloc(Cronet_BufferPtr self, uint64_t size) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Buffer_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->InitWithAlloc_called_ = true; +} +uint64_t TestCronet_Buffer_GetSize(Cronet_BufferPtr self) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Buffer_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->GetSize_called_ = true; + + return static_cast(0); +} +Cronet_RawDataPtr TestCronet_Buffer_GetData(Cronet_BufferPtr self) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Buffer_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->GetData_called_ = true; + + return static_cast(0); +} +} // namespace + +// Test that Cronet_Buffer stub forwards function calls as expected. +TEST_F(Cronet_BufferTest, TestCreate) { + Cronet_BufferPtr test = Cronet_Buffer_CreateWith( + TestCronet_Buffer_InitWithDataAndCallback, + TestCronet_Buffer_InitWithAlloc, TestCronet_Buffer_GetSize, + TestCronet_Buffer_GetData); + CHECK(test); + Cronet_Buffer_SetClientContext(test, this); + CHECK(!InitWithDataAndCallback_called_); + CHECK(!InitWithAlloc_called_); + Cronet_Buffer_GetSize(test); + CHECK(GetSize_called_); + Cronet_Buffer_GetData(test); + CHECK(GetData_called_); + + Cronet_Buffer_Destroy(test); +} + +// Test of Cronet_BufferCallback interface. +class Cronet_BufferCallbackTest : public ::testing::Test { + public: + Cronet_BufferCallbackTest(const Cronet_BufferCallbackTest&) = delete; + Cronet_BufferCallbackTest& operator=(const Cronet_BufferCallbackTest&) = + delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + Cronet_BufferCallbackTest() = default; + ~Cronet_BufferCallbackTest() override = default; + + public: + bool OnDestroy_called_ = false; +}; + +namespace { +// Implementation of Cronet_BufferCallback methods for testing. +void TestCronet_BufferCallback_OnDestroy(Cronet_BufferCallbackPtr self, + Cronet_BufferPtr buffer) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_BufferCallback_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->OnDestroy_called_ = true; +} +} // namespace + +// Test that Cronet_BufferCallback stub forwards function calls as expected. +TEST_F(Cronet_BufferCallbackTest, TestCreate) { + Cronet_BufferCallbackPtr test = + Cronet_BufferCallback_CreateWith(TestCronet_BufferCallback_OnDestroy); + CHECK(test); + Cronet_BufferCallback_SetClientContext(test, this); + CHECK(!OnDestroy_called_); + + Cronet_BufferCallback_Destroy(test); +} + +// Test of Cronet_Runnable interface. +class Cronet_RunnableTest : public ::testing::Test { + public: + Cronet_RunnableTest(const Cronet_RunnableTest&) = delete; + Cronet_RunnableTest& operator=(const Cronet_RunnableTest&) = delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + Cronet_RunnableTest() = default; + ~Cronet_RunnableTest() override = default; + + public: + bool Run_called_ = false; +}; + +namespace { +// Implementation of Cronet_Runnable methods for testing. +void TestCronet_Runnable_Run(Cronet_RunnablePtr self) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Runnable_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->Run_called_ = true; +} +} // namespace + +// Test that Cronet_Runnable stub forwards function calls as expected. +TEST_F(Cronet_RunnableTest, TestCreate) { + Cronet_RunnablePtr test = Cronet_Runnable_CreateWith(TestCronet_Runnable_Run); + CHECK(test); + Cronet_Runnable_SetClientContext(test, this); + Cronet_Runnable_Run(test); + CHECK(Run_called_); + + Cronet_Runnable_Destroy(test); +} + +// Test of Cronet_Executor interface. +class Cronet_ExecutorTest : public ::testing::Test { + public: + Cronet_ExecutorTest(const Cronet_ExecutorTest&) = delete; + Cronet_ExecutorTest& operator=(const Cronet_ExecutorTest&) = delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + Cronet_ExecutorTest() = default; + ~Cronet_ExecutorTest() override = default; + + public: + bool Execute_called_ = false; +}; + +namespace { +// Implementation of Cronet_Executor methods for testing. +void TestCronet_Executor_Execute(Cronet_ExecutorPtr self, + Cronet_RunnablePtr command) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Executor_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->Execute_called_ = true; +} +} // namespace + +// Test that Cronet_Executor stub forwards function calls as expected. +TEST_F(Cronet_ExecutorTest, TestCreate) { + Cronet_ExecutorPtr test = + Cronet_Executor_CreateWith(TestCronet_Executor_Execute); + CHECK(test); + Cronet_Executor_SetClientContext(test, this); + CHECK(!Execute_called_); + + Cronet_Executor_Destroy(test); +} + +// Test of Cronet_Engine interface. +class Cronet_EngineTest : public ::testing::Test { + public: + Cronet_EngineTest(const Cronet_EngineTest&) = delete; + Cronet_EngineTest& operator=(const Cronet_EngineTest&) = delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + Cronet_EngineTest() = default; + ~Cronet_EngineTest() override = default; + + public: + bool StartWithParams_called_ = false; + bool StartNetLogToFile_called_ = false; + bool StopNetLog_called_ = false; + bool Shutdown_called_ = false; + bool GetVersionString_called_ = false; + bool GetDefaultUserAgent_called_ = false; + bool AddRequestFinishedListener_called_ = false; + bool RemoveRequestFinishedListener_called_ = false; +}; + +namespace { +// Implementation of Cronet_Engine methods for testing. +Cronet_RESULT TestCronet_Engine_StartWithParams(Cronet_EnginePtr self, + Cronet_EngineParamsPtr params) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Engine_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->StartWithParams_called_ = true; + + return static_cast(0); +} +bool TestCronet_Engine_StartNetLogToFile(Cronet_EnginePtr self, + Cronet_String file_name, + bool log_all) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Engine_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->StartNetLogToFile_called_ = true; + + return static_cast(0); +} +void TestCronet_Engine_StopNetLog(Cronet_EnginePtr self) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Engine_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->StopNetLog_called_ = true; +} +Cronet_RESULT TestCronet_Engine_Shutdown(Cronet_EnginePtr self) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Engine_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->Shutdown_called_ = true; + + return static_cast(0); +} +Cronet_String TestCronet_Engine_GetVersionString(Cronet_EnginePtr self) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Engine_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->GetVersionString_called_ = true; + + return static_cast(0); +} +Cronet_String TestCronet_Engine_GetDefaultUserAgent(Cronet_EnginePtr self) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Engine_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->GetDefaultUserAgent_called_ = true; + + return static_cast(0); +} +void TestCronet_Engine_AddRequestFinishedListener( + Cronet_EnginePtr self, + Cronet_RequestFinishedInfoListenerPtr listener, + Cronet_ExecutorPtr executor) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Engine_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->AddRequestFinishedListener_called_ = true; +} +void TestCronet_Engine_RemoveRequestFinishedListener( + Cronet_EnginePtr self, + Cronet_RequestFinishedInfoListenerPtr listener) { + CHECK(self); + Cronet_ClientContext client_context = Cronet_Engine_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->RemoveRequestFinishedListener_called_ = true; +} +} // namespace + +// Test that Cronet_Engine stub forwards function calls as expected. +TEST_F(Cronet_EngineTest, TestCreate) { + Cronet_EnginePtr test = Cronet_Engine_CreateWith( + TestCronet_Engine_StartWithParams, TestCronet_Engine_StartNetLogToFile, + TestCronet_Engine_StopNetLog, TestCronet_Engine_Shutdown, + TestCronet_Engine_GetVersionString, TestCronet_Engine_GetDefaultUserAgent, + TestCronet_Engine_AddRequestFinishedListener, + TestCronet_Engine_RemoveRequestFinishedListener); + CHECK(test); + Cronet_Engine_SetClientContext(test, this); + CHECK(!StartWithParams_called_); + CHECK(!StartNetLogToFile_called_); + Cronet_Engine_StopNetLog(test); + CHECK(StopNetLog_called_); + Cronet_Engine_Shutdown(test); + CHECK(Shutdown_called_); + Cronet_Engine_GetVersionString(test); + CHECK(GetVersionString_called_); + Cronet_Engine_GetDefaultUserAgent(test); + CHECK(GetDefaultUserAgent_called_); + CHECK(!AddRequestFinishedListener_called_); + CHECK(!RemoveRequestFinishedListener_called_); + + Cronet_Engine_Destroy(test); +} + +// Test of Cronet_UrlRequestStatusListener interface. +class Cronet_UrlRequestStatusListenerTest : public ::testing::Test { + public: + Cronet_UrlRequestStatusListenerTest( + const Cronet_UrlRequestStatusListenerTest&) = delete; + Cronet_UrlRequestStatusListenerTest& operator=( + const Cronet_UrlRequestStatusListenerTest&) = delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + Cronet_UrlRequestStatusListenerTest() = default; + ~Cronet_UrlRequestStatusListenerTest() override = default; + + public: + bool OnStatus_called_ = false; +}; + +namespace { +// Implementation of Cronet_UrlRequestStatusListener methods for testing. +void TestCronet_UrlRequestStatusListener_OnStatus( + Cronet_UrlRequestStatusListenerPtr self, + Cronet_UrlRequestStatusListener_Status status) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequestStatusListener_GetClientContext(self); + auto* test = + static_cast(client_context); + CHECK(test); + test->OnStatus_called_ = true; +} +} // namespace + +// Test that Cronet_UrlRequestStatusListener stub forwards function calls as +// expected. +TEST_F(Cronet_UrlRequestStatusListenerTest, TestCreate) { + Cronet_UrlRequestStatusListenerPtr test = + Cronet_UrlRequestStatusListener_CreateWith( + TestCronet_UrlRequestStatusListener_OnStatus); + CHECK(test); + Cronet_UrlRequestStatusListener_SetClientContext(test, this); + CHECK(!OnStatus_called_); + + Cronet_UrlRequestStatusListener_Destroy(test); +} + +// Test of Cronet_UrlRequestCallback interface. +class Cronet_UrlRequestCallbackTest : public ::testing::Test { + public: + Cronet_UrlRequestCallbackTest(const Cronet_UrlRequestCallbackTest&) = delete; + Cronet_UrlRequestCallbackTest& operator=( + const Cronet_UrlRequestCallbackTest&) = delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + Cronet_UrlRequestCallbackTest() = default; + ~Cronet_UrlRequestCallbackTest() override = default; + + public: + bool OnRedirectReceived_called_ = false; + bool OnResponseStarted_called_ = false; + bool OnReadCompleted_called_ = false; + bool OnSucceeded_called_ = false; + bool OnFailed_called_ = false; + bool OnCanceled_called_ = false; +}; + +namespace { +// Implementation of Cronet_UrlRequestCallback methods for testing. +void TestCronet_UrlRequestCallback_OnRedirectReceived( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String new_location_url) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequestCallback_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->OnRedirectReceived_called_ = true; +} +void TestCronet_UrlRequestCallback_OnResponseStarted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequestCallback_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->OnResponseStarted_called_ = true; +} +void TestCronet_UrlRequestCallback_OnReadCompleted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytes_read) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequestCallback_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->OnReadCompleted_called_ = true; +} +void TestCronet_UrlRequestCallback_OnSucceeded( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequestCallback_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->OnSucceeded_called_ = true; +} +void TestCronet_UrlRequestCallback_OnFailed(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequestCallback_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->OnFailed_called_ = true; +} +void TestCronet_UrlRequestCallback_OnCanceled(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequestCallback_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->OnCanceled_called_ = true; +} +} // namespace + +// Test that Cronet_UrlRequestCallback stub forwards function calls as expected. +TEST_F(Cronet_UrlRequestCallbackTest, TestCreate) { + Cronet_UrlRequestCallbackPtr test = Cronet_UrlRequestCallback_CreateWith( + TestCronet_UrlRequestCallback_OnRedirectReceived, + TestCronet_UrlRequestCallback_OnResponseStarted, + TestCronet_UrlRequestCallback_OnReadCompleted, + TestCronet_UrlRequestCallback_OnSucceeded, + TestCronet_UrlRequestCallback_OnFailed, + TestCronet_UrlRequestCallback_OnCanceled); + CHECK(test); + Cronet_UrlRequestCallback_SetClientContext(test, this); + CHECK(!OnRedirectReceived_called_); + CHECK(!OnResponseStarted_called_); + CHECK(!OnReadCompleted_called_); + CHECK(!OnSucceeded_called_); + CHECK(!OnFailed_called_); + CHECK(!OnCanceled_called_); + + Cronet_UrlRequestCallback_Destroy(test); +} + +// Test of Cronet_UploadDataSink interface. +class Cronet_UploadDataSinkTest : public ::testing::Test { + public: + Cronet_UploadDataSinkTest(const Cronet_UploadDataSinkTest&) = delete; + Cronet_UploadDataSinkTest& operator=(const Cronet_UploadDataSinkTest&) = + delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + Cronet_UploadDataSinkTest() = default; + ~Cronet_UploadDataSinkTest() override = default; + + public: + bool OnReadSucceeded_called_ = false; + bool OnReadError_called_ = false; + bool OnRewindSucceeded_called_ = false; + bool OnRewindError_called_ = false; +}; + +namespace { +// Implementation of Cronet_UploadDataSink methods for testing. +void TestCronet_UploadDataSink_OnReadSucceeded(Cronet_UploadDataSinkPtr self, + uint64_t bytes_read, + bool final_chunk) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UploadDataSink_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->OnReadSucceeded_called_ = true; +} +void TestCronet_UploadDataSink_OnReadError(Cronet_UploadDataSinkPtr self, + Cronet_String error_message) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UploadDataSink_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->OnReadError_called_ = true; +} +void TestCronet_UploadDataSink_OnRewindSucceeded( + Cronet_UploadDataSinkPtr self) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UploadDataSink_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->OnRewindSucceeded_called_ = true; +} +void TestCronet_UploadDataSink_OnRewindError(Cronet_UploadDataSinkPtr self, + Cronet_String error_message) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UploadDataSink_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->OnRewindError_called_ = true; +} +} // namespace + +// Test that Cronet_UploadDataSink stub forwards function calls as expected. +TEST_F(Cronet_UploadDataSinkTest, TestCreate) { + Cronet_UploadDataSinkPtr test = Cronet_UploadDataSink_CreateWith( + TestCronet_UploadDataSink_OnReadSucceeded, + TestCronet_UploadDataSink_OnReadError, + TestCronet_UploadDataSink_OnRewindSucceeded, + TestCronet_UploadDataSink_OnRewindError); + CHECK(test); + Cronet_UploadDataSink_SetClientContext(test, this); + CHECK(!OnReadSucceeded_called_); + CHECK(!OnReadError_called_); + Cronet_UploadDataSink_OnRewindSucceeded(test); + CHECK(OnRewindSucceeded_called_); + CHECK(!OnRewindError_called_); + + Cronet_UploadDataSink_Destroy(test); +} + +// Test of Cronet_UploadDataProvider interface. +class Cronet_UploadDataProviderTest : public ::testing::Test { + public: + Cronet_UploadDataProviderTest(const Cronet_UploadDataProviderTest&) = delete; + Cronet_UploadDataProviderTest& operator=( + const Cronet_UploadDataProviderTest&) = delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + Cronet_UploadDataProviderTest() = default; + ~Cronet_UploadDataProviderTest() override = default; + + public: + bool GetLength_called_ = false; + bool Read_called_ = false; + bool Rewind_called_ = false; + bool Close_called_ = false; +}; + +namespace { +// Implementation of Cronet_UploadDataProvider methods for testing. +int64_t TestCronet_UploadDataProvider_GetLength( + Cronet_UploadDataProviderPtr self) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UploadDataProvider_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->GetLength_called_ = true; + + return static_cast(0); +} +void TestCronet_UploadDataProvider_Read( + Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink, + Cronet_BufferPtr buffer) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UploadDataProvider_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->Read_called_ = true; +} +void TestCronet_UploadDataProvider_Rewind( + Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UploadDataProvider_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->Rewind_called_ = true; +} +void TestCronet_UploadDataProvider_Close(Cronet_UploadDataProviderPtr self) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UploadDataProvider_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->Close_called_ = true; +} +} // namespace + +// Test that Cronet_UploadDataProvider stub forwards function calls as expected. +TEST_F(Cronet_UploadDataProviderTest, TestCreate) { + Cronet_UploadDataProviderPtr test = Cronet_UploadDataProvider_CreateWith( + TestCronet_UploadDataProvider_GetLength, + TestCronet_UploadDataProvider_Read, TestCronet_UploadDataProvider_Rewind, + TestCronet_UploadDataProvider_Close); + CHECK(test); + Cronet_UploadDataProvider_SetClientContext(test, this); + Cronet_UploadDataProvider_GetLength(test); + CHECK(GetLength_called_); + CHECK(!Read_called_); + CHECK(!Rewind_called_); + Cronet_UploadDataProvider_Close(test); + CHECK(Close_called_); + + Cronet_UploadDataProvider_Destroy(test); +} + +// Test of Cronet_UrlRequest interface. +class Cronet_UrlRequestTest : public ::testing::Test { + public: + Cronet_UrlRequestTest(const Cronet_UrlRequestTest&) = delete; + Cronet_UrlRequestTest& operator=(const Cronet_UrlRequestTest&) = delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + Cronet_UrlRequestTest() = default; + ~Cronet_UrlRequestTest() override = default; + + public: + bool InitWithParams_called_ = false; + bool Start_called_ = false; + bool FollowRedirect_called_ = false; + bool Read_called_ = false; + bool Cancel_called_ = false; + bool IsDone_called_ = false; + bool GetStatus_called_ = false; +}; + +namespace { +// Implementation of Cronet_UrlRequest methods for testing. +Cronet_RESULT TestCronet_UrlRequest_InitWithParams( + Cronet_UrlRequestPtr self, + Cronet_EnginePtr engine, + Cronet_String url, + Cronet_UrlRequestParamsPtr params, + Cronet_UrlRequestCallbackPtr callback, + Cronet_ExecutorPtr executor) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequest_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->InitWithParams_called_ = true; + + return static_cast(0); +} +Cronet_RESULT TestCronet_UrlRequest_Start(Cronet_UrlRequestPtr self) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequest_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->Start_called_ = true; + + return static_cast(0); +} +Cronet_RESULT TestCronet_UrlRequest_FollowRedirect(Cronet_UrlRequestPtr self) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequest_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->FollowRedirect_called_ = true; + + return static_cast(0); +} +Cronet_RESULT TestCronet_UrlRequest_Read(Cronet_UrlRequestPtr self, + Cronet_BufferPtr buffer) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequest_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->Read_called_ = true; + + return static_cast(0); +} +void TestCronet_UrlRequest_Cancel(Cronet_UrlRequestPtr self) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequest_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->Cancel_called_ = true; +} +bool TestCronet_UrlRequest_IsDone(Cronet_UrlRequestPtr self) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequest_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->IsDone_called_ = true; + + return static_cast(0); +} +void TestCronet_UrlRequest_GetStatus( + Cronet_UrlRequestPtr self, + Cronet_UrlRequestStatusListenerPtr listener) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_UrlRequest_GetClientContext(self); + auto* test = static_cast(client_context); + CHECK(test); + test->GetStatus_called_ = true; +} +} // namespace + +// Test that Cronet_UrlRequest stub forwards function calls as expected. +TEST_F(Cronet_UrlRequestTest, TestCreate) { + Cronet_UrlRequestPtr test = Cronet_UrlRequest_CreateWith( + TestCronet_UrlRequest_InitWithParams, TestCronet_UrlRequest_Start, + TestCronet_UrlRequest_FollowRedirect, TestCronet_UrlRequest_Read, + TestCronet_UrlRequest_Cancel, TestCronet_UrlRequest_IsDone, + TestCronet_UrlRequest_GetStatus); + CHECK(test); + Cronet_UrlRequest_SetClientContext(test, this); + CHECK(!InitWithParams_called_); + Cronet_UrlRequest_Start(test); + CHECK(Start_called_); + Cronet_UrlRequest_FollowRedirect(test); + CHECK(FollowRedirect_called_); + CHECK(!Read_called_); + Cronet_UrlRequest_Cancel(test); + CHECK(Cancel_called_); + Cronet_UrlRequest_IsDone(test); + CHECK(IsDone_called_); + CHECK(!GetStatus_called_); + + Cronet_UrlRequest_Destroy(test); +} + +// Test of Cronet_RequestFinishedInfoListener interface. +class Cronet_RequestFinishedInfoListenerTest : public ::testing::Test { + public: + Cronet_RequestFinishedInfoListenerTest( + const Cronet_RequestFinishedInfoListenerTest&) = delete; + Cronet_RequestFinishedInfoListenerTest& operator=( + const Cronet_RequestFinishedInfoListenerTest&) = delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + Cronet_RequestFinishedInfoListenerTest() = default; + ~Cronet_RequestFinishedInfoListenerTest() override = default; + + public: + bool OnRequestFinished_called_ = false; +}; + +namespace { +// Implementation of Cronet_RequestFinishedInfoListener methods for testing. +void TestCronet_RequestFinishedInfoListener_OnRequestFinished( + Cronet_RequestFinishedInfoListenerPtr self, + Cronet_RequestFinishedInfoPtr request_info, + Cronet_UrlResponseInfoPtr response_info, + Cronet_ErrorPtr error) { + CHECK(self); + Cronet_ClientContext client_context = + Cronet_RequestFinishedInfoListener_GetClientContext(self); + auto* test = + static_cast(client_context); + CHECK(test); + test->OnRequestFinished_called_ = true; +} +} // namespace + +// Test that Cronet_RequestFinishedInfoListener stub forwards function calls as +// expected. +TEST_F(Cronet_RequestFinishedInfoListenerTest, TestCreate) { + Cronet_RequestFinishedInfoListenerPtr test = + Cronet_RequestFinishedInfoListener_CreateWith( + TestCronet_RequestFinishedInfoListener_OnRequestFinished); + CHECK(test); + Cronet_RequestFinishedInfoListener_SetClientContext(test, this); + CHECK(!OnRequestFinished_called_); + + Cronet_RequestFinishedInfoListener_Destroy(test); +} diff --git a/src/components/cronet/native/generated/cronet.idl_impl_struct.cc b/src/components/cronet/native/generated/cronet.idl_impl_struct.cc new file mode 100644 index 0000000000..9ff9bc81cc --- /dev/null +++ b/src/components/cronet/native/generated/cronet.idl_impl_struct.cc @@ -0,0 +1,1273 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from components/cronet/native/generated/cronet.idl */ + +#include "components/cronet/native/generated/cronet.idl_impl_struct.h" + +#include + +#include "base/check.h" + +// Struct Cronet_Error. +Cronet_Error::Cronet_Error() = default; + +Cronet_Error::Cronet_Error(const Cronet_Error& from) = default; + +Cronet_Error::Cronet_Error(Cronet_Error&& from) = default; + +Cronet_Error::~Cronet_Error() = default; + +Cronet_ErrorPtr Cronet_Error_Create() { + return new Cronet_Error(); +} + +void Cronet_Error_Destroy(Cronet_ErrorPtr self) { + delete self; +} + +// Struct Cronet_Error setters. +void Cronet_Error_error_code_set(Cronet_ErrorPtr self, + const Cronet_Error_ERROR_CODE error_code) { + DCHECK(self); + self->error_code = error_code; +} + +void Cronet_Error_message_set(Cronet_ErrorPtr self, + const Cronet_String message) { + DCHECK(self); + self->message = message; +} + +void Cronet_Error_internal_error_code_set(Cronet_ErrorPtr self, + const int32_t internal_error_code) { + DCHECK(self); + self->internal_error_code = internal_error_code; +} + +void Cronet_Error_immediately_retryable_set(Cronet_ErrorPtr self, + const bool immediately_retryable) { + DCHECK(self); + self->immediately_retryable = immediately_retryable; +} + +void Cronet_Error_quic_detailed_error_code_set( + Cronet_ErrorPtr self, + const int32_t quic_detailed_error_code) { + DCHECK(self); + self->quic_detailed_error_code = quic_detailed_error_code; +} + +// Struct Cronet_Error getters. +Cronet_Error_ERROR_CODE Cronet_Error_error_code_get( + const Cronet_ErrorPtr self) { + DCHECK(self); + return self->error_code; +} + +Cronet_String Cronet_Error_message_get(const Cronet_ErrorPtr self) { + DCHECK(self); + return self->message.c_str(); +} + +int32_t Cronet_Error_internal_error_code_get(const Cronet_ErrorPtr self) { + DCHECK(self); + return self->internal_error_code; +} + +bool Cronet_Error_immediately_retryable_get(const Cronet_ErrorPtr self) { + DCHECK(self); + return self->immediately_retryable; +} + +int32_t Cronet_Error_quic_detailed_error_code_get(const Cronet_ErrorPtr self) { + DCHECK(self); + return self->quic_detailed_error_code; +} + +// Struct Cronet_QuicHint. +Cronet_QuicHint::Cronet_QuicHint() = default; + +Cronet_QuicHint::Cronet_QuicHint(const Cronet_QuicHint& from) = default; + +Cronet_QuicHint::Cronet_QuicHint(Cronet_QuicHint&& from) = default; + +Cronet_QuicHint::~Cronet_QuicHint() = default; + +Cronet_QuicHintPtr Cronet_QuicHint_Create() { + return new Cronet_QuicHint(); +} + +void Cronet_QuicHint_Destroy(Cronet_QuicHintPtr self) { + delete self; +} + +// Struct Cronet_QuicHint setters. +void Cronet_QuicHint_host_set(Cronet_QuicHintPtr self, + const Cronet_String host) { + DCHECK(self); + self->host = host; +} + +void Cronet_QuicHint_port_set(Cronet_QuicHintPtr self, const int32_t port) { + DCHECK(self); + self->port = port; +} + +void Cronet_QuicHint_alternate_port_set(Cronet_QuicHintPtr self, + const int32_t alternate_port) { + DCHECK(self); + self->alternate_port = alternate_port; +} + +// Struct Cronet_QuicHint getters. +Cronet_String Cronet_QuicHint_host_get(const Cronet_QuicHintPtr self) { + DCHECK(self); + return self->host.c_str(); +} + +int32_t Cronet_QuicHint_port_get(const Cronet_QuicHintPtr self) { + DCHECK(self); + return self->port; +} + +int32_t Cronet_QuicHint_alternate_port_get(const Cronet_QuicHintPtr self) { + DCHECK(self); + return self->alternate_port; +} + +// Struct Cronet_PublicKeyPins. +Cronet_PublicKeyPins::Cronet_PublicKeyPins() = default; + +Cronet_PublicKeyPins::Cronet_PublicKeyPins(const Cronet_PublicKeyPins& from) = + default; + +Cronet_PublicKeyPins::Cronet_PublicKeyPins(Cronet_PublicKeyPins&& from) = + default; + +Cronet_PublicKeyPins::~Cronet_PublicKeyPins() = default; + +Cronet_PublicKeyPinsPtr Cronet_PublicKeyPins_Create() { + return new Cronet_PublicKeyPins(); +} + +void Cronet_PublicKeyPins_Destroy(Cronet_PublicKeyPinsPtr self) { + delete self; +} + +// Struct Cronet_PublicKeyPins setters. +void Cronet_PublicKeyPins_host_set(Cronet_PublicKeyPinsPtr self, + const Cronet_String host) { + DCHECK(self); + self->host = host; +} + +void Cronet_PublicKeyPins_pins_sha256_add(Cronet_PublicKeyPinsPtr self, + const Cronet_String element) { + DCHECK(self); + self->pins_sha256.push_back(element); +} + +void Cronet_PublicKeyPins_include_subdomains_set( + Cronet_PublicKeyPinsPtr self, + const bool include_subdomains) { + DCHECK(self); + self->include_subdomains = include_subdomains; +} + +void Cronet_PublicKeyPins_expiration_date_set(Cronet_PublicKeyPinsPtr self, + const int64_t expiration_date) { + DCHECK(self); + self->expiration_date = expiration_date; +} + +// Struct Cronet_PublicKeyPins getters. +Cronet_String Cronet_PublicKeyPins_host_get( + const Cronet_PublicKeyPinsPtr self) { + DCHECK(self); + return self->host.c_str(); +} + +uint32_t Cronet_PublicKeyPins_pins_sha256_size(Cronet_PublicKeyPinsPtr self) { + DCHECK(self); + return self->pins_sha256.size(); +} +Cronet_String Cronet_PublicKeyPins_pins_sha256_at( + const Cronet_PublicKeyPinsPtr self, + uint32_t index) { + DCHECK(self); + DCHECK(index < self->pins_sha256.size()); + return self->pins_sha256[index].c_str(); +} +void Cronet_PublicKeyPins_pins_sha256_clear(Cronet_PublicKeyPinsPtr self) { + DCHECK(self); + self->pins_sha256.clear(); +} + +bool Cronet_PublicKeyPins_include_subdomains_get( + const Cronet_PublicKeyPinsPtr self) { + DCHECK(self); + return self->include_subdomains; +} + +int64_t Cronet_PublicKeyPins_expiration_date_get( + const Cronet_PublicKeyPinsPtr self) { + DCHECK(self); + return self->expiration_date; +} + +// Struct Cronet_EngineParams. +Cronet_EngineParams::Cronet_EngineParams() = default; + +Cronet_EngineParams::Cronet_EngineParams(const Cronet_EngineParams& from) = + default; + +Cronet_EngineParams::Cronet_EngineParams(Cronet_EngineParams&& from) = default; + +Cronet_EngineParams::~Cronet_EngineParams() = default; + +Cronet_EngineParamsPtr Cronet_EngineParams_Create() { + return new Cronet_EngineParams(); +} + +void Cronet_EngineParams_Destroy(Cronet_EngineParamsPtr self) { + delete self; +} + +// Struct Cronet_EngineParams setters. +void Cronet_EngineParams_enable_check_result_set( + Cronet_EngineParamsPtr self, + const bool enable_check_result) { + DCHECK(self); + self->enable_check_result = enable_check_result; +} + +void Cronet_EngineParams_user_agent_set(Cronet_EngineParamsPtr self, + const Cronet_String user_agent) { + DCHECK(self); + self->user_agent = user_agent; +} + +void Cronet_EngineParams_accept_language_set( + Cronet_EngineParamsPtr self, + const Cronet_String accept_language) { + DCHECK(self); + self->accept_language = accept_language; +} + +void Cronet_EngineParams_storage_path_set(Cronet_EngineParamsPtr self, + const Cronet_String storage_path) { + DCHECK(self); + self->storage_path = storage_path; +} + +void Cronet_EngineParams_enable_quic_set(Cronet_EngineParamsPtr self, + const bool enable_quic) { + DCHECK(self); + self->enable_quic = enable_quic; +} + +void Cronet_EngineParams_enable_http2_set(Cronet_EngineParamsPtr self, + const bool enable_http2) { + DCHECK(self); + self->enable_http2 = enable_http2; +} + +void Cronet_EngineParams_enable_brotli_set(Cronet_EngineParamsPtr self, + const bool enable_brotli) { + DCHECK(self); + self->enable_brotli = enable_brotli; +} + +void Cronet_EngineParams_http_cache_mode_set( + Cronet_EngineParamsPtr self, + const Cronet_EngineParams_HTTP_CACHE_MODE http_cache_mode) { + DCHECK(self); + self->http_cache_mode = http_cache_mode; +} + +void Cronet_EngineParams_http_cache_max_size_set( + Cronet_EngineParamsPtr self, + const int64_t http_cache_max_size) { + DCHECK(self); + self->http_cache_max_size = http_cache_max_size; +} + +void Cronet_EngineParams_quic_hints_add(Cronet_EngineParamsPtr self, + const Cronet_QuicHintPtr element) { + DCHECK(self); + self->quic_hints.push_back(*element); +} + +void Cronet_EngineParams_public_key_pins_add( + Cronet_EngineParamsPtr self, + const Cronet_PublicKeyPinsPtr element) { + DCHECK(self); + self->public_key_pins.push_back(*element); +} + +void Cronet_EngineParams_enable_public_key_pinning_bypass_for_local_trust_anchors_set( + Cronet_EngineParamsPtr self, + const bool enable_public_key_pinning_bypass_for_local_trust_anchors) { + DCHECK(self); + self->enable_public_key_pinning_bypass_for_local_trust_anchors = + enable_public_key_pinning_bypass_for_local_trust_anchors; +} + +void Cronet_EngineParams_network_thread_priority_set( + Cronet_EngineParamsPtr self, + const double network_thread_priority) { + DCHECK(self); + self->network_thread_priority = network_thread_priority; +} + +void Cronet_EngineParams_experimental_options_set( + Cronet_EngineParamsPtr self, + const Cronet_String experimental_options) { + DCHECK(self); + self->experimental_options = experimental_options; +} + +// Struct Cronet_EngineParams getters. +bool Cronet_EngineParams_enable_check_result_get( + const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->enable_check_result; +} + +Cronet_String Cronet_EngineParams_user_agent_get( + const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->user_agent.c_str(); +} + +Cronet_String Cronet_EngineParams_accept_language_get( + const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->accept_language.c_str(); +} + +Cronet_String Cronet_EngineParams_storage_path_get( + const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->storage_path.c_str(); +} + +bool Cronet_EngineParams_enable_quic_get(const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->enable_quic; +} + +bool Cronet_EngineParams_enable_http2_get(const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->enable_http2; +} + +bool Cronet_EngineParams_enable_brotli_get(const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->enable_brotli; +} + +Cronet_EngineParams_HTTP_CACHE_MODE Cronet_EngineParams_http_cache_mode_get( + const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->http_cache_mode; +} + +int64_t Cronet_EngineParams_http_cache_max_size_get( + const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->http_cache_max_size; +} + +uint32_t Cronet_EngineParams_quic_hints_size(Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->quic_hints.size(); +} +Cronet_QuicHintPtr Cronet_EngineParams_quic_hints_at( + const Cronet_EngineParamsPtr self, + uint32_t index) { + DCHECK(self); + DCHECK(index < self->quic_hints.size()); + return &(self->quic_hints[index]); +} +void Cronet_EngineParams_quic_hints_clear(Cronet_EngineParamsPtr self) { + DCHECK(self); + self->quic_hints.clear(); +} + +uint32_t Cronet_EngineParams_public_key_pins_size(Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->public_key_pins.size(); +} +Cronet_PublicKeyPinsPtr Cronet_EngineParams_public_key_pins_at( + const Cronet_EngineParamsPtr self, + uint32_t index) { + DCHECK(self); + DCHECK(index < self->public_key_pins.size()); + return &(self->public_key_pins[index]); +} +void Cronet_EngineParams_public_key_pins_clear(Cronet_EngineParamsPtr self) { + DCHECK(self); + self->public_key_pins.clear(); +} + +bool Cronet_EngineParams_enable_public_key_pinning_bypass_for_local_trust_anchors_get( + const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->enable_public_key_pinning_bypass_for_local_trust_anchors; +} + +double Cronet_EngineParams_network_thread_priority_get( + const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->network_thread_priority; +} + +Cronet_String Cronet_EngineParams_experimental_options_get( + const Cronet_EngineParamsPtr self) { + DCHECK(self); + return self->experimental_options.c_str(); +} + +// Struct Cronet_HttpHeader. +Cronet_HttpHeader::Cronet_HttpHeader() = default; + +Cronet_HttpHeader::Cronet_HttpHeader(const Cronet_HttpHeader& from) = default; + +Cronet_HttpHeader::Cronet_HttpHeader(Cronet_HttpHeader&& from) = default; + +Cronet_HttpHeader::~Cronet_HttpHeader() = default; + +Cronet_HttpHeaderPtr Cronet_HttpHeader_Create() { + return new Cronet_HttpHeader(); +} + +void Cronet_HttpHeader_Destroy(Cronet_HttpHeaderPtr self) { + delete self; +} + +// Struct Cronet_HttpHeader setters. +void Cronet_HttpHeader_name_set(Cronet_HttpHeaderPtr self, + const Cronet_String name) { + DCHECK(self); + self->name = name; +} + +void Cronet_HttpHeader_value_set(Cronet_HttpHeaderPtr self, + const Cronet_String value) { + DCHECK(self); + self->value = value; +} + +// Struct Cronet_HttpHeader getters. +Cronet_String Cronet_HttpHeader_name_get(const Cronet_HttpHeaderPtr self) { + DCHECK(self); + return self->name.c_str(); +} + +Cronet_String Cronet_HttpHeader_value_get(const Cronet_HttpHeaderPtr self) { + DCHECK(self); + return self->value.c_str(); +} + +// Struct Cronet_UrlResponseInfo. +Cronet_UrlResponseInfo::Cronet_UrlResponseInfo() = default; + +Cronet_UrlResponseInfo::Cronet_UrlResponseInfo( + const Cronet_UrlResponseInfo& from) = default; + +Cronet_UrlResponseInfo::Cronet_UrlResponseInfo(Cronet_UrlResponseInfo&& from) = + default; + +Cronet_UrlResponseInfo::~Cronet_UrlResponseInfo() = default; + +Cronet_UrlResponseInfoPtr Cronet_UrlResponseInfo_Create() { + return new Cronet_UrlResponseInfo(); +} + +void Cronet_UrlResponseInfo_Destroy(Cronet_UrlResponseInfoPtr self) { + delete self; +} + +// Struct Cronet_UrlResponseInfo setters. +void Cronet_UrlResponseInfo_url_set(Cronet_UrlResponseInfoPtr self, + const Cronet_String url) { + DCHECK(self); + self->url = url; +} + +void Cronet_UrlResponseInfo_url_chain_add(Cronet_UrlResponseInfoPtr self, + const Cronet_String element) { + DCHECK(self); + self->url_chain.push_back(element); +} + +void Cronet_UrlResponseInfo_http_status_code_set( + Cronet_UrlResponseInfoPtr self, + const int32_t http_status_code) { + DCHECK(self); + self->http_status_code = http_status_code; +} + +void Cronet_UrlResponseInfo_http_status_text_set( + Cronet_UrlResponseInfoPtr self, + const Cronet_String http_status_text) { + DCHECK(self); + self->http_status_text = http_status_text; +} + +void Cronet_UrlResponseInfo_all_headers_list_add( + Cronet_UrlResponseInfoPtr self, + const Cronet_HttpHeaderPtr element) { + DCHECK(self); + self->all_headers_list.push_back(*element); +} + +void Cronet_UrlResponseInfo_was_cached_set(Cronet_UrlResponseInfoPtr self, + const bool was_cached) { + DCHECK(self); + self->was_cached = was_cached; +} + +void Cronet_UrlResponseInfo_negotiated_protocol_set( + Cronet_UrlResponseInfoPtr self, + const Cronet_String negotiated_protocol) { + DCHECK(self); + self->negotiated_protocol = negotiated_protocol; +} + +void Cronet_UrlResponseInfo_proxy_server_set(Cronet_UrlResponseInfoPtr self, + const Cronet_String proxy_server) { + DCHECK(self); + self->proxy_server = proxy_server; +} + +void Cronet_UrlResponseInfo_received_byte_count_set( + Cronet_UrlResponseInfoPtr self, + const int64_t received_byte_count) { + DCHECK(self); + self->received_byte_count = received_byte_count; +} + +// Struct Cronet_UrlResponseInfo getters. +Cronet_String Cronet_UrlResponseInfo_url_get( + const Cronet_UrlResponseInfoPtr self) { + DCHECK(self); + return self->url.c_str(); +} + +uint32_t Cronet_UrlResponseInfo_url_chain_size(Cronet_UrlResponseInfoPtr self) { + DCHECK(self); + return self->url_chain.size(); +} +Cronet_String Cronet_UrlResponseInfo_url_chain_at( + const Cronet_UrlResponseInfoPtr self, + uint32_t index) { + DCHECK(self); + DCHECK(index < self->url_chain.size()); + return self->url_chain[index].c_str(); +} +void Cronet_UrlResponseInfo_url_chain_clear(Cronet_UrlResponseInfoPtr self) { + DCHECK(self); + self->url_chain.clear(); +} + +int32_t Cronet_UrlResponseInfo_http_status_code_get( + const Cronet_UrlResponseInfoPtr self) { + DCHECK(self); + return self->http_status_code; +} + +Cronet_String Cronet_UrlResponseInfo_http_status_text_get( + const Cronet_UrlResponseInfoPtr self) { + DCHECK(self); + return self->http_status_text.c_str(); +} + +uint32_t Cronet_UrlResponseInfo_all_headers_list_size( + Cronet_UrlResponseInfoPtr self) { + DCHECK(self); + return self->all_headers_list.size(); +} +Cronet_HttpHeaderPtr Cronet_UrlResponseInfo_all_headers_list_at( + const Cronet_UrlResponseInfoPtr self, + uint32_t index) { + DCHECK(self); + DCHECK(index < self->all_headers_list.size()); + return &(self->all_headers_list[index]); +} +void Cronet_UrlResponseInfo_all_headers_list_clear( + Cronet_UrlResponseInfoPtr self) { + DCHECK(self); + self->all_headers_list.clear(); +} + +bool Cronet_UrlResponseInfo_was_cached_get( + const Cronet_UrlResponseInfoPtr self) { + DCHECK(self); + return self->was_cached; +} + +Cronet_String Cronet_UrlResponseInfo_negotiated_protocol_get( + const Cronet_UrlResponseInfoPtr self) { + DCHECK(self); + return self->negotiated_protocol.c_str(); +} + +Cronet_String Cronet_UrlResponseInfo_proxy_server_get( + const Cronet_UrlResponseInfoPtr self) { + DCHECK(self); + return self->proxy_server.c_str(); +} + +int64_t Cronet_UrlResponseInfo_received_byte_count_get( + const Cronet_UrlResponseInfoPtr self) { + DCHECK(self); + return self->received_byte_count; +} + +// Struct Cronet_UrlRequestParams. +Cronet_UrlRequestParams::Cronet_UrlRequestParams() = default; + +Cronet_UrlRequestParams::Cronet_UrlRequestParams( + const Cronet_UrlRequestParams& from) = default; + +Cronet_UrlRequestParams::Cronet_UrlRequestParams( + Cronet_UrlRequestParams&& from) = default; + +Cronet_UrlRequestParams::~Cronet_UrlRequestParams() = default; + +Cronet_UrlRequestParamsPtr Cronet_UrlRequestParams_Create() { + return new Cronet_UrlRequestParams(); +} + +void Cronet_UrlRequestParams_Destroy(Cronet_UrlRequestParamsPtr self) { + delete self; +} + +// Struct Cronet_UrlRequestParams setters. +void Cronet_UrlRequestParams_http_method_set(Cronet_UrlRequestParamsPtr self, + const Cronet_String http_method) { + DCHECK(self); + self->http_method = http_method; +} + +void Cronet_UrlRequestParams_request_headers_add( + Cronet_UrlRequestParamsPtr self, + const Cronet_HttpHeaderPtr element) { + DCHECK(self); + self->request_headers.push_back(*element); +} + +void Cronet_UrlRequestParams_disable_cache_set(Cronet_UrlRequestParamsPtr self, + const bool disable_cache) { + DCHECK(self); + self->disable_cache = disable_cache; +} + +void Cronet_UrlRequestParams_priority_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_UrlRequestParams_REQUEST_PRIORITY priority) { + DCHECK(self); + self->priority = priority; +} + +void Cronet_UrlRequestParams_upload_data_provider_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_UploadDataProviderPtr upload_data_provider) { + DCHECK(self); + self->upload_data_provider = upload_data_provider; +} + +void Cronet_UrlRequestParams_upload_data_provider_executor_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_ExecutorPtr upload_data_provider_executor) { + DCHECK(self); + self->upload_data_provider_executor = upload_data_provider_executor; +} + +void Cronet_UrlRequestParams_allow_direct_executor_set( + Cronet_UrlRequestParamsPtr self, + const bool allow_direct_executor) { + DCHECK(self); + self->allow_direct_executor = allow_direct_executor; +} + +void Cronet_UrlRequestParams_annotations_add(Cronet_UrlRequestParamsPtr self, + const Cronet_RawDataPtr element) { + DCHECK(self); + self->annotations.push_back(element); +} + +void Cronet_UrlRequestParams_request_finished_listener_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_RequestFinishedInfoListenerPtr request_finished_listener) { + DCHECK(self); + self->request_finished_listener = request_finished_listener; +} + +void Cronet_UrlRequestParams_request_finished_executor_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_ExecutorPtr request_finished_executor) { + DCHECK(self); + self->request_finished_executor = request_finished_executor; +} + +void Cronet_UrlRequestParams_idempotency_set( + Cronet_UrlRequestParamsPtr self, + const Cronet_UrlRequestParams_IDEMPOTENCY idempotency) { + DCHECK(self); + self->idempotency = idempotency; +} + +// Struct Cronet_UrlRequestParams getters. +Cronet_String Cronet_UrlRequestParams_http_method_get( + const Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + return self->http_method.c_str(); +} + +uint32_t Cronet_UrlRequestParams_request_headers_size( + Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + return self->request_headers.size(); +} +Cronet_HttpHeaderPtr Cronet_UrlRequestParams_request_headers_at( + const Cronet_UrlRequestParamsPtr self, + uint32_t index) { + DCHECK(self); + DCHECK(index < self->request_headers.size()); + return &(self->request_headers[index]); +} +void Cronet_UrlRequestParams_request_headers_clear( + Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + self->request_headers.clear(); +} + +bool Cronet_UrlRequestParams_disable_cache_get( + const Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + return self->disable_cache; +} + +Cronet_UrlRequestParams_REQUEST_PRIORITY Cronet_UrlRequestParams_priority_get( + const Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + return self->priority; +} + +Cronet_UploadDataProviderPtr Cronet_UrlRequestParams_upload_data_provider_get( + const Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + return self->upload_data_provider; +} + +Cronet_ExecutorPtr Cronet_UrlRequestParams_upload_data_provider_executor_get( + const Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + return self->upload_data_provider_executor; +} + +bool Cronet_UrlRequestParams_allow_direct_executor_get( + const Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + return self->allow_direct_executor; +} + +uint32_t Cronet_UrlRequestParams_annotations_size( + Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + return self->annotations.size(); +} +Cronet_RawDataPtr Cronet_UrlRequestParams_annotations_at( + const Cronet_UrlRequestParamsPtr self, + uint32_t index) { + DCHECK(self); + DCHECK(index < self->annotations.size()); + return self->annotations[index]; +} +void Cronet_UrlRequestParams_annotations_clear( + Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + self->annotations.clear(); +} + +Cronet_RequestFinishedInfoListenerPtr +Cronet_UrlRequestParams_request_finished_listener_get( + const Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + return self->request_finished_listener; +} + +Cronet_ExecutorPtr Cronet_UrlRequestParams_request_finished_executor_get( + const Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + return self->request_finished_executor; +} + +Cronet_UrlRequestParams_IDEMPOTENCY Cronet_UrlRequestParams_idempotency_get( + const Cronet_UrlRequestParamsPtr self) { + DCHECK(self); + return self->idempotency; +} + +// Struct Cronet_DateTime. +Cronet_DateTime::Cronet_DateTime() = default; + +Cronet_DateTime::Cronet_DateTime(const Cronet_DateTime& from) = default; + +Cronet_DateTime::Cronet_DateTime(Cronet_DateTime&& from) = default; + +Cronet_DateTime::~Cronet_DateTime() = default; + +Cronet_DateTimePtr Cronet_DateTime_Create() { + return new Cronet_DateTime(); +} + +void Cronet_DateTime_Destroy(Cronet_DateTimePtr self) { + delete self; +} + +// Struct Cronet_DateTime setters. +void Cronet_DateTime_value_set(Cronet_DateTimePtr self, const int64_t value) { + DCHECK(self); + self->value = value; +} + +// Struct Cronet_DateTime getters. +int64_t Cronet_DateTime_value_get(const Cronet_DateTimePtr self) { + DCHECK(self); + return self->value; +} + +// Struct Cronet_Metrics. +Cronet_Metrics::Cronet_Metrics() = default; + +Cronet_Metrics::Cronet_Metrics(const Cronet_Metrics& from) = default; + +Cronet_Metrics::Cronet_Metrics(Cronet_Metrics&& from) = default; + +Cronet_Metrics::~Cronet_Metrics() = default; + +Cronet_MetricsPtr Cronet_Metrics_Create() { + return new Cronet_Metrics(); +} + +void Cronet_Metrics_Destroy(Cronet_MetricsPtr self) { + delete self; +} + +// Struct Cronet_Metrics setters. +void Cronet_Metrics_request_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr request_start) { + DCHECK(self); + self->request_start.reset(); + if (request_start != nullptr) + self->request_start.emplace(*request_start); +} +void Cronet_Metrics_request_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr request_start) { + DCHECK(self); + self->request_start.reset(); + if (request_start != nullptr) + self->request_start.emplace(std::move(*request_start)); +} + +void Cronet_Metrics_dns_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr dns_start) { + DCHECK(self); + self->dns_start.reset(); + if (dns_start != nullptr) + self->dns_start.emplace(*dns_start); +} +void Cronet_Metrics_dns_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr dns_start) { + DCHECK(self); + self->dns_start.reset(); + if (dns_start != nullptr) + self->dns_start.emplace(std::move(*dns_start)); +} + +void Cronet_Metrics_dns_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr dns_end) { + DCHECK(self); + self->dns_end.reset(); + if (dns_end != nullptr) + self->dns_end.emplace(*dns_end); +} +void Cronet_Metrics_dns_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr dns_end) { + DCHECK(self); + self->dns_end.reset(); + if (dns_end != nullptr) + self->dns_end.emplace(std::move(*dns_end)); +} + +void Cronet_Metrics_connect_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr connect_start) { + DCHECK(self); + self->connect_start.reset(); + if (connect_start != nullptr) + self->connect_start.emplace(*connect_start); +} +void Cronet_Metrics_connect_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr connect_start) { + DCHECK(self); + self->connect_start.reset(); + if (connect_start != nullptr) + self->connect_start.emplace(std::move(*connect_start)); +} + +void Cronet_Metrics_connect_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr connect_end) { + DCHECK(self); + self->connect_end.reset(); + if (connect_end != nullptr) + self->connect_end.emplace(*connect_end); +} +void Cronet_Metrics_connect_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr connect_end) { + DCHECK(self); + self->connect_end.reset(); + if (connect_end != nullptr) + self->connect_end.emplace(std::move(*connect_end)); +} + +void Cronet_Metrics_ssl_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr ssl_start) { + DCHECK(self); + self->ssl_start.reset(); + if (ssl_start != nullptr) + self->ssl_start.emplace(*ssl_start); +} +void Cronet_Metrics_ssl_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr ssl_start) { + DCHECK(self); + self->ssl_start.reset(); + if (ssl_start != nullptr) + self->ssl_start.emplace(std::move(*ssl_start)); +} + +void Cronet_Metrics_ssl_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr ssl_end) { + DCHECK(self); + self->ssl_end.reset(); + if (ssl_end != nullptr) + self->ssl_end.emplace(*ssl_end); +} +void Cronet_Metrics_ssl_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr ssl_end) { + DCHECK(self); + self->ssl_end.reset(); + if (ssl_end != nullptr) + self->ssl_end.emplace(std::move(*ssl_end)); +} + +void Cronet_Metrics_sending_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr sending_start) { + DCHECK(self); + self->sending_start.reset(); + if (sending_start != nullptr) + self->sending_start.emplace(*sending_start); +} +void Cronet_Metrics_sending_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr sending_start) { + DCHECK(self); + self->sending_start.reset(); + if (sending_start != nullptr) + self->sending_start.emplace(std::move(*sending_start)); +} + +void Cronet_Metrics_sending_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr sending_end) { + DCHECK(self); + self->sending_end.reset(); + if (sending_end != nullptr) + self->sending_end.emplace(*sending_end); +} +void Cronet_Metrics_sending_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr sending_end) { + DCHECK(self); + self->sending_end.reset(); + if (sending_end != nullptr) + self->sending_end.emplace(std::move(*sending_end)); +} + +void Cronet_Metrics_push_start_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr push_start) { + DCHECK(self); + self->push_start.reset(); + if (push_start != nullptr) + self->push_start.emplace(*push_start); +} +void Cronet_Metrics_push_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr push_start) { + DCHECK(self); + self->push_start.reset(); + if (push_start != nullptr) + self->push_start.emplace(std::move(*push_start)); +} + +void Cronet_Metrics_push_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr push_end) { + DCHECK(self); + self->push_end.reset(); + if (push_end != nullptr) + self->push_end.emplace(*push_end); +} +void Cronet_Metrics_push_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr push_end) { + DCHECK(self); + self->push_end.reset(); + if (push_end != nullptr) + self->push_end.emplace(std::move(*push_end)); +} + +void Cronet_Metrics_response_start_set( + Cronet_MetricsPtr self, + const Cronet_DateTimePtr response_start) { + DCHECK(self); + self->response_start.reset(); + if (response_start != nullptr) + self->response_start.emplace(*response_start); +} +void Cronet_Metrics_response_start_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr response_start) { + DCHECK(self); + self->response_start.reset(); + if (response_start != nullptr) + self->response_start.emplace(std::move(*response_start)); +} + +void Cronet_Metrics_request_end_set(Cronet_MetricsPtr self, + const Cronet_DateTimePtr request_end) { + DCHECK(self); + self->request_end.reset(); + if (request_end != nullptr) + self->request_end.emplace(*request_end); +} +void Cronet_Metrics_request_end_move(Cronet_MetricsPtr self, + Cronet_DateTimePtr request_end) { + DCHECK(self); + self->request_end.reset(); + if (request_end != nullptr) + self->request_end.emplace(std::move(*request_end)); +} + +void Cronet_Metrics_socket_reused_set(Cronet_MetricsPtr self, + const bool socket_reused) { + DCHECK(self); + self->socket_reused = socket_reused; +} + +void Cronet_Metrics_sent_byte_count_set(Cronet_MetricsPtr self, + const int64_t sent_byte_count) { + DCHECK(self); + self->sent_byte_count = sent_byte_count; +} + +void Cronet_Metrics_received_byte_count_set(Cronet_MetricsPtr self, + const int64_t received_byte_count) { + DCHECK(self); + self->received_byte_count = received_byte_count; +} + +// Struct Cronet_Metrics getters. +Cronet_DateTimePtr Cronet_Metrics_request_start_get( + const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->request_start == absl::nullopt) + return nullptr; + return &self->request_start.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_dns_start_get(const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->dns_start == absl::nullopt) + return nullptr; + return &self->dns_start.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_dns_end_get(const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->dns_end == absl::nullopt) + return nullptr; + return &self->dns_end.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_connect_start_get( + const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->connect_start == absl::nullopt) + return nullptr; + return &self->connect_start.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_connect_end_get( + const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->connect_end == absl::nullopt) + return nullptr; + return &self->connect_end.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_ssl_start_get(const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->ssl_start == absl::nullopt) + return nullptr; + return &self->ssl_start.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_ssl_end_get(const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->ssl_end == absl::nullopt) + return nullptr; + return &self->ssl_end.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_sending_start_get( + const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->sending_start == absl::nullopt) + return nullptr; + return &self->sending_start.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_sending_end_get( + const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->sending_end == absl::nullopt) + return nullptr; + return &self->sending_end.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_push_start_get(const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->push_start == absl::nullopt) + return nullptr; + return &self->push_start.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_push_end_get(const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->push_end == absl::nullopt) + return nullptr; + return &self->push_end.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_response_start_get( + const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->response_start == absl::nullopt) + return nullptr; + return &self->response_start.value(); +} + +Cronet_DateTimePtr Cronet_Metrics_request_end_get( + const Cronet_MetricsPtr self) { + DCHECK(self); + if (self->request_end == absl::nullopt) + return nullptr; + return &self->request_end.value(); +} + +bool Cronet_Metrics_socket_reused_get(const Cronet_MetricsPtr self) { + DCHECK(self); + return self->socket_reused; +} + +int64_t Cronet_Metrics_sent_byte_count_get(const Cronet_MetricsPtr self) { + DCHECK(self); + return self->sent_byte_count; +} + +int64_t Cronet_Metrics_received_byte_count_get(const Cronet_MetricsPtr self) { + DCHECK(self); + return self->received_byte_count; +} + +// Struct Cronet_RequestFinishedInfo. +Cronet_RequestFinishedInfo::Cronet_RequestFinishedInfo() = default; + +Cronet_RequestFinishedInfo::Cronet_RequestFinishedInfo( + const Cronet_RequestFinishedInfo& from) = default; + +Cronet_RequestFinishedInfo::Cronet_RequestFinishedInfo( + Cronet_RequestFinishedInfo&& from) = default; + +Cronet_RequestFinishedInfo::~Cronet_RequestFinishedInfo() = default; + +Cronet_RequestFinishedInfoPtr Cronet_RequestFinishedInfo_Create() { + return new Cronet_RequestFinishedInfo(); +} + +void Cronet_RequestFinishedInfo_Destroy(Cronet_RequestFinishedInfoPtr self) { + delete self; +} + +// Struct Cronet_RequestFinishedInfo setters. +void Cronet_RequestFinishedInfo_metrics_set(Cronet_RequestFinishedInfoPtr self, + const Cronet_MetricsPtr metrics) { + DCHECK(self); + self->metrics.reset(); + if (metrics != nullptr) + self->metrics.emplace(*metrics); +} +void Cronet_RequestFinishedInfo_metrics_move(Cronet_RequestFinishedInfoPtr self, + Cronet_MetricsPtr metrics) { + DCHECK(self); + self->metrics.reset(); + if (metrics != nullptr) + self->metrics.emplace(std::move(*metrics)); +} + +void Cronet_RequestFinishedInfo_annotations_add( + Cronet_RequestFinishedInfoPtr self, + const Cronet_RawDataPtr element) { + DCHECK(self); + self->annotations.push_back(element); +} + +void Cronet_RequestFinishedInfo_finished_reason_set( + Cronet_RequestFinishedInfoPtr self, + const Cronet_RequestFinishedInfo_FINISHED_REASON finished_reason) { + DCHECK(self); + self->finished_reason = finished_reason; +} + +// Struct Cronet_RequestFinishedInfo getters. +Cronet_MetricsPtr Cronet_RequestFinishedInfo_metrics_get( + const Cronet_RequestFinishedInfoPtr self) { + DCHECK(self); + if (self->metrics == absl::nullopt) + return nullptr; + return &self->metrics.value(); +} + +uint32_t Cronet_RequestFinishedInfo_annotations_size( + Cronet_RequestFinishedInfoPtr self) { + DCHECK(self); + return self->annotations.size(); +} +Cronet_RawDataPtr Cronet_RequestFinishedInfo_annotations_at( + const Cronet_RequestFinishedInfoPtr self, + uint32_t index) { + DCHECK(self); + DCHECK(index < self->annotations.size()); + return self->annotations[index]; +} +void Cronet_RequestFinishedInfo_annotations_clear( + Cronet_RequestFinishedInfoPtr self) { + DCHECK(self); + self->annotations.clear(); +} + +Cronet_RequestFinishedInfo_FINISHED_REASON +Cronet_RequestFinishedInfo_finished_reason_get( + const Cronet_RequestFinishedInfoPtr self) { + DCHECK(self); + return self->finished_reason; +} diff --git a/src/components/cronet/native/generated/cronet.idl_impl_struct.h b/src/components/cronet/native/generated/cronet.idl_impl_struct.h new file mode 100644 index 0000000000..84d6db6c3c --- /dev/null +++ b/src/components/cronet/native/generated/cronet.idl_impl_struct.h @@ -0,0 +1,231 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from components/cronet/native/generated/cronet.idl */ + +#ifndef COMPONENTS_CRONET_NATIVE_GENERATED_CRONET_IDL_IMPL_STRUCT_H_ +#define COMPONENTS_CRONET_NATIVE_GENERATED_CRONET_IDL_IMPL_STRUCT_H_ + +#include "components/cronet/native/generated/cronet.idl_c.h" + +#include +#include +#include + +#include "third_party/abseil-cpp/absl/types/optional.h" + +// Struct Cronet_Error. +struct Cronet_Error { + public: + Cronet_Error(); + explicit Cronet_Error(const Cronet_Error& from); + + Cronet_Error& operator=(const Cronet_Error&) = delete; + + explicit Cronet_Error(Cronet_Error&& from); + + ~Cronet_Error(); + + Cronet_Error_ERROR_CODE error_code = Cronet_Error_ERROR_CODE_ERROR_CALLBACK; + std::string message; + int32_t internal_error_code = 0; + bool immediately_retryable = false; + int32_t quic_detailed_error_code = 0; +}; + +// Struct Cronet_QuicHint. +struct Cronet_QuicHint { + public: + Cronet_QuicHint(); + explicit Cronet_QuicHint(const Cronet_QuicHint& from); + + Cronet_QuicHint& operator=(const Cronet_QuicHint&) = delete; + + explicit Cronet_QuicHint(Cronet_QuicHint&& from); + + ~Cronet_QuicHint(); + + std::string host; + int32_t port = 0; + int32_t alternate_port = 0; +}; + +// Struct Cronet_PublicKeyPins. +struct Cronet_PublicKeyPins { + public: + Cronet_PublicKeyPins(); + explicit Cronet_PublicKeyPins(const Cronet_PublicKeyPins& from); + + Cronet_PublicKeyPins& operator=(const Cronet_PublicKeyPins&) = delete; + + explicit Cronet_PublicKeyPins(Cronet_PublicKeyPins&& from); + + ~Cronet_PublicKeyPins(); + + std::string host; + std::vector pins_sha256; + bool include_subdomains = false; + int64_t expiration_date = 0; +}; + +// Struct Cronet_EngineParams. +struct Cronet_EngineParams { + public: + Cronet_EngineParams(); + explicit Cronet_EngineParams(const Cronet_EngineParams& from); + + Cronet_EngineParams& operator=(const Cronet_EngineParams&) = delete; + + explicit Cronet_EngineParams(Cronet_EngineParams&& from); + + ~Cronet_EngineParams(); + + bool enable_check_result = true; + std::string user_agent; + std::string accept_language; + std::string storage_path; + bool enable_quic = true; + bool enable_http2 = true; + bool enable_brotli = true; + Cronet_EngineParams_HTTP_CACHE_MODE http_cache_mode = + Cronet_EngineParams_HTTP_CACHE_MODE_DISABLED; + int64_t http_cache_max_size = 0; + std::vector quic_hints; + std::vector public_key_pins; + bool enable_public_key_pinning_bypass_for_local_trust_anchors = true; + double network_thread_priority = std::numeric_limits::quiet_NaN(); + std::string experimental_options; +}; + +// Struct Cronet_HttpHeader. +struct Cronet_HttpHeader { + public: + Cronet_HttpHeader(); + explicit Cronet_HttpHeader(const Cronet_HttpHeader& from); + + Cronet_HttpHeader& operator=(const Cronet_HttpHeader&) = delete; + + explicit Cronet_HttpHeader(Cronet_HttpHeader&& from); + + ~Cronet_HttpHeader(); + + std::string name; + std::string value; +}; + +// Struct Cronet_UrlResponseInfo. +struct Cronet_UrlResponseInfo { + public: + Cronet_UrlResponseInfo(); + explicit Cronet_UrlResponseInfo(const Cronet_UrlResponseInfo& from); + + Cronet_UrlResponseInfo& operator=(const Cronet_UrlResponseInfo&) = delete; + + explicit Cronet_UrlResponseInfo(Cronet_UrlResponseInfo&& from); + + ~Cronet_UrlResponseInfo(); + + std::string url; + std::vector url_chain; + int32_t http_status_code = 0; + std::string http_status_text; + std::vector all_headers_list; + bool was_cached = false; + std::string negotiated_protocol; + std::string proxy_server; + int64_t received_byte_count = 0; +}; + +// Struct Cronet_UrlRequestParams. +struct Cronet_UrlRequestParams { + public: + Cronet_UrlRequestParams(); + explicit Cronet_UrlRequestParams(const Cronet_UrlRequestParams& from); + + Cronet_UrlRequestParams& operator=(const Cronet_UrlRequestParams&) = delete; + + explicit Cronet_UrlRequestParams(Cronet_UrlRequestParams&& from); + + ~Cronet_UrlRequestParams(); + + std::string http_method; + std::vector request_headers; + bool disable_cache = false; + Cronet_UrlRequestParams_REQUEST_PRIORITY priority = + Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_MEDIUM; + Cronet_UploadDataProviderPtr upload_data_provider = nullptr; + Cronet_ExecutorPtr upload_data_provider_executor = nullptr; + bool allow_direct_executor = false; + std::vector annotations; + Cronet_RequestFinishedInfoListenerPtr request_finished_listener = nullptr; + Cronet_ExecutorPtr request_finished_executor = nullptr; + Cronet_UrlRequestParams_IDEMPOTENCY idempotency = + Cronet_UrlRequestParams_IDEMPOTENCY_DEFAULT_IDEMPOTENCY; +}; + +// Struct Cronet_DateTime. +struct Cronet_DateTime { + public: + Cronet_DateTime(); + explicit Cronet_DateTime(const Cronet_DateTime& from); + + Cronet_DateTime& operator=(const Cronet_DateTime&) = delete; + + explicit Cronet_DateTime(Cronet_DateTime&& from); + + ~Cronet_DateTime(); + + int64_t value = 0; +}; + +// Struct Cronet_Metrics. +struct Cronet_Metrics { + public: + Cronet_Metrics(); + explicit Cronet_Metrics(const Cronet_Metrics& from); + + Cronet_Metrics& operator=(const Cronet_Metrics&) = delete; + + explicit Cronet_Metrics(Cronet_Metrics&& from); + + ~Cronet_Metrics(); + + absl::optional request_start; + absl::optional dns_start; + absl::optional dns_end; + absl::optional connect_start; + absl::optional connect_end; + absl::optional ssl_start; + absl::optional ssl_end; + absl::optional sending_start; + absl::optional sending_end; + absl::optional push_start; + absl::optional push_end; + absl::optional response_start; + absl::optional request_end; + bool socket_reused = false; + int64_t sent_byte_count = -1; + int64_t received_byte_count = -1; +}; + +// Struct Cronet_RequestFinishedInfo. +struct Cronet_RequestFinishedInfo { + public: + Cronet_RequestFinishedInfo(); + explicit Cronet_RequestFinishedInfo(const Cronet_RequestFinishedInfo& from); + + Cronet_RequestFinishedInfo& operator=(const Cronet_RequestFinishedInfo&) = + delete; + + explicit Cronet_RequestFinishedInfo(Cronet_RequestFinishedInfo&& from); + + ~Cronet_RequestFinishedInfo(); + + absl::optional metrics; + std::vector annotations; + Cronet_RequestFinishedInfo_FINISHED_REASON finished_reason = + Cronet_RequestFinishedInfo_FINISHED_REASON_SUCCEEDED; +}; + +#endif // COMPONENTS_CRONET_NATIVE_GENERATED_CRONET_IDL_IMPL_STRUCT_H_ diff --git a/src/components/cronet/native/generated/cronet.idl_impl_struct_unittest.cc b/src/components/cronet/native/generated/cronet.idl_impl_struct_unittest.cc new file mode 100644 index 0000000000..06f48ee97b --- /dev/null +++ b/src/components/cronet/native/generated/cronet.idl_impl_struct_unittest.cc @@ -0,0 +1,511 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from components/cronet/native/generated/cronet.idl */ + +#include "components/cronet/native/generated/cronet.idl_c.h" + +#include + +#include "testing/gtest/include/gtest/gtest.h" + +class CronetStructTest : public ::testing::Test { + public: + CronetStructTest(const CronetStructTest&) = delete; + CronetStructTest& operator=(const CronetStructTest&) = delete; + + protected: + void SetUp() override {} + + void TearDown() override {} + + CronetStructTest() {} + ~CronetStructTest() override {} +}; + +// Test Struct Cronet_Error setters and getters. +TEST_F(CronetStructTest, TestCronet_Error) { + Cronet_ErrorPtr first = Cronet_Error_Create(); + Cronet_ErrorPtr second = Cronet_Error_Create(); + + // Copy values from |first| to |second|. + Cronet_Error_error_code_set(second, Cronet_Error_error_code_get(first)); + EXPECT_EQ(Cronet_Error_error_code_get(first), + Cronet_Error_error_code_get(second)); + Cronet_Error_message_set(second, Cronet_Error_message_get(first)); + EXPECT_STREQ(Cronet_Error_message_get(first), + Cronet_Error_message_get(second)); + Cronet_Error_internal_error_code_set( + second, Cronet_Error_internal_error_code_get(first)); + EXPECT_EQ(Cronet_Error_internal_error_code_get(first), + Cronet_Error_internal_error_code_get(second)); + Cronet_Error_immediately_retryable_set( + second, Cronet_Error_immediately_retryable_get(first)); + EXPECT_EQ(Cronet_Error_immediately_retryable_get(first), + Cronet_Error_immediately_retryable_get(second)); + Cronet_Error_quic_detailed_error_code_set( + second, Cronet_Error_quic_detailed_error_code_get(first)); + EXPECT_EQ(Cronet_Error_quic_detailed_error_code_get(first), + Cronet_Error_quic_detailed_error_code_get(second)); + Cronet_Error_Destroy(first); + Cronet_Error_Destroy(second); +} + +// Test Struct Cronet_QuicHint setters and getters. +TEST_F(CronetStructTest, TestCronet_QuicHint) { + Cronet_QuicHintPtr first = Cronet_QuicHint_Create(); + Cronet_QuicHintPtr second = Cronet_QuicHint_Create(); + + // Copy values from |first| to |second|. + Cronet_QuicHint_host_set(second, Cronet_QuicHint_host_get(first)); + EXPECT_STREQ(Cronet_QuicHint_host_get(first), + Cronet_QuicHint_host_get(second)); + Cronet_QuicHint_port_set(second, Cronet_QuicHint_port_get(first)); + EXPECT_EQ(Cronet_QuicHint_port_get(first), Cronet_QuicHint_port_get(second)); + Cronet_QuicHint_alternate_port_set(second, + Cronet_QuicHint_alternate_port_get(first)); + EXPECT_EQ(Cronet_QuicHint_alternate_port_get(first), + Cronet_QuicHint_alternate_port_get(second)); + Cronet_QuicHint_Destroy(first); + Cronet_QuicHint_Destroy(second); +} + +// Test Struct Cronet_PublicKeyPins setters and getters. +TEST_F(CronetStructTest, TestCronet_PublicKeyPins) { + Cronet_PublicKeyPinsPtr first = Cronet_PublicKeyPins_Create(); + Cronet_PublicKeyPinsPtr second = Cronet_PublicKeyPins_Create(); + + // Copy values from |first| to |second|. + Cronet_PublicKeyPins_host_set(second, Cronet_PublicKeyPins_host_get(first)); + EXPECT_STREQ(Cronet_PublicKeyPins_host_get(first), + Cronet_PublicKeyPins_host_get(second)); + // TODO(mef): Test array |pins_sha256|. + Cronet_PublicKeyPins_include_subdomains_set( + second, Cronet_PublicKeyPins_include_subdomains_get(first)); + EXPECT_EQ(Cronet_PublicKeyPins_include_subdomains_get(first), + Cronet_PublicKeyPins_include_subdomains_get(second)); + Cronet_PublicKeyPins_expiration_date_set( + second, Cronet_PublicKeyPins_expiration_date_get(first)); + EXPECT_EQ(Cronet_PublicKeyPins_expiration_date_get(first), + Cronet_PublicKeyPins_expiration_date_get(second)); + Cronet_PublicKeyPins_Destroy(first); + Cronet_PublicKeyPins_Destroy(second); +} + +// Test Struct Cronet_EngineParams setters and getters. +TEST_F(CronetStructTest, TestCronet_EngineParams) { + Cronet_EngineParamsPtr first = Cronet_EngineParams_Create(); + Cronet_EngineParamsPtr second = Cronet_EngineParams_Create(); + + // Copy values from |first| to |second|. + Cronet_EngineParams_enable_check_result_set( + second, Cronet_EngineParams_enable_check_result_get(first)); + EXPECT_EQ(Cronet_EngineParams_enable_check_result_get(first), + Cronet_EngineParams_enable_check_result_get(second)); + Cronet_EngineParams_user_agent_set(second, + Cronet_EngineParams_user_agent_get(first)); + EXPECT_STREQ(Cronet_EngineParams_user_agent_get(first), + Cronet_EngineParams_user_agent_get(second)); + Cronet_EngineParams_accept_language_set( + second, Cronet_EngineParams_accept_language_get(first)); + EXPECT_STREQ(Cronet_EngineParams_accept_language_get(first), + Cronet_EngineParams_accept_language_get(second)); + Cronet_EngineParams_storage_path_set( + second, Cronet_EngineParams_storage_path_get(first)); + EXPECT_STREQ(Cronet_EngineParams_storage_path_get(first), + Cronet_EngineParams_storage_path_get(second)); + Cronet_EngineParams_enable_quic_set( + second, Cronet_EngineParams_enable_quic_get(first)); + EXPECT_EQ(Cronet_EngineParams_enable_quic_get(first), + Cronet_EngineParams_enable_quic_get(second)); + Cronet_EngineParams_enable_http2_set( + second, Cronet_EngineParams_enable_http2_get(first)); + EXPECT_EQ(Cronet_EngineParams_enable_http2_get(first), + Cronet_EngineParams_enable_http2_get(second)); + Cronet_EngineParams_enable_brotli_set( + second, Cronet_EngineParams_enable_brotli_get(first)); + EXPECT_EQ(Cronet_EngineParams_enable_brotli_get(first), + Cronet_EngineParams_enable_brotli_get(second)); + Cronet_EngineParams_http_cache_mode_set( + second, Cronet_EngineParams_http_cache_mode_get(first)); + EXPECT_EQ(Cronet_EngineParams_http_cache_mode_get(first), + Cronet_EngineParams_http_cache_mode_get(second)); + Cronet_EngineParams_http_cache_max_size_set( + second, Cronet_EngineParams_http_cache_max_size_get(first)); + EXPECT_EQ(Cronet_EngineParams_http_cache_max_size_get(first), + Cronet_EngineParams_http_cache_max_size_get(second)); + // TODO(mef): Test array |quic_hints|. + // TODO(mef): Test array |public_key_pins|. + Cronet_EngineParams_enable_public_key_pinning_bypass_for_local_trust_anchors_set( + second, + Cronet_EngineParams_enable_public_key_pinning_bypass_for_local_trust_anchors_get( + first)); + EXPECT_EQ( + Cronet_EngineParams_enable_public_key_pinning_bypass_for_local_trust_anchors_get( + first), + Cronet_EngineParams_enable_public_key_pinning_bypass_for_local_trust_anchors_get( + second)); + Cronet_EngineParams_network_thread_priority_set( + second, Cronet_EngineParams_network_thread_priority_get(first)); + EXPECT_TRUE( + Cronet_EngineParams_network_thread_priority_get(first) == + Cronet_EngineParams_network_thread_priority_get(second) || + isnan(Cronet_EngineParams_network_thread_priority_get(first)) && + isnan(Cronet_EngineParams_network_thread_priority_get(second))); + Cronet_EngineParams_experimental_options_set( + second, Cronet_EngineParams_experimental_options_get(first)); + EXPECT_STREQ(Cronet_EngineParams_experimental_options_get(first), + Cronet_EngineParams_experimental_options_get(second)); + Cronet_EngineParams_Destroy(first); + Cronet_EngineParams_Destroy(second); +} + +// Test Struct Cronet_HttpHeader setters and getters. +TEST_F(CronetStructTest, TestCronet_HttpHeader) { + Cronet_HttpHeaderPtr first = Cronet_HttpHeader_Create(); + Cronet_HttpHeaderPtr second = Cronet_HttpHeader_Create(); + + // Copy values from |first| to |second|. + Cronet_HttpHeader_name_set(second, Cronet_HttpHeader_name_get(first)); + EXPECT_STREQ(Cronet_HttpHeader_name_get(first), + Cronet_HttpHeader_name_get(second)); + Cronet_HttpHeader_value_set(second, Cronet_HttpHeader_value_get(first)); + EXPECT_STREQ(Cronet_HttpHeader_value_get(first), + Cronet_HttpHeader_value_get(second)); + Cronet_HttpHeader_Destroy(first); + Cronet_HttpHeader_Destroy(second); +} + +// Test Struct Cronet_UrlResponseInfo setters and getters. +TEST_F(CronetStructTest, TestCronet_UrlResponseInfo) { + Cronet_UrlResponseInfoPtr first = Cronet_UrlResponseInfo_Create(); + Cronet_UrlResponseInfoPtr second = Cronet_UrlResponseInfo_Create(); + + // Copy values from |first| to |second|. + Cronet_UrlResponseInfo_url_set(second, Cronet_UrlResponseInfo_url_get(first)); + EXPECT_STREQ(Cronet_UrlResponseInfo_url_get(first), + Cronet_UrlResponseInfo_url_get(second)); + // TODO(mef): Test array |url_chain|. + Cronet_UrlResponseInfo_http_status_code_set( + second, Cronet_UrlResponseInfo_http_status_code_get(first)); + EXPECT_EQ(Cronet_UrlResponseInfo_http_status_code_get(first), + Cronet_UrlResponseInfo_http_status_code_get(second)); + Cronet_UrlResponseInfo_http_status_text_set( + second, Cronet_UrlResponseInfo_http_status_text_get(first)); + EXPECT_STREQ(Cronet_UrlResponseInfo_http_status_text_get(first), + Cronet_UrlResponseInfo_http_status_text_get(second)); + // TODO(mef): Test array |all_headers_list|. + Cronet_UrlResponseInfo_was_cached_set( + second, Cronet_UrlResponseInfo_was_cached_get(first)); + EXPECT_EQ(Cronet_UrlResponseInfo_was_cached_get(first), + Cronet_UrlResponseInfo_was_cached_get(second)); + Cronet_UrlResponseInfo_negotiated_protocol_set( + second, Cronet_UrlResponseInfo_negotiated_protocol_get(first)); + EXPECT_STREQ(Cronet_UrlResponseInfo_negotiated_protocol_get(first), + Cronet_UrlResponseInfo_negotiated_protocol_get(second)); + Cronet_UrlResponseInfo_proxy_server_set( + second, Cronet_UrlResponseInfo_proxy_server_get(first)); + EXPECT_STREQ(Cronet_UrlResponseInfo_proxy_server_get(first), + Cronet_UrlResponseInfo_proxy_server_get(second)); + Cronet_UrlResponseInfo_received_byte_count_set( + second, Cronet_UrlResponseInfo_received_byte_count_get(first)); + EXPECT_EQ(Cronet_UrlResponseInfo_received_byte_count_get(first), + Cronet_UrlResponseInfo_received_byte_count_get(second)); + Cronet_UrlResponseInfo_Destroy(first); + Cronet_UrlResponseInfo_Destroy(second); +} + +// Test Struct Cronet_UrlRequestParams setters and getters. +TEST_F(CronetStructTest, TestCronet_UrlRequestParams) { + Cronet_UrlRequestParamsPtr first = Cronet_UrlRequestParams_Create(); + Cronet_UrlRequestParamsPtr second = Cronet_UrlRequestParams_Create(); + + // Copy values from |first| to |second|. + Cronet_UrlRequestParams_http_method_set( + second, Cronet_UrlRequestParams_http_method_get(first)); + EXPECT_STREQ(Cronet_UrlRequestParams_http_method_get(first), + Cronet_UrlRequestParams_http_method_get(second)); + // TODO(mef): Test array |request_headers|. + Cronet_UrlRequestParams_disable_cache_set( + second, Cronet_UrlRequestParams_disable_cache_get(first)); + EXPECT_EQ(Cronet_UrlRequestParams_disable_cache_get(first), + Cronet_UrlRequestParams_disable_cache_get(second)); + Cronet_UrlRequestParams_priority_set( + second, Cronet_UrlRequestParams_priority_get(first)); + EXPECT_EQ(Cronet_UrlRequestParams_priority_get(first), + Cronet_UrlRequestParams_priority_get(second)); + Cronet_UrlRequestParams_upload_data_provider_set( + second, Cronet_UrlRequestParams_upload_data_provider_get(first)); + EXPECT_EQ(Cronet_UrlRequestParams_upload_data_provider_get(first), + Cronet_UrlRequestParams_upload_data_provider_get(second)); + Cronet_UrlRequestParams_upload_data_provider_executor_set( + second, Cronet_UrlRequestParams_upload_data_provider_executor_get(first)); + EXPECT_EQ(Cronet_UrlRequestParams_upload_data_provider_executor_get(first), + Cronet_UrlRequestParams_upload_data_provider_executor_get(second)); + Cronet_UrlRequestParams_allow_direct_executor_set( + second, Cronet_UrlRequestParams_allow_direct_executor_get(first)); + EXPECT_EQ(Cronet_UrlRequestParams_allow_direct_executor_get(first), + Cronet_UrlRequestParams_allow_direct_executor_get(second)); + // TODO(mef): Test array |annotations|. + Cronet_UrlRequestParams_request_finished_listener_set( + second, Cronet_UrlRequestParams_request_finished_listener_get(first)); + EXPECT_EQ(Cronet_UrlRequestParams_request_finished_listener_get(first), + Cronet_UrlRequestParams_request_finished_listener_get(second)); + Cronet_UrlRequestParams_request_finished_executor_set( + second, Cronet_UrlRequestParams_request_finished_executor_get(first)); + EXPECT_EQ(Cronet_UrlRequestParams_request_finished_executor_get(first), + Cronet_UrlRequestParams_request_finished_executor_get(second)); + Cronet_UrlRequestParams_idempotency_set( + second, Cronet_UrlRequestParams_idempotency_get(first)); + EXPECT_EQ(Cronet_UrlRequestParams_idempotency_get(first), + Cronet_UrlRequestParams_idempotency_get(second)); + Cronet_UrlRequestParams_Destroy(first); + Cronet_UrlRequestParams_Destroy(second); +} + +// Test Struct Cronet_DateTime setters and getters. +TEST_F(CronetStructTest, TestCronet_DateTime) { + Cronet_DateTimePtr first = Cronet_DateTime_Create(); + Cronet_DateTimePtr second = Cronet_DateTime_Create(); + + // Copy values from |first| to |second|. + Cronet_DateTime_value_set(second, Cronet_DateTime_value_get(first)); + EXPECT_EQ(Cronet_DateTime_value_get(first), + Cronet_DateTime_value_get(second)); + Cronet_DateTime_Destroy(first); + Cronet_DateTime_Destroy(second); +} + +// Test Struct Cronet_Metrics setters and getters. +TEST_F(CronetStructTest, TestCronet_Metrics) { + Cronet_MetricsPtr first = Cronet_Metrics_Create(); + Cronet_MetricsPtr second = Cronet_Metrics_Create(); + + // Copy values from |first| to |second|. + Cronet_DateTimePtr test_request_start = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_request_start_get(first), nullptr); + + Cronet_Metrics_request_start_set(first, test_request_start); + EXPECT_NE(Cronet_Metrics_request_start_get(first), nullptr); + Cronet_Metrics_request_start_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_request_start_get(first), nullptr); + + Cronet_Metrics_request_start_move(first, test_request_start); + EXPECT_NE(Cronet_Metrics_request_start_get(first), nullptr); + Cronet_Metrics_request_start_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_request_start_get(first), nullptr); + + Cronet_DateTime_Destroy(test_request_start); + Cronet_DateTimePtr test_dns_start = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_dns_start_get(first), nullptr); + + Cronet_Metrics_dns_start_set(first, test_dns_start); + EXPECT_NE(Cronet_Metrics_dns_start_get(first), nullptr); + Cronet_Metrics_dns_start_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_dns_start_get(first), nullptr); + + Cronet_Metrics_dns_start_move(first, test_dns_start); + EXPECT_NE(Cronet_Metrics_dns_start_get(first), nullptr); + Cronet_Metrics_dns_start_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_dns_start_get(first), nullptr); + + Cronet_DateTime_Destroy(test_dns_start); + Cronet_DateTimePtr test_dns_end = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_dns_end_get(first), nullptr); + + Cronet_Metrics_dns_end_set(first, test_dns_end); + EXPECT_NE(Cronet_Metrics_dns_end_get(first), nullptr); + Cronet_Metrics_dns_end_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_dns_end_get(first), nullptr); + + Cronet_Metrics_dns_end_move(first, test_dns_end); + EXPECT_NE(Cronet_Metrics_dns_end_get(first), nullptr); + Cronet_Metrics_dns_end_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_dns_end_get(first), nullptr); + + Cronet_DateTime_Destroy(test_dns_end); + Cronet_DateTimePtr test_connect_start = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_connect_start_get(first), nullptr); + + Cronet_Metrics_connect_start_set(first, test_connect_start); + EXPECT_NE(Cronet_Metrics_connect_start_get(first), nullptr); + Cronet_Metrics_connect_start_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_connect_start_get(first), nullptr); + + Cronet_Metrics_connect_start_move(first, test_connect_start); + EXPECT_NE(Cronet_Metrics_connect_start_get(first), nullptr); + Cronet_Metrics_connect_start_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_connect_start_get(first), nullptr); + + Cronet_DateTime_Destroy(test_connect_start); + Cronet_DateTimePtr test_connect_end = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_connect_end_get(first), nullptr); + + Cronet_Metrics_connect_end_set(first, test_connect_end); + EXPECT_NE(Cronet_Metrics_connect_end_get(first), nullptr); + Cronet_Metrics_connect_end_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_connect_end_get(first), nullptr); + + Cronet_Metrics_connect_end_move(first, test_connect_end); + EXPECT_NE(Cronet_Metrics_connect_end_get(first), nullptr); + Cronet_Metrics_connect_end_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_connect_end_get(first), nullptr); + + Cronet_DateTime_Destroy(test_connect_end); + Cronet_DateTimePtr test_ssl_start = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_ssl_start_get(first), nullptr); + + Cronet_Metrics_ssl_start_set(first, test_ssl_start); + EXPECT_NE(Cronet_Metrics_ssl_start_get(first), nullptr); + Cronet_Metrics_ssl_start_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_ssl_start_get(first), nullptr); + + Cronet_Metrics_ssl_start_move(first, test_ssl_start); + EXPECT_NE(Cronet_Metrics_ssl_start_get(first), nullptr); + Cronet_Metrics_ssl_start_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_ssl_start_get(first), nullptr); + + Cronet_DateTime_Destroy(test_ssl_start); + Cronet_DateTimePtr test_ssl_end = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_ssl_end_get(first), nullptr); + + Cronet_Metrics_ssl_end_set(first, test_ssl_end); + EXPECT_NE(Cronet_Metrics_ssl_end_get(first), nullptr); + Cronet_Metrics_ssl_end_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_ssl_end_get(first), nullptr); + + Cronet_Metrics_ssl_end_move(first, test_ssl_end); + EXPECT_NE(Cronet_Metrics_ssl_end_get(first), nullptr); + Cronet_Metrics_ssl_end_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_ssl_end_get(first), nullptr); + + Cronet_DateTime_Destroy(test_ssl_end); + Cronet_DateTimePtr test_sending_start = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_sending_start_get(first), nullptr); + + Cronet_Metrics_sending_start_set(first, test_sending_start); + EXPECT_NE(Cronet_Metrics_sending_start_get(first), nullptr); + Cronet_Metrics_sending_start_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_sending_start_get(first), nullptr); + + Cronet_Metrics_sending_start_move(first, test_sending_start); + EXPECT_NE(Cronet_Metrics_sending_start_get(first), nullptr); + Cronet_Metrics_sending_start_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_sending_start_get(first), nullptr); + + Cronet_DateTime_Destroy(test_sending_start); + Cronet_DateTimePtr test_sending_end = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_sending_end_get(first), nullptr); + + Cronet_Metrics_sending_end_set(first, test_sending_end); + EXPECT_NE(Cronet_Metrics_sending_end_get(first), nullptr); + Cronet_Metrics_sending_end_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_sending_end_get(first), nullptr); + + Cronet_Metrics_sending_end_move(first, test_sending_end); + EXPECT_NE(Cronet_Metrics_sending_end_get(first), nullptr); + Cronet_Metrics_sending_end_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_sending_end_get(first), nullptr); + + Cronet_DateTime_Destroy(test_sending_end); + Cronet_DateTimePtr test_push_start = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_push_start_get(first), nullptr); + + Cronet_Metrics_push_start_set(first, test_push_start); + EXPECT_NE(Cronet_Metrics_push_start_get(first), nullptr); + Cronet_Metrics_push_start_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_push_start_get(first), nullptr); + + Cronet_Metrics_push_start_move(first, test_push_start); + EXPECT_NE(Cronet_Metrics_push_start_get(first), nullptr); + Cronet_Metrics_push_start_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_push_start_get(first), nullptr); + + Cronet_DateTime_Destroy(test_push_start); + Cronet_DateTimePtr test_push_end = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_push_end_get(first), nullptr); + + Cronet_Metrics_push_end_set(first, test_push_end); + EXPECT_NE(Cronet_Metrics_push_end_get(first), nullptr); + Cronet_Metrics_push_end_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_push_end_get(first), nullptr); + + Cronet_Metrics_push_end_move(first, test_push_end); + EXPECT_NE(Cronet_Metrics_push_end_get(first), nullptr); + Cronet_Metrics_push_end_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_push_end_get(first), nullptr); + + Cronet_DateTime_Destroy(test_push_end); + Cronet_DateTimePtr test_response_start = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_response_start_get(first), nullptr); + + Cronet_Metrics_response_start_set(first, test_response_start); + EXPECT_NE(Cronet_Metrics_response_start_get(first), nullptr); + Cronet_Metrics_response_start_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_response_start_get(first), nullptr); + + Cronet_Metrics_response_start_move(first, test_response_start); + EXPECT_NE(Cronet_Metrics_response_start_get(first), nullptr); + Cronet_Metrics_response_start_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_response_start_get(first), nullptr); + + Cronet_DateTime_Destroy(test_response_start); + Cronet_DateTimePtr test_request_end = Cronet_DateTime_Create(); + EXPECT_EQ(Cronet_Metrics_request_end_get(first), nullptr); + + Cronet_Metrics_request_end_set(first, test_request_end); + EXPECT_NE(Cronet_Metrics_request_end_get(first), nullptr); + Cronet_Metrics_request_end_set(first, nullptr); + EXPECT_EQ(Cronet_Metrics_request_end_get(first), nullptr); + + Cronet_Metrics_request_end_move(first, test_request_end); + EXPECT_NE(Cronet_Metrics_request_end_get(first), nullptr); + Cronet_Metrics_request_end_move(first, nullptr); + EXPECT_EQ(Cronet_Metrics_request_end_get(first), nullptr); + + Cronet_DateTime_Destroy(test_request_end); + Cronet_Metrics_socket_reused_set(second, + Cronet_Metrics_socket_reused_get(first)); + EXPECT_EQ(Cronet_Metrics_socket_reused_get(first), + Cronet_Metrics_socket_reused_get(second)); + Cronet_Metrics_sent_byte_count_set(second, + Cronet_Metrics_sent_byte_count_get(first)); + EXPECT_EQ(Cronet_Metrics_sent_byte_count_get(first), + Cronet_Metrics_sent_byte_count_get(second)); + Cronet_Metrics_received_byte_count_set( + second, Cronet_Metrics_received_byte_count_get(first)); + EXPECT_EQ(Cronet_Metrics_received_byte_count_get(first), + Cronet_Metrics_received_byte_count_get(second)); + Cronet_Metrics_Destroy(first); + Cronet_Metrics_Destroy(second); +} + +// Test Struct Cronet_RequestFinishedInfo setters and getters. +TEST_F(CronetStructTest, TestCronet_RequestFinishedInfo) { + Cronet_RequestFinishedInfoPtr first = Cronet_RequestFinishedInfo_Create(); + Cronet_RequestFinishedInfoPtr second = Cronet_RequestFinishedInfo_Create(); + + // Copy values from |first| to |second|. + Cronet_MetricsPtr test_metrics = Cronet_Metrics_Create(); + EXPECT_EQ(Cronet_RequestFinishedInfo_metrics_get(first), nullptr); + + Cronet_RequestFinishedInfo_metrics_set(first, test_metrics); + EXPECT_NE(Cronet_RequestFinishedInfo_metrics_get(first), nullptr); + Cronet_RequestFinishedInfo_metrics_set(first, nullptr); + EXPECT_EQ(Cronet_RequestFinishedInfo_metrics_get(first), nullptr); + + Cronet_RequestFinishedInfo_metrics_move(first, test_metrics); + EXPECT_NE(Cronet_RequestFinishedInfo_metrics_get(first), nullptr); + Cronet_RequestFinishedInfo_metrics_move(first, nullptr); + EXPECT_EQ(Cronet_RequestFinishedInfo_metrics_get(first), nullptr); + + Cronet_Metrics_Destroy(test_metrics); + // TODO(mef): Test array |annotations|. + Cronet_RequestFinishedInfo_finished_reason_set( + second, Cronet_RequestFinishedInfo_finished_reason_get(first)); + EXPECT_EQ(Cronet_RequestFinishedInfo_finished_reason_get(first), + Cronet_RequestFinishedInfo_finished_reason_get(second)); + Cronet_RequestFinishedInfo_Destroy(first); + Cronet_RequestFinishedInfo_Destroy(second); +} diff --git a/src/components/cronet/native/include/DEPS b/src/components/cronet/native/include/DEPS new file mode 100644 index 0000000000..0223c4a4e2 --- /dev/null +++ b/src/components/cronet/native/include/DEPS @@ -0,0 +1,8 @@ +# Files in this directory are copied externally and can't have any dependencies +include_rules = [ + # TODO(mef): There doesn't appear to be a way to specify that no includes + # are allowed, so currently we just don't allow a dependency on //base, which + # should disqualify most code. It would be nice to be able to actual prevent + # all dependencies in the future. + "-base", +] \ No newline at end of file diff --git a/src/components/cronet/native/include/cronet_c.h b/src/components/cronet/native/include/cronet_c.h new file mode 100644 index 0000000000..d9cd1115c2 --- /dev/null +++ b/src/components/cronet/native/include/cronet_c.h @@ -0,0 +1,38 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_CRONET_NATIVE_INCLUDE_CRONET_C_H_ +#define COMPONENTS_CRONET_NATIVE_INCLUDE_CRONET_C_H_ + +#include "cronet_export.h" + +// Cronet public C API is generated from cronet.idl +#include "cronet.idl_c.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Stream Engine used by Bidirectional Stream C API for GRPC. +typedef struct stream_engine stream_engine; + +// Additional Cronet C API not generated from cronet.idl. + +// Sets net::CertVerifier* raw_mock_cert_verifier for testing of Cronet_Engine. +// Must be called before Cronet_Engine_InitWithParams(). +CRONET_EXPORT void Cronet_Engine_SetMockCertVerifierForTesting( + Cronet_EnginePtr engine, + /* net::CertVerifier* */ void* raw_mock_cert_verifier); + +// Returns "stream_engine" interface for bidirectionsl stream support for GRPC. +// Returned stream engine is owned by Cronet Engine and is only valid until +// Cronet_Engine_Shutdown(). +CRONET_EXPORT stream_engine* Cronet_Engine_GetStreamEngine( + Cronet_EnginePtr engine); + +#ifdef __cplusplus +} +#endif + +#endif // COMPONENTS_CRONET_NATIVE_INCLUDE_CRONET_C_H_ diff --git a/src/components/cronet/native/include/cronet_export.h b/src/components/cronet/native/include/cronet_export.h new file mode 100644 index 0000000000..68379ae619 --- /dev/null +++ b/src/components/cronet/native/include/cronet_export.h @@ -0,0 +1,14 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_CRONET_NATIVE_INCLUDE_CRONET_EXPORT_H_ +#define COMPONENTS_CRONET_NATIVE_INCLUDE_CRONET_EXPORT_H_ + +#if defined(WIN32) +#define CRONET_EXPORT __declspec(dllexport) +#else +#define CRONET_EXPORT __attribute__((visibility("default"))) +#endif + +#endif // COMPONENTS_CRONET_NATIVE_INCLUDE_CRONET_EXPORT_H_ diff --git a/src/components/cronet/native/include/headers.gni b/src/components/cronet/native/include/headers.gni new file mode 100644 index 0000000000..506db3a8b6 --- /dev/null +++ b/src/components/cronet/native/include/headers.gni @@ -0,0 +1,5 @@ +cronet_native_public_headers = [ + "//components/cronet/native/generated/cronet.idl_c.h", + "//components/cronet/native/include/cronet_c.h", + "//components/cronet/native/include/cronet_export.h", +] diff --git a/src/components/cronet/native/io_buffer_with_cronet_buffer.cc b/src/components/cronet/native/io_buffer_with_cronet_buffer.cc new file mode 100644 index 0000000000..6a4c5951d4 --- /dev/null +++ b/src/components/cronet/native/io_buffer_with_cronet_buffer.cc @@ -0,0 +1,60 @@ +// Copyright 2018 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 "components/cronet/native/io_buffer_with_cronet_buffer.h" + +#include "base/no_destructor.h" +#include "components/cronet/native/generated/cronet.idl_impl_interface.h" + +namespace { +// Implementation of Cronet_BufferCallback that doesn't free the data as it +// is not owned by the buffer. +class Cronet_BufferCallbackUnowned : public Cronet_BufferCallback { + public: + Cronet_BufferCallbackUnowned() = default; + + Cronet_BufferCallbackUnowned(const Cronet_BufferCallbackUnowned&) = delete; + Cronet_BufferCallbackUnowned& operator=(const Cronet_BufferCallbackUnowned&) = + delete; + + ~Cronet_BufferCallbackUnowned() override = default; + + void OnDestroy(Cronet_BufferPtr buffer) override {} +}; + +} // namespace + +namespace cronet { + +IOBufferWithCronet_Buffer::IOBufferWithCronet_Buffer( + Cronet_BufferPtr cronet_buffer) + : net::WrappedIOBuffer( + reinterpret_cast(cronet_buffer->GetData())), + cronet_buffer_(cronet_buffer) {} + +IOBufferWithCronet_Buffer::~IOBufferWithCronet_Buffer() { + if (cronet_buffer_) { + Cronet_Buffer_Destroy(cronet_buffer_.release()); + } +} + +Cronet_BufferPtr IOBufferWithCronet_Buffer::Release() { + data_ = nullptr; + return cronet_buffer_.release(); +} + +Cronet_BufferWithIOBuffer::Cronet_BufferWithIOBuffer( + scoped_refptr io_buffer, + size_t io_buffer_len) + : io_buffer_(std::move(io_buffer)), + io_buffer_len_(io_buffer_len), + cronet_buffer_(Cronet_Buffer_Create()) { + static base::NoDestructor static_callback; + cronet_buffer_->InitWithDataAndCallback(io_buffer_->data(), io_buffer_len_, + static_callback.get()); +} + +Cronet_BufferWithIOBuffer::~Cronet_BufferWithIOBuffer() = default; + +} // namespace cronet diff --git a/src/components/cronet/native/io_buffer_with_cronet_buffer.h b/src/components/cronet/native/io_buffer_with_cronet_buffer.h new file mode 100644 index 0000000000..84d9ebeda8 --- /dev/null +++ b/src/components/cronet/native/io_buffer_with_cronet_buffer.h @@ -0,0 +1,69 @@ +// Copyright 2018 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_NATIVE_IO_BUFFER_WITH_CRONET_BUFFER_H_ +#define COMPONENTS_CRONET_NATIVE_IO_BUFFER_WITH_CRONET_BUFFER_H_ + +#include + +#include "components/cronet/native/generated/cronet.idl_c.h" +#include "net/base/io_buffer.h" + +namespace cronet { + +// net::WrappedIOBuffer subclass for a buffer owned by a Cronet_Buffer. +// Keeps the Cronet_Buffer alive until destroyed or released. +// Uses WrappedIOBuffer because data() is owned by the Cronet_Buffer. +class IOBufferWithCronet_Buffer : public net::WrappedIOBuffer { + public: + // Creates a buffer that takes ownership of the Cronet_Buffer. + explicit IOBufferWithCronet_Buffer(Cronet_BufferPtr cronet_buffer); + + IOBufferWithCronet_Buffer(const IOBufferWithCronet_Buffer&) = delete; + IOBufferWithCronet_Buffer& operator=(const IOBufferWithCronet_Buffer&) = + delete; + + // Releases ownership of |cronet_buffer_| and returns it to caller. + Cronet_BufferPtr Release(); + + private: + ~IOBufferWithCronet_Buffer() override; + + // Cronet buffer owned by |this|. + std::unique_ptr cronet_buffer_; +}; + +// Represents a Cronet_Buffer backed by a net::IOBuffer. Keeps both the +// net::IOBuffer and the Cronet_Buffer object alive until destroyed. +class Cronet_BufferWithIOBuffer { + public: + Cronet_BufferWithIOBuffer(scoped_refptr io_buffer, + size_t io_buffer_len); + + Cronet_BufferWithIOBuffer(const Cronet_BufferWithIOBuffer&) = delete; + Cronet_BufferWithIOBuffer& operator=(const Cronet_BufferWithIOBuffer&) = + delete; + + ~Cronet_BufferWithIOBuffer(); + + const net::IOBuffer* io_buffer() const { return io_buffer_.get(); } + size_t io_buffer_len() const { return io_buffer_len_; } + + // Returns pointer to Cronet buffer owned by |this|. + Cronet_BufferPtr cronet_buffer() { + CHECK(io_buffer_->HasAtLeastOneRef()); + return cronet_buffer_.get(); + } + + private: + scoped_refptr io_buffer_; + size_t io_buffer_len_; + + // Cronet buffer owned by |this|. + std::unique_ptr cronet_buffer_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_NATIVE_IO_BUFFER_WITH_CRONET_BUFFER_H_ diff --git a/src/components/cronet/native/native_metrics_util.cc b/src/components/cronet/native/native_metrics_util.cc new file mode 100644 index 0000000000..d4524e05ee --- /dev/null +++ b/src/components/cronet/native/native_metrics_util.cc @@ -0,0 +1,28 @@ +// Copyright 2019 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 "components/cronet/native/native_metrics_util.h" + +#include "components/cronet/metrics_util.h" + +namespace cronet { + +namespace native_metrics_util { + +void ConvertTime(const base::TimeTicks& ticks, + const base::TimeTicks& start_ticks, + const base::Time& start_time, + absl::optional* out) { + Cronet_DateTime date_time; + date_time.value = metrics_util::ConvertTime(ticks, start_ticks, start_time); + if (date_time.value == metrics_util::kNullTime) { + (*out).reset(); + return; + } + (*out).emplace(date_time); +} + +} // namespace native_metrics_util + +} // namespace cronet diff --git a/src/components/cronet/native/native_metrics_util.h b/src/components/cronet/native/native_metrics_util.h new file mode 100644 index 0000000000..e9fab6f484 --- /dev/null +++ b/src/components/cronet/native/native_metrics_util.h @@ -0,0 +1,51 @@ +// Copyright 2019 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_NATIVE_NATIVE_METRICS_UTIL_H_ +#define COMPONENTS_CRONET_NATIVE_NATIVE_METRICS_UTIL_H_ + +#include "base/time/time.h" +#include "components/cronet/native/generated/cronet.idl_impl_struct.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +namespace cronet { + +namespace native_metrics_util { + +// Converts timing metrics stored as TimeTicks into the format expected by the +// native layer: a absl::optional (which may be valueless if +// either |ticks| or |start_ticks| is null) -- this is returned via |out|. An +// out parameter is used because Cronet IDL structs like Cronet_DateTime aren't +// assignable. +// +// By calculating time values using a base (|start_ticks|, |start_time|) pair, +// time values are normalized. This allows time deltas between pairs of events +// to be accurately computed, even if the system clock changed between those +// events, as long as times for both events were calculated using the same +// (|start_ticks|, |start_time|) pair. +// +// Args: +// +// ticks: The ticks value corresponding to the time of the event -- the returned +// time corresponds to this event. +// +// start_ticks: Ticks measurement at some base time -- the ticks equivalent of +// start_time. Should be smaller than ticks. +// +// start_time: Time measurement at some base time -- the time equivalent of +// start_ticks. Must not be null. +// +// out: The output of the function -- the existing pointee object is mutated to +// either hold the new Cronet_DateTime or nothing (if either |ticks| or +// |start_ticks| is null). +void ConvertTime(const base::TimeTicks& ticks, + const base::TimeTicks& start_ticks, + const base::Time& start_time, + absl::optional* out); + +} // namespace native_metrics_util + +} // namespace cronet + +#endif // COMPONENTS_CRONET_NATIVE_NATIVE_METRICS_UTIL_H_ diff --git a/src/components/cronet/native/native_metrics_util_test.cc b/src/components/cronet/native/native_metrics_util_test.cc new file mode 100644 index 0000000000..b8abc7bd52 --- /dev/null +++ b/src/components/cronet/native/native_metrics_util_test.cc @@ -0,0 +1,76 @@ +// Copyright 2019 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 "components/cronet/native/native_metrics_util.h" + +#include "base/test/gtest_util.h" +#include "base/time/time.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace cronet { + +namespace native_metrics_util { + +namespace { + +TEST(NativeMetricsUtilTest, ConvertsTimes) { + constexpr auto start_delta = base::Milliseconds(20); + constexpr auto event_delta = base::Milliseconds(30); + + absl::optional converted; + ConvertTime(base::TimeTicks::UnixEpoch() + event_delta, + base::TimeTicks::UnixEpoch() + start_delta, + base::Time::UnixEpoch() + start_delta, &converted); + ASSERT_TRUE(converted.has_value()); + EXPECT_EQ(converted->value, 30); +} + +TEST(NativeMetricsUtilTest, OverwritesOldOutParam) { + constexpr auto start_delta = base::Milliseconds(20); + constexpr auto event_delta = base::Milliseconds(30); + + absl::optional converted; + converted.emplace(); + converted->value = 60; + ConvertTime(base::TimeTicks::UnixEpoch() + event_delta, + base::TimeTicks::UnixEpoch() + start_delta, + base::Time::UnixEpoch() + start_delta, &converted); + ASSERT_TRUE(converted.has_value()); + EXPECT_EQ(converted->value, 30); +} + +TEST(NativeMetricsUtilTest, NullTicks) { + constexpr auto start_delta = base::Milliseconds(20); + + absl::optional converted; + ConvertTime(base::TimeTicks(), base::TimeTicks::UnixEpoch() + start_delta, + base::Time::UnixEpoch() + start_delta, &converted); + ASSERT_FALSE(converted.has_value()); +} + +TEST(NativeMetricsUtilTest, NullStartTicks) { + constexpr auto start_delta = base::Milliseconds(20); + constexpr auto event_delta = base::Milliseconds(30); + + absl::optional converted; + ConvertTime(base::TimeTicks::UnixEpoch() + event_delta, base::TimeTicks(), + base::Time::UnixEpoch() + start_delta, &converted); + ASSERT_FALSE(converted.has_value()); +} + +TEST(NativeMetricsUtilTest, NullStartTime) { + constexpr auto start_delta = base::Milliseconds(20); + constexpr auto event_delta = base::Milliseconds(30); + + absl::optional converted; + EXPECT_DCHECK_DEATH(ConvertTime(base::TimeTicks::UnixEpoch() + event_delta, + base::TimeTicks::UnixEpoch() + start_delta, + base::Time(), &converted)); +} + +} // namespace + +} // namespace native_metrics_util + +} // namespace cronet diff --git a/src/components/cronet/native/perftest/main.cc b/src/components/cronet/native/perftest/main.cc new file mode 100644 index 0000000000..092163aaee --- /dev/null +++ b/src/components/cronet/native/perftest/main.cc @@ -0,0 +1,16 @@ +// Copyright 2018 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 "components/cronet/native/perftest/perf_test.h" + +#include + +#include "base/check_op.h" + +// When invoked, passes first and only argument to native performance test. +int main(int argc, char* argv[]) { + CHECK_EQ(argc, 2) << "Must include experimental options in JSON as only arg."; + PerfTest(argv[1]); + return 0; +} diff --git a/src/components/cronet/native/perftest/perf_test.cc b/src/components/cronet/native/perftest/perf_test.cc new file mode 100644 index 0000000000..1633280377 --- /dev/null +++ b/src/components/cronet/native/perftest/perf_test.cc @@ -0,0 +1,453 @@ +// Copyright 2018 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 + +#include "base/at_exit.h" +#include "base/atomic_sequence_num.h" +#include "base/check_op.h" +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" +#include "base/run_loop.h" +#include "base/strings/stringprintf.h" +#include "base/task/single_thread_task_runner.h" +#include "base/test/task_environment.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/values.h" +#include "components/cronet/native/test/test_upload_data_provider.h" +#include "components/cronet/native/test/test_url_request_callback.h" +#include "components/cronet/native/test/test_util.h" +#include "cronet_c.h" +#include "net/base/net_errors.h" +#include "net/cert/mock_cert_verifier.h" + +namespace { + +// Type of executor to use for a particular benchmark: +enum ExecutorType { + EXECUTOR_DIRECT, // Direct executor (on network thread). + EXECUTOR_THREAD, // Post to main thread. +}; + +// Upload or download benchmark. +enum Direction { + DIRECTION_UP, + DIRECTION_DOWN, +}; + +// Small or large benchmark payload. +enum Size { + SIZE_LARGE, + SIZE_SMALL, +}; + +// Protocol to benchmark. +enum Protocol { + PROTOCOL_HTTP, + PROTOCOL_QUIC, +}; + +// Dictionary of benchmark options. +std::unique_ptr g_options; + +// Return a string configuration option. +std::string GetConfigString(const char* key) { + std::string value; + CHECK(g_options->GetString(key, &value)) << "Cannot find key: " << key; + return value; +} + +// Return an int configuration option. +int GetConfigInt(const char* key) { + absl::optional config = g_options->FindIntKey(key); + CHECK(config) << "Cannot find key: " << key; + return *config; +} + +// Put together a benchmark configuration into a benchmark name. +// Make it fixed length for more readable tables. +// Benchmark names are written to the JSON output file and slurped up by +// Telemetry on the host. +std::string BuildBenchmarkName(ExecutorType executor, + Direction direction, + Protocol protocol, + int concurrency, + int iterations) { + std::string name = direction == DIRECTION_UP ? "Up___" : "Down_"; + switch (protocol) { + case PROTOCOL_HTTP: + name += "H_"; + break; + case PROTOCOL_QUIC: + name += "Q_"; + break; + } + name += std::to_string(iterations) + "_" + std::to_string(concurrency) + "_"; + switch (executor) { + case EXECUTOR_DIRECT: + name += "ExDir"; + break; + case EXECUTOR_THREAD: + name += "ExThr"; + break; + } + return name; +} + +// Cronet UploadDataProvider to use for benchmark. +class UploadDataProvider : public cronet::test::TestUploadDataProvider { + public: + // |length| indicates how many bytes to upload. + explicit UploadDataProvider(size_t length) + : TestUploadDataProvider(cronet::test::TestUploadDataProvider::SYNC, + nullptr), + length_(length), + remaining_(length) {} + + private: + int64_t GetLength() const override { return length_; } + + // Override of TestUploadDataProvider::Read() to simply report buffers filled. + void Read(Cronet_UploadDataSinkPtr upload_data_sink, + Cronet_BufferPtr buffer) override { + CHECK(remaining_ > 0); + size_t buffer_size = Cronet_Buffer_GetSize(buffer); + size_t sending = std::min(buffer_size, remaining_); + Cronet_UploadDataSink_OnReadSucceeded(upload_data_sink, sending, false); + remaining_ -= sending; + } + + const size_t length_; + // Count of bytes remaining to be uploaded. + size_t remaining_; +}; + +// Cronet UrlRequestCallback to use for benchmarking. +class Callback : public cronet::test::TestUrlRequestCallback { + public: + Callback() + : TestUrlRequestCallback(true), + task_runner_(base::ThreadTaskRunnerHandle::Get()) {} + ~Callback() override { Cronet_UrlRequestCallback_Destroy(callback_); } + + // Start one repeated UrlRequest. |iterations_completed| is used to keep track + // of how many requests have completed. Final iteration should Quit() + // |run_loop|. + void Start(size_t buffer_size, + int iterations, + int concurrency, + size_t length, + const std::string& url, + base::AtomicSequenceNumber* iterations_completed, + Cronet_EnginePtr engine, + ExecutorType executor, + Direction direction, + base::RunLoop* run_loop) { + iterations_ = iterations; + concurrency_ = concurrency; + length_ = length; + url_ = &url; + iterations_completed_ = iterations_completed; + engine_ = engine; + callback_ = CreateUrlRequestCallback(); + CHECK(!executor_); + switch (executor) { + case EXECUTOR_DIRECT: + // TestUrlRequestCallback(true) was called above, so parent will create + // a direct executor. + GetExecutor(); + break; + case EXECUTOR_THREAD: + // Create an executor that posts back to this thread. + executor_ = Cronet_Executor_CreateWith(Callback::Execute); + Cronet_Executor_SetClientContext(executor_, this); + break; + } + CHECK(executor_); + direction_ = direction; + buffer_size_ = buffer_size; + run_loop_ = run_loop; + StartRequest(); + } + + private: + // Create and start a UrlRequest. + void StartRequest() { + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = + Cronet_UrlRequestParams_Create(); + if (direction_ == DIRECTION_UP) { + // Create and set an UploadDataProvider on the UrlRequest. + upload_data_provider_ = std::make_unique(length_); + cronet_upload_data_provider_ = + upload_data_provider_->CreateUploadDataProvider(); + Cronet_UrlRequestParams_upload_data_provider_set( + request_params, cronet_upload_data_provider_); + // Set Content-Type header. + Cronet_HttpHeaderPtr header = Cronet_HttpHeader_Create(); + Cronet_HttpHeader_name_set(header, "Content-Type"); + Cronet_HttpHeader_value_set(header, "application/octet-stream"); + Cronet_UrlRequestParams_request_headers_add(request_params, header); + Cronet_HttpHeader_Destroy(header); + } + Cronet_UrlRequest_InitWithParams(request, engine_, url_->c_str(), + request_params, callback_, executor_); + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequest_Start(request); + } + + void OnResponseStarted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) override { + CHECK_EQ(200, Cronet_UrlResponseInfo_http_status_code_get(info)); + response_step_ = ON_RESPONSE_STARTED; + Cronet_BufferPtr buffer = Cronet_Buffer_Create(); + Cronet_Buffer_InitWithAlloc(buffer, buffer_size_); + StartNextRead(request, buffer); + } + + void OnSucceeded(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) override { + Cronet_UrlRequest_Destroy(request); + if (cronet_upload_data_provider_) + Cronet_UploadDataProvider_Destroy(cronet_upload_data_provider_); + + int iteration = iterations_completed_->GetNext(); + // If this was the final iteration, quit the RunLoop. + if (iteration == (iterations_ - 1)) + run_loop_->Quit(); + // Don't start another request if complete. + if (iteration >= (iterations_ - concurrency_)) + return; + // Start another request. + StartRequest(); + } + + void OnFailed(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error) override { + CHECK(false) << "Request failed with error code " + << Cronet_Error_error_code_get(error) << ", QUIC error code " + << Cronet_Error_quic_detailed_error_code_get(error) + << ", message " << Cronet_Error_message_get(error); + } + + // A simple executor that posts back to |task_runner_|. + static void Execute(Cronet_ExecutorPtr self, Cronet_RunnablePtr runnable) { + auto* callback = + static_cast(Cronet_Executor_GetClientContext(self)); + callback->task_runner_->PostTask( + FROM_HERE, cronet::test::RunnableWrapper::CreateOnceClosure(runnable)); + } + + Direction direction_; + int iterations_; + int concurrency_; + size_t length_; + raw_ptr url_; + raw_ptr iterations_completed_; + Cronet_EnginePtr engine_; + Cronet_UrlRequestCallbackPtr callback_; + Cronet_UploadDataProviderPtr cronet_upload_data_provider_ = nullptr; + scoped_refptr task_runner_; + raw_ptr run_loop_; + size_t buffer_size_; + std::unique_ptr upload_data_provider_; +}; + +// An individual benchmark instance. +class Benchmark { + public: + ~Benchmark() { Cronet_Engine_Destroy(engine_); } + + // Run and time the benchmark. + static void Run(ExecutorType executor, + Direction direction, + Size size, + Protocol protocol, + int concurrency, + base::DictionaryValue* results) { + std::string resource; + int iterations; + size_t length; + switch (size) { + case SIZE_SMALL: + resource = GetConfigString("SMALL_RESOURCE"); + iterations = GetConfigInt("SMALL_ITERATIONS"); + length = GetConfigInt("SMALL_RESOURCE_SIZE"); + break; + case SIZE_LARGE: + // When measuring a large upload, only download a small amount so + // download time isn't significant. + resource = GetConfigString( + direction == DIRECTION_UP ? "SMALL_RESOURCE" : "LARGE_RESOURCE"); + iterations = GetConfigInt("LARGE_ITERATIONS"); + length = GetConfigInt("LARGE_RESOURCE_SIZE"); + break; + } + std::string name = BuildBenchmarkName(executor, direction, protocol, + concurrency, iterations); + std::string scheme; + std::string host; + int port; + switch (protocol) { + case PROTOCOL_HTTP: + scheme = "http"; + host = GetConfigString("HOST_IP"); + port = GetConfigInt("HTTP_PORT"); + break; + case PROTOCOL_QUIC: + scheme = "https"; + host = GetConfigString("HOST"); + port = GetConfigInt("QUIC_PORT"); + break; + } + std::string url = + scheme + "://" + host + ":" + std::to_string(port) + "/" + resource; + size_t buffer_size = length > (size_t)GetConfigInt("MAX_BUFFER_SIZE") + ? GetConfigInt("MAX_BUFFER_SIZE") + : length; + Benchmark(executor, direction, size, protocol, concurrency, iterations, + length, buffer_size, name, url, host, port, results) + .RunInternal(); + } + + private: + Benchmark(ExecutorType executor, + Direction direction, + Size size, + Protocol protocol, + int concurrency, + int iterations, + size_t length, + size_t buffer_size, + const std::string& name, + const std::string& url, + const std::string& host, + int port, + base::DictionaryValue* results) + : iterations_(iterations), + concurrency_(concurrency), + length_(length), + buffer_size_(buffer_size), + name_(name), + url_(url), + callbacks_(concurrency), + executor_(executor), + direction_(direction), + results_(results) { + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + // Add Host Resolver Rules. + std::string host_resolver_rules = + "MAP test.example.com " + GetConfigString("HOST_IP") + ","; + Cronet_EngineParams_experimental_options_set( + engine_params, + base::StringPrintf( + "{ \"HostResolverRules\": { \"host_resolver_rules\" : \"%s\" } }", + host_resolver_rules.c_str()) + .c_str()); + // Create Cronet Engine. + engine_ = Cronet_Engine_Create(); + if (protocol == PROTOCOL_QUIC) { + Cronet_EngineParams_enable_quic_set(engine_params, true); + // Set QUIC hint. + Cronet_QuicHintPtr quic_hint = Cronet_QuicHint_Create(); + Cronet_QuicHint_host_set(quic_hint, host.c_str()); + Cronet_QuicHint_port_set(quic_hint, port); + Cronet_QuicHint_alternate_port_set(quic_hint, port); + Cronet_EngineParams_quic_hints_add(engine_params, quic_hint); + Cronet_QuicHint_Destroy(quic_hint); + // Set Mock Cert Verifier. + auto cert_verifier = std::make_unique(); + cert_verifier->set_default_result(net::OK); + Cronet_Engine_SetMockCertVerifierForTesting(engine_, + cert_verifier.release()); + } + + // Start Cronet Engine. + Cronet_Engine_StartWithParams(engine_, engine_params); + Cronet_EngineParams_Destroy(engine_params); + } + + // Run and time the benchmark. + void RunInternal() { + base::RunLoop run_loop; + base::TimeTicks start_time = base::TimeTicks::Now(); + // Start all concurrent requests. + for (auto& callback : callbacks_) { + callback.Start(buffer_size_, iterations_, concurrency_, length_, url_, + &iterations_completed_, engine_, executor_, direction_, + &run_loop); + } + run_loop.Run(); + base::TimeDelta run_time = base::TimeTicks::Now() - start_time; + results_->SetInteger(name_, static_cast(run_time.InMilliseconds())); + } + + scoped_refptr task_runner_; + const int iterations_; + const int concurrency_; + const size_t length_; + const size_t buffer_size_; + const std::string name_; + const std::string url_; + std::vector callbacks_; + base::AtomicSequenceNumber iterations_completed_; + Cronet_EnginePtr engine_; + const ExecutorType executor_; + const Direction direction_; + const raw_ptr results_; +}; + +} // namespace + +void PerfTest(const char* json_args) { + base::AtExitManager exit_manager; + + // Initialize the benchmark environment. See + // https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/threading_and_tasks_testing.md#full-fledged-base_test_taskenvironment + // for more details. + base::test::TaskEnvironment task_environment; + + // Parse benchmark options into |g_options|. + std::string benchmark_options = json_args; + std::unique_ptr options_value = + base::JSONReader::ReadDeprecated(benchmark_options); + CHECK(options_value) << "Parsing benchmark options failed: " + << benchmark_options; + g_options = base::DictionaryValue::From(std::move(options_value)); + CHECK(g_options) << "Benchmark options string is not a dictionary: " + << benchmark_options + << " See DEFAULT_BENCHMARK_CONFIG in perf_test_util.py."; + + // Run benchmarks putting timing results into |results|. + base::DictionaryValue results; + for (ExecutorType executor : {EXECUTOR_DIRECT, EXECUTOR_THREAD}) { + for (Direction direction : {DIRECTION_DOWN, DIRECTION_UP}) { + for (Protocol protocol : {PROTOCOL_HTTP, PROTOCOL_QUIC}) { + // Run large and small benchmarks one at a time to test single-threaded + // use. Also run them four at a time to see how they benefit from + // concurrency. The value four was chosen as many devices are now + // quad-core. + Benchmark::Run(executor, direction, SIZE_LARGE, protocol, 1, &results); + Benchmark::Run(executor, direction, SIZE_LARGE, protocol, 4, &results); + Benchmark::Run(executor, direction, SIZE_SMALL, protocol, 1, &results); + Benchmark::Run(executor, direction, SIZE_SMALL, protocol, 4, &results); + // Large benchmarks are generally bandwidth bound and unaffected by + // per-request overhead. Small benchmarks are not, so test at + // further increased concurrency to see if further benefit is possible. + Benchmark::Run(executor, direction, SIZE_SMALL, protocol, 8, &results); + } + } + } + + // Write |results| into results file. + std::string results_string; + base::JSONWriter::Write(results, &results_string); + FILE* results_file = fopen(GetConfigString("RESULTS_FILE").c_str(), "wb"); + fwrite(results_string.c_str(), results_string.length(), 1, results_file); + fclose(results_file); + fclose(fopen(GetConfigString("DONE_FILE").c_str(), "wb")); +} diff --git a/src/components/cronet/native/perftest/perf_test.h b/src/components/cronet/native/perftest/perf_test.h new file mode 100644 index 0000000000..c3ccb99bf9 --- /dev/null +++ b/src/components/cronet/native/perftest/perf_test.h @@ -0,0 +1,7 @@ +// Copyright 2018 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. + +// Run Cronet native performance test. |json_args| is the string containing +// the JSON formatted arguments from components/cronet/native/perftest/run.py. +void PerfTest(const char* json_args); diff --git a/src/components/cronet/native/perftest/run.py b/src/components/cronet/native/perftest/run.py new file mode 100755 index 0000000000..79f7849a50 --- /dev/null +++ b/src/components/cronet/native/perftest/run.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# Copyright 2018 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. + +"""This script runs an automated Cronet native performance benchmark. + +This script: +1. Starts HTTP and QUIC servers on the host machine. +2. Runs benchmark executable. + +Prerequisites: +1. quic_server and cronet_native_perf_test have been built for the host machine, + e.g. via: + gn gen out/Release --args="is_debug=false" + ninja -C out/Release quic_server cronet_native_perf_test +2. sudo apt-get install lighttpd + +Invocation: +./run.py + +Output: +Benchmark timings are output to /tmp/cronet_perf_test_results.txt + +""" + +import json +import os +import shutil +import sys +import tempfile + +REPOSITORY_ROOT = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', '..', '..')) + +sys.path.append(os.path.join(REPOSITORY_ROOT, 'build', 'android')) +import lighttpd_server # pylint: disable=wrong-import-position +sys.path.append(os.path.join(REPOSITORY_ROOT, 'components')) +from cronet.tools import perf_test_utils # pylint: disable=wrong-import-position + +def main(): + device = perf_test_utils.NativeDevice() + # Start HTTP server. + http_server_doc_root = perf_test_utils.GenerateHttpTestResources() + config_file = tempfile.NamedTemporaryFile() + http_server = lighttpd_server.LighttpdServer(http_server_doc_root, + port=perf_test_utils.HTTP_PORT, + base_config_path=config_file.name) + perf_test_utils.GenerateLighttpdConfig(config_file, http_server_doc_root, + http_server) + assert http_server.StartupHttpServer() + config_file.close() + # Start QUIC server. + quic_server_doc_root = perf_test_utils.GenerateQuicTestResources(device) + quic_server = perf_test_utils.QuicServer(quic_server_doc_root) + quic_server.StartupQuicServer(device) + # Run test + os.environ['LD_LIBRARY_PATH'] = perf_test_utils.BUILD_DIR + device.RunShellCommand( + [os.path.join(perf_test_utils.BUILD_DIR, 'cronet_native_perf_test'), + json.dumps(perf_test_utils.GetConfig(device))], + check_return=True) + # Shutdown. + quic_server.ShutdownQuicServer() + shutil.rmtree(quic_server_doc_root) + http_server.ShutdownHttpServer() + shutil.rmtree(http_server_doc_root) + + +if __name__ == '__main__': + main() diff --git a/src/components/cronet/native/runnables.cc b/src/components/cronet/native/runnables.cc new file mode 100644 index 0000000000..6d08d8415c --- /dev/null +++ b/src/components/cronet/native/runnables.cc @@ -0,0 +1,20 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/cronet/native/runnables.h" + +#include + +namespace cronet { + +OnceClosureRunnable::OnceClosureRunnable(base::OnceClosure task) + : task_(std::move(task)) {} + +OnceClosureRunnable::~OnceClosureRunnable() = default; + +void OnceClosureRunnable::Run() { + std::move(task_).Run(); +} + +} // namespace cronet diff --git a/src/components/cronet/native/runnables.h b/src/components/cronet/native/runnables.h new file mode 100644 index 0000000000..e57699c939 --- /dev/null +++ b/src/components/cronet/native/runnables.h @@ -0,0 +1,33 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_CRONET_NATIVE_RUNNABLES_H_ +#define COMPONENTS_CRONET_NATIVE_RUNNABLES_H_ + +#include "base/callback.h" +#include "components/cronet/native/generated/cronet.idl_impl_interface.h" + +namespace cronet { + +// Implementation of CronetRunnable that runs arbitrary base::OnceClosure. +// Runnable destroys itself after execution. +class OnceClosureRunnable : public Cronet_Runnable { + public: + explicit OnceClosureRunnable(base::OnceClosure task); + + OnceClosureRunnable(const OnceClosureRunnable&) = delete; + OnceClosureRunnable& operator=(const OnceClosureRunnable&) = delete; + + ~OnceClosureRunnable() override; + + void Run() override; + + private: + // Closure to run. + base::OnceClosure task_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_NATIVE_RUNNABLES_H_ diff --git a/src/components/cronet/native/runnables_unittest.cc b/src/components/cronet/native/runnables_unittest.cc new file mode 100644 index 0000000000..9c58cb883b --- /dev/null +++ b/src/components/cronet/native/runnables_unittest.cc @@ -0,0 +1,203 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/cronet/native/runnables.h" + +#include + +#include "base/bind.h" +#include "base/check.h" +#include "base/run_loop.h" +#include "base/test/task_environment.h" +#include "components/cronet/native/generated/cronet.idl_impl_interface.h" +#include "components/cronet/native/include/cronet_c.h" +#include "components/cronet/native/test/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class RunnablesTest : public ::testing::Test { + public: + RunnablesTest() = default; + + RunnablesTest(const RunnablesTest&) = delete; + RunnablesTest& operator=(const RunnablesTest&) = delete; + + ~RunnablesTest() override {} + + protected: + static void UrlRequestCallback_OnRedirectReceived( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String newLocationUrl); + static void UrlRequestCallback_OnResponseStarted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + static void UrlRequestCallback_OnReadCompleted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytesRead); + + bool callback_called() const { return callback_called_; } + + // Provide a message loop for use by TestExecutor instances. + base::test::SingleThreadTaskEnvironment task_environment_; + + private: + bool callback_called_ = false; +}; + +class OnRedirectReceived_Runnable : public Cronet_Runnable { + public: + OnRedirectReceived_Runnable(Cronet_UrlRequestCallbackPtr callback, + Cronet_String new_location_url) + : callback_(callback), new_location_url_(new_location_url) {} + + ~OnRedirectReceived_Runnable() override = default; + + void Run() override { + Cronet_UrlRequestCallback_OnRedirectReceived( + callback_, /* request = */ nullptr, /* info = */ nullptr, + new_location_url_.c_str()); + } + + private: + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback_; + // New location to redirect to. + std::string new_location_url_; +}; + +// Implementation of Cronet_UrlRequestCallback methods for testing. + +// static +void RunnablesTest::UrlRequestCallback_OnRedirectReceived( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String newLocationUrl) { + CHECK(self); + Cronet_ClientContext context = + Cronet_UrlRequestCallback_GetClientContext(self); + RunnablesTest* test = static_cast(context); + CHECK(test); + test->callback_called_ = true; + ASSERT_STREQ(newLocationUrl, "newUrl"); +} + +// static +void RunnablesTest::UrlRequestCallback_OnResponseStarted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + CHECK(self); + Cronet_ClientContext context = + Cronet_UrlRequestCallback_GetClientContext(self); + RunnablesTest* test = static_cast(context); + CHECK(test); + test->callback_called_ = true; +} + +// static +void RunnablesTest::UrlRequestCallback_OnReadCompleted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytesRead) { + CHECK(self); + CHECK(buffer); + // Destroy the |buffer|. + Cronet_Buffer_Destroy(buffer); + Cronet_ClientContext context = + Cronet_UrlRequestCallback_GetClientContext(self); + RunnablesTest* test = static_cast(context); + CHECK(test); + test->callback_called_ = true; +} + +// Example of posting application callback to the executor. +TEST_F(RunnablesTest, TestRunCallbackOnExecutor) { + // Executor provided by the application. + Cronet_ExecutorPtr executor = cronet::test::CreateTestExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = Cronet_UrlRequestCallback_CreateWith( + RunnablesTest::UrlRequestCallback_OnRedirectReceived, + /* OnResponseStartedFunc = */ nullptr, + /* OnReadCompletedFunc = */ nullptr, + /* OnSucceededFunc = */ nullptr, + /* OnFailedFunc = */ nullptr, + /* OnCanceledFunc = */ nullptr); + // New location to redirect to. + Cronet_String new_location_url = "newUrl"; + // Invoke Cronet_UrlRequestCallback_OnRedirectReceived + Cronet_RunnablePtr runnable = + new OnRedirectReceived_Runnable(callback, new_location_url); + new_location_url = "bad"; + Cronet_UrlRequestCallback_SetClientContext(callback, this); + Cronet_Executor_Execute(executor, runnable); + base::RunLoop().RunUntilIdle(); + ASSERT_TRUE(callback_called()); + Cronet_Executor_Destroy(executor); + Cronet_UrlRequestCallback_Destroy(callback); +} + +// Example of posting application callback to the executor using OneClosure. +TEST_F(RunnablesTest, TestRunOnceClosureOnExecutor) { + // Executor provided by the application. + Cronet_ExecutorPtr executor = cronet::test::CreateTestExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = Cronet_UrlRequestCallback_CreateWith( + RunnablesTest::UrlRequestCallback_OnRedirectReceived, + RunnablesTest::UrlRequestCallback_OnResponseStarted, + /* OnReadCompletedFunc = */ nullptr, + /* OnSucceededFunc = */ nullptr, + /* OnFailedFunc = */ nullptr, + /* OnCanceledFunc = */ nullptr); + // Invoke Cronet_UrlRequestCallback_OnResponseStarted using OnceClosure + Cronet_RunnablePtr runnable = new cronet::OnceClosureRunnable( + base::BindOnce(Cronet_UrlRequestCallback_OnResponseStarted, callback, + /* request = */ nullptr, /* info = */ nullptr)); + Cronet_UrlRequestCallback_SetClientContext(callback, this); + Cronet_Executor_Execute(executor, runnable); + base::RunLoop().RunUntilIdle(); + ASSERT_TRUE(callback_called()); + Cronet_Executor_Destroy(executor); + Cronet_UrlRequestCallback_Destroy(callback); +} + +// Example of posting application callback to the executor and passing +// Cronet_Buffer to it. +TEST_F(RunnablesTest, TestCronetBuffer) { + // Executor provided by the application. + Cronet_ExecutorPtr executor = cronet::test::CreateTestExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = Cronet_UrlRequestCallback_CreateWith( + RunnablesTest::UrlRequestCallback_OnRedirectReceived, + RunnablesTest::UrlRequestCallback_OnResponseStarted, + RunnablesTest::UrlRequestCallback_OnReadCompleted, + /* OnSucceededFunc = */ nullptr, + /* OnFailedFunc = */ nullptr, + /* OnCanceledFunc = */ nullptr); + // Create Cronet buffer and allocate buffer data. + Cronet_BufferPtr buffer = Cronet_Buffer_Create(); + Cronet_Buffer_InitWithAlloc(buffer, 20); + // Invoke Cronet_UrlRequestCallback_OnReadCompleted using OnceClosure. + Cronet_RunnablePtr runnable = new cronet::OnceClosureRunnable(base::BindOnce( + RunnablesTest::UrlRequestCallback_OnReadCompleted, callback, + /* request = */ nullptr, + /* info = */ nullptr, buffer, /* bytes_read = */ 0)); + Cronet_UrlRequestCallback_SetClientContext(callback, this); + Cronet_Executor_Execute(executor, runnable); + base::RunLoop().RunUntilIdle(); + ASSERT_TRUE(callback_called()); + Cronet_Executor_Destroy(executor); + Cronet_UrlRequestCallback_Destroy(callback); +} + +} // namespace diff --git a/src/components/cronet/native/sample/DEPS b/src/components/cronet/native/sample/DEPS new file mode 100644 index 0000000000..0223c4a4e2 --- /dev/null +++ b/src/components/cronet/native/sample/DEPS @@ -0,0 +1,8 @@ +# Files in this directory are copied externally and can't have any dependencies +include_rules = [ + # TODO(mef): There doesn't appear to be a way to specify that no includes + # are allowed, so currently we just don't allow a dependency on //base, which + # should disqualify most code. It would be nice to be able to actual prevent + # all dependencies in the future. + "-base", +] \ No newline at end of file diff --git a/src/components/cronet/native/sample/main.cc b/src/components/cronet/native/sample/main.cc new file mode 100644 index 0000000000..fb2272e713 --- /dev/null +++ b/src/components/cronet/native/sample/main.cc @@ -0,0 +1,59 @@ +// Copyright 2018 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 + +#include "cronet_c.h" +#include "sample_executor.h" +#include "sample_url_request_callback.h" + +Cronet_EnginePtr CreateCronetEngine() { + Cronet_EnginePtr cronet_engine = Cronet_Engine_Create(); + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_EngineParams_user_agent_set(engine_params, "CronetSample/1"); + Cronet_EngineParams_enable_quic_set(engine_params, true); + + Cronet_Engine_StartWithParams(cronet_engine, engine_params); + Cronet_EngineParams_Destroy(engine_params); + return cronet_engine; +} + +void PerformRequest(Cronet_EnginePtr cronet_engine, + const std::string& url, + Cronet_ExecutorPtr executor) { + SampleUrlRequestCallback url_request_callback; + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + Cronet_UrlRequestParams_http_method_set(request_params, "GET"); + + Cronet_UrlRequest_InitWithParams( + request, cronet_engine, url.c_str(), request_params, + url_request_callback.GetUrlRequestCallback(), executor); + Cronet_UrlRequestParams_Destroy(request_params); + + Cronet_UrlRequest_Start(request); + url_request_callback.WaitForDone(); + Cronet_UrlRequest_Destroy(request); + + std::cout << "Response Data:" << std::endl + << url_request_callback.response_as_string() << std::endl; +} + +// Download a resource from the Internet. Optional argument must specify +// a valid URL. +int main(int argc, const char* argv[]) { + std::cout << "Hello from Cronet!\n"; + Cronet_EnginePtr cronet_engine = CreateCronetEngine(); + std::cout << "Cronet version: " + << Cronet_Engine_GetVersionString(cronet_engine) << std::endl; + + std::string url(argc > 1 ? argv[1] : "https://www.example.com"); + std::cout << "URL: " << url << std::endl; + SampleExecutor executor; + PerformRequest(cronet_engine, url, executor.GetExecutor()); + + Cronet_Engine_Shutdown(cronet_engine); + Cronet_Engine_Destroy(cronet_engine); + return 0; +} diff --git a/src/components/cronet/native/sample/sample_executor.cc b/src/components/cronet/native/sample/sample_executor.cc new file mode 100644 index 0000000000..da9ab56fea --- /dev/null +++ b/src/components/cronet/native/sample/sample_executor.cc @@ -0,0 +1,93 @@ +// Copyright 2018 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 "sample_executor.h" + +SampleExecutor::SampleExecutor() + : executor_thread_(SampleExecutor::ThreadLoop, this), + executor_(Cronet_Executor_CreateWith(SampleExecutor::Execute)) { + Cronet_Executor_SetClientContext(executor_, this); +} + +SampleExecutor::~SampleExecutor() { + ShutdownExecutor(); + Cronet_Executor_Destroy(executor_); +} + +Cronet_ExecutorPtr SampleExecutor::GetExecutor() { + return executor_; +} + +void SampleExecutor::ShutdownExecutor() { + // Break tasks loop. + { + std::lock_guard lock(lock_); + stop_thread_loop_ = true; + } + task_available_.notify_one(); + // Wait for executor thread. + executor_thread_.join(); +} + +void SampleExecutor::RunTasksInQueue() { + // Process runnables in |task_queue_|. + while (true) { + Cronet_RunnablePtr runnable = nullptr; + { + // Wait for a task to run or stop signal. + std::unique_lock lock(lock_); + while (task_queue_.empty() && !stop_thread_loop_) + task_available_.wait(lock); + + if (stop_thread_loop_) + break; + + if (task_queue_.empty()) + continue; + + runnable = task_queue_.front(); + task_queue_.pop(); + } + Cronet_Runnable_Run(runnable); + Cronet_Runnable_Destroy(runnable); + } + // Delete remaining tasks. + std::queue tasks_to_destroy; + { + std::unique_lock lock(lock_); + tasks_to_destroy.swap(task_queue_); + } + while (!tasks_to_destroy.empty()) { + Cronet_Runnable_Destroy(tasks_to_destroy.front()); + tasks_to_destroy.pop(); + } +} + +/* static */ +void SampleExecutor::ThreadLoop(SampleExecutor* executor) { + executor->RunTasksInQueue(); +} + +void SampleExecutor::Execute(Cronet_RunnablePtr runnable) { + { + std::lock_guard lock(lock_); + if (!stop_thread_loop_) { + task_queue_.push(runnable); + runnable = nullptr; + } + } + if (runnable) { + Cronet_Runnable_Destroy(runnable); + } else { + task_available_.notify_one(); + } +} + +/* static */ +void SampleExecutor::Execute(Cronet_ExecutorPtr self, + Cronet_RunnablePtr runnable) { + auto* executor = + static_cast(Cronet_Executor_GetClientContext(self)); + executor->Execute(runnable); +} diff --git a/src/components/cronet/native/sample/sample_executor.h b/src/components/cronet/native/sample/sample_executor.h new file mode 100644 index 0000000000..f7108d7f0a --- /dev/null +++ b/src/components/cronet/native/sample/sample_executor.h @@ -0,0 +1,56 @@ +// Copyright 2018 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_NATIVE_SAMPLE_SAMPLE_EXECUTOR_H_ +#define COMPONENTS_CRONET_NATIVE_SAMPLE_SAMPLE_EXECUTOR_H_ + +// Cronet sample is expected to be used outside of Chromium infrastructure, +// and as such has to rely on STL directly instead of //base alternatives. +#include +#include +#include +#include + +#include "cronet_c.h" + +// Sample implementation of Cronet_Executor interface using static +// methods to map C API into instance of C++ class. +class SampleExecutor { + public: + SampleExecutor(); + ~SampleExecutor(); + + // Gets Cronet_ExecutorPtr implemented by |this|. + Cronet_ExecutorPtr GetExecutor(); + + // Shuts down the executor, so all pending tasks are destroyed without + // getting executed. + void ShutdownExecutor(); + + private: + // Runs tasks in |task_queue_| until |stop_thread_loop_| is set to true. + void RunTasksInQueue(); + static void ThreadLoop(SampleExecutor* executor); + + // Adds |runnable| to |task_queue_| to execute on |executor_thread_|. + void Execute(Cronet_RunnablePtr runnable); + // Implementation of Cronet_Executor methods. + static void Execute(Cronet_ExecutorPtr self, Cronet_RunnablePtr runnable); + + // Synchronise access to |task_queue_| and |stop_thread_loop_|; + std::mutex lock_; + // Tasks to run. + std::queue task_queue_; + // Notified if task is added to |task_queue_| or |stop_thread_loop_| is set. + std::condition_variable task_available_; + // Set to true to stop running tasks. + bool stop_thread_loop_ = false; + + // Thread on which tasks are executed. + std::thread executor_thread_; + + Cronet_ExecutorPtr const executor_; +}; + +#endif // COMPONENTS_CRONET_NATIVE_SAMPLE_SAMPLE_EXECUTOR_H_ diff --git a/src/components/cronet/native/sample/sample_url_request_callback.cc b/src/components/cronet/native/sample/sample_url_request_callback.cc new file mode 100644 index 0000000000..9ab5bd63fa --- /dev/null +++ b/src/components/cronet/native/sample/sample_url_request_callback.cc @@ -0,0 +1,138 @@ +// Copyright 2018 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 "sample_url_request_callback.h" + +#include + +SampleUrlRequestCallback::SampleUrlRequestCallback() + : callback_(Cronet_UrlRequestCallback_CreateWith( + SampleUrlRequestCallback::OnRedirectReceived, + SampleUrlRequestCallback::OnResponseStarted, + SampleUrlRequestCallback::OnReadCompleted, + SampleUrlRequestCallback::OnSucceeded, + SampleUrlRequestCallback::OnFailed, + SampleUrlRequestCallback::OnCanceled)) { + Cronet_UrlRequestCallback_SetClientContext(callback_, this); +} + +SampleUrlRequestCallback::~SampleUrlRequestCallback() { + Cronet_UrlRequestCallback_Destroy(callback_); +} + +Cronet_UrlRequestCallbackPtr SampleUrlRequestCallback::GetUrlRequestCallback() { + return callback_; +} + +void SampleUrlRequestCallback::OnRedirectReceived( + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String newLocationUrl) { + std::cout << "OnRedirectReceived called: " << newLocationUrl << std::endl; + Cronet_UrlRequest_FollowRedirect(request); +} + +void SampleUrlRequestCallback::OnResponseStarted( + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + std::cout << "OnResponseStarted called." << std::endl; + std::cout << "HTTP Status: " + << Cronet_UrlResponseInfo_http_status_code_get(info) << " " + << Cronet_UrlResponseInfo_http_status_text_get(info) << std::endl; + // Create and allocate 32kb buffer. + Cronet_BufferPtr buffer = Cronet_Buffer_Create(); + Cronet_Buffer_InitWithAlloc(buffer, 32 * 1024); + // Started reading the response. + Cronet_UrlRequest_Read(request, buffer); +} + +void SampleUrlRequestCallback::OnReadCompleted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytes_read) { + std::cout << "OnReadCompleted called: " << bytes_read << " bytes read." + << std::endl; + std::string last_read_data( + reinterpret_cast(Cronet_Buffer_GetData(buffer)), bytes_read); + response_as_string_ += last_read_data; + // Continue reading the response. + Cronet_UrlRequest_Read(request, buffer); +} + +void SampleUrlRequestCallback::OnSucceeded(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + std::cout << "OnSucceeded called." << std::endl; + SignalDone(true); +} + +void SampleUrlRequestCallback::OnFailed(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error) { + std::cout << "OnFailed called: " << Cronet_Error_message_get(error) + << std::endl; + last_error_message_ = Cronet_Error_message_get(error); + SignalDone(false); +} + +void SampleUrlRequestCallback::OnCanceled(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + std::cout << "OnCanceled called." << std::endl; + SignalDone(false); +} + +/* static */ +SampleUrlRequestCallback* SampleUrlRequestCallback::GetThis( + Cronet_UrlRequestCallbackPtr self) { + return static_cast( + Cronet_UrlRequestCallback_GetClientContext(self)); +} + +/* static */ +void SampleUrlRequestCallback::OnRedirectReceived( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String newLocationUrl) { + GetThis(self)->OnRedirectReceived(request, info, newLocationUrl); +} + +/* static */ +void SampleUrlRequestCallback::OnResponseStarted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + GetThis(self)->OnResponseStarted(request, info); +} + +/* static */ +void SampleUrlRequestCallback::OnReadCompleted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytesRead) { + GetThis(self)->OnReadCompleted(request, info, buffer, bytesRead); +} + +/* static */ +void SampleUrlRequestCallback::OnSucceeded(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + GetThis(self)->OnSucceeded(request, info); +} + +/* static */ +void SampleUrlRequestCallback::OnFailed(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error) { + GetThis(self)->OnFailed(request, info, error); +} + +/* static */ +void SampleUrlRequestCallback::OnCanceled(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + GetThis(self)->OnCanceled(request, info); +} diff --git a/src/components/cronet/native/sample/sample_url_request_callback.h b/src/components/cronet/native/sample/sample_url_request_callback.h new file mode 100644 index 0000000000..b4e7ac5c0a --- /dev/null +++ b/src/components/cronet/native/sample/sample_url_request_callback.h @@ -0,0 +1,102 @@ +// Copyright 2018 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_NATIVE_SAMPLE_SAMPLE_URL_REQUEST_CALLBACK_H_ +#define COMPONENTS_CRONET_NATIVE_SAMPLE_SAMPLE_URL_REQUEST_CALLBACK_H_ + +// Cronet sample is expected to be used outside of Chromium infrastructure, +// and as such has to rely on STL directly instead of //base alternatives. +#include +#include +#include +#include + +#include "cronet_c.h" + +// Sample implementation of Cronet_UrlRequestCallback interface using static +// methods to map C API into instance of C++ class. +class SampleUrlRequestCallback { + public: + SampleUrlRequestCallback(); + ~SampleUrlRequestCallback(); + + // Gets Cronet_UrlRequestCallbackPtr implemented by |this|. + Cronet_UrlRequestCallbackPtr GetUrlRequestCallback(); + + // Waits until request is done. + void WaitForDone() { is_done_.wait(); } + + // Returns error message if OnFailed callback is invoked. + std::string last_error_message() const { return last_error_message_; } + // Returns string representation of the received response. + std::string response_as_string() const { return response_as_string_; } + + protected: + void OnRedirectReceived(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String newLocationUrl); + + void OnResponseStarted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + + void OnReadCompleted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytes_read); + + void OnSucceeded(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + + void OnFailed(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error); + + void OnCanceled(Cronet_UrlRequestPtr request, Cronet_UrlResponseInfoPtr info); + + void SignalDone(bool success) { done_with_success_.set_value(success); } + + static SampleUrlRequestCallback* GetThis(Cronet_UrlRequestCallbackPtr self); + + // Implementation of Cronet_UrlRequestCallback methods. + static void OnRedirectReceived(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String newLocationUrl); + + static void OnResponseStarted(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + + static void OnReadCompleted(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytesRead); + + static void OnSucceeded(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + + static void OnFailed(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error); + + static void OnCanceled(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + + // Error message copied from |error| if OnFailed callback is invoked. + std::string last_error_message_; + // Accumulated string representation of the received response. + std::string response_as_string_; + // Promise that is set when request is done. + std::promise done_with_success_; + // Future that is signalled when request is done. + std::future is_done_ = done_with_success_.get_future(); + + Cronet_UrlRequestCallbackPtr const callback_; +}; + +#endif // COMPONENTS_CRONET_NATIVE_SAMPLE_SAMPLE_URL_REQUEST_CALLBACK_H_ diff --git a/src/components/cronet/native/sample/test/sample_test.cc b/src/components/cronet/native/sample/test/sample_test.cc new file mode 100644 index 0000000000..cbaee5934d --- /dev/null +++ b/src/components/cronet/native/sample/test/sample_test.cc @@ -0,0 +1,53 @@ +// Copyright 2018 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 +#include +#include +#include +#include + +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// Path to the test app used to locate sample app. +std::string s_test_app_path; + +// Returns directory name with trailing separator extracted from the file path. +std::string DirName(const std::string& file_path) { + size_t pos = file_path.find_last_of("\\/"); + if (std::string::npos == pos) + return std::string(); + return file_path.substr(0, pos + 1); +} + +// Runs |command_line| and returns string representation of its stdout. +std::string RunCommand(std::string command_line) { + std::string result_out = "command_result.tmp"; + EXPECT_EQ(0, std::system((command_line + " >" + result_out).c_str())); + std::stringstream result; + result << std::ifstream(result_out).rdbuf(); + std::remove(result_out.c_str()); + return result.str(); +} + +// Test that cronet_sample runs and gets connection refused from localhost. +TEST(SampleTest, TestConnectionRefused) { + // Expect "cronet_sample" app to be located in same directory as the test. + std::string cronet_sample_path = DirName(s_test_app_path) + "cronet_sample"; + std::string url = "http://localhost:99999"; + std::string sample_out = RunCommand(cronet_sample_path + " " + url); + + // Expect cronet sample to run and fail with net::ERR_INVALID_URL. + EXPECT_NE(std::string::npos, sample_out.find("net::ERR_INVALID_URL")); +} + +} // namespace + +int main(int argc, char** argv) { + s_test_app_path = argv[0]; + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/components/cronet/native/test/BUILD.gn b/src/components/cronet/native/test/BUILD.gn new file mode 100644 index 0000000000..75b3b59946 --- /dev/null +++ b/src/components/cronet/native/test/BUILD.gn @@ -0,0 +1,60 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//components/cronet/native/include/headers.gni") +import("//testing/test.gni") + +source_set("cronet_native_testutil") { + testonly = true + + deps = [ + "//base", + "//components/cronet/native:cronet_native_headers", + "//net:test_support", + ] + + configs += [ "//components/cronet/native:cronet_native_include_config" ] + public_configs = [ "//components/cronet/native:cronet_native_include_config" ] + + sources = [ + "test_util.cc", + "test_util.h", + ] +} + +# Tests for publicly exported Cronet Native API. This target does NOT depend on +# cronet_native_impl to prevent static linking of implementation into test app. +source_set("cronet_native_tests") { + testonly = true + + deps = [ + ":cronet_native_testutil", + "//base", + "//base/allocator:buildflags", + "//base/test:test_support", + "//components/cronet/native:cronet_native_headers", + "//components/cronet/testing:test_support", + "//components/grpc_support:bidirectional_stream_test", + "//components/grpc_support/test:get_stream_engine_header", + "//net:test_support", + "//testing/gmock", + "//testing/gtest", + ] + + configs += [ "//components/cronet/native:cronet_native_include_config" ] + + sources = [ + "buffer_test.cc", + "engine_test.cc", + "executors_test.cc", + "test_request_finished_info_listener.cc", + "test_request_finished_info_listener.h", + "test_stream_engine.cc", + "test_upload_data_provider.cc", + "test_upload_data_provider.h", + "test_url_request_callback.cc", + "test_url_request_callback.h", + "url_request_test.cc", + ] +} diff --git a/src/components/cronet/native/test/buffer_test.cc b/src/components/cronet/native/test/buffer_test.cc new file mode 100644 index 0000000000..6bbe9611b6 --- /dev/null +++ b/src/components/cronet/native/test/buffer_test.cc @@ -0,0 +1,148 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cronet_c.h" + +#include + +#include "base/allocator/buildflags.h" +#include "base/check.h" +#include "base/run_loop.h" +#include "base/test/task_environment.h" +#include "build/build_config.h" +#include "components/cronet/native/test/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class BufferTest : public ::testing::Test { + public: + BufferTest() = default; + + BufferTest(const BufferTest&) = delete; + BufferTest& operator=(const BufferTest&) = delete; + + ~BufferTest() override {} + + protected: + static void BufferCallback_OnDestroy(Cronet_BufferCallbackPtr self, + Cronet_BufferPtr buffer); + bool on_destroy_called() const { return on_destroy_called_; } + + // Provide a task environment for use by TestExecutor instances. Do not + // initialize the ThreadPool as this is done by the Cronet_Engine + base::test::SingleThreadTaskEnvironment task_environment_; + + private: + void set_on_destroy_called(bool value) { on_destroy_called_ = value; } + + bool on_destroy_called_ = false; +}; + +const uint64_t kTestBufferSize = 20; + +// static +void BufferTest::BufferCallback_OnDestroy(Cronet_BufferCallbackPtr self, + Cronet_BufferPtr buffer) { + CHECK(self); + Cronet_ClientContext context = Cronet_BufferCallback_GetClientContext(self); + BufferTest* test = static_cast(context); + CHECK(test); + test->set_on_destroy_called(true); + // Free buffer_data. + void* buffer_data = Cronet_Buffer_GetData(buffer); + CHECK(buffer_data); + free(buffer_data); +} + +// Test on_destroy that destroys the buffer set in context. +void TestRunnable_DestroyBuffer(Cronet_RunnablePtr self) { + CHECK(self); + Cronet_ClientContext context = Cronet_Runnable_GetClientContext(self); + Cronet_BufferPtr buffer = static_cast(context); + CHECK(buffer); + // Destroy buffer. TestCronet_BufferCallback_OnDestroy should be invoked. + Cronet_Buffer_Destroy(buffer); +} + +// Example of allocating buffer with reasonable size. +TEST_F(BufferTest, TestInitWithAlloc) { + // Create Cronet buffer and allocate buffer data. + Cronet_BufferPtr buffer = Cronet_Buffer_Create(); + Cronet_Buffer_InitWithAlloc(buffer, kTestBufferSize); + EXPECT_TRUE(Cronet_Buffer_GetData(buffer)); + EXPECT_EQ(Cronet_Buffer_GetSize(buffer), kTestBufferSize); + Cronet_Buffer_Destroy(buffer); + ASSERT_FALSE(on_destroy_called()); +} + +#if defined(ARCH_CPU_64_BITS) && \ + (defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(THREAD_SANITIZER) || BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) || \ + BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_FUCHSIA)) +// - ASAN and MSAN malloc by default triggers crash instead of returning null on +// failure. +// - PartitionAlloc malloc also crashes on allocation failure by design. +// - Fuchsia malloc() also crashes on allocation failure in some kernel builds. +// - On Linux and Chrome OS, the allocator shims crash for large allocations, on +// purpose. +#define MAYBE_TestInitWithHugeAllocFails DISABLED_TestInitWithHugeAllocFails +#else +#define MAYBE_TestInitWithHugeAllocFails TestInitWithHugeAllocFails +#endif +// Verify behaviour when an unsatisfiably huge buffer allocation is requested. +// On 32-bit platforms, we want to ensure that a 64-bit range allocation size +// is rejected, rather than resulting in a 32-bit truncated allocation. +// Some platforms over-commit allocations, so we request an allocation of the +// whole 64-bit address-space, which cannot possibly be satisfied in a 32- or +// 64-bit process. +TEST_F(BufferTest, MAYBE_TestInitWithHugeAllocFails) { + Cronet_BufferPtr buffer = Cronet_Buffer_Create(); + const uint64_t kHugeTestBufferSize = std::numeric_limits::max(); + Cronet_Buffer_InitWithAlloc(buffer, kHugeTestBufferSize); + EXPECT_FALSE(Cronet_Buffer_GetData(buffer)); + EXPECT_EQ(Cronet_Buffer_GetSize(buffer), 0ull); + Cronet_Buffer_Destroy(buffer); + ASSERT_FALSE(on_destroy_called()); +} + +// Example of initializing buffer with app-allocated data. +TEST_F(BufferTest, TestInitWithDataAndCallback) { + Cronet_BufferCallbackPtr buffer_callback = + Cronet_BufferCallback_CreateWith(BufferCallback_OnDestroy); + Cronet_BufferCallback_SetClientContext(buffer_callback, this); + // Create Cronet buffer and allocate buffer data. + Cronet_BufferPtr buffer = Cronet_Buffer_Create(); + Cronet_Buffer_InitWithDataAndCallback(buffer, malloc(kTestBufferSize), + kTestBufferSize, buffer_callback); + EXPECT_TRUE(Cronet_Buffer_GetData(buffer)); + EXPECT_EQ(Cronet_Buffer_GetSize(buffer), kTestBufferSize); + Cronet_Buffer_Destroy(buffer); + ASSERT_TRUE(on_destroy_called()); + Cronet_BufferCallback_Destroy(buffer_callback); +} + +// Example of posting application on_destroy to the executor and passing +// buffer to it, expecting buffer to be destroyed and freed. +TEST_F(BufferTest, TestCronetBufferAsync) { + // Executor provided by the application. + Cronet_ExecutorPtr executor = cronet::test::CreateTestExecutor(); + Cronet_BufferCallbackPtr buffer_callback = + Cronet_BufferCallback_CreateWith(BufferCallback_OnDestroy); + Cronet_BufferCallback_SetClientContext(buffer_callback, this); + // Create Cronet buffer and allocate buffer data. + Cronet_BufferPtr buffer = Cronet_Buffer_Create(); + Cronet_Buffer_InitWithDataAndCallback(buffer, malloc(kTestBufferSize), + kTestBufferSize, buffer_callback); + Cronet_RunnablePtr runnable = + Cronet_Runnable_CreateWith(TestRunnable_DestroyBuffer); + Cronet_Runnable_SetClientContext(runnable, buffer); + Cronet_Executor_Execute(executor, runnable); + base::RunLoop().RunUntilIdle(); + ASSERT_TRUE(on_destroy_called()); + Cronet_Executor_Destroy(executor); + Cronet_BufferCallback_Destroy(buffer_callback); +} + +} // namespace diff --git a/src/components/cronet/native/test/engine_test.cc b/src/components/cronet/native/test/engine_test.cc new file mode 100644 index 0000000000..9156b0a686 --- /dev/null +++ b/src/components/cronet/native/test/engine_test.cc @@ -0,0 +1,227 @@ +// Copyright 2018 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_c.h" + +#include "base/check.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "components/cronet/native/test/test_util.h" +#include "net/cert/mock_cert_verifier.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char* kUserAgent = "EngineTest/1"; + +class EngineTest : public ::testing::Test { + public: + EngineTest(const EngineTest&) = delete; + EngineTest& operator=(const EngineTest&) = delete; + + protected: + EngineTest() = default; + ~EngineTest() override {} +}; + +TEST_F(EngineTest, StartCronetEngine) { + Cronet_EnginePtr engine = Cronet_Engine_Create(); + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_EngineParams_user_agent_set(engine_params, kUserAgent); + EXPECT_EQ(Cronet_RESULT_SUCCESS, + Cronet_Engine_StartWithParams(engine, engine_params)); + Cronet_Engine_Destroy(engine); + Cronet_EngineParams_Destroy(engine_params); +} + +TEST_F(EngineTest, CronetEngineDefaultUserAgent) { + Cronet_EnginePtr engine = Cronet_Engine_Create(); + // Version and DefaultUserAgent don't require engine start. + std::string version = Cronet_Engine_GetVersionString(engine); + std::string default_agent = Cronet_Engine_GetDefaultUserAgent(engine); + EXPECT_NE(default_agent.find(version), std::string::npos); + Cronet_Engine_Destroy(engine); +} + +TEST_F(EngineTest, InitDifferentEngines) { + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_EnginePtr first_engine = Cronet_Engine_Create(); + Cronet_Engine_StartWithParams(first_engine, engine_params); + Cronet_EnginePtr second_engine = Cronet_Engine_Create(); + Cronet_Engine_StartWithParams(second_engine, engine_params); + Cronet_EnginePtr third_engine = Cronet_Engine_Create(); + Cronet_Engine_StartWithParams(third_engine, engine_params); + Cronet_EngineParams_Destroy(engine_params); + Cronet_Engine_Destroy(first_engine); + Cronet_Engine_Destroy(second_engine); + Cronet_Engine_Destroy(third_engine); +} + +TEST_F(EngineTest, StartResults) { + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_EnginePtr engine = Cronet_Engine_Create(); + // Disable runtime CHECK of the result, so it could be verified. + Cronet_EngineParams_enable_check_result_set(engine_params, false); + Cronet_EngineParams_http_cache_mode_set( + engine_params, Cronet_EngineParams_HTTP_CACHE_MODE_DISK); + EXPECT_EQ(Cronet_RESULT_ILLEGAL_ARGUMENT_STORAGE_PATH_MUST_EXIST, + Cronet_Engine_StartWithParams(engine, engine_params)); + Cronet_EngineParams_storage_path_set(engine_params, "InvalidPath"); + EXPECT_EQ(Cronet_RESULT_ILLEGAL_ARGUMENT_STORAGE_PATH_MUST_EXIST, + Cronet_Engine_StartWithParams(engine, engine_params)); + base::ScopedTempDir temp_dir; + EXPECT_TRUE(temp_dir.CreateUniqueTempDir()); + base::FilePath temp_path = base::MakeAbsoluteFilePath(temp_dir.GetPath()); + Cronet_EngineParams_storage_path_set(engine_params, + temp_path.AsUTF8Unsafe().c_str()); + // Now the engine should start successfully. + EXPECT_EQ(Cronet_RESULT_SUCCESS, + Cronet_Engine_StartWithParams(engine, engine_params)); + // The second start should fail. + EXPECT_EQ(Cronet_RESULT_ILLEGAL_STATE_ENGINE_ALREADY_STARTED, + Cronet_Engine_StartWithParams(engine, engine_params)); + // The second engine should fail because storage path is already used. + Cronet_EnginePtr second_engine = Cronet_Engine_Create(); + EXPECT_EQ(Cronet_RESULT_ILLEGAL_STATE_STORAGE_PATH_IN_USE, + Cronet_Engine_StartWithParams(second_engine, engine_params)); + // Shutdown first engine to free storage path. + EXPECT_EQ(Cronet_RESULT_SUCCESS, Cronet_Engine_Shutdown(engine)); + // Now the second engine should start. + EXPECT_EQ(Cronet_RESULT_SUCCESS, + Cronet_Engine_StartWithParams(second_engine, engine_params)); + Cronet_Engine_Destroy(second_engine); + Cronet_Engine_Destroy(engine); + Cronet_EngineParams_Destroy(engine_params); +} + +TEST_F(EngineTest, InvalidPkpParams) { + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_EnginePtr engine = Cronet_Engine_Create(); + // Disable runtime CHECK of the result, so it could be verified. + Cronet_EngineParams_enable_check_result_set(engine_params, false); + // Try adding invalid public key pins. + Cronet_PublicKeyPinsPtr public_key_pins = Cronet_PublicKeyPins_Create(); + Cronet_EngineParams_public_key_pins_add(engine_params, public_key_pins); + EXPECT_EQ(Cronet_RESULT_NULL_POINTER_HOSTNAME, + Cronet_Engine_StartWithParams(engine, engine_params)); + Cronet_EngineParams_public_key_pins_clear(engine_params); + // Detect long host name. + Cronet_PublicKeyPins_host_set(public_key_pins, std::string(256, 'a').c_str()); + Cronet_EngineParams_public_key_pins_add(engine_params, public_key_pins); + EXPECT_EQ(Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_HOSTNAME, + Cronet_Engine_StartWithParams(engine, engine_params)); + Cronet_EngineParams_public_key_pins_clear(engine_params); + // Detect invalid host name. + Cronet_PublicKeyPins_host_set(public_key_pins, "invalid:host/name"); + Cronet_EngineParams_public_key_pins_add(engine_params, public_key_pins); + EXPECT_EQ(Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_HOSTNAME, + Cronet_Engine_StartWithParams(engine, engine_params)); + Cronet_EngineParams_public_key_pins_clear(engine_params); + // Set valid host name. + Cronet_PublicKeyPins_host_set(public_key_pins, "valid.host.name"); + Cronet_EngineParams_public_key_pins_add(engine_params, public_key_pins); + // Detect missing pins. + EXPECT_EQ(Cronet_RESULT_NULL_POINTER_SHA256_PINS, + Cronet_Engine_StartWithParams(engine, engine_params)); + // Detect invalid pin. + Cronet_EngineParams_public_key_pins_clear(engine_params); + Cronet_PublicKeyPins_pins_sha256_add(public_key_pins, "invalid_sha256"); + Cronet_EngineParams_public_key_pins_add(engine_params, public_key_pins); + EXPECT_EQ(Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_PIN, + Cronet_Engine_StartWithParams(engine, engine_params)); + // THe engine cannot start with these params, and have to be destroyed. + Cronet_Engine_Destroy(engine); + Cronet_EngineParams_Destroy(engine_params); + Cronet_PublicKeyPins_Destroy(public_key_pins); +} + +TEST_F(EngineTest, ValidPkpParams) { + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_EnginePtr engine = Cronet_Engine_Create(); + // Disable runtime CHECK of the result, so it could be verified. + Cronet_EngineParams_enable_check_result_set(engine_params, false); + // Add valid public key pins. + Cronet_PublicKeyPinsPtr public_key_pins = Cronet_PublicKeyPins_Create(); + Cronet_PublicKeyPins_host_set(public_key_pins, "valid.host.name"); + Cronet_PublicKeyPins_pins_sha256_add( + public_key_pins, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); + Cronet_EngineParams_public_key_pins_add(engine_params, public_key_pins); + // The engine should start successfully. + EXPECT_EQ(Cronet_RESULT_SUCCESS, + Cronet_Engine_StartWithParams(engine, engine_params)); + Cronet_Engine_Destroy(engine); + Cronet_EngineParams_Destroy(engine_params); + Cronet_PublicKeyPins_Destroy(public_key_pins); +} + +// Verify that Cronet_Engine_SetMockCertVerifierForTesting() doesn't crash or +// leak anything. +TEST_F(EngineTest, SetMockCertVerifierForTesting) { + auto cert_verifier(std::make_unique()); + Cronet_EnginePtr engine = Cronet_Engine_Create(); + Cronet_Engine_SetMockCertVerifierForTesting(engine, cert_verifier.release()); + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_Engine_StartWithParams(engine, engine_params); + Cronet_Engine_Destroy(engine); + Cronet_EngineParams_Destroy(engine_params); +} + +TEST_F(EngineTest, StartNetLogToFile) { + base::ScopedTempDir temp_dir; + EXPECT_TRUE(temp_dir.CreateUniqueTempDir()); + base::FilePath temp_path = base::MakeAbsoluteFilePath(temp_dir.GetPath()); + base::FilePath net_log_file = + temp_path.Append(FILE_PATH_LITERAL("netlog.json")); + + Cronet_EnginePtr engine = Cronet_Engine_Create(); + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_EngineParams_experimental_options_set( + engine_params, + "{ \"QUIC\" : {\"max_server_configs_stored_in_properties\" : 8} }"); + // Test that net log cannot start/stop before engine start. + EXPECT_FALSE(Cronet_Engine_StartNetLogToFile( + engine, net_log_file.AsUTF8Unsafe().c_str(), true)); + Cronet_Engine_StopNetLog(engine); + + // Start the engine. + Cronet_Engine_StartWithParams(engine, engine_params); + Cronet_EngineParams_Destroy(engine_params); + + // Test that normal start/stop net log works. + EXPECT_TRUE(Cronet_Engine_StartNetLogToFile( + engine, net_log_file.AsUTF8Unsafe().c_str(), true)); + Cronet_Engine_StopNetLog(engine); + + // Test that double start/stop net log works. + EXPECT_TRUE(Cronet_Engine_StartNetLogToFile( + engine, net_log_file.AsUTF8Unsafe().c_str(), true)); + // Test that second start fails. + EXPECT_FALSE(Cronet_Engine_StartNetLogToFile( + engine, net_log_file.AsUTF8Unsafe().c_str(), true)); + // Test that multiple stops work. + Cronet_Engine_StopNetLog(engine); + Cronet_Engine_StopNetLog(engine); + Cronet_Engine_StopNetLog(engine); + + // Test that net log contains effective experimental options. + std::string net_log; + EXPECT_TRUE(base::ReadFileToString(net_log_file, &net_log)); + EXPECT_TRUE( + net_log.find( + "{\"QUIC\":{\"max_server_configs_stored_in_properties\":8}") != + std::string::npos); + + // Test that bad file name fails. + EXPECT_FALSE(Cronet_Engine_StartNetLogToFile(engine, "bad/file/name", true)); + + Cronet_Engine_Shutdown(engine); + // Test that net log cannot start/stop after engine shutdown. + EXPECT_FALSE(Cronet_Engine_StartNetLogToFile( + engine, net_log_file.AsUTF8Unsafe().c_str(), true)); + Cronet_Engine_StopNetLog(engine); + Cronet_Engine_Destroy(engine); +} + +} // namespace diff --git a/src/components/cronet/native/test/executors_test.cc b/src/components/cronet/native/test/executors_test.cc new file mode 100644 index 0000000000..504480ea5f --- /dev/null +++ b/src/components/cronet/native/test/executors_test.cc @@ -0,0 +1,81 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cronet_c.h" + +#include "base/check.h" +#include "base/run_loop.h" +#include "base/test/task_environment.h" +#include "components/cronet/native/test/test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class ExecutorsTest : public ::testing::Test { + public: + ExecutorsTest() = default; + + ExecutorsTest(const ExecutorsTest&) = delete; + ExecutorsTest& operator=(const ExecutorsTest&) = delete; + + ~ExecutorsTest() override = default; + + protected: + static void TestRunnable_Run(Cronet_RunnablePtr self); + bool runnable_called() const { return runnable_called_; } + + // Provide a task environment for use by TestExecutor instances. Do not + // initialize the ThreadPool as this is done by the Cronet_Engine + base::test::SingleThreadTaskEnvironment task_environment_; + + private: + void set_runnable_called(bool value) { runnable_called_ = value; } + + bool runnable_called_ = false; +}; + +// App implementation of Cronet_Executor methods. +void TestExecutor_Execute(Cronet_ExecutorPtr self, Cronet_RunnablePtr command) { + CHECK(self); + Cronet_Runnable_Run(command); + Cronet_Runnable_Destroy(command); +} + +// Implementation of TestRunnable methods. +// static +void ExecutorsTest::TestRunnable_Run(Cronet_RunnablePtr self) { + CHECK(self); + Cronet_ClientContext context = Cronet_Runnable_GetClientContext(self); + ExecutorsTest* test = static_cast(context); + CHECK(test); + test->set_runnable_called(true); +} + +// Test that custom Executor defined by the app runs the runnable. +TEST_F(ExecutorsTest, TestCustom) { + ASSERT_FALSE(runnable_called()); + Cronet_RunnablePtr runnable = + Cronet_Runnable_CreateWith(ExecutorsTest::TestRunnable_Run); + Cronet_Runnable_SetClientContext(runnable, this); + Cronet_ExecutorPtr executor = + Cronet_Executor_CreateWith(TestExecutor_Execute); + Cronet_Executor_Execute(executor, runnable); + Cronet_Executor_Destroy(executor); + base::RunLoop().RunUntilIdle(); + ASSERT_TRUE(runnable_called()); +} + +// Test that cronet::test::TestExecutor runs the runnable. +TEST_F(ExecutorsTest, TestTestExecutor) { + ASSERT_FALSE(runnable_called()); + Cronet_RunnablePtr runnable = Cronet_Runnable_CreateWith(TestRunnable_Run); + Cronet_Runnable_SetClientContext(runnable, this); + Cronet_ExecutorPtr executor = cronet::test::CreateTestExecutor(); + Cronet_Executor_Execute(executor, runnable); + Cronet_Executor_Destroy(executor); + base::RunLoop().RunUntilIdle(); + ASSERT_TRUE(runnable_called()); +} + +} // namespace diff --git a/src/components/cronet/native/test/test_request_finished_info_listener.cc b/src/components/cronet/native/test/test_request_finished_info_listener.cc new file mode 100644 index 0000000000..d1e79f66fa --- /dev/null +++ b/src/components/cronet/native/test/test_request_finished_info_listener.cc @@ -0,0 +1,54 @@ +// Copyright 2019 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 "components/cronet/native/test/test_request_finished_info_listener.h" + +#include "base/check.h" + +namespace cronet { +namespace test { + +Cronet_RequestFinishedInfoListenerPtr +TestRequestFinishedInfoListener::CreateRequestFinishedListener() { + auto* listener = Cronet_RequestFinishedInfoListener_CreateWith( + TestRequestFinishedInfoListener::OnRequestFinished); + Cronet_RequestFinishedInfoListener_SetClientContext(listener, this); + return listener; +} + +void TestRequestFinishedInfoListener::WaitForDone() { + done_.Wait(); +} + +/* static */ +TestRequestFinishedInfoListener* TestRequestFinishedInfoListener::GetThis( + Cronet_RequestFinishedInfoListenerPtr self) { + CHECK(self); + return static_cast( + Cronet_RequestFinishedInfoListener_GetClientContext(self)); +} + +/* static */ +void TestRequestFinishedInfoListener::OnRequestFinished( + Cronet_RequestFinishedInfoListenerPtr self, + Cronet_RequestFinishedInfoPtr request_finished_info, + Cronet_UrlResponseInfoPtr url_response_info, + Cronet_ErrorPtr error) { + GetThis(self)->OnRequestFinished(request_finished_info, url_response_info, + error); + Cronet_RequestFinishedInfoListener_Destroy(self); +} + +void TestRequestFinishedInfoListener::OnRequestFinished( + Cronet_RequestFinishedInfoPtr request_finished_info, + Cronet_UrlResponseInfoPtr url_response_info, + Cronet_ErrorPtr error) { + request_finished_info_ = request_finished_info; + url_response_info_ = url_response_info; + error_ = error; + done_.Signal(); +} + +} // namespace test +} // namespace cronet diff --git a/src/components/cronet/native/test/test_request_finished_info_listener.h b/src/components/cronet/native/test/test_request_finished_info_listener.h new file mode 100644 index 0000000000..2acd58730c --- /dev/null +++ b/src/components/cronet/native/test/test_request_finished_info_listener.h @@ -0,0 +1,85 @@ +// Copyright 2019 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 "base/check.h" +#include "base/synchronization/waitable_event.h" +#include "cronet_c.h" + +#ifndef COMPONENTS_CRONET_NATIVE_TEST_TEST_REQUEST_FINISHED_INFO_LISTENER_H_ +#define COMPONENTS_CRONET_NATIVE_TEST_TEST_REQUEST_FINISHED_INFO_LISTENER_H_ + +namespace cronet { +namespace test { + +// A RequestFinishedInfoListener implementation that allows waiting for and +// accessing callback results from tests. +// +// Note that the RequestFinishedInfo for a request is shared-owned by its +// UrlRequest and the code calling the listeners. +class TestRequestFinishedInfoListener { + public: + // Create a listener that can be registered with Cronet. + // + // The listener deletes itself when OnRequestFinished() is run. + Cronet_RequestFinishedInfoListenerPtr CreateRequestFinishedListener(); + + // Wait until a listener created with CreateRequestFinishedListener() runs + // OnRequestFinished(). + void WaitForDone(); + + Cronet_RequestFinishedInfoPtr request_finished_info() { + CHECK(done_.IsSignaled()); + return request_finished_info_; + } + + Cronet_UrlResponseInfoPtr url_response_info() { + CHECK(done_.IsSignaled()); + return url_response_info_; + } + + Cronet_ErrorPtr error() { + CHECK(done_.IsSignaled()); + return error_; + } + + private: + static TestRequestFinishedInfoListener* GetThis( + Cronet_RequestFinishedInfoListenerPtr self); + + // Implementation of Cronet_RequestFinishedInfoListener methods. + static void OnRequestFinished( + Cronet_RequestFinishedInfoListenerPtr self, + Cronet_RequestFinishedInfoPtr request_finished_info, + Cronet_UrlResponseInfoPtr url_response_info, + Cronet_ErrorPtr error); + + virtual void OnRequestFinished( + Cronet_RequestFinishedInfoPtr request_finished_info, + Cronet_UrlResponseInfoPtr url_response_info, + Cronet_ErrorPtr error); + + // RequestFinishedInfo from the request -- will be set when the listener is + // called, which only happens if there are metrics to report. Won't be + // destroyed if the UrlRequest object hasn't been destroyed. + Cronet_RequestFinishedInfoPtr request_finished_info_ = nullptr; + + // UrlResponseInfo from the request -- will be set when the listener is + // called, which only happens if there are metrics to report. Won't be + // destroyed if the UrlRequest object hasn't been destroyed. + Cronet_UrlResponseInfoPtr url_response_info_ = nullptr; + + // Error from the request -- will be will be set when the listener is called, + // which only happens if there are metrics to report. Won't be destroyed if + // the UrlRequest object hasn't been destroyed. + Cronet_ErrorPtr error_ = nullptr; + + // Signaled by OnRequestFinished() on a listener created by + // CreateRequestFinishedListener(). + base::WaitableEvent done_; +}; + +} // namespace test +} // namespace cronet + +#endif // COMPONENTS_CRONET_NATIVE_TEST_TEST_REQUEST_FINISHED_INFO_LISTENER_H_ diff --git a/src/components/cronet/native/test/test_stream_engine.cc b/src/components/cronet/native/test/test_stream_engine.cc new file mode 100644 index 0000000000..f694d4d61d --- /dev/null +++ b/src/components/cronet/native/test/test_stream_engine.cc @@ -0,0 +1,49 @@ +// Copyright 2018 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_c.h" + +#include "base/check_op.h" +#include "components/cronet/native/test/test_util.h" +#include "components/grpc_support/test/get_stream_engine.h" + +namespace grpc_support { + +// Provides stream_engine support for testing of bidirectional_stream C API for +// GRPC using native Cronet_Engine. + +Cronet_EnginePtr g_cronet_engine = nullptr; +int quic_server_port = 0; + +// Returns a stream_engine* for testing with the QuicTestServer. +// The engine returned resolve "test.example.com" as localhost:|port|, +// and should have "test.example.com" configured as a QUIC server. +stream_engine* GetTestStreamEngine(int port) { + CHECK(g_cronet_engine); + CHECK_EQ(port, quic_server_port); + return Cronet_Engine_GetStreamEngine(g_cronet_engine); +} + +// Starts the stream_engine* for testing with the QuicTestServer. +// Has the same properties as GetTestStreamEngine. This function is +// used when the stream_engine* needs to be shut down and restarted +// between test cases (including between all of the bidirectional +// stream test cases and all other tests for the engine; this is the +// situation for Cronet). +void StartTestStreamEngine(int port) { + CHECK(!g_cronet_engine); + quic_server_port = port; + g_cronet_engine = cronet::test::CreateTestEngine(port); +} + +// Shuts a stream_engine* started with |StartTestStreamEngine| down. +// See comment above. +void ShutdownTestStreamEngine() { + CHECK(g_cronet_engine); + Cronet_Engine_Destroy(g_cronet_engine); + g_cronet_engine = nullptr; + quic_server_port = 0; +} + +} // namespace grpc_support diff --git a/src/components/cronet/native/test/test_upload_data_provider.cc b/src/components/cronet/native/test/test_upload_data_provider.cc new file mode 100644 index 0000000000..47746d24f7 --- /dev/null +++ b/src/components/cronet/native/test/test_upload_data_provider.cc @@ -0,0 +1,315 @@ +// Copyright 2018 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 "components/cronet/native/test/test_upload_data_provider.h" + +#include "base/bind.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// Helper class that runs base::OnceClosure. +class TestRunnable { + public: + // Creates Cronet runnable that runs |task| once and destroys itself. + static Cronet_RunnablePtr CreateRunnable(base::OnceClosure task); + + TestRunnable(const TestRunnable&) = delete; + TestRunnable& operator=(const TestRunnable&) = delete; + + private: + explicit TestRunnable(base::OnceClosure task); + ~TestRunnable(); + + // Runs |self| and destroys it. + static void Run(Cronet_RunnablePtr self); + + // Closure to run. + base::OnceClosure task_; +}; + +TestRunnable::TestRunnable(base::OnceClosure task) : task_(std::move(task)) {} + +TestRunnable::~TestRunnable() = default; + +// static +Cronet_RunnablePtr TestRunnable::CreateRunnable(base::OnceClosure task) { + Cronet_RunnablePtr runnable = Cronet_Runnable_CreateWith(TestRunnable::Run); + Cronet_Runnable_SetClientContext(runnable, new TestRunnable(std::move(task))); + return runnable; +} + +// static +void TestRunnable::Run(Cronet_RunnablePtr self) { + CHECK(self); + Cronet_ClientContext context = Cronet_Runnable_GetClientContext(self); + TestRunnable* runnable = static_cast(context); + CHECK(runnable); + std::move(runnable->task_).Run(); + delete runnable; +} + +} // namespace + +namespace cronet { +// Various test utility functions for testing Cronet. +namespace test { + +TestUploadDataProvider::TestUploadDataProvider( + SuccessCallbackMode success_callback_mode, + Cronet_ExecutorPtr executor) + : success_callback_mode_(success_callback_mode), executor_(executor) {} + +TestUploadDataProvider::~TestUploadDataProvider() = default; + +Cronet_UploadDataProviderPtr +TestUploadDataProvider::CreateUploadDataProvider() { + Cronet_UploadDataProviderPtr upload_data_provider = + Cronet_UploadDataProvider_CreateWith( + TestUploadDataProvider::GetLength, TestUploadDataProvider::Read, + TestUploadDataProvider::Rewind, TestUploadDataProvider::Close); + Cronet_UploadDataProvider_SetClientContext(upload_data_provider, this); + return upload_data_provider; +} + +void TestUploadDataProvider::AddRead(std::string read) { + EXPECT_TRUE(!started_) << "Adding bytes after read"; + reads_.push_back(read); +} + +void TestUploadDataProvider::SetReadFailure(int read_fail_index, + FailMode read_fail_mode) { + read_fail_index_ = read_fail_index; + read_fail_mode_ = read_fail_mode; +} + +void TestUploadDataProvider::SetRewindFailure(FailMode rewind_fail_mode) { + rewind_fail_mode_ = rewind_fail_mode; +} + +void TestUploadDataProvider::SetReadCancel(int read_cancel_index, + CancelMode read_cancel_mode) { + read_cancel_index_ = read_cancel_index; + read_cancel_mode_ = read_cancel_mode; +} + +void TestUploadDataProvider::SetRewindCancel(CancelMode rewind_cancel_mode) { + rewind_cancel_mode_ = rewind_cancel_mode; +} + +int64_t TestUploadDataProvider::GetLength() const { + EXPECT_TRUE(!closed_.IsSet()) << "Data Provider is closed"; + if (bad_length_ != -1) + return bad_length_; + + return GetUploadedLength(); +} + +int64_t TestUploadDataProvider::GetUploadedLength() const { + if (chunked_) + return -1ll; + + int64_t length = 0ll; + for (const auto& read : reads_) + length += read.size(); + + return length; +} + +void TestUploadDataProvider::Read(Cronet_UploadDataSinkPtr upload_data_sink, + Cronet_BufferPtr buffer) { + int current_read_call = num_read_calls_; + ++num_read_calls_; + EXPECT_TRUE(!closed_.IsSet()) << "Data Provider is closed"; + + AssertIdle(); + + if (current_read_call == read_cancel_index_) + MaybeCancelRequest(read_cancel_mode_); + + if (MaybeFailRead(current_read_call, upload_data_sink)) { + failed_ = true; + return; + } + + read_pending_ = true; + started_ = true; + + bool final_chunk = (chunked_ && next_read_ == reads_.size() - 1); + EXPECT_TRUE(next_read_ < reads_.size()) << "Too many reads: " << next_read_; + const auto& read = reads_[next_read_]; + EXPECT_TRUE(read.size() <= Cronet_Buffer_GetSize(buffer)) + << "Read buffer smaller than expected."; + memcpy(Cronet_Buffer_GetData(buffer), read.data(), read.size()); + ++next_read_; + + auto complete_closure = base::BindOnce( + [](TestUploadDataProvider* upload_data_provider, + Cronet_UploadDataSink* upload_data_sink, uint64_t bytes_read, + bool final_chunk) { + upload_data_provider->read_pending_ = false; + Cronet_UploadDataSink_OnReadSucceeded(upload_data_sink, bytes_read, + final_chunk); + }, + this, upload_data_sink, read.size(), final_chunk); + + if (success_callback_mode_ == SYNC) { + std::move(complete_closure).Run(); + } else { + PostTaskToExecutor(std::move(complete_closure)); + } +} + +void TestUploadDataProvider::Rewind(Cronet_UploadDataSinkPtr upload_data_sink) { + ++num_rewind_calls_; + EXPECT_TRUE(!closed_.IsSet()) << "Data Provider is closed"; + AssertIdle(); + + MaybeCancelRequest(rewind_cancel_mode_); + + if (MaybeFailRewind(upload_data_sink)) { + failed_ = true; + return; + } + + // Should never try and rewind when rewinding does nothing. + EXPECT_TRUE(next_read_ != 0) << "Unexpected rewind when already at beginning"; + + rewind_pending_ = true; + next_read_ = 0; + + auto complete_closure = base::BindOnce( + [](TestUploadDataProvider* upload_data_provider, + Cronet_UploadDataSink* upload_data_sink) { + upload_data_provider->rewind_pending_ = false; + Cronet_UploadDataSink_OnRewindSucceeded(upload_data_sink); + }, + this, upload_data_sink); + + if (success_callback_mode_ == SYNC) { + std::move(complete_closure).Run(); + } else { + PostTaskToExecutor(std::move(complete_closure)); + } +} + +void TestUploadDataProvider::PostTaskToExecutor(base::OnceClosure task) { + EXPECT_TRUE(executor_); + // |runnable| is passed to executor, which destroys it after execution. + Cronet_Executor_Execute(executor_, + TestRunnable::CreateRunnable(std::move(task))); +} + +void TestUploadDataProvider::AssertIdle() const { + EXPECT_TRUE(!read_pending_) << "Unexpected operation during read"; + EXPECT_TRUE(!rewind_pending_) << "Unexpected operation during rewind"; + EXPECT_TRUE(!failed_) << "Unexpected operation after failure"; +} + +bool TestUploadDataProvider::MaybeFailRead( + int read_index, + Cronet_UploadDataSinkPtr upload_data_sink) { + if (read_fail_mode_ == NONE) + return false; + if (read_index != read_fail_index_) + return false; + + if (read_fail_mode_ == CALLBACK_SYNC) { + Cronet_UploadDataSink_OnReadError(upload_data_sink, "Sync read failure"); + return true; + } + EXPECT_EQ(read_fail_mode_, CALLBACK_ASYNC); + + PostTaskToExecutor(base::BindOnce( + [](Cronet_UploadDataSink* upload_data_sink) { + Cronet_UploadDataSink_OnReadError(upload_data_sink, + "Async read failure"); + }, + upload_data_sink)); + return true; +} + +bool TestUploadDataProvider::MaybeFailRewind( + Cronet_UploadDataSinkPtr upload_data_sink) { + if (rewind_fail_mode_ == NONE) + return false; + + if (rewind_fail_mode_ == CALLBACK_SYNC) { + Cronet_UploadDataSink_OnRewindError(upload_data_sink, + "Sync rewind failure"); + return true; + } + EXPECT_EQ(rewind_fail_mode_, CALLBACK_ASYNC); + + PostTaskToExecutor(base::BindOnce( + [](Cronet_UploadDataSink* upload_data_sink) { + Cronet_UploadDataSink_OnRewindError(upload_data_sink, + "Async rewind failure"); + }, + upload_data_sink)); + return true; +} + +void TestUploadDataProvider::MaybeCancelRequest(CancelMode cancel_mode) { + if (cancel_mode == CANCEL_NONE) + return; + + CHECK(url_request_); + + if (cancel_mode == CANCEL_SYNC) { + Cronet_UrlRequest_Cancel(url_request_); + return; + } + + EXPECT_EQ(cancel_mode, CANCEL_ASYNC); + PostTaskToExecutor(base::BindOnce( + [](Cronet_UrlRequestPtr url_request) { + Cronet_UrlRequest_Cancel(url_request); + }, + url_request_)); +} + +void TestUploadDataProvider::Close() { + EXPECT_TRUE(!closed_.IsSet()) << "Closed twice"; + closed_.Set(); + awaiting_close_.Signal(); +} + +void TestUploadDataProvider::AssertClosed() { + awaiting_close_.TimedWait(base::Milliseconds(5000)); + EXPECT_TRUE(closed_.IsSet()) << "Was not closed"; +} + +/* static */ +TestUploadDataProvider* TestUploadDataProvider::GetThis( + Cronet_UploadDataProviderPtr self) { + return static_cast( + Cronet_UploadDataProvider_GetClientContext(self)); +} + +/* static */ +int64_t TestUploadDataProvider::GetLength(Cronet_UploadDataProviderPtr self) { + return GetThis(self)->GetLength(); +} + +/* static */ +void TestUploadDataProvider::Read(Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink, + Cronet_BufferPtr buffer) { + return GetThis(self)->Read(upload_data_sink, buffer); +} + +/* static */ +void TestUploadDataProvider::Rewind(Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink) { + return GetThis(self)->Rewind(upload_data_sink); +} + +/* static */ +void TestUploadDataProvider::Close(Cronet_UploadDataProviderPtr self) { + return GetThis(self)->Close(); +} + +} // namespace test +} // namespace cronet diff --git a/src/components/cronet/native/test/test_upload_data_provider.h b/src/components/cronet/native/test/test_upload_data_provider.h new file mode 100644 index 0000000000..99bf753153 --- /dev/null +++ b/src/components/cronet/native/test/test_upload_data_provider.h @@ -0,0 +1,153 @@ +// Copyright 2018 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_NATIVE_TEST_TEST_UPLOAD_DATA_PROVIDER_H_ +#define COMPONENTS_CRONET_NATIVE_TEST_TEST_UPLOAD_DATA_PROVIDER_H_ + +#include +#include +#include +#include + +#include "cronet_c.h" + +#include "base/bind.h" +#include "base/synchronization/atomic_flag.h" +#include "base/synchronization/lock.h" +#include "base/synchronization/waitable_event.h" +#include "base/threading/thread.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace cronet { +// Various test utility functions for testing Cronet. +namespace test { + +/** + * An UploadDataProvider implementation used in tests. + */ +class TestUploadDataProvider { + public: + // Indicates whether all success callbacks are synchronous or asynchronous. + // Doesn't apply to errors. + enum SuccessCallbackMode { SYNC, ASYNC }; + + // Indicates whether failures should invoke callbacks synchronously, or + // invoke callback asynchronously. + enum FailMode { NONE, CALLBACK_SYNC, CALLBACK_ASYNC }; + + // Indicates whether request should be canceled synchronously before + // the callback or asynchronously after. + enum CancelMode { CANCEL_NONE, CANCEL_SYNC, CANCEL_ASYNC }; + + TestUploadDataProvider(SuccessCallbackMode success_callback_mode, + Cronet_ExecutorPtr executor); + + virtual ~TestUploadDataProvider(); + + Cronet_UploadDataProviderPtr CreateUploadDataProvider(); + + // Adds the result to be returned by a successful read request. The + // returned bytes must all fit within the read buffer provided by Cronet. + // After a rewind, if there is one, all reads will be repeated. + void AddRead(std::string read); + + void SetReadFailure(int read_fail_index, FailMode read_fail_mode); + + void SetRewindFailure(FailMode rewind_fail_mode); + + void SetReadCancel(int read_cancel_index, CancelMode read_cancel_mode); + + void SetRewindCancel(CancelMode rewind_cancel_mode); + + void set_bad_length(int64_t bad_length) { bad_length_ = bad_length; } + + void set_chunked(bool chunked) { chunked_ = chunked; } + + void set_url_request(Cronet_UrlRequestPtr request) { url_request_ = request; } + + Cronet_ExecutorPtr executor() const { return executor_; } + + int num_read_calls() const { return num_read_calls_; } + + int num_rewind_calls() const { return num_rewind_calls_; } + + /** + * Returns the cumulative length of all data added by calls to addRead. + */ + virtual int64_t GetLength() const; + + int64_t GetUploadedLength() const; + + virtual void Read(Cronet_UploadDataSinkPtr upload_data_sink, + Cronet_BufferPtr buffer); + + void Rewind(Cronet_UploadDataSinkPtr upload_data_sink); + + void AssertClosed(); + + private: + void PostTaskToExecutor(base::OnceClosure task); + + void AssertIdle() const; + + bool MaybeFailRead(int read_index, Cronet_UploadDataSinkPtr upload_data_sink); + + bool MaybeFailRewind(Cronet_UploadDataSinkPtr upload_data_sink); + + void MaybeCancelRequest(CancelMode cancel_mode); + + void Close(); + + // Implementation of Cronet_UploadDataProvider methods. + static TestUploadDataProvider* GetThis(Cronet_UploadDataProviderPtr self); + + static int64_t GetLength(Cronet_UploadDataProviderPtr self); + static void Read(Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink, + Cronet_BufferPtr buffer); + static void Rewind(Cronet_UploadDataProviderPtr self, + Cronet_UploadDataSinkPtr upload_data_sink); + static void Close(Cronet_UploadDataProviderPtr self); + + std::vector reads_; + const SuccessCallbackMode success_callback_mode_ = SYNC; + const Cronet_ExecutorPtr executor_; + + Cronet_UrlRequestPtr url_request_; + + bool chunked_ = false; + + // Index of read to fail on. + int read_fail_index_ = -1; + // Indicates how to fail on a read. + FailMode read_fail_mode_ = NONE; + FailMode rewind_fail_mode_ = NONE; + + // Index of read to cancel on. + int read_cancel_index_ = -1; + // Indicates how to cancel on a read. + CancelMode read_cancel_mode_ = CANCEL_NONE; + CancelMode rewind_cancel_mode_ = CANCEL_NONE; + + // Report bad length if not set to -1. + int64_t bad_length_ = -1; + + int num_read_calls_ = 0; + int num_rewind_calls_ = 0; + + size_t next_read_ = 0; + bool started_ = false; + bool read_pending_ = false; + bool rewind_pending_ = false; + // Used to ensure there are no read/rewind requests after a failure. + bool failed_ = false; + + base::AtomicFlag closed_; + base::WaitableEvent awaiting_close_; +}; + +} // namespace test +} // namespace cronet + +#endif // COMPONENTS_CRONET_NATIVE_TEST_TEST_UPLOAD_DATA_PROVIDER_H_ diff --git a/src/components/cronet/native/test/test_url_request_callback.cc b/src/components/cronet/native/test/test_url_request_callback.cc new file mode 100644 index 0000000000..7db0c82eed --- /dev/null +++ b/src/components/cronet/native/test/test_url_request_callback.cc @@ -0,0 +1,351 @@ +// Copyright 2018 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 "components/cronet/native/test/test_url_request_callback.h" + +#include "base/bind.h" +#include "components/cronet/native/test/test_util.h" + +namespace cronet { +namespace test { + +TestUrlRequestCallback::UrlResponseInfo::UrlResponseInfo( + Cronet_UrlResponseInfoPtr response_info) + : url(Cronet_UrlResponseInfo_url_get(response_info)), + http_status_code( + Cronet_UrlResponseInfo_http_status_code_get(response_info)), + http_status_text( + Cronet_UrlResponseInfo_http_status_text_get(response_info)), + was_cached(Cronet_UrlResponseInfo_was_cached_get(response_info)), + negotiated_protocol( + Cronet_UrlResponseInfo_negotiated_protocol_get(response_info)), + proxy_server(Cronet_UrlResponseInfo_proxy_server_get(response_info)), + received_byte_count( + Cronet_UrlResponseInfo_received_byte_count_get(response_info)) { + for (uint32_t url_id = 0; + url_id < Cronet_UrlResponseInfo_url_chain_size(response_info); + ++url_id) { + url_chain.push_back( + Cronet_UrlResponseInfo_url_chain_at(response_info, url_id)); + } + for (uint32_t i = 0; + i < Cronet_UrlResponseInfo_all_headers_list_size(response_info); ++i) { + Cronet_HttpHeaderPtr header = + Cronet_UrlResponseInfo_all_headers_list_at(response_info, i); + all_headers.push_back(std::pair( + Cronet_HttpHeader_name_get(header), + Cronet_HttpHeader_value_get(header))); + } +} + +TestUrlRequestCallback::UrlResponseInfo::UrlResponseInfo( + const std::vector& urls, + const std::string& message, + int32_t status_code, + int64_t received_bytes, + std::vector headers) + : url(urls.back()), + url_chain(urls), + http_status_code(status_code), + http_status_text(message), + negotiated_protocol("unknown"), + proxy_server(":0"), + received_byte_count(received_bytes) { + for (uint32_t i = 0; i < headers.size(); i += 2) { + all_headers.push_back( + std::pair(headers[i], headers[i + 1])); + } +} + +TestUrlRequestCallback::UrlResponseInfo::~UrlResponseInfo() = default; + +TestUrlRequestCallback::TestUrlRequestCallback(bool direct_executor) + : direct_executor_(direct_executor), + done_(base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED), + step_block_(base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED) {} + +TestUrlRequestCallback::~TestUrlRequestCallback() { + ShutdownExecutor(); +} + +Cronet_ExecutorPtr TestUrlRequestCallback::GetExecutor() { + if (executor_) + return executor_; + if (direct_executor_) { + executor_ = + Cronet_Executor_CreateWith(TestUrlRequestCallback::ExecuteDirect); + } else { + executor_thread_ = + std::make_unique("TestUrlRequestCallback executor"); + executor_thread_->Start(); + executor_ = Cronet_Executor_CreateWith(TestUrlRequestCallback::Execute); + Cronet_Executor_SetClientContext(executor_, this); + } + return executor_; +} + +Cronet_UrlRequestCallbackPtr +TestUrlRequestCallback::CreateUrlRequestCallback() { + Cronet_UrlRequestCallbackPtr callback = Cronet_UrlRequestCallback_CreateWith( + TestUrlRequestCallback::OnRedirectReceived, + TestUrlRequestCallback::OnResponseStarted, + TestUrlRequestCallback::OnReadCompleted, + TestUrlRequestCallback::OnSucceeded, TestUrlRequestCallback::OnFailed, + TestUrlRequestCallback::OnCanceled); + Cronet_UrlRequestCallback_SetClientContext(callback, this); + return callback; +} + +void TestUrlRequestCallback::OnRedirectReceived(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String newLocationUrl) { + CheckExecutorThread(); + + CHECK(!Cronet_UrlRequest_IsDone(request)); + CHECK(response_step_ == NOTHING || response_step_ == ON_RECEIVED_REDIRECT); + CHECK(!last_error_); + + response_step_ = ON_RECEIVED_REDIRECT; + redirect_url_list_.push_back(newLocationUrl); + redirect_response_info_list_.push_back( + std::make_unique(info)); + ++redirect_count_; + if (MaybeCancelOrPause(request)) { + return; + } + Cronet_UrlRequest_FollowRedirect(request); +} + +void TestUrlRequestCallback::OnResponseStarted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + CheckExecutorThread(); + CHECK(!Cronet_UrlRequest_IsDone(request)); + CHECK(response_step_ == NOTHING || response_step_ == ON_RECEIVED_REDIRECT); + CHECK(!last_error_); + response_step_ = ON_RESPONSE_STARTED; + original_response_info_ = info; + response_info_ = std::make_unique(info); + if (MaybeCancelOrPause(request)) { + return; + } + StartNextRead(request); +} + +void TestUrlRequestCallback::OnReadCompleted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytes_read) { + CheckExecutorThread(); + CHECK(!Cronet_UrlRequest_IsDone(request)); + CHECK(response_step_ == ON_RESPONSE_STARTED || + response_step_ == ON_READ_COMPLETED); + CHECK(!last_error_); + response_step_ = ON_READ_COMPLETED; + original_response_info_ = info; + response_info_ = std::make_unique(info); + response_data_length_ += bytes_read; + + if (accumulate_response_data_) { + std::string last_read_data( + reinterpret_cast(Cronet_Buffer_GetData(buffer)), bytes_read); + response_as_string_ += last_read_data; + } + + if (MaybeCancelOrPause(request)) { + Cronet_Buffer_Destroy(buffer); + return; + } + StartNextRead(request, buffer); +} + +void TestUrlRequestCallback::OnSucceeded(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + CheckExecutorThread(); + CHECK(Cronet_UrlRequest_IsDone(request)); + CHECK(response_step_ == ON_RESPONSE_STARTED || + response_step_ == ON_READ_COMPLETED); + CHECK(!on_error_called_); + CHECK(!on_canceled_called_); + CHECK(!last_error_); + response_step_ = ON_SUCCEEDED; + original_response_info_ = info; + response_info_ = std::make_unique(info); + + MaybeCancelOrPause(request); + SignalDone(); +} + +void TestUrlRequestCallback::OnFailed(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error) { + CheckExecutorThread(); + CHECK(Cronet_UrlRequest_IsDone(request)); + // Shouldn't happen after success. + CHECK(response_step_ != ON_SUCCEEDED); + // Should happen at most once for a single request. + CHECK(!on_error_called_); + CHECK(!on_canceled_called_); + CHECK(!last_error_); + + response_step_ = ON_FAILED; + on_error_called_ = true; + // It is possible that |info| is nullptr if response has not started. + if (info) { + original_response_info_ = info; + response_info_ = std::make_unique(info); + } + last_error_ = error; + last_error_code_ = Cronet_Error_error_code_get(error); + last_error_message_ = Cronet_Error_message_get(error); + MaybeCancelOrPause(request); + SignalDone(); +} + +void TestUrlRequestCallback::OnCanceled(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + CheckExecutorThread(); + CHECK(Cronet_UrlRequest_IsDone(request)); + CHECK(!on_error_called_); + // Should happen at most once for a single request. + CHECK(!on_canceled_called_); + CHECK(!last_error_); + + response_step_ = ON_CANCELED; + on_canceled_called_ = true; + // It is possible |info| is nullptr if the response has not started. + if (info) { + original_response_info_ = info; + response_info_ = std::make_unique(info); + } + MaybeCancelOrPause(request); + SignalDone(); +} + +void TestUrlRequestCallback::ShutdownExecutor() { + base::AutoLock lock(executor_lock_); + if (executor_ == nullptr) + return; + Cronet_Executor_Destroy(executor_); + executor_ = nullptr; + // Stop executor thread outside of lock to allow runnables to complete. + auto executor_thread(std::move(executor_thread_)); + executor_lock_.Release(); + executor_thread.reset(); + executor_lock_.Acquire(); +} + +void TestUrlRequestCallback::CheckExecutorThread() { + base::AutoLock lock(executor_lock_); + if (executor_thread_ && !direct_executor_) + CHECK(executor_thread_->task_runner()->BelongsToCurrentThread()); +} + +bool TestUrlRequestCallback::MaybeCancelOrPause(Cronet_UrlRequestPtr request) { + CheckExecutorThread(); + if (response_step_ != failure_step_ || failure_type_ == NONE) { + if (!auto_advance_) { + step_block_.Signal(); + return true; + } + return false; + } + + if (failure_type_ == CANCEL_SYNC) { + Cronet_UrlRequest_Cancel(request); + } + if (failure_type_ == CANCEL_ASYNC || + failure_type_ == CANCEL_ASYNC_WITHOUT_PAUSE) { + if (direct_executor_) { + Cronet_UrlRequest_Cancel(request); + } else { + base::AutoLock lock(executor_lock_); + CHECK(executor_thread_); + executor_thread_->task_runner()->PostTask( + FROM_HERE, base::BindOnce(&Cronet_UrlRequest_Cancel, request)); + } + } + return failure_type_ != CANCEL_ASYNC_WITHOUT_PAUSE; +} + +/* static */ +TestUrlRequestCallback* TestUrlRequestCallback::GetThis( + Cronet_UrlRequestCallbackPtr self) { + return static_cast( + Cronet_UrlRequestCallback_GetClientContext(self)); +} + +/* static */ +void TestUrlRequestCallback::OnRedirectReceived( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String newLocationUrl) { + GetThis(self)->OnRedirectReceived(request, info, newLocationUrl); +} + +/* static */ +void TestUrlRequestCallback::OnResponseStarted( + Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + GetThis(self)->OnResponseStarted(request, info); +} + +/* static */ +void TestUrlRequestCallback::OnReadCompleted(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytesRead) { + GetThis(self)->OnReadCompleted(request, info, buffer, bytesRead); +} + +/* static */ +void TestUrlRequestCallback::OnSucceeded(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + GetThis(self)->OnSucceeded(request, info); +} + +/* static */ +void TestUrlRequestCallback::OnFailed(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error) { + GetThis(self)->OnFailed(request, info, error); +} + +/* static */ +void TestUrlRequestCallback::OnCanceled(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) { + GetThis(self)->OnCanceled(request, info); +} + +/* static */ +void TestUrlRequestCallback::Execute(Cronet_ExecutorPtr self, + Cronet_RunnablePtr runnable) { + CHECK(self); + auto* callback = static_cast( + Cronet_Executor_GetClientContext(self)); + CHECK(callback); + base::AutoLock lock(callback->executor_lock_); + CHECK(callback->executor_thread_); + // Post |runnable| onto executor thread. + callback->executor_thread_->task_runner()->PostTask( + FROM_HERE, RunnableWrapper::CreateOnceClosure(runnable)); +} + +/* static */ +void TestUrlRequestCallback::ExecuteDirect(Cronet_ExecutorPtr self, + Cronet_RunnablePtr runnable) { + // Run |runnable| directly. + Cronet_Runnable_Run(runnable); + Cronet_Runnable_Destroy(runnable); +} + +} // namespace test +} // namespace cronet diff --git a/src/components/cronet/native/test/test_url_request_callback.h b/src/components/cronet/native/test/test_url_request_callback.h new file mode 100644 index 0000000000..28b2baeaec --- /dev/null +++ b/src/components/cronet/native/test/test_url_request_callback.h @@ -0,0 +1,239 @@ +// Copyright 2018 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_NATIVE_TEST_TEST_URL_REQUEST_CALLBACK_H_ +#define COMPONENTS_CRONET_NATIVE_TEST_TEST_URL_REQUEST_CALLBACK_H_ + +#include +#include +#include +#include + +#include "cronet_c.h" + +#include "base/synchronization/lock.h" +#include "base/synchronization/waitable_event.h" +#include "base/threading/thread.h" + +namespace cronet { +// Various test utility functions for testing Cronet. +namespace test { + +class TestUrlRequestCallback { + public: + enum ResponseStep { + NOTHING, + ON_RECEIVED_REDIRECT, + ON_RESPONSE_STARTED, + ON_READ_COMPLETED, + ON_SUCCEEDED, + ON_FAILED, + ON_CANCELED, + }; + + enum FailureType { + NONE, + CANCEL_SYNC, + CANCEL_ASYNC, + // Same as above, but continues to advance the request after posting + // the cancellation task. + CANCEL_ASYNC_WITHOUT_PAUSE, + }; + + class UrlResponseInfo { + public: + // Construct actual response info copied from Cronet_UrlResponseInfoPtr. + explicit UrlResponseInfo(Cronet_UrlResponseInfoPtr response_info); + // Construct expected response info for testing. + UrlResponseInfo(const std::vector& urls, + const std::string& message, + int32_t status_code, + int64_t received_bytes, + std::vector headers); + ~UrlResponseInfo(); + + // Data copied from response_info to make it available after request is + // done. + std::string url; + std::vector url_chain; + int32_t http_status_code = 0; + std::string http_status_text; + std::vector> all_headers; + bool was_cached = false; + std::string negotiated_protocol; + std::string proxy_server; + int64_t received_byte_count = 0; + }; + + // TODO(crbug.com/969048): Make these private with public accessors. + std::vector> redirect_response_info_list_; + std::vector redirect_url_list_; + // Owned by UrlRequest, only valid until UrlRequest is destroyed. + Cronet_UrlResponseInfoPtr original_response_info_ = nullptr; + // |response_info_| is copied from |original_response_info_|, valid after + // UrlRequest is destroyed. + std::unique_ptr response_info_; + // Owned by UrlRequest, only valid until UrlRequest is destroyed. + Cronet_ErrorPtr last_error_ = nullptr; + // Values copied from |last_error_| valid after UrlRequest is destroyed. + Cronet_Error_ERROR_CODE last_error_code_ = + Cronet_Error_ERROR_CODE_ERROR_OTHER; + std::string last_error_message_; + + ResponseStep response_step_ = NOTHING; + + int redirect_count_ = 0; + bool on_error_called_ = false; + bool on_canceled_called_ = false; + + int response_data_length_ = 0; + std::string response_as_string_; + + explicit TestUrlRequestCallback(bool direct_executor); + virtual ~TestUrlRequestCallback(); + + Cronet_ExecutorPtr GetExecutor(); + + Cronet_UrlRequestCallbackPtr CreateUrlRequestCallback(); + + void set_auto_advance(bool auto_advance) { auto_advance_ = auto_advance; } + + void set_accumulate_response_data(bool accuumulate) { + accumulate_response_data_ = accuumulate; + } + + void set_failure(FailureType failure_type, ResponseStep failure_step) { + failure_step_ = failure_step; + failure_type_ = failure_type; + } + + void WaitForDone() { done_.Wait(); } + + void WaitForNextStep() { + step_block_.Wait(); + step_block_.Reset(); + } + + void ShutdownExecutor(); + + bool IsDone() { return done_.IsSignaled(); } + + protected: + class Executor; + + virtual void OnRedirectReceived(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String newLocationUrl); + + virtual void OnResponseStarted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + + virtual void OnReadCompleted(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytes_read); + + virtual void OnSucceeded(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + + virtual void OnFailed(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error); + + virtual void OnCanceled(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + + void StartNextRead(Cronet_UrlRequestPtr request) { + Cronet_BufferPtr buffer = Cronet_Buffer_Create(); + Cronet_Buffer_InitWithAlloc(buffer, READ_BUFFER_SIZE); + + StartNextRead(request, buffer); + } + + void StartNextRead(Cronet_UrlRequestPtr request, Cronet_BufferPtr buffer) { + Cronet_UrlRequest_Read(request, buffer); + } + + void SignalDone() { done_.Signal(); } + + void CheckExecutorThread(); + + /** + * Returns false if the callback should continue to advance the + * request. + */ + bool MaybeCancelOrPause(Cronet_UrlRequestPtr request); + + // Implementation of Cronet_UrlRequestCallback methods. + static TestUrlRequestCallback* GetThis(Cronet_UrlRequestCallbackPtr self); + + static void OnRedirectReceived(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_String newLocationUrl); + + static void OnResponseStarted(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + + static void OnReadCompleted(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_BufferPtr buffer, + uint64_t bytesRead); + + static void OnSucceeded(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + + static void OnFailed(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info, + Cronet_ErrorPtr error); + + static void OnCanceled(Cronet_UrlRequestCallbackPtr self, + Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info); + + // Implementation of Cronet_Executor methods. + static void Execute(Cronet_ExecutorPtr self, Cronet_RunnablePtr runnable); + static void ExecuteDirect(Cronet_ExecutorPtr self, + Cronet_RunnablePtr runnable); + + const int READ_BUFFER_SIZE = 32 * 1024; + + // When false, the consumer is responsible for all calls into the request + // that advance it. + bool auto_advance_ = true; + + // When false response data is not accuumulated for better performance. + bool accumulate_response_data_ = true; + + // Whether to create direct executors. + const bool direct_executor_; + + // Conditionally fail on certain steps. + FailureType failure_type_ = NONE; + ResponseStep failure_step_ = NOTHING; + + // Signals when request is done either successfully or not. + base::WaitableEvent done_; + + // Signaled on each step when |auto_advance_| is false. + base::WaitableEvent step_block_; + + // Lock that synchronizes access to |executor_| and |executor_thread_|. + base::Lock executor_lock_; + + // Executor that runs callback tasks. + Cronet_ExecutorPtr executor_ = nullptr; + + // Thread on which |executor_| runs callback tasks. + std::unique_ptr executor_thread_; +}; + +} // namespace test +} // namespace cronet + +#endif // COMPONENTS_CRONET_NATIVE_TEST_TEST_URL_REQUEST_CALLBACK_H_ diff --git a/src/components/cronet/native/test/test_util.cc b/src/components/cronet/native/test/test_util.cc new file mode 100644 index 0000000000..3575606413 --- /dev/null +++ b/src/components/cronet/native/test/test_util.cc @@ -0,0 +1,102 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/cronet/native/test/test_util.h" + +#include +#include + +#include "base/bind.h" +#include "base/strings/stringprintf.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "components/cronet/native/generated/cronet.idl_c.h" +#include "net/base/net_errors.h" +#include "net/cert/mock_cert_verifier.h" + +namespace { +// Implementation of PostTaskExecutor methods. +void TestExecutor_Execute(Cronet_ExecutorPtr self, + Cronet_RunnablePtr runnable) { + CHECK(self); + DVLOG(1) << "Post Task"; + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, cronet::test::RunnableWrapper::CreateOnceClosure(runnable)); +} + +// Test Cert Verifier that successfully verifies any cert from test.example.com. +class TestCertVerifier : public net::MockCertVerifier { + public: + TestCertVerifier() = default; + ~TestCertVerifier() override = default; + + // CertVerifier implementation + int Verify(const RequestParams& params, + net::CertVerifyResult* verify_result, + net::CompletionOnceCallback callback, + std::unique_ptr* out_req, + const net::NetLogWithSource& net_log) override { + verify_result->Reset(); + if (params.hostname() == "test.example.com") { + verify_result->verified_cert = params.certificate(); + verify_result->is_issued_by_known_root = true; + return net::OK; + } + return net::MockCertVerifier::Verify(params, verify_result, + std::move(callback), out_req, net_log); + } +}; + +} // namespace + +namespace cronet { +namespace test { + +Cronet_EnginePtr CreateTestEngine(int quic_server_port) { + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_EngineParams_user_agent_set(engine_params, "test"); + // Add Host Resolver Rules. + std::string host_resolver_rules = base::StringPrintf( + "MAP test.example.com 127.0.0.1:%d," + "MAP notfound.example.com ~NOTFOUND", + quic_server_port); + Cronet_EngineParams_experimental_options_set( + engine_params, + base::StringPrintf( + "{ \"HostResolverRules\": { \"host_resolver_rules\" : \"%s\" } }", + host_resolver_rules.c_str()) + .c_str()); + // Enable QUIC. + Cronet_EngineParams_enable_quic_set(engine_params, true); + // Add QUIC Hint. + Cronet_QuicHintPtr quic_hint = Cronet_QuicHint_Create(); + Cronet_QuicHint_host_set(quic_hint, "test.example.com"); + Cronet_QuicHint_port_set(quic_hint, 443); + Cronet_QuicHint_alternate_port_set(quic_hint, 443); + Cronet_EngineParams_quic_hints_add(engine_params, quic_hint); + Cronet_QuicHint_Destroy(quic_hint); + // Create Cronet Engine. + Cronet_EnginePtr cronet_engine = Cronet_Engine_Create(); + // Set Mock Cert Verifier. + auto cert_verifier = std::make_unique(); + Cronet_Engine_SetMockCertVerifierForTesting(cronet_engine, + cert_verifier.release()); + // Start Cronet Engine. + Cronet_Engine_StartWithParams(cronet_engine, engine_params); + Cronet_EngineParams_Destroy(engine_params); + return cronet_engine; +} + +Cronet_ExecutorPtr CreateTestExecutor() { + return Cronet_Executor_CreateWith(TestExecutor_Execute); +} + +// static +base::OnceClosure RunnableWrapper::CreateOnceClosure( + Cronet_RunnablePtr runnable) { + return base::BindOnce(&RunnableWrapper::Run, + std::make_unique(runnable)); +} + +} // namespace test +} // namespace cronet diff --git a/src/components/cronet/native/test/test_util.h b/src/components/cronet/native/test/test_util.h new file mode 100644 index 0000000000..b956a5628d --- /dev/null +++ b/src/components/cronet/native/test/test_util.h @@ -0,0 +1,43 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_CRONET_NATIVE_TEST_TEST_UTIL_H_ +#define COMPONENTS_CRONET_NATIVE_TEST_TEST_UTIL_H_ + +#include "base/callback.h" +#include "cronet_c.h" + +namespace cronet { +// Various test utility functions for testing Cronet. +namespace test { + +// Create an engine that is configured to support local test servers. +Cronet_EnginePtr CreateTestEngine(int quic_server_port); + +// Create an executor that runs tasks on different background thread. +Cronet_ExecutorPtr CreateTestExecutor(); + +// Class to wrap Cronet_Runnable into a base::OnceClosure. +class RunnableWrapper { + public: + ~RunnableWrapper() { Cronet_Runnable_Destroy(runnable_); } + + // Wrap a Cronet_Runnable into a base::OnceClosure. + static base::OnceClosure CreateOnceClosure(Cronet_RunnablePtr runnable); + + private: + friend std::unique_ptr std::make_unique( + Cronet_RunnablePtr&); + + explicit RunnableWrapper(Cronet_RunnablePtr runnable) : runnable_(runnable) {} + + void Run() { Cronet_Runnable_Run(runnable_); } + + const Cronet_RunnablePtr runnable_; +}; + +} // namespace test +} // namespace cronet + +#endif // COMPONENTS_CRONET_NATIVE_TEST_TEST_UTIL_H_ diff --git a/src/components/cronet/native/test/url_request_test.cc b/src/components/cronet/native/test/url_request_test.cc new file mode 100644 index 0000000000..ebb63ed0ea --- /dev/null +++ b/src/components/cronet/native/test/url_request_test.cc @@ -0,0 +1,1836 @@ +// Copyright 2018 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 +#include +#include + +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/logging.h" +#include "base/memory/raw_ptr.h" +#include "base/run_loop.h" +#include "base/synchronization/waitable_event.h" +#include "base/test/task_environment.h" +#include "components/cronet/native/test/test_request_finished_info_listener.h" +#include "components/cronet/native/test/test_upload_data_provider.h" +#include "components/cronet/native/test/test_url_request_callback.h" +#include "components/cronet/native/test/test_util.h" +#include "components/cronet/testing/test_server/test_server.h" +#include "cronet_c.h" +#include "net/test/embedded_test_server/default_handlers.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +using cronet::test::TestRequestFinishedInfoListener; +using cronet::test::TestUploadDataProvider; +using cronet::test::TestUrlRequestCallback; +using ::testing::HasSubstr; + +namespace { + +// A Cronet_UrlRequestStatusListener impl that waits for OnStatus callback. +class StatusListener { + public: + // |callback| is verified to not yet have reached a final state when + // OnStatus() is called back. + explicit StatusListener(TestUrlRequestCallback* callback) + : status_listener_(Cronet_UrlRequestStatusListener_CreateWith( + StatusListener::OnStatus)), + callback_(callback), + expect_request_not_done_(false) { + Cronet_UrlRequestStatusListener_SetClientContext(status_listener_, this); + } + + StatusListener(const StatusListener&) = delete; + StatusListener& operator=(const StatusListener&) = delete; + + ~StatusListener() { + Cronet_UrlRequestStatusListener_Destroy(status_listener_); + } + + // Wait for and return request status. + Cronet_UrlRequestStatusListener_Status GetStatus( + Cronet_UrlRequestPtr request) { + Cronet_UrlRequest_GetStatus(request, status_listener_); + // NOTE(pauljensen): There's no guarantee this line will get executed + // before OnStatus() reads |expect_request_not_done_|. It's very unlikely + // it will get read before this write, but if it does it just means + // OnStatus() won't check that the final callback has not been issued yet. + expect_request_not_done_ = !Cronet_UrlRequest_IsDone(request); + awaiting_status_.Wait(); + return status_; + } + + private: + // Cronet_UrlRequestStatusListener OnStatus impl. + static void OnStatus(Cronet_UrlRequestStatusListenerPtr self, + Cronet_UrlRequestStatusListener_Status status) { + StatusListener* listener = static_cast( + Cronet_UrlRequestStatusListener_GetClientContext(self)); + + // Enforce we call OnStatus() before OnSucceeded/OnFailed/OnCanceled(). + if (listener->expect_request_not_done_) + EXPECT_FALSE(listener->callback_->IsDone()); + + listener->status_ = status; + listener->awaiting_status_.Signal(); + } + + Cronet_UrlRequestStatusListenerPtr const status_listener_; + const raw_ptr callback_; + + Cronet_UrlRequestStatusListener_Status status_ = + Cronet_UrlRequestStatusListener_Status_INVALID; + base::WaitableEvent awaiting_status_; + + // Indicates if GetStatus() was called before request finished, indicating + // that OnStatus() should be called before request finishes. The writing of + // this variable races the reading of it, but it's initialized to a safe + // value. + std::atomic_bool expect_request_not_done_; +}; + +// Query and return status of |request|. |callback| is verified to not yet have +// reached a final state by the time OnStatus is called. +Cronet_UrlRequestStatusListener_Status GetRequestStatus( + Cronet_UrlRequestPtr request, + TestUrlRequestCallback* callback) { + return StatusListener(callback).GetStatus(request); +} + +enum class RequestFinishedListenerType { + kNoListener, // Don't add a request finished listener. + kUrlRequestListener, // Add a request finished listener to the UrlRequest. + kEngineListener, // Add a request finished listener to the Engine. +}; + +// Converts a Cronet_DateTimePtr into the int64 number of milliseconds since +// the UNIX epoch. +// +// Returns -1 if |date_time| is nullptr. +int64_t DateToMillis(Cronet_DateTimePtr date_time) { + if (date_time == nullptr) { + return -1; + } + int64_t value = Cronet_DateTime_value_get(date_time); + // Cronet_DateTime fields shouldn't be before the UNIX epoch. + // + // While DateToMillis() callers can easily check this themselves (and + // produce more descriptive errors showing which field is violating), they + // can't easily distinguish a nullptr vs -1 value, so we check for -1 here. + EXPECT_NE(-1, value); + return value; +} + +// Sanity check that the date isn't wildly off, somehow (perhaps due to read of +// used memory, wild pointer, etc.). +// +// Interpreted as milliseconds after the UNIX timestamp, this timestamp occurs +// at 37,648 C.E. +constexpr int64_t kDateOverrunThreshold = 1LL << 50; + +// Basic sanity checking of all Cronet_Metrics fields. For optional fields, we +// allow the field to be non-present. Start/end pairs should be monotonic (end +// not less than start). +// +// Ordering of events is also checked. +void VerifyRequestMetrics(Cronet_MetricsPtr metrics) { + EXPECT_GE(DateToMillis(Cronet_Metrics_request_start_get(metrics)), 0); + EXPECT_LT(DateToMillis(Cronet_Metrics_request_start_get(metrics)), + kDateOverrunThreshold); + EXPECT_GE(DateToMillis(Cronet_Metrics_request_end_get(metrics)), + DateToMillis(Cronet_Metrics_request_start_get(metrics))); + EXPECT_LT(DateToMillis(Cronet_Metrics_request_end_get(metrics)), + kDateOverrunThreshold); + + EXPECT_GE(DateToMillis(Cronet_Metrics_dns_start_get(metrics)), -1); + EXPECT_LT(DateToMillis(Cronet_Metrics_dns_start_get(metrics)), + kDateOverrunThreshold); + EXPECT_GE(DateToMillis(Cronet_Metrics_dns_end_get(metrics)), + DateToMillis(Cronet_Metrics_dns_start_get(metrics))); + EXPECT_LT(DateToMillis(Cronet_Metrics_dns_end_get(metrics)), + kDateOverrunThreshold); + + EXPECT_GE(DateToMillis(Cronet_Metrics_connect_start_get(metrics)), -1); + EXPECT_LT(DateToMillis(Cronet_Metrics_connect_start_get(metrics)), + kDateOverrunThreshold); + EXPECT_GE(DateToMillis(Cronet_Metrics_connect_end_get(metrics)), + DateToMillis(Cronet_Metrics_connect_start_get(metrics))); + EXPECT_LT(DateToMillis(Cronet_Metrics_connect_end_get(metrics)), + kDateOverrunThreshold); + + EXPECT_GE(DateToMillis(Cronet_Metrics_ssl_start_get(metrics)), -1); + EXPECT_LT(DateToMillis(Cronet_Metrics_ssl_start_get(metrics)), + kDateOverrunThreshold); + EXPECT_GE(DateToMillis(Cronet_Metrics_ssl_end_get(metrics)), + DateToMillis(Cronet_Metrics_ssl_start_get(metrics))); + EXPECT_LT(DateToMillis(Cronet_Metrics_ssl_end_get(metrics)), + kDateOverrunThreshold); + + EXPECT_GE(DateToMillis(Cronet_Metrics_sending_start_get(metrics)), -1); + EXPECT_LT(DateToMillis(Cronet_Metrics_sending_start_get(metrics)), + kDateOverrunThreshold); + EXPECT_GE(DateToMillis(Cronet_Metrics_sending_end_get(metrics)), + DateToMillis(Cronet_Metrics_sending_start_get(metrics))); + EXPECT_LT(DateToMillis(Cronet_Metrics_sending_end_get(metrics)), + kDateOverrunThreshold); + + EXPECT_GE(DateToMillis(Cronet_Metrics_push_start_get(metrics)), -1); + EXPECT_LT(DateToMillis(Cronet_Metrics_push_start_get(metrics)), + kDateOverrunThreshold); + EXPECT_GE(DateToMillis(Cronet_Metrics_push_end_get(metrics)), + DateToMillis(Cronet_Metrics_push_start_get(metrics))); + EXPECT_LT(DateToMillis(Cronet_Metrics_push_end_get(metrics)), + kDateOverrunThreshold); + + EXPECT_GE(DateToMillis(Cronet_Metrics_response_start_get(metrics)), -1); + EXPECT_LT(DateToMillis(Cronet_Metrics_response_start_get(metrics)), + kDateOverrunThreshold); + + EXPECT_GE(Cronet_Metrics_sent_byte_count_get(metrics), -1); + EXPECT_GE(Cronet_Metrics_received_byte_count_get(metrics), -1); + + // Verify order of events. + if (Cronet_Metrics_dns_start_get(metrics) != nullptr) { + EXPECT_GE(DateToMillis(Cronet_Metrics_dns_start_get(metrics)), + DateToMillis(Cronet_Metrics_request_start_get(metrics))); + } + + if (Cronet_Metrics_connect_start_get(metrics) != nullptr && + Cronet_Metrics_dns_end_get(metrics) != nullptr) { + EXPECT_GE(DateToMillis(Cronet_Metrics_connect_start_get(metrics)), + DateToMillis(Cronet_Metrics_dns_end_get(metrics))); + } + + if (Cronet_Metrics_ssl_start_get(metrics) != nullptr && + Cronet_Metrics_connect_start_get(metrics) != nullptr) { + EXPECT_GE(DateToMillis(Cronet_Metrics_ssl_start_get(metrics)), + DateToMillis(Cronet_Metrics_connect_start_get(metrics))); + } + + if (Cronet_Metrics_connect_end_get(metrics) != nullptr && + Cronet_Metrics_ssl_end_get(metrics) != nullptr) { + EXPECT_GE(DateToMillis(Cronet_Metrics_connect_end_get(metrics)), + DateToMillis(Cronet_Metrics_ssl_end_get(metrics))); + } + + if (Cronet_Metrics_sending_start_get(metrics) != nullptr && + Cronet_Metrics_connect_end_get(metrics) != nullptr) { + EXPECT_GE(DateToMillis(Cronet_Metrics_sending_start_get(metrics)), + DateToMillis(Cronet_Metrics_connect_end_get(metrics))); + } + + if (Cronet_Metrics_response_start_get(metrics) != nullptr && + Cronet_Metrics_sending_end_get(metrics) != nullptr) { + EXPECT_GE(DateToMillis(Cronet_Metrics_response_start_get(metrics)), + DateToMillis(Cronet_Metrics_sending_end_get(metrics))); + } + + if (Cronet_Metrics_response_start_get(metrics) != nullptr) { + EXPECT_GE(DateToMillis(Cronet_Metrics_request_end_get(metrics)), + DateToMillis(Cronet_Metrics_response_start_get(metrics))); + } +} + +// Convert a TestUrlRequestCallback::ResponseStep into the equivalent +// RequestFinishedInfo.FINISHED_REASON. +Cronet_RequestFinishedInfo_FINISHED_REASON MapFinishedReason( + TestUrlRequestCallback::ResponseStep response_step) { + switch (response_step) { + case TestUrlRequestCallback::ON_SUCCEEDED: + return Cronet_RequestFinishedInfo_FINISHED_REASON_SUCCEEDED; + case TestUrlRequestCallback::ON_FAILED: + return Cronet_RequestFinishedInfo_FINISHED_REASON_FAILED; + case TestUrlRequestCallback::ON_CANCELED: + return Cronet_RequestFinishedInfo_FINISHED_REASON_CANCELED; + default: + CHECK(false) << "Unknown TestUrlRequestCallback::ResponseStep: " + << response_step; + return Cronet_RequestFinishedInfo_FINISHED_REASON_FAILED; + } +} + +// Basic sanity checking of all Cronet_RequestFinishedInfo, +// Cronet_UrlResponseInfoPtr, and Cronet_ErrorPtr fields passed to +// RequestFinishedInfoListener.OnRequestFinished(). +// +// All fields are checked except for |annotations|. +// +// |test_request_finished_info_listener| Test listener -- will verify all fields +// of this listener. +// |callback| Callback associated with the UrlRequest associated with +// |request_info|. +void VerifyRequestFinishedInfoListener( + TestRequestFinishedInfoListener* test_request_finished_info_listener, + const TestUrlRequestCallback& callback) { + Cronet_RequestFinishedInfoPtr request_info = + test_request_finished_info_listener->request_finished_info(); + VerifyRequestMetrics(Cronet_RequestFinishedInfo_metrics_get(request_info)); + auto finished_reason = + Cronet_RequestFinishedInfo_finished_reason_get(request_info); + EXPECT_EQ(MapFinishedReason(callback.response_step_), finished_reason); + EXPECT_EQ(callback.original_response_info_, + test_request_finished_info_listener->url_response_info()); + EXPECT_EQ(callback.last_error_, test_request_finished_info_listener->error()); +} + +// Parameterized off whether to use a direct executor, and whether (if so, how) +// to add a RequestFinishedInfoListener. +class UrlRequestTest : public ::testing::TestWithParam< + std::tuple> { + public: + UrlRequestTest(const UrlRequestTest&) = delete; + UrlRequestTest& operator=(const UrlRequestTest&) = delete; + + protected: + UrlRequestTest() {} + ~UrlRequestTest() override {} + + void SetUp() override { EXPECT_TRUE(cronet::TestServer::Start()); } + + void TearDown() override { cronet::TestServer::Shutdown(); } + + bool GetDirectExecutorParam() { return std::get<0>(GetParam()); } + + RequestFinishedListenerType GetRequestFinishedListenerTypeParam() { + return std::get<1>(GetParam()); + } + + std::unique_ptr StartAndWaitForComplete( + const std::string& url, + std::unique_ptr test_callback, + const std::string& http_method, + TestUploadDataProvider* test_upload_data_provider, + int remapped_port) { + Cronet_EnginePtr engine = cronet::test::CreateTestEngine(remapped_port); + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = + Cronet_UrlRequestParams_Create(); + Cronet_UrlRequestParams_http_method_set(request_params, + http_method.c_str()); + Cronet_UploadDataProviderPtr upload_data_provider = nullptr; + + // Add upload data provider and set content type required for upload. + if (test_upload_data_provider != nullptr) { + test_upload_data_provider->set_url_request(request); + upload_data_provider = + test_upload_data_provider->CreateUploadDataProvider(); + Cronet_UrlRequestParams_upload_data_provider_set(request_params, + upload_data_provider); + Cronet_UrlRequestParams_upload_data_provider_executor_set( + request_params, test_upload_data_provider->executor()); + Cronet_HttpHeaderPtr header = Cronet_HttpHeader_Create(); + Cronet_HttpHeader_name_set(header, "Content-Type"); + Cronet_HttpHeader_value_set(header, "Useless/string"); + Cronet_UrlRequestParams_request_headers_add(request_params, header); + Cronet_HttpHeader_Destroy(header); + } + + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback->GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback->CreateUrlRequestCallback(); + + TestRequestFinishedInfoListener test_request_finished_info_listener; + MaybeAddRequestFinishedListener(request_params, engine, executor, + &test_request_finished_info_listener); + + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), + request_params, callback, executor); + + Cronet_UrlRequest_Start(request); + test_callback->WaitForDone(); + MaybeVerifyRequestFinishedInfo(&test_request_finished_info_listener, + *test_callback); + CleanupRequestFinishedListener(request_params, engine); + // Wait for all posted tasks to be executed to ensure there is no unhandled + // exception. + test_callback->ShutdownExecutor(); + EXPECT_TRUE(test_callback->IsDone()); + EXPECT_TRUE(Cronet_UrlRequest_IsDone(request)); + if (upload_data_provider != nullptr) + Cronet_UploadDataProvider_Destroy(upload_data_provider); + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequest_Destroy(request); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); + return test_callback; + } + + std::unique_ptr StartAndWaitForComplete( + const std::string& url, + std::unique_ptr test_callback, + const std::string& http_method, + TestUploadDataProvider* test_upload_data_provider) { + return StartAndWaitForComplete(url, std::move(test_callback), http_method, + test_upload_data_provider, + /* remapped_port = */ 0); + } + + std::unique_ptr StartAndWaitForComplete( + const std::string& url, + std::unique_ptr test_callback) { + return StartAndWaitForComplete(url, std::move(test_callback), + /* http_method = */ std::string(), + /* upload_data_provider = */ nullptr); + } + + std::unique_ptr StartAndWaitForComplete( + const std::string& url) { + return StartAndWaitForComplete( + url, + std::make_unique(GetDirectExecutorParam())); + } + + void CheckResponseInfo( + const TestUrlRequestCallback::UrlResponseInfo& response_info, + const std::string& expected_url, + int expected_http_status_code, + const std::string& expected_http_status_text) { + EXPECT_EQ(expected_url, response_info.url); + EXPECT_EQ(expected_url, response_info.url_chain.back()); + EXPECT_EQ(expected_http_status_code, response_info.http_status_code); + EXPECT_EQ(expected_http_status_text, response_info.http_status_text); + EXPECT_FALSE(response_info.was_cached); + } + + void ExpectResponseInfoEquals( + const TestUrlRequestCallback::UrlResponseInfo& expected, + const TestUrlRequestCallback::UrlResponseInfo& actual) { + EXPECT_EQ(expected.url, actual.url); + EXPECT_EQ(expected.url_chain, actual.url_chain); + EXPECT_EQ(expected.http_status_code, actual.http_status_code); + EXPECT_EQ(expected.http_status_text, actual.http_status_text); + EXPECT_EQ(expected.all_headers, actual.all_headers); + EXPECT_EQ(expected.was_cached, actual.was_cached); + EXPECT_EQ(expected.negotiated_protocol, actual.negotiated_protocol); + EXPECT_EQ(expected.proxy_server, actual.proxy_server); + EXPECT_EQ(expected.received_byte_count, actual.received_byte_count); + } + + // Depending on the test parameterization, adds a RequestFinishedInfoListener + // to the Engine or UrlRequest, or does nothing. + // + // This method should be called before the call to + // Cronet_UrlRequest_InitWithParams(). + void MaybeAddRequestFinishedListener( + Cronet_UrlRequestParamsPtr url_request_params, + Cronet_EnginePtr engine, + Cronet_ExecutorPtr executor, + TestRequestFinishedInfoListener* test_request_finished_info_listener) { + auto request_finished_listener_type = GetRequestFinishedListenerTypeParam(); + + if (request_finished_listener_type == + RequestFinishedListenerType::kNoListener) + return; + + request_finished_listener_ = + test_request_finished_info_listener->CreateRequestFinishedListener(); + + switch (request_finished_listener_type) { + case RequestFinishedListenerType::kUrlRequestListener: + Cronet_UrlRequestParams_request_finished_listener_set( + url_request_params, request_finished_listener_); + Cronet_UrlRequestParams_request_finished_executor_set( + url_request_params, executor); + break; + case RequestFinishedListenerType::kEngineListener: + Cronet_Engine_AddRequestFinishedListener( + engine, request_finished_listener_, executor); + break; + default: + NOTREACHED(); + } + } + + // Cleans up any leftover resources from MaybeAddRequestFinishedListener(). + // + // NOTE: It's only necessary to call this method if + // MaybeAddRequestFinishedListener() is called multiple times in a test case + // (like in a loop). + void CleanupRequestFinishedListener( + Cronet_UrlRequestParamsPtr url_request_params, + Cronet_EnginePtr engine) { + auto request_finished_listener_type = GetRequestFinishedListenerTypeParam(); + if (request_finished_listener_type == + RequestFinishedListenerType::kEngineListener) { + Cronet_Engine_RemoveRequestFinishedListener(engine, + request_finished_listener_); + } + Cronet_UrlRequestParams_request_finished_listener_set(url_request_params, + nullptr); + Cronet_UrlRequestParams_request_finished_executor_set(url_request_params, + nullptr); + } + + // TestRequestFinishedInfoListener.WaitForDone() is called and checks are + // performed only if a RequestFinishedInfoListener is registered. + // + // This method should be called after TestUrlRequestCallback.WaitForDone(). + void MaybeVerifyRequestFinishedInfo( + TestRequestFinishedInfoListener* test_request_finished_info_listener, + const TestUrlRequestCallback& callback) { + if (GetRequestFinishedListenerTypeParam() == + RequestFinishedListenerType::kNoListener) + return; + test_request_finished_info_listener->WaitForDone(); + VerifyRequestFinishedInfoListener(test_request_finished_info_listener, + callback); + } + + void TestCancel(TestUrlRequestCallback::FailureType failure_type, + TestUrlRequestCallback::ResponseStep failure_step, + bool expect_response_info, + bool expect_error); + + protected: + // Provide a task environment for use by TestExecutor instances. Do not + // initialize the ThreadPool as this is done by the Cronet_Engine + base::test::SingleThreadTaskEnvironment task_environment_; + + // Not owned, |request_finished_listener_| destroys itself when run. This + // pointer is only needed to unregister the listener from the Engine in + // CleanupRequestFinishedListener() and to allow tests that never run the + // |request_finished_listener_| to be able to destroy it. + Cronet_RequestFinishedInfoListenerPtr request_finished_listener_ = nullptr; +}; + +const bool kDirectExecutorEnabled[]{true, false}; +INSTANTIATE_TEST_SUITE_P( + NoRequestFinishedListener, + UrlRequestTest, + testing::Combine( + testing::ValuesIn(kDirectExecutorEnabled), + testing::Values(RequestFinishedListenerType::kNoListener))); +INSTANTIATE_TEST_SUITE_P( + RequestFinishedListenerOnUrlRequest, + UrlRequestTest, + testing::Combine( + testing::ValuesIn(kDirectExecutorEnabled), + testing::Values(RequestFinishedListenerType::kUrlRequestListener))); +INSTANTIATE_TEST_SUITE_P( + RequestFinishedListenerOnEngine, + UrlRequestTest, + testing::Combine( + testing::ValuesIn(kDirectExecutorEnabled), + testing::Values(RequestFinishedListenerType::kEngineListener))); + +TEST_P(UrlRequestTest, InitChecks) { + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_EnginePtr engine = Cronet_Engine_Create(); + // Disable runtime CHECK of the result, so it could be verified. + Cronet_EngineParams_enable_check_result_set(engine_params, false); + EXPECT_EQ(Cronet_RESULT_SUCCESS, + Cronet_Engine_StartWithParams(engine, engine_params)); + Cronet_EngineParams_Destroy(engine_params); + + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + const std::string url = cronet::TestServer::GetEchoMethodURL(); + + TestUrlRequestCallback test_callback(GetDirectExecutorParam()); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + TestRequestFinishedInfoListener test_request_finished_info_listener; + MaybeAddRequestFinishedListener(request_params, engine, executor, + &test_request_finished_info_listener); + EXPECT_EQ(Cronet_RESULT_NULL_POINTER_URL, + Cronet_UrlRequest_InitWithParams( + request, engine, /* url = */ nullptr, + /* request_params = */ nullptr, /* callback = */ nullptr, + /* executor = */ nullptr)); + Cronet_UrlRequest_Destroy(request); + + request = Cronet_UrlRequest_Create(); + EXPECT_EQ(Cronet_RESULT_NULL_POINTER_PARAMS, + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), + /* request_params = */ nullptr, + /* callback = */ nullptr, + /* executor = */ nullptr)); + Cronet_UrlRequest_Destroy(request); + + request = Cronet_UrlRequest_Create(); + EXPECT_EQ(Cronet_RESULT_NULL_POINTER_CALLBACK, + Cronet_UrlRequest_InitWithParams( + request, engine, url.c_str(), request_params, + /* callback = */ nullptr, /* executor = */ nullptr)); + Cronet_UrlRequest_Destroy(request); + + request = Cronet_UrlRequest_Create(); + EXPECT_EQ(Cronet_RESULT_NULL_POINTER_EXECUTOR, + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), + request_params, callback, + /* executor = */ nullptr)); + Cronet_UrlRequest_Destroy(request); + + request = Cronet_UrlRequest_Create(); + EXPECT_EQ(Cronet_RESULT_NULL_POINTER_EXECUTOR, + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), + request_params, callback, + /* executor = */ nullptr)); + Cronet_UrlRequest_Destroy(request); + + request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParams_http_method_set(request_params, "bad:method"); + EXPECT_EQ( + Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_HTTP_METHOD, + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), + request_params, callback, executor)); + Cronet_UrlRequest_Destroy(request); + + request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParams_http_method_set(request_params, "HEAD"); + Cronet_UrlRequestParams_priority_set( + request_params, + Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_IDLE); + // Check header validation + Cronet_HttpHeaderPtr http_header = Cronet_HttpHeader_Create(); + Cronet_UrlRequestParams_request_headers_add(request_params, http_header); + EXPECT_EQ( + Cronet_RESULT_NULL_POINTER_HEADER_NAME, + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), + request_params, callback, executor)); + Cronet_UrlRequestParams_request_headers_clear(request_params); + Cronet_UrlRequest_Destroy(request); + + request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParams_priority_set( + request_params, + Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_LOWEST); + Cronet_HttpHeader_name_set(http_header, "bad:name"); + Cronet_UrlRequestParams_request_headers_add(request_params, http_header); + EXPECT_EQ( + Cronet_RESULT_NULL_POINTER_HEADER_VALUE, + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), + request_params, callback, executor)); + Cronet_UrlRequestParams_request_headers_clear(request_params); + Cronet_UrlRequest_Destroy(request); + + request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParams_priority_set( + request_params, + Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_LOW); + Cronet_HttpHeader_value_set(http_header, "header value"); + Cronet_UrlRequestParams_request_headers_add(request_params, http_header); + EXPECT_EQ( + Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_HTTP_HEADER, + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), + request_params, callback, executor)); + Cronet_UrlRequestParams_request_headers_clear(request_params); + Cronet_UrlRequest_Destroy(request); + + request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParams_priority_set( + request_params, + Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_HIGHEST); + Cronet_HttpHeader_name_set(http_header, "header-name"); + Cronet_UrlRequestParams_request_headers_add(request_params, http_header); + EXPECT_EQ(Cronet_RESULT_SUCCESS, Cronet_UrlRequest_InitWithParams( + request, engine, url.c_str(), + request_params, callback, executor)); + EXPECT_EQ( + Cronet_RESULT_ILLEGAL_STATE_REQUEST_ALREADY_INITIALIZED, + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), + request_params, callback, executor)); + Cronet_HttpHeader_Destroy(http_header); + Cronet_UrlRequest_Destroy(request); + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); + if (request_finished_listener_ != nullptr) { + // This test never actually runs |request_finished_listener_|, so we delete + // it here. + Cronet_RequestFinishedInfoListener_Destroy(request_finished_listener_); + } +} + +TEST_P(UrlRequestTest, SimpleGet) { + const std::string url = cronet::TestServer::GetEchoMethodURL(); + auto callback = StartAndWaitForComplete(url); + EXPECT_EQ(200, callback->response_info_->http_status_code); + // Default method is 'GET'. + EXPECT_EQ("GET", callback->response_as_string_); + EXPECT_EQ(0, callback->redirect_count_); + EXPECT_EQ(callback->response_step_, callback->ON_SUCCEEDED); + CheckResponseInfo(*callback->response_info_, url, 200, "OK"); + TestUrlRequestCallback::UrlResponseInfo expected_response_info( + std::vector({url}), "OK", 200, 86, + std::vector({"Connection", "close", "Content-Length", "3", + "Content-Type", "text/plain"})); + ExpectResponseInfoEquals(expected_response_info, *callback->response_info_); +} + +TEST_P(UrlRequestTest, UploadEmptyBodySync) { + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + /* executor = */ nullptr); + auto callback = + std::make_unique(GetDirectExecutorParam()); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(0, data_provider.GetUploadedLength()); + EXPECT_EQ(0, data_provider.num_read_calls()); + EXPECT_EQ(0, data_provider.num_rewind_calls()); + EXPECT_EQ(200, callback->response_info_->http_status_code); + EXPECT_EQ("", callback->response_as_string_); +} + +TEST_P(UrlRequestTest, UploadSync) { + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + /* executor = */ nullptr); + data_provider.AddRead("Test"); + auto callback = + std::make_unique(GetDirectExecutorParam()); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(4, data_provider.GetUploadedLength()); + EXPECT_EQ(1, data_provider.num_read_calls()); + EXPECT_EQ(0, data_provider.num_rewind_calls()); + EXPECT_EQ(200, callback->response_info_->http_status_code); + EXPECT_EQ("Test", callback->response_as_string_); +} + +TEST_P(UrlRequestTest, SSLCertificateError) { + net::EmbeddedTestServer ssl_server(net::EmbeddedTestServer::TYPE_HTTPS); + ssl_server.SetSSLConfig(net::EmbeddedTestServer::CERT_EXPIRED); + ASSERT_TRUE(ssl_server.Start()); + + const std::string url = ssl_server.GetURL("/").spec(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + /* executor = */ nullptr); + data_provider.AddRead("Test"); + auto callback = + std::make_unique(GetDirectExecutorParam()); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(4, data_provider.GetUploadedLength()); + EXPECT_EQ(0, data_provider.num_read_calls()); + EXPECT_EQ(0, data_provider.num_rewind_calls()); + EXPECT_EQ(nullptr, callback->response_info_); + EXPECT_EQ("", callback->response_as_string_); + EXPECT_EQ("net::ERR_CERT_INVALID", callback->last_error_message_); +} + +TEST_P(UrlRequestTest, SSLUpload) { + net::EmbeddedTestServer ssl_server(net::EmbeddedTestServer::TYPE_HTTPS); + net::test_server::RegisterDefaultHandlers(&ssl_server); + ASSERT_TRUE(ssl_server.Start()); + + constexpr char kUrl[] = "https://test.example.com/echoall"; + constexpr char kUploadString[] = + "The quick brown fox jumps over the lazy dog."; + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + /* executor = */ nullptr); + data_provider.AddRead(kUploadString); + auto callback = + std::make_unique(GetDirectExecutorParam()); + callback = StartAndWaitForComplete(kUrl, std::move(callback), std::string(), + &data_provider, ssl_server.port()); + data_provider.AssertClosed(); + EXPECT_NE(nullptr, callback->response_info_); + EXPECT_EQ("", callback->last_error_message_); + EXPECT_EQ(200, callback->response_info_->http_status_code); + EXPECT_THAT(callback->response_as_string_, HasSubstr(kUploadString)); +} + +TEST_P(UrlRequestTest, UploadMultiplePiecesSync) { + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + auto callback = + std::make_unique(GetDirectExecutorParam()); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + callback->GetExecutor()); + data_provider.AddRead("Y"); + data_provider.AddRead("et "); + data_provider.AddRead("another "); + data_provider.AddRead("test"); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(16, data_provider.GetUploadedLength()); + EXPECT_EQ(4, data_provider.num_read_calls()); + EXPECT_EQ(0, data_provider.num_rewind_calls()); + EXPECT_EQ(200, callback->response_info_->http_status_code); + EXPECT_EQ("Yet another test", callback->response_as_string_); +} + +TEST_P(UrlRequestTest, UploadMultiplePiecesAsync) { + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + auto callback = + std::make_unique(GetDirectExecutorParam()); + TestUploadDataProvider data_provider(TestUploadDataProvider::ASYNC, + callback->GetExecutor()); + data_provider.AddRead("Y"); + data_provider.AddRead("et "); + data_provider.AddRead("another "); + data_provider.AddRead("test"); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(16, data_provider.GetUploadedLength()); + EXPECT_EQ(4, data_provider.num_read_calls()); + EXPECT_EQ(0, data_provider.num_rewind_calls()); + EXPECT_EQ(200, callback->response_info_->http_status_code); + EXPECT_EQ("Yet another test", callback->response_as_string_); +} + +TEST_P(UrlRequestTest, UploadChangesDefaultMethod) { + const std::string url = cronet::TestServer::GetEchoMethodURL(); + TestUploadDataProvider upload_data_provider(TestUploadDataProvider::SYNC, + /* executor = */ nullptr); + upload_data_provider.AddRead("Test"); + auto callback = + std::make_unique(GetDirectExecutorParam()); + + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &upload_data_provider); + EXPECT_EQ(200, callback->response_info_->http_status_code); + // Setting upload provider should change method to 'POST'. + EXPECT_EQ("POST", callback->response_as_string_); +} + +TEST_P(UrlRequestTest, UploadWithSetMethod) { + const std::string url = cronet::TestServer::GetEchoMethodURL(); + TestUploadDataProvider upload_data_provider(TestUploadDataProvider::SYNC, + /* executor = */ nullptr); + upload_data_provider.AddRead("Test"); + auto callback = + std::make_unique(GetDirectExecutorParam()); + + callback = StartAndWaitForComplete(url, std::move(callback), + std::string("PUT"), &upload_data_provider); + EXPECT_EQ(200, callback->response_info_->http_status_code); + // Setting upload provider should change method to 'POST'. + EXPECT_EQ("PUT", callback->response_as_string_); +} + +TEST_P(UrlRequestTest, UploadWithBigRead) { + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + TestUploadDataProvider upload_data_provider(TestUploadDataProvider::SYNC, + /* executor = */ nullptr); + // Use reads that match exact size of read buffer, which is 16384 bytes. + upload_data_provider.AddRead(std::string(16384, 'a')); + upload_data_provider.AddRead(std::string(32768 - 16384, 'a')); + auto callback = + std::make_unique(GetDirectExecutorParam()); + + callback = StartAndWaitForComplete(url, std::move(callback), + std::string("PUT"), &upload_data_provider); + EXPECT_EQ(200, callback->response_info_->http_status_code); + // Confirm that body is uploaded correctly. + EXPECT_EQ(std::string(32768, 'a'), callback->response_as_string_); +} + +TEST_P(UrlRequestTest, UploadWithDirectExecutor) { + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + auto callback = std::make_unique(true); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + callback->GetExecutor()); + data_provider.AddRead("Test"); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(4, data_provider.GetUploadedLength()); + EXPECT_EQ(1, data_provider.num_read_calls()); + EXPECT_EQ(0, data_provider.num_rewind_calls()); + EXPECT_EQ(200, callback->response_info_->http_status_code); + EXPECT_EQ("Test", callback->response_as_string_); +} + +TEST_P(UrlRequestTest, UploadRedirectSync) { + const std::string url = cronet::TestServer::GetRedirectToEchoBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + /* executor = */ nullptr); + data_provider.AddRead("Test"); + auto callback = + std::make_unique(GetDirectExecutorParam()); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(4, data_provider.GetUploadedLength()); + EXPECT_EQ(2, data_provider.num_read_calls()); + EXPECT_EQ(1, data_provider.num_rewind_calls()); + EXPECT_EQ(200, callback->response_info_->http_status_code); + EXPECT_EQ("Test", callback->response_as_string_); +} + +TEST_P(UrlRequestTest, UploadRedirectAsync) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetRedirectToEchoBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::ASYNC, + callback->GetExecutor()); + data_provider.AddRead("Test"); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(4, data_provider.GetUploadedLength()); + EXPECT_EQ(2, data_provider.num_read_calls()); + EXPECT_EQ(1, data_provider.num_rewind_calls()); + EXPECT_EQ(200, callback->response_info_->http_status_code); + EXPECT_EQ("Test", callback->response_as_string_); +} + +TEST_P(UrlRequestTest, UploadWithBadLength) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + callback->GetExecutor()); + data_provider.set_bad_length(1ll); + data_provider.AddRead("12"); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(2, data_provider.GetUploadedLength()); + EXPECT_EQ(1, data_provider.num_read_calls()); + EXPECT_EQ(0, data_provider.num_rewind_calls()); + EXPECT_EQ(nullptr, callback->response_info_); + EXPECT_NE(nullptr, callback->last_error_); + EXPECT_EQ(Cronet_Error_ERROR_CODE_ERROR_CALLBACK, callback->last_error_code_); + EXPECT_EQ(0ul, callback->last_error_message_.find( + "Failure from UploadDataProvider")); + EXPECT_NE(std::string::npos, + callback->last_error_message_.find( + "Read upload data length 2 exceeds expected length 1")); +} + +TEST_P(UrlRequestTest, UploadWithBadLengthBufferAligned) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + callback->GetExecutor()); + data_provider.set_bad_length(8191ll); + // Add 8192 bytes to read. + for (int i = 0; i < 512; ++i) + data_provider.AddRead("0123456789abcdef"); + + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(8192, data_provider.GetUploadedLength()); + EXPECT_EQ(512, data_provider.num_read_calls()); + EXPECT_EQ(0, data_provider.num_rewind_calls()); + EXPECT_EQ(nullptr, callback->response_info_); + EXPECT_NE(nullptr, callback->last_error_); + EXPECT_EQ(Cronet_Error_ERROR_CODE_ERROR_CALLBACK, callback->last_error_code_); + EXPECT_EQ(0ul, callback->last_error_message_.find( + "Failure from UploadDataProvider")); + EXPECT_NE(std::string::npos, + callback->last_error_message_.find( + "Read upload data length 8192 exceeds expected length 8191")); +} + +TEST_P(UrlRequestTest, UploadReadFailSync) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + callback->GetExecutor()); + data_provider.SetReadFailure(0, TestUploadDataProvider::CALLBACK_SYNC); + data_provider.AddRead("Test"); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(4, data_provider.GetUploadedLength()); + EXPECT_EQ(1, data_provider.num_read_calls()); + EXPECT_EQ(0, data_provider.num_rewind_calls()); + EXPECT_EQ(nullptr, callback->response_info_); + EXPECT_NE(nullptr, callback->last_error_); + EXPECT_EQ(Cronet_Error_ERROR_CODE_ERROR_CALLBACK, callback->last_error_code_); + EXPECT_EQ(0ul, callback->last_error_message_.find( + "Failure from UploadDataProvider")); + EXPECT_NE(std::string::npos, + callback->last_error_message_.find("Sync read failure")); +} + +TEST_P(UrlRequestTest, UploadReadFailAsync) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + callback->GetExecutor()); + data_provider.SetReadFailure(0, TestUploadDataProvider::CALLBACK_ASYNC); + data_provider.AddRead("Test"); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(4, data_provider.GetUploadedLength()); + EXPECT_EQ(1, data_provider.num_read_calls()); + EXPECT_EQ(0, data_provider.num_rewind_calls()); + EXPECT_EQ(nullptr, callback->response_info_); + EXPECT_NE(nullptr, callback->last_error_); + EXPECT_EQ(Cronet_Error_ERROR_CODE_ERROR_CALLBACK, callback->last_error_code_); + EXPECT_EQ(0ul, callback->last_error_message_.find( + "Failure from UploadDataProvider")); + EXPECT_NE(std::string::npos, + callback->last_error_message_.find("Async read failure")); +} + +TEST_P(UrlRequestTest, UploadRewindFailSync) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetRedirectToEchoBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + callback->GetExecutor()); + data_provider.SetRewindFailure(TestUploadDataProvider::CALLBACK_SYNC); + data_provider.AddRead("Test"); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(4, data_provider.GetUploadedLength()); + EXPECT_EQ(1, data_provider.num_read_calls()); + EXPECT_EQ(1, data_provider.num_rewind_calls()); + EXPECT_NE(nullptr, callback->last_error_); + EXPECT_EQ(Cronet_Error_ERROR_CODE_ERROR_CALLBACK, callback->last_error_code_); + EXPECT_EQ(0ul, callback->last_error_message_.find( + "Failure from UploadDataProvider")); + EXPECT_NE(std::string::npos, + callback->last_error_message_.find("Sync rewind failure")); +} + +TEST_P(UrlRequestTest, UploadRewindFailAsync) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetRedirectToEchoBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + callback->GetExecutor()); + data_provider.SetRewindFailure(TestUploadDataProvider::CALLBACK_ASYNC); + data_provider.AddRead("Test"); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(4, data_provider.GetUploadedLength()); + EXPECT_EQ(1, data_provider.num_read_calls()); + EXPECT_EQ(1, data_provider.num_rewind_calls()); + EXPECT_NE(nullptr, callback->last_error_); + EXPECT_EQ(Cronet_Error_ERROR_CODE_ERROR_CALLBACK, callback->last_error_code_); + EXPECT_EQ(0ul, callback->last_error_message_.find( + "Failure from UploadDataProvider")); + EXPECT_NE(std::string::npos, + callback->last_error_message_.find("Async rewind failure")); +} + +TEST_P(UrlRequestTest, UploadChunked) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + callback->GetExecutor()); + data_provider.AddRead("Test Hello"); + data_provider.set_chunked(true); + EXPECT_EQ(-1, data_provider.GetLength()); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(-1, data_provider.GetUploadedLength()); + EXPECT_EQ(1, data_provider.num_read_calls()); + EXPECT_EQ(200, callback->response_info_->http_status_code); + EXPECT_EQ("Test Hello", callback->response_as_string_); +} + +TEST_P(UrlRequestTest, UploadChunkedLastReadZeroLengthBody) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + callback->GetExecutor()); + // Add 3 reads. The last read has a 0-length body. + data_provider.AddRead("hello there"); + data_provider.AddRead("!"); + data_provider.AddRead(""); + data_provider.set_chunked(true); + EXPECT_EQ(-1, data_provider.GetLength()); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(-1, data_provider.GetUploadedLength()); + // 2 read call for the first two data chunks, and 1 for final chunk. + EXPECT_EQ(3, data_provider.num_read_calls()); + EXPECT_EQ(200, callback->response_info_->http_status_code); + EXPECT_EQ("hello there!", callback->response_as_string_); +} + +// Test where an upload fails without ever initializing the +// UploadDataStream, because it can't connect to the server. +TEST_P(UrlRequestTest, UploadFailsWithoutInitializingStream) { + // The port for PTP will always refuse a TCP connection + const std::string url = "http://127.0.0.1:319"; + TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC, + /* executor = */ nullptr); + data_provider.AddRead("Test"); + auto callback = + std::make_unique(GetDirectExecutorParam()); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(0, data_provider.num_read_calls()); + EXPECT_EQ(0, data_provider.num_rewind_calls()); + EXPECT_EQ(nullptr, callback->response_info_); + EXPECT_EQ("", callback->response_as_string_); + EXPECT_TRUE(callback->on_error_called_); +} + +// TODO(https://crbug.com/954372): Flakes in AssertClosed(). +TEST_P(UrlRequestTest, DISABLED_UploadCancelReadSync) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::ASYNC, + callback->GetExecutor()); + data_provider.AddRead("One"); + data_provider.AddRead("Two"); + data_provider.AddRead("Three"); + data_provider.SetReadCancel(1, TestUploadDataProvider::CANCEL_SYNC); + data_provider.SetReadFailure(1, TestUploadDataProvider::CALLBACK_ASYNC); + + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + + EXPECT_EQ(11, data_provider.GetUploadedLength()); + EXPECT_EQ(2, data_provider.num_read_calls()); + EXPECT_TRUE(callback->on_canceled_called_); +} + +TEST_P(UrlRequestTest, UploadCancelReadAsync) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetEchoRequestBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::ASYNC, + callback->GetExecutor()); + data_provider.AddRead("One"); + data_provider.AddRead("Two"); + data_provider.AddRead("Three"); + data_provider.SetReadCancel(2, TestUploadDataProvider::CANCEL_ASYNC); + + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + + EXPECT_EQ(11, data_provider.GetUploadedLength()); + EXPECT_EQ(3, data_provider.num_read_calls()); + EXPECT_TRUE(callback->on_canceled_called_); +} + +// TODO(https://crbug.com/954372): Flakes in AssertClosed(). +TEST_P(UrlRequestTest, DISABLED_UploadCancelRewindSync) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetRedirectToEchoBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::ASYNC, + callback->GetExecutor()); + data_provider.SetRewindCancel(TestUploadDataProvider::CANCEL_SYNC); + data_provider.SetRewindFailure(TestUploadDataProvider::CALLBACK_ASYNC); + data_provider.AddRead("Test"); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(4, data_provider.GetUploadedLength()); + EXPECT_EQ(1, data_provider.num_read_calls()); + EXPECT_EQ(1, data_provider.num_rewind_calls()); + EXPECT_TRUE(callback->on_canceled_called_); +} + +TEST_P(UrlRequestTest, UploadCancelRewindAsync) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + const std::string url = cronet::TestServer::GetRedirectToEchoBodyURL(); + TestUploadDataProvider data_provider(TestUploadDataProvider::ASYNC, + callback->GetExecutor()); + data_provider.SetRewindCancel(TestUploadDataProvider::CANCEL_ASYNC); + data_provider.AddRead("Test"); + callback = StartAndWaitForComplete(url, std::move(callback), std::string(), + &data_provider); + data_provider.AssertClosed(); + EXPECT_EQ(4, data_provider.GetUploadedLength()); + EXPECT_EQ(1, data_provider.num_read_calls()); + EXPECT_EQ(1, data_provider.num_rewind_calls()); + EXPECT_TRUE(callback->on_canceled_called_); +} + +TEST_P(UrlRequestTest, SimpleRequest) { + Cronet_EnginePtr engine = cronet::test::CreateTestEngine(0); + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + std::string url = cronet::TestServer::GetSimpleURL(); + + TestUrlRequestCallback test_callback(GetDirectExecutorParam()); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + TestRequestFinishedInfoListener test_request_finished_info_listener; + MaybeAddRequestFinishedListener(request_params, engine, executor, + &test_request_finished_info_listener); + + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), request_params, + callback, executor); + + Cronet_UrlRequest_Start(request); + + test_callback.WaitForDone(); + MaybeVerifyRequestFinishedInfo(&test_request_finished_info_listener, + test_callback); + EXPECT_TRUE(test_callback.IsDone()); + ASSERT_EQ("The quick brown fox jumps over the lazy dog.", + test_callback.response_as_string_); + + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequest_Destroy(request); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); +} + +TEST_P(UrlRequestTest, ReceiveBackAnnotations) { + Cronet_EnginePtr engine = cronet::test::CreateTestEngine(0); + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + std::string url = cronet::TestServer::GetSimpleURL(); + + TestUrlRequestCallback test_callback(GetDirectExecutorParam()); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + TestRequestFinishedInfoListener test_request_finished_info_listener; + MaybeAddRequestFinishedListener(request_params, engine, executor, + &test_request_finished_info_listener); + + int object_to_annotate = 0; + Cronet_UrlRequestParams_annotations_add(request_params, &object_to_annotate); + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), request_params, + callback, executor); + + Cronet_UrlRequest_Start(request); + + test_callback.WaitForDone(); + MaybeVerifyRequestFinishedInfo(&test_request_finished_info_listener, + test_callback); + EXPECT_TRUE(test_callback.IsDone()); + if (GetRequestFinishedListenerTypeParam() != + RequestFinishedListenerType::kNoListener) { + ASSERT_EQ(1u, + Cronet_RequestFinishedInfo_annotations_size( + test_request_finished_info_listener.request_finished_info())); + EXPECT_EQ( + &object_to_annotate, + Cronet_RequestFinishedInfo_annotations_at( + test_request_finished_info_listener.request_finished_info(), 0)); + } + + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequest_Destroy(request); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); +} + +TEST_P(UrlRequestTest, UrlParamsAnnotationsUnchanged) { + Cronet_EnginePtr engine = cronet::test::CreateTestEngine(0); + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + std::string url = cronet::TestServer::GetSimpleURL(); + + TestUrlRequestCallback test_callback(GetDirectExecutorParam()); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + TestRequestFinishedInfoListener test_request_finished_info_listener; + MaybeAddRequestFinishedListener(request_params, engine, executor, + &test_request_finished_info_listener); + + int object_to_annotate = 0; + Cronet_UrlRequestParams_annotations_add(request_params, &object_to_annotate); + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), request_params, + callback, executor); + ASSERT_EQ(1u, Cronet_UrlRequestParams_annotations_size(request_params)); + EXPECT_EQ(&object_to_annotate, + Cronet_UrlRequestParams_annotations_at(request_params, 0)); + EXPECT_EQ(0, object_to_annotate); + + if (request_finished_listener_ != nullptr) { + // This test never actually runs |request_finished_listener_|, so we delete + // it here. + Cronet_RequestFinishedInfoListener_Destroy(request_finished_listener_); + } + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequest_Destroy(request); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); +} + +TEST_P(UrlRequestTest, MultiRedirect) { + const std::string url = cronet::TestServer::GetMultiRedirectURL(); + auto callback = StartAndWaitForComplete(url); + EXPECT_EQ(2, callback->redirect_count_); + EXPECT_EQ(200, callback->response_info_->http_status_code); + EXPECT_EQ(2ul, callback->redirect_response_info_list_.size()); + EXPECT_EQ(2ul, callback->redirect_url_list_.size()); + + // Check first redirect (multiredirect.html -> redirect.html). + TestUrlRequestCallback::UrlResponseInfo first_expected_response_info( + std::vector({url}), "Found", 302, 76, + std::vector( + {"Location", GURL(cronet::TestServer::GetRedirectURL()).path(), + "redirect-header0", "header-value"})); + ExpectResponseInfoEquals(first_expected_response_info, + *callback->redirect_response_info_list_.front()); + EXPECT_EQ(cronet::TestServer::GetRedirectURL(), + callback->redirect_url_list_.front()); + + // Check second redirect (redirect.html -> success.txt). + TestUrlRequestCallback::UrlResponseInfo second_expected_response_info( + std::vector({cronet::TestServer::GetMultiRedirectURL(), + cronet::TestServer::GetRedirectURL()}), + "Found", 302, 149, + std::vector( + {"Location", GURL(cronet::TestServer::GetSuccessURL()).path(), + "redirect-header", "header-value"})); + ExpectResponseInfoEquals(second_expected_response_info, + *callback->redirect_response_info_list_.back()); + EXPECT_EQ(cronet::TestServer::GetSuccessURL(), + callback->redirect_url_list_.back()); + + // Check final response (success.txt). + TestUrlRequestCallback::UrlResponseInfo final_expected_response_info( + std::vector({cronet::TestServer::GetMultiRedirectURL(), + cronet::TestServer::GetRedirectURL(), + cronet::TestServer::GetSuccessURL()}), + "OK", 200, 334, + std::vector( + {"Content-Type", "text/plain", "Access-Control-Allow-Origin", "*", + "header-name", "header-value", "multi-header-name", "header-value1", + "multi-header-name", "header-value2"})); + ExpectResponseInfoEquals(final_expected_response_info, + *callback->response_info_); + EXPECT_NE(0, callback->response_data_length_); + EXPECT_EQ(callback->ON_SUCCEEDED, callback->response_step_); +} + +TEST_P(UrlRequestTest, CancelRequest) { + Cronet_EnginePtr engine = cronet::test::CreateTestEngine(0); + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + std::string url = cronet::TestServer::GetSimpleURL(); + + TestUrlRequestCallback test_callback(GetDirectExecutorParam()); + test_callback.set_failure(test_callback.CANCEL_SYNC, + test_callback.ON_RESPONSE_STARTED); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + TestRequestFinishedInfoListener test_request_finished_info_listener; + MaybeAddRequestFinishedListener(request_params, engine, executor, + &test_request_finished_info_listener); + + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), request_params, + callback, executor); + + Cronet_UrlRequest_Start(request); + + test_callback.WaitForDone(); + MaybeVerifyRequestFinishedInfo(&test_request_finished_info_listener, + test_callback); + EXPECT_TRUE(test_callback.IsDone()); + EXPECT_TRUE(test_callback.on_canceled_called_); + ASSERT_FALSE(test_callback.on_error_called_); + EXPECT_TRUE(test_callback.response_as_string_.empty()); + + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequest_Destroy(request); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); +} + +TEST_P(UrlRequestTest, FailedRequestHostNotFound) { + Cronet_EnginePtr engine = cronet::test::CreateTestEngine(0); + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + std::string url = "https://notfound.example.com"; + + TestUrlRequestCallback test_callback(GetDirectExecutorParam()); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + TestRequestFinishedInfoListener test_request_finished_info_listener; + MaybeAddRequestFinishedListener(request_params, engine, executor, + &test_request_finished_info_listener); + + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), request_params, + callback, executor); + + Cronet_UrlRequest_Start(request); + + test_callback.WaitForDone(); + MaybeVerifyRequestFinishedInfo(&test_request_finished_info_listener, + test_callback); + EXPECT_TRUE(test_callback.IsDone()); + EXPECT_TRUE(test_callback.on_error_called_); + EXPECT_FALSE(test_callback.on_canceled_called_); + + EXPECT_TRUE(test_callback.response_as_string_.empty()); + EXPECT_EQ(nullptr, test_callback.response_info_); + EXPECT_NE(nullptr, test_callback.last_error_); + + EXPECT_EQ(Cronet_Error_ERROR_CODE_ERROR_HOSTNAME_NOT_RESOLVED, + Cronet_Error_error_code_get(test_callback.last_error_)); + EXPECT_FALSE( + Cronet_Error_immediately_retryable_get(test_callback.last_error_)); + EXPECT_STREQ("net::ERR_NAME_NOT_RESOLVED", + Cronet_Error_message_get(test_callback.last_error_)); + EXPECT_EQ(-105, + Cronet_Error_internal_error_code_get(test_callback.last_error_)); + EXPECT_EQ( + 0, Cronet_Error_quic_detailed_error_code_get(test_callback.last_error_)); + + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequest_Destroy(request); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); +} + +void UrlRequestTest::TestCancel( + TestUrlRequestCallback::FailureType failure_type, + TestUrlRequestCallback::ResponseStep failure_step, + bool expect_response_info, + bool expect_error) { + auto callback = + std::make_unique(GetDirectExecutorParam()); + callback->set_failure(failure_type, failure_step); + const std::string url = cronet::TestServer::GetRedirectURL(); + callback = StartAndWaitForComplete(url, std::move(callback)); + EXPECT_EQ(1, callback->redirect_count_); + EXPECT_EQ(1ul, callback->redirect_response_info_list_.size()); + + if (failure_type == TestUrlRequestCallback::CANCEL_SYNC || + failure_type == TestUrlRequestCallback::CANCEL_ASYNC) { + EXPECT_EQ(TestUrlRequestCallback::ON_CANCELED, callback->response_step_); + } + + EXPECT_EQ(expect_response_info, callback->response_info_ != nullptr); + EXPECT_EQ(expect_error, callback->last_error_ != nullptr); + EXPECT_EQ(expect_error, callback->on_error_called_); + + // When |failure_type| is CANCEL_ASYNC_WITHOUT_PAUSE and |failure_step| + // is ON_READ_COMPLETED, there might be an onSucceeded() task + // already posted. If that's the case, onCanceled() will not be invoked. See + // crbug.com/657415. + if (!(failure_type == TestUrlRequestCallback::CANCEL_ASYNC_WITHOUT_PAUSE && + failure_step == TestUrlRequestCallback::ON_READ_COMPLETED)) { + EXPECT_TRUE(callback->on_canceled_called_); + } +} + +TEST_P(UrlRequestTest, TestCancel) { + TestCancel(TestUrlRequestCallback::CANCEL_SYNC, + TestUrlRequestCallback::ON_RECEIVED_REDIRECT, true, false); + TestCancel(TestUrlRequestCallback::CANCEL_ASYNC, + TestUrlRequestCallback::ON_RECEIVED_REDIRECT, true, false); + TestCancel(TestUrlRequestCallback::CANCEL_ASYNC_WITHOUT_PAUSE, + TestUrlRequestCallback::ON_RECEIVED_REDIRECT, true, false); + + TestCancel(TestUrlRequestCallback::CANCEL_SYNC, + TestUrlRequestCallback::ON_RESPONSE_STARTED, true, false); + TestCancel(TestUrlRequestCallback::CANCEL_ASYNC, + TestUrlRequestCallback::ON_RESPONSE_STARTED, true, false); + // https://crbug.com/812334 - If request is canceled asynchronously, the + // 'OnReadCompleted' callback may arrive AFTER 'OnCanceled'. + TestCancel(TestUrlRequestCallback::CANCEL_ASYNC_WITHOUT_PAUSE, + TestUrlRequestCallback::ON_RESPONSE_STARTED, true, false); + + TestCancel(TestUrlRequestCallback::CANCEL_SYNC, + TestUrlRequestCallback::ON_READ_COMPLETED, true, false); + TestCancel(TestUrlRequestCallback::CANCEL_ASYNC, + TestUrlRequestCallback::ON_READ_COMPLETED, true, false); + TestCancel(TestUrlRequestCallback::CANCEL_ASYNC_WITHOUT_PAUSE, + TestUrlRequestCallback::ON_READ_COMPLETED, true, false); +} + +TEST_P(UrlRequestTest, PerfTest) { + const int kTestIterations = 10; + const int kDownloadSize = 19307439; // used for internal server only + + Cronet_EnginePtr engine = Cronet_Engine_Create(); + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_Engine_StartWithParams(engine, engine_params); + + std::string url = cronet::TestServer::PrepareBigDataURL(kDownloadSize); + + base::Time start = base::Time::Now(); + + for (int i = 0; i < kTestIterations; ++i) { + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = + Cronet_UrlRequestParams_Create(); + TestUrlRequestCallback test_callback(GetDirectExecutorParam()); + test_callback.set_accumulate_response_data(false); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + TestRequestFinishedInfoListener test_request_finished_info_listener; + MaybeAddRequestFinishedListener(request_params, engine, executor, + &test_request_finished_info_listener); + + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), + request_params, callback, executor); + + Cronet_UrlRequest_Start(request); + test_callback.WaitForDone(); + MaybeVerifyRequestFinishedInfo(&test_request_finished_info_listener, + test_callback); + + EXPECT_TRUE(test_callback.IsDone()); + ASSERT_EQ(kDownloadSize, test_callback.response_data_length_); + + CleanupRequestFinishedListener(request_params, engine); + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequest_Destroy(request); + Cronet_UrlRequestCallback_Destroy(callback); + } + base::Time end = base::Time::Now(); + base::TimeDelta delta = end - start; + + LOG(INFO) << "Total time " << delta.InMillisecondsF() << " ms"; + LOG(INFO) << "Single Iteration time " + << delta.InMillisecondsF() / kTestIterations << " ms"; + + const double bytes_per_second = + kDownloadSize * kTestIterations / delta.InSecondsF(); + const double megabits_per_second = bytes_per_second / 1'000'000 * 8; + LOG(INFO) << "Average Throughput: " << megabits_per_second << " mbps"; + + Cronet_EngineParams_Destroy(engine_params); + Cronet_Engine_Destroy(engine); + cronet::TestServer::ReleaseBigDataURL(); +} + +TEST_P(UrlRequestTest, GetStatus) { + Cronet_EnginePtr engine = cronet::test::CreateTestEngine(0); + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + std::string url = cronet::TestServer::GetSimpleURL(); + + TestUrlRequestCallback test_callback(GetDirectExecutorParam()); + test_callback.set_auto_advance(false); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + TestRequestFinishedInfoListener test_request_finished_info_listener; + MaybeAddRequestFinishedListener(request_params, engine, executor, + &test_request_finished_info_listener); + + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), request_params, + callback, executor); + EXPECT_EQ(Cronet_UrlRequestStatusListener_Status_INVALID, + GetRequestStatus(request, &test_callback)); + + Cronet_UrlRequest_Start(request); + EXPECT_LE(Cronet_UrlRequestStatusListener_Status_IDLE, + GetRequestStatus(request, &test_callback)); + EXPECT_GE(Cronet_UrlRequestStatusListener_Status_READING_RESPONSE, + GetRequestStatus(request, &test_callback)); + + test_callback.WaitForNextStep(); + EXPECT_EQ(Cronet_UrlRequestStatusListener_Status_WAITING_FOR_DELEGATE, + GetRequestStatus(request, &test_callback)); + + Cronet_BufferPtr buffer = Cronet_Buffer_Create(); + Cronet_Buffer_InitWithAlloc(buffer, 100); + Cronet_UrlRequest_Read(request, buffer); + EXPECT_LE(Cronet_UrlRequestStatusListener_Status_IDLE, + GetRequestStatus(request, &test_callback)); + EXPECT_GE(Cronet_UrlRequestStatusListener_Status_READING_RESPONSE, + GetRequestStatus(request, &test_callback)); + + test_callback.WaitForNextStep(); + EXPECT_LE(Cronet_UrlRequestStatusListener_Status_IDLE, + GetRequestStatus(request, &test_callback)); + EXPECT_GE(Cronet_UrlRequestStatusListener_Status_READING_RESPONSE, + GetRequestStatus(request, &test_callback)); + + do { + buffer = Cronet_Buffer_Create(); + Cronet_Buffer_InitWithAlloc(buffer, 100); + Cronet_UrlRequest_Read(request, buffer); + // Verify that late calls to GetStatus() don't invoke OnStatus() after + // final callbacks. + GetRequestStatus(request, &test_callback); + test_callback.WaitForNextStep(); + } while (!Cronet_UrlRequest_IsDone(request)); + MaybeVerifyRequestFinishedInfo(&test_request_finished_info_listener, + test_callback); + + EXPECT_EQ(Cronet_UrlRequestStatusListener_Status_INVALID, + GetRequestStatus(request, &test_callback)); + ASSERT_EQ("The quick brown fox jumps over the lazy dog.", + test_callback.response_as_string_); + + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequest_Destroy(request); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); +} + +class UrlRequestTestNoParam : public ::testing::Test { + void SetUp() override { cronet::TestServer::Start(); } + + void TearDown() override { cronet::TestServer::Shutdown(); } +}; + +TEST_F(UrlRequestTestNoParam, + RequestFinishedListenerWithoutExecutorReturnsError) { + Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create(); + Cronet_EnginePtr engine = Cronet_Engine_Create(); + // Disable runtime CHECK of the result, so it could be verified. + Cronet_EngineParams_enable_check_result_set(engine_params, false); + EXPECT_EQ(Cronet_RESULT_SUCCESS, + Cronet_Engine_StartWithParams(engine, engine_params)); + Cronet_EngineParams_Destroy(engine_params); + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + TestRequestFinishedInfoListener test_request_finished_info_listener; + Cronet_RequestFinishedInfoListenerPtr request_finished_listener = + test_request_finished_info_listener.CreateRequestFinishedListener(); + // Executor type doesn't matter for this test. + TestUrlRequestCallback test_callback(/*direct_executor=*/true); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + + Cronet_UrlRequestParams_request_finished_listener_set( + request_params, request_finished_listener); + + EXPECT_EQ(Cronet_RESULT_NULL_POINTER_REQUEST_FINISHED_INFO_LISTENER_EXECUTOR, + Cronet_UrlRequest_InitWithParams( + request, engine, "http://fakeurl.example.com", request_params, + callback, executor)); + + // This test never actually runs |request_finished_listener|, so we delete + // it here. + Cronet_RequestFinishedInfoListener_Destroy(request_finished_listener); + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequest_Destroy(request); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); +} + +TEST_F(UrlRequestTestNoParam, + UseRequestFinishedInfoAfterUrlRequestDestructionSuccess) { + Cronet_EnginePtr engine = cronet::test::CreateTestEngine(0); + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + std::string url = cronet::TestServer::GetSimpleURL(); + + // The UrlRequest executor type doesn't matter, but the + // RequestFinishedInfoListener executor type can't be direct. + TestUrlRequestCallback test_callback(/* direct_executor= */ false); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + + base::WaitableEvent done_event; + struct ListenerContext { + raw_ptr test_callback; + Cronet_UrlRequestPtr url_request; + raw_ptr done_event; + }; + ListenerContext listener_context = {&test_callback, request, &done_event}; + + auto* request_finished_listener = + Cronet_RequestFinishedInfoListener_CreateWith( + +[](Cronet_RequestFinishedInfoListenerPtr self, + Cronet_RequestFinishedInfoPtr request_finished_info, + Cronet_UrlResponseInfoPtr response_info, Cronet_ErrorPtr error) { + auto* listener_context = static_cast( + Cronet_RequestFinishedInfoListener_GetClientContext(self)); + listener_context->test_callback->WaitForDone(); + Cronet_UrlRequest_Destroy(listener_context->url_request); + // The next few get methods shouldn't use-after-free on + // |request_finished_info| or |response_info|. + EXPECT_NE(nullptr, Cronet_RequestFinishedInfo_metrics_get( + request_finished_info)); + EXPECT_NE(nullptr, Cronet_UrlResponseInfo_url_get(response_info)); + Cronet_RequestFinishedInfoListener_Destroy(self); + listener_context->done_event->Signal(); + }); + Cronet_RequestFinishedInfoListener_SetClientContext(request_finished_listener, + &listener_context); + + Cronet_UrlRequestParams_request_finished_listener_set( + request_params, request_finished_listener); + Cronet_UrlRequestParams_request_finished_executor_set(request_params, + executor); + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), request_params, + callback, executor); + Cronet_UrlRequest_Start(request); + + done_event.Wait(); + EXPECT_TRUE(test_callback.IsDone()); + ASSERT_EQ("The quick brown fox jumps over the lazy dog.", + test_callback.response_as_string_); + + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); +} + +TEST_F(UrlRequestTestNoParam, + UseRequestFinishedInfoAfterUrlRequestDestructionFailure) { + Cronet_EnginePtr engine = cronet::test::CreateTestEngine(0); + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + std::string url = "https://notfound.example.com"; + + // The UrlRequest executor type doesn't matter, but the + // RequestFinishedInfoListener executor type can't be direct. + TestUrlRequestCallback test_callback(/* direct_executor= */ false); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + + base::WaitableEvent done_event; + struct ListenerContext { + raw_ptr test_callback; + Cronet_UrlRequestPtr url_request; + raw_ptr done_event; + }; + ListenerContext listener_context = {&test_callback, request, &done_event}; + + auto* request_finished_listener = + Cronet_RequestFinishedInfoListener_CreateWith( + +[](Cronet_RequestFinishedInfoListenerPtr self, + Cronet_RequestFinishedInfoPtr request_finished_info, + Cronet_UrlResponseInfoPtr response_info, Cronet_ErrorPtr error) { + auto* listener_context = static_cast( + Cronet_RequestFinishedInfoListener_GetClientContext(self)); + listener_context->test_callback->WaitForDone(); + Cronet_UrlRequest_Destroy(listener_context->url_request); + // The next few get methods shouldn't use-after-free on + // |request_finished_info| or |error|. + EXPECT_NE(nullptr, Cronet_RequestFinishedInfo_metrics_get( + request_finished_info)); + EXPECT_NE(nullptr, Cronet_Error_message_get(error)); + Cronet_RequestFinishedInfoListener_Destroy(self); + listener_context->done_event->Signal(); + }); + Cronet_RequestFinishedInfoListener_SetClientContext(request_finished_listener, + &listener_context); + + Cronet_UrlRequestParams_request_finished_listener_set( + request_params, request_finished_listener); + Cronet_UrlRequestParams_request_finished_executor_set(request_params, + executor); + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), request_params, + callback, executor); + Cronet_UrlRequest_Start(request); + + done_event.Wait(); + EXPECT_TRUE(test_callback.IsDone()); + + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); +} + +TEST_F(UrlRequestTestNoParam, + CorrelateCallbackAndRequestInfoWithoutSynchronization) { + class TestUrlRequestCallbackWithCorrelation : public TestUrlRequestCallback { + public: + using TestUrlRequestCallback::TestUrlRequestCallback; + + void OnSucceeded(Cronet_UrlRequestPtr request, + Cronet_UrlResponseInfoPtr info) override { + // This method is guaranteed to run after + // RequestFinishedInfoListener.OnRequestFinished(), **on the same + // thread** (due to the use of a direct executor with the + // RequestFinishedInfoListener). + // + // The following read should therefore not need synchronization -- we rely + // on running this test under sanitizers to verify this. + EXPECT_NE(nullptr, + Cronet_RequestFinishedInfo_metrics_get(request_finished_info_)); + TestUrlRequestCallback::OnSucceeded(request, info); + } + + Cronet_RequestFinishedInfoPtr request_finished_info_; + }; + + Cronet_EnginePtr engine = cronet::test::CreateTestEngine(0); + Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create(); + Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create(); + std::string url = cronet::TestServer::GetSimpleURL(); + + // The UrlRequest executor type doesn't matter, but the + // RequestFinishedInfoListener executor type *must* be direct. + TestUrlRequestCallbackWithCorrelation test_callback( + /* direct_executor= */ true); + // Executor provided by the application is owned by |test_callback|. + Cronet_ExecutorPtr executor = test_callback.GetExecutor(); + // Callback provided by the application. + Cronet_UrlRequestCallbackPtr callback = + test_callback.CreateUrlRequestCallback(); + + auto* request_finished_listener = + Cronet_RequestFinishedInfoListener_CreateWith( + +[](Cronet_RequestFinishedInfoListenerPtr self, + Cronet_RequestFinishedInfoPtr request_finished_info, + Cronet_UrlResponseInfoPtr, Cronet_ErrorPtr) { + auto* test_callback = + static_cast( + Cronet_RequestFinishedInfoListener_GetClientContext(self)); + test_callback->request_finished_info_ = request_finished_info; + Cronet_RequestFinishedInfoListener_Destroy(self); + }); + Cronet_RequestFinishedInfoListener_SetClientContext(request_finished_listener, + &test_callback); + + Cronet_UrlRequestParams_request_finished_listener_set( + request_params, request_finished_listener); + Cronet_UrlRequestParams_request_finished_executor_set(request_params, + executor); + Cronet_UrlRequest_InitWithParams(request, engine, url.c_str(), request_params, + callback, executor); + Cronet_UrlRequest_Start(request); + + test_callback.WaitForDone(); + EXPECT_TRUE(test_callback.IsDone()); + ASSERT_EQ("The quick brown fox jumps over the lazy dog.", + test_callback.response_as_string_); + + Cronet_UrlRequest_Destroy(request); + Cronet_UrlRequestParams_Destroy(request_params); + Cronet_UrlRequestCallback_Destroy(callback); + Cronet_Engine_Destroy(engine); +} + +} // namespace diff --git a/src/components/cronet/native/test_instructions.md b/src/components/cronet/native/test_instructions.md new file mode 100644 index 0000000000..56549831b5 --- /dev/null +++ b/src/components/cronet/native/test_instructions.md @@ -0,0 +1,45 @@ +# Testing Cronet native API on desktop + +[TOC] + +## Overview + +The Cronet native API is cross-platform, usable on multiple desktop and mobile +platforms. + +TODO(caraitto): Add mobile test information for the native API in the +Android and iOS pages as instructions for testing vary by platform. + +## Checkout and build + +See instructions in the [common checkout and +build](/components/cronet/build_instructions.md). + +## Running tests locally + +To run Cronet native API unit and integration tests: + +```shell +$ gn gen out/Default # Generate Ninja build files. +$ ninja -C out/Default cronet_unittests cronet_tests # Build both test suites. +$ ./out/Default/cronet_unittests # Run unit tests. +$ ./out/Default/cronet_tests # Run the integration tests. +``` + +# Running tests remotely + +To test against all tryjobs: + +```shell +$ git cl upload # Upload to Gerrit. +$ git cl try # Run the tryjob, results posted in the Gerrit review. +``` + +This will test against several mobile and desktop platforms, along with +special configurations like ASAN and TSAN. + +You can use the -b flag to test against just one of those, like this: + +```shell +$ git cl try -b linux-rel +``` diff --git a/src/components/cronet/native/upload_data_sink.cc b/src/components/cronet/native/upload_data_sink.cc new file mode 100644 index 0000000000..9530713637 --- /dev/null +++ b/src/components/cronet/native/upload_data_sink.cc @@ -0,0 +1,297 @@ +// Copyright 2018 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 "components/cronet/native/upload_data_sink.h" + +#include +#include + +#include "base/bind.h" +#include "base/check_op.h" +#include "base/memory/raw_ptr.h" +#include "base/strings/strcat.h" +#include "base/strings/stringprintf.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/cronet/cronet_upload_data_stream.h" +#include "components/cronet/native/engine.h" +#include "components/cronet/native/generated/cronet.idl_impl_struct.h" +#include "components/cronet/native/include/cronet_c.h" +#include "components/cronet/native/io_buffer_with_cronet_buffer.h" +#include "components/cronet/native/runnables.h" +#include "components/cronet/native/url_request.h" +#include "net/base/io_buffer.h" + +namespace cronet { + +// This class is called by Cronet's network stack as an implementation of +// CronetUploadDataStream::Delegate, and forwards the calls along to +// Cronet_UploadDataSinkImpl on the embedder's executor. +// This class is always called on the network thread and is destroyed in +// OnUploadDataStreamDestroyed() callback. +class Cronet_UploadDataSinkImpl::NetworkTasks + : public CronetUploadDataStream::Delegate { + public: + NetworkTasks(Cronet_UploadDataSinkImpl* upload_data_sink, + Cronet_Executor* upload_data_provider_executor); + + NetworkTasks(const NetworkTasks&) = delete; + NetworkTasks& operator=(const NetworkTasks&) = delete; + + ~NetworkTasks() override; + + private: + // CronetUploadDataStream::Delegate implementation: + void InitializeOnNetworkThread( + base::WeakPtr upload_data_stream) override; + void Read(scoped_refptr buffer, int buf_len) override; + void Rewind() override; + void OnUploadDataStreamDestroyed() override; + + // Post |task| to client executor. + void PostTaskToExecutor(base::OnceClosure task); + + // The upload data sink that is owned by url request and always accessed on + // the client thread. It always outlives |this| callback. + const raw_ptr upload_data_sink_ = nullptr; + + // Executor for provider callback, used, but not owned, by |this|. Always + // outlives |this| callback. + Cronet_ExecutorPtr const upload_data_provider_executor_ = nullptr; + + THREAD_CHECKER(network_thread_checker_); +}; + +Cronet_UploadDataSinkImpl::NetworkTasks::NetworkTasks( + Cronet_UploadDataSinkImpl* upload_data_sink, + Cronet_Executor* upload_data_provider_executor) + : upload_data_sink_(upload_data_sink), + upload_data_provider_executor_(upload_data_provider_executor) { + DETACH_FROM_THREAD(network_thread_checker_); +} + +Cronet_UploadDataSinkImpl::NetworkTasks::~NetworkTasks() = default; + +Cronet_UploadDataSinkImpl::Cronet_UploadDataSinkImpl( + Cronet_UrlRequestImpl* url_request, + Cronet_UploadDataProvider* upload_data_provider, + Cronet_Executor* upload_data_provider_executor) + : url_request_(url_request), + upload_data_provider_executor_(upload_data_provider_executor), + upload_data_provider_(upload_data_provider) {} + +Cronet_UploadDataSinkImpl::~Cronet_UploadDataSinkImpl() = default; + +void Cronet_UploadDataSinkImpl::InitRequest(CronetURLRequest* request) { + int64_t length = upload_data_provider_->GetLength(); + if (length == -1) { + is_chunked_ = true; + } else { + CHECK_GE(length, 0); + length_ = static_cast(length); + remaining_length_ = length_; + } + + request->SetUpload(std::make_unique( + new NetworkTasks(this, upload_data_provider_executor_), length)); +} + +void Cronet_UploadDataSinkImpl::OnReadSucceeded(uint64_t bytes_read, + bool final_chunk) { + { + base::AutoLock lock(lock_); + CheckState(READ); + in_which_user_callback_ = NOT_IN_CALLBACK; + if (!upload_data_provider_) + return; + } + if (url_request_->IsDone()) + return; + if (close_when_not_in_callback_) { + PostCloseToExecutor(); + return; + } + CHECK(bytes_read > 0 || (final_chunk && bytes_read == 0)); + // Bytes read exceeds buffer length. + CHECK_LE(static_cast(bytes_read), buffer_->io_buffer_len()); + if (!is_chunked_) { + // Only chunked upload can have the final chunk. + CHECK(!final_chunk); + // Read upload data length exceeds specified length. + if (bytes_read > remaining_length_) { + PostCloseToExecutor(); + std::string error_message = + base::StringPrintf("Read upload data length %" PRIu64 + " exceeds expected length %" PRIu64, + length_ - remaining_length_ + bytes_read, length_); + url_request_->OnUploadDataProviderError(error_message.c_str()); + return; + } + remaining_length_ -= bytes_read; + } + network_task_runner_->PostTask( + FROM_HERE, base::BindOnce(&CronetUploadDataStream::OnReadSuccess, + upload_data_stream_, bytes_read, final_chunk)); +} + +void Cronet_UploadDataSinkImpl::OnReadError(Cronet_String error_message) { + { + base::AutoLock lock(lock_); + CheckState(READ); + in_which_user_callback_ = NOT_IN_CALLBACK; + if (!upload_data_provider_) + return; + } + if (url_request_->IsDone()) + return; + PostCloseToExecutor(); + url_request_->OnUploadDataProviderError(error_message); +} + +void Cronet_UploadDataSinkImpl::OnRewindSucceeded() { + { + base::AutoLock lock(lock_); + CheckState(REWIND); + in_which_user_callback_ = NOT_IN_CALLBACK; + if (!upload_data_provider_) + return; + } + remaining_length_ = length_; + if (url_request_->IsDone()) + return; + if (close_when_not_in_callback_) { + PostCloseToExecutor(); + return; + } + network_task_runner_->PostTask( + FROM_HERE, base::BindOnce(&CronetUploadDataStream::OnRewindSuccess, + upload_data_stream_)); +} + +void Cronet_UploadDataSinkImpl::OnRewindError(Cronet_String error_message) { + { + base::AutoLock lock(lock_); + CheckState(REWIND); + in_which_user_callback_ = NOT_IN_CALLBACK; + if (!upload_data_provider_) + return; + } + if (url_request_->IsDone()) + return; + PostCloseToExecutor(); + url_request_->OnUploadDataProviderError(error_message); +} + +void Cronet_UploadDataSinkImpl::InitializeUploadDataStream( + base::WeakPtr upload_data_stream, + scoped_refptr network_task_runner) { + DCHECK(!upload_data_stream_); + DCHECK(!network_task_runner_.get()); + upload_data_stream_ = upload_data_stream; + network_task_runner_ = network_task_runner; +} + +void Cronet_UploadDataSinkImpl::PostCloseToExecutor() { + Cronet_RunnablePtr runnable = new cronet::OnceClosureRunnable(base::BindOnce( + &Cronet_UploadDataSinkImpl::Close, base::Unretained(this))); + // |runnable| is passed to executor, which destroys it after execution. + Cronet_Executor_Execute(upload_data_provider_executor_, runnable); +} + +void Cronet_UploadDataSinkImpl::Read(scoped_refptr buffer, + int buf_len) { + if (url_request_->IsDone()) + return; + Cronet_UploadDataProviderPtr upload_data_provider = nullptr; + { + base::AutoLock lock(lock_); + if (!upload_data_provider_) + return; + CheckState(NOT_IN_CALLBACK); + in_which_user_callback_ = READ; + upload_data_provider = upload_data_provider_; + } + buffer_ = + std::make_unique(std::move(buffer), buf_len); + Cronet_UploadDataProvider_Read(upload_data_provider, this, + buffer_->cronet_buffer()); +} + +void Cronet_UploadDataSinkImpl::Rewind() { + if (url_request_->IsDone()) + return; + Cronet_UploadDataProviderPtr upload_data_provider = nullptr; + { + base::AutoLock lock(lock_); + if (!upload_data_provider_) + return; + CheckState(NOT_IN_CALLBACK); + in_which_user_callback_ = REWIND; + upload_data_provider = upload_data_provider_; + } + Cronet_UploadDataProvider_Rewind(upload_data_provider, this); +} + +void Cronet_UploadDataSinkImpl::Close() { + Cronet_UploadDataProviderPtr upload_data_provider = nullptr; + { + base::AutoLock lock(lock_); + // If |upload_data_provider_| is already closed from OnResponseStarted(), + // don't close it again from OnError() or OnCanceled(). + if (!upload_data_provider_) + return; + if (in_which_user_callback_ != NOT_IN_CALLBACK) { + // If currently in the callback, then wait until return from callback + // before closing. + close_when_not_in_callback_ = true; + return; + } + upload_data_provider = upload_data_provider_; + upload_data_provider_ = nullptr; + } + Cronet_UploadDataProvider_Close(upload_data_provider); +} + +void Cronet_UploadDataSinkImpl::CheckState(UserCallback expected_state) { + lock_.AssertAcquired(); + CHECK(in_which_user_callback_ == expected_state); +} + +void Cronet_UploadDataSinkImpl::NetworkTasks::InitializeOnNetworkThread( + base::WeakPtr upload_data_stream) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + PostTaskToExecutor( + base::BindOnce(&Cronet_UploadDataSinkImpl::InitializeUploadDataStream, + base::Unretained(upload_data_sink_), upload_data_stream, + base::ThreadTaskRunnerHandle::Get())); +} + +void Cronet_UploadDataSinkImpl::NetworkTasks::Read( + scoped_refptr buffer, + int buf_len) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + PostTaskToExecutor(base::BindOnce(&Cronet_UploadDataSinkImpl::Read, + base::Unretained(upload_data_sink_), + std::move(buffer), buf_len)); +} + +void Cronet_UploadDataSinkImpl::NetworkTasks::Rewind() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + PostTaskToExecutor(base::BindOnce(&Cronet_UploadDataSinkImpl::Rewind, + base::Unretained(upload_data_sink_))); +} + +void Cronet_UploadDataSinkImpl::NetworkTasks::OnUploadDataStreamDestroyed() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + delete this; +} + +void Cronet_UploadDataSinkImpl::NetworkTasks::PostTaskToExecutor( + base::OnceClosure task) { + Cronet_RunnablePtr runnable = + new cronet::OnceClosureRunnable(std::move(task)); + // |runnable| is passed to executor, which destroys it after execution. + Cronet_Executor_Execute(upload_data_provider_executor_, runnable); +} + +} // namespace cronet diff --git a/src/components/cronet/native/upload_data_sink.h b/src/components/cronet/native/upload_data_sink.h new file mode 100644 index 0000000000..8028ff41f3 --- /dev/null +++ b/src/components/cronet/native/upload_data_sink.h @@ -0,0 +1,96 @@ +// Copyright 2018 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_NATIVE_UPLOAD_DATA_SINK_H_ +#define COMPONENTS_CRONET_NATIVE_UPLOAD_DATA_SINK_H_ + +#include + +#include "base/memory/raw_ptr.h" +#include "base/synchronization/lock.h" +#include "base/synchronization/waitable_event.h" +#include "components/cronet/cronet_upload_data_stream.h" +#include "components/cronet/cronet_url_request.h" +#include "components/cronet/cronet_url_request_context.h" +#include "components/cronet/native/generated/cronet.idl_impl_interface.h" + +namespace cronet { + +class Cronet_UrlRequestImpl; +class Cronet_BufferWithIOBuffer; + +// Implementation of Cronet_UploadDataSink that uses CronetUploadDataStream. +// Always accessed on client executor. +class Cronet_UploadDataSinkImpl : public Cronet_UploadDataSink { + public: + Cronet_UploadDataSinkImpl(Cronet_UrlRequestImpl* url_request, + Cronet_UploadDataProvider* upload_data_provider, + Cronet_Executor* upload_data_provider_executor); + + Cronet_UploadDataSinkImpl(const Cronet_UploadDataSinkImpl&) = delete; + Cronet_UploadDataSinkImpl& operator=(const Cronet_UploadDataSinkImpl&) = + delete; + + ~Cronet_UploadDataSinkImpl() override; + + // Initialize length and attach upload to request. Called on client thread. + void InitRequest(CronetURLRequest* request); + + // Mark stream as closed and post |Close()| callback to consumer. + void PostCloseToExecutor(); + + private: + class NetworkTasks; + enum UserCallback { READ, REWIND, GET_LENGTH, NOT_IN_CALLBACK }; + + // Cronet_UploadDataSink + void OnReadSucceeded(uint64_t bytes_read, bool final_chunk) override; + void OnReadError(Cronet_String error_message) override; + void OnRewindSucceeded() override; + void OnRewindError(Cronet_String error_message) override; + + // CronetUploadDataStream::Delegate methods posted from the network thread. + void InitializeUploadDataStream( + base::WeakPtr upload_data_stream, + scoped_refptr network_task_runner); + void Read(scoped_refptr buffer, int buf_len); + void Rewind(); + void Close(); + + void CheckState(UserCallback expected_state); + + // Cronet objects not owned by |this| and accessed on client thread. + + // The request, which owns |this|. + const raw_ptr url_request_ = nullptr; + // Executor for provider callback, used, but not owned, by |this|. Always + // outlives |this| callback. + Cronet_ExecutorPtr const upload_data_provider_executor_ = nullptr; + + // These are initialized in InitializeUploadDataStream(), so are safe to + // access during client callbacks, which all happen after initialization. + scoped_refptr network_task_runner_; + base::WeakPtr upload_data_stream_; + + bool is_chunked_ = false; + uint64_t length_ = 0; + uint64_t remaining_length_ = 0; + + // Synchronize access to |buffer_| and other objects below from different + // threads. + base::Lock lock_; + // Data provider callback interface, used, but not owned, by |this|. + // Set to nullptr when data provider is closed. + Cronet_UploadDataProviderPtr upload_data_provider_ = nullptr; + + UserCallback in_which_user_callback_ = NOT_IN_CALLBACK; + // Close data provider once it returns from the callback. + bool close_when_not_in_callback_ = false; + // Keeps the net::IOBuffer and Cronet ByteBuffer alive until the next Read(). + std::unique_ptr buffer_; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_NATIVE_UPLOAD_DATA_SINK_H_ diff --git a/src/components/cronet/native/url_request.cc b/src/components/cronet/native/url_request.cc new file mode 100644 index 0000000000..58bc48be3f --- /dev/null +++ b/src/components/cronet/native/url_request.cc @@ -0,0 +1,884 @@ +// Copyright 2018 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 "components/cronet/native/url_request.h" + +#include +#include + +#include "base/bind.h" +#include "base/logging.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_refptr.h" +#include "components/cronet/cronet_upload_data_stream.h" +#include "components/cronet/native/engine.h" +#include "components/cronet/native/generated/cronet.idl_impl_struct.h" +#include "components/cronet/native/include/cronet_c.h" +#include "components/cronet/native/io_buffer_with_cronet_buffer.h" +#include "components/cronet/native/native_metrics_util.h" +#include "components/cronet/native/runnables.h" +#include "components/cronet/native/upload_data_sink.h" +#include "net/base/io_buffer.h" +#include "net/base/load_states.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +namespace { + +using RequestFinishedInfo = base::RefCountedData; +using UrlResponseInfo = base::RefCountedData; +using CronetError = base::RefCountedData; + +template +T* GetData(scoped_refptr> ptr) { + return ptr == nullptr ? nullptr : &ptr->data; +} + +net::RequestPriority ConvertRequestPriority( + Cronet_UrlRequestParams_REQUEST_PRIORITY priority) { + switch (priority) { + case Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_IDLE: + return net::IDLE; + case Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_LOWEST: + return net::LOWEST; + case Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_LOW: + return net::LOW; + case Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_MEDIUM: + return net::MEDIUM; + case Cronet_UrlRequestParams_REQUEST_PRIORITY_REQUEST_PRIORITY_HIGHEST: + return net::HIGHEST; + } + return net::DEFAULT_PRIORITY; +} + +net::Idempotency ConvertIdempotency( + Cronet_UrlRequestParams_IDEMPOTENCY idempotency) { + switch (idempotency) { + case Cronet_UrlRequestParams_IDEMPOTENCY_DEFAULT_IDEMPOTENCY: + return net::DEFAULT_IDEMPOTENCY; + case Cronet_UrlRequestParams_IDEMPOTENCY_IDEMPOTENT: + return net::IDEMPOTENT; + case Cronet_UrlRequestParams_IDEMPOTENCY_NOT_IDEMPOTENT: + return net::NOT_IDEMPOTENT; + } + return net::DEFAULT_IDEMPOTENCY; +} + +scoped_refptr CreateCronet_UrlResponseInfo( + const std::vector& url_chain, + int http_status_code, + const std::string& http_status_text, + const net::HttpResponseHeaders* headers, + bool was_cached, + const std::string& negotiated_protocol, + const std::string& proxy_server, + int64_t received_byte_count) { + auto response_info = base::MakeRefCounted(); + response_info->data.url = url_chain.back(); + response_info->data.url_chain = url_chain; + response_info->data.http_status_code = http_status_code; + response_info->data.http_status_text = http_status_text; + // |headers| could be nullptr. + if (headers != nullptr) { + size_t iter = 0; + std::string header_name; + std::string header_value; + while (headers->EnumerateHeaderLines(&iter, &header_name, &header_value)) { + Cronet_HttpHeader header; + header.name = header_name; + header.value = header_value; + response_info->data.all_headers_list.push_back(std::move(header)); + } + } + response_info->data.was_cached = was_cached; + response_info->data.negotiated_protocol = negotiated_protocol; + response_info->data.proxy_server = proxy_server; + response_info->data.received_byte_count = received_byte_count; + return response_info; +} + +Cronet_Error_ERROR_CODE NetErrorToCronetErrorCode(int net_error) { + switch (net_error) { + case net::ERR_NAME_NOT_RESOLVED: + return Cronet_Error_ERROR_CODE_ERROR_HOSTNAME_NOT_RESOLVED; + case net::ERR_INTERNET_DISCONNECTED: + return Cronet_Error_ERROR_CODE_ERROR_INTERNET_DISCONNECTED; + case net::ERR_NETWORK_CHANGED: + return Cronet_Error_ERROR_CODE_ERROR_NETWORK_CHANGED; + case net::ERR_TIMED_OUT: + return Cronet_Error_ERROR_CODE_ERROR_TIMED_OUT; + case net::ERR_CONNECTION_CLOSED: + return Cronet_Error_ERROR_CODE_ERROR_CONNECTION_CLOSED; + case net::ERR_CONNECTION_TIMED_OUT: + return Cronet_Error_ERROR_CODE_ERROR_CONNECTION_TIMED_OUT; + case net::ERR_CONNECTION_REFUSED: + return Cronet_Error_ERROR_CODE_ERROR_CONNECTION_REFUSED; + case net::ERR_CONNECTION_RESET: + return Cronet_Error_ERROR_CODE_ERROR_CONNECTION_RESET; + case net::ERR_ADDRESS_UNREACHABLE: + return Cronet_Error_ERROR_CODE_ERROR_ADDRESS_UNREACHABLE; + case net::ERR_QUIC_PROTOCOL_ERROR: + return Cronet_Error_ERROR_CODE_ERROR_QUIC_PROTOCOL_FAILED; + default: + return Cronet_Error_ERROR_CODE_ERROR_OTHER; + } +} + +bool IsCronetErrorImmediatelyRetryable(Cronet_Error_ERROR_CODE error_code) { + switch (error_code) { + case Cronet_Error_ERROR_CODE_ERROR_HOSTNAME_NOT_RESOLVED: + case Cronet_Error_ERROR_CODE_ERROR_INTERNET_DISCONNECTED: + case Cronet_Error_ERROR_CODE_ERROR_CONNECTION_REFUSED: + case Cronet_Error_ERROR_CODE_ERROR_ADDRESS_UNREACHABLE: + case Cronet_Error_ERROR_CODE_ERROR_OTHER: + default: + return false; + case Cronet_Error_ERROR_CODE_ERROR_NETWORK_CHANGED: + case Cronet_Error_ERROR_CODE_ERROR_TIMED_OUT: + case Cronet_Error_ERROR_CODE_ERROR_CONNECTION_CLOSED: + case Cronet_Error_ERROR_CODE_ERROR_CONNECTION_TIMED_OUT: + case Cronet_Error_ERROR_CODE_ERROR_CONNECTION_RESET: + return true; + } +} + +scoped_refptr CreateCronet_Error(int net_error, + int quic_error, + const std::string& error_string) { + auto error = base::MakeRefCounted(); + error->data.error_code = NetErrorToCronetErrorCode(net_error); + error->data.message = error_string; + error->data.internal_error_code = net_error; + error->data.quic_detailed_error_code = quic_error; + error->data.immediately_retryable = + IsCronetErrorImmediatelyRetryable(error->data.error_code); + return error; +} + +#if DCHECK_IS_ON() +// Runnable used to verify that Executor calls Cronet_Runnable_Destroy(). +class VerifyDestructionRunnable : public Cronet_Runnable { + public: + VerifyDestructionRunnable(base::WaitableEvent* destroyed) + : destroyed_(destroyed) {} + + VerifyDestructionRunnable(const VerifyDestructionRunnable&) = delete; + VerifyDestructionRunnable& operator=(const VerifyDestructionRunnable&) = + delete; + + // Signal event indicating Runnable was properly Destroyed. + ~VerifyDestructionRunnable() override { destroyed_->Signal(); } + + void Run() override {} + + private: + // Event indicating destructor is called. + const raw_ptr destroyed_; +}; +#endif // DCHECK_IS_ON() + +// Convert net::LoadState to Cronet_UrlRequestStatusListener_Status. +Cronet_UrlRequestStatusListener_Status ConvertLoadState( + net::LoadState load_state) { + switch (load_state) { + case net::LOAD_STATE_IDLE: + return Cronet_UrlRequestStatusListener_Status_IDLE; + + case net::LOAD_STATE_WAITING_FOR_STALLED_SOCKET_POOL: + return Cronet_UrlRequestStatusListener_Status_WAITING_FOR_STALLED_SOCKET_POOL; + + case net::LOAD_STATE_WAITING_FOR_AVAILABLE_SOCKET: + return Cronet_UrlRequestStatusListener_Status_WAITING_FOR_AVAILABLE_SOCKET; + + case net::LOAD_STATE_WAITING_FOR_DELEGATE: + return Cronet_UrlRequestStatusListener_Status_WAITING_FOR_DELEGATE; + + case net::LOAD_STATE_WAITING_FOR_CACHE: + return Cronet_UrlRequestStatusListener_Status_WAITING_FOR_CACHE; + + case net::LOAD_STATE_DOWNLOADING_PAC_FILE: + return Cronet_UrlRequestStatusListener_Status_DOWNLOADING_PAC_FILE; + + case net::LOAD_STATE_RESOLVING_PROXY_FOR_URL: + return Cronet_UrlRequestStatusListener_Status_RESOLVING_PROXY_FOR_URL; + + case net::LOAD_STATE_RESOLVING_HOST_IN_PAC_FILE: + return Cronet_UrlRequestStatusListener_Status_RESOLVING_HOST_IN_PAC_FILE; + + case net::LOAD_STATE_ESTABLISHING_PROXY_TUNNEL: + return Cronet_UrlRequestStatusListener_Status_ESTABLISHING_PROXY_TUNNEL; + + case net::LOAD_STATE_RESOLVING_HOST: + return Cronet_UrlRequestStatusListener_Status_RESOLVING_HOST; + + case net::LOAD_STATE_CONNECTING: + return Cronet_UrlRequestStatusListener_Status_CONNECTING; + + case net::LOAD_STATE_SSL_HANDSHAKE: + return Cronet_UrlRequestStatusListener_Status_SSL_HANDSHAKE; + + case net::LOAD_STATE_SENDING_REQUEST: + return Cronet_UrlRequestStatusListener_Status_SENDING_REQUEST; + + case net::LOAD_STATE_WAITING_FOR_RESPONSE: + return Cronet_UrlRequestStatusListener_Status_WAITING_FOR_RESPONSE; + + case net::LOAD_STATE_READING_RESPONSE: + return Cronet_UrlRequestStatusListener_Status_READING_RESPONSE; + + default: + // A load state is retrieved but there is no corresponding + // request status. This most likely means that the mapping is + // incorrect. + CHECK(false); + return Cronet_UrlRequestStatusListener_Status_INVALID; + } +} + +} // namespace + +namespace cronet { + +// NetworkTasks is owned by CronetURLRequest. It is constructed on client +// thread, but invoked and deleted on the network thread. +class Cronet_UrlRequestImpl::NetworkTasks : public CronetURLRequest::Callback { + public: + NetworkTasks(const std::string& url, Cronet_UrlRequestImpl* url_request); + + NetworkTasks(const NetworkTasks&) = delete; + NetworkTasks& operator=(const NetworkTasks&) = delete; + + ~NetworkTasks() override = default; + + // Callback function used for GetStatus(). + void OnStatus(Cronet_UrlRequestStatusListenerPtr listener, + net::LoadState load_state); + + private: + // CronetURLRequest::Callback implementation: + void OnReceivedRedirect(const std::string& new_location, + int http_status_code, + const std::string& http_status_text, + const net::HttpResponseHeaders* headers, + bool was_cached, + const std::string& negotiated_protocol, + const std::string& proxy_server, + int64_t received_byte_count) override; + void OnResponseStarted(int http_status_code, + const std::string& http_status_text, + const net::HttpResponseHeaders* headers, + bool was_cached, + const std::string& negotiated_protocol, + const std::string& proxy_server, + int64_t received_byte_count) override; + void OnReadCompleted(scoped_refptr buffer, + int bytes_read, + int64_t received_byte_count) override; + void OnSucceeded(int64_t received_byte_count) override; + void OnError(int net_error, + int quic_error, + const std::string& error_string, + int64_t received_byte_count) override; + void OnCanceled() override; + void OnDestroyed() override; + void OnMetricsCollected(const base::Time& request_start_time, + const base::TimeTicks& request_start, + const base::TimeTicks& dns_start, + const base::TimeTicks& dns_end, + const base::TimeTicks& connect_start, + const base::TimeTicks& connect_end, + const base::TimeTicks& ssl_start, + const base::TimeTicks& ssl_end, + const base::TimeTicks& send_start, + const base::TimeTicks& send_end, + const base::TimeTicks& push_start, + const base::TimeTicks& push_end, + const base::TimeTicks& receive_headers_end, + const base::TimeTicks& request_end, + bool socket_reused, + int64_t sent_bytes_count, + int64_t received_bytes_count) + LOCKS_EXCLUDED(url_request_->lock_) override; + + // The UrlRequest which owns context that owns the callback. + const raw_ptr url_request_ = nullptr; + + // URL chain contains the URL currently being requested, and + // all URLs previously requested. New URLs are added before + // Cronet_UrlRequestCallback::OnRedirectReceived is called. + std::vector url_chain_; + + // Set to true when OnCanceled/OnSucceeded/OnFailed is posted. + // When true it is unsafe to attempt to post other callbacks + // like OnStatus because the request may be destroyed. + bool final_callback_posted_ = false; + + // All methods except constructor are invoked on the network thread. + THREAD_CHECKER(network_thread_checker_); +}; + +Cronet_UrlRequestImpl::Cronet_UrlRequestImpl() = default; + +Cronet_UrlRequestImpl::~Cronet_UrlRequestImpl() { + base::AutoLock lock(lock_); + // Only request that has never started is allowed to exist at this point. + // The app must wait for OnSucceeded / OnFailed / OnCanceled callback before + // destroying |this|. + if (request_) { + CHECK(!started_); + DestroyRequestUnlessDoneLocked( + Cronet_RequestFinishedInfo_FINISHED_REASON_SUCCEEDED); + } +} + +Cronet_RESULT Cronet_UrlRequestImpl::InitWithParams( + Cronet_EnginePtr engine, + Cronet_String url, + Cronet_UrlRequestParamsPtr params, + Cronet_UrlRequestCallbackPtr callback, + Cronet_ExecutorPtr executor) { + CHECK(engine); + engine_ = reinterpret_cast(engine); + if (!url || std::string(url).empty()) + return engine_->CheckResult(Cronet_RESULT_NULL_POINTER_URL); + if (!params) + return engine_->CheckResult(Cronet_RESULT_NULL_POINTER_PARAMS); + if (!callback) + return engine_->CheckResult(Cronet_RESULT_NULL_POINTER_CALLBACK); + if (!executor) + return engine_->CheckResult(Cronet_RESULT_NULL_POINTER_EXECUTOR); + + VLOG(1) << "New Cronet_UrlRequest: " << url; + + base::AutoLock lock(lock_); + if (request_) { + return engine_->CheckResult( + Cronet_RESULT_ILLEGAL_STATE_REQUEST_ALREADY_INITIALIZED); + } + + callback_ = callback; + executor_ = executor; + + if (params->request_finished_listener != nullptr && + params->request_finished_executor == nullptr) { + return engine_->CheckResult( + Cronet_RESULT_NULL_POINTER_REQUEST_FINISHED_INFO_LISTENER_EXECUTOR); + } + + request_finished_listener_ = params->request_finished_listener; + request_finished_executor_ = params->request_finished_executor; + // Copy, don't move -- this function isn't allowed to change |params|. + annotations_ = params->annotations; + + auto network_tasks = std::make_unique(url, this); + network_tasks_ = network_tasks.get(); + + request_ = new CronetURLRequest( + engine_->cronet_url_request_context(), std::move(network_tasks), + GURL(url), ConvertRequestPriority(params->priority), + params->disable_cache, true /* params->disableConnectionMigration */, + request_finished_listener_ != nullptr || + engine_->HasRequestFinishedListener() /* params->enableMetrics */, + // TODO(pauljensen): Consider exposing TrafficStats API via C++ API. + false /* traffic_stats_tag_set */, 0 /* traffic_stats_tag */, + false /* traffic_stats_uid_set */, 0 /* traffic_stats_uid */, + ConvertIdempotency(params->idempotency)); + + if (params->upload_data_provider) { + upload_data_sink_ = std::make_unique( + this, params->upload_data_provider, + params->upload_data_provider_executor + ? params->upload_data_provider_executor + : executor); + upload_data_sink_->InitRequest(request_); + request_->SetHttpMethod("POST"); + } + + if (!params->http_method.empty() && + !request_->SetHttpMethod(params->http_method)) { + return engine_->CheckResult( + Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_HTTP_METHOD); + } + + for (const auto& request_header : params->request_headers) { + if (request_header.name.empty()) + return engine_->CheckResult(Cronet_RESULT_NULL_POINTER_HEADER_NAME); + if (request_header.value.empty()) + return engine_->CheckResult(Cronet_RESULT_NULL_POINTER_HEADER_VALUE); + if (!request_->AddRequestHeader(request_header.name, + request_header.value)) { + return engine_->CheckResult( + Cronet_RESULT_ILLEGAL_ARGUMENT_INVALID_HTTP_HEADER); + } + } + return engine_->CheckResult(Cronet_RESULT_SUCCESS); +} + +Cronet_RESULT Cronet_UrlRequestImpl::Start() { + base::AutoLock lock(lock_); + if (started_) { + return engine_->CheckResult( + Cronet_RESULT_ILLEGAL_STATE_REQUEST_ALREADY_STARTED); + } + if (!request_) { + return engine_->CheckResult( + Cronet_RESULT_ILLEGAL_STATE_REQUEST_NOT_INITIALIZED); + } +#if DCHECK_IS_ON() + Cronet_Executor_Execute(executor_, + new VerifyDestructionRunnable(&runnable_destroyed_)); +#endif // DCHECK_IS_ON() + request_->Start(); + started_ = true; + return engine_->CheckResult(Cronet_RESULT_SUCCESS); +} + +Cronet_RESULT Cronet_UrlRequestImpl::FollowRedirect() { + base::AutoLock lock(lock_); + if (!waiting_on_redirect_) { + return engine_->CheckResult( + Cronet_RESULT_ILLEGAL_STATE_UNEXPECTED_REDIRECT); + } + waiting_on_redirect_ = false; + if (!IsDoneLocked()) + request_->FollowDeferredRedirect(); + return engine_->CheckResult(Cronet_RESULT_SUCCESS); +} + +Cronet_RESULT Cronet_UrlRequestImpl::Read(Cronet_BufferPtr buffer) { + base::AutoLock lock(lock_); + if (!waiting_on_read_) + return engine_->CheckResult(Cronet_RESULT_ILLEGAL_STATE_UNEXPECTED_READ); + waiting_on_read_ = false; + if (IsDoneLocked()) { + Cronet_Buffer_Destroy(buffer); + return engine_->CheckResult(Cronet_RESULT_SUCCESS); + } + // Create IOBuffer that will own |buffer| while it is used by |request_|. + net::IOBuffer* io_buffer = new IOBufferWithCronet_Buffer(buffer); + if (request_->ReadData(io_buffer, Cronet_Buffer_GetSize(buffer))) + return engine_->CheckResult(Cronet_RESULT_SUCCESS); + return engine_->CheckResult(Cronet_RESULT_ILLEGAL_STATE_READ_FAILED); +} + +void Cronet_UrlRequestImpl::Cancel() { + base::AutoLock lock(lock_); + if (started_) { + // If request has posted callbacks to client executor, then it is possible + // that |request_| will be destroyed before callback is executed. The + // callback runnable uses IsDone() to avoid calling client callback in this + // case. + DestroyRequestUnlessDoneLocked( + Cronet_RequestFinishedInfo_FINISHED_REASON_CANCELED); + } +} + +bool Cronet_UrlRequestImpl::IsDone() { + base::AutoLock lock(lock_); + return IsDoneLocked(); +} + +bool Cronet_UrlRequestImpl::IsDoneLocked() const { + lock_.AssertAcquired(); + return started_ && request_ == nullptr; +} + +bool Cronet_UrlRequestImpl::DestroyRequestUnlessDone( + Cronet_RequestFinishedInfo_FINISHED_REASON finished_reason) { + base::AutoLock lock(lock_); + return DestroyRequestUnlessDoneLocked(finished_reason); +} + +bool Cronet_UrlRequestImpl::DestroyRequestUnlessDoneLocked( + Cronet_RequestFinishedInfo_FINISHED_REASON finished_reason) { + lock_.AssertAcquired(); + if (request_ == nullptr) + return true; + DCHECK(error_ == nullptr || + finished_reason == Cronet_RequestFinishedInfo_FINISHED_REASON_FAILED); + request_->Destroy(finished_reason == + Cronet_RequestFinishedInfo_FINISHED_REASON_CANCELED); + // Request can no longer be used as CronetURLRequest::Destroy() will + // eventually delete |request_| from the network thread, so setting |request_| + // to nullptr doesn't introduce a memory leak. + request_ = nullptr; + return false; +} + +void Cronet_UrlRequestImpl::GetStatus( + Cronet_UrlRequestStatusListenerPtr listener) { + { + base::AutoLock lock(lock_); + if (started_ && request_) { + status_listeners_.insert(listener); + request_->GetStatus( + base::BindOnce(&Cronet_UrlRequestImpl::NetworkTasks::OnStatus, + base::Unretained(network_tasks_), listener)); + return; + } + } + PostTaskToExecutor( + base::BindOnce(Cronet_UrlRequestStatusListener_OnStatus, listener, + Cronet_UrlRequestStatusListener_Status_INVALID)); +} + +void Cronet_UrlRequestImpl::PostCallbackOnFailedToExecutor() { + PostTaskToExecutor(base::BindOnce( + &Cronet_UrlRequestImpl::InvokeCallbackOnFailed, base::Unretained(this))); +} + +void Cronet_UrlRequestImpl::OnUploadDataProviderError( + const std::string& error_message) { + base::AutoLock lock(lock_); + // If |error_| is not nullptr, that means that another network error is + // already reported. + if (error_) + return; + error_ = CreateCronet_Error( + 0, 0, "Failure from UploadDataProvider: " + error_message); + error_->data.error_code = Cronet_Error_ERROR_CODE_ERROR_CALLBACK; + + request_->MaybeReportMetricsAndRunCallback( + base::BindOnce(&Cronet_UrlRequestImpl::PostCallbackOnFailedToExecutor, + base::Unretained(this))); +} + +void Cronet_UrlRequestImpl::PostTaskToExecutor(base::OnceClosure task) { + Cronet_RunnablePtr runnable = + new cronet::OnceClosureRunnable(std::move(task)); + // |runnable| is passed to executor, which destroys it after execution. + Cronet_Executor_Execute(executor_, runnable); +} + +void Cronet_UrlRequestImpl::InvokeCallbackOnRedirectReceived( + const std::string& new_location) { + if (IsDone()) + return; + Cronet_UrlRequestCallback_OnRedirectReceived( + callback_, this, GetData(response_info_), new_location.c_str()); +} + +void Cronet_UrlRequestImpl::InvokeCallbackOnResponseStarted() { + if (IsDone()) + return; +#if DCHECK_IS_ON() + // Verify that Executor calls Cronet_Runnable_Destroy(). + if (!runnable_destroyed_.TimedWait(base::Seconds(5))) { + LOG(ERROR) << "Cronet Executor didn't call Cronet_Runnable_Destroy() in " + "5s; still waiting."; + runnable_destroyed_.Wait(); + } +#endif // DCHECK_IS_ON() + Cronet_UrlRequestCallback_OnResponseStarted(callback_, this, + GetData(response_info_)); +} + +void Cronet_UrlRequestImpl::InvokeCallbackOnReadCompleted( + std::unique_ptr cronet_buffer, + int bytes_read) { + if (IsDone()) + return; + Cronet_UrlRequestCallback_OnReadCompleted( + callback_, this, GetData(response_info_), cronet_buffer.release(), + bytes_read); +} + +void Cronet_UrlRequestImpl::InvokeCallbackOnSucceeded() { + if (DestroyRequestUnlessDone( + Cronet_RequestFinishedInfo_FINISHED_REASON_SUCCEEDED)) { + return; + } + InvokeAllStatusListeners(); + MaybeReportMetrics(Cronet_RequestFinishedInfo_FINISHED_REASON_SUCCEEDED); + Cronet_UrlRequestCallback_OnSucceeded(callback_, this, + GetData(response_info_)); + // |this| may have been deleted here. +} + +void Cronet_UrlRequestImpl::InvokeCallbackOnFailed() { + if (DestroyRequestUnlessDone( + Cronet_RequestFinishedInfo_FINISHED_REASON_FAILED)) { + return; + } + InvokeAllStatusListeners(); + MaybeReportMetrics(Cronet_RequestFinishedInfo_FINISHED_REASON_FAILED); + Cronet_UrlRequestCallback_OnFailed(callback_, this, GetData(response_info_), + GetData(error_)); + // |this| may have been deleted here. +} + +void Cronet_UrlRequestImpl::InvokeCallbackOnCanceled() { + InvokeAllStatusListeners(); + MaybeReportMetrics(Cronet_RequestFinishedInfo_FINISHED_REASON_CANCELED); + Cronet_UrlRequestCallback_OnCanceled(callback_, this, + GetData(response_info_)); + // |this| may have been deleted here. +} + +void Cronet_UrlRequestImpl::InvokeAllStatusListeners() { + std::unordered_multiset status_listeners; + { + base::AutoLock lock(lock_); + // Verify the request has already been destroyed, which ensures no more + // status listeners can be added. + DCHECK(!request_); + status_listeners.swap(status_listeners_); + } + for (Cronet_UrlRequestStatusListener* status_listener : status_listeners) { + Cronet_UrlRequestStatusListener_OnStatus( + status_listener, Cronet_UrlRequestStatusListener_Status_INVALID); + } +#if DCHECK_IS_ON() + // Verify no status listeners added during OnStatus() callbacks. + base::AutoLock lock(lock_); + DCHECK(status_listeners_.empty()); +#endif // DCHECK_IS_ON() +} + +void Cronet_UrlRequestImpl::MaybeReportMetrics( + Cronet_RequestFinishedInfo_FINISHED_REASON finished_reason) { + if (request_finished_info_ == nullptr) + return; + request_finished_info_->data.annotations = std::move(annotations_); + request_finished_info_->data.finished_reason = finished_reason; + + engine_->ReportRequestFinished(request_finished_info_, response_info_, + error_); + if (request_finished_listener_ != nullptr) { + DCHECK(request_finished_executor_ != nullptr); + // Execute() owns and deletes the runnable. + request_finished_executor_->Execute( + new cronet::OnceClosureRunnable(base::BindOnce( + [](Cronet_RequestFinishedInfoListenerPtr request_finished_listener, + scoped_refptr request_finished_info, + scoped_refptr response_info, + scoped_refptr error) { + request_finished_listener->OnRequestFinished( + GetData(request_finished_info), GetData(response_info), + GetData(error)); + }, + request_finished_listener_, request_finished_info_, response_info_, + error_))); + } +} + +Cronet_UrlRequestImpl::NetworkTasks::NetworkTasks( + const std::string& url, + Cronet_UrlRequestImpl* url_request) + : url_request_(url_request), url_chain_({url}) { + DETACH_FROM_THREAD(network_thread_checker_); + DCHECK(url_request); +} + +void Cronet_UrlRequestImpl::NetworkTasks::OnReceivedRedirect( + const std::string& new_location, + int http_status_code, + const std::string& http_status_text, + const net::HttpResponseHeaders* headers, + bool was_cached, + const std::string& negotiated_protocol, + const std::string& proxy_server, + int64_t received_byte_count) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + { + base::AutoLock lock(url_request_->lock_); + url_request_->waiting_on_redirect_ = true; + url_request_->response_info_ = CreateCronet_UrlResponseInfo( + url_chain_, http_status_code, http_status_text, headers, was_cached, + negotiated_protocol, proxy_server, received_byte_count); + } + + // Have to do this after creating responseInfo. + url_chain_.push_back(new_location); + + // Invoke Cronet_UrlRequestCallback_OnRedirectReceived on client executor. + url_request_->PostTaskToExecutor( + base::BindOnce(&Cronet_UrlRequestImpl::InvokeCallbackOnRedirectReceived, + base::Unretained(url_request_), new_location)); +} + +void Cronet_UrlRequestImpl::NetworkTasks::OnResponseStarted( + int http_status_code, + const std::string& http_status_text, + const net::HttpResponseHeaders* headers, + bool was_cached, + const std::string& negotiated_protocol, + const std::string& proxy_server, + int64_t received_byte_count) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + { + base::AutoLock lock(url_request_->lock_); + url_request_->waiting_on_read_ = true; + url_request_->response_info_ = CreateCronet_UrlResponseInfo( + url_chain_, http_status_code, http_status_text, headers, was_cached, + negotiated_protocol, proxy_server, received_byte_count); + } + + if (url_request_->upload_data_sink_) + url_request_->upload_data_sink_->PostCloseToExecutor(); + + // Invoke Cronet_UrlRequestCallback_OnResponseStarted on client executor. + url_request_->PostTaskToExecutor( + base::BindOnce(&Cronet_UrlRequestImpl::InvokeCallbackOnResponseStarted, + base::Unretained(url_request_))); +} + +void Cronet_UrlRequestImpl::NetworkTasks::OnReadCompleted( + scoped_refptr buffer, + int bytes_read, + int64_t received_byte_count) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + IOBufferWithCronet_Buffer* io_buffer = + reinterpret_cast(buffer.get()); + std::unique_ptr cronet_buffer(io_buffer->Release()); + { + base::AutoLock lock(url_request_->lock_); + url_request_->waiting_on_read_ = true; + url_request_->response_info_->data.received_byte_count = + received_byte_count; + } + + // Invoke Cronet_UrlRequestCallback_OnReadCompleted on client executor. + url_request_->PostTaskToExecutor(base::BindOnce( + &Cronet_UrlRequestImpl::InvokeCallbackOnReadCompleted, + base::Unretained(url_request_), std::move(cronet_buffer), bytes_read)); +} + +void Cronet_UrlRequestImpl::NetworkTasks::OnSucceeded( + int64_t received_byte_count) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + { + base::AutoLock lock(url_request_->lock_); + url_request_->response_info_->data.received_byte_count = + received_byte_count; + } + + // Invoke Cronet_UrlRequestCallback_OnSucceeded on client executor. + url_request_->PostTaskToExecutor( + base::BindOnce(&Cronet_UrlRequestImpl::InvokeCallbackOnSucceeded, + base::Unretained(url_request_))); + final_callback_posted_ = true; +} + +void Cronet_UrlRequestImpl::NetworkTasks::OnError( + int net_error, + int quic_error, + const std::string& error_string, + int64_t received_byte_count) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + { + base::AutoLock lock(url_request_->lock_); + if (url_request_->response_info_) + url_request_->response_info_->data.received_byte_count = + received_byte_count; + url_request_->error_ = + CreateCronet_Error(net_error, quic_error, error_string); + } + + if (url_request_->upload_data_sink_) + url_request_->upload_data_sink_->PostCloseToExecutor(); + + // Invoke Cronet_UrlRequestCallback_OnFailed on client executor. + url_request_->PostTaskToExecutor( + base::BindOnce(&Cronet_UrlRequestImpl::InvokeCallbackOnFailed, + base::Unretained(url_request_))); + final_callback_posted_ = true; +} + +void Cronet_UrlRequestImpl::NetworkTasks::OnCanceled() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + if (url_request_->upload_data_sink_) + url_request_->upload_data_sink_->PostCloseToExecutor(); + + // Invoke Cronet_UrlRequestCallback_OnCanceled on client executor. + url_request_->PostTaskToExecutor( + base::BindOnce(&Cronet_UrlRequestImpl::InvokeCallbackOnCanceled, + base::Unretained(url_request_))); + final_callback_posted_ = true; +} + +void Cronet_UrlRequestImpl::NetworkTasks::OnDestroyed() { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + DCHECK(url_request_); +} + +void Cronet_UrlRequestImpl::NetworkTasks::OnMetricsCollected( + const base::Time& request_start_time, + const base::TimeTicks& request_start, + const base::TimeTicks& dns_start, + const base::TimeTicks& dns_end, + const base::TimeTicks& connect_start, + const base::TimeTicks& connect_end, + const base::TimeTicks& ssl_start, + const base::TimeTicks& ssl_end, + const base::TimeTicks& send_start, + const base::TimeTicks& send_end, + const base::TimeTicks& push_start, + const base::TimeTicks& push_end, + const base::TimeTicks& receive_headers_end, + const base::TimeTicks& request_end, + bool socket_reused, + int64_t sent_bytes_count, + int64_t received_bytes_count) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + base::AutoLock lock(url_request_->lock_); + DCHECK_EQ(url_request_->request_finished_info_, nullptr) + << "Metrics collection should only happen once."; + url_request_->request_finished_info_ = + base::MakeRefCounted(); + auto& metrics = url_request_->request_finished_info_->data.metrics; + metrics.emplace(); + using native_metrics_util::ConvertTime; + ConvertTime(request_start, request_start, request_start_time, + &metrics->request_start); + ConvertTime(dns_start, request_start, request_start_time, + &metrics->dns_start); + ConvertTime(dns_end, request_start, request_start_time, &metrics->dns_end); + ConvertTime(connect_start, request_start, request_start_time, + &metrics->connect_start); + ConvertTime(connect_end, request_start, request_start_time, + &metrics->connect_end); + ConvertTime(ssl_start, request_start, request_start_time, + &metrics->ssl_start); + ConvertTime(ssl_end, request_start, request_start_time, &metrics->ssl_end); + ConvertTime(send_start, request_start, request_start_time, + &metrics->sending_start); + ConvertTime(send_end, request_start, request_start_time, + &metrics->sending_end); + ConvertTime(push_start, request_start, request_start_time, + &metrics->push_start); + ConvertTime(push_end, request_start, request_start_time, &metrics->push_end); + ConvertTime(receive_headers_end, request_start, request_start_time, + &metrics->response_start); + ConvertTime(request_end, request_start, request_start_time, + &metrics->request_end); + metrics->socket_reused = socket_reused; + metrics->sent_byte_count = sent_bytes_count; + metrics->received_byte_count = received_bytes_count; +} + +void Cronet_UrlRequestImpl::NetworkTasks::OnStatus( + Cronet_UrlRequestStatusListenerPtr listener, + net::LoadState load_state) { + DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_); + if (final_callback_posted_) + return; + { + base::AutoLock lock(url_request_->lock_); + auto element = url_request_->status_listeners_.find(listener); + CHECK(element != url_request_->status_listeners_.end()); + url_request_->status_listeners_.erase(element); + } + + // Invoke Cronet_UrlRequestCallback_OnCanceled on client executor. + url_request_->PostTaskToExecutor( + base::BindOnce(&Cronet_UrlRequestStatusListener_OnStatus, listener, + ConvertLoadState(load_state))); +} + +} // namespace cronet + +CRONET_EXPORT Cronet_UrlRequestPtr Cronet_UrlRequest_Create() { + return new cronet::Cronet_UrlRequestImpl(); +} diff --git a/src/components/cronet/native/url_request.h b/src/components/cronet/native/url_request.h new file mode 100644 index 0000000000..f1732f93f4 --- /dev/null +++ b/src/components/cronet/native/url_request.h @@ -0,0 +1,209 @@ +// Copyright 2018 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_NATIVE_URL_REQUEST_H_ +#define COMPONENTS_CRONET_NATIVE_URL_REQUEST_H_ + +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/synchronization/lock.h" +#include "base/synchronization/waitable_event.h" +#include "base/thread_annotations.h" +#include "components/cronet/cronet_url_request.h" +#include "components/cronet/cronet_url_request_context.h" +#include "components/cronet/native/generated/cronet.idl_impl_interface.h" + +namespace net { +enum LoadState; +} // namespace net + +namespace cronet { + +class Cronet_EngineImpl; +class Cronet_UploadDataSinkImpl; + +// Implementation of Cronet_UrlRequest that uses CronetURLRequestContext. +class Cronet_UrlRequestImpl : public Cronet_UrlRequest { + public: + Cronet_UrlRequestImpl(); + + Cronet_UrlRequestImpl(const Cronet_UrlRequestImpl&) = delete; + Cronet_UrlRequestImpl& operator=(const Cronet_UrlRequestImpl&) = delete; + + ~Cronet_UrlRequestImpl() override; + + // Cronet_UrlRequest + Cronet_RESULT InitWithParams(Cronet_EnginePtr engine, + Cronet_String url, + Cronet_UrlRequestParamsPtr params, + Cronet_UrlRequestCallbackPtr callback, + Cronet_ExecutorPtr executor) override; + Cronet_RESULT Start() override; + Cronet_RESULT FollowRedirect() override; + Cronet_RESULT Read(Cronet_BufferPtr buffer) override; + void Cancel() override; + bool IsDone() override; + void GetStatus(Cronet_UrlRequestStatusListenerPtr listener) override; + + // Upload data provider has reported error while reading or rewinding + // so request must fail. + void OnUploadDataProviderError(const std::string& error_message); + + private: + class NetworkTasks; + + // Return |true| if request has started and is now done. + // Must be called under |lock_| held. + bool IsDoneLocked() const SHARED_LOCKS_REQUIRED(lock_); + + // Helper method to set final status of CronetUrlRequest and clean up the + // native request adapter. Returns true if request is already done, false + // request is not done and is destroyed. + bool DestroyRequestUnlessDone( + Cronet_RequestFinishedInfo_FINISHED_REASON finished_reason); + + // Helper method to set final status of CronetUrlRequest and clean up the + // native request adapter. Returns true if request is already done, false + // request is not done and is destroyed. Must be called under |lock_| held. + bool DestroyRequestUnlessDoneLocked( + Cronet_RequestFinishedInfo_FINISHED_REASON finished_reason) + EXCLUSIVE_LOCKS_REQUIRED(lock_); + + // Helper method to post |task| to the |executor_|. + void PostTaskToExecutor(base::OnceClosure task); + + // Helper methods to invoke application |callback_|. + void InvokeCallbackOnRedirectReceived(const std::string& new_location); + void InvokeCallbackOnResponseStarted(); + void InvokeCallbackOnReadCompleted( + std::unique_ptr cronet_buffer, + int bytes_read); + void InvokeCallbackOnSucceeded(); + void InvokeCallbackOnFailed(); + void InvokeCallbackOnCanceled(); + + // Runs InvokeCallbackOnFailed() on the client executor. + void PostCallbackOnFailedToExecutor(); + + // Invoke all members of |status_listeners_|. Should be called prior to + // invoking a final callback. Once a final callback has been called, |this| + // and |executor_| may be deleted and so the callbacks cannot be issued. + void InvokeAllStatusListeners(); + + // Reports metrics if metrics were collected, otherwise does nothing. This + // method should only be called once on Callback's executor thread and before + // Callback's OnSucceeded, OnFailed and OnCanceled. + // + // Adds |finished_reason| to the reported RequestFinishedInfo. Also passes + // pointers to |response_info_| and |error_|. + // + // Also, the field |annotations_| is moved into the RequestFinishedInfo. + // + // |finished_reason|: Success / fail / cancel status of request. + void MaybeReportMetrics( + Cronet_RequestFinishedInfo_FINISHED_REASON finished_reason); + + // Synchronize access to |request_| and other objects below from different + // threads. + base::Lock lock_; + // NetworkTask object lives on the network thread. Owned by |request_|. + // Outlives this. + raw_ptr network_tasks_ GUARDED_BY(lock_) = nullptr; + // Cronet URLRequest used for this operation. + raw_ptr request_ GUARDED_BY(lock_) = nullptr; + bool started_ GUARDED_BY(lock_) = false; + bool waiting_on_redirect_ GUARDED_BY(lock_) = false; + bool waiting_on_read_ GUARDED_BY(lock_) = false; + // Set of status_listeners_ that have not yet been called back. + std::unordered_multiset status_listeners_ + GUARDED_BY(lock_); + + // Report containing metrics and other information to send to attached + // RequestFinishedListener(s). A nullptr value indicates that metrics haven't + // been collected. + // + // Ownership is shared since we guarantee that the RequestFinishedInfo will + // be valid if its UrlRequest isn't destroyed. We also guarantee that it's + // valid in RequestFinishedListener.OnRequestFinished() even if the + // UrlRequest is destroyed (and furthermore, each listener finishes at + // different times). + // + // NOTE: this field isn't protected by |lock_| since we pass this field as a + // unowned pointer to OnRequestFinished(). The pointee of this field cannot + // be updated after that call is made. + scoped_refptr> + request_finished_info_; + + // Annotations passed via UrlRequestParams.annotations. These annotations + // aren't used by Cronet itself -- they're just moved into the + // RequestFinishedInfo passed to RequestFinishedInfoListener instances. + std::vector annotations_; + + // Optional; allows a listener to receive request info and stats. + // + // A nullptr value indicates that there is no RequestFinishedInfo listener + // specified for the request (however, the Engine may have additional + // listeners -- Engine listeners apply to all its UrlRequests). + // + // Owned by the app -- must outlive this UrlRequest. + Cronet_RequestFinishedInfoListenerPtr request_finished_listener_ = nullptr; + + // Executor upon which |request_finished_listener_| will run. If + // |request_finished_listener_| is not nullptr, this won't be nullptr either. + // + // Owned by the app -- must outlive this UrlRequest. + Cronet_ExecutorPtr request_finished_executor_ = nullptr; + + // Response info updated by callback with number of bytes received. May be + // nullptr, if no response has been received. + // + // Ownership is shared since we guarantee that the UrlResponseInfo will + // be valid if its UrlRequest isn't destroyed. We also guarantee that it's + // valid in RequestFinishedListener.OnRequestFinished() even if the + // UrlRequest is destroyed (and furthermore, each listener finishes at + // different times). + // + // NOTE: the synchronization of this field is complex -- it can't be + // completely protected by |lock_| since we pass this field as a unowned + // pointer to OnSucceed(), OnFailed(), and OnCanceled(). The pointee of this + // field cannot be updated after one of those callback calls is made. + scoped_refptr> response_info_; + + // The error reported by request. May be nullptr if no error has occurred. + // + // Ownership is shared since we guarantee that the Error will be valid if its + // UrlRequest isn't destroyed. We also guarantee that it's valid in + // RequestFinishedListener.OnRequestFinished() even if the UrlRequest is + // destroyed (and furthermore, each listener finishes at different times). + // + // NOTE: the synchronization of this field is complex -- it can't be + // completely protected by |lock_| since we pass this field as an unowned + // pointer to OnSucceed(), OnFailed(), and OnCanceled(). The pointee of this + // field cannot be updated after one of those callback calls is made. + scoped_refptr> error_; + + // The upload data stream if specified. + std::unique_ptr upload_data_sink_; + + // Application callback interface, used, but not owned, by |this|. + Cronet_UrlRequestCallbackPtr callback_ = nullptr; + // Executor for application callback, used, but not owned, by |this|. + Cronet_ExecutorPtr executor_ = nullptr; + + // Cronet Engine used to run network operations. Not owned, accessed from + // client thread. Must outlive this request. + raw_ptr engine_ = nullptr; + +#if DCHECK_IS_ON() + // Event indicating Executor is properly destroying Runnables. + base::WaitableEvent runnable_destroyed_; +#endif // DCHECK_IS_ON() +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_NATIVE_URL_REQUEST_H_ diff --git a/src/components/cronet/pylintrc b/src/components/cronet/pylintrc new file mode 100644 index 0000000000..b45622a467 --- /dev/null +++ b/src/components/cronet/pylintrc @@ -0,0 +1,17 @@ +[MESSAGES CONTROL] + +# Disable the message, report, category or checker with the given id(s). +# TODO(pauljensen): Shrink this list to as small as possible. +disable=abstract-class-little-used,abstract-class-not-used,bad-continuation,broad-except,fixme,global-statement,interface-not-implemented,invalid-name,locally-disabled,locally-enabled,missing-docstring,no-self-use,protected-access,star-args,too-few-public-methods,too-many-arguments,too-many-branches,too-many-function-args,too-many-instance-attributes,too-many-lines,too-many-locals,too-many-public-methods,too-many-return-statements,too-many-statements,unused-argument,unnecessary-semicolon,bad-whitespace,superfluous-parens + + +[REPORTS] + +# Don't write out full reports, just messages. +reports=no + + +[FORMAT] + +# We use two spaces for indents, instead of the usual four spaces or tab. +indent-string=' ' diff --git a/src/components/cronet/run_all_unittests.cc b/src/components/cronet/run_all_unittests.cc new file mode 100644 index 0000000000..1e7f6bd4ed --- /dev/null +++ b/src/components/cronet/run_all_unittests.cc @@ -0,0 +1,20 @@ +// 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 "base/bind.h" +#include "base/test/launcher/unit_test_launcher.h" +#include "base/test/test_suite.h" + +int main(int argc, char** argv) { + base::TestSuite test_suite(argc, argv); +#if defined(CRONET_TESTS_IMPLEMENTATION) + // cronet_tests[_android] link the Cronet implementation into the test + // suite statically in many configurations, causing globals initialized by + // the library (e.g. ThreadPool) to be visible to the TestSuite. + test_suite.DisableCheckForLeakedGlobals(); +#endif + return base::LaunchUnitTests( + argc, argv, + base::BindOnce(&base::TestSuite::Run, base::Unretained(&test_suite))); +} diff --git a/src/components/cronet/stale_host_resolver.cc b/src/components/cronet/stale_host_resolver.cc new file mode 100644 index 0000000000..004b7b19d6 --- /dev/null +++ b/src/components/cronet/stale_host_resolver.cc @@ -0,0 +1,408 @@ +// Copyright 2016 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 "components/cronet/stale_host_resolver.h" + +#include +#include +#include +#include +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/check_op.h" +#include "base/notreached.h" +#include "base/timer/timer.h" +#include "base/values.h" +#include "net/base/host_port_pair.h" +#include "net/base/net_errors.h" +#include "net/base/network_isolation_key.h" +#include "net/dns/context_host_resolver.h" +#include "net/dns/dns_util.h" +#include "net/dns/host_resolver.h" +#include "net/dns/host_resolver_results.h" +#include "net/dns/public/host_resolver_source.h" +#include "net/dns/public/resolve_error_info.h" +#include "net/log/net_log_with_source.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "url/scheme_host_port.h" + +namespace cronet { + +// A request made by the StaleHostResolver. May return fresh cached data, +// network data, or stale cached data. +class StaleHostResolver::RequestImpl + : public net::HostResolver::ResolveHostRequest { + public: + // StaleOptions will be read directly from |resolver|. + RequestImpl(base::WeakPtr resolver, + const net::HostPortPair& host, + const net::NetworkIsolationKey& network_isolation_key, + const net::NetLogWithSource& net_log, + const ResolveHostParameters& input_parameters, + const base::TickClock* tick_clock); + ~RequestImpl() override = default; + + // net::HostResolver::ResolveHostRequest implementation: + int Start(net::CompletionOnceCallback result_callback) override; + const net::AddressList* GetAddressResults() const override; + const std::vector* GetEndpointResults() + const override; + const absl::optional>& GetTextResults() + const override; + const absl::optional>& GetHostnameResults() + const override; + const std::set* GetDnsAliasResults() const override; + net::ResolveErrorInfo GetResolveErrorInfo() const override; + const absl::optional& GetStaleInfo() + const override; + void ChangeRequestPriority(net::RequestPriority priority) override; + + // Called on completion of an asynchronous (network) inner request. Expected + // to be called by StaleHostResolver::OnNetworkRequestComplete(). + void OnNetworkRequestComplete(int error); + + private: + bool have_network_request() const { return network_request_ != nullptr; } + bool have_cache_data() const { + return cache_error_ != net::ERR_DNS_CACHE_MISS; + } + bool have_returned() const { return result_callback_.is_null(); } + + // Determines if |cache_error_| and |cache_request_| represents a usable entry + // per the requirements of |resolver_->options_|. + bool CacheDataIsUsable() const; + + // Callback for |stale_timer_| that returns stale results. + void OnStaleDelayElapsed(); + + base::WeakPtr resolver_; + + const net::HostPortPair host_; + const net::NetworkIsolationKey network_isolation_key_; + const net::NetLogWithSource net_log_; + const ResolveHostParameters input_parameters_; + + // The callback passed into |Start()| to be called when the request returns. + net::CompletionOnceCallback result_callback_; + + // The error from the stale cache entry, if there was one. + // If not, net::ERR_DNS_CACHE_MISS. + int cache_error_; + // Inner local-only/stale-allowed request. + std::unique_ptr cache_request_; + // A timer that fires when the |Request| should return stale results, if the + // underlying network request has not finished yet. + base::OneShotTimer stale_timer_; + + // An inner request for network results. Only set if |cache_request_| gave a + // stale or unusable result, and unset if the stale result is to be used as + // the overall result. + std::unique_ptr network_request_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +StaleHostResolver::RequestImpl::RequestImpl( + base::WeakPtr resolver, + const net::HostPortPair& host, + const net::NetworkIsolationKey& network_isolation_key, + const net::NetLogWithSource& net_log, + const ResolveHostParameters& input_parameters, + const base::TickClock* tick_clock) + : resolver_(std::move(resolver)), + host_(host), + network_isolation_key_(network_isolation_key), + net_log_(net_log), + input_parameters_(input_parameters), + cache_error_(net::ERR_DNS_CACHE_MISS), + stale_timer_(tick_clock) { + DCHECK(resolver_); +} + +int StaleHostResolver::RequestImpl::Start( + net::CompletionOnceCallback result_callback) { + DCHECK(resolver_); + DCHECK(!result_callback.is_null()); + + net::HostResolver::ResolveHostParameters cache_parameters = input_parameters_; + cache_parameters.cache_usage = + net::HostResolver::ResolveHostParameters::CacheUsage::STALE_ALLOWED; + cache_parameters.source = net::HostResolverSource::LOCAL_ONLY; + cache_request_ = resolver_->inner_resolver_->CreateRequest( + host_, network_isolation_key_, net_log_, cache_parameters); + int error = + cache_request_->Start(base::BindOnce([](int error) { NOTREACHED(); })); + DCHECK_NE(net::ERR_IO_PENDING, error); + cache_error_ = cache_request_->GetResolveErrorInfo().error; + DCHECK_NE(net::ERR_IO_PENDING, cache_error_); + // If it's a fresh cache hit (or literal), return it synchronously. + if (cache_error_ != net::ERR_DNS_CACHE_MISS && + (!cache_request_->GetStaleInfo() || + !cache_request_->GetStaleInfo().value().is_stale())) { + return cache_error_; + } + + if (cache_error_ != net::ERR_DNS_CACHE_MISS && + input_parameters_.cache_usage == + net::HostResolver::ResolveHostParameters::CacheUsage::STALE_ALLOWED) { + return cache_error_; + } + + result_callback_ = std::move(result_callback); + + if (CacheDataIsUsable()) { + // |stale_timer_| is deleted when the Request is deleted, so it's safe to + // use Unretained here. + stale_timer_.Start( + FROM_HERE, resolver_->options_.delay, + base::BindOnce(&StaleHostResolver::RequestImpl::OnStaleDelayElapsed, + base::Unretained(this))); + } else { + cache_error_ = net::ERR_DNS_CACHE_MISS; + cache_request_.reset(); + } + + // Don't check the cache again. + net::HostResolver::ResolveHostParameters no_cache_parameters = + input_parameters_; + no_cache_parameters.cache_usage = + net::HostResolver::ResolveHostParameters::CacheUsage::DISALLOWED; + network_request_ = resolver_->inner_resolver_->CreateRequest( + host_, network_isolation_key_, net_log_, no_cache_parameters); + int network_rv = network_request_->Start( + base::BindOnce(&StaleHostResolver::OnNetworkRequestComplete, resolver_, + network_request_.get(), weak_ptr_factory_.GetWeakPtr())); + + // Network resolver has returned synchronously (for example by resolving from + // /etc/hosts). + if (network_rv != net::ERR_IO_PENDING) { + stale_timer_.Stop(); + } + return network_rv; +} + +const net::AddressList* StaleHostResolver::RequestImpl::GetAddressResults() + const { + if (network_request_) + return network_request_->GetAddressResults(); + + DCHECK(cache_request_); + return cache_request_->GetAddressResults(); +} + +const std::vector* +StaleHostResolver::RequestImpl::GetEndpointResults() const { + if (network_request_) + return network_request_->GetEndpointResults(); + + DCHECK(cache_request_); + return cache_request_->GetEndpointResults(); +} + +const absl::optional>& +StaleHostResolver::RequestImpl::GetTextResults() const { + if (network_request_) + return network_request_->GetTextResults(); + + DCHECK(cache_request_); + return cache_request_->GetTextResults(); +} + +const absl::optional>& +StaleHostResolver::RequestImpl::GetHostnameResults() const { + if (network_request_) + return network_request_->GetHostnameResults(); + + DCHECK(cache_request_); + return cache_request_->GetHostnameResults(); +} + +const std::set* +StaleHostResolver::RequestImpl::GetDnsAliasResults() const { + if (network_request_) + return network_request_->GetDnsAliasResults(); + + DCHECK(cache_request_); + return cache_request_->GetDnsAliasResults(); +} + +net::ResolveErrorInfo StaleHostResolver::RequestImpl::GetResolveErrorInfo() + const { + if (network_request_) + return network_request_->GetResolveErrorInfo(); + DCHECK(cache_request_); + return cache_request_->GetResolveErrorInfo(); +} + +const absl::optional& +StaleHostResolver::RequestImpl::GetStaleInfo() const { + if (network_request_) + return network_request_->GetStaleInfo(); + + DCHECK(cache_request_); + return cache_request_->GetStaleInfo(); +} + +void StaleHostResolver::RequestImpl::ChangeRequestPriority( + net::RequestPriority priority) { + if (network_request_) { + network_request_->ChangeRequestPriority(priority); + } else { + DCHECK(cache_request_); + cache_request_->ChangeRequestPriority(priority); + } +} + +void StaleHostResolver::RequestImpl::OnNetworkRequestComplete(int error) { + DCHECK(resolver_); + DCHECK(have_network_request()); + DCHECK(!have_returned()); + + bool return_stale_data_instead_of_network_name_not_resolved = + resolver_->options_.use_stale_on_name_not_resolved && + error == net::ERR_NAME_NOT_RESOLVED && have_cache_data(); + + stale_timer_.Stop(); + + if (return_stale_data_instead_of_network_name_not_resolved) { + network_request_.reset(); + std::move(result_callback_).Run(cache_error_); + } else { + cache_request_.reset(); + std::move(result_callback_).Run(error); + } +} + +bool StaleHostResolver::RequestImpl::CacheDataIsUsable() const { + DCHECK(resolver_); + DCHECK(cache_request_); + + if (cache_error_ != net::OK) + return false; + + DCHECK(cache_request_->GetStaleInfo()); + const net::HostCache::EntryStaleness& staleness = + cache_request_->GetStaleInfo().value(); + + if (resolver_->options_.max_expired_time != base::TimeDelta() && + staleness.expired_by > resolver_->options_.max_expired_time) { + return false; + } + if (resolver_->options_.max_stale_uses > 0 && + staleness.stale_hits > resolver_->options_.max_stale_uses) { + return false; + } + if (!resolver_->options_.allow_other_network && + staleness.network_changes > 0) { + return false; + } + return true; +} + +void StaleHostResolver::RequestImpl::OnStaleDelayElapsed() { + DCHECK(!have_returned()); + DCHECK(have_cache_data()); + DCHECK(have_network_request()); + + // If resolver is destroyed after starting a request, the request is + // considered cancelled and callbacks must not be invoked. Logging the + // cancellation will happen on destruction of |this|. + if (!resolver_) { + network_request_.reset(); + return; + } + DCHECK(CacheDataIsUsable()); + + // Detach |network_request_| to allow it to complete and backfill the cache + // even if |this| is destroyed. + resolver_->DetachRequest(std::move(network_request_)); + + std::move(result_callback_).Run(cache_error_); +} + +StaleHostResolver::StaleOptions::StaleOptions() + : allow_other_network(false), + max_stale_uses(0), + use_stale_on_name_not_resolved(false) {} + +StaleHostResolver::StaleHostResolver( + std::unique_ptr inner_resolver, + const StaleOptions& stale_options) + : inner_resolver_(std::move(inner_resolver)), options_(stale_options) { + DCHECK_LE(0, stale_options.max_expired_time.InMicroseconds()); + DCHECK_LE(0, stale_options.max_stale_uses); +} + +StaleHostResolver::~StaleHostResolver() {} + +void StaleHostResolver::OnShutdown() { + inner_resolver_->OnShutdown(); +} + +std::unique_ptr +StaleHostResolver::CreateRequest( + url::SchemeHostPort host, + net::NetworkIsolationKey network_isolation_key, + net::NetLogWithSource net_log, + absl::optional optional_parameters) { + // TODO(crbug.com/1206799): Propagate scheme. + return CreateRequest(net::HostPortPair::FromSchemeHostPort(host), + network_isolation_key, net_log, optional_parameters); +} + +std::unique_ptr +StaleHostResolver::CreateRequest( + const net::HostPortPair& host, + const net::NetworkIsolationKey& network_isolation_key, + const net::NetLogWithSource& net_log, + const absl::optional& optional_parameters) { + DCHECK(tick_clock_); + return std::make_unique( + weak_ptr_factory_.GetWeakPtr(), host, network_isolation_key, net_log, + optional_parameters.value_or(ResolveHostParameters()), tick_clock_); +} + +net::HostCache* StaleHostResolver::GetHostCache() { + return inner_resolver_->GetHostCache(); +} + +base::Value StaleHostResolver::GetDnsConfigAsValue() const { + return inner_resolver_->GetDnsConfigAsValue(); +} + +void StaleHostResolver::SetRequestContext( + net::URLRequestContext* request_context) { + inner_resolver_->SetRequestContext(request_context); +} + +void StaleHostResolver::OnNetworkRequestComplete( + ResolveHostRequest* network_request, + base::WeakPtr stale_request, + int error) { + if (detached_requests_.erase(network_request)) + return; + + // If not a detached request, there should still be an owning RequestImpl. + // Otherwise the request should have been cancelled and this method never + // called. + DCHECK(stale_request); + + stale_request->OnNetworkRequestComplete(error); +} + +void StaleHostResolver::DetachRequest( + std::unique_ptr request) { + DCHECK_EQ(0u, detached_requests_.count(request.get())); + detached_requests_[request.get()] = std::move(request); +} + +void StaleHostResolver::SetTickClockForTesting( + const base::TickClock* tick_clock) { + tick_clock_ = tick_clock; + inner_resolver_->SetTickClockForTesting(tick_clock); +} + +} // namespace cronet diff --git a/src/components/cronet/stale_host_resolver.h b/src/components/cronet/stale_host_resolver.h new file mode 100644 index 0000000000..7125aca0e5 --- /dev/null +++ b/src/components/cronet/stale_host_resolver.h @@ -0,0 +1,146 @@ +// Copyright 2016 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_STALE_HOST_RESOLVER_H_ +#define COMPONENTS_CRONET_STALE_HOST_RESOLVER_H_ + +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/time/default_tick_clock.h" +#include "net/base/completion_once_callback.h" +#include "net/base/network_isolation_key.h" +#include "net/dns/host_resolver.h" +#include "net/log/net_log_with_source.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "url/scheme_host_port.h" + +namespace base { +class TickClock; +} // namespace base + +namespace net { +class ContextHostResolver; +} // namespace net + +namespace cronet { +namespace { +class StaleHostResolverTest; +} // namespace + +// A HostResolver that wraps a ContextHostResolver and uses it to make requests, +// but "impatiently" returns stale data (if available and usable) after a delay, +// to reduce DNS latency at the expense of accuracy. +class StaleHostResolver : public net::HostResolver { + public: + struct StaleOptions { + StaleOptions(); + + // How long to wait before returning stale data, if available. + base::TimeDelta delay; + + // If positive, how long stale data can be past the expiration time before + // it's considered unusable. If zero or negative, stale data can be used + // indefinitely. + base::TimeDelta max_expired_time; + + // If set, stale data from previous networks is usable; if clear, it's not. + // + // If the other network had a working, correct DNS setup, this can increase + // the availability of useful stale results. + // + // If the other network had a broken (e.g. hijacked for captive portal) DNS + // setup, this will instead end up returning useless results. + bool allow_other_network; + + // If positive, the maximum number of times a stale entry can be used. If + // zero, there is no limit. + int max_stale_uses; + + // If network resolution returns ERR_NAME_NOT_RESOLVED, use stale result if + // available. + bool use_stale_on_name_not_resolved; + }; + + // Creates a StaleHostResolver that uses |inner_resolver| for actual + // resolution, but potentially returns stale data according to + // |stale_options|. + StaleHostResolver(std::unique_ptr inner_resolver, + const StaleOptions& stale_options); + + StaleHostResolver(const StaleHostResolver&) = delete; + StaleHostResolver& operator=(const StaleHostResolver&) = delete; + + ~StaleHostResolver() override; + + // HostResolver implementation: + + void OnShutdown() override; + + // Resolves as a regular HostResolver, but if stale data is available and + // usable (according to the options passed to the constructor), and fresh data + // is not returned before the specified delay, returns the stale data instead. + // + // If stale data is returned, the StaleHostResolver allows the underlying + // request to continue in order to repopulate the cache. + std::unique_ptr CreateRequest( + url::SchemeHostPort host, + net::NetworkIsolationKey network_isolation_key, + net::NetLogWithSource net_log, + absl::optional optional_parameters) override; + std::unique_ptr CreateRequest( + const net::HostPortPair& host, + const net::NetworkIsolationKey& network_isolation_key, + const net::NetLogWithSource& net_log, + const absl::optional& optional_parameters) + override; + + // The remaining public methods pass through to the inner resolver: + + net::HostCache* GetHostCache() override; + base::Value GetDnsConfigAsValue() const override; + void SetRequestContext(net::URLRequestContext* request_context) override; + + private: + class RequestImpl; + friend class StaleHostResolverTest; + + // Called on completion of |network_request| when completed asynchronously (a + // "network" request). Determines if the request is owned by a RequestImpl or + // if it is a detached request and handles appropriately. + void OnNetworkRequestComplete(ResolveHostRequest* network_request, + base::WeakPtr stale_request, + int error); + + // Detach an inner request from a RequestImpl, letting it finish (and populate + // the host cache) as long as |this| is not destroyed. + void DetachRequest(std::unique_ptr request); + + // Set |tick_clock_| for testing. Must be set before issuing any requests. + void SetTickClockForTesting(const base::TickClock* tick_clock); + + // The underlying ContextHostResolver that will be used to make cache and + // network requests. + std::unique_ptr inner_resolver_; + + // Shared instance of tick clock, overridden for testing. + raw_ptr tick_clock_ = + base::DefaultTickClock::GetInstance(); + + // Options that govern when a stale response can or can't be returned. + const StaleOptions options_; + + // Requests not used for returned results but allowed to continue (unless + // |this| is destroyed) to backfill the cache. + std::unordered_map> + detached_requests_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_STALE_HOST_RESOLVER_H_ diff --git a/src/components/cronet/stale_host_resolver_unittest.cc b/src/components/cronet/stale_host_resolver_unittest.cc new file mode 100644 index 0000000000..c7beace8e6 --- /dev/null +++ b/src/components/cronet/stale_host_resolver_unittest.cc @@ -0,0 +1,729 @@ +// Copyright 2016 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 "components/cronet/stale_host_resolver.h" + +#include +#include +#include + +#include "base/bind.h" +#include "base/check.h" +#include "base/cxx17_backports.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/run_loop.h" +#include "base/test/simple_test_tick_clock.h" +#include "base/test/task_environment.h" +#include "base/test/test_timeouts.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "base/values.h" +#include "build/build_config.h" +#include "components/cronet/url_request_context_config.h" +#include "net/base/address_family.h" +#include "net/base/host_port_pair.h" +#include "net/base/mock_network_change_notifier.h" +#include "net/base/net_errors.h" +#include "net/base/network_change_notifier.h" +#include "net/base/network_isolation_key.h" +#include "net/cert/cert_verifier.h" +#include "net/dns/context_host_resolver.h" +#include "net/dns/dns_config.h" +#include "net/dns/dns_hosts.h" +#include "net/dns/dns_test_util.h" +#include "net/dns/host_cache.h" +#include "net/dns/host_resolver_manager.h" +#include "net/dns/host_resolver_proc.h" +#include "net/dns/public/dns_protocol.h" +#include "net/dns/public/dns_query_type.h" +#include "net/dns/public/host_resolver_source.h" +#include "net/http/http_network_session.h" +#include "net/log/net_log_with_source.h" +#include "net/proxy_resolution/proxy_config.h" +#include "net/proxy_resolution/proxy_config_service_fixed.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_context_builder.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +namespace cronet { + +namespace { + +const char kHostname[] = "example.com"; +const char kCacheAddress[] = "1.1.1.1"; +const char kNetworkAddress[] = "2.2.2.2"; +const char kHostsAddress[] = "4.4.4.4"; +const int kCacheEntryTTLSec = 300; + +const int kNoStaleDelaySec = 0; +const int kLongStaleDelaySec = 3600; +const uint16_t kPort = 12345; + +const int kAgeFreshSec = 0; +const int kAgeExpiredSec = kCacheEntryTTLSec * 2; + +// How long to wait for resolve calls to return. If the tests are working +// correctly, we won't end up waiting this long -- it's just a backup. +const int kWaitTimeoutSec = 1; + +net::AddressList MakeAddressList(const char* ip_address_str) { + net::IPAddress address; + bool rv = address.AssignFromIPLiteral(ip_address_str); + DCHECK(rv); + + net::AddressList address_list; + address_list.push_back(net::IPEndPoint(address, 0u)); + return address_list; +} + +std::unique_ptr CreateMockDnsClientForHosts() { + net::DnsConfig config; + config.nameservers.push_back(net::IPEndPoint()); + net::ParseHosts("4.4.4.4 example.com", &config.hosts); + + return std::make_unique(config, + net::MockDnsClientRuleList()); +} + +// Create a net::DnsClient where address requests for |kHostname| will hang +// until unblocked via CompleteDelayedTransactions() and then fail. +std::unique_ptr CreateHangingMockDnsClient() { + net::DnsConfig config; + config.nameservers.push_back(net::IPEndPoint()); + + net::MockDnsClientRuleList rules; + rules.emplace_back( + kHostname, net::dns_protocol::kTypeA, false /* secure */, + net::MockDnsClientRule::Result(net::MockDnsClientRule::ResultType::kFail), + true /* delay */); + rules.emplace_back( + kHostname, net::dns_protocol::kTypeAAAA, false /* secure */, + net::MockDnsClientRule::Result(net::MockDnsClientRule::ResultType::kFail), + true /* delay */); + + return std::make_unique(config, std::move(rules)); +} + +class MockHostResolverProc : public net::HostResolverProc { + public: + // |result| is the net error code to return from resolution attempts. + explicit MockHostResolverProc(int result) + : HostResolverProc(nullptr), result_(result) {} + + int Resolve(const std::string& hostname, + net::AddressFamily address_family, + net::HostResolverFlags host_resolver_flags, + net::AddressList* address_list, + int* os_error) override { + *address_list = MakeAddressList(kNetworkAddress); + return result_; + } + + protected: + ~MockHostResolverProc() override {} + + private: + // Result code to return from Resolve(). + const int result_; +}; + +class StaleHostResolverTest : public testing::Test { + protected: + StaleHostResolverTest() + : task_environment_(base::test::TaskEnvironment::MainThreadType::IO), + mock_network_change_notifier_( + net::test::MockNetworkChangeNotifier::Create()), + mock_proc_(new MockHostResolverProc(net::OK)), + resolver_(nullptr), + resolve_pending_(false), + resolve_complete_(false) { + // Make value clock not empty. + tick_clock_.Advance(base::Microseconds(1)); + } + + ~StaleHostResolverTest() override {} + + void SetStaleDelay(int stale_delay_sec) { + DCHECK(!resolver_); + + options_.delay = base::Seconds(stale_delay_sec); + } + + void SetUseStaleOnNameNotResolved() { + DCHECK(!resolver_); + + options_.use_stale_on_name_not_resolved = true; + } + + void SetStaleUsability(int max_expired_time_sec, + int max_stale_uses, + bool allow_other_network) { + DCHECK(!resolver_); + + options_.max_expired_time = base::Seconds(max_expired_time_sec); + options_.max_stale_uses = max_stale_uses; + options_.allow_other_network = allow_other_network; + } + + void SetNetResult(int result) { + DCHECK(!resolver_); + + mock_proc_ = new MockHostResolverProc(result); + } + + std::unique_ptr + CreateMockInnerResolverWithDnsClient( + std::unique_ptr dns_client, + net::URLRequestContext* context = nullptr) { + std::unique_ptr inner_resolver( + net::HostResolver::CreateStandaloneContextResolver(nullptr)); + if (context) + inner_resolver->SetRequestContext(context); + + net::ProcTaskParams proc_params(mock_proc_.get(), 1u); + inner_resolver->SetProcParamsForTesting(proc_params); + if (dns_client) { + inner_resolver->GetManagerForTesting()->SetDnsClientForTesting( + std::move(dns_client)); + inner_resolver->GetManagerForTesting()->SetInsecureDnsClientEnabled( + /*enabled=*/true, + /*additional_dns_types_enabled=*/true); + } else { + inner_resolver->GetManagerForTesting()->SetInsecureDnsClientEnabled( + /*enabled=*/false, + /*additional_dns_types_enabled=*/false); + } + return inner_resolver; + } + + void CreateResolverWithDnsClient(std::unique_ptr dns_client) { + DCHECK(!resolver_); + + stale_resolver_ = std::make_unique( + CreateMockInnerResolverWithDnsClient(std::move(dns_client)), options_); + stale_resolver_->SetTickClockForTesting(&tick_clock_); + resolver_ = stale_resolver_.get(); + } + + void CreateResolver() { CreateResolverWithDnsClient(nullptr); } + + void DestroyResolver() { + DCHECK(stale_resolver_); + + stale_resolver_ = nullptr; + resolver_ = nullptr; + } + + void SetResolver(StaleHostResolver* stale_resolver, + net::URLRequestContext* context = nullptr) { + DCHECK(!resolver_); + stale_resolver->inner_resolver_ = + CreateMockInnerResolverWithDnsClient(nullptr /* dns_client */, context); + resolver_ = stale_resolver; + } + + // Creates a cache entry for |kHostname| that is |age_sec| seconds old. + void CreateCacheEntry(int age_sec, int error) { + DCHECK(resolver_); + DCHECK(resolver_->GetHostCache()); + + base::TimeDelta ttl(base::Seconds(kCacheEntryTTLSec)); + net::HostCache::Key key(kHostname, net::DnsQueryType::UNSPECIFIED, 0, + net::HostResolverSource::ANY, + net::NetworkIsolationKey()); + net::HostCache::Entry entry( + error, + error == net::OK ? MakeAddressList(kCacheAddress) : net::AddressList(), + net::HostCache::Entry::SOURCE_UNKNOWN, ttl); + base::TimeDelta age = base::Seconds(age_sec); + base::TimeTicks then = tick_clock_.NowTicks() - age; + resolver_->GetHostCache()->Set(key, entry, then, ttl); + } + + void OnNetworkChange() { + // Real network changes on Android will send both notifications. + net::NetworkChangeNotifier::NotifyObserversOfIPAddressChangeForTests(); + net::NetworkChangeNotifier::NotifyObserversOfDNSChangeForTests(); + base::RunLoop().RunUntilIdle(); // Wait for notification. + } + + void LookupStale() { + DCHECK(resolver_); + DCHECK(resolver_->GetHostCache()); + + net::HostCache::Key key(kHostname, net::DnsQueryType::UNSPECIFIED, 0, + net::HostResolverSource::ANY, + net::NetworkIsolationKey()); + base::TimeTicks now = tick_clock_.NowTicks(); + net::HostCache::EntryStaleness stale; + EXPECT_TRUE(resolver_->GetHostCache()->LookupStale(key, now, &stale)); + EXPECT_TRUE(stale.is_stale()); + } + + void Resolve(const absl::optional& + optional_parameters) { + DCHECK(resolver_); + EXPECT_FALSE(resolve_pending_); + + request_ = resolver_->CreateRequest( + net::HostPortPair(kHostname, kPort), net::NetworkIsolationKey(), + net::NetLogWithSource(), optional_parameters); + resolve_pending_ = true; + resolve_complete_ = false; + resolve_error_ = net::ERR_UNEXPECTED; + + int rv = request_->Start(base::BindOnce( + &StaleHostResolverTest::OnResolveComplete, base::Unretained(this))); + if (rv != net::ERR_IO_PENDING) { + resolve_pending_ = false; + resolve_complete_ = true; + resolve_error_ = rv; + } + } + + void WaitForResolve() { + if (!resolve_pending_) + return; + + base::RunLoop run_loop; + + // Run until resolve completes or timeout. + resolve_closure_ = run_loop.QuitWhenIdleClosure(); + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, resolve_closure_, base::Seconds(kWaitTimeoutSec)); + run_loop.Run(); + } + + void WaitForIdle() { + base::RunLoop run_loop; + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, run_loop.QuitWhenIdleClosure()); + run_loop.Run(); + } + + void WaitForNetworkResolveComplete() { + // The stale host resolver cache is initially setup with |kCacheAddress|, + // so getting that address means that network resolve is still pending. + // The network resolve is guaranteed to return |kNetworkAddress| at some + // point because inner resolver is using MockHostResolverProc that always + // returns |kNetworkAddress|. + while (resolve_error() != net::OK || + resolve_addresses()[0].ToStringWithoutPort() != kNetworkAddress) { + Resolve(absl::nullopt); + WaitForResolve(); + } + } + + void Cancel() { + DCHECK(resolver_); + EXPECT_TRUE(resolve_pending_); + + request_ = nullptr; + + resolve_pending_ = false; + } + + void OnResolveComplete(int error) { + EXPECT_TRUE(resolve_pending_); + + resolve_error_ = error; + resolve_pending_ = false; + resolve_complete_ = true; + + if (!resolve_closure_.is_null()) + std::move(resolve_closure_).Run(); + } + + void AdvanceTickClock(base::TimeDelta delta) { tick_clock_.Advance(delta); } + + bool resolve_complete() const { return resolve_complete_; } + int resolve_error() const { return resolve_error_; } + const net::AddressList& resolve_addresses() const { + DCHECK(resolve_complete_); + return *request_->GetAddressResults(); + } + + private: + // Needed for HostResolver to run HostResolverProc callbacks. + base::test::TaskEnvironment task_environment_; + base::SimpleTestTickClock tick_clock_; + std::unique_ptr + mock_network_change_notifier_; + + scoped_refptr mock_proc_; + + raw_ptr resolver_; + StaleHostResolver::StaleOptions options_; + std::unique_ptr stale_resolver_; + + base::TimeTicks now_; + std::unique_ptr request_; + bool resolve_pending_; + bool resolve_complete_; + int resolve_error_; + + base::RepeatingClosure resolve_closure_; +}; + +// Make sure that test harness can be created and destroyed without crashing. +TEST_F(StaleHostResolverTest, Null) {} + +// Make sure that resolver can be created and destroyed without crashing. +TEST_F(StaleHostResolverTest, Create) { + CreateResolver(); +} + +TEST_F(StaleHostResolverTest, Network) { + CreateResolver(); + + Resolve(absl::nullopt); + WaitForResolve(); + + EXPECT_TRUE(resolve_complete()); + EXPECT_EQ(net::OK, resolve_error()); + EXPECT_EQ(1u, resolve_addresses().size()); + EXPECT_EQ(kNetworkAddress, resolve_addresses()[0].ToStringWithoutPort()); +} + +TEST_F(StaleHostResolverTest, Hosts) { + CreateResolverWithDnsClient(CreateMockDnsClientForHosts()); + + Resolve(absl::nullopt); + WaitForResolve(); + + EXPECT_TRUE(resolve_complete()); + EXPECT_EQ(net::OK, resolve_error()); + EXPECT_EQ(1u, resolve_addresses().size()); + EXPECT_EQ(kHostsAddress, resolve_addresses()[0].ToStringWithoutPort()); +} + +TEST_F(StaleHostResolverTest, FreshCache) { + CreateResolver(); + CreateCacheEntry(kAgeFreshSec, net::OK); + + Resolve(absl::nullopt); + + EXPECT_TRUE(resolve_complete()); + EXPECT_EQ(net::OK, resolve_error()); + EXPECT_EQ(1u, resolve_addresses().size()); + EXPECT_EQ(kCacheAddress, resolve_addresses()[0].ToStringWithoutPort()); + + WaitForIdle(); +} + +// Flaky on Linux ASan, crbug.com/838524. +#if defined(ADDRESS_SANITIZER) +#define MAYBE_StaleCache DISABLED_StaleCache +#else +#define MAYBE_StaleCache StaleCache +#endif +TEST_F(StaleHostResolverTest, MAYBE_StaleCache) { + SetStaleDelay(kNoStaleDelaySec); + CreateResolver(); + CreateCacheEntry(kAgeExpiredSec, net::OK); + + Resolve(absl::nullopt); + WaitForResolve(); + + EXPECT_TRUE(resolve_complete()); + EXPECT_EQ(net::OK, resolve_error()); + EXPECT_EQ(1u, resolve_addresses().size()); + EXPECT_EQ(kCacheAddress, resolve_addresses()[0].ToStringWithoutPort()); +} + +// If the resolver is destroyed before a stale cache entry is returned, the +// resolve should not complete. +TEST_F(StaleHostResolverTest, StaleCache_DestroyedResolver) { + SetStaleDelay(kNoStaleDelaySec); + CreateResolverWithDnsClient(CreateHangingMockDnsClient()); + CreateCacheEntry(kAgeExpiredSec, net::OK); + + Resolve(absl::nullopt); + DestroyResolver(); + WaitForResolve(); + + EXPECT_FALSE(resolve_complete()); +} + +// Ensure that |use_stale_on_name_not_resolved| causes stale results to be +// returned when ERR_NAME_NOT_RESOLVED is returned from network resolution. +TEST_F(StaleHostResolverTest, StaleCacheNameNotResolvedEnabled) { + SetStaleDelay(kLongStaleDelaySec); + SetUseStaleOnNameNotResolved(); + SetNetResult(net::ERR_NAME_NOT_RESOLVED); + CreateResolver(); + CreateCacheEntry(kAgeExpiredSec, net::OK); + + Resolve(absl::nullopt); + WaitForResolve(); + + EXPECT_TRUE(resolve_complete()); + EXPECT_EQ(net::OK, resolve_error()); + EXPECT_EQ(1u, resolve_addresses().size()); + EXPECT_EQ(kCacheAddress, resolve_addresses()[0].ToStringWithoutPort()); +} + +// Ensure that without |use_stale_on_name_not_resolved| network resolution +// failing causes StaleHostResolver jobs to fail with the same error code. +TEST_F(StaleHostResolverTest, StaleCacheNameNotResolvedDisabled) { + SetStaleDelay(kLongStaleDelaySec); + SetNetResult(net::ERR_NAME_NOT_RESOLVED); + CreateResolver(); + CreateCacheEntry(kAgeExpiredSec, net::OK); + + Resolve(absl::nullopt); + WaitForResolve(); + + EXPECT_TRUE(resolve_complete()); + EXPECT_EQ(net::ERR_NAME_NOT_RESOLVED, resolve_error()); +} + +TEST_F(StaleHostResolverTest, NetworkWithStaleCache) { + SetStaleDelay(kLongStaleDelaySec); + CreateResolver(); + CreateCacheEntry(kAgeExpiredSec, net::OK); + + Resolve(absl::nullopt); + WaitForResolve(); + + EXPECT_TRUE(resolve_complete()); + EXPECT_EQ(net::OK, resolve_error()); + EXPECT_EQ(1u, resolve_addresses().size()); + EXPECT_EQ(kNetworkAddress, resolve_addresses()[0].ToStringWithoutPort()); +} + +TEST_F(StaleHostResolverTest, CancelWithNoCache) { + SetStaleDelay(kNoStaleDelaySec); + CreateResolver(); + + Resolve(absl::nullopt); + + Cancel(); + + EXPECT_FALSE(resolve_complete()); + + // Make sure there's no lingering |OnResolveComplete()| callback waiting. + WaitForIdle(); +} + +TEST_F(StaleHostResolverTest, CancelWithStaleCache) { + SetStaleDelay(kLongStaleDelaySec); + CreateResolver(); + CreateCacheEntry(kAgeExpiredSec, net::OK); + + Resolve(absl::nullopt); + + Cancel(); + + EXPECT_FALSE(resolve_complete()); + + // Make sure there's no lingering |OnResolveComplete()| callback waiting. + WaitForIdle(); +} + +TEST_F(StaleHostResolverTest, ReturnStaleCacheSync) { + SetStaleDelay(kLongStaleDelaySec); + CreateResolver(); + CreateCacheEntry(kAgeExpiredSec, net::OK); + + StaleHostResolver::ResolveHostParameters parameters; + parameters.cache_usage = + StaleHostResolver::ResolveHostParameters::CacheUsage::STALE_ALLOWED; + + Resolve(parameters); + + EXPECT_TRUE(resolve_complete()); + EXPECT_EQ(net::OK, resolve_error()); + EXPECT_EQ(1u, resolve_addresses().size()); + EXPECT_EQ(kCacheAddress, resolve_addresses()[0].ToStringWithoutPort()); + + WaitForIdle(); +} + +// CancelWithFreshCache makes no sense; the request would've returned +// synchronously. + +// Disallow other networks cases fail under Fuchsia (crbug.com/816143). +// Flaky on Win buildbots. See crbug.com/836106 +#if BUILDFLAG(IS_WIN) +#define MAYBE_StaleUsability DISABLED_StaleUsability +#else +#define MAYBE_StaleUsability StaleUsability +#endif +TEST_F(StaleHostResolverTest, MAYBE_StaleUsability) { + const struct { + int max_expired_time_sec; + int max_stale_uses; + bool allow_other_network; + + int age_sec; + int stale_use; + int network_changes; + int error; + + bool usable; + } kUsabilityTestCases[] = { + // Fresh data always accepted. + {0, 0, true, -1, 1, 0, net::OK, true}, + {1, 1, false, -1, 1, 0, net::OK, true}, + + // Unlimited expired time accepts non-zero time. + {0, 0, true, 1, 1, 0, net::OK, true}, + + // Limited expired time accepts before but not after limit. + {2, 0, true, 1, 1, 0, net::OK, true}, + {2, 0, true, 3, 1, 0, net::OK, false}, + + // Unlimited stale uses accepts first and later uses. + {2, 0, true, 1, 1, 0, net::OK, true}, + {2, 0, true, 1, 9, 0, net::OK, true}, + + // Limited stale uses accepts up to and including limit. + {2, 2, true, 1, 1, 0, net::OK, true}, + {2, 2, true, 1, 2, 0, net::OK, true}, + {2, 2, true, 1, 3, 0, net::OK, false}, + {2, 2, true, 1, 9, 0, net::OK, false}, + + // Allowing other networks accepts zero or more network changes. + {2, 0, true, 1, 1, 0, net::OK, true}, + {2, 0, true, 1, 1, 1, net::OK, true}, + {2, 0, true, 1, 1, 9, net::OK, true}, + + // Disallowing other networks only accepts zero network changes. + {2, 0, false, 1, 1, 0, net::OK, true}, + {2, 0, false, 1, 1, 1, net::OK, false}, + {2, 0, false, 1, 1, 9, net::OK, false}, + + // Errors are only accepted if fresh. + {0, 0, true, -1, 1, 0, net::ERR_NAME_NOT_RESOLVED, true}, + {1, 1, false, -1, 1, 0, net::ERR_NAME_NOT_RESOLVED, true}, + {0, 0, true, 1, 1, 0, net::ERR_NAME_NOT_RESOLVED, false}, + {2, 0, true, 1, 1, 0, net::ERR_NAME_NOT_RESOLVED, false}, + {2, 0, true, 1, 1, 0, net::ERR_NAME_NOT_RESOLVED, false}, + {2, 2, true, 1, 2, 0, net::ERR_NAME_NOT_RESOLVED, false}, + {2, 0, true, 1, 1, 1, net::ERR_NAME_NOT_RESOLVED, false}, + {2, 0, false, 1, 1, 0, net::ERR_NAME_NOT_RESOLVED, false}, + }; + + SetStaleDelay(kNoStaleDelaySec); + + for (size_t i = 0; i < base::size(kUsabilityTestCases); ++i) { + const auto& test_case = kUsabilityTestCases[i]; + + SetStaleUsability(test_case.max_expired_time_sec, test_case.max_stale_uses, + test_case.allow_other_network); + CreateResolver(); + CreateCacheEntry(kCacheEntryTTLSec + test_case.age_sec, test_case.error); + + AdvanceTickClock(base::Milliseconds(1)); + for (int j = 0; j < test_case.network_changes; ++j) + OnNetworkChange(); + + AdvanceTickClock(base::Milliseconds(1)); + for (int j = 0; j < test_case.stale_use - 1; ++j) + LookupStale(); + + AdvanceTickClock(base::Milliseconds(1)); + Resolve(absl::nullopt); + WaitForResolve(); + EXPECT_TRUE(resolve_complete()) << i; + + if (test_case.error == net::OK) { + EXPECT_EQ(test_case.error, resolve_error()) << i; + EXPECT_EQ(1u, resolve_addresses().size()) << i; + { + const char* expected = + test_case.usable ? kCacheAddress : kNetworkAddress; + EXPECT_EQ(expected, resolve_addresses()[0].ToStringWithoutPort()) << i; + } + } else { + if (test_case.usable) { + EXPECT_EQ(test_case.error, resolve_error()) << i; + } else { + EXPECT_EQ(net::OK, resolve_error()) << i; + EXPECT_EQ(1u, resolve_addresses().size()) << i; + EXPECT_EQ(kNetworkAddress, resolve_addresses()[0].ToStringWithoutPort()) + << i; + } + } + // Make sure that all tasks complete so jobs are freed properly. + AdvanceTickClock(base::Seconds(kLongStaleDelaySec)); + WaitForNetworkResolveComplete(); + base::RunLoop run_loop; + run_loop.RunUntilIdle(); + + DestroyResolver(); + } +} + +TEST_F(StaleHostResolverTest, CreatedByContext) { + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"AsyncDNS\":{\"enable\":false}," + "\"StaleDNS\":{\"enable\":true," + "\"delay_ms\":0," + "\"max_expired_time_ms\":0," + "\"max_stale_uses\":0}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + + // Experimental options ensure context's resolver is a StaleHostResolver. + SetResolver(static_cast(context->host_resolver()), + context.get()); + // Note: Experimental config above sets 0ms stale delay. + CreateCacheEntry(kAgeExpiredSec, net::OK); + + Resolve(absl::nullopt); + EXPECT_FALSE(resolve_complete()); + WaitForResolve(); + + EXPECT_TRUE(resolve_complete()); + EXPECT_EQ(net::OK, resolve_error()); + EXPECT_EQ(1u, resolve_addresses().size()); + EXPECT_EQ(kCacheAddress, resolve_addresses()[0].ToStringWithoutPort()); + WaitForNetworkResolveComplete(); +} + +} // namespace + +} // namespace cronet diff --git a/src/components/cronet/testing/BUILD.gn b/src/components/cronet/testing/BUILD.gn new file mode 100644 index 0000000000..68c96d2d86 --- /dev/null +++ b/src/components/cronet/testing/BUILD.gn @@ -0,0 +1,55 @@ +# Copyright 2018 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. + +import("//testing/test.gni") + +# This section can be updated from globbing rules using: +# find data -type f | sort | sed 's/\(.*\)/"\1",/g' +bundle_data("test_support_bundle_data") { + visibility = [ ":test_support" ] + testonly = true + sources = [ + "test_server/data/cacheable.txt", + "test_server/data/cacheable.txt.mock-http-headers", + "test_server/data/content_length_mismatch.html", + "test_server/data/content_length_mismatch.html.mock-http-headers", + "test_server/data/gzipped.html", + "test_server/data/gzipped.html.mock-http-headers", + "test_server/data/multiredirect.html", + "test_server/data/multiredirect.html.mock-http-headers", + "test_server/data/notfound.html", + "test_server/data/notfound.html.mock-http-headers", + "test_server/data/quic_data/simple.txt", + "test_server/data/redirect.html", + "test_server/data/redirect.html.mock-http-headers", + "test_server/data/redirect_invalid_scheme.html", + "test_server/data/redirect_invalid_scheme.html.mock-http-headers", + "test_server/data/secureproxychecksuccess.txt", + "test_server/data/secureproxychecksuccess.txt.mock-http-headers", + "test_server/data/set_cookie.html", + "test_server/data/set_cookie.html.mock-http-headers", + "test_server/data/success.txt", + "test_server/data/success.txt.mock-http-headers", + ] + outputs = [ "{{bundle_resources_dir}}/" + + "{{source_root_relative_dir}}/{{source_file_part}}" ] +} + +# Test support for Cronet common implementation. +source_set("test_support") { + testonly = true + + deps = [ "//net:test_support" ] + + data = [ "test_server/data/" ] + + sources = [ + "test_server/test_server.cc", + "test_server/test_server.h", + ] + + if (is_ios) { + deps += [ ":test_support_bundle_data" ] + } +} diff --git a/src/components/cronet/testing/README.md b/src/components/cronet/testing/README.md new file mode 100644 index 0000000000..31127e81e9 --- /dev/null +++ b/src/components/cronet/testing/README.md @@ -0,0 +1 @@ +This folder contains support code for testing purposes, but not any test code itself. \ No newline at end of file diff --git a/src/components/cronet/testing/test_server/data/cacheable.txt b/src/components/cronet/testing/test_server/data/cacheable.txt new file mode 100644 index 0000000000..90e8404571 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/cacheable.txt @@ -0,0 +1 @@ +this is a cacheable file diff --git a/src/components/cronet/testing/test_server/data/cacheable.txt.mock-http-headers b/src/components/cronet/testing/test_server/data/cacheable.txt.mock-http-headers new file mode 100644 index 0000000000..df1c600ccf --- /dev/null +++ b/src/components/cronet/testing/test_server/data/cacheable.txt.mock-http-headers @@ -0,0 +1,3 @@ +HTTP/1.1 200 OK +Content-Type: text/plain +Cache-Control: max-age=300 diff --git a/src/components/cronet/testing/test_server/data/content_length_mismatch.html b/src/components/cronet/testing/test_server/data/content_length_mismatch.html new file mode 100644 index 0000000000..f883f8f1fe --- /dev/null +++ b/src/components/cronet/testing/test_server/data/content_length_mismatch.html @@ -0,0 +1 @@ +Response that lies about content length. \ No newline at end of file diff --git a/src/components/cronet/testing/test_server/data/content_length_mismatch.html.mock-http-headers b/src/components/cronet/testing/test_server/data/content_length_mismatch.html.mock-http-headers new file mode 100644 index 0000000000..b10b62182e --- /dev/null +++ b/src/components/cronet/testing/test_server/data/content_length_mismatch.html.mock-http-headers @@ -0,0 +1,3 @@ +HTTP/1.1 200 OK +Content-Type: text/html +Content-Length: 1000000 diff --git a/src/components/cronet/testing/test_server/data/gzipped.html b/src/components/cronet/testing/test_server/data/gzipped.html new file mode 100644 index 0000000000..9635bf347e Binary files /dev/null and b/src/components/cronet/testing/test_server/data/gzipped.html differ diff --git a/src/components/cronet/testing/test_server/data/gzipped.html.mock-http-headers b/src/components/cronet/testing/test_server/data/gzipped.html.mock-http-headers new file mode 100644 index 0000000000..a99a8701d8 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/gzipped.html.mock-http-headers @@ -0,0 +1,3 @@ +HTTP/1.1 200 OK +Content-Encoding: gzip +foo: bar diff --git a/src/components/cronet/testing/test_server/data/multiredirect.html b/src/components/cronet/testing/test_server/data/multiredirect.html new file mode 100644 index 0000000000..f1ecab2c34 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/multiredirect.html @@ -0,0 +1,7 @@ + + + +Redirect +

Redirecting...

+ + diff --git a/src/components/cronet/testing/test_server/data/multiredirect.html.mock-http-headers b/src/components/cronet/testing/test_server/data/multiredirect.html.mock-http-headers new file mode 100644 index 0000000000..4f8471661c --- /dev/null +++ b/src/components/cronet/testing/test_server/data/multiredirect.html.mock-http-headers @@ -0,0 +1,3 @@ +HTTP/1.1 302 Found +Location: /redirect.html +redirect-header0: header-value diff --git a/src/components/cronet/testing/test_server/data/notfound.html b/src/components/cronet/testing/test_server/data/notfound.html new file mode 100644 index 0000000000..60e9261454 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/notfound.html @@ -0,0 +1,7 @@ + + + +Not found +

Test page loaded.

+ + diff --git a/src/components/cronet/testing/test_server/data/notfound.html.mock-http-headers b/src/components/cronet/testing/test_server/data/notfound.html.mock-http-headers new file mode 100644 index 0000000000..aed272fed7 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/notfound.html.mock-http-headers @@ -0,0 +1 @@ +HTTP/1.1 404 Not Found diff --git a/src/components/cronet/testing/test_server/data/quic_data/simple.txt b/src/components/cronet/testing/test_server/data/quic_data/simple.txt new file mode 100644 index 0000000000..b79be8e0b5 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/quic_data/simple.txt @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +Cache-Control: private, max-age=0 +Content-Type: text/plain +X-Original-Url: https://test.example.com:6121/simple.txt + +This is a simple text file served by QUIC. diff --git a/src/components/cronet/testing/test_server/data/redirect.html b/src/components/cronet/testing/test_server/data/redirect.html new file mode 100644 index 0000000000..f1ecab2c34 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/redirect.html @@ -0,0 +1,7 @@ + + + +Redirect +

Redirecting...

+ + diff --git a/src/components/cronet/testing/test_server/data/redirect.html.mock-http-headers b/src/components/cronet/testing/test_server/data/redirect.html.mock-http-headers new file mode 100644 index 0000000000..2035e3f735 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/redirect.html.mock-http-headers @@ -0,0 +1,3 @@ +HTTP/1.1 302 Found +Location: /success.txt +redirect-header: header-value diff --git a/src/components/cronet/testing/test_server/data/redirect_broken_header.html b/src/components/cronet/testing/test_server/data/redirect_broken_header.html new file mode 100644 index 0000000000..f1ecab2c34 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/redirect_broken_header.html @@ -0,0 +1,7 @@ + + + +Redirect +

Redirecting...

+ + diff --git a/src/components/cronet/testing/test_server/data/redirect_broken_header.html.mock-http-headers b/src/components/cronet/testing/test_server/data/redirect_broken_header.html.mock-http-headers new file mode 100644 index 0000000000..e5c7b9f13d --- /dev/null +++ b/src/components/cronet/testing/test_server/data/redirect_broken_header.html.mock-http-headers @@ -0,0 +1 @@ +HTTP/1.1 302 Found diff --git a/src/components/cronet/testing/test_server/data/redirect_invalid_scheme.html b/src/components/cronet/testing/test_server/data/redirect_invalid_scheme.html new file mode 100644 index 0000000000..f1ecab2c34 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/redirect_invalid_scheme.html @@ -0,0 +1,7 @@ + + + +Redirect +

Redirecting...

+ + diff --git a/src/components/cronet/testing/test_server/data/redirect_invalid_scheme.html.mock-http-headers b/src/components/cronet/testing/test_server/data/redirect_invalid_scheme.html.mock-http-headers new file mode 100644 index 0000000000..7565904046 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/redirect_invalid_scheme.html.mock-http-headers @@ -0,0 +1,3 @@ +HTTP/1.1 302 Found +Location: https://127.0.0.1:8000/success.txt +redirect-header: header-value diff --git a/src/components/cronet/testing/test_server/data/secureproxychecksuccess.txt b/src/components/cronet/testing/test_server/data/secureproxychecksuccess.txt new file mode 100644 index 0000000000..d86bac9de5 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/secureproxychecksuccess.txt @@ -0,0 +1 @@ +OK diff --git a/src/components/cronet/testing/test_server/data/secureproxychecksuccess.txt.mock-http-headers b/src/components/cronet/testing/test_server/data/secureproxychecksuccess.txt.mock-http-headers new file mode 100644 index 0000000000..5c695b9136 --- /dev/null +++ b/src/components/cronet/testing/test_server/data/secureproxychecksuccess.txt.mock-http-headers @@ -0,0 +1,2 @@ +HTTP/1.1 200 OK +Content-Type: text/plain diff --git a/src/components/cronet/testing/test_server/data/set_cookie.html b/src/components/cronet/testing/test_server/data/set_cookie.html new file mode 100644 index 0000000000..4effa19f4f --- /dev/null +++ b/src/components/cronet/testing/test_server/data/set_cookie.html @@ -0,0 +1 @@ +hello! diff --git a/src/components/cronet/testing/test_server/data/set_cookie.html.mock-http-headers b/src/components/cronet/testing/test_server/data/set_cookie.html.mock-http-headers new file mode 100644 index 0000000000..cea4a1630c --- /dev/null +++ b/src/components/cronet/testing/test_server/data/set_cookie.html.mock-http-headers @@ -0,0 +1,2 @@ +HTTP/1.1 200 OK +Set-Cookie: A=B diff --git a/src/components/cronet/testing/test_server/data/success.txt b/src/components/cronet/testing/test_server/data/success.txt new file mode 100644 index 0000000000..e9ea42a12b --- /dev/null +++ b/src/components/cronet/testing/test_server/data/success.txt @@ -0,0 +1 @@ +this is a text file diff --git a/src/components/cronet/testing/test_server/data/success.txt.mock-http-headers b/src/components/cronet/testing/test_server/data/success.txt.mock-http-headers new file mode 100644 index 0000000000..e2b336eadf --- /dev/null +++ b/src/components/cronet/testing/test_server/data/success.txt.mock-http-headers @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +Content-Type: text/plain +Access-Control-Allow-Origin: * +header-name: header-value +multi-header-name: header-value1 +multi-header-name: header-value2 diff --git a/src/components/cronet/testing/test_server/test_server.cc b/src/components/cronet/testing/test_server/test_server.cc new file mode 100644 index 0000000000..c34e481fe7 --- /dev/null +++ b/src/components/cronet/testing/test_server/test_server.cc @@ -0,0 +1,288 @@ +// Copyright 2016 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 "components/cronet/testing/test_server/test_server.h" + +#include +#include + +#include "base/base_paths.h" +#include "base/bind.h" +#include "base/format_macros.h" +#include "base/lazy_instance.h" +#include "base/path_service.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "net/http/http_status_code.h" +#include "net/test/embedded_test_server/default_handlers.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" + +namespace { + +// Cronet test data directory, relative to source root. +const base::FilePath::CharType kTestDataRelativePath[] = + FILE_PATH_LITERAL("components/cronet/testing/test_server/data"); + +const char kSimplePath[] = "/simple"; +const char kEchoHeaderPath[] = "/echo_header?"; +const char kEchoMethodPath[] = "/echo_method"; +const char kEchoAllHeadersPath[] = "/echo_all_headers"; +const char kRedirectToEchoBodyPath[] = "/redirect_to_echo_body"; +const char kSetCookiePath[] = "/set_cookie?"; +const char kBigDataPath[] = "/big_data?"; +const char kUseEncodingPath[] = "/use_encoding?"; +const char kEchoBodyPath[] = "/echo_body"; + +const char kSimpleResponse[] = "The quick brown fox jumps over the lazy dog."; + +std::unique_ptr g_test_server; +base::LazyInstance::Leaky g_big_data_body = + LAZY_INSTANCE_INITIALIZER; + +std::unique_ptr SimpleRequest() { + auto http_response = std::make_unique(); + http_response->set_code(net::HTTP_OK); + http_response->set_content(kSimpleResponse); + return std::move(http_response); +} + +std::unique_ptr UseEncodingInResponse( + const net::test_server::HttpRequest& request) { + std::string encoding; + DCHECK(base::StartsWith(request.relative_url, kUseEncodingPath, + base::CompareCase::INSENSITIVE_ASCII)); + + encoding = request.relative_url.substr(strlen(kUseEncodingPath)); + auto http_response = std::make_unique(); + if (!encoding.compare("brotli")) { + const uint8_t quickfoxCompressed[] = { + 0x0b, 0x15, 0x80, 0x54, 0x68, 0x65, 0x20, 0x71, 0x75, 0x69, 0x63, 0x6b, + 0x20, 0x62, 0x72, 0x6f, 0x77, 0x6e, 0x20, 0x66, 0x6f, 0x78, 0x20, 0x6a, + 0x75, 0x6d, 0x70, 0x73, 0x20, 0x6f, 0x76, 0x65, 0x72, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x6c, 0x61, 0x7a, 0x79, 0x20, 0x64, 0x6f, 0x67, 0x03}; + std::string quickfoxCompressedStr( + reinterpret_cast(quickfoxCompressed), + sizeof(quickfoxCompressed)); + http_response->set_code(net::HTTP_OK); + http_response->set_content(quickfoxCompressedStr); + http_response->AddCustomHeader(std::string("content-encoding"), + std::string("br")); + } + return std::move(http_response); +} + +std::unique_ptr ReturnBigDataInResponse( + const net::test_server::HttpRequest& request) { + DCHECK(base::StartsWith(request.relative_url, kBigDataPath, + base::CompareCase::INSENSITIVE_ASCII)); + std::string data_size_str = request.relative_url.substr(strlen(kBigDataPath)); + int64_t data_size; + CHECK(base::StringToInt64(base::StringPiece(data_size_str), &data_size)); + CHECK(data_size == static_cast(g_big_data_body.Get().size())); + return std::make_unique( + std::string(), g_big_data_body.Get()); +} + +std::unique_ptr SetAndEchoCookieInResponse( + const net::test_server::HttpRequest& request) { + std::string cookie_line; + DCHECK(base::StartsWith(request.relative_url, kSetCookiePath, + base::CompareCase::INSENSITIVE_ASCII)); + cookie_line = request.relative_url.substr(strlen(kSetCookiePath)); + auto http_response = std::make_unique(); + http_response->set_code(net::HTTP_OK); + http_response->set_content(cookie_line); + http_response->AddCustomHeader("Set-Cookie", cookie_line); + return std::move(http_response); +} + +std::unique_ptr CronetTestRequestHandler( + const net::test_server::HttpRequest& request) { + if (base::StartsWith(request.relative_url, kSimplePath, + base::CompareCase::INSENSITIVE_ASCII)) { + return SimpleRequest(); + } + if (base::StartsWith(request.relative_url, kSetCookiePath, + base::CompareCase::INSENSITIVE_ASCII)) { + return SetAndEchoCookieInResponse(request); + } + if (base::StartsWith(request.relative_url, kBigDataPath, + base::CompareCase::INSENSITIVE_ASCII)) { + return ReturnBigDataInResponse(request); + } + if (base::StartsWith(request.relative_url, kUseEncodingPath, + base::CompareCase::INSENSITIVE_ASCII)) { + return UseEncodingInResponse(request); + } + + std::unique_ptr response( + new net::test_server::BasicHttpResponse()); + response->set_content_type("text/plain"); + + if (request.relative_url == kEchoBodyPath) { + if (request.has_content) { + response->set_content(request.content); + } else { + response->set_content("Request has no body. :("); + } + return std::move(response); + } + + if (base::StartsWith(request.relative_url, kEchoHeaderPath, + base::CompareCase::SENSITIVE)) { + GURL url = g_test_server->GetURL(request.relative_url); + auto it = request.headers.find(url.query()); + if (it != request.headers.end()) { + response->set_content(it->second); + } else { + response->set_content("Header not found. :("); + } + return std::move(response); + } + + if (request.relative_url == kEchoAllHeadersPath) { + response->set_content(request.all_headers); + return std::move(response); + } + + if (request.relative_url == kEchoMethodPath) { + response->set_content(request.method_string); + return std::move(response); + } + + if (request.relative_url == kRedirectToEchoBodyPath) { + response->set_code(net::HTTP_TEMPORARY_REDIRECT); + response->AddCustomHeader("Location", kEchoBodyPath); + return std::move(response); + } + + // Unhandled requests result in the Embedded test server sending a 404. + return nullptr; +} + +} // namespace + +namespace cronet { + +/* static */ +bool TestServer::StartServeFilesFromDirectory( + const base::FilePath& test_files_root) { + // Shouldn't happen. + if (g_test_server) + return false; + + g_test_server = std::make_unique( + net::EmbeddedTestServer::TYPE_HTTP); + g_test_server->RegisterRequestHandler( + base::BindRepeating(&CronetTestRequestHandler)); + g_test_server->ServeFilesFromDirectory(test_files_root); + net::test_server::RegisterDefaultHandlers(g_test_server.get()); + CHECK(g_test_server->Start()); + return true; +} + +/* static */ +bool TestServer::Start() { + base::FilePath src_root; + CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_root)); + return StartServeFilesFromDirectory(src_root.Append(kTestDataRelativePath)); +} + +/* static */ +void TestServer::Shutdown() { + if (!g_test_server) + return; + g_test_server.reset(); +} + +/* static */ +int TestServer::GetPort() { + DCHECK(g_test_server); + return g_test_server->port(); +} + +/* static */ +std::string TestServer::GetHostPort() { + DCHECK(g_test_server); + return net::HostPortPair::FromURL(g_test_server->base_url()).ToString(); +} + +/* static */ +std::string TestServer::GetSimpleURL() { + return GetFileURL(kSimplePath); +} + +/* static */ +std::string TestServer::GetEchoMethodURL() { + return GetFileURL(kEchoMethodPath); +} + +/* static */ +std::string TestServer::GetEchoHeaderURL(const std::string& header_name) { + return GetFileURL(kEchoHeaderPath + header_name); +} + +/* static */ +std::string TestServer::GetUseEncodingURL(const std::string& encoding_name) { + return GetFileURL(kUseEncodingPath + encoding_name); +} + +/* static */ +std::string TestServer::GetSetCookieURL(const std::string& cookie_line) { + return GetFileURL(kSetCookiePath + cookie_line); +} + +/* static */ +std::string TestServer::GetEchoAllHeadersURL() { + return GetFileURL(kEchoAllHeadersPath); +} + +/* static */ +std::string TestServer::GetEchoRequestBodyURL() { + return GetFileURL(kEchoBodyPath); +} + +/* static */ +std::string TestServer::GetRedirectToEchoBodyURL() { + return GetFileURL(kRedirectToEchoBodyPath); +} + +/* static */ +std::string TestServer::GetExabyteResponseURL() { + return GetFileURL("/exabyte_response"); +} + +/* static */ +std::string TestServer::PrepareBigDataURL(size_t data_size) { + DCHECK(g_test_server); + DCHECK(g_big_data_body.Get().empty()); + // Response line with headers. + std::string response_builder; + base::StringAppendF(&response_builder, "HTTP/1.1 200 OK\r\n"); + base::StringAppendF(&response_builder, "Content-Length: %" PRIuS "\r\n", + data_size); + base::StringAppendF(&response_builder, "\r\n"); + response_builder += std::string(data_size, 'c'); + g_big_data_body.Get() = response_builder; + return g_test_server + ->GetURL(kBigDataPath + base::NumberToString(response_builder.size())) + .spec(); +} + +/* static */ +void TestServer::ReleaseBigDataURL() { + DCHECK(!g_big_data_body.Get().empty()); + g_big_data_body.Get() = std::string(); +} + +/* static */ +std::string TestServer::GetFileURL(const std::string& file_path) { + DCHECK(g_test_server); + return g_test_server->GetURL(file_path).spec(); +} + +} // namespace cronet diff --git a/src/components/cronet/testing/test_server/test_server.h b/src/components/cronet/testing/test_server/test_server.h new file mode 100644 index 0000000000..f2753adc51 --- /dev/null +++ b/src/components/cronet/testing/test_server/test_server.h @@ -0,0 +1,84 @@ +// Copyright 2016 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_TESTING_TEST_SERVER_TEST_SERVER_H_ +#define COMPONENTS_CRONET_TESTING_TEST_SERVER_TEST_SERVER_H_ + +#include + +namespace base { +class FilePath; +} // namespace base + +namespace cronet { + +class TestServer { + public: + // Starts the server serving files from default test data directory. + // Returns true if started, false if server is already running. + static bool Start(); + // Starts the server serving files from |test_files_root| directory. + // Returns true if started, false if server is already running. + static bool StartServeFilesFromDirectory( + const base::FilePath& test_files_root); + // Shuts down the server. + static void Shutdown(); + + // Returns port number of the server. + static int GetPort(); + // Returns host:port string of the server. + static std::string GetHostPort(); + + // Returns URL which responds with the body "The quick brown fox jumps over + // the lazy dog". + static std::string GetSimpleURL(); + // Returns URL which respond with echo of the method in response body. + static std::string GetEchoMethodURL(); + // Returns URL which respond with echo of header with |header_name| in + // response body. + static std::string GetEchoHeaderURL(const std::string& header_name); + // Returns URL which responds with "The quick brown fox jumps over the lazy + // dog" in specified encoding. + static std::string GetUseEncodingURL(const std::string& encoding_name); + // Returns URL which respond with setting cookie to |cookie_line| and echo it + // in response body. + static std::string GetSetCookieURL(const std::string& cookie_line); + // Returns URL which echoes all request headers. + static std::string GetEchoAllHeadersURL(); + // Returns URL which echoes data in a request body. + static std::string GetEchoRequestBodyURL(); + // Returns URL which redirects to URL that echoes data in a request body. + static std::string GetRedirectToEchoBodyURL(); + // Returns a URL that the server will return an Exabyte of data. + static std::string GetExabyteResponseURL(); + // Prepares response and returns URL which respond with |data_size| of bytes + // in response body. + static std::string PrepareBigDataURL(size_t data_size); + // Releases response created by PrepareBigDataURL(). + static void ReleaseBigDataURL(); + + // The following URLs will make TestServer serve a response based on + // the contents of the corresponding file and its mock-http-headers file. + + // Returns URL which responds with content of file at |file_path|. + static std::string GetFileURL(const std::string& file_path); + + // Returns URL which responds with plain/text success. + static std::string GetSuccessURL() { return GetFileURL("/success.txt"); } + + // Returns URL which redirects to plain/text success. + static std::string GetRedirectURL() { return GetFileURL("/redirect.html"); } + + // Returns URL which redirects to redirect to plain/text success. + static std::string GetMultiRedirectURL() { + return GetFileURL("/multiredirect.html"); + } + + // Returns URL which responds with status code 404 - page not found.. + static std::string GetNotFoundURL() { return GetFileURL("/notfound.html"); } +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_TESTING_TEST_SERVER_TEST_SERVER_H_ diff --git a/src/components/cronet/tools/__init__.py b/src/components/cronet/tools/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/cronet/tools/android_rndis_forwarder.py b/src/components/cronet/tools/android_rndis_forwarder.py new file mode 100644 index 0000000000..4eff5da1d6 --- /dev/null +++ b/src/components/cronet/tools/android_rndis_forwarder.py @@ -0,0 +1,534 @@ +# Copyright 2016 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. + +import atexit +import logging +import os +import re +import socket +import struct +import subprocess +import sys + +REPOSITORY_ROOT = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', '..')) +sys.path.append(os.path.join(REPOSITORY_ROOT, 'tools', 'perf')) +from core import path_util # pylint: disable=wrong-import-position +sys.path.append(path_util.GetTelemetryDir()) + +# pylint: disable=wrong-import-position +from telemetry.core import platform +from telemetry.internal.platform import android_device +from telemetry.internal.util import binary_manager + +from devil.android import device_errors +from devil.android import device_utils + +import py_utils +# pylint: enable=wrong-import-position + + +class AndroidRndisForwarder(object): + """Forwards traffic using RNDIS. Assumes the device has root access.""" + + def __init__(self, device, rndis_configurator): + self._device = device + self._rndis_configurator = rndis_configurator + self._device_iface = rndis_configurator.device_iface + self._host_ip = rndis_configurator.host_ip + self._original_dns = None, None, None + self._RedirectPorts() + # The netd commands fail on Lollipop and newer releases, but aren't + # necessary as DNS isn't used. + # self._OverrideDns() + self._OverrideDefaultGateway() + # Need to override routing policy again since call to setifdns + # sometimes resets policy table + self._rndis_configurator.OverrideRoutingPolicy() + atexit.register(self.Close) + # TODO(tonyg): Verify that each port can connect to host. + + @property + def host_ip(self): + return self._host_ip + + def Close(self): + #if self._forwarding: + # self._rndis_configurator.RestoreRoutingPolicy() + # self._SetDns(*self._original_dns) + # self._RestoreDefaultGateway() + #super(AndroidRndisForwarder, self).Close() + pass + + def _RedirectPorts(self): + """Sets the local to remote pair mappings to use for RNDIS.""" + # Flush any old nat rules. + self._device.RunShellCommand( + ['iptables', '-F', '-t', 'nat'], check_return=True) + + def _OverrideDns(self): + """Overrides DNS on device to point at the host.""" + self._original_dns = self._GetCurrentDns() + self._SetDns(self._device_iface, self.host_ip, self.host_ip) + + def _SetDns(self, iface, dns1, dns2): + """Overrides device's DNS configuration. + + Args: + iface: name of the network interface to make default + dns1, dns2: nameserver IP addresses + """ + if not iface: + return # If there is no route, then nobody cares about DNS. + # DNS proxy in older versions of Android is configured via properties. + # TODO(szym): run via su -c if necessary. + self._device.SetProp('net.dns1', dns1) + self._device.SetProp('net.dns2', dns2) + dnschange = self._device.GetProp('net.dnschange') + if dnschange: + self._device.SetProp('net.dnschange', str(int(dnschange) + 1)) + # Since commit 8b47b3601f82f299bb8c135af0639b72b67230e6 to frameworks/base + # the net.dns1 properties have been replaced with explicit commands for netd + self._device.RunShellCommand( + ['netd', 'resolver', 'setifdns', iface, dns1, dns2], check_return=True) + # TODO(szym): if we know the package UID, we could setifaceforuidrange + self._device.RunShellCommand( + ['netd', 'resolver', 'setdefaultif', iface], check_return=True) + + def _GetCurrentDns(self): + """Returns current gateway, dns1, and dns2.""" + routes = self._device.RunShellCommand( + ['cat', '/proc/net/route'], check_return=True)[1:] + routes = [route.split() for route in routes] + default_routes = [route[0] for route in routes if route[1] == '00000000'] + return ( + default_routes[0] if default_routes else None, + self._device.GetProp('net.dns1'), + self._device.GetProp('net.dns2'), + ) + + def _OverrideDefaultGateway(self): + """Force traffic to go through RNDIS interface. + + Override any default gateway route. Without this traffic may go through + the wrong interface. + + This introduces the risk that _RestoreDefaultGateway() is not called + (e.g. Telemetry crashes). A power cycle or "adb reboot" is a simple + workaround around in that case. + """ + # NOTE(pauljensen): On Nougat this can produce a weird message and return + # a non-zero value, but routing still seems fine, so don't check_return. + self._device.RunShellCommand( + ['route', 'add', 'default', 'gw', self.host_ip, + 'dev', self._device_iface]) + + def _RestoreDefaultGateway(self): + self._device.RunShellCommand( + ['netcfg', self._device_iface, 'down'], check_return=True) + + +class AndroidRndisConfigurator(object): + """Configures a linux host to connect to an android device via RNDIS. + + Note that we intentionally leave RNDIS running on the device. This is + because the setup is slow and potentially flaky and leaving it running + doesn't seem to interfere with any other developer or bot use-cases. + """ + + _RNDIS_DEVICE = '/sys/class/android_usb/android0' + _NETWORK_INTERFACES = '/etc/network/interfaces' + _INTERFACES_INCLUDE = 'source /etc/network/interfaces.d/*.conf' + _TELEMETRY_INTERFACE_FILE = '/etc/network/interfaces.d/telemetry-{}.conf' + _DEVICE_IP_ADDRESS = '192.168.123.2' + + def __init__(self, device): + self._device = device + + try: + self._device.EnableRoot() + except device_errors.CommandFailedError: + logging.error('RNDIS forwarding requires a rooted device.') + raise + + self._device_ip = None + self._host_iface = None + self._host_ip = None + self.device_iface = None + + if platform.GetHostPlatform().GetOSName() == 'mac': + self._InstallHorndis(platform.GetHostPlatform().GetArchName()) + + assert self._IsRndisSupported(), 'Device does not support RNDIS.' + self._CheckConfigureNetwork() + + @property + def host_ip(self): + return self._host_ip + + def _IsRndisSupported(self): + """Checks that the device has RNDIS support in the kernel.""" + return self._device.FileExists('%s/f_rndis/device' % self._RNDIS_DEVICE) + + def _FindDeviceRndisInterface(self): + """Returns the name of the RNDIS network interface if present.""" + config = self._device.RunShellCommand( + ['ip', '-o', 'link', 'show'], check_return=True) + interfaces = [line.split(':')[1].strip() for line in config] + candidates = [iface for iface in interfaces if re.match('rndis|usb', iface)] + if candidates: + candidates.sort() + if len(candidates) == 2 and candidates[0].startswith('rndis') and \ + candidates[1].startswith('usb'): + return candidates[0] + assert len(candidates) == 1, 'Found more than one rndis device!' + return candidates[0] + + def _FindDeviceRndisMacAddress(self, interface): + """Returns the MAC address of the RNDIS network interface if present.""" + config = self._device.RunShellCommand( + ['ip', '-o', 'link', 'show', interface], check_return=True)[0] + return config.split('link/ether ')[1][:17] + + def _EnumerateHostInterfaces(self): + host_platform = platform.GetHostPlatform().GetOSName() + if host_platform == 'linux': + return subprocess.check_output(['ip', 'addr']).splitlines() + if host_platform == 'mac': + return subprocess.check_output(['ifconfig']).splitlines() + raise NotImplementedError('Platform %s not supported!' % host_platform) + + def _FindHostRndisInterface(self, device_mac_address): + """Returns the name of the host-side network interface.""" + interface_list = self._EnumerateHostInterfaces() + ether_address = self._device.ReadFile( + '%s/f_rndis/ethaddr' % self._RNDIS_DEVICE, + as_root=True, force_pull=True).strip() + interface_name = None + for line in interface_list: + if not line.startswith((' ', '\t')): + interface_name = line.split(':')[-2].strip() + # Attempt to ping device to trigger ARP for device. + with open(os.devnull, 'wb') as devnull: + subprocess.call(['ping', '-w1', '-c1', '-I', interface_name, + self._DEVICE_IP_ADDRESS], stdout=devnull, stderr=devnull) + # Check if ARP cache now has device in it. + arp = subprocess.check_output( + ['arp', '-i', interface_name, self._DEVICE_IP_ADDRESS]) + if device_mac_address in arp: + return interface_name + elif ether_address in line: + return interface_name + # NOTE(pauljensen): |ether_address| seems incorrect on Nougat devices, + # but just going by the host interface name seems safe enough. + elif interface_name == 'usb0': + return interface_name + + def _WriteProtectedFile(self, file_path, contents): + subprocess.check_call( + ['/usr/bin/sudo', 'bash', '-c', + 'echo -e "%s" > %s' % (contents, file_path)]) + + def _LoadInstalledHoRNDIS(self): + """Attempt to load HoRNDIS if installed. + If kext could not be loaded or if HoRNDIS is not installed, return False. + """ + if not os.path.isdir('/System/Library/Extensions/HoRNDIS.kext'): + logging.info('HoRNDIS not present on system.') + return False + + def HoRNDISLoaded(): + return 'HoRNDIS' in subprocess.check_output(['kextstat']) + + if HoRNDISLoaded(): + return True + + logging.info('HoRNDIS installed but not running, trying to load manually.') + subprocess.check_call( + ['/usr/bin/sudo', 'kextload', '-b', 'com.joshuawise.kexts.HoRNDIS']) + + return HoRNDISLoaded() + + def _InstallHorndis(self, arch_name): + if self._LoadInstalledHoRNDIS(): + logging.info('HoRNDIS kext loaded successfully.') + return + logging.info('Installing HoRNDIS...') + pkg_path = binary_manager.FetchPath('horndis', 'mac', arch_name) + subprocess.check_call( + ['/usr/bin/sudo', 'installer', '-pkg', pkg_path, '-target', '/']) + + def _DisableRndis(self): + # Set expect_status=None as this will temporarily break the adb connection. + self._device.adb.Shell('setprop sys.usb.config adb', expect_status=None) + self._device.WaitUntilFullyBooted() + + def _EnableRndis(self): + """Enables the RNDIS network interface.""" + script_prefix = '/data/local/tmp/rndis' + # This could be accomplished via "svc usb setFunction rndis" but only on + # devices which have the "USB tethering" feature. + # Also, on some devices, it's necessary to go through "none" function. + script = """ +trap '' HUP +trap '' TERM +trap '' PIPE + +function manual_config() { + echo %(functions)s > %(dev)s/functions + echo 224 > %(dev)s/bDeviceClass + echo 1 > %(dev)s/enable + start adbd + setprop sys.usb.state %(functions)s +} + +# This function kills adb transport, so it has to be run "detached". +function doit() { + setprop sys.usb.config none + while [ `getprop sys.usb.state` != "none" ]; do + sleep 1 + done + manual_config + # For some combinations of devices and host kernels, adb won't work unless the + # interface is up, but if we bring it up immediately, it will break adb. + #sleep 1 + if ip link show rndis0 ; then + ifconfig rndis0 %(device_ip_address)s netmask 255.255.255.0 up + else + ifconfig usb0 %(device_ip_address)s netmask 255.255.255.0 up + fi + echo DONE >> %(prefix)s.log +} + +doit & + """ % {'dev': self._RNDIS_DEVICE, + 'functions': 'rndis,adb', + 'prefix': script_prefix, + 'device_ip_address': self._DEVICE_IP_ADDRESS} + script_file = '%s.sh' % script_prefix + log_file = '%s.log' % script_prefix + self._device.WriteFile(script_file, script) + # TODO(szym): run via su -c if necessary. + self._device.RemovePath(log_file, force=True) + self._device.RunShellCommand(['.', script_file], check_return=True) + self._device.WaitUntilFullyBooted() + result = self._device.ReadFile(log_file).splitlines() + assert any('DONE' in line for line in result), 'RNDIS script did not run!' + + def _CheckEnableRndis(self, force): + """Enables the RNDIS network interface, retrying if necessary. + Args: + force: Disable RNDIS first, even if it appears already enabled. + Returns: + device_iface: RNDIS interface name on the device + host_iface: corresponding interface name on the host + """ + for _ in range(3): + if not force: + device_iface = self._FindDeviceRndisInterface() + if device_iface: + device_mac_address = self._FindDeviceRndisMacAddress(device_iface) + host_iface = self._FindHostRndisInterface(device_mac_address) + if host_iface: + return device_iface, host_iface + self._DisableRndis() + self._EnableRndis() + force = False + raise Exception('Could not enable RNDIS, giving up.') + + def _Ip2Long(self, addr): + return struct.unpack('!L', socket.inet_aton(addr))[0] + + def _IpPrefix2AddressMask(self, addr): + def _Length2Mask(length): + return 0xFFFFFFFF & ~((1 << (32 - length)) - 1) + + addr, masklen = addr.split('/') + return self._Ip2Long(addr), _Length2Mask(int(masklen)) + + def _GetHostAddresses(self, iface): + """Returns the IP addresses on host's interfaces, breaking out |iface|.""" + interface_list = self._EnumerateHostInterfaces() + addresses = [] + iface_address = None + found_iface = False + for line in interface_list: + if not line.startswith((' ', '\t')): + found_iface = iface in line + match = re.search(r'(?<=inet )\S+', line) + if match: + address = match.group(0) + if '/' in address: + address = self._IpPrefix2AddressMask(address) + else: + match = re.search(r'(?<=netmask )\S+', line) + address = self._Ip2Long(address), int(match.group(0), 16) + if found_iface: + assert not iface_address, ( + 'Found %s twice when parsing host interfaces.' % iface) + iface_address = address + else: + addresses.append(address) + return addresses, iface_address + + def _GetDeviceAddresses(self, excluded_iface): + """Returns the IP addresses on all connected devices. + Excludes interface |excluded_iface| on the selected device. + """ + my_device = str(self._device) + addresses = [] + for device_serial in android_device.GetDeviceSerials(None): + try: + device = device_utils.DeviceUtils(device_serial) + if device_serial == my_device: + excluded = excluded_iface + else: + excluded = 'no interfaces excluded on other devices' + output = device.RunShellCommand( + ['ip', '-o', '-4', 'addr'], check_return=True) + addresses += [ + line.split()[3] for line in output if excluded not in line] + except device_errors.CommandFailedError: + logging.warning('Unable to determine IP addresses for %s', + device_serial) + return addresses + + def _ConfigureNetwork(self, device_iface, host_iface): + """Configures the |device_iface| to be on the same network as |host_iface|. + """ + def _Long2Ip(value): + return socket.inet_ntoa(struct.pack('!L', value)) + + def _IsNetworkUnique(network, addresses): + return all((addr & mask != network & mask) for addr, mask in addresses) + + def _NextUnusedAddress(network, netmask, used_addresses): + # Excludes '0' and broadcast. + for suffix in range(1, 0xFFFFFFFF & ~netmask): + candidate = network | suffix + if candidate not in used_addresses: + return candidate + + def HasHostAddress(): + _, host_address = self._GetHostAddresses(host_iface) + return bool(host_address) + + if not HasHostAddress(): + if platform.GetHostPlatform().GetOSName() == 'mac': + if 'Telemetry' not in subprocess.check_output( + ['networksetup', '-listallnetworkservices']): + subprocess.check_call( + ['/usr/bin/sudo', 'networksetup', + '-createnetworkservice', 'Telemetry', host_iface]) + subprocess.check_call( + ['/usr/bin/sudo', 'networksetup', + '-setmanual', 'Telemetry', '192.168.123.1', '255.255.255.0']) + elif platform.GetHostPlatform().GetOSName() == 'linux': + with open(self._NETWORK_INTERFACES) as f: + orig_interfaces = f.read() + if self._INTERFACES_INCLUDE not in orig_interfaces: + interfaces = '\n'.join([ + orig_interfaces, + '', + '# Added by Telemetry.', + self._INTERFACES_INCLUDE]) + self._WriteProtectedFile(self._NETWORK_INTERFACES, interfaces) + interface_conf_file = self._TELEMETRY_INTERFACE_FILE.format(host_iface) + if not os.path.exists(interface_conf_file): + interface_conf_dir = os.path.dirname(interface_conf_file) + if not os.path.exists(interface_conf_dir): + subprocess.call(['/usr/bin/sudo', '/bin/mkdir', interface_conf_dir]) + subprocess.call( + ['/usr/bin/sudo', '/bin/chmod', '755', interface_conf_dir]) + interface_conf = '\n'.join([ + '# Added by Telemetry for RNDIS forwarding.', + 'allow-hotplug %s' % host_iface, + 'iface %s inet static' % host_iface, + ' address 192.168.123.1', + ' netmask 255.255.255.0', + ]) + self._WriteProtectedFile(interface_conf_file, interface_conf) + subprocess.check_call(['/usr/bin/sudo', 'ifup', host_iface]) + logging.info('Waiting for RNDIS connectivity...') + py_utils.WaitFor(HasHostAddress, 30) + + addresses, host_address = self._GetHostAddresses(host_iface) + assert host_address, 'Interface %s could not be configured.' % host_iface + + host_ip, netmask = host_address # pylint: disable=unpacking-non-sequence + network = host_ip & netmask + + if not _IsNetworkUnique(network, addresses): + logging.warning( + 'The IP address configuration %s of %s is not unique!\n' + 'Check your /etc/network/interfaces. If this overlap is intended,\n' + 'you might need to use: ip rule add from lookup \n' + 'or add the interface to a bridge in order to route to this network.', + host_address, host_iface + ) + + # Find unused IP address. + used_addresses = [addr for addr, _ in addresses] + used_addresses += [self._IpPrefix2AddressMask(addr)[0] + for addr in self._GetDeviceAddresses(device_iface)] + used_addresses += [host_ip] + + device_ip = _NextUnusedAddress(network, netmask, used_addresses) + assert device_ip, ('The network %s on %s is full.' % + (host_address, host_iface)) + + host_ip = _Long2Ip(host_ip) + device_ip = _Long2Ip(device_ip) + netmask = _Long2Ip(netmask) + + # TODO(szym) run via su -c if necessary. + self._device.RunShellCommand( + ['ifconfig', device_iface, device_ip, 'netmask', netmask, 'up'], + check_return=True) + # Enabling the interface sometimes breaks adb. + self._device.WaitUntilFullyBooted() + self._host_iface = host_iface + self._host_ip = host_ip + self.device_iface = device_iface + self._device_ip = device_ip + + def _TestConnectivity(self): + with open(os.devnull, 'wb') as devnull: + return subprocess.call(['ping', '-q', '-c1', '-W1', self._device_ip], + stdout=devnull) == 0 + + def OverrideRoutingPolicy(self): + """Override any routing policy that could prevent + packets from reaching the rndis interface + """ + policies = self._device.RunShellCommand(['ip', 'rule'], check_return=True) + if len(policies) > 1 and not 'lookup main' in policies[1]: + self._device.RunShellCommand( + ['ip', 'rule', 'add', 'prio', '1', 'from', 'all', 'table', 'main'], + check_return=True) + self._device.RunShellCommand( + ['ip', 'route', 'flush', 'cache'], check_return=True) + + def RestoreRoutingPolicy(self): + policies = self._device.RunShellCommand(['ip', 'rule'], check_return=True) + if len(policies) > 1 and re.match("^1:.*lookup main", policies[1]): + self._device.RunShellCommand( + ['ip', 'rule', 'del', 'prio', '1'], check_return=True) + self._device.RunShellCommand( + ['ip', 'route', 'flush', 'cache'], check_return=True) + + def _CheckConfigureNetwork(self): + """Enables RNDIS and configures it, retrying until we have connectivity.""" + force = False + for _ in range(3): + device_iface, host_iface = self._CheckEnableRndis(force) + self._ConfigureNetwork(device_iface, host_iface) + self.OverrideRoutingPolicy() + # Sometimes the first packet will wake up the connection. + for _ in range(3): + if self._TestConnectivity(): + return + force = True + self.RestoreRoutingPolicy() + raise Exception('No connectivity, giving up.') diff --git a/src/components/cronet/tools/api_static_checks.py b/src/components/cronet/tools/api_static_checks.py new file mode 100755 index 0000000000..5c8bc779ce --- /dev/null +++ b/src/components/cronet/tools/api_static_checks.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# Copyright 2016 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. + +"""api_static_checks.py - Enforce Cronet API requirements.""" + + + +import argparse +import os +import re +import shutil +import sys +import tempfile + +REPOSITORY_ROOT = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)) + +sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'build/android/gyp')) +from util import build_utils # pylint: disable=wrong-import-position + +sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'components')) +from cronet.tools import update_api # pylint: disable=wrong-import-position + + +# These regular expressions catch the beginning of lines that declare classes +# and methods. The first group returned by a match is the class or method name. +from cronet.tools.update_api import CLASS_RE # pylint: disable=wrong-import-position +METHOD_RE = re.compile(r'.* ([^ ]*)\(.*\);') + +# Allowed exceptions. Adding anything to this list is dangerous and should be +# avoided if possible. For now these exceptions are for APIs that existed in +# the first version of Cronet and will be supported forever. +# TODO(pauljensen): Remove these. +ALLOWED_EXCEPTIONS = [ +'org.chromium.net.impl.CronetEngineBuilderImpl/build ->' + ' org/chromium/net/ExperimentalCronetEngine/getVersionString:' + '()Ljava/lang/String;', +'org.chromium.net.urlconnection.CronetFixedModeOutputStream$UploadDataProviderI' + 'mpl/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V', +'org.chromium.net.urlconnection.CronetFixedModeOutputStream$UploadDataProviderI' + 'mpl/rewind -> org/chromium/net/UploadDataSink/onRewindError:' + '(Ljava/lang/Exception;)V', +'org.chromium.net.urlconnection.CronetHttpURLConnection/disconnect ->' + ' org/chromium/net/UrlRequest/cancel:()V', +'org.chromium.net.urlconnection.CronetHttpURLConnection/disconnect ->' + ' org/chromium/net/UrlResponseInfo/getHttpStatusText:()Ljava/lang/String;', +'org.chromium.net.urlconnection.CronetHttpURLConnection/disconnect ->' + ' org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I', +'org.chromium.net.urlconnection.CronetHttpURLConnection/getHeaderField ->' + ' org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I', +'org.chromium.net.urlconnection.CronetHttpURLConnection/getErrorStream ->' + ' org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I', +'org.chromium.net.urlconnection.CronetHttpURLConnection/setConnectTimeout ->' + ' org/chromium/net/UrlRequest/read:(Ljava/nio/ByteBuffer;)V', +'org.chromium.net.urlconnection.CronetHttpURLConnection$CronetUrlRequestCallbac' + 'k/onRedirectReceived -> org/chromium/net/UrlRequest/followRedirect:()V', +'org.chromium.net.urlconnection.CronetHttpURLConnection$CronetUrlRequestCallbac' + 'k/onRedirectReceived -> org/chromium/net/UrlRequest/cancel:()V', +'org.chromium.net.urlconnection.CronetChunkedOutputStream$UploadDataProviderImp' + 'l/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V', +'org.chromium.net.urlconnection.CronetChunkedOutputStream$UploadDataProviderImp' + 'l/rewind -> org/chromium/net/UploadDataSink/onRewindError:' + '(Ljava/lang/Exception;)V', +'org.chromium.net.urlconnection.CronetBufferedOutputStream$UploadDataProviderIm' + 'pl/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V', +'org.chromium.net.urlconnection.CronetBufferedOutputStream$UploadDataProviderIm' + 'pl/rewind -> org/chromium/net/UploadDataSink/onRewindSucceeded:()V', +'org.chromium.net.urlconnection.CronetHttpURLStreamHandler/org.chromium.net.url' + 'connection.CronetHttpURLStreamHandler -> org/chromium/net/ExperimentalCron' + 'etEngine/openConnection:(Ljava/net/URL;)Ljava/net/URLConnection;', +'org.chromium.net.urlconnection.CronetHttpURLStreamHandler/org.chromium.net.url' + 'connection.CronetHttpURLStreamHandler -> org/chromium/net/ExperimentalCron' + 'etEngine/openConnection:(Ljava/net/URL;Ljava/net/Proxy;)Ljava/net/URLConne' + 'ction;', +# getMessage() is an java.lang.Exception member, and so cannot be removed. +'org.chromium.net.impl.NetworkExceptionImpl/getMessage -> ' + 'org/chromium/net/NetworkException/getMessage:()Ljava/lang/String;', +] + + +def find_api_calls(dump, api_classes, bad_calls): + # Given a dump of an implementation class, find calls through API classes. + # |dump| is the output of "javap -c" on the implementation class files. + # |api_classes| is the list of classes comprising the API. + # |bad_calls| is the list of calls through API classes. This list is built up + # by this function. + + for line in dump: + if CLASS_RE.match(line): + caller_class = CLASS_RE.match(line).group(1) + if METHOD_RE.match(line): + caller_method = METHOD_RE.match(line).group(1) + if line[8:16] == ': invoke': + callee = line.split(' // ')[1].split('Method ')[1].split('\n')[0] + callee_class = callee.split('.')[0] + assert callee_class + if callee_class in api_classes: + callee_method = callee.split('.')[1] + assert callee_method + # Ignore constructor calls for now as every implementation class + # that extends an API class will call them. + # TODO(pauljensen): Look into enforcing restricting constructor calls. + # https://crbug.com/674975 + if callee_method.startswith('""'): + continue + # Ignore VersionSafe calls + if 'VersionSafeCallbacks' in caller_class: + continue + bad_call = '%s/%s -> %s/%s' % (caller_class, caller_method, + callee_class, callee_method) + if bad_call in ALLOWED_EXCEPTIONS: + continue + bad_calls += [bad_call] + + +def check_api_calls(opts): + # Returns True if no calls through API classes in implementation. + + temp_dir = tempfile.mkdtemp() + + # Extract API class files from jar + jar_cmd = ['jar', 'xf', os.path.abspath(opts.api_jar)] + build_utils.CheckOutput(jar_cmd, cwd=temp_dir) + shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) + + # Collect names of API classes + api_classes = [] + for dirpath, _, filenames in os.walk(temp_dir): + if not filenames: + continue + package = os.path.relpath(dirpath, temp_dir) + for filename in filenames: + if filename.endswith('.class'): + classname = filename[:-len('.class')] + api_classes += [os.path.normpath(os.path.join(package, classname))] + + shutil.rmtree(temp_dir) + temp_dir = tempfile.mkdtemp() + + # Extract impl class files from jars + for impl_jar in opts.impl_jar: + jar_cmd = ['jar', 'xf', os.path.abspath(impl_jar)] + build_utils.CheckOutput(jar_cmd, cwd=temp_dir) + shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) + + # Process classes + bad_api_calls = [] + for dirpath, _, filenames in os.walk(temp_dir): + if not filenames: + continue + # Dump classes + dump_file = os.path.join(temp_dir, 'dump.txt') + if os.system('javap -c %s > %s' % ( + ' '.join(os.path.join(dirpath, f) for f in filenames).replace( + '$', '\\$'), + dump_file)): + print('ERROR: javap failed on ' + ' '.join(filenames)) + return False + # Process class dump + with open(dump_file, 'r') as dump: + find_api_calls(dump, api_classes, bad_api_calls) + + shutil.rmtree(temp_dir) + + if bad_api_calls: + print('ERROR: Found the following calls from implementation classes ' + 'through') + print(' API classes. These could fail if older API is used that') + print(' does not contain newer methods. Please call through a') + print(' wrapper class from VersionSafeCallbacks.') + print('\n'.join(bad_api_calls)) + return not bad_api_calls + + +def check_api_version(opts): + if update_api.check_up_to_date(opts.api_jar): + return True + print('ERROR: API file out of date. Please run this command:') + print(' components/cronet/tools/update_api.py --api_jar %s' % ( + os.path.abspath(opts.api_jar))) + return False + + +def main(args): + parser = argparse.ArgumentParser( + description='Enforce Cronet API requirements.') + parser.add_argument('--api_jar', + help='Path to API jar (i.e. cronet_api.jar)', + required=True, + metavar='path/to/cronet_api.jar') + parser.add_argument('--impl_jar', + help='Path to implementation jar ' + '(i.e. cronet_impl_native_java.jar)', + required=True, + metavar='path/to/cronet_impl_native_java.jar', + action='append') + parser.add_argument('--stamp', help='Path to touch on success.') + opts = parser.parse_args(args) + + ret = True + ret = check_api_calls(opts) and ret + ret = check_api_version(opts) and ret + if ret and opts.stamp: + build_utils.Touch(opts.stamp) + return ret + + +if __name__ == '__main__': + sys.exit(0 if main(sys.argv[1:]) else -1) diff --git a/src/components/cronet/tools/api_static_checks_unittest.py b/src/components/cronet/tools/api_static_checks_unittest.py new file mode 100755 index 0000000000..1ca46ec527 --- /dev/null +++ b/src/components/cronet/tools/api_static_checks_unittest.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python +# Copyright 2016 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. + +"""api_static_checks_unittest.py - Unittests for api_static_checks.py""" + + +import contextlib +import hashlib +import os +import shutil +import sys +import tempfile +import unittest +import six + +REPOSITORY_ROOT = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', '..')) + +sys.path.append(os.path.join(REPOSITORY_ROOT, 'components')) +from cronet.tools import api_static_checks # pylint: disable=wrong-import-position + + +ERROR_PREFIX_CHECK_API_CALLS = ( +"""ERROR: Found the following calls from implementation classes through + API classes. These could fail if older API is used that + does not contain newer methods. Please call through a + wrapper class from VersionSafeCallbacks. +""") + + +ERROR_PREFIX_UPDATE_API = ( +"""ERROR: This API was modified or removed: + """) + + +ERROR_SUFFIX_UPDATE_API = ( +""" + + Cronet API methods and classes cannot be modified. +""") + + +CHECK_API_VERSION_PREFIX = ( +"""DO NOT EDIT THIS FILE, USE update_api.py TO UPDATE IT + +""") + + +API_FILENAME = './android/api.txt' +API_VERSION_FILENAME = './android/api_version.txt' + + +@contextlib.contextmanager +def capture_output(): + # A contextmanger that collects the stdout and stderr of wrapped code + + oldout,olderr = sys.stdout, sys.stderr + try: + out=[six.StringIO(), six.StringIO()] + sys.stdout,sys.stderr = out + yield out + finally: + sys.stdout,sys.stderr = oldout, olderr + out[0] = out[0].getvalue() + out[1] = out[1].getvalue() + + +class ApiStaticCheckUnitTest(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + os.chdir(self.temp_dir) + os.mkdir('android') + with open(API_VERSION_FILENAME, 'w') as api_version_file: + api_version_file.write('0') + with open(API_FILENAME, 'w') as api_file: + api_file.write('}\nStamp: 7d9d25f71cb8a5aba86202540a20d405\n') + shutil.copytree(os.path.dirname(__file__), 'tools') + + + def tearDown(self): + shutil.rmtree(self.temp_dir) + + + def make_jar(self, java, class_name): + # Compile |java| wrapped in a class named |class_name| to a jar file and + # return jar filename. + + java_filename = class_name + '.java' + class_filenames = class_name + '*.class' + jar_filename = class_name + '.jar' + + with open(java_filename, 'w') as java_file: + java_file.write('public class %s {' % class_name) + java_file.write(java) + java_file.write('}') + os.system('javac %s' % java_filename) + os.system('jar cf %s %s' % (jar_filename, class_filenames)) + return jar_filename + + + def run_check_api_calls(self, api_java, impl_java): + test = self + class MockOpts(object): + def __init__(self): + self.api_jar = test.make_jar(api_java, 'Api') + self.impl_jar = [test.make_jar(impl_java, 'Impl')] + opts = MockOpts() + with capture_output() as return_output: + return_code = api_static_checks.check_api_calls(opts) + return [return_code, return_output[0]] + + + def test_check_api_calls_success(self): + # Test simple classes with functions + self.assertEqual(self.run_check_api_calls( + 'void a(){}', 'void b(){}'), [True, '']) + # Test simple classes with functions calling themselves + self.assertEqual(self.run_check_api_calls( + 'void a(){} void b(){a();}', 'void c(){} void d(){c();}'), [True, '']) + + + def test_check_api_calls_failure(self): + # Test static call + self.assertEqual(self.run_check_api_calls( + 'public static void a(){}', 'void b(){Api.a();}'), + [False, ERROR_PREFIX_CHECK_API_CALLS + 'Impl/b -> Api/a:()V\n']) + # Test virtual call + self.assertEqual(self.run_check_api_calls( + 'public void a(){}', 'void b(){new Api().a();}'), + [False, ERROR_PREFIX_CHECK_API_CALLS + 'Impl/b -> Api/a:()V\n']) + + + def run_check_api_version(self, java): + OUT_FILENAME = 'out.txt' + return_code = os.system('./tools/update_api.py --api_jar %s > %s' % + (self.make_jar(java, 'Api'), OUT_FILENAME)) + with open(API_FILENAME, 'r') as api_file: + api = api_file.read() + with open(API_VERSION_FILENAME, 'r') as api_version_file: + api_version = api_version_file.read() + with open(OUT_FILENAME, 'r') as out_file: + output = out_file.read() + + # Verify stamp + api_stamp = api.split('\n')[-2] + stamp_length = len('Stamp: 78418460c193047980ae9eabb79293f2\n') + api = api[:-stamp_length] + api_hash = hashlib.md5() + api_hash.update(api) + self.assertEqual(api_stamp, 'Stamp: %s' % api_hash.hexdigest()) + + return [return_code == 0, output, api, api_version] + + + def test_update_api_success(self): + # Test simple new API + self.assertEqual(self.run_check_api_version( + 'public void a(){}'), + [True, '', CHECK_API_VERSION_PREFIX + """public class Api { + public Api(); + public void a(); +} +""", '1']) + # Test version number not increased when API not changed + self.assertEqual(self.run_check_api_version( + 'public void a(){}'), + [True, '', CHECK_API_VERSION_PREFIX + """public class Api { + public Api(); + public void a(); +} +""", '1']) + # Test acceptable API method addition + self.assertEqual(self.run_check_api_version( + 'public void a(){} public void b(){}'), + [True, '', CHECK_API_VERSION_PREFIX + """public class Api { + public Api(); + public void a(); + public void b(); +} +""", '2']) + # Test version number not increased when API not changed + self.assertEqual(self.run_check_api_version( + 'public void a(){} public void b(){}'), + [True, '', CHECK_API_VERSION_PREFIX + """public class Api { + public Api(); + public void a(); + public void b(); +} +""", '2']) + # Test acceptable API class addition + self.assertEqual(self.run_check_api_version( + 'public void a(){} public void b(){} public class C {}'), + [True, '', CHECK_API_VERSION_PREFIX + """public class Api$C { + public Api$C(Api); +} +public class Api { + public Api(); + public void a(); + public void b(); +} +""", '3']) + # Test version number not increased when API not changed + self.assertEqual(self.run_check_api_version( + 'public void a(){} public void b(){} public class C {}'), + [True, '', CHECK_API_VERSION_PREFIX + """public class Api$C { + public Api$C(Api); +} +public class Api { + public Api(); + public void a(); + public void b(); +} +""", '3']) + + + def test_update_api_failure(self): + # Create a simple new API + self.assertEqual(self.run_check_api_version( + 'public void a(){}'), + [True, '', CHECK_API_VERSION_PREFIX + """public class Api { + public Api(); + public void a(); +} +""", '1']) + # Test removing API method not allowed + self.assertEqual(self.run_check_api_version(''), + [False, ERROR_PREFIX_UPDATE_API + 'public void a();' + + ERROR_SUFFIX_UPDATE_API, + CHECK_API_VERSION_PREFIX + """public class Api { + public Api(); + public void a(); +} +""", '1']) + # Test modifying API method not allowed + self.assertEqual(self.run_check_api_version( + 'public void a(int x){}'), + [False, ERROR_PREFIX_UPDATE_API + 'public void a();' + + ERROR_SUFFIX_UPDATE_API, + CHECK_API_VERSION_PREFIX + """public class Api { + public Api(); + public void a(); +} +""", '1']) diff --git a/src/components/cronet/tools/check_no_neon.py b/src/components/cronet/tools/check_no_neon.py new file mode 100755 index 0000000000..df419a6308 --- /dev/null +++ b/src/components/cronet/tools/check_no_neon.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# Copyright 2016 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. + +"""check_no_neon.py - Check modules do not contain ARM Neon instructions.""" + +import argparse +import os +import sys + +REPOSITORY_ROOT = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)) + +sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'build/android/gyp')) +from util import build_utils # pylint: disable=wrong-import-position + + +def main(args): + parser = argparse.ArgumentParser( + description='Check modules do not contain ARM Neon instructions.') + parser.add_argument('objdump', metavar='path/to/ARM/objdump') + parser.add_argument('objects', metavar='files/to/check/*.o') + parser.add_argument('--stamp', help='Path to touch on success.') + opts = parser.parse_args(args) + ret = os.system(opts.objdump + ' -d --no-show-raw-insn ' + + opts.objects + ' | grep -q "vld[1-9]\\|vst[1-9]"') + + # Non-zero exit code means no neon. + if ret and opts.stamp: + build_utils.Touch(opts.stamp) + return ret + + +if __name__ == '__main__': + sys.exit(0 if main(sys.argv[1:]) != 0 else -1) diff --git a/src/components/cronet/tools/cr_cronet.py b/src/components/cronet/tools/cr_cronet.py new file mode 100755 index 0000000000..d90c8fae60 --- /dev/null +++ b/src/components/cronet/tools/cr_cronet.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python +# Copyright 2014 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. + +""" +cr_cronet.py - cr - like helper tool for cronet developers +""" + +import argparse +import os +import pipes +import subprocess +import sys + + +def quoted_args(args): + return ' '.join([pipes.quote(arg) for arg in args]) + + +def run(command, **kwargs): + print(command, kwargs) + return subprocess.call(command, **kwargs) + + +def run_shell(command, extra_options=''): + command = command + ' ' + extra_options + print(command) + return os.system(command) + + +def gn(out_dir, gn_args, gn_extra=None): + cmd = ['gn', 'gen', out_dir, "--args=%s" % gn_args] + if gn_extra: + cmd += gn_extra + return run(cmd) + + +def build(out_dir, build_target, extra_options=None): + cmd = ['ninja', '-C', out_dir, build_target] + get_ninja_jobs_options() + if extra_options: + cmd += extra_options + return run(cmd) + + +def install(out_dir): + cmd = ['build/android/adb_install_apk.py'] + # Propagate PATH to avoid issues with missing tools http://crbug/1217979 + env = { + 'BUILDTYPE': out_dir[4:], + 'PATH': os.environ.get('PATH', '') + } + return run(cmd + ['CronetTestInstrumentation.apk'], env=env) or \ + run(cmd + ['ChromiumNetTestSupport.apk'], env=env) + + +def test(out_dir, extra_options): + return run([out_dir + '/bin/run_cronet_test_instrumentation_apk'] + + extra_options) + + +def unittest(out_dir, extra_options): + return run([out_dir + '/bin/run_cronet_unittests_android'] + + extra_options) + + +def test_ios(out_dir, extra_options): + return run([out_dir + '/iossim', '-c', quoted_args(extra_options), + out_dir + '/cronet_test.app']) + + +def unittest_ios(out_dir, extra_options): + return run([out_dir + '/iossim', '-c', quoted_args(extra_options), + out_dir + '/cronet_unittests_ios.app']) + + +def debug(extra_options): + return run(['build/android/adb_gdb', '--start', + '--activity=.CronetTestActivity', + '--program-name=CronetTest', + '--package-name=org.chromium.net'] + + extra_options) + + +def stack(out_dir): + return run_shell( + 'adb logcat -d | CHROMIUM_OUTPUT_DIR=' + pipes.quote(out_dir) + + ' third_party/android_platform/development/scripts/stack') + + +def use_goma(): + goma_dir = subprocess.check_output(['goma_ctl', 'goma_dir']).strip() + result = run(['goma_ctl', 'ensure_start']) + if not result: + return 'use_goma=true goma_dir="' + goma_dir + '" ' + return '' + + +def get_ninja_jobs_options(): + if use_goma(): + return ["-j1000"] + return [] + + +def get_default_gn_args(target_os, is_release): + gn_args = 'target_os="' + target_os + ('" enable_websockets=false ' + 'disable_file_support=true ' + 'disable_brotli_filter=false ' + 'is_component_build=false ' + 'use_crash_key_stubs=true ' + 'use_partition_alloc=false ' + 'include_transport_security_state_preload_list=false ') + use_goma() + if (is_release): + gn_args += 'is_debug=false is_official_build=true ' + return gn_args + + +def get_mobile_gn_args(target_os, is_release): + return get_default_gn_args(target_os, is_release) + \ + 'use_platform_icu_alternatives=true ' + + +def get_ios_gn_args(is_release, bundle_id_prefix, target_cpu): + print(is_release, bundle_id_prefix, target_cpu) + return get_mobile_gn_args('ios', is_release) + \ + ('is_cronet_build=true ' + 'enable_remoting=false ' + 'ios_app_bundle_id_prefix="%s" ' + 'ios_deployment_target="10.0" ' + 'enable_dsyms=true ' + 'ios_stack_profiler_enabled=false ' + 'target_cpu="%s" ') % (bundle_id_prefix, target_cpu) + + +def get_android_gn_args(is_release): + return (get_mobile_gn_args('android', is_release) + + # Keep in sync with //tools/mb/mb_config.pyl cronet_android config. + 'default_min_sdk_version = 19 ' + + 'use_errorprone_java_compiler=true ' + + 'enable_reporting=true ' + + 'use_hashed_jni_names=true ') + + +def get_mac_gn_args(is_release): + return get_default_gn_args('mac', is_release) + \ + 'disable_histogram_support=true ' + \ + 'enable_dsyms=true ' + + +def main(): + is_ios = (sys.platform == 'darwin') + parser = argparse.ArgumentParser() + parser.add_argument('command', + choices=['gn', + 'sync', + 'build', + 'install', + 'proguard', + 'test', + 'build-test', + 'unit', + 'build-unit', + 'stack', + 'debug', + 'build-debug']) + parser.add_argument('-d', '--out_dir', action='store', + help='name of the build directory') + parser.add_argument('-x', '--x86', action='store_true', + help='build for Intel x86 architecture') + parser.add_argument('-r', '--release', action='store_true', + help='use release configuration') + parser.add_argument('-a', '--asan', action='store_true', + help='use address sanitizer') + if is_ios: + parser.add_argument('-i', '--iphoneos', action='store_true', + help='build for physical iphone') + parser.add_argument('-b', '--bundle-id-prefix', action='store', + dest='bundle_id_prefix', default='org.chromium', + help='configure bundle id prefix') + + options, extra_options = parser.parse_known_args() + print(options) + print(extra_options) + + if is_ios: + test_target = 'cronet_test' + unit_target = 'cronet_unittests_ios' + gn_extra = ['--ide=xcode', '--filters=//components/cronet/*'] + if options.iphoneos: + gn_args = get_ios_gn_args(options.release, options.bundle_id_prefix, + 'arm64') + out_dir_suffix = '-iphoneos' + else: + gn_args = get_ios_gn_args(options.release, options.bundle_id_prefix, + 'x64') + out_dir_suffix = '-iphonesimulator' + if options.asan: + gn_args += 'is_asan=true ' + out_dir_suffix += '-asan' + else: + test_target = 'cronet_test_instrumentation_apk' + unit_target = 'cronet_unittests_android' + gn_args = get_android_gn_args(options.release) + gn_extra = [] + out_dir_suffix = '' + if options.x86: + gn_args += 'target_cpu="x86" ' + out_dir_suffix = '-x86' + else: + gn_args += 'arm_use_neon=false ' + if options.asan: + # ASAN on Android requires one-time setup described here: + # https://www.chromium.org/developers/testing/addresssanitizer + gn_args += 'is_asan=true is_clang=true is_debug=false ' + out_dir_suffix += '-asan' + + if options.release: + out_dir = 'out/Release' + out_dir_suffix + else: + out_dir = 'out/Debug' + out_dir_suffix + + if options.out_dir: + out_dir = options.out_dir + + if (options.command=='gn'): + return gn(out_dir, gn_args, gn_extra) + if (options.command=='sync'): + return run(['git', 'pull', '--rebase']) or run(['gclient', 'sync']) + if (options.command=='build'): + return build(out_dir, test_target, extra_options) + if (not is_ios): + if (options.command=='install'): + return install(out_dir) + if (options.command=='proguard'): + return build(out_dir, 'cronet_sample_proguard_apk') + if (options.command=='test'): + return install(out_dir) or test(out_dir, extra_options) + if (options.command=='build-test'): + return build(out_dir, test_target) or install(out_dir) or \ + test(out_dir, extra_options) + if (options.command=='stack'): + return stack(out_dir) + if (options.command=='debug'): + return install(out_dir) or debug(extra_options) + if (options.command=='build-debug'): + return build(out_dir, test_target) or install(out_dir) or \ + debug(extra_options) + if (options.command=='unit'): + return unittest(out_dir, extra_options) + if (options.command=='build-unit'): + return build(out_dir, unit_target) or unittest(out_dir, extra_options) + else: + if (options.command=='test'): + return test_ios(out_dir, extra_options) + if (options.command=='build-test'): + return build(out_dir, test_target) or test_ios(out_dir, extra_options) + if (options.command=='unit'): + return unittest_ios(out_dir, extra_options) + if (options.command=='build-unit'): + return build(out_dir, unit_target) or unittest_ios(out_dir, extra_options) + + parser.print_help() + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/components/cronet/tools/generate_accept_languages.py b/src/components/cronet/tools/generate_accept_languages.py new file mode 100644 index 0000000000..2db0a44112 --- /dev/null +++ b/src/components/cronet/tools/generate_accept_languages.py @@ -0,0 +1,51 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# This script generates a header containing a dictionary from locales to +# accept language strings from chromium's .xtb files. It is not very +# robust at the moment, and makes some assumptions about the format of +# the files, including at least the following: +# * assumes necessary data is contained only with files of the form +# components/strings/components_locale_settings_${LANG}.xtb +# * assumes ${LANG} is identified in the lang attribute of the root +# element of the file's xml data +# * assumes that there is only one relevant element with the +# IDS_ACCEPT_LANGUAGES attribute + +from __future__ import print_function + +import os +import re +import sys +from xml.etree import ElementTree + +STRINGS_DIR = sys.argv[2] + 'components/strings/' + +def extract_accept_langs(filename): + tree = ElementTree.parse(STRINGS_DIR + filename).getroot() + for child in tree: + if child.get('id') == 'IDS_ACCEPT_LANGUAGES': + return tree.get('lang'), child.text + +def gen_accept_langs_table(): + accept_langs_list = [extract_accept_langs(filename) + for filename in os.listdir(STRINGS_DIR) + if re.match(r'components_locale_settings_\S+.xtb', filename)] + return dict(accept_langs for accept_langs in accept_langs_list + if accept_langs) + +HEADER = "static NSDictionary* const acceptLangs = @{" +def LINE(locale, accept_langs): + return ' @"' + locale + '" : @"' + accept_langs + '",' +FOOTER = "};" + +def main(): + with open(sys.argv[1] + "/accept_languages_table.h", "w+") as f: + print(HEADER, file=f) + for (locale, accept_langs) in gen_accept_langs_table().items(): + print(LINE(locale, accept_langs), file=f) + print(FOOTER, file=f) + +if __name__ == "__main__": + main() diff --git a/src/components/cronet/tools/generate_idl_bindings.py b/src/components/cronet/tools/generate_idl_bindings.py new file mode 100755 index 0000000000..93599da25d --- /dev/null +++ b/src/components/cronet/tools/generate_idl_bindings.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import optparse +import os +import shutil +import sys +import tempfile + +def run(command, extra_options=''): + command = command + ' ' + extra_options + print(command) + ret = os.system(command) + if ret != 0: + raise OSError(ret) + + +def GenerateIdlBindings(output_path, input_files): + bytecode_path = tempfile.mkdtemp('idl_bytecode') + generator = 'components/cronet/tools/generators/cronet_bindings_generator.py' + input_file = 'components/cronet/native/cronet.idl' + # Precompile bindings templates + run(generator + ' precompile -o ', bytecode_path) + # Generate C interface. + run(generator + ' --use_bundled_pylibs generate ' + input_file + + ' --bytecode_path ' + bytecode_path + ' -g c') + # TODO(mef): Use output_path to put generated code into + # out//gen/components/cronet/native directory. + # -o ' + output_path) + # Format generated code. + run('git cl format --full components/cronet/native') + shutil.rmtree(bytecode_path) + + +def main(): + parser = optparse.OptionParser() + parser.add_option('--output-path', + help='Output path for generated bindings') + + options, input_files = parser.parse_args() + GenerateIdlBindings(options.output_path, input_files) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/components/cronet/tools/generate_javadoc.py b/src/components/cronet/tools/generate_javadoc.py new file mode 100755 index 0000000000..cc878ca5d9 --- /dev/null +++ b/src/components/cronet/tools/generate_javadoc.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# +# 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. + +import optparse +import os +import shutil +import sys +import tempfile + +REPOSITORY_ROOT = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)) + +sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'build/android/gyp')) +sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'net/tools/net_docs')) +# pylint: disable=wrong-import-position +from util import build_utils +import net_docs +from markdown.postprocessors import Postprocessor +from markdown.extensions import Extension +# pylint: enable=wrong-import-position + +DOCLAVA_DIR = os.path.join(REPOSITORY_ROOT, 'buildtools', 'android', 'doclava') +SDK_DIR = os.path.join(REPOSITORY_ROOT, 'third_party', 'android_sdk', 'public') +JAVADOC_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'javadoc') + +JAVADOC_WARNING = """\ +javadoc: warning - The old Doclet and Taglet APIs in the packages +com.sun.javadoc, com.sun.tools.doclets and their implementations +are planned to be removed in a future JDK release. These +components have been superseded by the new APIs in jdk.javadoc.doclet. +Users are strongly recommended to migrate to the new APIs. +""" + +class CronetPostprocessor(Postprocessor): + def run(self, text): + return text.replace('@Override', '@Override') + + +class CronetExtension(Extension): + def extendMarkdown(self, md, md_globals): + md.postprocessors.add('CronetPostprocessor', + CronetPostprocessor(md), '_end') + + +def GenerateJavadoc(options, src_dir, output_dir): + working_dir = os.path.join(options.input_dir, 'android', 'api') + overview_file = os.path.abspath(options.overview_file) + + android_sdk_jar = options.android_sdk_jar + if not android_sdk_jar: + android_sdk_jar = os.path.join( + SDK_DIR, 'platforms', 'android-27', 'android.jar') + + build_utils.DeleteDirectory(output_dir) + build_utils.MakeDirectory(output_dir) + javadoc_cmd = [ + os.path.abspath(JAVADOC_PATH), + '-d', output_dir, + '-quiet', + '-overview', overview_file, + '-doclet', 'com.google.doclava.Doclava', + '-docletpath', + '%s:%s' % (os.path.join(DOCLAVA_DIR, 'jsilver.jar'), + os.path.join(DOCLAVA_DIR, 'doclava.jar')), + '-title', 'Cronet API', + '-federate', 'Android', 'https://developer.android.com/', + '-federationapi', 'Android', os.path.join(DOCLAVA_DIR, 'current.txt'), + '-classpath', + '%s:%s' % (os.path.abspath(android_sdk_jar), + os.path.abspath(options.support_annotations_jar)), + ] + for subdir, _, files in os.walk(src_dir): + for filename in files: + if filename.endswith(".java"): + javadoc_cmd += [os.path.join(subdir, filename)] + try: + def stderr_filter(stderr): + return stderr.replace(JAVADOC_WARNING, '') + + build_utils.CheckOutput(javadoc_cmd, cwd=working_dir, + stderr_filter=stderr_filter) + except build_utils.CalledProcessError: + build_utils.DeleteDirectory(output_dir) + raise + + # Create an index.html file at the root as this is the accepted format. + # Do this by copying reference/index.html and adjusting the path. + with open(os.path.join(output_dir, 'reference', 'index.html'), 'r') as \ + old_index, open(os.path.join(output_dir, 'index.html'), 'w') as new_index: + for line in old_index: + new_index.write(line.replace('classes.html', + os.path.join('reference', 'classes.html'))) + + +def main(): + parser = optparse.OptionParser() + build_utils.AddDepfileOption(parser) + parser.add_option('--output-dir', help='Directory to put javadoc') + parser.add_option('--input-dir', help='Root of cronet source') + parser.add_option('--input-src-jar', help='Cronet api source jar') + parser.add_option('--overview-file', help='Path of the overview page') + parser.add_option('--readme-file', help='Path of the README.md') + parser.add_option('--zip-file', help='Path to ZIP archive of javadocs.') + parser.add_option('--android-sdk-jar', help='Path to android.jar') + parser.add_option('--support-annotations-jar', + help='Path to support-annotations-$VERSION.jar') + + options, _ = parser.parse_args() + # A temporary directory to put the output of cronet api source jar files. + unzipped_jar_path = tempfile.mkdtemp(dir=options.output_dir) + if os.path.exists(options.input_src_jar): + jar_cmd = ['jar', 'xf', os.path.abspath(options.input_src_jar)] + build_utils.CheckOutput(jar_cmd, cwd=unzipped_jar_path) + else: + raise Exception('Jar file does not exist: %s' % options.input_src_jar) + + net_docs.ProcessDocs([options.readme_file], options.input_dir, + options.output_dir, extensions=[CronetExtension()]) + + output_dir = os.path.abspath(os.path.join(options.output_dir, 'javadoc')) + GenerateJavadoc(options, os.path.abspath(unzipped_jar_path), output_dir) + + if options.zip_file: + assert options.zip_file.endswith('.zip') + shutil.make_archive(options.zip_file[:-4], 'zip', output_dir) + if options.depfile: + assert options.zip_file + deps = [] + for root, _, filenames in os.walk(options.input_dir): + # Ignore .pyc files here, it might be re-generated during build. + deps.extend(os.path.join(root, f) for f in filenames + if not f.endswith('.pyc')) + build_utils.WriteDepfile(options.depfile, options.zip_file, deps) + # Clean up temporary output directory. + build_utils.DeleteDirectory(unzipped_jar_path) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/components/cronet/tools/generate_javadoc.pydeps b/src/components/cronet/tools/generate_javadoc.pydeps new file mode 100644 index 0000000000..0de5776e54 --- /dev/null +++ b/src/components/cronet/tools/generate_javadoc.pydeps @@ -0,0 +1,19 @@ +# Generated by running: +# build/print_python_deps.py --root components/cronet/tools --output components/cronet/tools/generate_javadoc.pydeps components/cronet/tools/generate_javadoc.py +../../../build/android/gyp/util/__init__.py +../../../build/android/gyp/util/build_utils.py +../../../build/gn_helpers.py +../../../net/tools/net_docs/net_docs.py +../../../third_party/markdown/__init__.py +../../../third_party/markdown/__version__.py +../../../third_party/markdown/blockparser.py +../../../third_party/markdown/blockprocessors.py +../../../third_party/markdown/extensions/__init__.py +../../../third_party/markdown/inlinepatterns.py +../../../third_party/markdown/odict.py +../../../third_party/markdown/postprocessors.py +../../../third_party/markdown/preprocessors.py +../../../third_party/markdown/serializers.py +../../../third_party/markdown/treeprocessors.py +../../../third_party/markdown/util.py +generate_javadoc.py diff --git a/src/components/cronet/tools/generate_proguard_file.py b/src/components/cronet/tools/generate_proguard_file.py new file mode 100755 index 0000000000..8ff42cf41b --- /dev/null +++ b/src/components/cronet/tools/generate_proguard_file.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# +# Copyright 2016 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. + +# Tool that combines a sequence of input proguard files and outputs a single +# proguard file. +# +# The final output file is formed by concatenating all of the +# input proguard files. + +import optparse +import sys + + +def ReadFile(path): + with open(path, 'rb') as f: + return f.read() + + +def main(): + parser = optparse.OptionParser() + parser.add_option('--output-file', + help='Output file for the generated proguard file') + + options, input_files = parser.parse_args() + + # Concatenate all the proguard files. + with open(options.output_file, 'wb') as target: + for input_file in input_files: + target.write(ReadFile(input_file)) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/components/cronet/tools/generators/c_templates/module_c.h.tmpl b/src/components/cronet/tools/generators/c_templates/module_c.h.tmpl new file mode 100644 index 0000000000..8bccaecebf --- /dev/null +++ b/src/components/cronet/tools/generators/c_templates/module_c.h.tmpl @@ -0,0 +1,189 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from {{module.path}} */ + +{%- if variant -%} +{%- set variant_path = "%s-%s"|format(module.path, variant) -%} +{%- else -%} +{%- set variant_path = module.path -%} +{%- endif -%} + +{%- set header_guard = "%s_C_H_"|format( + variant_path|upper|replace("/","_")|replace(".","_")| + replace("-", "_")) %} + +{%- import "module_macros.tmpl" as module_macros %} + +#ifndef {{header_guard}} +#define {{header_guard}} + +{#-- TODO(mef): Derive EXPORT_MACRO from module name --#} +{%- set export_macro = "CRONET_EXPORT" %} +#include "cronet_export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef const char* Cronet_String; +typedef void* Cronet_RawDataPtr; +typedef void* Cronet_ClientContext; + +// Forward declare interfaces. +{%- for interface in interfaces %} +{%- set interface_name = interface|get_name_for_kind %} +typedef struct {{interface_name}} {{interface_name}}; +typedef struct {{interface_name}}* {{interface_name}}Ptr; +{%- endfor %} + +// Forward declare structs. +{%- for struct in structs %} +{%- set struct_name = struct|get_name_for_kind %} +typedef struct {{struct_name}} {{struct_name}}; +typedef struct {{struct_name}}* {{struct_name}}Ptr; +{%- endfor %} + +// Declare enums +{%- for enum in all_enums %} +{%- set enum_name = enum|get_name_for_kind(flatten_nested_kind=False) %} +typedef enum {{enum_name}} { +{%- for field in enum.fields %} +{%- if field.value %} + {{enum_name}}_{{field.name}} = {{field.value|expression_to_text}}, +{%- else %} + {{enum_name}}_{{field.name}}, +{%- endif %} +{%- endfor %} +} {{enum_name}}; + +{% endfor %} + +// Declare constants +{%- for constant in module.constants %} +{{constant|format_constant_declaration}}; +{%- endfor %} + +{#--- Interface Stubs -#} +{% for interface in interfaces %} +{%- set interface_name = interface|get_name_for_kind %} + +/////////////////////// +{%- if interface|is_abstract %} +// Abstract interface {{interface_name}} is implemented by the app. + +// There is no method to create a concrete implementation. + +{% else %} +// Concrete interface {{interface_name}}. + +// Create an instance of {{interface_name}}. +{{export_macro}} {{interface_name}}Ptr {{interface_name}}_Create(void); +{%- endif %} +// Destroy an instance of {{interface_name}}. +{{export_macro}} void {{interface_name}}_Destroy({{interface_name}}Ptr self); +// Set and get app-specific Cronet_ClientContext. +{{export_macro}} void {{interface_name}}_SetClientContext({{interface_name}}Ptr self, Cronet_ClientContext client_context); +{{export_macro}} Cronet_ClientContext {{interface_name}}_GetClientContext({{interface_name}}Ptr self); +{%- if interface|is_abstract %} +// Abstract interface {{interface_name}} is implemented by the app. +// The following concrete methods forward call to app implementation. +// The app doesn't normally call them. +{%- else %} +// Concrete methods of {{interface_name}} implemented by Cronet. +// The app calls them to manipulate {{interface_name}}. +{%- endif %} +{%- for method in interface.methods %} +{{export_macro}} +{%- if method.response_parameters and method.sync %} +{%- for param in method.response_parameters %} +{{param.kind|c_wrapper_type}} +{%- endfor -%} +{%- else %} +void +{%- endif %} + {{interface_name}}_{{method.name}}({{interface_name}}Ptr self +{%- if method.parameters %}, {{module_macros.declare_c_params("", method.parameters)}} +{%- endif %}); +{%- endfor %} + +{%- if interface|is_abstract %} +// The app implements abstract interface {{interface_name}} by defining custom functions +// for each method. +{%- else %} +// Concrete interface {{interface_name}} is implemented by Cronet. +// The app can implement these for testing / mocking. +{%- endif %} +{%- for method in interface.methods %} +{%- if method.response_parameters and method.sync %} +{%- for param in method.response_parameters %} +typedef {{param.kind|c_wrapper_type}} +{%- endfor -%} +{%- else %} +typedef void +{%- endif %} + (*{{interface_name}}_{{method.name}}Func)({{interface_name}}Ptr self +{%- if method.parameters %}, {{module_macros.declare_c_params("", method.parameters)}} +{%- endif %}); +{%- endfor %} + +{%- if interface|is_abstract %} +// The app creates an instance of {{interface_name}} by providing custom functions +// for each method. +{%- else %} +// Concrete interface {{interface_name}} is implemented by Cronet. +// The app can use this for testing / mocking. +{%- endif %} +{{export_macro}} {{interface_name}}Ptr {{interface_name}}_CreateWith( +{%- for method in interface.methods -%} + {{interface_name}}_{{method.name}}Func {{method.name}}Func +{%- if not loop.last %}, {% endif %} +{%- endfor %} + ); +{%- endfor %} + +{% for struct in structs %} +{% set struct_name = struct|get_name_for_kind %} +/////////////////////// +// Struct {{struct_name}}. +{{export_macro}} {{struct_name}}Ptr {{struct_name}}_Create(void); +{{export_macro}} void {{struct_name}}_Destroy({{struct_name}}Ptr self); +// {{struct_name}} setters. +{%- for packed_field in struct.packed.packed_fields_in_ordinal_order %} +{{export_macro}} +{%- set kind = packed_field.field.kind %} +{%- if kind|is_array_kind %} +void {{struct_name}}_{{packed_field.field.name}}_add({{struct_name}}Ptr self, const {{kind.kind|c_wrapper_type}} element); +{%- else %} +void {{struct_name}}_{{packed_field.field.name}}_set({{struct_name}}Ptr self, const {{packed_field.field.kind|c_wrapper_type}} {{packed_field.field.name}}); +{%- endif %} +{%- if kind|is_struct_kind %} +// Move data from |{{packed_field.field.name}}|. The caller retains ownership of |{{packed_field.field.name}}| and must destroy it. +void {{struct_name}}_{{packed_field.field.name}}_move({{struct_name}}Ptr self, {{packed_field.field.kind|c_wrapper_type}} {{packed_field.field.name}}); +{%- endif %} +{%- endfor %} +// {{struct_name}} getters. +{%- for packed_field in struct.packed.packed_fields_in_ordinal_order %} +{{export_macro}} +{%- set kind = packed_field.field.kind %} +{%- if kind|is_array_kind %} +uint32_t {{struct_name}}_{{packed_field.field.name}}_size(const {{struct_name}}Ptr self); +{{export_macro}} +{{kind.kind|c_wrapper_type}} {{struct_name}}_{{packed_field.field.name}}_at(const {{struct_name}}Ptr self, uint32_t index); +{{export_macro}} +void {{struct_name}}_{{packed_field.field.name}}_clear({{struct_name}}Ptr self); +{%- else %} +{{packed_field.field.kind|c_wrapper_type}} {{struct_name}}_{{packed_field.field.name}}_get(const {{struct_name}}Ptr self); +{%- endif %} +{%- endfor %} +{%- endfor %} + +#ifdef __cplusplus +} +#endif + +#endif // {{header_guard}} + diff --git a/src/components/cronet/tools/generators/c_templates/module_impl_interface.cc.tmpl b/src/components/cronet/tools/generators/c_templates/module_impl_interface.cc.tmpl new file mode 100644 index 0000000000..4d3fda2d46 --- /dev/null +++ b/src/components/cronet/tools/generators/c_templates/module_impl_interface.cc.tmpl @@ -0,0 +1,112 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from {{module.path}} */ + +{%- if variant -%} +{%- set variant_path = "%s-%s"|format(module.path, variant) -%} +{%- else -%} +{%- set variant_path = module.path -%} +{%- endif %} + +{%- import "module_macros.tmpl" as module_macros %} + +#include "{{variant_path}}_impl_interface.h" + +#include "base/check.h" + +{#--- Interface Stubs -#} +{% for interface in interfaces %} +{%- set interface_name = interface|get_name_for_kind %} + +// C functions of {{interface_name}} that forward calls to C++ implementation. +void {{interface_name}}_Destroy({{interface_name}}Ptr self) { + DCHECK(self); + return delete self; +} + +void {{interface_name}}_SetClientContext({{interface_name}}Ptr self, Cronet_ClientContext client_context) { + DCHECK(self); + self->set_client_context(client_context); +} + +Cronet_ClientContext {{interface_name}}_GetClientContext({{interface_name}}Ptr self) { + DCHECK(self); + return self->client_context(); +} + +{% for method in interface.methods %} +{%- if method.response_parameters and method.sync %} +{%- for param in method.response_parameters %} +{{param.kind|c_wrapper_type}} +{%- endfor -%} +{%- else %} +void +{%- endif %} + {{interface_name}}_{{method.name}}({{interface_name}}Ptr self +{%- if method.parameters %}, {{module_macros.declare_c_params("", method.parameters)}} +{%- endif %}) { + DCHECK(self); +{%- if method.response_parameters and method.sync %} + return +{%- endif %} + self->{{method.name}}({{module_macros.forward_c_params("", method.parameters)}}); +} + +{% endfor %} +// Implementation of {{interface_name}} that forwards calls to C functions implemented by the app. +class {{interface_name}}Stub : public {{interface_name}} { + public: +{%- if interface.methods|count == 1 -%} + explicit +{%- endif %} + {{interface_name}}Stub( +{%- for method in interface.methods -%} + {{interface_name}}_{{method.name}}Func {{method.name}}Func +{%- if not loop.last %}, {% endif %} +{%- endfor %}) : +{%- for method in interface.methods -%} + {{method.name}}Func_({{method.name}}Func) +{%- if not loop.last %}, {% endif %} +{%- endfor %} {} + + {{interface_name}}Stub(const {{interface_name}}Stub&) = delete; + {{interface_name}}Stub& operator=(const {{interface_name}}Stub&) = delete; + + ~{{interface_name}}Stub() override {} + + protected: +{% for method in interface.methods %} + {{module_macros.declare_c_return(method)}} + {{method.name}}({{module_macros.declare_c_params("", method.parameters)}}) override { +{%- if method.response_parameters and method.sync %} + return +{%- endif %} + {{method.name}}Func_(this +{%- if method.parameters %}, {{module_macros.forward_c_params("", method.parameters)}} +{%- endif %}); + } +{%- endfor %} + private: +{%- for method in interface.methods -%} + const {{interface_name}}_{{method.name}}Func {{method.name}}Func_; +{%- endfor %} +}; + +{{interface_name}}Ptr {{interface_name}}_CreateWith( +{%- for method in interface.methods -%} + {{interface_name}}_{{method.name}}Func {{method.name}}Func +{%- if not loop.last %}, {% endif %} +{%- endfor %} + ) { + return new {{interface_name}}Stub( +{%- for method in interface.methods -%} + {{method.name}}Func +{%- if not loop.last %}, {% endif %} +{%- endfor %} + ); +} + +{% endfor %} + diff --git a/src/components/cronet/tools/generators/c_templates/module_impl_interface.h.tmpl b/src/components/cronet/tools/generators/c_templates/module_impl_interface.h.tmpl new file mode 100644 index 0000000000..742062add8 --- /dev/null +++ b/src/components/cronet/tools/generators/c_templates/module_impl_interface.h.tmpl @@ -0,0 +1,59 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from {{module.path}} */ + +{%- if variant -%} +{%- set variant_path = "%s-%s"|format(module.path, variant) -%} +{%- else -%} +{%- set variant_path = module.path -%} +{%- endif %} + +{%- set header_guard = "%s_IMPL_INTERFACE_H_"|format( + variant_path|upper|replace("/","_")|replace(".","_")| + replace("-", "_")) %} + +{%- import "module_macros.tmpl" as module_macros %} + +#ifndef {{header_guard}} +#define {{header_guard}} + +#include +#include + +#include "{{variant_path}}_c.h" + +{# Interface Stubs #} +{% for interface in interfaces %} +{%- set interface_name = interface|get_name_for_kind %} +struct {{interface_name}} { + {{interface_name}}() = default; + + {{interface_name}}(const {{interface_name}}&) = delete; + {{interface_name}}& operator=(const {{interface_name}}&) = delete; + + virtual ~{{interface_name}}() = default; + + void set_client_context(Cronet_ClientContext client_context) { + client_context_ = client_context; + } + Cronet_ClientContext client_context() const { return client_context_; } + +{% for method in interface.methods %} +{%- if method.response_parameters and method.sync %} +{%- for param in method.response_parameters %} +virtual {{param.kind|c_wrapper_type}} +{%- endfor -%} +{%- else %} +virtual void +{%- endif %} + {{method.name}}({{module_macros.declare_c_params("", method.parameters)}}) = 0; +{%- endfor %} + private: + Cronet_ClientContext client_context_ = nullptr; +}; + +{% endfor %} +#endif // {{header_guard}} + diff --git a/src/components/cronet/tools/generators/c_templates/module_impl_interface_unittest.cc.tmpl b/src/components/cronet/tools/generators/c_templates/module_impl_interface_unittest.cc.tmpl new file mode 100644 index 0000000000..e539a29bdd --- /dev/null +++ b/src/components/cronet/tools/generators/c_templates/module_impl_interface_unittest.cc.tmpl @@ -0,0 +1,92 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from {{module.path}} */ + +{%- if variant -%} +{%- set variant_path = "%s-%s"|format(module.path, variant) -%} +{%- else -%} +{%- set variant_path = module.path -%} +{%- endif %} + +{%- import "module_macros.tmpl" as module_macros %} + +#include "{{variant_path}}_c.h" + +#include "base/check.h" +#include "testing/gtest/include/gtest/gtest.h" + + +{#--- Interface Stubs -#} +{% for interface in interfaces %} +{%- set interface_name = interface|get_name_for_kind %} +{% set test_class_name = interface_name %} + +// Test of {{interface_name}} interface. +class {{test_class_name}}Test : public ::testing::Test { + public: + {{test_class_name}}Test(const {{test_class_name}}Test&) = delete; + {{test_class_name}}Test& operator=(const {{test_class_name}}Test&) = delete; + + protected: + void SetUp() override { + } + + void TearDown() override { + } + + {{test_class_name}}Test() = default; + ~{{test_class_name}}Test() override = default; + + public: +{%- for method in interface.methods -%} + bool {{method.name}}_called_ = false; +{%- endfor %} +}; + +namespace { +// Implementation of {{interface_name}} methods for testing. +{%- for method in interface.methods -%} +{{module_macros.declare_c_return(method)}} + Test{{interface_name}}_{{method.name}}({{interface_name}}Ptr self +{%- if method.parameters %}, {{module_macros.declare_c_params("", method.parameters)}} +{%- endif %}) { + CHECK(self); + Cronet_ClientContext client_context = {{interface_name}}_GetClientContext(self); + auto* test = static_cast<{{interface_name}}Test*>(client_context); + CHECK(test); + test->{{method.name}}_called_ = true; + {%- if method.response_parameters and method.sync %} + {% set return_param = method.response_parameters|first %} + return static_cast<{{return_param.kind|c_wrapper_type}}>(0); + {%- endif %} +} +{%- endfor %} +} // namespace + +// Test that {{test_class_name}} stub forwards function calls as expected. +TEST_F({{test_class_name}}Test, TestCreate) { + {{interface_name}}Ptr test = {{interface_name}}_CreateWith( +{%- for method in interface.methods -%} + Test{{interface_name}}_{{method.name}} +{%- if not loop.last %}, {% endif %} +{%- endfor %} + ); + CHECK(test); + {{interface_name}}_SetClientContext(test, this); +{%- for method in interface.methods -%} +{%- if not method.parameters %} + {{interface_name}}_{{method.name}}(test); + CHECK({{method.name}}_called_); +{%- else %} + CHECK(!{{method.name}}_called_); +{%- endif %} +{%- endfor %} + + + {{interface_name}}_Destroy(test); +} + +{%- endfor %} + diff --git a/src/components/cronet/tools/generators/c_templates/module_impl_struct.cc.tmpl b/src/components/cronet/tools/generators/c_templates/module_impl_struct.cc.tmpl new file mode 100644 index 0000000000..e932b50c82 --- /dev/null +++ b/src/components/cronet/tools/generators/c_templates/module_impl_struct.cc.tmpl @@ -0,0 +1,113 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from {{module.path}} */ + + +{%- if variant -%} +{%- set variant_path = "%s-%s"|format(module.path, variant) -%} +{%- else -%} +{%- set variant_path = module.path -%} +{%- endif %} + +#include "{{variant_path}}_impl_struct.h" + +#include + +#include "base/check.h" + +{%- for struct in structs %} +{%- set struct_name = struct|get_name_for_kind %} + +// Struct {{struct_name}}. +{{struct_name}}::{{struct_name}}() = default; + +{{struct_name}}::{{struct_name}}(const {{struct_name}}&from) = default; + +{{struct_name}}::{{struct_name}}({{struct_name}}&&from) = default; + +{{struct_name}}::~{{struct_name}}() = default; + +{{struct_name}}Ptr {{struct_name}}_Create() { + return new {{struct_name}}(); +} + +void {{struct_name}}_Destroy({{struct_name}}Ptr self) { + delete self; +} + +// Struct {{struct_name}} setters. +{%- for packed_field in struct.packed.packed_fields_in_ordinal_order %} +{%- set kind = packed_field.field.kind %} +{%- if kind|is_array_kind %} +void {{struct_name}}_{{packed_field.field.name}}_add({{struct_name}}Ptr self, const {{kind.kind|c_wrapper_type}} element) { + DCHECK(self); +{%- if kind.kind|is_struct_kind %} + self->{{packed_field.field.name}}.push_back(*element); +{%- else %} + self->{{packed_field.field.name}}.push_back(element); +{%- endif %} +} +{%- else %} +void {{struct_name}}_{{packed_field.field.name}}_set({{struct_name}}Ptr self, const {{packed_field.field.kind|c_wrapper_type}} {{packed_field.field.name}}) { + DCHECK(self); +{%- if kind|is_struct_kind %} + self->{{packed_field.field.name}}.reset(); + if ({{packed_field.field.name}} != nullptr) + self->{{packed_field.field.name}}.emplace(*{{packed_field.field.name}}); +{%- else %} + self->{{packed_field.field.name}} = {{packed_field.field.name}}; +{%- endif %} +} +{%- endif %} +{%- if kind|is_struct_kind %} +void {{struct_name}}_{{packed_field.field.name}}_move({{struct_name}}Ptr self, {{packed_field.field.kind|c_wrapper_type}} {{packed_field.field.name}}) { + DCHECK(self); + self->{{packed_field.field.name}}.reset(); + if ({{packed_field.field.name}} != nullptr) + self->{{packed_field.field.name}}.emplace(std::move(*{{packed_field.field.name}})); +} +{%- endif %} +{% endfor %} + +// Struct {{struct_name}} getters. +{%- for packed_field in struct.packed.packed_fields_in_ordinal_order %} +{%- set kind = packed_field.field.kind %} +{%- if kind|is_array_kind %} +uint32_t {{struct_name}}_{{packed_field.field.name}}_size({{struct_name}}Ptr self) { + DCHECK(self); + return self->{{packed_field.field.name}}.size(); +} +{{kind.kind|c_wrapper_type}} {{struct_name}}_{{packed_field.field.name}}_at(const {{struct_name}}Ptr self, uint32_t index) { + DCHECK(self); + DCHECK(index < self->{{packed_field.field.name}}.size()); +{%- if kind.kind|is_struct_kind %} + return &(self->{{packed_field.field.name}}[index]); +{%- elif kind.kind|is_string_kind %} + return self->{{packed_field.field.name}}[index].c_str(); +{%- else %} + return self->{{packed_field.field.name}}[index]; +{%- endif %} +} +void {{struct_name}}_{{packed_field.field.name}}_clear({{struct_name}}Ptr self) { + DCHECK(self); + self->{{packed_field.field.name}}.clear(); +} +{%- else %} +{{packed_field.field.kind|c_wrapper_type}} {{struct_name}}_{{packed_field.field.name}}_get(const {{struct_name}}Ptr self) { + DCHECK(self); +{%- if kind|is_struct_kind %} + if (self->{{packed_field.field.name}} == absl::nullopt) + return nullptr; + return &self->{{packed_field.field.name}}.value(); +{%- elif kind|is_string_kind %} + return self->{{packed_field.field.name}}.c_str(); +{%- else %} + return self->{{packed_field.field.name}}; +{%- endif %} +} +{%- endif %} +{% endfor %} +{%- endfor %} + diff --git a/src/components/cronet/tools/generators/c_templates/module_impl_struct.h.tmpl b/src/components/cronet/tools/generators/c_templates/module_impl_struct.h.tmpl new file mode 100644 index 0000000000..39935a34d0 --- /dev/null +++ b/src/components/cronet/tools/generators/c_templates/module_impl_struct.h.tmpl @@ -0,0 +1,60 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from {{module.path}} */ + +{%- if variant -%} +{%- set variant_path = "%s-%s"|format(module.path, variant) -%} +{%- else -%} +{%- set variant_path = module.path -%} +{%- endif %} + +{%- set header_guard = "%s_IMPL_STRUCT_H_"|format( + variant_path|upper|replace("/","_")|replace(".","_")| + replace("-", "_")) %} + +#ifndef {{header_guard}} +#define {{header_guard}} + +#include "{{variant_path}}_c.h" + +#include +#include +#include + +#include "third_party/abseil-cpp/absl/types/optional.h" + +{%- for struct in structs %} +{%- set struct_name = struct|get_name_for_kind %} + +// Struct {{struct_name}}. +struct {{struct_name}} { + public: + {{struct_name}}(); + explicit {{struct_name}}(const {{struct_name}}&from); + + {{struct_name}}& operator=(const {{struct_name}}&) = delete; + + explicit {{struct_name}}({{struct_name}}&&from); + + ~{{struct_name}}(); + +{% for packed_field in struct.packed.packed_fields_in_ordinal_order %} +{%- set kind = packed_field.field.kind %} +{%- if kind|is_array_kind %} +std::vector<{{kind.kind|cpp_wrapper_type}}> {{packed_field.field.name}}; +{%- elif kind|is_struct_kind %} +absl::optional<{{kind|cpp_wrapper_type}}> {{packed_field.field.name}}; +{%- else %} +{{packed_field.field.kind|cpp_wrapper_type}} {{packed_field.field.name}} +{%- if packed_field.field.default %} = {{packed_field.field|default_value}} {%- endif %} +{%- if packed_field.field.kind|is_any_interface_kind %} = nullptr {%- endif %}; +{%- endif %} +{%- endfor %} +}; +{%- endfor %} + + +#endif // {{header_guard}} + diff --git a/src/components/cronet/tools/generators/c_templates/module_impl_struct_unittest.cc.tmpl b/src/components/cronet/tools/generators/c_templates/module_impl_struct_unittest.cc.tmpl new file mode 100644 index 0000000000..ef3f8c41a8 --- /dev/null +++ b/src/components/cronet/tools/generators/c_templates/module_impl_struct_unittest.cc.tmpl @@ -0,0 +1,91 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* DO NOT EDIT. Generated from {{module.path}} */ + +{%- if variant -%} +{%- set variant_path = "%s-%s"|format(module.path, variant) -%} +{%- else -%} +{%- set variant_path = module.path -%} +{%- endif %} + +#include "{{variant_path}}_c.h" + +#include + +#include "testing/gtest/include/gtest/gtest.h" + +{% set test_class_name = namespaces_as_array|first|title %} + +class {{test_class_name}}StructTest : public ::testing::Test { + public: + {{test_class_name}}StructTest(const {{test_class_name}}StructTest&) = delete; + {{test_class_name}}StructTest& operator=(const {{test_class_name}}StructTest&) = delete; + + protected: + void SetUp() override { + } + + void TearDown() override { + } + + {{test_class_name}}StructTest() {} + ~{{test_class_name}}StructTest() override {} +}; + +{%- for struct in structs %} +{%- set struct_name = struct|get_name_for_kind %} + +// Test Struct {{struct_name}} setters and getters. +TEST_F({{test_class_name}}StructTest, Test{{struct_name}}) { + {{struct_name}}Ptr first = {{struct_name}}_Create(); + {{struct_name}}Ptr second = {{struct_name}}_Create(); + + // Copy values from |first| to |second|. +{%- for packed_field in struct.packed.packed_fields_in_ordinal_order %} +{%- set kind = packed_field.field.kind %} +{%- if kind|is_array_kind %} + // TODO(mef): Test array |{{packed_field.field.name}}|. +{%- elif kind|is_struct_kind %} + {{kind|cpp_wrapper_type}}Ptr test_{{packed_field.field.name}} = {{kind|cpp_wrapper_type}}_Create(); + EXPECT_EQ({{struct_name}}_{{packed_field.field.name}}_get(first), nullptr); + + {{struct_name}}_{{packed_field.field.name}}_set(first, test_{{packed_field.field.name}}); + EXPECT_NE({{struct_name}}_{{packed_field.field.name}}_get(first), nullptr); + {{struct_name}}_{{packed_field.field.name}}_set(first, nullptr); + EXPECT_EQ({{struct_name}}_{{packed_field.field.name}}_get(first), nullptr); + + {{struct_name}}_{{packed_field.field.name}}_move(first, test_{{packed_field.field.name}}); + EXPECT_NE({{struct_name}}_{{packed_field.field.name}}_get(first), nullptr); + {{struct_name}}_{{packed_field.field.name}}_move(first, nullptr); + EXPECT_EQ({{struct_name}}_{{packed_field.field.name}}_get(first), nullptr); + + {{kind|cpp_wrapper_type}}_Destroy(test_{{packed_field.field.name}}); +{%- else %} + {{struct_name}}_{{packed_field.field.name}}_set(second, + {{struct_name}}_{{packed_field.field.name}}_get(first)); + +{%- if kind|cpp_wrapper_type == 'double' %} + EXPECT_TRUE( + {{struct_name}}_{{packed_field.field.name}}_get(first) == + {{struct_name}}_{{packed_field.field.name}}_get(second) || + isnan({{struct_name}}_{{packed_field.field.name}}_get(first)) && + isnan({{struct_name}}_{{packed_field.field.name}}_get(second))); +{%- else %} +{%- if kind|is_string_kind %} + EXPECT_STREQ( +{%- else %} + EXPECT_EQ( +{%- endif %} + {{struct_name}}_{{packed_field.field.name}}_get(first), + {{struct_name}}_{{packed_field.field.name}}_get(second)); +{%- endif %} +{%- endif %} +{%- endfor %} + {{struct_name}}_Destroy(first); + {{struct_name}}_Destroy(second); +} + +{%- endfor %} + diff --git a/src/components/cronet/tools/generators/c_templates/module_macros.tmpl b/src/components/cronet/tools/generators/c_templates/module_macros.tmpl new file mode 100644 index 0000000000..205f998d04 --- /dev/null +++ b/src/components/cronet/tools/generators/c_templates/module_macros.tmpl @@ -0,0 +1,23 @@ + +{%- macro declare_c_params(prefix, parameters) %} +{%- for param in parameters -%} +{{param.kind|c_wrapper_type}} {{prefix}}{{param.name}} +{%- if not loop.last %}, {% endif %} +{%- endfor %} +{%- endmacro %} + +{%- macro forward_c_params(prefix, parameters) %} +{%- for param in parameters -%} +{{prefix}}{{param.name}} +{%- if not loop.last %}, {% endif %} +{%- endfor %} +{%- endmacro %} + +{%- macro declare_c_return(method) %} +{%- if method.response_parameters and method.sync %} +{%- set return_param = method.response_parameters|first %} +{{return_param.kind|c_wrapper_type}} +{%- else %} +void +{%- endif %} +{%- endmacro %} diff --git a/src/components/cronet/tools/generators/cronet_bindings_generator.py b/src/components/cronet/tools/generators/cronet_bindings_generator.py new file mode 100755 index 0000000000..9e880c5ee1 --- /dev/null +++ b/src/components/cronet/tools/generators/cronet_bindings_generator.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python +# Copyright 2013 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. + +"""The frontend for the Mojo bindings system.""" + + +import argparse +import hashlib +import imp +import json +import os +import re +import struct +import sys + +# Disable lint check for finding modules: +# pylint: disable=F0401 + +def _GetDirAbove(dirname): + """Returns the directory "above" this file containing |dirname| (which must + also be "above" this file).""" + path = os.path.abspath(__file__) + while True: + path, tail = os.path.split(path) + assert tail + if tail == dirname: + return path + +# Manually check for the command-line flag. (This isn't quite right, since it +# ignores, e.g., "--", but it's close enough.) +if "--use_bundled_pylibs" in sys.argv[1:]: + sys.path.insert(0, os.path.join(_GetDirAbove("components"), "third_party")) + +sys.path.insert(0, os.path.join(_GetDirAbove("components"), + "mojo", "public", "tools", "mojom")) + +# pylint: disable=wrong-import-position +from mojom.error import Error +import mojom.fileutil as fileutil +from mojom.generate import translate +from mojom.generate import template_expander +from mojom.generate.generator import AddComputedData +from mojom.parse.parser import Parse +# pylint: enable=wrong-import-position + + +_BUILTIN_GENERATORS = { + "c": "cronet_c_generator.py", +} + + +def LoadGenerators(generators_string): + if not generators_string: + return [] # No generators. + + script_dir = os.path.dirname(os.path.abspath(__file__)) + generators = {} + for generator_name in [s.strip() for s in generators_string.split(",")]: + language = generator_name.lower() + if language in _BUILTIN_GENERATORS: + generator_name = os.path.join(script_dir, + _BUILTIN_GENERATORS[language]) + else: + print("Unknown generator name %s" % generator_name) + sys.exit(1) + generator_module = imp.load_source(os.path.basename(generator_name)[:-3], + generator_name) + generators[language] = generator_module + return generators + + +def MakeImportStackMessage(imported_filename_stack): + """Make a (human-readable) message listing a chain of imports. (Returned + string begins with a newline (if nonempty) and does not end with one.)""" + return ''.join( + reversed(["\n %s was imported by %s" % (a, b) for (a, b) in \ + zip(imported_filename_stack[1:], imported_filename_stack)])) + + +class RelativePath(object): + """Represents a path relative to the source tree.""" + def __init__(self, path, source_root): + self.path = path + self.source_root = source_root + + def relative_path(self): + return os.path.relpath(os.path.abspath(self.path), + os.path.abspath(self.source_root)) + + +def FindImportFile(rel_dir, file_name, search_rel_dirs): + """Finds |file_name| in either |rel_dir| or |search_rel_dirs|. Returns a + RelativePath with first file found, or an arbitrary non-existent file + otherwise.""" + for rel_search_dir in [rel_dir] + search_rel_dirs: + path = os.path.join(rel_search_dir.path, file_name) + if os.path.isfile(path): + return RelativePath(path, rel_search_dir.source_root) + return RelativePath(os.path.join(rel_dir.path, file_name), + rel_dir.source_root) + + +def ScrambleMethodOrdinals(interfaces, salt): + already_generated = set() + for interface in interfaces: + i = 0 + already_generated.clear() + for method in interface.methods: + while True: + i = i + 1 + if i == 1000000: + raise Exception("Could not generate %d method ordinals for %s" % + (len(interface.methods), interface.mojom_name)) + # Generate a scrambled method.ordinal value. The algorithm doesn't have + # to be very strong, cryptographically. It just needs to be non-trivial + # to guess the results without the secret salt, in order to make it + # harder for a compromised process to send fake Mojo messages. + sha256 = hashlib.sha256(salt) + sha256.update(interface.mojom_name) + sha256.update(str(i)) + # Take the first 4 bytes as a little-endian uint32. + ordinal = struct.unpack('= 2: + args.import_directories[idx] = RelativePath(tokens[0], tokens[1]) + else: + args.import_directories[idx] = RelativePath(tokens[0], args.depth) + generator_modules = LoadGenerators(args.generators_string) + + fileutil.EnsureDirectoryExists(args.output_dir) + + processor = MojomProcessor(lambda filename: filename in args.filename) + processor.LoadTypemaps(set(args.typemaps)) + for filename in args.filename: + processor.ProcessFile(args, remaining_args, generator_modules, filename) + if args.depfile: + assert args.depfile_target + with open(args.depfile, 'w') as f: + f.write('%s: %s' % ( + args.depfile_target, + ' '.join(list(processor._parsed_files.keys())))) + + return 0 + + +def _Precompile(args, _): + generator_modules = LoadGenerators(",".join(list(_BUILTIN_GENERATORS.keys()))) + + template_expander.PrecompileTemplates(generator_modules, args.output_dir) + return 0 + + + +def main(): + parser = argparse.ArgumentParser( + description="Generate bindings from mojom files.") + parser.add_argument("--use_bundled_pylibs", action="store_true", + help="use Python modules bundled in the SDK") + + subparsers = parser.add_subparsers() + generate_parser = subparsers.add_parser( + "generate", description="Generate bindings from mojom files.") + generate_parser.add_argument("filename", nargs="+", + help="mojom input file") + generate_parser.add_argument("-d", "--depth", dest="depth", default=".", + help="depth from source root") + generate_parser.add_argument("-o", "--output_dir", dest="output_dir", + default=".", + help="output directory for generated files") + generate_parser.add_argument("-g", "--generators", + dest="generators_string", + metavar="GENERATORS", + default="c++,javascript,java", + help="comma-separated list of generators") + generate_parser.add_argument( + "-I", dest="import_directories", action="append", metavar="directory", + default=[], + help="add a directory to be searched for import files. The depth from " + "source root can be specified for each import by appending it after " + "a colon") + generate_parser.add_argument("--typemap", action="append", metavar="TYPEMAP", + default=[], dest="typemaps", + help="apply TYPEMAP to generated output") + generate_parser.add_argument("--variant", dest="variant", default=None, + help="output a named variant of the bindings") + generate_parser.add_argument( + "--bytecode_path", required=True, help=( + "the path from which to load template bytecode; to generate template " + "bytecode, run %s precompile BYTECODE_PATH" % os.path.basename( + sys.argv[0]))) + generate_parser.add_argument("--for_blink", action="store_true", + help="Use WTF types as generated types for mojo " + "string/array/map.") + generate_parser.add_argument( + "--export_attribute", default="", + help="Optional attribute to specify on class declaration to export it " + "for the component build.") + generate_parser.add_argument( + "--export_header", default="", + help="Optional header to include in the generated headers to support the " + "component build.") + generate_parser.add_argument( + "--generate_non_variant_code", action="store_true", + help="Generate code that is shared by different variants.") + generate_parser.add_argument( + "--depfile", + help="A file into which the list of input files will be written.") + generate_parser.add_argument( + "--depfile_target", + help="The target name to use in the depfile.") + generate_parser.add_argument( + "--scrambled_message_id_salt_path", + dest="scrambled_message_id_salt_paths", + help="If non-empty, the path to a file whose contents should be used as" + "a salt for generating scrambled message IDs. If this switch is specified" + "more than once, the contents of all salt files are concatenated to form" + "the salt value.", default=[], action="append") + generate_parser.add_argument( + "--support_lazy_serialization", + help="If set, generated bindings will serialize lazily when possible.", + action="store_true") + generate_parser.set_defaults(func=_Generate) + + precompile_parser = subparsers.add_parser("precompile", + description="Precompile templates for the mojom bindings generator.") + precompile_parser.add_argument( + "-o", "--output_dir", dest="output_dir", default=".", + help="output directory for precompiled templates") + precompile_parser.set_defaults(func=_Precompile) + + args, remaining_args = parser.parse_known_args() + return args.func(args, remaining_args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/components/cronet/tools/generators/cronet_c_generator.py b/src/components/cronet/tools/generators/cronet_c_generator.py new file mode 100644 index 0000000000..3c80f08b90 --- /dev/null +++ b/src/components/cronet/tools/generators/cronet_c_generator.py @@ -0,0 +1,865 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Generates C source files from an idl (mojom) Module.""" + +import os + +# This is called from cronet_bindings_generator.py which does some magic to add +# libraries to the lookup path which pylint does not +# pylint: disable=import-error +import mojom.generate.generator as generator +import mojom.generate.module as mojom +import mojom.generate.pack as pack +from mojom.generate.template_expander import UseJinja +# pylint: enable=import-error + + +_kind_to_cpp_type = { + mojom.BOOL: "bool", + mojom.INT8: "int8_t", + mojom.UINT8: "uint8_t", + mojom.INT16: "int16_t", + mojom.UINT16: "uint16_t", + mojom.INT32: "int32_t", + mojom.UINT32: "uint32_t", + mojom.FLOAT: "float", + mojom.INT64: "int64_t", + mojom.UINT64: "uint64_t", + mojom.DOUBLE: "double", +} + +_kind_to_cpp_literal_suffix = { + mojom.UINT8: "U", + mojom.UINT16: "U", + mojom.UINT32: "U", + mojom.FLOAT: "f", + mojom.UINT64: "ULL", +} + +ATTRIBUTE_ABSTRACT = "Abstract" + +class _NameFormatter(object): + """A formatter for the names of kinds or values.""" + + def __init__(self, token, variant): + self._token = token + self._variant = variant + + def Format(self, separator, prefixed=False, internal=False, + include_variant=False, omit_namespace_for_module=None, + flatten_nested_kind=False): + """Formats the name according to the given configuration. + + Args: + separator: Separator between different parts of the name. + prefixed: Whether a leading separator should be added. + internal: Returns the name in the "internal" namespace. + include_variant: Whether to include variant as namespace. If |internal| is + True, then this flag is ignored and variant is not included. + omit_namespace_for_module: If the token is from the specified module, + don't add the namespaces of the module to the name. + flatten_nested_kind: It is allowed to define enums inside structs and + interfaces. If this flag is set to True, this method concatenates the + parent kind and the nested kind with '_', instead of treating the + parent kind as a scope.""" + + parts = [] + if self._ShouldIncludeNamespace(omit_namespace_for_module): + parts.extend(self._GetNamespace()) + if include_variant and self._variant and not internal: + parts.append(self._variant) + parts.extend(self._GetName(internal, flatten_nested_kind)) + return separator.join(parts) + + def FormatForCpp(self, omit_namespace_for_module=None, internal=False, + flatten_nested_kind=False): + return self.Format( + "_", prefixed=True, + omit_namespace_for_module=omit_namespace_for_module, + internal=internal, include_variant=True, + flatten_nested_kind=flatten_nested_kind) + + def FormatForMojom(self): + return self.Format(".") + + def _MapKindName(self, token, internal): + if not internal: + return token.name + if (mojom.IsStructKind(token) or mojom.IsUnionKind(token) or + mojom.IsEnumKind(token)): + return token.name + "_Data" + return token.name + + def _GetName(self, internal, flatten_nested_kind): + if isinstance(self._token, mojom.EnumValue): + name_parts = _NameFormatter(self._token.enum, self._variant)._GetName( + internal, flatten_nested_kind) + name_parts.append(self._token.name) + return name_parts + + name_parts = [] + if internal: + name_parts.append("internal") + + if (flatten_nested_kind and mojom.IsEnumKind(self._token) and + self._token.parent_kind): + name = "%s_%s" % (self._token.parent_kind.name, + self._MapKindName(self._token, internal)) + name_parts.append(name) + return name_parts + + if self._token.parent_kind: + name_parts.append(self._MapKindName(self._token.parent_kind, internal)) + name_parts.append(self._MapKindName(self._token, internal)) + return name_parts + + def _ShouldIncludeNamespace(self, omit_namespace_for_module): + return self._token.module + + def _GetNamespace(self): + if self._token.module: + return NamespaceToArray(self._token.module.namespace) + + +def NamespaceToArray(namespace): + return namespace.split(".") if namespace else [] + + +def GetWtfHashFnNameForEnum(enum): + return _NameFormatter(enum, None).Format("_", internal=True, + flatten_nested_kind=True) + "HashFn" + + +def IsNativeOnlyKind(kind): + return (mojom.IsStructKind(kind) or mojom.IsEnumKind(kind)) and \ + kind.native_only + + +def UseCustomSerializer(kind): + return mojom.IsStructKind(kind) and kind.custom_serializer + + +def AllEnumValues(enum): + """Return all enum values associated with an enum. + + Args: + enum: {mojom.Enum} The enum type. + + Returns: + {Set[int]} The values. + """ + return set(field.numeric_value for field in enum.fields) + + +def GetCppPodType(kind): + return _kind_to_cpp_type[kind] + + +def RequiresContextForDataView(kind): + for field in kind.fields: + if mojom.IsReferenceKind(field.kind): + return True + return False + + +def ShouldInlineStruct(struct): + # TODO(darin): Base this on the size of the wrapper class. + if len(struct.fields) > 4: + return False + for field in struct.fields: + if mojom.IsReferenceKind(field.kind) and not mojom.IsStringKind(field.kind): + return False + return True + + +def ShouldInlineUnion(union): + return not any( + mojom.IsReferenceKind(field.kind) and not mojom.IsStringKind(field.kind) + for field in union.fields) + +def IsAbstract(kind): + return kind.attributes.get(ATTRIBUTE_ABSTRACT, False) \ + if kind.attributes else False + +class StructConstructor(object): + """Represents a constructor for a generated struct. + + Fields: + fields: {[Field]} All struct fields in order. + params: {[Field]} The fields that are passed as params. + """ + + def __init__(self, fields, params): + self._fields = fields + self._params = set(params) + + @property + def params(self): + return [field for field in self._fields if field in self._params] + + @property + def fields(self): + for field in self._fields: + yield (field, field in self._params) + + +class Generator(generator.Generator): + def __init__(self, *args, **kwargs): + super(Generator, self).__init__(*args, **kwargs) + + def _GetExtraTraitsHeaders(self): + extra_headers = set() + for typemap in self._GetAllUsedTypemaps(): + extra_headers.update(typemap.get("traits_headers", [])) + return sorted(extra_headers) + + def _GetAllUsedTypemaps(self): + """Returns the typemaps for types needed for serialization in this module. + + A type is needed for serialization if it is contained by a struct or union + defined in this module, is a parameter of a message in an interface in + this module or is contained within another type needed for serialization. + """ + used_typemaps = [] + seen_types = set() + def IsBasicKind(kind): + return (mojom.IsIntegralKind(kind) or mojom.IsStringKind(kind) or + mojom.IsDoubleKind(kind) or mojom.IsFloatKind(kind) or + mojom.IsAnyHandleKind(kind) or + mojom.IsInterfaceKind(kind) or + mojom.IsInterfaceRequestKind(kind) or + mojom.IsAssociatedKind(kind)) + + def AddKind(kind): + if IsBasicKind(kind): + pass + elif mojom.IsArrayKind(kind): + AddKind(kind.kind) + elif mojom.IsMapKind(kind): + AddKind(kind.key_kind) + AddKind(kind.value_kind) + else: + name = self._GetFullMojomNameForKind(kind) + if name in seen_types: + return + seen_types.add(name) + + typemap = self.typemap.get(name, None) + if typemap: + used_typemaps.append(typemap) + if mojom.IsStructKind(kind) or mojom.IsUnionKind(kind): + for field in kind.fields: + AddKind(field.kind) + + for kind in self.module.structs + self.module.unions: + for field in kind.fields: + AddKind(field.kind) + + for interface in self.module.interfaces: + for method in interface.methods: + for parameter in method.parameters + (method.response_parameters or []): + AddKind(parameter.kind) + + return used_typemaps + + def _GetExtraPublicHeaders(self): + all_enums = list(self.module.enums) + for struct in self.module.structs: + all_enums.extend(struct.enums) + for interface in self.module.interfaces: + all_enums.extend(interface.enums) + + types = set(self._GetFullMojomNameForKind(typename) + for typename in + self.module.structs + all_enums + self.module.unions) + headers = set() + for typename, typemap in self.typemap.items(): + if typename in types: + headers.update(typemap.get("public_headers", [])) + return sorted(headers) + + def _GetDirectlyUsedKinds(self): + for struct in self.module.structs + self.module.unions: + for field in struct.fields: + yield field.kind + + for interface in self.module.interfaces: + for method in interface.methods: + for param in method.parameters + (method.response_parameters or []): + yield param.kind + + def _GetJinjaExports(self): + all_enums = list(self.module.enums) + for struct in self.module.structs: + all_enums.extend(struct.enums) + for interface in self.module.interfaces: + all_enums.extend(interface.enums) + + return { + "all_enums": all_enums, + "enums": self.module.enums, + "export_attribute": self.export_attribute, + "export_header": self.export_header, + "extra_public_headers": self._GetExtraPublicHeaders(), + "extra_traits_headers": self._GetExtraTraitsHeaders(), + "for_blink": self.for_blink, + "imports": self.module.imports, + "interfaces": self.module.interfaces, + "kinds": self.module.kinds, + "module": self.module, + "namespace": self.module.namespace, + "namespaces_as_array": NamespaceToArray(self.module.namespace), + "structs": self.module.structs, + "support_lazy_serialization": self.support_lazy_serialization, + "unions": self.module.unions, + "variant": self.variant, + } + + @staticmethod + def GetTemplatePrefix(): + return "c_templates" + + def GetFilters(self): + cpp_filters = { + "all_enum_values": AllEnumValues, + "c_wrapper_type": self._GetCWrapperType, + "constant_value": self._ConstantValue, + "contains_handles_or_interfaces": mojom.ContainsHandlesOrInterfaces, + "contains_move_only_members": self._ContainsMoveOnlyMembers, + "cpp_wrapper_param_type": self._GetCppWrapperParamType, + "cpp_data_view_type": self._GetCppDataViewType, + "cpp_field_type": self._GetCppFieldType, + "cpp_union_field_type": self._GetCppUnionFieldType, + "cpp_pod_type": GetCppPodType, + "cpp_union_getter_return_type": self._GetUnionGetterReturnType, + "cpp_union_trait_getter_return_type": self._GetUnionTraitGetterReturnType, + "cpp_wrapper_type": self._GetCppWrapperType, + "default_value": self._DefaultValue, + "expression_to_text": self._ExpressionToText, + "format_constant_declaration": self._FormatConstantDeclaration, + "get_container_validate_params_ctor_args": + self._GetContainerValidateParamsCtorArgs, + "get_name_for_kind": self._GetNameForKind, + "get_pad": pack.GetPad, + "get_qualified_name_for_kind": self._GetQualifiedNameForKind, + "has_callbacks": mojom.HasCallbacks, + "has_sync_methods": mojom.HasSyncMethods, + "method_supports_lazy_serialization": + self._MethodSupportsLazySerialization, + "requires_context_for_data_view": RequiresContextForDataView, + "should_inline": ShouldInlineStruct, + "should_inline_union": ShouldInlineUnion, + "is_abstract": IsAbstract, + "is_array_kind": mojom.IsArrayKind, + "is_enum_kind": mojom.IsEnumKind, + "is_integral_kind": mojom.IsIntegralKind, + "is_native_only_kind": IsNativeOnlyKind, + "is_any_handle_kind": mojom.IsAnyHandleKind, + "is_any_interface_kind": mojom.IsAnyInterfaceKind, + "is_any_handle_or_interface_kind": mojom.IsAnyHandleOrInterfaceKind, + "is_associated_kind": mojom.IsAssociatedKind, + "is_hashable": self._IsHashableKind, + "is_map_kind": mojom.IsMapKind, + "is_nullable_kind": mojom.IsNullableKind, + "is_object_kind": mojom.IsObjectKind, + "is_reference_kind": mojom.IsReferenceKind, + "is_string_kind": mojom.IsStringKind, + "is_struct_kind": mojom.IsStructKind, + "is_typemapped_kind": self._IsTypemappedKind, + "is_union_kind": mojom.IsUnionKind, + "passes_associated_kinds": mojom.PassesAssociatedKinds, + "struct_constructors": self._GetStructConstructors, + "under_to_camel": generator.ToCamel, + "unmapped_type_for_serializer": self._GetUnmappedTypeForSerializer, + "wtf_hash_fn_name_for_enum": GetWtfHashFnNameForEnum, + } + return cpp_filters + + @UseJinja("module_c.h.tmpl") + def _GenerateModuleHeader(self): + return self._GetJinjaExports() + + @UseJinja("module_impl_interface.h.tmpl") + def _GenerateModuleInterfaceHeader(self): + return self._GetJinjaExports() + + @UseJinja("module_impl_interface.cc.tmpl") + def _GenerateModuleInterfaceSource(self): + return self._GetJinjaExports() + + @UseJinja("module_impl_interface_unittest.cc.tmpl") + def _GenerateModuleInterfaceUnittest(self): + return self._GetJinjaExports() + + @UseJinja("module_impl_struct.h.tmpl") + def _GenerateModuleStructHeader(self): + return self._GetJinjaExports() + + @UseJinja("module_impl_struct.cc.tmpl") + def _GenerateModuleStructSource(self): + return self._GetJinjaExports() + + @UseJinja("module_impl_struct_unittest.cc.tmpl") + def _GenerateModuleStructUnittest(self): + return self._GetJinjaExports() + + @UseJinja("module.cc.tmpl") + def _GenerateModuleSource(self): + return self._GetJinjaExports() + + @UseJinja("module-shared.h.tmpl") + def _GenerateModuleSharedHeader(self): + return self._GetJinjaExports() + + @UseJinja("module-shared-internal.h.tmpl") + def _GenerateModuleSharedInternalHeader(self): + return self._GetJinjaExports() + + @UseJinja("module-shared.cc.tmpl") + def _GenerateModuleSharedSource(self): + return self._GetJinjaExports() + + def GenerateFiles(self, args): + self.module.Stylize(generator.Stylizer()) + # TODO(mef): Remove this when generated files are not checked in. + path, module = os.path.split(self.module.path) + self.module.path = os.path.join(path, "generated", module) + + if self.generate_non_variant_code: + self.Write(self._GenerateModuleSharedHeader(), + "%s-shared.h" % self.module.path) + self.Write(self._GenerateModuleSharedInternalHeader(), + "%s-shared-internal.h" % self.module.path) + self.Write(self._GenerateModuleSharedSource(), + "%s-shared.cc" % self.module.path) + else: + suffix = "-%s" % self.variant if self.variant else "" + self.Write(self._GenerateModuleHeader(), + "%s%s_c.h" % (self.module.path, suffix)) + self.Write(self._GenerateModuleInterfaceHeader(), + "%s%s_impl_interface.h" % (self.module.path, suffix)) + self.Write(self._GenerateModuleInterfaceSource(), + "%s%s_impl_interface.cc" % (self.module.path, suffix)) + self.Write(self._GenerateModuleInterfaceUnittest(), + "%s%s_impl_interface_unittest.cc" % (self.module.path, suffix)) + self.Write(self._GenerateModuleStructHeader(), + "%s%s_impl_struct.h" % (self.module.path, suffix)) + self.Write(self._GenerateModuleStructSource(), + "%s%s_impl_struct.cc" % (self.module.path, suffix)) + self.Write(self._GenerateModuleStructUnittest(), + "%s%s_impl_struct_unittest.cc" % (self.module.path, suffix)) + + def _ConstantValue(self, constant): + return self._ExpressionToText(constant.value, kind=constant.kind) + + def _DefaultValue(self, field): + if not field.default: + return "" + + if mojom.IsStructKind(field.kind): + assert field.default == "default" + if self._IsTypemappedKind(field.kind): + return "" + return "%s::New()" % self._GetNameForKind(field.kind) + + expression = self._ExpressionToText(field.default, kind=field.kind) + if mojom.IsEnumKind(field.kind) and self._IsTypemappedKind(field.kind): + expression = "mojo::internal::ConvertEnumValue<%s, %s>(%s)" % ( + self._GetNameForKind(field.kind), self._GetCppWrapperType(field.kind), + expression) + return expression + + def _GetNameForKind(self, kind, internal=False, flatten_nested_kind=False, + add_same_module_namespaces=False): + return _NameFormatter(kind, self.variant).FormatForCpp( + internal=internal, flatten_nested_kind=flatten_nested_kind, + omit_namespace_for_module = (None if add_same_module_namespaces + else self.module)) + + def _GetQualifiedNameForKind(self, kind, internal=False, + flatten_nested_kind=False, include_variant=True): + return _NameFormatter( + kind, self.variant if include_variant else None).FormatForCpp( + internal=internal, flatten_nested_kind=flatten_nested_kind) + + def _GetFullMojomNameForKind(self, kind): + return _NameFormatter(kind, self.variant).FormatForMojom() + + def _IsTypemappedKind(self, kind): + return hasattr(kind, "name") and \ + self._GetFullMojomNameForKind(kind) in self.typemap + + def _IsHashableKind(self, kind): + """Check if the kind can be hashed. + + Args: + kind: {Kind} The kind to check. + + Returns: + {bool} True if a value of this kind can be hashed. + """ + checked = set() + def Check(kind): + if kind.spec in checked: + return True + checked.add(kind.spec) + if mojom.IsNullableKind(kind): + return False + elif mojom.IsStructKind(kind): + if kind.native_only: + return False + if (self._IsTypemappedKind(kind) and + not self.typemap[self._GetFullMojomNameForKind(kind)]["hashable"]): + return False + return all(Check(field.kind) for field in kind.fields) + elif mojom.IsEnumKind(kind): + return not self._IsTypemappedKind(kind) or self.typemap[ + self._GetFullMojomNameForKind(kind)]["hashable"] + elif mojom.IsUnionKind(kind): + return all(Check(field.kind) for field in kind.fields) + elif mojom.IsAnyHandleKind(kind): + return False + elif mojom.IsAnyInterfaceKind(kind): + return False + # TODO(crbug.com/735301): Arrays and maps could be made hashable. We just + # don't have a use case yet. + elif mojom.IsArrayKind(kind): + return False + elif mojom.IsMapKind(kind): + return False + else: + return True + return Check(kind) + + def _GetNativeTypeName(self, typemapped_kind): + return self.typemap[self._GetFullMojomNameForKind(typemapped_kind)][ + "typename"] + + def _FormatConstantDeclaration(self, constant, nested=False): + if mojom.IsStringKind(constant.kind): + if nested: + return "const char %s[]" % constant.name + return "%sextern const char %s[]" % \ + ((self.export_attribute + " ") if self.export_attribute else "", + constant.name) + return "const %s %s_%s = %s" % ( + GetCppPodType(constant.kind), self.module.namespace, constant.name, + self._ConstantValue(constant)) + + def _GetCppWrapperType(self, kind, add_same_module_namespaces=False): + def _AddOptional(type_name): + return "absl::optional<%s>" % type_name + + if self._IsTypemappedKind(kind): + type_name = self._GetNativeTypeName(kind) + if (mojom.IsNullableKind(kind) and + not self.typemap[self._GetFullMojomNameForKind(kind)][ + "nullable_is_same_type"]): + type_name = _AddOptional(type_name) + return type_name + if mojom.IsEnumKind(kind): + return self._GetNameForKind( + kind, add_same_module_namespaces=add_same_module_namespaces) + if mojom.IsStructKind(kind) or mojom.IsUnionKind(kind): + return "%s" % self._GetNameForKind( + kind, add_same_module_namespaces=add_same_module_namespaces) + if mojom.IsArrayKind(kind): + pattern = "WTF::Vector<%s>" if self.for_blink else "std::vector<%s>" + if mojom.IsNullableKind(kind): + pattern = _AddOptional(pattern) + return pattern % self._GetCppWrapperType( + kind.kind, add_same_module_namespaces=add_same_module_namespaces) + if mojom.IsMapKind(kind): + pattern = ("WTF::HashMap<%s, %s>" if self.for_blink else + "std::unordered_map<%s, %s>") + if mojom.IsNullableKind(kind): + pattern = _AddOptional(pattern) + return pattern % ( + self._GetCppWrapperType( + kind.key_kind, + add_same_module_namespaces=add_same_module_namespaces), + self._GetCppWrapperType( + kind.value_kind, + add_same_module_namespaces=add_same_module_namespaces)) + if mojom.IsInterfaceKind(kind): + return "%sPtr" % self._GetNameForKind( + kind, add_same_module_namespaces=add_same_module_namespaces) + if mojom.IsInterfaceRequestKind(kind): + return "%sRequest" % self._GetNameForKind( + kind.kind, add_same_module_namespaces=add_same_module_namespaces) + if mojom.IsAssociatedInterfaceKind(kind): + return "%sAssociatedPtrInfo" % self._GetNameForKind( + kind.kind, add_same_module_namespaces=add_same_module_namespaces) + if mojom.IsAssociatedInterfaceRequestKind(kind): + return "%sAssociatedRequest" % self._GetNameForKind( + kind.kind, add_same_module_namespaces=add_same_module_namespaces) + if mojom.IsStringKind(kind): + if self.for_blink: + return "WTF::String" + type_name = "std::string" + return (_AddOptional(type_name) if mojom.IsNullableKind(kind) + else type_name) + if mojom.IsGenericHandleKind(kind): + return "Cronet_RawDataPtr" + if mojom.IsDataPipeConsumerKind(kind): + return "mojo::ScopedDataPipeConsumerHandle" + if mojom.IsDataPipeProducerKind(kind): + return "mojo::ScopedDataPipeProducerHandle" + if mojom.IsMessagePipeKind(kind): + return "mojo::ScopedMessagePipeHandle" + if mojom.IsSharedBufferKind(kind): + return "mojo::ScopedSharedBufferHandle" + if not kind in _kind_to_cpp_type: + raise Exception("Unrecognized kind %s" % kind.spec) + return _kind_to_cpp_type[kind] + + def _IsMoveOnlyKind(self, kind): + if self._IsTypemappedKind(kind): + if mojom.IsEnumKind(kind): + return False + return self.typemap[self._GetFullMojomNameForKind(kind)]["move_only"] + if mojom.IsStructKind(kind) or mojom.IsUnionKind(kind): + return True + if mojom.IsArrayKind(kind): + return self._IsMoveOnlyKind(kind.kind) + if mojom.IsMapKind(kind): + return (self._IsMoveOnlyKind(kind.value_kind) or + self._IsMoveOnlyKind(kind.key_kind)) + if mojom.IsAnyHandleOrInterfaceKind(kind): + return True + return False + + def _IsCopyablePassByValue(self, kind): + if not self._IsTypemappedKind(kind): + return False + return self.typemap[self._GetFullMojomNameForKind(kind)][ + "copyable_pass_by_value"] + + def _ShouldPassParamByValue(self, kind): + return ((not mojom.IsReferenceKind(kind)) or self._IsMoveOnlyKind(kind) or + self._IsCopyablePassByValue(kind)) + + def _GetCWrapperType(self, kind): + if mojom.IsStringKind(kind): + return "Cronet_String" + if mojom.IsStructKind(kind) or mojom.IsUnionKind(kind): + return "%sPtr" % self._GetNameForKind(kind) + return self._GetCppWrapperType(kind) + + def _GetCppWrapperParamType(self, kind): + cpp_wrapper_type = self._GetCppWrapperType(kind) + return (cpp_wrapper_type if self._ShouldPassParamByValue(kind) + else "const %s&" % cpp_wrapper_type) + + def _GetCppFieldType(self, kind): + if mojom.IsStructKind(kind): + return ("mojo::internal::Pointer<%s>" % + self._GetNameForKind(kind, internal=True)) + if mojom.IsUnionKind(kind): + return "%s" % self._GetNameForKind(kind, internal=True) + if mojom.IsArrayKind(kind): + return ("mojo::internal::Pointer>" % + self._GetCppFieldType(kind.kind)) + if mojom.IsMapKind(kind): + return ("mojo::internal::Pointer>" % + (self._GetCppFieldType(kind.key_kind), + self._GetCppFieldType(kind.value_kind))) + if mojom.IsInterfaceKind(kind): + return "mojo::internal::Interface_Data" + if mojom.IsInterfaceRequestKind(kind): + return "mojo::internal::Handle_Data" + if mojom.IsAssociatedInterfaceKind(kind): + return "mojo::internal::AssociatedInterface_Data" + if mojom.IsAssociatedInterfaceRequestKind(kind): + return "mojo::internal::AssociatedEndpointHandle_Data" + if mojom.IsEnumKind(kind): + return "int32_t" + if mojom.IsStringKind(kind): + return "mojo::internal::Pointer" + if mojom.IsAnyHandleKind(kind): + return "mojo::internal::Handle_Data" + return _kind_to_cpp_type[kind] + + def _GetCppUnionFieldType(self, kind): + if mojom.IsUnionKind(kind): + return ("mojo::internal::Pointer<%s>" % + self._GetNameForKind(kind, internal=True)) + return self._GetCppFieldType(kind) + + def _GetUnionGetterReturnType(self, kind): + if mojom.IsReferenceKind(kind): + return "%s&" % self._GetCppWrapperType(kind) + return self._GetCppWrapperType(kind) + + def _GetUnionTraitGetterReturnType(self, kind): + """Get field type used in UnionTraits template specialization. + + The type may be qualified as UnionTraits specializations live outside the + namespace where e.g. structs are defined. + + Args: + kind: {Kind} The type of the field. + + Returns: + {str} The C++ type to use for the field. + """ + if mojom.IsReferenceKind(kind): + return "%s&" % self._GetCppWrapperType(kind, + add_same_module_namespaces=True) + return self._GetCppWrapperType(kind, add_same_module_namespaces=True) + + def _MethodSupportsLazySerialization(self, method): + # TODO(crbug.com/753431,crbug.com/753433): Support lazy serialization for + # methods which pass associated handles and InterfacePtrs. + return self.support_lazy_serialization and ( + not mojom.MethodPassesAssociatedKinds(method) and + not mojom.MethodPassesInterfaces(method)) + + def _TranslateConstants(self, token, kind): + if isinstance(token, mojom.NamedValue): + return self._GetNameForKind(token, flatten_nested_kind=True) + + if isinstance(token, mojom.BuiltinValue): + if token.value == "double.INFINITY": + return "std::numeric_limits::infinity()" + if token.value == "float.INFINITY": + return "std::numeric_limits::infinity()" + if token.value == "double.NEGATIVE_INFINITY": + return "-std::numeric_limits::infinity()" + if token.value == "float.NEGATIVE_INFINITY": + return "-std::numeric_limits::infinity()" + if token.value == "double.NAN": + return "std::numeric_limits::quiet_NaN()" + if token.value == "float.NAN": + return "std::numeric_limits::quiet_NaN()" + + if (kind is not None and mojom.IsFloatKind(kind)): + return token if token.isdigit() else token + "f"; + + return "%s%s" % (token, _kind_to_cpp_literal_suffix.get(kind, "")) + + def _ExpressionToText(self, value, kind=None): + return self._TranslateConstants(value, kind) + + def _ContainsMoveOnlyMembers(self, struct): + for field in struct.fields: + if self._IsMoveOnlyKind(field.kind): + return True + return False + + def _GetStructConstructors(self, struct): + """Returns a list of constructors for a struct. + + Params: + struct: {Struct} The struct to return constructors for. + + Returns: + {[StructConstructor]} A list of StructConstructors that should be + generated for |struct|. + """ + if not mojom.IsStructKind(struct): + raise TypeError + # Types that are neither copyable nor movable can't be passed to a struct + # constructor so only generate a default constructor. + if any(self._IsTypemappedKind(field.kind) and self.typemap[ + self._GetFullMojomNameForKind(field.kind)]["non_copyable_non_movable"] + for field in struct.fields): + return [StructConstructor(struct.fields, [])] + + param_counts = [0] + for version in struct.versions: + if param_counts[-1] != version.num_fields: + param_counts.append(version.num_fields) + + ordinal_fields = sorted(struct.fields, key=lambda field: field.ordinal) + return (StructConstructor(struct.fields, ordinal_fields[:param_count]) + for param_count in param_counts) + + def _GetContainerValidateParamsCtorArgs(self, kind): + if mojom.IsStringKind(kind): + expected_num_elements = 0 + element_is_nullable = False + key_validate_params = "nullptr" + element_validate_params = "nullptr" + enum_validate_func = "nullptr" + elif mojom.IsMapKind(kind): + expected_num_elements = 0 + element_is_nullable = False + key_validate_params = self._GetNewContainerValidateParams(mojom.Array( + kind=kind.key_kind)) + element_validate_params = self._GetNewContainerValidateParams(mojom.Array( + kind=kind.value_kind)) + enum_validate_func = "nullptr" + else: # mojom.IsArrayKind(kind) + expected_num_elements = generator.ExpectedArraySize(kind) or 0 + element_is_nullable = mojom.IsNullableKind(kind.kind) + key_validate_params = "nullptr" + element_validate_params = self._GetNewContainerValidateParams(kind.kind) + if mojom.IsEnumKind(kind.kind): + enum_validate_func = ("%s::Validate" % + self._GetQualifiedNameForKind(kind.kind, internal=True, + flatten_nested_kind=True)) + else: + enum_validate_func = "nullptr" + + if enum_validate_func == "nullptr": + if key_validate_params == "nullptr": + return "%d, %s, %s" % (expected_num_elements, + "true" if element_is_nullable else "false", + element_validate_params) + else: + return "%s, %s" % (key_validate_params, element_validate_params) + else: + return "%d, %s" % (expected_num_elements, enum_validate_func) + + def _GetNewContainerValidateParams(self, kind): + if (not mojom.IsArrayKind(kind) and not mojom.IsMapKind(kind) and + not mojom.IsStringKind(kind)): + return "nullptr" + + return "new mojo::internal::ContainerValidateParams(%s)" % ( + self._GetContainerValidateParamsCtorArgs(kind)) + + def _GetCppDataViewType(self, kind, qualified=False): + def _GetName(input_kind): + return _NameFormatter(input_kind, None).FormatForCpp( + omit_namespace_for_module=(None if qualified else self.module), + flatten_nested_kind=True) + + if mojom.IsEnumKind(kind): + return _GetName(kind) + if mojom.IsStructKind(kind) or mojom.IsUnionKind(kind): + return "%sDataView" % _GetName(kind) + if mojom.IsArrayKind(kind): + return "mojo::ArrayDataView<%s>" % ( + self._GetCppDataViewType(kind.kind, qualified)) + if mojom.IsMapKind(kind): + return ("mojo::MapDataView<%s, %s>" % ( + self._GetCppDataViewType(kind.key_kind, qualified), + self._GetCppDataViewType(kind.value_kind, qualified))) + if mojom.IsStringKind(kind): + return "mojo::StringDataView" + if mojom.IsInterfaceKind(kind): + return "%sPtrDataView" % _GetName(kind) + if mojom.IsInterfaceRequestKind(kind): + return "%sRequestDataView" % _GetName(kind.kind) + if mojom.IsAssociatedInterfaceKind(kind): + return "%sAssociatedPtrInfoDataView" % _GetName(kind.kind) + if mojom.IsAssociatedInterfaceRequestKind(kind): + return "%sAssociatedRequestDataView" % _GetName(kind.kind) + if mojom.IsGenericHandleKind(kind): + return "mojo::ScopedHandle" + if mojom.IsDataPipeConsumerKind(kind): + return "mojo::ScopedDataPipeConsumerHandle" + if mojom.IsDataPipeProducerKind(kind): + return "mojo::ScopedDataPipeProducerHandle" + if mojom.IsMessagePipeKind(kind): + return "mojo::ScopedMessagePipeHandle" + if mojom.IsSharedBufferKind(kind): + return "mojo::ScopedSharedBufferHandle" + return _kind_to_cpp_type[kind] + + def _GetUnmappedTypeForSerializer(self, kind): + return self._GetCppDataViewType(kind, qualified=True) diff --git a/src/components/cronet/tools/hide_symbols.py b/src/components/cronet/tools/hide_symbols.py new file mode 100755 index 0000000000..8bc50c3822 --- /dev/null +++ b/src/components/cronet/tools/hide_symbols.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python + +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Create a static library which exposes only symbols which are explicitly marked +# as visible e.g., by __attribute__((visibility("default"))). +# +# See BUILD.gn in this directory for usage example. +# +# This way, we can reduce risk of symbol conflict when linking it into apps +# by exposing internal symbols, especially in third-party libraries. + + + +import glob +import optparse +import os +import subprocess +import sys + + +# Mapping from GN's target_cpu attribute to ld's -arch parameter. +# Taken from the definition of config("compiler") in: +# //build/config/mac/BUILD.gn +GN_CPU_TO_LD_ARCH = { + 'x64': 'x86_64', + 'x86': 'i386', + 'armv7': 'armv7', + 'arm': 'armv7', + 'arm64': 'arm64', +} + + +def main(): + parser = optparse.OptionParser() + parser.add_option( + '--input_libs', + help='Comma-separated paths to input .a files which contain symbols ' + 'which must be always linked.') + parser.add_option( + '--deps_lib', + help='The path to a complete static library (.a file) which contains all ' + 'dependencies of --input_libs. .o files in this library are also ' + 'added to the output library, but only if they are referred from ' + '--input_libs.') + parser.add_option( + '--output_obj', + help='Outputs the generated .o file here. This is an intermediate file.') + parser.add_option( + '--output_lib', + help='Outputs the generated .a file here.') + parser.add_option( + '--current_cpu', + help='The current processor architecture in the format of the target_cpu ' + 'attribute in GN.') + parser.add_option( + '--use_custom_libcxx', default=False, action='store_true', + help='Confirm there is a custom libc++ linked in.') + (options, args) = parser.parse_args() + assert not args + + developer_dir = subprocess.check_output( + ['xcode-select', '--print-path'], universal_newlines=True).strip() + + xctoolchain_libs = glob.glob(developer_dir + + '/Toolchains/XcodeDefault.xctoolchain/usr/lib' + + '/clang/*/lib/darwin/*.ios.a') + print("Adding xctoolchain_libs: ", xctoolchain_libs) + + # ld -r concatenates multiple .o files and .a files into a single .o file, + # while "hiding" symbols not marked as visible. + command = [ + 'xcrun', 'ld', + '-arch', GN_CPU_TO_LD_ARCH[options.current_cpu], + '-r', + ] + for input_lib in options.input_libs.split(','): + # By default, ld only pulls .o files out of a static library if needed to + # resolve some symbol reference. We apply -force_load option to input_lib + # (but not to deps_lib) to force pulling all .o files. + command += ['-force_load', input_lib] + command += xctoolchain_libs + command += [ + options.deps_lib, + '-o', options.output_obj + ] + try: + subprocess.check_call(command) + except subprocess.CalledProcessError: + # Work around LD failure for x86 Debug buiilds when it fails with error: + # ld: scattered reloc r_address too large for architecture i386 + if options.current_cpu == "x86": + # Combmine input lib with dependencies into output lib. + command = [ + 'xcrun', 'libtool', '-static', '-no_warning_for_no_symbols', + '-o', options.output_lib, + options.input_libs, options.deps_lib, + ] + subprocess.check_call(command) + # Strip debug info from output lib so its size doesn't exceed 512mb. + command = [ + 'xcrun', 'strip', '-S', options.output_lib, + ] + subprocess.check_call(command) + return + else: + exit(1) + + if os.path.exists(options.output_lib): + os.remove(options.output_lib) + + # Creates a .a file which contains a single .o file. + command = [ + 'xcrun', 'ar', '-r', + options.output_lib, + options.output_obj, + ] + + # When compiling for 64bit targets, the symbols in call_with_eh_frame.o are + # referenced in assembly and eventually stripped by the call to ld -r above, + # perhaps because the linker incorrectly assumes that those symbols are not + # used. Using -keep_private_externs fixes the compile issue, but breaks + # other parts of cronet. Instead, simply add a second .o file with the + # personality routine. Note that this issue was not caught by Chrome tests, + # it was only detected when apps tried to link the resulting .a file. + if options.current_cpu == 'x64' or options.current_cpu == 'arm64': + command += [ 'obj/base/base/call_with_eh_frame.o' ] + + subprocess.check_call(command) + + if options.use_custom_libcxx: + ret = os.system('xcrun nm -u "' + options.output_obj + + '" | grep ___cxa_pure_virtual') + if ret == 0: + print("ERROR: Found undefined libc++ symbols, " + "is libc++ included in dependencies?") + sys.exit(2) + + +if __name__ == "__main__": + main() diff --git a/src/components/cronet/tools/jar_src.py b/src/components/cronet/tools/jar_src.py new file mode 100755 index 0000000000..67c7ddcc57 --- /dev/null +++ b/src/components/cronet/tools/jar_src.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# +# Copyright 2014 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. + +import argparse +import os +import sys +import zipfile + +REPOSITORY_ROOT = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)) + +sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'build/android/gyp')) +from util import build_utils # pylint: disable=wrong-import-position + +JAVA_PACKAGE_PREFIX = 'org/chromium/' + + +def main(): + parser = argparse.ArgumentParser() + build_utils.AddDepfileOption(parser) + parser.add_argument( + '--excluded-classes', + help='A list of .class file patterns to exclude from the jar.') + parser.add_argument( + '--src-search-dirs', + action='append', + help='A list of directories that should be searched' + ' for the source files.') + parser.add_argument( + '--src-files', action='append', help='A list of source files to jar.') + parser.add_argument( + '--src-jars', + action='append', + help='A list of source jars to include in addition to source files.') + parser.add_argument( + '--src-list-files', + action='append', + help='A list of files that contain a list of sources,' + ' e.g. a list of \'.sources\' files generated by GN.') + parser.add_argument('--jar-path', help='Jar output path.', required=True) + + options = parser.parse_args() + + src_jars = [] + for gn_list in options.src_jars: + src_jars.extend(build_utils.ParseGnList(gn_list)) + + src_search_dirs = [] + for gn_src_search_dirs in options.src_search_dirs: + src_search_dirs.extend(build_utils.ParseGnList(gn_src_search_dirs)) + + src_list_files = [] + if options.src_list_files: + for gn_src_list_file in options.src_list_files: + src_list_files.extend(build_utils.ParseGnList(gn_src_list_file)) + + src_files = [] + for gn_src_files in options.src_files: + src_files.extend(build_utils.ParseGnList(gn_src_files)) + + # Add files from --source_list_files + for src_list_file in src_list_files: + with open(src_list_file, 'r') as f: + src_files.extend(f.read().splitlines()) + + # Preprocess source files by removing any prefix that comes before + # the Java package name. + for i, s in enumerate(src_files): + prefix_position = s.find(JAVA_PACKAGE_PREFIX) + if prefix_position != -1: + src_files[i] = s[prefix_position:] + + excluded_classes = [] + if options.excluded_classes: + classes = build_utils.ParseGnList(options.excluded_classes) + excluded_classes.extend(f.replace('.class', '.java') for f in classes) + + predicate = None + if excluded_classes: + predicate = lambda f: not build_utils.MatchesGlob(f, excluded_classes) + + # Create a dictionary that maps every source directory + # to source files that it contains. + dir_to_files_map = {} + # Initialize the map. + for src_search_dir in src_search_dirs: + dir_to_files_map[src_search_dir] = [] + # Fill the map. + for src_file in src_files: + number_of_file_instances = 0 + for src_search_dir in src_search_dirs: + target_path = os.path.join(src_search_dir, src_file) + if os.path.isfile(target_path): + number_of_file_instances += 1 + if not predicate or predicate(src_file): + dir_to_files_map[src_search_dir].append(target_path) + if (number_of_file_instances > 1): + raise Exception( + 'There is more than one instance of file %s in %s' + % (src_file, src_search_dirs)) + if (number_of_file_instances < 1): + raise Exception( + 'Unable to find file %s in %s' % (src_file, src_search_dirs)) + + # Jar the sources from every source search directory. + with build_utils.AtomicOutput(options.jar_path) as o, \ + zipfile.ZipFile(o, 'w', zipfile.ZIP_DEFLATED) as z: + for src_search_dir in src_search_dirs: + subpaths = dir_to_files_map[src_search_dir] + if subpaths: + build_utils.DoZip(subpaths, z, base_dir=src_search_dir) + else: + raise Exception( + 'Directory %s does not contain any files and can be' + ' removed from the list of directories to search' % src_search_dir) + + # Jar additional src jars + if src_jars: + build_utils.MergeZips(z, src_jars, compress=True) + + if options.depfile: + deps = [] + for sources in dir_to_files_map.values(): + deps.extend(sources) + # Srcjar deps already captured in GN rules (no need to list them here). + build_utils.WriteDepfile(options.depfile, options.jar_path, deps) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/components/cronet/tools/jar_src.pydeps b/src/components/cronet/tools/jar_src.pydeps new file mode 100644 index 0000000000..cab77c0304 --- /dev/null +++ b/src/components/cronet/tools/jar_src.pydeps @@ -0,0 +1,6 @@ +# Generated by running: +# build/print_python_deps.py --root components/cronet/tools --output components/cronet/tools/jar_src.pydeps components/cronet/tools/jar_src.py +../../../build/android/gyp/util/__init__.py +../../../build/android/gyp/util/build_utils.py +../../../build/gn_helpers.py +jar_src.py diff --git a/src/components/cronet/tools/link_dependencies.py b/src/components/cronet/tools/link_dependencies.py new file mode 100755 index 0000000000..bd25c0c424 --- /dev/null +++ b/src/components/cronet/tools/link_dependencies.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# +# Copyright 2014 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. + +"""Links the deps of a binary into a static library. + +Run with a working directory, the name of a binary target, and the name of the +static library that should be produced. For example: + + $ link_dependencies.py out/Release-iphoneos \ + crnet_consumer.app/crnet_consumer \ + out/Release-iphoneos/crnet_standalone.a +""" + +import argparse +import os +import re +import subprocess +import sys + + +class SubprocessError(Exception): + pass + + +def extract_inputs(query_result, prefix=''): + """Extracts inputs from ninja query output. + + Given 'ninja -t query' output for a target, extracts all the inputs of that + target, prefixing them with an optional prefix. Inputs prefixed with '|' are + implicit, so we discard them as they shouldn't be linked into the resulting + binary (these are things like the .ninja files themselves, dep lists, and so + on). + + Example query result: + arch/crnet_consumer.armv7: + input: link + obj/[long path...]/crnet_consumer.crnet_consumer_app_delegate.armv7.o + obj/[long path...]/crnet_consumer.crnet_consumer_view_controller.armv7.o + obj/[long path...]/crnet_consumer.main.armv7.o + libcrnet.a + libdata_reduction_proxy_code_browser.a + ... many more inputs ... + liburl_util.a + | obj/content/content.actions_depends.stamp + | gen/components/data_reduction_proxy/common/version.h + | obj/ui/resources/ui_resources.actions_rules_copies.stamp + ... more implicit inputs ... + outputs: + crnet_consumer.app/crnet_consumer + + Args: + query_result: output from 'ninja -t query' + prefix: optional file system path to prefix to returned inputs + + Returns: + A list of the inputs. + """ + extracting = False + inputs = [] + for line in query_result.splitlines(): + if line.startswith(' input:'): + extracting = True + elif line.startswith(' outputs:'): + extracting = False + elif extracting and '|' not in line: + inputs.append(os.path.join(prefix, line.strip())) + return inputs + + +def query_ninja(target, workdir, prefix=''): + """Returns the inputs for the named target. + + Queries ninja for the set of inputs of the named target, then returns the list + of inputs to that target. + + Args: + target: ninja target name to query for + workdir: workdir for ninja + prefix: optional file system path to prefix to returned inputs + + Returns: + A list of file system paths to the inputs to the named target. + """ + proc = subprocess.Popen(['ninja', '-C', workdir, '-t', 'query', target], + stdout=subprocess.PIPE) + stdout, _ = proc.communicate() + return extract_inputs(stdout, prefix) + + +def is_library(target): + """Returns whether target is a library file.""" + return os.path.splitext(target)[1] in ('.a', '.o') + + +def library_deps(targets, workdir, query=query_ninja): + """Returns the set of library dependencies for the supplied targets. + + The entries in the targets list can be either a static library, an object + file, or an executable. Static libraries and object files are incorporated + directly; executables are treated as being thin executable inputs to a fat + executable link step, and have their own library dependencies added in their + place. + + Args: + targets: list of targets to include library dependencies from + workdir: working directory to run ninja queries in + query: function taking target, workdir, and prefix and returning an input + set + Returns: + Set of library dependencies. + """ + deps = set() + for target in targets: + if is_library(target): + deps.add(os.path.join(workdir, target)) + else: + deps = deps.union(query(target, workdir, workdir)) + return deps + + +def link(output, inputs): + """Links output from inputs using libtool. + + Args: + output: file system path to desired output library + inputs: list of file system paths to input libraries + """ + libtool_re = re.compile(r'^.*libtool: (?:for architecture: \S* )?' + r'file: .* has no symbols$') + p = subprocess.Popen( + ['libtool', '-o', output] + inputs, stderr=subprocess.PIPE) + _, err = p.communicate() + for line in err.splitlines(): + if not libtool_re.match(line): + sys.stderr.write(line) + if p.returncode != 0: + message = "subprocess libtool returned {0}".format(p.returncode) + raise SubprocessError(message) + + +def main(): + parser = argparse.ArgumentParser( + description='Link dependencies of a ninja target into a static library') + parser.add_argument('workdir', nargs=1, help='ninja working directory') + parser.add_argument('target', nargs=1, help='target to query for deps') + parser.add_argument('output', nargs=1, help='path to output static library') + args = parser.parse_args() + + inputs = query_ninja(args.target[0], args.workdir[0]) + link(args.output[0], list(library_deps(inputs, args.workdir[0]))) + + +if __name__ == '__main__': + main() diff --git a/src/components/cronet/tools/perf_test_utils.py b/src/components/cronet/tools/perf_test_utils.py new file mode 100755 index 0000000000..b1b5d9bbd4 --- /dev/null +++ b/src/components/cronet/tools/perf_test_utils.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python +# Copyright 2018 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. +"""Utilities for Cronet performance tests.""" + +import logging +import os +import posixpath +import subprocess +import tempfile +from time import sleep + +from cronet.tools import android_rndis_forwarder + + +REPOSITORY_ROOT = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', '..', '..')) +BUILD_TYPE = 'Release' +BUILD_DIR = os.path.join(REPOSITORY_ROOT, 'out', BUILD_TYPE) +QUIC_SERVER = os.path.join(BUILD_DIR, 'quic_server') +CERT_PATH = os.path.join('net', 'data', 'ssl', 'certificates') +QUIC_CERT_DIR = os.path.join(REPOSITORY_ROOT, CERT_PATH) +QUIC_CERT_HOST = 'test.example.com' +QUIC_CERT_FILENAME = 'quic-chain.pem' +QUIC_CERT = os.path.join(QUIC_CERT_DIR, QUIC_CERT_FILENAME) +QUIC_KEY = os.path.join(QUIC_CERT_DIR, 'quic-leaf-cert.key') +APP_APK = os.path.join(BUILD_DIR, 'apks', 'CronetPerfTest.apk') +APP_PACKAGE = 'org.chromium.net' +APP_ACTIVITY = '.CronetPerfTestActivity' +APP_ACTION = 'android.intent.action.MAIN' +HTTP_PORT = None # Value will be overridden by DEFAULT_BENCHMARK_CONFIG. +# TODO(pauljensen): Consider whether we can avoid loading this +# DEFAULT_BENCHMARK_CONFIG dict into globals. +DEFAULT_BENCHMARK_CONFIG = { + # Control various metric recording for further investigation. + 'CAPTURE_NETLOG': False, + 'CAPTURE_TRACE': False, + 'CAPTURE_SAMPLED_TRACE': False, + # While running Cronet Async API benchmarks, indicate if callbacks should be + # run on network thread rather than posted back to caller thread. This allows + # measuring if thread-hopping overhead is significant. + 'CRONET_ASYNC_USE_NETWORK_THREAD': False, + # A small resource for device to fetch from host. + 'SMALL_RESOURCE': 'small.html', + 'SMALL_RESOURCE_SIZE': 26, + # Number of times to fetch SMALL_RESOURCE. + 'SMALL_ITERATIONS': 1000, + # A large resource for device to fetch from host. + 'LARGE_RESOURCE': 'large.html', + 'LARGE_RESOURCE_SIZE': 10000026, + # Number of times to fetch LARGE_RESOURCE. + 'LARGE_ITERATIONS': 4, + # Ports of HTTP and QUIC servers on host. + 'HTTP_PORT': 9000, + 'QUIC_PORT': 9001, + # Maximum read/write buffer size to use. + 'MAX_BUFFER_SIZE': 16384, + 'HOST': QUIC_CERT_HOST, + 'QUIC_CERT_FILE': QUIC_CERT_FILENAME, +} +# Add benchmark config to global state for easy access. +globals().update(DEFAULT_BENCHMARK_CONFIG) +# Pylint doesn't really interpret the file, so it won't find the definitions +# added from DEFAULT_BENCHMARK_CONFIG, so suppress the undefined variable +# warning. +#pylint: disable=undefined-variable + +class NativeDevice(object): + def GetExternalStoragePath(self): + return '/tmp' + + def RunShellCommand(self, cmd, check_return=False): + if check_return: + subprocess.check_call(cmd) + else: + subprocess.call(cmd) + + def WriteFile(self, path, data): + with open(path, 'w') as f: + f.write(data) + +def GetConfig(device): + config = DEFAULT_BENCHMARK_CONFIG + config['HOST_IP'] = GetServersHost(device) + if isinstance(device, NativeDevice): + config['RESULTS_FILE'] = '/tmp/cronet_perf_test_results.txt' + config['DONE_FILE'] = '/tmp/cronet_perf_test_done.txt' + else: + # An on-device file containing benchmark timings. Written by benchmark app. + config['RESULTS_FILE'] = '/data/data/' + APP_PACKAGE + '/results.txt' + # An on-device file whose presence indicates benchmark app has terminated. + config['DONE_FILE'] = '/data/data/' + APP_PACKAGE + '/done.txt' + return config + + +def GetAndroidRndisConfig(device): + return android_rndis_forwarder.AndroidRndisConfigurator(device) + + +def GetServersHost(device): + if isinstance(device, NativeDevice): + return '127.0.0.1' + return GetAndroidRndisConfig(device).host_ip + + +def GetHttpServerURL(device, resource): + return 'http://%s:%d/%s' % (GetServersHost(device), HTTP_PORT, resource) + + +class QuicServer(object): + + def __init__(self, quic_server_doc_root): + self._process = None + self._quic_server_doc_root = quic_server_doc_root + + def StartupQuicServer(self, device): + cmd = [QUIC_SERVER, + '--quic_response_cache_dir=%s' % self._quic_server_doc_root, + '--certificate_file=%s' % QUIC_CERT, + '--key_file=%s' % QUIC_KEY, + '--port=%d' % QUIC_PORT] + logging.info("Starting Quic Server: %s", cmd) + self._process = subprocess.Popen(cmd) + assert self._process != None + # Wait for quic_server to start serving. + waited_s = 0 + while subprocess.call(['lsof', '-i', 'udp:%d' % QUIC_PORT, '-p', + '%d' % self._process.pid], + stdout=open(os.devnull, 'w')) != 0: + sleep(0.1) + waited_s += 0.1 + assert waited_s < 5, "quic_server failed to start after %fs" % waited_s + # Push certificate to device. + cert = open(QUIC_CERT, 'r').read() + device_cert_path = posixpath.join( + device.GetExternalStoragePath(), 'chromium_tests_root', CERT_PATH) + device.RunShellCommand(['mkdir', '-p', device_cert_path], check_return=True) + device.WriteFile(os.path.join(device_cert_path, QUIC_CERT_FILENAME), cert) + + def ShutdownQuicServer(self): + if self._process: + self._process.terminate() + + +def GenerateHttpTestResources(): + http_server_doc_root = tempfile.mkdtemp() + # Create a small test file to serve. + small_file_name = os.path.join(http_server_doc_root, SMALL_RESOURCE) + small_file = open(small_file_name, 'wb') + small_file.write(''); + small_file.close() + assert SMALL_RESOURCE_SIZE == os.path.getsize(small_file_name) + # Create a large (10MB) test file to serve. + large_file_name = os.path.join(http_server_doc_root, LARGE_RESOURCE) + large_file = open(large_file_name, 'wb') + large_file.write(''); + for _ in range(0, 1000000): + large_file.write('1234567890'); + large_file.write(''); + large_file.close() + assert LARGE_RESOURCE_SIZE == os.path.getsize(large_file_name) + return http_server_doc_root + + +def GenerateQuicTestResources(device): + quic_server_doc_root = tempfile.mkdtemp() + # Use wget to build up fake QUIC in-memory cache dir for serving. + # quic_server expects the dir/file layout that wget produces. + for resource in [SMALL_RESOURCE, LARGE_RESOURCE]: + assert subprocess.Popen(['wget', '-p', '-q', '--save-headers', + GetHttpServerURL(device, resource)], + cwd=quic_server_doc_root).wait() == 0 + # wget places results in host:port directory. Adjust for QUIC port. + os.rename(os.path.join(quic_server_doc_root, + "%s:%d" % (GetServersHost(device), HTTP_PORT)), + os.path.join(quic_server_doc_root, + "%s:%d" % (QUIC_CERT_HOST, QUIC_PORT))) + return quic_server_doc_root + + +def GenerateLighttpdConfig(config_file, http_server_doc_root, http_server): + # Must create customized config file to allow overriding the server.bind + # setting. + config_file.write('server.document-root = "%s"\n' % http_server_doc_root) + config_file.write('server.port = %d\n' % HTTP_PORT) + # These lines are added so lighttpd_server.py's internal test succeeds. + config_file.write('server.tag = "%s"\n' % http_server.server_tag) + config_file.write('server.pid-file = "%s"\n' % http_server.pid_file) + config_file.write('dir-listing.activate = "enable"\n') + config_file.flush() diff --git a/src/components/cronet/tools/symbolicate.sh b/src/components/cronet/tools/symbolicate.sh new file mode 100755 index 0000000000..2b01472b77 --- /dev/null +++ b/src/components/cronet/tools/symbolicate.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Copyright (c) 2019 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. + +set -e +CHROME_SRC="$(dirname "$0")/../../.." + +if [ $# -lt 1 ] ; then + echo "Usage: "$0" [--debug-libs] stack-trace-file" + echo " --debug-libs uses Debug (rather than Release) libraries." + exit 1 +fi + +LIB_TYPE=Release +if [[ "$1" == "--debug-libs" ]] ; then + LIB_TYPE=Debug + shift +fi + +if [ ! -f "$1" ] ; then + echo "$1: file not found" + exit 1 +fi + +# try to automatically identify architecture and version, if there are no user- +# supplied values. +if [ -z $ARCH ] ; then + if grep -q arm64 "$1" ; then + ARCH=arm64-v8a + elif grep -q armeabi-v7a "$1" ; then + ARCH=armeabi-v7a + elif grep -q armeabi "$1" ; then + ARCH=armeabi + elif grep -q "ABI: 'arm'" "$1" ; then + ARCH=armeabi-v7a + elif grep -q "ABI: 'arm64'" "$1" ; then + ARCH=arm64-v8a + elif grep -q "ABI: 'x86_64'" "$1" ; then + ARCH=x86_64 + elif grep -q "ABI: 'x86'" "$1" ; then + ARCH=x86 + else + echo "Cannot determine architecture." + echo "Set the ARCH environment variable explicitly to continue." + exit 1 + fi +fi +if [ -z "$VERSION" ] ; then + VERSION=$(grep -o -m1 'libcronet\..*\.so' "$1" | + sed 's/libcronet\.\(.*\)\.so/\1/') +fi + +echo VERSION=$VERSION +echo ARCH=$ARCH +echo Using symbolicator from: $CHROME_SRC +echo + +ARCHOPT= +if [[ "$ARCH" == "arm64-v8a" ]] ; then + ARCHOPT="--arch=arm64" +fi + +FILE=${VERSION}/${LIB_TYPE}/cronet/symbols/${ARCH}/libcronet.${VERSION}.so +GSUTIL="$CHROME_SRC/third_party/depot_tools/gsutil.py" +$GSUTIL -m cp -R gs://chromium-cronet/android/${FILE} ~/Downloads + +TRACER="$CHROME_SRC/third_party/android_platform/development/scripts/stack" +CHROMIUM_OUTPUT_DIR="$HOME/Downloads" "$TRACER" $ARCHOPT "$1" diff --git a/src/components/cronet/tools/update_api.py b/src/components/cronet/tools/update_api.py new file mode 100755 index 0000000000..8dff75d8e5 --- /dev/null +++ b/src/components/cronet/tools/update_api.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# Copyright 2016 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. + +"""update_api.py - Update committed Cronet API.""" + + + +import argparse +import filecmp +import fileinput +import hashlib +import os +import re +import shutil +import sys +import tempfile + + +# Filename of dump of current API. +API_FILENAME = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', 'android', 'api.txt')) +# Filename of file containing API version number. +API_VERSION_FILENAME = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', 'android', 'api_version.txt')) + +# Regular expression that catches the beginning of lines that declare classes. +# The first group returned by a match is the class name. +CLASS_RE = re.compile(r'.*class ([^ ]*) .*\{') + +# Regular expression that matches a string containing an unnamed class name, +# for example 'Foo$1'. +UNNAMED_CLASS_RE = re.compile(r'.*\$[0-9]') + + +def generate_api(api_jar, output_filename): + # Dumps the API in |api_jar| into |outpuf_filename|. + + with open(output_filename, 'w') as output_file: + output_file.write( + 'DO NOT EDIT THIS FILE, USE update_api.py TO UPDATE IT\n\n') + + # Extract API class files from api_jar. + temp_dir = tempfile.mkdtemp() + old_cwd = os.getcwd() + api_jar_path = os.path.abspath(api_jar) + os.chdir(temp_dir) + if os.system('jar xf %s' % api_jar_path): + print('ERROR: jar failed on ' + api_jar) + return False + os.chdir(old_cwd) + shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) + + # Collect names of all API class files + api_class_files = [] + for root, _, filenames in os.walk(temp_dir): + api_class_files += [os.path.join(root, f) for f in filenames] + api_class_files.sort() + + # Dump API class files into |output_filename| + javap_cmd = ('javap -protected %s >> %s' % (' '.join(api_class_files), + output_filename)).replace('$', '\\$') + if os.system(javap_cmd): + print('ERROR: javap command failed: ' + javap_cmd) + return False + shutil.rmtree(temp_dir) + + # Strip out pieces we don't need to compare. + output_file = fileinput.FileInput(output_filename, inplace=True) + skip_to_next_class = False + md5_hash = hashlib.md5() + for line in output_file: + # Skip 'Compiled from ' lines as they're not part of the API. + if line.startswith('Compiled from "'): + continue + if CLASS_RE.match(line): + skip_to_next_class = ( + # Skip internal classes, they aren't exposed. + UNNAMED_CLASS_RE.match(line) + ) + if skip_to_next_class: + skip_to_next_class = line != '}' + continue + md5_hash.update(line.encode('utf8')) + sys.stdout.write(line) + output_file.close() + with open(output_filename, 'a') as output_file: + output_file.write('Stamp: %s\n' % md5_hash.hexdigest()) + return True + + +def check_up_to_date(api_jar): + # Returns True if API_FILENAME matches the API exposed by |api_jar|. + + [_, temp_filename] = tempfile.mkstemp() + if not generate_api(api_jar, temp_filename): + return False + ret = filecmp.cmp(API_FILENAME, temp_filename) + os.remove(temp_filename) + return ret + + +def check_api_update(old_api, new_api): + # Enforce that lines are only added when updating API. + new_hash = hashlib.md5() + old_hash = hashlib.md5() + seen_stamp = False + with open(old_api, 'r') as old_api_file, open(new_api, 'r') as new_api_file: + for old_line in old_api_file: + while True: + new_line = new_api_file.readline() + if seen_stamp: + print('ERROR: Stamp is not the last line.') + return False + if new_line.startswith('Stamp: ') and old_line.startswith('Stamp: '): + if old_line != 'Stamp: %s\n' % old_hash.hexdigest(): + print('ERROR: Prior api.txt not stamped by update_api.py') + return False + if new_line != 'Stamp: %s\n' % new_hash.hexdigest(): + print('ERROR: New api.txt not stamped by update_api.py') + return False + seen_stamp = True + break + new_hash.update(new_line) + if new_line == old_line: + break + if not new_line: + if old_line.startswith('Stamp: '): + print('ERROR: New api.txt not stamped by update_api.py') + else: + print('ERROR: This API was modified or removed:') + print(' ' + old_line) + print(' Cronet API methods and classes cannot be modified.') + return False + old_hash.update(old_line) + if not seen_stamp: + print('ERROR: api.txt not stamped by update_api.py.') + return False + return True + + +def main(args): + parser = argparse.ArgumentParser(description='Update Cronet api.txt.') + parser.add_argument('--api_jar', + help='Path to API jar (i.e. cronet_api.jar)', + required=True, + metavar='path/to/cronet_api.jar') + opts = parser.parse_args(args) + + if check_up_to_date(opts.api_jar): + return True + + [_, temp_filename] = tempfile.mkstemp() + if (generate_api(opts.api_jar, temp_filename) and + check_api_update(API_FILENAME, temp_filename)): + # Update API version number to new version number + with open(API_VERSION_FILENAME,'r+') as f: + version = int(f.read()) + f.seek(0) + f.write(str(version + 1)) + # Update API file to new API + shutil.move(temp_filename, API_FILENAME) + return True + os.remove(temp_filename) + return False + + +if __name__ == '__main__': + sys.exit(0 if main(sys.argv[1:]) else -1) diff --git a/src/components/cronet/tools_unittest.py b/src/components/cronet/tools_unittest.py new file mode 100755 index 0000000000..80cd57007e --- /dev/null +++ b/src/components/cronet/tools_unittest.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# Copyright 2016 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. + +"""Run tools/ unittests.""" + +import sys +import unittest + +if __name__ == '__main__': + suite = unittest.TestLoader().discover('tools', pattern = "*_unittest.py") + sys.exit(0 if unittest.TextTestRunner().run(suite).wasSuccessful() else 1) diff --git a/src/components/cronet/url_request_context_config.cc b/src/components/cronet/url_request_context_config.cc new file mode 100644 index 0000000000..6eb06fcc07 --- /dev/null +++ b/src/components/cronet/url_request_context_config.cc @@ -0,0 +1,864 @@ +// Copyright 2014 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 "components/cronet/url_request_context_config.h" + +#include +#include + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_split.h" +#include "base/task/sequenced_task_runner.h" +#include "base/values.h" +#include "build/build_config.h" +#include "components/cronet/stale_host_resolver.h" +#include "net/base/address_family.h" +#include "net/cert/caching_cert_verifier.h" +#include "net/cert/cert_verifier.h" +#include "net/cert/cert_verify_proc.h" +#include "net/cert/ct_policy_enforcer.h" +#include "net/cert/ct_policy_status.h" +#include "net/cert/multi_threaded_cert_verifier.h" +#include "net/dns/context_host_resolver.h" +#include "net/dns/host_resolver.h" +#include "net/dns/mapped_host_resolver.h" +#include "net/http/http_network_session.h" +#include "net/http/http_server_properties.h" +#include "net/log/net_log.h" +#include "net/nqe/network_quality_estimator_params.h" +#include "net/reporting/reporting_policy.h" +#include "net/socket/ssl_client_socket.h" +#include "net/ssl/ssl_key_logger_impl.h" +#include "net/third_party/quiche/src/quic/core/quic_packets.h" +#include "net/third_party/quiche/src/quic/core/quic_tag.h" +#include "net/url_request/url_request_context_builder.h" +#include "url/origin.h" + +#if BUILDFLAG(ENABLE_REPORTING) +#include "net/reporting/reporting_policy.h" +#endif // BUILDFLAG(ENABLE_REPORTING) + +namespace cronet { + +namespace { + +// Name of disk cache directory. +const base::FilePath::CharType kDiskCacheDirectoryName[] = + FILE_PATH_LITERAL("disk_cache"); +const char kQuicFieldTrialName[] = "QUIC"; +const char kQuicConnectionOptions[] = "connection_options"; +const char kQuicClientConnectionOptions[] = "client_connection_options"; +const char kQuicStoreServerConfigsInProperties[] = + "store_server_configs_in_properties"; +const char kQuicMaxServerConfigsStoredInProperties[] = + "max_server_configs_stored_in_properties"; +const char kQuicIdleConnectionTimeoutSeconds[] = + "idle_connection_timeout_seconds"; +const char kQuicMaxTimeBeforeCryptoHandshakeSeconds[] = + "max_time_before_crypto_handshake_seconds"; +const char kQuicMaxIdleTimeBeforeCryptoHandshakeSeconds[] = + "max_idle_time_before_crypto_handshake_seconds"; +const char kQuicCloseSessionsOnIpChange[] = "close_sessions_on_ip_change"; +const char kQuicGoAwaySessionsOnIpChange[] = "goaway_sessions_on_ip_change"; +const char kQuicAllowServerMigration[] = "allow_server_migration"; +const char kQuicMigrateSessionsOnNetworkChangeV2[] = + "migrate_sessions_on_network_change_v2"; +const char kQuicMigrateIdleSessions[] = "migrate_idle_sessions"; +const char kQuicRetransmittableOnWireTimeoutMilliseconds[] = + "retransmittable_on_wire_timeout_milliseconds"; +const char kQuicIdleSessionMigrationPeriodSeconds[] = + "idle_session_migration_period_seconds"; +const char kQuicMaxTimeOnNonDefaultNetworkSeconds[] = + "max_time_on_non_default_network_seconds"; +const char kQuicMaxMigrationsToNonDefaultNetworkOnWriteError[] = + "max_migrations_to_non_default_network_on_write_error"; +const char kQuicMaxMigrationsToNonDefaultNetworkOnPathDegrading[] = + "max_migrations_to_non_default_network_on_path_degrading"; +const char kQuicUserAgentId[] = "user_agent_id"; +const char kQuicMigrateSessionsEarlyV2[] = "migrate_sessions_early_v2"; +const char kQuicRetryOnAlternateNetworkBeforeHandshake[] = + "retry_on_alternate_network_before_handshake"; +const char kQuicRaceStaleDNSOnConnection[] = "race_stale_dns_on_connection"; +const char kQuicDisableBidirectionalStreams[] = + "quic_disable_bidirectional_streams"; +const char kQuicHostWhitelist[] = "host_whitelist"; +const char kQuicEnableSocketRecvOptimization[] = + "enable_socket_recv_optimization"; +const char kQuicVersion[] = "quic_version"; +const char kQuicObsoleteVersionsAllowed[] = "obsolete_versions_allowed"; +const char kQuicFlags[] = "set_quic_flags"; +const char kQuicIOSNetworkServiceType[] = "ios_network_service_type"; +const char kRetryWithoutAltSvcOnQuicErrors[] = + "retry_without_alt_svc_on_quic_errors"; + +// AsyncDNS experiment dictionary name. +const char kAsyncDnsFieldTrialName[] = "AsyncDNS"; +// Name of boolean to enable AsyncDNS experiment. +const char kAsyncDnsEnable[] = "enable"; + +// Stale DNS (StaleHostResolver) experiment dictionary name. +const char kStaleDnsFieldTrialName[] = "StaleDNS"; +// Name of boolean to enable stale DNS experiment. +const char kStaleDnsEnable[] = "enable"; +// Name of integer delay in milliseconds before a stale DNS result will be +// used. +const char kStaleDnsDelayMs[] = "delay_ms"; +// Name of integer maximum age (past expiration) in milliseconds of a stale DNS +// result that will be used, or 0 for no limit. +const char kStaleDnsMaxExpiredTimeMs[] = "max_expired_time_ms"; +// Name of integer maximum times each stale DNS result can be used, or 0 for no +// limit. +const char kStaleDnsMaxStaleUses[] = "max_stale_uses"; +// Name of boolean to allow stale DNS results from other networks to be used on +// the current network. +const char kStaleDnsAllowOtherNetwork[] = "allow_other_network"; +// Name of boolean to enable persisting the DNS cache to disk. +const char kStaleDnsPersist[] = "persist_to_disk"; +// Name of integer minimum time in milliseconds between writes to disk for DNS +// cache persistence. Default value is one minute. Only relevant if +// "persist_to_disk" is true. +const char kStaleDnsPersistTimer[] = "persist_delay_ms"; +// Name of boolean to allow use of stale DNS results when network resolver +// returns ERR_NAME_NOT_RESOLVED. +const char kStaleDnsUseStaleOnNameNotResolved[] = + "use_stale_on_name_not_resolved"; + +// Rules to override DNS resolution. Intended for testing. +// See explanation of format in net/dns/mapped_host_resolver.h. +const char kHostResolverRulesFieldTrialName[] = "HostResolverRules"; +const char kHostResolverRules[] = "host_resolver_rules"; + +// NetworkQualityEstimator (NQE) experiment dictionary name. +const char kNetworkQualityEstimatorFieldTrialName[] = "NetworkQualityEstimator"; + +// Network Error Logging experiment dictionary name. +const char kNetworkErrorLoggingFieldTrialName[] = "NetworkErrorLogging"; +// Name of boolean to enable Reporting API. +const char kNetworkErrorLoggingEnable[] = "enable"; +// Name of list of preloaded "Report-To" headers. +const char kNetworkErrorLoggingPreloadedReportToHeaders[] = + "preloaded_report_to_headers"; +// Name of list of preloaded "NEL" headers. +const char kNetworkErrorLoggingPreloadedNELHeaders[] = "preloaded_nel_headers"; +// Name of key (for above two lists) for header origin. +const char kNetworkErrorLoggingOrigin[] = "origin"; +// Name of key (for above two lists) for header value. +const char kNetworkErrorLoggingValue[] = "value"; + +// Disable IPv6 when on WiFi. This is a workaround for a known issue on certain +// Android phones, and should not be necessary when not on one of those devices. +// See https://crbug.com/696569 for details. +const char kDisableIPv6OnWifi[] = "disable_ipv6_on_wifi"; + +const char kSSLKeyLogFile[] = "ssl_key_log_file"; + +const char kGoAwayOnPathDegrading[] = "go_away_on_path_degrading"; + +const char kAllowPortMigration[] = "allow_port_migration"; + +const char kDisableTlsZeroRtt[] = "disable_tls_zero_rtt"; + +// Whether SPDY sessions should be closed or marked as going away upon relevant +// network changes. When not specified, /net behavior varies depending on the +// underlying OS. +const char kSpdyGoAwayOnIpChange[] = "spdy_go_away_on_ip_change"; + +// Whether the connection status of all bidirectional streams (created through +// the Cronet engine) should be monitored. +// The value must be an integer (> 0) and will be interpreted as a suggestion +// for the period of the heartbeat signal. See +// SpdySession#EnableBrokenConnectionDetection for more info. +const char kBidiStreamDetectBrokenConnection[] = + "bidi_stream_detect_broken_connection"; + +// "goaway_sessions_on_ip_change" is default on for iOS unless overridden via +// experimental options explicitly. +#if BUILDFLAG(IS_IOS) +const bool kDefaultQuicGoAwaySessionsOnIpChange = true; +#else +const bool kDefaultQuicGoAwaySessionsOnIpChange = false; +#endif + +// Serializes a base::Value into a string that can be used as the value of +// JFV-encoded HTTP header [1]. If |value| is a list, we remove the outermost +// [] delimiters from the result. +// +// [1] https://tools.ietf.org/html/draft-reschke-http-jfv +std::string SerializeJFVHeader(const base::Value& value) { + std::string result; + if (!base::JSONWriter::Write(value, &result)) + return std::string(); + if (value.is_list()) { + DCHECK(result.size() >= 2); + return result.substr(1, result.size() - 2); + } + return result; +} + +std::vector +ParseNetworkErrorLoggingHeaders( + base::Value::ConstListView preloaded_headers_config) { + std::vector result; + for (const auto& preloaded_header_config : preloaded_headers_config) { + if (!preloaded_header_config.is_dict()) + continue; + + auto* origin_config = preloaded_header_config.FindKeyOfType( + kNetworkErrorLoggingOrigin, base::Value::Type::STRING); + if (!origin_config) + continue; + GURL origin_url(origin_config->GetString()); + if (!origin_url.is_valid()) + continue; + auto origin = url::Origin::Create(origin_url); + + auto* value = preloaded_header_config.FindKey(kNetworkErrorLoggingValue); + if (!value) + continue; + + result.push_back(URLRequestContextConfig::PreloadedNelAndReportingHeader( + origin, SerializeJFVHeader(*value))); + } + return result; +} + +// Applies |f| to the value contained by |maybe|, returns empty optional +// otherwise. +template +auto map(absl::optional maybe, F&& f) { + if (!maybe) + return absl::optional>(); + return absl::optional>(f(maybe.value())); +} + +} // namespace + +URLRequestContextConfig::QuicHint::QuicHint(const std::string& host, + int port, + int alternate_port) + : host(host), port(port), alternate_port(alternate_port) {} + +URLRequestContextConfig::QuicHint::~QuicHint() {} + +URLRequestContextConfig::Pkp::Pkp(const std::string& host, + bool include_subdomains, + const base::Time& expiration_date) + : host(host), + include_subdomains(include_subdomains), + expiration_date(expiration_date) {} + +URLRequestContextConfig::Pkp::~Pkp() {} + +URLRequestContextConfig::PreloadedNelAndReportingHeader:: + PreloadedNelAndReportingHeader(const url::Origin& origin, std::string value) + : origin(origin), value(std::move(value)) {} + +URLRequestContextConfig::PreloadedNelAndReportingHeader:: + ~PreloadedNelAndReportingHeader() = default; + +URLRequestContextConfig::URLRequestContextConfig( + bool enable_quic, + const std::string& quic_user_agent_id, + bool enable_spdy, + bool enable_brotli, + HttpCacheType http_cache, + int http_cache_max_size, + bool load_disable_cache, + const std::string& storage_path, + const std::string& accept_language, + const std::string& user_agent, + base::Value::DictStorage experimental_options, + std::unique_ptr mock_cert_verifier, + bool enable_network_quality_estimator, + bool bypass_public_key_pinning_for_local_trust_anchors, + absl::optional network_thread_priority) + : enable_quic(enable_quic), + quic_user_agent_id(quic_user_agent_id), + enable_spdy(enable_spdy), + enable_brotli(enable_brotli), + http_cache(http_cache), + http_cache_max_size(http_cache_max_size), + load_disable_cache(load_disable_cache), + storage_path(storage_path), + accept_language(accept_language), + user_agent(user_agent), + mock_cert_verifier(std::move(mock_cert_verifier)), + enable_network_quality_estimator(enable_network_quality_estimator), + bypass_public_key_pinning_for_local_trust_anchors( + bypass_public_key_pinning_for_local_trust_anchors), + effective_experimental_options( + base::Value(experimental_options).TakeDictDeprecated()), + experimental_options(std::move(experimental_options)), + network_thread_priority(network_thread_priority), + bidi_stream_detect_broken_connection(false), + heartbeat_interval(base::Seconds(0)) { + SetContextConfigExperimentalOptions(); +} + +URLRequestContextConfig::~URLRequestContextConfig() {} + +// static +std::unique_ptr +URLRequestContextConfig::CreateURLRequestContextConfig( + bool enable_quic, + const std::string& quic_user_agent_id, + bool enable_spdy, + bool enable_brotli, + HttpCacheType http_cache, + int http_cache_max_size, + bool load_disable_cache, + const std::string& storage_path, + const std::string& accept_language, + const std::string& user_agent, + const std::string& unparsed_experimental_options, + std::unique_ptr mock_cert_verifier, + bool enable_network_quality_estimator, + bool bypass_public_key_pinning_for_local_trust_anchors, + absl::optional network_thread_priority) { + absl::optional experimental_options = + ParseExperimentalOptions(unparsed_experimental_options); + if (!experimental_options) { + // For the time being maintain backward compatibility by only failing to + // parse when DCHECKs are enabled. + if (ExperimentalOptionsParsingIsAllowedToFail()) + return nullptr; + else + experimental_options = base::Value::DictStorage(); + } + return base::WrapUnique(new URLRequestContextConfig( + enable_quic, quic_user_agent_id, enable_spdy, enable_brotli, http_cache, + http_cache_max_size, load_disable_cache, storage_path, accept_language, + user_agent, std::move(experimental_options.value()), + std::move(mock_cert_verifier), enable_network_quality_estimator, + bypass_public_key_pinning_for_local_trust_anchors, + network_thread_priority)); +} + +// static +absl::optional +URLRequestContextConfig::ParseExperimentalOptions( + std::string unparsed_experimental_options) { + // From a user perspective no experimental options means an empty string. The + // underlying code instead expects and empty dictionary. Normalize this. + if (unparsed_experimental_options.empty()) + unparsed_experimental_options = "{}"; + DVLOG(1) << "Experimental Options:" << unparsed_experimental_options; + base::JSONReader::ValueWithError parsed_json = + base::JSONReader::ReadAndReturnValueWithError( + unparsed_experimental_options); + if (!parsed_json.value) { + LOG(ERROR) << "Parsing experimental options failed: '" + << unparsed_experimental_options << "', error " + << parsed_json.error_message; + return absl::nullopt; + } + + base::Value experimental_options_value = std::move(parsed_json.value.value()); + if (!experimental_options_value.is_dict()) { + LOG(ERROR) << "Experimental options string is not a dictionary: " + << experimental_options_value; + return absl::nullopt; + } + + return std::move(experimental_options_value).TakeDictDeprecated(); +} + +void URLRequestContextConfig::SetContextConfigExperimentalOptions() { + auto iter = experimental_options.find(kBidiStreamDetectBrokenConnection); + if (iter == experimental_options.end()) + return; + + const base::Value& heartbeat_interval_value = iter->second; + if (!heartbeat_interval_value.is_int()) { + LOG(ERROR) << "\"" << kBidiStreamDetectBrokenConnection + << "\" config params \"" << heartbeat_interval_value + << "\" is not an int"; + experimental_options.erase(iter); + effective_experimental_options.erase(kBidiStreamDetectBrokenConnection); + return; + } + + int heartbeat_interval_secs = heartbeat_interval_value.GetInt(); + heartbeat_interval = base::Seconds(heartbeat_interval_secs); + bidi_stream_detect_broken_connection = heartbeat_interval_secs > 0; + experimental_options.erase(iter); +} + +void URLRequestContextConfig::SetContextBuilderExperimentalOptions( + net::URLRequestContextBuilder* context_builder, + net::HttpNetworkSessionParams* session_params, + net::QuicParams* quic_params) { + if (experimental_options.empty()) + return; + + bool async_dns_enable = false; + bool stale_dns_enable = false; + bool host_resolver_rules_enable = false; + bool disable_ipv6_on_wifi = false; + bool nel_enable = false; + + StaleHostResolver::StaleOptions stale_dns_options; + const std::string* host_resolver_rules_string; + + for (const auto& iter : experimental_options) { + if (iter.first == kQuicFieldTrialName) { + if (!iter.second.is_dict()) { + LOG(ERROR) << "Quic config params \"" << iter.second + << "\" is not a dictionary value"; + effective_experimental_options.erase(iter.first); + continue; + } + + const base::Value& quic_args = iter.second; + const std::string* quic_version_string = + quic_args.FindStringKey(kQuicVersion); + if (quic_version_string) { + quic::ParsedQuicVersionVector supported_versions = + quic::ParseQuicVersionVectorString(*quic_version_string); + if (!quic_args.FindBoolKey(kQuicObsoleteVersionsAllowed) + .value_or(false)) { + quic::ParsedQuicVersionVector filtered_versions; + quic::ParsedQuicVersionVector obsolete_versions = + net::ObsoleteQuicVersions(); + for (const quic::ParsedQuicVersion& version : supported_versions) { + if (version == quic::ParsedQuicVersion::Q043()) { + // TODO(dschinazi) Remove this special-casing of Q043 once we no + // longer have cronet applications that require it. + filtered_versions.push_back(version); + continue; + } + if (std::find(obsolete_versions.begin(), obsolete_versions.end(), + version) == obsolete_versions.end()) { + filtered_versions.push_back(version); + } + } + supported_versions = filtered_versions; + } + if (!supported_versions.empty()) + quic_params->supported_versions = supported_versions; + } + + const std::string* quic_connection_options = + quic_args.FindStringKey(kQuicConnectionOptions); + if (quic_connection_options) { + quic_params->connection_options = + quic::ParseQuicTagVector(*quic_connection_options); + } + + const std::string* quic_client_connection_options = + quic_args.FindStringKey(kQuicClientConnectionOptions); + if (quic_client_connection_options) { + quic_params->client_connection_options = + quic::ParseQuicTagVector(*quic_client_connection_options); + } + + // TODO(rtenneti): Delete this option after apps stop using it. + // Added this for backward compatibility. + if (quic_args.FindBoolKey(kQuicStoreServerConfigsInProperties) + .value_or(false)) { + quic_params->max_server_configs_stored_in_properties = + net::kDefaultMaxQuicServerEntries; + } + + quic_params->max_server_configs_stored_in_properties = + static_cast( + quic_args.FindIntKey(kQuicMaxServerConfigsStoredInProperties) + .value_or( + quic_params->max_server_configs_stored_in_properties)); + + quic_params->idle_connection_timeout = + map(quic_args.FindIntKey(kQuicIdleConnectionTimeoutSeconds), + base::Seconds) + .value_or(quic_params->idle_connection_timeout); + + quic_params->max_time_before_crypto_handshake = + map(quic_args.FindIntKey(kQuicMaxTimeBeforeCryptoHandshakeSeconds), + base::Seconds) + .value_or(quic_params->max_time_before_crypto_handshake); + + quic_params->max_idle_time_before_crypto_handshake = + map(quic_args.FindIntKey( + kQuicMaxIdleTimeBeforeCryptoHandshakeSeconds), + base::Seconds) + .value_or(quic_params->max_idle_time_before_crypto_handshake); + + quic_params->close_sessions_on_ip_change = + quic_args.FindBoolKey(kQuicCloseSessionsOnIpChange) + .value_or(quic_params->close_sessions_on_ip_change); + if (quic_params->close_sessions_on_ip_change && + kDefaultQuicGoAwaySessionsOnIpChange) { + // "close_sessions_on_ip_change" and "goaway_sessions_on_ip_change" + // are mutually exclusive. Turn off the goaway option which is + // default on for iOS if "close_sessions_on_ip_change" is set via + // experimental options. + quic_params->goaway_sessions_on_ip_change = false; + } + + quic_params->goaway_sessions_on_ip_change = + quic_args.FindBoolKey(kQuicGoAwaySessionsOnIpChange) + .value_or(quic_params->goaway_sessions_on_ip_change); + quic_params->go_away_on_path_degrading = + quic_args.FindBoolKey(kGoAwayOnPathDegrading) + .value_or(quic_params->go_away_on_path_degrading); + quic_params->allow_server_migration = + quic_args.FindBoolKey(kQuicAllowServerMigration) + .value_or(quic_params->allow_server_migration); + + const std::string* user_agent_id = + quic_args.FindStringKey(kQuicUserAgentId); + if (user_agent_id) { + quic_params->user_agent_id = *user_agent_id; + } + + quic_params->enable_socket_recv_optimization = + quic_args.FindBoolKey(kQuicEnableSocketRecvOptimization) + .value_or(quic_params->enable_socket_recv_optimization); + + absl::optional quic_migrate_sessions_on_network_change_v2_in = + quic_args.FindBoolKey(kQuicMigrateSessionsOnNetworkChangeV2); + if (quic_migrate_sessions_on_network_change_v2_in.has_value()) { + quic_params->migrate_sessions_on_network_change_v2 = + quic_migrate_sessions_on_network_change_v2_in.value(); + quic_params->max_time_on_non_default_network = + map(quic_args.FindIntKey(kQuicMaxTimeOnNonDefaultNetworkSeconds), + base::Seconds) + .value_or(quic_params->max_time_on_non_default_network); + quic_params->max_migrations_to_non_default_network_on_write_error = + quic_args + .FindIntKey(kQuicMaxMigrationsToNonDefaultNetworkOnWriteError) + .value_or( + quic_params + ->max_migrations_to_non_default_network_on_write_error); + quic_params->max_migrations_to_non_default_network_on_path_degrading = + quic_args + .FindIntKey( + kQuicMaxMigrationsToNonDefaultNetworkOnPathDegrading) + .value_or( + quic_params + ->max_migrations_to_non_default_network_on_path_degrading); + } + + absl::optional quic_migrate_idle_sessions_in = + quic_args.FindBoolKey(kQuicMigrateIdleSessions); + if (quic_migrate_idle_sessions_in.has_value()) { + quic_params->migrate_idle_sessions = + quic_migrate_idle_sessions_in.value(); + quic_params->idle_session_migration_period = + map(quic_args.FindIntKey(kQuicIdleSessionMigrationPeriodSeconds), + base::Seconds) + .value_or(quic_params->idle_session_migration_period); + } + + quic_params->migrate_sessions_early_v2 = + quic_args.FindBoolKey(kQuicMigrateSessionsEarlyV2) + .value_or(quic_params->migrate_sessions_early_v2); + + quic_params->retransmittable_on_wire_timeout = + map(quic_args.FindIntKey( + kQuicRetransmittableOnWireTimeoutMilliseconds), + base::Milliseconds) + .value_or(quic_params->retransmittable_on_wire_timeout); + + quic_params->retry_on_alternate_network_before_handshake = + quic_args.FindBoolKey(kQuicRetryOnAlternateNetworkBeforeHandshake) + .value_or( + quic_params->retry_on_alternate_network_before_handshake); + + quic_params->race_stale_dns_on_connection = + quic_args.FindBoolKey(kQuicRaceStaleDNSOnConnection) + .value_or(quic_params->race_stale_dns_on_connection); + + quic_params->allow_port_migration = + quic_args.FindBoolKey(kAllowPortMigration) + .value_or(quic_params->allow_port_migration); + + quic_params->retry_without_alt_svc_on_quic_errors = + quic_args.FindBoolKey(kRetryWithoutAltSvcOnQuicErrors) + .value_or(quic_params->retry_without_alt_svc_on_quic_errors); + + quic_params->disable_tls_zero_rtt = + quic_args.FindBoolKey(kDisableTlsZeroRtt) + .value_or(quic_params->disable_tls_zero_rtt); + + quic_params->disable_bidirectional_streams = + quic_args.FindBoolKey(kQuicDisableBidirectionalStreams) + .value_or(quic_params->disable_bidirectional_streams); + + const std::string* quic_host_allowlist = + quic_args.FindStringKey(kQuicHostWhitelist); + if (quic_host_allowlist) { + std::vector host_vector = + base::SplitString(*quic_host_allowlist, ",", base::TRIM_WHITESPACE, + base::SPLIT_WANT_ALL); + session_params->quic_host_allowlist.clear(); + for (const std::string& host : host_vector) { + session_params->quic_host_allowlist.insert(host); + } + } + + const std::string* quic_flags = quic_args.FindStringKey(kQuicFlags); + if (quic_flags) { + for (const auto& flag : + base::SplitString(*quic_flags, ",", base::TRIM_WHITESPACE, + base::SPLIT_WANT_ALL)) { + std::vector tokens = base::SplitString( + flag, "=", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + if (tokens.size() != 2) + continue; + SetQuicFlagByName(tokens[0], tokens[1]); + } + } + + quic_params->ios_network_service_type = + quic_args.FindIntKey(kQuicIOSNetworkServiceType) + .value_or(quic_params->ios_network_service_type); + } else if (iter.first == kAsyncDnsFieldTrialName) { + if (!iter.second.is_dict()) { + LOG(ERROR) << "\"" << iter.first << "\" config params \"" << iter.second + << "\" is not a dictionary value"; + effective_experimental_options.erase(iter.first); + continue; + } + const base::Value& async_dns_args = iter.second; + async_dns_enable = async_dns_args.FindBoolKey(kAsyncDnsEnable) + .value_or(async_dns_enable); + } else if (iter.first == kStaleDnsFieldTrialName) { + if (!iter.second.is_dict()) { + LOG(ERROR) << "\"" << iter.first << "\" config params \"" << iter.second + << "\" is not a dictionary value"; + effective_experimental_options.erase(iter.first); + continue; + } + const base::Value& stale_dns_args = iter.second; + stale_dns_enable = + stale_dns_args.FindBoolKey(kStaleDnsEnable).value_or(false); + + if (stale_dns_enable) { + stale_dns_options.delay = + map(stale_dns_args.FindIntKey(kStaleDnsDelayMs), + base::Milliseconds) + .value_or(stale_dns_options.delay); + stale_dns_options.max_expired_time = + map(stale_dns_args.FindIntKey(kStaleDnsMaxExpiredTimeMs), + base::Milliseconds) + .value_or(stale_dns_options.max_expired_time); + stale_dns_options.max_stale_uses = + stale_dns_args.FindIntKey(kStaleDnsMaxStaleUses) + .value_or(stale_dns_options.max_stale_uses); + stale_dns_options.allow_other_network = + stale_dns_args.FindBoolKey(kStaleDnsAllowOtherNetwork) + .value_or(stale_dns_options.allow_other_network); + enable_host_cache_persistence = + stale_dns_args.FindBoolKey(kStaleDnsPersist) + .value_or(enable_host_cache_persistence); + host_cache_persistence_delay_ms = + stale_dns_args.FindIntKey(kStaleDnsPersistTimer) + .value_or(host_cache_persistence_delay_ms); + stale_dns_options.use_stale_on_name_not_resolved = + stale_dns_args.FindBoolKey(kStaleDnsUseStaleOnNameNotResolved) + .value_or(stale_dns_options.use_stale_on_name_not_resolved); + } + } else if (iter.first == kHostResolverRulesFieldTrialName) { + if (!iter.second.is_dict()) { + LOG(ERROR) << "\"" << iter.first << "\" config params \"" << iter.second + << "\" is not a dictionary value"; + effective_experimental_options.erase(iter.first); + continue; + } + const base::Value& host_resolver_rules_args = iter.second; + host_resolver_rules_string = + host_resolver_rules_args.FindStringKey(kHostResolverRules); + host_resolver_rules_enable = !!host_resolver_rules_string; + } else if (iter.first == kNetworkErrorLoggingFieldTrialName) { + if (!iter.second.is_dict()) { + LOG(ERROR) << "\"" << iter.first << "\" config params \"" << iter.second + << "\" is not a dictionary value"; + effective_experimental_options.erase(iter.first); + continue; + } + const base::Value& nel_args = iter.second; + nel_enable = + nel_args.FindBoolKey(kNetworkErrorLoggingEnable).value_or(nel_enable); + + const auto* preloaded_report_to_headers_config = + nel_args.FindListKey(kNetworkErrorLoggingPreloadedReportToHeaders); + if (preloaded_report_to_headers_config) { + preloaded_report_to_headers = ParseNetworkErrorLoggingHeaders( + preloaded_report_to_headers_config->GetListDeprecated()); + } + + const auto* preloaded_nel_headers_config = + nel_args.FindListKey(kNetworkErrorLoggingPreloadedNELHeaders); + if (preloaded_nel_headers_config) { + preloaded_nel_headers = ParseNetworkErrorLoggingHeaders( + preloaded_nel_headers_config->GetListDeprecated()); + } + } else if (iter.first == kDisableIPv6OnWifi) { + if (!iter.second.is_bool()) { + LOG(ERROR) << "\"" << iter.first << "\" config params \"" << iter.second + << "\" is not a bool"; + effective_experimental_options.erase(iter.first); + continue; + } + disable_ipv6_on_wifi = iter.second.GetBool(); + } else if (iter.first == kSSLKeyLogFile) { + if (iter.second.is_string()) { + base::FilePath ssl_key_log_file( + base::FilePath::FromUTF8Unsafe(iter.second.GetString())); + if (!ssl_key_log_file.empty()) { + // SetSSLKeyLogger is only safe to call before any SSLClientSockets + // are created. This should not be used if there are multiple + // CronetEngine. + // TODO(xunjieli): Expose this as a stable API after crbug.com/458365 + // is resolved. + net::SSLClientSocket::SetSSLKeyLogger( + std::make_unique(ssl_key_log_file)); + } + } + } else if (iter.first == kNetworkQualityEstimatorFieldTrialName) { + if (!iter.second.is_dict()) { + LOG(ERROR) << "\"" << iter.first << "\" config params \"" << iter.second + << "\" is not a dictionary value"; + effective_experimental_options.erase(iter.first); + continue; + } + + const base::Value& nqe_args = iter.second; + const std::string* nqe_option = + nqe_args.FindStringKey(net::kForceEffectiveConnectionType); + if (nqe_option) { + nqe_forced_effective_connection_type = + net::GetEffectiveConnectionTypeForName(*nqe_option); + if (!nqe_option->empty() && !nqe_forced_effective_connection_type) { + LOG(ERROR) << "\"" << nqe_option + << "\" is not a valid effective connection type value"; + } + } + } else if (iter.first == kSpdyGoAwayOnIpChange) { + if (!iter.second.is_bool()) { + LOG(ERROR) << "\"" << iter.first << "\" config params \"" << iter.second + << "\" is not a bool"; + effective_experimental_options.erase(iter.first); + continue; + } + session_params->spdy_go_away_on_ip_change = iter.second.GetBool(); + } else { + LOG(WARNING) << "Unrecognized Cronet experimental option \"" << iter.first + << "\" with params \"" << iter.second; + effective_experimental_options.erase(iter.first); + } + } + + if (async_dns_enable || stale_dns_enable || host_resolver_rules_enable || + disable_ipv6_on_wifi) { + std::unique_ptr host_resolver; + net::HostResolver::ManagerOptions host_resolver_manager_options; + host_resolver_manager_options.insecure_dns_client_enabled = + async_dns_enable; + host_resolver_manager_options.check_ipv6_on_wifi = !disable_ipv6_on_wifi; + // TODO(crbug.com/934402): Consider using a shared HostResolverManager for + // Cronet HostResolvers. + if (stale_dns_enable) { + DCHECK(!disable_ipv6_on_wifi); + host_resolver = std::make_unique( + net::HostResolver::CreateStandaloneContextResolver( + net::NetLog::Get(), std::move(host_resolver_manager_options)), + stale_dns_options); + } else { + host_resolver = net::HostResolver::CreateStandaloneResolver( + net::NetLog::Get(), std::move(host_resolver_manager_options)); + } + if (host_resolver_rules_enable) { + std::unique_ptr remapped_resolver( + new net::MappedHostResolver(std::move(host_resolver))); + remapped_resolver->SetRulesFromString(*host_resolver_rules_string); + host_resolver = std::move(remapped_resolver); + } + context_builder->set_host_resolver(std::move(host_resolver)); + } + +#if BUILDFLAG(ENABLE_REPORTING) + if (nel_enable) { + auto policy = net::ReportingPolicy::Create(); + + // Apps (like Cronet embedders) are generally allowed to run in the + // background, even across network changes, so use more relaxed privacy + // settings than when Reporting is running in the browser. + policy->persist_reports_across_restarts = true; + policy->persist_clients_across_restarts = true; + policy->persist_reports_across_network_changes = true; + policy->persist_clients_across_network_changes = true; + + context_builder->set_reporting_policy(std::move(policy)); + context_builder->set_network_error_logging_enabled(true); + } +#endif // BUILDFLAG(ENABLE_REPORTING) +} + +void URLRequestContextConfig::ConfigureURLRequestContextBuilder( + net::URLRequestContextBuilder* context_builder) { + std::string config_cache; + if (http_cache != DISABLED) { + net::URLRequestContextBuilder::HttpCacheParams cache_params; + if (http_cache == DISK && !storage_path.empty()) { + cache_params.type = net::URLRequestContextBuilder::HttpCacheParams::DISK; + cache_params.path = base::FilePath::FromUTF8Unsafe(storage_path) + .Append(kDiskCacheDirectoryName); + } else { + cache_params.type = + net::URLRequestContextBuilder::HttpCacheParams::IN_MEMORY; + } + cache_params.max_size = http_cache_max_size; + context_builder->EnableHttpCache(cache_params); + } else { + context_builder->DisableHttpCache(); + } + context_builder->set_accept_language(accept_language); + context_builder->set_user_agent(user_agent); + net::HttpNetworkSessionParams session_params; + session_params.enable_http2 = enable_spdy; + session_params.enable_quic = enable_quic; + auto quic_context = std::make_unique(); + if (enable_quic) { + quic_context->params()->user_agent_id = quic_user_agent_id; + // Note goaway sessions on ip change will be turned on by default + // for iOS unless overrided via experiemental options. + quic_context->params()->goaway_sessions_on_ip_change = + kDefaultQuicGoAwaySessionsOnIpChange; + } + + SetContextBuilderExperimentalOptions(context_builder, &session_params, + quic_context->params()); + + context_builder->set_http_network_session_params(session_params); + context_builder->set_quic_context(std::move(quic_context)); + + if (mock_cert_verifier) + context_builder->SetCertVerifier(std::move(mock_cert_verifier)); + // Certificate Transparency is intentionally ignored in Cronet. + // See //net/docs/certificate-transparency.md for more details. + context_builder->set_ct_policy_enforcer( + std::make_unique()); + // TODO(mef): Use |config| to set cookies. +} + +URLRequestContextConfigBuilder::URLRequestContextConfigBuilder() {} +URLRequestContextConfigBuilder::~URLRequestContextConfigBuilder() {} + +std::unique_ptr +URLRequestContextConfigBuilder::Build() { + return URLRequestContextConfig::CreateURLRequestContextConfig( + enable_quic, quic_user_agent_id, enable_spdy, enable_brotli, http_cache, + http_cache_max_size, load_disable_cache, storage_path, accept_language, + user_agent, experimental_options, std::move(mock_cert_verifier), + enable_network_quality_estimator, + bypass_public_key_pinning_for_local_trust_anchors, + network_thread_priority); +} + +} // namespace cronet diff --git a/src/components/cronet/url_request_context_config.h b/src/components/cronet/url_request_context_config.h new file mode 100644 index 0000000000..6537dcd491 --- /dev/null +++ b/src/components/cronet/url_request_context_config.h @@ -0,0 +1,338 @@ +// Copyright 2014 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_URL_REQUEST_CONTEXT_CONFIG_H_ +#define COMPONENTS_CRONET_URL_REQUEST_CONTEXT_CONFIG_H_ + +#include +#include +#include + +#include "base/memory/ref_counted.h" +#include "base/time/time.h" +#include "base/values.h" +#include "net/base/hash_value.h" +#include "net/cert/cert_verifier.h" +#include "net/nqe/effective_connection_type.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "url/origin.h" + +namespace net { +class CertVerifier; +struct HttpNetworkSessionParams; +struct QuicParams; +class URLRequestContextBuilder; +} // namespace net + +namespace cronet { + +// Common configuration parameters used by Cronet to configure +// URLRequestContext. +// TODO(mgersh): This shouldn't be a struct, and experimental option parsing +// should be kept more separate from applying the configuration. +struct URLRequestContextConfig { + // Type of HTTP cache. + // GENERATED_JAVA_ENUM_PACKAGE: org.chromium.net.impl + enum HttpCacheType { + // No HTTP cache. + DISABLED, + // HTTP cache persisted to disk. + DISK, + // HTTP cache kept in memory. + MEMORY, + }; + + // App-provided hint that server supports QUIC. + struct QuicHint { + QuicHint(const std::string& host, int port, int alternate_port); + + QuicHint(const QuicHint&) = delete; + QuicHint& operator=(const QuicHint&) = delete; + + ~QuicHint(); + + // Host name of the server that supports QUIC. + const std::string host; + // Port of the server that supports QUIC. + const int port; + // Alternate protocol port. + const int alternate_port; + }; + + // Public-Key-Pinning configuration structure. + struct Pkp { + Pkp(const std::string& host, + bool include_subdomains, + const base::Time& expiration_date); + + Pkp(const Pkp&) = delete; + Pkp& operator=(const Pkp&) = delete; + + ~Pkp(); + + // Host name. + const std::string host; + // Pin hashes (currently SHA256 only). + net::HashValueVector pin_hashes; + // Indicates whether the pinning should apply to the pinned host subdomains. + const bool include_subdomains; + // Expiration date for the pins. + const base::Time expiration_date; + }; + + // Simulated headers, used to preconfigure the Reporting API and Network Error + // Logging before receiving those actual configuration headers from the + // origins. + struct PreloadedNelAndReportingHeader { + PreloadedNelAndReportingHeader(const url::Origin& origin, + std::string value); + ~PreloadedNelAndReportingHeader(); + + // Origin that is "sending" this header. + const url::Origin origin; + + // Value of the header that is "sent". + const std::string value; + }; + + URLRequestContextConfig(const URLRequestContextConfig&) = delete; + URLRequestContextConfig& operator=(const URLRequestContextConfig&) = delete; + + ~URLRequestContextConfig(); + + // Configures |context_builder| based on |this|. + void ConfigureURLRequestContextBuilder( + net::URLRequestContextBuilder* context_builder); + + // Enable QUIC. + const bool enable_quic; + // QUIC User Agent ID. + const std::string quic_user_agent_id; + // Enable SPDY. + const bool enable_spdy; + // Enable Brotli. + const bool enable_brotli; + // Type of http cache. + const HttpCacheType http_cache; + // Max size of http cache in bytes. + const int http_cache_max_size; + // Disable caching for HTTP responses. Other information may be stored in + // the cache. + const bool load_disable_cache; + // Storage path for http cache and cookie storage. + const std::string storage_path; + // Accept-Language request header field. + const std::string accept_language; + // User-Agent request header field. + const std::string user_agent; + + // Certificate verifier for testing. + std::unique_ptr mock_cert_verifier; + + // Enable Network Quality Estimator (NQE). + const bool enable_network_quality_estimator; + + // Enable public key pinning bypass for local trust anchors. + const bool bypass_public_key_pinning_for_local_trust_anchors; + + // App-provided list of servers that support QUIC. + std::vector> quic_hints; + + // The list of public key pins. + std::vector> pkp_list; + + // Enable DNS cache persistence. + bool enable_host_cache_persistence = false; + + // Minimum time in milliseconds between writing the HostCache contents to + // prefs. Only relevant when |enable_host_cache_persistence| is true. + int host_cache_persistence_delay_ms = 60000; + + // Experimental options that are recognized by the config parser. + base::Value::DictStorage effective_experimental_options; + base::Value::DictStorage experimental_options; + + // If set, forces NQE to return the set value as the effective connection + // type. + absl::optional + nqe_forced_effective_connection_type; + + // Preloaded Report-To headers, to preconfigure the Reporting API. + std::vector preloaded_report_to_headers; + + // Preloaded NEL headers, to preconfigure Network Error Logging. + std::vector preloaded_nel_headers; + + // Optional network thread priority. + // On Android, corresponds to android.os.Process.setThreadPriority() values. + // On iOS, corresponds to NSThread::setThreadPriority values. + const absl::optional network_thread_priority; + + // Whether the connection status of active bidirectional streams should be + // monitored. + bool bidi_stream_detect_broken_connection; + // If |bidi_stream_detect_broken_connection_| is true, this suggests the + // period of the heartbeat signal. + base::TimeDelta heartbeat_interval; + + static bool ExperimentalOptionsParsingIsAllowedToFail() { + return DCHECK_IS_ON(); + } + + static std::unique_ptr CreateURLRequestContextConfig( + // Enable QUIC. + bool enable_quic, + // QUIC User Agent ID. + const std::string& quic_user_agent_id, + // Enable SPDY. + bool enable_spdy, + // Enable Brotli. + bool enable_brotli, + // Type of http cache. + HttpCacheType http_cache, + // Max size of http cache in bytes. + int http_cache_max_size, + // Disable caching for HTTP responses. Other information may be stored in + // the cache. + bool load_disable_cache, + // Storage path for http cache and cookie storage. + const std::string& storage_path, + // Accept-Language request header field. + const std::string& accept_language, + // User-Agent request header field. + const std::string& user_agent, + // JSON encoded experimental options. + const std::string& unparsed_experimental_options, + // MockCertVerifier to use for testing purposes. + std::unique_ptr mock_cert_verifier, + // Enable network quality estimator. + bool enable_network_quality_estimator, + // Enable bypassing of public key pinning for local trust anchors + bool bypass_public_key_pinning_for_local_trust_anchors, + // Optional network thread priority. + // On Android, corresponds to android.os.Process.setThreadPriority() + // values. On iOS, corresponds to NSThread::setThreadPriority values. Do + // not specify for other targets. + absl::optional network_thread_priority); + + private: + URLRequestContextConfig( + // Enable QUIC. + bool enable_quic, + // QUIC User Agent ID. + const std::string& quic_user_agent_id, + // Enable SPDY. + bool enable_spdy, + // Enable Brotli. + bool enable_brotli, + // Type of http cache. + HttpCacheType http_cache, + // Max size of http cache in bytes. + int http_cache_max_size, + // Disable caching for HTTP responses. Other information may be stored in + // the cache. + bool load_disable_cache, + // Storage path for http cache and cookie storage. + const std::string& storage_path, + // Accept-Language request header field. + const std::string& accept_language, + // User-Agent request header field. + const std::string& user_agent, + // Parsed experimental options. + base::Value::DictStorage experimental_options, + // MockCertVerifier to use for testing purposes. + std::unique_ptr mock_cert_verifier, + // Enable network quality estimator. + bool enable_network_quality_estimator, + // Enable bypassing of public key pinning for local trust anchors + bool bypass_public_key_pinning_for_local_trust_anchors, + // Optional network thread priority. + // On Android, corresponds to android.os.Process.setThreadPriority() + // values. On iOS, corresponds to NSThread::setThreadPriority values. Do + // not specify for other targets. + absl::optional network_thread_priority); + + // Parses experimental options from their JSON format to the format used + // internally. + // Returns an empty optional if the operation was unsuccessful. + static absl::optional ParseExperimentalOptions( + std::string unparsed_experimental_options); + + // Makes appropriate changes to settings in |this|. + void SetContextConfigExperimentalOptions(); + + // Makes appropriate changes to settings in the URLRequestContextBuilder. + void SetContextBuilderExperimentalOptions( + net::URLRequestContextBuilder* context_builder, + net::HttpNetworkSessionParams* session_params, + net::QuicParams* quic_params); +}; + +// Stores intermediate state for URLRequestContextConfig. Initializes with +// (mostly) sane defaults, then the appropriate member variables can be +// modified, and it can be finalized with Build(). +struct URLRequestContextConfigBuilder { + URLRequestContextConfigBuilder(); + + URLRequestContextConfigBuilder(const URLRequestContextConfigBuilder&) = + delete; + URLRequestContextConfigBuilder& operator=( + const URLRequestContextConfigBuilder&) = delete; + + ~URLRequestContextConfigBuilder(); + + // Finalize state into a URLRequestContextConfig. Must only be called once, + // as once |mock_cert_verifier| is moved into a URLRequestContextConfig, it + // cannot be used again. + std::unique_ptr Build(); + + // Enable QUIC. + bool enable_quic = true; + // QUIC User Agent ID. + std::string quic_user_agent_id = ""; + // Enable SPDY. + bool enable_spdy = true; + // Enable Brotli. + bool enable_brotli = false; + // Type of http cache. + URLRequestContextConfig::HttpCacheType http_cache = + URLRequestContextConfig::DISABLED; + // Max size of http cache in bytes. + int http_cache_max_size = 0; + // Disable caching for HTTP responses. Other information may be stored in + // the cache. + bool load_disable_cache = false; + // Storage path for http cache and cookie storage. + std::string storage_path = ""; + // Accept-Language request header field. + std::string accept_language = ""; + // User-Agent request header field. + std::string user_agent = ""; + // Experimental options encoded as a string in a JSON format containing + // experiments and their corresponding configuration options. The format + // is a JSON object with the name of the experiment as the key, and the + // configuration options as the value. An example: + // {"experiment1": {"option1": "option_value1", "option2": "option_value2", + // ...}, "experiment2: {"option3", "option_value3", ...}, ...} + std::string experimental_options = "{}"; + + // Certificate verifier for testing. + std::unique_ptr mock_cert_verifier; + + // Enable network quality estimator. + bool enable_network_quality_estimator = false; + + // Enable public key pinning bypass for local trust anchors. + bool bypass_public_key_pinning_for_local_trust_anchors = true; + + // Optional network thread priority. + // On Android, corresponds to android.os.Process.setThreadPriority() values. + // On iOS, corresponds to NSThread::setThreadPriority values. + // Do not specify for other targets. + absl::optional network_thread_priority; +}; + +} // namespace cronet + +#endif // COMPONENTS_CRONET_URL_REQUEST_CONTEXT_CONFIG_H_ diff --git a/src/components/cronet/url_request_context_config_unittest.cc b/src/components/cronet/url_request_context_config_unittest.cc new file mode 100644 index 0000000000..9566be33c0 --- /dev/null +++ b/src/components/cronet/url_request_context_config_unittest.cc @@ -0,0 +1,1609 @@ +// 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 "components/cronet/url_request_context_config.h" + +#include + +#include "base/bind.h" +#include "base/check.h" +#include "base/containers/contains.h" +#include "base/json/json_writer.h" +#include "base/notreached.h" +#include "base/strings/strcat.h" +#include "base/strings/string_piece.h" +#include "base/test/task_environment.h" +#include "base/test/values_test_util.h" +#include "base/values.h" +#include "build/build_config.h" +#include "net/base/host_port_pair.h" +#include "net/base/http_user_agent_settings.h" +#include "net/base/net_errors.h" +#include "net/base/network_isolation_key.h" +#include "net/cert/cert_verifier.h" +#include "net/dns/host_resolver.h" +#include "net/dns/host_resolver_manager.h" +#include "net/http/http_network_session.h" +#include "net/log/net_log_with_source.h" +#include "net/proxy_resolution/proxy_config.h" +#include "net/proxy_resolution/proxy_config_service_fixed.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_context_builder.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if BUILDFLAG(ENABLE_REPORTING) +#include "net/network_error_logging/network_error_logging_service.h" +#include "net/reporting/reporting_service.h" +#endif // BUILDFLAG(ENABLE_REPORTING) + +namespace cronet { + +namespace { + +std::string WrapJsonHeader(base::StringPiece value) { + return base::StrCat({"[", value, "]"}); +} + +// Returns whether two JSON-encoded headers contain the same content, ignoring +// irrelevant encoding issues like whitespace and map element ordering. +bool JsonHeaderEquals(base::StringPiece expected, base::StringPiece actual) { + return base::test::ParseJson(WrapJsonHeader(expected)) == + base::test::ParseJson(WrapJsonHeader(actual)); +} + +} // namespace + +TEST(URLRequestContextConfigTest, TestExperimentalOptionParsing) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + // Create JSON for experimental options. + base::DictionaryValue options; + options.SetPath({"QUIC", "max_server_configs_stored_in_properties"}, + base::Value(2)); + options.SetPath({"QUIC", "user_agent_id"}, base::Value("Custom QUIC UAID")); + options.SetPath({"QUIC", "idle_connection_timeout_seconds"}, + base::Value(300)); + options.SetPath({"QUIC", "close_sessions_on_ip_change"}, base::Value(true)); + options.SetPath({"QUIC", "connection_options"}, base::Value("TIME,TBBR,REJ")); + options.SetPath( + {"QUIC", "set_quic_flags"}, + base::Value("FLAGS_quic_reloadable_flag_quic_testonly_default_false=true," + "FLAGS_quic_restart_flag_quic_testonly_default_true=false")); + options.SetPath({"AsyncDNS", "enable"}, base::Value(true)); + options.SetPath({"NetworkErrorLogging", "enable"}, base::Value(true)); + options.SetPath({"NetworkErrorLogging", "preloaded_report_to_headers"}, + base::test::ParseJson(R"json( + [ + { + "origin": "https://test-origin/", + "value": { + "group": "test-group", + "max_age": 86400, + "endpoints": [ + {"url": "https://test-endpoint/"}, + ], + }, + }, + { + "origin": "https://test-origin-2/", + "value": [ + { + "group": "test-group-2", + "max_age": 86400, + "endpoints": [ + {"url": "https://test-endpoint-2/"}, + ], + }, + { + "group": "test-group-3", + "max_age": 86400, + "endpoints": [ + {"url": "https://test-endpoint-3/"}, + ], + }, + ], + }, + { + "origin": "https://value-is-missing/", + }, + { + "value": "origin is missing", + }, + { + "origin": 123, + "value": "origin is not a string", + }, + { + "origin": "this is not a URL", + "value": "origin not a URL", + }, + ] + )json")); + options.SetPath({"NetworkErrorLogging", "preloaded_nel_headers"}, + base::test::ParseJson(R"json( + [ + { + "origin": "https://test-origin/", + "value": { + "report_to": "test-group", + "max_age": 86400, + }, + }, + ] + )json")); + options.SetPath({"UnknownOption", "foo"}, base::Value(true)); + options.SetPath({"HostResolverRules", "host_resolver_rules"}, + base::Value("MAP * 127.0.0.1")); + // See http://crbug.com/696569. + options.SetKey("disable_ipv6_on_wifi", base::Value(true)); + options.SetKey("spdy_go_away_on_ip_change", base::Value(true)); + options.SetPath({"QUIC", "ios_network_service_type"}, base::Value(2)); + std::string options_json; + EXPECT_TRUE(base::JSONWriter::Write(options, &options_json)); + + // Initialize QUIC flags set by the config. + FLAGS_quic_reloadable_flag_quic_testonly_default_false = false; + FLAGS_quic_restart_flag_quic_testonly_default_true = true; + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + options_json, + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional(42.0)); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + EXPECT_FALSE( + config->effective_experimental_options.contains("UnknownOption")); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + // Check Quic Connection options. + quic::QuicTagVector quic_connection_options; + quic_connection_options.push_back(quic::kTIME); + quic_connection_options.push_back(quic::kTBBR); + quic_connection_options.push_back(quic::kREJ); + EXPECT_EQ(quic_connection_options, quic_params->connection_options); + + // Check QUIC flags. + EXPECT_TRUE(FLAGS_quic_reloadable_flag_quic_testonly_default_false); + EXPECT_FALSE(FLAGS_quic_restart_flag_quic_testonly_default_true); + + // Check Custom QUIC User Agent Id. + EXPECT_EQ("Custom QUIC UAID", quic_params->user_agent_id); + + // Check max_server_configs_stored_in_properties. + EXPECT_EQ(2u, quic_params->max_server_configs_stored_in_properties); + + // Check idle_connection_timeout. + EXPECT_EQ(300, quic_params->idle_connection_timeout.InSeconds()); + + EXPECT_TRUE(quic_params->close_sessions_on_ip_change); + EXPECT_FALSE(quic_params->goaway_sessions_on_ip_change); + EXPECT_FALSE(quic_params->allow_server_migration); + EXPECT_FALSE(quic_params->migrate_sessions_on_network_change_v2); + EXPECT_FALSE(quic_params->migrate_sessions_early_v2); + EXPECT_FALSE(quic_params->migrate_idle_sessions); + EXPECT_FALSE(quic_params->retry_on_alternate_network_before_handshake); + EXPECT_FALSE(quic_params->race_stale_dns_on_connection); + EXPECT_FALSE(quic_params->go_away_on_path_degrading); + EXPECT_FALSE(quic_params->allow_port_migration); + EXPECT_FALSE(quic_params->disable_tls_zero_rtt); + EXPECT_TRUE(quic_params->retry_without_alt_svc_on_quic_errors); + + // Check network_service_type for iOS. + EXPECT_EQ(2, quic_params->ios_network_service_type); + +#if defined(ENABLE_BUILT_IN_DNS) + // Check AsyncDNS resolver is enabled (not supported on iOS). + EXPECT_TRUE(context->host_resolver()->GetDnsConfigAsValue()); +#endif // defined(ENABLE_BUILT_IN_DNS) + +#if BUILDFLAG(ENABLE_REPORTING) + // Check Reporting and Network Error Logging are enabled (can be disabled at + // build time). + EXPECT_TRUE(context->reporting_service()); + EXPECT_TRUE(context->network_error_logging_service()); +#endif // BUILDFLAG(ENABLE_REPORTING) + + ASSERT_EQ(2u, config->preloaded_report_to_headers.size()); + EXPECT_EQ(url::Origin::CreateFromNormalizedTuple("https", "test-origin", 443), + config->preloaded_report_to_headers[0].origin); + EXPECT_TRUE(JsonHeaderEquals( // + R"json( + { + "group": "test-group", + "max_age": 86400, + "endpoints": [ + {"url": "https://test-endpoint/"}, + ], + } + )json", + config->preloaded_report_to_headers[0].value)); + EXPECT_EQ( + url::Origin::CreateFromNormalizedTuple("https", "test-origin-2", 443), + config->preloaded_report_to_headers[1].origin); + EXPECT_TRUE(JsonHeaderEquals( // + R"json( + { + "group": "test-group-2", + "max_age": 86400, + "endpoints": [ + {"url": "https://test-endpoint-2/"}, + ], + }, + { + "group": "test-group-3", + "max_age": 86400, + "endpoints": [ + {"url": "https://test-endpoint-3/"}, + ], + } + )json", + config->preloaded_report_to_headers[1].value)); + + ASSERT_EQ(1u, config->preloaded_nel_headers.size()); + EXPECT_EQ(url::Origin::CreateFromNormalizedTuple("https", "test-origin", 443), + config->preloaded_nel_headers[0].origin); + EXPECT_TRUE(JsonHeaderEquals( // + R"json( + { + "report_to": "test-group", + "max_age": 86400, + } + )json", + config->preloaded_nel_headers[0].value)); + + // Check IPv6 is disabled when on wifi. + EXPECT_FALSE(context->host_resolver() + ->GetManagerForTesting() + ->check_ipv6_on_wifi_for_testing()); + + const net::HttpNetworkSessionParams* params = + context->GetNetworkSessionParams(); + EXPECT_TRUE(params->spdy_go_away_on_ip_change); + + // All host resolution expected to be mapped to an immediately-resolvable IP. + std::unique_ptr resolve_request = + context->host_resolver()->CreateRequest( + net::HostPortPair("abcde", 80), net::NetworkIsolationKey(), + net::NetLogWithSource(), absl::nullopt); + EXPECT_EQ(net::OK, resolve_request->Start( + base::BindOnce([](int error) { NOTREACHED(); }))); + + EXPECT_TRUE(config->network_thread_priority); + EXPECT_EQ(42.0, config->network_thread_priority.value()); + EXPECT_FALSE(config->bidi_stream_detect_broken_connection); +} + +TEST(URLRequestContextConfigTest, SetSupportedQuicVersion) { + // Note that this test covers the legacy mechanism which relies on + // QuicVersionToString. We should now be using ALPNs instead. + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + quic::ParsedQuicVersion version = + quic::AllSupportedVersionsWithQuicCrypto().front(); + std::string experimental_options = + "{\"QUIC\":{\"quic_version\":\"" + + quic::QuicVersionToString(version.transport_version) + "\"}}"; + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + experimental_options, + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + EXPECT_EQ(quic_params->supported_versions.size(), 1u); + EXPECT_EQ(quic_params->supported_versions[0], version); +} + +TEST(URLRequestContextConfigTest, SetSupportedQuicVersionByAlpn) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + quic::ParsedQuicVersion version = quic::AllSupportedVersions().front(); + std::string experimental_options = "{\"QUIC\":{\"quic_version\":\"" + + quic::ParsedQuicVersionToString(version) + + "\"}}"; + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + experimental_options, + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + EXPECT_EQ(quic_params->supported_versions.size(), 1u); + EXPECT_EQ(quic_params->supported_versions[0], version); +} + +TEST(URLRequestContextConfigTest, SetUnsupportedQuicVersion) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"quic_version\":\"h3-Q047\"}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + EXPECT_EQ(quic_params->supported_versions, + net::DefaultSupportedQuicVersions()); +} + +TEST(URLRequestContextConfigTest, SetObsoleteQuicVersion) { + // This test configures cronet with an obsolete QUIC version and validates + // that cronet ignores that version and uses the default versions. + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + std::string("{\"QUIC\":{\"quic_version\":\"") + + quic::ParsedQuicVersionToString( + net::ObsoleteQuicVersions().back()) + + "\"}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + EXPECT_EQ(quic_params->supported_versions, + net::DefaultSupportedQuicVersions()); +} + +TEST(URLRequestContextConfigTest, SetObsoleteQuicVersionWhenAllowed) { + // This test configures cronet with an obsolete QUIC version and explicitly + // allows it, then validates that cronet uses that version. + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + std::string("{\"QUIC\":{\"quic_version\":\"") + + quic::ParsedQuicVersionToString( + net::ObsoleteQuicVersions().back()) + + "\",\"obsolete_versions_allowed\":true}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + quic::ParsedQuicVersionVector supported_versions = { + net::ObsoleteQuicVersions().back()}; + EXPECT_EQ(quic_params->supported_versions, supported_versions); +} + +TEST(URLRequestContextConfigTest, SetQuicServerMigrationOptions) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"allow_server_migration\":true}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + EXPECT_FALSE(quic_params->close_sessions_on_ip_change); + EXPECT_TRUE(quic_params->allow_server_migration); +} + +// Test that goaway_sessions_on_ip_change is set on by default for iOS. +#if BUILDFLAG(IS_IOS) +#define MAYBE_SetQuicGoAwaySessionsOnIPChangeByDefault \ + SetQuicGoAwaySessionsOnIPChangeByDefault +#else +#define MAYBE_SetQuicGoAwaySessionsOnIPChangeByDefault \ + DISABLED_SetQuicGoAwaySessionsOnIPChangeByDefault +#endif +TEST(URLRequestContextConfigTest, + MAYBE_SetQuicGoAwaySessionsOnIPChangeByDefault) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + EXPECT_FALSE(quic_params->close_sessions_on_ip_change); + EXPECT_TRUE(quic_params->goaway_sessions_on_ip_change); +} + +// Tests that goaway_sessions_on_ip_changes can be set on via +// experimental options on non-iOS. +#if !BUILDFLAG(IS_IOS) +#define MAYBE_SetQuicGoAwaySessionsOnIPChangeViaExperimentOptions \ + SetQuicGoAwaySessionsOnIPChangeViaExperimentOptions +#else +#define MAYBE_SetQuicGoAwaySessionsOnIPChangeViaExperimentOptions \ + DISABLED_SetQuicGoAwaySessionsOnIPChangeViaExperimentOptions +#endif +TEST(URLRequestContextConfigTest, + MAYBE_SetQuicGoAwaySessionsOnIPChangeViaExperimentOptions) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"goaway_sessions_on_ip_change\":true}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + EXPECT_FALSE(quic_params->close_sessions_on_ip_change); + EXPECT_TRUE(quic_params->goaway_sessions_on_ip_change); +} + +// Test that goaway_sessions_on_ip_change can be set to false via +// experimental options on iOS. +#if BUILDFLAG(IS_IOS) +#define MAYBE_DisableQuicGoAwaySessionsOnIPChangeViaExperimentOptions \ + DisableQuicGoAwaySessionsOnIPChangeViaExperimentOptions +#else +#define MAYBE_DisableQuicGoAwaySessionsOnIPChangeViaExperimentOptions \ + DISABLED_DisableQuicGoAwaySessionsOnIPChangeViaExperimentOptions +#endif +TEST(URLRequestContextConfigTest, + MAYBE_DisableQuicGoAwaySessionsOnIPChangeViaExperimentOptions) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"goaway_sessions_on_ip_change\":false}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + EXPECT_FALSE(quic_params->close_sessions_on_ip_change); + EXPECT_FALSE(quic_params->goaway_sessions_on_ip_change); +} + +TEST(URLRequestContextConfigTest, SetQuicConnectionMigrationV2Options) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + // Explicitly turn off "goaway_sessions_on_ip_change" which is default + // enabled on iOS but cannot be simultaneously set with migration + // option. + "{\"QUIC\":{\"migrate_sessions_on_network_change_v2\":true," + "\"goaway_sessions_on_ip_change\":false," + "\"migrate_sessions_early_v2\":true," + "\"retry_on_alternate_network_before_handshake\":true," + "\"migrate_idle_sessions\":true," + "\"retransmittable_on_wire_timeout_milliseconds\":1000," + "\"idle_session_migration_period_seconds\":15," + "\"max_time_on_non_default_network_seconds\":10," + "\"max_migrations_to_non_default_network_on_write_error\":3," + "\"max_migrations_to_non_default_network_on_path_degrading\":4}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + EXPECT_TRUE(quic_params->migrate_sessions_on_network_change_v2); + EXPECT_TRUE(quic_params->migrate_sessions_early_v2); + EXPECT_TRUE(quic_params->retry_on_alternate_network_before_handshake); + EXPECT_EQ(1000, + quic_params->retransmittable_on_wire_timeout.InMilliseconds()); + EXPECT_TRUE(quic_params->migrate_idle_sessions); + EXPECT_EQ(base::Seconds(15), quic_params->idle_session_migration_period); + EXPECT_EQ(base::Seconds(10), quic_params->max_time_on_non_default_network); + EXPECT_EQ(3, + quic_params->max_migrations_to_non_default_network_on_write_error); + EXPECT_EQ( + 4, quic_params->max_migrations_to_non_default_network_on_path_degrading); + EXPECT_EQ(net::DefaultSupportedQuicVersions(), + quic_params->supported_versions); +} + +TEST(URLRequestContextConfigTest, SetQuicStaleDNSracing) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"race_stale_dns_on_connection\":true}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + EXPECT_TRUE(quic_params->race_stale_dns_on_connection); +} + +TEST(URLRequestContextConfigTest, SetQuicAllowPortMigration) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"allow_port_migration\":true}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + EXPECT_TRUE(quic_params->allow_port_migration); +} + +TEST(URLRequestContextConfigTest, DisableQuicRetryWithoutAltSvcOnQuicErrors) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"retry_without_alt_svc_on_quic_errors\":false}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + EXPECT_FALSE(quic_params->retry_without_alt_svc_on_quic_errors); +} + +TEST(URLRequestContextConfigTest, SetDisableTlsZeroRtt) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"disable_tls_zero_rtt\":true}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + EXPECT_TRUE(quic_params->disable_tls_zero_rtt); +} + +TEST(URLRequestContextConfigTest, SetQuicGoawayOnPathDegrading) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"go_away_on_path_degrading\":true}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + EXPECT_TRUE(quic_params->go_away_on_path_degrading); +} + +TEST(URLRequestContextConfigTest, SetQuicHostWhitelist) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"host_whitelist\":\"www.example.com,www.example.org\"}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::HttpNetworkSessionParams* params = + context->GetNetworkSessionParams(); + + EXPECT_TRUE(base::Contains(params->quic_host_allowlist, "www.example.com")); + EXPECT_TRUE(base::Contains(params->quic_host_allowlist, "www.example.org")); +} + +TEST(URLRequestContextConfigTest, SetQuicMaxTimeBeforeCryptoHandshake) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"max_time_before_crypto_handshake_seconds\":7," + "\"max_idle_time_before_crypto_handshake_seconds\":11}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + EXPECT_EQ(7, quic_params->max_time_before_crypto_handshake.InSeconds()); + EXPECT_EQ(11, quic_params->max_idle_time_before_crypto_handshake.InSeconds()); +} + +TEST(URLURLRequestContextConfigTest, SetQuicConnectionOptions) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"QUIC\":{\"connection_options\":\"TIME,TBBR,REJ\"," + "\"client_connection_options\":\"TBBR,1RTT\"}}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::QuicParams* quic_params = context->quic_context()->params(); + + quic::QuicTagVector connection_options; + connection_options.push_back(quic::kTIME); + connection_options.push_back(quic::kTBBR); + connection_options.push_back(quic::kREJ); + EXPECT_EQ(connection_options, quic_params->connection_options); + + quic::QuicTagVector client_connection_options; + client_connection_options.push_back(quic::kTBBR); + client_connection_options.push_back(quic::k1RTT); + EXPECT_EQ(client_connection_options, quic_params->client_connection_options); +} + +TEST(URLURLRequestContextConfigTest, SetAcceptLanguageAndUserAgent) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + EXPECT_EQ("foreign-language", + context->http_user_agent_settings()->GetAcceptLanguage()); + EXPECT_EQ("fake agent", context->http_user_agent_settings()->GetUserAgent()); +} + +TEST(URLURLRequestContextConfigTest, TurningOffQuic) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + false, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::HttpNetworkSessionParams* params = + context->GetNetworkSessionParams(); + EXPECT_EQ(false, params->enable_quic); +} + +TEST(URLURLRequestContextConfigTest, DefaultEnableQuic) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + URLRequestContextConfigBuilder config_builder; + std::unique_ptr config = config_builder.Build(); + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::HttpNetworkSessionParams* params = + context->GetNetworkSessionParams(); + EXPECT_EQ(true, params->enable_quic); +} + +TEST(URLRequestContextConfigTest, SetSpdyGoAwayOnIPChange) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"spdy_go_away_on_ip_change\":false}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + // Set a ProxyConfigService to avoid DCHECK failure when building. + builder.set_proxy_config_service( + std::make_unique( + net::ProxyConfigWithAnnotation::CreateDirect())); + std::unique_ptr context(builder.Build()); + const net::HttpNetworkSessionParams* params = + context->GetNetworkSessionParams(); + EXPECT_FALSE(params->spdy_go_away_on_ip_change); +} + +TEST(URLRequestContextConfigTest, WrongSpdyGoAwayOnIPChangeValue) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"spdy_go_away_on_ip_change\":\"not a bool\"}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + EXPECT_FALSE(config->effective_experimental_options.contains( + "spdy_go_away_on_ip_change")); +} + +TEST(URLRequestContextConfigTest, BidiStreamDetectBrokenConnection) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"bidi_stream_detect_broken_connection\":10}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + EXPECT_TRUE(config->effective_experimental_options.contains( + "bidi_stream_detect_broken_connection")); + EXPECT_TRUE(config->bidi_stream_detect_broken_connection); + EXPECT_EQ(config->heartbeat_interval, base::Seconds(10)); +} + +TEST(URLRequestContextConfigTest, WrongBidiStreamDetectBrokenConnectionValue) { + base::test::TaskEnvironment task_environment_( + base::test::TaskEnvironment::MainThreadType::IO); + + std::unique_ptr config = + URLRequestContextConfig::CreateURLRequestContextConfig( + // Enable QUIC. + true, + // QUIC User Agent ID. + "Default QUIC User Agent ID", + // Enable SPDY. + true, + // Enable Brotli. + false, + // Type of http cache. + URLRequestContextConfig::HttpCacheType::DISK, + // Max size of http cache in bytes. + 1024000, + // Disable caching for HTTP responses. Other information may be stored + // in the cache. + false, + // Storage path for http cache and cookie storage. + "/data/data/org.chromium.net/app_cronet_test/test_storage", + // Accept-Language request header field. + "foreign-language", + // User-Agent request header field. + "fake agent", + // JSON encoded experimental options. + "{\"bidi_stream_detect_broken_connection\": \"not an int\"}", + // MockCertVerifier to use for testing purposes. + std::unique_ptr(), + // Enable network quality estimator. + false, + // Enable Public Key Pinning bypass for local trust anchors. + true, + // Optional network thread priority. + absl::optional()); + + net::URLRequestContextBuilder builder; + config->ConfigureURLRequestContextBuilder(&builder); + EXPECT_FALSE(config->effective_experimental_options.contains( + "bidi_stream_detect_broken_connection")); +} + +// See stale_host_resolver_unittest.cc for test of StaleDNS options. + +} // namespace cronet diff --git a/src/components/cronet/version.h.in b/src/components/cronet/version.h.in new file mode 100644 index 0000000000..013b667b58 --- /dev/null +++ b/src/components/cronet/version.h.in @@ -0,0 +1,12 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source is governed by a BSD-style license that can be +// found in the LICENSE file. + +// version.h is generated from version.h.in. Edit the source! + +#ifndef COMPONENTS_CRONET_VERSION_H_ +#define COMPONENTS_CRONET_VERSION_H_ + +#define CRONET_VERSION "@VERSION_FULL@" + +#endif // COMPONENTS_CRONET_VERSION_H_ diff --git a/src/components/grpc_support/BUILD.gn b/src/components/grpc_support/BUILD.gn new file mode 100644 index 0000000000..133aa30159 --- /dev/null +++ b/src/components/grpc_support/BUILD.gn @@ -0,0 +1,49 @@ +source_set("headers") { + public = [ "include/bidirectional_stream_c.h" ] +} + +source_set("grpc_support") { + sources = [ + "bidirectional_stream.cc", + "bidirectional_stream.h", + "bidirectional_stream_c.cc", + "include/bidirectional_stream_c.h", + ] + + deps = [ + ":headers", + "//base", + "//net", + "//url", + ] +} + +# Depends on ":grpc_support" implementation. +source_set("bidirectional_stream_unittest") { + testonly = true + sources = [ "bidirectional_stream_unittest.cc" ] + + deps = [ + ":grpc_support", + "//base", + "//net", + "//net:test_support", + ] + + public_deps = [ "//components/grpc_support/test:get_stream_engine_header" ] +} + +# Depends on ":headers" to avoid ":grpc_support" implementation. +source_set("bidirectional_stream_test") { + testonly = true + sources = [ "bidirectional_stream_unittest.cc" ] + + deps = [ + ":headers", + "//base", + "//net", + "//net:test_support", + ] + + public_deps = [ "//components/grpc_support/test:get_stream_engine_header" ] +} diff --git a/src/components/grpc_support/DEPS b/src/components/grpc_support/DEPS new file mode 100644 index 0000000000..8fa9d48d88 --- /dev/null +++ b/src/components/grpc_support/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+net", +] diff --git a/src/components/grpc_support/DIR_METADATA b/src/components/grpc_support/DIR_METADATA new file mode 100644 index 0000000000..9e5f77e85f --- /dev/null +++ b/src/components/grpc_support/DIR_METADATA @@ -0,0 +1,5 @@ +monorail { + component: "Internals>Network>Library" +} + +team_email: "net-dev@chromium.org" diff --git a/src/components/grpc_support/OWNERS b/src/components/grpc_support/OWNERS new file mode 100644 index 0000000000..cbf009022d --- /dev/null +++ b/src/components/grpc_support/OWNERS @@ -0,0 +1,3 @@ +gcasto@chromium.org +xyzzyz@chromium.org +file://net/OWNERS diff --git a/src/components/grpc_support/README.md b/src/components/grpc_support/README.md new file mode 100644 index 0000000000..9449c50d1e --- /dev/null +++ b/src/components/grpc_support/README.md @@ -0,0 +1,14 @@ +gRPC Support +=== + +This directory contains the interface and implementation of the API to use +an external network stack from gRPC. The implementation is essentially a thin +wrapper around net::BidirectionalStream. The API specifies that the caller to +gRPC will pass in an opaque binary blob (stream_engine) that can be used to +created binary streams. In Chromium, this binary blob is a +net::URLRequestContextGetter, which is used by grpc_support::BidirectionalStream +to drive a net::BidirectionalStream. + +Currently Cronet (//components/cronet/ios) is the only consumer of this API, +but eventually code inside of Chromium should be able to use gRPC by providing +a net::URLRequestContextGetter. diff --git a/src/components/grpc_support/bidirectional_stream.cc b/src/components/grpc_support/bidirectional_stream.cc new file mode 100644 index 0000000000..2bb1ddbfa9 --- /dev/null +++ b/src/components/grpc_support/bidirectional_stream.cc @@ -0,0 +1,407 @@ +// Copyright 2016 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 "components/grpc_support/bidirectional_stream.h" + +#include +#include +#include +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/memory/ref_counted.h" +#include "base/strings/abseil_string_conversions.h" +#include "base/strings/string_number_conversions.h" +#include "base/task/single_thread_task_runner.h" +#include "net/base/http_user_agent_settings.h" +#include "net/base/io_buffer.h" +#include "net/base/net_errors.h" +#include "net/base/request_priority.h" +#include "net/http/bidirectional_stream.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/ssl/ssl_info.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_context_getter.h" +#include "url/gurl.h" + +namespace grpc_support { + +BidirectionalStream::WriteBuffers::WriteBuffers() {} + +BidirectionalStream::WriteBuffers::~WriteBuffers() {} + +void BidirectionalStream::WriteBuffers::Clear() { + write_buffer_list.clear(); + write_buffer_len_list.clear(); +} + +void BidirectionalStream::WriteBuffers::AppendBuffer( + const scoped_refptr& buffer, + int buffer_size) { + write_buffer_list.push_back(buffer); + write_buffer_len_list.push_back(buffer_size); +} + +void BidirectionalStream::WriteBuffers::MoveTo(WriteBuffers* target) { + std::move(write_buffer_list.begin(), write_buffer_list.end(), + std::back_inserter(target->write_buffer_list)); + std::move(write_buffer_len_list.begin(), write_buffer_len_list.end(), + std::back_inserter(target->write_buffer_len_list)); + Clear(); +} + +bool BidirectionalStream::WriteBuffers::Empty() const { + return write_buffer_list.empty(); +} + +BidirectionalStream::BidirectionalStream( + net::URLRequestContextGetter* request_context_getter, + Delegate* delegate) + : read_state_(NOT_STARTED), + write_state_(NOT_STARTED), + write_end_of_stream_(false), + request_headers_sent_(false), + disable_auto_flush_(false), + delay_headers_until_flush_(false), + request_context_getter_(request_context_getter), + pending_write_data_(new WriteBuffers()), + flushing_write_data_(new WriteBuffers()), + sending_write_data_(new WriteBuffers()), + delegate_(delegate) { + weak_this_ = weak_factory_.GetWeakPtr(); +} + +BidirectionalStream::~BidirectionalStream() { + DCHECK(IsOnNetworkThread()); +} + +int BidirectionalStream::Start(const char* url, + int priority, + const char* method, + const net::HttpRequestHeaders& headers, + bool end_of_stream) { + // Prepare request info here to be able to return the error. + std::unique_ptr request_info( + new net::BidirectionalStreamRequestInfo()); + request_info->url = GURL(url); + request_info->priority = static_cast(priority); + // Http method is a token, just as header name. + request_info->method = method; + if (!net::HttpUtil::IsValidHeaderName(request_info->method)) + return -1; + request_info->extra_headers.CopyFrom(headers); + request_info->end_stream_on_headers = end_of_stream; + write_end_of_stream_ = end_of_stream; + PostToNetworkThread(FROM_HERE, + base::BindOnce(&BidirectionalStream::StartOnNetworkThread, + weak_this_, std::move(request_info))); + return 0; +} + +bool BidirectionalStream::ReadData(char* buffer, int capacity) { + if (!buffer) + return false; + scoped_refptr read_buffer = + base::MakeRefCounted(buffer); + + PostToNetworkThread( + FROM_HERE, base::BindOnce(&BidirectionalStream::ReadDataOnNetworkThread, + weak_this_, std::move(read_buffer), capacity)); + return true; +} + +bool BidirectionalStream::WriteData(const char* buffer, + int count, + bool end_of_stream) { + if (!buffer) + return false; + + scoped_refptr write_buffer = + base::MakeRefCounted(buffer); + + PostToNetworkThread( + FROM_HERE, + base::BindOnce(&BidirectionalStream::WriteDataOnNetworkThread, weak_this_, + std::move(write_buffer), count, end_of_stream)); + + return true; +} + +void BidirectionalStream::Flush() { + PostToNetworkThread( + FROM_HERE, + base::BindOnce(&BidirectionalStream::FlushOnNetworkThread, weak_this_)); +} + +void BidirectionalStream::Cancel() { + PostToNetworkThread( + FROM_HERE, + base::BindOnce(&BidirectionalStream::CancelOnNetworkThread, weak_this_)); +} + +void BidirectionalStream::Destroy() { + // 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. + PostToNetworkThread( + FROM_HERE, base::BindOnce(&BidirectionalStream::DestroyOnNetworkThread, + base::Unretained(this))); +} + +void BidirectionalStream::OnStreamReady(bool request_headers_sent) { + DCHECK(IsOnNetworkThread()); + DCHECK_EQ(STARTED, write_state_); + if (!bidi_stream_) + return; + request_headers_sent_ = request_headers_sent; + write_state_ = WAITING_FOR_FLUSH; + if (write_end_of_stream_) { + if (!request_headers_sent) { + // If there is no data to write, then just send headers explicitly. + bidi_stream_->SendRequestHeaders(); + request_headers_sent_ = true; + } + write_state_ = WRITING_DONE; + } + delegate_->OnStreamReady(); +} + +void BidirectionalStream::OnHeadersReceived( + const spdy::Http2HeaderBlock& response_headers) { + DCHECK(IsOnNetworkThread()); + DCHECK_EQ(STARTED, read_state_); + if (!bidi_stream_) + return; + read_state_ = WAITING_FOR_READ; + // Get http status code from response headers. + int http_status_code = 0; + const auto http_status_header = response_headers.find(":status"); + if (http_status_header != response_headers.end()) + base::StringToInt(base::StringViewToStringPiece(http_status_header->second), + &http_status_code); + const char* protocol = "unknown"; + switch (bidi_stream_->GetProtocol()) { + case net::kProtoHTTP2: + protocol = "h2"; + break; + case net::kProtoQUIC: + protocol = "quic/1+spdy/3"; + break; + default: + break; + } + delegate_->OnHeadersReceived(response_headers, protocol); +} + +void BidirectionalStream::OnDataRead(int bytes_read) { + DCHECK(IsOnNetworkThread()); + DCHECK_EQ(READING, read_state_); + if (!bidi_stream_) + return; + read_state_ = WAITING_FOR_READ; + delegate_->OnDataRead(read_buffer_->data(), bytes_read); + + // Free the read buffer. + read_buffer_ = nullptr; + if (bytes_read == 0) + read_state_ = READING_DONE; + MaybeOnSucceded(); +} + +void BidirectionalStream::OnDataSent() { + DCHECK(IsOnNetworkThread()); + if (!bidi_stream_) + return; + DCHECK_EQ(WRITING, write_state_); + write_state_ = WAITING_FOR_FLUSH; + for (const scoped_refptr& buffer : + sending_write_data_->buffers()) { + delegate_->OnDataSent(buffer->data()); + } + sending_write_data_->Clear(); + // Send data flushed while other data was sending. + if (!flushing_write_data_->Empty()) { + SendFlushingWriteData(); + return; + } + if (write_end_of_stream_ && pending_write_data_->Empty()) { + write_state_ = WRITING_DONE; + MaybeOnSucceded(); + } +} + +void BidirectionalStream::OnTrailersReceived( + const spdy::Http2HeaderBlock& response_trailers) { + DCHECK(IsOnNetworkThread()); + if (!bidi_stream_) + return; + delegate_->OnTrailersReceived(response_trailers); +} + +void BidirectionalStream::OnFailed(int error) { + DCHECK(IsOnNetworkThread()); + if (!bidi_stream_ && read_state_ != NOT_STARTED) + return; + read_state_ = write_state_ = ERR; + weak_factory_.InvalidateWeakPtrs(); + // Delete underlying |bidi_stream_| asynchronously as it may still be used. + PostToNetworkThread( + FROM_HERE, base::BindOnce(&base::DeletePointer, + bidi_stream_.release())); + delegate_->OnFailed(error); +} + +void BidirectionalStream::StartOnNetworkThread( + std::unique_ptr request_info) { + DCHECK(IsOnNetworkThread()); + DCHECK(!bidi_stream_); + DCHECK(request_context_getter_->GetURLRequestContext()); + net::URLRequestContext* request_context = + request_context_getter_->GetURLRequestContext(); + request_info->extra_headers.SetHeaderIfMissing( + net::HttpRequestHeaders::kUserAgent, + request_context->http_user_agent_settings()->GetUserAgent()); + bidi_stream_ = std::make_unique( + std::move(request_info), + request_context->http_transaction_factory()->GetSession(), + !delay_headers_until_flush_, this); + DCHECK(read_state_ == NOT_STARTED && write_state_ == NOT_STARTED); + read_state_ = write_state_ = STARTED; +} + +void BidirectionalStream::ReadDataOnNetworkThread( + scoped_refptr read_buffer, + int buffer_size) { + DCHECK(IsOnNetworkThread()); + DCHECK(read_buffer); + DCHECK(!read_buffer_); + if (read_state_ != WAITING_FOR_READ) { + DLOG(ERROR) << "Unexpected Read Data in read_state " << read_state_; + // Invoke OnFailed unless it is already invoked. + if (read_state_ != ERR) + OnFailed(net::ERR_UNEXPECTED); + return; + } + read_state_ = READING; + 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 BidirectionalStream::WriteDataOnNetworkThread( + scoped_refptr write_buffer, + int buffer_size, + bool end_of_stream) { + DCHECK(IsOnNetworkThread()); + DCHECK(write_buffer); + DCHECK(!write_end_of_stream_); + if (!bidi_stream_ || write_end_of_stream_) { + DLOG(ERROR) << "Unexpected Flush Data in write_state " << write_state_; + // Invoke OnFailed unless it is already invoked. + if (write_state_ != ERR) + OnFailed(net::ERR_UNEXPECTED); + return; + } + pending_write_data_->AppendBuffer(write_buffer, buffer_size); + write_end_of_stream_ = end_of_stream; + if (!disable_auto_flush_) + FlushOnNetworkThread(); +} + +void BidirectionalStream::FlushOnNetworkThread() { + DCHECK(IsOnNetworkThread()); + if (!bidi_stream_) + return; + // If there is no data to flush, may need to send headers. + if (pending_write_data_->Empty()) { + if (!request_headers_sent_) { + request_headers_sent_ = true; + bidi_stream_->SendRequestHeaders(); + } + return; + } + // If request headers are not sent yet, they will be sent with the data. + if (!request_headers_sent_) + request_headers_sent_ = true; + + // Move pending data to the flushing list. + pending_write_data_->MoveTo(flushing_write_data_.get()); + DCHECK(pending_write_data_->Empty()); + if (write_state_ != WRITING) + SendFlushingWriteData(); +} + +void BidirectionalStream::SendFlushingWriteData() { + DCHECK(bidi_stream_); + // If previous send is not done, or there is nothing to flush, then exit. + if (write_state_ == WRITING || flushing_write_data_->Empty()) + return; + DCHECK(sending_write_data_->Empty()); + write_state_ = WRITING; + flushing_write_data_->MoveTo(sending_write_data_.get()); + bidi_stream_->SendvData(sending_write_data_->buffers(), + sending_write_data_->lengths(), + write_end_of_stream_ && pending_write_data_->Empty()); +} + +void BidirectionalStream::CancelOnNetworkThread() { + DCHECK(IsOnNetworkThread()); + if (!bidi_stream_) + return; + read_state_ = write_state_ = CANCELED; + bidi_stream_.reset(); + weak_factory_.InvalidateWeakPtrs(); + delegate_->OnCanceled(); +} + +void BidirectionalStream::DestroyOnNetworkThread() { + DCHECK(IsOnNetworkThread()); + delete this; +} + +void BidirectionalStream::MaybeOnSucceded() { + DCHECK(IsOnNetworkThread()); + if (!bidi_stream_) + return; + if (read_state_ == READING_DONE && write_state_ == WRITING_DONE) { + read_state_ = write_state_ = SUCCESS; + weak_factory_.InvalidateWeakPtrs(); + // Delete underlying |bidi_stream_| asynchronously as it may still be used. + PostToNetworkThread( + FROM_HERE, + base::BindOnce(&base::DeletePointer, + bidi_stream_.release())); + delegate_->OnSucceeded(); + } +} + +bool BidirectionalStream::IsOnNetworkThread() { + return request_context_getter_->GetNetworkTaskRunner() + ->BelongsToCurrentThread(); +} + +void BidirectionalStream::PostToNetworkThread(const base::Location& from_here, + base::OnceClosure task) { + request_context_getter_->GetNetworkTaskRunner()->PostTask(from_here, + std::move(task)); +} + +} // namespace grpc_support diff --git a/src/components/grpc_support/bidirectional_stream.h b/src/components/grpc_support/bidirectional_stream.h new file mode 100644 index 0000000000..4c39bb3b27 --- /dev/null +++ b/src/components/grpc_support/bidirectional_stream.h @@ -0,0 +1,247 @@ +// Copyright 2016 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_GRPC_SUPPORT_BIDIRECTIONAL_STREAM_H_ +#define COMPONENTS_GRPC_SUPPORT_BIDIRECTIONAL_STREAM_H_ + +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/synchronization/lock.h" +#include "net/http/bidirectional_stream.h" +#include "net/third_party/quiche/src/spdy/core/spdy_header_block.h" +#include "net/url_request/url_request_context_getter.h" + +namespace base { +class Location; +} // namespace base + +namespace net { +class HttpRequestHeaders; +class WrappedIOBuffer; +} // namespace net + +namespace grpc_support { + +// An adapter to net::BidirectionalStream. +// Created and configured from any 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 Delegate are done on the network thread. +// The app is expected to initiate the next step like ReadData or Destroy. +// Public methods can be called on any thread. +class BidirectionalStream : public net::BidirectionalStream::Delegate { + public: + class Delegate { + public: + virtual void OnStreamReady() = 0; + + virtual void OnHeadersReceived( + const spdy::Http2HeaderBlock& response_headers, + const char* negotiated_protocol) = 0; + + virtual void OnDataRead(char* data, int size) = 0; + + virtual void OnDataSent(const char* data) = 0; + + virtual void OnTrailersReceived(const spdy::Http2HeaderBlock& trailers) = 0; + + virtual void OnSucceeded() = 0; + + virtual void OnFailed(int error) = 0; + + virtual void OnCanceled() = 0; + }; + + BidirectionalStream(net::URLRequestContextGetter* request_context_getter, + Delegate* delegate); + + BidirectionalStream(const BidirectionalStream&) = delete; + BidirectionalStream& operator=(const BidirectionalStream&) = delete; + + ~BidirectionalStream() override; + + // Disables automatic flushing of each buffer passed to WriteData(). + void disable_auto_flush(bool disable_auto_flush) { + disable_auto_flush_ = disable_auto_flush; + } + + // Delays sending request headers until first call to Flush(). + void delay_headers_until_flush(bool delay_headers_until_flush) { + delay_headers_until_flush_ = delay_headers_until_flush; + } + + // Validates method and headers, initializes and starts the request. If + // |end_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 |method| is not valid HTTP method name. + // Returns position of invalid header value in |headers| if header name is + // not valid. + int Start(const char* url, + int priority, + const char* method, + const net::HttpRequestHeaders& headers, + bool end_of_stream); + + // Reads more data into |buffer| up to |capacity| bytes. + bool ReadData(char* buffer, int capacity); + + // Writes |count| bytes of data from |buffer|. The |end_of_stream| is + // passed to remote to indicate end of stream. + bool WriteData(const char* buffer, int count, bool end_of_stream); + + // Sends buffers passed to WriteData(). + void Flush(); + + // Cancels the request. The OnCanceled callback is invoked when request is + // caneceled, and not other callbacks are invoked afterwards.. + void Cancel(); + + // Releases all resources for the request and deletes the object itself. + void Destroy(); + + private: + // States of BidirectionalStream are tracked in |read_state_| and + // |write_state_|. + // The write state is separated as it changes independently of the read state. + // There is one initial state: NOT_STARTED. There is one normal final state: + // SUCCESS, reached after READING_DONE and WRITING_DONE. There are two + // exceptional final states: CANCELED and ERROR, which can be reached from + // any other non-final state. + enum State { + // Initial state, stream not started. + NOT_STARTED, + // Stream started, request headers are being sent. + STARTED, + // Waiting for ReadData() to be called. + WAITING_FOR_READ, + // Reading from the remote, OnDataRead callback will be invoked 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. + ERR, + // Reading and writing are done, and the stream is closed successfully. + SUCCESS, + // Waiting for Flush() to be called. + WAITING_FOR_FLUSH, + // Writing to the remote, callback will be invoked when done. + WRITING, + // There is no more data to write and stream is half-closed by the local + // side. + WRITING_DONE, + }; + + // Container to hold buffers and sizes of the pending data to be written. + class WriteBuffers { + public: + WriteBuffers(); + + WriteBuffers(const WriteBuffers&) = delete; + WriteBuffers& operator=(const WriteBuffers&) = delete; + + ~WriteBuffers(); + + // Clears Write Buffers list. + void Clear(); + + // Appends |buffer| of |buffer_size| length to the end of buffer list. + void AppendBuffer(const scoped_refptr& buffer, + int buffer_size); + + void MoveTo(WriteBuffers* target); + + // Returns true of Write Buffers list is empty. + bool Empty() const; + + const std::vector>& buffers() const { + return write_buffer_list; + } + + const std::vector& lengths() const { return write_buffer_len_list; } + + private: + // Every IOBuffer in |write_buffer_list| points to the memory owned by the + // application. + std::vector> write_buffer_list; + // A list of the length of each IOBuffer in |write_buffer_list|. + std::vector write_buffer_len_list; + }; + + // net::BidirectionalStream::Delegate implementations: + void OnStreamReady(bool request_headers_sent) override; + void OnHeadersReceived( + const spdy::Http2HeaderBlock& response_headers) override; + void OnDataRead(int bytes_read) override; + void OnDataSent() override; + void OnTrailersReceived(const spdy::Http2HeaderBlock& trailers) override; + void OnFailed(int error) override; + // Helper method to derive OnSucceeded. + void MaybeOnSucceded(); + + void StartOnNetworkThread( + std::unique_ptr request_info); + void ReadDataOnNetworkThread(scoped_refptr read_buffer, + int buffer_size); + void WriteDataOnNetworkThread(scoped_refptr read_buffer, + int buffer_size, + bool end_of_stream); + void FlushOnNetworkThread(); + void SendFlushingWriteData(); + void CancelOnNetworkThread(); + void DestroyOnNetworkThread(); + + bool IsOnNetworkThread(); + void PostToNetworkThread(const base::Location& from_here, + base::OnceClosure task); + + // Read state is tracking reading flow. Only accessed on network thread. + // | <--- READING <--- | + // | | + // | | + // NOT_STARTED -> STARTED --> WAITING_FOR_READ -> READING_DONE -> SUCCESS + State read_state_; + + // Write state is tracking writing flow. Only accessed on network thread. + // | <--- WRITING <--- | + // | | + // | | + // NOT_STARTED -> STARTED --> WAITING_FOR_FLUSH -> WRITING_DONE -> SUCCESS + State write_state_; + + bool write_end_of_stream_; + bool request_headers_sent_; + + bool disable_auto_flush_; + bool delay_headers_until_flush_; + + const raw_ptr request_context_getter_; + + scoped_refptr read_buffer_; + + // Write data that is pending the flush. + std::unique_ptr pending_write_data_; + // Write data that is flushed, but not sending yet. + std::unique_ptr flushing_write_data_; + // Write data that is sending. + std::unique_ptr sending_write_data_; + + std::unique_ptr bidi_stream_; + raw_ptr delegate_; + + base::WeakPtr weak_this_; + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace grpc_support + +#endif // COMPONENTS_GRPC_SUPPORT_BIDIRECTIONAL_STREAM_H_ diff --git a/src/components/grpc_support/bidirectional_stream_c.cc b/src/components/grpc_support/bidirectional_stream_c.cc new file mode 100644 index 0000000000..ea58964eda --- /dev/null +++ b/src/components/grpc_support/bidirectional_stream_c.cc @@ -0,0 +1,304 @@ +// Copyright 2016 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 "components/grpc_support/include/bidirectional_stream_c.h" + +#include + +#include +#include +#include + +#include "base/bind.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/task/single_thread_task_runner.h" +#include "components/grpc_support/bidirectional_stream.h" +#include "net/base/io_buffer.h" +#include "net/base/net_errors.h" +#include "net/base/request_priority.h" +#include "net/http/bidirectional_stream.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/ssl/ssl_info.h" +#include "net/third_party/quiche/src/spdy/core/spdy_header_block.h" +#include "net/url_request/url_request_context.h" +#include "url/gurl.h" + +namespace { + +class HeadersArray : public bidirectional_stream_header_array { + public: + explicit HeadersArray(const spdy::Http2HeaderBlock& header_block); + + HeadersArray(const HeadersArray&) = delete; + HeadersArray& operator=(const HeadersArray&) = delete; + + ~HeadersArray(); + + private: + base::StringPairs headers_strings_; +}; + +HeadersArray::HeadersArray(const spdy::Http2HeaderBlock& header_block) + : headers_strings_(header_block.size()) { + // Split coalesced headers by '\0' and copy them into |header_strings_|. + for (const auto& it : header_block) { + auto value = std::string(it.second); + size_t start = 0; + size_t end = 0; + do { + end = value.find('\0', start); + std::string split_value; + if (end != value.npos) { + split_value = value.substr(start, end - start); + } else { + split_value = value.substr(start); + } + // |headers_strings_| is initialized to the size of header_block, but + // split headers might take up more space. + headers_strings_.push_back( + std::make_pair(std::string(it.first), split_value)); + start = end + 1; + } while (end != value.npos); + } + count = capacity = headers_strings_.size(); + headers = new bidirectional_stream_header[count]; + size_t i = 0; + for (const auto& it : headers_strings_) { + headers[i].key = it.first.c_str(); + headers[i].value = it.second.c_str(); + ++i; + } +} + +HeadersArray::~HeadersArray() { + delete[] headers; +} + +class BidirectionalStreamAdapter + : public grpc_support::BidirectionalStream::Delegate { + public: + BidirectionalStreamAdapter(stream_engine* engine, + void* annotation, + bidirectional_stream_callback* callback); + + virtual ~BidirectionalStreamAdapter(); + + void OnStreamReady() override; + + void OnHeadersReceived(const spdy::Http2HeaderBlock& headers_block, + const char* negotiated_protocol) override; + + void OnDataRead(char* data, int size) override; + + void OnDataSent(const char* data) override; + + void OnTrailersReceived( + const spdy::Http2HeaderBlock& trailers_block) override; + + void OnSucceeded() override; + + void OnFailed(int error) override; + + void OnCanceled() override; + + bidirectional_stream* c_stream() const { return c_stream_.get(); } + + static grpc_support::BidirectionalStream* GetStream( + bidirectional_stream* stream); + + static void DestroyAdapterForStream(bidirectional_stream* stream); + + private: + void DestroyOnNetworkThread(); + + // None of these objects are owned by |this|. + raw_ptr request_context_getter_; + raw_ptr bidirectional_stream_; + // C side + std::unique_ptr c_stream_; + raw_ptr c_callback_; +}; + +BidirectionalStreamAdapter::BidirectionalStreamAdapter( + stream_engine* engine, + void* annotation, + bidirectional_stream_callback* callback) + : request_context_getter_( + reinterpret_cast(engine->obj)), + c_stream_(std::make_unique()), + c_callback_(callback) { + DCHECK(request_context_getter_); + bidirectional_stream_ = + new grpc_support::BidirectionalStream(request_context_getter_, this); + c_stream_->obj = this; + c_stream_->annotation = annotation; +} + +BidirectionalStreamAdapter::~BidirectionalStreamAdapter() {} + +void BidirectionalStreamAdapter::OnStreamReady() { + DCHECK(c_callback_->on_response_headers_received); + c_callback_->on_stream_ready(c_stream()); +} + +void BidirectionalStreamAdapter::OnHeadersReceived( + const spdy::Http2HeaderBlock& headers_block, + const char* negotiated_protocol) { + DCHECK(c_callback_->on_response_headers_received); + HeadersArray response_headers(headers_block); + c_callback_->on_response_headers_received(c_stream(), &response_headers, + negotiated_protocol); +} + +void BidirectionalStreamAdapter::OnDataRead(char* data, int size) { + DCHECK(c_callback_->on_read_completed); + c_callback_->on_read_completed(c_stream(), data, size); +} + +void BidirectionalStreamAdapter::OnDataSent(const char* data) { + DCHECK(c_callback_->on_write_completed); + c_callback_->on_write_completed(c_stream(), data); +} + +void BidirectionalStreamAdapter::OnTrailersReceived( + const spdy::Http2HeaderBlock& trailers_block) { + DCHECK(c_callback_->on_response_trailers_received); + HeadersArray response_trailers(trailers_block); + c_callback_->on_response_trailers_received(c_stream(), &response_trailers); +} + +void BidirectionalStreamAdapter::OnSucceeded() { + DCHECK(c_callback_->on_succeded); + c_callback_->on_succeded(c_stream()); +} + +void BidirectionalStreamAdapter::OnFailed(int error) { + DCHECK(c_callback_->on_failed); + c_callback_->on_failed(c_stream(), error); +} + +void BidirectionalStreamAdapter::OnCanceled() { + DCHECK(c_callback_->on_canceled); + c_callback_->on_canceled(c_stream()); +} + +grpc_support::BidirectionalStream* BidirectionalStreamAdapter::GetStream( + bidirectional_stream* stream) { + DCHECK(stream); + BidirectionalStreamAdapter* adapter = + static_cast(stream->obj); + DCHECK(adapter->c_stream() == stream); + DCHECK(adapter->bidirectional_stream_); + return adapter->bidirectional_stream_; +} + +void BidirectionalStreamAdapter::DestroyAdapterForStream( + bidirectional_stream* stream) { + DCHECK(stream); + BidirectionalStreamAdapter* adapter = + static_cast(stream->obj); + DCHECK(adapter->c_stream() == stream); + // 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. + adapter->bidirectional_stream_->Destroy(); + adapter->request_context_getter_->GetNetworkTaskRunner()->PostTask( + FROM_HERE, + base::BindOnce(&BidirectionalStreamAdapter::DestroyOnNetworkThread, + base::Unretained(adapter))); +} + +void BidirectionalStreamAdapter::DestroyOnNetworkThread() { + DCHECK(request_context_getter_->GetNetworkTaskRunner() + ->BelongsToCurrentThread()); + delete this; +} + +} // namespace + +bidirectional_stream* bidirectional_stream_create( + stream_engine* engine, + void* annotation, + bidirectional_stream_callback* callback) { + // Allocate new C++ adapter that will invoke |callback|. + BidirectionalStreamAdapter* stream_adapter = + new BidirectionalStreamAdapter(engine, annotation, callback); + return stream_adapter->c_stream(); +} + +int bidirectional_stream_destroy(bidirectional_stream* stream) { + BidirectionalStreamAdapter::DestroyAdapterForStream(stream); + return 1; +} + +void bidirectional_stream_disable_auto_flush(bidirectional_stream* stream, + bool disable_auto_flush) { + BidirectionalStreamAdapter::GetStream(stream)->disable_auto_flush( + disable_auto_flush); +} + +void bidirectional_stream_delay_request_headers_until_flush( + bidirectional_stream* stream, + bool delay_headers_until_flush) { + BidirectionalStreamAdapter::GetStream(stream)->delay_headers_until_flush( + delay_headers_until_flush); +} + +int bidirectional_stream_start(bidirectional_stream* stream, + const char* url, + int priority, + const char* method, + const bidirectional_stream_header_array* headers, + bool end_of_stream) { + grpc_support::BidirectionalStream* internal_stream = + BidirectionalStreamAdapter::GetStream(stream); + net::HttpRequestHeaders request_headers; + if (headers) { + for (size_t i = 0; i < headers->count; ++i) { + std::string name(headers->headers[i].key); + std::string value(headers->headers[i].value); + if (!net::HttpUtil::IsValidHeaderName(name) || + !net::HttpUtil::IsValidHeaderValue(value)) { + DLOG(ERROR) << "Invalid Header " << name << "=" << value; + return i + 1; + } + request_headers.SetHeader(name, value); + } + } + return internal_stream->Start(url, priority, method, request_headers, + end_of_stream); +} + +int bidirectional_stream_read(bidirectional_stream* stream, + char* buffer, + int capacity) { + return BidirectionalStreamAdapter::GetStream(stream)->ReadData(buffer, + capacity); +} + +int bidirectional_stream_write(bidirectional_stream* stream, + const char* buffer, + int count, + bool end_of_stream) { + return BidirectionalStreamAdapter::GetStream(stream)->WriteData( + buffer, count, end_of_stream); +} + +void bidirectional_stream_flush(bidirectional_stream* stream) { + return BidirectionalStreamAdapter::GetStream(stream)->Flush(); +} + +void bidirectional_stream_cancel(bidirectional_stream* stream) { + BidirectionalStreamAdapter::GetStream(stream)->Cancel(); +} diff --git a/src/components/grpc_support/bidirectional_stream_unittest.cc b/src/components/grpc_support/bidirectional_stream_unittest.cc new file mode 100644 index 0000000000..43d16a053c --- /dev/null +++ b/src/components/grpc_support/bidirectional_stream_unittest.cc @@ -0,0 +1,786 @@ +// Copyright 2016 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 +#include +#include +#include + +#include + +#include "base/logging.h" +#include "base/memory/raw_ptr.h" +#include "base/strings/strcat.h" +#include "base/synchronization/waitable_event.h" +#include "build/build_config.h" +#include "components/grpc_support/include/bidirectional_stream_c.h" +#include "components/grpc_support/test/get_stream_engine.h" +#include "net/base/net_errors.h" +#include "net/test/quic_simple_test_server.h" +#include "net/test/test_data_directory.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace { + +bidirectional_stream_header kTestHeaders[] = { + {"header1", "foo"}, + {"header2", "bar"}, +}; +const bidirectional_stream_header_array kTestHeadersArray = {2, 2, + kTestHeaders}; +} // namespace + +namespace grpc_support { + +// BidirectionalStreamTest, specifically GetTestStreamEngine, fails under TSan. +// The tests are disabled here rather than as a TSan suppression because the +// stack trace cannot be distinguished from code in //net and losing TSan +// coverage for everything in //net is undesirable. See https://crbug.com/965714 +#define MAYBE_BidirectionalStreamTest BidirectionalStreamTest +#if defined(__has_feature) +#if __has_feature(thread_sanitizer) +#undef MAYBE_BidirectionalStreamTest +#define MAYBE_BidirectionalStreamTest DISABLED_BidirectionalStreamTest +#endif +#endif + +class MAYBE_BidirectionalStreamTest : public ::testing::TestWithParam { + public: + MAYBE_BidirectionalStreamTest(const MAYBE_BidirectionalStreamTest&) = delete; + MAYBE_BidirectionalStreamTest& operator=( + const MAYBE_BidirectionalStreamTest&) = delete; + + protected: + void SetUp() override { + net::QuicSimpleTestServer::Start(); + StartTestStreamEngine(net::QuicSimpleTestServer::GetPort()); + quic_server_hello_url_ = net::QuicSimpleTestServer::GetHelloURL().spec(); + } + + void TearDown() override { + ShutdownTestStreamEngine(); + net::QuicSimpleTestServer::Shutdown(); + } + + MAYBE_BidirectionalStreamTest() {} + ~MAYBE_BidirectionalStreamTest() override {} + + stream_engine* engine() { + return GetTestStreamEngine(net::QuicSimpleTestServer::GetPort()); + } + + const char* test_hello_url() const { return quic_server_hello_url_.c_str(); } + + private: + std::string quic_server_hello_url_; +}; + +class TestBidirectionalStreamCallback { + public: + enum ResponseStep { + NOTHING, + ON_STREAM_READY, + ON_RESPONSE_STARTED, + ON_READ_COMPLETED, + ON_WRITE_COMPLETED, + ON_TRAILERS, + ON_CANCELED, + ON_FAILED, + ON_SUCCEEDED + }; + + struct WriteData { + std::string buffer; + // If |flush| is true, then bidirectional_stream_flush() will be + // called after writing of the |buffer|. + bool flush; + + WriteData(const std::string& buffer, bool flush); + + WriteData(const WriteData&) = delete; + WriteData& operator=(const WriteData&) = delete; + + ~WriteData(); + }; + + raw_ptr stream; + base::WaitableEvent stream_done_event; + + // Test parameters. + std::map request_headers; + std::list> write_data; + std::string expected_negotiated_protocol; + ResponseStep cancel_from_step; + size_t read_buffer_size; + + // Test results. + ResponseStep response_step; + raw_ptr read_buffer; + std::map response_headers; + std::map response_trailers; + std::vector read_data; + int net_error; + + TestBidirectionalStreamCallback() + : stream(nullptr), + stream_done_event(base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED), + expected_negotiated_protocol("quic/1+spdy/3"), + cancel_from_step(NOTHING), + read_buffer_size(32768), + response_step(NOTHING), + read_buffer(nullptr), + net_error(0) {} + + ~TestBidirectionalStreamCallback() { delete[] read_buffer; } + + static TestBidirectionalStreamCallback* FromStream( + bidirectional_stream* stream) { + DCHECK(stream); + return reinterpret_cast( + stream->annotation); + } + + virtual bool MaybeCancel(bidirectional_stream* bidir_stream, + ResponseStep step) { + DCHECK_EQ(bidir_stream, stream); + response_step = step; + DVLOG(3) << "Step: " << step; + + if (step != cancel_from_step) + return false; + + bidirectional_stream_cancel(stream); + bidirectional_stream_write(stream, "abc", 3, false); + + return true; + } + + void SignalDone() { stream_done_event.Signal(); } + + void BlockForDone() { stream_done_event.Wait(); } + + void AddWriteData(const std::string& data) { AddWriteData(data, true); } + void AddWriteData(const std::string& data, bool flush) { + write_data.push_back(std::make_unique(data, flush)); + } + + virtual void MaybeWriteNextData(bidirectional_stream* bidir_stream) { + DCHECK_EQ(bidir_stream, stream); + if (write_data.empty()) + return; + for (const auto& data : write_data) { + bidirectional_stream_write(stream, data->buffer.c_str(), + data->buffer.size(), + data == write_data.back()); + if (data->flush) { + bidirectional_stream_flush(stream); + break; + } + } + } + + bidirectional_stream_callback* callback() const { return &s_callback; } + + private: + // C callbacks. + static void on_stream_ready_callback(bidirectional_stream* stream) { + TestBidirectionalStreamCallback* test = FromStream(stream); + if (test->MaybeCancel(stream, ON_STREAM_READY)) + return; + test->MaybeWriteNextData(stream); + } + + static void on_response_headers_received_callback( + bidirectional_stream* stream, + const bidirectional_stream_header_array* headers, + const char* negotiated_protocol) { + TestBidirectionalStreamCallback* test = FromStream(stream); + ASSERT_EQ(test->expected_negotiated_protocol, + std::string(negotiated_protocol)); + for (size_t i = 0; i < headers->count; ++i) { + if (test->response_headers.find(headers->headers[i].key) == + test->response_headers.end()) { + test->response_headers[headers->headers[i].key] = + headers->headers[i].value; + } else { + // For testing purposes, headers with the same key are combined with + // comma. + test->response_headers[headers->headers[i].key] = + test->response_headers[headers->headers[i].key] + ", " + + headers->headers[i].value; + } + } + if (test->MaybeCancel(stream, ON_RESPONSE_STARTED)) + return; + test->read_buffer = new char[test->read_buffer_size]; + bidirectional_stream_read(stream, test->read_buffer, + test->read_buffer_size); + } + + static void on_read_completed_callback(bidirectional_stream* stream, + char* data, + int count) { + TestBidirectionalStreamCallback* test = FromStream(stream); + test->read_data.push_back(std::string(data, count)); + if (test->MaybeCancel(stream, ON_READ_COMPLETED)) + return; + if (count == 0) + return; + bidirectional_stream_read(stream, test->read_buffer, + test->read_buffer_size); + } + + static void on_write_completed_callback(bidirectional_stream* stream, + const char* data) { + TestBidirectionalStreamCallback* test = FromStream(stream); + ASSERT_EQ(test->write_data.front()->buffer.c_str(), data); + if (test->MaybeCancel(stream, ON_WRITE_COMPLETED)) + return; + bool continue_writing = test->write_data.front()->flush; + test->write_data.pop_front(); + if (continue_writing) + test->MaybeWriteNextData(stream); + } + + static void on_response_trailers_received_callback( + bidirectional_stream* stream, + const bidirectional_stream_header_array* trailers) { + TestBidirectionalStreamCallback* test = FromStream(stream); + for (size_t i = 0; i < trailers->count; ++i) { + test->response_trailers[trailers->headers[i].key] = + trailers->headers[i].value; + } + + if (test->MaybeCancel(stream, ON_TRAILERS)) + return; + } + + static void on_succeded_callback(bidirectional_stream* stream) { + TestBidirectionalStreamCallback* test = FromStream(stream); + ASSERT_TRUE(test->write_data.empty()); + test->MaybeCancel(stream, ON_SUCCEEDED); + test->SignalDone(); + } + + static void on_failed_callback(bidirectional_stream* stream, int net_error) { + TestBidirectionalStreamCallback* test = FromStream(stream); + test->net_error = net_error; + test->MaybeCancel(stream, ON_FAILED); + test->SignalDone(); + } + + static void on_canceled_callback(bidirectional_stream* stream) { + TestBidirectionalStreamCallback* test = FromStream(stream); + test->MaybeCancel(stream, ON_CANCELED); + test->SignalDone(); + } + + static bidirectional_stream_callback s_callback; +}; + +bidirectional_stream_callback TestBidirectionalStreamCallback::s_callback = { + on_stream_ready_callback, + on_response_headers_received_callback, + on_read_completed_callback, + on_write_completed_callback, + on_response_trailers_received_callback, + on_succeded_callback, + on_failed_callback, + on_canceled_callback}; + +TestBidirectionalStreamCallback::WriteData::WriteData(const std::string& data, + bool flush_after) + : buffer(data), flush(flush_after) {} + +TestBidirectionalStreamCallback::WriteData::~WriteData() {} + +// Regression test for b/144733928. Test that coalesced headers will be split by +// cronet by '\0' separator. +TEST_P(MAYBE_BidirectionalStreamTest, CoalescedHeadersAreSplit) { + TestBidirectionalStreamCallback test; + test.AddWriteData("Hello, "); + test.AddWriteData("world!"); + test.read_buffer_size = 2; + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + bidirectional_stream_start(test.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, false); + test.BlockForDone(); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloStatus(), + test.response_headers[net::QuicSimpleTestServer::GetStatusHeaderName()]); + // Assert the original "foo\0bar" is split into "foo" and "bar". + ASSERT_EQ("foo, bar", + test.response_headers + [net::QuicSimpleTestServer::GetCombinedHeaderName()]); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_SUCCEEDED, test.response_step); + ASSERT_EQ(std::string(net::QuicSimpleTestServer::GetHelloBodyValue(), 0, 2), + test.read_data.front()); + // Verify that individual read data joined using empty separator match + // expected body. + ASSERT_EQ(net::QuicSimpleTestServer::GetHelloBodyValue(), + base::StrCat(test.read_data)); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloTrailerValue(), + test.response_trailers[net::QuicSimpleTestServer::GetHelloTrailerName()]); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, StartExampleBidiStream) { + TestBidirectionalStreamCallback test_callback; + test_callback.AddWriteData("Hello, "); + test_callback.AddWriteData("world!"); + // Use small read buffer size to test that response is split properly. + test_callback.read_buffer_size = 2; + test_callback.stream = bidirectional_stream_create(engine(), &test_callback, + test_callback.callback()); + DCHECK(test_callback.stream); + bidirectional_stream_delay_request_headers_until_flush(test_callback.stream, + GetParam()); + bidirectional_stream_start(test_callback.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, false); + test_callback.BlockForDone(); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloStatus(), + test_callback + .response_headers[net::QuicSimpleTestServer::GetStatusHeaderName()]); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloHeaderValue(), + test_callback + .response_headers[net::QuicSimpleTestServer::GetHelloHeaderName()]); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_SUCCEEDED, + test_callback.response_step); + ASSERT_EQ(std::string(net::QuicSimpleTestServer::GetHelloBodyValue(), 0, 2), + test_callback.read_data.front()); + // Verify that individual read data joined using empty separator match + // expected body. + ASSERT_EQ(net::QuicSimpleTestServer::GetHelloBodyValue(), + base::StrCat(test_callback.read_data)); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloTrailerValue(), + test_callback + .response_trailers[net::QuicSimpleTestServer::GetHelloTrailerName()]); + bidirectional_stream_destroy(test_callback.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, SimplePutWithEmptyWriteDataAtTheEnd) { + TestBidirectionalStreamCallback test; + test.AddWriteData("Hello, "); + test.AddWriteData("world!"); + test.AddWriteData(""); + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + bidirectional_stream_start(test.stream, test_hello_url(), 0, "PUT", + &kTestHeadersArray, false); + test.BlockForDone(); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloStatus(), + test.response_headers[net::QuicSimpleTestServer::GetStatusHeaderName()]); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloHeaderValue(), + test.response_headers[net::QuicSimpleTestServer::GetHelloHeaderName()]); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_SUCCEEDED, test.response_step); + ASSERT_EQ(net::QuicSimpleTestServer::GetHelloBodyValue(), + test.read_data.front()); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloTrailerValue(), + test.response_trailers[net::QuicSimpleTestServer::GetHelloTrailerName()]); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, SimpleGetWithFlush) { + TestBidirectionalStreamCallback test; + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_disable_auto_flush(test.stream, true); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + // Flush before start is ignored. + bidirectional_stream_flush(test.stream); + bidirectional_stream_start(test.stream, test_hello_url(), 0, "GET", + &kTestHeadersArray, true); + test.BlockForDone(); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloStatus(), + test.response_headers[net::QuicSimpleTestServer::GetStatusHeaderName()]); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloHeaderValue(), + test.response_headers[net::QuicSimpleTestServer::GetHelloHeaderName()]); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_SUCCEEDED, test.response_step); + ASSERT_EQ(net::QuicSimpleTestServer::GetHelloBodyValue(), + test.read_data.front()); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloTrailerValue(), + test.response_trailers[net::QuicSimpleTestServer::GetHelloTrailerName()]); + // Flush after done is ignored. + bidirectional_stream_flush(test.stream); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, SimplePostWithFlush) { + TestBidirectionalStreamCallback test; + test.AddWriteData("Test String", false); + test.AddWriteData("1234567890", false); + test.AddWriteData("woot!", true); + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_disable_auto_flush(test.stream, true); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + // Flush before start is ignored. + bidirectional_stream_flush(test.stream); + bidirectional_stream_start(test.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, false); + test.BlockForDone(); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloStatus(), + test.response_headers[net::QuicSimpleTestServer::GetStatusHeaderName()]); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloHeaderValue(), + test.response_headers[net::QuicSimpleTestServer::GetHelloHeaderName()]); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_SUCCEEDED, test.response_step); + ASSERT_EQ(net::QuicSimpleTestServer::GetHelloBodyValue(), + base::StrCat(test.read_data)); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloTrailerValue(), + test.response_trailers[net::QuicSimpleTestServer::GetHelloTrailerName()]); + // Flush after done is ignored. + bidirectional_stream_flush(test.stream); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, SimplePostWithFlushTwice) { + TestBidirectionalStreamCallback test; + test.AddWriteData("Test String", false); + test.AddWriteData("1234567890", false); + test.AddWriteData("woot!", true); + test.AddWriteData("Test String", false); + test.AddWriteData("1234567890", false); + test.AddWriteData("woot!", true); + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_disable_auto_flush(test.stream, true); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + // Flush before start is ignored. + bidirectional_stream_flush(test.stream); + bidirectional_stream_start(test.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, false); + test.BlockForDone(); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloStatus(), + test.response_headers[net::QuicSimpleTestServer::GetStatusHeaderName()]); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloHeaderValue(), + test.response_headers[net::QuicSimpleTestServer::GetHelloHeaderName()]); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_SUCCEEDED, test.response_step); + ASSERT_EQ(net::QuicSimpleTestServer::GetHelloBodyValue(), + base::StrCat(test.read_data)); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloTrailerValue(), + test.response_trailers[net::QuicSimpleTestServer::GetHelloTrailerName()]); + // Flush after done is ignored. + bidirectional_stream_flush(test.stream); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, SimplePostWithFlushAfterOneWrite) { + TestBidirectionalStreamCallback test; + test.AddWriteData("Test String", false); + test.AddWriteData("1234567890", false); + test.AddWriteData("woot!", true); + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_disable_auto_flush(test.stream, true); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + // Flush before start is ignored. + bidirectional_stream_flush(test.stream); + bidirectional_stream_start(test.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, false); + test.BlockForDone(); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloStatus(), + test.response_headers[net::QuicSimpleTestServer::GetStatusHeaderName()]); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloHeaderValue(), + test.response_headers[net::QuicSimpleTestServer::GetHelloHeaderName()]); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_SUCCEEDED, test.response_step); + ASSERT_EQ(net::QuicSimpleTestServer::GetHelloBodyValue(), + base::StrCat(test.read_data)); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloTrailerValue(), + test.response_trailers[net::QuicSimpleTestServer::GetHelloTrailerName()]); + // Flush after done is ignored. + bidirectional_stream_flush(test.stream); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, TestDelayedFlush) { + class CustomTestBidirectionalStreamCallback + : public TestBidirectionalStreamCallback { + void MaybeWriteNextData(bidirectional_stream* stream) override { + DCHECK_EQ(stream, this->stream); + if (write_data.empty()) + return; + // Write all buffers when stream is ready. + // Flush after "3" and "5". + // EndOfStream is set with "6" but not flushed, so it is not sent. + if (write_data.front()->buffer == "1") { + for (const auto& data : write_data) { + bidirectional_stream_write(stream, data->buffer.c_str(), + data->buffer.size(), + data == write_data.back()); + if (data->flush) { + bidirectional_stream_flush(stream); + } + } + } + // Flush the final buffer with endOfStream flag. + if (write_data.front()->buffer == "6") + bidirectional_stream_flush(stream); + } + }; + + CustomTestBidirectionalStreamCallback test; + test.AddWriteData("1", false); + test.AddWriteData("2", false); + test.AddWriteData("3", true); + test.AddWriteData("4", false); + test.AddWriteData("5", true); + test.AddWriteData("6", false); + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_disable_auto_flush(test.stream, true); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + // Flush before start is ignored. + bidirectional_stream_flush(test.stream); + bidirectional_stream_start(test.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, false); + test.BlockForDone(); + // Flush after done is ignored. + bidirectional_stream_flush(test.stream); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, CancelOnRead) { + TestBidirectionalStreamCallback test; + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + test.cancel_from_step = TestBidirectionalStreamCallback::ON_READ_COMPLETED; + bidirectional_stream_start(test.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, true); + test.BlockForDone(); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloStatus(), + test.response_headers[net::QuicSimpleTestServer::GetStatusHeaderName()]); + ASSERT_EQ(net::QuicSimpleTestServer::GetHelloBodyValue(), + test.read_data.front()); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_CANCELED, test.response_step); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, CancelOnResponse) { + TestBidirectionalStreamCallback test; + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + test.cancel_from_step = TestBidirectionalStreamCallback::ON_RESPONSE_STARTED; + bidirectional_stream_start(test.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, true); + test.BlockForDone(); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloStatus(), + test.response_headers[net::QuicSimpleTestServer::GetStatusHeaderName()]); + ASSERT_TRUE(test.read_data.empty()); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_CANCELED, test.response_step); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, CancelOnSucceeded) { + TestBidirectionalStreamCallback test; + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + test.cancel_from_step = TestBidirectionalStreamCallback::ON_SUCCEEDED; + bidirectional_stream_start(test.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, true); + test.BlockForDone(); + ASSERT_EQ( + net::QuicSimpleTestServer::GetHelloStatus(), + test.response_headers[net::QuicSimpleTestServer::GetStatusHeaderName()]); + ASSERT_EQ(net::QuicSimpleTestServer::GetHelloBodyValue(), + test.read_data.front()); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_SUCCEEDED, test.response_step); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, ReadFailsBeforeRequestStarted) { + TestBidirectionalStreamCallback test; + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + char read_buffer[1]; + bidirectional_stream_read(test.stream, read_buffer, sizeof(read_buffer)); + test.BlockForDone(); + ASSERT_TRUE(test.read_data.empty()); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_FAILED, test.response_step); + ASSERT_EQ(net::ERR_UNEXPECTED, test.net_error); + bidirectional_stream_destroy(test.stream); +} + +// TODO(https://crbug.com/880474): This test is flaky on fuchsia_x64 builder. +#if BUILDFLAG(IS_FUCHSIA) +#define MAYBE_StreamFailBeforeReadIsExecutedOnNetworkThread \ + DISABLED_StreamFailBeforeReadIsExecutedOnNetworkThread +#else +#define MAYBE_StreamFailBeforeReadIsExecutedOnNetworkThread \ + StreamFailBeforeReadIsExecutedOnNetworkThread +#endif +TEST_P(MAYBE_BidirectionalStreamTest, + MAYBE_StreamFailBeforeReadIsExecutedOnNetworkThread) { + class CustomTestBidirectionalStreamCallback + : public TestBidirectionalStreamCallback { + bool MaybeCancel(bidirectional_stream* stream, ResponseStep step) override { + if (step == ResponseStep::ON_READ_COMPLETED) { + // Shut down the server dispatcher, and the stream should error out. + net::QuicSimpleTestServer::ShutdownDispatcherForTesting(); + } + return TestBidirectionalStreamCallback::MaybeCancel(stream, step); + } + }; + + CustomTestBidirectionalStreamCallback test; + test.AddWriteData("Hello, "); + test.AddWriteData("world!"); + test.read_buffer_size = 2; + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + bidirectional_stream_start(test.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, false); + test.BlockForDone(); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_FAILED, test.response_step); + ASSERT_TRUE(test.net_error == net::ERR_QUIC_PROTOCOL_ERROR || + test.net_error == net::ERR_CONNECTION_REFUSED); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, WriteFailsBeforeRequestStarted) { + TestBidirectionalStreamCallback test; + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + bidirectional_stream_write(test.stream, "1", 1, false); + test.BlockForDone(); + ASSERT_TRUE(test.read_data.empty()); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_FAILED, test.response_step); + ASSERT_EQ(net::ERR_UNEXPECTED, test.net_error); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, StreamFailAfterStreamReadyCallback) { + class CustomTestBidirectionalStreamCallback + : public TestBidirectionalStreamCallback { + bool MaybeCancel(bidirectional_stream* stream, ResponseStep step) override { + if (step == ResponseStep::ON_STREAM_READY) { + // Shut down the server dispatcher, and the stream should error out. + net::QuicSimpleTestServer::ShutdownDispatcherForTesting(); + } + return TestBidirectionalStreamCallback::MaybeCancel(stream, step); + } + }; + + CustomTestBidirectionalStreamCallback test; + test.AddWriteData("Test String"); + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + bidirectional_stream_start(test.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, false); + test.BlockForDone(); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_FAILED, test.response_step); + ASSERT_TRUE(test.net_error == net::ERR_QUIC_PROTOCOL_ERROR || + test.net_error == net::ERR_QUIC_HANDSHAKE_FAILED || + test.net_error == net::ERR_CONNECTION_REFUSED || + test.net_error == net::ERR_QUIC_GOAWAY_REQUEST_CAN_BE_RETRIED) + << net::ErrorToString(test.net_error); + bidirectional_stream_destroy(test.stream); +} + +// TODO(crbug.com/1246489): Flaky on Win64. +#if BUILDFLAG(IS_WIN) +#define MAYBE_StreamFailBeforeWriteIsExecutedOnNetworkThread \ + DISABLED_StreamFailBeforeWriteIsExecutedOnNetworkThread +#else +#define MAYBE_StreamFailBeforeWriteIsExecutedOnNetworkThread \ + StreamFailBeforeWriteIsExecutedOnNetworkThread +#endif + +TEST_P(MAYBE_BidirectionalStreamTest, + MAYBE_StreamFailBeforeWriteIsExecutedOnNetworkThread) { + class CustomTestBidirectionalStreamCallback + : public TestBidirectionalStreamCallback { + bool MaybeCancel(bidirectional_stream* stream, ResponseStep step) override { + if (step == ResponseStep::ON_WRITE_COMPLETED) { + // Shut down the server dispatcher, and the stream should error out. + net::QuicSimpleTestServer::ShutdownDispatcherForTesting(); + } + return TestBidirectionalStreamCallback::MaybeCancel(stream, step); + } + }; + + CustomTestBidirectionalStreamCallback test; + test.AddWriteData("Test String"); + test.AddWriteData("1234567890"); + test.AddWriteData("woot!"); + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + bidirectional_stream_start(test.stream, test_hello_url(), 0, "POST", + &kTestHeadersArray, false); + test.BlockForDone(); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_FAILED, test.response_step); + ASSERT_TRUE(test.net_error == net::ERR_QUIC_PROTOCOL_ERROR || + test.net_error == net::ERR_QUIC_HANDSHAKE_FAILED); + bidirectional_stream_destroy(test.stream); +} + +TEST_P(MAYBE_BidirectionalStreamTest, FailedResolution) { + TestBidirectionalStreamCallback test; + test.stream = bidirectional_stream_create(engine(), &test, test.callback()); + DCHECK(test.stream); + bidirectional_stream_delay_request_headers_until_flush(test.stream, + GetParam()); + test.cancel_from_step = TestBidirectionalStreamCallback::ON_FAILED; + bidirectional_stream_start(test.stream, "https://notfound.example.com", 0, + "GET", &kTestHeadersArray, true); + test.BlockForDone(); + ASSERT_TRUE(test.read_data.empty()); + ASSERT_EQ(TestBidirectionalStreamCallback::ON_FAILED, test.response_step); + ASSERT_EQ(net::ERR_NAME_NOT_RESOLVED, test.net_error); + bidirectional_stream_destroy(test.stream); +} + +INSTANTIATE_TEST_SUITE_P(BidirectionalStreamDelayRequestHeadersUntilFlush, + MAYBE_BidirectionalStreamTest, + ::testing::Values(true, false)); + +} // namespace grpc_support diff --git a/src/components/grpc_support/include/DEPS b/src/components/grpc_support/include/DEPS new file mode 100644 index 0000000000..7af416026c --- /dev/null +++ b/src/components/grpc_support/include/DEPS @@ -0,0 +1,8 @@ +# Files in this directory are copied externally and can't have any dependencies +include_rules = [ + # TODO(gcasto): There doesn't appear to be a way to specify that no includes + # are allowed, so currently we just don't allow a dependency on //base, which + # should disqualify most code. It would be nice to be able to actual prevent + # all dependencies in the future. + "-base", +] \ No newline at end of file diff --git a/src/components/grpc_support/include/bidirectional_stream_c.h b/src/components/grpc_support/include/bidirectional_stream_c.h new file mode 100644 index 0000000000..3a6fae85d3 --- /dev/null +++ b/src/components/grpc_support/include/bidirectional_stream_c.h @@ -0,0 +1,245 @@ +// Copyright 2016 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_GRPC_SUPPORT_INCLUDE_BIDIRECTIONAL_STREAM_C_H_ +#define COMPONENTS_GRPC_SUPPORT_INCLUDE_BIDIRECTIONAL_STREAM_C_H_ + +#if defined(WIN32) +#define GRPC_SUPPORT_EXPORT __declspec(dllexport) +#else +#define GRPC_SUPPORT_EXPORT __attribute__((visibility("default"))) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* Engine API. */ + +/* Opaque object representing a Bidirectional stream creating engine. Created + * and configured outside of this API to facilitate sharing with other + * components */ +typedef struct stream_engine { + void* obj; + void* annotation; +} stream_engine; + +/* Bidirectional Stream API */ + +/* Opaque object representing Bidirectional Stream. */ +typedef struct bidirectional_stream { + void* obj; + void* annotation; +} bidirectional_stream; + +/* A single request or response header element. */ +typedef struct bidirectional_stream_header { + const char* key; + const char* value; +} bidirectional_stream_header; + +/* Array of request or response headers or trailers. */ +typedef struct bidirectional_stream_header_array { + size_t count; + size_t capacity; + bidirectional_stream_header* headers; +} bidirectional_stream_header_array; + +/* Set of callbacks used to receive callbacks from bidirectional stream. */ +typedef struct bidirectional_stream_callback { + /* Invoked when the stream is ready for reading and writing. + * Consumer may call bidirectional_stream_read() to start reading data. + * Consumer may call bidirectional_stream_write() to start writing + * data. + */ + void (*on_stream_ready)(bidirectional_stream* stream); + + /* Invoked when initial response headers are received. + * Consumer must call bidirectional_stream_read() to start reading. + * Consumer may call bidirectional_stream_write() to start writing or + * close the stream. Contents of |headers| is valid for duration of the call. + */ + void (*on_response_headers_received)( + bidirectional_stream* stream, + const bidirectional_stream_header_array* headers, + const char* negotiated_protocol); + + /* Invoked when data is read into the buffer passed to + * bidirectional_stream_read(). Only part of the buffer may be + * populated. To continue reading, call bidirectional_stream_read(). + * It may be invoked after on_response_trailers_received()}, if there was + * pending read data before trailers were received. + * + * If |bytes_read| is 0, it means the remote side has signaled that it will + * send no more data; future calls to bidirectional_stream_read() + * will result in the on_data_read() callback or on_succeded() callback if + * bidirectional_stream_write() was invoked with end_of_stream set to + * true. + */ + void (*on_read_completed)(bidirectional_stream* stream, + char* data, + int bytes_read); + + /** + * Invoked when all data passed to bidirectional_stream_write() is + * sent. To continue writing, call bidirectional_stream_write(). + */ + void (*on_write_completed)(bidirectional_stream* stream, const char* data); + + /* Invoked when trailers are received before closing the stream. Only invoked + * when server sends trailers, which it may not. May be invoked while there is + * read data remaining in local buffer. Contents of |trailers| is valid for + * duration of the call. + */ + void (*on_response_trailers_received)( + bidirectional_stream* stream, + const bidirectional_stream_header_array* trailers); + + /** + * Invoked when there is no data to be read or written and the stream is + * closed successfully remotely and locally. Once invoked, no further callback + * methods will be invoked. + */ + void (*on_succeded)(bidirectional_stream* stream); + + /** + * Invoked if the stream failed for any reason after + * bidirectional_stream_start(). HTTP/2 error codes are + * mapped to chrome net error codes. Once invoked, no further callback methods + * will be invoked. + */ + void (*on_failed)(bidirectional_stream* stream, int net_error); + + /** + * Invoked if the stream was canceled via + * bidirectional_stream_cancel(). Once invoked, no further callback + * methods will be invoked. + */ + void (*on_canceled)(bidirectional_stream* stream); +} bidirectional_stream_callback; + +/* Creates a new stream object that uses |engine| and |callback|. All stream + * tasks are performed asynchronously on the |engine| network thread. |callback| + * methods are invoked synchronously on the |engine| network thread, but must + * not run tasks on the current thread to prevent blocking networking operations + * and causing exceptions during shutdown. The |annotation| is stored in + * bidirectional stream for arbitrary use by application. + * + * Returned |bidirectional_stream*| is owned by the caller, and must be + * destroyed using |bidirectional_stream_destroy|. + * + * Both |calback| and |engine| must remain valid until stream is destroyed. + */ +GRPC_SUPPORT_EXPORT +bidirectional_stream* bidirectional_stream_create( + stream_engine* engine, + void* annotation, + bidirectional_stream_callback* callback); + +/* TBD: The following methods return int. Should it be a custom type? */ + +/* Destroys stream object. Destroy could be called from any thread, including + * network thread, but is posted, so |stream| is valid until calling task is + * complete. + */ +GRPC_SUPPORT_EXPORT +int bidirectional_stream_destroy(bidirectional_stream* stream); + +/** + * Disables or enables auto flush. By default, data is flushed after + * every bidirectional_stream_write(). If the auto flush is disabled, + * the client should explicitly call bidirectional_stream_flush to flush + * the data. + */ +GRPC_SUPPORT_EXPORT void bidirectional_stream_disable_auto_flush( + bidirectional_stream* stream, + bool disable_auto_flush); + +/** + * Delays sending request headers until bidirectional_stream_flush() + * is called. This flag is currently only respected when QUIC is negotiated. + * When true, QUIC will send request header frame along with data frame(s) + * as a single packet when possible. + */ +GRPC_SUPPORT_EXPORT +void bidirectional_stream_delay_request_headers_until_flush( + bidirectional_stream* stream, + bool delay_headers_until_flush); + +/* Starts the stream by sending request to |url| using |method| and |headers|. + * If |end_of_stream| is true, then no data is expected to be written. The + * |method| is HTTP verb. + */ +GRPC_SUPPORT_EXPORT +int bidirectional_stream_start(bidirectional_stream* stream, + const char* url, + int priority, + const char* method, + const bidirectional_stream_header_array* headers, + bool end_of_stream); + +/* Reads response data into |buffer| of |capacity| length. Must only be called + * at most once in response to each invocation of the + * on_stream_ready()/on_response_headers_received() and on_read_completed() + * methods of the bidirectional_stream_callback. + * Each call will result in an invocation of the callback's + * on_read_completed() method if data is read, or its on_failed() method if + * there's an error. The callback's on_succeeded() method is also invoked if + * there is no more data to read and |end_of_stream| was previously sent. + */ +GRPC_SUPPORT_EXPORT +int bidirectional_stream_read(bidirectional_stream* stream, + char* buffer, + int capacity); + +/* Writes request data from |buffer| of |buffer_length| length. If auto flush is + * disabled, data will be sent only after bidirectional_stream_flush() is + * called. + * Each call will result in an invocation the callback's on_write_completed() + * method if data is sent, or its on_failed() method if there's an error. + * The callback's on_succeeded() method is also invoked if |end_of_stream| is + * set and all response data has been read. + */ +GRPC_SUPPORT_EXPORT +int bidirectional_stream_write(bidirectional_stream* stream, + const char* buffer, + int buffer_length, + bool end_of_stream); + +/** + * Flushes pending writes. This method should not be called before invocation of + * on_stream_ready() method of the bidirectional_stream_callback. + * For each previously called bidirectional_stream_write() + * a corresponding on_write_completed() callback will be invoked when the buffer + * is sent. + */ +GRPC_SUPPORT_EXPORT +void bidirectional_stream_flush(bidirectional_stream* stream); + +/* Cancels the stream. Can be called at any time after + * bidirectional_stream_start(). The on_canceled() method of + * bidirectional_stream_callback will be invoked when cancelation + * is complete and no further callback methods will be invoked. If the + * stream has completed or has not started, calling + * bidirectional_stream_cancel() has no effect and on_canceled() will not + * be invoked. At most one callback method may be invoked after + * bidirectional_stream_cancel() has completed. + */ +GRPC_SUPPORT_EXPORT +void bidirectional_stream_cancel(bidirectional_stream* stream); + +/* Returns true if the |stream| was successfully started and is now done + * (succeeded, canceled, or failed). + * Returns false if the |stream| stream is not yet started or is in progress. + */ +GRPC_SUPPORT_EXPORT +bool bidirectional_stream_is_done(bidirectional_stream* stream); + +#ifdef __cplusplus +} +#endif + +#endif // COMPONENTS_GRPC_SUPPORT_INCLUDE_BIDIRECTIONAL_STREAM_C_H_ diff --git a/src/components/grpc_support/include/headers.gni b/src/components/grpc_support/include/headers.gni new file mode 100644 index 0000000000..790332f058 --- /dev/null +++ b/src/components/grpc_support/include/headers.gni @@ -0,0 +1,2 @@ +grpc_public_headers = + [ "//components/grpc_support/include/bidirectional_stream_c.h" ] diff --git a/src/components/grpc_support/test/BUILD.gn b/src/components/grpc_support/test/BUILD.gn new file mode 100644 index 0000000000..62b72d2324 --- /dev/null +++ b/src/components/grpc_support/test/BUILD.gn @@ -0,0 +1,22 @@ +source_set("unit_tests") { + testonly = true + sources = [ "get_stream_engine.cc" ] + + deps = [ + "//base", + "//components/grpc_support", + "//components/grpc_support:bidirectional_stream_unittest", + "//net", + "//net:test_support", + ] +} + +source_set("get_stream_engine_header") { + testonly = true + sources = [ "get_stream_engine.h" ] + + deps = [ + "//base", + "//net", + ] +} diff --git a/src/components/grpc_support/test/get_stream_engine.cc b/src/components/grpc_support/test/get_stream_engine.cc new file mode 100644 index 0000000000..91ffac1f5e --- /dev/null +++ b/src/components/grpc_support/test/get_stream_engine.cc @@ -0,0 +1,157 @@ +// Copyright 2016 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 +#include + +#include "components/grpc_support/test/get_stream_engine.h" + +#include "base/lazy_instance.h" +#include "base/memory/ref_counted.h" +#include "base/message_loop/message_pump_type.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/task/single_thread_task_runner.h" +#include "base/threading/thread.h" +#include "components/grpc_support/include/bidirectional_stream_c.h" +#include "net/base/host_port_pair.h" +#include "net/base/network_isolation_key.h" +#include "net/cert/mock_cert_verifier.h" +#include "net/dns/mapped_host_resolver.h" +#include "net/dns/mock_host_resolver.h" +#include "net/http/http_network_session.h" +#include "net/http/http_server_properties.h" +#include "net/test/cert_test_util.h" +#include "net/test/quic_simple_test_server.h" +#include "net/test/test_data_directory.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_context_builder.h" +#include "net/url_request/url_request_test_util.h" + +namespace grpc_support { +namespace { + +// URLRequestContextGetter for BidirectionalStreamTest. This is used instead of +// net::TestURLRequestContextGetter because the URLRequestContext needs to be +// created on the test_io_thread_ for the test, and TestURLRequestContextGetter +// does not allow for lazy instantiation of the URLRequestContext if additional +// setup is required. +class BidirectionalStreamTestURLRequestContextGetter + : public net::URLRequestContextGetter { + public: + BidirectionalStreamTestURLRequestContextGetter( + const scoped_refptr& task_runner) + : task_runner_(task_runner) {} + + BidirectionalStreamTestURLRequestContextGetter( + const BidirectionalStreamTestURLRequestContextGetter&) = delete; + BidirectionalStreamTestURLRequestContextGetter& operator=( + const BidirectionalStreamTestURLRequestContextGetter&) = delete; + + net::URLRequestContext* GetURLRequestContext() override { + if (!request_context_) { + auto context_builder = net::CreateTestURLRequestContextBuilder(); + auto mock_host_resolver = std::make_unique(); + auto host_resolver = std::make_unique( + std::move(mock_host_resolver)); + auto test_cert = net::ImportCertFromFile(net::GetTestCertsDirectory(), + "quic-chain.pem"); + auto mock_cert_verifier = std::make_unique(); + net::CertVerifyResult verify_result; + verify_result.verified_cert = test_cert; + verify_result.is_issued_by_known_root = true; + mock_cert_verifier->AddResultForCert(test_cert, verify_result, net::OK); + + net::HttpNetworkSessionParams params; + params.enable_quic = true; + params.enable_http2 = true; + + context_builder->SetCertVerifier(std::move(mock_cert_verifier)); + context_builder->set_host_resolver(std::move(host_resolver)); + context_builder->set_http_network_session_params(params); + request_context_ = context_builder->Build(); + UpdateHostResolverRules(); + + // Need to enable QUIC for the test server. + net::AlternativeService alternative_service(net::kProtoQUIC, "", 443); + url::SchemeHostPort quic_hint_server( + "https", net::QuicSimpleTestServer::GetHost(), 443); + request_context_->http_server_properties()->SetQuicAlternativeService( + quic_hint_server, net::NetworkIsolationKey(), alternative_service, + base::Time::Max(), quic::ParsedQuicVersionVector()); + } + return request_context_.get(); + } + + net::MappedHostResolver* host_resolver() { + if (!request_context_) { + return nullptr; + } + // This is safe because we set a MappedHostResolver in + // GetURLRequestContext(). + return static_cast( + request_context_->host_resolver()); + } + + scoped_refptr GetNetworkTaskRunner() + const override { + return task_runner_; + } + + void SetTestServerPort(int port) { + test_server_port_ = port; + UpdateHostResolverRules(); + } + + private: + void UpdateHostResolverRules() { + if (!host_resolver()) + return; + host_resolver()->SetRulesFromString( + base::StringPrintf("MAP notfound.example.com ~NOTFOUND," + "MAP test.example.com 127.0.0.1:%d", + test_server_port_)); + } + ~BidirectionalStreamTestURLRequestContextGetter() override {} + + int test_server_port_; + std::unique_ptr request_context_; + scoped_refptr task_runner_; +}; + +base::LazyInstance< + scoped_refptr> + ::Leaky g_request_context_getter_ = LAZY_INSTANCE_INITIALIZER; +bool g_initialized_ = false; + +} // namespace + +void CreateRequestContextGetterIfNecessary() { + if (!g_initialized_) { + g_initialized_ = true; + static base::Thread* test_io_thread_ = + new base::Thread("grpc_support_test_io_thread"); + base::Thread::Options options; + options.message_pump_type = base::MessagePumpType::IO; + bool started = test_io_thread_->StartWithOptions(std::move(options)); + DCHECK(started); + + g_request_context_getter_.Get() = + new BidirectionalStreamTestURLRequestContextGetter( + test_io_thread_->task_runner()); + } +} + +stream_engine* GetTestStreamEngine(int port) { + CreateRequestContextGetterIfNecessary(); + g_request_context_getter_.Get()->SetTestServerPort(port); + static stream_engine engine; + engine.obj = g_request_context_getter_.Get().get(); + return &engine; +} + +void StartTestStreamEngine(int port) {} +void ShutdownTestStreamEngine() {} + +} // namespace grpc_support diff --git a/src/components/grpc_support/test/get_stream_engine.h b/src/components/grpc_support/test/get_stream_engine.h new file mode 100644 index 0000000000..8eaa0dbf09 --- /dev/null +++ b/src/components/grpc_support/test/get_stream_engine.h @@ -0,0 +1,31 @@ +// Copyright 2016 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_GRPC_SUPPORT_TEST_GET_STREAM_ENGINE_H_ +#define COMPONENTS_GRPC_SUPPORT_TEST_GET_STREAM_ENGINE_H_ + +struct stream_engine; + +namespace grpc_support { + +// Returns a stream_engine* for testing with the QuicTestServer. +// The engine returned should resolve kTestServerHost as localhost:|port|, +// and should have kTestServerHost configured as a QUIC server. +stream_engine* GetTestStreamEngine(int port); + +// Starts the stream_engine* for testing with the QuicTestServer. +// Has the same properties as GetTestStreamEngine. This function is +// used when the stream_engine* needs to be shut down and restarted +// between test cases (including between all of the bidirectional +// stream test cases and all other tests for the engine; this is the +// situation for Cronet). +void StartTestStreamEngine(int port); + +// Shuts a stream_engine* started with |StartTestStreamEngine| down. +// See comment above. +void ShutdownTestStreamEngine(); + +} // namespace grpc_support + +#endif // COMPONENTS_GRPC_SUPPORT_TEST_GET_STREAM_ENGINE_H_ diff --git a/src/components/prefs/BUILD.gn b/src/components/prefs/BUILD.gn new file mode 100644 index 0000000000..c1828d1027 --- /dev/null +++ b/src/components/prefs/BUILD.gn @@ -0,0 +1,120 @@ +# 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. + +import("//build/config/chromeos/ui_mode.gni") + +component("prefs") { + sources = [ + "command_line_pref_store.cc", + "command_line_pref_store.h", + "default_pref_store.cc", + "default_pref_store.h", + "in_memory_pref_store.cc", + "in_memory_pref_store.h", + "json_pref_store.cc", + "json_pref_store.h", + "overlay_user_pref_store.cc", + "overlay_user_pref_store.h", + "persistent_pref_store.cc", + "persistent_pref_store.h", + "pref_change_registrar.cc", + "pref_change_registrar.h", + "pref_filter.h", + "pref_member.cc", + "pref_member.h", + "pref_notifier.h", + "pref_notifier_impl.cc", + "pref_notifier_impl.h", + "pref_observer.h", + "pref_registry.cc", + "pref_registry.h", + "pref_registry_simple.cc", + "pref_registry_simple.h", + "pref_service.cc", + "pref_service.h", + "pref_service_factory.cc", + "pref_service_factory.h", + "pref_store.cc", + "pref_store.h", + "pref_value_map.cc", + "pref_value_map.h", + "pref_value_store.cc", + "pref_value_store.h", + "prefs_export.h", + "scoped_user_pref_update.cc", + "scoped_user_pref_update.h", + "segregated_pref_store.cc", + "segregated_pref_store.h", + "value_map_pref_store.cc", + "value_map_pref_store.h", + "writeable_pref_store.cc", + "writeable_pref_store.h", + ] + + defines = [ "COMPONENTS_PREFS_IMPLEMENTATION" ] + + deps = [] + + public_deps = [ "//base" ] + + if (is_android) { + sources += [ + "android/pref_service_android.cc", + "android/pref_service_android.h", + ] + deps += [ "android:jni_headers" ] + } +} + +static_library("test_support") { + testonly = true + sources = [ + "mock_pref_change_callback.cc", + "mock_pref_change_callback.h", + "pref_store_observer_mock.cc", + "pref_store_observer_mock.h", + "pref_test_utils.cc", + "pref_test_utils.h", + "testing_pref_service.cc", + "testing_pref_service.h", + "testing_pref_store.cc", + "testing_pref_store.h", + ] + + public_deps = [ ":prefs" ] + deps = [ + "//base", + "//base/test:test_support", + "//testing/gmock", + "//testing/gtest", + ] +} + +source_set("unit_tests") { + testonly = true + sources = [ + "default_pref_store_unittest.cc", + "in_memory_pref_store_unittest.cc", + "json_pref_store_unittest.cc", + "overlay_user_pref_store_unittest.cc", + "persistent_pref_store_unittest.cc", + "persistent_pref_store_unittest.h", + "pref_change_registrar_unittest.cc", + "pref_member_unittest.cc", + "pref_notifier_impl_unittest.cc", + "pref_service_unittest.cc", + "pref_value_map_unittest.cc", + "pref_value_store_unittest.cc", + "scoped_user_pref_update_unittest.cc", + "segregated_pref_store_unittest.cc", + ] + + deps = [ + ":test_support", + "//base", + "//base/test:test_support", + "//testing/gmock", + "//testing/gtest", + ] +} diff --git a/src/components/prefs/DIR_METADATA b/src/components/prefs/DIR_METADATA new file mode 100644 index 0000000000..e9153b069b --- /dev/null +++ b/src/components/prefs/DIR_METADATA @@ -0,0 +1,5 @@ +monorail { + component: "Internals>Preferences" +} + +team_email: "chromium-dev@chromium.org" diff --git a/src/components/prefs/OWNERS b/src/components/prefs/OWNERS new file mode 100644 index 0000000000..a4dab038d5 --- /dev/null +++ b/src/components/prefs/OWNERS @@ -0,0 +1,2 @@ +battre@chromium.org +gab@chromium.org diff --git a/src/components/prefs/android/BUILD.gn b/src/components/prefs/android/BUILD.gn new file mode 100644 index 0000000000..4d5e50fdcd --- /dev/null +++ b/src/components/prefs/android/BUILD.gn @@ -0,0 +1,35 @@ +# Copyright 2020 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. + +import("//build/config/android/rules.gni") + +generate_jni("jni_headers") { + sources = [ "java/src/org/chromium/components/prefs/PrefService.java" ] +} + +android_library("java") { + sources = [ "java/src/org/chromium/components/prefs/PrefService.java" ] + deps = [ + "//base:base_java", + "//third_party/androidx:androidx_annotation_annotation_java", + ] + annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] +} + +java_library("junit") { + # Skip platform checks since Robolectric depends on requires_android targets. + bypass_platform_checks = true + testonly = true + sources = [ "java/src/org/chromium/components/prefs/PrefServiceTest.java" ] + deps = [ + ":java", + "//base:base_java", + "//base:base_java_test_support", + "//base:base_junit_test_support", + "//base/test:test_support_java", + "//third_party/android_deps:robolectric_all_java", + "//third_party/junit", + "//third_party/mockito:mockito_java", + ] +} diff --git a/src/components/prefs/android/DIR_METADATA b/src/components/prefs/android/DIR_METADATA new file mode 100644 index 0000000000..1744a1fc93 --- /dev/null +++ b/src/components/prefs/android/DIR_METADATA @@ -0,0 +1,2 @@ + +os: ANDROID diff --git a/src/components/prefs/android/OWNERS b/src/components/prefs/android/OWNERS new file mode 100644 index 0000000000..0fa757c4fd --- /dev/null +++ b/src/components/prefs/android/OWNERS @@ -0,0 +1 @@ +twellington@chromium.org diff --git a/src/components/prefs/android/java/src/org/chromium/components/prefs/PrefService.java b/src/components/prefs/android/java/src/org/chromium/components/prefs/PrefService.java new file mode 100644 index 0000000000..e3cf3ccc75 --- /dev/null +++ b/src/components/prefs/android/java/src/org/chromium/components/prefs/PrefService.java @@ -0,0 +1,115 @@ +// Copyright 2020 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.components.prefs; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.NativeMethods; + +/** PrefService provides read and write access to native PrefService. */ +public class PrefService { + private long mNativePrefServiceAndroid; + + @CalledByNative + private static PrefService create(long nativePrefServiceAndroid) { + return new PrefService(nativePrefServiceAndroid); + } + + @CalledByNative + private void clearNativePtr() { + mNativePrefServiceAndroid = 0; + } + + @VisibleForTesting + PrefService(long nativePrefServiceAndroid) { + mNativePrefServiceAndroid = nativePrefServiceAndroid; + } + + /** + * @param preference The name of the preference. + */ + public void clearPref(@NonNull String preference) { + PrefServiceJni.get().clearPref(mNativePrefServiceAndroid, preference); + } + + /** + * @param preference The name of the preference. + */ + public boolean hasPrefPath(@NonNull String preference) { + return PrefServiceJni.get().hasPrefPath(mNativePrefServiceAndroid, preference); + } + + /** + * @param preference The name of the preference. + * @return Whether the specified preference is enabled. + */ + public boolean getBoolean(@NonNull String preference) { + return PrefServiceJni.get().getBoolean(mNativePrefServiceAndroid, preference); + } + + /** + * @param preference The name of the preference. + * @param value The value the specified preference will be set to. + */ + public void setBoolean(@NonNull String preference, boolean value) { + PrefServiceJni.get().setBoolean(mNativePrefServiceAndroid, preference, value); + } + + /** + * @param preference The name of the preference. + * @return value The value of the specified preference. + */ + public int getInteger(@NonNull String preference) { + return PrefServiceJni.get().getInteger(mNativePrefServiceAndroid, preference); + } + + /** + * @param preference The name of the preference. + * @param value The value the specified preference will be set to. + */ + public void setInteger(@NonNull String preference, int value) { + PrefServiceJni.get().setInteger(mNativePrefServiceAndroid, preference, value); + } + + /** + * @param preference The name of the preference. + * @return value The value of the specified preference. + */ + @NonNull + public String getString(@NonNull String preference) { + return PrefServiceJni.get().getString(mNativePrefServiceAndroid, preference); + } + + /** + * @param preference The name of the preference. + * @param value The value the specified preference will be set to. + */ + public void setString(@NonNull String preference, @NonNull String value) { + PrefServiceJni.get().setString(mNativePrefServiceAndroid, preference, value); + } + + /** + * @param preference The name of the preference. + * @return Whether the specified preference is managed. + */ + public boolean isManagedPreference(@NonNull String preference) { + return PrefServiceJni.get().isManagedPreference(mNativePrefServiceAndroid, preference); + } + + @NativeMethods + interface Natives { + void clearPref(long nativePrefServiceAndroid, String preference); + boolean hasPrefPath(long nativePrefServiceAndroid, String preference); + boolean getBoolean(long nativePrefServiceAndroid, String preference); + void setBoolean(long nativePrefServiceAndroid, String preference, boolean value); + int getInteger(long nativePrefServiceAndroid, String preference); + void setInteger(long nativePrefServiceAndroid, String preference, int value); + String getString(long nativePrefServiceAndroid, String preference); + void setString(long nativePrefServiceAndroid, String preference, String value); + boolean isManagedPreference(long nativePrefServiceAndroid, String preference); + } +} diff --git a/src/components/prefs/android/java/src/org/chromium/components/prefs/PrefServiceTest.java b/src/components/prefs/android/java/src/org/chromium/components/prefs/PrefServiceTest.java new file mode 100644 index 0000000000..e1de428a01 --- /dev/null +++ b/src/components/prefs/android/java/src/org/chromium/components/prefs/PrefServiceTest.java @@ -0,0 +1,108 @@ +// Copyright 2019 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. + +// generate_java_test.py + +package org.chromium.components.prefs; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; + +import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.base.test.util.JniMocker; + +/** Unit tests for {@link PrefService}. */ +@RunWith(BaseRobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class PrefServiceTest { + private static final String PREF = "42"; + private static final long NATIVE_HANDLE = 117; + + @Rule + public JniMocker mocker = new JniMocker(); + @Mock + private PrefService.Natives mNativeMock; + + PrefService mPrefService; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mocker.mock(PrefServiceJni.TEST_HOOKS, mNativeMock); + mPrefService = new PrefService(NATIVE_HANDLE); + } + + @Test + public void testGetBoolean() { + boolean expected = false; + + doReturn(expected).when(mNativeMock).getBoolean(NATIVE_HANDLE, PREF); + + assertEquals(expected, mPrefService.getBoolean(PREF)); + } + + @Test + public void testSetBoolean() { + boolean value = true; + + mPrefService.setBoolean(PREF, value); + + verify(mNativeMock).setBoolean(eq(NATIVE_HANDLE), eq(PREF), eq(value)); + } + + @Test + public void testGetInteger() { + int expected = 26; + + doReturn(expected).when(mNativeMock).getInteger(NATIVE_HANDLE, PREF); + + assertEquals(expected, mPrefService.getInteger(PREF)); + } + + @Test + public void testSetInteger() { + int value = 62; + + mPrefService.setInteger(PREF, value); + + verify(mNativeMock).setInteger(eq(NATIVE_HANDLE), eq(PREF), eq(value)); + } + + @Test + public void testGetString() { + String expected = "foo"; + + doReturn(expected).when(mNativeMock).getString(NATIVE_HANDLE, PREF); + + assertEquals(expected, mPrefService.getString(PREF)); + } + + @Test + public void testSetString() { + String value = "bar"; + + mPrefService.setString(PREF, value); + + verify(mNativeMock).setString(eq(NATIVE_HANDLE), eq(PREF), eq(value)); + } + + @Test + public void testIsManaged() { + boolean expected = true; + + doReturn(expected).when(mNativeMock).isManagedPreference(NATIVE_HANDLE, PREF); + + assertEquals(expected, mPrefService.isManagedPreference(PREF)); + } +} diff --git a/src/components/prefs/android/pref_service_android.cc b/src/components/prefs/android/pref_service_android.cc new file mode 100644 index 0000000000..022adc6cbe --- /dev/null +++ b/src/components/prefs/android/pref_service_android.cc @@ -0,0 +1,97 @@ +// Copyright 2020 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 "components/prefs/android/pref_service_android.h" + +#include + +#include "components/prefs/android/jni_headers/PrefService_jni.h" +#include "components/prefs/pref_service.h" + +using base::android::JavaParamRef; +using base::android::ScopedJavaLocalRef; + +PrefServiceAndroid::PrefServiceAndroid(PrefService* pref_service) + : pref_service_(pref_service) {} + +PrefServiceAndroid::~PrefServiceAndroid() { + if (java_ref_) { + Java_PrefService_clearNativePtr(base::android::AttachCurrentThread(), + java_ref_); + java_ref_.Reset(); + } +} + +ScopedJavaLocalRef PrefServiceAndroid::GetJavaObject() { + JNIEnv* env = base::android::AttachCurrentThread(); + if (!java_ref_) { + java_ref_.Reset( + Java_PrefService_create(env, reinterpret_cast(this))); + } + return ScopedJavaLocalRef(java_ref_); +} + +void PrefServiceAndroid::ClearPref(JNIEnv* env, + const JavaParamRef& j_preference) { + pref_service_->ClearPref( + base::android::ConvertJavaStringToUTF8(env, j_preference)); +} + +jboolean PrefServiceAndroid::HasPrefPath( + JNIEnv* env, + const base::android::JavaParamRef& j_preference) { + return pref_service_->HasPrefPath( + base::android::ConvertJavaStringToUTF8(env, j_preference)); +} + +jboolean PrefServiceAndroid::GetBoolean( + JNIEnv* env, + const JavaParamRef& j_preference) { + return pref_service_->GetBoolean( + base::android::ConvertJavaStringToUTF8(env, j_preference)); +} + +void PrefServiceAndroid::SetBoolean(JNIEnv* env, + const JavaParamRef& j_preference, + const jboolean j_value) { + pref_service_->SetBoolean( + base::android::ConvertJavaStringToUTF8(env, j_preference), j_value); +} + +jint PrefServiceAndroid::GetInteger(JNIEnv* env, + const JavaParamRef& j_preference) { + return pref_service_->GetInteger( + base::android::ConvertJavaStringToUTF8(env, j_preference)); +} + +void PrefServiceAndroid::SetInteger(JNIEnv* env, + const JavaParamRef& j_preference, + const jint j_value) { + pref_service_->SetInteger( + base::android::ConvertJavaStringToUTF8(env, j_preference), j_value); +} + +ScopedJavaLocalRef PrefServiceAndroid::GetString( + JNIEnv* env, + const JavaParamRef& j_preference) { + return base::android::ConvertUTF8ToJavaString( + env, pref_service_->GetString( + base::android::ConvertJavaStringToUTF8(env, j_preference))); +} + +void PrefServiceAndroid::SetString( + JNIEnv* env, + const JavaParamRef& j_preference, + const base::android::JavaParamRef& j_value) { + pref_service_->SetString( + base::android::ConvertJavaStringToUTF8(env, j_preference), + base::android::ConvertJavaStringToUTF8(env, j_value)); +} + +jboolean PrefServiceAndroid::IsManagedPreference( + JNIEnv* env, + const JavaParamRef& j_preference) { + return pref_service_->IsManagedPreference( + base::android::ConvertJavaStringToUTF8(env, j_preference)); +} diff --git a/src/components/prefs/android/pref_service_android.h b/src/components/prefs/android/pref_service_android.h new file mode 100644 index 0000000000..c829c0e22c --- /dev/null +++ b/src/components/prefs/android/pref_service_android.h @@ -0,0 +1,57 @@ +// Copyright 2020 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_PREFS_ANDROID_PREF_SERVICE_ANDROID_H_ +#define COMPONENTS_PREFS_ANDROID_PREF_SERVICE_ANDROID_H_ + +#include + +#include "base/android/jni_string.h" +#include "base/android/scoped_java_ref.h" +#include "base/memory/raw_ptr.h" + +class PrefService; + +// The native side of the PrefServiceAndroid is created and destroyed by the +// Java. +class PrefServiceAndroid { + public: + explicit PrefServiceAndroid(PrefService* pref_service); + PrefServiceAndroid(const PrefServiceAndroid& other) = delete; + PrefServiceAndroid& operator=(const PrefServiceAndroid& other) = delete; + ~PrefServiceAndroid(); + + base::android::ScopedJavaLocalRef GetJavaObject(); + + void ClearPref(JNIEnv* env, + const base::android::JavaParamRef& j_preference); + jboolean HasPrefPath( + JNIEnv* env, + const base::android::JavaParamRef& j_preference); + jboolean GetBoolean(JNIEnv* env, + const base::android::JavaParamRef& j_preference); + void SetBoolean(JNIEnv* env, + const base::android::JavaParamRef& j_preference, + const jboolean j_value); + jint GetInteger(JNIEnv* env, + const base::android::JavaParamRef& j_preference); + void SetInteger(JNIEnv* env, + const base::android::JavaParamRef& j_preference, + const jint j_value); + base::android::ScopedJavaLocalRef GetString( + JNIEnv* env, + const base::android::JavaParamRef& j_preference); + void SetString(JNIEnv* env, + const base::android::JavaParamRef& j_preference, + const base::android::JavaParamRef& j_value); + jboolean IsManagedPreference( + JNIEnv* env, + const base::android::JavaParamRef& j_preference); + + private: + raw_ptr pref_service_; + base::android::ScopedJavaGlobalRef java_ref_; +}; + +#endif // COMPONENTS_PREFS_ANDROID_PREF_SERVICE_ANDROID_H_ diff --git a/src/components/prefs/command_line_pref_store.cc b/src/components/prefs/command_line_pref_store.cc new file mode 100644 index 0000000000..35c74a4b62 --- /dev/null +++ b/src/components/prefs/command_line_pref_store.cc @@ -0,0 +1,80 @@ +// Copyright 2016 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 "components/prefs/command_line_pref_store.h" + +#include +#include + +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/values.h" + +CommandLinePrefStore::CommandLinePrefStore( + const base::CommandLine* command_line) + : command_line_(command_line) {} + +CommandLinePrefStore::~CommandLinePrefStore() {} + +void CommandLinePrefStore::ApplyStringSwitches( + const CommandLinePrefStore::SwitchToPreferenceMapEntry string_switch[], + size_t size) { + for (size_t i = 0; i < size; ++i) { + if (command_line_->HasSwitch(string_switch[i].switch_name)) { + SetValue(string_switch[i].preference_path, + std::make_unique(command_line_->GetSwitchValueASCII( + string_switch[i].switch_name)), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + } + } +} + +void CommandLinePrefStore::ApplyPathSwitches( + const CommandLinePrefStore::SwitchToPreferenceMapEntry path_switch[], + size_t size) { + for (size_t i = 0; i < size; ++i) { + if (command_line_->HasSwitch(path_switch[i].switch_name)) { + SetValue(path_switch[i].preference_path, + std::make_unique( + command_line_->GetSwitchValuePath(path_switch[i].switch_name) + .AsUTF8Unsafe()), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + } + } +} + +void CommandLinePrefStore::ApplyIntegerSwitches( + const CommandLinePrefStore::SwitchToPreferenceMapEntry integer_switch[], + size_t size) { + for (size_t i = 0; i < size; ++i) { + if (command_line_->HasSwitch(integer_switch[i].switch_name)) { + std::string str_value = command_line_->GetSwitchValueASCII( + integer_switch[i].switch_name); + int int_value = 0; + if (!base::StringToInt(str_value, &int_value)) { + LOG(ERROR) << "The value " << str_value << " of " + << integer_switch[i].switch_name + << " can not be converted to integer, ignoring!"; + continue; + } + SetValue(integer_switch[i].preference_path, + std::make_unique(int_value), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + } + } +} + +void CommandLinePrefStore::ApplyBooleanSwitches( + const CommandLinePrefStore::BooleanSwitchToPreferenceMapEntry + boolean_switch_map[], size_t size) { + for (size_t i = 0; i < size; ++i) { + if (command_line_->HasSwitch(boolean_switch_map[i].switch_name)) { + SetValue(boolean_switch_map[i].preference_path, + std::make_unique(boolean_switch_map[i].set_value), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + } + } +} diff --git a/src/components/prefs/command_line_pref_store.h b/src/components/prefs/command_line_pref_store.h new file mode 100644 index 0000000000..188672caee --- /dev/null +++ b/src/components/prefs/command_line_pref_store.h @@ -0,0 +1,66 @@ +// Copyright (c) 2016 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_PREFS_COMMAND_LINE_PREF_STORE_H_ +#define COMPONENTS_PREFS_COMMAND_LINE_PREF_STORE_H_ + +#include "base/command_line.h" +#include "base/memory/raw_ptr.h" +#include "components/prefs/value_map_pref_store.h" + +// Base class for a PrefStore that maps command line switches to preferences. +// The Apply...Switches() methods can be called by subclasses with their own +// maps, or delegated to other code. +class COMPONENTS_PREFS_EXPORT CommandLinePrefStore : public ValueMapPrefStore { + public: + struct SwitchToPreferenceMapEntry { + const char* switch_name; + const char* preference_path; + }; + + // |set_value| indicates what the preference should be set to if the switch + // is present. + struct BooleanSwitchToPreferenceMapEntry { + const char* switch_name; + const char* preference_path; + bool set_value; + }; + + CommandLinePrefStore(const CommandLinePrefStore&) = delete; + CommandLinePrefStore& operator=(const CommandLinePrefStore&) = delete; + + // Apply command-line switches to the corresponding preferences of the switch + // map, where the value associated with the switch is a string. + void ApplyStringSwitches( + const SwitchToPreferenceMapEntry string_switch_map[], size_t size); + + // Apply command-line switches to the corresponding preferences of the switch + // map, where the value associated with the switch is a path. + void ApplyPathSwitches(const SwitchToPreferenceMapEntry path_switch_map[], + size_t size); + + // Apply command-line switches to the corresponding preferences of the switch + // map, where the value associated with the switch is an integer. + void ApplyIntegerSwitches( + const SwitchToPreferenceMapEntry integer_switch_map[], size_t size); + + // Apply command-line switches to the corresponding preferences of the + // boolean switch map. + void ApplyBooleanSwitches( + const BooleanSwitchToPreferenceMapEntry boolean_switch_map[], + size_t size); + + + protected: + explicit CommandLinePrefStore(const base::CommandLine* command_line); + ~CommandLinePrefStore() override; + + const base::CommandLine* command_line() { return command_line_; } + + private: + // Weak reference. + raw_ptr command_line_; +}; + +#endif // COMPONENTS_PREFS_COMMAND_LINE_PREF_STORE_H_ diff --git a/src/components/prefs/default_pref_store.cc b/src/components/prefs/default_pref_store.cc new file mode 100644 index 0000000000..4104dd39dd --- /dev/null +++ b/src/components/prefs/default_pref_store.cc @@ -0,0 +1,59 @@ +// Copyright (c) 2012 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 "components/prefs/default_pref_store.h" + +#include + +#include "base/check.h" + +using base::Value; + +DefaultPrefStore::DefaultPrefStore() {} + +bool DefaultPrefStore::GetValue(const std::string& key, + const Value** result) const { + return prefs_.GetValue(key, result); +} + +std::unique_ptr DefaultPrefStore::GetValues() const { + return prefs_.AsDictionaryValue(); +} + +void DefaultPrefStore::AddObserver(PrefStore::Observer* observer) { + observers_.AddObserver(observer); +} + +void DefaultPrefStore::RemoveObserver(PrefStore::Observer* observer) { + observers_.RemoveObserver(observer); +} + +bool DefaultPrefStore::HasObservers() const { + return !observers_.empty(); +} + +void DefaultPrefStore::SetDefaultValue(const std::string& key, Value value) { + DCHECK(!GetValue(key, nullptr)); + prefs_.SetValue(key, std::move(value)); +} + +void DefaultPrefStore::ReplaceDefaultValue(const std::string& key, + Value value) { + DCHECK(GetValue(key, nullptr)); + bool notify = prefs_.SetValue(key, std::move(value)); + if (notify) { + for (Observer& observer : observers_) + observer.OnPrefValueChanged(key); + } +} + +DefaultPrefStore::const_iterator DefaultPrefStore::begin() const { + return prefs_.begin(); +} + +DefaultPrefStore::const_iterator DefaultPrefStore::end() const { + return prefs_.end(); +} + +DefaultPrefStore::~DefaultPrefStore() {} diff --git a/src/components/prefs/default_pref_store.h b/src/components/prefs/default_pref_store.h new file mode 100644 index 0000000000..d6b691b6b7 --- /dev/null +++ b/src/components/prefs/default_pref_store.h @@ -0,0 +1,54 @@ +// Copyright (c) 2012 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_PREFS_DEFAULT_PREF_STORE_H_ +#define COMPONENTS_PREFS_DEFAULT_PREF_STORE_H_ + +#include +#include + +#include "base/observer_list.h" +#include "base/values.h" +#include "components/prefs/pref_store.h" +#include "components/prefs/pref_value_map.h" +#include "components/prefs/prefs_export.h" + +// Used within a PrefRegistry to keep track of default preference values. +class COMPONENTS_PREFS_EXPORT DefaultPrefStore : public PrefStore { + public: + typedef PrefValueMap::const_iterator const_iterator; + + DefaultPrefStore(); + + DefaultPrefStore(const DefaultPrefStore&) = delete; + DefaultPrefStore& operator=(const DefaultPrefStore&) = delete; + + // PrefStore implementation: + bool GetValue(const std::string& key, + const base::Value** result) const override; + std::unique_ptr GetValues() const override; + void AddObserver(PrefStore::Observer* observer) override; + void RemoveObserver(PrefStore::Observer* observer) override; + bool HasObservers() const override; + + // Sets a |value| for |key|. Should only be called if a value has not been + // set yet; otherwise call ReplaceDefaultValue(). + void SetDefaultValue(const std::string& key, base::Value value); + + // Replaces the the value for |key| with a new value. Should only be called + // if a value has alreday been set; otherwise call SetDefaultValue(). + void ReplaceDefaultValue(const std::string& key, base::Value value); + + const_iterator begin() const; + const_iterator end() const; + + private: + ~DefaultPrefStore() override; + + PrefValueMap prefs_; + + base::ObserverList::Unchecked observers_; +}; + +#endif // COMPONENTS_PREFS_DEFAULT_PREF_STORE_H_ diff --git a/src/components/prefs/default_pref_store_unittest.cc b/src/components/prefs/default_pref_store_unittest.cc new file mode 100644 index 0000000000..28adc8b64b --- /dev/null +++ b/src/components/prefs/default_pref_store_unittest.cc @@ -0,0 +1,67 @@ +// Copyright (c) 2012 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 "components/prefs/default_pref_store.h" +#include "base/memory/raw_ptr.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::Value; + +namespace { + +class MockPrefStoreObserver : public PrefStore::Observer { + public: + explicit MockPrefStoreObserver(DefaultPrefStore* pref_store); + + MockPrefStoreObserver(const MockPrefStoreObserver&) = delete; + MockPrefStoreObserver& operator=(const MockPrefStoreObserver&) = delete; + + ~MockPrefStoreObserver() override; + + int change_count() { + return change_count_; + } + + // PrefStore::Observer implementation: + void OnPrefValueChanged(const std::string& key) override; + void OnInitializationCompleted(bool succeeded) override {} + + private: + raw_ptr pref_store_; + + int change_count_; +}; + +MockPrefStoreObserver::MockPrefStoreObserver(DefaultPrefStore* pref_store) + : pref_store_(pref_store), change_count_(0) { + pref_store_->AddObserver(this); +} + +MockPrefStoreObserver::~MockPrefStoreObserver() { + pref_store_->RemoveObserver(this); +} + +void MockPrefStoreObserver::OnPrefValueChanged(const std::string& key) { + change_count_++; +} + +} // namespace + +TEST(DefaultPrefStoreTest, NotifyPrefValueChanged) { + scoped_refptr pref_store(new DefaultPrefStore); + MockPrefStoreObserver observer(pref_store.get()); + std::string kPrefKey("pref_key"); + + // Setting a default value shouldn't send a change notification. + pref_store->SetDefaultValue(kPrefKey, Value("foo")); + EXPECT_EQ(0, observer.change_count()); + + // Replacing the default value should send a change notification... + pref_store->ReplaceDefaultValue(kPrefKey, Value("bar")); + EXPECT_EQ(1, observer.change_count()); + + // But only if the value actually changed. + pref_store->ReplaceDefaultValue(kPrefKey, Value("bar")); + EXPECT_EQ(1, observer.change_count()); +} diff --git a/src/components/prefs/in_memory_pref_store.cc b/src/components/prefs/in_memory_pref_store.cc new file mode 100644 index 0000000000..662e7a18d1 --- /dev/null +++ b/src/components/prefs/in_memory_pref_store.cc @@ -0,0 +1,91 @@ +// Copyright (c) 2016 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 "components/prefs/in_memory_pref_store.h" + +#include +#include + +#include "base/values.h" + +InMemoryPrefStore::InMemoryPrefStore() {} + +InMemoryPrefStore::~InMemoryPrefStore() {} + +bool InMemoryPrefStore::GetValue(const std::string& key, + const base::Value** value) const { + return prefs_.GetValue(key, value); +} + +std::unique_ptr InMemoryPrefStore::GetValues() const { + return prefs_.AsDictionaryValue(); +} + +bool InMemoryPrefStore::GetMutableValue(const std::string& key, + base::Value** value) { + return prefs_.GetValue(key, value); +} + +void InMemoryPrefStore::AddObserver(PrefStore::Observer* observer) { + observers_.AddObserver(observer); +} + +void InMemoryPrefStore::RemoveObserver(PrefStore::Observer* observer) { + observers_.RemoveObserver(observer); +} + +bool InMemoryPrefStore::HasObservers() const { + return !observers_.empty(); +} + +bool InMemoryPrefStore::IsInitializationComplete() const { + return true; +} + +void InMemoryPrefStore::SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + DCHECK(value); + if (prefs_.SetValue(key, base::Value::FromUniquePtrValue(std::move(value)))) + ReportValueChanged(key, flags); +} + +void InMemoryPrefStore::SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + DCHECK(value); + prefs_.SetValue(key, base::Value::FromUniquePtrValue(std::move(value))); +} + +void InMemoryPrefStore::RemoveValue(const std::string& key, uint32_t flags) { + if (prefs_.RemoveValue(key)) + ReportValueChanged(key, flags); +} + +void InMemoryPrefStore::RemoveValuesByPrefixSilently( + const std::string& prefix) { + prefs_.ClearWithPrefix(prefix); +} + +bool InMemoryPrefStore::ReadOnly() const { + return false; +} + +PersistentPrefStore::PrefReadError InMemoryPrefStore::GetReadError() const { + return PersistentPrefStore::PREF_READ_ERROR_NONE; +} + +PersistentPrefStore::PrefReadError InMemoryPrefStore::ReadPrefs() { + return PersistentPrefStore::PREF_READ_ERROR_NONE; +} + +void InMemoryPrefStore::ReportValueChanged(const std::string& key, + uint32_t flags) { + for (Observer& observer : observers_) + observer.OnPrefValueChanged(key); +} + +bool InMemoryPrefStore::IsInMemoryPrefStore() const { + return true; +} diff --git a/src/components/prefs/in_memory_pref_store.h b/src/components/prefs/in_memory_pref_store.h new file mode 100644 index 0000000000..ed21f7e0ac --- /dev/null +++ b/src/components/prefs/in_memory_pref_store.h @@ -0,0 +1,67 @@ +// Copyright (c) 2016 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_PREFS_IN_MEMORY_PREF_STORE_H_ +#define COMPONENTS_PREFS_IN_MEMORY_PREF_STORE_H_ + +#include + +#include + +#include "base/compiler_specific.h" +#include "base/observer_list.h" +#include "components/prefs/persistent_pref_store.h" +#include "components/prefs/pref_value_map.h" + +// A light-weight prefstore implementation that keeps preferences +// in a memory backed store. This is not a persistent prefstore -- we +// subclass the PersistentPrefStore here since it is needed by the +// PrefService, which in turn is needed by various components. +class COMPONENTS_PREFS_EXPORT InMemoryPrefStore : public PersistentPrefStore { + public: + InMemoryPrefStore(); + + InMemoryPrefStore(const InMemoryPrefStore&) = delete; + InMemoryPrefStore& operator=(const InMemoryPrefStore&) = delete; + + // PrefStore implementation. + bool GetValue(const std::string& key, + const base::Value** result) const override; + std::unique_ptr GetValues() const override; + void AddObserver(PrefStore::Observer* observer) override; + void RemoveObserver(PrefStore::Observer* observer) override; + bool HasObservers() const override; + bool IsInitializationComplete() const override; + + // PersistentPrefStore implementation. + bool GetMutableValue(const std::string& key, base::Value** result) override; + void ReportValueChanged(const std::string& key, uint32_t flags) override; + void SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + void SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + void RemoveValue(const std::string& key, uint32_t flags) override; + bool ReadOnly() const override; + PrefReadError GetReadError() const override; + PersistentPrefStore::PrefReadError ReadPrefs() override; + void ReadPrefsAsync(ReadErrorDelegate* error_delegate) override {} + void SchedulePendingLossyWrites() override {} + void ClearMutableValues() override {} + void OnStoreDeletionFromDisk() override {} + bool IsInMemoryPrefStore() const override; + void RemoveValuesByPrefixSilently(const std::string& prefix) override; + + protected: + ~InMemoryPrefStore() override; + + private: + // Stores the preference values. + PrefValueMap prefs_; + + base::ObserverList::Unchecked observers_; +}; + +#endif // COMPONENTS_PREFS_IN_MEMORY_PREF_STORE_H_ diff --git a/src/components/prefs/in_memory_pref_store_unittest.cc b/src/components/prefs/in_memory_pref_store_unittest.cc new file mode 100644 index 0000000000..81fcf83b9e --- /dev/null +++ b/src/components/prefs/in_memory_pref_store_unittest.cc @@ -0,0 +1,133 @@ +// Copyright 2016 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 "components/prefs/in_memory_pref_store.h" + +#include + +#include "base/test/task_environment.h" +#include "base/values.h" +#include "components/prefs/persistent_pref_store_unittest.h" +#include "components/prefs/pref_store_observer_mock.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { +const char kTestPref[] = "test.pref"; + +class InMemoryPrefStoreTest : public testing::Test { + public: + InMemoryPrefStoreTest() { } + + void SetUp() override { store_ = new InMemoryPrefStore(); } + protected: + base::test::TaskEnvironment task_environment_; + scoped_refptr store_; + PrefStoreObserverMock observer_; +}; + +TEST_F(InMemoryPrefStoreTest, SetGetValue) { + const base::Value* value = nullptr; + base::Value* mutable_value = nullptr; + EXPECT_FALSE(store_->GetValue(kTestPref, &value)); + EXPECT_FALSE(store_->GetMutableValue(kTestPref, &mutable_value)); + + store_->SetValue(kTestPref, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_TRUE(store_->GetValue(kTestPref, &value)); + EXPECT_EQ(base::Value(42), *value); + EXPECT_TRUE(store_->GetMutableValue(kTestPref, &mutable_value)); + EXPECT_EQ(base::Value(42), *mutable_value); + + store_->RemoveValue(kTestPref, WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_FALSE(store_->GetValue(kTestPref, &value)); + EXPECT_FALSE(store_->GetMutableValue(kTestPref, &mutable_value)); +} + +TEST_F(InMemoryPrefStoreTest, GetSetObserver) { + // Starts with no observers. + EXPECT_FALSE(store_->HasObservers()); + + // Add one. + store_->AddObserver(&observer_); + EXPECT_TRUE(store_->HasObservers()); + + // Remove only observer. + store_->RemoveObserver(&observer_); + EXPECT_FALSE(store_->HasObservers()); +} + +TEST_F(InMemoryPrefStoreTest, CallObserver) { + // With observer included. + store_->AddObserver(&observer_); + + // Triggers on SetValue. + store_->SetValue(kTestPref, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + observer_.VerifyAndResetChangedKey(kTestPref); + + // And RemoveValue. + store_->RemoveValue(kTestPref, WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + observer_.VerifyAndResetChangedKey(kTestPref); + + // But not SetValueSilently. + store_->SetValueSilently(kTestPref, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_EQ(0u, observer_.changed_keys.size()); + + // On multiple RemoveValues only the first one triggers observer. + store_->RemoveValue(kTestPref, WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + observer_.VerifyAndResetChangedKey(kTestPref); + store_->RemoveValue(kTestPref, WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_EQ(0u, observer_.changed_keys.size()); + + // Doesn't make call on removed observers. + store_->RemoveObserver(&observer_); + store_->SetValue(kTestPref, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + store_->RemoveValue(kTestPref, WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_EQ(0u, observer_.changed_keys.size()); +} + +TEST_F(InMemoryPrefStoreTest, Initialization) { + EXPECT_TRUE(store_->IsInitializationComplete()); +} + +TEST_F(InMemoryPrefStoreTest, ReadOnly) { + EXPECT_FALSE(store_->ReadOnly()); +} + +TEST_F(InMemoryPrefStoreTest, GetReadError) { + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, store_->GetReadError()); +} + +TEST_F(InMemoryPrefStoreTest, ReadPrefs) { + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, store_->ReadPrefs()); +} + +TEST_F(InMemoryPrefStoreTest, CommitPendingWriteWithCallback) { + TestCommitPendingWriteWithCallback(store_.get(), &task_environment_); +} + +TEST_F(InMemoryPrefStoreTest, RemoveValuesByPrefix) { + const base::Value* value; + const std::string prefix = "pref"; + const std::string subpref_name1 = "pref.a"; + const std::string subpref_name2 = "pref.b"; + const std::string other_name = "other"; + + store_->SetValue(subpref_name1, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + store_->SetValue(subpref_name2, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + store_->SetValue(other_name, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + store_->RemoveValuesByPrefixSilently(prefix); + EXPECT_FALSE(store_->GetValue(subpref_name1, &value)); + EXPECT_FALSE(store_->GetValue(subpref_name2, &value)); + EXPECT_TRUE(store_->GetValue(other_name, &value)); +} + +} // namespace diff --git a/src/components/prefs/ios/BUILD.gn b/src/components/prefs/ios/BUILD.gn new file mode 100644 index 0000000000..82aa654591 --- /dev/null +++ b/src/components/prefs/ios/BUILD.gn @@ -0,0 +1,15 @@ +# Copyright 2018 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. + +source_set("ios") { + configs += [ "//build/config/compiler:enable_arc" ] + sources = [ + "pref_observer_bridge.h", + "pref_observer_bridge.mm", + ] + deps = [ + "//base", + "//components/prefs", + ] +} diff --git a/src/components/prefs/ios/pref_observer_bridge.h b/src/components/prefs/ios/pref_observer_bridge.h new file mode 100644 index 0000000000..425ef87027 --- /dev/null +++ b/src/components/prefs/ios/pref_observer_bridge.h @@ -0,0 +1,32 @@ +// Copyright 2012 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_PREFS_IOS_PREF_OBSERVER_BRIDGE_H_ +#define COMPONENTS_PREFS_IOS_PREF_OBSERVER_BRIDGE_H_ + +#import + +#include + +class PrefChangeRegistrar; + +@protocol PrefObserverDelegate +- (void)onPreferenceChanged:(const std::string&)preferenceName; +@end + +class PrefObserverBridge { + public: + explicit PrefObserverBridge(id delegate); + virtual ~PrefObserverBridge(); + + virtual void ObserveChangesForPreference(const std::string& pref_name, + PrefChangeRegistrar* registrar); + + private: + virtual void OnPreferenceChanged(const std::string& pref_name); + + __weak id delegate_ = nil; +}; + +#endif // COMPONENTS_PREFS_IOS_PREF_OBSERVER_BRIDGE_H_ diff --git a/src/components/prefs/ios/pref_observer_bridge.mm b/src/components/prefs/ios/pref_observer_bridge.mm new file mode 100644 index 0000000000..852309ff24 --- /dev/null +++ b/src/components/prefs/ios/pref_observer_bridge.mm @@ -0,0 +1,29 @@ +// Copyright 2012 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. + +#import "components/prefs/ios/pref_observer_bridge.h" + +#include "base/bind.h" +#include "components/prefs/pref_change_registrar.h" + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +PrefObserverBridge::PrefObserverBridge(id delegate) + : delegate_(delegate) {} + +PrefObserverBridge::~PrefObserverBridge() {} + +void PrefObserverBridge::ObserveChangesForPreference( + const std::string& pref_name, + PrefChangeRegistrar* registrar) { + PrefChangeRegistrar::NamedChangeCallback callback = base::BindRepeating( + &PrefObserverBridge::OnPreferenceChanged, base::Unretained(this)); + registrar->Add(pref_name.c_str(), callback); +} + +void PrefObserverBridge::OnPreferenceChanged(const std::string& pref_name) { + [delegate_ onPreferenceChanged:pref_name]; +} diff --git a/src/components/prefs/json_pref_store.cc b/src/components/prefs/json_pref_store.cc new file mode 100644 index 0000000000..f84b6625d2 --- /dev/null +++ b/src/components/prefs/json_pref_store.cc @@ -0,0 +1,551 @@ +// Copyright (c) 2012 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 "components/prefs/json_pref_store.h" + +#include + +#include +#include + +#include "base/bind.h" +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/json/json_file_value_serializer.h" +#include "base/json/json_string_value_serializer.h" +#include "base/logging.h" +#include "base/memory/ref_counted.h" +#include "base/metrics/histogram.h" +#include "base/ranges/algorithm.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/task_runner_util.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/time/default_clock.h" +#include "base/values.h" +#include "components/prefs/pref_filter.h" + +// Result returned from internal read tasks. +struct JsonPrefStore::ReadResult { + public: + ReadResult(); + ~ReadResult(); + ReadResult(const ReadResult&) = delete; + ReadResult& operator=(const ReadResult&) = delete; + + std::unique_ptr value; + PrefReadError error = PersistentPrefStore::PREF_READ_ERROR_NONE; + bool no_dir = false; + size_t num_bytes_read = 0u; +}; + +JsonPrefStore::ReadResult::ReadResult() = default; +JsonPrefStore::ReadResult::~ReadResult() = default; + +namespace { + +// Some extensions we'll tack on to copies of the Preferences files. +const base::FilePath::CharType kBadExtension[] = FILE_PATH_LITERAL("bad"); + +bool BackupPrefsFile(const base::FilePath& path) { + const base::FilePath bad = path.ReplaceExtension(kBadExtension); + const bool bad_existed = base::PathExists(bad); + base::Move(path, bad); + return bad_existed; +} + +PersistentPrefStore::PrefReadError HandleReadErrors( + const base::Value* value, + const base::FilePath& path, + int error_code, + const std::string& error_msg) { + if (!value) { + DVLOG(1) << "Error while loading JSON file: " << error_msg + << ", file: " << path.value(); + switch (error_code) { + case JSONFileValueDeserializer::JSON_ACCESS_DENIED: + return PersistentPrefStore::PREF_READ_ERROR_ACCESS_DENIED; + case JSONFileValueDeserializer::JSON_CANNOT_READ_FILE: + return PersistentPrefStore::PREF_READ_ERROR_FILE_OTHER; + case JSONFileValueDeserializer::JSON_FILE_LOCKED: + return PersistentPrefStore::PREF_READ_ERROR_FILE_LOCKED; + case JSONFileValueDeserializer::JSON_NO_SUCH_FILE: + return PersistentPrefStore::PREF_READ_ERROR_NO_FILE; + default: + // JSON errors indicate file corruption of some sort. + // Since the file is corrupt, move it to the side and continue with + // empty preferences. This will result in them losing their settings. + // We keep the old file for possible support and debugging assistance + // as well as to detect if they're seeing these errors repeatedly. + // TODO(erikkay) Instead, use the last known good file. + // If they've ever had a parse error before, put them in another bucket. + // TODO(erikkay) if we keep this error checking for very long, we may + // want to differentiate between recent and long ago errors. + const bool bad_existed = BackupPrefsFile(path); + return bad_existed ? PersistentPrefStore::PREF_READ_ERROR_JSON_REPEAT + : PersistentPrefStore::PREF_READ_ERROR_JSON_PARSE; + } + } + if (!value->is_dict()) + return PersistentPrefStore::PREF_READ_ERROR_JSON_TYPE; + return PersistentPrefStore::PREF_READ_ERROR_NONE; +} + +// Records a sample for |size| in the Settings.JsonDataReadSizeKilobytes +// histogram suffixed with the base name of the JSON file under |path|. +void RecordJsonDataSizeHistogram(const base::FilePath& path, size_t size) { + std::string spaceless_basename; + base::ReplaceChars(path.BaseName().MaybeAsASCII(), " ", "_", + &spaceless_basename); + + // The histogram below is an expansion of the UMA_HISTOGRAM_CUSTOM_COUNTS + // macro adapted to allow for a dynamically suffixed histogram name. + // Note: The factory creates and owns the histogram. + // This histogram is expired but the code was intentionally left behind so + // it can be re-enabled on Stable in a single config tweak if needed. + base::HistogramBase* histogram = base::Histogram::FactoryGet( + "Settings.JsonDataReadSizeKilobytes." + spaceless_basename, 1, 10000, 50, + base::HistogramBase::kUmaTargetedHistogramFlag); + histogram->Add(static_cast(size) / 1024); +} + +std::unique_ptr ReadPrefsFromDisk( + const base::FilePath& path) { + int error_code; + std::string error_msg; + auto read_result = std::make_unique(); + JSONFileValueDeserializer deserializer(path); + read_result->value = deserializer.Deserialize(&error_code, &error_msg); + read_result->error = + HandleReadErrors(read_result->value.get(), path, error_code, error_msg); + read_result->no_dir = !base::PathExists(path.DirName()); + read_result->num_bytes_read = deserializer.get_last_read_size(); + + if (read_result->error == PersistentPrefStore::PREF_READ_ERROR_NONE) + RecordJsonDataSizeHistogram(path, deserializer.get_last_read_size()); + + return read_result; +} + +// Returns the a histogram suffix for a few allowlisted JsonPref files. +const char* GetHistogramSuffix(const base::FilePath& path) { + std::string spaceless_basename; + base::ReplaceChars(path.BaseName().MaybeAsASCII(), " ", "_", + &spaceless_basename); + static constexpr std::array kAllowList{ + "Secure_Preferences", "Preferences", "Local_State"}; + const char* const* it = base::ranges::find(kAllowList, spaceless_basename); + return it != kAllowList.end() ? *it : ""; +} + +} // namespace + +JsonPrefStore::JsonPrefStore( + const base::FilePath& pref_filename, + std::unique_ptr pref_filter, + scoped_refptr file_task_runner, + bool read_only) + : path_(pref_filename), + file_task_runner_(std::move(file_task_runner)), + prefs_(new base::DictionaryValue()), + read_only_(read_only), + writer_(pref_filename, + file_task_runner_, + GetHistogramSuffix(pref_filename)), + pref_filter_(std::move(pref_filter)), + initialized_(false), + filtering_in_progress_(false), + pending_lossy_write_(false), + read_error_(PREF_READ_ERROR_NONE), + has_pending_write_reply_(false) { + DCHECK(!path_.empty()); +} + +bool JsonPrefStore::GetValue(const std::string& key, + const base::Value** result) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + base::Value* tmp = prefs_->FindPath(key); + if (!tmp) + return false; + + if (result) + *result = tmp; + return true; +} + +std::unique_ptr JsonPrefStore::GetValues() const { + return prefs_->CreateDeepCopy(); +} + +void JsonPrefStore::AddObserver(PrefStore::Observer* observer) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + observers_.AddObserver(observer); +} + +void JsonPrefStore::RemoveObserver(PrefStore::Observer* observer) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + observers_.RemoveObserver(observer); +} + +bool JsonPrefStore::HasObservers() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return !observers_.empty(); +} + +bool JsonPrefStore::IsInitializationComplete() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + return initialized_; +} + +bool JsonPrefStore::GetMutableValue(const std::string& key, + base::Value** result) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + base::Value* tmp = prefs_->FindPath(key); + if (!tmp) + return false; + + if (result) + *result = tmp; + return true; +} + +void JsonPrefStore::SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + DCHECK(value); + base::Value* old_value = prefs_->FindPath(key); + if (!old_value || *value != *old_value) { + prefs_->SetPath(key, std::move(*value)); + ReportValueChanged(key, flags); + } +} + +void JsonPrefStore::SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + DCHECK(value); + base::Value* old_value = prefs_->FindPath(key); + if (!old_value || *value != *old_value) { + prefs_->SetPath(key, std::move(*value)); + ScheduleWrite(flags); + } +} + +void JsonPrefStore::RemoveValue(const std::string& key, uint32_t flags) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (prefs_->RemovePath(key)) + ReportValueChanged(key, flags); +} + +void JsonPrefStore::RemoveValueSilently(const std::string& key, + uint32_t flags) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + prefs_->RemovePath(key); + ScheduleWrite(flags); +} + +void JsonPrefStore::RemoveValuesByPrefixSilently(const std::string& prefix) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + RemoveValueSilently(prefix, /*flags*/ 0); +} + +bool JsonPrefStore::ReadOnly() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + return read_only_; +} + +PersistentPrefStore::PrefReadError JsonPrefStore::GetReadError() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + return read_error_; +} + +PersistentPrefStore::PrefReadError JsonPrefStore::ReadPrefs() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + OnFileRead(ReadPrefsFromDisk(path_)); + return filtering_in_progress_ ? PREF_READ_ERROR_ASYNCHRONOUS_TASK_INCOMPLETE + : read_error_; +} + +void JsonPrefStore::ReadPrefsAsync(ReadErrorDelegate* error_delegate) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + initialized_ = false; + error_delegate_.reset(error_delegate); + + // Weakly binds the read task so that it doesn't kick in during shutdown. + base::PostTaskAndReplyWithResult( + file_task_runner_.get(), FROM_HERE, + base::BindOnce(&ReadPrefsFromDisk, path_), + base::BindOnce(&JsonPrefStore::OnFileRead, AsWeakPtr())); +} + +void JsonPrefStore::CommitPendingWrite( + base::OnceClosure reply_callback, + base::OnceClosure synchronous_done_callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // Schedule a write for any lossy writes that are outstanding to ensure that + // they get flushed when this function is called. + SchedulePendingLossyWrites(); + + if (writer_.HasPendingWrite() && !read_only_) + writer_.DoScheduledWrite(); + + // Since disk operations occur on |file_task_runner_|, the reply of a task + // posted to |file_task_runner_| will run after currently pending disk + // operations. Also, by definition of PostTaskAndReply(), the reply (in the + // |reply_callback| case will run on the current sequence. + + if (synchronous_done_callback) { + file_task_runner_->PostTask(FROM_HERE, + std::move(synchronous_done_callback)); + } + + if (reply_callback) { + file_task_runner_->PostTaskAndReply(FROM_HERE, base::DoNothing(), + std::move(reply_callback)); + } +} + +void JsonPrefStore::SchedulePendingLossyWrites() { + if (pending_lossy_write_) + writer_.ScheduleWrite(this); +} + +void JsonPrefStore::ReportValueChanged(const std::string& key, uint32_t flags) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (pref_filter_) + pref_filter_->FilterUpdate(key); + + for (PrefStore::Observer& observer : observers_) + observer.OnPrefValueChanged(key); + + ScheduleWrite(flags); +} + +void JsonPrefStore::RunOrScheduleNextSuccessfulWriteCallback( + bool write_success) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + has_pending_write_reply_ = false; + if (!on_next_successful_write_reply_.is_null()) { + base::OnceClosure on_successful_write = + std::move(on_next_successful_write_reply_); + if (write_success) { + std::move(on_successful_write).Run(); + } else { + RegisterOnNextSuccessfulWriteReply(std::move(on_successful_write)); + } + } +} + +// static +void JsonPrefStore::PostWriteCallback( + base::OnceCallback on_next_write_callback, + base::OnceCallback on_next_write_reply, + scoped_refptr reply_task_runner, + bool write_success) { + if (!on_next_write_callback.is_null()) + std::move(on_next_write_callback).Run(write_success); + + // We can't run |on_next_write_reply| on the current thread. Bounce back to + // the |reply_task_runner| which is the correct sequenced thread. + reply_task_runner->PostTask( + FROM_HERE, base::BindOnce(std::move(on_next_write_reply), write_success)); +} + +void JsonPrefStore::RegisterOnNextSuccessfulWriteReply( + base::OnceClosure on_next_successful_write_reply) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(on_next_successful_write_reply_.is_null()); + + on_next_successful_write_reply_ = std::move(on_next_successful_write_reply); + + // If there are pending callbacks, avoid erasing them; the reply will be used + // as we set |on_next_successful_write_reply_|. Otherwise, setup a reply with + // an empty callback. + if (!has_pending_write_reply_) { + has_pending_write_reply_ = true; + writer_.RegisterOnNextWriteCallbacks( + base::OnceClosure(), + base::BindOnce( + &PostWriteCallback, base::OnceCallback(), + base::BindOnce( + &JsonPrefStore::RunOrScheduleNextSuccessfulWriteCallback, + AsWeakPtr()), + base::SequencedTaskRunnerHandle::Get())); + } +} + +void JsonPrefStore::RegisterOnNextWriteSynchronousCallbacks( + OnWriteCallbackPair callbacks) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + has_pending_write_reply_ = true; + + writer_.RegisterOnNextWriteCallbacks( + std::move(callbacks.first), + base::BindOnce( + &PostWriteCallback, std::move(callbacks.second), + base::BindOnce( + &JsonPrefStore::RunOrScheduleNextSuccessfulWriteCallback, + AsWeakPtr()), + base::SequencedTaskRunnerHandle::Get())); +} + +void JsonPrefStore::ClearMutableValues() { + NOTIMPLEMENTED(); +} + +void JsonPrefStore::OnStoreDeletionFromDisk() { + if (pref_filter_) + pref_filter_->OnStoreDeletionFromDisk(); +} + +void JsonPrefStore::OnFileRead(std::unique_ptr read_result) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + DCHECK(read_result); + + auto unfiltered_prefs = std::make_unique(); + + read_error_ = read_result->error; + + bool initialization_successful = !read_result->no_dir; + + if (initialization_successful) { + switch (read_error_) { + case PREF_READ_ERROR_ACCESS_DENIED: + case PREF_READ_ERROR_FILE_OTHER: + case PREF_READ_ERROR_FILE_LOCKED: + case PREF_READ_ERROR_JSON_TYPE: + case PREF_READ_ERROR_FILE_NOT_SPECIFIED: + read_only_ = true; + break; + case PREF_READ_ERROR_NONE: + DCHECK(read_result->value); + writer_.set_previous_data_size(read_result->num_bytes_read); + unfiltered_prefs.reset( + static_cast(read_result->value.release())); + break; + case PREF_READ_ERROR_NO_FILE: + // If the file just doesn't exist, maybe this is first run. In any case + // there's no harm in writing out default prefs in this case. + case PREF_READ_ERROR_JSON_PARSE: + case PREF_READ_ERROR_JSON_REPEAT: + break; + case PREF_READ_ERROR_ASYNCHRONOUS_TASK_INCOMPLETE: + // This is a special error code to be returned by ReadPrefs when it + // can't complete synchronously, it should never be returned by the read + // operation itself. + case PREF_READ_ERROR_MAX_ENUM: + NOTREACHED(); + break; + } + } + + if (pref_filter_) { + filtering_in_progress_ = true; + PrefFilter::PostFilterOnLoadCallback post_filter_on_load_callback( + base::BindOnce(&JsonPrefStore::FinalizeFileRead, AsWeakPtr(), + initialization_successful)); + pref_filter_->FilterOnLoad(std::move(post_filter_on_load_callback), + std::move(unfiltered_prefs)); + } else { + FinalizeFileRead(initialization_successful, std::move(unfiltered_prefs), + false); + } +} + +JsonPrefStore::~JsonPrefStore() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + CommitPendingWrite(); +} + +bool JsonPrefStore::SerializeData(std::string* output) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + pending_lossy_write_ = false; + + if (pref_filter_) { + OnWriteCallbackPair callbacks = + pref_filter_->FilterSerializeData(prefs_.get()); + if (!callbacks.first.is_null() || !callbacks.second.is_null()) + RegisterOnNextWriteSynchronousCallbacks(std::move(callbacks)); + } + + JSONStringValueSerializer serializer(output); + // Not pretty-printing prefs shrinks pref file size by ~30%. To obtain + // readable prefs for debugging purposes, you can dump your prefs into any + // command-line or online JSON pretty printing tool. + serializer.set_pretty_print(false); + const bool success = serializer.Serialize(*prefs_); + if (!success) { + // Failed to serialize prefs file. Backup the existing prefs file and + // crash. + BackupPrefsFile(path_); + CHECK(false) << "Failed to serialize preferences : " << path_ + << "\nBacked up under " + << path_.ReplaceExtension(kBadExtension); + } + return success; +} + +void JsonPrefStore::FinalizeFileRead( + bool initialization_successful, + std::unique_ptr prefs, + bool schedule_write) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + filtering_in_progress_ = false; + + if (!initialization_successful) { + for (PrefStore::Observer& observer : observers_) + observer.OnInitializationCompleted(false); + return; + } + + prefs_ = std::move(prefs); + + initialized_ = true; + + if (schedule_write) + ScheduleWrite(DEFAULT_PREF_WRITE_FLAGS); + + if (error_delegate_ && read_error_ != PREF_READ_ERROR_NONE) + error_delegate_->OnError(read_error_); + + for (PrefStore::Observer& observer : observers_) + observer.OnInitializationCompleted(true); + + return; +} + +void JsonPrefStore::ScheduleWrite(uint32_t flags) { + if (read_only_) + return; + + if (flags & LOSSY_PREF_WRITE_FLAG) + pending_lossy_write_ = true; + else + writer_.ScheduleWrite(this); +} diff --git a/src/components/prefs/json_pref_store.h b/src/components/prefs/json_pref_store.h new file mode 100644 index 0000000000..a660cc8347 --- /dev/null +++ b/src/components/prefs/json_pref_store.h @@ -0,0 +1,211 @@ +// Copyright (c) 2012 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_PREFS_JSON_PREF_STORE_H_ +#define COMPONENTS_PREFS_JSON_PREF_STORE_H_ + +#include + +#include +#include +#include + +#include "base/callback_forward.h" +#include "base/compiler_specific.h" +#include "base/files/file_path.h" +#include "base/files/important_file_writer.h" +#include "base/gtest_prod_util.h" +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/sequence_checker.h" +#include "base/task/post_task.h" +#include "base/task/thread_pool.h" +#include "components/prefs/persistent_pref_store.h" +#include "components/prefs/pref_filter.h" +#include "components/prefs/prefs_export.h" + +class PrefFilter; + +namespace base { +class DictionaryValue; +class FilePath; +class JsonPrefStoreCallbackTest; +class JsonPrefStoreLossyWriteTest; +class SequencedTaskRunner; +class WriteCallbacksObserver; +class Value; +} + +// A writable PrefStore implementation that is used for user preferences. +class COMPONENTS_PREFS_EXPORT JsonPrefStore + : public PersistentPrefStore, + public base::ImportantFileWriter::DataSerializer, + public base::SupportsWeakPtr { + public: + struct ReadResult; + + // A pair of callbacks to call before and after the preference file is written + // to disk. + using OnWriteCallbackPair = + std::pair>; + + // |pref_filename| is the path to the file to read prefs from. It is incorrect + // to create multiple JsonPrefStore with the same |pref_filename|. + // |file_task_runner| is used for asynchronous reads and writes. It must + // have the base::TaskShutdownBehavior::BLOCK_SHUTDOWN and base::MayBlock() + // traits. Unless external tasks need to run on the same sequence as + // JsonPrefStore tasks, keep the default value. + // The initial read is done synchronously, the TaskPriority is thus only used + // for flushes to disks and BEST_EFFORT is therefore appropriate. Priority of + // remaining BEST_EFFORT+BLOCK_SHUTDOWN tasks is bumped by the ThreadPool on + // shutdown. However, some shutdown use cases happen without + // ThreadPoolInstance::Shutdown() (e.g. ChromeRestartRequest::Start() and + // BrowserProcessImpl::EndSession()) and we must thus unfortunately make this + // USER_VISIBLE until we solve https://crbug.com/747495 to allow bumping + // priority of a sequence on demand. + JsonPrefStore(const base::FilePath& pref_filename, + std::unique_ptr pref_filter = nullptr, + scoped_refptr file_task_runner = + base::ThreadPool::CreateSequencedTaskRunner( + {base::MayBlock(), base::TaskPriority::USER_VISIBLE, + base::TaskShutdownBehavior::BLOCK_SHUTDOWN}), + bool read_only = false); + + JsonPrefStore(const JsonPrefStore&) = delete; + JsonPrefStore& operator=(const JsonPrefStore&) = delete; + + // PrefStore overrides: + bool GetValue(const std::string& key, + const base::Value** result) const override; + std::unique_ptr GetValues() const override; + void AddObserver(PrefStore::Observer* observer) override; + void RemoveObserver(PrefStore::Observer* observer) override; + bool HasObservers() const override; + bool IsInitializationComplete() const override; + + // PersistentPrefStore overrides: + bool GetMutableValue(const std::string& key, base::Value** result) override; + void SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + void SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + void RemoveValue(const std::string& key, uint32_t flags) override; + bool ReadOnly() const override; + PrefReadError GetReadError() const override; + // Note this method may be asynchronous if this instance has a |pref_filter_| + // in which case it will return PREF_READ_ERROR_ASYNCHRONOUS_TASK_INCOMPLETE. + // See details in pref_filter.h. + PrefReadError ReadPrefs() override; + void ReadPrefsAsync(ReadErrorDelegate* error_delegate) override; + void CommitPendingWrite( + base::OnceClosure reply_callback = base::OnceClosure(), + base::OnceClosure synchronous_done_callback = + base::OnceClosure()) override; + void SchedulePendingLossyWrites() override; + void ReportValueChanged(const std::string& key, uint32_t flags) override; + + // Just like RemoveValue(), but doesn't notify observers. Used when doing some + // cleanup that shouldn't otherwise alert observers. + void RemoveValueSilently(const std::string& key, uint32_t flags); + + // Just like RemoveValue(), but removes all the prefs that start with + // |prefix|. Used for pref-initialization cleanup. + void RemoveValuesByPrefixSilently(const std::string& prefix) override; + // Registers |on_next_successful_write_reply| to be called once, on the next + // successful write event of |writer_|. + // |on_next_successful_write_reply| will be called on the thread from which + // this method is called and does not need to be thread safe. + void RegisterOnNextSuccessfulWriteReply( + base::OnceClosure on_next_successful_write_reply); + + void ClearMutableValues() override; + + void OnStoreDeletionFromDisk() override; + +#if defined(UNIT_TEST) + base::ImportantFileWriter& get_writer() { return writer_; } +#endif + + private: + friend class base::JsonPrefStoreCallbackTest; + friend class base::JsonPrefStoreLossyWriteTest; + friend class base::WriteCallbacksObserver; + + ~JsonPrefStore() override; + + // If |write_success| is true, runs |on_next_successful_write_|. + // Otherwise, re-registers |on_next_successful_write_|. + void RunOrScheduleNextSuccessfulWriteCallback(bool write_success); + + // Handles the result of a write with result |write_success|. Runs + // |on_next_write_callback| on the current thread and posts + // |on_next_write_reply| on |reply_task_runner|. + static void PostWriteCallback( + base::OnceCallback on_next_write_callback, + base::OnceCallback on_next_write_reply, + scoped_refptr reply_task_runner, + bool write_success); + + // Registers the |callbacks| pair to be called once synchronously before and + // after, respectively, the next write event of |writer_|. + // Both callbacks must be thread-safe. + void RegisterOnNextWriteSynchronousCallbacks(OnWriteCallbackPair callbacks); + + // This method is called after the JSON file has been read. It then hands + // |value| (or an empty dictionary in some read error cases) to the + // |pref_filter| if one is set. It also gives a callback pointing at + // FinalizeFileRead() to that |pref_filter_| which is then responsible for + // invoking it when done. If there is no |pref_filter_|, FinalizeFileRead() + // is invoked directly. + void OnFileRead(std::unique_ptr read_result); + + // ImportantFileWriter::DataSerializer overrides: + bool SerializeData(std::string* output) override; + + // This method is called after the JSON file has been read and the result has + // potentially been intercepted and modified by |pref_filter_|. + // |initialization_successful| is pre-determined by OnFileRead() and should + // be used when reporting OnInitializationCompleted(). + // |schedule_write| indicates whether a write should be immediately scheduled + // (typically because the |pref_filter_| has already altered the |prefs|) -- + // this will be ignored if this store is read-only. + void FinalizeFileRead(bool initialization_successful, + std::unique_ptr prefs, + bool schedule_write); + + // Schedule a write with the file writer as long as |flags| doesn't contain + // WriteablePrefStore::LOSSY_PREF_WRITE_FLAG. + void ScheduleWrite(uint32_t flags); + + const base::FilePath path_; + const scoped_refptr file_task_runner_; + + std::unique_ptr prefs_; + + bool read_only_; + + // Helper for safely writing pref data. + base::ImportantFileWriter writer_; + + std::unique_ptr pref_filter_; + base::ObserverList::Unchecked observers_; + + std::unique_ptr error_delegate_; + + bool initialized_; + bool filtering_in_progress_; + bool pending_lossy_write_; + PrefReadError read_error_; + + std::set keys_need_empty_value_; + + bool has_pending_write_reply_ = true; + base::OnceClosure on_next_successful_write_reply_; + + SEQUENCE_CHECKER(sequence_checker_); +}; + +#endif // COMPONENTS_PREFS_JSON_PREF_STORE_H_ diff --git a/src/components/prefs/json_pref_store_unittest.cc b/src/components/prefs/json_pref_store_unittest.cc new file mode 100644 index 0000000000..6deeb68010 --- /dev/null +++ b/src/components/prefs/json_pref_store_unittest.cc @@ -0,0 +1,1018 @@ +// Copyright (c) 2012 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 "components/prefs/json_pref_store.h" + +#include + +#include +#include + +#include "base/bind.h" +#include "base/compiler_specific.h" +#include "base/cxx17_backports.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/location.h" +#include "base/memory/ref_counted.h" +#include "base/metrics/histogram_samples.h" +#include "base/path_service.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/synchronization/waitable_event.h" +#include "base/task/single_thread_task_runner.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/task_environment.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/threading/thread.h" +#include "base/values.h" +#include "components/prefs/persistent_pref_store_unittest.h" +#include "components/prefs/pref_filter.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace base { +namespace { + +const char kHomePage[] = "homepage"; + +const char kReadJson[] = + "{\n" + " \"homepage\": \"http://www.cnn.com\",\n" + " \"some_directory\": \"/usr/local/\",\n" + " \"tabs\": {\n" + " \"new_windows_in_tabs\": true,\n" + " \"max_tabs\": 20\n" + " }\n" + "}"; + +const char kInvalidJson[] = "!@#$%^&"; + +// Expected output for tests using RunBasicJsonPrefStoreTest(). +const char kWriteGolden[] = + "{\"homepage\":\"http://www.cnn.com\"," + "\"long_int\":{\"pref\":\"214748364842\"}," + "\"some_directory\":\"/usr/sbin/\"," + "\"tabs\":{\"max_tabs\":10,\"new_windows_in_tabs\":false}}"; + +// A PrefFilter that will intercept all calls to FilterOnLoad() and hold on +// to the |prefs| until explicitly asked to release them. +class InterceptingPrefFilter : public PrefFilter { + public: + InterceptingPrefFilter(); + InterceptingPrefFilter(OnWriteCallbackPair callback_pair); + + InterceptingPrefFilter(const InterceptingPrefFilter&) = delete; + InterceptingPrefFilter& operator=(const InterceptingPrefFilter&) = delete; + + ~InterceptingPrefFilter() override; + + // PrefFilter implementation: + void FilterOnLoad( + PostFilterOnLoadCallback post_filter_on_load_callback, + std::unique_ptr pref_store_contents) override; + void FilterUpdate(const std::string& path) override {} + OnWriteCallbackPair FilterSerializeData( + base::DictionaryValue* pref_store_contents) override { + return std::move(on_write_callback_pair_); + } + void OnStoreDeletionFromDisk() override {} + + bool has_intercepted_prefs() const { return intercepted_prefs_ != nullptr; } + + // Finalize an intercepted read, handing |intercepted_prefs_| back to its + // JsonPrefStore. + void ReleasePrefs(); + + private: + PostFilterOnLoadCallback post_filter_on_load_callback_; + std::unique_ptr intercepted_prefs_; + OnWriteCallbackPair on_write_callback_pair_; +}; + +InterceptingPrefFilter::InterceptingPrefFilter() {} + +InterceptingPrefFilter::InterceptingPrefFilter( + OnWriteCallbackPair callback_pair) { + on_write_callback_pair_ = std::move(callback_pair); +} + +InterceptingPrefFilter::~InterceptingPrefFilter() {} + +void InterceptingPrefFilter::FilterOnLoad( + PostFilterOnLoadCallback post_filter_on_load_callback, + std::unique_ptr pref_store_contents) { + post_filter_on_load_callback_ = std::move(post_filter_on_load_callback); + intercepted_prefs_ = std::move(pref_store_contents); +} + +void InterceptingPrefFilter::ReleasePrefs() { + EXPECT_FALSE(post_filter_on_load_callback_.is_null()); + std::move(post_filter_on_load_callback_) + .Run(std::move(intercepted_prefs_), false); +} + +class MockPrefStoreObserver : public PrefStore::Observer { + public: + MOCK_METHOD1(OnPrefValueChanged, void (const std::string&)); + MOCK_METHOD1(OnInitializationCompleted, void (bool)); +}; + +class MockReadErrorDelegate : public PersistentPrefStore::ReadErrorDelegate { + public: + MOCK_METHOD1(OnError, void(PersistentPrefStore::PrefReadError)); +}; + +enum class CommitPendingWriteMode { + // Basic mode. + WITHOUT_CALLBACK, + // With reply callback. + WITH_CALLBACK, + // With synchronous notify callback (synchronous after the write -- shouldn't + // require pumping messages to observe). + WITH_SYNCHRONOUS_CALLBACK, +}; + +base::test::TaskEnvironment::ThreadPoolExecutionMode GetExecutionMode( + CommitPendingWriteMode commit_mode) { + switch (commit_mode) { + case CommitPendingWriteMode::WITHOUT_CALLBACK: + [[fallthrough]]; + case CommitPendingWriteMode::WITH_CALLBACK: + return base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED; + case CommitPendingWriteMode::WITH_SYNCHRONOUS_CALLBACK: + // Synchronous callbacks require async tasks to run on their own. + return base::test::TaskEnvironment::ThreadPoolExecutionMode::ASYNC; + } +} + +void CommitPendingWrite(JsonPrefStore* pref_store, + CommitPendingWriteMode commit_pending_write_mode, + base::test::TaskEnvironment* task_environment) { + switch (commit_pending_write_mode) { + case CommitPendingWriteMode::WITHOUT_CALLBACK: { + pref_store->CommitPendingWrite(); + task_environment->RunUntilIdle(); + break; + } + case CommitPendingWriteMode::WITH_CALLBACK: { + TestCommitPendingWriteWithCallback(pref_store, task_environment); + break; + } + case CommitPendingWriteMode::WITH_SYNCHRONOUS_CALLBACK: { + base::WaitableEvent written; + pref_store->CommitPendingWrite( + base::OnceClosure(), + base::BindOnce(&base::WaitableEvent::Signal, Unretained(&written))); + written.Wait(); + break; + } + } +} + +class JsonPrefStoreTest + : public testing::TestWithParam { + public: + JsonPrefStoreTest() + : task_environment_(base::test::TaskEnvironment::MainThreadType::DEFAULT, + GetExecutionMode(GetParam())) {} + + JsonPrefStoreTest(const JsonPrefStoreTest&) = delete; + JsonPrefStoreTest& operator=(const JsonPrefStoreTest&) = delete; + + protected: + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + } + + // The path to temporary directory used to contain the test operations. + base::ScopedTempDir temp_dir_; + + base::test::TaskEnvironment task_environment_; +}; + +} // namespace + +// Test fallback behavior for a nonexistent file. +TEST_P(JsonPrefStoreTest, NonExistentFile) { + base::FilePath bogus_input_file = temp_dir_.GetPath().AppendASCII("read.txt"); + ASSERT_FALSE(PathExists(bogus_input_file)); + auto pref_store = base::MakeRefCounted(bogus_input_file); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NO_FILE, + pref_store->ReadPrefs()); + EXPECT_FALSE(pref_store->ReadOnly()); + EXPECT_EQ(0u, pref_store->get_writer().previous_data_size()); +} + +// Test fallback behavior for an invalid file. +TEST_P(JsonPrefStoreTest, InvalidFile) { + base::FilePath invalid_file = temp_dir_.GetPath().AppendASCII("invalid.json"); + ASSERT_LT(0, base::WriteFile(invalid_file, kInvalidJson, + base::size(kInvalidJson) - 1)); + + auto pref_store = base::MakeRefCounted(invalid_file); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_JSON_PARSE, + pref_store->ReadPrefs()); + EXPECT_FALSE(pref_store->ReadOnly()); + + // The file should have been moved aside. + EXPECT_FALSE(PathExists(invalid_file)); + base::FilePath moved_aside = temp_dir_.GetPath().AppendASCII("invalid.bad"); + EXPECT_TRUE(PathExists(moved_aside)); + + std::string moved_aside_contents; + ASSERT_TRUE(base::ReadFileToString(moved_aside, &moved_aside_contents)); + EXPECT_EQ(kInvalidJson, moved_aside_contents); +} + +// This function is used to avoid code duplication while testing synchronous +// and asynchronous version of the JsonPrefStore loading. It validates that the +// given output file's contents matches kWriteGolden. +void RunBasicJsonPrefStoreTest(JsonPrefStore* pref_store, + const base::FilePath& output_file, + CommitPendingWriteMode commit_pending_write_mode, + base::test::TaskEnvironment* task_environment) { + const char kNewWindowsInTabs[] = "tabs.new_windows_in_tabs"; + const char kMaxTabs[] = "tabs.max_tabs"; + const char kLongIntPref[] = "long_int.pref"; + + std::string cnn("http://www.cnn.com"); + + const Value* actual; + EXPECT_TRUE(pref_store->GetValue(kHomePage, &actual)); + EXPECT_TRUE(actual->is_string()); + EXPECT_EQ(cnn, actual->GetString()); + + const char kSomeDirectory[] = "some_directory"; + + EXPECT_TRUE(pref_store->GetValue(kSomeDirectory, &actual)); + EXPECT_TRUE(actual->is_string()); + EXPECT_EQ("/usr/local/", actual->GetString()); + base::FilePath some_path(FILE_PATH_LITERAL("/usr/sbin/")); + + pref_store->SetValue(kSomeDirectory, + std::make_unique(some_path.AsUTF8Unsafe()), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_TRUE(pref_store->GetValue(kSomeDirectory, &actual)); + EXPECT_TRUE(actual->is_string()); + EXPECT_EQ(some_path.AsUTF8Unsafe(), actual->GetString()); + + // Test reading some other data types from sub-dictionaries. + EXPECT_TRUE(pref_store->GetValue(kNewWindowsInTabs, &actual)); + EXPECT_TRUE(actual->is_bool()); + EXPECT_TRUE(actual->GetBool()); + + pref_store->SetValue(kNewWindowsInTabs, std::make_unique(false), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_TRUE(pref_store->GetValue(kNewWindowsInTabs, &actual)); + EXPECT_TRUE(actual->is_bool()); + EXPECT_FALSE(actual->GetBool()); + + EXPECT_TRUE(pref_store->GetValue(kMaxTabs, &actual)); + ASSERT_TRUE(actual->is_int()); + EXPECT_EQ(20, actual->GetInt()); + pref_store->SetValue(kMaxTabs, std::make_unique(10), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_TRUE(pref_store->GetValue(kMaxTabs, &actual)); + ASSERT_TRUE(actual->is_int()); + EXPECT_EQ(10, actual->GetInt()); + + pref_store->SetValue( + kLongIntPref, + std::make_unique(base::NumberToString(214748364842LL)), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_TRUE(pref_store->GetValue(kLongIntPref, &actual)); + EXPECT_TRUE(actual->is_string()); + int64_t value; + base::StringToInt64(actual->GetString(), &value); + EXPECT_EQ(214748364842LL, value); + + // Serialize and compare to expected output. + CommitPendingWrite(pref_store, commit_pending_write_mode, task_environment); + + std::string output_contents; + ASSERT_TRUE(base::ReadFileToString(output_file, &output_contents)); + EXPECT_EQ(kWriteGolden, output_contents); + ASSERT_TRUE(base::DeleteFile(output_file)); +} + +TEST_P(JsonPrefStoreTest, Basic) { + base::FilePath input_file = temp_dir_.GetPath().AppendASCII("write.json"); + ASSERT_LT(0, + base::WriteFile(input_file, kReadJson, base::size(kReadJson) - 1)); + + // Test that the persistent value can be loaded. + ASSERT_TRUE(PathExists(input_file)); + auto pref_store = base::MakeRefCounted(input_file); + ASSERT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, pref_store->ReadPrefs()); + EXPECT_FALSE(pref_store->ReadOnly()); + EXPECT_TRUE(pref_store->IsInitializationComplete()); + EXPECT_GT(pref_store->get_writer().previous_data_size(), 0u); + + // The JSON file looks like this: + // { + // "homepage": "http://www.cnn.com", + // "some_directory": "/usr/local/", + // "tabs": { + // "new_windows_in_tabs": true, + // "max_tabs": 20 + // } + // } + + RunBasicJsonPrefStoreTest(pref_store.get(), input_file, GetParam(), + &task_environment_); +} + +TEST_P(JsonPrefStoreTest, BasicAsync) { + base::FilePath input_file = temp_dir_.GetPath().AppendASCII("write.json"); + ASSERT_LT(0, + base::WriteFile(input_file, kReadJson, base::size(kReadJson) - 1)); + + // Test that the persistent value can be loaded. + auto pref_store = base::MakeRefCounted(input_file); + + { + MockPrefStoreObserver mock_observer; + pref_store->AddObserver(&mock_observer); + + MockReadErrorDelegate* mock_error_delegate = new MockReadErrorDelegate; + pref_store->ReadPrefsAsync(mock_error_delegate); + + EXPECT_CALL(mock_observer, OnInitializationCompleted(true)).Times(1); + EXPECT_CALL(*mock_error_delegate, + OnError(PersistentPrefStore::PREF_READ_ERROR_NONE)).Times(0); + task_environment_.RunUntilIdle(); + pref_store->RemoveObserver(&mock_observer); + + EXPECT_FALSE(pref_store->ReadOnly()); + EXPECT_TRUE(pref_store->IsInitializationComplete()); + EXPECT_GT(pref_store->get_writer().previous_data_size(), 0u); + } + + // The JSON file looks like this: + // { + // "homepage": "http://www.cnn.com", + // "some_directory": "/usr/local/", + // "tabs": { + // "new_windows_in_tabs": true, + // "max_tabs": 20 + // } + // } + + RunBasicJsonPrefStoreTest(pref_store.get(), input_file, GetParam(), + &task_environment_); +} + +TEST_P(JsonPrefStoreTest, PreserveEmptyValues) { + FilePath pref_file = temp_dir_.GetPath().AppendASCII("empty_values.json"); + + auto pref_store = base::MakeRefCounted(pref_file); + + // Set some keys with empty values. + pref_store->SetValue("list", std::make_unique(), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + pref_store->SetValue("dict", std::make_unique(), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + // Write to file. + CommitPendingWrite(pref_store.get(), GetParam(), &task_environment_); + + // Reload. + pref_store = base::MakeRefCounted(pref_file); + ASSERT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, pref_store->ReadPrefs()); + ASSERT_FALSE(pref_store->ReadOnly()); + + // Check values. + const Value* result = nullptr; + EXPECT_TRUE(pref_store->GetValue("list", &result)); + EXPECT_EQ(ListValue(), *result); + EXPECT_TRUE(pref_store->GetValue("dict", &result)); + EXPECT_EQ(DictionaryValue(), *result); +} + +// This test is just documenting some potentially non-obvious behavior. It +// shouldn't be taken as normative. +TEST_P(JsonPrefStoreTest, RemoveClearsEmptyParent) { + FilePath pref_file = temp_dir_.GetPath().AppendASCII("empty_values.json"); + + auto pref_store = base::MakeRefCounted(pref_file); + + std::unique_ptr dict(new base::DictionaryValue); + dict->SetString("key", "value"); + pref_store->SetValue("dict", std::move(dict), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + pref_store->RemoveValue("dict.key", + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + const base::Value* retrieved_dict = nullptr; + bool has_dict = pref_store->GetValue("dict", &retrieved_dict); + EXPECT_FALSE(has_dict); +} + +// Tests asynchronous reading of the file when there is no file. +TEST_P(JsonPrefStoreTest, AsyncNonExistingFile) { + base::FilePath bogus_input_file = temp_dir_.GetPath().AppendASCII("read.txt"); + ASSERT_FALSE(PathExists(bogus_input_file)); + auto pref_store = base::MakeRefCounted(bogus_input_file); + MockPrefStoreObserver mock_observer; + pref_store->AddObserver(&mock_observer); + + MockReadErrorDelegate *mock_error_delegate = new MockReadErrorDelegate; + pref_store->ReadPrefsAsync(mock_error_delegate); + + EXPECT_CALL(mock_observer, OnInitializationCompleted(true)).Times(1); + EXPECT_CALL(*mock_error_delegate, + OnError(PersistentPrefStore::PREF_READ_ERROR_NO_FILE)).Times(1); + task_environment_.RunUntilIdle(); + pref_store->RemoveObserver(&mock_observer); + + EXPECT_FALSE(pref_store->ReadOnly()); +} + +TEST_P(JsonPrefStoreTest, ReadWithInterceptor) { + base::FilePath input_file = temp_dir_.GetPath().AppendASCII("write.json"); + ASSERT_LT(0, + base::WriteFile(input_file, kReadJson, base::size(kReadJson) - 1)); + + std::unique_ptr intercepting_pref_filter( + new InterceptingPrefFilter()); + InterceptingPrefFilter* raw_intercepting_pref_filter_ = + intercepting_pref_filter.get(); + auto pref_store = base::MakeRefCounted( + input_file, std::move(intercepting_pref_filter)); + + ASSERT_EQ(PersistentPrefStore::PREF_READ_ERROR_ASYNCHRONOUS_TASK_INCOMPLETE, + pref_store->ReadPrefs()); + EXPECT_FALSE(pref_store->ReadOnly()); + + // The store shouldn't be considered initialized until the interceptor + // returns. + EXPECT_TRUE(raw_intercepting_pref_filter_->has_intercepted_prefs()); + EXPECT_FALSE(pref_store->IsInitializationComplete()); + EXPECT_FALSE(pref_store->GetValue(kHomePage, nullptr)); + + raw_intercepting_pref_filter_->ReleasePrefs(); + + EXPECT_FALSE(raw_intercepting_pref_filter_->has_intercepted_prefs()); + EXPECT_TRUE(pref_store->IsInitializationComplete()); + EXPECT_TRUE(pref_store->GetValue(kHomePage, nullptr)); + + // The JSON file looks like this: + // { + // "homepage": "http://www.cnn.com", + // "some_directory": "/usr/local/", + // "tabs": { + // "new_windows_in_tabs": true, + // "max_tabs": 20 + // } + // } + + RunBasicJsonPrefStoreTest(pref_store.get(), input_file, GetParam(), + &task_environment_); +} + +TEST_P(JsonPrefStoreTest, ReadAsyncWithInterceptor) { + base::FilePath input_file = temp_dir_.GetPath().AppendASCII("write.json"); + ASSERT_LT(0, + base::WriteFile(input_file, kReadJson, base::size(kReadJson) - 1)); + + std::unique_ptr intercepting_pref_filter( + new InterceptingPrefFilter()); + InterceptingPrefFilter* raw_intercepting_pref_filter_ = + intercepting_pref_filter.get(); + auto pref_store = base::MakeRefCounted( + input_file, std::move(intercepting_pref_filter)); + + MockPrefStoreObserver mock_observer; + pref_store->AddObserver(&mock_observer); + + // Ownership of the |mock_error_delegate| is handed to the |pref_store| below. + MockReadErrorDelegate* mock_error_delegate = new MockReadErrorDelegate; + + { + pref_store->ReadPrefsAsync(mock_error_delegate); + + EXPECT_CALL(mock_observer, OnInitializationCompleted(true)).Times(0); + // EXPECT_CALL(*mock_error_delegate, + // OnError(PersistentPrefStore::PREF_READ_ERROR_NONE)).Times(0); + task_environment_.RunUntilIdle(); + + EXPECT_FALSE(pref_store->ReadOnly()); + EXPECT_TRUE(raw_intercepting_pref_filter_->has_intercepted_prefs()); + EXPECT_FALSE(pref_store->IsInitializationComplete()); + EXPECT_FALSE(pref_store->GetValue(kHomePage, nullptr)); + } + + { + EXPECT_CALL(mock_observer, OnInitializationCompleted(true)).Times(1); + // EXPECT_CALL(*mock_error_delegate, + // OnError(PersistentPrefStore::PREF_READ_ERROR_NONE)).Times(0); + + raw_intercepting_pref_filter_->ReleasePrefs(); + + EXPECT_FALSE(pref_store->ReadOnly()); + EXPECT_FALSE(raw_intercepting_pref_filter_->has_intercepted_prefs()); + EXPECT_TRUE(pref_store->IsInitializationComplete()); + EXPECT_TRUE(pref_store->GetValue(kHomePage, nullptr)); + } + + pref_store->RemoveObserver(&mock_observer); + + // The JSON file looks like this: + // { + // "homepage": "http://www.cnn.com", + // "some_directory": "/usr/local/", + // "tabs": { + // "new_windows_in_tabs": true, + // "max_tabs": 20 + // } + // } + + RunBasicJsonPrefStoreTest(pref_store.get(), input_file, GetParam(), + &task_environment_); +} + +TEST_P(JsonPrefStoreTest, RemoveValuesByPrefix) { + FilePath pref_file = temp_dir_.GetPath().AppendASCII("empty.json"); + + auto pref_store = base::MakeRefCounted(pref_file); + + const Value* value; + const std::string prefix = "pref"; + const std::string subpref_name1 = "pref.a"; + const std::string subpref_name2 = "pref.b"; + const std::string other_name = "other"; + + pref_store->SetValue(subpref_name1, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + pref_store->SetValue(subpref_name2, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + pref_store->SetValue(other_name, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + pref_store->RemoveValuesByPrefixSilently(prefix); + EXPECT_FALSE(pref_store->GetValue(subpref_name1, &value)); + EXPECT_FALSE(pref_store->GetValue(subpref_name2, &value)); + EXPECT_TRUE(pref_store->GetValue(other_name, &value)); +} + +INSTANTIATE_TEST_SUITE_P( + WithoutCallback, + JsonPrefStoreTest, + ::testing::Values(CommitPendingWriteMode::WITHOUT_CALLBACK)); +INSTANTIATE_TEST_SUITE_P( + WithCallback, + JsonPrefStoreTest, + ::testing::Values(CommitPendingWriteMode::WITH_CALLBACK)); +INSTANTIATE_TEST_SUITE_P( + WithSynchronousCallback, + JsonPrefStoreTest, + ::testing::Values(CommitPendingWriteMode::WITH_SYNCHRONOUS_CALLBACK)); + +class JsonPrefStoreLossyWriteTest : public JsonPrefStoreTest { + public: + JsonPrefStoreLossyWriteTest() = default; + + JsonPrefStoreLossyWriteTest(const JsonPrefStoreLossyWriteTest&) = delete; + JsonPrefStoreLossyWriteTest& operator=(const JsonPrefStoreLossyWriteTest&) = + delete; + + protected: + void SetUp() override { + JsonPrefStoreTest::SetUp(); + test_file_ = temp_dir_.GetPath().AppendASCII("test.json"); + } + + scoped_refptr CreatePrefStore() { + return base::MakeRefCounted(test_file_); + } + + // Return the ImportantFileWriter for a given JsonPrefStore. + ImportantFileWriter* GetImportantFileWriter(JsonPrefStore* pref_store) { + return &(pref_store->writer_); + } + + // Get the contents of kTestFile. Pumps the message loop before returning the + // result. + std::string GetTestFileContents() { + task_environment_.RunUntilIdle(); + std::string file_contents; + ReadFileToString(test_file_, &file_contents); + return file_contents; + } + + private: + base::FilePath test_file_; +}; + +TEST_P(JsonPrefStoreLossyWriteTest, LossyWriteBasic) { + scoped_refptr pref_store = CreatePrefStore(); + ImportantFileWriter* file_writer = GetImportantFileWriter(pref_store.get()); + + // Set a normal pref and check that it gets scheduled to be written. + ASSERT_FALSE(file_writer->HasPendingWrite()); + pref_store->SetValue("normal", std::make_unique("normal"), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + ASSERT_TRUE(file_writer->HasPendingWrite()); + file_writer->DoScheduledWrite(); + ASSERT_EQ("{\"normal\":\"normal\"}", GetTestFileContents()); + ASSERT_FALSE(file_writer->HasPendingWrite()); + + // Set a lossy pref and check that it is not scheduled to be written. + // SetValue/RemoveValue. + pref_store->SetValue("lossy", std::make_unique("lossy"), + WriteablePrefStore::LOSSY_PREF_WRITE_FLAG); + ASSERT_FALSE(file_writer->HasPendingWrite()); + pref_store->RemoveValue("lossy", WriteablePrefStore::LOSSY_PREF_WRITE_FLAG); + ASSERT_FALSE(file_writer->HasPendingWrite()); + + // SetValueSilently/RemoveValueSilently. + pref_store->SetValueSilently("lossy", std::make_unique("lossy"), + WriteablePrefStore::LOSSY_PREF_WRITE_FLAG); + ASSERT_FALSE(file_writer->HasPendingWrite()); + pref_store->RemoveValueSilently("lossy", + WriteablePrefStore::LOSSY_PREF_WRITE_FLAG); + ASSERT_FALSE(file_writer->HasPendingWrite()); + + // ReportValueChanged. + pref_store->SetValue("lossy", std::make_unique("lossy"), + WriteablePrefStore::LOSSY_PREF_WRITE_FLAG); + ASSERT_FALSE(file_writer->HasPendingWrite()); + pref_store->ReportValueChanged("lossy", + WriteablePrefStore::LOSSY_PREF_WRITE_FLAG); + ASSERT_FALSE(file_writer->HasPendingWrite()); + + // Call CommitPendingWrite and check that the lossy pref and the normal pref + // are there with the last values set above. + pref_store->CommitPendingWrite(base::OnceClosure()); + ASSERT_FALSE(file_writer->HasPendingWrite()); + ASSERT_EQ("{\"lossy\":\"lossy\",\"normal\":\"normal\"}", + GetTestFileContents()); +} + +TEST_P(JsonPrefStoreLossyWriteTest, LossyWriteMixedLossyFirst) { + scoped_refptr pref_store = CreatePrefStore(); + ImportantFileWriter* file_writer = GetImportantFileWriter(pref_store.get()); + + // Set a lossy pref and check that it is not scheduled to be written. + ASSERT_FALSE(file_writer->HasPendingWrite()); + pref_store->SetValue("lossy", std::make_unique("lossy"), + WriteablePrefStore::LOSSY_PREF_WRITE_FLAG); + ASSERT_FALSE(file_writer->HasPendingWrite()); + + // Set a normal pref and check that it is scheduled to be written. + pref_store->SetValue("normal", std::make_unique("normal"), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + ASSERT_TRUE(file_writer->HasPendingWrite()); + + // Call DoScheduledWrite and check both prefs get written. + file_writer->DoScheduledWrite(); + ASSERT_EQ("{\"lossy\":\"lossy\",\"normal\":\"normal\"}", + GetTestFileContents()); + ASSERT_FALSE(file_writer->HasPendingWrite()); +} + +TEST_P(JsonPrefStoreLossyWriteTest, LossyWriteMixedLossySecond) { + scoped_refptr pref_store = CreatePrefStore(); + ImportantFileWriter* file_writer = GetImportantFileWriter(pref_store.get()); + + // Set a normal pref and check that it is scheduled to be written. + ASSERT_FALSE(file_writer->HasPendingWrite()); + pref_store->SetValue("normal", std::make_unique("normal"), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + ASSERT_TRUE(file_writer->HasPendingWrite()); + + // Set a lossy pref and check that the write is still scheduled. + pref_store->SetValue("lossy", std::make_unique("lossy"), + WriteablePrefStore::LOSSY_PREF_WRITE_FLAG); + ASSERT_TRUE(file_writer->HasPendingWrite()); + + // Call DoScheduledWrite and check both prefs get written. + file_writer->DoScheduledWrite(); + ASSERT_EQ("{\"lossy\":\"lossy\",\"normal\":\"normal\"}", + GetTestFileContents()); + ASSERT_FALSE(file_writer->HasPendingWrite()); +} + +TEST_P(JsonPrefStoreLossyWriteTest, ScheduleLossyWrite) { + scoped_refptr pref_store = CreatePrefStore(); + ImportantFileWriter* file_writer = GetImportantFileWriter(pref_store.get()); + + // Set a lossy pref and check that it is not scheduled to be written. + pref_store->SetValue("lossy", std::make_unique("lossy"), + WriteablePrefStore::LOSSY_PREF_WRITE_FLAG); + ASSERT_FALSE(file_writer->HasPendingWrite()); + + // Schedule pending lossy writes and check that it is scheduled. + pref_store->SchedulePendingLossyWrites(); + ASSERT_TRUE(file_writer->HasPendingWrite()); + + // Call CommitPendingWrite and check that the lossy pref is there with the + // last value set above. + pref_store->CommitPendingWrite(base::OnceClosure()); + ASSERT_FALSE(file_writer->HasPendingWrite()); + ASSERT_EQ("{\"lossy\":\"lossy\"}", GetTestFileContents()); +} + +INSTANTIATE_TEST_SUITE_P( + WithoutCallback, + JsonPrefStoreLossyWriteTest, + ::testing::Values(CommitPendingWriteMode::WITHOUT_CALLBACK)); +INSTANTIATE_TEST_SUITE_P( + WithReply, + JsonPrefStoreLossyWriteTest, + ::testing::Values(CommitPendingWriteMode::WITH_CALLBACK)); +INSTANTIATE_TEST_SUITE_P( + WithNotify, + JsonPrefStoreLossyWriteTest, + ::testing::Values(CommitPendingWriteMode::WITH_SYNCHRONOUS_CALLBACK)); + +class SuccessfulWriteReplyObserver { + public: + SuccessfulWriteReplyObserver() = default; + + SuccessfulWriteReplyObserver(const SuccessfulWriteReplyObserver&) = delete; + SuccessfulWriteReplyObserver& operator=(const SuccessfulWriteReplyObserver&) = + delete; + + // Returns true if a successful write was observed via on_successful_write() + // and resets the observation state to false regardless. + bool GetAndResetObservationState() { + bool was_successful_write_observed = successful_write_reply_observed_; + successful_write_reply_observed_ = false; + return was_successful_write_observed; + } + + // Register OnWrite() to be called on the next write of |json_pref_store|. + void ObserveNextWriteCallback(JsonPrefStore* json_pref_store); + + void OnSuccessfulWrite() { + EXPECT_FALSE(successful_write_reply_observed_); + successful_write_reply_observed_ = true; + } + + private: + bool successful_write_reply_observed_ = false; +}; + +void SuccessfulWriteReplyObserver::ObserveNextWriteCallback( + JsonPrefStore* json_pref_store) { + json_pref_store->RegisterOnNextSuccessfulWriteReply( + base::BindOnce(&SuccessfulWriteReplyObserver::OnSuccessfulWrite, + base::Unretained(this))); +} + +enum WriteCallbackObservationState { + NOT_CALLED, + CALLED_WITH_ERROR, + CALLED_WITH_SUCCESS, +}; + +class WriteCallbacksObserver { + public: + WriteCallbacksObserver() = default; + + WriteCallbacksObserver(const WriteCallbacksObserver&) = delete; + WriteCallbacksObserver& operator=(const WriteCallbacksObserver&) = delete; + + // Register OnWrite() to be called on the next write of |json_pref_store|. + void ObserveNextWriteCallback(JsonPrefStore* json_pref_store); + + // Returns whether OnPreWrite() was called, and resets the observation state + // to false. + bool GetAndResetPreWriteObservationState(); + + // Returns the |WriteCallbackObservationState| which was observed, then resets + // it to |NOT_CALLED|. + WriteCallbackObservationState GetAndResetPostWriteObservationState(); + + JsonPrefStore::OnWriteCallbackPair GetCallbackPair() { + return std::make_pair(base::BindOnce(&WriteCallbacksObserver::OnPreWrite, + base::Unretained(this)), + base::BindOnce(&WriteCallbacksObserver::OnPostWrite, + base::Unretained(this))); + } + + void OnPreWrite() { + EXPECT_FALSE(pre_write_called_); + pre_write_called_ = true; + } + + void OnPostWrite(bool success) { + EXPECT_EQ(NOT_CALLED, post_write_observation_state_); + post_write_observation_state_ = + success ? CALLED_WITH_SUCCESS : CALLED_WITH_ERROR; + } + + private: + bool pre_write_called_ = false; + WriteCallbackObservationState post_write_observation_state_ = NOT_CALLED; +}; + +void WriteCallbacksObserver::ObserveNextWriteCallback(JsonPrefStore* writer) { + writer->RegisterOnNextWriteSynchronousCallbacks(GetCallbackPair()); +} + +bool WriteCallbacksObserver::GetAndResetPreWriteObservationState() { + bool observation_state = pre_write_called_; + pre_write_called_ = false; + return observation_state; +} + +WriteCallbackObservationState +WriteCallbacksObserver::GetAndResetPostWriteObservationState() { + WriteCallbackObservationState state = post_write_observation_state_; + pre_write_called_ = false; + post_write_observation_state_ = NOT_CALLED; + return state; +} + +class JsonPrefStoreCallbackTest : public testing::Test { + public: + JsonPrefStoreCallbackTest() = default; + + JsonPrefStoreCallbackTest(const JsonPrefStoreCallbackTest&) = delete; + JsonPrefStoreCallbackTest& operator=(const JsonPrefStoreCallbackTest&) = + delete; + + protected: + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + test_file_ = temp_dir_.GetPath().AppendASCII("test.json"); + } + + scoped_refptr CreatePrefStore() { + return base::MakeRefCounted(test_file_); + } + + // Return the ImportantFileWriter for a given JsonPrefStore. + ImportantFileWriter* GetImportantFileWriter(JsonPrefStore* pref_store) { + return &(pref_store->writer_); + } + + void TriggerFakeWriteForCallback(JsonPrefStore* pref_store, bool success) { + JsonPrefStore::PostWriteCallback( + base::BindOnce(&JsonPrefStore::RunOrScheduleNextSuccessfulWriteCallback, + pref_store->AsWeakPtr()), + base::BindOnce(&WriteCallbacksObserver::OnPostWrite, + base::Unretained(&write_callback_observer_)), + base::SequencedTaskRunnerHandle::Get(), success); + } + + SuccessfulWriteReplyObserver successful_write_reply_observer_; + WriteCallbacksObserver write_callback_observer_; + + protected: + base::test::TaskEnvironment task_environment_{ + base::test::TaskEnvironment::MainThreadType::DEFAULT, + base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED}; + + base::ScopedTempDir temp_dir_; + + private: + base::FilePath test_file_; +}; + +TEST_F(JsonPrefStoreCallbackTest, TestSerializeDataCallbacks) { + base::FilePath input_file = temp_dir_.GetPath().AppendASCII("write.json"); + ASSERT_LT(0, + base::WriteFile(input_file, kReadJson, base::size(kReadJson) - 1)); + + std::unique_ptr intercepting_pref_filter( + new InterceptingPrefFilter(write_callback_observer_.GetCallbackPair())); + auto pref_store = base::MakeRefCounted( + input_file, std::move(intercepting_pref_filter)); + ImportantFileWriter* file_writer = GetImportantFileWriter(pref_store.get()); + + EXPECT_EQ(NOT_CALLED, + write_callback_observer_.GetAndResetPostWriteObservationState()); + pref_store->SetValue("normal", std::make_unique("normal"), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + file_writer->DoScheduledWrite(); + + // The observer should not be invoked right away. + EXPECT_FALSE(write_callback_observer_.GetAndResetPreWriteObservationState()); + EXPECT_EQ(NOT_CALLED, + write_callback_observer_.GetAndResetPostWriteObservationState()); + + task_environment_.RunUntilIdle(); + + EXPECT_TRUE(write_callback_observer_.GetAndResetPreWriteObservationState()); + EXPECT_EQ(CALLED_WITH_SUCCESS, + write_callback_observer_.GetAndResetPostWriteObservationState()); +} + +TEST_F(JsonPrefStoreCallbackTest, TestPostWriteCallbacks) { + scoped_refptr pref_store = CreatePrefStore(); + ImportantFileWriter* file_writer = GetImportantFileWriter(pref_store.get()); + + // Test RegisterOnNextWriteSynchronousCallbacks after + // RegisterOnNextSuccessfulWriteReply. + successful_write_reply_observer_.ObserveNextWriteCallback(pref_store.get()); + write_callback_observer_.ObserveNextWriteCallback(pref_store.get()); + file_writer->WriteNow(std::make_unique("foo")); + task_environment_.RunUntilIdle(); + EXPECT_TRUE(successful_write_reply_observer_.GetAndResetObservationState()); + EXPECT_TRUE(write_callback_observer_.GetAndResetPreWriteObservationState()); + EXPECT_EQ(CALLED_WITH_SUCCESS, + write_callback_observer_.GetAndResetPostWriteObservationState()); + + // Test RegisterOnNextSuccessfulWriteReply after + // RegisterOnNextWriteSynchronousCallbacks. + successful_write_reply_observer_.ObserveNextWriteCallback(pref_store.get()); + write_callback_observer_.ObserveNextWriteCallback(pref_store.get()); + file_writer->WriteNow(std::make_unique("foo")); + task_environment_.RunUntilIdle(); + EXPECT_TRUE(successful_write_reply_observer_.GetAndResetObservationState()); + EXPECT_TRUE(write_callback_observer_.GetAndResetPreWriteObservationState()); + EXPECT_EQ(CALLED_WITH_SUCCESS, + write_callback_observer_.GetAndResetPostWriteObservationState()); + + // Test RegisterOnNextSuccessfulWriteReply only. + successful_write_reply_observer_.ObserveNextWriteCallback(pref_store.get()); + file_writer->WriteNow(std::make_unique("foo")); + task_environment_.RunUntilIdle(); + EXPECT_TRUE(successful_write_reply_observer_.GetAndResetObservationState()); + EXPECT_FALSE(write_callback_observer_.GetAndResetPreWriteObservationState()); + EXPECT_EQ(NOT_CALLED, + write_callback_observer_.GetAndResetPostWriteObservationState()); + + // Test RegisterOnNextWriteSynchronousCallbacks only. + write_callback_observer_.ObserveNextWriteCallback(pref_store.get()); + file_writer->WriteNow(std::make_unique("foo")); + task_environment_.RunUntilIdle(); + EXPECT_FALSE(successful_write_reply_observer_.GetAndResetObservationState()); + EXPECT_TRUE(write_callback_observer_.GetAndResetPreWriteObservationState()); + EXPECT_EQ(CALLED_WITH_SUCCESS, + write_callback_observer_.GetAndResetPostWriteObservationState()); +} + +TEST_F(JsonPrefStoreCallbackTest, TestPostWriteCallbacksWithFakeFailure) { + scoped_refptr pref_store = CreatePrefStore(); + + // Confirm that the observers are invoked. + successful_write_reply_observer_.ObserveNextWriteCallback(pref_store.get()); + TriggerFakeWriteForCallback(pref_store.get(), true); + task_environment_.RunUntilIdle(); + EXPECT_TRUE(successful_write_reply_observer_.GetAndResetObservationState()); + EXPECT_EQ(CALLED_WITH_SUCCESS, + write_callback_observer_.GetAndResetPostWriteObservationState()); + + // Confirm that the observation states were reset. + EXPECT_FALSE(successful_write_reply_observer_.GetAndResetObservationState()); + EXPECT_EQ(NOT_CALLED, + write_callback_observer_.GetAndResetPostWriteObservationState()); + + // Confirm that re-installing the observers works for another write. + successful_write_reply_observer_.ObserveNextWriteCallback(pref_store.get()); + TriggerFakeWriteForCallback(pref_store.get(), true); + task_environment_.RunUntilIdle(); + EXPECT_TRUE(successful_write_reply_observer_.GetAndResetObservationState()); + EXPECT_EQ(CALLED_WITH_SUCCESS, + write_callback_observer_.GetAndResetPostWriteObservationState()); + + // Confirm that the successful observer is not invoked by an unsuccessful + // write, and that the synchronous observer is invoked. + successful_write_reply_observer_.ObserveNextWriteCallback(pref_store.get()); + TriggerFakeWriteForCallback(pref_store.get(), false); + task_environment_.RunUntilIdle(); + EXPECT_FALSE(successful_write_reply_observer_.GetAndResetObservationState()); + EXPECT_EQ(CALLED_WITH_ERROR, + write_callback_observer_.GetAndResetPostWriteObservationState()); + + // Do a real write, and confirm that the successful observer was invoked after + // being set by |PostWriteCallback| by the last TriggerFakeWriteCallback. + ImportantFileWriter* file_writer = GetImportantFileWriter(pref_store.get()); + file_writer->WriteNow(std::make_unique("foo")); + task_environment_.RunUntilIdle(); + EXPECT_TRUE(successful_write_reply_observer_.GetAndResetObservationState()); + EXPECT_EQ(NOT_CALLED, + write_callback_observer_.GetAndResetPostWriteObservationState()); +} + +TEST_F(JsonPrefStoreCallbackTest, TestPostWriteCallbacksDuringProfileDeath) { + // Create a JsonPrefStore and attach observers to it, then delete it by making + // it go out of scope to simulate profile switch or Chrome shutdown. + { + scoped_refptr soon_out_of_scope_pref_store = + CreatePrefStore(); + ImportantFileWriter* file_writer = + GetImportantFileWriter(soon_out_of_scope_pref_store.get()); + successful_write_reply_observer_.ObserveNextWriteCallback( + soon_out_of_scope_pref_store.get()); + write_callback_observer_.ObserveNextWriteCallback( + soon_out_of_scope_pref_store.get()); + file_writer->WriteNow(std::make_unique("foo")); + } + task_environment_.RunUntilIdle(); + EXPECT_FALSE(successful_write_reply_observer_.GetAndResetObservationState()); + EXPECT_TRUE(write_callback_observer_.GetAndResetPreWriteObservationState()); + EXPECT_EQ(CALLED_WITH_SUCCESS, + write_callback_observer_.GetAndResetPostWriteObservationState()); +} + +} // namespace base diff --git a/src/components/prefs/mock_pref_change_callback.cc b/src/components/prefs/mock_pref_change_callback.cc new file mode 100644 index 0000000000..d23314704b --- /dev/null +++ b/src/components/prefs/mock_pref_change_callback.cc @@ -0,0 +1,24 @@ +// Copyright (c) 2011 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 "components/prefs/mock_pref_change_callback.h" + +#include "base/bind.h" + +MockPrefChangeCallback::MockPrefChangeCallback(PrefService* prefs) + : prefs_(prefs) { +} + +MockPrefChangeCallback::~MockPrefChangeCallback() {} + +PrefChangeRegistrar::NamedChangeCallback MockPrefChangeCallback::GetCallback() { + return base::BindRepeating(&MockPrefChangeCallback::OnPreferenceChanged, + base::Unretained(this)); +} + +void MockPrefChangeCallback::Expect(const std::string& pref_name, + const base::Value* value) { + EXPECT_CALL(*this, OnPreferenceChanged(pref_name)) + .With(PrefValueMatches(prefs_.get(), pref_name, value)); +} diff --git a/src/components/prefs/mock_pref_change_callback.h b/src/components/prefs/mock_pref_change_callback.h new file mode 100644 index 0000000000..6227f8a1ac --- /dev/null +++ b/src/components/prefs/mock_pref_change_callback.h @@ -0,0 +1,48 @@ +// Copyright (c) 2011 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_PREFS_MOCK_PREF_CHANGE_CALLBACK_H_ +#define COMPONENTS_PREFS_MOCK_PREF_CHANGE_CALLBACK_H_ + +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/values_equivalent.h" +#include "components/prefs/pref_change_registrar.h" +#include "components/prefs/pref_service.h" +#include "testing/gmock/include/gmock/gmock.h" + +using testing::Pointee; +using testing::Property; +using testing::Truly; + +// Matcher that checks whether the current value of the preference named +// |pref_name| in |prefs| matches |value|. If |value| is NULL, the matcher +// checks that the value is not set. +MATCHER_P3(PrefValueMatches, prefs, pref_name, value, "") { + const PrefService::Preference* pref = prefs->FindPreference(pref_name); + if (!pref) + return false; + + return base::ValuesEquivalent(value, pref->GetValue()); +} + +// A mock for testing preference notifications and easy setup of expectations. +class MockPrefChangeCallback { + public: + explicit MockPrefChangeCallback(PrefService* prefs); + virtual ~MockPrefChangeCallback(); + + PrefChangeRegistrar::NamedChangeCallback GetCallback(); + + MOCK_METHOD1(OnPreferenceChanged, void(const std::string&)); + + void Expect(const std::string& pref_name, + const base::Value* value); + + private: + raw_ptr prefs_; +}; + +#endif // COMPONENTS_PREFS_MOCK_PREF_CHANGE_CALLBACK_H_ diff --git a/src/components/prefs/overlay_user_pref_store.cc b/src/components/prefs/overlay_user_pref_store.cc new file mode 100644 index 0000000000..4e5c460f87 --- /dev/null +++ b/src/components/prefs/overlay_user_pref_store.cc @@ -0,0 +1,253 @@ +// Copyright (c) 2012 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 "components/prefs/overlay_user_pref_store.h" + +#include +#include +#include + +#include "base/memory/ptr_util.h" +#include "base/memory/raw_ptr.h" +#include "base/values.h" +#include "components/prefs/in_memory_pref_store.h" + +// Allows us to monitor two pref stores and tell updates from them apart. It +// essentially mimics a Callback for the Observer interface (e.g. it allows +// binding additional arguments). +class OverlayUserPrefStore::ObserverAdapter : public PrefStore::Observer { + public: + ObserverAdapter(bool ephemeral, OverlayUserPrefStore* parent) + : ephemeral_user_pref_store_(ephemeral), parent_(parent) {} + + // Methods of PrefStore::Observer. + void OnPrefValueChanged(const std::string& key) override { + parent_->OnPrefValueChanged(ephemeral_user_pref_store_, key); + } + void OnInitializationCompleted(bool succeeded) override { + parent_->OnInitializationCompleted(ephemeral_user_pref_store_, succeeded); + } + + private: + // Is the update for the ephemeral? + const bool ephemeral_user_pref_store_; + const raw_ptr parent_; +}; + +OverlayUserPrefStore::OverlayUserPrefStore(PersistentPrefStore* persistent) + : OverlayUserPrefStore(new InMemoryPrefStore(), persistent) {} + +OverlayUserPrefStore::OverlayUserPrefStore(PersistentPrefStore* ephemeral, + PersistentPrefStore* persistent) + : ephemeral_pref_store_observer_( + std::make_unique(true, this)), + persistent_pref_store_observer_( + std::make_unique(false, this)), + ephemeral_user_pref_store_(ephemeral), + persistent_user_pref_store_(persistent) { + DCHECK(ephemeral->IsInitializationComplete()); + ephemeral_user_pref_store_->AddObserver(ephemeral_pref_store_observer_.get()); + persistent_user_pref_store_->AddObserver( + persistent_pref_store_observer_.get()); +} + +bool OverlayUserPrefStore::IsSetInOverlay(const std::string& key) const { + return ephemeral_user_pref_store_->GetValue(key, nullptr); +} + +void OverlayUserPrefStore::AddObserver(PrefStore::Observer* observer) { + observers_.AddObserver(observer); +} + +void OverlayUserPrefStore::RemoveObserver(PrefStore::Observer* observer) { + observers_.RemoveObserver(observer); +} + +bool OverlayUserPrefStore::HasObservers() const { + return !observers_.empty(); +} + +bool OverlayUserPrefStore::IsInitializationComplete() const { + return persistent_user_pref_store_->IsInitializationComplete() && + ephemeral_user_pref_store_->IsInitializationComplete(); +} + +bool OverlayUserPrefStore::GetValue(const std::string& key, + const base::Value** result) const { + // If the |key| shall NOT be stored in the ephemeral store, there must not + // be an entry. + DCHECK(!ShallBeStoredInPersistent(key) || + !ephemeral_user_pref_store_->GetValue(key, nullptr)); + + if (ephemeral_user_pref_store_->GetValue(key, result)) + return true; + return persistent_user_pref_store_->GetValue(key, result); +} + +std::unique_ptr OverlayUserPrefStore::GetValues() const { + auto values = ephemeral_user_pref_store_->GetValues(); + auto persistent_values = persistent_user_pref_store_->GetValues(); + + // Output |values| are read from |ephemeral_user_pref_store_| (in-memory + // store). Then the values of preferences in |persistent_names_set_| are + // overwritten by the content of |persistent_user_pref_store_| (the persistent + // store). + for (const auto& key : persistent_names_set_) { + absl::optional out_value = persistent_values->ExtractPath(key); + if (out_value.has_value()) { + values->SetPath(key, std::move(*out_value)); + } + } + return values; +} + +bool OverlayUserPrefStore::GetMutableValue(const std::string& key, + base::Value** result) { + if (ShallBeStoredInPersistent(key)) + return persistent_user_pref_store_->GetMutableValue(key, result); + + written_ephemeral_names_.insert(key); + if (ephemeral_user_pref_store_->GetMutableValue(key, result)) + return true; + + // Try to create copy of persistent if the ephemeral does not contain a value. + base::Value* persistent_value = nullptr; + if (!persistent_user_pref_store_->GetMutableValue(key, &persistent_value)) + return false; + + ephemeral_user_pref_store_->SetValue( + key, base::Value::ToUniquePtrValue(persistent_value->Clone()), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + ephemeral_user_pref_store_->GetMutableValue(key, result); + return true; +} + +void OverlayUserPrefStore::SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + if (ShallBeStoredInPersistent(key)) { + persistent_user_pref_store_->SetValue(key, std::move(value), flags); + return; + } + + // TODO(https://crbug.com/861722): If we always store in in-memory storage + // and conditionally also stored in persistent one, we wouldn't have to do a + // complex merge in GetValues(). + written_ephemeral_names_.insert(key); + ephemeral_user_pref_store_->SetValue(key, std::move(value), flags); +} + +void OverlayUserPrefStore::SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + if (ShallBeStoredInPersistent(key)) { + persistent_user_pref_store_->SetValueSilently(key, std::move(value), flags); + return; + } + + written_ephemeral_names_.insert(key); + ephemeral_user_pref_store_->SetValueSilently(key, std::move(value), flags); +} + +void OverlayUserPrefStore::RemoveValue(const std::string& key, uint32_t flags) { + if (ShallBeStoredInPersistent(key)) { + persistent_user_pref_store_->RemoveValue(key, flags); + return; + } + + written_ephemeral_names_.insert(key); + ephemeral_user_pref_store_->RemoveValue(key, flags); +} + +void OverlayUserPrefStore::RemoveValuesByPrefixSilently( + const std::string& prefix) { + NOTIMPLEMENTED(); +} + +bool OverlayUserPrefStore::ReadOnly() const { + return false; +} + +PersistentPrefStore::PrefReadError OverlayUserPrefStore::GetReadError() const { + return PersistentPrefStore::PREF_READ_ERROR_NONE; +} + +PersistentPrefStore::PrefReadError OverlayUserPrefStore::ReadPrefs() { + // We do not read intentionally. + OnInitializationCompleted(/* ephemeral */ false, true); + return PersistentPrefStore::PREF_READ_ERROR_NONE; +} + +void OverlayUserPrefStore::ReadPrefsAsync( + ReadErrorDelegate* error_delegate_raw) { + std::unique_ptr error_delegate(error_delegate_raw); + // We do not read intentionally. + OnInitializationCompleted(/* ephemeral */ false, true); +} + +void OverlayUserPrefStore::CommitPendingWrite( + base::OnceClosure reply_callback, + base::OnceClosure synchronous_done_callback) { + persistent_user_pref_store_->CommitPendingWrite( + std::move(reply_callback), std::move(synchronous_done_callback)); + // We do not write our content intentionally. +} + +void OverlayUserPrefStore::SchedulePendingLossyWrites() { + persistent_user_pref_store_->SchedulePendingLossyWrites(); +} + +void OverlayUserPrefStore::ReportValueChanged(const std::string& key, + uint32_t flags) { + for (PrefStore::Observer& observer : observers_) + observer.OnPrefValueChanged(key); +} + +void OverlayUserPrefStore::RegisterPersistentPref(const std::string& key) { + DCHECK(!key.empty()) << "Key is empty"; + DCHECK(persistent_names_set_.find(key) == persistent_names_set_.end()) + << "Key already registered: " << key; + persistent_names_set_.insert(key); +} + +void OverlayUserPrefStore::ClearMutableValues() { + for (const auto& key : written_ephemeral_names_) { + ephemeral_user_pref_store_->RemoveValue( + key, WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + } +} + +void OverlayUserPrefStore::OnStoreDeletionFromDisk() { + persistent_user_pref_store_->OnStoreDeletionFromDisk(); +} + +OverlayUserPrefStore::~OverlayUserPrefStore() { + ephemeral_user_pref_store_->RemoveObserver( + ephemeral_pref_store_observer_.get()); + persistent_user_pref_store_->RemoveObserver( + persistent_pref_store_observer_.get()); +} + +void OverlayUserPrefStore::OnPrefValueChanged(bool ephemeral, + const std::string& key) { + if (ephemeral) { + ReportValueChanged(key, DEFAULT_PREF_WRITE_FLAGS); + } else { + if (!ephemeral_user_pref_store_->GetValue(key, nullptr)) + ReportValueChanged(key, DEFAULT_PREF_WRITE_FLAGS); + } +} + +void OverlayUserPrefStore::OnInitializationCompleted(bool ephemeral, + bool succeeded) { + if (!IsInitializationComplete()) + return; + for (PrefStore::Observer& observer : observers_) + observer.OnInitializationCompleted(succeeded); +} + +bool OverlayUserPrefStore::ShallBeStoredInPersistent( + const std::string& key) const { + return persistent_names_set_.find(key) != persistent_names_set_.end(); +} diff --git a/src/components/prefs/overlay_user_pref_store.h b/src/components/prefs/overlay_user_pref_store.h new file mode 100644 index 0000000000..d4f676c56e --- /dev/null +++ b/src/components/prefs/overlay_user_pref_store.h @@ -0,0 +1,98 @@ +// Copyright (c) 2012 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_PREFS_OVERLAY_USER_PREF_STORE_H_ +#define COMPONENTS_PREFS_OVERLAY_USER_PREF_STORE_H_ + +#include + +#include +#include + +#include "base/memory/ref_counted.h" +#include "base/observer_list.h" +#include "components/prefs/persistent_pref_store.h" +#include "components/prefs/pref_value_map.h" +#include "components/prefs/prefs_export.h" + +// PersistentPrefStore that directs all write operations into an in-memory +// PrefValueMap. Read operations are first answered by the PrefValueMap. +// If the PrefValueMap does not contain a value for the requested key, +// the look-up is passed on to an underlying PersistentPrefStore +// |persistent_user_pref_store_|. +class COMPONENTS_PREFS_EXPORT OverlayUserPrefStore + : public PersistentPrefStore { + public: + explicit OverlayUserPrefStore(PersistentPrefStore* persistent); + // The |ephemeral| store must already be initialized. + OverlayUserPrefStore(PersistentPrefStore* ephemeral, + PersistentPrefStore* persistent); + + OverlayUserPrefStore(const OverlayUserPrefStore&) = delete; + OverlayUserPrefStore& operator=(const OverlayUserPrefStore&) = delete; + + // Returns true if a value has been set for the |key| in this + // OverlayUserPrefStore, i.e. if it potentially overrides a value + // from the |persistent_user_pref_store_|. + virtual bool IsSetInOverlay(const std::string& key) const; + + // Methods of PrefStore. + void AddObserver(PrefStore::Observer* observer) override; + void RemoveObserver(PrefStore::Observer* observer) override; + bool HasObservers() const override; + bool IsInitializationComplete() const override; + bool GetValue(const std::string& key, + const base::Value** result) const override; + std::unique_ptr GetValues() const override; + + // Methods of PersistentPrefStore. + bool GetMutableValue(const std::string& key, base::Value** result) override; + void SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + void SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + void RemoveValue(const std::string& key, uint32_t flags) override; + void RemoveValuesByPrefixSilently(const std::string& prefix) override; + bool ReadOnly() const override; + PrefReadError GetReadError() const override; + PrefReadError ReadPrefs() override; + void ReadPrefsAsync(ReadErrorDelegate* delegate) override; + void CommitPendingWrite(base::OnceClosure reply_callback, + base::OnceClosure synchronous_done_callback) override; + void SchedulePendingLossyWrites() override; + void ReportValueChanged(const std::string& key, uint32_t flags) override; + + // Registers preferences that should be stored in the persistent preferences + // (|persistent_user_pref_store_|). + void RegisterPersistentPref(const std::string& key); + + void ClearMutableValues() override; + void OnStoreDeletionFromDisk() override; + + protected: + ~OverlayUserPrefStore() override; + + private: + typedef std::set NamesSet; + class ObserverAdapter; + + void OnPrefValueChanged(bool ephemeral, const std::string& key); + void OnInitializationCompleted(bool ephemeral, bool succeeded); + + // Returns true if |key| corresponds to a preference that shall be stored in + // persistent PrefStore. + bool ShallBeStoredInPersistent(const std::string& key) const; + + base::ObserverList::Unchecked observers_; + std::unique_ptr ephemeral_pref_store_observer_; + std::unique_ptr persistent_pref_store_observer_; + scoped_refptr ephemeral_user_pref_store_; + scoped_refptr persistent_user_pref_store_; + NamesSet persistent_names_set_; + NamesSet written_ephemeral_names_; +}; + +#endif // COMPONENTS_PREFS_OVERLAY_USER_PREF_STORE_H_ diff --git a/src/components/prefs/overlay_user_pref_store_unittest.cc b/src/components/prefs/overlay_user_pref_store_unittest.cc new file mode 100644 index 0000000000..067188d965 --- /dev/null +++ b/src/components/prefs/overlay_user_pref_store_unittest.cc @@ -0,0 +1,282 @@ +// Copyright (c) 2012 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 "components/prefs/overlay_user_pref_store.h" + +#include + +#include "base/test/task_environment.h" +#include "base/values.h" +#include "components/prefs/persistent_pref_store_unittest.h" +#include "components/prefs/pref_store_observer_mock.h" +#include "components/prefs/testing_pref_store.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::Mock; +using ::testing::StrEq; + +namespace base { +namespace { + +const char kBrowserWindowPlacement[] = "browser.window_placement"; +const char kShowBookmarkBar[] = "bookmark_bar.show_on_all_tabs"; +const char kSharedKey[] = "sync_promo.show_on_first_run_allowed"; + +const char* const regular_key = kBrowserWindowPlacement; +const char* const persistent_key = kShowBookmarkBar; +const char* const shared_key = kSharedKey; + +} // namespace + +class OverlayUserPrefStoreTest : public testing::Test { + protected: + OverlayUserPrefStoreTest() + : underlay_(new TestingPrefStore()), + overlay_(new OverlayUserPrefStore(underlay_.get())) { + overlay_->RegisterPersistentPref(persistent_key); + overlay_->RegisterPersistentPref(shared_key); + } + + ~OverlayUserPrefStoreTest() override {} + + base::test::TaskEnvironment task_environment_; + scoped_refptr underlay_; + scoped_refptr overlay_; +}; + +TEST_F(OverlayUserPrefStoreTest, Observer) { + PrefStoreObserverMock obs; + overlay_->AddObserver(&obs); + + // Check that underlay first value is reported. + underlay_->SetValue(regular_key, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + obs.VerifyAndResetChangedKey(regular_key); + + // Check that underlay overwriting is reported. + underlay_->SetValue(regular_key, std::make_unique(43), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + obs.VerifyAndResetChangedKey(regular_key); + + // Check that overwriting change in overlay is reported. + overlay_->SetValue(regular_key, std::make_unique(44), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + obs.VerifyAndResetChangedKey(regular_key); + + // Check that hidden underlay change is not reported. + underlay_->SetValue(regular_key, std::make_unique(45), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_TRUE(obs.changed_keys.empty()); + + // Check that overlay remove is reported. + overlay_->RemoveValue(regular_key, + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + obs.VerifyAndResetChangedKey(regular_key); + + // Check that underlay remove is reported. + underlay_->RemoveValue(regular_key, + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + obs.VerifyAndResetChangedKey(regular_key); + + // Check respecting of silence. + overlay_->SetValueSilently(regular_key, std::make_unique(46), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_TRUE(obs.changed_keys.empty()); + + overlay_->RemoveObserver(&obs); + + // Check successful unsubscription. + underlay_->SetValue(regular_key, std::make_unique(47), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + overlay_->SetValue(regular_key, std::make_unique(48), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_TRUE(obs.changed_keys.empty()); +} + +TEST_F(OverlayUserPrefStoreTest, GetAndSet) { + const Value* value = nullptr; + EXPECT_FALSE(overlay_->GetValue(regular_key, &value)); + EXPECT_FALSE(underlay_->GetValue(regular_key, &value)); + + underlay_->SetValue(regular_key, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + // Value shines through: + EXPECT_TRUE(overlay_->GetValue(regular_key, &value)); + EXPECT_EQ(base::Value(42), *value); + + EXPECT_TRUE(underlay_->GetValue(regular_key, &value)); + EXPECT_EQ(base::Value(42), *value); + + overlay_->SetValue(regular_key, std::make_unique(43), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + EXPECT_TRUE(overlay_->GetValue(regular_key, &value)); + EXPECT_EQ(base::Value(43), *value); + + EXPECT_TRUE(underlay_->GetValue(regular_key, &value)); + EXPECT_EQ(base::Value(42), *value); + + overlay_->RemoveValue(regular_key, + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + // Value shines through: + EXPECT_TRUE(overlay_->GetValue(regular_key, &value)); + EXPECT_EQ(base::Value(42), *value); + + EXPECT_TRUE(underlay_->GetValue(regular_key, &value)); + EXPECT_EQ(base::Value(42), *value); +} + +// Check that GetMutableValue does not return the dictionary of the underlay. +TEST_F(OverlayUserPrefStoreTest, ModifyDictionaries) { + underlay_->SetValue(regular_key, std::make_unique(), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + Value* modify = nullptr; + EXPECT_TRUE(overlay_->GetMutableValue(regular_key, &modify)); + ASSERT_TRUE(modify); + ASSERT_TRUE(modify->is_dict()); + static_cast(modify)->SetInteger(regular_key, 42); + + Value* original_in_underlay = nullptr; + EXPECT_TRUE(underlay_->GetMutableValue(regular_key, &original_in_underlay)); + ASSERT_TRUE(original_in_underlay); + ASSERT_TRUE(original_in_underlay->is_dict()); + EXPECT_TRUE(original_in_underlay->DictEmpty()); + + Value* modified = nullptr; + EXPECT_TRUE(overlay_->GetMutableValue(regular_key, &modified)); + ASSERT_TRUE(modified); + ASSERT_TRUE(modified->is_dict()); + EXPECT_EQ(*modify, *modified); +} + +// Here we consider a global preference that is not overlayed. +TEST_F(OverlayUserPrefStoreTest, GlobalPref) { + PrefStoreObserverMock obs; + overlay_->AddObserver(&obs); + + const Value* value = nullptr; + + // Check that underlay first value is reported. + underlay_->SetValue(persistent_key, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + obs.VerifyAndResetChangedKey(persistent_key); + + // Check that underlay overwriting is reported. + underlay_->SetValue(persistent_key, std::make_unique(43), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + obs.VerifyAndResetChangedKey(persistent_key); + + // Check that we get this value from the overlay + EXPECT_TRUE(overlay_->GetValue(persistent_key, &value)); + EXPECT_EQ(base::Value(43), *value); + + // Check that overwriting change in overlay is reported. + overlay_->SetValue(persistent_key, std::make_unique(44), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + obs.VerifyAndResetChangedKey(persistent_key); + + // Check that we get this value from the overlay and the underlay. + EXPECT_TRUE(overlay_->GetValue(persistent_key, &value)); + EXPECT_EQ(base::Value(44), *value); + EXPECT_TRUE(underlay_->GetValue(persistent_key, &value)); + EXPECT_EQ(base::Value(44), *value); + + // Check that overlay remove is reported. + overlay_->RemoveValue(persistent_key, + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + obs.VerifyAndResetChangedKey(persistent_key); + + // Check that value was removed from overlay and underlay + EXPECT_FALSE(overlay_->GetValue(persistent_key, &value)); + EXPECT_FALSE(underlay_->GetValue(persistent_key, &value)); + + // Check respecting of silence. + overlay_->SetValueSilently(persistent_key, std::make_unique(46), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_TRUE(obs.changed_keys.empty()); + + overlay_->RemoveObserver(&obs); + + // Check successful unsubscription. + underlay_->SetValue(persistent_key, std::make_unique(47), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + overlay_->SetValue(persistent_key, std::make_unique(48), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_TRUE(obs.changed_keys.empty()); +} + +// Check that mutable values are removed correctly. +TEST_F(OverlayUserPrefStoreTest, ClearMutableValues) { + // Set in overlay and underlay the same preference. + underlay_->SetValue(regular_key, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + overlay_->SetValue(regular_key, std::make_unique(43), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + const Value* value = nullptr; + // Check that an overlay preference is returned. + EXPECT_TRUE(overlay_->GetValue(regular_key, &value)); + EXPECT_EQ(base::Value(43), *value); + overlay_->ClearMutableValues(); + + // Check that an underlay preference is returned. + EXPECT_TRUE(overlay_->GetValue(regular_key, &value)); + EXPECT_EQ(base::Value(42), *value); +} + +// Check that mutable values are removed correctly when using a silent set. +TEST_F(OverlayUserPrefStoreTest, ClearMutableValues_Silently) { + // Set in overlay and underlay the same preference. + underlay_->SetValueSilently(regular_key, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + overlay_->SetValueSilently(regular_key, std::make_unique(43), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + const Value* value = nullptr; + // Check that an overlay preference is returned. + EXPECT_TRUE(overlay_->GetValue(regular_key, &value)); + EXPECT_EQ(base::Value(43), *value); + overlay_->ClearMutableValues(); + + // Check that an underlay preference is returned. + EXPECT_TRUE(overlay_->GetValue(regular_key, &value)); + EXPECT_EQ(base::Value(42), *value); +} + +TEST_F(OverlayUserPrefStoreTest, GetValues) { + // To check merge behavior, create underlay and overlay so each has a key the + // other doesn't have and they have one key in common. + underlay_->SetValue(persistent_key, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + overlay_->SetValue(regular_key, std::make_unique(43), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + underlay_->SetValue(shared_key, std::make_unique(42), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + overlay_->SetValue(shared_key, std::make_unique(43), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + auto values = overlay_->GetValues(); + const Value* value = nullptr; + // Check that an overlay preference is returned. + ASSERT_TRUE(values->Get(persistent_key, &value)); + EXPECT_EQ(base::Value(42), *value); + + // Check that an underlay preference is returned. + ASSERT_TRUE(values->Get(regular_key, &value)); + EXPECT_EQ(base::Value(43), *value); + + // Check that the overlay is preferred. + ASSERT_TRUE(values->Get(shared_key, &value)); + EXPECT_EQ(base::Value(43), *value); +} + +TEST_F(OverlayUserPrefStoreTest, CommitPendingWriteWithCallback) { + TestCommitPendingWriteWithCallback(overlay_.get(), &task_environment_); +} + +} // namespace base diff --git a/src/components/prefs/persistent_pref_store.cc b/src/components/prefs/persistent_pref_store.cc new file mode 100644 index 0000000000..9086c1ed87 --- /dev/null +++ b/src/components/prefs/persistent_pref_store.cc @@ -0,0 +1,31 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/prefs/persistent_pref_store.h" + +#include + +#include "base/threading/sequenced_task_runner_handle.h" + +void PersistentPrefStore::CommitPendingWrite( + base::OnceClosure reply_callback, + base::OnceClosure synchronous_done_callback) { + // Default behavior for PersistentPrefStore implementation that don't issue + // disk operations: schedule the callback immediately. + // |synchronous_done_callback| is allowed to be invoked synchronously (and + // must be here since we have no other way to post it which isn't the current + // sequence). + + if (synchronous_done_callback) + std::move(synchronous_done_callback).Run(); + + if (reply_callback) { + base::SequencedTaskRunnerHandle::Get()->PostTask(FROM_HERE, + std::move(reply_callback)); + } +} + +bool PersistentPrefStore::IsInMemoryPrefStore() const { + return false; +} diff --git a/src/components/prefs/persistent_pref_store.h b/src/components/prefs/persistent_pref_store.h new file mode 100644 index 0000000000..df39505198 --- /dev/null +++ b/src/components/prefs/persistent_pref_store.h @@ -0,0 +1,93 @@ +// Copyright (c) 2012 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_PREFS_PERSISTENT_PREF_STORE_H_ +#define COMPONENTS_PREFS_PERSISTENT_PREF_STORE_H_ + +#include "base/callback.h" +#include "components/prefs/prefs_export.h" +#include "components/prefs/writeable_pref_store.h" + +// This interface is complementary to the PrefStore interface, declaring +// additional functionality that adds support for setting values and persisting +// the data to some backing store. +class COMPONENTS_PREFS_EXPORT PersistentPrefStore : public WriteablePrefStore { + public: + // Unique integer code for each type of error so we can report them + // distinctly in a histogram. + // NOTE: Don't change the explicit values of the enums as it will change the + // server's meaning of the histogram. + enum PrefReadError { + PREF_READ_ERROR_NONE = 0, + PREF_READ_ERROR_JSON_PARSE = 1, + PREF_READ_ERROR_JSON_TYPE = 2, + PREF_READ_ERROR_ACCESS_DENIED = 3, + PREF_READ_ERROR_FILE_OTHER = 4, + PREF_READ_ERROR_FILE_LOCKED = 5, + PREF_READ_ERROR_NO_FILE = 6, + PREF_READ_ERROR_JSON_REPEAT = 7, + // PREF_READ_ERROR_OTHER = 8, // Deprecated. + PREF_READ_ERROR_FILE_NOT_SPECIFIED = 9, + // Indicates that ReadPrefs() couldn't complete synchronously and is waiting + // for an asynchronous task to complete first. + PREF_READ_ERROR_ASYNCHRONOUS_TASK_INCOMPLETE = 10, + PREF_READ_ERROR_MAX_ENUM + }; + + class ReadErrorDelegate { + public: + virtual ~ReadErrorDelegate() {} + + virtual void OnError(PrefReadError error) = 0; + }; + + // Whether the store is in a pseudo-read-only mode where changes are not + // actually persisted to disk. This happens in some cases when there are + // read errors during startup. + virtual bool ReadOnly() const = 0; + + // Gets the read error. Only valid if IsInitializationComplete() returns true. + virtual PrefReadError GetReadError() const = 0; + + // Reads the preferences from disk. Notifies observers via + // "PrefStore::OnInitializationCompleted" when done. + virtual PrefReadError ReadPrefs() = 0; + + // Reads the preferences from disk asynchronously. Notifies observers via + // "PrefStore::OnInitializationCompleted" when done. Also it fires + // |error_delegate| if it is not NULL and reading error has occurred. + // Owns |error_delegate|. + virtual void ReadPrefsAsync(ReadErrorDelegate* error_delegate) = 0; + + // Lands pending writes to disk. |reply_callback| will be posted to the + // current sequence when changes have been written. + // |synchronous_done_callback| on the other hand will be invoked right away + // wherever the writes complete (could even be invoked synchronously if no + // writes need to occur); this is useful when the current thread cannot pump + // messages to observe the reply (e.g. nested loops banned on main thread + // during shutdown). |synchronous_done_callback| must be thread-safe. + virtual void CommitPendingWrite( + base::OnceClosure reply_callback = base::OnceClosure(), + base::OnceClosure synchronous_done_callback = base::OnceClosure()); + + // Schedules a write if there is any lossy data pending. Unlike + // CommitPendingWrite() this does not immediately sync to disk, instead it + // triggers an eventual write if there is lossy data pending and if there + // isn't one scheduled already. + virtual void SchedulePendingLossyWrites() = 0; + + // It should be called only for Incognito pref store. + virtual void ClearMutableValues() = 0; + + // Cleans preference data that may have been saved outside of the store. + virtual void OnStoreDeletionFromDisk() = 0; + + // TODO(crbug.com/942491) Remove this after fixing the bug. + virtual bool IsInMemoryPrefStore() const; + + protected: + ~PersistentPrefStore() override {} +}; + +#endif // COMPONENTS_PREFS_PERSISTENT_PREF_STORE_H_ diff --git a/src/components/prefs/persistent_pref_store_unittest.cc b/src/components/prefs/persistent_pref_store_unittest.cc new file mode 100644 index 0000000000..9ea3f988c5 --- /dev/null +++ b/src/components/prefs/persistent_pref_store_unittest.cc @@ -0,0 +1,26 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/prefs/persistent_pref_store.h" + +#include "base/bind.h" +#include "base/run_loop.h" +#include "base/sequence_checker_impl.h" +#include "base/test/task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +void TestCommitPendingWriteWithCallback( + PersistentPrefStore* store, + base::test::TaskEnvironment* task_environment) { + base::RunLoop run_loop; + base::SequenceCheckerImpl sequence_checker; + store->CommitPendingWrite(base::BindOnce( + [](base::SequenceCheckerImpl* sequence_checker, base::RunLoop* run_loop) { + EXPECT_TRUE(sequence_checker->CalledOnValidSequence()); + run_loop->Quit(); + }, + base::Unretained(&sequence_checker), base::Unretained(&run_loop))); + task_environment->RunUntilIdle(); + run_loop.Run(); +} diff --git a/src/components/prefs/persistent_pref_store_unittest.h b/src/components/prefs/persistent_pref_store_unittest.h new file mode 100644 index 0000000000..121f942b1a --- /dev/null +++ b/src/components/prefs/persistent_pref_store_unittest.h @@ -0,0 +1,24 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_PREFS_PERSISTENT_PREF_STORE_UNITTEST_H_ +#define COMPONENTS_PREFS_PERSISTENT_PREF_STORE_UNITTEST_H_ + +namespace base { +namespace test { +class TaskEnvironment; +} +} // namespace base + +class PersistentPrefStore; + +// Calls CommitPendingWrite() on |store| with a callback. Verifies that the +// callback runs on the appropriate sequence. |task_environment| is the +// test's TaskEnvironment. This function is meant to be reused in the +// tests of various PersistentPrefStore implementations. +void TestCommitPendingWriteWithCallback( + PersistentPrefStore* store, + base::test::TaskEnvironment* task_environment); + +#endif // COMPONENTS_PREFS_PERSISTENT_PREF_STORE_UNITTEST_H_ diff --git a/src/components/prefs/pref_change_registrar.cc b/src/components/prefs/pref_change_registrar.cc new file mode 100644 index 0000000000..6048ef403d --- /dev/null +++ b/src/components/prefs/pref_change_registrar.cc @@ -0,0 +1,102 @@ +// Copyright (c) 2010 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 "components/prefs/pref_change_registrar.h" + +#include "base/bind.h" +#include "base/check.h" +#include "base/notreached.h" +#include "components/prefs/pref_service.h" + +PrefChangeRegistrar::PrefChangeRegistrar() : service_(nullptr) {} + +PrefChangeRegistrar::~PrefChangeRegistrar() { + // If you see an invalid memory access in this destructor, this + // PrefChangeRegistrar might be subscribed to an OffTheRecordProfileImpl that + // has been destroyed. This should not happen any more but be warned. + // Feel free to contact battre@chromium.org in case this happens. + // + // This can also happen for non-OTR profiles, when the + // DestroyProfileOnBrowserClose flag is enabled. In that case, contact + // nicolaso@chromium.org. + RemoveAll(); +} + +void PrefChangeRegistrar::Init(PrefService* service) { + DCHECK(IsEmpty() || service_ == service); + service_ = service; +} + +void PrefChangeRegistrar::Add(const std::string& path, + const base::RepeatingClosure& obs) { + Add(path, + base::BindRepeating(&PrefChangeRegistrar::InvokeUnnamedCallback, obs)); +} + +void PrefChangeRegistrar::Add(const std::string& path, + const NamedChangeCallback& obs) { + if (!service_) { + NOTREACHED(); + return; + } + DCHECK(!IsObserved(path)) << "Already had pref, \"" << path + << "\", registered."; + + service_->AddPrefObserver(path, this); + observers_[path] = obs; +} + +void PrefChangeRegistrar::Remove(const std::string& path) { + DCHECK(IsObserved(path)); + + observers_.erase(path); + service_->RemovePrefObserver(path, this); +} + +void PrefChangeRegistrar::RemoveAll() { + for (ObserverMap::const_iterator it = observers_.begin(); + it != observers_.end(); ++it) { + service_->RemovePrefObserver(it->first, this); + } + + observers_.clear(); +} + +bool PrefChangeRegistrar::IsEmpty() const { + return observers_.empty(); +} + +bool PrefChangeRegistrar::IsObserved(const std::string& pref) { + return observers_.find(pref) != observers_.end(); +} + +bool PrefChangeRegistrar::IsManaged() { + for (ObserverMap::const_iterator it = observers_.begin(); + it != observers_.end(); ++it) { + const PrefService::Preference* pref = service_->FindPreference(it->first); + if (pref && pref->IsManaged()) + return true; + } + return false; +} + +void PrefChangeRegistrar::OnPreferenceChanged(PrefService* service, + const std::string& pref) { + if (IsObserved(pref)) + observers_[pref].Run(pref); +} + +void PrefChangeRegistrar::InvokeUnnamedCallback( + const base::RepeatingClosure& callback, + const std::string& pref_name) { + callback.Run(); +} + +PrefService* PrefChangeRegistrar::prefs() { + return service_; +} + +const PrefService* PrefChangeRegistrar::prefs() const { + return service_; +} diff --git a/src/components/prefs/pref_change_registrar.h b/src/components/prefs/pref_change_registrar.h new file mode 100644 index 0000000000..d41e707cac --- /dev/null +++ b/src/components/prefs/pref_change_registrar.h @@ -0,0 +1,83 @@ +// Copyright (c) 2010 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_PREFS_PREF_CHANGE_REGISTRAR_H_ +#define COMPONENTS_PREFS_PREF_CHANGE_REGISTRAR_H_ + +#include +#include + +#include "base/callback.h" +#include "base/memory/raw_ptr.h" +#include "components/prefs/pref_observer.h" +#include "components/prefs/prefs_export.h" + +class PrefService; + +// Automatically manages the registration of one or more pref change observers +// with a PrefStore. Functions much like NotificationRegistrar, but specifically +// manages observers of preference changes. When the Registrar is destroyed, +// all registered observers are automatically unregistered with the PrefStore. +class COMPONENTS_PREFS_EXPORT PrefChangeRegistrar final : public PrefObserver { + public: + // You can register this type of callback if you need to know the + // path of the preference that is changing. + using NamedChangeCallback = base::RepeatingCallback; + + PrefChangeRegistrar(); + + PrefChangeRegistrar(const PrefChangeRegistrar&) = delete; + PrefChangeRegistrar& operator=(const PrefChangeRegistrar&) = delete; + + ~PrefChangeRegistrar(); + + // Must be called before adding or removing observers. Can be called more + // than once as long as the value of |service| doesn't change. + void Init(PrefService* service); + + // Adds a pref observer for the specified pref |path| and |obs| observer + // object. All registered observers will be automatically unregistered + // when the registrar's destructor is called. + // + // The second version binds a callback that will receive the path of + // the preference that is changing as its parameter. + // + // Only one observer may be registered per path. + void Add(const std::string& path, const base::RepeatingClosure& obs); + void Add(const std::string& path, const NamedChangeCallback& obs); + + // Removes the pref observer registered for |path|. + void Remove(const std::string& path); + + // Removes all observers that have been previously added with a call to Add. + void RemoveAll(); + + // Returns true if no pref observers are registered. + bool IsEmpty() const; + + // Check whether |pref| is in the set of preferences being observed. + bool IsObserved(const std::string& pref); + + // Check whether any of the observed preferences has the managed bit set. + bool IsManaged(); + + // Return the PrefService for this registrar. + PrefService* prefs(); + const PrefService* prefs() const; + + private: + // PrefObserver: + void OnPreferenceChanged(PrefService* service, + const std::string& pref_name) override; + + static void InvokeUnnamedCallback(const base::RepeatingClosure& callback, + const std::string& pref_name); + + using ObserverMap = std::map; + + ObserverMap observers_; + raw_ptr service_; +}; + +#endif // COMPONENTS_PREFS_PREF_CHANGE_REGISTRAR_H_ diff --git a/src/components/prefs/pref_change_registrar_unittest.cc b/src/components/prefs/pref_change_registrar_unittest.cc new file mode 100644 index 0000000000..8e94bfaf69 --- /dev/null +++ b/src/components/prefs/pref_change_registrar_unittest.cc @@ -0,0 +1,204 @@ +// Copyright (c) 2010 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 "components/prefs/pref_change_registrar.h" + +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "components/prefs/pref_observer.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/testing_pref_service.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::Mock; +using testing::Eq; + +namespace base { +namespace { + +const char kHomePage[] = "homepage"; +const char kHomePageIsNewTabPage[] = "homepage_is_newtabpage"; +const char kApplicationLocale[] = "intl.app_locale"; + +// A mock provider that allows us to capture pref observer changes. +class MockPrefService : public TestingPrefServiceSimple { + public: + MockPrefService() {} + ~MockPrefService() override {} + + MOCK_METHOD2(AddPrefObserver, void(const std::string&, PrefObserver*)); + MOCK_METHOD2(RemovePrefObserver, void(const std::string&, PrefObserver*)); +}; + +// Due to overloads, base::DoNothing() cannot be passed directly to +// PrefChangeRegistrar::Add() as it is convertible to all callbacks. +base::RepeatingClosure DoNothingClosure() { + return base::DoNothing(); +} + +} // namespace + +class PrefChangeRegistrarTest : public testing::Test { + public: + PrefChangeRegistrarTest() {} + ~PrefChangeRegistrarTest() override {} + + protected: + void SetUp() override; + + MockPrefService* service() const { return service_.get(); } + + private: + std::unique_ptr service_; +}; + +void PrefChangeRegistrarTest::SetUp() { + service_ = std::make_unique(); +} + +TEST_F(PrefChangeRegistrarTest, AddAndRemove) { + PrefChangeRegistrar registrar; + registrar.Init(service()); + + // Test adding. + EXPECT_CALL(*service(), + AddPrefObserver(Eq(std::string("test.pref.1")), ®istrar)); + EXPECT_CALL(*service(), + AddPrefObserver(Eq(std::string("test.pref.2")), ®istrar)); + registrar.Add("test.pref.1", DoNothingClosure()); + registrar.Add("test.pref.2", DoNothingClosure()); + EXPECT_FALSE(registrar.IsEmpty()); + + // Test removing. + Mock::VerifyAndClearExpectations(service()); + EXPECT_CALL(*service(), + RemovePrefObserver(Eq(std::string("test.pref.1")), ®istrar)); + EXPECT_CALL(*service(), + RemovePrefObserver(Eq(std::string("test.pref.2")), ®istrar)); + registrar.Remove("test.pref.1"); + registrar.Remove("test.pref.2"); + EXPECT_TRUE(registrar.IsEmpty()); + + // Explicitly check the expectations now to make sure that the Removes + // worked (rather than the registrar destructor doing the work). + Mock::VerifyAndClearExpectations(service()); +} + +TEST_F(PrefChangeRegistrarTest, AutoRemove) { + PrefChangeRegistrar registrar; + registrar.Init(service()); + + // Setup of auto-remove. + EXPECT_CALL(*service(), + AddPrefObserver(Eq(std::string("test.pref.1")), ®istrar)); + registrar.Add("test.pref.1", DoNothingClosure()); + Mock::VerifyAndClearExpectations(service()); + EXPECT_FALSE(registrar.IsEmpty()); + + // Test auto-removing. + EXPECT_CALL(*service(), + RemovePrefObserver(Eq(std::string("test.pref.1")), ®istrar)); +} + +TEST_F(PrefChangeRegistrarTest, RemoveAll) { + PrefChangeRegistrar registrar; + registrar.Init(service()); + + EXPECT_CALL(*service(), + AddPrefObserver(Eq(std::string("test.pref.1")), ®istrar)); + EXPECT_CALL(*service(), + AddPrefObserver(Eq(std::string("test.pref.2")), ®istrar)); + registrar.Add("test.pref.1", DoNothingClosure()); + registrar.Add("test.pref.2", DoNothingClosure()); + Mock::VerifyAndClearExpectations(service()); + + EXPECT_CALL(*service(), + RemovePrefObserver(Eq(std::string("test.pref.1")), ®istrar)); + EXPECT_CALL(*service(), + RemovePrefObserver(Eq(std::string("test.pref.2")), ®istrar)); + registrar.RemoveAll(); + EXPECT_TRUE(registrar.IsEmpty()); + + // Explicitly check the expectations now to make sure that the RemoveAll + // worked (rather than the registrar destructor doing the work). + Mock::VerifyAndClearExpectations(service()); +} + +class ObserveSetOfPreferencesTest : public testing::Test { + public: + void SetUp() override { + pref_service_ = std::make_unique(); + PrefRegistrySimple* registry = pref_service_->registry(); + registry->RegisterStringPref(kHomePage, "http://google.com"); + registry->RegisterBooleanPref(kHomePageIsNewTabPage, false); + registry->RegisterStringPref(kApplicationLocale, std::string()); + } + + PrefChangeRegistrar* CreatePrefChangeRegistrar() { + PrefChangeRegistrar* pref_set = new PrefChangeRegistrar(); + pref_set->Init(pref_service_.get()); + pref_set->Add(kHomePage, DoNothingClosure()); + pref_set->Add(kHomePageIsNewTabPage, DoNothingClosure()); + return pref_set; + } + + MOCK_METHOD1(OnPreferenceChanged, void(const std::string&)); + + std::unique_ptr pref_service_; +}; + +TEST_F(ObserveSetOfPreferencesTest, IsObserved) { + std::unique_ptr pref_set(CreatePrefChangeRegistrar()); + EXPECT_TRUE(pref_set->IsObserved(kHomePage)); + EXPECT_TRUE(pref_set->IsObserved(kHomePageIsNewTabPage)); + EXPECT_FALSE(pref_set->IsObserved(kApplicationLocale)); +} + +TEST_F(ObserveSetOfPreferencesTest, IsManaged) { + std::unique_ptr pref_set(CreatePrefChangeRegistrar()); + EXPECT_FALSE(pref_set->IsManaged()); + pref_service_->SetManagedPref(kHomePage, + std::make_unique("http://crbug.com")); + EXPECT_TRUE(pref_set->IsManaged()); + pref_service_->SetManagedPref(kHomePageIsNewTabPage, + std::make_unique(true)); + EXPECT_TRUE(pref_set->IsManaged()); + pref_service_->RemoveManagedPref(kHomePage); + EXPECT_TRUE(pref_set->IsManaged()); + pref_service_->RemoveManagedPref(kHomePageIsNewTabPage); + EXPECT_FALSE(pref_set->IsManaged()); +} + +TEST_F(ObserveSetOfPreferencesTest, Observe) { + using testing::_; + using testing::Mock; + + PrefChangeRegistrar pref_set; + PrefChangeRegistrar::NamedChangeCallback callback = + base::BindRepeating(&ObserveSetOfPreferencesTest::OnPreferenceChanged, + base::Unretained(this)); + pref_set.Init(pref_service_.get()); + pref_set.Add(kHomePage, callback); + pref_set.Add(kHomePageIsNewTabPage, callback); + + EXPECT_CALL(*this, OnPreferenceChanged(kHomePage)); + pref_service_->SetUserPref(kHomePage, + std::make_unique("http://crbug.com")); + Mock::VerifyAndClearExpectations(this); + + EXPECT_CALL(*this, OnPreferenceChanged(kHomePageIsNewTabPage)); + pref_service_->SetUserPref(kHomePageIsNewTabPage, + std::make_unique(true)); + Mock::VerifyAndClearExpectations(this); + + EXPECT_CALL(*this, OnPreferenceChanged(_)).Times(0); + pref_service_->SetUserPref(kApplicationLocale, + std::make_unique("en_US.utf8")); + Mock::VerifyAndClearExpectations(this); +} + +} // namespace base diff --git a/src/components/prefs/pref_filter.h b/src/components/prefs/pref_filter.h new file mode 100644 index 0000000000..33e61ccc53 --- /dev/null +++ b/src/components/prefs/pref_filter.h @@ -0,0 +1,66 @@ +// Copyright 2013 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_PREFS_PREF_FILTER_H_ +#define COMPONENTS_PREFS_PREF_FILTER_H_ + +#include +#include +#include + +#include "base/callback_forward.h" +#include "components/prefs/prefs_export.h" + +namespace base { +class DictionaryValue; +} // namespace base + +// Filters preferences as they are loaded from disk or updated at runtime. +// Currently supported only by JsonPrefStore. +class COMPONENTS_PREFS_EXPORT PrefFilter { + public: + // A pair of pre-write and post-write callbacks. + using OnWriteCallbackPair = + std::pair>; + + // A callback to be invoked when |prefs| have been read (and possibly + // pre-modified) and are now ready to be handed back to this callback's + // builder. |schedule_write| indicates whether a write should be immediately + // scheduled (typically because the |prefs| were pre-modified). + using PostFilterOnLoadCallback = + base::OnceCallback prefs, + bool schedule_write)>; + + virtual ~PrefFilter() {} + + // This method is given ownership of the |pref_store_contents| read from disk + // before the underlying PersistentPrefStore gets to use them. It must hand + // them back via |post_filter_on_load_callback|, but may modify them first. + // Note: This method is asynchronous, which may make calls like + // PersistentPrefStore::ReadPrefs() asynchronous. The owner of filtered + // PersistentPrefStores should handle this to make the reads look synchronous + // to external users (see SegregatedPrefStore::ReadPrefs() for an example). + virtual void FilterOnLoad( + PostFilterOnLoadCallback post_filter_on_load_callback, + std::unique_ptr pref_store_contents) = 0; + + // Receives notification when a pref store value is changed, before Observers + // are notified. + virtual void FilterUpdate(const std::string& path) = 0; + + // Receives notification when the pref store is about to serialize data + // contained in |pref_store_contents| to a string. Modifications to + // |pref_store_contents| will be persisted to disk and also affect the + // in-memory state. + // If the returned callbacks are non-null, they will be registered to be + // invoked synchronously after the next write (from the I/O TaskRunner so they + // must not be bound to thread-unsafe member state). + virtual OnWriteCallbackPair FilterSerializeData( + base::DictionaryValue* pref_store_contents) = 0; + + // Cleans preference data that may have been saved outside of the store. + virtual void OnStoreDeletionFromDisk() = 0; +}; + +#endif // COMPONENTS_PREFS_PREF_FILTER_H_ diff --git a/src/components/prefs/pref_member.cc b/src/components/prefs/pref_member.cc new file mode 100644 index 0000000000..8ee5e7d4ec --- /dev/null +++ b/src/components/prefs/pref_member.cc @@ -0,0 +1,232 @@ +// Copyright (c) 2012 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 "components/prefs/pref_member.h" + +#include + +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/json/values_util.h" +#include "base/location.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/values.h" +#include "components/prefs/pref_service.h" + +using base::SequencedTaskRunner; + +namespace subtle { + +PrefMemberBase::PrefMemberBase() : prefs_(nullptr), setting_value_(false) {} + +PrefMemberBase::~PrefMemberBase() { + Destroy(); +} + +void PrefMemberBase::Init(const std::string& pref_name, + PrefService* prefs, + const NamedChangeCallback& observer) { + observer_ = observer; + Init(pref_name, prefs); +} + +void PrefMemberBase::Init(const std::string& pref_name, PrefService* prefs) { + DCHECK(prefs); + DCHECK(pref_name_.empty()); // Check that Init is only called once. + prefs_ = prefs; + pref_name_ = pref_name; + // Check that the preference is registered. + DCHECK(prefs_->FindPreference(pref_name_)) << pref_name << " not registered."; + + // Add ourselves as a pref observer so we can keep our local value in sync. + prefs_->AddPrefObserver(pref_name, this); +} + +void PrefMemberBase::Destroy() { + if (prefs_ && !pref_name_.empty()) { + prefs_->RemovePrefObserver(pref_name_, this); + prefs_ = nullptr; + } +} + +void PrefMemberBase::MoveToSequence( + scoped_refptr task_runner) { + VerifyValuePrefName(); + // Load the value from preferences if it hasn't been loaded so far. + if (!internal()) + UpdateValueFromPref(base::OnceClosure()); + internal()->MoveToSequence(std::move(task_runner)); +} + +void PrefMemberBase::OnPreferenceChanged(PrefService* service, + const std::string& pref_name) { + VerifyValuePrefName(); + UpdateValueFromPref((!setting_value_ && !observer_.is_null()) + ? base::BindOnce(observer_, pref_name) + : base::OnceClosure()); +} + +void PrefMemberBase::UpdateValueFromPref(base::OnceClosure callback) const { + VerifyValuePrefName(); + const PrefService::Preference* pref = prefs_->FindPreference(pref_name_); + DCHECK(pref); + if (!internal()) + CreateInternal(); + internal()->UpdateValue( + base::Value::ToUniquePtrValue(pref->GetValue()->Clone()).release(), + pref->IsManaged(), pref->IsUserModifiable(), pref->IsDefaultValue(), + std::move(callback)); +} + +void PrefMemberBase::VerifyPref() const { + VerifyValuePrefName(); + if (!internal()) + UpdateValueFromPref(base::OnceClosure()); +} + +void PrefMemberBase::InvokeUnnamedCallback( + const base::RepeatingClosure& callback, + const std::string& pref_name) { + callback.Run(); +} + +PrefMemberBase::Internal::Internal() + : owning_task_runner_(base::SequencedTaskRunnerHandle::Get()) {} +PrefMemberBase::Internal::~Internal() = default; + +bool PrefMemberBase::Internal::IsOnCorrectSequence() const { + return owning_task_runner_->RunsTasksInCurrentSequence(); +} + +void PrefMemberBase::Internal::UpdateValue(base::Value* v, + bool is_managed, + bool is_user_modifiable, + bool is_default_value, + base::OnceClosure callback) const { + std::unique_ptr value(v); + base::ScopedClosureRunner closure_runner(std::move(callback)); + if (IsOnCorrectSequence()) { + bool rv = UpdateValueInternal(*value); + DCHECK(rv); + is_managed_ = is_managed; + is_user_modifiable_ = is_user_modifiable; + is_default_value_ = is_default_value; + } else { + bool may_run = owning_task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&PrefMemberBase::Internal::UpdateValue, this, + value.release(), is_managed, is_user_modifiable, + is_default_value, closure_runner.Release())); + DCHECK(may_run); + } +} + +void PrefMemberBase::Internal::MoveToSequence( + scoped_refptr task_runner) { + CheckOnCorrectSequence(); + owning_task_runner_ = std::move(task_runner); +} + +bool PrefMemberVectorStringUpdate(const base::Value& value, + std::vector* string_vector) { + if (!value.is_list()) + return false; + + std::vector local_vector; + for (const auto& item : value.GetListDeprecated()) { + if (!item.is_string()) + return false; + local_vector.push_back(item.GetString()); + } + + string_vector->swap(local_vector); + return true; +} + +} // namespace subtle + +template <> +void PrefMember::UpdatePref(const bool& value) { + prefs()->SetBoolean(pref_name(), value); +} + +template <> +bool PrefMember::Internal::UpdateValueInternal( + const base::Value& value) const { + if (value.is_bool()) + value_ = value.GetBool(); + return value.is_bool(); +} + +template <> +void PrefMember::UpdatePref(const int& value) { + prefs()->SetInteger(pref_name(), value); +} + +template <> +bool PrefMember::Internal::UpdateValueInternal( + const base::Value& value) const { + if (value.is_int()) + value_ = value.GetInt(); + return value.is_int(); +} + +template <> +void PrefMember::UpdatePref(const double& value) { + prefs()->SetDouble(pref_name(), value); +} + +template <> +bool PrefMember::Internal::UpdateValueInternal(const base::Value& value) + const { + if (value.is_double() || value.is_int()) + value_ = value.GetDouble(); + return value.is_double() || value.is_int(); +} + +template <> +void PrefMember::UpdatePref(const std::string& value) { + prefs()->SetString(pref_name(), value); +} + +template <> +bool PrefMember::Internal::UpdateValueInternal( + const base::Value& value) + const { + if (value.is_string()) + value_ = value.GetString(); + return value.is_string(); +} + +template <> +void PrefMember::UpdatePref(const base::FilePath& value) { + prefs()->SetFilePath(pref_name(), value); +} + +template <> +bool PrefMember::Internal::UpdateValueInternal( + const base::Value& value) + const { + absl::optional path = base::ValueToFilePath(value); + if (!path) + return false; + value_ = *path; + return true; +} + +template <> +void PrefMember >::UpdatePref( + const std::vector& value) { + base::ListValue list_value; + for (const std::string& val : value) + list_value.Append(val); + + prefs()->Set(pref_name(), list_value); +} + +template <> +bool PrefMember >::Internal::UpdateValueInternal( + const base::Value& value) const { + return subtle::PrefMemberVectorStringUpdate(value, &value_); +} diff --git a/src/components/prefs/pref_member.h b/src/components/prefs/pref_member.h new file mode 100644 index 0000000000..5ec70ea9b2 --- /dev/null +++ b/src/components/prefs/pref_member.h @@ -0,0 +1,365 @@ +// Copyright (c) 2012 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. +// +// A helper class that stays in sync with a preference (bool, int, real, +// string or filepath). For example: +// +// class MyClass { +// public: +// MyClass(PrefService* prefs) { +// my_string_.Init(prefs::kHomePage, prefs); +// } +// private: +// StringPrefMember my_string_; +// }; +// +// my_string_ should stay in sync with the prefs::kHomePage pref and will +// update if either the pref changes or if my_string_.SetValue is called. +// +// An optional observer can be passed into the Init method which can be used to +// notify MyClass of changes. Note that if you use SetValue(), the observer +// will not be notified. + +#ifndef COMPONENTS_PREFS_PREF_MEMBER_H_ +#define COMPONENTS_PREFS_PREF_MEMBER_H_ + +#include +#include + +#include "base/bind.h" +#include "base/callback_forward.h" +#include "base/check.h" +#include "base/files/file_path.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/task/sequenced_task_runner.h" +#include "base/values.h" +#include "components/prefs/pref_observer.h" +#include "components/prefs/prefs_export.h" + +class PrefService; + +namespace subtle { + +class COMPONENTS_PREFS_EXPORT PrefMemberBase : public PrefObserver { + public: + // Type of callback you can register if you need to know the name of + // the pref that is changing. + using NamedChangeCallback = base::RepeatingCallback; + + PrefService* prefs() { return prefs_; } + const PrefService* prefs() const { return prefs_; } + + protected: + class COMPONENTS_PREFS_EXPORT Internal + : public base::RefCountedThreadSafe { + public: + Internal(); + + Internal(const Internal&) = delete; + Internal& operator=(const Internal&) = delete; + + // Update the value, either by calling |UpdateValueInternal| directly + // or by dispatching to the right sequence. + // Takes ownership of |value|. + void UpdateValue(base::Value* value, + bool is_managed, + bool is_user_modifiable, + bool is_default_value, + base::OnceClosure callback) const; + + void MoveToSequence(scoped_refptr task_runner); + + // See PrefMember<> for description. + bool IsManaged() const { return is_managed_; } + bool IsUserModifiable() const { return is_user_modifiable_; } + bool IsDefaultValue() const { return is_default_value_; } + + protected: + friend class base::RefCountedThreadSafe; + virtual ~Internal(); + + void CheckOnCorrectSequence() const { DCHECK(IsOnCorrectSequence()); } + + private: + // This method actually updates the value. It should only be called from + // the sequence the PrefMember is on. + virtual bool UpdateValueInternal(const base::Value& value) const = 0; + + bool IsOnCorrectSequence() const; + + scoped_refptr owning_task_runner_; + mutable bool is_managed_ = false; + mutable bool is_user_modifiable_ = false; + mutable bool is_default_value_ = false; + }; + + PrefMemberBase(); + virtual ~PrefMemberBase(); + + // See PrefMember<> for description. + void Init(const std::string& pref_name, + PrefService* prefs, + const NamedChangeCallback& observer); + void Init(const std::string& pref_name, PrefService* prefs); + + virtual void CreateInternal() const = 0; + + // See PrefMember<> for description. + void Destroy(); + + void MoveToSequence(scoped_refptr task_runner); + + // PrefObserver + void OnPreferenceChanged(PrefService* service, + const std::string& pref_name) override; + + void VerifyValuePrefName() const { + DCHECK(!pref_name_.empty()); + } + + // This method is used to do the actual sync with the preference. + // Note: it is logically const, because it doesn't modify the state + // seen by the outside world. It is just doing a lazy load behind the scenes. + void UpdateValueFromPref(base::OnceClosure callback) const; + + // Verifies the preference name, and lazily loads the preference value if + // it hasn't been loaded yet. + void VerifyPref() const; + + const std::string& pref_name() const { return pref_name_; } + + virtual Internal* internal() const = 0; + + // Used to allow registering plain base::RepeatingClosure callbacks. + static void InvokeUnnamedCallback(const base::RepeatingClosure& callback, + const std::string& pref_name); + + private: + // Ordered the members to compact the class instance. + std::string pref_name_; + NamedChangeCallback observer_; + raw_ptr prefs_; + + protected: + bool setting_value_; +}; + +// This function implements StringListPrefMember::UpdateValue(). +// It is exposed here for testing purposes. +bool COMPONENTS_PREFS_EXPORT PrefMemberVectorStringUpdate( + const base::Value& value, + std::vector* string_vector); + +} // namespace subtle + +template +class PrefMember : public subtle::PrefMemberBase { + public: + // Defer initialization to an Init method so it's easy to make this class be + // a member variable. + PrefMember() {} + + PrefMember(const PrefMember&) = delete; + PrefMember& operator=(const PrefMember&) = delete; + + virtual ~PrefMember() {} + + // Do the actual initialization of the class. Use the two-parameter + // version if you don't want any notifications of changes. This + // method should only be called on the UI thread. + void Init(const std::string& pref_name, + PrefService* prefs, + const NamedChangeCallback& observer) { + subtle::PrefMemberBase::Init(pref_name, prefs, observer); + } + void Init(const std::string& pref_name, + PrefService* prefs, + const base::RepeatingClosure& observer) { + subtle::PrefMemberBase::Init( + pref_name, prefs, + base::BindRepeating(&PrefMemberBase::InvokeUnnamedCallback, observer)); + } + void Init(const std::string& pref_name, PrefService* prefs) { + subtle::PrefMemberBase::Init(pref_name, prefs); + } + + // Unsubscribes the PrefMember from the PrefService. After calling this + // function, the PrefMember may not be used any more on the UI thread. + // Assuming |MoveToSequence| was previously called, |GetValue|, |IsManaged|, + // and |IsUserModifiable| can still be called from the other sequence but + // the results will no longer update from the PrefService. + // This method should only be called on the UI thread. + void Destroy() { + subtle::PrefMemberBase::Destroy(); + } + + // Moves the PrefMember to another sequence, allowing read accesses from + // there. Changes from the PrefService will be propagated asynchronously + // via PostTask. + // This method should only be used from the sequence the PrefMember is + // currently on, which is the UI thread by default. + void MoveToSequence(scoped_refptr task_runner) { + subtle::PrefMemberBase::MoveToSequence(task_runner); + } + + // Check whether the pref is managed, i.e. controlled externally through + // enterprise configuration management (e.g. windows group policy). Returns + // false for unknown prefs. + // This method should only be used from the sequence the PrefMember is + // currently on, which is the UI thread unless changed by |MoveToSequence|. + bool IsManaged() const { + VerifyPref(); + return internal_->IsManaged(); + } + + // Checks whether the pref can be modified by the user. This returns false + // when the pref is managed by a policy or an extension, and when a command + // line flag overrides the pref. + // This method should only be used from the sequence the PrefMember is + // currently on, which is the UI thread unless changed by |MoveToSequence|. + bool IsUserModifiable() const { + VerifyPref(); + return internal_->IsUserModifiable(); + } + + // Checks whether the pref is currently using its default value, and has not + // been set by any higher-priority source (even with the same value). This + // method should only be used from the sequence the PrefMember is currently + // on, which is the UI thread unless changed by |MoveToSequence|. + bool IsDefaultValue() const { + VerifyPref(); + return internal_->IsDefaultValue(); + } + + // Retrieve the value of the member variable. + // This method should only be used from the sequence the PrefMember is + // currently on, which is the UI thread unless changed by |MoveToSequence|. + ValueType GetValue() const { + VerifyPref(); + return internal_->value(); + } + + // Provided as a convenience. + ValueType operator*() const { + return GetValue(); + } + + // Set the value of the member variable. + // This method should only be called on the UI thread. + void SetValue(const ValueType& value) { + VerifyValuePrefName(); + setting_value_ = true; + UpdatePref(value); + setting_value_ = false; + } + + // Returns the pref name. + const std::string& GetPrefName() const { + return pref_name(); + } + + private: + class Internal : public subtle::PrefMemberBase::Internal { + public: + Internal() : value_(ValueType()) {} + + Internal(const Internal&) = delete; + Internal& operator=(const Internal&) = delete; + + ValueType value() { + CheckOnCorrectSequence(); + return value_; + } + + protected: + ~Internal() override {} + + COMPONENTS_PREFS_EXPORT bool UpdateValueInternal( + const base::Value& value) const override; + + // We cache the value of the pref so we don't have to keep walking the pref + // tree. + mutable ValueType value_; + }; + + Internal* internal() const override { return internal_.get(); } + void CreateInternal() const override { internal_ = new Internal(); } + + // This method is used to do the actual sync with pref of the specified type. + void COMPONENTS_PREFS_EXPORT UpdatePref(const ValueType& value); + + mutable scoped_refptr internal_; +}; + +// Declaration of template specialization need to be repeated here +// specifically for each specialization (rather than just once above) +// or at least one of our compilers won't be happy in all cases. +// Specifically, it was failing on ChromeOS with a complaint about +// PrefMember::UpdateValueInternal not being defined when +// built in a chroot with the following parameters: +// +// FEATURES="noclean nostrip" USE="-chrome_debug -chrome_remoting +// -chrome_internal -chrome_pdf component_build" +// ~/trunk/goma/goma-wrapper cros_chrome_make --board=${BOARD} +// --install --runhooks + +template <> +COMPONENTS_PREFS_EXPORT void PrefMember::UpdatePref(const bool& value); + +template <> +COMPONENTS_PREFS_EXPORT bool PrefMember::Internal::UpdateValueInternal( + const base::Value& value) const; + +template <> +COMPONENTS_PREFS_EXPORT void PrefMember::UpdatePref(const int& value); + +template <> +COMPONENTS_PREFS_EXPORT bool PrefMember::Internal::UpdateValueInternal( + const base::Value& value) const; + +template <> +COMPONENTS_PREFS_EXPORT void +PrefMember::UpdatePref(const double& value); + +template <> +COMPONENTS_PREFS_EXPORT bool PrefMember::Internal::UpdateValueInternal( + const base::Value& value) const; + +template <> +COMPONENTS_PREFS_EXPORT void PrefMember::UpdatePref( + const std::string& value); + +template <> +COMPONENTS_PREFS_EXPORT bool +PrefMember::Internal::UpdateValueInternal( + const base::Value& value) const; + +template <> +COMPONENTS_PREFS_EXPORT void PrefMember::UpdatePref( + const base::FilePath& value); + +template <> +COMPONENTS_PREFS_EXPORT bool +PrefMember::Internal::UpdateValueInternal( + const base::Value& value) const; + +template <> +COMPONENTS_PREFS_EXPORT void PrefMember>::UpdatePref( + const std::vector& value); + +template <> +COMPONENTS_PREFS_EXPORT bool +PrefMember>::Internal::UpdateValueInternal( + const base::Value& value) const; + +typedef PrefMember BooleanPrefMember; +typedef PrefMember IntegerPrefMember; +typedef PrefMember DoublePrefMember; +typedef PrefMember StringPrefMember; +typedef PrefMember FilePathPrefMember; +// This preference member is expensive for large string arrays. +typedef PrefMember> StringListPrefMember; + +#endif // COMPONENTS_PREFS_PREF_MEMBER_H_ diff --git a/src/components/prefs/pref_member_unittest.cc b/src/components/prefs/pref_member_unittest.cc new file mode 100644 index 0000000000..ca1bbe35f5 --- /dev/null +++ b/src/components/prefs/pref_member_unittest.cc @@ -0,0 +1,352 @@ +// Copyright (c) 2011 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 "components/prefs/pref_member.h" + +#include + +#include "base/bind.h" +#include "base/location.h" +#include "base/memory/raw_ptr.h" +#include "base/synchronization/waitable_event.h" +#include "base/task/post_task.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/thread_pool.h" +#include "base/test/task_environment.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/testing_pref_service.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char kBoolPref[] = "bool"; +const char kIntPref[] = "int"; +const char kDoublePref[] = "double"; +const char kStringPref[] = "string"; +const char kStringListPref[] = "string_list"; + +void RegisterTestPrefs(PrefRegistrySimple* registry) { + registry->RegisterBooleanPref(kBoolPref, false); + registry->RegisterIntegerPref(kIntPref, 0); + registry->RegisterDoublePref(kDoublePref, 0.0); + registry->RegisterStringPref(kStringPref, "default"); + registry->RegisterListPref(kStringListPref); +} + +class GetPrefValueHelper + : public base::RefCountedThreadSafe { + public: + GetPrefValueHelper() + : value_(false), + task_runner_(base::ThreadPool::CreateSequencedTaskRunner({})) {} + + void Init(const std::string& pref_name, PrefService* prefs) { + pref_.Init(pref_name, prefs); + pref_.MoveToSequence(task_runner_); + } + + void Destroy() { + pref_.Destroy(); + } + + void FetchValue() { + base::WaitableEvent event(base::WaitableEvent::ResetPolicy::MANUAL, + base::WaitableEvent::InitialState::NOT_SIGNALED); + ASSERT_TRUE(task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&GetPrefValueHelper::GetPrefValue, this, &event))); + event.Wait(); + } + + bool value() { return value_; } + + private: + friend class base::RefCountedThreadSafe; + ~GetPrefValueHelper() {} + + void GetPrefValue(base::WaitableEvent* event) { + value_ = pref_.GetValue(); + event->Signal(); + } + + BooleanPrefMember pref_; + bool value_; + + // The sequence |pref_| runs on. + scoped_refptr task_runner_; +}; + +class PrefMemberTestClass { + public: + explicit PrefMemberTestClass(PrefService* prefs) + : observe_cnt_(0), prefs_(prefs) { + str_.Init(kStringPref, prefs, + base::BindRepeating(&PrefMemberTestClass::OnPreferenceChanged, + base::Unretained(this))); + } + + void OnPreferenceChanged(const std::string& pref_name) { + EXPECT_EQ(pref_name, kStringPref); + EXPECT_EQ(str_.GetValue(), prefs_->GetString(kStringPref)); + EXPECT_EQ(str_.IsDefaultValue(), + prefs_->FindPreference(kStringPref)->IsDefaultValue()); + ++observe_cnt_; + } + + StringPrefMember str_; + int observe_cnt_; + + private: + raw_ptr prefs_; +}; + +} // anonymous namespace + +class PrefMemberTest : public testing::Test { + base::test::TaskEnvironment task_environment_; +}; + +TEST_F(PrefMemberTest, BasicGetAndSet) { + TestingPrefServiceSimple prefs; + RegisterTestPrefs(prefs.registry()); + + // Test bool + BooleanPrefMember boolean; + boolean.Init(kBoolPref, &prefs); + + // Check the defaults + EXPECT_FALSE(prefs.GetBoolean(kBoolPref)); + EXPECT_FALSE(boolean.GetValue()); + EXPECT_FALSE(*boolean); + EXPECT_TRUE(boolean.IsDefaultValue()); + + // Try changing through the member variable. + boolean.SetValue(true); + EXPECT_TRUE(boolean.GetValue()); + EXPECT_TRUE(prefs.GetBoolean(kBoolPref)); + EXPECT_TRUE(*boolean); + EXPECT_FALSE(boolean.IsDefaultValue()); + + // Try changing back through the pref. + prefs.SetBoolean(kBoolPref, false); + EXPECT_FALSE(prefs.GetBoolean(kBoolPref)); + EXPECT_FALSE(boolean.GetValue()); + EXPECT_FALSE(*boolean); + EXPECT_FALSE(boolean.IsDefaultValue()); + + // Test int + IntegerPrefMember integer; + integer.Init(kIntPref, &prefs); + + // Check the defaults + EXPECT_EQ(0, prefs.GetInteger(kIntPref)); + EXPECT_EQ(0, integer.GetValue()); + EXPECT_EQ(0, *integer); + EXPECT_TRUE(integer.IsDefaultValue()); + + // Try changing through the member variable. + integer.SetValue(5); + EXPECT_EQ(5, integer.GetValue()); + EXPECT_EQ(5, prefs.GetInteger(kIntPref)); + EXPECT_EQ(5, *integer); + EXPECT_FALSE(integer.IsDefaultValue()); + + // Try changing back through the pref. + prefs.SetInteger(kIntPref, 2); + EXPECT_EQ(2, prefs.GetInteger(kIntPref)); + EXPECT_EQ(2, integer.GetValue()); + EXPECT_EQ(2, *integer); + EXPECT_FALSE(integer.IsDefaultValue()); + + // Test double + DoublePrefMember double_member; + double_member.Init(kDoublePref, &prefs); + + // Check the defaults + EXPECT_EQ(0.0, prefs.GetDouble(kDoublePref)); + EXPECT_EQ(0.0, double_member.GetValue()); + EXPECT_EQ(0.0, *double_member); + EXPECT_TRUE(double_member.IsDefaultValue()); + + // Try changing through the member variable. + double_member.SetValue(1.0); + EXPECT_EQ(1.0, double_member.GetValue()); + EXPECT_EQ(1.0, prefs.GetDouble(kDoublePref)); + EXPECT_EQ(1.0, *double_member); + EXPECT_FALSE(double_member.IsDefaultValue()); + + // Try changing back through the pref. + prefs.SetDouble(kDoublePref, 3.0); + EXPECT_EQ(3.0, prefs.GetDouble(kDoublePref)); + EXPECT_EQ(3.0, double_member.GetValue()); + EXPECT_EQ(3.0, *double_member); + EXPECT_FALSE(double_member.IsDefaultValue()); + + // Test string + StringPrefMember string; + string.Init(kStringPref, &prefs); + + // Check the defaults + EXPECT_EQ("default", prefs.GetString(kStringPref)); + EXPECT_EQ("default", string.GetValue()); + EXPECT_EQ("default", *string); + EXPECT_TRUE(string.IsDefaultValue()); + + // Try changing through the member variable. + string.SetValue("foo"); + EXPECT_EQ("foo", string.GetValue()); + EXPECT_EQ("foo", prefs.GetString(kStringPref)); + EXPECT_EQ("foo", *string); + EXPECT_FALSE(string.IsDefaultValue()); + + // Try changing back through the pref. + prefs.SetString(kStringPref, "bar"); + EXPECT_EQ("bar", prefs.GetString(kStringPref)); + EXPECT_EQ("bar", string.GetValue()); + EXPECT_EQ("bar", *string); + EXPECT_FALSE(string.IsDefaultValue()); + + // Test string list + base::Value expected_list(base::Value::Type::LIST); + std::vector expected_vector; + StringListPrefMember string_list; + string_list.Init(kStringListPref, &prefs); + + // Check the defaults + EXPECT_EQ(expected_list, *prefs.GetList(kStringListPref)); + EXPECT_EQ(expected_vector, string_list.GetValue()); + EXPECT_EQ(expected_vector, *string_list); + EXPECT_TRUE(string_list.IsDefaultValue()); + + // Try changing through the pref member. + expected_list.Append("foo"); + expected_vector.push_back("foo"); + string_list.SetValue(expected_vector); + + EXPECT_EQ(expected_list, *prefs.GetList(kStringListPref)); + EXPECT_EQ(expected_vector, string_list.GetValue()); + EXPECT_EQ(expected_vector, *string_list); + EXPECT_FALSE(string_list.IsDefaultValue()); + + // Try adding through the pref. + expected_list.Append("bar"); + expected_vector.push_back("bar"); + prefs.Set(kStringListPref, expected_list); + + EXPECT_EQ(expected_list, *prefs.GetList(kStringListPref)); + EXPECT_EQ(expected_vector, string_list.GetValue()); + EXPECT_EQ(expected_vector, *string_list); + EXPECT_FALSE(string_list.IsDefaultValue()); + + // Try removing through the pref. + EXPECT_TRUE( + expected_list.EraseListIter(expected_list.GetListDeprecated().begin())); + expected_vector.erase(expected_vector.begin()); + prefs.Set(kStringListPref, expected_list); + + EXPECT_EQ(expected_list, *prefs.GetList(kStringListPref)); + EXPECT_EQ(expected_vector, string_list.GetValue()); + EXPECT_EQ(expected_vector, *string_list); + EXPECT_FALSE(string_list.IsDefaultValue()); +} + +TEST_F(PrefMemberTest, InvalidList) { + // Set the vector to an initial good value. + std::vector expected_vector; + expected_vector.push_back("foo"); + + // Try to add a valid list first. + base::Value list(base::Value::Type::LIST); + list.Append("foo"); + std::vector vector; + EXPECT_TRUE(subtle::PrefMemberVectorStringUpdate(list, &vector)); + EXPECT_EQ(expected_vector, vector); + + // Now try to add an invalid list. |vector| should not be changed. + list.Append(0); + EXPECT_FALSE(subtle::PrefMemberVectorStringUpdate(list, &vector)); + EXPECT_EQ(expected_vector, vector); +} + +TEST_F(PrefMemberTest, TwoPrefs) { + // Make sure two DoublePrefMembers stay in sync. + TestingPrefServiceSimple prefs; + RegisterTestPrefs(prefs.registry()); + + DoublePrefMember pref1; + pref1.Init(kDoublePref, &prefs); + DoublePrefMember pref2; + pref2.Init(kDoublePref, &prefs); + + pref1.SetValue(2.3); + EXPECT_EQ(2.3, *pref2); + + pref2.SetValue(3.5); + EXPECT_EQ(3.5, *pref1); + + prefs.SetDouble(kDoublePref, 4.2); + EXPECT_EQ(4.2, *pref1); + EXPECT_EQ(4.2, *pref2); +} + +TEST_F(PrefMemberTest, Observer) { + TestingPrefServiceSimple prefs; + RegisterTestPrefs(prefs.registry()); + + PrefMemberTestClass test_obj(&prefs); + EXPECT_EQ("default", *test_obj.str_); + EXPECT_TRUE(test_obj.str_.IsDefaultValue()); + + // Changing the pref from the default value to an explicitly-set version of + // the same value fires the observer. The caller may be sensitive to + // IsDefaultValue(). + prefs.SetString(kStringPref, "default"); + EXPECT_EQ("default", *test_obj.str_); + EXPECT_EQ(1, test_obj.observe_cnt_); + EXPECT_FALSE(test_obj.str_.IsDefaultValue()); + + // Calling SetValue should not fire the observer. + test_obj.str_.SetValue("hello"); + EXPECT_EQ(1, test_obj.observe_cnt_); + EXPECT_EQ("hello", prefs.GetString(kStringPref)); + + // Changing the pref does fire the observer. + prefs.SetString(kStringPref, "world"); + EXPECT_EQ(2, test_obj.observe_cnt_); + EXPECT_EQ("world", *(test_obj.str_)); + + // Not changing the value should not fire the observer. + prefs.SetString(kStringPref, "world"); + EXPECT_EQ(2, test_obj.observe_cnt_); + EXPECT_EQ("world", *(test_obj.str_)); + + prefs.SetString(kStringPref, "hello"); + EXPECT_EQ(3, test_obj.observe_cnt_); + EXPECT_EQ("hello", prefs.GetString(kStringPref)); +} + +TEST_F(PrefMemberTest, NoInit) { + // Make sure not calling Init on a PrefMember doesn't cause problems. + IntegerPrefMember pref; +} + +TEST_F(PrefMemberTest, MoveToSequence) { + TestingPrefServiceSimple prefs; + scoped_refptr helper(new GetPrefValueHelper()); + RegisterTestPrefs(prefs.registry()); + helper->Init(kBoolPref, &prefs); + + helper->FetchValue(); + EXPECT_FALSE(helper->value()); + + prefs.SetBoolean(kBoolPref, true); + + helper->FetchValue(); + EXPECT_TRUE(helper->value()); + + helper->Destroy(); + + helper->FetchValue(); + EXPECT_TRUE(helper->value()); +} diff --git a/src/components/prefs/pref_notifier.h b/src/components/prefs/pref_notifier.h new file mode 100644 index 0000000000..2abc213cff --- /dev/null +++ b/src/components/prefs/pref_notifier.h @@ -0,0 +1,26 @@ +// Copyright (c) 2011 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_PREFS_PREF_NOTIFIER_H_ +#define COMPONENTS_PREFS_PREF_NOTIFIER_H_ + +#include + +// Delegate interface used by PrefValueStore to notify its owner about changes +// to the preference values. +// TODO(mnissler, danno): Move this declaration to pref_value_store.h once we've +// cleaned up all public uses of this interface. +class PrefNotifier { + public: + virtual ~PrefNotifier() {} + + // Sends out a change notification for the preference identified by + // |pref_name|. + virtual void OnPreferenceChanged(const std::string& pref_name) = 0; + + // Broadcasts the intialization completed notification. + virtual void OnInitializationCompleted(bool succeeded) = 0; +}; + +#endif // COMPONENTS_PREFS_PREF_NOTIFIER_H_ diff --git a/src/components/prefs/pref_notifier_impl.cc b/src/components/prefs/pref_notifier_impl.cc new file mode 100644 index 0000000000..a6001cacd4 --- /dev/null +++ b/src/components/prefs/pref_notifier_impl.cc @@ -0,0 +1,150 @@ +// Copyright (c) 2012 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 "components/prefs/pref_notifier_impl.h" + +#include "base/debug/alias.h" +#include "base/debug/dump_without_crashing.h" +#include "base/logging.h" +#include "base/memory/ptr_util.h" +#include "base/strings/strcat.h" +#include "components/prefs/pref_service.h" + +PrefNotifierImpl::PrefNotifierImpl() : pref_service_(nullptr) {} + +PrefNotifierImpl::PrefNotifierImpl(PrefService* service) + : pref_service_(service) { +} + +PrefNotifierImpl::~PrefNotifierImpl() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // Verify that there are no pref observers when we shut down. + for (const auto& observer_list : pref_observers_) { + if (observer_list.second->begin() != observer_list.second->end()) { + // Generally, there should not be any subscribers left when the profile + // is destroyed because a) those may indicate that the subscriber class + // maintains an active pointer to the profile that might be used for + // accessing a destroyed profile and b) those subscribers will try to + // unsubscribe from a PrefService that has been destroyed with the + // profile. + // There is one exception that is safe: Static objects that are leaked + // on process termination, if these objects just subscribe to preferences + // and never access the profile after destruction. As these objects are + // leaked on termination, it is guaranteed that they don't attempt to + // unsubscribe. + const auto& pref_name = observer_list.first; + std::string message = base::StrCat( + {"Pref observer for ", pref_name, " found at shutdown."}); + LOG(WARNING) << message; + DEBUG_ALIAS_FOR_CSTR(aliased_message, message.c_str(), 128); + + // TODO(crbug.com/942491, 946668, 945772) The following code collects + // stacktraces that show how the profile is destroyed that owns + // preferences which are known to have subscriptions outliving the + // profile. + if ( + // For DbusAppmenu, crbug.com/946668 + pref_name == "bookmark_bar.show_on_all_tabs" || + // For BrowserWindowPropertyManager, crbug.com/942491 + pref_name == "profile.icon_version") { + base::debug::DumpWithoutCrashing(); + } + } + } + + // Same for initialization observers. + if (!init_observers_.empty()) + LOG(WARNING) << "Init observer found at shutdown."; + + pref_observers_.clear(); + init_observers_.clear(); +} + +void PrefNotifierImpl::AddPrefObserver(const std::string& path, + PrefObserver* obs) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // Get the pref observer list associated with the path. + PrefObserverList* observer_list = nullptr; + auto observer_iterator = pref_observers_.find(path); + if (observer_iterator == pref_observers_.end()) { + observer_list = new PrefObserverList; + pref_observers_[path] = base::WrapUnique(observer_list); + } else { + observer_list = observer_iterator->second.get(); + } + + // Add the pref observer. ObserverList will DCHECK if it already is + // in the list. + observer_list->AddObserver(obs); +} + +void PrefNotifierImpl::RemovePrefObserver(const std::string& path, + PrefObserver* obs) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + auto observer_iterator = pref_observers_.find(path); + if (observer_iterator == pref_observers_.end()) { + return; + } + + PrefObserverList* observer_list = observer_iterator->second.get(); + observer_list->RemoveObserver(obs); +} + +void PrefNotifierImpl::AddPrefObserverAllPrefs(PrefObserver* observer) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + all_prefs_pref_observers_.AddObserver(observer); +} + +void PrefNotifierImpl::RemovePrefObserverAllPrefs(PrefObserver* observer) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + all_prefs_pref_observers_.RemoveObserver(observer); +} + +void PrefNotifierImpl::AddInitObserver(base::OnceCallback obs) { + init_observers_.push_back(std::move(obs)); +} + +void PrefNotifierImpl::OnPreferenceChanged(const std::string& path) { + FireObservers(path); +} + +void PrefNotifierImpl::OnInitializationCompleted(bool succeeded) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // We must move init_observers_ to a local variable before we run + // observers, or we can end up in this method re-entrantly before + // clearing the observers list. + PrefInitObserverList observers; + std::swap(observers, init_observers_); + + for (auto& observer : observers) + std::move(observer).Run(succeeded); +} + +void PrefNotifierImpl::FireObservers(const std::string& path) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // Only send notifications for registered preferences. + if (!pref_service_->FindPreference(path)) + return; + + // Fire observers for any preference change. + for (auto& observer : all_prefs_pref_observers_) + observer.OnPreferenceChanged(pref_service_, path); + + auto observer_iterator = pref_observers_.find(path); + if (observer_iterator == pref_observers_.end()) + return; + + for (PrefObserver& observer : *(observer_iterator->second)) + observer.OnPreferenceChanged(pref_service_, path); +} + +void PrefNotifierImpl::SetPrefService(PrefService* pref_service) { + DCHECK(pref_service_ == nullptr); + pref_service_ = pref_service; +} diff --git a/src/components/prefs/pref_notifier_impl.h b/src/components/prefs/pref_notifier_impl.h new file mode 100644 index 0000000000..182dc14846 --- /dev/null +++ b/src/components/prefs/pref_notifier_impl.h @@ -0,0 +1,89 @@ +// Copyright (c) 2011 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_PREFS_PREF_NOTIFIER_IMPL_H_ +#define COMPONENTS_PREFS_PREF_NOTIFIER_IMPL_H_ + +#include +#include +#include +#include + +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/memory/raw_ptr.h" +#include "base/observer_list.h" +#include "base/sequence_checker.h" +#include "components/prefs/pref_notifier.h" +#include "components/prefs/pref_observer.h" +#include "components/prefs/prefs_export.h" + +class PrefService; + +// The PrefNotifier implementation used by the PrefService. +class COMPONENTS_PREFS_EXPORT PrefNotifierImpl : public PrefNotifier { + public: + PrefNotifierImpl(); + explicit PrefNotifierImpl(PrefService* pref_service); + + PrefNotifierImpl(const PrefNotifierImpl&) = delete; + PrefNotifierImpl& operator=(const PrefNotifierImpl&) = delete; + + ~PrefNotifierImpl() override; + + // If the pref at the given path changes, we call the observer's + // OnPreferenceChanged method. + void AddPrefObserver(const std::string& path, PrefObserver* observer); + void RemovePrefObserver(const std::string& path, PrefObserver* observer); + + // These observers are called for any pref changes. + // + // AVOID ADDING THESE. See the long comment in the identically-named + // functions on PrefService for background. + void AddPrefObserverAllPrefs(PrefObserver* observer); + void RemovePrefObserverAllPrefs(PrefObserver* observer); + + // We run the callback once, when initialization completes. The bool + // parameter will be set to true for successful initialization, + // false for unsuccessful. + void AddInitObserver(base::OnceCallback observer); + + void SetPrefService(PrefService* pref_service); + + // PrefNotifier overrides. + void OnPreferenceChanged(const std::string& pref_name) override; + + protected: + // PrefNotifier overrides. + void OnInitializationCompleted(bool succeeded) override; + + // A map from pref names to a list of observers. Observers get fired in the + // order they are added. These should only be accessed externally for unit + // testing. + typedef base::ObserverList::Unchecked PrefObserverList; + typedef std::unordered_map> + PrefObserverMap; + + typedef std::list> PrefInitObserverList; + + const PrefObserverMap* pref_observers() const { return &pref_observers_; } + + private: + // For the given pref_name, fire any observer of the pref. Virtual so it can + // be mocked for unit testing. + virtual void FireObservers(const std::string& path); + + // Weak reference; the notifier is owned by the PrefService. + raw_ptr pref_service_; + + PrefObserverMap pref_observers_; + PrefInitObserverList init_observers_; + + // Observers for changes to any preference. + PrefObserverList all_prefs_pref_observers_; + + SEQUENCE_CHECKER(sequence_checker_); +}; + +#endif // COMPONENTS_PREFS_PREF_NOTIFIER_IMPL_H_ diff --git a/src/components/prefs/pref_notifier_impl_unittest.cc b/src/components/prefs/pref_notifier_impl_unittest.cc new file mode 100644 index 0000000000..ee42891bab --- /dev/null +++ b/src/components/prefs/pref_notifier_impl_unittest.cc @@ -0,0 +1,218 @@ +// Copyright (c) 2011 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 + +#include "base/bind.h" +#include "base/callback.h" +#include "components/prefs/mock_pref_change_callback.h" +#include "components/prefs/pref_notifier_impl.h" +#include "components/prefs/pref_observer.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/pref_value_store.h" +#include "components/prefs/testing_pref_service.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::Field; +using testing::Invoke; +using testing::Mock; +using testing::Truly; + +namespace { + +const char kChangedPref[] = "changed_pref"; +const char kUnchangedPref[] = "unchanged_pref"; + +class MockPrefInitObserver { + public: + MOCK_METHOD1(OnInitializationCompleted, void(bool)); +}; + +// This is an unmodified PrefNotifierImpl, except we make +// OnPreferenceChanged public for tests. +class TestingPrefNotifierImpl : public PrefNotifierImpl { + public: + explicit TestingPrefNotifierImpl(PrefService* service) + : PrefNotifierImpl(service) { + } + + // Make public for tests. + using PrefNotifierImpl::OnPreferenceChanged; +}; + +// Mock PrefNotifier that allows tracking of observers and notifications. +class MockPrefNotifier : public PrefNotifierImpl { + public: + explicit MockPrefNotifier(PrefService* pref_service) + : PrefNotifierImpl(pref_service) {} + ~MockPrefNotifier() override {} + + MOCK_METHOD1(FireObservers, void(const std::string& path)); + + size_t CountObserver(const std::string& path, PrefObserver* obs) { + auto observer_iterator = pref_observers()->find(path); + if (observer_iterator == pref_observers()->end()) + return false; + + size_t count = 0; + for (auto& existing_obs : *observer_iterator->second) { + if (&existing_obs == obs) + count++; + } + + return count; + } + + // Make public for tests below. + using PrefNotifierImpl::OnPreferenceChanged; + using PrefNotifierImpl::OnInitializationCompleted; +}; + +class PrefObserverMock : public PrefObserver { + public: + PrefObserverMock() {} + virtual ~PrefObserverMock() {} + + MOCK_METHOD2(OnPreferenceChanged, void(PrefService*, const std::string&)); +}; + +// Test fixture class. +class PrefNotifierTest : public testing::Test { + protected: + void SetUp() override { + pref_service_.registry()->RegisterBooleanPref(kChangedPref, true); + pref_service_.registry()->RegisterBooleanPref(kUnchangedPref, true); + } + + TestingPrefServiceSimple pref_service_; + + PrefObserverMock obs1_; + PrefObserverMock obs2_; +}; + +TEST_F(PrefNotifierTest, OnPreferenceChanged) { + MockPrefNotifier notifier(&pref_service_); + EXPECT_CALL(notifier, FireObservers(kChangedPref)).Times(1); + notifier.OnPreferenceChanged(kChangedPref); +} + +TEST_F(PrefNotifierTest, OnInitializationCompleted) { + MockPrefNotifier notifier(&pref_service_); + MockPrefInitObserver observer; + notifier.AddInitObserver( + base::BindOnce(&MockPrefInitObserver::OnInitializationCompleted, + base::Unretained(&observer))); + EXPECT_CALL(observer, OnInitializationCompleted(true)); + notifier.OnInitializationCompleted(true); +} + +TEST_F(PrefNotifierTest, AddAndRemovePrefObservers) { + const char pref_name[] = "homepage"; + const char pref_name2[] = "proxy"; + + MockPrefNotifier notifier(&pref_service_); + notifier.AddPrefObserver(pref_name, &obs1_); + ASSERT_EQ(1u, notifier.CountObserver(pref_name, &obs1_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name2, &obs1_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name, &obs2_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name2, &obs2_)); + + // Re-adding the same observer for the same pref doesn't change anything. + // Skip this in debug mode, since it hits a DCHECK and death tests aren't + // thread-safe. +#if defined(NDEBUG) && !defined(DCHECK_ALWAYS_ON) + notifier.AddPrefObserver(pref_name, &obs1_); + ASSERT_EQ(1u, notifier.CountObserver(pref_name, &obs1_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name2, &obs1_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name, &obs2_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name2, &obs2_)); +#endif + + // Ensure that we can add the same observer to a different pref. + notifier.AddPrefObserver(pref_name2, &obs1_); + ASSERT_EQ(1u, notifier.CountObserver(pref_name, &obs1_)); + ASSERT_EQ(1u, notifier.CountObserver(pref_name2, &obs1_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name, &obs2_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name2, &obs2_)); + + // Ensure that we can add another observer to the same pref. + notifier.AddPrefObserver(pref_name, &obs2_); + ASSERT_EQ(1u, notifier.CountObserver(pref_name, &obs1_)); + ASSERT_EQ(1u, notifier.CountObserver(pref_name2, &obs1_)); + ASSERT_EQ(1u, notifier.CountObserver(pref_name, &obs2_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name2, &obs2_)); + + // Ensure that we can remove all observers, and that removing a non-existent + // observer is harmless. + notifier.RemovePrefObserver(pref_name, &obs1_); + ASSERT_EQ(0u, notifier.CountObserver(pref_name, &obs1_)); + ASSERT_EQ(1u, notifier.CountObserver(pref_name2, &obs1_)); + ASSERT_EQ(1u, notifier.CountObserver(pref_name, &obs2_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name2, &obs2_)); + + notifier.RemovePrefObserver(pref_name, &obs2_); + ASSERT_EQ(0u, notifier.CountObserver(pref_name, &obs1_)); + ASSERT_EQ(1u, notifier.CountObserver(pref_name2, &obs1_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name, &obs2_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name2, &obs2_)); + + notifier.RemovePrefObserver(pref_name, &obs1_); + ASSERT_EQ(0u, notifier.CountObserver(pref_name, &obs1_)); + ASSERT_EQ(1u, notifier.CountObserver(pref_name2, &obs1_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name, &obs2_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name2, &obs2_)); + + notifier.RemovePrefObserver(pref_name2, &obs1_); + ASSERT_EQ(0u, notifier.CountObserver(pref_name, &obs1_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name2, &obs1_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name, &obs2_)); + ASSERT_EQ(0u, notifier.CountObserver(pref_name2, &obs2_)); +} + +TEST_F(PrefNotifierTest, FireObservers) { + TestingPrefNotifierImpl notifier(&pref_service_); + notifier.AddPrefObserver(kChangedPref, &obs1_); + notifier.AddPrefObserver(kUnchangedPref, &obs1_); + + EXPECT_CALL(obs1_, OnPreferenceChanged(&pref_service_, kChangedPref)); + EXPECT_CALL(obs2_, OnPreferenceChanged(_, _)).Times(0); + notifier.OnPreferenceChanged(kChangedPref); + Mock::VerifyAndClearExpectations(&obs1_); + Mock::VerifyAndClearExpectations(&obs2_); + + notifier.AddPrefObserver(kChangedPref, &obs2_); + notifier.AddPrefObserver(kUnchangedPref, &obs2_); + + EXPECT_CALL(obs1_, OnPreferenceChanged(&pref_service_, kChangedPref)); + EXPECT_CALL(obs2_, OnPreferenceChanged(&pref_service_, kChangedPref)); + notifier.OnPreferenceChanged(kChangedPref); + Mock::VerifyAndClearExpectations(&obs1_); + Mock::VerifyAndClearExpectations(&obs2_); + + // Make sure removing an observer from one pref doesn't affect anything else. + notifier.RemovePrefObserver(kChangedPref, &obs1_); + + EXPECT_CALL(obs1_, OnPreferenceChanged(_, _)).Times(0); + EXPECT_CALL(obs2_, OnPreferenceChanged(&pref_service_, kChangedPref)); + notifier.OnPreferenceChanged(kChangedPref); + Mock::VerifyAndClearExpectations(&obs1_); + Mock::VerifyAndClearExpectations(&obs2_); + + // Make sure removing an observer entirely doesn't affect anything else. + notifier.RemovePrefObserver(kUnchangedPref, &obs1_); + + EXPECT_CALL(obs1_, OnPreferenceChanged(_, _)).Times(0); + EXPECT_CALL(obs2_, OnPreferenceChanged(&pref_service_, kChangedPref)); + notifier.OnPreferenceChanged(kChangedPref); + Mock::VerifyAndClearExpectations(&obs1_); + Mock::VerifyAndClearExpectations(&obs2_); + + notifier.RemovePrefObserver(kChangedPref, &obs2_); + notifier.RemovePrefObserver(kUnchangedPref, &obs2_); +} + +} // namespace diff --git a/src/components/prefs/pref_observer.h b/src/components/prefs/pref_observer.h new file mode 100644 index 0000000000..3f489318e9 --- /dev/null +++ b/src/components/prefs/pref_observer.h @@ -0,0 +1,21 @@ +// Copyright (c) 2012 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_PREFS_PREF_OBSERVER_H_ +#define COMPONENTS_PREFS_PREF_OBSERVER_H_ + +#include + +class PrefService; + +// Used internally to the Prefs subsystem to pass preference change +// notifications between PrefService, PrefNotifierImpl and +// PrefChangeRegistrar. +class PrefObserver { + public: + virtual void OnPreferenceChanged(PrefService* service, + const std::string& pref_name) = 0; +}; + +#endif // COMPONENTS_PREFS_PREF_OBSERVER_H_ diff --git a/src/components/prefs/pref_registry.cc b/src/components/prefs/pref_registry.cc new file mode 100644 index 0000000000..6761a2e6d5 --- /dev/null +++ b/src/components/prefs/pref_registry.cc @@ -0,0 +1,84 @@ +// Copyright (c) 2012 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 "components/prefs/pref_registry.h" + +#include +#include + +#include "base/check_op.h" +#include "base/containers/contains.h" +#include "base/values.h" +#include "components/prefs/default_pref_store.h" +#include "components/prefs/pref_store.h" + +PrefRegistry::PrefRegistry() + : defaults_(base::MakeRefCounted()) {} + +PrefRegistry::~PrefRegistry() { +} + +uint32_t PrefRegistry::GetRegistrationFlags( + const std::string& pref_name) const { + const auto& it = registration_flags_.find(pref_name); + return it != registration_flags_.end() ? it->second : NO_REGISTRATION_FLAGS; +} + +scoped_refptr PrefRegistry::defaults() { + return defaults_.get(); +} + +PrefRegistry::const_iterator PrefRegistry::begin() const { + return defaults_->begin(); +} + +PrefRegistry::const_iterator PrefRegistry::end() const { + return defaults_->end(); +} + +void PrefRegistry::SetDefaultPrefValue(const std::string& pref_name, + base::Value value) { + const base::Value* current_value = nullptr; + DCHECK(defaults_->GetValue(pref_name, ¤t_value)) + << "Setting default for unregistered pref: " << pref_name; + DCHECK(value.type() == current_value->type()) + << "Wrong type for new default: " << pref_name; + + defaults_->ReplaceDefaultValue(pref_name, std::move(value)); +} + +void PrefRegistry::SetDefaultForeignPrefValue(const std::string& path, + base::Value default_value, + uint32_t flags) { + auto erased = foreign_pref_keys_.erase(path); + DCHECK_EQ(1u, erased); + RegisterPreference(path, std::move(default_value), flags); +} + +void PrefRegistry::RegisterPreference(const std::string& path, + base::Value default_value, + uint32_t flags) { + base::Value::Type orig_type = default_value.type(); + DCHECK(orig_type != base::Value::Type::NONE && + orig_type != base::Value::Type::BINARY) << + "invalid preference type: " << orig_type; + DCHECK(!defaults_->GetValue(path, nullptr)) + << "Trying to register a previously registered pref: " << path; + DCHECK(!base::Contains(registration_flags_, path)) + << "Trying to register a previously registered pref: " << path; + + defaults_->SetDefaultValue(path, std::move(default_value)); + if (flags != NO_REGISTRATION_FLAGS) + registration_flags_[path] = flags; + + OnPrefRegistered(path, flags); +} + +void PrefRegistry::RegisterForeignPref(const std::string& path) { + bool inserted = foreign_pref_keys_.insert(path).second; + DCHECK(inserted); +} + +void PrefRegistry::OnPrefRegistered(const std::string& path, + uint32_t flags) {} diff --git a/src/components/prefs/pref_registry.h b/src/components/prefs/pref_registry.h new file mode 100644 index 0000000000..18f9df3a1a --- /dev/null +++ b/src/components/prefs/pref_registry.h @@ -0,0 +1,114 @@ +// Copyright (c) 2012 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_PREFS_PREF_REGISTRY_H_ +#define COMPONENTS_PREFS_PREF_REGISTRY_H_ + +#include + +#include +#include + +#include "base/memory/ref_counted.h" +#include "components/prefs/pref_value_map.h" +#include "components/prefs/prefs_export.h" + +namespace base { +class Value; +} + +class DefaultPrefStore; +class PrefStore; + +// Preferences need to be registered with a type and default value +// before they are used. +// +// The way you use a PrefRegistry is that you register all required +// preferences on it (via one of its subclasses), then pass it as a +// construction parameter to PrefService. +// +// Currently, registrations after constructing the PrefService will +// also work, but this is being deprecated. +class COMPONENTS_PREFS_EXPORT PrefRegistry + : public base::RefCounted { + public: + // Registration flags that can be specified which impact how the pref will + // behave or be stored. This will be passed in a bitmask when the pref is + // registered. Subclasses of PrefRegistry can specify their own flags. Care + // must be taken to ensure none of these overlap with the flags below. + enum PrefRegistrationFlags : uint32_t { + // No flags are specified. + NO_REGISTRATION_FLAGS = 0, + + // The first 8 bits are reserved for subclasses of PrefRegistry to use. + + // This marks the pref as "lossy". There is no strict time guarantee on when + // a lossy pref will be persisted to permanent storage when it is modified. + LOSSY_PREF = 1 << 8, + + // Registering a pref as public allows other services to access it. + PUBLIC = 1 << 9, + }; + + typedef PrefValueMap::const_iterator const_iterator; + typedef std::unordered_map PrefRegistrationFlagsMap; + + PrefRegistry(); + + PrefRegistry(const PrefRegistry&) = delete; + PrefRegistry& operator=(const PrefRegistry&) = delete; + + // Retrieve the set of registration flags for the given preference. The return + // value is a bitmask of PrefRegistrationFlags. + uint32_t GetRegistrationFlags(const std::string& pref_name) const; + + // Gets the registered defaults. + scoped_refptr defaults(); + + // Allows iteration over defaults. + const_iterator begin() const; + const_iterator end() const; + + // Changes the default value for a preference. + // + // |pref_name| must be a previously registered preference. + void SetDefaultPrefValue(const std::string& pref_name, base::Value value); + + // Registers a pref owned by another service for use with the current service. + // The owning service must register that pref with the |PUBLIC| flag. + void RegisterForeignPref(const std::string& path); + + // Sets the default value and flags of a previously-registered foreign pref + // value. + void SetDefaultForeignPrefValue(const std::string& path, + base::Value default_value, + uint32_t flags); + + const std::set& foreign_pref_keys() const { + return foreign_pref_keys_; + } + + protected: + friend class base::RefCounted; + virtual ~PrefRegistry(); + + // Used by subclasses to register a default value and registration flags for + // a preference. |flags| is a bitmask of |PrefRegistrationFlags|. + void RegisterPreference(const std::string& path, + base::Value default_value, + uint32_t flags); + + // Allows subclasses to hook into pref registration. + virtual void OnPrefRegistered(const std::string& path, + uint32_t flags); + + scoped_refptr defaults_; + + // A map of pref name to a bitmask of PrefRegistrationFlags. + PrefRegistrationFlagsMap registration_flags_; + + std::set foreign_pref_keys_; +}; + +#endif // COMPONENTS_PREFS_PREF_REGISTRY_H_ diff --git a/src/components/prefs/pref_registry_simple.cc b/src/components/prefs/pref_registry_simple.cc new file mode 100644 index 0000000000..23af262a71 --- /dev/null +++ b/src/components/prefs/pref_registry_simple.cc @@ -0,0 +1,95 @@ +// Copyright (c) 2012 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 "components/prefs/pref_registry_simple.h" + +#include + +#include "base/files/file_path.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/values.h" + +PrefRegistrySimple::PrefRegistrySimple() = default; +PrefRegistrySimple::~PrefRegistrySimple() = default; + +void PrefRegistrySimple::RegisterBooleanPref(const std::string& path, + bool default_value, + uint32_t flags) { + RegisterPreference(path, base::Value(default_value), flags); +} + +void PrefRegistrySimple::RegisterIntegerPref(const std::string& path, + int default_value, + uint32_t flags) { + RegisterPreference(path, base::Value(default_value), flags); +} + +void PrefRegistrySimple::RegisterDoublePref(const std::string& path, + double default_value, + uint32_t flags) { + RegisterPreference(path, base::Value(default_value), flags); +} + +void PrefRegistrySimple::RegisterStringPref(const std::string& path, + const std::string& default_value, + uint32_t flags) { + RegisterPreference(path, base::Value(default_value), flags); +} + +void PrefRegistrySimple::RegisterFilePathPref( + const std::string& path, + const base::FilePath& default_value, + uint32_t flags) { + RegisterPreference(path, base::Value(default_value.AsUTF8Unsafe()), flags); +} + +void PrefRegistrySimple::RegisterListPref(const std::string& path, + uint32_t flags) { + RegisterPreference(path, base::Value(base::Value::Type::LIST), flags); +} + +void PrefRegistrySimple::RegisterListPref(const std::string& path, + base::Value default_value, + uint32_t flags) { + RegisterPreference(path, std::move(default_value), flags); +} + +void PrefRegistrySimple::RegisterDictionaryPref(const std::string& path, + uint32_t flags) { + RegisterPreference(path, base::Value(base::Value::Type::DICTIONARY), flags); +} + +void PrefRegistrySimple::RegisterDictionaryPref(const std::string& path, + base::Value default_value, + uint32_t flags) { + RegisterPreference(path, std::move(default_value), flags); +} + +void PrefRegistrySimple::RegisterInt64Pref(const std::string& path, + int64_t default_value, + uint32_t flags) { + RegisterPreference(path, base::Value(base::NumberToString(default_value)), + flags); +} + +void PrefRegistrySimple::RegisterUint64Pref(const std::string& path, + uint64_t default_value, + uint32_t flags) { + RegisterPreference(path, base::Value(base::NumberToString(default_value)), + flags); +} + +void PrefRegistrySimple::RegisterTimePref(const std::string& path, + base::Time default_value, + uint32_t flags) { + RegisterInt64Pref( + path, default_value.ToDeltaSinceWindowsEpoch().InMicroseconds(), flags); +} + +void PrefRegistrySimple::RegisterTimeDeltaPref(const std::string& path, + base::TimeDelta default_value, + uint32_t flags) { + RegisterInt64Pref(path, default_value.InMicroseconds(), flags); +} diff --git a/src/components/prefs/pref_registry_simple.h b/src/components/prefs/pref_registry_simple.h new file mode 100644 index 0000000000..387075f08a --- /dev/null +++ b/src/components/prefs/pref_registry_simple.h @@ -0,0 +1,86 @@ +// Copyright (c) 2012 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_PREFS_PREF_REGISTRY_SIMPLE_H_ +#define COMPONENTS_PREFS_PREF_REGISTRY_SIMPLE_H_ + +#include + +#include +#include + +#include "base/time/time.h" +#include "components/prefs/pref_registry.h" +#include "components/prefs/prefs_export.h" + +namespace base { +class Value; +class FilePath; +} + +// A simple implementation of PrefRegistry. +class COMPONENTS_PREFS_EXPORT PrefRegistrySimple : public PrefRegistry { + public: + PrefRegistrySimple(); + + PrefRegistrySimple(const PrefRegistrySimple&) = delete; + PrefRegistrySimple& operator=(const PrefRegistrySimple&) = delete; + + // For each of these registration methods, |flags| is an optional bitmask of + // PrefRegistrationFlags. + void RegisterBooleanPref(const std::string& path, + bool default_value, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterIntegerPref(const std::string& path, + int default_value, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterDoublePref(const std::string& path, + double default_value, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterStringPref(const std::string& path, + const std::string& default_value, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterFilePathPref(const std::string& path, + const base::FilePath& default_value, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterListPref(const std::string& path, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterListPref(const std::string& path, + base::Value default_value, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterDictionaryPref(const std::string& path, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterDictionaryPref(const std::string& path, + base::Value default_value, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterInt64Pref(const std::string& path, + int64_t default_value, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterUint64Pref(const std::string& path, + uint64_t default_value, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterTimePref(const std::string& path, + base::Time default_value, + uint32_t flags = NO_REGISTRATION_FLAGS); + + void RegisterTimeDeltaPref(const std::string& path, + base::TimeDelta default_value, + uint32_t flags = NO_REGISTRATION_FLAGS); + + protected: + ~PrefRegistrySimple() override; +}; + +#endif // COMPONENTS_PREFS_PREF_REGISTRY_SIMPLE_H_ diff --git a/src/components/prefs/pref_service.cc b/src/components/prefs/pref_service.cc new file mode 100644 index 0000000000..f7f709787d --- /dev/null +++ b/src/components/prefs/pref_service.cc @@ -0,0 +1,791 @@ +// Copyright (c) 2012 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 "components/prefs/pref_service.h" + +#include +#include +#include + +#include "base/bind.h" +#include "base/check_op.h" +#include "base/debug/alias.h" +#include "base/debug/dump_without_crashing.h" +#include "base/files/file_path.h" +#include "base/json/values_util.h" +#include "base/location.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram.h" +#include "base/notreached.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/task/sequenced_task_runner.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/values.h" +#include "build/build_config.h" +#include "components/prefs/default_pref_store.h" +#include "components/prefs/pref_notifier_impl.h" +#include "components/prefs/pref_registry.h" + +#if BUILDFLAG(IS_ANDROID) +#include "components/prefs/android/pref_service_android.h" +#endif + +namespace { + +class ReadErrorHandler : public PersistentPrefStore::ReadErrorDelegate { + public: + using ErrorCallback = + base::RepeatingCallback; + explicit ReadErrorHandler(ErrorCallback cb) : callback_(cb) {} + + ReadErrorHandler(const ReadErrorHandler&) = delete; + ReadErrorHandler& operator=(const ReadErrorHandler&) = delete; + + void OnError(PersistentPrefStore::PrefReadError error) override { + callback_.Run(error); + } + + private: + ErrorCallback callback_; +}; + +// Returns the WriteablePrefStore::PrefWriteFlags for the pref with the given +// |path|. +uint32_t GetWriteFlags(const PrefService::Preference* pref) { + uint32_t write_flags = WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS; + + if (!pref) + return write_flags; + + if (pref->registration_flags() & PrefRegistry::LOSSY_PREF) + write_flags |= WriteablePrefStore::LOSSY_PREF_WRITE_FLAG; + return write_flags; +} + +// For prefs names in |pref_store| that are not presented in |pref_changed_map|, +// check if their values differ from those in pref_service->FindPreference() and +// add the result into |pref_changed_map|. +void CheckForNewPrefChangesInPrefStore( + std::map* pref_changed_map, + PrefStore* pref_store, + PrefService* pref_service) { + if (!pref_store) + return; + auto values = pref_store->GetValues(); + for (auto item : values->DictItems()) { + // If the key already presents, skip it as a store with higher precedence + // already sets the entry. + if (pref_changed_map->find(item.first) != pref_changed_map->end()) + continue; + const PrefService::Preference* pref = + pref_service->FindPreference(item.first); + if (!pref) + continue; + pref_changed_map->emplace(item.first, *(pref->GetValue()) != item.second); + } +} + +} // namespace + +PrefService::PersistentPrefStoreLoadingObserver:: + PersistentPrefStoreLoadingObserver(PrefService* pref_service) + : pref_service_(pref_service) { + DCHECK(pref_service_); +} + +void PrefService::PersistentPrefStoreLoadingObserver::OnInitializationCompleted( + bool succeeded) { + pref_service_->CheckPrefsLoaded(); +} + +PrefService::PrefService( + std::unique_ptr pref_notifier, + std::unique_ptr pref_value_store, + scoped_refptr user_prefs, + scoped_refptr standalone_browser_prefs, + scoped_refptr pref_registry, + base::RepeatingCallback + read_error_callback, + bool async) + : pref_notifier_(std::move(pref_notifier)), + pref_value_store_(std::move(pref_value_store)), + user_pref_store_(std::move(user_prefs)), + standalone_browser_pref_store_(std::move(standalone_browser_prefs)), + read_error_callback_(std::move(read_error_callback)), + pref_registry_(std::move(pref_registry)), + pref_store_observer_( + std::make_unique( + this)) { + pref_notifier_->SetPrefService(this); + + DCHECK(pref_registry_); + DCHECK(pref_value_store_); + + InitFromStorage(async); +} + +PrefService::~PrefService() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // Remove observers. This could be necessary if this service is destroyed + // before the prefs are fully loaded. + user_pref_store_->RemoveObserver(pref_store_observer_.get()); + if (standalone_browser_pref_store_) { + standalone_browser_pref_store_->RemoveObserver(pref_store_observer_.get()); + } + + // TODO(crbug.com/942491, 946668, 945772) The following code collects + // augments stack dumps created by ~PrefNotifierImpl() with information + // whether the profile owning the PrefService is an incognito profile. + // Delete this, once the bugs are closed. + const bool is_incognito_profile = user_pref_store_->IsInMemoryPrefStore(); + base::debug::Alias(&is_incognito_profile); + // Export value of is_incognito_profile to a string so that `grep` + // is a sufficient tool to analyze crashdumps. + char is_incognito_profile_string[32]; + strncpy(is_incognito_profile_string, + is_incognito_profile ? "is_incognito: yes" : "is_incognito: no", + sizeof(is_incognito_profile_string)); + base::debug::Alias(&is_incognito_profile_string); +} + +void PrefService::InitFromStorage(bool async) { + if (!async) { + if (!user_pref_store_->IsInitializationComplete()) { + user_pref_store_->ReadPrefs(); + } + if (standalone_browser_pref_store_ && + !standalone_browser_pref_store_->IsInitializationComplete()) { + standalone_browser_pref_store_->ReadPrefs(); + } + CheckPrefsLoaded(); + return; + } + + CheckPrefsLoaded(); + + if (!user_pref_store_->IsInitializationComplete()) { + user_pref_store_->AddObserver(pref_store_observer_.get()); + user_pref_store_->ReadPrefsAsync(nullptr); + } + + if (standalone_browser_pref_store_ && + !standalone_browser_pref_store_->IsInitializationComplete()) { + standalone_browser_pref_store_->AddObserver(pref_store_observer_.get()); + standalone_browser_pref_store_->ReadPrefsAsync(nullptr); + } +} + +void PrefService::CheckPrefsLoaded() { + if (!(user_pref_store_->IsInitializationComplete() && + (!standalone_browser_pref_store_ || + standalone_browser_pref_store_->IsInitializationComplete()))) { + // Not done initializing both prefstores. + return; + } + + user_pref_store_->RemoveObserver(pref_store_observer_.get()); + if (standalone_browser_pref_store_) { + standalone_browser_pref_store_->RemoveObserver(pref_store_observer_.get()); + } + + // Both prefstores are initialized, get the read errors. + PersistentPrefStore::PrefReadError user_store_error = + user_pref_store_->GetReadError(); + if (!standalone_browser_pref_store_) { + read_error_callback_.Run(user_store_error); + return; + } + PersistentPrefStore::PrefReadError standalone_browser_store_error = + standalone_browser_pref_store_->GetReadError(); + + // If both stores have the same error (or no error), run the callback with + // either one. This avoids double-reporting (either way prefs weren't + // successfully fully loaded) + if (user_store_error == standalone_browser_store_error) { + read_error_callback_.Run(user_store_error); + } else if (user_store_error == PersistentPrefStore::PREF_READ_ERROR_NONE || + user_store_error == PersistentPrefStore::PREF_READ_ERROR_NO_FILE) { + // Prefer to report the standalone_browser_pref_store error if the + // user_pref_store error is not significant. + read_error_callback_.Run(standalone_browser_store_error); + } else { + // Either the user_pref_store error is significant, or + // both stores failed to load but for different reasons. + // The user_store error is more significant in essentially all cases, + // so prefer to report that. + read_error_callback_.Run(user_store_error); + } +} + +void PrefService::CommitPendingWrite( + base::OnceClosure reply_callback, + base::OnceClosure synchronous_done_callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + user_pref_store_->CommitPendingWrite(std::move(reply_callback), + std::move(synchronous_done_callback)); +} + +void PrefService::SchedulePendingLossyWrites() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + user_pref_store_->SchedulePendingLossyWrites(); +} + +bool PrefService::GetBoolean(const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const base::Value* value = GetPreferenceValueChecked(path); + if (!value || !value->is_bool()) + return false; + return value->GetBool(); +} + +int PrefService::GetInteger(const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const base::Value* value = GetPreferenceValueChecked(path); + if (!value || !value->is_int()) + return 0; + return value->GetInt(); +} + +double PrefService::GetDouble(const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const base::Value* value = GetPreferenceValueChecked(path); + if (!value || !value->is_double()) + return 0.0; + return value->GetDouble(); +} + +std::string PrefService::GetString(const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const base::Value* value = GetPreferenceValueChecked(path); + if (!value || !value->is_string()) + return std::string(); + return value->GetString(); +} + +base::FilePath PrefService::GetFilePath(const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const base::Value* value = GetPreferenceValueChecked(path); + if (!value) + return base::FilePath(); + absl::optional result = base::ValueToFilePath(*value); + DCHECK(result); + return *result; +} + +bool PrefService::HasPrefPath(const std::string& path) const { + const Preference* pref = FindPreference(path); + return pref && !pref->IsDefaultValue(); +} + +void PrefService::IteratePreferenceValues( + base::RepeatingCallback callback) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + for (const auto& it : *pref_registry_) + callback.Run(it.first, *GetPreferenceValue(it.first)); +} + +base::Value PrefService::GetPreferenceValues( + IncludeDefaults include_defaults) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + base::Value out(base::Value::Type::DICTIONARY); + for (const auto& it : *pref_registry_) { + if (include_defaults == INCLUDE_DEFAULTS) { + out.SetPath(it.first, GetPreferenceValue(it.first)->Clone()); + } else { + const Preference* pref = FindPreference(it.first); + if (pref->IsDefaultValue()) + continue; + out.SetPath(it.first, pref->GetValue()->Clone()); + } + } + return out; +} + +const PrefService::Preference* PrefService::FindPreference( + const std::string& pref_name) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + auto it = prefs_map_.find(pref_name); + if (it != prefs_map_.end()) + return &(it->second); + const base::Value* default_value = nullptr; + if (!pref_registry_->defaults()->GetValue(pref_name, &default_value)) + return nullptr; + it = prefs_map_ + .insert(std::make_pair( + pref_name, Preference(this, pref_name, default_value->type()))) + .first; + return &(it->second); +} + +bool PrefService::ReadOnly() const { + return user_pref_store_->ReadOnly(); +} + +PrefService::PrefInitializationStatus PrefService::GetInitializationStatus() + const { + if (!user_pref_store_->IsInitializationComplete()) + return INITIALIZATION_STATUS_WAITING; + + switch (user_pref_store_->GetReadError()) { + case PersistentPrefStore::PREF_READ_ERROR_NONE: + return INITIALIZATION_STATUS_SUCCESS; + case PersistentPrefStore::PREF_READ_ERROR_NO_FILE: + return INITIALIZATION_STATUS_CREATED_NEW_PREF_STORE; + default: + return INITIALIZATION_STATUS_ERROR; + } +} + +PrefService::PrefInitializationStatus +PrefService::GetAllPrefStoresInitializationStatus() const { + if (!pref_value_store_->IsInitializationComplete()) + return INITIALIZATION_STATUS_WAITING; + + return GetInitializationStatus(); +} + +bool PrefService::IsManagedPreference(const std::string& pref_name) const { + const Preference* pref = FindPreference(pref_name); + return pref && pref->IsManaged(); +} + +bool PrefService::IsPreferenceManagedByCustodian( + const std::string& pref_name) const { + const Preference* pref = FindPreference(pref_name); + return pref && pref->IsManagedByCustodian(); +} + +bool PrefService::IsUserModifiablePreference( + const std::string& pref_name) const { + const Preference* pref = FindPreference(pref_name); + return pref && pref->IsUserModifiable(); +} + +const base::Value* PrefService::Get(const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return GetPreferenceValueChecked(path); +} + +const base::Value* PrefService::GetDictionary(const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const base::Value* value = GetPreferenceValueChecked(path); + if (!value) + return nullptr; + if (value->type() != base::Value::Type::DICTIONARY) { + NOTREACHED(); + return nullptr; + } + return value; +} + +const base::Value* PrefService::GetUserPrefValue( + const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const Preference* pref = FindPreference(path); + if (!pref) { + NOTREACHED() << "Trying to get an unregistered pref: " << path; + return nullptr; + } + + // Look for an existing preference in the user store. If it doesn't + // exist, return NULL. + base::Value* value = nullptr; + if (!user_pref_store_->GetMutableValue(path, &value)) + return nullptr; + + if (value->type() != pref->GetType()) { + NOTREACHED() << "Pref value type doesn't match registered type."; + return nullptr; + } + + return value; +} + +void PrefService::SetDefaultPrefValue(const std::string& path, + base::Value value) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + pref_registry_->SetDefaultPrefValue(path, std::move(value)); +} + +const base::Value* PrefService::GetDefaultPrefValue( + const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + // Lookup the preference in the default store. + const base::Value* value = nullptr; + bool has_value = pref_registry_->defaults()->GetValue(path, &value); + DCHECK(has_value) << "Default value missing for pref: " << path; + return value; +} + +const base::Value* PrefService::GetList(const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const base::Value* value = GetPreferenceValueChecked(path); + if (!value) + return nullptr; + if (value->type() != base::Value::Type::LIST) { + NOTREACHED(); + return nullptr; + } + return value; +} + +void PrefService::AddPrefObserver(const std::string& path, PrefObserver* obs) { + pref_notifier_->AddPrefObserver(path, obs); +} + +void PrefService::RemovePrefObserver(const std::string& path, + PrefObserver* obs) { + pref_notifier_->RemovePrefObserver(path, obs); +} + +void PrefService::AddPrefInitObserver(base::OnceCallback obs) { + pref_notifier_->AddInitObserver(std::move(obs)); +} + +PrefRegistry* PrefService::DeprecatedGetPrefRegistry() { + return pref_registry_.get(); +} + +void PrefService::ClearPref(const std::string& path) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const Preference* pref = FindPreference(path); + if (!pref) { + NOTREACHED() << "Trying to clear an unregistered pref: " << path; + return; + } + user_pref_store_->RemoveValue(path, GetWriteFlags(pref)); +} + +void PrefService::ClearPrefsWithPrefixSilently(const std::string& prefix) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + user_pref_store_->RemoveValuesByPrefixSilently(prefix); +} + +void PrefService::ClearMutableValues() { + user_pref_store_->ClearMutableValues(); +} + +void PrefService::OnStoreDeletionFromDisk() { + user_pref_store_->OnStoreDeletionFromDisk(); +} + +void PrefService::ChangePrefValueStore( + PrefStore* managed_prefs, + PrefStore* supervised_user_prefs, + PrefStore* extension_prefs, + PrefStore* recommended_prefs, + std::unique_ptr delegate) { + // Only adding new pref stores are supported. + DCHECK(!pref_value_store_->HasPrefStore(PrefValueStore::MANAGED_STORE) || + !managed_prefs); + DCHECK( + !pref_value_store_->HasPrefStore(PrefValueStore::SUPERVISED_USER_STORE) || + !supervised_user_prefs); + DCHECK(!pref_value_store_->HasPrefStore(PrefValueStore::EXTENSION_STORE) || + !extension_prefs); + DCHECK(!pref_value_store_->HasPrefStore(PrefValueStore::RECOMMENDED_STORE) || + !recommended_prefs); + + // If some of the stores are already initialized, check for pref value changes + // according to store precedence. + std::map pref_changed_map; + CheckForNewPrefChangesInPrefStore(&pref_changed_map, managed_prefs, this); + CheckForNewPrefChangesInPrefStore(&pref_changed_map, supervised_user_prefs, + this); + CheckForNewPrefChangesInPrefStore(&pref_changed_map, extension_prefs, this); + CheckForNewPrefChangesInPrefStore(&pref_changed_map, recommended_prefs, this); + + pref_value_store_ = pref_value_store_->CloneAndSpecialize( + managed_prefs, supervised_user_prefs, extension_prefs, + nullptr /* command_line_prefs */, nullptr /* user_prefs */, + nullptr /* standalone_browser_prefs */, recommended_prefs, + nullptr /* default_prefs */, pref_notifier_.get(), std::move(delegate)); + + // Notify |pref_notifier_| on all changed values. + for (const auto& kv : pref_changed_map) { + if (kv.second) + pref_notifier_.get()->OnPreferenceChanged(kv.first); + } +} + +void PrefService::AddPrefObserverAllPrefs(PrefObserver* obs) { + pref_notifier_->AddPrefObserverAllPrefs(obs); +} + +void PrefService::RemovePrefObserverAllPrefs(PrefObserver* obs) { + pref_notifier_->RemovePrefObserverAllPrefs(obs); +} + +#if BUILDFLAG(IS_ANDROID) +base::android::ScopedJavaLocalRef PrefService::GetJavaObject() { + if (!pref_service_android_) { + pref_service_android_ = std::make_unique(this); + } + return pref_service_android_->GetJavaObject(); +} +#endif + +void PrefService::Set(const std::string& path, const base::Value& value) { + SetUserPrefValue(path, value.Clone()); +} + +void PrefService::SetBoolean(const std::string& path, bool value) { + SetUserPrefValue(path, base::Value(value)); +} + +void PrefService::SetInteger(const std::string& path, int value) { + SetUserPrefValue(path, base::Value(value)); +} + +void PrefService::SetDouble(const std::string& path, double value) { + SetUserPrefValue(path, base::Value(value)); +} + +void PrefService::SetString(const std::string& path, const std::string& value) { + SetUserPrefValue(path, base::Value(value)); +} + +void PrefService::SetFilePath(const std::string& path, + const base::FilePath& value) { + SetUserPrefValue(path, base::FilePathToValue(value)); +} + +void PrefService::SetInt64(const std::string& path, int64_t value) { + SetUserPrefValue(path, base::Int64ToValue(value)); +} + +int64_t PrefService::GetInt64(const std::string& path) const { + const base::Value* value = GetPreferenceValueChecked(path); + absl::optional integer = base::ValueToInt64(value); + DCHECK(integer); + return integer.value_or(0); +} + +void PrefService::SetUint64(const std::string& path, uint64_t value) { + SetUserPrefValue(path, base::Value(base::NumberToString(value))); +} + +uint64_t PrefService::GetUint64(const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const base::Value* value = GetPreferenceValueChecked(path); + if (!value || !value->is_string()) + return 0; + + uint64_t result; + base::StringToUint64(value->GetString(), &result); + return result; +} + +void PrefService::SetTime(const std::string& path, base::Time value) { + SetUserPrefValue(path, base::TimeToValue(value)); +} + +base::Time PrefService::GetTime(const std::string& path) const { + const base::Value* value = GetPreferenceValueChecked(path); + absl::optional time = base::ValueToTime(value); + DCHECK(time); + return time.value_or(base::Time()); +} + +void PrefService::SetTimeDelta(const std::string& path, base::TimeDelta value) { + SetUserPrefValue(path, base::TimeDeltaToValue(value)); +} + +base::TimeDelta PrefService::GetTimeDelta(const std::string& path) const { + const base::Value* value = GetPreferenceValueChecked(path); + absl::optional time_delta = base::ValueToTimeDelta(value); + DCHECK(time_delta); + return time_delta.value_or(base::TimeDelta()); +} + +base::Value* PrefService::GetMutableUserPref(const std::string& path, + base::Value::Type type) { + CHECK(type == base::Value::Type::DICTIONARY || + type == base::Value::Type::LIST); + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const Preference* pref = FindPreference(path); + if (!pref) { + NOTREACHED() << "Trying to get an unregistered pref: " << path; + return nullptr; + } + if (pref->GetType() != type) { + NOTREACHED() << "Wrong type for GetMutableValue: " << path; + return nullptr; + } + + // Look for an existing preference in the user store. Return it in case it + // exists and has the correct type. + base::Value* value = nullptr; + if (user_pref_store_->GetMutableValue(path, &value) && + value->type() == type) { + return value; + } + + // If no user preference of the correct type exists, clone default value. + const base::Value* default_value = nullptr; + pref_registry_->defaults()->GetValue(path, &default_value); + DCHECK_EQ(default_value->type(), type); + user_pref_store_->SetValueSilently( + path, base::Value::ToUniquePtrValue(default_value->Clone()), + GetWriteFlags(pref)); + user_pref_store_->GetMutableValue(path, &value); + return value; +} + +void PrefService::ReportUserPrefChanged(const std::string& key) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + user_pref_store_->ReportValueChanged(key, GetWriteFlags(FindPreference(key))); +} + +void PrefService::ReportUserPrefChanged( + const std::string& key, + std::set> path_components) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + user_pref_store_->ReportSubValuesChanged(key, std::move(path_components), + GetWriteFlags(FindPreference(key))); +} + +void PrefService::SetUserPrefValue(const std::string& path, + base::Value new_value) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const Preference* pref = FindPreference(path); + if (!pref) { + NOTREACHED() << "Trying to write an unregistered pref: " << path; + return; + } + if (pref->GetType() != new_value.type()) { + NOTREACHED() << "Trying to set pref " << path << " of type " + << pref->GetType() << " to value of type " << new_value.type(); + return; + } + + user_pref_store_->SetValue( + path, base::Value::ToUniquePtrValue(std::move(new_value)), + GetWriteFlags(pref)); +} + +void PrefService::UpdateCommandLinePrefStore(PrefStore* command_line_store) { + pref_value_store_->UpdateCommandLinePrefStore(command_line_store); +} + +/////////////////////////////////////////////////////////////////////////////// +// PrefService::Preference + +PrefService::Preference::Preference(const PrefService* service, + std::string name, + base::Value::Type type) + : name_(std::move(name)), + type_(type), + // Cache the registration flags at creation time to avoid multiple map + // lookups later. + registration_flags_(service->pref_registry_->GetRegistrationFlags(name_)), + pref_service_(service) {} + +const base::Value* PrefService::Preference::GetValue() const { + return pref_service_->GetPreferenceValueChecked(name_); +} + +const base::Value* PrefService::Preference::GetRecommendedValue() const { + DCHECK(pref_service_->FindPreference(name_)) + << "Must register pref before getting its value"; + + const base::Value* found_value = nullptr; + if (pref_value_store()->GetRecommendedValue(name_, type_, &found_value)) { + DCHECK(found_value->type() == type_); + return found_value; + } + + // The pref has no recommended value. + return nullptr; +} + +bool PrefService::Preference::IsManaged() const { + return pref_value_store()->PrefValueInManagedStore(name_); +} + +bool PrefService::Preference::IsManagedByCustodian() const { + return pref_value_store()->PrefValueInSupervisedStore(name_); +} + +bool PrefService::Preference::IsRecommended() const { + return pref_value_store()->PrefValueFromRecommendedStore(name_); +} + +bool PrefService::Preference::HasExtensionSetting() const { + return pref_value_store()->PrefValueInExtensionStore(name_); +} + +bool PrefService::Preference::HasUserSetting() const { + return pref_value_store()->PrefValueInUserStore(name_); +} + +bool PrefService::Preference::IsExtensionControlled() const { + return pref_value_store()->PrefValueFromExtensionStore(name_); +} + +bool PrefService::Preference::IsUserControlled() const { + return pref_value_store()->PrefValueFromUserStore(name_); +} + +bool PrefService::Preference::IsDefaultValue() const { + return pref_value_store()->PrefValueFromDefaultStore(name_); +} + +bool PrefService::Preference::IsUserModifiable() const { + return pref_value_store()->PrefValueUserModifiable(name_); +} + +bool PrefService::Preference::IsExtensionModifiable() const { + return pref_value_store()->PrefValueExtensionModifiable(name_); +} + +const base::Value* PrefService::GetPreferenceValue( + const std::string& path) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // TODO(battre): This is a check for crbug.com/435208. After analyzing some + // crash dumps it looks like the PrefService is accessed even though it has + // been cleared already. + CHECK(pref_registry_); + CHECK(pref_registry_->defaults()); + CHECK(pref_value_store_); + + const base::Value* default_value = nullptr; + if (!pref_registry_->defaults()->GetValue(path, &default_value)) + return nullptr; + + const base::Value* found_value = nullptr; + base::Value::Type default_type = default_value->type(); + if (!pref_value_store_->GetValue(path, default_type, &found_value)) { + // Every registered preference has at least a default value. + NOTREACHED() << "no valid value found for registered pref " << path; + return nullptr; + } + + DCHECK_EQ(found_value->type(), default_type); + return found_value; +} + +const base::Value* PrefService::GetPreferenceValueChecked( + const std::string& path) const { + const base::Value* value = GetPreferenceValue(path); + DCHECK(value) << "Trying to read an unregistered pref: " << path; + return value; +} diff --git a/src/components/prefs/pref_service.h b/src/components/prefs/pref_service.h new file mode 100644 index 0000000000..1e9af59177 --- /dev/null +++ b/src/components/prefs/pref_service.h @@ -0,0 +1,514 @@ +// Copyright (c) 2012 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. + +// This provides a way to access the application's current preferences. + +// Chromium settings and storage represent user-selected preferences and +// information and MUST not be extracted, overwritten or modified except +// through Chromium defined APIs. + +#ifndef COMPONENTS_PREFS_PREF_SERVICE_H_ +#define COMPONENTS_PREFS_PREF_SERVICE_H_ + +#include + +#include +#include +#include +#include +#include + +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/sequence_checker.h" +#include "base/time/time.h" +#include "base/values.h" +#include "build/build_config.h" +#include "components/prefs/persistent_pref_store.h" +#include "components/prefs/pref_value_store.h" +#include "components/prefs/prefs_export.h" + +#if BUILDFLAG(IS_ANDROID) +#include "base/android/scoped_java_ref.h" +#endif + +class PrefNotifier; +class PrefNotifierImpl; +class PrefObserver; +class PrefRegistry; +class PrefStore; +#if BUILDFLAG(IS_ANDROID) +class PrefServiceAndroid; +#endif + +namespace base { +class FilePath; +} + +namespace prefs { +class ScopedDictionaryPrefUpdate; +} + +namespace subtle { +class PrefMemberBase; +class ScopedUserPrefUpdateBase; +} + +// Base class for PrefServices. You can use the base class to read and +// interact with preferences, but not to register new preferences; for +// that see e.g. PrefRegistrySimple. +// +// Settings and storage accessed through this class represent +// user-selected preferences and information and MUST not be +// extracted, overwritten or modified except through the defined APIs. +class COMPONENTS_PREFS_EXPORT PrefService { + public: + enum PrefInitializationStatus { + INITIALIZATION_STATUS_WAITING, + INITIALIZATION_STATUS_SUCCESS, + INITIALIZATION_STATUS_CREATED_NEW_PREF_STORE, + INITIALIZATION_STATUS_ERROR + }; + + enum IncludeDefaults { + INCLUDE_DEFAULTS, + EXCLUDE_DEFAULTS, + }; + + // A helper class to store all the information associated with a preference. + class COMPONENTS_PREFS_EXPORT Preference { + public: + // The type of the preference is determined by the type with which it is + // registered. This type needs to be a boolean, integer, double, string, + // dictionary (a branch), or list. You shouldn't need to construct this on + // your own; use the PrefService::Register*Pref methods instead. + Preference(const PrefService* service, + std::string name, + base::Value::Type type); + ~Preference() {} + + // Returns the name of the Preference (i.e., the key, e.g., + // browser.window_placement). + std::string name() const { return name_; } + + // Returns the registered type of the preference. + base::Value::Type GetType() const { return type_; } + + // Returns the value of the Preference, falling back to the registered + // default value if no other has been set. + const base::Value* GetValue() const; + + // Returns the value recommended by the admin, if any. + const base::Value* GetRecommendedValue() const; + + // Returns true if the Preference is managed, i.e. set by an admin policy. + // Since managed prefs have the highest priority, this also indicates + // whether the pref is actually being controlled by the policy setting. + bool IsManaged() const; + + // Returns true if the Preference is controlled by the custodian of the + // supervised user. Since a supervised user is not expected to have an admin + // policy, this is the controlling pref if set. + bool IsManagedByCustodian() const; + + // Returns true if the Preference's current value is one recommended by + // admin policy. Note that this will be false if any other higher-priority + // source overrides the value (e.g., the user has set a value). + bool IsRecommended() const; + + // Returns true if the Preference has a value set by an extension, even if + // that value is being overridden by a higher-priority source. + bool HasExtensionSetting() const; + + // Returns true if the Preference has a user setting, even if that value is + // being overridden by a higher-priority source. + bool HasUserSetting() const; + + // Returns true if the Preference value is currently being controlled by an + // extension, and not by any higher-priority source. + bool IsExtensionControlled() const; + + // Returns true if the Preference value is currently being controlled by a + // user setting, and not by any higher-priority source. + bool IsUserControlled() const; + + // Returns true if the Preference is currently using its default value, + // and has not been set by any higher-priority source (even with the same + // value). + bool IsDefaultValue() const; + + // Returns true if the user can change the Preference value, which is the + // case if no higher-priority source than the user store controls the + // Preference. + bool IsUserModifiable() const; + + // Returns true if an extension can change the Preference value, which is + // the case if no higher-priority source than the extension store controls + // the Preference. + bool IsExtensionModifiable() const; + + // Return the registration flags for this pref as a bitmask of + // PrefRegistry::PrefRegistrationFlags. + uint32_t registration_flags() const { return registration_flags_; } + + private: + friend class PrefService; + + PrefValueStore* pref_value_store() const { + return pref_service_->pref_value_store_.get(); + } + + const std::string name_; + + const base::Value::Type type_; + + const uint32_t registration_flags_; + + // Reference to the PrefService in which this pref was created. + const raw_ptr pref_service_; + }; + + // You may wish to use PrefServiceFactory or one of its subclasses + // for simplified construction. + PrefService(std::unique_ptr pref_notifier, + std::unique_ptr pref_value_store, + scoped_refptr user_prefs, + scoped_refptr standalone_browser_prefs, + scoped_refptr pref_registry, + base::RepeatingCallback + read_error_callback, + bool async); + + PrefService(const PrefService&) = delete; + PrefService& operator=(const PrefService&) = delete; + + virtual ~PrefService(); + + // Lands pending writes to disk. This should only be used if we need to save + // immediately (basically, during shutdown). |reply_callback| will be posted + // to the current sequence when changes have been written. + // |synchronous_done_callback| on the other hand will be invoked right away + // wherever the writes complete (could even be invoked synchronously if no + // writes need to occur); this is useful when the current thread cannot pump + // messages to observe the reply (e.g. nested loops banned on main thread + // during shutdown). |synchronous_done_callback| must be thread-safe. + void CommitPendingWrite( + base::OnceClosure reply_callback = base::OnceClosure(), + base::OnceClosure synchronous_done_callback = base::OnceClosure()); + + // Schedules a write if there is any lossy data pending. Unlike + // CommitPendingWrite() this does not immediately sync to disk, instead it + // triggers an eventual write if there is lossy data pending and if there + // isn't one scheduled already. + void SchedulePendingLossyWrites(); + + // Returns true if the preference for the given preference name is available + // and is managed. + bool IsManagedPreference(const std::string& pref_name) const; + + // Returns true if the preference for the given preference name is available + // and is controlled by the parent/guardian of the child Account. + bool IsPreferenceManagedByCustodian(const std::string& pref_name) const; + + // Returns |true| if a preference with the given name is available and its + // value can be changed by the user. + bool IsUserModifiablePreference(const std::string& pref_name) const; + + // Look up a preference. Returns NULL if the preference is not + // registered. + const PrefService::Preference* FindPreference(const std::string& path) const; + + // If the path is valid and the value at the end of the path matches the type + // specified, it will return the specified value. Otherwise, the default + // value (set when the pref was registered) will be returned. + bool GetBoolean(const std::string& path) const; + int GetInteger(const std::string& path) const; + double GetDouble(const std::string& path) const; + std::string GetString(const std::string& path) const; + base::FilePath GetFilePath(const std::string& path) const; + + // Returns the branch if it exists, or the registered default value otherwise. + // Note that |path| must point to a registered preference. In that case, these + // functions will never return NULL. + const base::Value* Get(const std::string& path) const; + const base::Value* GetDictionary(const std::string& path) const; + const base::Value* GetList(const std::string& path) const; + + // Removes a user pref and restores the pref to its default value. + void ClearPref(const std::string& path); + + // Removes user prefs that start with |prefix|. + void ClearPrefsWithPrefixSilently(const std::string& prefix); + + // If the path is valid (i.e., registered), update the pref value in the user + // prefs. + // To set the value of dictionary or list values in the pref tree use + // Set(), but to modify the value of a dictionary or list use either + // ListPrefUpdate or DictionaryPrefUpdate from scoped_user_pref_update.h. + void Set(const std::string& path, const base::Value& value); + void SetBoolean(const std::string& path, bool value); + void SetInteger(const std::string& path, int value); + void SetDouble(const std::string& path, double value); + void SetString(const std::string& path, const std::string& value); + void SetFilePath(const std::string& path, const base::FilePath& value); + + // Int64 helper methods that actually store the given value as a string. + // Note that if obtaining the named value via GetDictionary or GetList, the + // Value type will be Type::STRING. + void SetInt64(const std::string& path, int64_t value); + int64_t GetInt64(const std::string& path) const; + + // As above, but for unsigned values. + void SetUint64(const std::string& path, uint64_t value); + uint64_t GetUint64(const std::string& path) const; + + // Time helper methods that actually store the given value as a string, which + // represents the number of microseconds elapsed (absolute for TimeDelta and + // relative to Windows epoch for Time variants). Note that if obtaining the + // named value via GetDictionary or GetList, the Value type will be + // Type::STRING. + void SetTime(const std::string& path, base::Time value); + base::Time GetTime(const std::string& path) const; + void SetTimeDelta(const std::string& path, base::TimeDelta value); + base::TimeDelta GetTimeDelta(const std::string& path) const; + + // Returns the value of the given preference, from the user pref store. If + // the preference is not set in the user pref store, returns NULL. + const base::Value* GetUserPrefValue(const std::string& path) const; + + // Changes the default value for a preference. + // + // Will cause a pref change notification to be fired if this causes + // the effective value to change. + void SetDefaultPrefValue(const std::string& path, base::Value value); + + // Returns the default value of the given preference. |path| must point to a + // registered preference. In that case, will never return nullptr, so callers + // do not need to check this. + const base::Value* GetDefaultPrefValue(const std::string& path) const; + + // Returns true if a value has been set for the specified path. + // NOTE: this is NOT the same as FindPreference. In particular + // FindPreference returns whether RegisterXXX has been invoked, where as + // this checks if a value exists for the path. + bool HasPrefPath(const std::string& path) const; + + // Issues a callback for every preference value. The preferences must not be + // mutated during iteration. + void IteratePreferenceValues( + base::RepeatingCallback callback) const; + + // Returns a dictionary with effective preference values. This is an expensive + // operation which does a deep copy. Use only if you really need the results + // in a base::Value (for example, for JSON serialization). Otherwise use + // IteratePreferenceValues above to avoid the copies. + // + // If INCLUDE_DEFAULTS is requested, preferences set to their default values + // will be included. Otherwise, these will be omitted from the returned + // dictionary. + base::Value GetPreferenceValues(IncludeDefaults include_defaults) const; + + bool ReadOnly() const; + + // Returns the initialization state, taking only user prefs into account. + PrefInitializationStatus GetInitializationStatus() const; + + // Returns the initialization state, taking all pref stores into account. + PrefInitializationStatus GetAllPrefStoresInitializationStatus() const; + + // Tell our PrefValueStore to update itself to |command_line_store|. + // Takes ownership of the store. + virtual void UpdateCommandLinePrefStore(PrefStore* command_line_store); + + // We run the callback once, when initialization completes. The bool + // parameter will be set to true for successful initialization, + // false for unsuccessful. + void AddPrefInitObserver(base::OnceCallback callback); + + // Returns the PrefRegistry object for this service. You should not + // use this; the intent is for no registrations to take place after + // PrefService has been constructed. + // + // Instead of using this method, the recommended approach is to + // register all preferences for a class Xyz up front in a static + // Xyz::RegisterPrefs function, which gets invoked early in the + // application's start-up, before a PrefService is created. + // + // As an example, prefs registration in Chrome is triggered by the + // functions chrome::RegisterPrefs (for global preferences) and + // chrome::RegisterProfilePrefs (for user-specific preferences) + // implemented in chrome/browser/prefs/browser_prefs.cc. + PrefRegistry* DeprecatedGetPrefRegistry(); + + // Clears mutable values. + void ClearMutableValues(); + + // Invoked when the store is deleted from disk. Allows this PrefService + // to tangentially cleanup data it may have saved outside the store. + void OnStoreDeletionFromDisk(); + + // Add new pref stores to the existing PrefValueStore. Only adding new + // stores are allowed. If a corresponding store already exists, calling this + // will cause DCHECK failures. If the newly added stores already contain + // values, PrefNotifier associated with this object will be notified with + // these values. |delegate| can be passed to observe events of the new + // PrefValueStore. + // TODO(qinmin): packaging all the input params in a struct, and do the same + // for the constructor. + void ChangePrefValueStore( + PrefStore* managed_prefs, + PrefStore* supervised_user_prefs, + PrefStore* extension_prefs, + PrefStore* recommended_prefs, + std::unique_ptr delegate = nullptr); + + // A low level function for registering an observer for every single + // preference changed notification. The caller must ensure that the observer + // remains valid as long as it is registered. Pointer ownership is not + // transferred. + // + // Almost all calling code should use a PrefChangeRegistrar instead. + // + // AVOID ADDING THESE. These are low-level observer notifications that are + // called for every pref change. This can lead to inefficiency, and the lack + // of a "registrar" model makes it easy to forget to undregister. It is + // really designed for integrating other notification systems, not for normal + // observation. + void AddPrefObserverAllPrefs(PrefObserver* obs); + void RemovePrefObserverAllPrefs(PrefObserver* obs); + +#if BUILDFLAG(IS_ANDROID) + base::android::ScopedJavaLocalRef GetJavaObject(); +#endif + + protected: + // The PrefNotifier handles registering and notifying preference observers. + // It is created and owned by this PrefService. Subclasses may access it for + // unit testing. + const std::unique_ptr pref_notifier_; + + // The PrefValueStore provides prioritized preference values. It is owned by + // this PrefService. Subclasses may access it for unit testing. + std::unique_ptr pref_value_store_; + + // Pref Stores and profile that we passed to the PrefValueStore. + const scoped_refptr user_pref_store_; + const scoped_refptr standalone_browser_pref_store_; + + // Callback to call when a read error occurs. Always invoked on the sequence + // this PrefService was created own. + const base::RepeatingCallback + read_error_callback_; + + private: + // Hash map expected to be fastest here since it minimises expensive + // string comparisons. Order is unimportant, and deletions are rare. + // Confirmed on Android where this speeded Chrome startup by roughly 50ms + // vs. std::map, and by roughly 180ms vs. std::set of Preference pointers. + typedef std::unordered_map PreferenceMap; + + // Give access to ReportUserPrefChanged() and GetMutableUserPref(). + friend class subtle::ScopedUserPrefUpdateBase; + friend class PrefServiceTest_WriteablePrefStoreFlags_Test; + friend class prefs::ScopedDictionaryPrefUpdate; + + // Registration of pref change observers must be done using the + // PrefChangeRegistrar, which is declared as a friend here to grant it + // access to the otherwise protected members Add/RemovePrefObserver. + // PrefMember registers for preferences changes notification directly to + // avoid the storage overhead of the registrar, so its base class must be + // declared as a friend, too. + friend class PrefChangeRegistrar; + friend class subtle::PrefMemberBase; + + // These are protected so they can only be accessed by the friend + // classes listed above. + // + // If the pref at the given path changes, we call the observer's + // OnPreferenceChanged method. Note that observers should not call + // these methods directly but rather use a PrefChangeRegistrar to + // make sure the observer gets cleaned up properly. + // + // Virtual for testing. + virtual void AddPrefObserver(const std::string& path, PrefObserver* obs); + virtual void RemovePrefObserver(const std::string& path, PrefObserver* obs); + + // A PrefStore::Observer which reports loading errors from + // PersistentPrefStores after they are loaded. Usually this is only user_prefs + // however in ash it additionally includes standalone_browser_prefs. Errors + // are only reported once even though multiple files may be loaded. + class PersistentPrefStoreLoadingObserver : public PrefStore::Observer { + public: + explicit PersistentPrefStoreLoadingObserver(PrefService* pref_service_); + + // PrefStore::Observer implementation + void OnPrefValueChanged(const std::string& key) override {} + void OnInitializationCompleted(bool succeeded) override; + + private: + PrefService* pref_service_ = nullptr; + }; + + // Sends notification of a changed preference. This needs to be called by + // a ScopedUserPrefUpdate or ScopedDictionaryPrefUpdate if a DictionaryValue + // or ListValue is changed. + void ReportUserPrefChanged(const std::string& key); + void ReportUserPrefChanged( + const std::string& key, + std::set> path_components); + + // Sets the value for this pref path in the user pref store and informs the + // PrefNotifier of the change. + void SetUserPrefValue(const std::string& path, base::Value new_value); + + // Load preferences from storage, attempting to diagnose and handle errors. + // This should only be called from the constructor. + void InitFromStorage(bool async); + + // Verifies that prefs are fully loaded from disk, handling errors. This + // method may be called multiple times, but no more than once after all prefs + // are loaded. + void CheckPrefsLoaded(); + + // Used to set the value of dictionary or list values in the user pref store. + // This will create a dictionary or list if one does not exist in the user + // pref store. This method returns NULL only if you're requesting an + // unregistered pref or a non-dict/non-list pref. + // |type| may only be Values::Type::DICTIONARY or Values::Type::LIST and + // |path| must point to a registered preference of type |type|. + // Ownership of the returned value remains at the user pref store. + base::Value* GetMutableUserPref(const std::string& path, + base::Value::Type type); + + // GetPreferenceValue is the equivalent of FindPreference(path)->GetValue(), + // it has been added for performance. It is faster because it does + // not need to find or create a Preference object to get the + // value (GetValue() calls back though the preference service to + // actually get the value.). + const base::Value* GetPreferenceValue(const std::string& path) const; + const base::Value* GetPreferenceValueChecked(const std::string& path) const; + + const scoped_refptr pref_registry_; + + std::unique_ptr + pref_store_observer_; + + // Local cache of registered Preference objects. The pref_registry_ + // is authoritative with respect to what the types and default values + // of registered preferences are. + mutable PreferenceMap prefs_map_; + +#if BUILDFLAG(IS_ANDROID) + // Manage and fetch the java object that wraps this PrefService on + // android. + std::unique_ptr pref_service_android_; +#endif + + SEQUENCE_CHECKER(sequence_checker_); +}; + +#endif // COMPONENTS_PREFS_PREF_SERVICE_H_ diff --git a/src/components/prefs/pref_service_factory.cc b/src/components/prefs/pref_service_factory.cc new file mode 100644 index 0000000000..8030482fc2 --- /dev/null +++ b/src/components/prefs/pref_service_factory.cc @@ -0,0 +1,53 @@ +// Copyright 2013 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 "components/prefs/pref_service_factory.h" + +#include + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/task/sequenced_task_runner.h" +#include "components/prefs/default_pref_store.h" +#include "components/prefs/json_pref_store.h" +#include "components/prefs/pref_filter.h" +#include "components/prefs/pref_notifier_impl.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/pref_value_store.h" + +PrefServiceFactory::PrefServiceFactory() + : read_error_callback_(base::DoNothing()), async_(false) {} + +PrefServiceFactory::~PrefServiceFactory() {} + +void PrefServiceFactory::SetUserPrefsFile( + const base::FilePath& prefs_file, + base::SequencedTaskRunner* task_runner) { + user_prefs_ = + base::MakeRefCounted(prefs_file, nullptr, task_runner); +} + +std::unique_ptr PrefServiceFactory::Create( + scoped_refptr pref_registry, + std::unique_ptr delegate) { + auto pref_notifier = std::make_unique(); + auto pref_value_store = std::make_unique( + managed_prefs_.get(), supervised_user_prefs_.get(), + extension_prefs_.get(), standalone_browser_prefs_.get(), + command_line_prefs_.get(), user_prefs_.get(), recommended_prefs_.get(), + pref_registry->defaults().get(), pref_notifier.get(), + std::move(delegate)); + return std::make_unique( + std::move(pref_notifier), std::move(pref_value_store), user_prefs_.get(), + standalone_browser_prefs_.get(), std::move(pref_registry), + read_error_callback_, async_); +} + +void PrefServiceFactory::ChangePrefValueStore( + PrefService* pref_service, + std::unique_ptr delegate) { + pref_service->ChangePrefValueStore( + managed_prefs_.get(), supervised_user_prefs_.get(), + extension_prefs_.get(), recommended_prefs_.get(), std::move(delegate)); +} diff --git a/src/components/prefs/pref_service_factory.h b/src/components/prefs/pref_service_factory.h new file mode 100644 index 0000000000..ce5b3e6cd4 --- /dev/null +++ b/src/components/prefs/pref_service_factory.h @@ -0,0 +1,106 @@ +// Copyright 2013 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_PREFS_PREF_SERVICE_FACTORY_H_ +#define COMPONENTS_PREFS_PREF_SERVICE_FACTORY_H_ + +#include "base/callback.h" +#include "base/memory/ref_counted.h" +#include "components/prefs/persistent_pref_store.h" +#include "components/prefs/pref_registry.h" +#include "components/prefs/pref_store.h" +#include "components/prefs/pref_value_store.h" +#include "components/prefs/prefs_export.h" + +class PrefService; + +namespace base { +class FilePath; +class SequencedTaskRunner; +} + +// A class that allows convenient building of PrefService. +class COMPONENTS_PREFS_EXPORT PrefServiceFactory { + public: + PrefServiceFactory(); + + PrefServiceFactory(const PrefServiceFactory&) = delete; + PrefServiceFactory& operator=(const PrefServiceFactory&) = delete; + + virtual ~PrefServiceFactory(); + + // Functions for setting the various parameters of the PrefService to build. + void set_managed_prefs(scoped_refptr prefs) { + managed_prefs_.swap(prefs); + } + + void set_supervised_user_prefs(scoped_refptr prefs) { + supervised_user_prefs_.swap(prefs); + } + + void set_extension_prefs(scoped_refptr prefs) { + extension_prefs_.swap(prefs); + } + + void set_standalone_browser_prefs(scoped_refptr prefs) { + standalone_browser_prefs_.swap(prefs); + } + + void set_command_line_prefs(scoped_refptr prefs) { + command_line_prefs_.swap(prefs); + } + + void set_user_prefs(scoped_refptr prefs) { + user_prefs_.swap(prefs); + } + + void set_recommended_prefs(scoped_refptr prefs) { + recommended_prefs_.swap(prefs); + } + + // Sets up error callback for the PrefService. A do-nothing default is + // provided if this is not called. This callback is always invoked (async or + // not) on the sequence on which Create is invoked. + void set_read_error_callback( + base::RepeatingCallback + read_error_callback) { + read_error_callback_ = std::move(read_error_callback); + } + + // Specifies to use an actual file-backed user pref store. + void SetUserPrefsFile(const base::FilePath& prefs_file, + base::SequencedTaskRunner* task_runner); + + void set_async(bool async) { + async_ = async; + } + + // Creates a PrefService object initialized with the parameters from + // this factory. + std::unique_ptr Create( + scoped_refptr pref_registry, + std::unique_ptr delegate = nullptr); + + // Add pref stores from this object to the |pref_service|. + void ChangePrefValueStore( + PrefService* pref_service, + std::unique_ptr delegate = nullptr); + + protected: + scoped_refptr managed_prefs_; + scoped_refptr supervised_user_prefs_; + scoped_refptr extension_prefs_; + scoped_refptr standalone_browser_prefs_; + scoped_refptr command_line_prefs_; + scoped_refptr user_prefs_; + scoped_refptr recommended_prefs_; + + base::RepeatingCallback + read_error_callback_; + + // Defaults to false. + bool async_; +}; + +#endif // COMPONENTS_PREFS_PREF_SERVICE_FACTORY_H_ diff --git a/src/components/prefs/pref_service_unittest.cc b/src/components/prefs/pref_service_unittest.cc new file mode 100644 index 0000000000..8ba3c88c39 --- /dev/null +++ b/src/components/prefs/pref_service_unittest.cc @@ -0,0 +1,672 @@ +// Copyright (c) 2012 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 +#include + +#include + +#include "base/callback_helpers.h" +#include "base/cxx17_backports.h" +#include "base/time/time.h" +#include "base/values.h" +#include "components/prefs/json_pref_store.h" +#include "components/prefs/mock_pref_change_callback.h" +#include "components/prefs/pref_change_registrar.h" +#include "components/prefs/pref_notifier_impl.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/pref_service_factory.h" +#include "components/prefs/pref_value_store.h" +#include "components/prefs/testing_pref_service.h" +#include "components/prefs/testing_pref_store.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::Mock; + +namespace { + +const char kPrefName[] = "pref.name"; +const char kManagedPref[] = "managed_pref"; +const char kRecommendedPref[] = "recommended_pref"; +const char kSupervisedPref[] = "supervised_pref"; +const char kStandaloneBrowserPref[] = "standalone_browser_pref"; + +} // namespace + +TEST(PrefServiceTest, NoObserverFire) { + TestingPrefServiceSimple prefs; + + const char pref_name[] = "homepage"; + prefs.registry()->RegisterStringPref(pref_name, std::string()); + + const char new_pref_value[] = "http://www.google.com/"; + MockPrefChangeCallback obs(&prefs); + PrefChangeRegistrar registrar; + registrar.Init(&prefs); + registrar.Add(pref_name, obs.GetCallback()); + + // This should fire the checks in MockPrefChangeCallback::OnPreferenceChanged. + const base::Value expected_value(new_pref_value); + obs.Expect(pref_name, &expected_value); + prefs.SetString(pref_name, new_pref_value); + Mock::VerifyAndClearExpectations(&obs); + + // Setting the pref to the same value should not set the pref value a second + // time. + EXPECT_CALL(obs, OnPreferenceChanged(_)).Times(0); + prefs.SetString(pref_name, new_pref_value); + Mock::VerifyAndClearExpectations(&obs); + + // Clearing the pref should cause the pref to fire. + const base::Value expected_default_value((std::string())); + obs.Expect(pref_name, &expected_default_value); + prefs.ClearPref(pref_name); + Mock::VerifyAndClearExpectations(&obs); + + // Clearing the pref again should not cause the pref to fire. + EXPECT_CALL(obs, OnPreferenceChanged(_)).Times(0); + prefs.ClearPref(pref_name); + Mock::VerifyAndClearExpectations(&obs); +} + +TEST(PrefServiceTest, HasPrefPath) { + TestingPrefServiceSimple prefs; + + const char path[] = "fake.path"; + + // Shouldn't initially have a path. + EXPECT_FALSE(prefs.HasPrefPath(path)); + + // Register the path. This doesn't set a value, so the path still shouldn't + // exist. + prefs.registry()->RegisterStringPref(path, std::string()); + EXPECT_FALSE(prefs.HasPrefPath(path)); + + // Set a value and make sure we have a path. + prefs.SetString(path, "blah"); + EXPECT_TRUE(prefs.HasPrefPath(path)); +} + +TEST(PrefServiceTest, Observers) { + const char pref_name[] = "homepage"; + + TestingPrefServiceSimple prefs; + prefs.SetUserPref(pref_name, + std::make_unique("http://www.cnn.com")); + prefs.registry()->RegisterStringPref(pref_name, std::string()); + + const char new_pref_value[] = "http://www.google.com/"; + const base::Value expected_new_pref_value(new_pref_value); + MockPrefChangeCallback obs(&prefs); + PrefChangeRegistrar registrar; + registrar.Init(&prefs); + registrar.Add(pref_name, obs.GetCallback()); + + PrefChangeRegistrar registrar_two; + registrar_two.Init(&prefs); + + // This should fire the checks in MockPrefChangeCallback::OnPreferenceChanged. + obs.Expect(pref_name, &expected_new_pref_value); + prefs.SetString(pref_name, new_pref_value); + Mock::VerifyAndClearExpectations(&obs); + + // Now try adding a second pref observer. + const char new_pref_value2[] = "http://www.youtube.com/"; + const base::Value expected_new_pref_value2(new_pref_value2); + MockPrefChangeCallback obs2(&prefs); + obs.Expect(pref_name, &expected_new_pref_value2); + obs2.Expect(pref_name, &expected_new_pref_value2); + registrar_two.Add(pref_name, obs2.GetCallback()); + // This should fire the checks in obs and obs2. + prefs.SetString(pref_name, new_pref_value2); + Mock::VerifyAndClearExpectations(&obs); + Mock::VerifyAndClearExpectations(&obs2); + + // Set a recommended value. + const base::Value recommended_pref_value("http://www.gmail.com/"); + obs.Expect(pref_name, &expected_new_pref_value2); + obs2.Expect(pref_name, &expected_new_pref_value2); + // This should fire the checks in obs and obs2 but with an unchanged value + // as the recommended value is being overridden by the user-set value. + prefs.SetRecommendedPref(pref_name, recommended_pref_value.Clone()); + Mock::VerifyAndClearExpectations(&obs); + Mock::VerifyAndClearExpectations(&obs2); + + // Make sure obs2 still works after removing obs. + registrar.Remove(pref_name); + EXPECT_CALL(obs, OnPreferenceChanged(_)).Times(0); + obs2.Expect(pref_name, &expected_new_pref_value); + // This should only fire the observer in obs2. + prefs.SetString(pref_name, new_pref_value); + Mock::VerifyAndClearExpectations(&obs); + Mock::VerifyAndClearExpectations(&obs2); +} + +// Make sure that if a preference changes type, so the wrong type is stored in +// the user pref file, it uses the correct fallback value instead. +TEST(PrefServiceTest, GetValueChangedType) { + const int kTestValue = 10; + TestingPrefServiceSimple prefs; + prefs.registry()->RegisterIntegerPref(kPrefName, kTestValue); + + // Check falling back to a recommended value. + prefs.SetUserPref(kPrefName, std::make_unique("not an integer")); + const PrefService::Preference* pref = prefs.FindPreference(kPrefName); + ASSERT_TRUE(pref); + const base::Value* value = pref->GetValue(); + ASSERT_TRUE(value); + EXPECT_EQ(base::Value::Type::INTEGER, value->type()); + ASSERT_TRUE(value->is_int()); + EXPECT_EQ(kTestValue, value->GetInt()); +} + +TEST(PrefServiceTest, GetValueAndGetRecommendedValue) { + const int kDefaultValue = 5; + const int kUserValue = 10; + const int kRecommendedValue = 15; + TestingPrefServiceSimple prefs; + prefs.registry()->RegisterIntegerPref(kPrefName, kDefaultValue); + + // Create pref with a default value only. + const PrefService::Preference* pref = prefs.FindPreference(kPrefName); + ASSERT_TRUE(pref); + + // Check that GetValue() returns the default value. + const base::Value* value = pref->GetValue(); + ASSERT_TRUE(value); + EXPECT_EQ(base::Value::Type::INTEGER, value->type()); + ASSERT_TRUE(value->is_int()); + EXPECT_EQ(kDefaultValue, value->GetInt()); + + // Check that GetRecommendedValue() returns no value. + value = pref->GetRecommendedValue(); + ASSERT_FALSE(value); + + // Set a user-set value. + prefs.SetUserPref(kPrefName, std::make_unique(kUserValue)); + + // Check that GetValue() returns the user-set value. + value = pref->GetValue(); + ASSERT_TRUE(value); + EXPECT_EQ(base::Value::Type::INTEGER, value->type()); + ASSERT_TRUE(value->is_int()); + EXPECT_EQ(kUserValue, value->GetInt()); + + // Check that GetRecommendedValue() returns no value. + value = pref->GetRecommendedValue(); + ASSERT_FALSE(value); + + // Set a recommended value. + prefs.SetRecommendedPref(kPrefName, + std::make_unique(kRecommendedValue)); + + // Check that GetValue() returns the user-set value. + value = pref->GetValue(); + ASSERT_TRUE(value); + EXPECT_EQ(base::Value::Type::INTEGER, value->type()); + ASSERT_TRUE(value->is_int()); + EXPECT_EQ(kUserValue, value->GetInt()); + + // Check that GetRecommendedValue() returns the recommended value. + value = pref->GetRecommendedValue(); + ASSERT_TRUE(value); + EXPECT_EQ(base::Value::Type::INTEGER, value->type()); + ASSERT_TRUE(value->is_int()); + EXPECT_EQ(kRecommendedValue, value->GetInt()); + + // Remove the user-set value. + prefs.RemoveUserPref(kPrefName); + + // Check that GetValue() returns the recommended value. + value = pref->GetValue(); + ASSERT_TRUE(value); + EXPECT_EQ(base::Value::Type::INTEGER, value->type()); + ASSERT_TRUE(value->is_int()); + EXPECT_EQ(kRecommendedValue, value->GetInt()); + + // Check that GetRecommendedValue() returns the recommended value. + value = pref->GetRecommendedValue(); + ASSERT_TRUE(value); + EXPECT_EQ(base::Value::Type::INTEGER, value->type()); + ASSERT_TRUE(value->is_int()); + EXPECT_EQ(kRecommendedValue, value->GetInt()); +} + +TEST(PrefServiceTest, SetTimeValue_RegularTime) { + TestingPrefServiceSimple prefs; + + // Register a null time as the default. + prefs.registry()->RegisterTimePref(kPrefName, base::Time()); + EXPECT_TRUE(prefs.GetTime(kPrefName).is_null()); + + // Set a time and make sure that we can read it without any loss of precision. + const base::Time time = base::Time::Now(); + prefs.SetTime(kPrefName, time); + EXPECT_EQ(time, prefs.GetTime(kPrefName)); +} + +TEST(PrefServiceTest, SetTimeValue_NullTime) { + TestingPrefServiceSimple prefs; + + // Register a non-null time as the default. + const base::Time default_time = + base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(12345)); + prefs.registry()->RegisterTimePref(kPrefName, default_time); + EXPECT_FALSE(prefs.GetTime(kPrefName).is_null()); + + // Set a null time and make sure that it remains null upon deserialization. + prefs.SetTime(kPrefName, base::Time()); + EXPECT_TRUE(prefs.GetTime(kPrefName).is_null()); +} + +TEST(PrefServiceTest, SetTimeDeltaValue_RegularTimeDelta) { + TestingPrefServiceSimple prefs; + + // Register a zero time delta as the default. + prefs.registry()->RegisterTimeDeltaPref(kPrefName, base::TimeDelta()); + EXPECT_TRUE(prefs.GetTimeDelta(kPrefName).is_zero()); + + // Set a time delta and make sure that we can read it without any loss of + // precision. + const base::TimeDelta delta = base::Time::Now() - base::Time(); + prefs.SetTimeDelta(kPrefName, delta); + EXPECT_EQ(delta, prefs.GetTimeDelta(kPrefName)); +} + +TEST(PrefServiceTest, SetTimeDeltaValue_ZeroTimeDelta) { + TestingPrefServiceSimple prefs; + + // Register a non-zero time delta as the default. + const base::TimeDelta default_delta = base::Microseconds(12345); + prefs.registry()->RegisterTimeDeltaPref(kPrefName, default_delta); + EXPECT_FALSE(prefs.GetTimeDelta(kPrefName).is_zero()); + + // Set a zero time delta and make sure that it remains zero upon + // deserialization. + prefs.SetTimeDelta(kPrefName, base::TimeDelta()); + EXPECT_TRUE(prefs.GetTimeDelta(kPrefName).is_zero()); +} + +// A PrefStore which just stores the last write flags that were used to write +// values to it. +class WriteFlagChecker : public TestingPrefStore { + public: + WriteFlagChecker() {} + + void ReportValueChanged(const std::string& key, uint32_t flags) override { + SetLastWriteFlags(flags); + } + + void SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) override { + SetLastWriteFlags(flags); + } + + void SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) override { + SetLastWriteFlags(flags); + } + + void RemoveValue(const std::string& key, uint32_t flags) override { + SetLastWriteFlags(flags); + } + + uint32_t GetLastFlagsAndClear() { + CHECK(last_write_flags_set_); + uint32_t result = last_write_flags_; + last_write_flags_set_ = false; + last_write_flags_ = WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS; + return result; + } + + bool last_write_flags_set() { return last_write_flags_set_; } + + private: + ~WriteFlagChecker() override {} + + void SetLastWriteFlags(uint32_t flags) { + CHECK(!last_write_flags_set_); + last_write_flags_set_ = true; + last_write_flags_ = flags; + } + + bool last_write_flags_set_ = false; + uint32_t last_write_flags_ = WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS; +}; + +TEST(PrefServiceTest, WriteablePrefStoreFlags) { + scoped_refptr flag_checker(new WriteFlagChecker); + scoped_refptr registry(new PrefRegistrySimple); + PrefServiceFactory factory; + factory.set_user_prefs(flag_checker); + std::unique_ptr prefs(factory.Create(registry.get())); + + // The first 8 bits of write flags are reserved for subclasses. Create a + // custom flag in this range + uint32_t kCustomRegistrationFlag = 1 << 2; + + // A map of the registration flags that will be tested and the write flags + // they are expected to convert to. + struct RegistrationToWriteFlags { + const char* pref_name; + uint32_t registration_flags; + uint32_t write_flags; + }; + const RegistrationToWriteFlags kRegistrationToWriteFlags[] = { + {"none", + PrefRegistry::NO_REGISTRATION_FLAGS, + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS}, + {"lossy", + PrefRegistry::LOSSY_PREF, + WriteablePrefStore::LOSSY_PREF_WRITE_FLAG}, + {"custom", + kCustomRegistrationFlag, + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS}, + {"lossyandcustom", + PrefRegistry::LOSSY_PREF | kCustomRegistrationFlag, + WriteablePrefStore::LOSSY_PREF_WRITE_FLAG}}; + + for (size_t i = 0; i < base::size(kRegistrationToWriteFlags); ++i) { + RegistrationToWriteFlags entry = kRegistrationToWriteFlags[i]; + registry->RegisterDictionaryPref(entry.pref_name, + entry.registration_flags); + + SCOPED_TRACE("Currently testing pref with name: " + + std::string(entry.pref_name)); + + prefs->GetMutableUserPref(entry.pref_name, base::Value::Type::DICTIONARY); + EXPECT_TRUE(flag_checker->last_write_flags_set()); + EXPECT_EQ(entry.write_flags, flag_checker->GetLastFlagsAndClear()); + + prefs->ReportUserPrefChanged(entry.pref_name); + EXPECT_TRUE(flag_checker->last_write_flags_set()); + EXPECT_EQ(entry.write_flags, flag_checker->GetLastFlagsAndClear()); + + prefs->ClearPref(entry.pref_name); + EXPECT_TRUE(flag_checker->last_write_flags_set()); + EXPECT_EQ(entry.write_flags, flag_checker->GetLastFlagsAndClear()); + + prefs->SetUserPrefValue(entry.pref_name, + base::Value(base::Value::Type::DICTIONARY)); + EXPECT_TRUE(flag_checker->last_write_flags_set()); + EXPECT_EQ(entry.write_flags, flag_checker->GetLastFlagsAndClear()); + } +} + +class PrefServiceSetValueTest : public testing::Test { + protected: + static const char kName[]; + static const char kValue[]; + + PrefServiceSetValueTest() : observer_(&prefs_) {} + + TestingPrefServiceSimple prefs_; + MockPrefChangeCallback observer_; +}; + +const char PrefServiceSetValueTest::kName[] = "name"; +const char PrefServiceSetValueTest::kValue[] = "value"; + +TEST_F(PrefServiceSetValueTest, SetStringValue) { + const char default_string[] = "default"; + const base::Value default_value(default_string); + prefs_.registry()->RegisterStringPref(kName, default_string); + + PrefChangeRegistrar registrar; + registrar.Init(&prefs_); + registrar.Add(kName, observer_.GetCallback()); + + // Changing the controlling store from default to user triggers notification. + observer_.Expect(kName, &default_value); + prefs_.Set(kName, default_value); + Mock::VerifyAndClearExpectations(&observer_); + + EXPECT_CALL(observer_, OnPreferenceChanged(_)).Times(0); + prefs_.Set(kName, default_value); + Mock::VerifyAndClearExpectations(&observer_); + + base::Value new_value(kValue); + observer_.Expect(kName, &new_value); + prefs_.Set(kName, new_value); + Mock::VerifyAndClearExpectations(&observer_); +} + +TEST_F(PrefServiceSetValueTest, SetDictionaryValue) { + prefs_.registry()->RegisterDictionaryPref(kName); + PrefChangeRegistrar registrar; + registrar.Init(&prefs_); + registrar.Add(kName, observer_.GetCallback()); + + EXPECT_CALL(observer_, OnPreferenceChanged(_)).Times(0); + prefs_.RemoveUserPref(kName); + Mock::VerifyAndClearExpectations(&observer_); + + base::DictionaryValue new_value; + new_value.SetString(kName, kValue); + observer_.Expect(kName, &new_value); + prefs_.Set(kName, new_value); + Mock::VerifyAndClearExpectations(&observer_); + + EXPECT_CALL(observer_, OnPreferenceChanged(_)).Times(0); + prefs_.Set(kName, new_value); + Mock::VerifyAndClearExpectations(&observer_); + + base::DictionaryValue empty; + observer_.Expect(kName, &empty); + prefs_.Set(kName, empty); + Mock::VerifyAndClearExpectations(&observer_); +} + +TEST_F(PrefServiceSetValueTest, SetListValue) { + prefs_.registry()->RegisterListPref(kName); + PrefChangeRegistrar registrar; + registrar.Init(&prefs_); + registrar.Add(kName, observer_.GetCallback()); + + EXPECT_CALL(observer_, OnPreferenceChanged(_)).Times(0); + prefs_.RemoveUserPref(kName); + Mock::VerifyAndClearExpectations(&observer_); + + base::ListValue new_value; + new_value.Append(kValue); + observer_.Expect(kName, &new_value); + prefs_.Set(kName, new_value); + Mock::VerifyAndClearExpectations(&observer_); + + EXPECT_CALL(observer_, OnPreferenceChanged(_)).Times(0); + prefs_.Set(kName, new_value); + Mock::VerifyAndClearExpectations(&observer_); + + base::ListValue empty; + observer_.Expect(kName, &empty); + prefs_.Set(kName, empty); + Mock::VerifyAndClearExpectations(&observer_); +} + +class PrefValueStoreChangeTest : public testing::Test { + protected: + PrefValueStoreChangeTest() + : user_pref_store_(base::MakeRefCounted()), + pref_registry_(base::MakeRefCounted()) {} + + ~PrefValueStoreChangeTest() override = default; + + void SetUp() override { + auto pref_notifier = std::make_unique(); + auto pref_value_store = std::make_unique( + nullptr /* managed_prefs */, nullptr /* supervised_user_prefs */, + nullptr /* extension_prefs */, nullptr /* standalone_browser_prefs */, + new TestingPrefStore(), user_pref_store_.get(), + nullptr /* recommended_prefs */, pref_registry_->defaults().get(), + pref_notifier.get()); + pref_service_ = std::make_unique( + std::move(pref_notifier), std::move(pref_value_store), user_pref_store_, + nullptr, pref_registry_, base::DoNothing(), false); + pref_registry_->RegisterIntegerPref(kManagedPref, 1); + pref_registry_->RegisterIntegerPref(kRecommendedPref, 2); + pref_registry_->RegisterIntegerPref(kSupervisedPref, 3); + } + + std::unique_ptr pref_service_; + scoped_refptr user_pref_store_; + scoped_refptr pref_registry_; +}; + +// Check that value from the new PrefValueStore will be correctly retrieved. +TEST_F(PrefValueStoreChangeTest, ChangePrefValueStore) { + const PrefService::Preference* preference = + pref_service_->FindPreference(kManagedPref); + EXPECT_TRUE(preference->IsDefaultValue()); + EXPECT_EQ(base::Value(1), *(preference->GetValue())); + const PrefService::Preference* supervised = + pref_service_->FindPreference(kSupervisedPref); + EXPECT_TRUE(supervised->IsDefaultValue()); + EXPECT_EQ(base::Value(3), *(supervised->GetValue())); + const PrefService::Preference* recommended = + pref_service_->FindPreference(kRecommendedPref); + EXPECT_TRUE(recommended->IsDefaultValue()); + EXPECT_EQ(base::Value(2), *(recommended->GetValue())); + + user_pref_store_->SetInteger(kManagedPref, 10); + EXPECT_TRUE(preference->IsUserControlled()); + ASSERT_EQ(base::Value(10), *(preference->GetValue())); + + scoped_refptr managed_pref_store = + base::MakeRefCounted(); + pref_service_->ChangePrefValueStore( + managed_pref_store.get(), nullptr /* supervised_user_prefs */, + nullptr /* extension_prefs */, nullptr /* recommended_prefs */); + EXPECT_TRUE(preference->IsUserControlled()); + ASSERT_EQ(base::Value(10), *(preference->GetValue())); + + // Test setting a managed pref after overriding the managed PrefStore. + managed_pref_store->SetInteger(kManagedPref, 20); + EXPECT_TRUE(preference->IsManaged()); + ASSERT_EQ(base::Value(20), *(preference->GetValue())); + + // Test overriding the supervised and recommended PrefStore with already set + // prefs. + scoped_refptr supervised_pref_store = + base::MakeRefCounted(); + scoped_refptr recommened_pref_store = + base::MakeRefCounted(); + supervised_pref_store->SetInteger(kManagedPref, 30); + supervised_pref_store->SetInteger(kSupervisedPref, 31); + recommened_pref_store->SetInteger(kManagedPref, 40); + recommened_pref_store->SetInteger(kRecommendedPref, 41); + pref_service_->ChangePrefValueStore( + nullptr /* managed_prefs */, supervised_pref_store.get(), + nullptr /* extension_prefs */, recommened_pref_store.get()); + EXPECT_TRUE(preference->IsManaged()); + ASSERT_EQ(base::Value(20), *(preference->GetValue())); + EXPECT_TRUE(supervised->IsManagedByCustodian()); + EXPECT_EQ(base::Value(31), *(supervised->GetValue())); + EXPECT_TRUE(recommended->IsRecommended()); + EXPECT_EQ(base::Value(41), *(recommended->GetValue())); +} + +// Tests that PrefChangeRegistrar works after PrefValueStore is changed. +TEST_F(PrefValueStoreChangeTest, PrefChangeRegistrar) { + MockPrefChangeCallback obs(pref_service_.get()); + PrefChangeRegistrar registrar; + registrar.Init(pref_service_.get()); + registrar.Add(kManagedPref, obs.GetCallback()); + registrar.Add(kSupervisedPref, obs.GetCallback()); + registrar.Add(kRecommendedPref, obs.GetCallback()); + + base::Value expected_value(10); + obs.Expect(kManagedPref, &expected_value); + user_pref_store_->SetInteger(kManagedPref, 10); + Mock::VerifyAndClearExpectations(&obs); + expected_value = base::Value(11); + obs.Expect(kRecommendedPref, &expected_value); + user_pref_store_->SetInteger(kRecommendedPref, 11); + Mock::VerifyAndClearExpectations(&obs); + + // Test overriding the managed and supervised PrefStore with already set + // prefs. + scoped_refptr managed_pref_store = + base::MakeRefCounted(); + scoped_refptr supervised_pref_store = + base::MakeRefCounted(); + // Update |kManagedPref| before changing the PrefValueStore, the + // PrefChangeRegistrar should get notified on |kManagedPref| as its value + // changes. + managed_pref_store->SetInteger(kManagedPref, 20); + // Due to store precedence, the value of |kRecommendedPref| will not be + // changed so PrefChangeRegistrar will not be notified. + managed_pref_store->SetInteger(kRecommendedPref, 11); + supervised_pref_store->SetInteger(kManagedPref, 30); + supervised_pref_store->SetInteger(kRecommendedPref, 21); + expected_value = base::Value(20); + obs.Expect(kManagedPref, &expected_value); + pref_service_->ChangePrefValueStore( + managed_pref_store.get(), supervised_pref_store.get(), + nullptr /* extension_prefs */, nullptr /* recommended_prefs */); + Mock::VerifyAndClearExpectations(&obs); + + // Update a pref value after PrefValueStore change, it should also work. + expected_value = base::Value(31); + obs.Expect(kSupervisedPref, &expected_value); + supervised_pref_store->SetInteger(kSupervisedPref, 31); + Mock::VerifyAndClearExpectations(&obs); +} + +class PrefStandaloneBrowserPrefsTest : public testing::Test { + protected: + PrefStandaloneBrowserPrefsTest() + : user_pref_store_(base::MakeRefCounted()), + standalone_browser_pref_store_( + base::MakeRefCounted()), + pref_registry_(base::MakeRefCounted()) {} + + ~PrefStandaloneBrowserPrefsTest() override = default; + + void SetUp() override { + auto pref_notifier = std::make_unique(); + auto pref_value_store = std::make_unique( + nullptr /* managed_prefs */, nullptr /* supervised_user_prefs */, + nullptr /* extension_prefs */, standalone_browser_pref_store_.get(), + new TestingPrefStore(), user_pref_store_.get(), + nullptr /* recommended_prefs */, pref_registry_->defaults().get(), + pref_notifier.get()); + pref_service_ = std::make_unique( + std::move(pref_notifier), std::move(pref_value_store), user_pref_store_, + standalone_browser_pref_store_, pref_registry_, base::DoNothing(), + false); + pref_registry_->RegisterIntegerPref(kStandaloneBrowserPref, 4); + } + + std::unique_ptr pref_service_; + scoped_refptr user_pref_store_; + scoped_refptr standalone_browser_pref_store_; + scoped_refptr pref_registry_; +}; + +// Check that the standalone browser pref store is correctly initialized, +// written to, read, and has correct precedence. +TEST_F(PrefStandaloneBrowserPrefsTest, CheckStandaloneBrowserPref) { + const PrefService::Preference* preference = + pref_service_->FindPreference(kStandaloneBrowserPref); + EXPECT_TRUE(preference->IsDefaultValue()); + EXPECT_EQ(base::Value(4), *(preference->GetValue())); + user_pref_store_->SetInteger(kStandaloneBrowserPref, 11); + EXPECT_EQ(base::Value(11), *(preference->GetValue())); + // The standalone_browser_pref_store has higher precedence. + standalone_browser_pref_store_->SetInteger(kStandaloneBrowserPref, 10); + ASSERT_EQ(base::Value(10), *(preference->GetValue())); + // Removing user_pref_store value shouldn't change the pref value. + user_pref_store_->RemoveValue(kStandaloneBrowserPref, + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + ASSERT_EQ(base::Value(10), *(preference->GetValue())); + // Now removing the standalone_browser_pref_store value should revert the + // value to default. + standalone_browser_pref_store_->RemoveValue( + kStandaloneBrowserPref, WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + EXPECT_EQ(base::Value(4), *(preference->GetValue())); +} diff --git a/src/components/prefs/pref_store.cc b/src/components/prefs/pref_store.cc new file mode 100644 index 0000000000..2ae0afcb08 --- /dev/null +++ b/src/components/prefs/pref_store.cc @@ -0,0 +1,13 @@ +// Copyright (c) 2012 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 "components/prefs/pref_store.h" + +bool PrefStore::HasObservers() const { + return false; +} + +bool PrefStore::IsInitializationComplete() const { + return true; +} diff --git a/src/components/prefs/pref_store.h b/src/components/prefs/pref_store.h new file mode 100644 index 0000000000..4c0a4bd0b6 --- /dev/null +++ b/src/components/prefs/pref_store.h @@ -0,0 +1,67 @@ +// Copyright (c) 2012 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_PREFS_PREF_STORE_H_ +#define COMPONENTS_PREFS_PREF_STORE_H_ + +#include +#include + +#include "base/memory/ref_counted.h" +#include "components/prefs/prefs_export.h" + +namespace base { +class DictionaryValue; +class Value; +} + +// This is an abstract interface for reading and writing from/to a persistent +// preference store, used by PrefService. An implementation using a JSON file +// can be found in JsonPrefStore, while an implementation without any backing +// store for testing can be found in TestingPrefStore. Furthermore, there is +// CommandLinePrefStore, which bridges command line options to preferences and +// ConfigurationPolicyPrefStore, which is used for hooking up configuration +// policy with the preference subsystem. +class COMPONENTS_PREFS_EXPORT PrefStore : public base::RefCounted { + public: + // Observer interface for monitoring PrefStore. + class COMPONENTS_PREFS_EXPORT Observer { + public: + // Called when the value for the given |key| in the store changes. + virtual void OnPrefValueChanged(const std::string& key) = 0; + // Notification about the PrefStore being fully initialized. + virtual void OnInitializationCompleted(bool succeeded) = 0; + + protected: + virtual ~Observer() {} + }; + + PrefStore() {} + + PrefStore(const PrefStore&) = delete; + PrefStore& operator=(const PrefStore&) = delete; + + // Add and remove observers. + virtual void AddObserver(Observer* observer) {} + virtual void RemoveObserver(Observer* observer) {} + virtual bool HasObservers() const; + + // Whether the store has completed all asynchronous initialization. + virtual bool IsInitializationComplete() const; + + // Get the value for a given preference |key| and stores it in |*result|. + // |*result| is only modified if the return value is true and if |result| + // is not NULL. Ownership of the |*result| value remains with the PrefStore. + virtual bool GetValue(const std::string& key, + const base::Value** result) const = 0; + + // Get all the values. Never returns a null pointer. + virtual std::unique_ptr GetValues() const = 0; + + protected: + friend class base::RefCounted; + virtual ~PrefStore() {} +}; + +#endif // COMPONENTS_PREFS_PREF_STORE_H_ diff --git a/src/components/prefs/pref_store_observer_mock.cc b/src/components/prefs/pref_store_observer_mock.cc new file mode 100644 index 0000000000..a03c858005 --- /dev/null +++ b/src/components/prefs/pref_store_observer_mock.cc @@ -0,0 +1,29 @@ +// Copyright (c) 2011 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 "components/prefs/pref_store_observer_mock.h" + +#include "testing/gtest/include/gtest/gtest.h" + +PrefStoreObserverMock::PrefStoreObserverMock() + : initialized(false), initialization_success(false) {} + +PrefStoreObserverMock::~PrefStoreObserverMock() {} + +void PrefStoreObserverMock::VerifyAndResetChangedKey( + const std::string& expected) { + EXPECT_EQ(1u, changed_keys.size()); + if (changed_keys.size() >= 1) + EXPECT_EQ(expected, changed_keys.front()); + changed_keys.clear(); +} + +void PrefStoreObserverMock::OnPrefValueChanged(const std::string& key) { + changed_keys.push_back(key); +} + +void PrefStoreObserverMock::OnInitializationCompleted(bool success) { + initialized = true; + initialization_success = success; +} diff --git a/src/components/prefs/pref_store_observer_mock.h b/src/components/prefs/pref_store_observer_mock.h new file mode 100644 index 0000000000..9523b211a6 --- /dev/null +++ b/src/components/prefs/pref_store_observer_mock.h @@ -0,0 +1,35 @@ +// Copyright (c) 2011 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_PREFS_PREF_STORE_OBSERVER_MOCK_H_ +#define COMPONENTS_PREFS_PREF_STORE_OBSERVER_MOCK_H_ + +#include +#include + +#include "base/compiler_specific.h" +#include "components/prefs/pref_store.h" + +// A mock implementation of PrefStore::Observer. +class PrefStoreObserverMock : public PrefStore::Observer { + public: + PrefStoreObserverMock(); + + PrefStoreObserverMock(const PrefStoreObserverMock&) = delete; + PrefStoreObserverMock& operator=(const PrefStoreObserverMock&) = delete; + + ~PrefStoreObserverMock() override; + + void VerifyAndResetChangedKey(const std::string& expected); + + // PrefStore::Observer implementation + void OnPrefValueChanged(const std::string& key) override; + void OnInitializationCompleted(bool success) override; + + std::vector changed_keys; + bool initialized; + bool initialization_success; // Only valid if |initialized|. +}; + +#endif // COMPONENTS_PREFS_PREF_STORE_OBSERVER_MOCK_H_ diff --git a/src/components/prefs/pref_test_utils.cc b/src/components/prefs/pref_test_utils.cc new file mode 100644 index 0000000000..8f7bcd8ed5 --- /dev/null +++ b/src/components/prefs/pref_test_utils.cc @@ -0,0 +1,27 @@ +// Copyright (c) 2019 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 "components/prefs/pref_test_utils.h" + +#include "base/run_loop.h" +#include "base/test/bind.h" +#include "base/values.h" +#include "components/prefs/pref_change_registrar.h" +#include "components/prefs/pref_service.h" + +void WaitForPrefValue(PrefService* pref_service, + const std::string& path, + const base::Value& value) { + if (value == *(pref_service->Get(path))) + return; + + base::RunLoop run_loop; + PrefChangeRegistrar pref_changes; + pref_changes.Init(pref_service); + pref_changes.Add(path, base::BindLambdaForTesting([&]() { + if (value == *(pref_service->Get(path))) + run_loop.Quit(); + })); + run_loop.Run(); +} diff --git a/src/components/prefs/pref_test_utils.h b/src/components/prefs/pref_test_utils.h new file mode 100644 index 0000000000..9804c69766 --- /dev/null +++ b/src/components/prefs/pref_test_utils.h @@ -0,0 +1,20 @@ +// Copyright (c) 2019 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_PREFS_PREF_TEST_UTILS_H_ +#define COMPONENTS_PREFS_PREF_TEST_UTILS_H_ + +#include + +class PrefService; +namespace base { +class Value; +} + +// Spins a RunLoop until the preference at |path| has value |value|. +void WaitForPrefValue(PrefService* pref_service, + const std::string& path, + const base::Value& value); + +#endif // COMPONENTS_PREFS_PREF_TEST_UTILS_H_ diff --git a/src/components/prefs/pref_value_map.cc b/src/components/prefs/pref_value_map.cc new file mode 100644 index 0000000000..168638d220 --- /dev/null +++ b/src/components/prefs/pref_value_map.cc @@ -0,0 +1,178 @@ +// Copyright (c) 2011 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 "components/prefs/pref_value_map.h" + +#include +#include +#include +#include + +#include "base/values.h" + +PrefValueMap::PrefValueMap() {} + +PrefValueMap::~PrefValueMap() {} + +bool PrefValueMap::GetValue(const std::string& key, + const base::Value** value) const { + auto it = prefs_.find(key); + if (it == prefs_.end()) + return false; + + if (value) + *value = &it->second; + + return true; +} + +bool PrefValueMap::GetValue(const std::string& key, base::Value** value) { + auto it = prefs_.find(key); + if (it == prefs_.end()) + return false; + + if (value) + *value = &it->second; + + return true; +} + +bool PrefValueMap::SetValue(const std::string& key, base::Value value) { + base::Value& existing_value = prefs_[key]; + if (value == existing_value) + return false; + + existing_value = std::move(value); + return true; +} + +bool PrefValueMap::RemoveValue(const std::string& key) { + return prefs_.erase(key) != 0; +} + +void PrefValueMap::Clear() { + prefs_.clear(); +} + +void PrefValueMap::ClearWithPrefix(const std::string& prefix) { + Map::iterator low = prefs_.lower_bound(prefix); + // Appending maximum possible character so that there will be no string with + // prefix |prefix| that we may miss. + Map::iterator high = prefs_.upper_bound(prefix + char(CHAR_MAX)); + prefs_.erase(low, high); +} + +void PrefValueMap::Swap(PrefValueMap* other) { + prefs_.swap(other->prefs_); +} + +PrefValueMap::iterator PrefValueMap::begin() { + return prefs_.begin(); +} + +PrefValueMap::iterator PrefValueMap::end() { + return prefs_.end(); +} + +PrefValueMap::const_iterator PrefValueMap::begin() const { + return prefs_.begin(); +} + +PrefValueMap::const_iterator PrefValueMap::end() const { + return prefs_.end(); +} + +bool PrefValueMap::empty() const { + return prefs_.empty(); +} + +bool PrefValueMap::GetBoolean(const std::string& key, bool* value) const { + const base::Value* stored_value = nullptr; + if (GetValue(key, &stored_value) && stored_value->is_bool()) { + *value = stored_value->GetBool(); + return true; + } + return false; +} + +void PrefValueMap::SetBoolean(const std::string& key, bool value) { + SetValue(key, base::Value(value)); +} + +bool PrefValueMap::GetString(const std::string& key, std::string* value) const { + const base::Value* stored_value = nullptr; + if (GetValue(key, &stored_value) && stored_value->is_string()) { + *value = stored_value->GetString(); + return true; + } + return false; +} + +void PrefValueMap::SetString(const std::string& key, const std::string& value) { + SetValue(key, base::Value(value)); +} + +bool PrefValueMap::GetInteger(const std::string& key, int* value) const { + const base::Value* stored_value = nullptr; + if (GetValue(key, &stored_value) && stored_value->is_int()) { + *value = stored_value->GetInt(); + return true; + } + return false; +} + +void PrefValueMap::SetInteger(const std::string& key, const int value) { + SetValue(key, base::Value(value)); +} + +void PrefValueMap::SetDouble(const std::string& key, const double value) { + SetValue(key, base::Value(value)); +} + +void PrefValueMap::GetDifferingKeys( + const PrefValueMap* other, + std::vector* differing_keys) const { + differing_keys->clear(); + + // Put everything into ordered maps. + std::map this_prefs; + std::map other_prefs; + for (const auto& pair : prefs_) + this_prefs.emplace(pair.first, &pair.second); + for (const auto& pair : other->prefs_) + other_prefs.emplace(pair.first, &pair.second); + + // Walk over the maps in lockstep, adding everything that is different. + auto this_pref = this_prefs.begin(); + auto other_pref = other_prefs.begin(); + while (this_pref != this_prefs.end() && other_pref != other_prefs.end()) { + const int diff = this_pref->first.compare(other_pref->first); + if (diff == 0) { + if (*this_pref->second != *other_pref->second) + differing_keys->push_back(this_pref->first); + ++this_pref; + ++other_pref; + } else if (diff < 0) { + differing_keys->push_back(this_pref->first); + ++this_pref; + } else if (diff > 0) { + differing_keys->push_back(other_pref->first); + ++other_pref; + } + } + + // Add the remaining entries. + for (; this_pref != this_prefs.end(); ++this_pref) + differing_keys->push_back(this_pref->first); + for (; other_pref != other_prefs.end(); ++other_pref) + differing_keys->push_back(other_pref->first); +} + +std::unique_ptr PrefValueMap::AsDictionaryValue() const { + auto dictionary = std::make_unique(); + for (const auto& value : prefs_) + dictionary->SetPath(value.first, value.second.Clone()); + + return dictionary; +} diff --git a/src/components/prefs/pref_value_map.h b/src/components/prefs/pref_value_map.h new file mode 100644 index 0000000000..94f26b2738 --- /dev/null +++ b/src/components/prefs/pref_value_map.h @@ -0,0 +1,99 @@ +// Copyright (c) 2011 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_PREFS_PREF_VALUE_MAP_H_ +#define COMPONENTS_PREFS_PREF_VALUE_MAP_H_ + +#include +#include +#include +#include + +#include "components/prefs/prefs_export.h" + +namespace base { +class DictionaryValue; +class Value; +} + +// A generic string to value map used by the PrefStore implementations. +class COMPONENTS_PREFS_EXPORT PrefValueMap { + public: + using Map = std::map; + using iterator = Map::iterator; + using const_iterator = Map::const_iterator; + + PrefValueMap(); + + PrefValueMap(const PrefValueMap&) = delete; + PrefValueMap& operator=(const PrefValueMap&) = delete; + + virtual ~PrefValueMap(); + + // Gets the value for |key| and stores it in |value|. Ownership remains with + // the map. Returns true if a value is present. If not, |value| is not + // touched. + bool GetValue(const std::string& key, const base::Value** value) const; + bool GetValue(const std::string& key, base::Value** value); + + // Sets a new |value| for |key|. Returns true if the value changed. + bool SetValue(const std::string& key, base::Value value); + + // Removes the value for |key| from the map. Returns true if a value was + // removed. + bool RemoveValue(const std::string& key); + + // Clears the map. + void Clear(); + + // Clear the preferences which start with |prefix|. + void ClearWithPrefix(const std::string& prefix); + + // Swaps the contents of two maps. + void Swap(PrefValueMap* other); + + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + bool empty() const; + + // Gets a boolean value for |key| and stores it in |value|. Returns true if + // the value was found and of the proper type. + bool GetBoolean(const std::string& key, bool* value) const; + + // Sets the value for |key| to the boolean |value|. + void SetBoolean(const std::string& key, bool value); + + // Gets a string value for |key| and stores it in |value|. Returns true if + // the value was found and of the proper type. + bool GetString(const std::string& key, std::string* value) const; + + // Sets the value for |key| to the string |value|. + void SetString(const std::string& key, const std::string& value); + + // Gets an int value for |key| and stores it in |value|. Returns true if + // the value was found and of the proper type. + bool GetInteger(const std::string& key, int* value) const; + + // Sets the value for |key| to the int |value|. + void SetInteger(const std::string& key, const int value); + + // Sets the value for |key| to the double |value|. + void SetDouble(const std::string& key, const double value); + + // Compares this value map against |other| and stores all key names that have + // different values in |differing_keys|. This includes keys that are present + // only in one of the maps. + void GetDifferingKeys(const PrefValueMap* other, + std::vector* differing_keys) const; + + // Copies the map into a dictionary value. + std::unique_ptr AsDictionaryValue() const; + + private: + Map prefs_; +}; + +#endif // COMPONENTS_PREFS_PREF_VALUE_MAP_H_ diff --git a/src/components/prefs/pref_value_map_unittest.cc b/src/components/prefs/pref_value_map_unittest.cc new file mode 100644 index 0000000000..234a76ca92 --- /dev/null +++ b/src/components/prefs/pref_value_map_unittest.cc @@ -0,0 +1,169 @@ +// Copyright (c) 2011 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 "components/prefs/pref_value_map.h" + +#include "base/values.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace base { +namespace { + +TEST(PrefValueMapTest, SetValue) { + PrefValueMap map; + const Value* result = nullptr; + EXPECT_FALSE(map.GetValue("key", &result)); + EXPECT_FALSE(result); + + EXPECT_TRUE(map.SetValue("key", Value("test"))); + EXPECT_FALSE(map.SetValue("key", Value("test"))); + EXPECT_TRUE(map.SetValue("key", Value("hi mom!"))); + + EXPECT_TRUE(map.GetValue("key", &result)); + EXPECT_TRUE(Value("hi mom!").Equals(result)); +} + +TEST(PrefValueMapTest, GetAndSetIntegerValue) { + PrefValueMap map; + ASSERT_TRUE(map.SetValue("key", Value(5))); + + int int_value = 0; + EXPECT_TRUE(map.GetInteger("key", &int_value)); + EXPECT_EQ(5, int_value); + + map.SetInteger("key", -14); + EXPECT_TRUE(map.GetInteger("key", &int_value)); + EXPECT_EQ(-14, int_value); +} + +TEST(PrefValueMapTest, SetDoubleValue) { + PrefValueMap map; + ASSERT_TRUE(map.SetValue("key", Value(5.5))); + + const Value* result = nullptr; + ASSERT_TRUE(map.GetValue("key", &result)); + EXPECT_DOUBLE_EQ(5.5, result->GetDouble()); +} + +TEST(PrefValueMapTest, RemoveValue) { + PrefValueMap map; + EXPECT_FALSE(map.RemoveValue("key")); + + EXPECT_TRUE(map.SetValue("key", Value("test"))); + EXPECT_TRUE(map.GetValue("key", nullptr)); + + EXPECT_TRUE(map.RemoveValue("key")); + EXPECT_FALSE(map.GetValue("key", nullptr)); + + EXPECT_FALSE(map.RemoveValue("key")); +} + +TEST(PrefValueMapTest, Clear) { + PrefValueMap map; + EXPECT_TRUE(map.SetValue("key", Value("test"))); + EXPECT_TRUE(map.GetValue("key", nullptr)); + + map.Clear(); + + EXPECT_FALSE(map.GetValue("key", nullptr)); +} + +TEST(PrefValueMapTest, ClearWithPrefix) { + { + PrefValueMap map; + EXPECT_TRUE(map.SetValue("a", Value("test"))); + EXPECT_TRUE(map.SetValue("b", Value("test"))); + EXPECT_TRUE(map.SetValue("bb", Value("test"))); + EXPECT_TRUE(map.SetValue("z", Value("test"))); + + map.ClearWithPrefix("b"); + + EXPECT_TRUE(map.GetValue("a", nullptr)); + EXPECT_FALSE(map.GetValue("b", nullptr)); + EXPECT_FALSE(map.GetValue("bb", nullptr)); + EXPECT_TRUE(map.GetValue("z", nullptr)); + } + { + PrefValueMap map; + EXPECT_TRUE(map.SetValue("a", Value("test"))); + EXPECT_TRUE(map.SetValue("b", Value("test"))); + EXPECT_TRUE(map.SetValue("bb", Value("test"))); + EXPECT_TRUE(map.SetValue("z", Value("test"))); + + map.ClearWithPrefix("z"); + + EXPECT_TRUE(map.GetValue("a", nullptr)); + EXPECT_TRUE(map.GetValue("b", nullptr)); + EXPECT_TRUE(map.GetValue("bb", nullptr)); + EXPECT_FALSE(map.GetValue("z", nullptr)); + } + { + PrefValueMap map; + EXPECT_TRUE(map.SetValue("a", Value("test"))); + EXPECT_TRUE(map.SetValue("b", Value("test"))); + EXPECT_TRUE(map.SetValue("bb", Value("test"))); + EXPECT_TRUE(map.SetValue("z", Value("test"))); + + map.ClearWithPrefix("c"); + + EXPECT_TRUE(map.GetValue("a", nullptr)); + EXPECT_TRUE(map.GetValue("b", nullptr)); + EXPECT_TRUE(map.GetValue("bb", nullptr)); + EXPECT_TRUE(map.GetValue("z", nullptr)); + } +} + +TEST(PrefValueMapTest, GetDifferingKeys) { + PrefValueMap reference; + EXPECT_TRUE(reference.SetValue("b", Value("test"))); + EXPECT_TRUE(reference.SetValue("c", Value("test"))); + EXPECT_TRUE(reference.SetValue("e", Value("test"))); + + PrefValueMap check; + std::vector differing_paths; + std::vector expected_differing_paths; + + reference.GetDifferingKeys(&check, &differing_paths); + expected_differing_paths.push_back("b"); + expected_differing_paths.push_back("c"); + expected_differing_paths.push_back("e"); + EXPECT_EQ(expected_differing_paths, differing_paths); + + EXPECT_TRUE(check.SetValue("a", Value("test"))); + EXPECT_TRUE(check.SetValue("c", Value("test"))); + EXPECT_TRUE(check.SetValue("d", Value("test"))); + + reference.GetDifferingKeys(&check, &differing_paths); + expected_differing_paths.clear(); + expected_differing_paths.push_back("a"); + expected_differing_paths.push_back("b"); + expected_differing_paths.push_back("d"); + expected_differing_paths.push_back("e"); + EXPECT_EQ(expected_differing_paths, differing_paths); +} + +TEST(PrefValueMapTest, SwapTwoMaps) { + PrefValueMap first_map; + EXPECT_TRUE(first_map.SetValue("a", Value("test"))); + EXPECT_TRUE(first_map.SetValue("b", Value("test"))); + EXPECT_TRUE(first_map.SetValue("c", Value("test"))); + + PrefValueMap second_map; + EXPECT_TRUE(second_map.SetValue("d", Value("test"))); + EXPECT_TRUE(second_map.SetValue("e", Value("test"))); + EXPECT_TRUE(second_map.SetValue("f", Value("test"))); + + first_map.Swap(&second_map); + + EXPECT_TRUE(first_map.GetValue("d", nullptr)); + EXPECT_TRUE(first_map.GetValue("e", nullptr)); + EXPECT_TRUE(first_map.GetValue("f", nullptr)); + + EXPECT_TRUE(second_map.GetValue("a", nullptr)); + EXPECT_TRUE(second_map.GetValue("b", nullptr)); + EXPECT_TRUE(second_map.GetValue("c", nullptr)); +} + +} // namespace +} // namespace base diff --git a/src/components/prefs/pref_value_store.cc b/src/components/prefs/pref_value_store.cc new file mode 100644 index 0000000000..efa40276ca --- /dev/null +++ b/src/components/prefs/pref_value_store.cc @@ -0,0 +1,313 @@ +// Copyright (c) 2012 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 "components/prefs/pref_value_store.h" + +#include + +#include "base/logging.h" +#include "components/prefs/pref_notifier.h" +#include "components/prefs/pref_observer.h" + +PrefValueStore::PrefStoreKeeper::PrefStoreKeeper() + : pref_value_store_(nullptr), type_(PrefValueStore::INVALID_STORE) {} + +PrefValueStore::PrefStoreKeeper::~PrefStoreKeeper() { + if (pref_store_) { + pref_store_->RemoveObserver(this); + pref_store_ = nullptr; + } + pref_value_store_ = nullptr; +} + +void PrefValueStore::PrefStoreKeeper::Initialize( + PrefValueStore* store, + PrefStore* pref_store, + PrefValueStore::PrefStoreType type) { + if (pref_store_) { + pref_store_->RemoveObserver(this); + DCHECK(!pref_store_->HasObservers()); + } + type_ = type; + pref_value_store_ = store; + pref_store_ = pref_store; + if (pref_store_) + pref_store_->AddObserver(this); +} + +void PrefValueStore::PrefStoreKeeper::OnPrefValueChanged( + const std::string& key) { + pref_value_store_->OnPrefValueChanged(type_, key); +} + +void PrefValueStore::PrefStoreKeeper::OnInitializationCompleted( + bool succeeded) { + pref_value_store_->OnInitializationCompleted(type_, succeeded); +} + +PrefValueStore::PrefValueStore(PrefStore* managed_prefs, + PrefStore* supervised_user_prefs, + PrefStore* extension_prefs, + PrefStore* standalone_browser_prefs, + PrefStore* command_line_prefs, + PrefStore* user_prefs, + PrefStore* recommended_prefs, + PrefStore* default_prefs, + PrefNotifier* pref_notifier, + std::unique_ptr delegate) + : pref_notifier_(pref_notifier), + initialization_failed_(false), + delegate_(std::move(delegate)) { + InitPrefStore(MANAGED_STORE, managed_prefs); + InitPrefStore(SUPERVISED_USER_STORE, supervised_user_prefs); + InitPrefStore(EXTENSION_STORE, extension_prefs); + InitPrefStore(STANDALONE_BROWSER_STORE, standalone_browser_prefs); + InitPrefStore(COMMAND_LINE_STORE, command_line_prefs); + InitPrefStore(USER_STORE, user_prefs); + InitPrefStore(RECOMMENDED_STORE, recommended_prefs); + InitPrefStore(DEFAULT_STORE, default_prefs); + + CheckInitializationCompleted(); + if (delegate_) { + delegate_->Init(managed_prefs, supervised_user_prefs, extension_prefs, + standalone_browser_prefs, command_line_prefs, user_prefs, + recommended_prefs, default_prefs, pref_notifier); + } +} + +PrefValueStore::~PrefValueStore() {} + +std::unique_ptr PrefValueStore::CloneAndSpecialize( + PrefStore* managed_prefs, + PrefStore* supervised_user_prefs, + PrefStore* extension_prefs, + PrefStore* standalone_browser_prefs, + PrefStore* command_line_prefs, + PrefStore* user_prefs, + PrefStore* recommended_prefs, + PrefStore* default_prefs, + PrefNotifier* pref_notifier, + std::unique_ptr delegate) { + DCHECK(pref_notifier); + if (!managed_prefs) + managed_prefs = GetPrefStore(MANAGED_STORE); + if (!supervised_user_prefs) + supervised_user_prefs = GetPrefStore(SUPERVISED_USER_STORE); + if (!extension_prefs) + extension_prefs = GetPrefStore(EXTENSION_STORE); + if (!standalone_browser_prefs) + standalone_browser_prefs = GetPrefStore(STANDALONE_BROWSER_STORE); + if (!command_line_prefs) + command_line_prefs = GetPrefStore(COMMAND_LINE_STORE); + if (!user_prefs) + user_prefs = GetPrefStore(USER_STORE); + if (!recommended_prefs) + recommended_prefs = GetPrefStore(RECOMMENDED_STORE); + if (!default_prefs) + default_prefs = GetPrefStore(DEFAULT_STORE); + + return std::make_unique( + managed_prefs, supervised_user_prefs, extension_prefs, + standalone_browser_prefs, command_line_prefs, user_prefs, + recommended_prefs, default_prefs, pref_notifier, std::move(delegate)); +} + +bool PrefValueStore::GetValue(const std::string& name, + base::Value::Type type, + const base::Value** out_value) const { + // Check the |PrefStore|s in order of their priority from highest to lowest, + // looking for the first preference value with the given |name| and |type|. + for (size_t i = 0; i <= PREF_STORE_TYPE_MAX; ++i) { + if (GetValueFromStoreWithType(name, type, static_cast(i), + out_value)) + return true; + } + return false; +} + +bool PrefValueStore::GetRecommendedValue(const std::string& name, + base::Value::Type type, + const base::Value** out_value) const { + return GetValueFromStoreWithType(name, type, RECOMMENDED_STORE, out_value); +} + +void PrefValueStore::NotifyPrefChanged( + const std::string& path, + PrefValueStore::PrefStoreType new_store) { + DCHECK(new_store != INVALID_STORE); + // A notification is sent when the pref value in any store changes. If this + // store is currently being overridden by a higher-priority store, the + // effective value of the pref will not have changed. + pref_notifier_->OnPreferenceChanged(path); + if (!pref_changed_callback_.is_null()) + pref_changed_callback_.Run(path); +} + +bool PrefValueStore::PrefValueInManagedStore(const std::string& name) const { + return PrefValueInStore(name, MANAGED_STORE); +} + +bool PrefValueStore::PrefValueInSupervisedStore(const std::string& name) const { + return PrefValueInStore(name, SUPERVISED_USER_STORE); +} + +bool PrefValueStore::PrefValueInExtensionStore(const std::string& name) const { + return PrefValueInStore(name, EXTENSION_STORE); +} + +bool PrefValueStore::PrefValueInUserStore(const std::string& name) const { + return PrefValueInStore(name, USER_STORE); +} + +bool PrefValueStore::PrefValueFromExtensionStore( + const std::string& name) const { + return ControllingPrefStoreForPref(name) == EXTENSION_STORE; +} + +bool PrefValueStore::PrefValueFromUserStore(const std::string& name) const { + return ControllingPrefStoreForPref(name) == USER_STORE; +} + +bool PrefValueStore::PrefValueFromRecommendedStore( + const std::string& name) const { + return ControllingPrefStoreForPref(name) == RECOMMENDED_STORE; +} + +bool PrefValueStore::PrefValueFromDefaultStore(const std::string& name) const { + return ControllingPrefStoreForPref(name) == DEFAULT_STORE; +} + +bool PrefValueStore::PrefValueUserModifiable(const std::string& name) const { + PrefStoreType effective_store = ControllingPrefStoreForPref(name); + return effective_store >= USER_STORE || + effective_store == INVALID_STORE; +} + +bool PrefValueStore::PrefValueExtensionModifiable( + const std::string& name) const { + PrefStoreType effective_store = ControllingPrefStoreForPref(name); + return effective_store >= EXTENSION_STORE || + effective_store == INVALID_STORE; +} + +void PrefValueStore::UpdateCommandLinePrefStore(PrefStore* command_line_prefs) { + InitPrefStore(COMMAND_LINE_STORE, command_line_prefs); + if (delegate_) + delegate_->UpdateCommandLinePrefStore(command_line_prefs); +} + +bool PrefValueStore::IsInitializationComplete() const { + for (size_t i = 0; i <= PREF_STORE_TYPE_MAX; ++i) { + const PrefStore* pref_store = GetPrefStore(static_cast(i)); + if (pref_store && !pref_store->IsInitializationComplete()) + return false; + } + return true; +} + +bool PrefValueStore::HasPrefStore(PrefStoreType type) const { + return GetPrefStore(type); +} + +bool PrefValueStore::PrefValueInStore( + const std::string& name, + PrefValueStore::PrefStoreType store) const { + // Declare a temp Value* and call GetValueFromStore, + // ignoring the output value. + const base::Value* tmp_value = nullptr; + return GetValueFromStore(name, store, &tmp_value); +} + +bool PrefValueStore::PrefValueInStoreRange( + const std::string& name, + PrefValueStore::PrefStoreType first_checked_store, + PrefValueStore::PrefStoreType last_checked_store) const { + if (first_checked_store > last_checked_store) { + NOTREACHED(); + return false; + } + + for (size_t i = first_checked_store; + i <= static_cast(last_checked_store); ++i) { + if (PrefValueInStore(name, static_cast(i))) + return true; + } + return false; +} + +PrefValueStore::PrefStoreType PrefValueStore::ControllingPrefStoreForPref( + const std::string& name) const { + for (size_t i = 0; i <= PREF_STORE_TYPE_MAX; ++i) { + if (PrefValueInStore(name, static_cast(i))) + return static_cast(i); + } + return INVALID_STORE; +} + +bool PrefValueStore::GetValueFromStore(const std::string& name, + PrefValueStore::PrefStoreType store_type, + const base::Value** out_value) const { + // Only return true if we find a value and it is the correct type, so stale + // values with the incorrect type will be ignored. + const PrefStore* store = GetPrefStore(static_cast(store_type)); + if (store && store->GetValue(name, out_value)) + return true; + + // No valid value found for the given preference name: set the return value + // to false. + *out_value = nullptr; + return false; +} + +bool PrefValueStore::GetValueFromStoreWithType( + const std::string& name, + base::Value::Type type, + PrefStoreType store, + const base::Value** out_value) const { + if (GetValueFromStore(name, store, out_value)) { + if ((*out_value)->type() == type) + return true; + + LOG(WARNING) << "Expected type for " << name << " is " << type + << " but got " << (*out_value)->type() << " in store " + << store; + } + + *out_value = nullptr; + return false; +} + +void PrefValueStore::OnPrefValueChanged(PrefValueStore::PrefStoreType type, + const std::string& key) { + NotifyPrefChanged(key, type); +} + +void PrefValueStore::OnInitializationCompleted( + PrefValueStore::PrefStoreType type, bool succeeded) { + if (initialization_failed_) + return; + if (!succeeded) { + initialization_failed_ = true; + pref_notifier_->OnInitializationCompleted(false); + return; + } + CheckInitializationCompleted(); +} + +void PrefValueStore::InitPrefStore(PrefValueStore::PrefStoreType type, + PrefStore* pref_store) { + pref_stores_[type].Initialize(this, pref_store, type); +} + +void PrefValueStore::CheckInitializationCompleted() { + if (initialization_failed_) + return; + for (size_t i = 0; i <= PREF_STORE_TYPE_MAX; ++i) { + scoped_refptr store = + GetPrefStore(static_cast(i)); + if (store.get() && !store->IsInitializationComplete()) + return; + } + pref_notifier_->OnInitializationCompleted(true); +} diff --git a/src/components/prefs/pref_value_store.h b/src/components/prefs/pref_value_store.h new file mode 100644 index 0000000000..ea48380760 --- /dev/null +++ b/src/components/prefs/pref_value_store.h @@ -0,0 +1,332 @@ +// Copyright (c) 2012 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_PREFS_PREF_VALUE_STORE_H_ +#define COMPONENTS_PREFS_PREF_VALUE_STORE_H_ + +#include +#include +#include +#include +#include +#include + +#include "base/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/values.h" +#include "components/prefs/pref_store.h" +#include "components/prefs/prefs_export.h" + +class PersistentPrefStore; +class PrefNotifier; +class PrefRegistry; +class PrefStore; + +// The PrefValueStore manages various sources of values for Preferences +// (e.g., configuration policies, extensions, and user settings). It returns +// the value of a Preference from the source with the highest priority, and +// allows setting user-defined values for preferences that are not managed. +// +// Unless otherwise explicitly noted, all of the methods of this class must +// be called on the UI thread. +class COMPONENTS_PREFS_EXPORT PrefValueStore { + public: + using PrefChangedCallback = base::RepeatingCallback; + + // Delegate used to observe certain events in the |PrefValueStore|'s lifetime. + class Delegate { + public: + virtual ~Delegate() {} + + // Called by the PrefValueStore constructor with the PrefStores passed to + // it. + virtual void Init(PrefStore* managed_prefs, + PrefStore* supervised_user_prefs, + PrefStore* extension_prefs, + PrefStore* standalone_browser_prefs, + PrefStore* command_line_prefs, + PrefStore* user_prefs, + PrefStore* recommended_prefs, + PrefStore* default_prefs, + PrefNotifier* pref_notifier) = 0; + + virtual void InitIncognitoUserPrefs( + scoped_refptr incognito_user_prefs_overlay, + scoped_refptr incognito_user_prefs_underlay, + const std::vector& overlay_pref_names) = 0; + + virtual void InitPrefRegistry(PrefRegistry* pref_registry) = 0; + + // Called whenever PrefValueStore::UpdateCommandLinePrefStore is called, + // with the same argument. + virtual void UpdateCommandLinePrefStore(PrefStore* command_line_prefs) = 0; + }; + + // PrefStores must be listed here in order from highest to lowest priority. + // MANAGED contains all managed preferences that are provided by + // mandatory policies (e.g. Windows Group Policy or cloud policy). + // SUPERVISED_USER contains preferences that are valid for supervised users. + // EXTENSION contains preferences set by extensions. + // STANDALONE_BROWSER contains system preferences inherited from a separate + // Chrome instance. One relevant source is extension prefs in lacros + // passed to ash, so these prefs have similar precedence to extension + // prefs. + // COMMAND_LINE contains preferences set by command-line switches. + // USER contains all user-set preferences. + // RECOMMENDED contains all preferences that are provided by recommended + // policies. + // DEFAULT contains all application default preferences. + enum PrefStoreType { + // INVALID_STORE is not associated with an actual PrefStore but used as + // an invalid marker, e.g. as a return value. + INVALID_STORE = -1, + MANAGED_STORE = 0, + SUPERVISED_USER_STORE, + EXTENSION_STORE, + STANDALONE_BROWSER_STORE, + COMMAND_LINE_STORE, + USER_STORE, + RECOMMENDED_STORE, + DEFAULT_STORE, + PREF_STORE_TYPE_MAX = DEFAULT_STORE + }; + + // In decreasing order of precedence: + // |managed_prefs| contains all preferences from mandatory policies. + // |supervised_user_prefs| contains all preferences from supervised user + // settings, i.e. settings configured for a supervised user by their + // custodian. + // |extension_prefs| contains preference values set by extensions. + // |command_line_prefs| contains preference values set by command-line + // switches. + // |user_prefs| contains all user-set preference values. + // |recommended_prefs| contains all preferences from recommended policies. + // |default_prefs| contains application-default preference values. It must + // be non-null if any preferences are to be registered. + // + // |pref_notifier| facilitates broadcasting preference change notifications + // to the world. + PrefValueStore(PrefStore* managed_prefs, + PrefStore* supervised_user_prefs, + PrefStore* extension_prefs, + PrefStore* standalone_browser_prefs, + PrefStore* command_line_prefs, + PrefStore* user_prefs, + PrefStore* recommended_prefs, + PrefStore* default_prefs, + PrefNotifier* pref_notifier, + std::unique_ptr delegate = nullptr); + + PrefValueStore(const PrefValueStore&) = delete; + PrefValueStore& operator=(const PrefValueStore&) = delete; + + virtual ~PrefValueStore(); + + // Creates a clone of this PrefValueStore with PrefStores overwritten + // by the parameters passed, if unequal NULL. + // + // The new PrefValueStore is passed the |delegate| in its constructor. + std::unique_ptr CloneAndSpecialize( + PrefStore* managed_prefs, + PrefStore* supervised_user_prefs, + PrefStore* extension_prefs, + PrefStore* standalone_browser_prefs, + PrefStore* command_line_prefs, + PrefStore* user_prefs, + PrefStore* recommended_prefs, + PrefStore* default_prefs, + PrefNotifier* pref_notifier, + std::unique_ptr delegate = nullptr); + + // A PrefValueStore can have exactly one callback that is directly + // notified of preferences changing in the store. This does not + // filter through the PrefNotifier mechanism, which may not forward + // certain changes (e.g. unregistered prefs). + void set_callback(PrefChangedCallback callback) { + pref_changed_callback_ = std::move(callback); + } + + // Gets the value for the given preference name that has the specified value + // type. Values stored in a PrefStore that have the matching |name| but + // a non-matching |type| are silently skipped. Returns true if a valid value + // was found in any of the available PrefStores. Most callers should use + // Preference::GetValue() instead of calling this method directly. + bool GetValue(const std::string& name, + base::Value::Type type, + const base::Value** out_value) const; + + // Gets the recommended value for the given preference name that has the + // specified value type. A value stored in the recommended PrefStore that has + // the matching |name| but a non-matching |type| is silently ignored. Returns + // true if a valid value was found. Most callers should use + // Preference::GetRecommendedValue() instead of calling this method directly. + bool GetRecommendedValue(const std::string& name, + base::Value::Type type, + const base::Value** out_value) const; + + // These methods return true if a preference with the given name is in the + // indicated pref store, even if that value is currently being overridden by + // a higher-priority source. + bool PrefValueInManagedStore(const std::string& name) const; + bool PrefValueInSupervisedStore(const std::string& name) const; + bool PrefValueInExtensionStore(const std::string& name) const; + bool PrefValueInUserStore(const std::string& name) const; + + // These methods return true if a preference with the given name is actually + // being controlled by the indicated pref store and not being overridden by + // a higher-priority source. + bool PrefValueFromExtensionStore(const std::string& name) const; + bool PrefValueFromUserStore(const std::string& name) const; + bool PrefValueFromRecommendedStore(const std::string& name) const; + bool PrefValueFromDefaultStore(const std::string& name) const; + + // Check whether a Preference value is modifiable by the user, i.e. whether + // there is no higher-priority source controlling it. + bool PrefValueUserModifiable(const std::string& name) const; + + // Check whether a Preference value is modifiable by an extension, i.e. + // whether there is no higher-priority source controlling it. + bool PrefValueExtensionModifiable(const std::string& name) const; + + // Update the command line PrefStore with |command_line_prefs|. + void UpdateCommandLinePrefStore(PrefStore* command_line_prefs); + + bool IsInitializationComplete() const; + + // Check whether a particular type of PrefStore exists. + bool HasPrefStore(PrefStoreType type) const; + + private: + // Keeps a PrefStore reference on behalf of the PrefValueStore and monitors + // the PrefStore for changes, forwarding notifications to PrefValueStore. This + // indirection is here for the sake of disambiguating notifications from the + // individual PrefStores. + class PrefStoreKeeper : public PrefStore::Observer { + public: + PrefStoreKeeper(); + + PrefStoreKeeper(const PrefStoreKeeper&) = delete; + PrefStoreKeeper& operator=(const PrefStoreKeeper&) = delete; + + ~PrefStoreKeeper() override; + + // Takes ownership of |pref_store|. + void Initialize(PrefValueStore* store, + PrefStore* pref_store, + PrefStoreType type); + + PrefStore* store() { return pref_store_.get(); } + const PrefStore* store() const { return pref_store_.get(); } + + private: + // PrefStore::Observer implementation. + void OnPrefValueChanged(const std::string& key) override; + void OnInitializationCompleted(bool succeeded) override; + + // PrefValueStore this keeper is part of. + raw_ptr pref_value_store_; + + // The PrefStore managed by this keeper. + scoped_refptr pref_store_; + + // Type of the pref store. + PrefStoreType type_; + }; + + typedef std::map PrefTypeMap; + + // Returns true if the preference with the given name has a value in the + // given PrefStoreType, of the same value type as the preference was + // registered with. + bool PrefValueInStore(const std::string& name, PrefStoreType store) const; + + // Returns true if a preference has an explicit value in any of the + // stores in the range specified by |first_checked_store| and + // |last_checked_store|, even if that value is currently being + // overridden by a higher-priority store. + bool PrefValueInStoreRange(const std::string& name, + PrefStoreType first_checked_store, + PrefStoreType last_checked_store) const; + + // Returns the pref store type identifying the source that controls the + // Preference identified by |name|. If none of the sources has a value, + // INVALID_STORE is returned. In practice, the default PrefStore + // should always have a value for any registered preferencem, so INVALID_STORE + // indicates an error. + PrefStoreType ControllingPrefStoreForPref(const std::string& name) const; + + // Get a value from the specified |store|. + bool GetValueFromStore(const std::string& name, + PrefStoreType store, + const base::Value** out_value) const; + + // Get a value from the specified |store| if its |type| matches. + bool GetValueFromStoreWithType(const std::string& name, + base::Value::Type type, + PrefStoreType store, + const base::Value** out_value) const; + + // Called upon changes in individual pref stores in order to determine whether + // the user-visible pref value has changed. Triggers the change notification + // if the effective value of the preference has changed, or if the store + // controlling the pref has changed. + void NotifyPrefChanged(const std::string& path, PrefStoreType new_store); + + // Called from the PrefStoreKeeper implementation when a pref value for |key| + // changed in the pref store for |type|. + void OnPrefValueChanged(PrefStoreType type, const std::string& key); + + // Handle the event that the store for |type| has completed initialization. + void OnInitializationCompleted(PrefStoreType type, bool succeeded); + + // Initializes a pref store keeper. Sets up a PrefStoreKeeper that will take + // ownership of the passed |pref_store|. + void InitPrefStore(PrefStoreType type, PrefStore* pref_store); + + // Checks whether initialization is completed and tells the notifier if that + // is the case. + void CheckInitializationCompleted(); + + // Get the PrefStore pointer for the given type. May return NULL if there is + // no PrefStore for that type. + PrefStore* GetPrefStore(PrefStoreType type) { + return pref_stores_[type].store(); + } + const PrefStore* GetPrefStore(PrefStoreType type) const { + return pref_stores_[type].store(); + } + + // Keeps the PrefStore references in order of precedence. + PrefStoreKeeper pref_stores_[PREF_STORE_TYPE_MAX + 1]; + + PrefChangedCallback pref_changed_callback_; + + // Used for generating notifications. This is a weak reference, + // since the notifier is owned by the corresponding PrefService. + raw_ptr pref_notifier_; + + // A mapping of preference names to their registered types. + PrefTypeMap pref_types_; + + // True if not all of the PrefStores were initialized successfully. + bool initialization_failed_; + + // Might be null. + std::unique_ptr delegate_; +}; + +namespace std { + +template <> +struct hash { + size_t operator()(PrefValueStore::PrefStoreType type) const { + return std::hash< + std::underlying_type::type>()(type); + } +}; + +} // namespace std + +#endif // COMPONENTS_PREFS_PREF_VALUE_STORE_H_ diff --git a/src/components/prefs/pref_value_store_unittest.cc b/src/components/prefs/pref_value_store_unittest.cc new file mode 100644 index 0000000000..287196461f --- /dev/null +++ b/src/components/prefs/pref_value_store_unittest.cc @@ -0,0 +1,721 @@ +// Copyright (c) 2012 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 "components/prefs/pref_value_store.h" + +#include +#include + +#include "base/bind.h" +#include "base/memory/ref_counted.h" +#include "base/values.h" +#include "components/prefs/pref_notifier.h" +#include "components/prefs/testing_pref_store.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::Mock; +using testing::_; + +namespace { + +// Allows to capture pref notifications through gmock. +class MockPrefNotifier : public PrefNotifier { + public: + MOCK_METHOD1(OnPreferenceChanged, void(const std::string&)); + MOCK_METHOD1(OnInitializationCompleted, void(bool)); +}; + +// Allows to capture sync model associator interaction. +class MockPrefModelAssociator { + public: + MOCK_METHOD1(ProcessPrefChange, void(const std::string&)); +}; + +} // namespace + +// Names of the preferences used in this test. +namespace prefs { +const char kManagedPref[] = "this.pref.managed"; +const char kSupervisedUserPref[] = "this.pref.supervised_user"; +const char kCommandLinePref[] = "this.pref.command_line"; +const char kExtensionPref[] = "this.pref.extension"; +const char kStandaloneBrowserPref[] = "this.pref.standalone_browser"; +const char kUserPref[] = "this.pref.user"; +const char kRecommendedPref[] = "this.pref.recommended"; +const char kDefaultPref[] = "this.pref.default"; +const char kMissingPref[] = "this.pref.does_not_exist"; +} + +// Potentially expected values of all preferences used in this test program. +namespace managed_pref { +const char kManagedValue[] = "managed:managed"; +} + +namespace supervised_user_pref { +const char kManagedValue[] = "supervised_user:managed"; +const char kSupervisedUserValue[] = "supervised_user:supervised_user"; +} + +namespace extension_pref { +const char kManagedValue[] = "extension:managed"; +const char kSupervisedUserValue[] = "extension:supervised_user"; +const char kExtensionValue[] = "extension:extension"; +} + +namespace standalone_browser_pref { +const char kManagedValue[] = "standalone_browser:managed"; +const char kSupervisedUserValue[] = "standalone_browser:supervised_user"; +const char kExtensionValue[] = "standalone_browser:extension"; +const char kStandaloneBrowserValue[] = "standalone_browser:standalone_browser"; +} // namespace standalone_browser_pref + +namespace command_line_pref { +const char kManagedValue[] = "command_line:managed"; +const char kSupervisedUserValue[] = "command_line:supervised_user"; +const char kExtensionValue[] = "command_line:extension"; +const char kStandaloneBrowserValue[] = "command_line:standalone_browser"; +const char kCommandLineValue[] = "command_line:command_line"; +} + +namespace user_pref { +const char kManagedValue[] = "user:managed"; +const char kSupervisedUserValue[] = "supervised_user:supervised_user"; +const char kExtensionValue[] = "user:extension"; +const char kStandaloneBrowserValue[] = "user:standalone_browser"; +const char kCommandLineValue[] = "user:command_line"; +const char kUserValue[] = "user:user"; +} + +namespace recommended_pref { +const char kManagedValue[] = "recommended:managed"; +const char kSupervisedUserValue[] = "recommended:supervised_user"; +const char kExtensionValue[] = "recommended:extension"; +const char kStandaloneBrowserValue[] = "recommended:standalone_browser"; +const char kCommandLineValue[] = "recommended:command_line"; +const char kUserValue[] = "recommended:user"; +const char kRecommendedValue[] = "recommended:recommended"; +} + +namespace default_pref { +const char kManagedValue[] = "default:managed"; +const char kSupervisedUserValue[] = "default:supervised_user"; +const char kExtensionValue[] = "default:extension"; +const char kStandaloneBrowserValue[] = "default:standalone_browser"; +const char kCommandLineValue[] = "default:command_line"; +const char kUserValue[] = "default:user"; +const char kRecommendedValue[] = "default:recommended"; +const char kDefaultValue[] = "default:default"; +} + +class PrefValueStoreTest : public testing::Test { + protected: + void SetUp() override { + // Create TestingPrefStores. + CreateManagedPrefs(); + CreateSupervisedUserPrefs(); + CreateExtensionPrefs(); + CreateStandaloneBrowserPrefs(); + CreateCommandLinePrefs(); + CreateUserPrefs(); + CreateRecommendedPrefs(); + CreateDefaultPrefs(); + sync_associator_ = std::make_unique(); + + // Create a fresh PrefValueStore. + pref_value_store_ = std::make_unique( + managed_pref_store_.get(), supervised_user_pref_store_.get(), + extension_pref_store_.get(), standalone_browser_pref_store_.get(), + command_line_pref_store_.get(), user_pref_store_.get(), + recommended_pref_store_.get(), default_pref_store_.get(), + &pref_notifier_); + + pref_value_store_->set_callback( + base::BindRepeating(&MockPrefModelAssociator::ProcessPrefChange, + base::Unretained(sync_associator_.get()))); + } + + void CreateManagedPrefs() { + managed_pref_store_ = new TestingPrefStore; + managed_pref_store_->SetString( + prefs::kManagedPref, + managed_pref::kManagedValue); + } + + void CreateSupervisedUserPrefs() { + supervised_user_pref_store_ = new TestingPrefStore; + supervised_user_pref_store_->SetString( + prefs::kManagedPref, + supervised_user_pref::kManagedValue); + supervised_user_pref_store_->SetString( + prefs::kSupervisedUserPref, + supervised_user_pref::kSupervisedUserValue); + } + + void CreateExtensionPrefs() { + extension_pref_store_ = new TestingPrefStore; + extension_pref_store_->SetString( + prefs::kManagedPref, + extension_pref::kManagedValue); + extension_pref_store_->SetString( + prefs::kSupervisedUserPref, + extension_pref::kSupervisedUserValue); + extension_pref_store_->SetString( + prefs::kExtensionPref, + extension_pref::kExtensionValue); + } + + void CreateStandaloneBrowserPrefs() { + standalone_browser_pref_store_ = new TestingPrefStore; + standalone_browser_pref_store_->SetString( + prefs::kManagedPref, standalone_browser_pref::kManagedValue); + standalone_browser_pref_store_->SetString( + prefs::kSupervisedUserPref, + standalone_browser_pref::kSupervisedUserValue); + standalone_browser_pref_store_->SetString( + prefs::kExtensionPref, standalone_browser_pref::kExtensionValue); + standalone_browser_pref_store_->SetString( + prefs::kStandaloneBrowserPref, + standalone_browser_pref::kStandaloneBrowserValue); + } + + void CreateCommandLinePrefs() { + command_line_pref_store_ = new TestingPrefStore; + command_line_pref_store_->SetString( + prefs::kManagedPref, + command_line_pref::kManagedValue); + command_line_pref_store_->SetString( + prefs::kSupervisedUserPref, + command_line_pref::kSupervisedUserValue); + command_line_pref_store_->SetString( + prefs::kExtensionPref, + command_line_pref::kExtensionValue); + command_line_pref_store_->SetString( + prefs::kStandaloneBrowserPref, + command_line_pref::kStandaloneBrowserValue); + command_line_pref_store_->SetString( + prefs::kCommandLinePref, + command_line_pref::kCommandLineValue); + } + + void CreateUserPrefs() { + user_pref_store_ = new TestingPrefStore; + user_pref_store_->SetString( + prefs::kManagedPref, + user_pref::kManagedValue); + user_pref_store_->SetString( + prefs::kSupervisedUserPref, + user_pref::kSupervisedUserValue); + user_pref_store_->SetString( + prefs::kCommandLinePref, + user_pref::kCommandLineValue); + user_pref_store_->SetString( + prefs::kExtensionPref, + user_pref::kExtensionValue); + user_pref_store_->SetString(prefs::kStandaloneBrowserPref, + user_pref::kStandaloneBrowserValue); + user_pref_store_->SetString( + prefs::kUserPref, + user_pref::kUserValue); + } + + void CreateRecommendedPrefs() { + recommended_pref_store_ = new TestingPrefStore; + recommended_pref_store_->SetString( + prefs::kManagedPref, + recommended_pref::kManagedValue); + recommended_pref_store_->SetString( + prefs::kSupervisedUserPref, + recommended_pref::kSupervisedUserValue); + recommended_pref_store_->SetString( + prefs::kCommandLinePref, + recommended_pref::kCommandLineValue); + recommended_pref_store_->SetString( + prefs::kExtensionPref, + recommended_pref::kExtensionValue); + recommended_pref_store_->SetString( + prefs::kStandaloneBrowserPref, + recommended_pref::kStandaloneBrowserValue); + recommended_pref_store_->SetString( + prefs::kUserPref, + recommended_pref::kUserValue); + recommended_pref_store_->SetString( + prefs::kRecommendedPref, + recommended_pref::kRecommendedValue); + } + + void CreateDefaultPrefs() { + default_pref_store_ = new TestingPrefStore; + default_pref_store_->SetString( + prefs::kSupervisedUserPref, + default_pref::kSupervisedUserValue); + default_pref_store_->SetString( + prefs::kManagedPref, + default_pref::kManagedValue); + default_pref_store_->SetString( + prefs::kCommandLinePref, + default_pref::kCommandLineValue); + default_pref_store_->SetString( + prefs::kExtensionPref, + default_pref::kExtensionValue); + default_pref_store_->SetString(prefs::kStandaloneBrowserPref, + default_pref::kStandaloneBrowserValue); + default_pref_store_->SetString( + prefs::kUserPref, + default_pref::kUserValue); + default_pref_store_->SetString( + prefs::kRecommendedPref, + default_pref::kRecommendedValue); + default_pref_store_->SetString( + prefs::kDefaultPref, + default_pref::kDefaultValue); + } + + void ExpectValueChangeNotifications(const std::string& name) { + EXPECT_CALL(pref_notifier_, OnPreferenceChanged(name)); + EXPECT_CALL(*sync_associator_, ProcessPrefChange(name)); + } + + void CheckAndClearValueChangeNotifications() { + Mock::VerifyAndClearExpectations(&pref_notifier_); + Mock::VerifyAndClearExpectations(sync_associator_.get()); + } + + MockPrefNotifier pref_notifier_; + std::unique_ptr sync_associator_; + std::unique_ptr pref_value_store_; + + scoped_refptr managed_pref_store_; + scoped_refptr supervised_user_pref_store_; + scoped_refptr extension_pref_store_; + scoped_refptr standalone_browser_pref_store_; + scoped_refptr command_line_pref_store_; + scoped_refptr user_pref_store_; + scoped_refptr recommended_pref_store_; + scoped_refptr default_pref_store_; +}; + +TEST_F(PrefValueStoreTest, GetValue) { + const base::Value* value; + + // The following tests read a value from the PrefService. The preferences are + // set in a way such that all lower-priority stores have a value and we can + // test whether overrides work correctly. + + // Test getting a managed value. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetValue(prefs::kManagedPref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(managed_pref::kManagedValue, value->GetString()); + + // Test getting a supervised user value. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetValue(prefs::kSupervisedUserPref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(supervised_user_pref::kSupervisedUserValue, value->GetString()); + + // Test getting an extension value. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetValue(prefs::kExtensionPref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(extension_pref::kExtensionValue, value->GetString()); + + // Test getting a command-line value. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetValue(prefs::kCommandLinePref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(command_line_pref::kCommandLineValue, value->GetString()); + + // Test getting a user-set value. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetValue(prefs::kUserPref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(user_pref::kUserValue, value->GetString()); + + // Test getting a user set value overwriting a recommended value. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetValue(prefs::kRecommendedPref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(recommended_pref::kRecommendedValue, value->GetString()); + + // Test getting a default value. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetValue(prefs::kDefaultPref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(default_pref::kDefaultValue, value->GetString()); + + // Test getting a preference value that the |PrefValueStore| + // does not contain. + base::Value tmp_dummy_value(true); + value = &tmp_dummy_value; + ASSERT_FALSE(pref_value_store_->GetValue(prefs::kMissingPref, + base::Value::Type::STRING, &value)); + ASSERT_FALSE(value); +} + +TEST_F(PrefValueStoreTest, GetRecommendedValue) { + const base::Value* value; + + // The following tests read a value from the PrefService. The preferences are + // set in a way such that all lower-priority stores have a value and we can + // test whether overrides do not clutter the recommended value. + + // Test getting recommended value when a managed value is present. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetRecommendedValue( + prefs::kManagedPref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(recommended_pref::kManagedValue, value->GetString()); + + // Test getting recommended value when a supervised user value is present. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetRecommendedValue( + prefs::kSupervisedUserPref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(recommended_pref::kSupervisedUserValue, value->GetString()); + + // Test getting recommended value when an extension value is present. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetRecommendedValue( + prefs::kExtensionPref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(recommended_pref::kExtensionValue, value->GetString()); + + // Test getting recommended value when a command-line value is present. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetRecommendedValue( + prefs::kCommandLinePref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(recommended_pref::kCommandLineValue, value->GetString()); + + // Test getting recommended value when a user-set value is present. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetRecommendedValue( + prefs::kUserPref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(recommended_pref::kUserValue, value->GetString()); + + // Test getting recommended value when no higher-priority value is present. + value = nullptr; + ASSERT_TRUE(pref_value_store_->GetRecommendedValue( + prefs::kRecommendedPref, + base::Value::Type::STRING, &value)); + ASSERT_TRUE(value->is_string()); + EXPECT_EQ(recommended_pref::kRecommendedValue, value->GetString()); + + // Test getting recommended value when no recommended value is present. + base::Value tmp_dummy_value(true); + value = &tmp_dummy_value; + ASSERT_FALSE(pref_value_store_->GetRecommendedValue( + prefs::kDefaultPref, + base::Value::Type::STRING, &value)); + ASSERT_FALSE(value); + + // Test getting a preference value that the |PrefValueStore| + // does not contain. + value = &tmp_dummy_value; + ASSERT_FALSE(pref_value_store_->GetRecommendedValue( + prefs::kMissingPref, + base::Value::Type::STRING, &value)); + ASSERT_FALSE(value); +} + +TEST_F(PrefValueStoreTest, PrefChanges) { + // Check pref controlled by highest-priority store. + ExpectValueChangeNotifications(prefs::kManagedPref); + managed_pref_store_->NotifyPrefValueChanged(prefs::kManagedPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kManagedPref); + supervised_user_pref_store_->NotifyPrefValueChanged(prefs::kManagedPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kManagedPref); + extension_pref_store_->NotifyPrefValueChanged(prefs::kManagedPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kManagedPref); + command_line_pref_store_->NotifyPrefValueChanged(prefs::kManagedPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kManagedPref); + user_pref_store_->NotifyPrefValueChanged(prefs::kManagedPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kManagedPref); + recommended_pref_store_->NotifyPrefValueChanged(prefs::kManagedPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kManagedPref); + default_pref_store_->NotifyPrefValueChanged(prefs::kManagedPref); + CheckAndClearValueChangeNotifications(); + + // Check pref controlled by user store. + ExpectValueChangeNotifications(prefs::kUserPref); + managed_pref_store_->NotifyPrefValueChanged(prefs::kUserPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kUserPref); + extension_pref_store_->NotifyPrefValueChanged(prefs::kUserPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kUserPref); + command_line_pref_store_->NotifyPrefValueChanged(prefs::kUserPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kUserPref); + user_pref_store_->NotifyPrefValueChanged(prefs::kUserPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kUserPref); + recommended_pref_store_->NotifyPrefValueChanged(prefs::kUserPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kUserPref); + default_pref_store_->NotifyPrefValueChanged(prefs::kUserPref); + CheckAndClearValueChangeNotifications(); + + // Check pref controlled by default-pref store. + ExpectValueChangeNotifications(prefs::kDefaultPref); + managed_pref_store_->NotifyPrefValueChanged(prefs::kDefaultPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kDefaultPref); + extension_pref_store_->NotifyPrefValueChanged(prefs::kDefaultPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kDefaultPref); + command_line_pref_store_->NotifyPrefValueChanged(prefs::kDefaultPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kDefaultPref); + user_pref_store_->NotifyPrefValueChanged(prefs::kDefaultPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kDefaultPref); + recommended_pref_store_->NotifyPrefValueChanged(prefs::kDefaultPref); + CheckAndClearValueChangeNotifications(); + + ExpectValueChangeNotifications(prefs::kDefaultPref); + default_pref_store_->NotifyPrefValueChanged(prefs::kDefaultPref); + CheckAndClearValueChangeNotifications(); +} + +TEST_F(PrefValueStoreTest, OnInitializationCompleted) { + EXPECT_CALL(pref_notifier_, OnInitializationCompleted(true)).Times(0); + managed_pref_store_->SetInitializationCompleted(); + supervised_user_pref_store_->SetInitializationCompleted(); + extension_pref_store_->SetInitializationCompleted(); + standalone_browser_pref_store_->SetInitializationCompleted(); + command_line_pref_store_->SetInitializationCompleted(); + recommended_pref_store_->SetInitializationCompleted(); + default_pref_store_->SetInitializationCompleted(); + Mock::VerifyAndClearExpectations(&pref_notifier_); + + // The notification should only be triggered after the last store is done. + EXPECT_CALL(pref_notifier_, OnInitializationCompleted(true)).Times(1); + user_pref_store_->SetInitializationCompleted(); + Mock::VerifyAndClearExpectations(&pref_notifier_); +} + +TEST_F(PrefValueStoreTest, PrefValueInManagedStore) { + EXPECT_TRUE(pref_value_store_->PrefValueInManagedStore( + prefs::kManagedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInManagedStore( + prefs::kSupervisedUserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInManagedStore( + prefs::kExtensionPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInManagedStore( + prefs::kStandaloneBrowserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInManagedStore( + prefs::kCommandLinePref)); + EXPECT_FALSE(pref_value_store_->PrefValueInManagedStore( + prefs::kUserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInManagedStore( + prefs::kRecommendedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInManagedStore( + prefs::kDefaultPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInManagedStore( + prefs::kMissingPref)); +} + +TEST_F(PrefValueStoreTest, PrefValueInExtensionStore) { + EXPECT_TRUE(pref_value_store_->PrefValueInExtensionStore( + prefs::kManagedPref)); + EXPECT_TRUE(pref_value_store_->PrefValueInExtensionStore( + prefs::kSupervisedUserPref)); + EXPECT_TRUE(pref_value_store_->PrefValueInExtensionStore( + prefs::kExtensionPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInExtensionStore( + prefs::kStandaloneBrowserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInExtensionStore( + prefs::kCommandLinePref)); + EXPECT_FALSE(pref_value_store_->PrefValueInExtensionStore( + prefs::kUserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInExtensionStore( + prefs::kRecommendedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInExtensionStore( + prefs::kDefaultPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInExtensionStore( + prefs::kMissingPref)); +} + +TEST_F(PrefValueStoreTest, PrefValueInUserStore) { + EXPECT_TRUE(pref_value_store_->PrefValueInUserStore( + prefs::kManagedPref)); + EXPECT_TRUE(pref_value_store_->PrefValueInUserStore( + prefs::kSupervisedUserPref)); + EXPECT_TRUE(pref_value_store_->PrefValueInUserStore( + prefs::kExtensionPref)); + EXPECT_TRUE( + pref_value_store_->PrefValueInUserStore(prefs::kStandaloneBrowserPref)); + EXPECT_TRUE(pref_value_store_->PrefValueInUserStore( + prefs::kCommandLinePref)); + EXPECT_TRUE(pref_value_store_->PrefValueInUserStore( + prefs::kUserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInUserStore( + prefs::kRecommendedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInUserStore( + prefs::kDefaultPref)); + EXPECT_FALSE(pref_value_store_->PrefValueInUserStore( + prefs::kMissingPref)); +} + +TEST_F(PrefValueStoreTest, PrefValueFromExtensionStore) { + EXPECT_FALSE(pref_value_store_->PrefValueFromExtensionStore( + prefs::kManagedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromExtensionStore( + prefs::kSupervisedUserPref)); + EXPECT_TRUE(pref_value_store_->PrefValueFromExtensionStore( + prefs::kExtensionPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromExtensionStore( + prefs::kStandaloneBrowserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromExtensionStore( + prefs::kCommandLinePref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromExtensionStore( + prefs::kUserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromExtensionStore( + prefs::kRecommendedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromExtensionStore( + prefs::kDefaultPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromExtensionStore( + prefs::kMissingPref)); +} + +TEST_F(PrefValueStoreTest, PrefValueFromUserStore) { + EXPECT_FALSE(pref_value_store_->PrefValueFromUserStore( + prefs::kManagedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromUserStore( + prefs::kSupervisedUserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromUserStore( + prefs::kExtensionPref)); + EXPECT_FALSE( + pref_value_store_->PrefValueFromUserStore(prefs::kStandaloneBrowserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromUserStore( + prefs::kCommandLinePref)); + EXPECT_TRUE(pref_value_store_->PrefValueFromUserStore( + prefs::kUserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromUserStore( + prefs::kRecommendedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromUserStore( + prefs::kDefaultPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromUserStore( + prefs::kMissingPref)); +} + +TEST_F(PrefValueStoreTest, PrefValueFromRecommendedStore) { + EXPECT_FALSE(pref_value_store_->PrefValueFromRecommendedStore( + prefs::kManagedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromRecommendedStore( + prefs::kSupervisedUserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromRecommendedStore( + prefs::kExtensionPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromRecommendedStore( + prefs::kStandaloneBrowserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromRecommendedStore( + prefs::kCommandLinePref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromRecommendedStore( + prefs::kUserPref)); + EXPECT_TRUE(pref_value_store_->PrefValueFromRecommendedStore( + prefs::kRecommendedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromRecommendedStore( + prefs::kDefaultPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromRecommendedStore( + prefs::kMissingPref)); +} + +TEST_F(PrefValueStoreTest, PrefValueFromDefaultStore) { + EXPECT_FALSE(pref_value_store_->PrefValueFromDefaultStore( + prefs::kManagedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromDefaultStore( + prefs::kSupervisedUserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromDefaultStore( + prefs::kExtensionPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromDefaultStore( + prefs::kStandaloneBrowserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromDefaultStore( + prefs::kCommandLinePref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromDefaultStore( + prefs::kUserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromDefaultStore( + prefs::kRecommendedPref)); + EXPECT_TRUE(pref_value_store_->PrefValueFromDefaultStore( + prefs::kDefaultPref)); + EXPECT_FALSE(pref_value_store_->PrefValueFromDefaultStore( + prefs::kMissingPref)); +} + +TEST_F(PrefValueStoreTest, PrefValueUserModifiable) { + EXPECT_FALSE(pref_value_store_->PrefValueUserModifiable( + prefs::kManagedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueUserModifiable( + prefs::kSupervisedUserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueUserModifiable( + prefs::kExtensionPref)); + EXPECT_FALSE(pref_value_store_->PrefValueUserModifiable( + prefs::kStandaloneBrowserPref)); + EXPECT_FALSE(pref_value_store_->PrefValueUserModifiable( + prefs::kCommandLinePref)); + EXPECT_TRUE(pref_value_store_->PrefValueUserModifiable( + prefs::kUserPref)); + EXPECT_TRUE(pref_value_store_->PrefValueUserModifiable( + prefs::kRecommendedPref)); + EXPECT_TRUE(pref_value_store_->PrefValueUserModifiable( + prefs::kDefaultPref)); + EXPECT_TRUE(pref_value_store_->PrefValueUserModifiable( + prefs::kMissingPref)); +} + +TEST_F(PrefValueStoreTest, PrefValueExtensionModifiable) { + EXPECT_FALSE(pref_value_store_->PrefValueExtensionModifiable( + prefs::kManagedPref)); + EXPECT_FALSE(pref_value_store_->PrefValueExtensionModifiable( + prefs::kSupervisedUserPref)); + EXPECT_TRUE(pref_value_store_->PrefValueExtensionModifiable( + prefs::kExtensionPref)); + EXPECT_TRUE(pref_value_store_->PrefValueExtensionModifiable( + prefs::kStandaloneBrowserPref)); + EXPECT_TRUE(pref_value_store_->PrefValueExtensionModifiable( + prefs::kCommandLinePref)); + EXPECT_TRUE(pref_value_store_->PrefValueExtensionModifiable( + prefs::kUserPref)); + EXPECT_TRUE(pref_value_store_->PrefValueExtensionModifiable( + prefs::kRecommendedPref)); + EXPECT_TRUE(pref_value_store_->PrefValueExtensionModifiable( + prefs::kDefaultPref)); + EXPECT_TRUE(pref_value_store_->PrefValueExtensionModifiable( + prefs::kMissingPref)); +} diff --git a/src/components/prefs/prefs_export.h b/src/components/prefs/prefs_export.h new file mode 100644 index 0000000000..b5c8366b95 --- /dev/null +++ b/src/components/prefs/prefs_export.h @@ -0,0 +1,29 @@ +// Copyright (c) 2012 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_PREFS_PREFS_EXPORT_H_ +#define COMPONENTS_PREFS_PREFS_EXPORT_H_ + +#if defined(COMPONENT_BUILD) +#if defined(WIN32) + +#if defined(COMPONENTS_PREFS_IMPLEMENTATION) +#define COMPONENTS_PREFS_EXPORT __declspec(dllexport) +#else +#define COMPONENTS_PREFS_EXPORT __declspec(dllimport) +#endif // defined(COMPONENTS_PREFS_IMPLEMENTATION) + +#else // defined(WIN32) +#if defined(COMPONENTS_PREFS_IMPLEMENTATION) +#define COMPONENTS_PREFS_EXPORT __attribute__((visibility("default"))) +#else +#define COMPONENTS_PREFS_EXPORT +#endif +#endif + +#else // defined(COMPONENT_BUILD) +#define COMPONENTS_PREFS_EXPORT +#endif + +#endif // COMPONENTS_PREFS_PREFS_EXPORT_H_ diff --git a/src/components/prefs/scoped_user_pref_update.cc b/src/components/prefs/scoped_user_pref_update.cc new file mode 100644 index 0000000000..eb86b5ea7b --- /dev/null +++ b/src/components/prefs/scoped_user_pref_update.cc @@ -0,0 +1,44 @@ +// Copyright 2013 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 "components/prefs/scoped_user_pref_update.h" + +#include "base/check_op.h" +#include "components/prefs/pref_notifier.h" +#include "components/prefs/pref_service.h" + +namespace subtle { + +ScopedUserPrefUpdateBase::ScopedUserPrefUpdateBase(PrefService* service, + const std::string& path) + : service_(service), path_(path), value_(nullptr) { + DCHECK_CALLED_ON_VALID_SEQUENCE(service_->sequence_checker_); +} + +ScopedUserPrefUpdateBase::~ScopedUserPrefUpdateBase() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + Notify(); +} + +base::Value* ScopedUserPrefUpdateBase::GetValueOfType(base::Value::Type type) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!value_) + value_ = service_->GetMutableUserPref(path_, type); + + // |value_| might be downcast to base::DictionaryValue or base::ListValue, + // side-stepping CHECKs built into base::Value. Thus we need to be certain + // that the type matches. + if (value_) + CHECK_EQ(value_->type(), type); + return value_; +} + +void ScopedUserPrefUpdateBase::Notify() { + if (value_) { + service_->ReportUserPrefChanged(path_); + value_ = nullptr; + } +} + +} // namespace subtle diff --git a/src/components/prefs/scoped_user_pref_update.h b/src/components/prefs/scoped_user_pref_update.h new file mode 100644 index 0000000000..7f34c9465f --- /dev/null +++ b/src/components/prefs/scoped_user_pref_update.h @@ -0,0 +1,99 @@ +// Copyright 2013 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. +// +// A helper class that assists preferences in firing notifications when lists +// or dictionaries are changed. + +#ifndef COMPONENTS_PREFS_SCOPED_USER_PREF_UPDATE_H_ +#define COMPONENTS_PREFS_SCOPED_USER_PREF_UPDATE_H_ + +#include + +#include "base/memory/raw_ptr.h" +#include "base/sequence_checker.h" +#include "base/values.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/prefs_export.h" + +class PrefService; + +namespace subtle { + +// Base class for ScopedUserPrefUpdateTemplate that contains the parts +// that do not depend on ScopedUserPrefUpdateTemplate's template parameter. +// +// We need this base class mostly for making it a friend of PrefService +// and getting access to PrefService::GetMutableUserPref and +// PrefService::ReportUserPrefChanged. +class COMPONENTS_PREFS_EXPORT ScopedUserPrefUpdateBase { + public: + ScopedUserPrefUpdateBase(const ScopedUserPrefUpdateBase&) = delete; + ScopedUserPrefUpdateBase& operator=(const ScopedUserPrefUpdateBase&) = delete; + + protected: + ScopedUserPrefUpdateBase(PrefService* service, const std::string& path); + + // Calls Notify(). + ~ScopedUserPrefUpdateBase(); + + // Sets |value_| to |service_|->GetMutableUserPref and returns it. + base::Value* GetValueOfType(base::Value::Type type); + + private: + // If |value_| is not null, triggers a notification of PrefObservers and + // resets |value_|. + void Notify(); + + // Weak pointer. + raw_ptr service_; + // Path of the preference being updated. + std::string path_; + // Cache of value from user pref store (set between Get() and Notify() calls). + raw_ptr value_; + + SEQUENCE_CHECKER(sequence_checker_); +}; + +} // namespace subtle + +// Class to support modifications to dictionary and list base::Values while +// guaranteeing that PrefObservers are notified of changed values. +// +// This class may only be used on the UI thread as it requires access to the +// PrefService. +template +class ScopedUserPrefUpdate : public subtle::ScopedUserPrefUpdateBase { + public: + ScopedUserPrefUpdate(PrefService* service, const std::string& path) + : ScopedUserPrefUpdateBase(service, path) {} + + ScopedUserPrefUpdate(const ScopedUserPrefUpdate&) = delete; + ScopedUserPrefUpdate& operator=(const ScopedUserPrefUpdate&) = delete; + + // Triggers an update notification if Get() was called. + virtual ~ScopedUserPrefUpdate() {} + + // Returns a mutable |base::Value| instance that + // - is already in the user pref store, or + // - is (silently) created and written to the user pref store if none existed + // before. + // + // Calling Get() implies that an update notification is necessary at + // destruction time. + // + // The ownership of the return value remains with the user pref store. + // Virtual so it can be overriden in subclasses that transform the value + // before returning it (for example to return a subelement of a dictionary). + virtual base::Value* Get() { return GetValueOfType(type_enum_value); } + + base::Value& operator*() { return *Get(); } + + base::Value* operator->() { return Get(); } +}; + +typedef ScopedUserPrefUpdate + DictionaryPrefUpdate; +typedef ScopedUserPrefUpdate ListPrefUpdate; + +#endif // COMPONENTS_PREFS_SCOPED_USER_PREF_UPDATE_H_ diff --git a/src/components/prefs/scoped_user_pref_update_unittest.cc b/src/components/prefs/scoped_user_pref_update_unittest.cc new file mode 100644 index 0000000000..18eaa5114e --- /dev/null +++ b/src/components/prefs/scoped_user_pref_update_unittest.cc @@ -0,0 +1,108 @@ +// Copyright 2013 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 "components/prefs/mock_pref_change_callback.h" +#include "components/prefs/pref_change_registrar.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/scoped_user_pref_update.h" +#include "components/prefs/testing_pref_service.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::Mock; + +class ScopedUserPrefUpdateTest : public testing::Test { + public: + ScopedUserPrefUpdateTest() : observer_(&prefs_) {} + ~ScopedUserPrefUpdateTest() override {} + + protected: + void SetUp() override { + prefs_.registry()->RegisterDictionaryPref(kPref); + registrar_.Init(&prefs_); + registrar_.Add(kPref, observer_.GetCallback()); + } + + static const char kPref[]; + static const char kKey[]; + static const char kValue[]; + + TestingPrefServiceSimple prefs_; + MockPrefChangeCallback observer_; + PrefChangeRegistrar registrar_; +}; + +const char ScopedUserPrefUpdateTest::kPref[] = "name"; +const char ScopedUserPrefUpdateTest::kKey[] = "key"; +const char ScopedUserPrefUpdateTest::kValue[] = "value"; + +TEST_F(ScopedUserPrefUpdateTest, RegularUse) { + // Dictionary that will be expected to be set at the end. + base::Value expected_dictionary(base::Value::Type::DICTIONARY); + expected_dictionary.SetStringKey(kKey, kValue); + + { + EXPECT_CALL(observer_, OnPreferenceChanged(_)).Times(0); + DictionaryPrefUpdate update(&prefs_, kPref); + base::Value* value = update.Get(); + ASSERT_TRUE(value); + value->SetStringKey(kKey, kValue); + + // The dictionary was created for us but the creation should have happened + // silently without notifications. + Mock::VerifyAndClearExpectations(&observer_); + + // Modifications happen online and are instantly visible, though. + const base::Value* current_value = prefs_.GetDictionary(kPref); + ASSERT_TRUE(current_value); + EXPECT_EQ(expected_dictionary, *current_value); + + // Now we are leaving the scope of the update so we should be notified. + observer_.Expect(kPref, &expected_dictionary); + } + Mock::VerifyAndClearExpectations(&observer_); + + const base::Value* current_value = prefs_.GetDictionary(kPref); + ASSERT_TRUE(current_value); + EXPECT_EQ(expected_dictionary, *current_value); +} + +TEST_F(ScopedUserPrefUpdateTest, NeverTouchAnything) { + const base::Value* old_value = prefs_.GetDictionary(kPref); + EXPECT_CALL(observer_, OnPreferenceChanged(_)).Times(0); + { DictionaryPrefUpdate update(&prefs_, kPref); } + const base::Value* new_value = prefs_.GetDictionary(kPref); + EXPECT_EQ(old_value, new_value); + Mock::VerifyAndClearExpectations(&observer_); +} + +TEST_F(ScopedUserPrefUpdateTest, UpdatingListPrefWithDefaults) { + base::Value::ListStorage defaults; + defaults.emplace_back("firstvalue"); + defaults.emplace_back("secondvalue"); + + std::string pref_name = "mypref"; + prefs_.registry()->RegisterListPref(pref_name, + base::Value(std::move(defaults))); + EXPECT_EQ(2u, prefs_.GetList(pref_name)->GetListDeprecated().size()); + + ListPrefUpdate update(&prefs_, pref_name); + update->Append("thirdvalue"); + EXPECT_EQ(3u, prefs_.GetList(pref_name)->GetListDeprecated().size()); +} + +TEST_F(ScopedUserPrefUpdateTest, UpdatingDictionaryPrefWithDefaults) { + base::Value defaults(base::Value::Type::DICTIONARY); + defaults.SetStringKey("firstkey", "value"); + defaults.SetStringKey("secondkey", "value"); + + std::string pref_name = "mypref"; + prefs_.registry()->RegisterDictionaryPref(pref_name, std::move(defaults)); + EXPECT_EQ(2u, prefs_.GetDictionary(pref_name)->DictSize()); + + DictionaryPrefUpdate update(&prefs_, pref_name); + update->SetStringKey("thirdkey", "value"); + EXPECT_EQ(3u, prefs_.GetDictionary(pref_name)->DictSize()); +} diff --git a/src/components/prefs/segregated_pref_store.cc b/src/components/prefs/segregated_pref_store.cc new file mode 100644 index 0000000000..d27ada8a5b --- /dev/null +++ b/src/components/prefs/segregated_pref_store.cc @@ -0,0 +1,227 @@ +// Copyright 2014 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 "components/prefs/segregated_pref_store.h" + +#include + +#include "base/barrier_closure.h" +#include "base/check_op.h" +#include "base/containers/contains.h" +#include "base/notreached.h" +#include "base/values.h" + +SegregatedPrefStore::UnderlyingPrefStoreObserver::UnderlyingPrefStoreObserver( + SegregatedPrefStore* outer) + : outer_(outer) { + DCHECK(outer_); +} + +void SegregatedPrefStore::UnderlyingPrefStoreObserver::OnPrefValueChanged( + const std::string& key) { + // Notify Observers only after all underlying PrefStores of the outer + // SegregatedPrefStore are initialized. + if (!outer_->IsInitializationComplete()) + return; + + for (auto& observer : outer_->observers_) + observer.OnPrefValueChanged(key); +} + +void SegregatedPrefStore::UnderlyingPrefStoreObserver:: + OnInitializationCompleted(bool succeeded) { + initialization_succeeded_ = succeeded; + + // Notify Observers only after all underlying PrefStores of the outer + // SegregatedPrefStore are initialized. + if (!outer_->IsInitializationComplete()) + return; + + if (outer_->read_error_delegate_) { + PersistentPrefStore::PrefReadError read_error = outer_->GetReadError(); + if (read_error != PersistentPrefStore::PREF_READ_ERROR_NONE) + outer_->read_error_delegate_->OnError(read_error); + } + + for (auto& observer : outer_->observers_) + observer.OnInitializationCompleted(outer_->IsInitializationSuccessful()); +} + +SegregatedPrefStore::SegregatedPrefStore( + scoped_refptr default_pref_store, + scoped_refptr selected_pref_store, + std::set selected_pref_names) + : default_pref_store_(std::move(default_pref_store)), + selected_pref_store_(std::move(selected_pref_store)), + selected_preference_names_(std::move(selected_pref_names)), + default_observer_(this), + selected_observer_(this) { + default_pref_store_->AddObserver(&default_observer_); + selected_pref_store_->AddObserver(&selected_observer_); +} + +void SegregatedPrefStore::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void SegregatedPrefStore::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +bool SegregatedPrefStore::HasObservers() const { + return !observers_.empty(); +} + +bool SegregatedPrefStore::IsInitializationComplete() const { + return default_pref_store_->IsInitializationComplete() && + selected_pref_store_->IsInitializationComplete(); +} + +bool SegregatedPrefStore::IsInitializationSuccessful() const { + return default_observer_.initialization_succeeded() && + selected_observer_.initialization_succeeded(); +} + +bool SegregatedPrefStore::GetValue(const std::string& key, + const base::Value** result) const { + return StoreForKey(key)->GetValue(key, result); +} + +std::unique_ptr SegregatedPrefStore::GetValues() const { + auto values = default_pref_store_->GetValues(); + auto selected_pref_store_values = selected_pref_store_->GetValues(); + for (const auto& key : selected_preference_names_) { + if (const base::Value* value = selected_pref_store_values->FindPath(key)) { + values->SetPath(key, value->Clone()); + } else { + values->RemoveKey(key); + } + } + return values; +} + +void SegregatedPrefStore::SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + StoreForKey(key)->SetValue(key, std::move(value), flags); +} + +void SegregatedPrefStore::RemoveValue(const std::string& key, uint32_t flags) { + StoreForKey(key)->RemoveValue(key, flags); +} + +void SegregatedPrefStore::RemoveValuesByPrefixSilently( + const std::string& prefix) { + // Since we can't guarantee to have all the prefs in one the pref stores, we + // have to push the removal command down to both of them. + default_pref_store_->RemoveValuesByPrefixSilently(prefix); + selected_pref_store_->RemoveValuesByPrefixSilently(prefix); +} + +bool SegregatedPrefStore::GetMutableValue(const std::string& key, + base::Value** result) { + return StoreForKey(key)->GetMutableValue(key, result); +} + +void SegregatedPrefStore::ReportValueChanged(const std::string& key, + uint32_t flags) { + StoreForKey(key)->ReportValueChanged(key, flags); +} + +void SegregatedPrefStore::SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + StoreForKey(key)->SetValueSilently(key, std::move(value), flags); +} + +bool SegregatedPrefStore::ReadOnly() const { + return selected_pref_store_->ReadOnly() || default_pref_store_->ReadOnly(); +} + +PersistentPrefStore::PrefReadError SegregatedPrefStore::GetReadError() const { + PersistentPrefStore::PrefReadError read_error = + default_pref_store_->GetReadError(); + if (read_error == PersistentPrefStore::PREF_READ_ERROR_NONE) { + read_error = selected_pref_store_->GetReadError(); + // Ignore NO_FILE from selected_pref_store_. + if (read_error == PersistentPrefStore::PREF_READ_ERROR_NO_FILE) + read_error = PersistentPrefStore::PREF_READ_ERROR_NONE; + } + return read_error; +} + +PersistentPrefStore::PrefReadError SegregatedPrefStore::ReadPrefs() { + // Note: Both of these stores own PrefFilters which makes ReadPrefs + // asynchronous. This is okay in this case as only the first call will be + // truly asynchronous, the second call will then unblock the migration in + // TrackedPreferencesMigrator and complete synchronously. + default_pref_store_->ReadPrefs(); + PersistentPrefStore::PrefReadError selected_store_read_error = + selected_pref_store_->ReadPrefs(); + DCHECK_NE(PersistentPrefStore::PREF_READ_ERROR_ASYNCHRONOUS_TASK_INCOMPLETE, + selected_store_read_error); + + return GetReadError(); +} + +void SegregatedPrefStore::ReadPrefsAsync(ReadErrorDelegate* error_delegate) { + read_error_delegate_.reset(error_delegate); + default_pref_store_->ReadPrefsAsync(NULL); + selected_pref_store_->ReadPrefsAsync(NULL); +} + +void SegregatedPrefStore::CommitPendingWrite( + base::OnceClosure reply_callback, + base::OnceClosure synchronous_done_callback) { + // A BarrierClosure will run its callback wherever the last instance of the + // returned wrapper is invoked. As such it is guaranteed to respect the reply + // vs synchronous semantics assuming |default_pref_store_| and + // |selected_pref_store_| honor it. + + base::RepeatingClosure reply_callback_wrapper = + reply_callback ? base::BarrierClosure(2, std::move(reply_callback)) + : base::RepeatingClosure(); + + base::RepeatingClosure synchronous_callback_wrapper = + synchronous_done_callback + ? base::BarrierClosure(2, std::move(synchronous_done_callback)) + : base::RepeatingClosure(); + + default_pref_store_->CommitPendingWrite(reply_callback_wrapper, + synchronous_callback_wrapper); + selected_pref_store_->CommitPendingWrite(reply_callback_wrapper, + synchronous_callback_wrapper); +} + +void SegregatedPrefStore::SchedulePendingLossyWrites() { + default_pref_store_->SchedulePendingLossyWrites(); + selected_pref_store_->SchedulePendingLossyWrites(); +} + +void SegregatedPrefStore::ClearMutableValues() { + NOTIMPLEMENTED(); +} + +void SegregatedPrefStore::OnStoreDeletionFromDisk() { + default_pref_store_->OnStoreDeletionFromDisk(); + selected_pref_store_->OnStoreDeletionFromDisk(); +} + +SegregatedPrefStore::~SegregatedPrefStore() { + default_pref_store_->RemoveObserver(&default_observer_); + selected_pref_store_->RemoveObserver(&selected_observer_); +} + +PersistentPrefStore* SegregatedPrefStore::StoreForKey(const std::string& key) { + return (base::Contains(selected_preference_names_, key) ? selected_pref_store_ + : default_pref_store_) + .get(); +} + +const PersistentPrefStore* SegregatedPrefStore::StoreForKey( + const std::string& key) const { + return (base::Contains(selected_preference_names_, key) ? selected_pref_store_ + : default_pref_store_) + .get(); +} diff --git a/src/components/prefs/segregated_pref_store.h b/src/components/prefs/segregated_pref_store.h new file mode 100644 index 0000000000..e952fb7c2c --- /dev/null +++ b/src/components/prefs/segregated_pref_store.h @@ -0,0 +1,128 @@ +// Copyright 2014 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_PREFS_SEGREGATED_PREF_STORE_H_ +#define COMPONENTS_PREFS_SEGREGATED_PREF_STORE_H_ + +#include + +#include +#include +#include + +#include "base/compiler_specific.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/observer_list.h" +#include "components/prefs/persistent_pref_store.h" +#include "components/prefs/prefs_export.h" + +// Provides a unified PersistentPrefStore implementation that splits its storage +// and retrieval between two underlying PersistentPrefStore instances: a set of +// preference names is used to partition the preferences. +// +// Combines properties of the two stores as follows: +// * The unified read error will be: +// Selected Store Error +// Default Store Error | NO_ERROR | NO_FILE | other selected | +// NO_ERROR | NO_ERROR | NO_ERROR | other selected | +// NO_FILE | NO_FILE | NO_FILE | NO_FILE | +// other default | other default | other default | other default | +// * The unified initialization success, initialization completion, and +// read-only state are the boolean OR of the underlying stores' properties. +class COMPONENTS_PREFS_EXPORT SegregatedPrefStore : public PersistentPrefStore { + public: + // Creates an instance that delegates to |selected_pref_store| for the + // preferences named in |selected_pref_names| and to |default_pref_store| + // for all others. If an unselected preference is present in + // |selected_pref_store| (i.e. because it was previously selected) it will + // be migrated back to |default_pref_store| upon access via a non-const + // method. + SegregatedPrefStore(scoped_refptr default_pref_store, + scoped_refptr selected_pref_store, + std::set selected_pref_names); + + SegregatedPrefStore(const SegregatedPrefStore&) = delete; + SegregatedPrefStore& operator=(const SegregatedPrefStore&) = delete; + + // PrefStore implementation + void AddObserver(Observer* observer) override; + void RemoveObserver(Observer* observer) override; + bool HasObservers() const override; + bool IsInitializationComplete() const override; + bool GetValue(const std::string& key, + const base::Value** result) const override; + std::unique_ptr GetValues() const override; + + // WriteablePrefStore implementation + void SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + void RemoveValue(const std::string& key, uint32_t flags) override; + void RemoveValuesByPrefixSilently(const std::string& prefix) override; + + // PersistentPrefStore implementation + bool GetMutableValue(const std::string& key, base::Value** result) override; + void ReportValueChanged(const std::string& key, uint32_t flags) override; + void SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + bool ReadOnly() const override; + PrefReadError GetReadError() const override; + PrefReadError ReadPrefs() override; + void ReadPrefsAsync(ReadErrorDelegate* error_delegate) override; + void CommitPendingWrite( + base::OnceClosure reply_callback = base::OnceClosure(), + base::OnceClosure synchronous_done_callback = + base::OnceClosure()) override; + void SchedulePendingLossyWrites() override; + void ClearMutableValues() override; + void OnStoreDeletionFromDisk() override; + + protected: + ~SegregatedPrefStore() override; + + private: + // Caches event state from the underlying stores and exposes the state to the + // provided "outer" SegregatedPrefStore to synthesize external events via + // |read_error_delegate_| and |observers_|. + class UnderlyingPrefStoreObserver : public PrefStore::Observer { + public: + explicit UnderlyingPrefStoreObserver(SegregatedPrefStore* outer); + + UnderlyingPrefStoreObserver(const UnderlyingPrefStoreObserver&) = delete; + UnderlyingPrefStoreObserver& operator=(const UnderlyingPrefStoreObserver&) = + delete; + + // PrefStore::Observer implementation + void OnPrefValueChanged(const std::string& key) override; + void OnInitializationCompleted(bool succeeded) override; + + bool initialization_succeeded() const { return initialization_succeeded_; } + + private: + const raw_ptr outer_; + bool initialization_succeeded_ = false; + }; + + // Returns true only if all underlying PrefStores have initialized + // successfully, otherwise false. + bool IsInitializationSuccessful() const; + + // Returns |selected_pref_store| if |key| is selected and + // |default_pref_store| otherwise. + PersistentPrefStore* StoreForKey(const std::string& key); + const PersistentPrefStore* StoreForKey(const std::string& key) const; + + const scoped_refptr default_pref_store_; + const scoped_refptr selected_pref_store_; + const std::set selected_preference_names_; + + std::unique_ptr read_error_delegate_; + base::ObserverList::Unchecked observers_; + UnderlyingPrefStoreObserver default_observer_; + UnderlyingPrefStoreObserver selected_observer_; +}; + +#endif // COMPONENTS_PREFS_SEGREGATED_PREF_STORE_H_ diff --git a/src/components/prefs/segregated_pref_store_unittest.cc b/src/components/prefs/segregated_pref_store_unittest.cc new file mode 100644 index 0000000000..4014acd8ef --- /dev/null +++ b/src/components/prefs/segregated_pref_store_unittest.cc @@ -0,0 +1,406 @@ +// Copyright 2014 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 "components/prefs/segregated_pref_store.h" + +#include +#include +#include +#include + +#include "base/bind.h" +#include "base/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/run_loop.h" +#include "base/synchronization/waitable_event.h" +#include "base/test/task_environment.h" +#include "base/values.h" +#include "components/prefs/persistent_pref_store.h" +#include "components/prefs/pref_store_observer_mock.h" +#include "components/prefs/testing_pref_store.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char kSelectedPref[] = "selected_pref"; +const char kUnselectedPref[] = "unselected_pref"; +const char kSharedPref[] = "shared_pref"; + +const char kValue1[] = "value1"; +const char kValue2[] = "value2"; + +class MockReadErrorDelegate : public PersistentPrefStore::ReadErrorDelegate { + public: + struct Data { + Data(bool invoked_in, PersistentPrefStore::PrefReadError read_error_in) + : invoked(invoked_in), read_error(read_error_in) {} + + bool invoked; + PersistentPrefStore::PrefReadError read_error; + }; + + explicit MockReadErrorDelegate(Data* data) : data_(data) { + DCHECK(data_); + EXPECT_FALSE(data_->invoked); + } + + // PersistentPrefStore::ReadErrorDelegate implementation + void OnError(PersistentPrefStore::PrefReadError read_error) override { + EXPECT_FALSE(data_->invoked); + data_->invoked = true; + data_->read_error = read_error; + } + + private: + raw_ptr data_; +}; + +enum class CommitPendingWriteMode { + // Basic mode. + WITHOUT_CALLBACK, + // With reply callback. + WITH_CALLBACK, + // With synchronous notify callback (synchronous after the write -- shouldn't + // require pumping messages to observe). + WITH_SYNCHRONOUS_CALLBACK, +}; + +class SegregatedPrefStoreTest + : public testing::TestWithParam { + public: + SegregatedPrefStoreTest() + : read_error_delegate_data_(false, + PersistentPrefStore::PREF_READ_ERROR_NONE), + read_error_delegate_( + new MockReadErrorDelegate(&read_error_delegate_data_)) {} + + void SetUp() override { + selected_store_ = new TestingPrefStore; + default_store_ = new TestingPrefStore; + + selected_pref_names_.insert(kSelectedPref); + selected_pref_names_.insert(kSharedPref); + segregated_store_ = new SegregatedPrefStore(default_store_, selected_store_, + selected_pref_names_); + segregated_store_->AddObserver(&observer_); + } + + void TearDown() override { segregated_store_->RemoveObserver(&observer_); } + + protected: + std::unique_ptr + GetReadErrorDelegate() { + EXPECT_TRUE(read_error_delegate_); + return std::move(read_error_delegate_); + } + + base::test::TaskEnvironment task_environment_; + + PrefStoreObserverMock observer_; + + scoped_refptr default_store_; + scoped_refptr selected_store_; + scoped_refptr segregated_store_; + + std::set selected_pref_names_; + MockReadErrorDelegate::Data read_error_delegate_data_; + + private: + std::unique_ptr read_error_delegate_; +}; + +} // namespace + +TEST_P(SegregatedPrefStoreTest, StoreValues) { + ASSERT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, + segregated_store_->ReadPrefs()); + + // Properly stores new values. + segregated_store_->SetValue(kSelectedPref, + std::make_unique(kValue1), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + segregated_store_->SetValue(kUnselectedPref, + std::make_unique(kValue2), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + ASSERT_TRUE(selected_store_->GetValue(kSelectedPref, NULL)); + ASSERT_FALSE(selected_store_->GetValue(kUnselectedPref, NULL)); + ASSERT_FALSE(default_store_->GetValue(kSelectedPref, NULL)); + ASSERT_TRUE(default_store_->GetValue(kUnselectedPref, NULL)); + + ASSERT_TRUE(segregated_store_->GetValue(kSelectedPref, NULL)); + ASSERT_TRUE(segregated_store_->GetValue(kUnselectedPref, NULL)); + + ASSERT_FALSE(selected_store_->committed()); + ASSERT_FALSE(default_store_->committed()); + + switch (GetParam()) { + case CommitPendingWriteMode::WITHOUT_CALLBACK: { + segregated_store_->CommitPendingWrite(); + base::RunLoop().RunUntilIdle(); + break; + } + + case CommitPendingWriteMode::WITH_CALLBACK: { + base::RunLoop run_loop; + segregated_store_->CommitPendingWrite(run_loop.QuitClosure()); + run_loop.Run(); + break; + } + + case CommitPendingWriteMode::WITH_SYNCHRONOUS_CALLBACK: { + base::WaitableEvent written; + segregated_store_->CommitPendingWrite( + base::OnceClosure(), + base::BindOnce(&base::WaitableEvent::Signal, Unretained(&written))); + written.Wait(); + break; + } + } + + ASSERT_TRUE(selected_store_->committed()); + ASSERT_TRUE(default_store_->committed()); +} + +TEST_F(SegregatedPrefStoreTest, ReadValues) { + selected_store_->SetValue(kSelectedPref, + std::make_unique(kValue1), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + default_store_->SetValue(kUnselectedPref, + std::make_unique(kValue2), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + // Works properly with values that are already there. + ASSERT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, + segregated_store_->ReadPrefs()); + ASSERT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, + segregated_store_->GetReadError()); + + ASSERT_TRUE(selected_store_->GetValue(kSelectedPref, NULL)); + ASSERT_FALSE(selected_store_->GetValue(kUnselectedPref, NULL)); + ASSERT_FALSE(default_store_->GetValue(kSelectedPref, NULL)); + ASSERT_TRUE(default_store_->GetValue(kUnselectedPref, NULL)); + + ASSERT_TRUE(segregated_store_->GetValue(kSelectedPref, NULL)); + ASSERT_TRUE(segregated_store_->GetValue(kUnselectedPref, NULL)); +} + +TEST_F(SegregatedPrefStoreTest, RemoveValuesByPrefix) { + const std::string subpref_name1 = kSelectedPref; + const std::string subpref_name2 = std::string(kSelectedPref) + "b"; + const std::string other_name = kUnselectedPref; + const std::string prefix = kSelectedPref; + + selected_store_->SetValue(subpref_name1, + std::make_unique(kValue1), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + default_store_->SetValue(subpref_name2, + std::make_unique(kValue2), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + default_store_->SetValue(other_name, std::make_unique(kValue2), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + ASSERT_TRUE(selected_store_->GetValue(subpref_name1, nullptr)); + ASSERT_TRUE(default_store_->GetValue(subpref_name2, nullptr)); + ASSERT_TRUE(default_store_->GetValue(other_name, nullptr)); + + segregated_store_->RemoveValuesByPrefixSilently(kSelectedPref); + + ASSERT_FALSE(selected_store_->GetValue(subpref_name1, nullptr)); + ASSERT_FALSE(default_store_->GetValue(subpref_name2, nullptr)); + ASSERT_TRUE(default_store_->GetValue(other_name, nullptr)); +} + +TEST_F(SegregatedPrefStoreTest, Observer) { + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, + segregated_store_->ReadPrefs()); + EXPECT_TRUE(observer_.initialized); + EXPECT_TRUE(observer_.initialization_success); + EXPECT_TRUE(observer_.changed_keys.empty()); + segregated_store_->SetValue(kSelectedPref, + std::make_unique(kValue1), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + observer_.VerifyAndResetChangedKey(kSelectedPref); + segregated_store_->SetValue(kUnselectedPref, + std::make_unique(kValue2), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + observer_.VerifyAndResetChangedKey(kUnselectedPref); +} + +TEST_F(SegregatedPrefStoreTest, + ObserverAfterConstructionAfterSubInitialization) { + // Ensure that underlying PrefStores are initialized first. + default_store_->ReadPrefs(); + selected_store_->ReadPrefs(); + EXPECT_TRUE(default_store_->IsInitializationComplete()); + EXPECT_TRUE(selected_store_->IsInitializationComplete()); + + // Create a new SegregatedPrefStore based on the initialized PrefStores. + segregated_store_->RemoveObserver(&observer_); + segregated_store_ = base::MakeRefCounted( + default_store_, selected_store_, selected_pref_names_); + segregated_store_->AddObserver(&observer_); + EXPECT_TRUE(segregated_store_->IsInitializationComplete()); + + // The Observer should receive notifications from the SegregatedPrefStore. + EXPECT_TRUE(observer_.changed_keys.empty()); + segregated_store_->SetValue(kSelectedPref, + std::make_unique(kValue1), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + observer_.VerifyAndResetChangedKey(kSelectedPref); + segregated_store_->SetValue(kUnselectedPref, + std::make_unique(kValue2), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + observer_.VerifyAndResetChangedKey(kUnselectedPref); +} + +TEST_F(SegregatedPrefStoreTest, SelectedPrefReadNoFileError) { + // PREF_READ_ERROR_NO_FILE for the selected prefs file is silently converted + // to PREF_READ_ERROR_NONE. + selected_store_->set_read_error(PersistentPrefStore::PREF_READ_ERROR_NO_FILE); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, + segregated_store_->ReadPrefs()); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, + segregated_store_->GetReadError()); +} + +TEST_F(SegregatedPrefStoreTest, SelectedPrefReadError) { + selected_store_->set_read_error( + PersistentPrefStore::PREF_READ_ERROR_ACCESS_DENIED); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_ACCESS_DENIED, + segregated_store_->ReadPrefs()); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_ACCESS_DENIED, + segregated_store_->GetReadError()); +} + +TEST_F(SegregatedPrefStoreTest, SelectedPrefReadNoFileErrorAsync) { + // PREF_READ_ERROR_NO_FILE for the selected prefs file is silently converted + // to PREF_READ_ERROR_NONE. + selected_store_->set_read_error(PersistentPrefStore::PREF_READ_ERROR_NO_FILE); + + default_store_->SetBlockAsyncRead(true); + + EXPECT_FALSE(read_error_delegate_data_.invoked); + + segregated_store_->ReadPrefsAsync(GetReadErrorDelegate().release()); + + EXPECT_FALSE(read_error_delegate_data_.invoked); + + default_store_->SetBlockAsyncRead(false); + + // ReadErrorDelegate is not invoked for ERROR_NONE. + EXPECT_FALSE(read_error_delegate_data_.invoked); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, + segregated_store_->GetReadError()); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NONE, + segregated_store_->GetReadError()); +} + +TEST_F(SegregatedPrefStoreTest, UnselectedPrefReadNoFileError) { + default_store_->set_read_error(PersistentPrefStore::PREF_READ_ERROR_NO_FILE); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NO_FILE, + segregated_store_->ReadPrefs()); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NO_FILE, + segregated_store_->GetReadError()); +} + +TEST_F(SegregatedPrefStoreTest, UnselectedPrefReadError) { + default_store_->set_read_error( + PersistentPrefStore::PREF_READ_ERROR_ACCESS_DENIED); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_ACCESS_DENIED, + segregated_store_->ReadPrefs()); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_ACCESS_DENIED, + segregated_store_->GetReadError()); +} + +TEST_F(SegregatedPrefStoreTest, BothPrefReadError) { + default_store_->set_read_error(PersistentPrefStore::PREF_READ_ERROR_NO_FILE); + selected_store_->set_read_error( + PersistentPrefStore::PREF_READ_ERROR_ACCESS_DENIED); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NO_FILE, + segregated_store_->ReadPrefs()); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NO_FILE, + segregated_store_->GetReadError()); +} + +TEST_F(SegregatedPrefStoreTest, BothPrefReadErrorAsync) { + default_store_->set_read_error(PersistentPrefStore::PREF_READ_ERROR_NO_FILE); + selected_store_->set_read_error( + PersistentPrefStore::PREF_READ_ERROR_ACCESS_DENIED); + + selected_store_->SetBlockAsyncRead(true); + + EXPECT_FALSE(read_error_delegate_data_.invoked); + + segregated_store_->ReadPrefsAsync(GetReadErrorDelegate().release()); + + EXPECT_FALSE(read_error_delegate_data_.invoked); + + selected_store_->SetBlockAsyncRead(false); + + EXPECT_TRUE(read_error_delegate_data_.invoked); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NO_FILE, + segregated_store_->GetReadError()); + EXPECT_EQ(PersistentPrefStore::PREF_READ_ERROR_NO_FILE, + segregated_store_->GetReadError()); +} + +TEST_F(SegregatedPrefStoreTest, IsInitializationComplete) { + EXPECT_FALSE(segregated_store_->IsInitializationComplete()); + segregated_store_->ReadPrefs(); + EXPECT_TRUE(segregated_store_->IsInitializationComplete()); +} + +TEST_F(SegregatedPrefStoreTest, IsInitializationCompleteAsync) { + selected_store_->SetBlockAsyncRead(true); + default_store_->SetBlockAsyncRead(true); + EXPECT_FALSE(segregated_store_->IsInitializationComplete()); + segregated_store_->ReadPrefsAsync(NULL); + EXPECT_FALSE(segregated_store_->IsInitializationComplete()); + selected_store_->SetBlockAsyncRead(false); + EXPECT_FALSE(segregated_store_->IsInitializationComplete()); + default_store_->SetBlockAsyncRead(false); + EXPECT_TRUE(segregated_store_->IsInitializationComplete()); +} + +TEST_F(SegregatedPrefStoreTest, GetValues) { + // To check merge behavior, create selected and default stores so each has a + // key the other doesn't have and they have one key in common. + selected_store_->SetValue(kSelectedPref, + std::make_unique(kValue1), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + default_store_->SetValue(kUnselectedPref, + std::make_unique(kValue2), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + selected_store_->SetValue(kSharedPref, std::make_unique(kValue1), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); + + auto values = segregated_store_->GetValues(); + const base::Value* value = nullptr; + // Check that a selected preference is returned. + ASSERT_TRUE(values->Get(kSelectedPref, &value)); + EXPECT_EQ(base::Value(kValue1), *value); + + // Check that a a default preference is returned. + ASSERT_TRUE(values->Get(kUnselectedPref, &value)); + EXPECT_EQ(base::Value(kValue2), *value); + + // Check that the selected preference is preferred. + ASSERT_TRUE(values->Get(kSharedPref, &value)); + EXPECT_EQ(base::Value(kValue1), *value); +} + +INSTANTIATE_TEST_SUITE_P( + WithoutCallback, + SegregatedPrefStoreTest, + ::testing::Values(CommitPendingWriteMode::WITHOUT_CALLBACK)); +INSTANTIATE_TEST_SUITE_P( + WithCallback, + SegregatedPrefStoreTest, + ::testing::Values(CommitPendingWriteMode::WITH_CALLBACK)); +INSTANTIATE_TEST_SUITE_P( + WithSynchronousCallback, + SegregatedPrefStoreTest, + ::testing::Values(CommitPendingWriteMode::WITH_SYNCHRONOUS_CALLBACK)); diff --git a/src/components/prefs/testing_pref_service.cc b/src/components/prefs/testing_pref_service.cc new file mode 100644 index 0000000000..07d3400daa --- /dev/null +++ b/src/components/prefs/testing_pref_service.cc @@ -0,0 +1,68 @@ +// Copyright (c) 2012 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 "components/prefs/testing_pref_service.h" + +#include + +#include "base/bind.h" +#include "base/compiler_specific.h" +#include "components/prefs/default_pref_store.h" +#include "components/prefs/pref_notifier_impl.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/pref_value_store.h" +#include "testing/gtest/include/gtest/gtest.h" + +template <> +TestingPrefServiceBase::TestingPrefServiceBase( + TestingPrefStore* managed_prefs, + TestingPrefStore* supervised_user_prefs, + TestingPrefStore* extension_prefs, + TestingPrefStore* standalone_browser_prefs, + TestingPrefStore* user_prefs, + TestingPrefStore* recommended_prefs, + PrefRegistry* pref_registry, + PrefNotifierImpl* pref_notifier) + : PrefService( + std::unique_ptr(pref_notifier), + std::make_unique(managed_prefs, + supervised_user_prefs, + extension_prefs, + standalone_browser_prefs, + /*command_line_prefs=*/nullptr, + user_prefs, + recommended_prefs, + pref_registry->defaults().get(), + pref_notifier), + user_prefs, + standalone_browser_prefs, + pref_registry, + base::BindRepeating( + &TestingPrefServiceBase::HandleReadError), + false), + managed_prefs_(managed_prefs), + supervised_user_prefs_(supervised_user_prefs), + extension_prefs_(extension_prefs), + standalone_browser_prefs_(standalone_browser_prefs), + user_prefs_(user_prefs), + recommended_prefs_(recommended_prefs) {} + +TestingPrefServiceSimple::TestingPrefServiceSimple() + : TestingPrefServiceBase( + /*managed_prefs=*/new TestingPrefStore(), + /*supervised_user_prefs=*/new TestingPrefStore(), + /*extension_prefs=*/new TestingPrefStore(), + /*standalone_browser_prefs=*/new TestingPrefStore(), + /*user_prefs=*/new TestingPrefStore(), + /*recommended_prefs=*/new TestingPrefStore(), + new PrefRegistrySimple(), + new PrefNotifierImpl()) {} + +TestingPrefServiceSimple::~TestingPrefServiceSimple() { +} + +PrefRegistrySimple* TestingPrefServiceSimple::registry() { + return static_cast(DeprecatedGetPrefRegistry()); +} diff --git a/src/components/prefs/testing_pref_service.h b/src/components/prefs/testing_pref_service.h new file mode 100644 index 0000000000..429a11c596 --- /dev/null +++ b/src/components/prefs/testing_pref_service.h @@ -0,0 +1,304 @@ +// Copyright (c) 2012 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_PREFS_TESTING_PREF_SERVICE_H_ +#define COMPONENTS_PREFS_TESTING_PREF_SERVICE_H_ + +#include +#include + +#include "base/memory/ref_counted.h" +#include "components/prefs/pref_registry.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/testing_pref_store.h" + +class PrefNotifierImpl; +class PrefRegistrySimple; +class TestingPrefStore; + +// A PrefService subclass for testing. It operates totally in memory and +// provides additional API for manipulating preferences at the different levels +// (managed, extension, user) conveniently. +// +// Use this via its specializations, e.g. TestingPrefServiceSimple. +template +class TestingPrefServiceBase : public SuperPrefService { + public: + TestingPrefServiceBase(const TestingPrefServiceBase&) = delete; + TestingPrefServiceBase& operator=(const TestingPrefServiceBase&) = delete; + + virtual ~TestingPrefServiceBase(); + + // Reads the value of a preference from the managed layer. Returns NULL if the + // preference is not defined at the managed layer. + const base::Value* GetManagedPref(const std::string& path) const; + + // Sets a preference on the managed layer and fires observers if the + // preference changed. + void SetManagedPref(const std::string& path, + std::unique_ptr value); + void SetManagedPref(const std::string& path, base::Value value); + + // Clears the preference on the managed layer and fire observers if the + // preference has been defined previously. + void RemoveManagedPref(const std::string& path); + + // Similar to the above, but for supervised user preferences. + const base::Value* GetSupervisedUserPref(const std::string& path) const; + void SetSupervisedUserPref(const std::string& path, + std::unique_ptr value); + void RemoveSupervisedUserPref(const std::string& path); + + // Similar to the above, but for extension preferences. + // Does not really know about extensions and their order of installation. + // Useful in tests that only check that a preference is overridden by an + // extension. + const base::Value* GetExtensionPref(const std::string& path) const; + void SetExtensionPref(const std::string& path, + std::unique_ptr value); + void RemoveExtensionPref(const std::string& path); + + // Similar to the above, but for user preferences. + const base::Value* GetUserPref(const std::string& path) const; + void SetUserPref(const std::string& path, std::unique_ptr value); + void SetUserPref(const std::string& path, base::Value value); + void RemoveUserPref(const std::string& path); + + // Similar to the above, but for recommended policy preferences. + const base::Value* GetRecommendedPref(const std::string& path) const; + void SetRecommendedPref(const std::string& path, + std::unique_ptr value); + void SetRecommendedPref(const std::string& path, base::Value value); + void RemoveRecommendedPref(const std::string& path); + + // Do-nothing implementation for TestingPrefService. + static void HandleReadError(PersistentPrefStore::PrefReadError error) {} + + // Set initialization status of pref stores. + void SetInitializationCompleted(); + + scoped_refptr user_prefs_store() { return user_prefs_; } + + protected: + TestingPrefServiceBase(TestingPrefStore* managed_prefs, + TestingPrefStore* supervised_user_prefs, + TestingPrefStore* extension_prefs, + TestingPrefStore* standalone_browser_prefs, + TestingPrefStore* user_prefs, + TestingPrefStore* recommended_prefs, + ConstructionPrefRegistry* pref_registry, + PrefNotifierImpl* pref_notifier); + + private: + // Reads the value of the preference indicated by |path| from |pref_store|. + // Returns NULL if the preference was not found. + const base::Value* GetPref(TestingPrefStore* pref_store, + const std::string& path) const; + + // Sets the value for |path| in |pref_store|. + void SetPref(TestingPrefStore* pref_store, + const std::string& path, + std::unique_ptr value); + + // Removes the preference identified by |path| from |pref_store|. + void RemovePref(TestingPrefStore* pref_store, const std::string& path); + + // Pointers to the pref stores our value store uses. + scoped_refptr managed_prefs_; + scoped_refptr supervised_user_prefs_; + scoped_refptr extension_prefs_; + scoped_refptr standalone_browser_prefs_; + scoped_refptr user_prefs_; + scoped_refptr recommended_prefs_; +}; + +// Test version of PrefService. +class TestingPrefServiceSimple + : public TestingPrefServiceBase { + public: + TestingPrefServiceSimple(); + + TestingPrefServiceSimple(const TestingPrefServiceSimple&) = delete; + TestingPrefServiceSimple& operator=(const TestingPrefServiceSimple&) = delete; + + ~TestingPrefServiceSimple() override; + + // This is provided as a convenience for registering preferences on + // an existing TestingPrefServiceSimple instance. On a production + // PrefService you would do all registrations before constructing + // it, passing it a PrefRegistry via its constructor (or via + // e.g. PrefServiceFactory). + PrefRegistrySimple* registry(); +}; + +template <> +TestingPrefServiceBase::TestingPrefServiceBase( + TestingPrefStore* managed_prefs, + TestingPrefStore* supervised_user_prefs, + TestingPrefStore* extension_prefs, + TestingPrefStore* standalone_browser_prefs, + TestingPrefStore* user_prefs, + TestingPrefStore* recommended_prefs, + PrefRegistry* pref_registry, + PrefNotifierImpl* pref_notifier); + +template +TestingPrefServiceBase< + SuperPrefService, ConstructionPrefRegistry>::~TestingPrefServiceBase() { +} + +template +const base::Value* TestingPrefServiceBase< + SuperPrefService, + ConstructionPrefRegistry>::GetManagedPref(const std::string& path) const { + return GetPref(managed_prefs_.get(), path); +} + +template +void TestingPrefServiceBase:: + SetManagedPref(const std::string& path, + std::unique_ptr value) { + SetPref(managed_prefs_.get(), path, std::move(value)); +} + +template +void TestingPrefServiceBase:: + SetManagedPref(const std::string& path, base::Value value) { + SetManagedPref(path, base::Value::ToUniquePtrValue(std::move(value))); +} + +template +void TestingPrefServiceBase:: + RemoveManagedPref(const std::string& path) { + RemovePref(managed_prefs_.get(), path); +} + +template +const base::Value* +TestingPrefServiceBase:: + GetSupervisedUserPref(const std::string& path) const { + return GetPref(supervised_user_prefs_.get(), path); +} + +template +void TestingPrefServiceBase:: + SetSupervisedUserPref(const std::string& path, + std::unique_ptr value) { + SetPref(supervised_user_prefs_.get(), path, std::move(value)); +} + +template +void TestingPrefServiceBase:: + RemoveSupervisedUserPref(const std::string& path) { + RemovePref(supervised_user_prefs_.get(), path); +} + +template +const base::Value* TestingPrefServiceBase< + SuperPrefService, + ConstructionPrefRegistry>::GetExtensionPref(const std::string& path) const { + return GetPref(extension_prefs_.get(), path); +} + +template +void TestingPrefServiceBase:: + SetExtensionPref(const std::string& path, + std::unique_ptr value) { + SetPref(extension_prefs_.get(), path, std::move(value)); +} + +template +void TestingPrefServiceBase:: + RemoveExtensionPref(const std::string& path) { + RemovePref(extension_prefs_.get(), path); +} + +template +const base::Value* +TestingPrefServiceBase::GetUserPref( + const std::string& path) const { + return GetPref(user_prefs_.get(), path); +} + +template +void TestingPrefServiceBase:: + SetUserPref(const std::string& path, std::unique_ptr value) { + SetPref(user_prefs_.get(), path, std::move(value)); +} + +template +void TestingPrefServiceBase:: + SetUserPref(const std::string& path, base::Value value) { + SetUserPref(path, base::Value::ToUniquePtrValue(std::move(value))); +} + +template +void TestingPrefServiceBase:: + RemoveUserPref(const std::string& path) { + RemovePref(user_prefs_.get(), path); +} + +template +const base::Value* +TestingPrefServiceBase:: + GetRecommendedPref(const std::string& path) const { + return GetPref(recommended_prefs_, path); +} + +template +void TestingPrefServiceBase:: + SetRecommendedPref(const std::string& path, + std::unique_ptr value) { + SetPref(recommended_prefs_.get(), path, std::move(value)); +} + +template +void TestingPrefServiceBase:: + SetRecommendedPref(const std::string& path, base::Value value) { + SetPref(recommended_prefs_.get(), path, + base::Value::ToUniquePtrValue(std::move(value))); +} + +template +void TestingPrefServiceBase:: + RemoveRecommendedPref(const std::string& path) { + RemovePref(recommended_prefs_.get(), path); +} + +template +const base::Value* +TestingPrefServiceBase::GetPref( + TestingPrefStore* pref_store, + const std::string& path) const { + const base::Value* res; + return pref_store->GetValue(path, &res) ? res : NULL; +} + +template +void TestingPrefServiceBase:: + SetPref(TestingPrefStore* pref_store, + const std::string& path, + std::unique_ptr value) { + pref_store->SetValue(path, std::move(value), + WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); +} + +template +void TestingPrefServiceBase:: + RemovePref(TestingPrefStore* pref_store, const std::string& path) { + pref_store->RemoveValue(path, WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); +} + +template +void TestingPrefServiceBase:: + SetInitializationCompleted() { + managed_prefs_->SetInitializationCompleted(); + supervised_user_prefs_->SetInitializationCompleted(); + extension_prefs_->SetInitializationCompleted(); + recommended_prefs_->SetInitializationCompleted(); + // |user_prefs_| and |standalone_browser_prefs_| are initialized in + // PrefService constructor so no need to set initialization status again. +} + +#endif // COMPONENTS_PREFS_TESTING_PREF_SERVICE_H_ diff --git a/src/components/prefs/testing_pref_store.cc b/src/components/prefs/testing_pref_store.cc new file mode 100644 index 0000000000..5d3d1ec0b5 --- /dev/null +++ b/src/components/prefs/testing_pref_store.cc @@ -0,0 +1,232 @@ +// Copyright (c) 2012 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 "components/prefs/testing_pref_store.h" + +#include +#include + +#include "base/json/json_writer.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/values.h" +#include "testing/gtest/include/gtest/gtest.h" + +TestingPrefStore::TestingPrefStore() + : read_only_(true), + read_success_(true), + read_error_(PersistentPrefStore::PREF_READ_ERROR_NONE), + block_async_read_(false), + pending_async_read_(false), + init_complete_(false), + committed_(true) {} + +bool TestingPrefStore::GetValue(const std::string& key, + const base::Value** value) const { + return prefs_.GetValue(key, value); +} + +std::unique_ptr TestingPrefStore::GetValues() const { + return prefs_.AsDictionaryValue(); +} + +bool TestingPrefStore::GetMutableValue(const std::string& key, + base::Value** value) { + return prefs_.GetValue(key, value); +} + +void TestingPrefStore::AddObserver(PrefStore::Observer* observer) { + observers_.AddObserver(observer); +} + +void TestingPrefStore::RemoveObserver(PrefStore::Observer* observer) { + observers_.RemoveObserver(observer); +} + +bool TestingPrefStore::HasObservers() const { + return !observers_.empty(); +} + +bool TestingPrefStore::IsInitializationComplete() const { + return init_complete_; +} + +void TestingPrefStore::SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + DCHECK(value); + if (prefs_.SetValue(key, base::Value::FromUniquePtrValue(std::move(value)))) { + committed_ = false; + NotifyPrefValueChanged(key); + } +} + +void TestingPrefStore::SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + DCHECK(value); + CheckPrefIsSerializable(key, *value); + if (prefs_.SetValue(key, base::Value::FromUniquePtrValue(std::move(value)))) + committed_ = false; +} + +void TestingPrefStore::RemoveValue(const std::string& key, uint32_t flags) { + if (prefs_.RemoveValue(key)) { + committed_ = false; + NotifyPrefValueChanged(key); + } +} + +void TestingPrefStore::RemoveValuesByPrefixSilently(const std::string& prefix) { + prefs_.ClearWithPrefix(prefix); +} + +bool TestingPrefStore::ReadOnly() const { + return read_only_; +} + +PersistentPrefStore::PrefReadError TestingPrefStore::GetReadError() const { + return read_error_; +} + +PersistentPrefStore::PrefReadError TestingPrefStore::ReadPrefs() { + NotifyInitializationCompleted(); + return read_error_; +} + +void TestingPrefStore::ReadPrefsAsync(ReadErrorDelegate* error_delegate) { + DCHECK(!pending_async_read_); + error_delegate_.reset(error_delegate); + if (block_async_read_) + pending_async_read_ = true; + else + NotifyInitializationCompleted(); +} + +void TestingPrefStore::CommitPendingWrite( + base::OnceClosure reply_callback, + base::OnceClosure synchronous_done_callback) { + committed_ = true; + PersistentPrefStore::CommitPendingWrite(std::move(reply_callback), + std::move(synchronous_done_callback)); +} + +void TestingPrefStore::SchedulePendingLossyWrites() {} + +void TestingPrefStore::SetInitializationCompleted() { + NotifyInitializationCompleted(); +} + +void TestingPrefStore::NotifyPrefValueChanged(const std::string& key) { + for (Observer& observer : observers_) + observer.OnPrefValueChanged(key); +} + +void TestingPrefStore::NotifyInitializationCompleted() { + DCHECK(!init_complete_); + init_complete_ = true; + if (read_success_ && read_error_ != PREF_READ_ERROR_NONE && error_delegate_) + error_delegate_->OnError(read_error_); + for (Observer& observer : observers_) + observer.OnInitializationCompleted(read_success_); +} + +void TestingPrefStore::ReportValueChanged(const std::string& key, + uint32_t flags) { + const base::Value* value = nullptr; + if (prefs_.GetValue(key, &value)) + CheckPrefIsSerializable(key, *value); + + for (Observer& observer : observers_) + observer.OnPrefValueChanged(key); +} + +void TestingPrefStore::SetString(const std::string& key, + const std::string& value) { + SetValue(key, std::make_unique(value), DEFAULT_PREF_WRITE_FLAGS); +} + +void TestingPrefStore::SetInteger(const std::string& key, int value) { + SetValue(key, std::make_unique(value), DEFAULT_PREF_WRITE_FLAGS); +} + +void TestingPrefStore::SetBoolean(const std::string& key, bool value) { + SetValue(key, std::make_unique(value), DEFAULT_PREF_WRITE_FLAGS); +} + +bool TestingPrefStore::GetString(const std::string& key, + std::string* value) const { + const base::Value* stored_value; + if (!prefs_.GetValue(key, &stored_value) || !stored_value) + return false; + + if (value && stored_value->is_string()) { + *value = stored_value->GetString(); + return true; + } + return stored_value->is_string(); +} + +bool TestingPrefStore::GetInteger(const std::string& key, int* value) const { + const base::Value* stored_value; + if (!prefs_.GetValue(key, &stored_value) || !stored_value) + return false; + + if (value && stored_value->is_int()) { + *value = stored_value->GetInt(); + return true; + } + return stored_value->is_int(); +} + +bool TestingPrefStore::GetBoolean(const std::string& key, bool* value) const { + const base::Value* stored_value; + if (!prefs_.GetValue(key, &stored_value) || !stored_value) + return false; + + if (value && stored_value->is_bool()) { + *value = stored_value->GetBool(); + return true; + } + return stored_value->is_bool(); +} + +void TestingPrefStore::SetBlockAsyncRead(bool block_async_read) { + DCHECK(!init_complete_); + block_async_read_ = block_async_read; + if (pending_async_read_ && !block_async_read_) + NotifyInitializationCompleted(); +} + +void TestingPrefStore::ClearMutableValues() { + NOTIMPLEMENTED(); +} + +void TestingPrefStore::OnStoreDeletionFromDisk() {} + +void TestingPrefStore::set_read_only(bool read_only) { + read_only_ = read_only; +} + +void TestingPrefStore::set_read_success(bool read_success) { + DCHECK(!init_complete_); + read_success_ = read_success; +} + +void TestingPrefStore::set_read_error( + PersistentPrefStore::PrefReadError read_error) { + DCHECK(!init_complete_); + read_error_ = read_error; +} + +TestingPrefStore::~TestingPrefStore() { + for (auto& pref : prefs_) + CheckPrefIsSerializable(pref.first, pref.second); +} + +void TestingPrefStore::CheckPrefIsSerializable(const std::string& key, + const base::Value& value) { + std::string json; + EXPECT_TRUE(base::JSONWriter::Write(value, &json)) + << "Pref \"" << key << "\" is not serializable as JSON."; +} diff --git a/src/components/prefs/testing_pref_store.h b/src/components/prefs/testing_pref_store.h new file mode 100644 index 0000000000..a4b5d000b0 --- /dev/null +++ b/src/components/prefs/testing_pref_store.h @@ -0,0 +1,123 @@ +// Copyright (c) 2012 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_PREFS_TESTING_PREF_STORE_H_ +#define COMPONENTS_PREFS_TESTING_PREF_STORE_H_ + +#include + +#include + +#include "base/compiler_specific.h" +#include "base/observer_list.h" +#include "components/prefs/persistent_pref_store.h" +#include "components/prefs/pref_value_map.h" + +// |TestingPrefStore| is a preference store implementation that allows tests to +// explicitly manipulate the contents of the store, triggering notifications +// where appropriate. +class TestingPrefStore : public PersistentPrefStore { + public: + TestingPrefStore(); + + TestingPrefStore(const TestingPrefStore&) = delete; + TestingPrefStore& operator=(const TestingPrefStore&) = delete; + + // Overriden from PrefStore. + bool GetValue(const std::string& key, + const base::Value** result) const override; + std::unique_ptr GetValues() const override; + void AddObserver(PrefStore::Observer* observer) override; + void RemoveObserver(PrefStore::Observer* observer) override; + bool HasObservers() const override; + bool IsInitializationComplete() const override; + + // PersistentPrefStore overrides: + bool GetMutableValue(const std::string& key, base::Value** result) override; + void ReportValueChanged(const std::string& key, uint32_t flags) override; + void SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + void SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + void RemoveValue(const std::string& key, uint32_t flags) override; + void RemoveValuesByPrefixSilently(const std::string& prefix) override; + bool ReadOnly() const override; + PrefReadError GetReadError() const override; + PersistentPrefStore::PrefReadError ReadPrefs() override; + void ReadPrefsAsync(ReadErrorDelegate* error_delegate) override; + void CommitPendingWrite(base::OnceClosure reply_callback, + base::OnceClosure synchronous_done_callback) override; + void SchedulePendingLossyWrites() override; + + // Marks the store as having completed initialization. + void SetInitializationCompleted(); + + // Used for tests to trigger notifications explicitly. + void NotifyPrefValueChanged(const std::string& key); + void NotifyInitializationCompleted(); + + // Some convenience getters/setters. + void SetString(const std::string& key, const std::string& value); + void SetInteger(const std::string& key, int value); + void SetBoolean(const std::string& key, bool value); + + bool GetString(const std::string& key, std::string* value) const; + bool GetInteger(const std::string& key, int* value) const; + bool GetBoolean(const std::string& key, bool* value) const; + + // Determines whether ReadPrefsAsync completes immediately. Defaults to false + // (non-blocking). To block, invoke this with true (blocking) before the call + // to ReadPrefsAsync. To unblock, invoke again with false (non-blocking) after + // the call to ReadPrefsAsync. + void SetBlockAsyncRead(bool block_async_read); + + void ClearMutableValues() override; + void OnStoreDeletionFromDisk() override; + + // Getter and Setter methods for setting and getting the state of the + // |TestingPrefStore|. + virtual void set_read_only(bool read_only); + void set_read_success(bool read_success); + void set_read_error(PersistentPrefStore::PrefReadError read_error); + bool committed() { return committed_; } + + protected: + ~TestingPrefStore() override; + + private: + void CheckPrefIsSerializable(const std::string& key, + const base::Value& value); + + // Stores the preference values. + PrefValueMap prefs_; + + // Flag that indicates if the PrefStore is read-only + bool read_only_; + + // The result to pass to PrefStore::Observer::OnInitializationCompleted + bool read_success_; + + // The result to return from ReadPrefs or ReadPrefsAsync. + PersistentPrefStore::PrefReadError read_error_; + + // Whether a call to ReadPrefsAsync should block. + bool block_async_read_; + + // Whether there is a pending call to ReadPrefsAsync. + bool pending_async_read_; + + // Whether initialization has been completed. + bool init_complete_; + + // Whether the store contents have been committed to disk since the last + // mutation. + bool committed_; + + std::unique_ptr error_delegate_; + base::ObserverList::Unchecked observers_; +}; + +#endif // COMPONENTS_PREFS_TESTING_PREF_STORE_H_ diff --git a/src/components/prefs/value_map_pref_store.cc b/src/components/prefs/value_map_pref_store.cc new file mode 100644 index 0000000000..5f447334a1 --- /dev/null +++ b/src/components/prefs/value_map_pref_store.cc @@ -0,0 +1,80 @@ +// Copyright (c) 2012 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 "components/prefs/value_map_pref_store.h" + +#include +#include + +#include "base/values.h" + +ValueMapPrefStore::ValueMapPrefStore() {} + +bool ValueMapPrefStore::GetValue(const std::string& key, + const base::Value** value) const { + return prefs_.GetValue(key, value); +} + +std::unique_ptr ValueMapPrefStore::GetValues() const { + return prefs_.AsDictionaryValue(); +} + +void ValueMapPrefStore::AddObserver(PrefStore::Observer* observer) { + observers_.AddObserver(observer); +} + +void ValueMapPrefStore::RemoveObserver(PrefStore::Observer* observer) { + observers_.RemoveObserver(observer); +} + +bool ValueMapPrefStore::HasObservers() const { + return !observers_.empty(); +} + +void ValueMapPrefStore::SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + DCHECK(value); + if (prefs_.SetValue(key, base::Value::FromUniquePtrValue(std::move(value)))) { + for (Observer& observer : observers_) + observer.OnPrefValueChanged(key); + } +} + +void ValueMapPrefStore::RemoveValue(const std::string& key, uint32_t flags) { + if (prefs_.RemoveValue(key)) { + for (Observer& observer : observers_) + observer.OnPrefValueChanged(key); + } +} + +bool ValueMapPrefStore::GetMutableValue(const std::string& key, + base::Value** value) { + return prefs_.GetValue(key, value); +} + +void ValueMapPrefStore::ReportValueChanged(const std::string& key, + uint32_t flags) { + for (Observer& observer : observers_) + observer.OnPrefValueChanged(key); +} + +void ValueMapPrefStore::SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) { + DCHECK(value); + prefs_.SetValue(key, base::Value::FromUniquePtrValue(std::move(value))); +} + +ValueMapPrefStore::~ValueMapPrefStore() {} + +void ValueMapPrefStore::NotifyInitializationCompleted() { + for (Observer& observer : observers_) + observer.OnInitializationCompleted(true); +} + +void ValueMapPrefStore::RemoveValuesByPrefixSilently( + const std::string& prefix) { + prefs_.ClearWithPrefix(prefix); +} diff --git a/src/components/prefs/value_map_pref_store.h b/src/components/prefs/value_map_pref_store.h new file mode 100644 index 0000000000..68c0b0e43e --- /dev/null +++ b/src/components/prefs/value_map_pref_store.h @@ -0,0 +1,59 @@ +// Copyright (c) 2012 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_PREFS_VALUE_MAP_PREF_STORE_H_ +#define COMPONENTS_PREFS_VALUE_MAP_PREF_STORE_H_ + +#include + +#include +#include + +#include "base/observer_list.h" +#include "components/prefs/pref_value_map.h" +#include "components/prefs/prefs_export.h" +#include "components/prefs/writeable_pref_store.h" + +// A basic PrefStore implementation that uses a simple name-value map for +// storing the preference values. +class COMPONENTS_PREFS_EXPORT ValueMapPrefStore : public WriteablePrefStore { + public: + ValueMapPrefStore(); + + ValueMapPrefStore(const ValueMapPrefStore&) = delete; + ValueMapPrefStore& operator=(const ValueMapPrefStore&) = delete; + + // PrefStore overrides: + bool GetValue(const std::string& key, + const base::Value** value) const override; + std::unique_ptr GetValues() const override; + void AddObserver(PrefStore::Observer* observer) override; + void RemoveObserver(PrefStore::Observer* observer) override; + bool HasObservers() const override; + + // WriteablePrefStore overrides: + void SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + void RemoveValue(const std::string& key, uint32_t flags) override; + bool GetMutableValue(const std::string& key, base::Value** value) override; + void ReportValueChanged(const std::string& key, uint32_t flags) override; + void SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) override; + void RemoveValuesByPrefixSilently(const std::string& prefix) override; + + protected: + ~ValueMapPrefStore() override; + + // Notify observers about the initialization completed event. + void NotifyInitializationCompleted(); + + private: + PrefValueMap prefs_; + + base::ObserverList::Unchecked observers_; +}; + +#endif // COMPONENTS_PREFS_VALUE_MAP_PREF_STORE_H_ diff --git a/src/components/prefs/writeable_pref_store.cc b/src/components/prefs/writeable_pref_store.cc new file mode 100644 index 0000000000..d51985e823 --- /dev/null +++ b/src/components/prefs/writeable_pref_store.cc @@ -0,0 +1,14 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/prefs/writeable_pref_store.h" + +void WriteablePrefStore::ReportSubValuesChanged( + const std::string& key, + std::set> path_components, + uint32_t flags) { + // Default implementation. Subclasses may use |path_components| to improve + // performance. + ReportValueChanged(key, flags); +} diff --git a/src/components/prefs/writeable_pref_store.h b/src/components/prefs/writeable_pref_store.h new file mode 100644 index 0000000000..ffe1a00629 --- /dev/null +++ b/src/components/prefs/writeable_pref_store.h @@ -0,0 +1,89 @@ +// Copyright 2014 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_PREFS_WRITEABLE_PREF_STORE_H_ +#define COMPONENTS_PREFS_WRITEABLE_PREF_STORE_H_ + +#include + +#include +#include +#include +#include + +#include "components/prefs/pref_store.h" + +namespace base { +class Value; +} + +// A pref store that can be written to as well as read from. +class COMPONENTS_PREFS_EXPORT WriteablePrefStore : public PrefStore { + public: + // PrefWriteFlags can be used to change the way a pref will be written to + // storage. + enum PrefWriteFlags : uint32_t { + // No flags are specified. + DEFAULT_PREF_WRITE_FLAGS = 0, + + // This marks the pref as "lossy". There is no strict time guarantee on when + // a lossy pref will be persisted to permanent storage when it is modified. + LOSSY_PREF_WRITE_FLAG = 1 << 1 + }; + + WriteablePrefStore() {} + + WriteablePrefStore(const WriteablePrefStore&) = delete; + WriteablePrefStore& operator=(const WriteablePrefStore&) = delete; + + // Sets a |value| for |key| in the store. |value| must be non-NULL. |flags| is + // a bitmask of PrefWriteFlags. + virtual void SetValue(const std::string& key, + std::unique_ptr value, + uint32_t flags) = 0; + + // Removes the value for |key|. + virtual void RemoveValue(const std::string& key, uint32_t flags) = 0; + + // Equivalent to PrefStore::GetValue but returns a mutable value. + virtual bool GetMutableValue(const std::string& key, + base::Value** result) = 0; + + // Triggers a value changed notification. This function or + // ReportSubValuesChanged needs to be called if one retrieves a list or + // dictionary with GetMutableValue and change its value. SetValue takes care + // of notifications itself. Note that ReportValueChanged will trigger + // notifications even if nothing has changed. |flags| is a bitmask of + // PrefWriteFlags. + virtual void ReportValueChanged(const std::string& key, uint32_t flags) = 0; + + // Triggers a value changed notification for |path_components| in the |key| + // pref. This function or ReportValueChanged needs to be called if one + // retrieves a list or dictionary with GetMutableValue and change its value. + // SetValue takes care of notifications itself. Note that + // ReportSubValuesChanged will trigger notifications even if nothing has + // changed. |flags| is a bitmask of PrefWriteFlags. + virtual void ReportSubValuesChanged( + const std::string& key, + std::set> path_components, + uint32_t flags); + + // Same as SetValue, but doesn't generate notifications. This is used by + // PrefService::GetMutableUserPref() in order to put empty entries + // into the user pref store. Using SetValue is not an option since existing + // tests rely on the number of notifications generated. |flags| is a bitmask + // of PrefWriteFlags. + virtual void SetValueSilently(const std::string& key, + std::unique_ptr value, + uint32_t flags) = 0; + + // Clears all the preferences which names start with |prefix| and doesn't + // generate update notifications. + virtual void RemoveValuesByPrefixSilently(const std::string& prefix) = 0; + + protected: + ~WriteablePrefStore() override {} +}; + +#endif // COMPONENTS_PREFS_WRITEABLE_PREF_STORE_H_